/**
* Text editor - all contenteditable text-editing setup
*
* Reads (via globals):
* SFE.Context - .draftEditState
* SFE.ElementPrep - .getCleanHTML, .getContent
* SFE.OverlayManager
* SFE.PositionManager - .positionFloatingElements
* SFE.FocusManager - .createFocusManager
* SFE.handleInlineSave - set by SaveManager
* SFE.closeInPlaceEditor - set by EditorLifecycle
* SFE.startMediaEditing - set by frontend-inline-edit.js (via SFE.MediaEditor)
* SFE.MWPEditor - rich text editor class
* SFE.ManagerData - .handlers
*
* Exposes: SFE.TextEditor
* { startTextEditing, startMultiComponentEditing, refreshEditableComponents }
*/
(function() {
'use strict';
window.MWP = window.MWP || {};
window.MWP.SFE = window.MWP.SFE || {};
const SFE = window.MWP.SFE;
SFE.ManagerData = SFE.ManagerData || {};
const MWPEditor = SFE.MWPEditor;
function normalizeEditorFormatsSpec(rawFormats, depth = 0) {
if (depth > 3 || !Array.isArray(rawFormats)) {
return null;
}
const normalized = [];
rawFormats.forEach((item) => {
if (typeof item === 'string') {
const token = item.trim();
if (token) {
normalized.push(token);
}
return;
}
if (Array.isArray(item)) {
const group = normalizeEditorFormatsSpec(item, depth + 1);
if (Array.isArray(group) && group.length) {
normalized.push(group);
}
}
});
return normalized.length ? normalized : null;
}
function cloneEditorFormatsSpec(formatsSpec) {
if (!Array.isArray(formatsSpec)) return null;
return formatsSpec.map(item => (
Array.isArray(item) ? cloneEditorFormatsSpec(item) : item
));
}
function normalizeInlineFormatCapability(rawCapability) {
if (!rawCapability || typeof rawCapability !== 'object' || Array.isArray(rawCapability)) {
return null;
}
const tag = typeof rawCapability.tag === 'string'
? rawCapability.tag.trim().toLowerCase()
: '';
if (!tag) return null;
const normalizeStringArray = (rawValue) => {
if (!Array.isArray(rawValue)) return null;
const values = rawValue
.map((value) => (typeof value === 'string' ? value.trim() : ''))
.filter(Boolean);
return values.length ? values : null;
};
const normalized = { tag };
const attributes = normalizeStringArray(rawCapability.attributes);
const requiredAttributes = normalizeStringArray(rawCapability.requiredAttributes);
const allowedTargets = normalizeStringArray(rawCapability.allowedTargets);
const allowedRelTokens = normalizeStringArray(rawCapability.allowedRelTokens);
const allowedProtocols = normalizeStringArray(rawCapability.allowedProtocols);
if (attributes) normalized.attributes = attributes;
if (requiredAttributes) normalized.requiredAttributes = requiredAttributes;
if (allowedTargets) normalized.allowedTargets = allowedTargets;
if (allowedRelTokens) normalized.allowedRelTokens = allowedRelTokens;
if (allowedProtocols) normalized.allowedProtocols = allowedProtocols;
if (typeof rawCapability.allowsRelativeUrls === 'boolean') {
normalized.allowsRelativeUrls = rawCapability.allowsRelativeUrls;
}
if (typeof rawCapability.allowsAnchorLinks === 'boolean') {
normalized.allowsAnchorLinks = rawCapability.allowsAnchorLinks;
}
if (typeof rawCapability.autoProtocol === 'string' && rawCapability.autoProtocol.trim()) {
normalized.autoProtocol = rawCapability.autoProtocol.trim().toLowerCase();
}
if (typeof rawCapability.preservesUnknownRelTokens === 'boolean') {
normalized.preservesUnknownRelTokens = rawCapability.preservesUnknownRelTokens;
}
return normalized;
}
function cloneInlineFormatCapabilities(inlineFormatCapabilities) {
if (!inlineFormatCapabilities || typeof inlineFormatCapabilities !== 'object' || Array.isArray(inlineFormatCapabilities)) {
return null;
}
const cloned = {};
Object.keys(inlineFormatCapabilities).forEach((key) => {
const cleanKey = typeof key === 'string' ? key.trim() : '';
if (!cleanKey) return;
const capability = normalizeInlineFormatCapability(inlineFormatCapabilities[key]);
if (!capability) return;
cloned[cleanKey] = capability;
});
return Object.keys(cloned).length ? cloned : null;
}
function normalizeAttributeCapabilityValues(rawValues) {
if (!Array.isArray(rawValues)) return null;
const values = rawValues.filter(value => (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value === null
));
return values.length ? values : null;
}
function normalizeAttributeCapability(rawCapability) {
if (!rawCapability || typeof rawCapability !== 'object' || Array.isArray(rawCapability)) {
return null;
}
const normalized = {};
const attribute = typeof rawCapability.attribute === 'string'
? rawCapability.attribute.trim()
: '';
const attributes = Array.isArray(rawCapability.attributes)
? rawCapability.attributes
.map(value => (typeof value === 'string' ? value.trim() : ''))
.filter(Boolean)
: null;
if (attribute) {
normalized.attribute = attribute;
} else if (attributes && attributes.length) {
normalized.attributes = attributes;
} else {
return null;
}
const values = normalizeAttributeCapabilityValues(rawCapability.values);
if (values) {
normalized.values = values;
}
if (
Object.prototype.hasOwnProperty.call(rawCapability, 'unsetValue') &&
(
typeof rawCapability.unsetValue === 'string' ||
typeof rawCapability.unsetValue === 'number' ||
typeof rawCapability.unsetValue === 'boolean' ||
rawCapability.unsetValue === null
)
) {
normalized.unsetValue = rawCapability.unsetValue;
}
return normalized;
}
function cloneAttributeCapabilities(attributeCapabilities) {
if (!attributeCapabilities || typeof attributeCapabilities !== 'object' || Array.isArray(attributeCapabilities)) {
return null;
}
const cloned = {};
Object.keys(attributeCapabilities).forEach((key) => {
const cleanKey = typeof key === 'string' ? key.trim() : '';
if (!cleanKey) return;
const capability = normalizeAttributeCapability(attributeCapabilities[key]);
if (!capability) return;
cloned[cleanKey] = { ...capability };
if (Array.isArray(capability.attributes)) {
cloned[cleanKey].attributes = [ ...capability.attributes ];
}
if (Array.isArray(capability.values)) {
cloned[cleanKey].values = [ ...capability.values ];
}
});
return Object.keys(cloned).length ? cloned : null;
}
function normalizeEditorOperationValues(rawValues) {
if (!Array.isArray(rawValues)) return null;
const values = rawValues.filter(value => (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value === null
));
return values.length ? values : null;
}
function normalizeEditorOperationStringArray(rawValues) {
if (!Array.isArray(rawValues)) return null;
const values = rawValues
.map(value => (typeof value === 'string' ? value.trim() : ''))
.filter(Boolean);
return values.length ? values : null;
}
function normalizeEditorOperation(rawOperation) {
if (!rawOperation || typeof rawOperation !== 'object' || Array.isArray(rawOperation)) {
return null;
}
const id = typeof rawOperation.id === 'string' ? rawOperation.id.trim() : '';
const kind = typeof rawOperation.kind === 'string' ? rawOperation.kind.trim() : '';
const component = typeof rawOperation.component === 'string' ? rawOperation.component.trim() : '';
if (!id || !kind || !component) {
return null;
}
const normalized = { id, kind, component };
const attribute = typeof rawOperation.attribute === 'string' ? rawOperation.attribute.trim() : '';
const format = typeof rawOperation.format === 'string' ? rawOperation.format.trim() : '';
const attributes = normalizeEditorOperationStringArray(rawOperation.attributes);
const formats = normalizeEditorOperationStringArray(rawOperation.formats);
const targetModes = normalizeEditorOperationStringArray(rawOperation.targetModes);
const values = normalizeEditorOperationValues(rawOperation.values);
if (attribute) {
normalized.attribute = attribute;
}
if (attributes) {
normalized.attributes = attributes;
}
if (format) {
normalized.format = format;
}
if (formats) {
normalized.formats = formats;
}
if (targetModes) {
normalized.targetModes = targetModes;
}
if (values) {
normalized.values = values;
}
if (Object.prototype.hasOwnProperty.call(rawOperation, 'unsetValue')) {
normalized.unsetValue = rawOperation.unsetValue;
}
if (typeof rawOperation.preserveInlineFormatting === 'boolean') {
normalized.preserveInlineFormatting = rawOperation.preserveInlineFormatting;
}
if (typeof rawOperation.preserveUnchangedText === 'boolean') {
normalized.preserveUnchangedText = rawOperation.preserveUnchangedText;
}
if (typeof rawOperation.preserveUnspecifiedAttributes === 'boolean') {
normalized.preserveUnspecifiedAttributes = rawOperation.preserveUnspecifiedAttributes;
}
if (typeof rawOperation.mergeRelTokens === 'boolean') {
normalized.mergeRelTokens = rawOperation.mergeRelTokens;
}
return normalized;
}
function cloneEditorOperations(rawOperations) {
if (!Array.isArray(rawOperations)) return null;
const normalized = rawOperations
.map(normalizeEditorOperation)
.filter(Boolean);
return normalized.length ? normalized : null;
}
/**
* Resolve the concrete toolbar format config for one text component.
*
* @param {HTMLElement|null} element Active editable element.
* @param {Object} runtimeOptions Normalized component editor options.
* @returns {Array