Initial Upload
This commit is contained in:
5
modules/src/README.md
Normal file
5
modules/src/README.md
Normal 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.
|
||||
54
modules/src/editor-render.js
Normal file
54
modules/src/editor-render.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
132
modules/src/handlebars-helpers.js
Normal file
132
modules/src/handlebars-helpers.js
Normal 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;
|
||||
});
|
||||
68
modules/src/player_bundle.js
Normal file
68
modules/src/player_bundle.js
Normal 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');
|
||||
1412
modules/src/xibo-calendar-render.js
Normal file
1412
modules/src/xibo-calendar-render.js
Normal file
File diff suppressed because it is too large
Load Diff
166
modules/src/xibo-countdown-render.js
Normal file
166
modules/src/xibo-countdown-render.js
Normal 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);
|
||||
},
|
||||
});
|
||||
57
modules/src/xibo-dataset-render.js
Normal file
57
modules/src/xibo-dataset-render.js
Normal 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);
|
||||
},
|
||||
});
|
||||
212
modules/src/xibo-elements-render.js
Normal file
212
modules/src/xibo-elements-render.js
Normal 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;
|
||||
},
|
||||
});
|
||||
165
modules/src/xibo-finance-render.js
Normal file
165
modules/src/xibo-finance-render.js
Normal 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);
|
||||
},
|
||||
});
|
||||
83
modules/src/xibo-image-render.js
Normal file
83
modules/src/xibo-image-render.js
Normal 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;
|
||||
},
|
||||
});
|
||||
60
modules/src/xibo-layout-animate.js
Normal file
60
modules/src/xibo-layout-animate.js
Normal 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);
|
||||
},
|
||||
});
|
||||
180
modules/src/xibo-layout-scaler.js
Normal file
180
modules/src/xibo-layout-scaler.js
Normal 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);
|
||||
},
|
||||
});
|
||||
302
modules/src/xibo-legacy-template-render.js
Normal file
302
modules/src/xibo-legacy-template-render.js
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
108
modules/src/xibo-menuboard-render.js
Normal file
108
modules/src/xibo-menuboard-render.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
454
modules/src/xibo-metro-render.js
Normal file
454
modules/src/xibo-metro-render.js
Normal 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
1968
modules/src/xibo-player.js
Normal file
File diff suppressed because it is too large
Load Diff
90
modules/src/xibo-substitutes-parser.js
Normal file
90
modules/src/xibo-substitutes-parser.js
Normal 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;
|
||||
},
|
||||
});
|
||||
430
modules/src/xibo-text-render.js
Normal file
430
modules/src/xibo-text-render.js
Normal 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);
|
||||
},
|
||||
});
|
||||
154
modules/src/xibo-text-scaler.js
Normal file
154
modules/src/xibo-text-scaler.js
Normal 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);
|
||||
},
|
||||
});
|
||||
128
modules/src/xibo-webpage-render.js
Normal file
128
modules/src/xibo-webpage-render.js
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
125
modules/src/xibo-worldclock-render.js
Normal file
125
modules/src/xibo-worldclock-render.js
Normal 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);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user