suneditor 3.0.0-rc.4 → 3.0.0

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 (171) hide show
  1. package/README.md +4 -3
  2. package/dist/suneditor-contents.min.css +1 -1
  3. package/dist/suneditor.min.css +1 -1
  4. package/dist/suneditor.min.js +1 -1
  5. package/package.json +10 -6
  6. package/src/assets/design/color.css +14 -2
  7. package/src/assets/design/typography.css +5 -0
  8. package/src/assets/icons/defaultIcons.js +22 -4
  9. package/src/assets/suneditor-contents.css +1 -1
  10. package/src/assets/suneditor.css +312 -18
  11. package/src/core/config/eventManager.js +6 -9
  12. package/src/core/editor.js +1 -1
  13. package/src/core/event/actions/index.js +5 -0
  14. package/src/core/event/effects/keydown.registry.js +25 -0
  15. package/src/core/event/eventOrchestrator.js +69 -2
  16. package/src/core/event/handlers/handler_ww_mouse.js +1 -0
  17. package/src/core/event/rules/keydown.rule.backspace.js +9 -1
  18. package/src/core/kernel/coreKernel.js +4 -0
  19. package/src/core/kernel/store.js +2 -0
  20. package/src/core/logic/dom/char.js +11 -0
  21. package/src/core/logic/dom/format.js +22 -0
  22. package/src/core/logic/dom/html.js +126 -11
  23. package/src/core/logic/dom/nodeTransform.js +13 -0
  24. package/src/core/logic/dom/offset.js +100 -37
  25. package/src/core/logic/dom/selection.js +54 -22
  26. package/src/core/logic/panel/finder.js +982 -0
  27. package/src/core/logic/panel/menu.js +8 -6
  28. package/src/core/logic/panel/toolbar.js +112 -19
  29. package/src/core/logic/panel/viewer.js +214 -43
  30. package/src/core/logic/shell/_commandExecutor.js +7 -1
  31. package/src/core/logic/shell/commandDispatcher.js +1 -1
  32. package/src/core/logic/shell/component.js +5 -7
  33. package/src/core/logic/shell/history.js +24 -0
  34. package/src/core/logic/shell/shortcuts.js +3 -3
  35. package/src/core/logic/shell/ui.js +25 -26
  36. package/src/core/schema/frameContext.js +15 -1
  37. package/src/core/schema/options.js +180 -39
  38. package/src/core/section/constructor.js +61 -20
  39. package/src/core/section/documentType.js +2 -2
  40. package/src/events.js +12 -0
  41. package/src/helper/clipboard.js +1 -1
  42. package/src/helper/converter.js +15 -0
  43. package/src/helper/dom/domQuery.js +12 -0
  44. package/src/helper/dom/domUtils.js +26 -14
  45. package/src/helper/index.js +3 -0
  46. package/src/helper/markdown.js +876 -0
  47. package/src/interfaces/plugins.js +7 -5
  48. package/src/langs/ckb.js +9 -0
  49. package/src/langs/cs.js +9 -0
  50. package/src/langs/da.js +9 -0
  51. package/src/langs/de.js +9 -0
  52. package/src/langs/en.js +9 -0
  53. package/src/langs/es.js +9 -0
  54. package/src/langs/fa.js +9 -0
  55. package/src/langs/fr.js +9 -0
  56. package/src/langs/he.js +9 -0
  57. package/src/langs/hu.js +9 -0
  58. package/src/langs/it.js +9 -0
  59. package/src/langs/ja.js +9 -0
  60. package/src/langs/km.js +9 -0
  61. package/src/langs/ko.js +9 -0
  62. package/src/langs/lv.js +9 -0
  63. package/src/langs/nl.js +9 -0
  64. package/src/langs/pl.js +9 -0
  65. package/src/langs/pt_br.js +9 -0
  66. package/src/langs/ro.js +9 -0
  67. package/src/langs/ru.js +9 -0
  68. package/src/langs/se.js +9 -0
  69. package/src/langs/tr.js +9 -0
  70. package/src/langs/uk.js +9 -0
  71. package/src/langs/ur.js +9 -0
  72. package/src/langs/zh_cn.js +9 -0
  73. package/src/modules/contract/Browser.js +31 -1
  74. package/src/modules/contract/ColorPicker.js +6 -0
  75. package/src/modules/contract/Controller.js +77 -39
  76. package/src/modules/contract/Figure.js +57 -0
  77. package/src/modules/contract/Modal.js +6 -0
  78. package/src/modules/manager/ApiManager.js +53 -4
  79. package/src/modules/manager/FileManager.js +18 -1
  80. package/src/modules/ui/ModalAnchorEditor.js +35 -2
  81. package/src/modules/ui/SelectMenu.js +44 -12
  82. package/src/plugins/browser/fileBrowser.js +5 -2
  83. package/src/plugins/command/codeBlock.js +324 -0
  84. package/src/plugins/command/exportPDF.js +15 -3
  85. package/src/plugins/command/fileUpload.js +4 -1
  86. package/src/plugins/dropdown/backgroundColor.js +5 -1
  87. package/src/plugins/dropdown/blockStyle.js +8 -2
  88. package/src/plugins/dropdown/fontColor.js +5 -1
  89. package/src/plugins/dropdown/hr.js +6 -0
  90. package/src/plugins/dropdown/layout.js +4 -1
  91. package/src/plugins/dropdown/lineHeight.js +3 -0
  92. package/src/plugins/dropdown/paragraphStyle.js +5 -5
  93. package/src/plugins/dropdown/table/index.js +4 -1
  94. package/src/plugins/dropdown/table/render/table.html.js +1 -1
  95. package/src/plugins/dropdown/table/services/table.grid.js +16 -8
  96. package/src/plugins/dropdown/table/services/table.style.js +5 -9
  97. package/src/plugins/dropdown/template.js +3 -0
  98. package/src/plugins/dropdown/textStyle.js +5 -1
  99. package/src/plugins/field/mention.js +5 -1
  100. package/src/plugins/index.js +3 -0
  101. package/src/plugins/input/fontSize.js +10 -3
  102. package/src/plugins/modal/audio.js +7 -3
  103. package/src/plugins/modal/embed.js +23 -20
  104. package/src/plugins/modal/image/index.js +5 -1
  105. package/src/plugins/modal/math.js +7 -2
  106. package/src/plugins/modal/video/index.js +21 -4
  107. package/src/themes/cobalt.css +13 -4
  108. package/src/themes/cream.css +11 -2
  109. package/src/themes/dark.css +13 -4
  110. package/src/themes/midnight.css +13 -4
  111. package/src/typedef.js +4 -4
  112. package/types/assets/icons/defaultIcons.d.ts +12 -1
  113. package/types/assets/suneditor.css.d.ts +1 -1
  114. package/types/core/config/eventManager.d.ts +6 -8
  115. package/types/core/event/actions/index.d.ts +1 -0
  116. package/types/core/event/effects/keydown.registry.d.ts +2 -0
  117. package/types/core/event/eventOrchestrator.d.ts +2 -1
  118. package/types/core/kernel/coreKernel.d.ts +5 -0
  119. package/types/core/kernel/store.d.ts +5 -0
  120. package/types/core/logic/dom/char.d.ts +11 -0
  121. package/types/core/logic/dom/format.d.ts +22 -0
  122. package/types/core/logic/dom/html.d.ts +16 -0
  123. package/types/core/logic/dom/nodeTransform.d.ts +13 -0
  124. package/types/core/logic/dom/offset.d.ts +23 -2
  125. package/types/core/logic/dom/selection.d.ts +9 -3
  126. package/types/core/logic/panel/finder.d.ts +83 -0
  127. package/types/core/logic/panel/toolbar.d.ts +14 -1
  128. package/types/core/logic/panel/viewer.d.ts +22 -2
  129. package/types/core/logic/shell/shortcuts.d.ts +1 -1
  130. package/types/core/schema/frameContext.d.ts +22 -0
  131. package/types/core/schema/options.d.ts +362 -79
  132. package/types/events.d.ts +11 -0
  133. package/types/helper/converter.d.ts +15 -0
  134. package/types/helper/dom/domQuery.d.ts +12 -0
  135. package/types/helper/dom/domUtils.d.ts +23 -2
  136. package/types/helper/index.d.ts +5 -0
  137. package/types/helper/markdown.d.ts +27 -0
  138. package/types/interfaces/plugins.d.ts +7 -5
  139. package/types/langs/_Lang.d.ts +9 -0
  140. package/types/modules/contract/Browser.d.ts +36 -2
  141. package/types/modules/contract/ColorPicker.d.ts +6 -0
  142. package/types/modules/contract/Controller.d.ts +35 -1
  143. package/types/modules/contract/Figure.d.ts +57 -0
  144. package/types/modules/contract/Modal.d.ts +6 -0
  145. package/types/modules/manager/ApiManager.d.ts +26 -0
  146. package/types/modules/manager/FileManager.d.ts +17 -0
  147. package/types/modules/ui/ModalAnchorEditor.d.ts +41 -4
  148. package/types/modules/ui/SelectMenu.d.ts +40 -2
  149. package/types/plugins/browser/fileBrowser.d.ts +10 -4
  150. package/types/plugins/command/codeBlock.d.ts +53 -0
  151. package/types/plugins/command/fileUpload.d.ts +8 -2
  152. package/types/plugins/dropdown/backgroundColor.d.ts +10 -2
  153. package/types/plugins/dropdown/blockStyle.d.ts +14 -2
  154. package/types/plugins/dropdown/fontColor.d.ts +10 -2
  155. package/types/plugins/dropdown/hr.d.ts +12 -0
  156. package/types/plugins/dropdown/layout.d.ts +8 -2
  157. package/types/plugins/dropdown/lineHeight.d.ts +6 -0
  158. package/types/plugins/dropdown/paragraphStyle.d.ts +14 -3
  159. package/types/plugins/dropdown/table/index.d.ts +9 -3
  160. package/types/plugins/dropdown/template.d.ts +6 -0
  161. package/types/plugins/dropdown/textStyle.d.ts +10 -2
  162. package/types/plugins/field/mention.d.ts +10 -2
  163. package/types/plugins/index.d.ts +3 -0
  164. package/types/plugins/input/fontSize.d.ts +18 -4
  165. package/types/plugins/modal/audio.d.ts +14 -6
  166. package/types/plugins/modal/embed.d.ts +44 -38
  167. package/types/plugins/modal/image/index.d.ts +9 -1
  168. package/types/plugins/modal/link.d.ts +6 -2
  169. package/types/plugins/modal/math.d.ts +23 -5
  170. package/types/plugins/modal/video/index.d.ts +49 -9
  171. package/types/typedef.d.ts +5 -2
