@akilli/editor-src 5.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abbreviation/Abbreviation.js +32 -0
- package/abbreviation/AbbreviationDialog.js +21 -0
- package/audio/Audio.js +47 -0
- package/audio/AudioDialog.js +18 -0
- package/audio/AudioListener.js +50 -0
- package/base/AlignCommand.js +34 -0
- package/base/AlignableListener.js +45 -0
- package/base/Alignment.js +36 -0
- package/base/BarListener.js +95 -0
- package/base/Base.js +127 -0
- package/base/Command.js +139 -0
- package/base/CommandManager.js +60 -0
- package/base/ContentFilter.js +109 -0
- package/base/DeletableListener.js +36 -0
- package/base/DeleteCommand.js +18 -0
- package/base/Dialog.js +153 -0
- package/base/DialogManager.js +44 -0
- package/base/Dispatcher.js +88 -0
- package/base/Dom.js +790 -0
- package/base/EditableListener.js +82 -0
- package/base/Editor.js +448 -0
- package/base/Filter.js +35 -0
- package/base/FilterManager.js +44 -0
- package/base/FocusableListener.js +22 -0
- package/base/FocusbarListener.js +99 -0
- package/base/FormCreator.js +162 -0
- package/base/FormatbarListener.js +32 -0
- package/base/Key.js +258 -0
- package/base/Listener.js +51 -0
- package/base/NavigableListener.js +81 -0
- package/base/Plugin.js +176 -0
- package/base/PluginManager.js +51 -0
- package/base/SlotableListener.js +40 -0
- package/base/SortCommand.js +30 -0
- package/base/SortableListener.js +135 -0
- package/base/Sorting.js +36 -0
- package/base/Tag.js +113 -0
- package/base/TagGroup.js +183 -0
- package/base/TagListener.js +34 -0
- package/base/TagManager.js +61 -0
- package/base/TagName.js +470 -0
- package/base/ToolbarListener.js +11 -0
- package/base/util.js +59 -0
- package/block/Block.js +51 -0
- package/block/BlockDialog.js +11 -0
- package/block/BlockElement.js +21 -0
- package/block/BlockListener.js +60 -0
- package/blockquote/Blockquote.js +43 -0
- package/blockquote/BlockquoteFilter.js +22 -0
- package/blockquote/BlockquoteListener.js +34 -0
- package/bold/Bold.js +30 -0
- package/break/Break.js +33 -0
- package/break/BreakFilter.js +24 -0
- package/build/BuildEditor.js +97 -0
- package/build/editor.css +548 -0
- package/build/editor.woff2 +0 -0
- package/cite/Cite.js +30 -0
- package/code/Code.js +30 -0
- package/data/Data.js +32 -0
- package/data/DataDialog.js +13 -0
- package/definition/Definition.js +32 -0
- package/definition/DefinitionDialog.js +13 -0
- package/deletion/Deletion.js +30 -0
- package/details/Details.js +63 -0
- package/details/DetailsFilter.js +17 -0
- package/details/DetailsListener.js +102 -0
- package/division/Division.js +53 -0
- package/division/DivisionDialog.js +13 -0
- package/emphasis/Emphasis.js +30 -0
- package/figure/Figure.js +58 -0
- package/figure/FigureFilter.js +14 -0
- package/figure/FigureListener.js +23 -0
- package/heading/Heading.js +38 -0
- package/horizontalrule/HorizontalRule.js +37 -0
- package/i18n/I18n.js +26 -0
- package/i18n/de.js +167 -0
- package/iframe/Iframe.js +49 -0
- package/iframe/IframeDialog.js +20 -0
- package/iframe/IframeListener.js +48 -0
- package/image/Image.js +47 -0
- package/image/ImageDialog.js +23 -0
- package/image/ImageListener.js +47 -0
- package/insertion/Insertion.js +30 -0
- package/italic/Italic.js +30 -0
- package/keyboard/Keyboard.js +30 -0
- package/link/Link.js +34 -0
- package/link/LinkDialog.js +14 -0
- package/link/LinkListener.js +45 -0
- package/list/List.js +40 -0
- package/list/ListListener.js +91 -0
- package/mark/Mark.js +30 -0
- package/orderedlist/OrderedList.js +39 -0
- package/package.json +24 -0
- package/paragraph/Paragraph.js +42 -0
- package/paragraph/ParagraphListener.js +40 -0
- package/preformat/Preformat.js +43 -0
- package/preformat/PreformatFilter.js +22 -0
- package/preformat/PreformatListener.js +34 -0
- package/quote/Quote.js +30 -0
- package/sample/Sample.js +30 -0
- package/section/Section.js +55 -0
- package/section/SectionDialog.js +13 -0
- package/small/Small.js +30 -0
- package/strikethrough/Strikethrough.js +30 -0
- package/strong/Strong.js +30 -0
- package/subheading/Subheading.js +38 -0
- package/subscript/Subscript.js +30 -0
- package/superscript/Superscript.js +30 -0
- package/table/Table.js +113 -0
- package/table/TableCellListener.js +125 -0
- package/table/TableColumnAddCommand.js +19 -0
- package/table/TableColumnListener.js +34 -0
- package/table/TableCommand.js +23 -0
- package/table/TableDialog.js +14 -0
- package/table/TableFilter.js +35 -0
- package/table/TableListener.js +39 -0
- package/table/TableRowAddCommand.js +18 -0
- package/table/TableRowListener.js +34 -0
- package/time/Time.js +32 -0
- package/time/TimeDialog.js +13 -0
- package/underline/Underline.js +30 -0
- package/unorderedlist/UnorderedList.js +39 -0
- package/variable/Variable.js +30 -0
- package/video/Video.js +47 -0
- package/video/VideoDialog.js +20 -0
- package/video/VideoListener.js +48 -0
package/base/Dom.js
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import Editor from './Editor.js';
|
|
2
|
+
import Sorting from './Sorting.js';
|
|
3
|
+
import TagName from './TagName.js';
|
|
4
|
+
import { is, isFunction, isString } from './util.js';
|
|
5
|
+
|
|
6
|
+
export default class Dom {
|
|
7
|
+
/**
|
|
8
|
+
* @type {Editor}
|
|
9
|
+
*/
|
|
10
|
+
#editor;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @return {Editor}
|
|
14
|
+
*/
|
|
15
|
+
get editor() {
|
|
16
|
+
return this.#editor;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @type {Document}
|
|
21
|
+
*/
|
|
22
|
+
#document;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @return {Document}
|
|
26
|
+
*/
|
|
27
|
+
get document() {
|
|
28
|
+
return this.#document;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @type {Window}
|
|
33
|
+
*/
|
|
34
|
+
#window;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @return {Window}
|
|
38
|
+
*/
|
|
39
|
+
get window() {
|
|
40
|
+
return this.#window;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @type {HTMLHtmlElement}
|
|
45
|
+
*/
|
|
46
|
+
#root;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @return {HTMLHtmlElement}
|
|
50
|
+
*/
|
|
51
|
+
get root() {
|
|
52
|
+
return this.#root;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Browser window configuration
|
|
57
|
+
*
|
|
58
|
+
* @type {Object.<string, string>}
|
|
59
|
+
*/
|
|
60
|
+
#browser = {
|
|
61
|
+
alwaysRaised: 'yes',
|
|
62
|
+
dependent: 'yes',
|
|
63
|
+
height: '',
|
|
64
|
+
location: 'no',
|
|
65
|
+
menubar: 'no',
|
|
66
|
+
minimizable: 'no',
|
|
67
|
+
modal: 'yes',
|
|
68
|
+
resizable: 'yes',
|
|
69
|
+
scrollbars: 'yes',
|
|
70
|
+
toolbar: 'no',
|
|
71
|
+
width: '',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {Editor} editor
|
|
76
|
+
* @param {Document} document
|
|
77
|
+
*/
|
|
78
|
+
constructor(editor, document) {
|
|
79
|
+
if (!(editor instanceof Editor) || !(document instanceof Document)) {
|
|
80
|
+
throw new TypeError('Invalid argument');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.#editor = editor;
|
|
84
|
+
this.#document = document;
|
|
85
|
+
this.#window = this.document.defaultView;
|
|
86
|
+
this.#root = this.document.documentElement;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registers custom element
|
|
91
|
+
*
|
|
92
|
+
* @param {string} name
|
|
93
|
+
* @param {function} constructor
|
|
94
|
+
* @param {string|undefined} [parentName = undefined]
|
|
95
|
+
* @return {void}
|
|
96
|
+
*/
|
|
97
|
+
registerElement(name, constructor, parentName = undefined) {
|
|
98
|
+
if (!is(this.window.customElements.get(name))) {
|
|
99
|
+
this.window.customElements.define(name, constructor, parentName ? { extends: parentName } : undefined);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates HTML element in editor document
|
|
105
|
+
*
|
|
106
|
+
* @param {string} name
|
|
107
|
+
* @param {Object.<string, string>} [attributes = {}]
|
|
108
|
+
* @param {string} [html = '']
|
|
109
|
+
* @return {HTMLElement}
|
|
110
|
+
*/
|
|
111
|
+
createElement(name, { attributes = {}, html = '' } = {}) {
|
|
112
|
+
const element = this.document.createElement(name);
|
|
113
|
+
element.innerHTML = html;
|
|
114
|
+
Object.entries(attributes).forEach(([key, val]) => val && element.setAttribute(key, `${val}`));
|
|
115
|
+
|
|
116
|
+
return element;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Inserts element
|
|
121
|
+
*
|
|
122
|
+
* @param {HTMLElement} element
|
|
123
|
+
* @return {void}
|
|
124
|
+
*/
|
|
125
|
+
insert(element) {
|
|
126
|
+
if (!(element instanceof HTMLElement)) {
|
|
127
|
+
throw new TypeError('Invalid argument');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const editable = this.getSelectedEditable();
|
|
131
|
+
|
|
132
|
+
if (editable instanceof HTMLSlotElement && this.editor.tags.allowed(editable.parentElement, element)) {
|
|
133
|
+
this.insertBefore(element, editable);
|
|
134
|
+
} else if (editable) {
|
|
135
|
+
const sibling = this.closest(editable, element);
|
|
136
|
+
|
|
137
|
+
if (sibling) {
|
|
138
|
+
this.insertAfter(element, sibling);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (editable.hasAttribute('data-deletable') && !editable.textContent.trim()) {
|
|
142
|
+
editable.parentElement.removeChild(editable);
|
|
143
|
+
}
|
|
144
|
+
} else if (this.editor.tags.allowed(this.editor.root, element)) {
|
|
145
|
+
this.insertLastChild(element, this.editor.root);
|
|
146
|
+
} else {
|
|
147
|
+
throw new TypeError('Invalid argument');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} text
|
|
153
|
+
* @return {void}
|
|
154
|
+
*/
|
|
155
|
+
insertText(text) {
|
|
156
|
+
const editable = this.getSelectedEditable();
|
|
157
|
+
|
|
158
|
+
if (editable) {
|
|
159
|
+
const range = this.getRange();
|
|
160
|
+
range.deleteContents();
|
|
161
|
+
range.insertNode(this.createText(text));
|
|
162
|
+
range.collapse();
|
|
163
|
+
editable.normalize();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Adds/removes formatting to/from selected text
|
|
169
|
+
*
|
|
170
|
+
* @param {HTMLElement} element
|
|
171
|
+
* @return {void}
|
|
172
|
+
*/
|
|
173
|
+
format(element) {
|
|
174
|
+
if (!(element instanceof HTMLElement)) {
|
|
175
|
+
throw new TypeError('Invalid argument');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const range = this.getRange();
|
|
179
|
+
const editable = this.getSelectedEditable();
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
!range ||
|
|
183
|
+
range.collapsed ||
|
|
184
|
+
!range.toString().trim() ||
|
|
185
|
+
!editable ||
|
|
186
|
+
!this.editor.tags.allowed(editable, element)
|
|
187
|
+
) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (range.startContainer instanceof Text && range.startContainer.parentElement !== editable) {
|
|
192
|
+
range.setStartBefore(range.startContainer.parentElement);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (range.endContainer instanceof Text && range.endContainer.parentElement !== editable) {
|
|
196
|
+
range.setEndAfter(range.endContainer.parentElement);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const selText = range.toString();
|
|
200
|
+
const same = Array.from(range.cloneContents().childNodes).every(
|
|
201
|
+
(item) =>
|
|
202
|
+
(item instanceof Text && !item.textContent.trim()) ||
|
|
203
|
+
(item instanceof HTMLElement && item.localName === element.localName)
|
|
204
|
+
);
|
|
205
|
+
range.deleteContents();
|
|
206
|
+
|
|
207
|
+
if (same) {
|
|
208
|
+
range.insertNode(this.createText(selText));
|
|
209
|
+
} else {
|
|
210
|
+
element.textContent = selText;
|
|
211
|
+
range.insertNode(element);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
editable.normalize();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {string} text
|
|
219
|
+
* @return {Text}
|
|
220
|
+
*/
|
|
221
|
+
createText(text) {
|
|
222
|
+
return this.document.createTextNode(text);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {number} [rows = 1]
|
|
227
|
+
* @param {number} [cols = 1]
|
|
228
|
+
* @return {HTMLTableElement}
|
|
229
|
+
*/
|
|
230
|
+
createTable(rows = 1, cols = 1) {
|
|
231
|
+
const element = this.createElement(TagName.TABLE);
|
|
232
|
+
|
|
233
|
+
this.insertLastChild(this.createTableHeader(1, cols), element);
|
|
234
|
+
this.insertLastChild(this.createTableBody(rows, cols), element);
|
|
235
|
+
this.insertLastChild(this.createTableFooter(1, cols), element);
|
|
236
|
+
|
|
237
|
+
return element;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {number} [cols = 1]
|
|
242
|
+
* @return {HTMLTableColElement}
|
|
243
|
+
*/
|
|
244
|
+
createTableColumnGroup(cols = 1) {
|
|
245
|
+
const element = this.createElement(TagName.COLGROUP);
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < cols; i++) {
|
|
248
|
+
this.insertLastChild(this.createElement(TagName.COL), element);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return element;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {number} [rows = 1]
|
|
256
|
+
* @param {number} [cols = 1]
|
|
257
|
+
* @return {HTMLTableSectionElement}
|
|
258
|
+
*/
|
|
259
|
+
createTableHeader(rows = 1, cols = 1) {
|
|
260
|
+
return this.#createTableSection(TagName.THEAD, rows, cols);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {number} [rows = 1]
|
|
265
|
+
* @param {number} [cols = 1]
|
|
266
|
+
* @return {HTMLTableSectionElement}
|
|
267
|
+
*/
|
|
268
|
+
createTableBody(rows = 1, cols = 1) {
|
|
269
|
+
return this.#createTableSection(TagName.TBODY, rows, cols);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {number} [rows = 1]
|
|
274
|
+
* @param {number} [cols = 1]
|
|
275
|
+
* @return {HTMLTableSectionElement}
|
|
276
|
+
*/
|
|
277
|
+
createTableFooter(rows = 1, cols = 1) {
|
|
278
|
+
return this.#createTableSection(TagName.TFOOT, rows, cols);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {number} cols
|
|
283
|
+
* @return {HTMLTableRowElement}
|
|
284
|
+
*/
|
|
285
|
+
createTableHeaderRow(cols) {
|
|
286
|
+
const element = this.createElement(TagName.TR);
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < cols; i++) {
|
|
289
|
+
this.insertLastChild(this.createElement(TagName.TH), element);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return element;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {number} cols
|
|
297
|
+
* @return {HTMLTableRowElement}
|
|
298
|
+
*/
|
|
299
|
+
createTableRow(cols) {
|
|
300
|
+
const element = this.createElement(TagName.TR);
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < cols; i++) {
|
|
303
|
+
this.insertLastChild(this.createElement(TagName.TD), element);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return element;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {HTMLTableRowElement} element
|
|
311
|
+
* @return {void}
|
|
312
|
+
*/
|
|
313
|
+
createTableRowAfter(element) {
|
|
314
|
+
if (!(element instanceof HTMLTableRowElement)) {
|
|
315
|
+
throw new TypeError('Invalid argument');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.insertAfter(this.createTableRow(element.cells.length), element);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {HTMLTableColElement} element
|
|
323
|
+
* @return {void}
|
|
324
|
+
*/
|
|
325
|
+
createTableColumnAfter(element) {
|
|
326
|
+
if (!(element instanceof HTMLTableColElement) || element.localName !== TagName.COL) {
|
|
327
|
+
throw new TypeError('Invalid argument');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const colgroup = element.parentElement;
|
|
331
|
+
const table = colgroup.parentElement;
|
|
332
|
+
const index = Array.from(colgroup.children).indexOf(element);
|
|
333
|
+
Array.from(table.rows).forEach((row) =>
|
|
334
|
+
this.insertAfter(this.createElement(row.cells[index].localName), row.cells[index])
|
|
335
|
+
);
|
|
336
|
+
this.insertAfter(this.createElement(TagName.COL), element);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Sorts element
|
|
341
|
+
*
|
|
342
|
+
* @param {HTMLElement} element
|
|
343
|
+
* @param {string} sorting
|
|
344
|
+
* @return {void}
|
|
345
|
+
*/
|
|
346
|
+
sort(element, sorting) {
|
|
347
|
+
if (!(element instanceof HTMLElement) || !Sorting.values().includes(sorting)) {
|
|
348
|
+
throw new TypeError('Invalid argument');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (element.parentElement.children.length <= 1) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const parent = element.parentElement;
|
|
356
|
+
const grand = parent.parentElement;
|
|
357
|
+
const prev = element.previousElementSibling;
|
|
358
|
+
const next = element.nextElementSibling;
|
|
359
|
+
const first = parent.firstElementChild;
|
|
360
|
+
const last = parent.lastElementChild;
|
|
361
|
+
const isFirst = element === first;
|
|
362
|
+
const isLast = element === last;
|
|
363
|
+
const isCol = element.localName === TagName.COL;
|
|
364
|
+
const index = Array.from(parent.children).indexOf(element);
|
|
365
|
+
|
|
366
|
+
if (sorting === Sorting.PREV && !isFirst && prev.hasAttribute('data-sortable')) {
|
|
367
|
+
isCol && Array.from(grand.rows).forEach((row) => this.insertBefore(row.cells[index], row.cells[index - 1]));
|
|
368
|
+
this.insertBefore(element, prev);
|
|
369
|
+
} else if (sorting === Sorting.NEXT && !isLast && next.hasAttribute('data-sortable')) {
|
|
370
|
+
isCol && Array.from(grand.rows).forEach((row) => this.insertAfter(row.cells[index], row.cells[index + 1]));
|
|
371
|
+
this.insertAfter(element, next);
|
|
372
|
+
} else if (
|
|
373
|
+
((sorting === Sorting.FIRST && !isFirst) || (sorting === Sorting.NEXT && isLast)) &&
|
|
374
|
+
first.hasAttribute('data-sortable')
|
|
375
|
+
) {
|
|
376
|
+
isCol && Array.from(grand.rows).forEach((row) => this.insertFirstChild(row.cells[index], row));
|
|
377
|
+
this.insertBefore(element, first);
|
|
378
|
+
} else if (
|
|
379
|
+
((sorting === Sorting.LAST && !isLast) || (sorting === Sorting.PREV && isFirst)) &&
|
|
380
|
+
last.hasAttribute('data-sortable')
|
|
381
|
+
) {
|
|
382
|
+
isCol && Array.from(grand.rows).forEach((row) => this.insertLastChild(row.cells[index], row));
|
|
383
|
+
this.insertAfter(element, last);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Insert element before reference element
|
|
389
|
+
*
|
|
390
|
+
* @param {HTMLElement} element
|
|
391
|
+
* @param {HTMLElement} ref
|
|
392
|
+
* @return {void}
|
|
393
|
+
*/
|
|
394
|
+
insertBefore(element, ref) {
|
|
395
|
+
if (!(element instanceof HTMLElement) || !(ref instanceof HTMLElement)) {
|
|
396
|
+
throw new TypeError('Invalid argument');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
ref.insertAdjacentElement('beforebegin', element);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Insert element after reference element
|
|
404
|
+
*
|
|
405
|
+
* @param {HTMLElement} element
|
|
406
|
+
* @param {HTMLElement} ref
|
|
407
|
+
* @return {void}
|
|
408
|
+
*/
|
|
409
|
+
insertAfter(element, ref) {
|
|
410
|
+
if (!(element instanceof HTMLElement) || !(ref instanceof HTMLElement)) {
|
|
411
|
+
throw new TypeError('Invalid argument');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
ref.insertAdjacentElement('afterend', element);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Insert element as first child of reference element
|
|
419
|
+
*
|
|
420
|
+
* @param {HTMLElement} element
|
|
421
|
+
* @param {HTMLElement} ref
|
|
422
|
+
* @return {void}
|
|
423
|
+
*/
|
|
424
|
+
insertFirstChild(element, ref) {
|
|
425
|
+
if (!(element instanceof HTMLElement) || !(ref instanceof HTMLElement)) {
|
|
426
|
+
throw new TypeError('Invalid argument');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
ref.insertAdjacentElement('afterbegin', element);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Insert element as last child of reference element
|
|
434
|
+
*
|
|
435
|
+
* @param {HTMLElement} element
|
|
436
|
+
* @param {HTMLElement} ref
|
|
437
|
+
* @return {void}
|
|
438
|
+
*/
|
|
439
|
+
insertLastChild(element, ref) {
|
|
440
|
+
if (!(element instanceof HTMLElement) || !(ref instanceof HTMLElement)) {
|
|
441
|
+
throw new TypeError('Invalid argument');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
ref.insertAdjacentElement('beforeend', element);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Deletes element and focuses previous sibling if applicable
|
|
449
|
+
*
|
|
450
|
+
* @param {HTMLElement} element
|
|
451
|
+
* @return {void}
|
|
452
|
+
*/
|
|
453
|
+
delete(element) {
|
|
454
|
+
if (!(element instanceof HTMLElement)) {
|
|
455
|
+
throw new TypeError('Invalid argument');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if ([TagName.COL, TagName.TR].includes(element.localName) && element.matches(':only-child')) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (element.localName === TagName.COL) {
|
|
463
|
+
const index = Array.from(element.parentElement.children).indexOf(element);
|
|
464
|
+
const table = element.parentElement.parentElement;
|
|
465
|
+
Array.from(table.rows).forEach((row) => row.removeChild(row.cells[index]));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const prev = element.previousElementSibling;
|
|
469
|
+
const next = element.nextElementSibling;
|
|
470
|
+
element === this.getActiveElement() && element.blur();
|
|
471
|
+
element.parentElement.removeChild(element);
|
|
472
|
+
|
|
473
|
+
if (prev instanceof HTMLElement && prev.hasAttribute('data-focusable')) {
|
|
474
|
+
this.focusEnd(prev);
|
|
475
|
+
} else if (next instanceof HTMLElement && next.hasAttribute('data-focusable')) {
|
|
476
|
+
next.focus();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Indicates if element allows arbitrary amount of child elements
|
|
482
|
+
*
|
|
483
|
+
* @param {HTMLElement} element
|
|
484
|
+
* @return {boolean}
|
|
485
|
+
*/
|
|
486
|
+
arbitrary(element) {
|
|
487
|
+
return element === this.editor.root || element?.hasAttribute('data-arbitrary');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Indicates if given element is contained by editor content root or by a clone of it
|
|
492
|
+
*
|
|
493
|
+
* @param {HTMLElement} element
|
|
494
|
+
* @return {boolean}
|
|
495
|
+
*/
|
|
496
|
+
contains(element) {
|
|
497
|
+
if (!(element instanceof HTMLElement)) {
|
|
498
|
+
throw new TypeError('Invalid argument');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (this.editor.root.contains(element)) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const root = element.closest(this.editor.root.localName);
|
|
506
|
+
|
|
507
|
+
return root && !root.parentElement;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Returns first ancestor of given element whose parent element allows creating given child tag name or element,
|
|
512
|
+
* i.e. the returned element is the sibling element to add the new child before or after
|
|
513
|
+
*
|
|
514
|
+
* @param {HTMLElement} element
|
|
515
|
+
* @param {string|HTMLElement} child
|
|
516
|
+
* @return {HTMLElement|undefined}
|
|
517
|
+
*/
|
|
518
|
+
closest(element, child) {
|
|
519
|
+
if (!(element instanceof HTMLElement) || !this.contains(element.parentElement)) {
|
|
520
|
+
throw new TypeError('Invalid argument');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let sibling = element;
|
|
524
|
+
let parent = element.parentElement;
|
|
525
|
+
|
|
526
|
+
do {
|
|
527
|
+
if (this.arbitrary(parent) && this.editor.tags.allowed(parent, child)) {
|
|
528
|
+
return sibling;
|
|
529
|
+
}
|
|
530
|
+
} while ((sibling = parent) && (parent = parent.parentElement) && this.contains(parent));
|
|
531
|
+
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Wraps element with given parent if necessary and allowed
|
|
537
|
+
*
|
|
538
|
+
* @param {HTMLElement} element
|
|
539
|
+
* @param {string} name
|
|
540
|
+
* @param {Object} [opts = {}]
|
|
541
|
+
* @return {void}
|
|
542
|
+
*/
|
|
543
|
+
wrap(element, name, opts = {}) {
|
|
544
|
+
let sibling;
|
|
545
|
+
|
|
546
|
+
if (!(element instanceof HTMLElement)) {
|
|
547
|
+
throw new TypeError('Invalid argument');
|
|
548
|
+
} else if (element.parentElement.localName !== name && (sibling = this.closest(element, name))) {
|
|
549
|
+
const target = this.createElement(name, opts);
|
|
550
|
+
this.insertAfter(target, sibling);
|
|
551
|
+
this.insertLastChild(element, target);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* @return {HTMLElement|undefined}
|
|
557
|
+
*/
|
|
558
|
+
getActiveElement() {
|
|
559
|
+
const element = this.document.activeElement;
|
|
560
|
+
|
|
561
|
+
return element && this.contains(element) ? element : undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @return {HTMLElement|undefined}
|
|
566
|
+
*/
|
|
567
|
+
getSelectedElement() {
|
|
568
|
+
const sel = this.getSelection();
|
|
569
|
+
const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
|
|
570
|
+
const foc = sel.focusNode instanceof Text ? sel.focusNode.parentElement : sel.focusNode;
|
|
571
|
+
|
|
572
|
+
if (anc instanceof HTMLElement && foc instanceof HTMLElement && anc === foc && this.contains(anc)) {
|
|
573
|
+
return anc;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Returns current selected element only if its tag name matches given name
|
|
581
|
+
*
|
|
582
|
+
* @param {string} name
|
|
583
|
+
* @return {HTMLElement|undefined}
|
|
584
|
+
*/
|
|
585
|
+
getSelectedElementByName(name) {
|
|
586
|
+
const element = this.getSelectedElement();
|
|
587
|
+
|
|
588
|
+
return element?.localName === name ? element : undefined;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Extracts attributes from the current selected element only if its tag name matches given name
|
|
593
|
+
*
|
|
594
|
+
* @param {string} name
|
|
595
|
+
* @return {Object.<string, string>}
|
|
596
|
+
*/
|
|
597
|
+
getSelectedAttributesByName(name) {
|
|
598
|
+
const element = this.getSelectedElementByName(name);
|
|
599
|
+
const attributes = {};
|
|
600
|
+
|
|
601
|
+
if (element) {
|
|
602
|
+
Array.from(element.attributes).forEach((item) => (attributes[item.nodeName] = item.nodeValue));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return attributes;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @return {HTMLElement|undefined}
|
|
610
|
+
*/
|
|
611
|
+
getSelectedEditable() {
|
|
612
|
+
const sel = this.getSelection();
|
|
613
|
+
const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
|
|
614
|
+
const foc = sel.focusNode instanceof Text ? sel.focusNode.parentElement : sel.focusNode;
|
|
615
|
+
|
|
616
|
+
if (anc instanceof HTMLElement && foc instanceof HTMLElement) {
|
|
617
|
+
const ancEdit = anc.closest('[contenteditable=true]');
|
|
618
|
+
const focEdit = foc.closest('[contenteditable=true]');
|
|
619
|
+
|
|
620
|
+
if (ancEdit instanceof HTMLElement && ancEdit === focEdit && this.contains(ancEdit)) {
|
|
621
|
+
return ancEdit;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @return {Selection}
|
|
630
|
+
*/
|
|
631
|
+
getSelection() {
|
|
632
|
+
return this.window.getSelection();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @return {Range|undefined}
|
|
637
|
+
*/
|
|
638
|
+
getRange() {
|
|
639
|
+
const sel = this.getSelection();
|
|
640
|
+
|
|
641
|
+
return sel.rangeCount > 0 ? sel.getRangeAt(0) : undefined;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @param {Range} range
|
|
646
|
+
* @return {void}
|
|
647
|
+
*/
|
|
648
|
+
setRange(range) {
|
|
649
|
+
const sel = this.getSelection();
|
|
650
|
+
sel.removeAllRanges();
|
|
651
|
+
sel.addRange(range);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Selects given element's contents
|
|
656
|
+
*
|
|
657
|
+
* @param {HTMLElement} element
|
|
658
|
+
* @return {void}
|
|
659
|
+
*/
|
|
660
|
+
selectContents(element) {
|
|
661
|
+
if (!(element instanceof HTMLElement)) {
|
|
662
|
+
throw new TypeError('Invalid argument');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const range = this.document.createRange();
|
|
666
|
+
range.selectNodeContents(element);
|
|
667
|
+
this.setRange(range);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Focus end of contents
|
|
672
|
+
*
|
|
673
|
+
* @param {HTMLElement} element
|
|
674
|
+
* @return {void}
|
|
675
|
+
*/
|
|
676
|
+
focusEnd(element) {
|
|
677
|
+
if (!(element instanceof HTMLElement)) {
|
|
678
|
+
throw new TypeError('Invalid argument');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
element.focus();
|
|
682
|
+
const range = this.document.createRange();
|
|
683
|
+
range.selectNodeContents(element);
|
|
684
|
+
range.collapse();
|
|
685
|
+
this.setRange(range);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Focus end of contents
|
|
690
|
+
*
|
|
691
|
+
* @param {HTMLElement} element
|
|
692
|
+
* @param {...string} classes
|
|
693
|
+
* @return {void}
|
|
694
|
+
*/
|
|
695
|
+
removeClass(element, ...classes) {
|
|
696
|
+
if (!(element instanceof HTMLElement)) {
|
|
697
|
+
throw new TypeError('Invalid argument');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
element.classList.remove(...classes);
|
|
701
|
+
element.classList.length > 0 || element.removeAttribute('class');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Opens a browser window and registers a listener for communication between editor and browser windows
|
|
706
|
+
*
|
|
707
|
+
* @param {string} url
|
|
708
|
+
* @param {string} name
|
|
709
|
+
* @param {function} call
|
|
710
|
+
* @param {Object} [params = {}]
|
|
711
|
+
* @return {void}
|
|
712
|
+
*/
|
|
713
|
+
open({ url, name, call, params = {} }) {
|
|
714
|
+
if (!isString(url) || !isString(name) || !isFunction(call)) {
|
|
715
|
+
throw new TypeError('Invalid argument');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** @type {HTMLAnchorElement} */
|
|
719
|
+
const a = this.createElement(TagName.A, { attributes: { href: url } });
|
|
720
|
+
const urlObject = new URL(a.href);
|
|
721
|
+
Object.entries(params).forEach(([key, val]) => urlObject.searchParams.set(key, `${val}`));
|
|
722
|
+
const win = this.window.open(urlObject.toString(), name, this.#features());
|
|
723
|
+
this.window.addEventListener(
|
|
724
|
+
'message',
|
|
725
|
+
(event) => {
|
|
726
|
+
if (event.origin === urlObject.origin && event.source === win) {
|
|
727
|
+
call(event.data);
|
|
728
|
+
win.close();
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
false
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Returns window screen width
|
|
737
|
+
*
|
|
738
|
+
* @return {number}
|
|
739
|
+
*/
|
|
740
|
+
getWidth() {
|
|
741
|
+
return this.window.screen.width;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Returns window screen height
|
|
746
|
+
*
|
|
747
|
+
* @return {number}
|
|
748
|
+
*/
|
|
749
|
+
getHeight() {
|
|
750
|
+
return this.window.screen.height;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* @param {string} name
|
|
755
|
+
* @param {number} rows
|
|
756
|
+
* @param {number} cols
|
|
757
|
+
* @return {HTMLTableSectionElement}
|
|
758
|
+
*/
|
|
759
|
+
#createTableSection(name, rows, cols) {
|
|
760
|
+
const element = this.createElement(name);
|
|
761
|
+
|
|
762
|
+
for (let i = 0; i < rows; i++) {
|
|
763
|
+
this.insertLastChild(
|
|
764
|
+
name === TagName.THEAD ? this.createTableHeaderRow(cols) : this.createTableRow(cols),
|
|
765
|
+
element
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return element;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Converts options to window features
|
|
774
|
+
*
|
|
775
|
+
* @return {string}
|
|
776
|
+
*/
|
|
777
|
+
#features() {
|
|
778
|
+
const features = Object.assign(
|
|
779
|
+
{},
|
|
780
|
+
this.#browser,
|
|
781
|
+
{ height: `${this.getHeight()}`, width: `${this.getWidth()}` },
|
|
782
|
+
this.editor.config.base.browser
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
return Object.entries(features)
|
|
786
|
+
.filter(([, val]) => !!val)
|
|
787
|
+
.map(([key, val]) => `${key}=${val}`)
|
|
788
|
+
.join(',');
|
|
789
|
+
}
|
|
790
|
+
}
|