Initial Upload

This commit is contained in:
Matt Batchelder
2025-12-02 10:32:59 -05:00
commit 05ce0da296
2240 changed files with 467811 additions and 0 deletions

5
modules/src/README.md Normal file
View File

@@ -0,0 +1,5 @@
# /modules/src
This folder contains JavaScript source files.
By running build/publish, the files will be processed and placed in /modules.

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
$(function() {
// Get a message from the parent window
// RUN ON IFRAME
window.onmessage = function(e) {
if (
e.data.method == 'renderContent'
) {
// Update global options for the widget
globalOptions.originalWidth = e.data.options.originalWidth;
globalOptions.originalHeight = e.data.options.originalHeight;
// Set the pause state for animation to false
// To start right after the render effects are generated
globalOptions.pauseEffectOnStart =
e.data.options.pauseEffectOnStart ?? false;
// Arguments for both reRender
const args = (typeof widget != 'undefined') ? [
e.data.options.id, // id
$('body'), // target
widget.items, // items
Object.assign(widget.properties, globalOptions), // properties
widget.meta, // meta
] : [];
// Call render array of functions if exists and it's an array
if (window.renders && Array.isArray(window.renders)) {
window.renders.forEach((render) => {
render(...args);
});
}
}
};
});

View File

@@ -0,0 +1,132 @@
/* eslint-disable no-invalid-this */
Handlebars.registerHelper('eq', function(v1, v2, opts) {
if (v1 === v2) {
return opts.fn(this);
} else {
return opts.inverse(this);
}
});
Handlebars.registerHelper('neq', function(v1, v2, opts) {
if (v1 !== v2) {
return opts.fn(this);
} else {
return opts.inverse(this);
}
});
Handlebars.registerHelper('set', function(varName, varValue, opts) {
if (!opts.data.root) {
opts.data.root = {};
}
opts.data.root[varName] = varValue;
});
Handlebars.registerHelper('parseJSON', function(varName, varValue, opts) {
if (!opts.data.root) {
opts.data.root = {};
}
try {
opts.data.root[varName] = JSON.parse(varValue);
} catch (error) {
console.warn(error);
opts.data.root = {};
}
});
Handlebars.registerHelper('createGradientInSVG', function(
gradient,
uniqueId,
) {
if (gradient == '') {
return '';
}
const gradientObj = JSON.parse(gradient);
if (gradientObj.type === 'linear') {
// Convert angle to radians
const radians = (gradientObj.angle - 90) * Math.PI / 180;
// Calculate x and y components
const x = Math.cos(radians);
const y = Math.sin(radians);
// Determine x1, x2, y1, y2 points
const x1 = 0.5 - 0.5 * x;
const x2 = 0.5 + 0.5 * x;
const y1 = 0.5 - 0.5 * y;
const y2 = 0.5 + 0.5 * y;
return `<linearGradient id="gradLinear_${uniqueId}"
x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
<stop offset="0%" style="stop-color:${gradientObj.color1};" />
<stop offset="100%" style="stop-color:${gradientObj.color2};" />
</linearGradient>`;
} else {
// Radial
return `<radialGradient id="gradRadial_${uniqueId}"
cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color:${gradientObj.color1};" />
<stop offset="100%" style="stop-color:${gradientObj.color2};" />
</radialGradient>`;
}
});
Handlebars.registerHelper('weatherBackgroundImage', function(
icon,
cloudyImage,
dayCloudyImage,
dayClearImage,
fogImage,
hailImage,
nightClearImage,
nightPartlyCloudyImage,
rainImage,
snowImage,
windImage,
opts,
) {
let bgImage = false;
if ((typeof cloudyImage !== 'undefined' && cloudyImage !== '') &&
icon === 'cloudy') {
bgImage = cloudyImage;
} else if ((typeof dayCloudyImage !== 'undefined' && dayCloudyImage !== '') &&
icon === 'partly-cloudy-day') {
bgImage = dayCloudyImage;
} else if ((typeof dayClearImage !== 'undefined' && dayClearImage !== '') &&
icon === 'clear-day') {
bgImage = dayClearImage;
} else if ((typeof fogImage !== 'undefined' && fogImage !== '') &&
icon === 'fog') {
bgImage = fogImage;
} else if ((typeof hailImage !== 'undefined' && hailImage !== '') &&
icon === 'sleet') {
bgImage = hailImage;
} else if ((typeof nightClearImage !== 'undefined' &&
nightClearImage !== '') && icon === 'clear-night') {
bgImage = nightClearImage;
} else if ((typeof nightPartlyCloudyImage !== 'undefined' &&
nightPartlyCloudyImage !== '') && icon === 'partly-cloudy-night') {
bgImage = nightPartlyCloudyImage;
} else if ((typeof rainImage !== 'undefined' && rainImage !== '') &&
icon === 'rain') {
bgImage = rainImage;
} else if ((typeof snowImage !== 'undefined' && snowImage !== '') &&
icon === 'snow') {
bgImage = snowImage;
} else if ((typeof windImage !== 'undefined' && windImage !== '') &&
icon === 'wind') {
bgImage = windImage;
}
// If it's the media id, replace with path to be rendered
if (bgImage && !isNaN(bgImage) && imageDownloadUrl) {
bgImage = imageDownloadUrl.replace(':id', bgImage);
}
return bgImage;
});

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-unused-vars */
const globalThis = require('globalthis/polyfill')();
import 'core-js/stable/url-search-params';
// Our own imports
import 'xibo-interactive-control/dist/xibo-interactive-control.min.js';
import './xibo-calendar-render';
import './xibo-countdown-render';
import './xibo-finance-render';
import './xibo-image-render';
import './xibo-legacy-template-render';
import './xibo-layout-animate.js';
import './xibo-layout-scaler';
import './xibo-menuboard-render';
import './xibo-metro-render';
import './xibo-substitutes-parser';
import './xibo-text-render';
import './xibo-text-scaler';
import './xibo-dataset-render';
import './xibo-webpage-render';
import './xibo-worldclock-render';
import './xibo-elements-render';
import './editor-render';
window.PlayerHelper = require('../../ui/src/helpers/player-helper.js');
window.XiboPlayer = require('./xibo-player.js');
window.jQuery = window.$ = require('jquery');
require('babel-polyfill');
window.moment = require('moment');
require('moment-timezone');
window.Handlebars = require('handlebars/dist/handlebars.min.js');
require('./handlebars-helpers.js');
// Include HLS.js
window.Hls = require('hls.js');
// Include PDFjs
window.pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');
// Include common helpers/transformer
window.transformer = require('../../ui/src/helpers/transformer.js');
window.ArrayHelper = require('../../ui/src/helpers/array.js');
window.DateFormatHelper = require('../../ui/src/helpers/date-format-helper.js');
// Plugins
require('../vendor/jquery-cycle-2.1.6.min.js');
require('../vendor/jquery.marquee.min.js');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboCountdownRender: function(options, body) {
// Check if the given input is a number/offset
// or a date, and return the object
const getDate = function(inputDate) {
if (!isNaN(inputDate)) {
return moment().add(inputDate, 's');
} else if (moment(inputDate).isValid()) {
return moment(inputDate);
} else {
console.error('Invalid Date/Time!!!');
}
};
// Ge remaining time
const getTimeRemaining = function(endtime) {
const timeNow = moment().startOf('seconds');
const duration =
moment.duration(endtime.startOf('seconds').diff(timeNow));
return {
now: timeNow,
total: Math.floor(duration.asMilliseconds()),
seconds: Math.floor(duration.seconds()),
secondsAll: Math.floor(duration.asSeconds()),
minutes: Math.floor(duration.minutes()),
minutesAll: Math.floor(duration.asMinutes()),
hours: Math.floor(duration.hours()),
hoursAll: Math.floor(duration.asHours()),
days: Math.floor(duration.asDays()),
weeks: Math.floor(duration.asWeeks()),
months: Math.floor(duration.asMonths()),
years: Math.floor(duration.asYears()),
};
};
// Initialize clock
const initialiseClock = function(clock, deadlineDate, warningDate) {
const yearsSpan = clock.find('.years');
const monthsSpan = clock.find('.months');
const weeksSpan = clock.find('.weeks');
const daysSpan = clock.find('.days');
const hoursSpan = clock.find('.hours');
const hoursAllSpan = clock.find('.hoursAll');
const minutesSpan = clock.find('.minutes');
const minutesAllSpan = clock.find('.minutesAll');
const secondsSpan = clock.find('.seconds');
const secondsAllSpan = clock.find('.secondsAll');
// Remove warning and finished classes on init
$(clock).removeClass('warning finished');
// Clear interval if exists
if (window.timeinterval) {
clearInterval(window.timeinterval);
}
/**
* Update clock
*/
function updateClock() {
const t = getTimeRemaining(deadlineDate);
yearsSpan.html(t.years);
monthsSpan.html(t.months);
weeksSpan.html(t.weeks);
daysSpan.html(t.days);
hoursSpan.html(('0' + t.hours).slice(-2));
hoursAllSpan.html(t.hoursAll);
minutesSpan.html(('0' + t.minutes).slice(-2));
minutesAllSpan.html(t.minutesAll);
secondsSpan.html(('0' + t.seconds).slice(-2));
secondsAllSpan.html(t.secondsAll);
if (
warningDate && deadlineDate.diff(warningDate) != 0 &&
warningDate.diff(t.now) <= 0
) {
$(clock).addClass('warning');
}
if (t.total <= 0) {
$(clock).removeClass('warning').addClass('finished');
clearInterval(window.timeinterval);
yearsSpan.html('0');
monthsSpan.html('0');
daysSpan.html('0');
hoursSpan.html('00');
minutesSpan.html('00');
secondsSpan.html('00');
hoursAllSpan.html('0');
minutesAllSpan.html('0');
secondsAllSpan.html('0');
}
}
updateClock(); // run function once at first to avoid delay
// Update every second
window.timeinterval = setInterval(updateClock, 1000);
};
// Default options
const defaults = {
duration: '30',
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
};
options = $.extend({}, defaults, options);
// For each matched element
this.each(function(_idx, _el) {
// Calculate duration (use widget or given)
let initDuration = options.duration;
if (options.countdownType == 2) {
initDuration = options.countdownDuration;
} else if (options.countdownType == 3) {
initDuration = options.countdownDate;
}
// Get deadline date
const deadlineDate = getDate(initDuration);
// Calculate warning duration ( use widget or given)
let warningDuration = 0;
if (options.countdownType == 1 || options.countdownType == 2) {
warningDuration = options.countdownWarningDuration;
} else if (options.countdownType == 3) {
warningDuration = options.countdownWarningDate;
}
// Get warning date
const warningDate =
(
warningDuration == 0 ||
warningDuration == '' ||
warningDuration == null
) ? false : getDate(warningDuration);
// Initialise clock
initialiseClock($(_el), deadlineDate, warningDate);
});
return $(this);
},
});

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
dataSetRender: function(options) {
// Any options?
if (options === undefined || options === null) {
options = {
duration: 5,
transition: 'fade',
rowsPerPage: 0,
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
};
}
$(this).each(function(_idx, el) {
const numberItems = $(el).data('totalPages');
const duration =
(options.durationIsPerItem) ?
options.duration : options.duration / numberItems;
if (options.rowsPerPage > 0) {
// Cycle handles this for us
if ($(el).prop('isCycle')) {
$(el).cycle('destroy');
}
$(el).prop('isCycle', true).cycle({
fx: options.transition,
timeout: duration * 1000,
slides: '> table',
});
}
});
return $(this);
},
});

