204 lines
9.4 KiB
JavaScript
204 lines
9.4 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// General-purpose helpers used across the admin UI.
|
|
function registerGeneralHelpers(Handlebars) {
|
|
Handlebars.registerHelper('eq', function (left, right) {
|
|
return left === right;
|
|
});
|
|
|
|
Handlebars.registerHelper('gt', function (left, right) {
|
|
return left > right;
|
|
});
|
|
|
|
Handlebars.registerHelper('lt', function (left, right) {
|
|
return left < right;
|
|
});
|
|
|
|
Handlebars.registerHelper('json', function (value) {
|
|
return new Handlebars.SafeString(JSON.stringify(value).replace(/</g, '\\u003c'));
|
|
});
|
|
|
|
Handlebars.registerHelper('truncateText', function (value, maxLength) {
|
|
const text = String(value || '').trim();
|
|
const limit = Number(maxLength);
|
|
if (!text || !Number.isFinite(limit) || limit <= 0 || text.length <= limit) {
|
|
return text;
|
|
}
|
|
|
|
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
|
|
});
|
|
|
|
Handlebars.registerHelper('textLengthAttrs', function (maxLength) {
|
|
const limit = Number(maxLength);
|
|
if (!Number.isFinite(limit) || limit < 1) {
|
|
return '';
|
|
}
|
|
|
|
return new Handlebars.SafeString(`maxlength="${Handlebars.escapeExpression(String(Math.floor(limit)))}" data-limit-text-length`);
|
|
});
|
|
}
|
|
|
|
// URL helpers for generating links and external URL checks.
|
|
function registerUrlHelpers(Handlebars) {
|
|
|
|
Handlebars.registerHelper('isExternalUrl', function (value) {
|
|
return /^https?:\/\//i.test(String(value || ''));
|
|
});
|
|
|
|
Handlebars.registerHelper('assetHref', function (value) {
|
|
const href = String(value || '').trim();
|
|
if (!href) {
|
|
return '';
|
|
}
|
|
if (/^https?:\/\//i.test(href) || href.startsWith('/')) {
|
|
return href;
|
|
}
|
|
return `/assets/${href}`;
|
|
});
|
|
|
|
Handlebars.registerHelper('playerUrl', function (slug) {
|
|
const base = String(arguments[1] || '').trim().replace(/\/$/, '');
|
|
if (!base) {
|
|
return '';
|
|
}
|
|
|
|
return `${base}/screen/${encodeURIComponent(slug)}`;
|
|
});
|
|
}
|
|
|
|
// Permission helpers delegate to the RBAC layer.
|
|
function registerPermissionHelpers(Handlebars, deps) {
|
|
const hasPermission = deps && deps.hasPermission;
|
|
const hasAnyPermission = deps && deps.hasAnyPermission;
|
|
|
|
Handlebars.registerHelper('hasPermission', function (currentUser, permissionKey) {
|
|
return hasPermission(currentUser, permissionKey);
|
|
});
|
|
|
|
Handlebars.registerHelper('anyPermission', function (currentUser) {
|
|
const args = Array.prototype.slice.call(arguments, 1);
|
|
args.pop();
|
|
return hasAnyPermission(currentUser, args);
|
|
});
|
|
}
|
|
|
|
function registerUserHelpers(Handlebars) {
|
|
Handlebars.registerHelper('userInitial', function (name) {
|
|
const text = String(name || '').trim();
|
|
return text ? text.charAt(0).toUpperCase() : 'A';
|
|
});
|
|
}
|
|
|
|
// Action button helpers keep form submit controls consistent.
|
|
function registerActionHelpers(Handlebars) {
|
|
Handlebars.registerHelper('saveActionButtons', function (options) {
|
|
const hash = options && options.hash ? options.hash : {};
|
|
const formId = String(hash.formId || '').trim();
|
|
|
|
const save = {
|
|
label: String(hash.saveLabel || 'Save'),
|
|
className: String(hash.saveButtonClass || 'btn btn-success'),
|
|
actionName: String(hash.saveActionName || 'save_action'),
|
|
actionValue: String(hash.saveActionValue || 'save'),
|
|
closeLabel: String(hash.saveAndCloseLabel || 'Save and Close'),
|
|
closeValue: String(hash.saveAndCloseValue || 'close'),
|
|
newLabel: String(hash.saveAndNewLabel || 'Save and New'),
|
|
newValue: String(hash.saveAndNewValue || 'new')
|
|
};
|
|
|
|
const showSaveAndClose = hash.showSaveAndClose === undefined ? true : String(hash.showSaveAndClose).toLowerCase() !== 'false';
|
|
const showSaveAndNew = hash.showSaveAndNew === undefined ? true : String(hash.showSaveAndNew).toLowerCase() !== 'false';
|
|
const showDelete = hash.showDelete === undefined ? true : String(hash.showDelete).toLowerCase() !== 'false';
|
|
const hasSecondaryActions = showSaveAndClose || showSaveAndNew;
|
|
const ariaLabel = String(hash.ariaLabel || 'Save actions');
|
|
const saveUrl = String(hash.saveUrl || '').trim();
|
|
|
|
const cancel = {
|
|
label: String(hash.cancelLabel || 'Cancel'),
|
|
className: String(hash.cancelClass || 'btn btn-warning'),
|
|
url: String(hash.cancelUrl || '').trim(),
|
|
confirmMessage: String(hash.cancelConfirmMessage || hash.cancelConfirm || 'You have unsaved changes. Leave this page?')
|
|
};
|
|
|
|
const deleteAction = {
|
|
label: String(hash.deleteLabel || 'Delete'),
|
|
className: String(hash.deleteClass || 'btn btn-danger'),
|
|
url: String(hash.deleteUrl || '').trim(),
|
|
formId: String(hash.deleteFormId || '').trim(),
|
|
confirmMessage: String(hash.deleteConfirmMessage || '').trim(),
|
|
disabled: hash.deleteDisabled === undefined ? false : String(hash.deleteDisabled).toLowerCase() !== 'false',
|
|
title: String(hash.deleteTitle || '')
|
|
};
|
|
|
|
const resolvedDeleteUrl = deleteAction.url || (!deleteAction.disabled && saveUrl ? saveUrl.replace(/\/+$/, '') + '/delete' : '');
|
|
const resolvedDeleteFormId = deleteAction.formId || '';
|
|
|
|
if (!formId) {
|
|
return '';
|
|
}
|
|
|
|
const escape = Handlebars.escapeExpression;
|
|
const formAttr = ` form="${escape(formId)}"`;
|
|
|
|
const parts = [
|
|
'<div class="btn-group save-action-group" role="group">',
|
|
`<button type="submit" class="${escape(save.className)}"${formAttr} name="${escape(save.actionName)}" value="${escape(save.actionValue)}">${escape(save.label)}</button>`,
|
|
hasSecondaryActions ? `<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split rounded-0" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">${escape(ariaLabel)}</span></button>` : '',
|
|
hasSecondaryActions ? '<div class="dropdown-menu dropdown-menu-end">' : '',
|
|
hasSecondaryActions && showSaveAndClose ? `<button type="submit" class="dropdown-item"${formAttr} name="${escape(save.actionName)}" value="${escape(save.closeValue)}">${escape(save.closeLabel)}</button>` : '',
|
|
hasSecondaryActions && showSaveAndNew ? `<button type="submit" class="dropdown-item"${formAttr} name="${escape(save.actionName)}" value="${escape(save.newValue)}">${escape(save.newLabel)}</button>` : '',
|
|
hasSecondaryActions ? '</div>' : '',
|
|
];
|
|
|
|
if (cancel.url) {
|
|
parts.push(`<a class="${escape(cancel.className)}" href="${escape(cancel.url)}" data-confirm-unsaved="${escape(cancel.confirmMessage)}">${escape(cancel.label)}</a>`);
|
|
}
|
|
|
|
if (showDelete && (deleteAction.disabled || resolvedDeleteUrl)) {
|
|
const deleteActionAttr = resolvedDeleteUrl && !resolvedDeleteFormId ? ` data-delete-action-url="${escape(resolvedDeleteUrl)}"` : '';
|
|
const deleteFormAttr = resolvedDeleteFormId ? ` form="${escape(resolvedDeleteFormId)}"` : '';
|
|
const confirmAttr = deleteAction.confirmMessage ? ` data-confirm-message="${escape(deleteAction.confirmMessage)}"` : '';
|
|
const titleAttr = deleteAction.disabled && deleteAction.title ? ` title="${escape(deleteAction.title)}"` : '';
|
|
parts.push(`<button type="${resolvedDeleteFormId ? 'submit' : 'button'}" class="${escape(deleteAction.className)}"${deleteFormAttr}${deleteActionAttr}${confirmAttr}${deleteAction.disabled ? ' disabled' : ''}${titleAttr}>${escape(deleteAction.label)}</button>`);
|
|
}
|
|
|
|
parts.push('</div>');
|
|
|
|
return new Handlebars.SafeString(parts.join(''));
|
|
});
|
|
}
|
|
|
|
// Shared partials are registered once at startup.
|
|
function registerPartials(Handlebars, viewsRoot) {
|
|
Handlebars.registerPartial('modal-shell', fs.readFileSync(path.join(viewsRoot, 'shared', 'modal-shell.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('table-pagination', fs.readFileSync(path.join(viewsRoot, 'shared', 'table', 'table-pagination.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('playlists/form', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'form.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('signage/playlists/slide-row', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'slide-row.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('signage/playlists/schedule-rule-card', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'schedule-rule-card.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('signage/templates/animation-advanced-modal', fs.readFileSync(path.join(viewsRoot, 'signage', 'templates', 'animation-advanced-modal.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('data-sources/api-sources/form', fs.readFileSync(path.join(viewsRoot, 'data-sources', 'api-sources', 'form.hbs'), 'utf8'));
|
|
Handlebars.registerPartial('data-sources/rss-feeds/form', fs.readFileSync(path.join(viewsRoot, 'data-sources', 'rss-feeds', 'form.hbs'), 'utf8'));
|
|
}
|
|
|
|
// One entry point keeps Handlebars bootstrap centralized.
|
|
function registerHandlebars(Handlebars, deps) {
|
|
const viewsRoot = String(deps && deps.viewsRoot || '').trim();
|
|
const hasPermission = deps && deps.hasPermission;
|
|
const hasAnyPermission = deps && deps.hasAnyPermission;
|
|
|
|
if (!Handlebars || !viewsRoot || typeof hasPermission !== 'function' || typeof hasAnyPermission !== 'function') {
|
|
throw new Error('registerHandlebars requires Handlebars, viewsRoot, hasPermission, and hasAnyPermission.');
|
|
}
|
|
|
|
registerGeneralHelpers(Handlebars);
|
|
registerUrlHelpers(Handlebars);
|
|
registerPermissionHelpers(Handlebars, { hasPermission: hasPermission, hasAnyPermission: hasAnyPermission });
|
|
registerUserHelpers(Handlebars);
|
|
registerActionHelpers(Handlebars);
|
|
registerPartials(Handlebars, viewsRoot);
|
|
}
|
|
|
|
module.exports = {
|
|
registerHandlebars: registerHandlebars
|
|
}; |