@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.
Files changed (126) hide show
  1. package/abbreviation/Abbreviation.js +32 -0
  2. package/abbreviation/AbbreviationDialog.js +21 -0
  3. package/audio/Audio.js +47 -0
  4. package/audio/AudioDialog.js +18 -0
  5. package/audio/AudioListener.js +50 -0
  6. package/base/AlignCommand.js +34 -0
  7. package/base/AlignableListener.js +45 -0
  8. package/base/Alignment.js +36 -0
  9. package/base/BarListener.js +95 -0
  10. package/base/Base.js +127 -0
  11. package/base/Command.js +139 -0
  12. package/base/CommandManager.js +60 -0
  13. package/base/ContentFilter.js +109 -0
  14. package/base/DeletableListener.js +36 -0
  15. package/base/DeleteCommand.js +18 -0
  16. package/base/Dialog.js +153 -0
  17. package/base/DialogManager.js +44 -0
  18. package/base/Dispatcher.js +88 -0
  19. package/base/Dom.js +790 -0
  20. package/base/EditableListener.js +82 -0
  21. package/base/Editor.js +448 -0
  22. package/base/Filter.js +35 -0
  23. package/base/FilterManager.js +44 -0
  24. package/base/FocusableListener.js +22 -0
  25. package/base/FocusbarListener.js +99 -0
  26. package/base/FormCreator.js +162 -0
  27. package/base/FormatbarListener.js +32 -0
  28. package/base/Key.js +258 -0
  29. package/base/Listener.js +51 -0
  30. package/base/NavigableListener.js +81 -0
  31. package/base/Plugin.js +176 -0
  32. package/base/PluginManager.js +51 -0
  33. package/base/SlotableListener.js +40 -0
  34. package/base/SortCommand.js +30 -0
  35. package/base/SortableListener.js +135 -0
  36. package/base/Sorting.js +36 -0
  37. package/base/Tag.js +113 -0
  38. package/base/TagGroup.js +183 -0
  39. package/base/TagListener.js +34 -0
  40. package/base/TagManager.js +61 -0
  41. package/base/TagName.js +470 -0
  42. package/base/ToolbarListener.js +11 -0
  43. package/base/util.js +59 -0
  44. package/block/Block.js +51 -0
  45. package/block/BlockDialog.js +11 -0
  46. package/block/BlockElement.js +21 -0
  47. package/block/BlockListener.js +60 -0
  48. package/blockquote/Blockquote.js +43 -0
  49. package/blockquote/BlockquoteFilter.js +22 -0
  50. package/blockquote/BlockquoteListener.js +34 -0
  51. package/bold/Bold.js +30 -0
  52. package/break/Break.js +33 -0
  53. package/break/BreakFilter.js +24 -0
  54. package/build/BuildEditor.js +97 -0
  55. package/build/editor.css +548 -0
  56. package/build/editor.woff2 +0 -0
  57. package/cite/Cite.js +30 -0
  58. package/code/Code.js +30 -0
  59. package/data/Data.js +32 -0
  60. package/data/DataDialog.js +13 -0
  61. package/definition/Definition.js +32 -0
  62. package/definition/DefinitionDialog.js +13 -0
  63. package/deletion/Deletion.js +30 -0
  64. package/details/Details.js +63 -0
  65. package/details/DetailsFilter.js +17 -0
  66. package/details/DetailsListener.js +102 -0
  67. package/division/Division.js +53 -0
  68. package/division/DivisionDialog.js +13 -0
  69. package/emphasis/Emphasis.js +30 -0
  70. package/figure/Figure.js +58 -0
  71. package/figure/FigureFilter.js +14 -0
  72. package/figure/FigureListener.js +23 -0
  73. package/heading/Heading.js +38 -0
  74. package/horizontalrule/HorizontalRule.js +37 -0
  75. package/i18n/I18n.js +26 -0
  76. package/i18n/de.js +167 -0
  77. package/iframe/Iframe.js +49 -0
  78. package/iframe/IframeDialog.js +20 -0
  79. package/iframe/IframeListener.js +48 -0
  80. package/image/Image.js +47 -0
  81. package/image/ImageDialog.js +23 -0
  82. package/image/ImageListener.js +47 -0
  83. package/insertion/Insertion.js +30 -0
  84. package/italic/Italic.js +30 -0
  85. package/keyboard/Keyboard.js +30 -0
  86. package/link/Link.js +34 -0
  87. package/link/LinkDialog.js +14 -0
  88. package/link/LinkListener.js +45 -0
  89. package/list/List.js +40 -0
  90. package/list/ListListener.js +91 -0
  91. package/mark/Mark.js +30 -0
  92. package/orderedlist/OrderedList.js +39 -0
  93. package/package.json +24 -0
  94. package/paragraph/Paragraph.js +42 -0
  95. package/paragraph/ParagraphListener.js +40 -0
  96. package/preformat/Preformat.js +43 -0
  97. package/preformat/PreformatFilter.js +22 -0
  98. package/preformat/PreformatListener.js +34 -0
  99. package/quote/Quote.js +30 -0
  100. package/sample/Sample.js +30 -0
  101. package/section/Section.js +55 -0
  102. package/section/SectionDialog.js +13 -0
  103. package/small/Small.js +30 -0
  104. package/strikethrough/Strikethrough.js +30 -0
  105. package/strong/Strong.js +30 -0
  106. package/subheading/Subheading.js +38 -0
  107. package/subscript/Subscript.js +30 -0
  108. package/superscript/Superscript.js +30 -0
  109. package/table/Table.js +113 -0
  110. package/table/TableCellListener.js +125 -0
  111. package/table/TableColumnAddCommand.js +19 -0
  112. package/table/TableColumnListener.js +34 -0
  113. package/table/TableCommand.js +23 -0
  114. package/table/TableDialog.js +14 -0
  115. package/table/TableFilter.js +35 -0
  116. package/table/TableListener.js +39 -0
  117. package/table/TableRowAddCommand.js +18 -0
  118. package/table/TableRowListener.js +34 -0
  119. package/time/Time.js +32 -0
  120. package/time/TimeDialog.js +13 -0
  121. package/underline/Underline.js +30 -0
  122. package/unorderedlist/UnorderedList.js +39 -0
  123. package/variable/Variable.js +30 -0
  124. package/video/Video.js +47 -0
  125. package/video/VideoDialog.js +20 -0
  126. 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
+ }