/*
* 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 .
*/
const PlayerHelper = function() {
// Check the query params to see if we're in editor mode
const self = this;
this.getPinnedSlots = function(dataSlots) {
return Object.keys(dataSlots)
.reduce(function(a, b) {
const dataSlot = dataSlots[b];
if (dataSlot.hasPinnedSlot) return [...a, dataSlot.slot];
return a;
}, []);
};
this.getPinnedItems = function(dataSlotItems) {
if (Object.values(dataSlotItems).length === 0) {
return dataSlotItems;
}
return Object.keys(dataSlotItems).reduce(function(items, itemKey) {
const item = dataSlotItems[itemKey];
if (item.pinSlot) {
items[itemKey] = item;
}
return items;
}, {});
};
/**
* Get items by Key
* @param {Object} items
* @param {String} itemsKey
* @param {Boolean} isStandalone
*
* @return {Array}
*/
this.getItemsByKey = (items, itemsKey, isStandalone) => {
if (isStandalone && items.hasOwnProperty(itemsKey) &&
Object.keys(items[itemsKey]).length > 0
) {
return Object.keys(items[itemsKey]).reduce(function(a, itemKey) {
return [...a, items[itemsKey][itemKey]];
}, []);
}
if (items.hasOwnProperty(itemsKey)) {
return items[itemsKey];
}
return [];
};
/**
* Gets minimum and maximum slot
* If minSlot is zero, it means it's not a data slot
* @param {Array} collection
* @return {{minSlot: (number|number), maxSlot: (number|number)}}
*/
this.getMinAndMaxSlot = function(collection) {
const minValue = 1;
const getSlots = (items) => items.map(function(elem) {
return elem?.slot + 1 || 0;
});
const minSlot = collection === null ?
minValue :
Math.min(...getSlots(collection));
const maxSlot = collection === null ?
minValue :
Math.max(...getSlots(collection));
return {
minSlot,
maxSlot,
};
};
this.getMaxMinSlot = (objectsArray, itemsKey, isStandalone) => {
const minValue = 1;
const groupItems = objectsArray?.length > 0 ?
objectsArray.reduce(
(a, b) => {
return [...a, ...self.getItemsByKey(b, itemsKey, isStandalone)];
}, []) : null;
const getSlots = (items) => items.map(function(elem) {
return elem?.slot || 0;
});
const minSlot = groupItems === null ?
minValue :
Math.min(...getSlots(groupItems)) + 1;
const maxSlot = groupItems === null ?
minValue :
Math.max(...getSlots(groupItems)) + 1;
return {
minSlot,
maxSlot,
};
};
this.isGroup = function(element) {
return element.hasOwnProperty('groupId');
};
this.isMarquee = function(effect) {
return effect === 'marqueeLeft' ||
effect === 'marqueeRight' ||
effect === 'marqueeUp' ||
effect === 'marqueeDown';
};
this.renderElement = function(hbs, props, isStatic) {
const hbsTemplate = hbs(Object.assign(props, globalOptions));
let topPos = props.top;
let leftPos = props.left;
const hasGroup = Boolean(props.groupId);
const hasGroupProps = Boolean(props.groupProperties);
// @NOTE: I think this is deprecated but needs more checking
if (props.group) {
if (props.group.isMarquee) {
topPos = (props.top - props.group.top);
leftPos = (props.left - props.group.left);
} else {
if (props.top >= props.group.top) {
topPos = (props.top - props.group.top);
}
if (props.left >= props.group.left) {
leftPos = (props.left - props.group.left);
}
}
}
let cssStyles = {
height: props.height,
width: props.width,
position: 'absolute',
top: topPos,
left: leftPos,
zIndex: props.layer,
transform: `rotate(${props?.rotation || 0}deg)`,
};
if (isStatic) {
cssStyles = {
...cssStyles,
top: props.top,
left: props.left,
zIndex: props.layer,
};
if (hasGroup && hasGroupProps) {
cssStyles.top = (props.top >= props.groupProperties.top) ?
(props.top - props.groupProperties.top) : 0;
cssStyles.left = (props.left >= props.groupProperties.left) ?
(props.left - props.groupProperties.left) : 0;
cssStyles.zIndex = 'none';
}
}
if (!props.isGroup && props.dataOverride === 'text' &&
(props.group && props.group.isMarquee) &&
(props.effect === 'marqueeLeft' || props.effect === 'marqueeRight')
) {
cssStyles = {
...cssStyles,
position: 'static',
top: 'unset',
left: 'unset',
width: props?.textWrap ? props.width : 'initial',
display: 'flex',
flexShrink: '0',
wordWrap: 'break-word',
};
}
const $renderedElem = $(hbsTemplate).first()
.attr('id', props.elementId)
.addClass(`${props.uniqueID}--item`)
.css(cssStyles);
if (!props.isGroup && props.dataOverride === 'text' &&
(props.group && props.group.isMarquee) &&
(props.effect === 'marqueeLeft' || props.effect === 'marqueeRight')
) {
$renderedElem.get(0).style.removeProperty('white-space');
$renderedElem.get(0).style.setProperty(
'white-space',
props?.textWrap ? 'unset' : 'nowrap',
'important',
);
}
return $renderedElem.prop('outerHTML');
};
this.renderDataItem = function(
isGroup,
dataItemKey,
dataItem,
item,
slot,
maxSlot,
isPinSlot,
pinnedSlots,
groupId,
$groupContent,
groupObj,
meta,
$content,
) {
const $groupContentItem = $(`
`);
const groupKey = '.' + groupId + '--item[data-group-key=%key%]';
// For each data item, parse it and add it to the content;
if (item.hasOwnProperty('hbs') &&
typeof item.hbs === 'function' && dataItemKey !== 'empty'
) {
let groupItemStyles = {
width: groupObj.width,
height: groupObj.height,
};
if (groupObj && groupObj.isMarquee) {
groupItemStyles = {
...groupItemStyles,
position: 'relative',
display: 'flex',
flexShrink: '0',
};
}
$groupContentItem.css(groupItemStyles);
if ($groupContent &&
$groupContent.find(
groupKey.replace('%key%', dataItemKey),
).length === 0
) {
$groupContent.append($groupContentItem);
}
let isSingleElement = false;
if (!isGroup && item.dataOverride === 'text' && groupObj.isMarquee) {
if (item.effect === 'marqueeLeft' || item.effect === 'marqueeRight') {
if ($groupContent.find(
groupKey.replace('%key%', dataItemKey)).length === 1
) {
$groupContent.find(
groupKey.replace('%key%', dataItemKey),
).remove();
}
isSingleElement = true;
} else if (item.effect === 'marqueeDown' ||
item.effect === 'marqueeUp') {
isSingleElement = false;
}
}
const $itemContainer = isSingleElement ?
$groupContent : $groupContent.find(
groupKey.replace('%key%', dataItemKey),
);
const props = Object.assign(
item.templateData,
{isGroup},
(String(item.dataOverride).length > 0 &&
String(item.dataOverrideWith).length > 0) ?
dataItem : {data: dataItem},
{group: groupObj},
);
// Handle special cases where data field name for override
// that's the same as template variable
// E.g. When a dataset column is "text" and the element is using
// text element, extended or not
if (props.isExtended) {
if (props.type === 'dataset' &&
props.hasOwnProperty('datasetField') &&
dataItem.hasOwnProperty(props.datasetField)
) {
props[props.dataOverride] = dataItem[props.datasetField];
} else {
const extendWith =
transformer.getExtendedDataKey(props.dataOverrideWith);
if (props.dataOverride === extendWith &&
dataItem.hasOwnProperty(extendWith)
) {
props[props.dataOverride] = dataItem[extendWith];
}
}
}
const $elementContent = $(self.renderElement(
item.hbs,
props,
));
// Add style scope to container
const $elementContentContainer = $('');
$elementContentContainer.append($elementContent).attr(
'data-style-scope',
'element_' +
props.type + '__' +
props.id,
);
$itemContainer.append(
$elementContentContainer,
);
const itemID = item.uniqueID || item.templateData?.uniqueID;
// Handle the rendering of the template
(item.onTemplateRender() !== undefined) && item.onTemplateRender()(
item.elementId,
$itemContainer.find(`.${itemID}--item`).parent(),
dataItem,
{item, ...item.templateData, data: dataItem},
meta,
);
} else {
if ($groupContent &&
$groupContent.find(
groupKey.replace('%key%', dataItemKey)).length === 0
) {
$groupContent.append($groupContentItem);
}
const $itemContainer = $groupContent.find(
groupKey.replace('%key%', dataItemKey),
);
$itemContainer.append('');
}
};
return this;
};
module.exports = new PlayerHelper();