suneditor 3.0.0-beta.2 → 3.0.0-beta.20

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 (177) hide show
  1. package/CONTRIBUTING.md +186 -184
  2. package/LICENSE +21 -21
  3. package/README.md +157 -180
  4. package/dist/suneditor.min.css +1 -1
  5. package/dist/suneditor.min.js +1 -1
  6. package/package.json +126 -123
  7. package/src/assets/design/color.css +131 -121
  8. package/src/assets/design/index.css +3 -3
  9. package/src/assets/design/size.css +37 -35
  10. package/src/assets/design/typography.css +37 -37
  11. package/src/assets/icons/defaultIcons.js +247 -232
  12. package/src/assets/suneditor-contents.css +779 -778
  13. package/src/assets/suneditor.css +43 -35
  14. package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
  15. package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
  16. package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
  17. package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
  18. package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
  19. package/src/core/base/eventManager.js +1550 -1484
  20. package/src/core/base/history.js +355 -355
  21. package/src/core/class/char.js +163 -162
  22. package/src/core/class/component.js +856 -842
  23. package/src/core/class/format.js +3433 -3422
  24. package/src/core/class/html.js +1927 -1890
  25. package/src/core/class/menu.js +357 -346
  26. package/src/core/class/nodeTransform.js +424 -424
  27. package/src/core/class/offset.js +858 -891
  28. package/src/core/class/selection.js +710 -620
  29. package/src/core/class/shortcuts.js +98 -98
  30. package/src/core/class/toolbar.js +438 -430
  31. package/src/core/class/ui.js +424 -422
  32. package/src/core/class/viewer.js +750 -750
  33. package/src/core/editor.js +1810 -1708
  34. package/src/core/section/actives.js +268 -241
  35. package/src/core/section/constructor.js +1348 -1661
  36. package/src/core/section/context.js +102 -102
  37. package/src/core/section/documentType.js +582 -561
  38. package/src/core/section/options.js +367 -0
  39. package/src/core/util/instanceCheck.js +59 -0
  40. package/src/editorInjector/_classes.js +36 -36
  41. package/src/editorInjector/_core.js +92 -92
  42. package/src/editorInjector/index.js +75 -75
  43. package/src/events.js +634 -622
  44. package/src/helper/clipboard.js +59 -59
  45. package/src/helper/converter.js +586 -564
  46. package/src/helper/dom/domCheck.js +304 -304
  47. package/src/helper/dom/domQuery.js +677 -669
  48. package/src/helper/dom/domUtils.js +618 -557
  49. package/src/helper/dom/index.js +12 -12
  50. package/src/helper/env.js +249 -240
  51. package/src/helper/index.js +25 -25
  52. package/src/helper/keyCodeMap.js +183 -183
  53. package/src/helper/numbers.js +72 -72
  54. package/src/helper/unicode.js +47 -47
  55. package/src/langs/ckb.js +231 -231
  56. package/src/langs/cs.js +231 -231
  57. package/src/langs/da.js +231 -231
  58. package/src/langs/de.js +231 -231
  59. package/src/langs/en.js +230 -230
  60. package/src/langs/es.js +231 -231
  61. package/src/langs/fa.js +231 -231
  62. package/src/langs/fr.js +231 -231
  63. package/src/langs/he.js +231 -231
  64. package/src/langs/hu.js +230 -230
  65. package/src/langs/index.js +28 -28
  66. package/src/langs/it.js +231 -231
  67. package/src/langs/ja.js +230 -230
  68. package/src/langs/km.js +230 -230
  69. package/src/langs/ko.js +230 -230
  70. package/src/langs/lv.js +231 -231
  71. package/src/langs/nl.js +231 -231
  72. package/src/langs/pl.js +231 -231
  73. package/src/langs/pt_br.js +231 -231
  74. package/src/langs/ro.js +231 -231
  75. package/src/langs/ru.js +231 -231
  76. package/src/langs/se.js +231 -231
  77. package/src/langs/tr.js +231 -231
  78. package/src/langs/uk.js +231 -231
  79. package/src/langs/ur.js +231 -231
  80. package/src/langs/zh_cn.js +231 -231
  81. package/src/modules/ApiManager.js +191 -191
  82. package/src/modules/Browser.js +669 -667
  83. package/src/modules/ColorPicker.js +364 -362
  84. package/src/modules/Controller.js +474 -454
  85. package/src/modules/Figure.js +1620 -1617
  86. package/src/modules/FileManager.js +359 -359
  87. package/src/modules/HueSlider.js +577 -565
  88. package/src/modules/Modal.js +346 -346
  89. package/src/modules/ModalAnchorEditor.js +643 -643
  90. package/src/modules/SelectMenu.js +549 -549
  91. package/src/modules/_DragHandle.js +17 -17
  92. package/src/modules/index.js +14 -14
  93. package/src/plugins/browser/audioGallery.js +83 -83
  94. package/src/plugins/browser/fileBrowser.js +103 -103
  95. package/src/plugins/browser/fileGallery.js +83 -83
  96. package/src/plugins/browser/imageGallery.js +81 -81
  97. package/src/plugins/browser/videoGallery.js +103 -103
  98. package/src/plugins/command/blockquote.js +61 -60
  99. package/src/plugins/command/exportPDF.js +134 -134
  100. package/src/plugins/command/fileUpload.js +456 -456
  101. package/src/plugins/command/list_bulleted.js +149 -148
  102. package/src/plugins/command/list_numbered.js +152 -151
  103. package/src/plugins/dropdown/align.js +157 -155
  104. package/src/plugins/dropdown/backgroundColor.js +108 -104
  105. package/src/plugins/dropdown/font.js +141 -137
  106. package/src/plugins/dropdown/fontColor.js +109 -105
  107. package/src/plugins/dropdown/formatBlock.js +170 -178
  108. package/src/plugins/dropdown/hr.js +152 -152
  109. package/src/plugins/dropdown/layout.js +83 -83
  110. package/src/plugins/dropdown/lineHeight.js +131 -130
  111. package/src/plugins/dropdown/list.js +123 -122
  112. package/src/plugins/dropdown/paragraphStyle.js +138 -138
  113. package/src/plugins/dropdown/table.js +4110 -4000
  114. package/src/plugins/dropdown/template.js +83 -83
  115. package/src/plugins/dropdown/textStyle.js +149 -149
  116. package/src/plugins/field/mention.js +242 -242
  117. package/src/plugins/index.js +120 -120
  118. package/src/plugins/input/fontSize.js +414 -410
  119. package/src/plugins/input/pageNavigator.js +71 -70
  120. package/src/plugins/modal/audio.js +677 -677
  121. package/src/plugins/modal/drawing.js +537 -531
  122. package/src/plugins/modal/embed.js +886 -886
  123. package/src/plugins/modal/image.js +1377 -1376
  124. package/src/plugins/modal/link.js +248 -240
  125. package/src/plugins/modal/math.js +563 -563
  126. package/src/plugins/modal/video.js +1226 -1226
  127. package/src/plugins/popup/anchor.js +224 -222
  128. package/src/suneditor.js +114 -107
  129. package/src/themes/dark.css +132 -122
  130. package/src/typedef.js +132 -130
  131. package/types/assets/icons/defaultIcons.d.ts +8 -0
  132. package/types/core/base/eventManager.d.ts +29 -4
  133. package/types/core/class/char.d.ts +2 -1
  134. package/types/core/class/component.d.ts +1 -2
  135. package/types/core/class/format.d.ts +8 -1
  136. package/types/core/class/html.d.ts +8 -0
  137. package/types/core/class/menu.d.ts +8 -0
  138. package/types/core/class/offset.d.ts +24 -26
  139. package/types/core/class/selection.d.ts +2 -0
  140. package/types/core/class/toolbar.d.ts +6 -0
  141. package/types/core/class/ui.d.ts +1 -1
  142. package/types/core/editor.d.ts +34 -12
  143. package/types/core/section/constructor.d.ts +5 -638
  144. package/types/core/section/documentType.d.ts +12 -2
  145. package/types/core/section/options.d.ts +740 -0
  146. package/types/core/util/instanceCheck.d.ts +50 -0
  147. package/types/editorInjector/_core.d.ts +5 -5
  148. package/types/editorInjector/index.d.ts +2 -2
  149. package/types/events.d.ts +2 -0
  150. package/types/helper/converter.d.ts +9 -0
  151. package/types/helper/dom/domQuery.d.ts +5 -5
  152. package/types/helper/dom/domUtils.d.ts +8 -0
  153. package/types/helper/env.d.ts +6 -1
  154. package/types/helper/index.d.ts +4 -1
  155. package/types/index.d.ts +122 -120
  156. package/types/langs/_Lang.d.ts +194 -194
  157. package/types/modules/ColorPicker.d.ts +5 -1
  158. package/types/modules/Controller.d.ts +8 -4
  159. package/types/modules/Figure.d.ts +2 -1
  160. package/types/modules/HueSlider.d.ts +4 -1
  161. package/types/modules/SelectMenu.d.ts +1 -1
  162. package/types/plugins/command/blockquote.d.ts +1 -0
  163. package/types/plugins/command/list_bulleted.d.ts +1 -0
  164. package/types/plugins/command/list_numbered.d.ts +1 -0
  165. package/types/plugins/dropdown/align.d.ts +1 -0
  166. package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
  167. package/types/plugins/dropdown/font.d.ts +1 -0
  168. package/types/plugins/dropdown/fontColor.d.ts +1 -0
  169. package/types/plugins/dropdown/formatBlock.d.ts +3 -2
  170. package/types/plugins/dropdown/lineHeight.d.ts +1 -0
  171. package/types/plugins/dropdown/list.d.ts +1 -0
  172. package/types/plugins/dropdown/table.d.ts +6 -0
  173. package/types/plugins/input/fontSize.d.ts +1 -0
  174. package/types/plugins/modal/drawing.d.ts +4 -0
  175. package/types/plugins/modal/link.d.ts +32 -15
  176. package/types/suneditor.d.ts +13 -9
  177. package/types/typedef.d.ts +8 -0