View File

@@ -0,0 +1,212 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboElementsRender: function(options, items) {
const $this = $(this);
const defaults = {
selector: null,
effect: 'none',
pauseEffectOnStart: true,
duration: 50,
durationIsPerItem: false,
numItems: 1,
itemsPerPage: 1,
speed: 2,
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
marqueeInlineSelector: '.elements-render-item',
alignmentV: 'top',
displayDirection: 0,
parentId: '',
layer: 0,
seamless: true,
gap: 50,
};
const $content = $('#content');
const isAndroid = navigator.userAgent.indexOf('Android') > -1;
// Is marquee effect
const isMarquee =
options.effect === 'marqueeLeft' ||
options.effect === 'marqueeRight' ||
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown';
const isUseNewMarquee = options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown' ||
!isAndroid;
options = $.extend({}, defaults, options);
if (!isMarquee) {
options.speed = 1000;
} else {
options.speed = 1;
}
const cycleElement = `.${options.id}`;
if (isMarquee && isUseNewMarquee) {
$this.marquee('destroy');
} else if ($content.find(cycleElement + '.cycle-slideshow').length) {
$(cycleElement + '.cycle-slideshow').cycle('destroy');
}
let marquee = false;
if (options.effect === 'none') {
// Do nothing
} else if (!isMarquee && $content.find(cycleElement).length) {
const numberOfSlides = items?.length || 1;
const duration = (options.durationIsPerItem) ?
options.duration :
options.duration / numberOfSlides;
const timeout = duration * 1000;
const noTransitionSpeed = 200;
let cycle2Config = {
'data-cycle-fx': (options.effect === 'noTransition' ||
options.effect === 'none') ? 'none' : options.effect,
'data-cycle-speed': (
options.effect === 'noTransition' || options.effect === 'none'
) ? noTransitionSpeed : options.speed,
'data-cycle-timeout': timeout,
'data-cycle-slides': `> .${options.id}--item`,
'data-cycle-auto-height': false,
'data-cycle-paused': options.pauseEffectOnStart,
};
if (options.effect === 'scrollHorz') {
$(cycleElement).find(`> .${options.id}--item`)
.each(function(idx, elem) {
$(elem).css({width: '-webkit-fill-available'});
});
} else {
cycle2Config = {
...cycle2Config,
'data-cycle-sync': false,
};
}
$(cycleElement).addClass('cycle-slideshow anim-cycle')
.attr(cycle2Config).cycle();
// Add some margin for each slide when options.effect === scrollHorz
if (options.effect === 'scrollHorz') {
$(cycleElement).css({width: options.width + (options.gap / 2)});
$(cycleElement).find('.cycle-slide').css({
marginLeft: options.gap / 4,
marginRight: options.gap / 4,
});
}
} else if (
options.effect === 'marqueeLeft' ||
options.effect === 'marqueeRight'
) {
marquee = true;
options.direction =
((options.effect === 'marqueeLeft') ? 'left' : 'right');
// Make sure the speed is something sensible
// This speed calculation gives as 80 pixels per second
options.speed = (options.speed === 0) ? 1 : options.speed;
// Add gap between
if ($this.find('.scroll').length > 0) {
$this.find('.scroll').css({
paddingLeft: !options.seamless ? options.gap : 0,
paddingRight: !options.seamless ? options.gap : 0,
columnGap: options.gap,
});
}
} else if (
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown'
) {
// We want a marquee
marquee = true;
options.direction = ((options.effect === 'marqueeUp') ? 'up' : 'down');
// Make sure the speed is something sensible
// This speed calculation gives as 80 pixels per second
options.speed = (options.speed === 0) ?
1 : options.speed;
if ($this.find('.scroll').length > 0) {
$this.find('.scroll').css({
flexDirection: 'column',
height: 'auto',
});
}
}
if (marquee) {
if (isUseNewMarquee) {
// in old marquee scroll delay is 85 milliseconds
// options.speed is the scrollamount
// which is the number of pixels per 85 milliseconds
// our new plugin speed is pixels per second
$this.attr({
'data-is-legacy': false,
'data-speed': options.speed / 25 * 1000,
'data-direction': options.direction,
'data-duplicated': options.seamless,
'data-gap': options.gap,
}).marquee().addClass('animating');
} else {
let $scroller = $this.find('.scroll:not(.animating)');
if ($scroller.length !== 0) {
$scroller.attr({
'data-is-legacy': true,
scrollamount: options.speed,
behaviour: 'scroll',
direction: options.direction,
height: options.height,
width: options.width,
}).overflowMarquee().addClass('animating scroll');
$scroller = $this.find('.scroll.animating');
// Correct items alignment as $scroller styles are overridden
// after initializing overflowMarquee
if (options.effect === 'marqueeLeft' ||
options.effect === 'marqueeRight'
) {
$scroller.find('> div').css({
display: 'flex',
flexDirection: 'row',
});
}
}
}
// Correct for up / down
if (
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown'
) {
$this.find('.js-marquee').css({marginBottom: 0});
}
}
return $this;
},
});

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboFinanceRender: function(options, items, body) {
// Default options
const defaults = {
effect: 'none',
pauseEffectOnStart: true,
speed: '2',
duration: '30',
durationIsPerItem: false,
numItems: items.length,
itemsPerPage: 5,
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
};
options = $.extend({}, defaults, options);
if (!options.itemsPerPage) {
options.itemsPerPage = 1;
}
// Calculate the dimensions of this itemoptions.numItems
// based on the preview/original dimensions
let width = height = 0;
if (options.previewWidth === 0 || options.previewHeight === 0) {
width = options.originalWidth;
height = options.originalHeight;
} else {
width = options.previewWidth;
height = options.previewHeight;
}
if (options.scaleOverride !== 0) {
width = width / options.scaleOverride;
height = height / options.scaleOverride;
}
if (options.widgetDesignWidth > 0 && options.widgetDesignHeight > 0) {
options.widgetDesignWidth = options.widgetDesignWidth;
options.widgetDesignHeight = options.widgetDesignHeight;
width = options.widgetDesignWidth;
height = options.widgetDesignHeight;
}
const isEditor = xiboIC.checkIsEditor();
// For each matched element
this.each(function(_idx, _elem) {
// How many pages to we need?
const numberOfPages =
(options.numItems > options.itemsPerPage) ?
Math.ceil(options.numItems / options.itemsPerPage) : 1;
const $mainContainer = $(_elem);
// Destroy any existing cycle
$mainContainer.find('.anim-cycle')
.cycle('destroy');
// Remove previous content
$mainContainer.find('.container-main:not(.template-container)').remove();
// Clone the main HTML
// and remove template-container class when we are on the editor
const $mainHTML = isEditor ? $(body).clone()
.removeClass('template-container')
.show() : $(body);
// Hide main HTML if isEditor = true
if (isEditor) {
$(body).hide();
}
// Create the pages
for (let i = 0; i < numberOfPages; i++) {
// Create a page
const $itemsHTML = $('<div />').addClass('page');
for (let j = 0; j < options.itemsPerPage; j++) {
if (((i * options.itemsPerPage) + j) < options.numItems) {
const $item = $(items[(i * options.itemsPerPage) + j]);
// Clone and append the item to the page
// and remove template-item class when isEditor = true
(isEditor ? $item.clone() : $item).appendTo($itemsHTML)
.show().removeClass('template-item');
// Hide the original item when isEditor = true
if (isEditor) {
$item.hide();
}
}
}
// Append the page to the item container
$mainHTML.find('.items-container').append($itemsHTML);
}
// Append the main HTML to the container
$mainContainer.append($mainHTML);
const duration =
(options.durationIsPerItem) ?
options.duration :
options.duration / numberOfPages;
// Make sure the speed is something sensible
options.speed = (options.speed <= 200) ? 1000 : options.speed;
// Timeout is the duration in ms
const timeout = (duration * 1000) - (options.speed * 0.7);
const slides = (numberOfPages > 1) ? '.page' : '.item';
const $cycleContainer = $mainContainer.find('#cycle-container');
// Set the content div to the height of the original window
$cycleContainer.css('height', height);
// Set the width on the cycled slides
$cycleContainer.find(slides).css({
width: width,
height: height,
});
// Cycle handles this for us
$cycleContainer.addClass('anim-cycle')
.cycle({
fx: options.effect,
speed: options.speed,
timeout: timeout,
slides: '> ' + slides,
paused: options.pauseEffectOnStart,
log: false,
});
// Protect against images that don't load
$mainContainer.find('img').on('error', function(ev) {
$(ev.currentTarget).off('error')
// eslint-disable-next-line max-len
.attr('src', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNiYAAAAAkAAxkR2eQAAAAASUVORK5CYII=');
});
});
return $(this);
},
});

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboImageRender: function(options) {
// Default options
const defaults = {
reloadTime: 5000,
maxTries: -1, // -1: Infinite # times
};
// Extend options
options = $.extend({}, defaults, options);
const $self = $(this);
// Run all the selected elements individually
if ($self.length > 1) {
$self.each(function(i, el) {
$(el).xiboImageRender(options);
});
return $self;
}
// Handle the image error by replacing the original image
// with a transparent pixel and try to reload the original source again
const handleImageError = function() {
// Replace image with a single transparent pixel
$self.off('error')
.attr(
'src',
// eslint-disable-next-line max-len
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNiYAAAAAkAAxkR2eQAAAAASUVORK5CYII=');
let reloadTimes = $self.data('reload-times');
// Loop an infinite number of times ( maxTries == -1 )
// or until the loop reach options.maxTries times
if (reloadTimes < options.maxTries || options.maxTries == -1) {
// Create a timeout using the options reload time
setTimeout(function() {
// Try to change source to the original
$self.attr('src', $self.data('original-src'))
.on('error', handleImageError);
// Increase the control var and set it to the element
reloadTimes++;
$self.data('reload-times', reloadTimes);
}, options.reloadTime);
}
};
// Original image source
$self.data('original-src', $self.attr('src'));
// Initialise reload times var
$self.data('reload-times', 0);
// Bind handle image funtion to a error event
if ($self.data('original-src') != undefined) {
$self.bind('error', handleImageError);
}
return $self;
},
});

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboLayoutAnimate: function(options) {
// Default options
const defaults = {
effect: 'none',
};
options = $.extend({}, defaults, options);
this.each(function(_key, element) {
const isAndroid = navigator.userAgent.indexOf('Android') > -1;
const $contentDiv = $(element);
// Marquee effect
if (
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown'
) {
$contentDiv.find('.scroll:not(.animating)').marquee();
} else if (
options.effect === 'marqueeLeft' ||
options.effect === 'marqueeRight'
) {
if (isAndroid) {
$contentDiv.find('.scroll:not(.animating)').overflowMarquee();
} else {
$contentDiv.find('.scroll:not(.animating)').marquee();
}
} else if (options.effect !== 'none' ||
options.effect === 'noTransition'
) { // Cycle effect
// Resume effect
const $target = $contentDiv.is('.anim-cycle') ?
$contentDiv : $contentDiv.find('.anim-cycle');
$target.cycle('resume');
}
});
return $(this);
},
});

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboLayoutScaler: function(options) {
// Default options
const defaults = {
originalWidth: 0,
originalHeight: 0,
widgetDesignWidth: 0,
widgetDesignHeight: 0,
widgetDesignGap: 0,
itemsPerPage: 0,
alignmentH: 'center',
alignmentV: 'middle',
displayDirection: 0,
// 0 = undefined (default), 1 = horizontal, 2 = vertical
};
options = $.extend({}, defaults, options);
// Width and Height of the window we're in
const width = $(window).width();
const height = $(window).height();
// Calculate the ratio to apply as a scale transform
let ratio =
Math.min(width / options.originalWidth, height / options.originalHeight);
// Calculate a new width/height based on the ratio
let newWidth = width / ratio;
let newHeight = height / ratio;
// Does the widget have an original design width/height
// if so, we need to further scale the widget
if (options.widgetDesignWidth > 0 && options.widgetDesignHeight > 0) {
if (options.itemsPerPage > 0) {
if (
(newWidth >= newHeight && options.displayDirection == '0') ||
(options.displayDirection == '1')
) {
// Landscape or square size plus padding
// display direction is horizontal
options.widgetDesignWidth =
(options.itemsPerPage * options.widgetDesignWidth) +
(options.widgetDesignGap * (options.itemsPerPage - 1));
options.widgetDesignHeight = options.widgetDesignHeight;
} else if (
(newWidth < newHeight && options.displayDirection == '0') ||
(options.displayDirection == '2')
) {
// Portrait size plus padding
// display direction is vertical
options.widgetDesignHeight =
(options.itemsPerPage * options.widgetDesignHeight) +
(options.widgetDesignGap * (options.itemsPerPage - 1));
options.widgetDesignWidth = options.widgetDesignWidth;
}
}
// Calculate the ratio between the new
const widgetRatio =
Math.min(
newWidth / options.widgetDesignWidth,
newHeight / options.widgetDesignHeight);
ratio = ratio * widgetRatio;
newWidth = options.widgetDesignWidth;
newHeight = options.widgetDesignHeight;
}
// Multiple element options
const mElOptions = {};
// Multiple elements per page
if (options.numCols != undefined || options.numRows != undefined) {
// Content dimensions and scale ( to create
// multiple elements based on the body scale fomr the xibo scaler )
mElOptions.contentWidth =
(options.numCols > 1) ?
(options.widgetDesignWidth * options.numCols) :
options.widgetDesignWidth;
mElOptions.contentHeight =
(options.numRows > 1) ?
(options.widgetDesignHeight * options.numRows) :
options.widgetDesignHeight;
mElOptions.contentScaleX = width / mElOptions.contentWidth;
mElOptions.contentScaleY = height / mElOptions.contentHeight;
// calculate/update ratio
ratio = Math.min(mElOptions.contentScaleX, mElOptions.contentScaleY);
}
// Do nothing and return $(this) when ratio = 1
if (ratio == 1) {
return $(this);
}
// Apply these details
$(this).each(function(_idx, el) {
if (!$.isEmptyObject(mElOptions)) {
$(el).css('transform-origin', '0 0');
$(el).css('transform', 'scale(' + ratio + ')');
$(el).width(mElOptions.contentWidth);
$(el).height(mElOptions.contentHeight);
$(el).find('.multi-element').css({
overflow: 'hidden',
float: 'left',
width: options.widgetDesignWidth,
height: options.widgetDesignHeight,
});
} else {
$(el).css({
width: newWidth,
height: newHeight,
});
// Handle the scaling
// What IE are we?
if ($('body').hasClass('ie7') || $('body').hasClass('ie8')) {
$(el).css({
filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=' +
ratio +
', M12=0, M21=0, M22=' +
ratio +
', SizingMethod=\'auto expand\'',
});
} else {
$(el).css({
transform: 'scale(' + ratio + ')',
'transform-origin': '0 0',
});
}
}
// Set ratio on the body incase we want to get it easily
$(el).attr('data-ratio', ratio);
// Handle alignment (do not add position absolute unless needed)
if (!options.type || options.type !== 'text') {
$(el).css('position', 'absolute');
// Horizontal alignment
if (options.alignmentH === 'right') {
$(el).css('left', width - ($(el).width() * ratio));
} else if (options.alignmentH === 'center') {
$(el).css('left', (width / 2) - ($(el).width() * ratio) / 2);
}
// Vertical alignment
if (options.alignmentV === 'bottom') {
$(el).css('top', height - ($(el).height() * ratio));
} else if (options.alignmentV === 'middle') {
$(el).css('top', (height / 2) - ($(el).height() * ratio) / 2);
}
}
});
return $(this);
},
});

