mirror of https://gitlab.com/openlp/openlp.git
Merge branch 'footer-per-slide' into 'master'
Adding foundational support to Footer per slide See merge request openlp/openlp!576
This commit is contained in:
commit
9e34f9eca1
|
@ -427,3 +427,11 @@ section.text-slides.stack.present {
|
|||
|
||||
.reveal[class*=fade].overview .slides section {
|
||||
transition: none; }
|
||||
|
||||
.footer .footer-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer .footer-item.active {
|
||||
display: block;
|
||||
}
|
|
@ -290,8 +290,11 @@ function _fixFontName(fontName) {
|
|||
* The Display object is what we use from OpenLP
|
||||
*/
|
||||
var Display = {
|
||||
/** @type {HTMLElement} */
|
||||
_slidesContainer: null,
|
||||
/** @type {HTMLElement} */
|
||||
_footerContainer: null,
|
||||
/** @type {HTMLElement} */
|
||||
_backgroundsContainer: null,
|
||||
_alerts: [],
|
||||
_slides: {},
|
||||
|
@ -352,6 +355,7 @@ var Display = {
|
|||
Display._backgroundsContainer = $(".backgrounds")[0];
|
||||
Display._doTransitions = isDisplay;
|
||||
Reveal.initialize(Display._revealConfig);
|
||||
Reveal.addEventListener('slidechanged', Display._onSlideChanged);
|
||||
Display.setItemTransition(doItemTransitions && isDisplay);
|
||||
displayWatcher.setInitialised(true);
|
||||
},
|
||||
|
@ -649,6 +653,18 @@ var Display = {
|
|||
slide.innerHTML = html;
|
||||
return slide;
|
||||
},
|
||||
|
||||
_onSlideChanged: function(event) {
|
||||
Display._footerContainer.querySelectorAll('.footer-item')
|
||||
.forEach(footerItem => footerItem.classList.remove('active'));
|
||||
var currentSlideNth = parseInt(event.currentSlide.getAttribute('data-slide'));
|
||||
var newActiveFooter = Display._footerContainer.querySelector('.footer-item[data-slide="' + currentSlideNth + '"]');
|
||||
|
||||
if (newActiveFooter) {
|
||||
newActiveFooter.classList.add('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set text slides.
|
||||
* @param {Object[]} slides - A list of slides to add as JS objects: {"verse": "v1", "text": "line 1\nline2"}
|
||||
|
@ -658,12 +674,20 @@ var Display = {
|
|||
var slide_html;
|
||||
var parentSection = document.createElement("section");
|
||||
parentSection.classList = "text-slides";
|
||||
slides.forEach(function (slide) {
|
||||
slides.forEach(function (slide, index) {
|
||||
slide_html = Display._createTextSlide(slide.verse, slide.text);
|
||||
slide_html.setAttribute('data-slide', index);
|
||||
parentSection.appendChild(slide_html);
|
||||
Display._slides[slide.verse] = parentSection.children.length - 1;
|
||||
if (slide.footer) {
|
||||
Display._footerContainer.innerHTML = slide.footer;
|
||||
var footerSlide = document.createElement('div');
|
||||
footerSlide.classList.add('footer-item');
|
||||
footerSlide.setAttribute('data-slide', index);
|
||||
if (index == 0) {
|
||||
footerSlide.classList.add('active');
|
||||
}
|
||||
footerSlide.innerHTML = slide.footer;
|
||||
Display._footerContainer.append(footerSlide);
|
||||
}
|
||||
});
|
||||
Display.replaceSlides(parentSection, true);
|
||||
|
@ -689,6 +713,7 @@ var Display = {
|
|||
var parentSection = document.createElement("section");
|
||||
parentSection.classList = "text-slides";
|
||||
slide_html = Display._createTextSlide("test-slide", text);
|
||||
slide_html.setAttribute('data-slide', 0);
|
||||
parentSection.appendChild(slide_html);
|
||||
Display._slides["test-slide"] = 0;
|
||||
Display.applyTheme(parentSection);
|
||||
|
@ -711,6 +736,7 @@ var Display = {
|
|||
var img = document.createElement('img');
|
||||
img.src = slide.path;
|
||||
img.setAttribute("style", "width: 100%; height: 100%; margin: 0; object-fit: contain;");
|
||||
img.setAttribute('data-slide', index);
|
||||
section.appendChild(img);
|
||||
parentSection.appendChild(section);
|
||||
Display._slides[index.toString()] = index;
|
||||
|
@ -730,6 +756,7 @@ var Display = {
|
|||
videoElement.preload = "auto";
|
||||
videoElement.setAttribute("id", "video");
|
||||
videoElement.setAttribute("style", "height: 100%; width: 100%;");
|
||||
videoElement.setAttribute('data-slide', 0);
|
||||
videoElement.autoplay = false;
|
||||
// All the update methods below are Python functions, hence not camelCase
|
||||
videoElement.addEventListener("durationchange", function (event) {
|
||||
|
|
|
@ -221,12 +221,18 @@ class ServiceItem(RegistryProperties):
|
|||
pages = self.renderer.format_slide(raw_slide['text'], self)
|
||||
previous_pages[verse_tag] = (raw_slide, pages)
|
||||
for page in pages:
|
||||
footer_html = None
|
||||
has_footer_html = 'footer_html' in raw_slide
|
||||
if has_footer_html:
|
||||
footer_html = raw_slide['footer_html']
|
||||
else:
|
||||
footer_html = self.footer_html
|
||||
rendered_slide = {
|
||||
'title': raw_slide['title'],
|
||||
'text': render_tags(page),
|
||||
'chords': remove_tags(page),
|
||||
'verse': index,
|
||||
'footer': self.footer_html
|
||||
'footer': footer_html
|
||||
}
|
||||
self._rendered_slides.append(rendered_slide)
|
||||
display_slide = {
|
||||
|
@ -303,12 +309,13 @@ class ServiceItem(RegistryProperties):
|
|||
self.slides.append(slide)
|
||||
self._new_item()
|
||||
|
||||
def add_from_text(self, text, verse_tag=None):
|
||||
def add_from_text(self, text, verse_tag=None, footer_html=None):
|
||||
"""
|
||||
Add a text slide to the service item.
|
||||
|
||||
:param text: The raw text of the slide.
|
||||
:param verse_tag:
|
||||
:param footer_html: Custom HTML footer for current slide
|
||||
"""
|
||||
if verse_tag:
|
||||
verse_tag = verse_tag.upper()
|
||||
|
@ -317,7 +324,10 @@ class ServiceItem(RegistryProperties):
|
|||
verse_tag = str(len(self.slides) + 1)
|
||||
self.service_item_type = ServiceItemType.Text
|
||||
title = text[:30].split('\n')[0]
|
||||
self.slides.append({'title': title, 'text': text, 'verse': verse_tag})
|
||||
slide = {'title': title, 'text': text, 'verse': verse_tag}
|
||||
if footer_html is not None:
|
||||
slide['footer_html'] = footer_html
|
||||
self.slides.append(slide)
|
||||
self._new_item()
|
||||
|
||||
def add_from_command(self, path, file_name, image, display_title=None, notes=None, file_hash=None):
|
||||
|
@ -506,7 +516,8 @@ class ServiceItem(RegistryProperties):
|
|||
self.theme_overwritten = header.get('theme_overwritten', False)
|
||||
if self.service_item_type == ServiceItemType.Text:
|
||||
for slide in service_item['serviceitem']['data']:
|
||||
self.add_from_text(slide['raw_slide'], slide['verseTag'])
|
||||
footer_html = slide['footer_html'] if 'footer_html' in slide else None
|
||||
self.add_from_text(slide['raw_slide'], slide['verseTag'], footer_html)
|
||||
self._create_slides()
|
||||
elif self.service_item_type == ServiceItemType.Image:
|
||||
if path:
|
||||
|
|
|
@ -170,6 +170,7 @@ describe("The Display object", function () {
|
|||
|
||||
it("should initialise Reveal when init is called", function () {
|
||||
spyOn(Reveal, "initialize");
|
||||
spyOn(Reveal, "addEventListener");
|
||||
document.body.innerHTML = "";
|
||||
Display.init();
|
||||
expect(Reveal.initialize).toHaveBeenCalled();
|
||||
|
@ -177,6 +178,7 @@ describe("The Display object", function () {
|
|||
|
||||
it("should have checkerboard class when init is called when not display", function () {
|
||||
spyOn(Reveal, "initialize");
|
||||
spyOn(Reveal, "addEventListener");
|
||||
document.body.innerHTML = "";
|
||||
document.body.classList = "";
|
||||
Display.init({isDisplay: false});
|
||||
|
@ -185,6 +187,7 @@ describe("The Display object", function () {
|
|||
|
||||
it("should not have checkerboard class when init is called when is a display", function () {
|
||||
spyOn(Reveal, "initialize");
|
||||
spyOn(Reveal, "addEventListener");
|
||||
document.body.innerHTML = "";
|
||||
document.body.classList = "";
|
||||
Display.init({isDisplay: true});
|
||||
|
@ -207,6 +210,14 @@ describe("The Display object", function () {
|
|||
expect(Display.setItemTransition).toBeDefined();
|
||||
});
|
||||
|
||||
it("should register _onSlideChanged event for slide change", function () {
|
||||
spyOn(Reveal, "initialize");
|
||||
spyOn(Reveal, "addEventListener");
|
||||
document.body.innerHTML = "";
|
||||
Display.init();
|
||||
expect(Reveal.addEventListener).toHaveBeenCalledWith('slidechanged', Display._onSlideChanged);
|
||||
});
|
||||
|
||||
it("should have a correctly functioning clearSlides() method", function () {
|
||||
expect(Display.clearSlides).toBeDefined();
|
||||
|
||||
|
@ -936,6 +947,33 @@ describe("Display.setTextSlides", function () {
|
|||
expect(slidesDiv.style['width']).toEqual('1230px');
|
||||
expect(slidesDiv.style['height']).toEqual('4560px');
|
||||
})
|
||||
|
||||
it("should work correctly with different footer contents per slide", function () {
|
||||
var slides = [
|
||||
{
|
||||
"verse": "v1",
|
||||
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
|
||||
"I once was lost, but now I'm found\nWas blind but now I see",
|
||||
"footer": "Public Domain"
|
||||
},
|
||||
{
|
||||
"verse": "v2",
|
||||
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
|
||||
"How precious did that grace appear,\nthe hour I first believed.",
|
||||
"footer": "Public Domain, Second Test"
|
||||
}
|
||||
];
|
||||
spyOn(Display, "clearSlides");
|
||||
spyOn(Reveal, "sync");
|
||||
spyOn(Reveal, "slide");
|
||||
|
||||
Display.setTextSlides(slides);
|
||||
|
||||
expect(Display.clearSlides).toHaveBeenCalledTimes(0);
|
||||
expect($(".footer > .footer-item").length).toEqual(2);
|
||||
expect(document.querySelectorAll(".footer > .footer-item")[0].innerHTML).toEqual(slides[0].footer);
|
||||
expect(document.querySelectorAll(".footer > .footer-item")[1].innerHTML).toEqual(slides[1].footer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Display.setImageSlides", function () {
|
||||
|
@ -1171,3 +1209,45 @@ describe("Display.toggleVideoMute", function () {
|
|||
expect(mockVideo.muted).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reveal slidechanged event", function () {
|
||||
it("should swap footer content", function (done) {
|
||||
var slides = [
|
||||
{
|
||||
"verse": "v1",
|
||||
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
|
||||
"I once was lost, but now I'm found\nWas blind but now I see",
|
||||
"footer": "Public Domain"
|
||||
},
|
||||
{
|
||||
"verse": "v2",
|
||||
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
|
||||
"How precious did that grace appear,\nthe hour I first believed.",
|
||||
"footer": "Public Domain, Second Test"
|
||||
}
|
||||
];
|
||||
|
||||
var slidesDiv = _createDiv({"class": "slides"});
|
||||
slidesDiv.innerHTML = "<section><p></p></section>";
|
||||
Display._slidesContainer = slidesDiv;
|
||||
var footerDiv = _createDiv({"class": "footer"});
|
||||
Display._footerContainer = footerDiv;
|
||||
var revealDiv = _createDiv({"class": "reveal"});
|
||||
revealDiv.append(slidesDiv);
|
||||
revealDiv.append(footerDiv);
|
||||
document.body.appendChild(revealDiv);
|
||||
|
||||
Display.init({isDisplay: false, doItemTransitions: false});
|
||||
var oldDisplaySlideChanged = Display._onSlideChanged;
|
||||
Display._onSlideChanged = function(event) {
|
||||
oldDisplaySlideChanged(event);
|
||||
expect(document.querySelector(".footer > .footer-item.active").getAttribute('data-slide')).toEqual('1');
|
||||
done();
|
||||
}
|
||||
Display.setTextSlides(slides);
|
||||
|
||||
var currentSlide = Display._slidesContainer.querySelector('*:nth-child(2)');
|
||||
currentSlide.id = '1';
|
||||
Display._onSlideChanged({currentSlide: currentSlide});
|
||||
});
|
||||
});
|
|
@ -968,6 +968,58 @@ def test_to_dict_presentation_item(mocked_image_uri, mocked_get_data_path, state
|
|||
assert result == expected_dict
|
||||
|
||||
|
||||
def test_add_from_text_adds_per_slide_footer_html():
|
||||
"""
|
||||
Test the Service Item - adding text slides with per slide footer_html
|
||||
"""
|
||||
# GIVEN: A service item and two slides
|
||||
service_item = ServiceItem(None)
|
||||
slide1 = "This is the first slide"
|
||||
slide1FooterHtml = '<small>First Footer</small>'
|
||||
slide2 = "This is the second slide"
|
||||
slide2FooterHtml = '<small>Second Footer</small>'
|
||||
|
||||
# WHEN: adding text slides to service_item
|
||||
service_item.add_from_text(slide1, footer_html=slide1FooterHtml)
|
||||
service_item.add_from_text(slide2, footer_html=slide2FooterHtml)
|
||||
|
||||
# THEN: Slides should be added with correctly numbered verse tags (Should start at 1)
|
||||
assert service_item.slides == [
|
||||
{'text': 'This is the first slide', 'title': 'This is the first slide', 'verse': '1',
|
||||
'footer_html': slide1FooterHtml},
|
||||
{'text': 'This is the second slide', 'title': 'This is the second slide', 'verse': '2',
|
||||
'footer_html': slide2FooterHtml}
|
||||
]
|
||||
|
||||
|
||||
@patch('openlp.core.lib.serviceitem.UiIcons')
|
||||
def test_add_from_text_per_slide_footer_html_is_honoured(mock_uiicons, settings, registry):
|
||||
"""
|
||||
Test the Service Item - adding text slides with per slide footer_html is honoured
|
||||
"""
|
||||
# GIVEN: A service item, mocked live_controller and renderer, and two slides
|
||||
renderer_mock = MagicMock()
|
||||
Registry().register('live_controller', MagicMock())
|
||||
Registry().register('renderer', renderer_mock)
|
||||
Registry().register('service_list', MagicMock())
|
||||
renderer_mock.format_slide.side_effect = lambda text, item: [text]
|
||||
service_item = ServiceItem(None)
|
||||
slide1 = "This is the first slide"
|
||||
slide1FooterHtml = '<small>First Footer</small>'
|
||||
slide2 = "This is the second slide"
|
||||
slide2FooterHtml = '<small>Second Footer</small>'
|
||||
|
||||
# WHEN: adding text slides to service_item
|
||||
service_item.add_from_text(slide1, footer_html=slide1FooterHtml)
|
||||
service_item.add_from_text(slide2, footer_html=slide2FooterHtml)
|
||||
|
||||
service_item._create_slides()
|
||||
|
||||
# THEN: Slides should be added with correctly numbered verse tags (Should start at 1)
|
||||
assert service_item._rendered_slides[0]['footer'] == slide1FooterHtml
|
||||
assert service_item._rendered_slides[1]['footer'] == slide2FooterHtml
|
||||
|
||||
|
||||
@pytest.mark.parametrize('plugin_name,icon', [('songs', 'music'), ('bibles', 'bible'),
|
||||
('presentations', 'presentation'), ('images', 'picture'),
|
||||
('media', 'video')])
|
||||
|
|
Loading…
Reference in New Issue