2 * Handle alignment for embed (iframe/video) content.
3 * TinyMCE built-in handling doesn't work well for these when classes are used for
4 * alignment, since the editor wraps these elements in a non-editable preview span
5 * which looses tracking and setting of alignment options.
6 * Here we manually manage these properties and formatting events, by effectively
7 * syncing the alignment classes to the parent preview span.
8 * @param {Editor} editor
10 export function handleEmbedAlignmentChanges(editor) {
11 function updateClassesForPreview(previewElem) {
12 const mediaTarget = previewElem.querySelector('iframe, video');
17 const alignmentClasses = [...mediaTarget.classList.values()].filter(c => c.startsWith('align-'));
18 const previewAlignClasses = [...previewElem.classList.values()].filter(c => c.startsWith('align-'));
19 previewElem.classList.remove(...previewAlignClasses);
20 previewElem.classList.add(...alignmentClasses);
23 editor.on('SetContent', () => {
24 const previewElems = editor.dom.select('span.mce-preview-object');
25 for (const previewElem of previewElems) {
26 updateClassesForPreview(previewElem);
30 editor.on('FormatApply', event => {
31 const isAlignment = event.format.startsWith('align');
32 const isElement = event.node instanceof editor.dom.doc.defaultView.HTMLElement;
33 if (!isElement || !isAlignment || !event.node.matches('.mce-preview-object')) {
37 const realTarget = event.node.querySelector('iframe, video');
39 const className = (editor.formatter.get(event.format)[0]?.classes || [])[0];
40 const toAdd = !realTarget.classList.contains(className);
42 const wrapperClasses = (event.node.getAttribute('data-mce-p-class') || '').split(' ');
43 const wrapperClassesFiltered = wrapperClasses.filter(c => !c.startsWith('align-'));
45 wrapperClassesFiltered.push(className);
48 const classesToApply = wrapperClassesFiltered.join(' ');
49 event.node.setAttribute('data-mce-p-class', classesToApply);
51 realTarget.setAttribute('class', classesToApply);
52 editor.formatter.apply(event.format, {}, realTarget);
53 updateClassesForPreview(event.node);
59 * Cleans up and removes text-alignment specific properties on all child elements.
60 * @param {HTMLElement} element
62 function cleanChildAlignment(element) {
63 const alignedChildren = element.querySelectorAll('[align],[style*="text-align"],.align-center,.align-left,.align-right');
64 for (const child of alignedChildren) {
65 child.removeAttribute('align');
66 child.style.textAlign = null;
67 child.classList.remove('align-center', 'align-right', 'align-left');
72 * Cleans up the direction property for an element.
73 * Removes all inline direction control from child elements.
74 * Removes non "dir" attribute direction control from provided element.
75 * @param {HTMLElement} element
77 function cleanElementDirection(element) {
78 const directionChildren = element.querySelectorAll('[dir],[style*="direction"]');
79 for (const child of directionChildren) {
80 child.removeAttribute('dir');
81 child.style.direction = null;
84 cleanChildAlignment(element);
85 element.style.direction = null;
86 element.style.textAlign = null;
87 element.removeAttribute('align');
91 * @typedef {Function} TableCellHandler
92 * @param {HTMLTableCellElement} cell
96 * This tracks table cell range selection, so we can apply custom handling where
97 * required to actions applied to such selections.
98 * The events used don't seem to be advertised by TinyMCE.
99 * Found at https://github.com/tinymce/tinymce/blob/6.8.3/modules/tinymce/src/models/dom/main/ts/table/api/Events.ts
100 * @param {Editor} editor
102 export function handleTableCellRangeEvents(editor) {
103 /** @var {HTMLTableCellElement[]} * */
104 let selectedCells = [];
106 editor.on('TableSelectionChange', event => {
107 selectedCells = (event.cells || []).map(cell => cell.dom);
109 editor.on('TableSelectionClear', () => {
114 * @type {Object<String, TableCellHandler>}
116 const actionByCommand = {
117 // TinyMCE does not seem to do a great job on clearing styles in complex
118 // scenarios (like copied word content) when a range of table cells
119 // are selected. Here we watch for clear formatting events, so some manual
120 // cleanup can be performed.
121 RemoveFormat: cell => {
122 const attrsToRemove = ['class', 'style', 'width', 'height', 'align'];
123 for (const attr of attrsToRemove) {
124 cell.removeAttribute(attr);
128 // TinyMCE does not apply direction events to table cell range selections
129 // so here we hastily patch in that ability by setting the direction ourselves
130 // when a direction event is fired.
131 mceDirectionLTR: cell => {
132 cell.setAttribute('dir', 'ltr');
133 cleanElementDirection(cell);
135 mceDirectionRTL: cell => {
136 cell.setAttribute('dir', 'rtl');
137 cleanElementDirection(cell);
140 // The "align" attribute can exist on table elements so this clears
141 // the attribute, and also clears common child alignment properties,
142 // when a text direction action is made for a table cell range.
143 JustifyLeft: cell => {
144 cell.removeAttribute('align');
145 cleanChildAlignment(cell);
147 JustifyRight: this.JustifyLeft,
148 JustifyCenter: this.JustifyLeft,
149 JustifyFull: this.JustifyLeft,
152 editor.on('ExecCommand', event => {
153 const action = actionByCommand[event.command];
155 for (const cell of selectedCells) {
163 * Direction control might not work if there are other unexpected direction-handling styles
164 * or attributes involved nearby. This watches for direction change events to clean
165 * up direction controls, removing non-dir-attr direction controls, while removing
166 * directions from child elements that may be involved.
167 * @param {Editor} editor
169 export function handleTextDirectionCleaning(editor) {
170 editor.on('ExecCommand', event => {
171 const command = event.command;
172 if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {
176 const blocks = editor.selection.getSelectedBlocks();
177 for (const block of blocks) {
178 cleanElementDirection(block);