481 lines
16 KiB
JavaScript
481 lines
16 KiB
JavaScript
export function createSlideFormEditorController(options) {
|
|
var settings = options || {};
|
|
var templateFields = settings.templateFields || null;
|
|
var templateSelectorLock = settings.templateSelectorLock || null;
|
|
var requestPreviewRender = typeof settings.requestPreviewRender === 'function' ? settings.requestPreviewRender : function () {};
|
|
var defaultFontSize = Math.max(1, Number(settings.defaultFontSize || 32));
|
|
var fontFamilyFormats = String(settings.fontFamilyFormats || '').trim();
|
|
var fontStylesheetHref = String(settings.fontStylesheetHref || '').trim();
|
|
var defaultEditorFontFamily = 'Arial, Helvetica, sans-serif';
|
|
var editorInstances = new Map();
|
|
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () {
|
|
return null;
|
|
};
|
|
var getEditorBackgroundColor = typeof settings.getEditorBackgroundColor === 'function' ? settings.getEditorBackgroundColor : function () {
|
|
return '#111111';
|
|
};
|
|
|
|
function normalizeEditorData(value) {
|
|
return String(value || '');
|
|
}
|
|
|
|
function normalizeEditorMarkup(value) {
|
|
return String(value === undefined || value === null ? '' : value).trim();
|
|
}
|
|
|
|
function isEmptyRichTextValue(value) {
|
|
var raw = String(value === undefined || value === null ? '' : value).trim();
|
|
if (!raw) {
|
|
return true;
|
|
}
|
|
|
|
var stripped = raw
|
|
.replace(/<\s*br\s*\/?>/gi, '')
|
|
.replace(/<p[^>]*>(?:\s| |<br\s*\/?>)*<\/p>/gi, '')
|
|
.replace(/<[^>]+>/g, '')
|
|
.replace(/ /gi, '')
|
|
.trim();
|
|
|
|
return !stripped;
|
|
}
|
|
|
|
function normalizeFontSizeValue(value) {
|
|
var raw = String(value || '').trim().toLowerCase();
|
|
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
|
return String(Math.max(1, Math.round(Number(raw.replace(/px$/, '')))));
|
|
}
|
|
if (/^\d+(?:\.\d+)?$/.test(raw)) {
|
|
return String(Math.max(1, Math.round(Number(raw))));
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function getEditorHiddenInput(regionId) {
|
|
if (!templateFields) {
|
|
return null;
|
|
}
|
|
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
return card ? card.querySelector('input[type="hidden"][name="region_text_' + regionId + '"]') : null;
|
|
}
|
|
|
|
function getEditorFontSizeHiddenInput(regionId) {
|
|
if (!templateFields) {
|
|
return null;
|
|
}
|
|
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
return card ? card.querySelector('input[name="region_font_size_' + regionId + '"]') : null;
|
|
}
|
|
|
|
function getTinymce() {
|
|
return window.tinymce || null;
|
|
}
|
|
|
|
function getEditorContentSafely(editor, fallbackValue) {
|
|
if (!editor || typeof editor.getContent !== 'function') {
|
|
return String(fallbackValue || '');
|
|
}
|
|
|
|
try {
|
|
return String(editor.getContent({ format: 'html' }));
|
|
} catch (_error) {
|
|
return String(fallbackValue || '');
|
|
}
|
|
}
|
|
|
|
function getThemeName() {
|
|
var theme = String(document.documentElement && document.documentElement.dataset && document.documentElement.dataset.bsTheme || 'light').trim().toLowerCase();
|
|
if (theme === 'auto') {
|
|
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
return theme === 'dark' ? 'dark' : 'light';
|
|
}
|
|
|
|
function getTinyMceThemeAssets() {
|
|
var themeName = getThemeName();
|
|
var skinName = themeName === 'dark' ? 'oxide-dark' : 'oxide';
|
|
var skinBase = '/assets/vendor/tinymce/skins/ui/' + skinName;
|
|
return {
|
|
themeName: themeName,
|
|
skinName: skinName,
|
|
skinUrl: skinBase,
|
|
contentCss: skinBase + '/content.css',
|
|
bodyClass: themeName === 'dark' ? 'tinymce-theme-dark' : 'tinymce-theme-light'
|
|
};
|
|
}
|
|
|
|
function syncEditorState(regionId, editor, shouldLock, hydrated) {
|
|
var hidden = getEditorHiddenInput(regionId);
|
|
var sourceElm = editor && editor.targetElm ? editor.targetElm : null;
|
|
var fallbackContent = hidden && hidden.value !== undefined ? hidden.value : (sourceElm && sourceElm.value !== undefined ? sourceElm.value : '');
|
|
|
|
if (editor && typeof editor.save === 'function') {
|
|
if (typeof editor.blur === 'function') {
|
|
editor.blur();
|
|
}
|
|
editor.save();
|
|
}
|
|
|
|
var content = sourceElm && sourceElm.value !== undefined ? String(sourceElm.value || '') : getEditorContentSafely(editor, fallbackContent);
|
|
content = isEmptyRichTextValue(content) ? '' : content;
|
|
|
|
if (hidden) {
|
|
hidden.value = content;
|
|
}
|
|
if (sourceElm) {
|
|
sourceElm.value = content;
|
|
}
|
|
if (typeof editor.nodeChanged === 'function') {
|
|
editor.nodeChanged();
|
|
}
|
|
if (typeof editor.setDirty === 'function') {
|
|
editor.setDirty(true);
|
|
}
|
|
if (shouldLock && templateSelectorLock && typeof templateSelectorLock.arm === 'function') {
|
|
templateSelectorLock.arm();
|
|
}
|
|
if (shouldLock && templateSelectorLock && typeof templateSelectorLock.markEdited === 'function') {
|
|
templateSelectorLock.markEdited();
|
|
}
|
|
if (hydrated) {
|
|
requestPreviewRender();
|
|
}
|
|
}
|
|
|
|
function getEditorRegionType(card) {
|
|
if (!card) {
|
|
return '';
|
|
}
|
|
|
|
if (card.dataset && String(card.dataset.regionMediaType || '').trim()) {
|
|
return String(card.dataset.regionMediaType || '').trim();
|
|
}
|
|
|
|
var regionTypeInput = card.querySelector ? card.querySelector('input[type="hidden"][name="region_type[]"]') : null;
|
|
return String(regionTypeInput && regionTypeInput.value || '').trim();
|
|
}
|
|
|
|
function getEditorContentStyle(regionType) {
|
|
var module = getRegionTypeModule(regionType);
|
|
if (module && typeof module.getEditorContentStyle === 'function') {
|
|
return String(module.getEditorContentStyle() || '').trim();
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function syncEditorFontSizeHidden(regionId) {
|
|
var fontSizeHidden = getEditorFontSizeHiddenInput(regionId);
|
|
if (!fontSizeHidden) {
|
|
return;
|
|
}
|
|
|
|
fontSizeHidden.value = normalizeFontSizeValue(fontSizeHidden.value) || String(defaultFontSize);
|
|
}
|
|
|
|
function getEditorBackgroundColorValue() {
|
|
var value = String(getEditorBackgroundColor() || '').trim();
|
|
return value || '#111111';
|
|
}
|
|
|
|
function getFontFamilyFormats() {
|
|
if (fontFamilyFormats) {
|
|
return fontFamilyFormats;
|
|
}
|
|
|
|
return [
|
|
'Default=inherit',
|
|
'Arial=Arial,Helvetica,sans-serif',
|
|
'Comic Sans MS=Comic Sans MS,cursive,sans-serif',
|
|
'Courier New=Courier New,Courier,monospace',
|
|
'Georgia=Georgia,serif',
|
|
'Helvetica=Helvetica,Arial,sans-serif',
|
|
'Impact=Impact,Charcoal,sans-serif',
|
|
'Lucida Sans Unicode=Lucida Sans Unicode,Lucida Grande,sans-serif',
|
|
'Palatino Linotype=Palatino Linotype,Book Antiqua,Palatino,serif',
|
|
'Tahoma=Tahoma,Geneva,sans-serif',
|
|
'Times New Roman=Times New Roman,Times,serif',
|
|
'Trebuchet MS=Trebuchet MS,Helvetica,sans-serif',
|
|
'Verdana=Verdana,Geneva,sans-serif'
|
|
].join(';');
|
|
}
|
|
|
|
function getContentCss() {
|
|
var themeAssets = getTinyMceThemeAssets();
|
|
return fontStylesheetHref
|
|
? [themeAssets.contentCss, fontStylesheetHref]
|
|
: themeAssets.contentCss;
|
|
}
|
|
|
|
function attachEditorEvents(regionId, editor) {
|
|
var hidden = getEditorHiddenInput(regionId);
|
|
var source = editor && editor.targetElm ? editor.targetElm : null;
|
|
var hydrated = false;
|
|
|
|
function syncState(shouldLock) {
|
|
var currentEditor = editorInstances.get(regionId);
|
|
if (currentEditor !== editor) {
|
|
return;
|
|
}
|
|
|
|
syncEditorState(regionId, editor, shouldLock, hydrated);
|
|
if (!hydrated) {
|
|
requestPreviewRender();
|
|
return;
|
|
}
|
|
}
|
|
|
|
editor.on('init', function () {
|
|
var content = getEditorContentSafely(editor, hidden && hidden.value !== undefined ? hidden.value : (source && source.value !== undefined ? source.value : ''));
|
|
content = isEmptyRichTextValue(content) ? '' : content;
|
|
if (hidden) {
|
|
hidden.value = content;
|
|
}
|
|
if (source) {
|
|
source.value = content;
|
|
}
|
|
syncEditorFontSizeHidden(regionId);
|
|
hydrated = true;
|
|
requestPreviewRender();
|
|
});
|
|
|
|
['change', 'keyup', 'undo', 'redo', 'Paste', 'input'].forEach(function (eventName) {
|
|
editor.on(eventName, function () {
|
|
syncState(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
function createEditorForHolder(holder) {
|
|
if (!holder) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
var regionId = holder.getAttribute('data-region-id');
|
|
var card = holder.closest ? holder.closest('[data-region-id]') : null;
|
|
var source = holder.querySelector('.editor-source');
|
|
var hidden = getEditorHiddenInput(regionId);
|
|
var tinymce = getTinymce();
|
|
var themeAssets = getTinyMceThemeAssets();
|
|
var editorContentStyle = getEditorContentStyle(getEditorRegionType(card));
|
|
|
|
if (!source || !tinymce || typeof tinymce.init !== 'function') {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
source.value = normalizeEditorMarkup(normalizeEditorData(source && source.value !== undefined && source.value !== null ? source.value : (hidden ? hidden.value : '')));
|
|
|
|
if (!source.id) {
|
|
source.id = 'slide-editor-region-' + regionId;
|
|
}
|
|
|
|
function registerInlineFormat(editor, formatName, styles) {
|
|
editor.formatter.register(formatName, {
|
|
inline: 'span',
|
|
styles: styles
|
|
});
|
|
}
|
|
|
|
function applyInlineFormat(regionId, editor, formatName) {
|
|
if (editor && editor.undoManager && typeof editor.undoManager.transact === 'function') {
|
|
editor.undoManager.transact(function () {
|
|
editor.formatter.apply(formatName);
|
|
});
|
|
} else {
|
|
editor.formatter.apply(formatName);
|
|
}
|
|
syncEditorState(regionId, editor, false, true);
|
|
}
|
|
|
|
function applyEditorCommand(regionId, editor, commandName) {
|
|
if (editor && editor.undoManager && typeof editor.undoManager.transact === 'function') {
|
|
editor.undoManager.transact(function () {
|
|
editor.execCommand(commandName);
|
|
});
|
|
} else {
|
|
editor.execCommand(commandName);
|
|
}
|
|
syncEditorState(regionId, editor, false, true);
|
|
}
|
|
|
|
return tinymce.init({
|
|
target: source,
|
|
menubar: false,
|
|
branding: false,
|
|
promotion: false,
|
|
paste_data_images: false,
|
|
plugins: 'lists code advlist fullscreen table',
|
|
toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underlineformats removeformat | align lineheight indent outdent bullist numlist table chip | fullscreen',
|
|
toolbar_mode: 'sliding',
|
|
license_key: 'gpl',
|
|
skin: themeAssets.skinName,
|
|
skin_url: themeAssets.skinUrl,
|
|
content_css: getContentCss(),
|
|
body_class: themeAssets.bodyClass,
|
|
content_style: 'body { font-family: ' + defaultEditorFontFamily + '; font-size: 32px; line-height: 1.5; background-color: ' + getEditorBackgroundColorValue() + '; } p { margin: 1em 0; } p:first-child { margin-top: 0; } p:last-child { margin-bottom: 1em; } table { border-collapse: collapse; width: 100%; } td, th { border: 1px solid currentColor; padding: 0.35em 0.5em; vertical-align: top; } th { font-weight: 700; }' + (editorContentStyle ? ' ' + editorContentStyle : ''),
|
|
font_family_formats: getFontFamilyFormats(),
|
|
font_size_input_default_unit: 'px',
|
|
invalid_elements: 'a',
|
|
forced_root_block: 'p',
|
|
force_br_newlines: false,
|
|
newline_behavior: 'default',
|
|
placeholder: String(source.getAttribute('placeholder') || '').trim(),
|
|
setup: function (editor) {
|
|
editorInstances.set(regionId, editor);
|
|
editor.ui.registry.addButton('chip', {
|
|
icon: 'addtag',
|
|
tooltip: 'Chip style',
|
|
onAction: function () {
|
|
applyInlineFormat(regionId, editor, 'chip');
|
|
}
|
|
});
|
|
editor.ui.registry.addSplitButton('underlineformats', {
|
|
icon: 'underline',
|
|
tooltip: 'Text formats',
|
|
onAction: function () {
|
|
applyEditorCommand(regionId, editor, 'Underline');
|
|
},
|
|
onItemAction: function (_api, value) {
|
|
if (value === 'strikethrough') {
|
|
applyEditorCommand(regionId, editor, 'Strikethrough');
|
|
return;
|
|
}
|
|
if (value === 'subscript') {
|
|
applyEditorCommand(regionId, editor, 'Subscript');
|
|
return;
|
|
}
|
|
if (value === 'superscript') {
|
|
applyEditorCommand(regionId, editor, 'Superscript');
|
|
}
|
|
},
|
|
fetch: function (callback) {
|
|
callback([
|
|
{
|
|
type: 'choiceitem',
|
|
value: 'strikethrough',
|
|
icon: 'strikethrough',
|
|
text: 'Strikethrough',
|
|
},
|
|
{
|
|
type: 'choiceitem',
|
|
value: 'subscript',
|
|
icon: 'subscript',
|
|
text: 'Subscript',
|
|
},
|
|
{
|
|
type: 'choiceitem',
|
|
value: 'superscript',
|
|
icon: 'superscript',
|
|
text: 'Superscript',
|
|
}
|
|
]);
|
|
}
|
|
});
|
|
editor.on('init', function () {
|
|
registerInlineFormat(editor, 'chip', {
|
|
display: 'inline-block',
|
|
padding: '0.2em 0.6em',
|
|
borderRadius: '999px',
|
|
fontWeight: '700',
|
|
letterSpacing: '0.02em'
|
|
});
|
|
});
|
|
attachEditorEvents(regionId, editor);
|
|
}
|
|
}).then(function (editors) {
|
|
var editor = Array.isArray(editors) && editors.length ? editors[0] : tinymce.get(source.id);
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
|
|
editorInstances.set(regionId, editor);
|
|
if (editor.targetElm) {
|
|
editor.targetElm.value = editor.getContent({ format: 'html' });
|
|
}
|
|
return editor;
|
|
}).catch(function (error) {
|
|
console.error('Failed to initialize TinyMCE.', error);
|
|
return null;
|
|
});
|
|
}
|
|
|
|
function renderEditors() {
|
|
if (!templateFields) {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
watchThemeChanges();
|
|
|
|
var holders = Array.prototype.slice.call(templateFields.querySelectorAll('.editor-holder'));
|
|
return Promise.all(holders.map(function (holder) {
|
|
return createEditorForHolder(holder);
|
|
}));
|
|
}
|
|
|
|
function syncEditors() {
|
|
if (!templateFields) {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.editor-holder'), function (holder) {
|
|
var regionId = holder.getAttribute('data-region-id');
|
|
var editor = editorInstances.get(regionId);
|
|
|
|
if (!editor) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
syncEditorState(regionId, editor, false, true);
|
|
return Promise.resolve();
|
|
});
|
|
|
|
return Promise.all(saves);
|
|
}
|
|
|
|
function destroyEditors() {
|
|
editorInstances.forEach(function (editor) {
|
|
if (editor && typeof editor.remove === 'function') {
|
|
editor.remove();
|
|
}
|
|
});
|
|
editorInstances.clear();
|
|
|
|
var tinymce = getTinymce();
|
|
if (tinymce && typeof tinymce.remove === 'function') {
|
|
tinymce.remove();
|
|
}
|
|
}
|
|
|
|
var themeObserver = null;
|
|
|
|
function watchThemeChanges() {
|
|
if (themeObserver || !window.MutationObserver || !templateFields) {
|
|
return;
|
|
}
|
|
|
|
themeObserver = new MutationObserver(function () {
|
|
destroyEditors();
|
|
renderEditors();
|
|
});
|
|
|
|
themeObserver.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ['data-bs-theme']
|
|
});
|
|
}
|
|
|
|
return {
|
|
renderEditors: renderEditors,
|
|
syncEditors: syncEditors,
|
|
destroyEditors: function () {
|
|
if (themeObserver) {
|
|
themeObserver.disconnect();
|
|
themeObserver = null;
|
|
}
|
|
destroyEditors();
|
|
},
|
|
watchThemeChanges: watchThemeChanges
|
|
};
|
|
} |