suneditor 3.1.4 → 3.2.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 (216) hide show
  1. package/dist/suneditor-contents.min.css +1 -1
  2. package/dist/suneditor.min.css +28 -1
  3. package/dist/suneditor.min.js +1 -1
  4. package/package.json +2 -1
  5. package/src/assets/design/color.css +12 -0
  6. package/src/assets/design/size.css +9 -2
  7. package/src/assets/icons/defaultIcons.js +96 -47
  8. package/src/assets/suneditor-contents.css +3 -2
  9. package/src/assets/suneditor.css +480 -31
  10. package/src/core/config/contextProvider.js +2 -1
  11. package/src/core/config/eventManager.js +41 -2
  12. package/src/core/config/optionProvider.js +23 -2
  13. package/src/core/editor.js +32 -14
  14. package/src/core/event/actions/index.js +76 -9
  15. package/src/core/event/effects/common.registry.js +2 -1
  16. package/src/core/event/effects/keydown.registry.js +169 -33
  17. package/src/core/event/effects/ruleHelpers.js +20 -6
  18. package/src/core/event/eventOrchestrator.js +136 -23
  19. package/src/core/event/handlers/handler_toolbar.js +4 -2
  20. package/src/core/event/handlers/handler_ww_dragDrop.js +10 -0
  21. package/src/core/event/handlers/handler_ww_input.js +9 -2
  22. package/src/core/event/handlers/handler_ww_key.js +86 -17
  23. package/src/core/event/handlers/handler_ww_mouse.js +45 -8
  24. package/src/core/event/ports.js +23 -5
  25. package/src/core/event/reducers/keydown.reducer.js +15 -2
  26. package/src/core/event/rules/keydown.rule.arrow.js +12 -2
  27. package/src/core/event/rules/keydown.rule.backspace.js +110 -15
  28. package/src/core/event/rules/keydown.rule.delete.js +62 -7
  29. package/src/core/event/rules/keydown.rule.enter.js +62 -31
  30. package/src/core/event/support/defaultLineManager.js +9 -2
  31. package/src/core/event/support/selectionState.js +13 -3
  32. package/src/core/kernel/coreKernel.js +1 -0
  33. package/src/core/logic/dom/char.js +13 -3
  34. package/src/core/logic/dom/format.js +171 -39
  35. package/src/core/logic/dom/html.js +350 -72
  36. package/src/core/logic/dom/inline.js +229 -34
  37. package/src/core/logic/dom/listFormat.js +111 -20
  38. package/src/core/logic/dom/nodeTransform.js +36 -11
  39. package/src/core/logic/dom/offset.js +116 -31
  40. package/src/core/logic/dom/selection.js +147 -29
  41. package/src/core/logic/panel/blockHandle.js +785 -0
  42. package/src/core/logic/panel/blockResolver.js +278 -0
  43. package/src/core/logic/panel/finder.js +16 -6
  44. package/src/core/logic/panel/menu.js +119 -18
  45. package/src/core/logic/panel/toolbar.js +75 -19
  46. package/src/core/logic/panel/viewer.js +76 -20
  47. package/src/core/logic/shell/_commandExecutor.js +44 -9
  48. package/src/core/logic/shell/commandDispatcher.js +46 -4
  49. package/src/core/logic/shell/component.js +102 -25
  50. package/src/core/logic/shell/focusManager.js +13 -3
  51. package/src/core/logic/shell/history.js +21 -6
  52. package/src/core/logic/shell/pluginManager.js +21 -13
  53. package/src/core/logic/shell/shortcuts.js +9 -1
  54. package/src/core/logic/shell/ui.js +139 -37
  55. package/src/core/schema/frameContext.js +34 -3
  56. package/src/core/schema/options.js +46 -4
  57. package/src/core/section/constructor.js +537 -93
  58. package/src/core/section/documentType.js +35 -6
  59. package/src/helper/converter.js +24 -7
  60. package/src/helper/dom/domCheck.js +25 -5
  61. package/src/helper/dom/domQuery.js +2 -1
  62. package/src/helper/dom/domUtils.js +17 -4
  63. package/src/helper/env.js +11 -2
  64. package/src/helper/keyCodeMap.js +6 -2
  65. package/src/helper/markdown.js +24 -5
  66. package/src/helper/msOffice.js +9 -2
  67. package/src/interfaces/plugins.js +1 -1
  68. package/src/langs/ckb.js +0 -1
  69. package/src/langs/cs.js +0 -1
  70. package/src/langs/da.js +0 -1
  71. package/src/langs/de.js +0 -1
  72. package/src/langs/en.js +0 -1
  73. package/src/langs/es.js +0 -1
  74. package/src/langs/fa.js +0 -1
  75. package/src/langs/fr.js +0 -1
  76. package/src/langs/he.js +0 -1
  77. package/src/langs/hu.js +0 -1
  78. package/src/langs/index.js +27 -1
  79. package/src/langs/it.js +0 -1
  80. package/src/langs/ja.js +0 -1
  81. package/src/langs/km.js +0 -1
  82. package/src/langs/ko.js +0 -1
  83. package/src/langs/lv.js +0 -1
  84. package/src/langs/nl.js +0 -1
  85. package/src/langs/pl.js +0 -1
  86. package/src/langs/pt_br.js +0 -1
  87. package/src/langs/ro.js +0 -1
  88. package/src/langs/ru.js +0 -1
  89. package/src/langs/se.js +0 -1
  90. package/src/langs/tr.js +0 -1
  91. package/src/langs/uk.js +0 -1
  92. package/src/langs/ur.js +0 -1
  93. package/src/langs/zh_cn.js +0 -1
  94. package/src/modules/contract/Browser.js +58 -14
  95. package/src/modules/contract/ColorPicker.js +12 -4
  96. package/src/modules/contract/Controller.js +37 -7
  97. package/src/modules/contract/Figure.js +156 -36
  98. package/src/modules/contract/HueSlider.js +27 -4
  99. package/src/modules/contract/Modal.js +50 -10
  100. package/src/modules/manager/FileManager.js +50 -8
  101. package/src/modules/ui/CommandMenu.js +597 -0
  102. package/src/modules/ui/ModalAnchorEditor.js +33 -6
  103. package/src/modules/ui/SelectMenu.js +481 -36
  104. package/src/modules/ui/index.js +1 -0
  105. package/src/plugins/browser/audioGallery.js +2 -1
  106. package/src/plugins/browser/fileBrowser.js +9 -2
  107. package/src/plugins/browser/fileGallery.js +2 -1
  108. package/src/plugins/browser/imageGallery.js +18 -3
  109. package/src/plugins/browser/videoGallery.js +13 -4
  110. package/src/plugins/command/blockquote.js +6 -1
  111. package/src/plugins/command/codeBlock.js +43 -5
  112. package/src/plugins/command/exportPDF.js +20 -4
  113. package/src/plugins/command/fileUpload.js +46 -11
  114. package/src/plugins/command/list_bulleted.js +6 -1
  115. package/src/plugins/command/list_numbered.js +6 -1
  116. package/src/plugins/dropdown/align.js +21 -33
  117. package/src/plugins/dropdown/backgroundColor.js +10 -2
  118. package/src/plugins/dropdown/blockStyle.js +17 -22
  119. package/src/plugins/dropdown/font.js +34 -34
  120. package/src/plugins/dropdown/hr.js +15 -43
  121. package/src/plugins/dropdown/layout.js +10 -27
  122. package/src/plugins/dropdown/lineHeight.js +10 -33
  123. package/src/plugins/dropdown/list.js +19 -25
  124. package/src/plugins/dropdown/paragraphStyle.js +16 -24
  125. package/src/plugins/dropdown/table/index.js +72 -17
  126. package/src/plugins/dropdown/table/render/table.html.js +3 -1
  127. package/src/plugins/dropdown/table/services/table.cell.js +33 -7
  128. package/src/plugins/dropdown/table/services/table.clipboard.js +5 -1
  129. package/src/plugins/dropdown/table/services/table.grid.js +40 -9
  130. package/src/plugins/dropdown/table/services/table.resize.js +55 -11
  131. package/src/plugins/dropdown/table/services/table.selection.js +27 -7
  132. package/src/plugins/dropdown/table/services/table.style.js +120 -20
  133. package/src/plugins/dropdown/template.js +13 -29
  134. package/src/plugins/dropdown/textStyle.js +22 -35
  135. package/src/plugins/field/autocomplete.js +8 -4
  136. package/src/plugins/field/slashCommand.js +297 -0
  137. package/src/plugins/index.js +3 -0
  138. package/src/plugins/input/fontSize.js +37 -8
  139. package/src/plugins/input/pageNavigator.js +7 -1
  140. package/src/plugins/modal/audio.js +71 -17
  141. package/src/plugins/modal/drawing.js +41 -11
  142. package/src/plugins/modal/embed.js +92 -22
  143. package/src/plugins/modal/image/index.js +114 -28
  144. package/src/plugins/modal/image/services/image.size.js +9 -2
  145. package/src/plugins/modal/image/services/image.upload.js +38 -4
  146. package/src/plugins/modal/link.js +9 -2
  147. package/src/plugins/modal/math.js +10 -2
  148. package/src/plugins/modal/video/index.js +112 -26
  149. package/src/plugins/modal/video/render/video.html.js +4 -1
  150. package/src/plugins/modal/video/services/video.size.js +15 -4
  151. package/src/plugins/modal/video/services/video.upload.js +10 -2
  152. package/src/plugins/popup/anchor.js +14 -2
  153. package/src/suneditor.js +8 -2
  154. package/src/themes/cobalt.css +12 -0
  155. package/src/themes/cream.css +12 -0
  156. package/src/themes/dark.css +12 -0
  157. package/src/themes/midnight.css +12 -0
  158. package/src/typedef.js +32 -21
  159. package/types/assets/icons/defaultIcons.d.ts +1 -0
  160. package/types/core/config/contextProvider.d.ts +7 -1
  161. package/types/core/config/eventManager.d.ts +37 -4
  162. package/types/core/config/optionProvider.d.ts +5 -1
  163. package/types/core/event/actions/index.d.ts +44 -6
  164. package/types/core/event/effects/keydown.registry.d.ts +37 -7
  165. package/types/core/event/effects/ruleHelpers.d.ts +6 -1
  166. package/types/core/event/eventOrchestrator.d.ts +6 -1
  167. package/types/core/event/handlers/handler_ww_clipboard.d.ts +15 -3
  168. package/types/core/event/handlers/handler_ww_dragDrop.d.ts +14 -2
  169. package/types/core/event/handlers/handler_ww_input.d.ts +10 -2
  170. package/types/core/event/handlers/handler_ww_key.d.ts +10 -2
  171. package/types/core/event/handlers/handler_ww_mouse.d.ts +25 -5
  172. package/types/core/event/ports.d.ts +7 -2
  173. package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
  174. package/types/core/kernel/store.d.ts +4 -1
  175. package/types/core/logic/dom/format.d.ts +32 -14
  176. package/types/core/logic/dom/html.d.ts +11 -3
  177. package/types/core/logic/dom/nodeTransform.d.ts +6 -6
  178. package/types/core/logic/panel/blockHandle.d.ts +64 -0
  179. package/types/core/logic/panel/blockResolver.d.ts +50 -0
  180. package/types/core/logic/panel/menu.d.ts +72 -3
  181. package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
  182. package/types/core/logic/shell/component.d.ts +5 -3
  183. package/types/core/logic/shell/shortcuts.d.ts +10 -1
  184. package/types/core/logic/shell/ui.d.ts +7 -1
  185. package/types/core/schema/context.d.ts +7 -1
  186. package/types/core/schema/frameContext.d.ts +36 -4
  187. package/types/core/schema/options.d.ts +83 -2
  188. package/types/core/section/constructor.d.ts +38 -1
  189. package/types/events.d.ts +208 -32
  190. package/types/helper/converter.d.ts +4 -2
  191. package/types/helper/dom/domCheck.d.ts +12 -2
  192. package/types/helper/dom/domQuery.d.ts +35 -7
  193. package/types/helper/dom/domUtils.d.ts +4 -1
  194. package/types/interfaces/plugins.d.ts +2 -2
  195. package/types/langs/_Lang.d.ts +0 -1
  196. package/types/modules/contract/ColorPicker.d.ts +2 -2
  197. package/types/modules/contract/Controller.d.ts +7 -0
  198. package/types/modules/contract/Figure.d.ts +31 -4
  199. package/types/modules/ui/CommandMenu.d.ts +262 -0
  200. package/types/modules/ui/SelectMenu.d.ts +56 -3
  201. package/types/modules/ui/index.d.ts +1 -0
  202. package/types/plugins/browser/audioGallery.d.ts +1 -1
  203. package/types/plugins/browser/fileBrowser.d.ts +1 -1
  204. package/types/plugins/browser/fileGallery.d.ts +1 -1
  205. package/types/plugins/browser/imageGallery.d.ts +1 -1
  206. package/types/plugins/browser/videoGallery.d.ts +1 -1
  207. package/types/plugins/dropdown/align.d.ts +1 -1
  208. package/types/plugins/dropdown/blockStyle.d.ts +3 -1
  209. package/types/plugins/dropdown/table/index.d.ts +4 -1
  210. package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
  211. package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
  212. package/types/plugins/field/autocomplete.d.ts +0 -1
  213. package/types/plugins/field/slashCommand.d.ts +165 -0
  214. package/types/plugins/index.d.ts +3 -0
  215. package/types/plugins/modal/link.d.ts +4 -1
  216. package/types/typedef.d.ts +48 -23