View File

@@ -0,0 +1,302 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
// Based on https://github.com/octalmage/phptomoment/tree/master
const PHP_TO_MOMENT = {
d: 'DD',
D: 'ddd',
j: 'D',
l: 'dddd',
N: 'E',
S: 'o',
w: 'e',
z: 'DDD',
W: 'W',
F: 'MMMM',
m: 'MM',
M: 'MMM',
n: 'M',
t: '',
L: '',
o: 'YYYY',
Y: 'YYYY',
y: 'YY',
a: 'a',
A: 'A',
B: '',
g: 'h',
G: 'H',
h: 'hh',
H: 'HH',
i: 'mm',
s: 'ss',
u: 'SSS',
e: 'zz',
I: '',
O: '',
P: '',
T: '',
Z: '',
c: '',
r: '',
U: 'X',
'\\': '',
};
jQuery.fn.extend({
xiboLegacyTemplateRender: function(options, widget) {
// Default options
const defaults = {
moduleType: 'none',
};
const newOptions = {};
options = $.extend({}, defaults, options);
// For each matched element
this.each(function(_idx, element) {
// Forecast
if (options.moduleType == 'forecast') {
// Check if we have a dailyForecast placeholder
const elementHTML = $(element).html();
const match = elementHTML.match(/\[dailyForecast.*?\]/g);
if (match) {
// Get the number of days
const numDays = match[0].split('|')[1];
const offset = match[0].split('|')[2].replace(']', '');
// Replace HTML on the element
$(element).html(elementHTML.replace(
match[0],
'<div class="forecast-container" ' +
'data-days-num="' + numDays + '" ' +
'data-days-offset="' + offset + '"></div>',
));
}
// Check if we have a time placeholder
$(element).html(
$(element).html().replace(/\[time\|.*?\]/g, function(match) {
const oldFormat = match.split('|')[1].replace(']', '');
const newFormat = PHP_TO_MOMENT[oldFormat];
return '[time|' + newFormat + ']';
}),
);
}
// Social Media
if (options.moduleType == 'social-media') {
// Template HTML
let templateHTML = $(element).find('.item-template').html();
// If we have NameTrimmed, replace it with a trimmed Name
const matches = templateHTML.match(/\[(.*?)\]/g);
if (Array.isArray(matches)) {
for (let index = 0; index < matches.length; index++) {
const match = matches[index];
const matchCropped = match.substring(1, match.length - 1);
let replacement = '';
switch (matchCropped) {
case 'Tweet':
replacement = '{{text}}';
break;
case 'User':
replacement = '{{user}}';
break;
case 'ScreenName':
replacement = '{{screenName}}';
break;
case 'Date':
replacement = '{{date}}';
break;
case 'Location':
replacement = '{{location}}';
break;
case 'ProfileImage':
replacement = '<img src="{{userProfileImage}}" />';
break;
case 'ProfileImage|normal':
replacement = '<img src="{{userProfileImage}}" />';
break;
case 'ProfileImage|mini':
replacement = '<img src="{{userProfileImageMini}}" />';
break;
case 'ProfileImage|bigger':
replacement = '<img src="{{userProfileImageBigger}}" />';
break;
case 'Photo':
replacement = '<img src="{{photo}}" />';
break;
case 'TwitterLogoWhite':
replacement =
$(element).find('.twitter-blue-logo').data('url');
break;
case 'TwitterLogoBlue':
replacement =
$(element).find('.twitter-white-logo').data('url');
default:
break;
}
// Replace HTML on the element
templateHTML = templateHTML.replace(
match,
replacement,
);
}
}
// Compile template for item
const itemTemplate = Handlebars.compile(
templateHTML,
);
// Apply template to items and add them to content
for (let i = 0; i < widget.items.length; i++) {
const item = widget.items[i];
// Create new media item
const $mediaItem =
$('<div class="social-media-item"></div>');
// Add template content to media item
$mediaItem.html(itemTemplate(item));
// Add to content
$mediaItem.appendTo($(element).find('#content'));
}
}
// Currencies and stocks
if (
options.moduleType == 'currencies' ||
options.moduleType == 'stocks'
) {
// Property to trim names
const trimmedNames = [];
const makeTemplateReplacements = function($template) {
let templateHTML = $template.html();
// Replace [itemsTemplate] with a new div element
templateHTML = templateHTML.replace(
'[itemsTemplate]',
'<div class="items-container-helper"></div>',
);
// If we have NameTrimmed, replace it with a trimmed Name
const matches = templateHTML.match(/\[NameTrimmed.*?\]/g);
if (Array.isArray(matches)) {
for (let index = 0; index < matches.length; index++) {
const match = matches[index];
// Get the string length
trimmedNames.push(match.split('|')[1].replace(']', ''));
// Replace HTML on the element
templateHTML = templateHTML.replace(
match,
'[NameTrimmed' + (trimmedNames.length - 1) + ']',
);
}
}
// Add html back to container
$template.html(templateHTML);
// Change new element parent to be the
// item-container class and clear it
$template.find('.items-container-helper')
.parent().addClass('items-container').empty();
// Replace image
let $templateImage = $template.find('img[src="[CurrencyFlag]"]');
if ($templateImage.length > 0) {
const imageTemplate = $(element).find('.sample-image').html();
// Replace HTML with the image template
$templateImage[0].outerHTML = imageTemplate;
// Get new image object
$templateImage = $($templateImage[0]);
}
// Replace curly brackets with double brackets
$template.html(
$template.html().replaceAll('[', '{{').replaceAll(']', '}}'),
);
// Return template
return $template;
};
// Make replacements for item template
$(element).find('.item-template').replaceWith(
makeTemplateReplacements(
$(element).find('.item-template'),
),
);
// Make replacements for container template
$(element).find('.template-container').replaceWith(
makeTemplateReplacements(
$(element).find('.template-container'),
),
);
// Compile template for item
const itemTemplate = Handlebars.compile(
$(element).find('.item-template').html(),
);
// Apply template to items and add them to content
for (let i = 0; i < widget.items.length; i++) {
const item = widget.items[i];
// if we have trimmedNames, add those proterties to each item
for (let index = 0; index < trimmedNames.length; index++) {
const trimmedLength = trimmedNames[index];
item['NameTrimmed' + index] = item.Name.substring(0, trimmedLength);
}
$(itemTemplate(item)).addClass('template-item')
.appendTo($(element).find('#content'));
}
}
// Article
if (options.moduleType == 'article') {
widget.properties.template = widget.properties.template
.replaceAll('[Link|image]', '<img src="[Image]"></div>');
}
});
return {
target: $(this),
options: newOptions,
};
},
});

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
menuBoardRender: function(options) {
function createPage(pageNum, container) {
var $newPage = $('<div>').addClass('menu-board-product-page').attr('data-page', pageNum);
$(container).append($newPage);
return $newPage;
}
$(this).each(function() {
var deltaDuration;
var maxPages = 0;
// Hide all elements
$('.menu-board-product').css('opacity', 0);
// Get height of each products container
$('.menu-board-products-container').each(function() {
var pageNum = 1;
var containerHeight = $(this).height();
var elementsTotalHeight = 0;
var $productContainer = $(this);
// Create first page
var $currentPage = createPage(pageNum, $productContainer);
// Create pages dynamically
$(this).find('.menu-board-product').each(function() {
var $product = $(this);
var productHeight = $product.outerHeight();
// If the current page is full, create a new page
if (productHeight + elementsTotalHeight > containerHeight) {
pageNum++;
elementsTotalHeight = 0;
// Create a new page
$currentPage = createPage(pageNum, $productContainer);
}
// Increase the total height
elementsTotalHeight += productHeight;
// Add element to the current page
$currentPage.append($product);
});
// Fill the last page with first elements
if (pageNum > 1 && elementsTotalHeight < containerHeight) {
$(this).find('.menu-board-product').each(function() {
var $product = $(this);
var productHeight = $product.outerHeight();
// If the current page is full, stop adding elements
if (productHeight + elementsTotalHeight > containerHeight) {
return false;
} else {
// Add cloned element to the current page
$currentPage.append($product.clone());
// Increase the total height
elementsTotalHeight += productHeight;
}
});
}
// Save maxPages if pageNum is higher
if (pageNum > maxPages) {
maxPages = pageNum;
}
});
// Calculate the delta duration ( duration / number of max pages )
deltaDuration = options.duration / maxPages;
// Cycle handles this for us
$('.menu-board-products-container').cycle({
fx: "fade",
timeout: deltaDuration * 1000,
"slides": "> div.menu-board-product-page"
});
// Re-show elements
$('.menu-board-product').css('opacity', 1);
return $(this);
});
}
});

