]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/fixes.js
MFA: Tweaked backup code wording
[bookstack] / resources / js / wysiwyg / fixes.js
1 /**
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
9  */
10 export function handleEmbedAlignmentChanges(editor) {
11     function updateClassesForPreview(previewElem) {
12         const mediaTarget = previewElem.querySelector('iframe, video');
13         if (!mediaTarget) {
14             return;
15         }
16
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);
21     }
22
23     editor.on('SetContent', () => {
24         const previewElems = editor.dom.select('span.mce-preview-object');
25         for (const previewElem of previewElems) {
26             updateClassesForPreview(previewElem);
27         }
28     });
29
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')) {
34             return;
35         }
36
37         const realTarget = event.node.querySelector('iframe, video');
38         if (realTarget) {
39             const className = (editor.formatter.get(event.format)[0]?.classes || [])[0];
40             const toAdd = !realTarget.classList.contains(className);
41
42             const wrapperClasses = (event.node.getAttribute('data-mce-p-class') || '').split(' ');
43             const wrapperClassesFiltered = wrapperClasses.filter(c => !c.startsWith('align-'));
44             if (toAdd) {
45                 wrapperClassesFiltered.push(className);
46             }
47
48             const classesToApply = wrapperClassesFiltered.join(' ');
49             event.node.setAttribute('data-mce-p-class', classesToApply);
50
51             realTarget.setAttribute('class', classesToApply);
52             editor.formatter.apply(event.format, {}, realTarget);
53             updateClassesForPreview(event.node);
54         }
55     });
56 }
57
58 /**
59  * Cleans up and removes text-alignment specific properties on all child elements.
60  * @param {HTMLElement} element
61  */
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');
68     }
69 }
70
71 /**
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
76  */
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;
82     }
83
84     cleanChildAlignment(element);
85     element.style.direction = null;
86     element.style.textAlign = null;
87     element.removeAttribute('align');
88 }
89
90 /**
91  * @typedef {Function} TableCellHandler
92  * @param {HTMLTableCellElement} cell
93  */
94
95 /**
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
101  */
102 export function handleTableCellRangeEvents(editor) {
103     /** @var {HTMLTableCellElement[]} * */
104     let selectedCells = [];
105
106     editor.on('TableSelectionChange', event => {
107         selectedCells = (event.cells || []).map(cell => cell.dom);
108     });
109     editor.on('TableSelectionClear', () => {
110         selectedCells = [];
111     });
112
113     /**
114      * @type {Object<String, TableCellHandler>}
115      */
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);
125             }
126         },
127
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);
134         },
135         mceDirectionRTL: cell => {
136             cell.setAttribute('dir', 'rtl');
137             cleanElementDirection(cell);
138         },
139
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);
146         },
147         JustifyRight: this.JustifyLeft,
148         JustifyCenter: this.JustifyLeft,
149         JustifyFull: this.JustifyLeft,
150     };
151
152     editor.on('ExecCommand', event => {
153         const action = actionByCommand[event.command];
154         if (action) {
155             for (const cell of selectedCells) {
156                 action(cell);
157             }
158         }
159     });
160 }
161
162 /**
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
168  */
169 export function handleTextDirectionCleaning(editor) {
170     editor.on('ExecCommand', event => {
171         const command = event.command;
172         if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {
173             return;
174         }
175
176         const blocks = editor.selection.getSelectedBlocks();
177         for (const block of blocks) {
178             cleanElementDirection(block);
179         }
180     });
181 }