suneditor 3.0.0-rc.5 → 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 (118) hide show
  1. package/README.md +3 -2
  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 +2 -3
  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/html.js +110 -11
  21. package/src/core/logic/dom/offset.js +89 -35
  22. package/src/core/logic/dom/selection.js +46 -19
  23. package/src/core/logic/panel/finder.js +982 -0
  24. package/src/core/logic/panel/menu.js +8 -6
  25. package/src/core/logic/panel/toolbar.js +112 -19
  26. package/src/core/logic/panel/viewer.js +214 -43
  27. package/src/core/logic/shell/_commandExecutor.js +7 -1
  28. package/src/core/logic/shell/commandDispatcher.js +1 -1
  29. package/src/core/logic/shell/component.js +5 -7
  30. package/src/core/logic/shell/history.js +24 -0
  31. package/src/core/logic/shell/shortcuts.js +3 -3
  32. package/src/core/logic/shell/ui.js +25 -26
  33. package/src/core/schema/frameContext.js +15 -1
  34. package/src/core/schema/options.js +75 -16
  35. package/src/core/section/constructor.js +61 -20
  36. package/src/core/section/documentType.js +1 -1
  37. package/src/events.js +12 -0
  38. package/src/helper/clipboard.js +1 -1
  39. package/src/helper/dom/domUtils.js +5 -14
  40. package/src/helper/index.js +3 -0
  41. package/src/helper/markdown.js +876 -0
  42. package/src/langs/ckb.js +9 -0
  43. package/src/langs/cs.js +9 -0
  44. package/src/langs/da.js +9 -0
  45. package/src/langs/de.js +9 -0
  46. package/src/langs/en.js +9 -0
  47. package/src/langs/es.js +9 -0
  48. package/src/langs/fa.js +9 -0
  49. package/src/langs/fr.js +9 -0
  50. package/src/langs/he.js +9 -0
  51. package/src/langs/hu.js +9 -0
  52. package/src/langs/it.js +9 -0
  53. package/src/langs/ja.js +9 -0
  54. package/src/langs/km.js +9 -0
  55. package/src/langs/ko.js +9 -0
  56. package/src/langs/lv.js +9 -0
  57. package/src/langs/nl.js +9 -0
  58. package/src/langs/pl.js +9 -0
  59. package/src/langs/pt_br.js +9 -0
  60. package/src/langs/ro.js +9 -0
  61. package/src/langs/ru.js +9 -0
  62. package/src/langs/se.js +9 -0
  63. package/src/langs/tr.js +9 -0
  64. package/src/langs/uk.js +9 -0
  65. package/src/langs/ur.js +9 -0
  66. package/src/langs/zh_cn.js +9 -0
  67. package/src/modules/contract/Controller.js +50 -39
  68. package/src/modules/manager/ApiManager.js +27 -4
  69. package/src/modules/manager/FileManager.js +1 -1
  70. package/src/modules/ui/SelectMenu.js +22 -11
  71. package/src/plugins/command/codeBlock.js +324 -0
  72. package/src/plugins/command/exportPDF.js +15 -3
  73. package/src/plugins/dropdown/blockStyle.js +1 -1
  74. package/src/plugins/dropdown/paragraphStyle.js +1 -2
  75. package/src/plugins/dropdown/table/render/table.html.js +1 -1
  76. package/src/plugins/dropdown/table/services/table.grid.js +16 -8
  77. package/src/plugins/dropdown/table/services/table.style.js +5 -9
  78. package/src/plugins/index.js +3 -0
  79. package/src/plugins/input/fontSize.js +4 -2
  80. package/src/plugins/modal/audio.js +2 -1
  81. package/src/plugins/modal/image/index.js +2 -1
  82. package/src/plugins/modal/math.js +2 -1
  83. package/src/plugins/modal/video/index.js +2 -1
  84. package/src/themes/cobalt.css +13 -4
  85. package/src/themes/cream.css +11 -2
  86. package/src/themes/dark.css +13 -4
  87. package/src/themes/midnight.css +13 -4
  88. package/src/typedef.js +4 -4
  89. package/types/assets/icons/defaultIcons.d.ts +12 -1
  90. package/types/core/config/eventManager.d.ts +6 -8
  91. package/types/core/event/actions/index.d.ts +1 -0
  92. package/types/core/event/effects/keydown.registry.d.ts +2 -0
  93. package/types/core/event/eventOrchestrator.d.ts +2 -1
  94. package/types/core/kernel/coreKernel.d.ts +5 -0
  95. package/types/core/kernel/store.d.ts +5 -0
  96. package/types/core/logic/dom/offset.d.ts +16 -3
  97. package/types/core/logic/dom/selection.d.ts +3 -3
  98. package/types/core/logic/panel/finder.d.ts +83 -0
  99. package/types/core/logic/panel/toolbar.d.ts +14 -1
  100. package/types/core/logic/panel/viewer.d.ts +22 -2
  101. package/types/core/logic/shell/shortcuts.d.ts +1 -1
  102. package/types/core/schema/frameContext.d.ts +22 -0
  103. package/types/core/schema/options.d.ts +153 -31
  104. package/types/events.d.ts +11 -0
  105. package/types/helper/dom/domUtils.d.ts +2 -2
  106. package/types/helper/index.d.ts +5 -0
  107. package/types/helper/markdown.d.ts +27 -0
  108. package/types/langs/_Lang.d.ts +9 -0
  109. package/types/modules/contract/Controller.d.ts +8 -1
  110. package/types/modules/ui/SelectMenu.d.ts +12 -0
  111. package/types/plugins/command/codeBlock.d.ts +53 -0
  112. package/types/plugins/index.d.ts +3 -0
  113. package/types/plugins/input/fontSize.d.ts +6 -2
  114. package/types/plugins/modal/audio.d.ts +4 -2
  115. package/types/plugins/modal/image/index.d.ts +3 -1
  116. package/types/plugins/modal/math.d.ts +3 -1
  117. package/types/plugins/modal/video/index.d.ts +3 -1
  118. package/types/typedef.d.ts +5 -2
