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
package/src/langs/tr.js CHANGED
@@ -126,6 +126,7 @@
126
126
  link_modal_url: "Bağlantı URL'si",
127
127
  link_modal_relAttribute: 'Rel niteliği',
128
128
  list: 'Liste',
129
+ markdownView: 'Markdown görünümü',
129
130
  math: 'Matematik',
130
131
  math_modal_fontSizeLabel: 'Yazı Tipi Boyutu',
131
132
  math_modal_inputLabel: 'Matematiksel Simgeler',
@@ -187,6 +188,7 @@
187
188
  tableProperties: 'Tablo özellikleri',
188
189
  tags: 'Etiketler',
189
190
  tag_blockquote: 'Alıntı',
191
+ codeBlock: 'Kod Bloğu',
190
192
  tag_div: 'Normal (DIV)',
191
193
  tag_h: 'Başlık',
192
194
  tag_p: 'Paragraf',
@@ -205,6 +207,13 @@
205
207
  video_modal_title: 'Video Ekle',
206
208
  video_modal_url: "Medya Ekleme URL'si (YouTube/Vimeo)",
207
209
  width: 'Genişlik',
210
+ codeLanguage: 'Dil',
211
+ codeLanguage_none: 'Hiçbiri',
212
+ finder_matchCase: 'Kibrit Kutusu',
213
+ finder_wholeWord: 'Tüm Kelime',
214
+ finder_regex: 'Düzenli İfade',
215
+ finder_prev: 'Önceki Maç',
216
+ finder_next: 'Sonraki Maç',
208
217
  message_copy_success: 'Panoya kopyalandı',
209
218
  message_copy_fail: 'Kopyalama başarısız oldu. Lütfen manuel olarak kopyalayın.',
210
219
  };
package/src/langs/uk.js CHANGED
@@ -126,6 +126,7 @@
126
126
  link_modal_url: 'Посилання',
127
127
  link_modal_relAttribute: 'Атрибут rel',
128
128
  list: 'Список',
129
+ markdownView: 'Перегляд Markdown',
129
130
  math: 'Формула',
130
131
  math_modal_fontSizeLabel: 'Розмір шрифту',
131
132
  math_modal_inputLabel: 'Математична запис',
@@ -187,6 +188,7 @@
187
188
  tableProperties: 'Властивості таблиці',
188
189
  tags: 'Теги',
189
190
  tag_blockquote: 'Цитата',
191
+ codeBlock: 'Блок коду',
190
192
  tag_div: 'Базовий',
191
193
  tag_h: 'Заголовок',
192
194
  tag_p: 'Абзац',
@@ -205,6 +207,13 @@
205
207
  video_modal_title: 'Вставити відео',
206
208
  video_modal_url: 'Посилання на відео, Youtube, Vimeo',
207
209
  width: 'Ширина',
210
+ codeLanguage: 'Мова',
211
+ codeLanguage_none: 'Жоден',
212
+ finder_matchCase: 'Зіставте регістр',
213
+ finder_wholeWord: 'Ціле слово',
214
+ finder_regex: 'Регулярний вираз',
215
+ finder_prev: 'Попередній матч',
216
+ finder_next: 'Наступний матч',
208
217
  message_copy_success: 'Скопійовано в буфер обміну',
209
218
  message_copy_fail: 'Не вдалося скопіювати. Будь ласка, скопіюйте вручну.',
210
219
  };
package/src/langs/ur.js CHANGED
@@ -126,6 +126,7 @@
126
126
  link_modal_url: 'لنک کرنے کے لیے URL',
127
127
  link_modal_relAttribute: 'Rel وصف',
128
128
  list: 'فہرست',
129
+ markdownView: 'مارک ڈاؤن منظر',
129
130
  math: 'ریاضی',
130
131
  math_modal_fontSizeLabel: 'حرف کا سائز',
131
132
  math_modal_inputLabel: 'ریاضیاتی اشارے',
@@ -187,6 +188,7 @@
187
188
  tableProperties: 'ٹیبل کی خصوصیات',
188
189
  tags: 'ٹیگز',
189
190
  tag_blockquote: 'اقتباس',
191
+ codeBlock: 'کوڈ بلاک',
190
192
  tag_div: 'عام (div)',
191
193
  tag_h: 'ہیڈر',
192
194
  tag_p: 'پیراگراف',