@@ -1,4 +1,4 @@
1
- import { dom, numbers, env } from '../../helper';
1
+ import { dom, env } from '../../helper';
2
2
  import { _DragHandle } from '../../modules/ui';
3
3
 
4
4
  const { _w, _d, NO_EVENT } = env;
@@ -54,9 +54,6 @@ class EventManager {
54
54
 
55
55
  return NO_EVENT;
56
56
  };
57
-
58
- /** @type {HTMLInputElement} */
59
- this.__focusTemp = contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
60
57
  }
61
58
 
62
59
  /**
@@ -64,7 +61,7 @@ class EventManager {
64
61
  * - Only events registered with this method are unregistered or re-registered when methods such as 'setOptions', 'destroy' are called.
65
62
  * @param {*} target Target element
66
63
  * @param {string} type Event type
67
- * @param {EventListenerOrEventListenerObject} listener Event handler
64
+ * @param {*} listener Event handler
68
65
  * @param {boolean|AddEventListenerOptions} [useCapture] Event useCapture option
69
66
  * @return {?SunEditor.Event.Info} Registered event information
70
67
  */
@@ -85,7 +82,7 @@ class EventManager {
85
82
  }
86
83
 
87
84
  return {
88
- target: len > 1 ? target : target[0],
85
+ target,
89
86
  type,
90
87
  listener,
91
88
  useCapture,
@@ -106,7 +103,7 @@ class EventManager {
106
103
  const useCapture = params.useCapture;
107
104
 
108
105
  if (!target) return;
109
- if (!numbers.is(target.length) || target.nodeName || (!Array.isArray(target) && target.length < 1)) target = /** @type {Array<Element>} */ ([target]);
106
+ if (target === _w || target === _d || typeof target.length !== 'number' || target.nodeType || (!Array.isArray(target) && target.length < 1)) target = [target];
110
107
  if (target.length === 0) return;
111
108
 
112
109
  for (let i = 0, len = target.length; i < len; i++) {
@@ -120,7 +117,7 @@ class EventManager {
120
117
  * @description Add an event to document.
121
118
  * - When created as an Iframe, the same event is added to the document in the Iframe.
122
119
  * @param {string} type Event type
123
- * @param {(...args: *) => *} listener Event listener
120
+ * @param {*} listener Event listener
124
121
  * @param {boolean|AddEventListenerOptions} [useCapture] Use event capture
125
122
  * @return {SunEditor.Event.GlobalInfo} Registered event information
126
123
  */
@@ -140,7 +137,7 @@ class EventManager {
140
137
  * @description Remove events from document.
141
138
  * - When created as an Iframe, the event of the document inside the Iframe is also removed.
142
139
  * @param {string|SunEditor.Event.GlobalInfo} type Event type or (Event info = this.addGlobalEvent())
143
- * @param {(...args: *) => *} [listener] Event listener
140
+ * @param {*} [listener] Event listener
144
141
  * @param {boolean|AddEventListenerOptions} [useCapture] Use event capture
145
142
  * @returns {undefined|null} Success: `null`, Not found: `undefined`
146
143
  */
@@ -224,7 +224,7 @@ class Editor {
224
224
  */
225
225
  #init(options) {
226
226
  this.$.pluginManager.init(options);
227
- this.$.shortcuts._registerCustomShortcuts();
227
+ this.$.shortcuts._registerShortcuts();
228
228
  this.$.commandDispatcher._initCommandButtons();
229
229
  this.$.ui.init();
230
230
  }
@@ -91,6 +91,11 @@ export const A = {
91
91
  * @returns {Action}
92
92
  */
93
93
  backspaceFormatMaintain: (formatEl) => ({ t: 'backspace.format.maintain', p: { formatEl } }),
94
+ /**
95
+ * @param {Element} formatEl - brLine element (e.g. PRE) to strip
96
+ * @returns {Action}
97
+ */
98
+ backspaceBrLineStrip: (formatEl) => ({ t: 'backspace.brline.strip', p: { formatEl } }),
94
99
  /**
95
100
  * @param {Node} selectionNode
96
101
  * @param {Range} range
@@ -30,6 +30,31 @@ export default {
30
30
 
31
31
  /** [backspace] */
32
32
 
33
+ /** @action backspaceBrLineStrip — extract first line from brLine (PRE) */
34
+ 'backspace.brline.strip': ({ ctx, ports }, { formatEl }) => {
35
+ const defaultTag = ctx.options.get('defaultLine');
36
+ const parent = formatEl.parentNode;
37
+ const newLine = dom.utils.createElement(defaultTag);
38
+
39
+ while (formatEl.firstChild && !dom.check.isBreak(formatEl.firstChild)) {
40
+ newLine.appendChild(formatEl.firstChild);
41
+ }
42
+
43
+ if (formatEl.firstChild && dom.check.isBreak(formatEl.firstChild)) {
44
+ formatEl.removeChild(formatEl.firstChild);
45
+ }
46
+
47
+ if (!newLine.firstChild) newLine.innerHTML = '<br>';
48
+ parent.insertBefore(newLine, formatEl);
49
+
50
+ if (!formatEl.firstChild || (!formatEl.textContent.trim() && !formatEl.querySelector('br'))) {
51
+ parent.removeChild(formatEl);
52
+ }
53
+
54
+ const focusNode = newLine.firstChild;
55
+ ports.selection.setRange(focusNode, 0, focusNode, 0);
56
+ },
57
+
33
58
  /** @action backspaceFormatMaintain */
34
59
  'backspace.format.maintain': ({ ctx }, { formatEl }) => {
35
60
  if (formatEl.nodeName.toUpperCase() === ctx.options.get('defaultLine').toUpperCase()) {
@@ -106,6 +106,9 @@ class EventOrchestrator extends KernelInjector {
106
106
  this.__eventDoc = null;
107
107
  /** @type {string} */
108
108
  this.__secopy = null;
109
+
110
+ /** @type {HTMLInputElement} */
111
+ this.__focusTemp = this.#contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
109
112
  }
110
113
 
111
114
  /**
@@ -343,6 +346,9 @@ class EventOrchestrator extends KernelInjector {
343
346
  entries.forEach((e) => {
344
347
  this.#ui._emitResizeEvent(this.$.frameRoots.get(e.target.getAttribute('data-root-key')), -1, e);
345
348
  });
349
+ if (this.#store.mode.isInline && this.#store.mode.isBottom) {
350
+ this.#toolbar._resetSticky();
351
+ }
346
352
  }, 0);
347
353
  });
348
354
  }
@@ -443,10 +449,35 @@ class EventOrchestrator extends KernelInjector {
443
449
  this.#eventManager.addEvent(codeArea, 'keyup', cvAuthHeight, false);
444
450
  this.#eventManager.addEvent(codeArea, 'paste', cvAuthHeight, false);
445
451
 
452
+ /** code view tab key */
453
+ if (!this.#options.get('tabDisable')) {
454
+ this.#eventManager.addEvent(codeArea, 'keydown', InsertTab, false);
455
+ }
456
+
446
457
  /** code view numbers */
447
458
  if (codeNumbers) this.#eventManager.addEvent(codeArea, 'scroll', this.$.viewer._scrollLineNumbers.bind(codeArea, codeNumbers), false);
448
459
  }
449
460
 
461
+ /** markdown view area */
462
+ const markdownArea = fc.get('markdown');
463
+ if (markdownArea) {
464
+ this.#eventManager.addEvent(markdownArea, 'mousedown', this.#OnFocus_markdown.bind(this, fc), false);
465
+
466
+ const mdNumbers = fc.get('markdownNumbers');
467
+ const mdAutoHeight = this.$.viewer._markdownViewAutoHeight.bind(this.$.viewer, markdownArea, mdNumbers, this.$.frameOptions.get('height') === 'auto');
468
+
469
+ this.#eventManager.addEvent(markdownArea, 'keydown', mdAutoHeight, false);
470
+ this.#eventManager.addEvent(markdownArea, 'keyup', mdAutoHeight, false);
471
+ this.#eventManager.addEvent(markdownArea, 'paste', mdAutoHeight, false);
472
+
473
+ /** markdown view tab key */
474
+ if (!this.#options.get('tabDisable')) {
475
+ this.#eventManager.addEvent(markdownArea, 'keydown', InsertTab, false);
476
+ }
477
+
478
+ if (mdNumbers) this.#eventManager.addEvent(markdownArea, 'scroll', this.$.viewer._scrollMarkdownLineNumbers.bind(markdownArea, mdNumbers), false);
479
+ }
480
+
450
481
  if (fc.has('statusbar')) this.__addStatusbarEvent(fc, fc.get('options'));
451
482
 
452
483
  const OnScrollAbs = this.#OnScroll_Abs.bind(this);
@@ -636,7 +667,16 @@ class EventOrchestrator extends KernelInjector {
636
667
  * @param {FocusEvent} event - Focus event object
637
668
  */
638
669
  __postBlurEvent(frameContext, event) {
639
- if (this.#store.mode.isInline || this.#store.mode.isBalloon) this._hideToolbar();
670
+ if (this.#store.mode.isInline) {
671
+ // Defer hide — prevents race with deferred focus show (setTimeout in #OnFocus_wysiwyg)
672
+ _w.setTimeout(() => {
673
+ if (!this.#store.get('hasFocus')) {
674
+ this._hideToolbar();
675
+ }
676
+ }, 0);
677
+ } else if (this.#store.mode.isBalloon) {
678
+ this._hideToolbar();
679
+ }
640
680
  if (this.#store.mode.isSubBalloon) this._hideToolbar_sub();
641
681
 
642
682
  // user event
@@ -699,7 +739,9 @@ class EventOrchestrator extends KernelInjector {
699
739
  this.#ui._iframeAutoHeight(fc);
700
740
 
701
741
  if (this.#toolbar.isSticky) {
702
- this.#context.get('toolbar_main').style.width = fc.get('topArea').offsetWidth - 2 + 'px';
742
+ if (!this.#toolbar.isCSSSticky) {
743
+ this.#context.get('toolbar_main').style.width = fc.get('topArea').offsetWidth - 2 + 'px';
744
+ }
703
745
  this.#toolbar._resetSticky();
704
746
  }
705
747
  }
@@ -939,6 +981,31 @@ class EventOrchestrator extends KernelInjector {
939
981
  dom.utils.addClass(this.$.commandDispatcher.targets.get('codeView'), 'active');
940
982
  this.#ui._toggleCodeViewButtons(true);
941
983
  }
984
+
985
+ /**
986
+ * @param {SunEditor.FrameContext} frameContext - frame context object
987
+ */
988
+ #OnFocus_markdown(frameContext) {
989
+ this.$.facade.changeFrameContext(frameContext.get('key'));
990
+ dom.utils.addClass(this.$.commandDispatcher.targets.get('markdownView'), 'active');
991
+ this.#ui._toggleCodeViewButtons(true);
992
+ }
993
+ }
994
+
995
+ /**
996
+ * @description Inserts a tab character at the cursor position in a textarea
997
+ * @param {KeyboardEvent} e
998
+ */
999
+ function InsertTab(e) {
1000
+ if (e.key !== 'Tab') return;
1001
+ e.preventDefault();
1002
+
1003
+ const textarea = /** @type {HTMLTextAreaElement} */ (e.target);
1004
+ const start = textarea.selectionStart;
1005
+ const end = textarea.selectionEnd;
1006
+
1007
+ textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end);
1008
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
942
1009
  }
943
1010
 
944
1011
  export default EventOrchestrator;
@@ -89,6 +89,7 @@ export async function OnClick_wysiwyg(fc, e) {
89
89
  // plugin event
90
90
  if ((await this._callPluginEventAsync('onClick', { frameContext: fc, event: e })) === false) return;
91
91
 
92
+ // component
92
93
  const componentInfo = this.$.component.get(eventTarget);
93
94
  if (componentInfo) {
94
95
  e.preventDefault();
@@ -59,7 +59,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
59
59
  !selectionNode.previousSibling &&
60
60
  !dom.check.isListCell(formatEl) &&
61
61
  format.isLine(formatEl) &&
62
- (!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl))
62
+ (!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl) || dom.check.isWysiwygFrame(formatEl.parentNode))
63
63
  ) {
64
64
  // closure range
65
65
  if (format.isClosureBlock(formatEl.parentNode)) {
@@ -67,6 +67,14 @@ export function reduceBackspaceDown(actions, ports, ctx) {
67
67
  return false;
68
68
  }
69
69
 
70
+ // brLine (pre): strip tag to default line(s)
71
+ if (format.isBrLine(formatEl) && dom.check.isWysiwygFrame(formatEl.parentNode)) {
72
+ actions.push(A.preventStop());
73
+ actions.push(A.backspaceBrLineStrip(formatEl));
74
+ actions.push(A.historyPush(true));
75
+ return false;
76
+ }
77
+
70
78
  // maintain default format
71
79
  if (dom.check.isWysiwygFrame(formatEl.parentNode) && formatEl.childNodes.length <= 1 && (!formatEl.firstChild || dom.check.isZeroWidth(formatEl.textContent))) {
72
80
  actions.push(A.preventStop());
@@ -30,6 +30,7 @@ import History from '../logic/shell/history';
30
30
  import Toolbar from '../logic/panel/toolbar';
31
31
  import Menu from '../logic/panel/menu';
32
32
  import Viewer from '../logic/panel/viewer';
33
+ import Finder from '../logic/panel/finder';
33
34
 
34
35
  // L4: event
35
36
  import EventOrchestrator from '../event/eventOrchestrator';
@@ -78,6 +79,7 @@ import EventOrchestrator from '../event/eventOrchestrator';
78
79
  * @property {import('../logic/panel/toolbar').default} subToolbar - L3: Sub-toolbar renderer
79
80
  * @property {import('../logic/panel/menu').default} menu - L3: Menu renderer
80
81
  * @property {import('../logic/panel/viewer').default} viewer - L3: View mode handler
82
+ * @property {import('../logic/panel/finder').default} finder - L3: Finder handler
81
83
  */
82
84
 
83
85
  /**
@@ -218,6 +220,7 @@ class CoreKernel {
218
220
  }
219
221
  this.#logic.set('menu', new Menu(this));
220
222
  this.#logic.set('viewer', new Viewer(this));
223
+ this.#logic.set('finder', new Finder(this));
221
224
 
222
225
  // history (last — closure captures all L3 modules above)
223
226
  this.#logic.set('history', History(this));
@@ -268,6 +271,7 @@ class CoreKernel {
268
271
  subToolbar: this.#logic.get('subToolbar'),
269
272
  menu: this.#logic.get('menu'),
270
273
  viewer: this.#logic.get('viewer'),
274
+ finder: this.#logic.get('finder'),
271
275
  });
272
276
  }
273
277
 
@@ -28,6 +28,7 @@ import { numbers } from '../../helper';
28
28
  * @property {boolean} isBalloonAlways - Whether the toolbar is in `balloon-always` mode (always visible as floating).
29
29
  * @property {boolean} isSubBalloon - Whether the sub-toolbar is in `balloon` mode.
30
30
  * @property {boolean} isSubBalloonAlways - Whether the sub-toolbar is in `balloon-always` mode.
31
+ * @property {boolean} isBottom - Whether the toolbar is placed at the bottom of the editor (`classic:bottom`, `inline:bottom`).
31
32
  */
32
33
 
33
34
  /**
@@ -64,6 +65,7 @@ class Store {
64
65
  isBalloonAlways: /balloon-always/i.test(mode),
65
66
  isSubBalloon: /balloon/i.test(subMode),
66
67
  isSubBalloonAlways: /balloon-always/i.test(subMode),
68
+ isBottom: !!options.get('_toolbar_bottom'),
67
69
  };
68
70
 
69
71
  this.#state = {
@@ -27,6 +27,8 @@ class Char {
27
27
  * @description Returns `false` if char count is greater than "frameOptions.get('charCounter_max')" when "html" is added to the current editor.
28
28
  * @param {Node|string} html Element node or String.
29
29
  * @returns {boolean}
30
+ * @example
31
+ * const canInsert = editor.$.char.check('<strong>new text</strong>');
30
32
  */
31
33
  check(html) {
32
34
  const maxCharCount = this.#frameOptions.get('charCounter_max');
@@ -45,6 +47,9 @@ class Char {
45
47
  * - If [content] is `undefined`, get the current editor's number of characters or binary data size.
46
48
  * @param {string} [content] Content to count. (defalut: this.#frameContext.get('wysiwyg'))
47
49
  * @returns {number}
50
+ * @example
51
+ * const currentLength = editor.$.char.getLength();
52
+ * const textLength = editor.$.char.getLength('Hello World');
48
53
  */
49
54
  getLength(content) {
50
55
  if (typeof content !== 'string') {
@@ -57,6 +62,8 @@ class Char {
57
62
  * @descriptionGets Get the length in bytes of a string.
58
63
  * @param {string} text String text
59
64
  * @returns {number}
65
+ * @example
66
+ * const bytes = editor.$.char.getByteLength('Hello 世界'); // 12
60
67
  */
61
68
  getByteLength(text) {
62
69
  if (!text || !text.toString) return 0;
@@ -105,6 +112,10 @@ class Char {
105
112
  * @param {string} inputText Text added.
106
113
  * @param {boolean} _fromInputEvent Whether the test is triggered from an input event.
107
114
  * @returns {boolean}
115
+ * @example
116
+ * if (!editor.$.char.test(inputData, true)) {
117
+ * e.preventDefault();
118
+ * }
108
119
  */
109
120
  test(inputText, _fromInputEvent) {
110
121
  let nextCharCount = 0;
@@ -94,6 +94,9 @@ class Format {
94
94
  * @param {Node} node Reference node.
95
95
  * @param {?(current: Node) => boolean} [validation] Additional validation function.
96
96
  * @returns {HTMLElement|null}
97
+ * @example
98
+ * const line = editor.$.format.getLine(editor.$.selection.getNode());
99
+ * const lineWithFilter = editor.$.format.getLine(node, (el) => el.nodeName !== 'LI');
97
100
  */
98
101
  getLine(node, validation) {
99
102
  if (!node) return null;
@@ -125,6 +128,8 @@ class Format {
125
128
  /**
126
129
  * @description Replace the br-line tag of the current selection.
127
130
  * @param {Node} element BR-Line element (PRE..)
131
+ * @example
132
+ * editor.$.format.setBrLine(document.createElement('pre'));
128
133
  */
129
134
  setBrLine(element) {
130
135
  if (!this.isBrLine(element)) {
@@ -195,6 +200,8 @@ class Format {
195
200
  * @param {Node} element Reference node.
196
201
  * @param {?(current: Node) => boolean} [validation] Additional validation function.
197
202
  * @returns {HTMLBRElement|null}
203
+ * @example
204
+ * const brLine = editor.$.format.getBrLine(editor.$.selection.getNode());
198
205
  */
199
206
  getBrLine(element, validation) {
200
207
  if (!element) return null;
@@ -245,6 +252,8 @@ class Format {
245
252
  * @param {Node} element Reference node.
246
253
  * @param {?(current: Node) => boolean} [validation] Additional validation function.
247
254
  * @returns {HTMLElement|null}
255
+ * @example
256
+ * const block = editor.$.format.getBlock(editor.$.selection.getNode());
248
257
  */
249
258
  getBlock(element, validation) {
250
259
  if (!element) return null;
@@ -777,6 +786,9 @@ class Format {
777
786
  * @description It is judged whether it is a node related to the text style.
778
787
  * @param {Node|string} element The node to check
779
788
  * @returns {element is HTMLElement}
789
+ * @example
790
+ * editor.$.format.isTextStyleNode('STRONG'); // true
791
+ * editor.$.format.isTextStyleNode('P'); // false
780
792
  */
781
793
  isTextStyleNode(element) {
782
794
  return typeof element === 'string' ? this.#textStyleTagsCheck.test(element) : element?.nodeType === 1 && this.#textStyleTagsCheck.test(element.nodeName);
@@ -788,6 +800,9 @@ class Format {
788
800
  * - `line` element also contain `brLine` element
789
801
  * @param {Node|string} element The node to check
790
802
  * @returns {element is HTMLElement}
803
+ * @example
804
+ * editor.$.format.isLine(document.createElement('p')); // true
805
+ * editor.$.format.isLine('SPAN'); // false
791
806
  */
792
807
  isLine(element) {
793
808
  return typeof element === 'string'
@@ -812,6 +827,8 @@ class Format {
812
827
  * ※ Entering the Enter key in the space on the last line ends `brLine` and appends `line`.
813
828
  * @param {Node|string} element The node to check
814
829
  * @returns {element is HTMLElement}
830
+ * @example
831
+ * editor.$.format.isBrLine(document.createElement('pre')); // true
815
832
  */
816
833
  isBrLine(element) {
817
834
  return (
@@ -828,6 +845,9 @@ class Format {
828
845
  * - `block` is wrap the `line` and `component`
829
846
  * @param {Node|string} element The node to check
830
847
  * @returns {element is HTMLElement}
848
+ * @example
849
+ * editor.$.format.isBlock(document.createElement('blockquote')); // true
850
+ * editor.$.format.isBlock(document.createElement('ul')); // true
831
851
  */
832
852
  isBlock(element) {
833
853
  return typeof element === 'string'
@@ -871,6 +891,8 @@ class Format {
871
891
  * @description Returns a `line` array from selected range.
872
892
  * @param {?(current: Node) => boolean} [validation] The validation function. (Replaces the default validation `format.isLine(current)`)
873
893
  * @returns {Array<HTMLElement>}
894
+ * @example
895
+ * const selectedLines = editor.$.format.getLines();
874
896
  */
875
897
  getLines(validation) {
876
898
  if (!this.#$.selection.resetRangeToTextNode()) return [];
@@ -1,4 +1,4 @@
1
- import { dom, converter, numbers, unicode, clipboard, env } from '../../../helper';
1
+ import { dom, converter, markdown, numbers, unicode, clipboard, env } from '../../../helper';
2
2
 
3
3
  const { _d } = env;
4
4
  const REQUIRED_DATA_ATTRS = 'data-se-[^\\s]+';
@@ -293,7 +293,7 @@ class HTML {
293
293
  for (const [key, value] of Object.entries(attrs)) {
294
294
  // Block event handler attributes and validate src protocol
295
295
  if (/^on/i.test(key)) continue;
296
- if (key === 'src' && !_SAFE_URL_PROTOCOL.test(String(value).replace(/[\s\r\n\t]+/g, ''))) continue;
296
+ if (key === 'src' && !_isSafeURL(String(value))) continue;
297
297
  iframe.setAttribute(key, value);
298
298
  }
299
299
 
@@ -1107,6 +1107,9 @@ class HTML {
1107
1107
  * @param {boolean} [options.includeFullPage=false] Return only the content of the body without headers when the `iframe_fullPage` option is `true`
1108
1108
  * @param {number|Array<number>} [options.rootKey=null] Root index
1109
1109
  * @returns {string|Object<*, string>}
1110
+ * @example
1111
+ * const html = editor.$.html.get();
1112
+ * const htmlWithFrame = editor.$.html.get({ withFrame: true });
1110
1113
  */
1111
1114
  get({ withFrame, includeFullPage, rootKey } = {}) {
1112
1115
  if (!rootKey) rootKey = [this.#store.get('rootKey')];
@@ -1146,6 +1149,9 @@ class HTML {
1146
1149
  emptyCells[j].innerHTML = '<br>';
1147
1150
  }
1148
1151
 
1152
+ // output: wrap code blocks <pre class="language-xxx"> → <pre><code class="language-xxx">
1153
+ this.#wrapPreCode(renderHTML);
1154
+
1149
1155
  const content = renderHTML.innerHTML;
1150
1156
  if (this.#frameOptions.get('iframe_fullPage')) {
1151
1157
  if (includeFullPage) {
@@ -1170,6 +1176,9 @@ class HTML {
1170
1176
  * @param {string} html HTML string
1171
1177
  * @param {Object} [options] Options
1172
1178
  * @param {number|Array<number>} [options.rootKey=null] Root index
1179
+ * @example
1180
+ * editor.$.html.set('<p>New content</p>');
1181
+ * editor.$.html.set(html, { rootKey: 'header' });
1173
1182
  */
1174
1183
  set(html, { rootKey } = {}) {
1175
1184
  this.#$.ui.offCurrentController();
@@ -1182,7 +1191,10 @@ class HTML {
1182
1191
  for (let i = 0; i < rootKey.length; i++) {
1183
1192
  this.#$.facade.changeFrameContext(rootKey[i]);
1184
1193
 
1185
- if (!this.#frameContext.get('isCodeView')) {
1194
+ if (this.#frameContext.get('isMarkdownView')) {
1195
+ const json = converter.htmlToJson(convertValue);
1196
+ this.#frameContext.get('markdown').value = markdown.jsonToMarkdown(json);
1197
+ } else if (!this.#frameContext.get('isCodeView')) {
1186
1198
  this.#frameContext.get('wysiwyg').innerHTML = convertValue;
1187
1199
  this.#$.pluginManager.resetFileInfo();
1188
1200
  this.#$.history.push(false, rootKey[i]);
@@ -1209,7 +1221,10 @@ class HTML {
1209
1221
  this.#$.facade.changeFrameContext(rootKey[i]);
1210
1222
  const convertValue = this.clean(html, { forceFormat: true, whitelist: null, blacklist: null });
1211
1223
 
1212
- if (!this.#frameContext.get('isCodeView')) {
1224
+ if (this.#frameContext.get('isMarkdownView')) {
1225
+ const json = converter.htmlToJson(convertValue);
1226
+ this.#frameContext.get('markdown').value += '\n' + markdown.jsonToMarkdown(json);
1227
+ } else if (!this.#frameContext.get('isCodeView')) {
1213
1228
  const temp = dom.utils.createElement('DIV', null, convertValue);
1214
1229
  const children = Array.from(temp.children);
1215
1230
  for (let j = 0, jLen = children.length; j < jLen; j++) {
@@ -1229,6 +1244,9 @@ class HTML {
1229
1244
  * @param {boolean} [options.withFrame=false] Gets the current content with containing parent `div.sun-editor-editable` (`<div class="sun-editor-editable">{content}</div>`).
1230
1245
  * @param {number|Array<number>} [options.rootKey=null] Root index
1231
1246
  * @returns {Object<string, *>} JSON data
1247
+ * @example
1248
+ * const json = editor.$.html.getJson();
1249
+ * const jsonWithFrame = editor.$.html.getJson({ withFrame: true });
1232
1250
  */
1233
1251
  getJson({ withFrame, rootKey } = {}) {
1234
1252
  return converter.htmlToJson(this.get({ withFrame, rootKey }));
@@ -1236,9 +1254,16 @@ class HTML {
1236
1254
 
1237
1255
  /**
1238
1256
  * @description Sets the JSON data to the editor content
1257
+ * (see @link converter.jsonToHtml)
1239
1258
  * @param {Object<string, *>} jsdonData HTML string
1240
1259
  * @param {Object} [options] Options
1241
1260
  * @param {number|Array<number>} [options.rootKey=null] Root index
1261
+ * @example
1262
+ * const html = editor.$.html.setJson({
1263
+ * type: 'element', tag: 'p', attributes: { class: 'txt' },
1264
+ * children: [{ type: 'text', content: 'Hello' }],
1265
+ * });
1266
+ * // '<p class="txt">Hello</p>'
1242
1267
  */
1243
1268
  setJson(jsdonData, { rootKey } = {}) {
1244
1269
  this.set(converter.jsonToHtml(jsdonData), { rootKey });
@@ -1505,6 +1530,9 @@ class HTML {
1505
1530
  }
1506
1531
  }
1507
1532
 
1533
+ // code block: unwrap <pre><code class="language-xxx"> → <pre class="language-xxx">
1534
+ if (current.nodeName === 'PRE') this.#unwrapPreCode(current);
1535
+
1508
1536
  const nrtag = !dom.query.getParentElement(current, dom.check.isExcludeFormat);
1509
1537
 
1510
1538
  // formatFilter
@@ -1984,13 +2012,58 @@ class HTML {
1984
2012
  this.#cleanStyleRegExpMap.clear();
1985
2013
  }
1986
2014
  }
2015
+
2016
+ /**
2017
+ * @description Input: unwrap `<pre><code class="language-xxx">` → `<pre class="language-xxx">`
2018
+ * @param {HTMLElement} pre
2019
+ */
2020
+ #unwrapPreCode(pre) {
2021
+ if (pre.children.length !== 1 || pre.firstElementChild?.nodeName !== 'CODE') return;
2022
+
2023
+ const codeEl = pre.firstElementChild;
2024
+ const langMatch = (codeEl.className || '').match(/language-(\S+)/);
2025
+
2026
+ if (langMatch) {
2027
+ dom.utils.addClass(pre, 'language-' + langMatch[1]);
2028
+ pre.setAttribute('data-se-lang', langMatch[1]);
2029
+ }
2030
+
2031
+ while (codeEl.firstChild) {
2032
+ pre.insertBefore(codeEl.firstChild, codeEl);
2033
+ }
2034
+
2035
+ pre.removeChild(codeEl);
2036
+ }
2037
+
2038
+ /**
2039
+ * @description Output: wrap `<pre class="language-xxx">` → `<pre><code class="language-xxx">`
2040
+ * @param {HTMLElement} container
2041
+ */
2042
+ #wrapPreCode(container) {
2043
+ const preEls = container.querySelectorAll('pre[class*="language-"]');
2044
+
2045
+ for (let j = 0, jlen = preEls.length, pre, lang, codeEl; j < jlen; j++) {
2046
+ pre = preEls[j];
2047
+ lang = (pre.className.match(/language-(\S+)/) || [])[1];
2048
+ if (!lang) continue;
2049
+
2050
+ codeEl = dom.utils.createElement('CODE', { class: 'language-' + lang });
2051
+ while (pre.firstChild) {
2052
+ codeEl.appendChild(pre.firstChild);
2053
+ }
2054
+
2055
+ pre.appendChild(codeEl);
2056
+ pre.className = pre.className.replace(/\s*language-\S+/g, '').trim();
2057
+ pre.removeAttribute('data-se-lang');
2058
+ }
2059
+ }
1987
2060
  }
1988
2061
 
1989
2062
  /** Module-level regex constants */
1990
2063
  // #CleanElements
1991
2064
  const _RE_XML_NS_TAG = /^<[a-z0-9]+:[a-z0-9]+/i;
1992
2065
  const _RE_TAG_NAME = /(?!<)[a-zA-Z0-9-]+/;
1993
- const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(")[^"]*\1/gi;
2066
+ const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(?:(["'])[^"']*\1|\S+)/gi;
1994
2067
  const _RE_STYLE_EQ = /style=/i;
1995
2068
  const _RE_STYLE_ATTR = /style\s*=\s*(?:"|')[^"']*(?:"|')/;
1996
2069
  const _RE_LEADING_SPACE = /^\s/;
@@ -2013,20 +2086,62 @@ const _RE_SPAN = /^span$/i;
2013
2086
 
2014
2087
  // _isSafeAttribute
2015
2088
  const _SAFE_URL_PROTOCOL = /^(?:https?|ftps?|mailto|tel|blob|sms|geo|webcal|callto):|^[#/]|^data:image\//i;
2016
- const _URL_ATTR_PATTERN = /^(?:href|src)\s*=\s*(?:'|"|\s)*/i;
2017
- const _RE_ATTR_VALUE = /=\s*(?:"|'|)\s*([^"'\s>]*)/;
2018
- const _RE_WHITESPACE = /[\s\r\n\t]+/g;
2019
- const _RE_HTML_ENTITY = /&(#x?[0-9a-f]+|[a-z]+);/gi;
2089
+ const _URL_ATTR_PATTERN = /^(?:href|src)\s*=/i;
2090
+ const _RE_ATTR_VALUE = /=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/;
2020
2091
  const _RE_COLON = /:/i;
2021
2092
 
2093
+ /**
2094
+ * @description Normalize a URL by decoding all HTML entities, URL-encoded characters,
2095
+ * and stripping whitespace/control characters. Used to detect obfuscated dangerous protocols.
2096
+ * e.g. `java&#x09;script:`, `java%09script:`, `&#106;avascript:`
2097
+ * @param {string} url Raw URL string
2098
+ * @returns {string} Normalized URL
2099
+ */
2100
+ function _normalizeURL(url) {
2101
+ // Decode HTML entities (&#x09; &#9; etc.) — repeat to handle nested encoding (e.g. &#38;#x6a; → &#x6a; → j)
2102
+ let prev,
2103
+ limit = 5;
2104
+ do {
2105
+ prev = url;
2106
+ url = url.replace(/&(#x([0-9a-f]+)|#([0-9]+)|([a-z]+));/gi, (_, __, hex, dec) => {
2107
+ if (hex) return String.fromCharCode(parseInt(hex, 16));
2108
+ if (dec) return String.fromCharCode(parseInt(dec, 10));
2109
+ return '';
2110
+ });
2111
+ } while (url !== prev && --limit);
2112
+
2113
+ // Decode URL-encoded characters (%09, %0a, etc.)
2114
+ try {
2115
+ url = decodeURIComponent(url);
2116
+ } catch {
2117
+ // malformed URI — keep as is
2118
+ }
2119
+
2120
+ // Strip all whitespace and control characters (U+0000–U+0020)
2121
+ // eslint-disable-next-line no-control-regex -- intentional: strip control chars used to bypass protocol detection
2122
+ url = url.replace(/[\u0000-\u0020]+/g, '');
2123
+
2124
+ return url;
2125
+ }
2126
+
2127
+ /**
2128
+ * @description Check if a URL is safe (matches the allowed protocol whitelist).
2129
+ * @param {string} url Raw URL string
2130
+ * @returns {boolean}
2131
+ */
2132
+ function _isSafeURL(url) {
2133
+ const normalized = _normalizeURL(url);
2134
+ return _SAFE_URL_PROTOCOL.test(normalized) || !_RE_COLON.test(normalized);
2135
+ }
2136
+
2022
2137
  function _isSafeAttribute(attr) {
2023
2138
  if (!_URL_ATTR_PATTERN.test(attr)) return true;
2024
2139
 
2025
2140
  const urlMatch = attr.match(_RE_ATTR_VALUE);
2026
2141
  if (!urlMatch) return true;
2027
2142
 
2028
- const url = urlMatch[1].replace(_RE_WHITESPACE, '').replace(_RE_HTML_ENTITY, '');
2029
- return _SAFE_URL_PROTOCOL.test(url) || !_RE_COLON.test(url);
2143
+ const url = urlMatch[1] ?? urlMatch[2] ?? urlMatch[3] ?? '';
2144
+ return _isSafeURL(url);
2030
2145
  }
2031
2146
 
2032
2147
  /**