View File

@@ -0,0 +1,454 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
// register hbs template
jQuery.fn.extend({
xiboMetroRender: function(options, items, colors) {
// Default options
const defaults = {
effect: 'none',
duration: '60',
numItems: 0,
speed: '2',
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
cellsPerRow: 6,
cellsPerPage: 18,
numberItemsLarge: 1,
numberItemsMedium: 2,
maxItemsLarge: 3,
maxItemsMedium: 4,
smallItemSize: 1,
mediumItemSize: 2,
largeItemSize: 3,
randomizeSizeRatio: false,
orientation: 'landscape',
};
options = $.extend({}, defaults, options);
options.randomizeSizeRatio = false;
// Set the cells per row according to the widgets original orientation
options.cellsPerRow =
(options.widgetDesignWidth < options.widgetDesignHeight) ? 3 : 6;
const resetRenderElements = function($contentDiv) {
// Destroy cycle plugin
$contentDiv.find('.anim-cycle').cycle('destroy');
// Empty container
$contentDiv.empty();
};
// For each matched element
this.each(function(_idx, element) {
// 1st objective - create an array that defines the
// positions of the items on the layout
// settings involved:
// positionsArray (the array that stores the positions
// of the items according to size)
// largeItems (number of large items to appear on the layout)
// mediumItems (number of medium items to appear on the layout)
// cellsPerPage (number of cells for each page)
// cellsPerRow (number of cells for each row)
// Reset the animation elements
resetRenderElements($(element));
// Create the positions array with size equal to the number
// of cells per page, and each positions starts as undefined
const positionsArray = new Array(options.cellsPerPage);
// Get the page small/medium/large Ratio ( by random or percentage )
let largeItems = 0;
let mediumItems = 0;
if (options.randomizeSizeRatio) {
// START OPTION 1 - RANDOM
// Randomize values so each one can
// have values from default to default+X
largeItems = options.numberItemsLarge + Math.floor(Math.random() * 2);
mediumItems = options.numberItemsMedium + Math.floor(Math.random() * 3);
} else {
// OPTION 2 - PERCENTAGE
// Count image tweets ratio
let tweetsWithImageCount = 0;
for (let i = 0; i < items.length; i++) {
if (checkBackgroundImage(items, i)) {
tweetsWithImageCount++;
}
}
const imageTweetsRatio = tweetsWithImageCount / items.length;
const imageTweetsCellsPerPage =
Math.floor(options.cellsPerPage * imageTweetsRatio);
// Calculate the large/medium quantity according
// to the ratio of withImage/all tweets
// Try to get a number of large items that fit
// on the calculated cells per page
largeItems =
Math.floor(imageTweetsCellsPerPage / options.largeItemSize);
// Get the number of medium items by the remaining cells
// per page "space" left by the large items
mediumItems =
Math.floor(
(imageTweetsCellsPerPage - (largeItems * options.largeItemSize)) /
options.mediumItemSize);
// If the reulting medium/large values are 0
// give them the default option values
if (largeItems == 0) {
largeItems = options.numberItemsLarge;
}
if (mediumItems == 0) {
mediumItems = options.numberItemsMedium;
}
// If the reulting medium/large values are
// over the maximum values set them to max
if (largeItems > options.maxItemsLarge) {
largeItems = options.maxItemsLarge;
}
if (mediumItems > options.maxItemsMedium) {
mediumItems = options.maxItemsMedium;
}
}
// Number of items displayed in each page
let numberOfItems = 0;
// Var to prevent the placement loop to run indefinitley
let loopMaxValue = 100;
// Try to place the large and medium items until theres none of those left
while (mediumItems + largeItems > 0 && loopMaxValue > 0) {
// Calculate a random position inside the array
const positionRandom = Math.floor(Math.random() * options.cellsPerPage);
// I f we still have large items to place
if (largeItems > 0) {
if (
checkFitPosition(
positionsArray,
positionRandom,
options.largeItemSize,
options.cellsPerRow,
) &&
checkCellEmpty(
positionsArray,
positionRandom,
options.largeItemSize,
)
) {
// Set the array positions to the pretended item type
for (let i = 0; i < options.largeItemSize; i++) {
positionsArray[positionRandom + i] = options.largeItemSize;
}
numberOfItems++;
// Decrease the items to place var
largeItems--;
}
} else if (mediumItems > 0) {
if (
checkFitPosition(positionsArray,
positionRandom,
options.mediumItemSize,
options.cellsPerRow,
) &&
checkCellEmpty(positionsArray,
positionRandom,
options.mediumItemSize,
)
) {
// Set the array positions to the pretended item type
for (let i = 0; i < options.mediumItemSize; i++) {
positionsArray[positionRandom + i] = options.mediumItemSize;
}
// Decrease the items to place var
numberOfItems++;
mediumItems--;
}
}
loopMaxValue--;
}
// Fill the rest of the array with small size items
for (let i = 0; i < positionsArray.length; i++) {
if (positionsArray[i] == undefined) {
numberOfItems++;
positionsArray[i] = options.smallItemSize;
}
}
// 2nd objective - put the items on the respective rows,
// add the rows to each page and build the resulting html
// settings involved:
// positionsArray (the array that stores the positions of
// the items according to size)
// How many pages to we need?
const numberOfPages =
(options.numItems > numberOfItems) ?
Math.floor(options.numItems / numberOfItems) : 1;
let rowNumber = 0;
let itemId = 0;
let pageId = 0;
// If we dont have enough items to fill a page,
// change the items array to have dummy position between items
if (items.length < numberOfItems) {
// Create a new array
const newItems = [];
// Distance between items so they can be spread in the page
const distance = Math.round(numberOfItems / items.length);
let idAux = 0;
for (let i = 0; i < numberOfItems; i++) {
if (i % distance == 0) {
// Place a real item
newItems.push(items[idAux]);
idAux++;
} else {
// Place a dummy item
newItems.push(undefined);
}
}
items = newItems;
}
// Create an auxiliary items array, so we can
// place the tweets at the same time we remove them from the new array
const itemsAux = items;
// Cycle through all the positions on the positionsArray
for (let i = 0; i < positionsArray.length; i++) {
// If we are on the first cell position, create a row
if (i % options.cellsPerRow == 0) {
rowNumber += 1;
$(element).append(
'<div class=\'row-1\' id=\'idrow-' +
rowNumber +
'\'></div>');
}
// Create a page and add it to the content div
$(element).append(
'<div id=\'page-' +
pageId +
'\' class="page metro-render-anim-item"></div>');
for (let j = 0; j < numberOfPages; j++) {
// Pass the item to a variable and replace some tags
// if there's no item we create a dummy item
let stringHTML = '';
// Search for the item to remove regarding the
// type of the tweet (with/without image)
const indexToRemove =
checkImageTweet(itemsAux, (positionsArray[i] > 1));
// Get a random color
const randomColor = colors[Math.floor(Math.random() * colors.length)];
if (itemsAux[indexToRemove] != undefined) {
// Get the item and replace the color tag
stringHTML = itemsAux[indexToRemove]
.replace('[Color]', randomColor);
} else {
stringHTML =
'<div class=\'cell-[itemType]\'>' +
'<div class=\'item-container\' style=\'background-color:' +
randomColor +
'\'><div class=\'item-text\'></div>' +
'<div class=\'userData\'></div></div></div>';
}
// Remove the element that we used to create the new html
itemsAux.splice(indexToRemove, 1);
// Increase the item ID
itemId++;
// Replace the item ID and Type on its html
stringHTML = stringHTML.replace('[itemId]', itemId);
stringHTML = stringHTML.replace('[itemType]', positionsArray[i]);
// Add animate class to item
const $newItem = $(stringHTML).addClass('metro-render-anim-item');
// Append item to the current page
$newItem.appendTo(
$(element).find('#page-' + pageId),
);
}
// Move the created page into the respective row
$(element).find('#idrow-' + rowNumber).append(
$(element).find('#page-' + pageId),
);
// Increase the page ID var
pageId++;
// Increase the iterator so it can move forward
// the number of cells that the current item occupies
i += positionsArray[i] - 1;
}
// 3rd objective - move the items around, start the timer
// settings involved:
// effect (the way we are moving effects the HTML required)
// speed (how fast we need to move
// Make sure the speed is something sensible
options.speed = (options.speed <= 200) ? 1000 : options.speed;
const slides = '.cell';
// Duration of each page
const pageDuration = options.duration / numberOfPages;
// Use cycle in all pages of items ( to cycle individually )
// only if we have an effect
if (options.effect !== 'none') {
for (let i = 0; i < numberOfItems; i++) {
// Timeout is the duration in ms
const timeout = (pageDuration * 1000);
const noTransitionSpeed = 10;
// The delay is calulated usign the distance between items
// ( random from 1 to 5 )
// that animate almost at the same time
// and a part of the timeout duration
const delayDistance = 1 + Math.random() * 4;
const delay = (timeout / delayDistance) * ((i + 1) % delayDistance);
// Get page element and start cycle
const $currentPage = $(element).find('#page-' + i)
.addClass('anim-cycle');
$currentPage.cycle({
fx: (options.effect === 'noTransition') ? 'none' : options.effect,
speed: (options.effect === 'noTransition') ?
noTransitionSpeed : options.speed,
delay: -delay,
timeout: timeout,
slides: '> ' + slides,
log: false,
});
}
}
// Protect against images that don't load
$(element).find('img').on('error', function() {
$(element).off('error')
.attr(
'src',
// eslint-disable-next-line max-len
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNiYAAAAAkAAxkR2eQAAAAASUVORK5CYII=');
});
});
return $(this);
},
});
/**
* Check if a set of given cells of an array are empty (undefined)
* @param {array} array - Array of items
* @param {int} index - Index of the item to check
* @param {int} size - Size of the item to check
* @return {boolean} - True if the cells are empty, false otherwise
*/
function checkCellEmpty(array, index, size) {
let check = true;
for (let i = 0; i < size; i++) {
if (array[index + i] != undefined) {
check = false;
}
}
return check;
}
/**
* Check if a given position of an array is good to
* fit an item given it's size and position
* @param {array} array - Array of items
* @param {int} index - Index of the item to check
* @param {int} size - Size of the item to check
* @param {int} cellsPerRow - Number of cells per row
* @return {boolean} - True if the item fits, false otherwise
*/
function checkFitPosition(array, index, size, cellsPerRow) {
return (index % cellsPerRow <= cellsPerRow - size);
}
/**
* Check if a given item has background image
* @param {array} array - Array of items
* @param {int} index - Index of the item to check
* @return {boolean} - True if the item has background image, false otherwise
*/
function checkBackgroundImage(array, index) {
// Prevent check if the item is undefined
if (array[index] == undefined) {
return false;
}
return (array[index].indexOf('background-image') >= 0);
}
/**
* Find a tweet with image (or one without image), if not return 0
* @param {array} array - Array of items
* @param {boolean} withImage - True if we are looking for a tweet with image
* false otherwise
* @return {int} - Index of the item found, 0 if not found
*/
function checkImageTweet(array, withImage) {
// Default return var
let returnVar = 0;
for (let i = 0; i < array.length; i++) {
// Find a tweet with image
if (withImage && checkBackgroundImage(array, i)) {
returnVar = i;
break;
}
// Find a tweet without image
if (!withImage && !checkBackgroundImage(array, i)) {
returnVar = i;
break;
}
}
return returnVar;
}

1968
modules/src/xibo-player.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboSubstitutesParser: function(
template,
dateFormat,
dateFields = [],
mapping = {},
) {
const items = [];
const parser = new RegExp('\\[.*?\\]', 'g');
const pipeParser = new RegExp('\\|{1}', 'g');
this.each(function(_idx, data) {
// Parse the template for a list of things to substitute, and match those
// with content from items.
let replacement = template;
let match = parser.exec(template);
while (match != null) {
// Matched text: match[0], match start: match.index,
// capturing group n: match[n]
// Remove the [] from the match
let variable = match[0]
.replace('[', '')
.replace(']', '');
variable = variable.charAt(0).toLowerCase() + variable.substring(1);
// Check if variable has its own formatting
// Then, parse it and use later as dateFormat
let formatFromTemplate = null;
if (variable.match(pipeParser) !== null &&
variable.match(pipeParser).length === 1) {
const variableWithFormat = variable.split('|');
formatFromTemplate = variableWithFormat[1];
variable = variableWithFormat[0];
}
if (mapping[variable]) {
variable = mapping[variable];
}
let value = '';
// Does this variable exist? or is it one of the ones in our map
if (data.hasOwnProperty(variable)) {
// Use it
value = data[variable];
// Is it a date field?
dateFields.forEach((field) => {
if (field === variable) {
value = moment(value).format(formatFromTemplate !== null ?
formatFromTemplate : dateFormat);
}
});
}
// If value is null, set it as empty string
(value === null) && (value = '');
// Finally set the replacement in the template
replacement = replacement.replace(match[0], value);
// Get the next match
match = parser.exec(template);
}
// Add to our items
items.push(replacement);
});
return items;
},
});