@@ -205,6 +207,13 @@
205
207
  video_modal_title: 'ویڈیو داخل کریں',
206
208
  video_modal_url: 'ذرائع ابلاغ کا یو آر ایل، یوٹیوب/ویمیو',
207
209
  width: 'چوڑائی',
210
+ codeLanguage: 'زبان',
211
+ codeLanguage_none: 'کوئی نہیں۔',
212
+ finder_matchCase: 'میچ کیس',
213
+ finder_wholeWord: 'پورا کلام',
214
+ finder_regex: 'باقاعدہ اظہار',
215
+ finder_prev: 'پچھلا میچ',
216
+ finder_next: 'اگلا میچ',
208
217
  message_copy_success: 'کلپ بورڈ میں کاپی ہو گیا',
209
218
  message_copy_fail: 'کاپی ناکام۔ براہ کرم دستی طور پر کاپی کریں۔',
210
219
  };
@@ -126,6 +126,7 @@
126
126
  link_modal_url: '网址',
127
127
  link_modal_relAttribute: 'Rel 属性',
128
128
  list: '列表',
129
+ markdownView: 'Markdown视图',
129
130
  math: '数学',
130
131
  math_modal_fontSizeLabel: '字号',
131
132
  math_modal_inputLabel: '数学符号',
@@ -187,6 +188,7 @@
187
188
  tableProperties: '表格属性',
188
189
  tags: '标签',
189
190
  tag_blockquote: '引用',
191
+ codeBlock: '代码块',
190
192
  tag_div: '正文 (DIV)',
191
193
  tag_h: '标题',
192
194
  tag_p: '段落',
@@ -205,6 +207,13 @@
205
207
  video_modal_title: '插入视频',
206
208
  video_modal_url: '嵌入网址, Youtube,Vimeo',
207
209
  width: '宽度',
210
+ codeLanguage: '语言',
211
+ codeLanguage_none: '没有任何',
212
+ finder_matchCase: '火柴盒',
213
+ finder_wholeWord: '整个单词',
214
+ finder_regex: '正则表达式',
215
+ finder_prev: '上一场比赛',
216
+ finder_next: '下一场比赛',
208
217
  message_copy_success: '已复制到剪贴板',
209
218
  message_copy_fail: '复制失败,请手动复制。',
210
219
  };
@@ -29,7 +29,11 @@ import ApiManager from '../manager/ApiManager';
29
29
  * @property {string} [searchUrl] - File server search url. Optional. Can be overridden in browser.
30
30
  * @property {Object<string, string>} [searchUrlHeader] - File server search http header. Optional. Can be overridden in browser.
31
31
  * @property {string} [listClass] - Class name of list div. Required. Can be overridden in browser.
32
- * @property {(item: BrowserFile) => string} [drawItemHandler] - Function that defines the HTML of a file item. Required. Can be overridden in browser.
32
+ * @property {(item: BrowserFile) => string} [drawItemHandler] - Function that returns HTML string for rendering each file item. Required. Can be overridden in browser.
33
+ * ```js
34
+ * // drawItemHandler
35
+ * (item) => `<div><img src="${item.thumbnail}"><span>${item.name}</span></div>`
36
+ * ```
33
37
  * @property {Array<*>} [props] - `props` argument to `drawItemHandler` function. Optional. Can be overridden in browser.
34
38
  * @property {number} [columnSize] - Number of `div.se-file-item-column` to be created.
35
39
  * - Optional. Can be overridden in browser. Default: 4.
