]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/fixes.js
CSS: Removed redundant calc
[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         if (!event.node || !event.node.matches('.mce-preview-object')) {
33             return;
34         }
35
36         const realTarget = event.node.querySelector('iframe, video');
37         if (isAlignment && realTarget) {
38             const className = (editor.formatter.get(event.format)[0]?.classes || [])[0];
39             const toAdd = !realTarget.classList.contains(className);
40
41             const wrapperClasses = (event.node.getAttribute('data-mce-p-class') || '').split(' ');
42             const wrapperClassesFiltered = wrapperClasses.filter(c => !c.startsWith('align-'));
43             if (toAdd) {
44                 wrapperClassesFiltered.push(className);
45             }
46
47             const classesToApply = wrapperClassesFiltered.join(' ');
48             event.node.setAttribute('data-mce-p-class', classesToApply);
49
50             realTarget.setAttribute('class', classesToApply);
51             editor.formatter.apply(event.format, {}, realTarget);
52             updateClassesForPreview(event.node);
53         }
54     });
55 }
56
57 /**
58  * Cleans up the direction property for an element.
59  * Removes all inline direction control from child elements.
60  * Removes non "dir" attribute direction control from provided element.
61  * @param {HTMLElement} element
62  */
63 function cleanElementDirection(element) {
64     const directionChildren = element.querySelectorAll('[dir],[style*="direction"],[style*="text-align"]');
65     for (const child of directionChildren) {
66         child.removeAttribute('dir');
67         child.style.direction = null;
68         child.style.textAlign = null;
69     }
70     element.style.direction = null;
71     element.style.textAlign = null;
72 }
73
74 /**
75  * This tracks table cell range selection, so we can apply custom handling where
76  * required to actions applied to such selections.
77  * The events used don't seem to be advertised by TinyMCE.
78  * Found at https://github.com/tinymce/tinymce/blob/6.8.3/modules/tinymce/src/models/dom/main/ts/table/api/Events.ts
79  * @param {Editor} editor
80  */
81 export function handleTableCellRangeEvents(editor) {
82     /** @var {HTMLTableCellElement[]} * */
83     let selectedCells = [];
84
85     editor.on('TableSelectionChange', event => {
86         selectedCells = (event.cells || []).map(cell => cell.dom);
87     });
88     editor.on('TableSelectionClear', () => {
89         selectedCells = [];
90     });
91
92     // TinyMCE does not seem to do a great job on clearing styles in complex
93     // scenarios (like copied word content) when a range of table cells
94     // are selected. Here we watch for clear formatting events, so some manual
95     // cleanup can be performed.
96     const attrsToRemove = ['class', 'style', 'width', 'height'];
97     editor.on('FormatRemove', () => {
98         for (const cell of selectedCells) {
99             for (const attr of attrsToRemove) {
100                 cell.removeAttribute(attr);
101             }
102         }
103     });
104
105     // TinyMCE does not apply direction events to table cell range selections
106     // so here we hastily patch in that ability by setting the direction ourselves
107     // when a direction event is fired.
108     editor.on('ExecCommand', event => {
109         const command = event.command;
110         if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {
111             return;
112         }
113
114         const dir = command === 'mceDirectionLTR' ? 'ltr' : 'rtl';
115         for (const cell of selectedCells) {
116             cell.setAttribute('dir', dir);
117             cleanElementDirection(cell);
118         }
119     });
120 }
121
122 /**
123  * Direction control might not work if there are other unexpected direction-handling styles
124  * or attributes involved nearby. This watches for direction change events to clean
125  * up direction controls, removing non-dir-attr direction controls, while removing
126  * directions from child elements that may be involved.
127  * @param {Editor} editor
128  */
129 export function handleTextDirectionCleaning(editor) {
130     editor.on('ExecCommand', event => {
131         const command = event.command;
132         if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {
133             return;
134         }
135
136         const blocks = editor.selection.getSelectedBlocks();
137         for (const block of blocks) {
138             cleanElementDirection(block);
139         }
140     });
141 }