View File

@@ -0,0 +1,430 @@
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboTextRender: function(options, items) {
// Default options
const defaults = {
effect: 'none',
pauseEffectOnStart: true,
duration: '50',
durationIsPerItem: false,
numItems: 1,
itemsPerPage: 0,
speed: '2',
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
randomiseItems: 0,
marqueeInlineSelector: '.text-render-item, .text-render-item p',
alignmentV: 'top',
widgetDesignWidth: 0,
widgetDesignHeight: 0,
widgetDesignGap: 0,
displayDirection: 0,
seamless: true,
};
options = $.extend({}, defaults, options);
const resetRenderElements = function($contentDiv) {
// Remove item classes
$contentDiv.find('.text-render-item').removeClass('text-render-item');
// Remove animation items
$contentDiv.find('.text-render-anim-item').remove();
// If options is seamless, remove second .scroll marquee div
// so we don't have duplicated elements
if (
options.seamless &&
$contentDiv.find('.scroll .js-marquee').length > 1
) {
$contentDiv.find('.scroll .js-marquee')[1].remove();
}
// Show and reset the hidden elements
const $originalElements =
$contentDiv.find('.text-render-hidden-element');
$originalElements.removeClass('text-render-hidden-element').show();
// If we have a scroll container, move elements
// to content and destroy container
if ($contentDiv.find('.scroll').length > 0) {
$originalElements.appendTo($contentDiv);
$contentDiv.find('.scroll').remove();
}
};
// If number of items is not defined, get it from the item count
options.numItems = options.numItems ? options.numItems : items.length;
// Calculate the dimensions of this item
// based on the preview/original dimensions
let width = height = 0;
if (options.previewWidth === 0 || options.previewHeight === 0) {
width = options.originalWidth;
height = options.originalHeight;
} else {
width = options.previewWidth;
height = options.previewHeight;
}
if (options.scaleOverride !== 0) {
width = width / options.scaleOverride;
height = height / options.scaleOverride;
}
let paddingBottom = paddingRight = 0;
if (options.widgetDesignWidth > 0 && options.widgetDesignHeight > 0) {
if (options.itemsPerPage > 0) {
if (
(
$(window).width() >= $(window).height() &&
options.displayDirection == '0'
) ||
(options.displayDirection == '1')
) {
// Landscape or square size plus padding
options.widgetDesignWidth =
(options.itemsPerPage * options.widgetDesignWidth) +
(options.widgetDesignGap * (options.itemsPerPage - 1));
options.widgetDesignHeight = options.widgetDesignHeight;
width = options.widgetDesignWidth;
height = options.widgetDesignHeight;
paddingRight = options.widgetDesignGap;
} else if (
(
$(window).width() < $(window).height() &&
options.displayDirection == '0'
) ||
(options.displayDirection == '2')
) {
// Portrait size plus padding
options.widgetDesignHeight =
(options.itemsPerPage * options.widgetDesignHeight) +
(options.widgetDesignGap * (options.itemsPerPage - 1));
options.widgetDesignWidth = options.widgetDesignWidth;
width = options.widgetDesignWidth;
height = options.widgetDesignHeight;
paddingBottom = options.widgetDesignGap;
}
}
}
const isAndroid = navigator.userAgent.indexOf('Android') > -1;
// For each matched element
this.each(function(_key, element) {
// console.log("[Xibo] Selected: " + this.tagName.toLowerCase());
// console.log("[Xibo] Options: " + JSON.stringify(options));
const $contentDiv = $(element).find('#content');
// Is marquee effect
const isMarquee =
options.effect === 'marqueeLeft' ||
options.effect === 'marqueeRight' ||
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown';
const isUseNewMarquee = options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown' ||
!isAndroid;
// Reset the animation elements
resetRenderElements($contentDiv);
// Store the number of items (we might change this to number of pages)
let numberOfItems = options.numItems;
// How many pages to we need?
// if there's no effect, we don't need any pages
const numberOfPages =
(options.itemsPerPage > 0 && options.effect !== 'none') ?
Math.ceil(options.numItems / options.itemsPerPage) :
options.numItems;
let itemsThisPage = 1;
// console.log("[Xibo] We need to have " + numberOfPages + " pages");
let appendTo = $contentDiv;
// Clear previous animation elements
if (isMarquee && isUseNewMarquee) {
$contentDiv.marquee('destroy');
} else {
// Destroy cycle plugin
$(element).find('.anim-cycle').cycle('destroy');
}
// If we have animations
// Loop around each of the items we have been given
// and append them to this element (in a div)
if (options.effect !== 'none') {
for (let i = 0; i < items.length; i++) {
// We don't add any pages for marquee
if (!isMarquee) {
// If we need to set pages, have we switched over to a new page?
if (
options.itemsPerPage > 1 &&
(itemsThisPage >= options.itemsPerPage || i === 0)
) {
// Append a new page to the body
appendTo = $('<div/>')
.addClass('text-render-page text-render-anim-item')
.appendTo($contentDiv);
// Reset the row count on this page
itemsThisPage = 0;
}
}
// For each item, create a DIV if element doesn't exist on the DOM
// Or clone the element if it does
// hide the original and show the clone
let $newItem;
let $oldItem;
if ($.contains(element, items[i])) {
$oldItem = $(items[i]);
$newItem = $oldItem.clone();
} else {
$oldItem = null;
$newItem = $('<div/>').html(items[i]);
}
// Hide and mark as hidden the original element
($oldItem) && $oldItem.hide().addClass('text-render-hidden-element');
// Append the item to the page
$newItem
.addClass('text-render-item text-render-anim-item')
.appendTo(appendTo);
itemsThisPage++;
}
}
// 4th objective - move the items around, start the timer
// settings involved:
// fx (the way we are moving effects the HTML required)
// speed (how fast we need to move)
let marquee = false;
if (options.effect === 'none') {
// Do nothing
} else if (!isMarquee) {
// Make sure the speed is something sensible
options.speed = (options.speed <= 200) ? 1000 : options.speed;
// Cycle slides are either page or item
let slides =
(options.itemsPerPage > 1) ?
'.text-render-page' :
'.text-render-item';
// If we only have 1 item, then
// we are in trouble and need to duplicate it.
if ($(slides).length <= 1 && options.type === 'text') {
// Change our slide tag to be the paragraphs inside
slides = slides + ' p';
// Change the number of items
numberOfItems = $(slides).length;
} else if (options.type === 'text') {
numberOfItems = $(slides).length;
}
const numberOfSlides = (options.itemsPerPage > 1) ?
numberOfPages :
numberOfItems;
const duration = (options.durationIsPerItem) ?
options.duration :
options.duration / numberOfSlides;
// console.log("[Xibo] initialising the cycle2 plugin with "
// + numberOfSlides + " slides and selector " + slides +
// ". Duration per slide is " + duration + " seconds.");
// Set the content div to the height of the original window
$contentDiv.css('height', height);
// Set the width on the cycled slides
$(slides, $contentDiv).css({
width: width,
height: height,
});
let timeout = duration * 1000;
const noTransitionSpeed = 10;
if (options.effect !== 'noTransition') {
timeout = timeout - options.speed;
} else {
timeout = timeout - noTransitionSpeed;
}
// Cycle handles this for us
$contentDiv.addClass('anim-cycle').cycle({
fx: (options.effect === 'noTransition') ? 'none' : options.effect,
speed: (options.effect === 'noTransition') ?
noTransitionSpeed : options.speed,
timeout: timeout,
slides: '> ' + slides,
autoHeight: false, // To fix the rogue sentinel issue
paused: options.pauseEffectOnStart,
log: false,
});
} else if (
options.effect === 'marqueeLeft' ||
options.effect === 'marqueeRight'
) {
marquee = true;
options.direction =
((options.effect === 'marqueeLeft') ? 'left' : 'right');
// Make sure the speed is something sensible
options.speed = (options.speed === 0) ? 1 : options.speed;
// Stack the articles up and move them across the screen
$(
options.marqueeInlineSelector + ':not(.text-render-hidden-element)',
$contentDiv,
).each(function(_idx, _el) {
if (!$(_el).hasClass('text-render-hidden-element')) {
$(_el).css({
display: 'inline-block',
'padding-left': '10px',
});
}
});
} else if (
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown'
) {
// We want a marquee
marquee = true;
options.direction = ((options.effect === 'marqueeUp') ? 'up' : 'down');
// Make sure the speed is something sensible
options.speed = (options.speed === 0) ? 1 : options.speed;
// Set the content div height, if we don't do this when the marquee
// plugin floats the content inside, this goes to 0 and up/down
// marquees don't work
$contentDiv.css('height', height);
}
if (marquee) {
// Create a DIV to scroll, and put this inside the body
const scroller = $('<div/>')
.addClass('scroll');
if (isUseNewMarquee) {
// in old marquee scroll delay is 85 milliseconds
// options.speed is the scrollamount
// which is the number of pixels per 85 milliseconds
// our new plugin speed is pixels per second
scroller.attr({
'data-is-legacy': false,
'data-speed': options.speed / 25 * 1000,
'data-direction': options.direction,
'data-duplicated': options.seamless,
scaleFactor: options.scaleFactor,
});
} else {
scroller.attr({
'data-is-legacy': true,
scrollamount: options.speed,
scaleFactor: options.scaleFactor,
behaviour: 'scroll',
direction: options.direction,
height: height,
width: width,
});
}
$contentDiv.wrapInner(scroller);
// Correct for up / down
if (
options.effect === 'marqueeUp' ||
options.effect === 'marqueeDown'
) {
// Set the height of the scroller to 100%
$contentDiv.find('.scroll')
.css('height', '100%')
.children()
.css({'white-space': 'normal', float: 'none'});
}
if (!options.pauseEffectOnStart) {
// Set some options on the extra DIV and make it a marquee
if (isUseNewMarquee) {
$contentDiv.find('.scroll').marquee();
} else {
$contentDiv.find('.scroll').overflowMarquee();
}
// Add animating class to prevent multiple inits
$contentDiv.find('.scroll').addClass('animating');
}
}
// Add aditional padding to the items
if (paddingRight > 0 || paddingBottom > 0) {
// Add padding to all item elements
$('.text-render-item').css(
'padding',
'0px ' + paddingRight + 'px ' + paddingBottom + 'px 0px',
);
// Exclude the last item on the page and
// the last on the content ( if there are no pages )
$('.text-render-page .text-render-item:last-child').css('padding', 0);
$('#content .text-render-item:last').css('padding', 0);
}
// Align the whole thing according to vAlignment
if (options.type && options.type === 'text') {
// The timeout just yields a bit to let our content get rendered
setTimeout(function() {
if (options.alignmentV === 'bottom') {
$contentDiv.css(
'margin-top',
$(window).height() -
($contentDiv.height() * $('body').data().ratio),
);
} else if (options.alignmentV === 'middle') {
$contentDiv.css(
'margin-top',
(
$(window).height() -
($contentDiv.height() * $('body').data().ratio)
) / 2,
);
}
}, 500);
}
});
return $(this);
},
});

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboTextScaler: function(options) {
// Default options
const defaults = {
fitTarget: '',
fontFamily: '',
fitScaleAxis: 'x',
isIcon: false,
};
options = $.extend({}, defaults, options);
// For each matched element
this.each(function(_key, el) {
const elWidth = $(el).width();
const elHeight = $(el).height();
// Continue only if we have a valid element
if (elWidth == 0 || elHeight == 0) {
return $(el);
}
const $fitTarget = (options.fitTarget != '') ?
$(el).find(options.fitTarget) :
$(el);
const waitForFontToLoad = function(font, callback) {
// Wait for font to load
// but also make sure target has any dimensions
if (
$fitTarget.width() > 0 &&
$fitTarget.height() > 0 &&
document.fonts.check(font)
) {
callback();
} else {
setTimeout(function() {
waitForFontToLoad(font, callback);
}, 100);
}
};
if (options.isIcon) {
const fontFamily = (options.fontFamily) ?
options.fontFamily : $fitTarget.css('font-family');
const maxFontSize = 1000;
let fontSize = 1;
$fitTarget.css('font-size', fontSize);
// Wait for font to load, then run resize
waitForFontToLoad(fontSize + 'px ' + fontFamily, function() {
while (fontSize < maxFontSize) {
const auxFontSize = fontSize + 2;
// Increase font
$fitTarget.css('font-size', fontSize);
const doesItBreak = (options.fitScaleAxis === 'y') ?
$fitTarget.height() > elHeight :
$fitTarget.width() > elWidth;
// When it breaks, use previous fontSize
if (doesItBreak) {
break;
} else {
// Increase font size and continue
fontSize = auxFontSize;
}
}
// Set font size to element
$fitTarget.css('font-size', fontSize);
});
} else {
const maxFontSize = 1000;
let fontSize = 1;
// Text
const fontFamily = (options.fontFamily) ?
options.fontFamily : 'sans-serif';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const text = $fitTarget.html().trim();
const fontStyle = $fitTarget.css('font-style');
const fontWeight = $fitTarget.css('font-weight');
// If text is empty, dont resize
if (text.length === 0) {
return $(el);
}
// Set a low font size to begin with
$fitTarget.css('font-size', fontSize);
$fitTarget.hide();
// Wait for font to load, then run resize
waitForFontToLoad(fontWeight + ' ' + fontStyle + ' ' +
fontSize + 'px ' + fontFamily, function() {
context.font =
fontWeight + ' ' + fontStyle + ' ' +
fontSize + 'px ' + fontFamily;
while (fontSize < maxFontSize) {
const auxFontSize = fontSize + 1;
// Increase font
context.font =
fontWeight + ' ' + fontStyle + ' ' +
auxFontSize + 'px ' + fontFamily;
const doesItBreak = (options.fitScaleAxis === 'y') ?
context.measureText(text).height > elHeight :
context.measureText(text).width > elWidth;
// When it breaks, use previous fontSize
if (doesItBreak) {
break;
} else {
// Increase font size and continue
fontSize = auxFontSize;
}
}
// Set font size to element
$fitTarget.css('font-size', fontSize);
$fitTarget.show();
});
}
});
return $(this);
},
});

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboIframeScaler: function(options) {
let width;
let height;
const iframeWidth = parseInt(options.iframeWidth);
const iframeHeight = parseInt(options.iframeHeight);
// All we worry about is the item we have been working on ($(this))
$(this).each(function(_idx, el) {
// Mode
if (options.modeid == 1) {
// Open Natively
// We shouldn't ever get here, because the
// Layout Designer will not show a preview for mode 1, and
// the client will not call GetResource at all for mode 1
$(el).css({
width: options.originalWidth,
height: options.originalHeight,
});
} else if (options.modeid == 3) {
// Best fit, set the scale so that the web-page fits inside the region
// If there is a preview width and height
// then we want to reset the original width and height in the
// ratio calculation so that it represents the
// preview width/height * the scale override
let originalWidth = options.originalWidth;
let originalHeight = options.originalHeight;
if (options.scaleOverride !== 0) {
// console.log("Iframe: Scale Override is set,
// meaning we want to scale according to the provided
// scale of " + options.scaleOverride + ". Provided Width is " +
// options.previewWidth + ". Provided Height is " +
// options.previewHeight + ".");
ratio = options.scaleOverride;
originalWidth = options.previewWidth / ratio;
originalHeight = options.previewHeight / ratio;
}
options.scale = Math.min(
originalWidth / iframeWidth,
originalHeight / iframeHeight,
);
// Remove the offsets
options.offsetTop = 0;
options.offsetLeft = 0;
// Set frame to the full size and scale it back to fit inside the window
if ($('body').hasClass('ie7') || $('body').hasClass('ie8')) {
$(el).css({
filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=' +
options.scale + ', M12=0, M21=0, M22=' + options.scale +
', SizingMethod=\'auto expand\'',
});
} else {
$(el).css({
transform: 'scale(' + options.scale + ')',
'transform-origin': '0 0',
width: iframeWidth,
height: iframeHeight,
});
}
} else {
// Manual Position. This is the default.
// We want to set its margins and scale
// according to the provided options.
// Offsets
const offsetTop = parseInt(options.offsetTop) ?
parseInt(options.offsetTop) : 0;
const offsetLeft = parseInt(options.offsetLeft) ?
parseInt(options.offsetLeft) : 0;
// Dimensions
width = iframeWidth + offsetLeft;
height = iframeHeight + offsetTop;
// Margins on frame
$(el).css({
'margin-top': -1 * offsetTop,
'margin-left': -1 * offsetLeft,
width: width,
height: height,
});
// Do we need to scale?
if (options.scale !== 1 && options.scale !== 0) {
if ($('body').hasClass('ie7') || $('body').hasClass('ie8')) {
$(el).css({
filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=' +
options.scale + ', M12=0, M21=0, M22=' +
options.scale + ', SizingMethod=\'auto expand\'',
});
} else {
$(el).css({
transform: 'scale(' + options.scale + ')',
'transform-origin': '0 0',
width: width / options.scale,
height: height / options.scale,
});
}
}
}
});
},
});

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
jQuery.fn.extend({
xiboWorldClockRender: function(options, template) {
const worldClocks =
(options.worldClocks != '' && options.worldClocks != null) ?
JSON.parse(options.worldClocks) : [];
const self = this;
// Make replacements to the template
const $templateHTML = $(template).html();
$(template).html($templateHTML.replace(/\[.*?\]/g, function(match) {
const matchContent = match.replace(/[\[\]]/g, '');
if (matchContent == 'label') {
return '<span class="world-clock-label"></span>';
} else {
return '<span class="momentClockTag" format="' +
matchContent + '"></span>';
}
}));
// Update clocks
const updateClocks = function() {
const timeNow = moment();
for (let index = 0; index < worldClocks.length; index++) {
// Get time according to timezone
const t = timeNow.tz(worldClocks[index].clockTimezone);
const $clockContainer = $(self).find('#clock' + index);
$clockContainer.find('.year').html(t.year());
$clockContainer.find('.month').html(t.month() + 1);
$clockContainer.find('.week').html(t.week());
$clockContainer.find('.day').html(t.date());
$clockContainer.find('.hour').html(t.hours());
$clockContainer.find('.minutes').html(t.minutes());
$clockContainer.find('.seconds').html(t.seconds());
$clockContainer.find('.momentClockTag').each(function(_k, el) {
$(el).html(t.format($(el).attr('format')));
});
const secondAnalog = t.seconds() * 6;
const minuteAnalog = t.minutes() * 6 + secondAnalog / 60;
const hourAnalog =
((t.hours() % 12) / 12) * 360 + 90 + minuteAnalog / 12;
$clockContainer.find('.analogue-clock-hour')
.css('transform', 'rotate(' + hourAnalog + 'deg)');
$clockContainer.find('.analogue-clock-minute')
.css('transform', 'rotate(' + minuteAnalog + 'deg)');
$clockContainer.find('.analogue-clock-second')
.css('transform', 'rotate(' + secondAnalog + 'deg)');
}
};
// Default options
const defaults = {
duration: '30',
previewWidth: 0,
previewHeight: 0,
scaleOverride: 0,
};
options = $.extend({}, defaults, options);
// For each matched element
this.each(function() {
for (let index = 0; index < worldClocks.length; index++) {
// Create new item and add a copy of the template html
const $newItem =
$('<div>').attr('id', 'clock' + index)
.addClass('world-clock multi-element')
.html($(template).html());
// Add label or timezone name
$newItem.find('.world-clock-label')
.html(
(worldClocks[index].clockLabel != '') ?
worldClocks[index].clockLabel :
worldClocks[index].clockTimezone,
);
// Check if clock has highlighted class
if (worldClocks[index].clockHighlight) {
$newItem.addClass('highlighted');
}
// Check if the element is outside the drawing area ( cols * rows )
if ((index + 1) > (options.numCols * options.numRows)) {
$newItem.css('display', 'none');
}
// Add content to the main container
$(self).find('#content').append($newItem);
}
// Update clocks
updateClocks(); // run function once at first to avoid delay
// Update every second
setInterval(updateClocks, 1000);
});
return $(this);
},
});