@@ -54,6 +58,17 @@ class Browser {
54
58
  * @param {*} host The instance object that called the constructor.
55
59
  * @param {SunEditor.Deps} $ Kernel dependencies
56
60
  * @param {BrowserParams} params Browser options
61
+ * @example
62
+ * // Inside a PluginBrowser constructor:
63
+ * this.browser = new Browser(this, this.$, {
64
+ * title: this.$.lang.imageGallery,
65
+ * data: pluginOptions.data,
66
+ * url: pluginOptions.url,
67
+ * headers: pluginOptions.headers,
68
+ * selectorHandler: this.#OnSelect.bind(this),
69
+ * columnSize: 4,
70
+ * className: 'se-image-gallery',
71
+ * });
57
72
  */
58
73
  constructor(host, $, params) {
59
74
  this.#$ = $;
@@ -136,6 +151,16 @@ class Browser {
136
151
  * @param {string} [params.title] - File browser window title. If not, use `this.title`.
137
152
  * @param {string} [params.url] - File server url. If not, use `this.url`.
138
153
  * @param {Object<string, string>} [params.urlHeader] - File server http header. If not, use `this.urlHeader`.
154
+ * @example
155
+ * // Open with default settings (configured at construction):
156
+ * this.browser.open();
157
+ *
158
+ * // Open with runtime overrides:
159
+ * this.browser.open({
160
+ * title: 'Select a video',
161
+ * url: '/api/videos',
162
+ * urlHeader: { Authorization: 'Bearer token' },
163
+ * });
139
164
  */
140
165
  open(params = {}) {
141
166
  this.#addGlobalEvent();
@@ -199,6 +224,11 @@ class Browser {
199
224
  * @description Filter items by tag
200
225
  * @param {Array<BrowserFile>} items - Items to filter
201
226
  * @returns {Array<BrowserFile>}
227
+ * @example
228
+ * // Filter items by currently selected tags:
229
+ * browser.selectedTags = ['photo', 'landscape'];
230
+ * const filtered = browser.tagfilter(items);
231
+ * // Returns only items whose `tag` array includes 'photo' or 'landscape'
202
232
  */
203
233
  tagfilter(items) {
204
234
  const selectedTags = this.selectedTags;
@@ -143,6 +143,12 @@ class ColorPicker {
143
143
  * @param {?(current: Node) => boolean} [stopCondition] - A function used to stop traversing parent nodes while finding the color.
144
144
  * - When this function returns `true`, the traversal ends at that node.
145
145
  * - e.g., `(node) => this.format.isLine(node)` stops at line-level elements like <p>, <div>.
146
+ * @example
147
+ * // Initialize with a selected node and stop traversal at line-level elements
148
+ * this.colorPicker.init(this.$.selection.getNode(), target, (current) => this.$.format.isLine(current));
149
+ *
150
+ * // Initialize with a color string directly (e.g., from a table cell style)
151
+ * this.colorPicker.init(color?.value || '', button);
146
152
  */
147
153
  init(nodeOrColor, target, stopCondition) {
148
154
  this.targetButton = target;
@@ -6,6 +6,7 @@ const INDEX_00 = '2147483646';
6
6
  const INDEX_0 = '2147483645';
7
7
  const INDEX_S_1 = '2147483642';
8
8
  const INDEX_1 = '2147483641';
9
+ const ADD_OFFSET_VALUE = { left: 0, right: 0, top: 0 };
9
10
 
10
11
  /**
11
12
  * Controller information object
@@ -46,11 +47,10 @@ class Controller {
46
47
  #initMethod;
47
48
  #globalEventHandlers;
48
49
 
49
- #addOffset = { left: 0, top: 0 };
50
+ #addOffset = ADD_OFFSET_VALUE;
50
51
  #reserveIndex = false;
51
52
  #preventClose = false;
52
- #shadowRootEventForm = null;
53
- #shadowRootEventListener = null;
53
+ #bindShadowRootEvent = null;
54
54
  #bindClose_key = null;
55
55
  #bindClose_mouse = null;
56
56
 
@@ -124,13 +124,26 @@ class Controller {
124
124
  * @param {Node} [positionTarget] Position target element
125
125
  * @param {Object} [params={}] params
126
126
  * @param {boolean} [params.isWWTarget] If the controller is in the WYSIWYG area, set it to `true`.
127
+ * @param {boolean} [params.passive] If `true`, opens the controller visually without affecting editor state
128
+ * - (`_preventBlur`, `controlActive`, `onControllerContext`, `opendControllers`).
129
+ * - Used for lightweight, non-intrusive display such as hover-triggered UI (e.g., codeLang selector on `<pre>` hover).
130
+ * - Automatically set to `true` when opened during component hover selection (`ON_OVER_COMPONENT`).
127
131
  * @param {() => void} [params.initMethod] Method to be called when the controller is closed.
128
132
  * @param {boolean} [params.disabled] If `true`, When the `controller` is opened, buttons without the `se-component-enabled` class are disabled. (default: `this.disabled`)
129
- * @param {{left?: number, top?: number}} [params.addOffset] Additional offset values
133
+ * @param {{left?: number, right?:number, top?: number}} [params.addOffset] Additional offset values
134
+ * @example
135
+ * // Open controller on a target element with default options
136
+ * this.controller.open(target);
137
+ *
138
+ * // Open with explicit options and additional offset
139
+ * this.controller.open(target, null, { isWWTarget: false, initMethod: null, addOffset: null });
140
+ *
141
+ * // Open on a Range target (e.g., text selection)
142
+ * this.controller.open(this.$.selection.getRange());
130
143
  */
131
- open(target, positionTarget, { isWWTarget, initMethod, disabled, addOffset } = {}) {
144
+ open(target, positionTarget, { isWWTarget, passive, initMethod, disabled, addOffset } = {}) {
132
145
  if (_DragHandle.get('__overInfo') === ON_OVER_COMPONENT) {
133
- return;
146
+ passive = true;
134
147
  }
135
148
 
136
149
  if (!target) {
@@ -142,35 +155,38 @@ class Controller {
142
155
  this.form.removeAttribute('data-se-hidden-by-children');
143
156
  this.#__hiddenByParents__.clear();
144
157
 
145
- if (this.#$.store.mode.isBalloon) this.#$.toolbar.hide();
146
- else if (this.#$.store.mode.isSubBalloon) this.#$.subToolbar.hide();
158
+ if (!passive) {
159
+ if (this.#$.store.mode.isBalloon) this.#$.toolbar.hide();
160
+ else if (this.#$.store.mode.isSubBalloon) this.#$.subToolbar.hide();
147
161
 
148
- if (!this.#$.store.get('hasFocus')) {
149
- if (disabled ?? this.disabled) {
150
- this.#$.ui.setControllerOnDisabledButtons(true);
151
- } else {
152
- this.#$.ui.setControllerOnDisabledButtons(false);
162
+ if (!this.#$.store.get('hasFocus')) {
163
+ if (disabled ?? this.disabled) {
164
+ this.#$.ui.setControllerOnDisabledButtons(true);
165
+ } else {
166
+ this.#$.ui.setControllerOnDisabledButtons(false);
167
+ }
153
168
  }
154
169
  }
155
170
 
156
171
  this.currentPositionTarget = positionTarget || target;
157
172
  this.isWWTarget = isWWTarget ?? this.isWWTarget;
158
173
  if (typeof initMethod === 'function') this.#initMethod = initMethod;
159
- this.#$.ui.currentControllerName = this.kind;
174
+ if (!passive) this.#$.ui.currentControllerName = this.kind;
160
175
 
161
- this.#addOffset = { left: 0, top: 0 };
162
- if (addOffset) this.#addOffset = { ...this.#addOffset, ...addOffset };
176
+ this.#addOffset = { left: 0, right: 0, top: 0, ...addOffset };
163
177
 
164
- const parents = this.isOutsideForm ? this.parentsForm : [];
165
- this.#$.ui.opendControllers?.forEach((e) => {
166
- if (!parents.includes(e.form)) e.form.style.zIndex = INDEX_1;
167
- });
168
-
169
- if (this.parentsHide) {
170
- this.parentsForm.forEach((e) => {
171
- e.style.display = 'none';
172
- e.setAttribute('data-se-hidden-by-children', '1');
178
+ if (!passive) {
179
+ const parents = this.isOutsideForm ? this.parentsForm : [];
180
+ this.#$.ui.opendControllers?.forEach((e) => {
181
+ if (!parents.includes(e.form)) e.form.style.zIndex = INDEX_1;
173
182
  });
183
+
184
+ if (this.parentsHide) {
185
+ this.parentsForm.forEach((e) => {
186
+ e.style.display = 'none';
187
+ e.setAttribute('data-se-hidden-by-children', '1');
188
+ });
189
+ }
174
190
  }
175
191
 
176
192
  this.#addGlobalEvent();
@@ -180,7 +196,7 @@ class Controller {
180
196
 
181
197
  const isRangeTarget = this.#$.instanceCheck.isRange(target);
182
198
  this.currentTarget = /** @type {HTMLElement} */ (isRangeTarget ? null : target);
183
- this.#controllerOn(this.form, target, isRangeTarget);
199
+ this.#controllerOn(this.form, target, isRangeTarget, passive);
184
200
  _w.setTimeout(() => _DragHandle.set('__overInfo', false), 0);
185
201
  }
186
202
 
@@ -188,6 +204,12 @@ class Controller {
188
204
  * @description Close a modal plugin
189
205
  * - The plugin's `init` method is called.
190
206
  * @param {boolean} [force] If `true`, parent controllers are forcibly closed.
207
+ * @example
208
+ * // Close the controller (skips if not open or preventClose is set)
209
+ * this.controller.close();
210
+ *
211
+ * // Force close, also closing parent controllers in the hierarchy
212
+ * this.controller.close(true);
191
213
  */
192
214
  close(force) {
193
215
  if (!force && (!this.isOpen || this.#preventClose)) return;
@@ -201,7 +223,7 @@ class Controller {
201
223
  this.isOpen = false;
202
224
  this.#preventClose = false;
203
225
  this.__offset = {};
204
- this.#addOffset = { left: 0, top: 0 };
226
+ this.#addOffset = ADD_OFFSET_VALUE;
205
227
 
206
228
  this.#removeGlobalEvent();
207
229
 
@@ -240,6 +262,12 @@ class Controller {
240
262
  /**
241
263
  * @description Sets whether the element (form) should be brought to the top based on `z-index`.
242
264
  * @param {boolean} value - `true`: `'2147483646'`, `false`: `'2147483645'`.
265
+ * @example
266
+ * // Bring controller to the highest z-index layer (2147483646)
267
+ * this.controller_cell.bringToTop(true);
268
+ *
269
+ * // Restore to the default top z-index (2147483645)
270
+ * this.controller_cell.bringToTop(false);
243
271
  */
244
272
  bringToTop(value) {
245
273
  this.toTop = value;
@@ -249,6 +277,12 @@ class Controller {
249
277
  /**
250
278
  * @description Reset controller position
251
279
  * @param {Node} [target]
280
+ * @example
281
+ * // Reposition using a new target element
282
+ * this.controller_cell.resetPosition(tdElement);
283
+ *
284
+ * // Reposition using the previously set target
285
+ * this.controller.resetPosition();
252
286
  */
253
287
  resetPosition(target) {
254
288
  this.#setControllerPosition(this.form, target || this.currentPositionTarget, true);
@@ -320,8 +354,9 @@ class Controller {
320
354
  * @param {HTMLFormElement} form Controller element
321
355
  * @param {Node|Range} target Controller target element
322
356
  * @param {boolean} isRangeTarget If the target is a `Range`, set it to `true`.
357
+ * @param {boolean} [passive=false] If `true`, opens without affecting editor state (_preventBlur, controlActive, etc.)
323
358
  */
324
- async #controllerOn(form, target, isRangeTarget) {
359
+ async #controllerOn(form, target, isRangeTarget, passive) {
325
360
  /** @type {ControllerInfo} */
326
361
  const info = {
327
362
  position: this.position,
@@ -336,20 +371,21 @@ class Controller {
336
371
 
337
372
  form.style.display = 'block';
338
373
  if (this.#$.contextProvider.shadowRoot) {
339
- this.#shadowRootEventForm = form;
340
- this.#shadowRootEventListener = (e) => e.stopPropagation();
341
- form.addEventListener('mousedown', this.#shadowRootEventListener);
374
+ this.#bindShadowRootEvent = this.#$.eventManager.addEvent(form, 'mousedown', (e) => e.stopPropagation());
342
375
  }
343
376
 
344
- this.#$.ui.onControllerContext();
377
+ if (!passive) {
378
+ this.#$.ui.onControllerContext();
379
+ this.#$.store.set('controlActive', true);
380
+ }
345
381
 
346
382
  if (!this.isOpen) {
347
383
  this.#$.ui.opendControllers.push(info);
348
384
  }
349
385
 
350
- this.isOpen = true;
351
386
  this.#$.store.set('_preventBlur', true);
352
- this.#$.store.set('controlActive', true);
387
+
388
+ this.isOpen = true;
353
389
 
354
390
  this.host.controllerOn?.(form, target);
355
391
  this.#$.eventManager.triggerEvent('onShowController', { caller: this.kind, frameContext: this.#$.frameContext, info });
@@ -374,10 +410,7 @@ class Controller {
374
410
  _w.setTimeout(() => {
375
411
  this.#$.store.set('controlActive', false);
376
412
  }, 0);
377
- if (this.#shadowRootEventForm) {
378
- this.#shadowRootEventForm.removeEventListener('mousedown', this.#shadowRootEventListener);
379
- this.#shadowRootEventForm = this.#shadowRootEventListener = null;
380
- }
413
+ this.#bindShadowRootEvent &&= this.#$.eventManager.removeEvent(this.#bindShadowRootEvent);
381
414
  }
382
415
 
383
416
  /**
@@ -473,7 +506,7 @@ class Controller {
473
506
 
474
507
  /**
475
508
  * @description Checks if the given target is within a form or controller.
476
- * @param {Node} target The target element.
509
+ * @param {Element} target The target element.
477
510
  * @returns {boolean} `true` if the target is inside a form or controller.
478
511
  */
479
512
  #checkForm(target) {
@@ -505,6 +538,11 @@ class Controller {
505
538
  e.stopPropagation();
506
539
  e.preventDefault();
507
540
 
541
+ if (target.getAttribute('data-command') === 'close') {
542
+ this.close();
543
+ return;
544
+ }
545
+
508
546
  this.host.controllerAction(target);
509
547
  }
510
548
 
@@ -233,6 +233,12 @@ class Figure {
233
233
  * @param {Node} element Target element
234
234
  * @param {string} [className] Class name of container (fixed: `se-component`)
235
235
  * @returns {FigureInfo} {target, container, cover, inlineCover, caption}
236
+ * @example
237
+ * const imgEl = document.createElement('IMG');
238
+ * imgEl.src = imageUrl;
239
+ * const figureInfo = Figure.CreateContainer(imgEl, 'se-image-container');
240
+ * // figureInfo.container → <div class="se-component se-image-container">
241
+ * // figureInfo.cover → <figure> wrapping the imgEl
236
242
  */
237
243
  static CreateContainer(element, className) {
238
244
  dom.utils.createElement('DIV', { class: 'se-component' + (className ? ' ' + className : '') }, dom.utils.createElement('FIGURE', null, element));
@@ -244,6 +250,12 @@ class Figure {
244
250
  * @param {Node} element Target element
245
251
  * @param {string} [className] Class name of container (fixed: `se-component` `se-inline-component`)
246
252
  * @returns {FigureInfo} {target, container, cover, inlineCover, caption}
253
+ * @example
254
+ * const imgEl = document.createElement('IMG');
255
+ * imgEl.src = imageUrl;
256
+ * const figureInfo = Figure.CreateInlineContainer(imgEl, 'se-image-container');
257
+ * // figureInfo.container → <span class="se-component se-inline-component se-image-container">
258
+ * // figureInfo.inlineCover → same as container (inline mode)
247
259
  */
248
260
  static CreateInlineContainer(element, className) {
249
261
  dom.utils.createElement('SPAN', { class: 'se-component se-inline-component' + (className ? ' ' + className : '') }, element);
@@ -288,6 +300,13 @@ class Figure {
288
300
  * @param {string|number} h Height size
289
301
  * @param {?string} [defaultSizeUnit="px"] Default size unit (default: `"px"`)
290
302
  * @return {{w: number, h: number}}
303
+ * @example
304
+ * const ratio = Figure.GetRatio(200, 100, 'px');
305
+ * // ratio → { w: 2, h: 0.5 }
306
+ *
307
+ * // Used with proportion-locked resizing
308
+ * const ratio = Figure.GetRatio(inputX.value, inputY.value, sizeUnit);
309
+ * const adjusted = Figure.CalcRatio(newWidth, newHeight, sizeUnit, ratio);
291
310
  */
292
311
  static GetRatio(w, h, defaultSizeUnit) {
293
312
  let rw = 0,
@@ -316,6 +335,11 @@ class Figure {
316
335
  * @param {string} defaultSizeUnit Default size unit (default: `"px"`)
317
336
  * @param {?{w: number, h: number}} [ratio] Ratio size (Figure.GetRatio)
318
337
  * @return {{w: string|number, h: string|number}}
338
+ * @example
339
+ * const ratio = Figure.GetRatio(200, 100, 'px');
340
+ * // When width changes, recalculate height to maintain aspect ratio
341
+ * const result = Figure.CalcRatio(inputX.value, inputY.value, 'px', ratio);
342
+ * inputY.value = result.h; // adjusted height preserving ratio
319
343
  */
320
344
  static CalcRatio(w, h, defaultSizeUnit, ratio) {
321
345
  if (ratio?.w && ratio?.h && /\d+/.test(w + '') && /\d+/.test(h + '')) {
@@ -366,6 +390,19 @@ class Figure {
366
390
  * @param {boolean} [params.figureTarget=false] If `true`, the target is a figure element
367
391
  * @param {boolean} [params.infoOnly=false] If `true`, returns only the figure target info without opening the controller
368
392
  * @returns {FigureTargetInfo|undefined} figure target info
393
+ * @example
394
+ * // Open controller with full UI (resize handles, size info, border)
395
+ * const info = this.figure.open(imgElement, {
396
+ * nonResizing: false, nonSizeInfo: false, nonBorder: false,
397
+ * figureTarget: false, infoOnly: false
398
+ * });
399
+ *
400
+ * // Get figure info without opening the controller UI
401
+ * const info = this.figure.open(oFrame, {
402
+ * nonResizing: false, nonSizeInfo: false, nonBorder: false,
403
+ * figureTarget: false, infoOnly: true
404
+ * });
405
+ * // info.width, info.height, info.ratio are available
369
406
  */
370
407
  open(targetNode, { nonResizing, nonSizeInfo, nonBorder, figureTarget, infoOnly }) {
371
408
  if (!targetNode) {
@@ -683,6 +720,13 @@ class Figure {
683
720
  * @param {?Node} targetNode Target element
684
721
  * @param {"block"|"inline"} formatStyle Format style
685
722
  * @returns {HTMLElement} New target element after conversion
723
+ * @example
724
+ * // Convert a block image to inline format
725
+ * const newImgEl = this.figure.convertAsFormat(imgElement, 'inline');
726
+ * // newImgEl is a cloned element inside a new inline container
727
+ *
728
+ * // Convert an inline image back to block format
729
+ * const newImgEl = this.figure.convertAsFormat(imgElement, 'block');
686
730
  */
687
731
  convertAsFormat(targetNode, formatStyle) {
688
732
  targetNode ||= this._element;
@@ -849,6 +893,13 @@ class Figure {
849
893
  * @param {Node} originEl - The original element of the figure component.
850
894
  * @param {Node} anchorCover - The anchor cover element of the figure component.
851
895
  * @param {import('../manager/FileManager').default} [fileManagerInst=null] - FileManager module instance, if used.
896
+ * @example
897
+ * // Insert a new image figure, replacing the original element in the DOM
898
+ * const figureInfo = Figure.CreateContainer(imgElement, 'se-image-container');
899
+ * this.figure.retainFigureFormat(figureInfo.container, this.#element, null, this.fileManager);
900
+ *
901
+ * // Replace with anchor cover (e.g., image wrapped in a link)
902
+ * this.figure.retainFigureFormat(container, this.#element, anchorEl, this.fileManager);
852
903
  */
853
904
  retainFigureFormat(container, originEl, anchorCover, fileManagerInst) {
854
905
  const isInline = this.#$.component.isInline(container);
@@ -910,6 +961,12 @@ class Figure {
910
961
  * @param {?string|number} width Element's width size
911
962
  * @param {?string|number} height Element's height size
912
963
  * @param {?number} deg rotate value
964
+ * @example
965
+ * // Rotate element 90 degrees clockwise
966
+ * this.figure.setTransform(imgElement, 100, 50, 90);
967
+ *
968
+ * // Apply size without additional rotation (deg=0 preserves current rotation)
969
+ * this.figure.setTransform(oFrame, width, height, 0);
913
970
  */
914
971
  setTransform(node, width, height, deg) {
915
972
  try {
@@ -80,6 +80,12 @@ class Modal {
80
80
  * - acceptedFormats: `"image/*, video/*, audio/*"`, etc.
81
81
  * - allowMultiple: `true` or `false`
82
82
  * @returns {string} HTML string
83
+ * @example
84
+ * // Inside a plugin's modal HTML template:
85
+ * const html = Modal.CreateFileInput(
86
+ * { icons, lang },
87
+ * { acceptedFormats: 'image/*', allowMultiple: true }
88
+ * );
83
89
  */
84
90
  static CreateFileInput({ icons, lang }, { acceptedFormats, allowMultiple }) {
85
91
  return /*html*/ `
@@ -51,6 +51,20 @@ class ApiManager {
51
51
  /**
52
52
  * @description Call API
53
53
  * @param {ApiManagerParams} params
54
+ * @example
55
+ * // POST with FormData and callbacks
56
+ * apiManager.call({
57
+ * method: 'POST', url: '/upload', headers: { 'x-custom': 'value' },
58
+ * data: formData,
59
+ * callBack: (xhr) => console.log(xhr.responseText),
60
+ * errorCallBack: (res, xhr) => res.errorMessage || 'Upload failed'
61
+ * });
62
+ *
63
+ * // GET request with minimal params (uses constructor defaults for omitted options)
64
+ * apiManager.call({
65
+ * method: 'GET', url: '/api/files',
66
+ * callBack: (xhr) => JSON.parse(xhr.responseText)
67
+ * });
54
68
  */
55
69
  call({ method, url, headers, data, callBack, errorCallBack, responseType }) {
56
70
  this.cancel();
@@ -90,6 +104,18 @@ class ApiManager {
90
104
  * @param {*} [params.data] - API data
91
105
  * @param {XMLHttpRequestResponseType} [params.responseType] - XMLHttpRequest.responseType
92
106
  * @returns {Promise<XMLHttpRequest>}
107
+ * @example
108
+ * // POST FormData and await the response
109
+ * const xhr = await apiManager.asyncCall({
110
+ * method: 'POST', url: '/upload',
111
+ * headers: { 'x-api-key': 'key' }, data: formData
112
+ * });
113
+ * const result = JSON.parse(xhr.responseText);
114
+ *
115
+ * // Send JSON data (uses constructor defaults for method/url)
116
+ * const xhr = await apiManager.asyncCall({
117
+ * data: JSON.stringify({ fileName: 'doc.pdf', htmlContent })
118
+ * });
93
119
  */
94
120
  asyncCall({ method, url, headers, data, responseType }) {
95
121
  this.cancel();
@@ -118,9 +144,9 @@ class ApiManager {
118
144
  this.#$.ui.hideLoading();
119
145
  }
120
146
  } else {
147
+ console.error(`[SUNEDITOR.ApiManager[${this.kind}].upload.serverException]`, xhr);
121
148
  try {
122
- const res = !xhr.responseText ? xhr : JSON.parse(xhr.responseText);
123
- reject(res);
149
+ reject(_parseErrorResponse(xhr));
124
150
  } finally {
125
151
  this.#$.ui.hideLoading();
126
152
  }
@@ -177,12 +203,12 @@ class ApiManager {
177
203
  // exception
178
204
  console.error(`[SUNEDITOR.ApiManager[${this.kind}].upload.serverException]`, xmlHttp);
179
205
  try {
180
- const res = !xmlHttp.responseText ? xmlHttp : JSON.parse(xmlHttp.responseText);
206
+ const res = _parseErrorResponse(xmlHttp);
181
207
  let message = '';
182
208
  if (typeof errorCallBack === 'function') {
183
209
  message = await errorCallBack(res, xmlHttp);
184
210
  }
185
- const err = `[SUNEDITOR.ApiManager[${this.kind}].upload.serverException] status: ${xmlHttp.status}, response: ${message || res.errorMessage || xmlHttp.responseText}`;
211
+ const err = `[SUNEDITOR.ApiManager[${this.kind}].upload.serverException] status: ${xmlHttp.status}, response: ${message || res.errorMessage || (typeof res === 'string' ? res : JSON.stringify(res))}`;
186
212
  this.#$.ui.alertOpen(err, 'error');
187
213
  } catch (error) {
188
214
  throw Error(`[SUNEDITOR.ApiManager[${this.kind}].upload.errorCallBack.fail] ${error.message}`);
@@ -194,4 +220,27 @@ class ApiManager {
194
220
  }
195
221
  }
196
222
 
223
+ /**
224
+ * @description Parses error response from XMLHttpRequest.
225
+ * Safely handles non-text responseTypes (blob, arraybuffer, etc.) where accessing responseText throws.
226
+ * @param {XMLHttpRequest} xhr
227
+ * @returns {Object|string} Parsed JSON object, raw text, or status string as fallback
228
+ */
229
+ function _parseErrorResponse(xhr) {
230
+ let text;
231
+ try {
232
+ text = xhr.responseText;
233
+ } catch {
234
+ return `status ${xhr.status}`;
235
+ }
236
+
237
+ if (!text) return `status ${xhr.status}`;
238
+
239
+ try {
240
+ return JSON.parse(text);
241
+ } catch {
242
+ return text;
243
+ }
244
+ }
245
+
197
246
  export default ApiManager;