@@ -1,5 +1,6 @@
1
1
  import { dom, converter, keyCodeMap, env, numbers } from '../../../helper';
2
2
  import { _DragHandle } from '../../../modules/ui';
3
+ import BlockHandle from '../panel/blockHandle.js';
3
4
  import { COMMAND_BUTTONS } from './commandDispatcher';
4
5
 
5
6
  const DISABLE_BUTTONS_CODEVIEW = `${COMMAND_BUTTONS}:not([class~="se-code-view-enabled"]):not([data-type="MORE"])`;
@@ -65,6 +66,12 @@ class UIManager {
65
66
  */
66
67
  #lineBreaker_b = null;
67
68
 
69
+ /**
70
+ * @description Block handle instance
71
+ * @type {BlockHandle}
72
+ */
73
+ #blockHandle = null;
74
+
68
75
  /**
69
76
  * @constructor
70
77
  * @param {SunEditor.Kernel} kernel
@@ -216,10 +223,17 @@ class UIManager {
216
223
  this.#options.set('textDirection', dir);
217
224
 
218
225
  // update _editableClass / printClass
219
- const editableClass = rtl ? this.#options.get('_editableClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('_editableClass').replace(/\s*se-rtl/, '');
226
+ const editableClass = rtl
227
+ ? this.#options.get('_editableClass').replace(/\s*se-rtl/, '') + ' se-rtl'
228
+ : this.#options.get('_editableClass').replace(/\s*se-rtl/, '');
220
229
  this.#options.set('_editableClass', editableClass);
221
230
  if (this.#options.get('printClass')) {
222
- this.#options.set('printClass', rtl ? this.#options.get('printClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('printClass').replace(/\s*se-rtl/, ''));
231
+ this.#options.set(
232
+ 'printClass',
233
+ rtl
234
+ ? this.#options.get('printClass').replace(/\s*se-rtl/, '') + ' se-rtl'
235
+ : this.#options.get('printClass').replace(/\s*se-rtl/, ''),
236
+ );
223
237
  }
224
238
 
225
239
  this.offCurrentController();
@@ -240,7 +254,10 @@ class UIManager {
240
254
  dom.utils.addClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
241
255
  } else {
242
256
  this.#contextProvider.applyToRoots((e) => {
243
- dom.utils.removeClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
257
+ dom.utils.removeClass(
258
+ [e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')],
259
+ 'se-rtl',
260
+ );
244
261
  e.get('wysiwyg').removeAttribute('dir');
245
262
  });
246
263
  dom.utils.removeClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
@@ -249,7 +266,10 @@ class UIManager {
249
266
  const lineNodes = dom.query.getListChildren(
250
267
  fc.get('wysiwyg'),
251
268
  (current) => {
252
- return this.#$.format.isLine(current) && !!(current.style.marginRight || current.style.marginLeft || current.style.textAlign);
269
+ return (
270
+ this.#$.format.isLine(current) &&
271
+ !!(current.style.marginRight || current.style.marginLeft || current.style.textAlign)
272
+ );
253
273
  },
254
274
  null,
255
275
  );
@@ -277,16 +297,6 @@ class UIManager {
277
297
  this.#reverseToolbarButtons(this.#context.get('toolbar_sub_buttonTray'));
278
298
  }
279
299
 
280
- // document type
281
- if (fc.has('documentType_use_header')) {
282
- if (rtl) fc.get('wrapper').appendChild(fc.get('documentTypeInner'));
283
- else fc.get('wrapper').insertBefore(fc.get('documentTypeInner'), fc.get('wysiwygFrame'));
284
- }
285
- if (fc.has('documentType_use_page')) {
286
- if (rtl) fc.get('wrapper').insertBefore(fc.get('documentTypePage'), fc.get('wysiwygFrame'));
287
- else fc.get('wrapper').appendChild(fc.get('documentTypePage'));
288
- }
289
-
290
300
  if (this.#store.mode.isBalloon) this.#$.toolbar._showBalloon();
291
301
  else if (this.#store.mode.isSubBalloon) this.#$.subToolbar._showBalloon();
292
302
  } catch (e) {
@@ -397,7 +407,11 @@ class UIManager {
397
407
  * @param {string} [rootKey] Root key
398
408
  */
399
409
  showLoading(rootKey) {
400
- const el = /** @type {HTMLElement} */ ((rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector('.se-loading-box'));
410
+ const el = /** @type {HTMLElement} */ (
411
+ (rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector(
412
+ '.se-loading-box',
413
+ )
414
+ );
401
415
  el.style.display = 'block';
402
416
  el.showPopover?.();
403
417
  }
@@ -407,7 +421,11 @@ class UIManager {
407
421
  * @param {string} [rootKey] Root key
408
422
  */
409
423
  hideLoading(rootKey) {
410
- const el = /** @type {HTMLElement} */ ((rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector('.se-loading-box'));
424
+ const el = /** @type {HTMLElement} */ (
425
+ (rootKey ? this.#frameRoots.get(rootKey).get('container') : this.#carrierWrapper).querySelector(
426
+ '.se-loading-box',
427
+ )
428
+ );
411
429
  el.hidePopover?.();
412
430
  el.style.display = 'none';
413
431
  }
@@ -423,7 +441,8 @@ class UIManager {
423
441
  dom.utils.removeClass(this.alertModal, 'se-alert-error|se-alert-success');
424
442
  if (type) dom.utils.addClass(this.alertModal, `se-alert-${type}`);
425
443
 
426
- if (this.#closeSignal) this.#bindAlertClick = this.#eventManager.addEvent(this.#alertInner, 'click', this.#closeListener[1]);
444
+ if (this.#closeSignal)
445
+ this.#bindAlertClick = this.#eventManager.addEvent(this.#alertInner, 'click', this.#closeListener[1]);
427
446
  this.#bindClose &&= this.#eventManager.removeGlobalEvent(this.#bindClose);
428
447
  this.#bindClose = this.#eventManager.addGlobalEvent('keydown', this.#closeListener[0]);
429
448
 
@@ -573,10 +592,31 @@ class UIManager {
573
592
  /**
574
593
  * @param {SunEditor.FrameContext} rt Root target[key] FrameContext
575
594
  */
595
+ /**
596
+ * @description Block handle instance for external access.
597
+ * @type {BlockHandle}
598
+ */
599
+ get blockHandle() {
600
+ return this.#blockHandle;
601
+ }
602
+
576
603
  reset(rt) {
577
604
  rt.set('_editorHeight', rt.get('wysiwygFrame').offsetHeight);
578
605
  this.#lineBreaker_t = rt.get('lineBreaker_t');
579
606
  this.#lineBreaker_b = rt.get('lineBreaker_b');
607
+
608
+ // block handle init
609
+ const blockHandleOpt = this.#options.get('blockHandle');
610
+ if (blockHandleOpt && rt.get('blockHandleArea') && !this.#blockHandle) {
611
+ this.#blockHandle = new BlockHandle(
612
+ this.#$,
613
+ rt.get('blockHandleArea'),
614
+ rt.get('blockHandle'),
615
+ rt.get('blockHandlePlus'),
616
+ rt.get('blockHandleDrag'),
617
+ blockHandleOpt.menu,
618
+ );
619
+ }
580
620
  }
581
621
 
582
622
  /**
@@ -624,6 +664,9 @@ class UIManager {
624
664
  this.#context.get('toolbar_sub_main').style.left = this.#$.subToolbar.balloonOffset.left - x + 'px';
625
665
  }
626
666
 
667
+ // block handle scroll sync
668
+ this.#blockHandle?.syncScroll();
669
+
627
670
  if (this.controllerTargetContext !== this.#frameContext.get('topArea')) {
628
671
  this.offCurrentController();
629
672
  }
@@ -724,7 +767,10 @@ class UIManager {
724
767
 
725
768
  // change dir buttons
726
769
  this.#$.commandDispatcher.applyTargets('dir', (e) => {
727
- dom.utils.changeTxt(e.querySelector('.se-tooltip-text'), this.#contextProvider.lang[rtl ? 'dir_ltr' : 'dir_rtl']);
770
+ dom.utils.changeTxt(
771
+ e.querySelector('.se-tooltip-text'),
772
+ this.#contextProvider.lang[rtl ? 'dir_ltr' : 'dir_rtl'],
773
+ );
728
774
  dom.utils.changeElement(e.firstElementChild, icons[rtl ? 'dir_ltr' : 'dir_rtl']);
729
775
  });
730
776
 
@@ -758,12 +804,22 @@ class UIManager {
758
804
  _initToggleButtons() {
759
805
  const ctx = this.#context;
760
806
 
761
- this.#codeViewDisabledButtons = converter.nodeListToArray(ctx.get('toolbar_buttonTray').querySelectorAll(DISABLE_BUTTONS_CODEVIEW));
762
- this.#controllerOnDisabledButtons = converter.nodeListToArray(ctx.get('toolbar_buttonTray').querySelectorAll(DISABLE_BUTTONS_CONTROLLER));
807
+ this.#codeViewDisabledButtons = converter.nodeListToArray(
808
+ ctx.get('toolbar_buttonTray').querySelectorAll(DISABLE_BUTTONS_CODEVIEW),
809
+ );
810
+ this.#controllerOnDisabledButtons = converter.nodeListToArray(
811
+ ctx.get('toolbar_buttonTray').querySelectorAll(DISABLE_BUTTONS_CONTROLLER),
812
+ );
763
813
 
764
814
  if (this.#options.has('_subMode')) {
765
- this.#codeViewDisabledButtons = this.#codeViewDisabledButtons.concat(converter.nodeListToArray(ctx.get('toolbar_sub_buttonTray').querySelectorAll(DISABLE_BUTTONS_CODEVIEW)));
766
- this.#controllerOnDisabledButtons = this.#controllerOnDisabledButtons.concat(converter.nodeListToArray(ctx.get('toolbar_sub_buttonTray').querySelectorAll(DISABLE_BUTTONS_CONTROLLER)));
815
+ this.#codeViewDisabledButtons = this.#codeViewDisabledButtons.concat(
816
+ converter.nodeListToArray(ctx.get('toolbar_sub_buttonTray').querySelectorAll(DISABLE_BUTTONS_CODEVIEW)),
817
+ );
818
+ this.#controllerOnDisabledButtons = this.#controllerOnDisabledButtons.concat(
819
+ converter.nodeListToArray(
820
+ ctx.get('toolbar_sub_buttonTray').querySelectorAll(DISABLE_BUTTONS_CONTROLLER),
821
+ ),
822
+ );
767
823
  }
768
824
  }
769
825
 
@@ -790,7 +846,10 @@ class UIManager {
790
846
  * @returns {boolean}
791
847
  */
792
848
  isButtonDisabled(button) {
793
- if (this.#frameContext.get('isReadOnly') && dom.utils.arrayIncludes(this.#controllerOnDisabledButtons, button)) {
849
+ if (
850
+ this.#frameContext.get('isReadOnly') &&
851
+ dom.utils.arrayIncludes(this.#controllerOnDisabledButtons, button)
852
+ ) {
794
853
  return true;
795
854
  }
796
855
  return false;
@@ -804,20 +863,45 @@ class UIManager {
804
863
  */
805
864
  _updatePlaceholder(fc) {
806
865
  fc ||= this.#frameContext;
807
- const placeholder = fc.get('placeholder');
866
+ const isCodeView = fc.get('isMarkdownView') || fc.get('isCodeView');
867
+
868
+ const lineShown = !isCodeView && this.#updateLinePlaceholder(fc);
808
869
 
870
+ const placeholder = fc.get('placeholder');
809
871
  if (placeholder) {
810
- if (fc.get('isCodeView')) {
811
- placeholder.style.display = 'none';
812
- return;
813
- }
872
+ placeholder.style.display = !isCodeView && !lineShown && this.#$.facade.isEmpty(fc) ? 'block' : 'none';
873
+ }
874
+ }
814
875
 
815
- if (this.#$.facade.isEmpty(fc)) {
816
- placeholder.style.display = 'block';
817
- } else {
818
- placeholder.style.display = 'none';
819
- }
876
+ /**
877
+ * @internal
878
+ * @description Notion-style per-line placeholder. Marks the focused empty line so a CSS `::before`
879
+ * renders the hint text on it.
880
+ * @param {SunEditor.FrameContext} fc - Frame context
881
+ * @returns {boolean} Whether the line placeholder is currently shown.
882
+ */
883
+ #updateLinePlaceholder(fc) {
884
+ const text = fc.get('placeholder_line');
885
+ if (!text) return false;
886
+
887
+ const wysiwyg = fc.get('wysiwyg');
888
+ const prev = wysiwyg.querySelector('.se-placeholder-line');
889
+
890
+ const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
891
+ const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) ? line : null;
892
+
893
+ // Single-marker invariant: drop the previous marker unless it is still the target line.
894
+ if (prev && prev !== target) {
895
+ prev.classList.remove('se-placeholder-line');
896
+ prev.removeAttribute('data-se-placeholder-line');
897
+ if (!prev.getAttribute('class')) prev.removeAttribute('class');
820
898
  }
899
+
900
+ if (!target) return false;
901
+
902
+ target.classList.add('se-placeholder-line');
903
+ target.setAttribute('data-se-placeholder-line', text);
904
+ return true;
821
905
  }
822
906
 
823
907
  /**
@@ -887,10 +971,17 @@ class UIManager {
887
971
  h === -1
888
972
  ? resizeObserverEntry?.borderBoxSize && resizeObserverEntry.borderBoxSize[0]
889
973
  ? resizeObserverEntry.borderBoxSize[0].blockSize
890
- : resizeObserverEntry.contentRect.height + numbers.get(fc.get('wwComputedStyle').getPropertyValue('padding-left')) + numbers.get(fc.get('wwComputedStyle').getPropertyValue('padding-right'))
974
+ : resizeObserverEntry.contentRect.height +
975
+ numbers.get(fc.get('wwComputedStyle').getPropertyValue('padding-left')) +
976
+ numbers.get(fc.get('wwComputedStyle').getPropertyValue('padding-right'))
891
977
  : h;
892
978
  if (fc.get('_editorHeight') !== h) {
893
- this.#eventManager.triggerEvent('onResizeEditor', { height: h, prevHeight: fc.get('_editorHeight'), frameContext: fc, observerEntry: resizeObserverEntry });
979
+ this.#eventManager.triggerEvent('onResizeEditor', {
980
+ height: h,
981
+ prevHeight: fc.get('_editorHeight'),
982
+ frameContext: fc,
983
+ observerEntry: resizeObserverEntry,
984
+ });
894
985
  fc.set('_editorHeight', h);
895
986
  }
896
987
 
@@ -901,7 +992,11 @@ class UIManager {
901
992
  }
902
993
 
903
994
  init() {
904
- this.#closeSignal = !this.#eventManager.addEvent(this.alertModal.querySelector('[data-command="close"]'), 'click', this.alertClose.bind(this));
995
+ this.#closeSignal = !this.#eventManager.addEvent(
996
+ this.alertModal.querySelector('[data-command="close"]'),
997
+ 'click',
998
+ this.alertClose.bind(this),
999
+ );
905
1000
  this._initToggleButtons();
906
1001
  }
907
1002
 
@@ -943,13 +1038,20 @@ class UIManager {
943
1038
  this.opendBrowser = null;
944
1039
  this.#lineBreaker_t = null;
945
1040
  this.#lineBreaker_b = null;
1041
+ this.#blockHandle?.destroy();
1042
+ this.#blockHandle = null;
946
1043
  this.#controllerOnDisabledButtons = null;
947
1044
  this.#codeViewDisabledButtons = null;
948
1045
  }
949
1046
  }
950
1047
 
951
1048
  function CreateAlertHTML({ lang, icons }) {
952
- const html = '<div><button class="close" data-command="close" title="' + lang.close + '">' + icons.cancel + '</button></div><div><span></span></div>';
1049
+ const html =
1050
+ '<div><button class="close" data-command="close" title="' +
1051
+ lang.close +
1052
+ '">' +
1053
+ icons.cancel +
1054
+ '</button></div><div><span></span></div>';
953
1055
  return dom.utils.createElement('DIV', { class: 'se-alert-content' }, html);
954
1056
  }
955
1057
 
@@ -34,6 +34,7 @@ import { get as getNumber } from '../../helper/numbers';
34
34
  * @property {HTMLTextAreaElement} markdown - Markdown view editing element (a <textarea>).
35
35
  * @property {HTMLTextAreaElement} markdownNumbers - Element displaying line numbers in markdown view mode.
36
36
  * @property {HTMLElement} placeholder - Placeholder element shown when the editor is empty.
37
+ * @property {string} placeholder_line - Per-line placeholder text, rendered via a `::before` on the focused empty line.
37
38
  * @property {HTMLElement} statusbar - Editor status bar element (for resizing, info, etc.).
38
39
  * @property {HTMLElement} navigation - Navigation element (e.g., for outline or bookmarks).
39
40
  * @property {HTMLElement} charWrapper - Wrapper for the character counter element.
@@ -46,6 +47,10 @@ import { get as getNumber } from '../../helper/numbers';
46
47
  * === UI Utilities & Visual Components ===
47
48
  * @property {HTMLElement} lineBreaker_t - Top floating line-breaker UI element (for line insertion).
48
49
  * @property {HTMLElement} lineBreaker_b - Bottom floating line-breaker UI element (for line insertion).
50
+ * @property {?HTMLElement} blockHandleArea - Gutter container (`.se-block-handle-area`) holding the per-line block handle. `null` when the `blockHandle` option is disabled.
51
+ * @property {?HTMLElement} blockHandle - Block handle group (`.se-block-handle`) containing the plus and drag buttons. `null` when disabled.
52
+ * @property {?HTMLElement} blockHandlePlus - Plus button (`.se-block-handle-plus`) — inserts a new line after the current block. `null` when disabled.
53
+ * @property {?HTMLElement} blockHandleDrag - Drag button (`.se-block-handle-drag`) — opens the action menu / starts drag-to-reorder. `null` when disabled.
49
54
  * @property {HTMLElement} [_stickyDummy] - Placeholder element used for sticky toolbar behavior.
50
55
  * @property {HTMLElement} [_toolbarShadow] - Shadow element below the toolbar for visual effects.
51
56
  * @property {{main: HTMLElement, border: HTMLElement, display: HTMLElement, handles: HTMLElement[]}} [_figure] - Current active figure component (image, table, etc.).
@@ -91,12 +96,27 @@ import { get as getNumber } from '../../helper/numbers';
91
96
  * @param {HTMLElement} wwFrame Editor wysiwyg frame
92
97
  * @param {HTMLElement} codeWrapper Editor code view wrapper
93
98
  * @param {HTMLElement} codeFrame Editor code view frame
94
- * @param {{inner: HTMLElement, page: HTMLElement, pageMirror: HTMLElement}} documentTypeInner Document type elements
99
+ * @param {?HTMLElement} markdownWrapper Editor markdown view wrapper (only present when the markdown view button is enabled)
100
+ * @param {?HTMLElement} markdownFrame Editor markdown view frame (only present when the markdown view button is enabled)
95
101
  * @param {?HTMLElement} statusbar Editor statusbar
102
+ * @param {{inner: HTMLElement, page: HTMLElement, pageMirror: HTMLElement}} documentTypeInner Document type elements
103
+ * @param {?HTMLElement} blockHandleArea Block handle gutter (`.se-block-handle-area`). `null` when the `blockHandle` option is disabled.
96
104
  * @param {*} key root key
97
105
  * @returns {FrameContexType}
98
106
  */
99
- export function CreateFrameContext(editorTarget, top, wwFrame, codeWrapper, codeFrame, markdownWrapper, markdownFrame, statusbar, documentTypeInner, key) {
107
+ export function CreateFrameContext(
108
+ editorTarget,
109
+ top,
110
+ wwFrame,
111
+ codeWrapper,
112
+ codeFrame,
113
+ markdownWrapper,
114
+ markdownFrame,
115
+ statusbar,
116
+ documentTypeInner,
117
+ blockHandleArea,
118
+ key,
119
+ ) {
100
120
  const m = /** @type {FrameContexType} */ (
101
121
  new Map([
102
122
  ['key', key],
@@ -114,10 +134,18 @@ export function CreateFrameContext(editorTarget, top, wwFrame, codeWrapper, code
114
134
  // markdown
115
135
  ['markdownWrapper', markdownWrapper],
116
136
  ['markdown', markdownFrame],
117
- ['markdownNumbers', /** @type {HTMLTextAreaElement} */ (markdownWrapper?.querySelector('.se-markdown-view-line'))],
137
+ [
138
+ 'markdownNumbers',
139
+ /** @type {HTMLTextAreaElement} */ (markdownWrapper?.querySelector('.se-markdown-view-line')),
140
+ ],
118
141
  // linebreak, toolbar internal
119
142
  ['lineBreaker_t', top.querySelector('.se-line-breaker-component-t')],
120
143
  ['lineBreaker_b', top.querySelector('.se-line-breaker-component-b')],
144
+ // block handle
145
+ ['blockHandleArea', blockHandleArea],
146
+ ['blockHandle', blockHandleArea?.querySelector('.se-block-handle') || null],
147
+ ['blockHandlePlus', blockHandleArea?.querySelector('.se-block-handle-plus') || null],
148
+ ['blockHandleDrag', blockHandleArea?.querySelector('.se-block-handle-drag') || null],
121
149
  ['_stickyDummy', top.querySelector('.se-toolbar-sticky-dummy')],
122
150
  ['_toolbarShadow', top.querySelector('.se-toolbar-shadow')],
123
151
  ['_minHeight', getNumber(wwFrame.style.minHeight || '65', 0)],
@@ -157,6 +185,9 @@ export function CreateFrameContext(editorTarget, top, wwFrame, codeWrapper, code
157
185
  const placeholder = top.querySelector('.se-placeholder');
158
186
  if (placeholder) m.set('placeholder', placeholder);
159
187
 
188
+ const placeholderLine = editorTarget.options.get('placeholder_line');
189
+ if (placeholderLine) m.set('placeholder_line', placeholderLine);
190
+
160
191
  return m;
161
192
  }
162
193
 
@@ -39,7 +39,8 @@ export const DEFAULTS = {
39
39
  FORMAT_BLOCK: 'BLOCKQUOTE|OL|UL|FIGCAPTION|TABLE|THEAD|TBODY|TR|CAPTION|DETAILS',
40
40
  FORMAT_CLOSURE_BLOCK: 'TH|TD',
41
41
 
42
- ALLOWED_EMPTY_NODE_LIST: '.se-component, pre, blockquote, hr, li, table, img, iframe, video, audio, canvas, details',
42
+ ALLOWED_EMPTY_NODE_LIST:
43
+ '.se-component, pre, blockquote, hr, li, table, img, iframe, video, audio, canvas, details',
43
44
 
44
45
  SIZE_UNITS: ['px', 'pt', 'em', 'rem'],
45
46
 
@@ -69,7 +70,8 @@ export const DEFAULTS = {
69
70
  TAG_STYLES: {
70
71
  '@text': 'font-family|font-size|color|background-color|width|height',
71
72
  '@line': 'text-align|margin|margin-left|margin-right|line-height',
72
- 'table|th|td': 'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
73
+ 'table|th|td':
74
+ 'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
73
75
  'table|td': 'width',
74
76
  tr: 'height',
75
77
  col: 'width',
@@ -77,7 +79,8 @@ export const DEFAULTS = {
77
79
  'ol|ul': 'list-style-type',
78
80
  figure: 'display|width|height|padding|padding-bottom',
79
81
  figcaption: 'margin|margin-top|margin-bottom|text-align',
80
- 'img|video|iframe': 'transform|transform-origin|width|min-width|max-width|height|min-height|max-height|float|margin|margin-top',
82
+ 'img|video|iframe':
83
+ 'transform|transform-origin|width|min-width|max-width|height|min-height|max-height|float|margin|margin-top',
81
84
  hr: '',
82
85
  },
83
86
 
@@ -99,7 +102,9 @@ export const DEFAULTS = {
99
102
  *
100
103
  * === Content & Editing ===
101
104
  * @property {string} [value=""] - Initial value for the editor.
102
- * @property {string} [placeholder=""] - Placeholder text.
105
+ * @property {string} [placeholder=""] - Placeholder text shown when the whole editor is empty.
106
+ * @property {string} [placeholder_line=""] - per-line placeholder shown on the focused
107
+ * line when that line is empty. Takes priority over `placeholder` while a line is focused.
103
108
  * @property {Object<string, string>} [editableFrameAttributes={spellcheck: "false"}] - Attributes for the editable frame[.sun-editor-editable].
104
109
  * ```js
105
110
  * { editableFrameAttributes: { spellcheck: 'true', autocomplete: 'on' } }
@@ -110,6 +115,13 @@ export const DEFAULTS = {
110
115
  * @property {string|number} [width="100%"] - Width for the editor.
111
116
  * @property {string|number} [minWidth=""] - Min width for the editor.
112
117
  * @property {string|number} [maxWidth=""] - Max width for the editor.
118
+ * @property {string} [innerWidth=""] - Optional max-width for the editor body (`.se-wrapper`).
119
+ * - **CSS length** (`"740px"`, `"60ch"`): editor body's content is clamped to this width and centered horizontally; scroll container stays full width (scrollbar at the outer edge, wheel/touch active over the whole area).
120
+ * - **`"auto"`** or **empty string** (default): no constraint — natural full-width layout with default content padding.
121
+ * - In `blockHandle` mode, the gutter handle tracks the centered text band.
122
+ * ```js
123
+ * { innerWidth: '740px' }
124
+ * ```
113
125
  * @property {string|number} [height="auto"] - Height for the editor.
114
126
  * @property {string|number} [minHeight=""] - Min height for the editor.
115
127
  * @property {string|number} [maxHeight=""] - Max height for the editor.
@@ -287,6 +299,22 @@ export const DEFAULTS = {
287
299
  * === Modes & Themes ===
288
300
  * @property {boolean} [v2Migration=false] - Enables migration mode for SunEditor v2.
289
301
  * @property {"classic"|"inline"|"balloon"|"balloon-always"|"classic:bottom"|"inline:bottom"} [mode="classic"] - Toolbar mode: `classic`, `inline`, `balloon`, `balloon-always`. Append `:bottom` to place toolbar at the bottom (e.g. `classic:bottom`, `inline:bottom`).
302
+ * @property {Object} [blockHandle] - Block handle configuration. When provided, a per-line block handle UI is shown. Works independently of `mode`.
303
+ * @property {Array<string | { title: string, icon?: string, action: (deps: SunEditor.Deps, ctx: { block: HTMLElement }) => void }>} [blockHandle.menu] - Menu entries.
304
+ * - **String entries** — resolved via `ResolveButton`. Accepts the same values as toolbar `buttonList`:
305
+ * - **Block format keys**: `p`, `h1`-`h6`, `heading` (H1-H6 submenu), `ul`, `ol`, `list` (UL/OL submenu), `blockquote`, `pre`.
306
+ * - **Plugin names**: Any registered plugin name (e.g. `blockStyle`, `align`, `image`, `link`). Dropdown plugins display their items as a submenu. Modal plugins open the modal on click.
307
+ * - **Built-in commands**: `bold`, `italic`, `underline`, `strike`, `undo`, `redo`, etc.
308
+ * - **Custom item objects** — `{ title, icon?, action }`. `icon` is an icon key from `$.icons` or a raw `<svg>` HTML string; `action` is invoked with `($, { block })`.
309
+ * - Defaults to `['p', 'heading', 'list', 'blockquote', 'pre']`.
310
+ * ```js
311
+ * blockHandle: {
312
+ * menu: [
313
+ * 'p', 'heading', 'blockStyle',
314
+ * { title: 'Duplicate', icon: 'copy', action: ($, { block }) => block.after(block.cloneNode(true)) },
315
+ * ],
316
+ * }
317
+ * ```
290
318
  * @property {string} [type=""] - Editor type. Use `"document"` for a document-style layout, with optional sub-types after `:`.
291
319
  * ```js
292
320
  * // type
@@ -470,6 +498,15 @@ export const DEFAULTS = {
470
498
  * @property {boolean} [syncTabIndent=true] - Synchronizes tab indent with spaces.
471
499
  * @property {boolean} [tabDisable=false] - Disables tab key input.
472
500
  * @property {string} [toolbar_width="auto"] - Toolbar width.
501
+ * @property {string} [toolbar_innerWidth=""] - Center the toolbar's button row (`.se-btn-tray`) horizontally.
502
+ * - **CSS length** (e.g. `"500px"`, `"740px"`, `"60ch"`): toolbar background spans full width while the button row is capped at that width and centered. If buttons fit, the row stays on a single line; if they exceed the cap, they wrap inside it.
503
+ * - **`"auto"`**: no cap — the row is sized to its content and simply centered.
504
+ * - **Empty string** (default): no constraint.
505
+ * - **Note on `responsiveButtonList`**: thresholds compare against the toolbar's full width, not the band. When this option is set, calibrate `responsiveButtonList` keys around the toolbar size you actually want each button set to kick in at.
506
+ * ```js
507
+ * { toolbar_innerWidth: '740px' }
508
+ * { toolbar_innerWidth: 'auto' }
509
+ * ```
473
510
  * @property {?HTMLElement} [toolbar_container] - Container element for the toolbar.
474
511
  * @property {number|{top: number, offset: number}} [toolbar_sticky=0] - Enables sticky toolbar.
475
512
  * - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
@@ -604,6 +641,7 @@ export const DEFAULTS = {
604
641
  * @property {import('../../plugins/modal/link.js').LinkPluginOptions} [link]
605
642
  * @property {import('../../plugins/modal/math.js').MathPluginOptions} [math]
606
643
  * @property {import('../../plugins/dropdown/paragraphStyle.js').ParagraphStylePluginOptions} [paragraphStyle]
644
+ * @property {import('../../plugins/field/slashCommand.js').SlashCommandPluginOptions} [slashCommand]
607
645
  * @property {import('../../plugins/dropdown/table/index.js').TablePluginOptions} [table]
608
646
  * @property {import('../../plugins/dropdown/template.js').TemplatePluginOptions} [template]
609
647
  * @property {import('../../plugins/dropdown/textStyle.js').TextStylePluginOptions} [textStyle]
@@ -671,10 +709,12 @@ export const DEFAULTS = {
671
709
  export const OPTION_FRAME_FIXED_FLAG = {
672
710
  value: 'fixed',
673
711
  placeholder: true,
712
+ placeholder_line: true,
674
713
  editableFrameAttributes: true,
675
714
  width: true,
676
715
  minWidth: true,
677
716
  maxWidth: true,
717
+ innerWidth: 'fixed',
678
718
  height: true,
679
719
  minHeight: true,
680
720
  maxHeight: true,
@@ -706,6 +746,7 @@ export const OPTION_FIXED_FLAG = {
706
746
  v2Migration: 'fixed',
707
747
  strictMode: 'fixed',
708
748
  mode: 'fixed',
749
+ blockHandle: 'fixed',
709
750
  type: 'fixed',
710
751
  theme: true,
711
752
  lang: 'fixed',
@@ -752,6 +793,7 @@ export const OPTION_FIXED_FLAG = {
752
793
  formatClosureBlock: 'fixed',
753
794
  allowedEmptyTags: true,
754
795
  toolbar_width: true,
796
+ toolbar_innerWidth: 'fixed',
755
797
  toolbar_container: 'fixed',
756
798
  toolbar_sticky: true,
757
799
  toolbar_hide: true,