@@ -1,669 +1,677 @@
1
- /**
2
- * @fileoverview Implements Helper for querying the DOM.
3
- */
4
-
5
- import { zeroWidthRegExp } from '../unicode';
6
- import domUtils from './domUtils';
7
- import domCheck from './domCheck';
8
-
9
- /**
10
- * @description Returns the index compared to other sibling nodes.
11
- * @param {Node} node The Node to find index
12
- * @returns {number}
13
- */
14
- export function getPositionIndex(node) {
15
- let idx = 0;
16
- while ((node = node.previousSibling)) {
17
- idx += 1;
18
- }
19
- return idx;
20
- }
21
-
22
- /**
23
- * @description Returns the position of the "node" in the "parentNode" in a numerical array.
24
- * - e.g.) <p><span>aa</span><span>bb</span></p> : getNodePath(node: "bb", parentNode: "<P>") -> [1, 0]
25
- * @param {Node} node The Node to find position path
26
- * @param {?Node} parentNode Parent node. If null, wysiwyg div area
27
- * @param {?{s: number, e: number}=} _newOffsets If you send an object of the form "{s: 0, e: 0}", the text nodes that are attached together are merged into one, centered on the "node" argument.
28
- * "_newOffsets.s" stores the length of the combined characters after "node" and "_newOffsets.e" stores the length of the combined characters before "node".
29
- * Do not use unless absolutely necessary.
30
- * @returns {Array<number>}
31
- */
32
- export function getNodePath(node, parentNode, _newOffsets) {
33
- const path = [];
34
- let finds = true;
35
-
36
- getParentElement(node, (el) => {
37
- if (el === parentNode) finds = false;
38
- if (finds && !domCheck.isWysiwygFrame(el)) {
39
- // merge text nodes
40
- if (_newOffsets && el.nodeType === 3) {
41
- let temp = null,
42
- tempText = null;
43
- _newOffsets.s = _newOffsets.e = 0;
44
-
45
- let previous = el.previousSibling;
46
- while (previous?.nodeType === 3) {
47
- tempText = previous.textContent.replace(zeroWidthRegExp, '');
48
- _newOffsets.s += tempText.length;
49
- el.textContent = tempText + el.textContent;
50
- temp = previous;
51
- previous = previous.previousSibling;
52
- domUtils.removeItem(temp);
53
- }
54
-
55
- let next = el.nextSibling;
56
- while (next?.nodeType === 3) {
57
- tempText = next.textContent.replace(zeroWidthRegExp, '');
58
- _newOffsets.e += tempText.length;
59
- el.textContent += tempText;
60
- temp = next;
61
- next = next.nextSibling;
62
- domUtils.removeItem(temp);
63
- }
64
- }
65
-
66
- // index push
67
- path.push(el);
68
- }
69
- return false;
70
- });
71
-
72
- return path.map(getPositionIndex).reverse();
73
- }
74
-
75
- /**
76
- * @template {Node} T
77
- * @description Returns the node in the location of the path array obtained from "helper.dom.getNodePath".
78
- * @param {Array<number>} offsets Position array, array obtained from "helper.dom.getNodePath"
79
- * @param {Node} parentNode Base parent element
80
- * @returns {T}
81
- */
82
- export function getNodeFromPath(offsets, parentNode) {
83
- let current = parentNode;
84
- let nodes;
85
-
86
- for (let i = 0, len = offsets.length; i < len; i++) {
87
- nodes = current.childNodes;
88
- if (nodes.length === 0) break;
89
- if (nodes.length <= offsets[i]) {
90
- current = nodes[nodes.length - 1];
91
- } else {
92
- current = nodes[offsets[i]];
93
- }
94
- }
95
-
96
- return /** @type {T} */ (current);
97
- }
98
-
99
- /**
100
- * @template {HTMLElement} T
101
- * @description Get all "children" of the argument value element (Without text nodes)
102
- * @param {Node} element element to get child node
103
- * @param {?(current: *) => boolean} validation Conditional function
104
- * @returns {Array<T>}
105
- */
106
- export function getListChildren(element, validation) {
107
- /** @type {Array<T>} */
108
- const children = [];
109
- if (!element) return children;
110
-
111
- const el = /** @type {Element} */ (element);
112
- if (!el.children || el.children.length === 0) return children;
113
-
114
- validation =
115
- validation ||
116
- function () {
117
- return true;
118
- };
119
-
120
- (function recursionFunc(current) {
121
- if (el !== current && validation(current)) {
122
- children.push(/** @type {T} */ (current));
123
- }
124
-
125
- if (current.children) {
126
- for (let i = 0, len = current.children.length; i < len; i++) {
127
- recursionFunc(current.children[i]);
128
- }
129
- }
130
- })(el);
131
-
132
- return /** @type {Array<T>} */ (children);
133
- }
134
-
135
- /**
136
- * @template {Node} T
137
- * @description Get all "childNodes" of the argument value element (Include text nodes)
138
- * @param {Node} element element to get child node
139
- * @param {?(current: *) => boolean} validation Conditional function
140
- * @returns {Array<T>}
141
- */
142
- export function getListChildNodes(element, validation) {
143
- const children = [];
144
- if (!element || element.childNodes.length === 0) return children;
145
-
146
- validation =
147
- validation ||
148
- function () {
149
- return true;
150
- };
151
-
152
- (function recursionFunc(current) {
153
- if (element !== current && validation(current)) {
154
- children.push(current);
155
- }
156
-
157
- for (let i = 0, len = current.childNodes.length; i < len; i++) {
158
- recursionFunc(current.childNodes[i]);
159
- }
160
- })(element);
161
-
162
- return /** @type {Array<T>} */ (children);
163
- }
164
-
165
- /**
166
- * @description Returns the number of parents nodes.
167
- * - "0" when the parent node is the WYSIWYG area.
168
- * - '-1' when the element argument is the WYSIWYG area.
169
- * @param {Node} node The element to check
170
- * @returns {number}
171
- */
172
- export function getNodeDepth(node) {
173
- if (!node || domCheck.isWysiwygFrame(node)) return -1;
174
-
175
- let depth = 0;
176
- node = node.parentNode;
177
-
178
- while (node && !domCheck.isWysiwygFrame(node)) {
179
- depth += 1;
180
- node = node.parentNode;
181
- }
182
-
183
- return depth;
184
- }
185
-
186
- /**
187
- * @description Sort a node array by depth of element.
188
- * @param {Array<Node>} array Node array
189
- * @param {boolean} des true: descending order / false: ascending order
190
- */
191
- export function sortNodeByDepth(array, des) {
192
- const t = !des ? -1 : 1;
193
- const f = t * -1;
194
-
195
- array.sort(function (a, b) {
196
- if (!domCheck.isListCell(a) || !domCheck.isListCell(b)) return 0;
197
- const a_i = getNodeDepth(a);
198
- const b_i = getNodeDepth(b);
199
- return a_i > b_i ? t : a_i < b_i ? f : 0;
200
- });
201
- }
202
-
203
- /**
204
- * @description Compares two elements to find a common ancestor, and returns the order of the two elements.
205
- * @param {Node} a Node to compare.
206
- * @param {Node} b Node to compare.
207
- * @returns {{ancestor: HTMLElement|null, a: Node, b: Node, result: number}} { ancesstor, a, b, result: (a > b ? 1 : a < b ? -1 : 0) };
208
- */
209
- export function compareElements(a, b) {
210
- let aNode = a,
211
- bNode = b;
212
- while (aNode && bNode && aNode.parentElement !== bNode.parentElement) {
213
- aNode = aNode.parentElement;
214
- bNode = bNode.parentElement;
215
- }
216
-
217
- if (!aNode || !bNode)
218
- return {
219
- ancestor: null,
220
- a: a,
221
- b: b,
222
- result: 0
223
- };
224
-
225
- const children = aNode.parentNode.childNodes;
226
- const aIndex = domUtils.getArrayIndex(children, aNode);
227
- const bIndex = domUtils.getArrayIndex(children, bNode);
228
-
229
- return {
230
- ancestor: aNode.parentElement,
231
- a: aNode,
232
- b: bNode,
233
- result: aIndex > bIndex ? 1 : aIndex < bIndex ? -1 : 0
234
- };
235
- }
236
-
237
- /**
238
- * @template {HTMLElement} T
239
- * @description Get the parent element of the argument value.
240
- * - A tag that satisfies the query condition is imported.
241
- * @param {Node} element Reference element
242
- * @param {string|((current: *) => boolean)|Node} query Query String (nodeName, .className, #ID, :name) or validation function.
243
- * - Not use it like jquery.
244
- * - Only one condition can be entered at a time.
245
- * @param {?number=} depth Number of parent levels to depth.
246
- * @returns {T|null} Not found: null
247
- */
248
- export function getParentElement(element, query, depth) {
249
- let valid;
250
-
251
- if (typeof query === 'function') {
252
- valid = query;
253
- } else if (typeof query === 'object') {
254
- /** @param {Node} current */
255
- valid = (current) => current === query;
256
- } else {
257
- let attr;
258
- if (/^\./.test(query)) {
259
- attr = 'className';
260
- query = '(\\s|^)' + query.split('.')[1] + '(\\s|$)';
261
- } else if (/^#/.test(query)) {
262
- attr = 'id';
263
- query = '^' + query.split('#')[1] + '$';
264
- } else if (/^:/.test(query)) {
265
- attr = 'name';
266
- query = '^' + query.split(':')[1] + '$';
267
- } else {
268
- attr = 'nodeName';
269
- query = '^' + query + '$';
270
- }
271
-
272
- const regExp = new RegExp(query, 'i');
273
- /** @param {Node} el */
274
- valid = (el) => regExp.test(el[attr]);
275
- }
276
-
277
- if (!depth) depth = Infinity;
278
- let index = 0;
279
- while (element && !valid(element)) {
280
- if (index >= depth || domCheck.isWysiwygFrame(element)) {
281
- return null;
282
- }
283
- element = element.parentElement;
284
- index++;
285
- }
286
-
287
- return /** @type {T} */ (element);
288
- }
289
-
290
- /**
291
- * @template {HTMLElement} T
292
- * @description Gets all ancestors of the argument value.
293
- * - Get all tags that satisfy the query condition.
294
- * @param {Node} element Reference element
295
- * @param {string|((current: *) => boolean)|Node} query Query String (nodeName, .className, #ID, :name) or validation function.
296
- * Not use it like jquery.
297
- * Only one condition can be entered at a time.
298
- * @param {?number=} depth Number of parent levels to depth.
299
- * @returns {Array<T>} Returned in an array in order.
300
- */
301
- export function getParentElements(element, query, depth) {
302
- let valid;
303
-
304
- if (typeof query === 'function') {
305
- valid = query;
306
- } else if (typeof query === 'object') {
307
- /** @param {Node} current */
308
- valid = (current) => current === query;
309
- } else {
310
- let attr;
311
- if (/^\./.test(query)) {
312
- attr = 'className';
313
- query = '(\\s|^)' + query.split('.')[1] + '(\\s|$)';
314
- } else if (/^#/.test(query)) {
315
- attr = 'id';
316
- query = '^' + query.split('#')[1] + '$';
317
- } else if (/^:/.test(query)) {
318
- attr = 'name';
319
- query = '^' + query.split(':')[1] + '$';
320
- } else {
321
- attr = 'nodeName';
322
- query = '^' + query + '$';
323
- }
324
-
325
- const regExp = new RegExp(query, 'i');
326
- /** @param {Node} el */
327
- valid = (el) => regExp.test(el[attr]);
328
- }
329
-
330
- const elementList = [];
331
- if (!depth) depth = Infinity;
332
- let index = 0;
333
- while (index <= depth && element && !domCheck.isWysiwygFrame(element)) {
334
- if (valid(element)) {
335
- elementList.push(element);
336
- }
337
- element = element.parentElement;
338
- index++;
339
- }
340
-
341
- return /** @type {Array<T>} */ (elementList);
342
- }
343
-
344
- /**
345
- * @template {HTMLElement} T
346
- * @description Gets the element with "data-command" attribute among the parent elements.
347
- * @param {Node} target Target element
348
- * @returns {T|null}
349
- */
350
- export function getCommandTarget(target) {
351
- let n = /** @type {HTMLElement} */ (target);
352
- while (n && !/^(UL)$/i.test(n.nodeName) && !domUtils.hasClass(n, 'sun-editor')) {
353
- if (n.hasAttribute('data-command')) return /** @type {T} */ (n);
354
- n = n.parentElement;
355
- }
356
-
357
- return null;
358
- }
359
-
360
- /**
361
- * @template {HTMLElement} T
362
- * @description Get the event.target element.
363
- * @param {Event} event Event object
364
- * @returns {T|null}
365
- */
366
- export function getEventTarget(event) {
367
- return /** @type {T} */ (event.target);
368
- }
369
-
370
- /**
371
- * @template {Node} T
372
- * @description Get the child element of the argument value.
373
- * - A tag that satisfies the query condition is imported.
374
- * @param {Node} node Reference element
375
- * @param {string|((current: *) => boolean)|Node} query Query String (nodeName, .className, #ID, :name) or validation function.
376
- * @param {boolean} last If true returns the last node among the found child nodes. (default: first node)
377
- * Not use it like jquery.
378
- * Only one condition can be entered at a time.
379
- * @returns {T|null} Not found: null
380
- */
381
- export function getEdgeChild(node, query, last) {
382
- let valid;
383
-
384
- if (typeof query === 'function') {
385
- valid = query;
386
- } else if (typeof query === 'object') {
387
- valid = function (current) {
388
- return current === query;
389
- };
390
- } else {
391
- let attr;
392
- if (/^\./.test(query)) {
393
- attr = 'className';
394
- query = '(\\s|^)' + query.split('.')[1] + '(\\s|$)';
395
- } else if (/^#/.test(query)) {
396
- attr = 'id';
397
- query = '^' + query.split('#')[1] + '$';
398
- } else if (/^:/.test(query)) {
399
- attr = 'name';
400
- query = '^' + query.split(':')[1] + '$';
401
- } else {
402
- attr = 'nodeName';
403
- query = '^' + (query === 'text' ? '#' + query : query) + '$';
404
- }
405
-
406
- const regExp = new RegExp(query, 'i');
407
- valid = function (el) {
408
- return regExp.test(el[attr]);
409
- };
410
- }
411
-
412
- const childList = getListChildNodes(node, (current) => valid(current));
413
-
414
- return /** @type {T} */ (childList[last ? childList.length - 1 : 0]);
415
- }
416
-
417
- /**
418
- * @description Get edge child nodes of the argument value.
419
- * - 1. The first node of all the child nodes of the "first" element is returned.
420
- * - 2. The last node of all the child nodes of the "last" element is returned.
421
- * - 3. When there is no "last" element, the first and last nodes of all the children of the "first" element are returned.
422
- * @param {Node} first First element
423
- * @param {Node|null} last Last element
424
- * @returns {{sc: Node, ec: Node}} { sc: "first", ec: "last" }
425
- */
426
- export function getEdgeChildNodes(first, last) {
427
- if (!first) return;
428
- if (!last) last = first;
429
-
430
- while (first && first.nodeType === 1 && first.childNodes.length > 0 && !domCheck.isBreak(first)) first = first.firstChild;
431
- while (last && last.nodeType === 1 && last.childNodes.length > 0 && !domCheck.isBreak(last)) last = last.lastChild;
432
-
433
- return {
434
- sc: first,
435
- ec: last || first
436
- };
437
- }
438
-
439
- /**
440
- * @template {Node} T
441
- * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child
442
- * @param {Node} node Reference element
443
- * @param {?Node=} ceiling Highest boundary allowed
444
- * @returns {T|null} Not found: null
445
- */
446
- export function getPreviousDeepestNode(node, ceiling) {
447
- let previousNode = node.previousSibling;
448
- if (!previousNode) {
449
- for (let parentNode = node.parentNode; parentNode; parentNode = parentNode.parentNode) {
450
- if (parentNode === ceiling) return null;
451
- if (parentNode.previousSibling) {
452
- previousNode = parentNode.previousSibling;
453
- break;
454
- }
455
- }
456
- if (!previousNode) return null;
457
- }
458
-
459
- if (domCheck.isNonEditable(previousNode)) return /** @type {T} */ (/** @type {unknown} */ (previousNode));
460
-
461
- while (previousNode.lastChild) previousNode = previousNode.lastChild;
462
-
463
- return /** @type {T} */ (/** @type {unknown} */ (previousNode));
464
- }
465
-
466
- /**
467
- * @template {Node} T
468
- * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child
469
- * @param {Node} node Reference element
470
- * @param {?Node=} ceiling Highest boundary allowed
471
- * @returns {T|null} Not found: null
472
- */
473
- export function getNextDeepestNode(node, ceiling) {
474
- let nextNode = node.nextSibling;
475
- if (!nextNode) {
476
- for (let parentNode = node.parentNode; parentNode; parentNode = parentNode.parentNode) {
477
- if (parentNode === ceiling) return null;
478
- if (parentNode.nextSibling) {
479
- nextNode = parentNode.nextSibling;
480
- break;
481
- }
482
- }
483
- if (!nextNode) return null;
484
- }
485
-
486
- if (domCheck.isNonEditable(nextNode)) return /** @type {T} */ (/** @type {unknown} */ (nextNode));
487
-
488
- while (nextNode.firstChild) nextNode = nextNode.firstChild;
489
-
490
- return /** @type {T} */ (/** @type {unknown} */ (nextNode));
491
- }
492
-
493
- /**
494
- * @description Find the index of the text node in the line element.
495
- * @param {Node} line Line element (p, div, etc.)
496
- * @param {Node} offsetContainer Base node to start searching
497
- * @param {number} offset Base offset to start searching
498
- * @param {?(current: *) => boolean=} validate Validation function
499
- * @returns {number}
500
- */
501
- export function findTextIndexOnLine(line, offsetContainer, offset, validate) {
502
- if (!line) return 0;
503
- if (!validate) validate = () => true;
504
-
505
- let index = 0;
506
- let found = false;
507
-
508
- (function recursionFunc(node) {
509
- if (found || node.nodeType === 8) return;
510
- if (validate(node)) return; // component.is
511
-
512
- if (node.nodeType === 3) {
513
- if (node === offsetContainer) {
514
- index += offset;
515
- found = true;
516
- return;
517
- }
518
- index += node.textContent.length;
519
- } else if (node.nodeType === 1) {
520
- const childNodes = node.childNodes;
521
- for (let i = 0, len = childNodes.length; i < len; i++) {
522
- recursionFunc(childNodes[i]);
523
- if (found) return;
524
- }
525
- }
526
- })(line);
527
-
528
- return index;
529
- }
530
-
531
- /**
532
- * @description Find the end index of a sequence of at least minTabSize consecutive non-breaking spaces or spaces
533
- * - which are interpreted as a tab key, occurring after a given base index in a text string.
534
- * @param {Node} line Line element (p, div, etc.)
535
- * @param {number} baseIndex Base index to start searching
536
- * @param {number} minTabSize Minimum number of consecutive spaces to consider as a tab
537
- * @returns {number} The adjusted index within the line element accounting for non-space characters
538
- */
539
- export function findTabEndIndex(line, baseIndex, minTabSize) {
540
- if (!line) return 0;
541
- const innerText = line.textContent;
542
- const regex = new RegExp(`((\\u00A0|\\s){${minTabSize},})`, 'g');
543
- let match;
544
-
545
- regex.lastIndex = baseIndex;
546
-
547
- while ((match = regex.exec(innerText)) !== null) {
548
- if (match.index >= baseIndex) {
549
- const spaceEndIndex = match.index + match[0].length - 1;
550
- const precedingText = innerText.slice(0, spaceEndIndex + 1);
551
- const nonSpaceCharCount = (precedingText.match(/[^\u00A0\s]/g) || []).length;
552
- return spaceEndIndex + nonSpaceCharCount + minTabSize;
553
- }
554
- }
555
-
556
- return 0;
557
- }
558
-
559
- /**
560
- * @description Finds the table cell that appears visually at the bottom-right position,
561
- * considering both rowSpan and colSpan, even if smaller cells are placed after large merged cells.
562
- *
563
- * @param {HTMLTableCellElement[]} cells
564
- * @returns {HTMLTableCellElement|null}
565
- */
566
- export function findVisualLastCell(cells) {
567
- if (!cells || cells.length === 0) return null;
568
-
569
- /**
570
- * @description visibility col index
571
- * @type {Object<number, boolean[]>}
572
- */
573
- const occupied = {};
574
-
575
- let target = null;
576
- let maxRowEnd = -1;
577
- let maxColEnd = -1;
578
-
579
- for (const cell of cells) {
580
- const row = /** @type {HTMLTableRowElement} */ (cell.parentElement);
581
- const rowIndex = row.rowIndex;
582
- const rowSpan = cell.rowSpan || 1;
583
- const colSpan = cell.colSpan || 1;
584
-
585
- // 현재 행에서 visual column index 찾기
586
- if (!occupied[rowIndex]) occupied[rowIndex] = [];
587
-
588
- let colIndex = 0;
589
- const rowOcc = occupied[rowIndex];
590
- while (rowOcc[colIndex]) colIndex++;
591
-
592
- for (let i = 0; i < colSpan; i++) {
593
- rowOcc[colIndex + i] = true;
594
- }
595
-
596
- for (let r = 1; r < rowSpan; r++) {
597
- const nextRow = rowIndex + r;
598
- if (!occupied[nextRow]) occupied[nextRow] = [];
599
- for (let i = 0; i < colSpan; i++) {
600
- occupied[nextRow][colIndex + i] = true;
601
- }
602
- }
603
-
604
- const visualRowEnd = rowIndex + rowSpan - 1;
605
- const visualColEnd = colIndex + colSpan - 1;
606
-
607
- // right-bottom
608
- if (visualRowEnd > maxRowEnd || (visualRowEnd === maxRowEnd && visualColEnd > maxColEnd)) {
609
- maxRowEnd = visualRowEnd;
610
- maxColEnd = visualColEnd;
611
- target = cell;
612
- }
613
- }
614
-
615
- return target;
616
- }
617
-
618
- /**
619
- * @description Get nearest scrollable parent
620
- * @param {Node} element Element
621
- * @returns {HTMLElement|null}
622
- */
623
- export function getScrollParent(element) {
624
- if (!element || /^(body|html)$/i.test(element.nodeName)) {
625
- return null;
626
- }
627
-
628
- const el = /** @type {HTMLElement} */ (element);
629
- if (el.scrollHeight > el.clientHeight) {
630
- return el;
631
- } else {
632
- return getScrollParent(el.parentNode);
633
- }
634
- }
635
-
636
- /**
637
- * @description Get the argument iframe's document object if use the "iframe" or "fullPage" options
638
- * @param {HTMLIFrameElement} iframe Iframe element (this.editor.frameContext.get('wysiwygFrame'))
639
- * @returns {Document}
640
- */
641
- export function getIframeDocument(iframe) {
642
- return iframe.contentWindow?.document || iframe.contentDocument;
643
- }
644
-
645
- const query = {
646
- getPositionIndex,
647
- getNodePath,
648
- getNodeFromPath,
649
- getListChildren,
650
- getListChildNodes,
651
- getNodeDepth,
652
- sortNodeByDepth,
653
- compareElements,
654
- getParentElement,
655
- getParentElements,
656
- getCommandTarget,
657
- getEventTarget,
658
- getEdgeChild,
659
- getEdgeChildNodes,
660
- getPreviousDeepestNode,
661
- getNextDeepestNode,
662
- findTextIndexOnLine,
663
- findTabEndIndex,
664
- findVisualLastCell,
665
- getScrollParent,
666
- getIframeDocument
667
- };
668
-
669
- export default query;
1
+ /**
2
+ * @fileoverview Implements Helper for querying the DOM.
3
+ */
4
+
5
+ import { _w } from '../env';
6
+ import { zeroWidthRegExp } from '../unicode';
7
+ import domUtils from './domUtils';
8
+ import domCheck from './domCheck';
9
+
10
+ /**
11
+ * @description Returns the index compared to other sibling nodes.
12
+ * @param {Node} node The Node to find index
13
+ * @returns {number}
14
+ */
15
+ export function getPositionIndex(node) {
16
+ let idx = 0;
17
+ while ((node = node.previousSibling)) {
18
+ idx += 1;
19
+ }
20
+ return idx;
21
+ }
22
+
23
+ /**
24
+ * @description Returns the position of the "node" in the "parentNode" in a numerical array.
25
+ * - e.g.) <p><span>aa</span><span>bb</span></p> : getNodePath(node: "bb", parentNode: "<P>") -> [1, 0]
26
+ * @param {Node} node The Node to find position path
27
+ * @param {?Node} parentNode Parent node. If null, wysiwyg div area
28
+ * @param {?{s: number, e: number}=} _newOffsets If you send an object of the form "{s: 0, e: 0}", the text nodes that are attached together are merged into one, centered on the "node" argument.
29
+ * "_newOffsets.s" stores the length of the combined characters after "node" and "_newOffsets.e" stores the length of the combined characters before "node".
30
+ * Do not use unless absolutely necessary.
31
+ * @returns {Array<number>}
32
+ */
33
+ export function getNodePath(node, parentNode, _newOffsets) {
34
+ const path = [];
35
+ let finds = true;
36
+
37
+ getParentElement(node, (el) => {
38
+ if (el === parentNode) finds = false;
39
+ if (finds && !domCheck.isWysiwygFrame(el)) {
40
+ // merge text nodes
41
+ if (_newOffsets && el.nodeType === 3) {
42
+ let temp = null,
43
+ tempText = null;
44
+ _newOffsets.s = _newOffsets.e = 0;
45
+
46
+ let previous = el.previousSibling;
47
+ while (previous?.nodeType === 3) {
48
+ tempText = previous.textContent.replace(zeroWidthRegExp, '');
49
+ _newOffsets.s += tempText.length;
50
+ el.textContent = tempText + el.textContent;
51
+ temp = previous;
52
+ previous = previous.previousSibling;
53
+ domUtils.removeItem(temp);
54
+ }
55
+
56
+ let next = el.nextSibling;
57
+ while (next?.nodeType === 3) {
58
+ tempText = next.textContent.replace(zeroWidthRegExp, '');
59
+ _newOffsets.e += tempText.length;
60
+ el.textContent += tempText;
61
+ temp = next;
62
+ next = next.nextSibling;
63
+ domUtils.removeItem(temp);
64
+ }
65
+ }
66
+
67
+ // index push
68
+ path.push(el);
69
+ }
70
+ return false;
71
+ });
72
+
73
+ return path.map(getPositionIndex).reverse();
74
+ }
75
+
76
+ /**
77
+ * @template {Node} T
78
+ * @description Returns the node in the location of the path array obtained from "helper.dom.getNodePath".
79
+ * @param {Array<number>} offsets Position array, array obtained from "helper.dom.getNodePath"
80
+ * @param {Node} parentNode Base parent element
81
+ * @returns {T}
82
+ */
83
+ export function getNodeFromPath(offsets, parentNode) {
84
+ let current = parentNode;
85
+ let nodes;
86
+
87
+ for (let i = 0, len = offsets.length; i < len; i++) {
88
+ nodes = current.childNodes;
89
+ if (nodes.length === 0) break;
90
+ if (nodes.length <= offsets[i]) {
91
+ current = nodes[nodes.length - 1];
92
+ } else {
93
+ current = nodes[offsets[i]];
94
+ }
95
+ }
96
+
97
+ return /** @type {T} */ (current);
98
+ }
99
+
100
+ /**
101
+ * @template {HTMLElement} T
102
+ * @description Get all "children" of the argument value element (Without text nodes)
103
+ * @param {Node} element element to get child node
104
+ * @param {?(current: *) => boolean} validation Conditional function
105
+ * @returns {Array<T>}
106
+ */
107
+ export function getListChildren(element, validation) {
108
+ /** @type {Array<T>} */
109
+ const children = [];
110
+ if (!element) return children;
111
+
112
+ const el = /** @type {Element} */ (element);
113
+ if (!el.children || el.children.length === 0) return children;
114
+
115
+ validation =
116
+ validation ||
117
+ function () {
118
+ return true;
119
+ };
120
+
121
+ (function recursionFunc(current) {
122
+ if (el !== current && validation(current)) {
123
+ children.push(/** @type {T} */ (current));
124
+ }
125
+
126
+ if (current.children) {
127
+ for (let i = 0, len = current.children.length; i < len; i++) {
128
+ recursionFunc(current.children[i]);
129
+ }
130
+ }
131
+ })(el);
132
+
133
+ return /** @type {Array<T>} */ (children);
134
+ }
135
+
136
+ /**
137
+ * @template {Node} T
138
+ * @description Get all "childNodes" of the argument value element (Include text nodes)
139
+ * @param {Node} element element to get child node
140
+ * @param {?(current: *) => boolean} validation Conditional function
141
+ * @returns {Array<T>}
142
+ */
143
+ export function getListChildNodes(element, validation) {
144
+ const children = [];
145
+ if (!element || element.childNodes.length === 0) return children;
146
+
147
+ validation =
148
+ validation ||
149
+ function () {
150
+ return true;
151
+ };
152
+
153
+ (function recursionFunc(current) {
154
+ if (element !== current && validation(current)) {
155
+ children.push(current);
156
+ }
157
+
158
+ for (let i = 0, len = current.childNodes.length; i < len; i++) {
159
+ recursionFunc(current.childNodes[i]);
160
+ }
161
+ })(element);
162
+
163
+ return /** @type {Array<T>} */ (children);
164
+ }
165
+
166
+ /**
167
+ * @description Returns the number of parents nodes.
168
+ * - "0" when the parent node is the WYSIWYG area.
169
+ * - '-1' when the element argument is the WYSIWYG area.
170
+ * @param {Node} node The element to check
171
+ * @returns {number}
172
+ */
173
+ export function getNodeDepth(node) {
174
+ if (!node || domCheck.isWysiwygFrame(node)) return -1;
175
+
176
+ let depth = 0;
177
+ node = node.parentNode;
178
+
179
+ while (node && !domCheck.isWysiwygFrame(node)) {
180
+ depth += 1;
181
+ node = node.parentNode;
182
+ }
183
+
184
+ return depth;
185
+ }
186
+
187
+ /**
188
+ * @description Sort a node array by depth of element.
189
+ * @param {Array<Node>} array Node array
190
+ * @param {boolean} des true: descending order / false: ascending order
191
+ */
192
+ export function sortNodeByDepth(array, des) {
193
+ const t = !des ? -1 : 1;
194
+ const f = t * -1;
195
+
196
+ array.sort(function (a, b) {
197
+ if (!domCheck.isListCell(a) || !domCheck.isListCell(b)) return 0;
198
+ const a_i = getNodeDepth(a);
199
+ const b_i = getNodeDepth(b);
200
+ return a_i > b_i ? t : a_i < b_i ? f : 0;
201
+ });
202
+ }
203
+
204
+ /**
205
+ * @description Compares two elements to find a common ancestor, and returns the order of the two elements.
206
+ * @param {Node} a Node to compare.
207
+ * @param {Node} b Node to compare.
208
+ * @returns {{ancestor: HTMLElement|null, a: Node, b: Node, result: number}} { ancesstor, a, b, result: (a > b ? 1 : a < b ? -1 : 0) };
209
+ */
210
+ export function compareElements(a, b) {
211
+ let aNode = a,
212
+ bNode = b;
213
+ while (aNode && bNode && aNode.parentElement !== bNode.parentElement) {
214
+ aNode = aNode.parentElement;
215
+ bNode = bNode.parentElement;
216
+ }
217
+
218
+ if (!aNode || !bNode)
219
+ return {
220
+ ancestor: null,
221
+ a: a,
222
+ b: b,
223
+ result: 0
224
+ };
225
+
226
+ const children = aNode.parentNode.childNodes;
227
+ const aIndex = domUtils.getArrayIndex(children, aNode);
228
+ const bIndex = domUtils.getArrayIndex(children, bNode);
229
+
230
+ return {
231
+ ancestor: aNode.parentElement,
232
+ a: aNode,
233
+ b: bNode,
234
+ result: aIndex > bIndex ? 1 : aIndex < bIndex ? -1 : 0
235
+ };
236
+ }
237
+
238
+ /**
239
+ * @template {HTMLElement} T
240
+ * @description Get the parent element of the argument value.
241
+ * - A tag that satisfies the query condition is imported.
242
+ * @param {Node} element Reference element
243
+ * @param {string|((current: *) => boolean)|Node} query Query String (nodeName, .className, #ID, :name) or validation function.
244
+ * - Not use it like jquery.
245
+ * - Only one condition can be entered at a time.
246
+ * @param {?number=} depth Number of parent levels to depth.
247
+ * @returns {T|null} Not found: null
248
+ */
249
+ export function getParentElement(element, query, depth) {
250
+ let valid;
251
+
252
+ if (typeof query === 'function') {
253
+ valid = query;
254
+ } else if (typeof query === 'object') {
255
+ /** @param {Node} current */
256
+ valid = (current) => current === query;
257
+ } else {
258
+ let attr;
259
+ if (/^\./.test(query)) {
260
+ attr = 'className';
261
+ query = '(\\s|^)' + query.split('.')[1] + '(\\s|$)';
262
+ } else if (/^#/.test(query)) {
263
+ attr = 'id';
264
+ query = '^' + query.split('#')[1] + '$';
265
+ } else if (/^:/.test(query)) {
266
+ attr = 'name';
267
+ query = '^' + query.split(':')[1] + '$';
268
+ } else {
269
+ attr = 'nodeName';
270
+ query = '^' + query + '$';
271
+ }
272
+
273
+ const regExp = new RegExp(query, 'i');
274
+ /** @param {Node} el */
275
+ valid = (el) => regExp.test(el[attr]);
276
+ }
277
+
278
+ if (!depth) depth = Infinity;
279
+ let index = 0;
280
+ while (element && !valid(element)) {
281
+ if (index >= depth || domCheck.isWysiwygFrame(element)) {
282
+ return null;
283
+ }
284
+ element = element.parentElement;
285
+ index++;
286
+ }
287
+
288
+ return /** @type {T} */ (element);
289
+ }
290
+
291
+ /**
292
+ * @template {HTMLElement} T
293
+ * @description Gets all ancestors of the argument value.
294
+ * - Get all tags that satisfy the query condition.
295
+ * @param {Node} element Reference element
296
+ * @param {string|((current: *) => boolean)|Node} query Query String (nodeName, .className, #ID, :name) or validation function.
297
+ * Not use it like jquery.
298
+ * Only one condition can be entered at a time.
299
+ * @param {?number=} depth Number of parent levels to depth.
300
+ * @returns {Array<T>} Returned in an array in order.
301
+ */
302
+ export function getParentElements(element, query, depth) {
303
+ let valid;
304
+
305
+ if (typeof query === 'function') {
306
+ valid = query;
307
+ } else if (typeof query === 'object') {
308
+ /** @param {Node} current */
309
+ valid = (current) => current === query;
310
+ } else {
311
+ let attr;
312
+ if (/^\./.test(query)) {
313
+ attr = 'className';
314
+ query = '(\\s|^)' + query.split('.')[1] + '(\\s|$)';
315
+ } else if (/^#/.test(query)) {
316
+ attr = 'id';
317
+ query = '^' + query.split('#')[1] + '$';
318
+ } else if (/^:/.test(query)) {
319
+ attr = 'name';
320
+ query = '^' + query.split(':')[1] + '$';
321
+ } else {
322
+ attr = 'nodeName';
323
+ query = '^' + query + '$';
324
+ }
325
+
326
+ const regExp = new RegExp(query, 'i');
327
+ /** @param {Node} el */
328
+ valid = (el) => regExp.test(el[attr]);
329
+ }
330
+
331
+ const elementList = [];
332
+ if (!depth) depth = Infinity;
333
+ let index = 0;
334
+ while (index <= depth && element && !domCheck.isWysiwygFrame(element)) {
335
+ if (valid(element)) {
336
+ elementList.push(element);
337
+ }
338
+ element = element.parentElement;
339
+ index++;
340
+ }
341
+
342
+ return /** @type {Array<T>} */ (elementList);
343
+ }
344
+
345
+ /**
346
+ * @template {HTMLElement} T
347
+ * @description Gets the element with "data-command" attribute among the parent elements.
348
+ * @param {Node} target Target element
349
+ * @returns {T|null}
350
+ */
351
+ export function getCommandTarget(target) {
352
+ let n = /** @type {HTMLElement} */ (target);
353
+ while (n && !/^(UL)$/i.test(n.nodeName) && !domUtils.hasClass(n, 'sun-editor')) {
354
+ if (n.hasAttribute('data-command')) return /** @type {T} */ (n);
355
+ n = n.parentElement;
356
+ }
357
+
358
+ return null;
359
+ }
360
+
361
+ /**
362
+ * @template {HTMLElement} T
363
+ * @description Get the event.target element.
364
+ * @param {Event} event Event object
365
+ * @returns {T|null}
366
+ */
367
+ export function getEventTarget(event) {
368
+ return /** @type {T} */ (event.target);
369
+ }
370
+
371
+ /**
372
+ * @template {Node} T
373
+ * @description Get the child element of the argument value.
374
+ * - A tag that satisfies the query condition is imported.
375
+ * @param {Node} node Reference element
376
+ * @param {string|((current: *) => boolean)|Node} query Query String (nodeName, .className, #ID, :name) or validation function.
377
+ * @param {boolean} last If true returns the last node among the found child nodes. (default: first node)
378
+ * Not use it like jquery.
379
+ * Only one condition can be entered at a time.
380
+ * @returns {T|null} Not found: null
381
+ */
382
+ export function getEdgeChild(node, query, last) {
383
+ let valid;
384
+
385
+ if (typeof query === 'function') {
386
+ valid = query;
387
+ } else if (typeof query === 'object') {
388
+ valid = function (current) {
389
+ return current === query;
390
+ };
391
+ } else {
392
+ let attr;
393
+ if (/^\./.test(query)) {
394
+ attr = 'className';
395
+ query = '(\\s|^)' + query.split('.')[1] + '(\\s|$)';
396
+ } else if (/^#/.test(query)) {
397
+ attr = 'id';
398
+ query = '^' + query.split('#')[1] + '$';
399
+ } else if (/^:/.test(query)) {
400
+ attr = 'name';
401
+ query = '^' + query.split(':')[1] + '$';
402
+ } else {
403
+ attr = 'nodeName';
404
+ query = '^' + (query === 'text' ? '#' + query : query) + '$';
405
+ }
406
+
407
+ const regExp = new RegExp(query, 'i');
408
+ valid = function (el) {
409
+ return regExp.test(el[attr]);
410
+ };
411
+ }
412
+
413
+ const childList = getListChildNodes(node, (current) => valid(current));
414
+
415
+ return /** @type {T} */ (childList[last ? childList.length - 1 : 0]);
416
+ }
417
+
418
+ /**
419
+ * @description Get edge child nodes of the argument value.
420
+ * - 1. The first node of all the child nodes of the "first" element is returned.
421
+ * - 2. The last node of all the child nodes of the "last" element is returned.
422
+ * - 3. When there is no "last" element, the first and last nodes of all the children of the "first" element are returned.
423
+ * @param {Node} first First element
424
+ * @param {Node|null} last Last element
425
+ * @returns {{sc: Node, ec: Node}} { sc: "first", ec: "last" }
426
+ */
427
+ export function getEdgeChildNodes(first, last) {
428
+ if (!first) return;
429
+ if (!last) last = first;
430
+
431
+ while (first && first.nodeType === 1 && first.childNodes.length > 0 && !domCheck.isBreak(first)) first = first.firstChild;
432
+ while (last && last.nodeType === 1 && last.childNodes.length > 0 && !domCheck.isBreak(last)) last = last.lastChild;
433
+
434
+ return {
435
+ sc: first,
436
+ ec: last || first
437
+ };
438
+ }
439
+
440
+ /**
441
+ * @template {Node} T
442
+ * @description Gets the previous sibling last child. If there is no sibling, then it'll take it from the closest ancestor with child
443
+ * @param {Node} node Reference element
444
+ * @param {?Node=} ceiling Highest boundary allowed
445
+ * @returns {T|null} Not found: null
446
+ */
447
+ export function getPreviousDeepestNode(node, ceiling) {
448
+ let previousNode = node.previousSibling;
449
+ if (!previousNode) {
450
+ for (let parentNode = node.parentNode; parentNode; parentNode = parentNode.parentNode) {
451
+ if (parentNode === ceiling) return null;
452
+ if (parentNode.previousSibling) {
453
+ previousNode = parentNode.previousSibling;
454
+ break;
455
+ }
456
+ }
457
+ if (!previousNode) return null;
458
+ }
459
+
460
+ if (domCheck.isNonEditable(previousNode)) return /** @type {T} */ (/** @type {unknown} */ (previousNode));
461
+
462
+ while (previousNode.lastChild) previousNode = previousNode.lastChild;
463
+
464
+ return /** @type {T} */ (/** @type {unknown} */ (previousNode));
465
+ }
466
+
467
+ /**
468
+ * @template {Node} T
469
+ * @description Gets the next sibling first child. If there is no sibling, then it'll take it from the closest ancestor with child
470
+ * @param {Node} node Reference element
471
+ * @param {?Node=} ceiling Highest boundary allowed
472
+ * @returns {T|null} Not found: null
473
+ */
474
+ export function getNextDeepestNode(node, ceiling) {
475
+ let nextNode = node.nextSibling;
476
+ if (!nextNode) {
477
+ for (let parentNode = node.parentNode; parentNode; parentNode = parentNode.parentNode) {
478
+ if (parentNode === ceiling) return null;
479
+ if (parentNode.nextSibling) {
480
+ nextNode = parentNode.nextSibling;
481
+ break;
482
+ }
483
+ }
484
+ if (!nextNode) return null;
485
+ }
486
+
487
+ if (domCheck.isNonEditable(nextNode)) return /** @type {T} */ (/** @type {unknown} */ (nextNode));
488
+
489
+ while (nextNode.firstChild) nextNode = nextNode.firstChild;
490
+
491
+ return /** @type {T} */ (/** @type {unknown} */ (nextNode));
492
+ }
493
+
494
+ /**
495
+ * @description Find the index of the text node in the line element.
496
+ * @param {Node} line Line element (p, div, etc.)
497
+ * @param {Node} offsetContainer Base node to start searching
498
+ * @param {number} offset Base offset to start searching
499
+ * @param {?(current: *) => boolean=} validate Validation function
500
+ * @returns {number}
501
+ */
502
+ export function findTextIndexOnLine(line, offsetContainer, offset, validate) {
503
+ if (!line) return 0;
504
+ if (!validate) validate = () => true;
505
+
506
+ let index = 0;
507
+ let found = false;
508
+
509
+ (function recursionFunc(node) {
510
+ if (found || node.nodeType === 8) return;
511
+ if (validate(node)) return; // component.is
512
+
513
+ if (node.nodeType === 3) {
514
+ if (node === offsetContainer) {
515
+ index += offset;
516
+ found = true;
517
+ return;
518
+ }
519
+ index += node.textContent.length;
520
+ } else if (node.nodeType === 1) {
521
+ const childNodes = node.childNodes;
522
+ for (let i = 0, len = childNodes.length; i < len; i++) {
523
+ recursionFunc(childNodes[i]);
524
+ if (found) return;
525
+ }
526
+ }
527
+ })(line);
528
+
529
+ return index;
530
+ }
531
+
532
+ /**
533
+ * @description Find the end index of a sequence of at least minTabSize consecutive non-breaking spaces or spaces
534
+ * - which are interpreted as a tab key, occurring after a given base index in a text string.
535
+ * @param {Node} line Line element (p, div, etc.)
536
+ * @param {number} baseIndex Base index to start searching
537
+ * @param {number} minTabSize Minimum number of consecutive spaces to consider as a tab
538
+ * @returns {number} The adjusted index within the line element accounting for non-space characters
539
+ */
540
+ export function findTabEndIndex(line, baseIndex, minTabSize) {
541
+ if (!line) return 0;
542
+ const innerText = line.textContent;
543
+ const regex = new RegExp(`((\\u00A0|\\s){${minTabSize},})`, 'g');
544
+ let match;
545
+
546
+ regex.lastIndex = baseIndex;
547
+
548
+ while ((match = regex.exec(innerText)) !== null) {
549
+ if (match.index >= baseIndex) {
550
+ const spaceEndIndex = match.index + match[0].length - 1;
551
+ const precedingText = innerText.slice(0, spaceEndIndex + 1);
552
+ const nonSpaceCharCount = (precedingText.match(/[^\u00A0\s]/g) || []).length;
553
+ return spaceEndIndex + nonSpaceCharCount + minTabSize;
554
+ }
555
+ }
556
+
557
+ return 0;
558
+ }
559
+
560
+ /**
561
+ * @description Finds the table cell that appears visually at the bottom-right position,
562
+ * considering both rowSpan and colSpan, even if smaller cells are placed after large merged cells.
563
+ *
564
+ * @param {HTMLTableCellElement[]} cells
565
+ * @returns {HTMLTableCellElement|null}
566
+ */
567
+ export function findVisualLastCell(cells) {
568
+ if (!cells || cells.length === 0) return null;
569
+
570
+ /**
571
+ * @description visibility col index
572
+ * @type {Object<number, boolean[]>}
573
+ */
574
+ const occupied = {};
575
+
576
+ let target = null;
577
+ let maxRowEnd = -1;
578
+ let maxColEnd = -1;
579
+
580
+ for (const cell of cells) {
581
+ const row = /** @type {HTMLTableRowElement} */ (cell.parentElement);
582
+ const rowIndex = row.rowIndex;
583
+ const rowSpan = cell.rowSpan || 1;
584
+ const colSpan = cell.colSpan || 1;
585
+
586
+ // 현재 행에서 visual column index 찾기
587
+ if (!occupied[rowIndex]) occupied[rowIndex] = [];
588
+
589
+ let colIndex = 0;
590
+ const rowOcc = occupied[rowIndex];
591
+ while (rowOcc[colIndex]) colIndex++;
592
+
593
+ for (let i = 0; i < colSpan; i++) {
594
+ rowOcc[colIndex + i] = true;
595
+ }
596
+
597
+ for (let r = 1; r < rowSpan; r++) {
598
+ const nextRow = rowIndex + r;
599
+ if (!occupied[nextRow]) occupied[nextRow] = [];
600
+ for (let i = 0; i < colSpan; i++) {
601
+ occupied[nextRow][colIndex + i] = true;
602
+ }
603
+ }
604
+
605
+ const visualRowEnd = rowIndex + rowSpan - 1;
606
+ const visualColEnd = colIndex + colSpan - 1;
607
+
608
+ // right-bottom
609
+ if (visualRowEnd > maxRowEnd || (visualRowEnd === maxRowEnd && visualColEnd > maxColEnd)) {
610
+ maxRowEnd = visualRowEnd;
611
+ maxColEnd = visualColEnd;
612
+ target = cell;
613
+ }
614
+ }
615
+
616
+ return target;
617
+ }
618
+
619
+ /**
620
+ * @description Finds and returns parent containers that are scrollable.
621
+ * @param {HTMLElement} element - Element to start with
622
+ * @returns {HTMLElement[]} - Array (in descending order)
623
+ */
624
+ export function getScrollParents(element) {
625
+ const scrollable = [];
626
+ let parent = element?.parentElement;
627
+
628
+ while (parent && !/^(body|html)$/i.test(parent.nodeName)) {
629
+ const style = _w.getComputedStyle(parent);
630
+ const { overflow, overflowX, overflowY } = style;
631
+
632
+ const canScroll = [overflow, overflowX, overflowY].some((prop) => ['auto', 'scroll', 'overlay'].includes(prop));
633
+
634
+ if (canScroll) {
635
+ scrollable.push(parent);
636
+ }
637
+
638
+ parent = parent.parentElement;
639
+ }
640
+
641
+ return scrollable;
642
+ }
643
+
644
+ /**
645
+ * @description Get the argument iframe's document object if use the "iframe" or "fullPage" options
646
+ * @param {HTMLIFrameElement} iframe Iframe element (this.editor.frameContext.get('wysiwygFrame'))
647
+ * @returns {Document}
648
+ */
649
+ export function getIframeDocument(iframe) {
650
+ return iframe.contentWindow?.document || iframe.contentDocument;
651
+ }
652
+
653
+ const query = {
654
+ getPositionIndex,
655
+ getNodePath,
656
+ getNodeFromPath,
657
+ getListChildren,
658
+ getListChildNodes,
659
+ getNodeDepth,
660
+ sortNodeByDepth,
661
+ compareElements,
662
+ getParentElement,
663
+ getParentElements,
664
+ getCommandTarget,
665
+ getEventTarget,
666
+ getEdgeChild,
667
+ getEdgeChildNodes,
668
+ getPreviousDeepestNode,
669
+ getNextDeepestNode,
670
+ findTextIndexOnLine,
671
+ findTabEndIndex,
672
+ findVisualLastCell,
673
+ getScrollParents,
674
+ getIframeDocument
675
+ };
676
+
677
+ export default query;