@@ -18,6 +18,8 @@ class Menu {
18
18
  #bindClose_dropdown_mouse = null;
19
19
  #bindClose_dropdown_key = null;
20
20
  #bindClose_cons_mouse = null;
21
+ #bindMenu_mousemove = null;
22
+ #bindMenu_mouseout = null;
21
23
  #menuBtn = null;
22
24
  #menuContainer = null;
23
25
 
@@ -114,8 +116,8 @@ class Menu {
114
116
  this.menus = converter.nodeListToArray(menu.querySelectorAll('[data-command]'));
115
117
  if (this.menus.length > 0) {
116
118
  this.#bindClose_dropdown_key = this.#eventManager.addGlobalEvent('keydown', this.#globalEventHandler.keydown, false);
117
- menu.addEventListener('mousemove', this.#globalEventHandler.mousemove, false);
118
- menu.addEventListener('mouseout', this.#globalEventHandler.mouseout, false);
119
+ this.#bindMenu_mousemove = this.#eventManager.addEvent(menu, 'mousemove', this.#globalEventHandler.mousemove, false);
120
+ this.#bindMenu_mouseout = this.#eventManager.addEvent(menu, 'mouseout', this.#globalEventHandler.mouseout, false);
119
121
  }
120
122
  }
121
123
 
@@ -220,7 +222,7 @@ class Menu {
220
222
  * @param {HTMLElement} menu Menu element
221
223
  */
222
224
  __resetMenuPosition(element, menu) {
223
- this.#$.offset.setRelPosition(menu, this.#contextProvider.carrierWrapper, element.parentElement, dom.query.getParentElement(element, '.se-toolbar'));
225
+ this.#$.offset.setRelPosition(menu, this.#contextProvider.carrierWrapper, element.parentElement, dom.query.getParentElement(element, '.se-toolbar'), { preferUp: this.#store.mode.isBottom });
224
226
  }
225
227
 
226
228
  /**
@@ -243,7 +245,7 @@ class Menu {
243
245
  menu.style.height = '';
244
246
  dom.utils.addClass(element.parentElement.children, 'on');
245
247
 
246
- this.#$.offset.setRelPosition(menu, this.#contextProvider.carrierWrapper, element.parentElement, dom.query.getParentElement(element, '.se-toolbar'));
248
+ this.#$.offset.setRelPosition(menu, this.#contextProvider.carrierWrapper, element.parentElement, dom.query.getParentElement(element, '.se-toolbar'), { preferUp: this.#store.mode.isBottom });
247
249
 
248
250
  menu.style.visibility = '';
249
251
 
@@ -293,10 +295,10 @@ class Menu {
293
295
  this.#bindClose_cons_mouse &&= this.#eventManager.removeGlobalEvent(this.#bindClose_cons_mouse);
294
296
  if (this.#bindClose_dropdown_key) {
295
297
  this.#bindClose_dropdown_key = this.#eventManager.removeGlobalEvent(this.#bindClose_dropdown_key);
298
+ this.#bindMenu_mousemove &&= this.#eventManager.removeEvent(this.#bindMenu_mousemove);
299
+ this.#bindMenu_mouseout &&= this.#eventManager.removeEvent(this.#bindMenu_mouseout);
296
300
  dom.utils.removeClass(this.menus, 'on');
297
301
  dom.utils.removeClass(this.currentDropdown, 'se-select-menu-key-action|se-select-menu-mouse-move');
298
- this.currentDropdown.removeEventListener('mousemove', this.#globalEventHandler.mousemove, false);
299
- this.currentDropdown.removeEventListener('mouseout', this.#globalEventHandler.mouseout, false);
300
302
  }
301
303
  }
302
304
 
@@ -26,6 +26,9 @@ class Toolbar {
26
26
  #rButtonsInfo = null;
27
27
  #rButtonsize = null;
28
28
 
29
+ #useCSSSticky = false;
30
+ #_isStickyFlag = false;
31
+
29
32
  /**
30
33
  * @constructor
31
34
  * @param {SunEditor.Kernel} kernel
@@ -65,7 +68,6 @@ class Toolbar {
65
68
  };
66
69
 
67
70
  this.currentMoreLayerActiveButton = null;
68
- this.isSticky = false;
69
71
  this.isBalloonMode = balloon;
70
72
  this.isInlineMode = inline;
71
73
  this.isBalloonAlwaysMode = balloonAlways;
@@ -84,9 +86,63 @@ class Toolbar {
84
86
  this.#rButtonArray = res;
85
87
  this.#isViewPortSize = 'visualViewport' in _w;
86
88
 
89
+ // CSS sticky: non-balloon, non-inline, non-container, sticky enabled
90
+ const isStickyPosible = !this.isSub && !balloon && !inline;
91
+ const stickyTop = this.#options.get('toolbar_sticky');
92
+
93
+ this.#useCSSSticky = isStickyPosible && stickyTop >= 0 && !this.#options.get('toolbar_container') && typeof CSS !== 'undefined' && CSS.supports('position', 'sticky');
94
+
95
+ this.isBottomMode = this.#store.mode.isBottom;
96
+
97
+ if (this.#useCSSSticky) {
98
+ // CSS sticky: browser handles positioning natively
99
+ const toolbar = this.#context.get(this.keyName.main);
100
+ if (this.isBottomMode) {
101
+ toolbar.style.bottom = stickyTop + 'px';
102
+ toolbar.style.top = 'auto';
103
+ } else if (stickyTop > 0) {
104
+ toolbar.style.top = stickyTop + 'px';
105
+ }
106
+ } else if (isStickyPosible) {
107
+ // JS fallback (toolbar_container) or sticky disabled (-1):
108
+ dom.utils.addClass(this.#context.get(this.keyName.main), 'se-toolbar-relative');
109
+ }
110
+
87
111
  this._setResponsive();
88
112
  }
89
113
 
114
+ /**
115
+ * @description Whether the toolbar is currently in a sticky (fixed) state.
116
+ * For CSS sticky mode, computed from the element's viewport position.
117
+ * For JS sticky mode (toolbar_container), uses a manual flag.
118
+ * @type {boolean}
119
+ */
120
+ get isSticky() {
121
+ if (this.isSub) return false;
122
+ const stickyTop = this.#options.get('toolbar_sticky');
123
+ if (stickyTop < 0) return false;
124
+
125
+ if (this.#useCSSSticky) {
126
+ const toolbar = this.#context.get(this.keyName.main);
127
+ if (!toolbar || toolbar.offsetWidth === 0 || toolbar.style.display === 'none') return false;
128
+ if (this.isBottomMode) {
129
+ return toolbar.getBoundingClientRect().bottom >= _w.innerHeight - stickyTop - 1;
130
+ }
131
+ return toolbar.getBoundingClientRect().top <= stickyTop + 1;
132
+ }
133
+
134
+ return this.#_isStickyFlag;
135
+ }
136
+
137
+ /**
138
+ * @description Whether the toolbar uses native CSS `position: sticky`.
139
+ * - When `false`, the JS-based sticky fallback (`position: fixed`) is active.
140
+ * @type {boolean}
141
+ */
142
+ get isCSSSticky() {
143
+ return this.#useCSSSticky;
144
+ }
145
+
90
146
  /**
91
147
  * @description Disables all toolbar buttons.
92
148
  */
@@ -198,6 +254,8 @@ class Toolbar {
198
254
  * @description Reset the sticky toolbar position based on the editor state.
199
255
  */
200
256
  _resetSticky() {
257
+ if (this.#useCSSSticky) return;
258
+
201
259
  const wrapper = this.#frameContext.get('wrapper');
202
260
  if (!wrapper) return;
203
261
 
@@ -210,18 +268,36 @@ class Toolbar {
210
268
  const minHeight = this.#frameContext.get('_minHeight');
211
269
  const editorHeight = wrapper.offsetHeight;
212
270
  const editorOffset = this.#$.offset.getGlobal(this.#frameContext.get('topArea'));
213
- const y = currentScrollY + stickyTop;
214
- const t = (this.isBalloonMode || this.isInlineMode ? editorOffset.top : this.#$.offset.getGlobal(this.#options.get('toolbar_container')).top) - (this.isInlineMode ? toolbar.offsetHeight : 0);
215
271
  const inlineOffset = 1;
216
272
 
217
- const offSticky = !this.#options.get('toolbar_container') ? editorHeight + t + stickyTop - y - minHeight : editorOffset.top - currentScrollY + editorHeight - minHeight - stickyTop - toolbar.offsetHeight;
218
- if (y < t) {
219
- this.#offSticky();
220
- } else if (offSticky < 0) {
221
- if (!this.isSticky) this.#onSticky(inlineOffset);
222
- toolbar.style.top = inlineOffset + offSticky + this.#getViewportTop() + 'px';
273
+ if (this.isBottomMode) {
274
+ const viewportBottom = currentScrollY + _w.innerHeight;
275
+ const editorBottom = editorOffset.top + editorHeight;
276
+ const y = viewportBottom - stickyTop - (this.isInlineMode ? toolbar.offsetHeight : 0);
277
+
278
+ const offSticky = !this.#options.get('toolbar_container') ? y - editorOffset.top - minHeight : viewportBottom - stickyTop - editorOffset.top - minHeight - toolbar.offsetHeight;
279
+ if (y > editorBottom) {
280
+ this.#offSticky();
281
+ } else if (offSticky < 0) {
282
+ if (!this.isSticky) this.#onSticky(inlineOffset);
283
+ toolbar.style.bottom = inlineOffset + offSticky + this.#getViewportTop() + 'px';
284
+ toolbar.style.top = 'auto';
285
+ } else {
286
+ this.#onSticky(inlineOffset);
287
+ }
223
288
  } else {
224
- this.#onSticky(inlineOffset);
289
+ const y = currentScrollY + stickyTop;
290
+ const t = (this.isBalloonMode || this.isInlineMode ? editorOffset.top : this.#$.offset.getGlobal(this.#options.get('toolbar_container')).top) - (this.isInlineMode ? toolbar.offsetHeight : 0);
291
+
292
+ const offSticky = !this.#options.get('toolbar_container') ? editorHeight + t + stickyTop - y - minHeight : editorOffset.top - currentScrollY + editorHeight - minHeight - stickyTop - toolbar.offsetHeight;
293
+ if (y < t) {
294
+ this.#offSticky();
295
+ } else if (offSticky < 0) {
296
+ if (!this.isSticky) this.#onSticky(inlineOffset);
297
+ toolbar.style.top = inlineOffset + offSticky + this.#getViewportTop() + 'px';
298
+ } else {
299
+ this.#onSticky(inlineOffset);
300
+ }
225
301
  }
226
302
  }
227
303
 
@@ -229,7 +305,7 @@ class Toolbar {
229
305
  * @description Reset the common buttons info.
230
306
  */
231
307
  #resetButtonInfo() {
232
- this.#$.shortcuts._registerCustomShortcuts();
308
+ this.#$.shortcuts._registerShortcuts();
233
309
  this.#$.commandDispatcher.resetTargets();
234
310
  this.#$.ui._initToggleButtons();
235
311
 
@@ -350,12 +426,17 @@ class Toolbar {
350
426
 
351
427
  const toolbar = this.#context.get(this.keyName.main);
352
428
  toolbar.style.visibility = 'hidden';
429
+ toolbar.style.display = 'block';
353
430
  this.#offSticky();
354
431
 
355
- toolbar.style.display = 'block';
356
432
  toolbar.style.top = '0px';
357
433
  this.inlineToolbarAttr.width = toolbar.style.width = this.#options.get(this.keyName.width);
358
- this.inlineToolbarAttr.top = toolbar.style.top = -1 + (this.#$.offset.getGlobal(this.#frameContext.get('topArea')).top - this.#$.offset.getGlobal(toolbar).top - toolbar.offsetHeight) + 'px';
434
+ if (this.isBottomMode) {
435
+ const topArea = this.#frameContext.get('topArea');
436
+ this.inlineToolbarAttr.top = toolbar.style.top = this.#$.offset.getGlobal(topArea).top + topArea.offsetHeight - this.#$.offset.getGlobal(toolbar).top + 'px';
437
+ } else {
438
+ this.inlineToolbarAttr.top = toolbar.style.top = -1 + (this.#$.offset.getGlobal(this.#frameContext.get('topArea')).top - this.#$.offset.getGlobal(toolbar).top - toolbar.offsetHeight) + 'px';
439
+ }
359
440
 
360
441
  this._resetSticky();
361
442
  this.inlineToolbarAttr.isShow = true;
@@ -403,11 +484,17 @@ class Toolbar {
403
484
  stickyDummy.style.display = 'block';
404
485
  }
405
486
 
406
- const toolbarTopPosition = this.#options.get('toolbar_sticky') + inlineOffset + this.#getViewportTop();
407
- toolbar.style.top = `${toolbarTopPosition}px`;
487
+ if (this.isBottomMode) {
488
+ const toolbarBottomPosition = this.#options.get('toolbar_sticky') + this.#getViewportTop();
489
+ toolbar.style.bottom = `${toolbarBottomPosition}px`;
490
+ toolbar.style.top = 'auto';
491
+ } else {
492
+ const toolbarTopPosition = this.#options.get('toolbar_sticky') + inlineOffset + this.#getViewportTop();
493
+ toolbar.style.top = `${toolbarTopPosition}px`;
494
+ }
408
495
  toolbar.style.width = this.isInlineMode ? this.inlineToolbarAttr.width : toolbar.offsetWidth + 'px';
409
496
  dom.utils.addClass(toolbar, 'se-toolbar-sticky');
410
- this.isSticky = true;
497
+ this.#_isStickyFlag = true;
411
498
  }
412
499
 
413
500
  /**
@@ -429,12 +516,18 @@ class Toolbar {
429
516
  stickyDummy.style.display = 'none';
430
517
 
431
518
  const toolbar = this.#context.get(this.keyName.main);
432
- toolbar.style.top = this.isInlineMode ? this.inlineToolbarAttr.top : '';
519
+ if (this.isBottomMode) {
520
+ toolbar.style.bottom = this.isInlineMode ? `${-toolbar.offsetHeight}px` : '';
521
+ toolbar.style.top = 'auto';
522
+ this.#frameContext.get('wrapper').style.marginBottom = '';
523
+ } else {
524
+ toolbar.style.top = this.isInlineMode ? this.inlineToolbarAttr.top : '';
525
+ this.#frameContext.get('wrapper').style.marginTop = '';
526
+ }
433
527
  toolbar.style.width = this.isInlineMode ? this.inlineToolbarAttr.width : '';
434
- this.#frameContext.get('wrapper').style.marginTop = '';
435
528
 
436
529
  dom.utils.removeClass(toolbar, 'se-toolbar-sticky');
437
- this.isSticky = false;
530
+ this.#_isStickyFlag = false;
438
531
  }
439
532
 
440
533
  /**
@@ -1,4 +1,4 @@
1
- import { dom, env, converter, numbers } from '../../../helper';
1
+ import { dom, env, converter, markdown } from '../../../helper';
2
2
 
3
3
  const { _w, _d } = env;
4
4
 
@@ -27,6 +27,9 @@ class Viewer {
27
27
  #codeWrapperOriginCssText = '';
28
28
  #codeOriginCssText = '';
29
29
  #codeNumberOriginCssText = '';
30
+ #markdownWrapperOriginCssText = '';
31
+ #markdownOriginCssText = '';
32
+ #markdownNumberOriginCssText = '';
30
33
  #toolbarOriginCssText = '';
31
34
  #arrowOriginCssText = '';
32
35
  #fullScreenInnerHeight = 0;
@@ -64,9 +67,15 @@ class Viewer {
64
67
  */
65
68
  codeView(value) {
66
69
  const fc = this.#frameContext;
70
+ if (!fc.get('codeWrapper')) return;
67
71
  if (value === undefined) value = !fc.get('isCodeView');
68
72
  if (value === fc.get('isCodeView')) return;
69
73
 
74
+ // Mutual exclusivity with markdown view
75
+ if (value && fc.get('isMarkdownView')) {
76
+ this.markdownView(false);
77
+ }
78
+
70
79
  fc.set('isCodeView', value);
71
80
  this.#$.ui.offCurrentController();
72
81
  this.#$.ui.offCurrentModal();
@@ -77,6 +86,7 @@ class Viewer {
77
86
  const wrapper = fc.get('wrapper');
78
87
 
79
88
  if (value) {
89
+ this.#$.finder.close();
80
90
  this.#setEditorDataToCodeView();
81
91
  codeWrapper.style.setProperty('display', 'flex', 'important');
82
92
  wysiwygFrame.style.display = 'none';
@@ -111,7 +121,7 @@ class Viewer {
111
121
  this.#store.set('_range', null);
112
122
  codeFrame.focus();
113
123
  dom.utils.addClass(this.#$.commandDispatcher.targets.get('codeView'), 'active');
114
- dom.utils.addClass(wrapper, 'se-code-view-status');
124
+ dom.utils.addClass(wrapper, 'se-source-view-status');
115
125
  } else {
116
126
  if (!dom.check.isNonEditable(wysiwygFrame)) this.#setCodeDataToEditor();
117
127
  wysiwygFrame.scrollTop = 0;
@@ -137,7 +147,7 @@ class Viewer {
137
147
  this.#$.history.push(false);
138
148
  this.#$.history.resetButtons(fc.get('key'), null);
139
149
  }
140
- dom.utils.removeClass(wrapper, 'se-code-view-status');
150
+ dom.utils.removeClass(wrapper, 'se-source-view-status');
141
151
  }
142
152
 
143
153
  this.#$.ui._updatePlaceholder(fc);
@@ -157,6 +167,108 @@ class Viewer {
157
167
  this.#eventManager.triggerEvent('onToggleCodeView', { frameContext: fc, is: fc.get('isCodeView') });
158
168
  }
159
169
 
170
+ /**
171
+ * @description Changes to markdown view or wysiwyg view
172
+ * @param {boolean} [value] `true`/`false`, If `undefined` toggle the `markdownView` mode.
173
+ */
174
+ markdownView(value) {
175
+ const fc = this.#frameContext;
176
+ if (!fc.get('markdownWrapper')) return;
177
+ if (value === undefined) value = !fc.get('isMarkdownView');
178
+ if (value === fc.get('isMarkdownView')) return;
179
+
180
+ // Mutual exclusivity with code view
181
+ if (value && fc.get('isCodeView')) {
182
+ this.codeView(false);
183
+ }
184
+
185
+ fc.set('isMarkdownView', value);
186
+ this.#$.ui.offCurrentController();
187
+ this.#$.ui.offCurrentModal();
188
+
189
+ const markdownWrapper = fc.get('markdownWrapper');
190
+ const markdownFrame = fc.get('markdown');
191
+ const wysiwygFrame = fc.get('wysiwygFrame');
192
+ const wrapper = fc.get('wrapper');
193
+
194
+ if (value) {
195
+ this.#$.finder.close();
196
+ this.#setEditorDataToMarkdownView();
197
+ markdownWrapper.style.setProperty('display', 'flex', 'important');
198
+ wysiwygFrame.style.display = 'none';
199
+
200
+ if (fc.get('isFullScreen')) {
201
+ markdownFrame.style.height = '100%';
202
+ } else if (this.#frameOptions.get('height') === 'auto') {
203
+ markdownFrame.style.height = markdownFrame.scrollHeight > 0 ? markdownFrame.scrollHeight + 'px' : 'auto';
204
+ }
205
+
206
+ if (!fc.get('isFullScreen')) {
207
+ this.#$.ui.preventToolbarHide(true);
208
+ if (this.#store.mode.isBalloon) {
209
+ this.#context.get('toolbar_arrow').style.display = 'none';
210
+ this.#context.get('toolbar_main').style.left = '';
211
+ this.#store.mode.isInline = this.#$.toolbar.isInlineMode = true;
212
+ this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = false;
213
+ this.#$.toolbar._showInline();
214
+ }
215
+ }
216
+
217
+ if (this.#store.mode.isBalloon) {
218
+ this.#$.subToolbar.hide();
219
+ }
220
+
221
+ CreateLineNumbers(fc, 'markdown');
222
+
223
+ this.#store.set('_range', null);
224
+ markdownFrame.focus();
225
+ dom.utils.addClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
226
+ dom.utils.addClass(wrapper, 'se-source-view-status');
227
+ } else {
228
+ if (!dom.check.isNonEditable(wysiwygFrame)) this.#setMarkdownDataToEditor();
229
+ wysiwygFrame.scrollTop = 0;
230
+ markdownWrapper.style.setProperty('display', 'none', 'important');
231
+ wysiwygFrame.style.display = 'block';
232
+
233
+ if (this.#frameOptions.get('height') === 'auto') markdownFrame.style.height = '0px';
234
+
235
+ if (!fc.get('isFullScreen')) {
236
+ this.#$.ui.preventToolbarHide(false);
237
+ if (/balloon/.test(this.#options.get('mode'))) {
238
+ this.#context.get('toolbar_arrow').style.display = '';
239
+ this.#store.mode.isInline = this.#$.toolbar.isInlineMode = false;
240
+ this.#store.mode.isBalloon = this.#$.toolbar.isBalloonMode = true;
241
+ this.#kernel._eventOrchestrator._hideToolbar();
242
+ }
243
+ }
244
+
245
+ this.#$.focusManager.nativeFocus();
246
+ dom.utils.removeClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
247
+
248
+ if (!dom.check.isNonEditable(wysiwygFrame)) {
249
+ this.#$.history.push(false);
250
+ this.#$.history.resetButtons(fc.get('key'), null);
251
+ }
252
+ dom.utils.removeClass(wrapper, 'se-source-view-status');
253
+ }
254
+
255
+ this.#$.ui._updatePlaceholder(fc);
256
+ this.#$.ui._toggleCodeViewButtons(value);
257
+
258
+ // document type
259
+ if (fc.has('documentType_use_header')) {
260
+ if (value) {
261
+ fc.get('documentTypeInner').style.display = 'none';
262
+ } else {
263
+ fc.get('documentTypeInner').style.display = '';
264
+ fc.get('documentType').reHeader();
265
+ }
266
+ }
267
+
268
+ // user event
269
+ this.#eventManager.triggerEvent('onToggleMarkdownView', { frameContext: fc, is: fc.get('isMarkdownView') });
270
+ }
271
+
160
272
  /**
161
273
  * @description Changes to full screen or default screen
162
274
  * @param {boolean} [value] `true`/`false`, If `undefined` toggle the `fullScreen` mode.
@@ -174,7 +286,11 @@ class Viewer {
174
286
  const codeWrapper = fc.get('codeWrapper');
175
287
  const code = fc.get('code');
176
288
  const codeNumbers = fc.get('codeNumbers');
289
+ const markdownWrapper = fc.get('markdownWrapper');
290
+ const markdownFrame = fc.get('markdown');
291
+ const markdownNumbers = fc.get('markdownNumbers');
177
292
  const isCodeView = this.#frameContext.get('isCodeView');
293
+ const isMarkdownView = this.#frameContext.get('isMarkdownView');
178
294
  const arrow = this.#context.get('toolbar_arrow');
179
295
 
180
296
  this.#$.ui.offCurrentController();
@@ -187,6 +303,9 @@ class Viewer {
187
303
  this.#codeWrapperOriginCssText = codeWrapper.style.cssText;
188
304
  this.#codeOriginCssText = code.style.cssText;
189
305
  this.#codeNumberOriginCssText = codeNumbers?.style.cssText;
306
+ this.#markdownWrapperOriginCssText = markdownWrapper?.style.cssText;
307
+ this.#markdownOriginCssText = markdownFrame?.style.cssText;
308
+ this.#markdownNumberOriginCssText = markdownNumbers?.style.cssText;
190
309
  this.#toolbarOriginCssText = toolbar.style.cssText;
191
310
  if (arrow) this.#arrowOriginCssText = arrow.style.cssText;
192
311
 
@@ -222,15 +341,23 @@ class Viewer {
222
341
 
223
342
  // frame
224
343
  editorArea.style.cssText = toolbar.style.cssText = '';
225
- wysiwygFrame.style.cssText = (wysiwygFrame.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + this.#frameOptions.get('_defaultStyles').editor + (isCodeView ? 'display: none;' : '');
344
+ wysiwygFrame.style.cssText = (wysiwygFrame.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + this.#frameOptions.get('_defaultStyles').editor + (isCodeView || isMarkdownView ? 'display: none;' : '');
226
345
 
227
346
  // code wrapper
228
347
  codeWrapper.style.cssText = (codeWrapper.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
229
348
  codeWrapper.style.overflow = 'auto';
230
349
  codeWrapper.style.height = '100%';
231
350
 
351
+ // markdown wrapper
352
+ if (markdownWrapper) {
353
+ markdownWrapper.style.cssText = (markdownWrapper.style.cssText.match(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/) || [''])[0] + `display: ${!isMarkdownView ? 'none' : 'flex'} !important;`;
354
+ markdownWrapper.style.overflow = 'auto';
355
+ markdownWrapper.style.height = '100%';
356
+ }
357
+
232
358
  // code
233
359
  code.style.height = '';
360
+ if (markdownFrame) markdownFrame.style.height = '';
234
361
 
235
362
  // toolbar, editor area
236
363
  toolbar.style.width = wysiwygFrame.style.height = '100%';
@@ -254,7 +381,7 @@ class Viewer {
254
381
  });
255
382
  } else {
256
383
  // frame
257
- wysiwygFrame.style.cssText = this.#wysiwygOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + (isCodeView ? 'display: none;' : '');
384
+ wysiwygFrame.style.cssText = this.#wysiwygOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + (isCodeView || isMarkdownView ? 'display: none;' : '');
258
385
 
259
386
  // code wrapper
260
387
  codeWrapper.style.cssText = this.#codeWrapperOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + `display: ${!isCodeView ? 'none' : 'flex'} !important;`;
@@ -263,6 +390,13 @@ class Viewer {
263
390
  code.style.cssText = this.#codeOriginCssText;
264
391
  if (codeNumbers) codeNumbers.style.cssText = this.#codeNumberOriginCssText;
265
392
 
393
+ // markdown wrapper
394
+ if (markdownWrapper) {
395
+ markdownWrapper.style.cssText = this.#markdownWrapperOriginCssText.replace(/\s?display(\s+)?:(\s+)?[a-zA-Z]+;/, '') + `display: ${!isMarkdownView ? 'none' : 'flex'} !important;`;
396
+ }
397
+ if (markdownFrame) markdownFrame.style.cssText = this.#markdownOriginCssText;
398
+ if (markdownNumbers) markdownNumbers.style.cssText = this.#markdownNumberOriginCssText;
399
+
266
400
  // toolbar, editor area
267
401
  toolbar.style.cssText = this.#toolbarOriginCssText;
268
402
  editorArea.style.cssText = this.#editorAreaOriginCssText;
@@ -304,7 +438,7 @@ class Viewer {
304
438
  });
305
439
  }
306
440
 
307
- if (wasToolbarHidden && !fc.get('isCodeView')) this.#$.toolbar.hide();
441
+ if (wasToolbarHidden && !fc.get('isCodeView') && !fc.get('isMarkdownView')) this.#$.toolbar.hide();
308
442
 
309
443
  // user event
310
444
  this.#eventManager.triggerEvent('onToggleFullScreen', { frameContext: fc, is: fc.get('isFullScreen') });
@@ -359,6 +493,13 @@ class Viewer {
359
493
  });
360
494
  }
361
495
 
496
+ // markdownView
497
+ if (fc.get('isMarkdownView')) {
498
+ dom.utils.addClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
499
+ } else {
500
+ dom.utils.removeClass(this.#$.commandDispatcher.targets.get('markdownView'), 'active');
501
+ }
502
+
362
503
  // showBlocks
363
504
  if (fc.get('isShowBlocks')) {
364
505
  dom.utils.addClass(this.#$.commandDispatcher.targets.get('showBlocks'), 'active');
@@ -582,7 +723,7 @@ class Viewer {
582
723
  * @internal
583
724
  * @description Adjusts the height of the code view area.
584
725
  * - Ensures the code block `auto`-resizes based on its content.
585
- * @param {HTMLElement} code - Code area
726
+ * @param {HTMLTextAreaElement} code - Code area
586
727
  * @param {HTMLTextAreaElement} codeNumbers - Code numbers area
587
728
  * @param {boolean} isAuto - `auto` height option
588
729
  */
@@ -603,6 +744,29 @@ class Viewer {
603
744
  codeNumbers.scrollLeft = this.scrollLeft;
604
745
  }
605
746
 
747
+ /**
748
+ * @internal
749
+ * @description Adjusts the height of the markdown view area.
750
+ * @param {HTMLTextAreaElement} md - Markdown area
751
+ * @param {HTMLTextAreaElement} mdNumbers - Markdown numbers area
752
+ * @param {boolean} isAuto - `auto` height option
753
+ */
754
+ _markdownViewAutoHeight(md, mdNumbers, isAuto) {
755
+ if (isAuto) md.style.height = md.scrollHeight + 'px';
756
+ this.#updateLineNumbers(mdNumbers, md);
757
+ }
758
+
759
+ /**
760
+ * @internal
761
+ * @this {HTMLElement} Markdown numbers area
762
+ * @description Synchronizes scrolling of line numbers with the markdown editor.
763
+ * @param {HTMLTextAreaElement} mdNumbers - Markdown numbers textarea
764
+ */
765
+ _scrollMarkdownLineNumbers(mdNumbers) {
766
+ mdNumbers.scrollTop = this.scrollTop;
767
+ mdNumbers.scrollLeft = this.scrollLeft;
768
+ }
769
+
606
770
  /**
607
771
  * @description Convert the data of the code view and put it in the `WYSIWYG` area.
608
772
  */
@@ -668,25 +832,44 @@ class Viewer {
668
832
  this._setCodeView(codeValue);
669
833
  }
670
834
 
835
+ /**
836
+ * @description Convert the data of the `WYSIWYG` area and put it in the markdown view area.
837
+ */
838
+ #setEditorDataToMarkdownView() {
839
+ const json = converter.htmlToJson(this.#frameContext.get('wysiwyg').innerHTML);
840
+ const md = markdown.jsonToMarkdown(json);
841
+ this.#frameContext.get('markdown').value = md;
842
+ }
843
+
844
+ /**
845
+ * @description Convert the data of the markdown view and put it in the `WYSIWYG` area.
846
+ */
847
+ #setMarkdownDataToEditor() {
848
+ const md = this.#frameContext.get('markdown').value;
849
+ const html = markdown.markdownToHtml(md, this.#options.get('defaultLine'));
850
+
851
+ this.#frameContext.get('wysiwyg').innerHTML =
852
+ html.length > 0 ? this.#$.html.clean(html, { forceFormat: true, whitelist: null, blacklist: null }) : '<' + this.#options.get('defaultLine') + '><br></' + this.#options.get('defaultLine') + '>';
853
+ }
854
+
671
855
  /**
672
856
  * @description Updates the line numbers for the code editor.
673
857
  * - Dynamically adjusts line numbers as content grows.
674
858
  * @param {HTMLTextAreaElement} lineNumbers - Code numbers area
675
- * @param {HTMLElement} code - Code area
859
+ * @param {HTMLTextAreaElement} code - Code area
676
860
  */
677
861
  #updateLineNumbers(lineNumbers, code) {
678
862
  if (!lineNumbers) return;
679
863
 
680
- const lineHeight = GetLineHeight(lineNumbers);
681
- const numberOfLinesNeeded = Math.ceil(code.scrollHeight / lineHeight);
682
-
864
+ const numberOfLinesNeeded = (code.value.match(/\n/g) || []).length + 1;
683
865
  const currentLineCount = (lineNumbers.value.match(/\n/g) || []).length;
684
- if (numberOfLinesNeeded > currentLineCount) {
866
+
867
+ if (numberOfLinesNeeded !== currentLineCount) {
685
868
  let n = '';
686
- for (let i = currentLineCount + 1; i <= numberOfLinesNeeded; i++) {
869
+ for (let i = 1; i <= numberOfLinesNeeded; i++) {
687
870
  n += `${i}\n`;
688
871
  }
689
- lineNumbers.value += n;
872
+ lineNumbers.value = n;
690
873
  }
691
874
  }
692
875
 
@@ -700,44 +883,32 @@ class Viewer {
700
883
  }
701
884
 
702
885
  /**
703
- * @description Create line numbers for the code view area
886
+ * @description Create line numbers for the code/markdown view area
704
887
  * @param {SunEditor.FrameContext} fc - Frame context
888
+ * @param {"code"|"markdown"} [type="code"] - View type
705
889
  */
706
- function CreateLineNumbers(fc) {
707
- const codeNumbers = fc.get('codeNumbers');
708
- if (!codeNumbers) return;
890
+ function CreateLineNumbers(fc, type) {
891
+ const numbersKey = type === 'markdown' ? 'markdownNumbers' : 'codeNumbers';
892
+ const contentKey = type === 'markdown' ? 'markdown' : 'code';
893
+ const lineNumbers = fc.get(numbersKey);
894
+ if (!lineNumbers) return;
709
895
 
710
- const lineHeight = GetLineHeight(codeNumbers);
711
- const numberOfLines = fc.get('code').scrollHeight / lineHeight;
896
+ const content = fc.get(contentKey);
897
+ const numberOfLines = (content.value.match(/\n/g) || []).length + 1;
712
898
 
713
899
  let n = '';
714
900
  for (let i = 1; i <= numberOfLines; i++) {
715
901
  n += `${i}\n`;
716
902
  }
717
903
 
718
- const { padding, margin } = _w.getComputedStyle(fc.get('code'));
719
- codeNumbers.value = n;
720
- codeNumbers.style.padding = padding || '';
721
- codeNumbers.style.margin = margin || '';
722
- }
723
-
724
- /**
725
- * @description Get the `line-height` of the textarea
726
- * @param {HTMLTextAreaElement} textarea Textarea element
727
- * @returns {number}
728
- */
729
- function GetLineHeight(textarea) {
730
- const lineHeight = _w.getComputedStyle(textarea).lineHeight;
731
- let lineHeightMatch;
732
-
733
- if (!numbers.is(lineHeight)) {
734
- const fontSize = _w.getComputedStyle(textarea).fontSize;
735
- lineHeightMatch = numbers.get(fontSize) * 1.2;
736
- } else {
737
- lineHeightMatch = numbers.get(lineHeight);
738
- }
739
-
740
- return lineHeightMatch;
904
+ // Sync font and line-height for accurate scroll alignment
905
+ const contentStyle = _w.getComputedStyle(content);
906
+ lineNumbers.style.lineHeight = contentStyle.lineHeight;
907
+ lineNumbers.style.fontSize = contentStyle.fontSize;
908
+ lineNumbers.style.fontFamily = contentStyle.fontFamily;
909
+ lineNumbers.style.paddingTop = contentStyle.paddingTop;
910
+ lineNumbers.style.paddingBottom = contentStyle.paddingBottom;
911
+ lineNumbers.value = n;
741
912
  }
742
913
 
743
914
  export default Viewer;