2023-12-16 07:22:40 -05:00
|
|
|
/**
|
|
|
|
* Handle alignment for embed (iframe/video) content.
|
|
|
|
* TinyMCE built-in handling doesn't work well for these when classes are used for
|
|
|
|
* alignment, since the editor wraps these elements in a non-editable preview span
|
|
|
|
* which looses tracking and setting of alignment options.
|
|
|
|
* Here we manually manage these properties and formatting events, by effectively
|
|
|
|
* syncing the alignment classes to the parent preview span.
|
|
|
|
* @param {Editor} editor
|
|
|
|
*/
|
|
|
|
export function handleEmbedAlignmentChanges(editor) {
|
|
|
|
function updateClassesForPreview(previewElem) {
|
|
|
|
const mediaTarget = previewElem.querySelector('iframe, video');
|
|
|
|
if (!mediaTarget) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const alignmentClasses = [...mediaTarget.classList.values()].filter(c => c.startsWith('align-'));
|
|
|
|
const previewAlignClasses = [...previewElem.classList.values()].filter(c => c.startsWith('align-'));
|
|
|
|
previewElem.classList.remove(...previewAlignClasses);
|
|
|
|
previewElem.classList.add(...alignmentClasses);
|
|
|
|
}
|
|
|
|
|
|
|
|
editor.on('SetContent', () => {
|
|
|
|
const previewElems = editor.dom.select('span.mce-preview-object');
|
|
|
|
for (const previewElem of previewElems) {
|
|
|
|
updateClassesForPreview(previewElem);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
editor.on('FormatApply', event => {
|
|
|
|
const isAlignment = event.format.startsWith('align');
|
2024-04-29 12:44:56 -04:00
|
|
|
const isElement = event.node instanceof editor.dom.doc.defaultView.HTMLElement;
|
|
|
|
if (!isElement || !isAlignment || !event.node.matches('.mce-preview-object')) {
|
2023-12-16 07:22:40 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const realTarget = event.node.querySelector('iframe, video');
|
2024-04-29 12:44:56 -04:00
|
|
|
if (realTarget) {
|
2023-12-16 07:22:40 -05:00
|
|
|
const className = (editor.formatter.get(event.format)[0]?.classes || [])[0];
|
|
|
|
const toAdd = !realTarget.classList.contains(className);
|
|
|
|
|
|
|
|
const wrapperClasses = (event.node.getAttribute('data-mce-p-class') || '').split(' ');
|
|
|
|
const wrapperClassesFiltered = wrapperClasses.filter(c => !c.startsWith('align-'));
|
|
|
|
if (toAdd) {
|
|
|
|
wrapperClassesFiltered.push(className);
|
|
|
|
}
|
|
|
|
|
|
|
|
const classesToApply = wrapperClassesFiltered.join(' ');
|
|
|
|
event.node.setAttribute('data-mce-p-class', classesToApply);
|
|
|
|
|
|
|
|
realTarget.setAttribute('class', classesToApply);
|
|
|
|
editor.formatter.apply(event.format, {}, realTarget);
|
|
|
|
updateClassesForPreview(event.node);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-02-15 11:29:37 -05:00
|
|
|
|
2024-05-16 09:54:44 -04:00
|
|
|
/**
|
|
|
|
* Cleans up and removes text-alignment specific properties on all child elements.
|
|
|
|
* @param {HTMLElement} element
|
|
|
|
*/
|
|
|
|
function cleanChildAlignment(element) {
|
|
|
|
const alignedChildren = element.querySelectorAll('[align],[style*="text-align"],.align-center,.align-left,.align-right');
|
|
|
|
for (const child of alignedChildren) {
|
|
|
|
child.removeAttribute('align');
|
|
|
|
child.style.textAlign = null;
|
|
|
|
child.classList.remove('align-center', 'align-right', 'align-left');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-15 11:29:37 -05:00
|
|
|
/**
|
2024-02-20 09:15:22 -05:00
|
|
|
* Cleans up the direction property for an element.
|
|
|
|
* Removes all inline direction control from child elements.
|
|
|
|
* Removes non "dir" attribute direction control from provided element.
|
|
|
|
* @param {HTMLElement} element
|
|
|
|
*/
|
|
|
|
function cleanElementDirection(element) {
|
2024-05-16 09:54:44 -04:00
|
|
|
const directionChildren = element.querySelectorAll('[dir],[style*="direction"]');
|
2024-02-20 09:15:22 -05:00
|
|
|
for (const child of directionChildren) {
|
|
|
|
child.removeAttribute('dir');
|
|
|
|
child.style.direction = null;
|
|
|
|
}
|
2024-05-16 09:54:44 -04:00
|
|
|
|
|
|
|
cleanChildAlignment(element);
|
2024-02-20 09:15:22 -05:00
|
|
|
element.style.direction = null;
|
|
|
|
element.style.textAlign = null;
|
2024-05-16 09:54:44 -04:00
|
|
|
element.removeAttribute('align');
|
2024-02-20 09:15:22 -05:00
|
|
|
}
|
|
|
|
|
2024-05-16 09:54:44 -04:00
|
|
|
/**
|
|
|
|
* @typedef {Function} TableCellHandler
|
|
|
|
* @param {HTMLTableCellElement} cell
|
|
|
|
*/
|
|
|
|
|
2024-02-20 09:15:22 -05:00
|
|
|
/**
|
|
|
|
* This tracks table cell range selection, so we can apply custom handling where
|
|
|
|
* required to actions applied to such selections.
|
2024-02-15 11:29:37 -05:00
|
|
|
* The events used don't seem to be advertised by TinyMCE.
|
|
|
|
* Found at https://github.com/tinymce/tinymce/blob/6.8.3/modules/tinymce/src/models/dom/main/ts/table/api/Events.ts
|
|
|
|
* @param {Editor} editor
|
|
|
|
*/
|
2024-02-20 09:15:22 -05:00
|
|
|
export function handleTableCellRangeEvents(editor) {
|
2024-02-15 11:29:37 -05:00
|
|
|
/** @var {HTMLTableCellElement[]} * */
|
|
|
|
let selectedCells = [];
|
|
|
|
|
|
|
|
editor.on('TableSelectionChange', event => {
|
|
|
|
selectedCells = (event.cells || []).map(cell => cell.dom);
|
|
|
|
});
|
|
|
|
editor.on('TableSelectionClear', () => {
|
|
|
|
selectedCells = [];
|
|
|
|
});
|
|
|
|
|
2024-05-16 09:54:44 -04:00
|
|
|
/**
|
|
|
|
* @type {Object<String, TableCellHandler>}
|
|
|
|
*/
|
|
|
|
const actionByCommand = {
|
|
|
|
// TinyMCE does not seem to do a great job on clearing styles in complex
|
|
|
|
// scenarios (like copied word content) when a range of table cells
|
|
|
|
// are selected. Here we watch for clear formatting events, so some manual
|
|
|
|
// cleanup can be performed.
|
|
|
|
RemoveFormat: cell => {
|
|
|
|
const attrsToRemove = ['class', 'style', 'width', 'height', 'align'];
|
|
|
|
for (const attr of attrsToRemove) {
|
|
|
|
cell.removeAttribute(attr);
|
2024-02-15 11:29:37 -05:00
|
|
|
}
|
2024-05-16 09:54:44 -04:00
|
|
|
},
|
2024-02-20 09:15:22 -05:00
|
|
|
|
2024-05-16 09:54:44 -04:00
|
|
|
// TinyMCE does not apply direction events to table cell range selections
|
|
|
|
// so here we hastily patch in that ability by setting the direction ourselves
|
|
|
|
// when a direction event is fired.
|
|
|
|
mceDirectionLTR: cell => {
|
|
|
|
cell.setAttribute('dir', 'ltr');
|
|
|
|
cleanElementDirection(cell);
|
|
|
|
},
|
|
|
|
mceDirectionRTL: cell => {
|
|
|
|
cell.setAttribute('dir', 'rtl');
|
2024-02-20 09:15:22 -05:00
|
|
|
cleanElementDirection(cell);
|
2024-05-16 09:54:44 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
// The "align" attribute can exist on table elements so this clears
|
|
|
|
// the attribute, and also clears common child alignment properties,
|
|
|
|
// when a text direction action is made for a table cell range.
|
|
|
|
JustifyLeft: cell => {
|
|
|
|
cell.removeAttribute('align');
|
|
|
|
cleanChildAlignment(cell);
|
|
|
|
},
|
|
|
|
JustifyRight: this.JustifyLeft,
|
|
|
|
JustifyCenter: this.JustifyLeft,
|
|
|
|
JustifyFull: this.JustifyLeft,
|
|
|
|
};
|
|
|
|
|
|
|
|
editor.on('ExecCommand', event => {
|
|
|
|
const action = actionByCommand[event.command];
|
|
|
|
if (action) {
|
|
|
|
for (const cell of selectedCells) {
|
|
|
|
action(cell);
|
|
|
|
}
|
2024-02-20 09:15:22 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Direction control might not work if there are other unexpected direction-handling styles
|
|
|
|
* or attributes involved nearby. This watches for direction change events to clean
|
|
|
|
* up direction controls, removing non-dir-attr direction controls, while removing
|
|
|
|
* directions from child elements that may be involved.
|
|
|
|
* @param {Editor} editor
|
|
|
|
*/
|
|
|
|
export function handleTextDirectionCleaning(editor) {
|
|
|
|
editor.on('ExecCommand', event => {
|
|
|
|
const command = event.command;
|
|
|
|
if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const blocks = editor.selection.getSelectedBlocks();
|
|
|
|
for (const block of blocks) {
|
|
|
|
cleanElementDirection(block);
|
|
|
|
}
|
|
|
|
});
|
2024-02-15 11:29:37 -05:00
|
|
|
}
|