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
@@ -35,7 +35,7 @@ class FileManager {
35
35
  this.uploadFileLength = 0;
36
36
  this.__updateTags = [];
37
37
  // api manager
38
- this.apiManager = new ApiManager(this, null);
38
+ this.apiManager = new ApiManager(this, $);
39
39
 
40
40
  // se-ts-ignore - call by editor
41
41
  void this._resetInfo;
@@ -49,6 +49,15 @@ class FileManager {
49
49
  * @param {FileList|File[]|{formData: FormData, size: number}} data FormData in body or Files array
50
50
  * @param {?(xmlHttp: XMLHttpRequest) => boolean} [callBack] Success call back function
51
51
  * @param {?(res: *, xmlHttp: XMLHttpRequest) => string} [errorCallBack] Error call back function
52
+ * @example
53
+ * // Upload with a File array
54
+ * const files = [new File(['content'], 'photo.jpg', { type: 'image/jpeg' })];
55
+ * fileManager.upload('/api/upload', { Authorization: 'Bearer token' }, files, onSuccess, onError);
56
+ *
57
+ * // Upload with a pre-built FormData
58
+ * const formData = new FormData();
59
+ * formData.append('file-0', myFile);
60
+ * fileManager.upload('/api/upload', null, { formData, size: 1 }, onSuccess, onError);
52
61
  */
53
62
  upload(uploadUrl, uploadHeader, data, callBack, errorCallBack) {
54
63
  this.#$.ui.showLoading();
@@ -75,6 +84,10 @@ class FileManager {
75
84
  * @param {?Object<string, string>} uploadHeader Request header
76
85
  * @param {FileList|File[]|{formData: FormData, size: number}} data FormData in body or Files array
77
86
  * @returns {Promise<XMLHttpRequest>}
87
+ * @example
88
+ * const files = [new File(['content'], 'photo.jpg')];
89
+ * const xmlHttp = await fileManager.asyncUpload('/api/upload', { Authorization: 'Bearer token' }, files);
90
+ * const response = JSON.parse(xmlHttp.responseText);
78
91
  */
79
92
  async asyncUpload(uploadUrl, uploadHeader, data) {
80
93
  this.#$.ui.showLoading();
@@ -102,6 +115,10 @@ class FileManager {
102
115
  * @param {string} params.name File name
103
116
  * @param {number} params.size File size
104
117
  * @returns
118
+ * @example
119
+ * const imgElement = document.createElement('img');
120
+ * imgElement.src = 'https://example.com/photo.jpg';
121
+ * fileManager.setFileData(imgElement, { name: 'photo.jpg', size: 2048 });
105
122
  */
106
123
  setFileData(element, { name, size }) {
107
124
  if (!element) return;
@@ -9,8 +9,12 @@ const { _w, NO_EVENT } = env;
9
9
  * @property {boolean} [textToDisplay=''] - Create Text to display input.
10
10
  * @property {boolean} [openNewWindow=false] - Default checked value of the "Open in new window" checkbox.
11
11
  * @property {boolean} [noAutoPrefix=false] - If `true`, disables the automatic prefixing of the host URL to the value of the link.
12
- * @property {Array<string>} [relList=[]] - The `rel` attribute list of anchor tag.
13
- * @property {{default?: string, check_new_window?: string, check_bookmark?: string}} [defaultRel={}] - Default `rel` attributes of anchor tag.
12
+ * @property {Array<string>} [relList=[]] - Available `rel` attribute values shown as checkboxes in the link modal.
13
+ * @property {{default?: string, check_new_window?: string, check_bookmark?: string}} [defaultRel={}] - Default `rel` values auto-applied by condition.
14
+ * `default` is always applied, `check_new_window` when "Open in new window" is checked, `check_bookmark` for bookmark links.
15
+ * ```js
16
+ * { relList: ['nofollow', 'noreferrer', 'noopener'], defaultRel: { default: 'noopener', check_new_window: 'noreferrer' } }
17
+ * ```
14
18
  * @property {string} [uploadUrl] - File upload URL.
15
19
  * @property {Object<string, string>} [uploadHeaders] - File upload headers.
16
20
  * @property {number} [uploadSizeLimit] - File upload size limit.
@@ -37,6 +41,17 @@ class ModalAnchorEditor {
37
41
  * @param {SunEditor.Deps} $ Kernel dependencies
38
42
  * @param {HTMLElement} modalForm Modal <form>
39
43
  * @param {ModalAnchorEditorParams} params ModalAnchorEditor options
44
+ * @example
45
+ * // In a link plugin (text anchor):
46
+ * this.anchor = new ModalAnchorEditor(this.$, modalEl, this.pluginOptions);
47
+ *
48
+ * // In an image plugin (non-text anchor with custom options):
49
+ * const linkOptions = this.$.plugins.link ? this.$.plugins.link.pluginOptions : {};
50
+ * this.anchor = new ModalAnchorEditor(this.$, modalEl.html, {
51
+ * ...linkOptions,
52
+ * textToDisplay: false,
53
+ * title: true,
54
+ * });
40
55
  */
41
56
  constructor($, modalForm, params) {
42
57
  this.#$ = $;
@@ -149,6 +164,13 @@ class ModalAnchorEditor {
149
164
  /**
150
165
  * @description Opens the anchor editor modal and populates it with data.
151
166
  * @param {boolean} isUpdate - Indicates whether an existing anchor is being updated (`true`) or a new one is being created (`false`).
167
+ * @example
168
+ * // Called from modalOn() — populate form for a new link:
169
+ * this.anchor.on(false);
170
+ *
171
+ * // Populate form to edit an existing link (call set() first):
172
+ * this.anchor.set(existingAnchorElement);
173
+ * this.anchor.on(true);
152
174
  */
153
175
  on(isUpdate) {
154
176
  if (!isUpdate) {
@@ -173,6 +195,17 @@ class ModalAnchorEditor {
173
195
  * @description Creates an anchor (`<a>`) element with the specified attributes.
174
196
  * @param {boolean} notText - If `true`, the anchor will not contain text content.
175
197
  * @returns {HTMLElement|null} - The newly created anchor element, or `null` if the URL is empty.
198
+ * @example
199
+ * // In a link plugin — create anchor with text content:
200
+ * const oA = this.anchor.create(false);
201
+ * if (oA === null) return false;
202
+ * this.$.html.insertNode(oA);
203
+ *
204
+ * // In an image plugin — create anchor without text (wraps an image):
205
+ * const anchor = this.anchor.create(true);
206
+ * if (anchor) {
207
+ * anchor.appendChild(imgElement);
208
+ * }
176
209
  */
177
210
  create(notText) {
178
211
  if (this.linkValue.length === 0) return null;
@@ -5,12 +5,19 @@ const MENU_MIN_HEIGHT = 38;
5
5
 
6
6
  /**
7
7
  * @typedef {Object} SelectMenuParams
8
- * @property {string} position Position of the select menu, specified as `"[left|right]-[middle|top|bottom]"` or `"[top|bottom]-[center|left|right]"`
8
+ * @property {string} position Position of the select menu, specified as `"[left|right]-[middle|top|bottom]"` or `"[top|bottom]-[center|left|right]"`.
9
+ * ```js
10
+ * // position
11
+ * 'left-bottom' // menu appears below, aligned to the left
12
+ * 'top-center' // menu appears above, centered
13
+ * ```
9
14
  * @property {boolean} [checkList=false] Flag to determine if the checklist is enabled (`true` or `false`)
10
15
  * @property {"rtl" | "ltr"} [dir="ltr"] Optional text direction: `"rtl"` for right-to-left, `"ltr"` for left-to-right
11
16
  * @property {number} [splitNum=0] Optional split number for horizontal positioning; defines how many items per row
12
17
  * @property {() => void} [openMethod] Optional method to call when the menu is opened
13
18
  * @property {() => void} [closeMethod] Optional method to call when the menu is closed
19
+ * @property {string} [maxHeight] Optional max-height CSS value (e.g. `"200px"`). Enables scrolling when items exceed this height.
20
+ * @property {string} [minWidth] Optional min-width CSS value (e.g. `"130px"`).
14
21
  */
15
22
 
16
23
  /**
@@ -59,6 +66,8 @@ class SelectMenu {
59
66
  this.horizontal = !!this.splitNum;
60
67
  this.openMethod = params.openMethod;
61
68
  this.closeMethod = params.closeMethod;
69
+ this.maxHeight = params.maxHeight || '';
70
+ this.minWidth = params.minWidth || '';
62
71
 
63
72
  this.#dirPosition = /^(left|right)$/.test(this.position) ? (this.position === 'left' ? 'right' : 'left') : this.position;
64
73
  this.#dirSubPosition = /^(left|right)$/.test(this.subPosition) ? (this.subPosition === 'left' ? 'right' : 'left') : this.subPosition;
@@ -101,26 +110,48 @@ class SelectMenu {
101
110
  * @param {Node} referElement - The element that triggers the select menu.
102
111
  * @param {(command: string) => void} selectMethod - The function to execute when an item is selected.
103
112
  * @param {{class?: string, style?: string}} [attr={}] - Additional attributes for the select menu container.
113
+ * @example
114
+ * // Basic: attach menu to a button with a selection callback
115
+ * selectMenu.on(this.alignButton, this.onAlignSelect.bind(this));
116
+ *
117
+ * // With custom attributes for styling
118
+ * selectMenu.on(this.alignButton, this.onAlignSelect.bind(this), { class: 'se-figure-select-list' });
104
119
  */
105
120
  on(referElement, selectMethod, attr = {}) {
106
121
  this.#refer = /** @type {HTMLElement} */ (referElement);
107
122
  this.#keydownTarget = dom.check.isInputElement(referElement) ? referElement : this.#$.frameContext.get('_ww');
108
123
  this.#selectMethod = selectMethod;
124
+
125
+ let innerStyle = '';
126
+ if (this.maxHeight) innerStyle += 'max-height:' + this.maxHeight + ';overflow-y:auto;';
127
+ if (this.minWidth) innerStyle += 'min-width:' + this.minWidth + ';';
128
+
109
129
  this.form = dom.utils.createElement(
110
130
  'DIV',
111
131
  {
112
132
  class: 'se-select-menu' + (attr.class ? ' ' + attr.class : ''),
113
133
  style: attr.style || '',
114
134
  },
115
- '<div class="se-list-inner"></div>',
135
+ '<div class="se-list-inner"' + (innerStyle ? ' style="' + innerStyle + '"' : '') + '></div>',
116
136
  );
137
+
117
138
  referElement.parentNode.insertBefore(this.form, referElement);
118
139
  }
119
140
 
120
141
  /**
121
142
  * @description Select menu open
122
143
  * @param {?string} [position] `"[left|right]-[middle|top|bottom] | [top|bottom]-[center|left|right]"`
144
+ * Always specify in LTR orientation. In RTL environments, left/right are automatically swapped.
123
145
  * @param {?string} [onItemQuerySelector] The querySelector string of the menu to be activated
146
+ * @example
147
+ * // Open with default position (uses constructor's position param)
148
+ * selectMenu.open();
149
+ *
150
+ * // Open at a specific position (always use LTR basis; RTL is auto-mirrored)
151
+ * selectMenu.open('bottom-left');
152
+ *
153
+ * // Open with an active item highlighted via querySelector
154
+ * selectMenu.open('', '[data-command="' + this.align + '"]');
124
155
  */
125
156
  open(position, onItemQuerySelector) {
126
157
  this.#$.ui.selectMenuOn = true;
@@ -386,11 +417,12 @@ class SelectMenu {
386
417
  */
387
418
  #addEvents() {
388
419
  this.#removeEvents();
389
- this.#events = this.#eventHandlers;
390
- this.form.addEventListener('mousedown', this.#events.mousedown);
391
- this.form.addEventListener('mousemove', this.#events.mousemove);
392
- this.form.addEventListener('click', this.#events.click);
393
- this.#keydownTarget.addEventListener('keydown', this.#events.keydown);
420
+ this.#events = {
421
+ mousedown: this.#$.eventManager.addEvent(this.form, 'mousedown', this.#eventHandlers.mousedown),
422
+ mousemove: this.#$.eventManager.addEvent(this.form, 'mousemove', this.#eventHandlers.mousemove),
423
+ click: this.#$.eventManager.addEvent(this.form, 'click', this.#eventHandlers.click),
424
+ keydown: this.#$.eventManager.addEvent(this.#keydownTarget, 'keydown', this.#eventHandlers.keydown),
425
+ };
394
426
  }
395
427
 
396
428
  /**
@@ -398,10 +430,10 @@ class SelectMenu {
398
430
  */
399
431
  #removeEvents() {
400
432
  if (!this.#events) return;
401
- this.form.removeEventListener('mousedown', this.#events.mousedown);
402
- this.form.removeEventListener('mousemove', this.#events.mousemove);
403
- this.form.removeEventListener('click', this.#events.click);
404
- this.#keydownTarget.removeEventListener('keydown', this.#events.keydown);
433
+ this.#$.eventManager.removeEvent(this.#events.mousedown);
434
+ this.#$.eventManager.removeEvent(this.#events.mousemove);
435
+ this.#$.eventManager.removeEvent(this.#events.click);
436
+ this.#$.eventManager.removeEvent(this.#events.keydown);
405
437
  this.#events = null;
406
438
  }
407
439
 
@@ -526,7 +558,7 @@ class SelectMenu {
526
558
  #CloseListener_mousedown(e) {
527
559
  const eventTarget = dom.query.getEventTarget(e);
528
560
  if (this.form.contains(eventTarget)) return;
529
- if (e.target !== this.#refer) {
561
+ if (!this.#refer.contains(eventTarget)) {
530
562
  this.close();
531
563
  } else if (!dom.check.isInputElement(eventTarget)) {
532
564
  this.#bindClose_click = this.#$.eventManager.addGlobalEvent('click', this.#globalEventHandlers.click, true);
@@ -3,11 +3,14 @@ import { Browser } from '../../modules/contract';
3
3
 
4
4
  /**
5
5
  * @typedef {Object} FileBrowserPluginOptions
6
- * @property {Object<string, *>|Array<*>} [data] - Direct data without server calls
6
+ * @property {Object<string, *>|Array<*>} [data] - Direct data without server calls (bypasses URL fetch).
7
7
  * @property {string} [url] - Server request URL
8
8
  * @property {Object<string, string>} [headers] - Server request headers
9
- * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
9
+ * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail URL or a function that returns a thumbnail URL per item.
10
10
  * @property {Array<string>} [props] - Additional tag names
11
+ * ```js
12
+ * { url: '/api/files', headers: { Authorization: 'Bearer token' }, thumbnail: (item) => item.thumbUrl }
13
+ * ```
11
14
  */
12
15
 
13
16
  /**
@@ -0,0 +1,324 @@
1
+ import { PluginCommand, PluginDropdown } from '../../interfaces';
2
+ import { converter, dom } from '../../helper';
3
+ import { Controller } from '../../modules/contract';
4
+ import { SelectMenu } from '../../modules/ui';
5
+
6
+ void PluginDropdown;
7
+
8
+ const DEFAULT_LANGS = ['javascript', 'typescript', 'html', 'css', 'json', 'python', 'java', 'c', 'cpp', 'csharp', 'go', 'rust', 'ruby', 'php', 'swift', 'kotlin', 'sql', 'bash', 'markdown', 'xml', 'yaml'];
9
+
10
+ /**
11
+ * @typedef {Object} CodeBlockPluginOptions
12
+ * @property {Array<string>} [langs] - List of selectable programming languages for code blocks.
13
+ * - Defaults to 21 common languages
14
+ * - [javascript, typescript, html, css, json, python, java, c, cpp, csharp, go, rust, ruby, php, swift, kotlin, sql, bash, markdown, xml, yaml].
15
+ * - Set to empty array `[]` to disable language selection UI entirely.
16
+ * ```js
17
+ * { codeBlock: { langs: ['javascript', 'python', 'html', 'css'] } }
18
+ * ```
19
+ */
20
+
21
+ /**
22
+ * @class
23
+ * @implements {PluginDropdown}
24
+ * @description Code block plugin — toggles `<pre>` formatting with language selection.
25
+ * - Toolbar: command button (toggle `<pre>`) + optional dropdown (language list)
26
+ * - Hover UI: shows language selector on `<pre>` hover (Controller + SelectMenu)
27
+ * - I/O conversion: `<pre class="language-xxx">` ↔ `<pre><code class="language-xxx">`
28
+ */
29
+ class CodeBlock extends PluginCommand {
30
+ static key = 'codeBlock';
31
+ static className = '';
32
+
33
+ #preTag;
34
+ #langItems;
35
+ #langs;
36
+
37
+ // hover UI
38
+ #hoverButton;
39
+ #hoverSelectMenu;
40
+ #hoverController;
41
+ #hoverCurrentPre;
42
+ #mouseLeaveEvent;
43
+ #removeEventFunc;
44
+
45
+ /**
46
+ * @constructor
47
+ * @param {SunEditor.Kernel} kernel - The Kernel instance
48
+ * @param {CodeBlockPluginOptions} pluginOptions - Configuration options for the CodeBlock plugin.
49
+ */
50
+ constructor(kernel, pluginOptions) {
51
+ super(kernel);
52
+ this.title = this.$.lang.codeBlock || 'Code Block';
53
+ this.icon = 'code_block';
54
+
55
+ this.#preTag = dom.utils.createElement('PRE');
56
+ this.#langs = pluginOptions?.langs ?? DEFAULT_LANGS;
57
+
58
+ if (!this.#langs.length) return;
59
+
60
+ /**
61
+ * ──────────────────────────────────
62
+ * [[ langs select ]]
63
+ * ──────────────────────────────────
64
+ */
65
+
66
+ // ───────────────── [[toolbar dropdown type]] ─────────────────
67
+ this.afterItem = dom.utils.createElement(
68
+ 'button',
69
+ { class: 'se-btn se-tooltip se-sub-arrow-btn', 'data-command': CodeBlock.key, 'data-type': 'dropdown' },
70
+ `${this.$.icons.arrow_down}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.codeLanguage || 'Language'}</span></span>`,
71
+ );
72
+
73
+ const menu = CreateDropdownHTML(this.$, this.#langs);
74
+ this.#langItems = menu.querySelectorAll('li button');
75
+ this.$.menu.initDropdownTarget({ key: CodeBlock.key, type: 'dropdown' }, menu);
76
+
77
+ // ───────────────── [hover UI] ─────────────────
78
+ // controller
79
+ const containerEl = dom.utils.createElement('DIV', { class: 'se-controller se-code-lang' });
80
+ this.#hoverButton = dom.utils.createElement('DIV', { class: 'se-code-lang-button' });
81
+ this.#updateHoverButtonText('');
82
+ containerEl.appendChild(this.#hoverButton);
83
+
84
+ this.#hoverController = new Controller(this, this.$, containerEl, { position: 'top', isWWTarget: true });
85
+
86
+ // mouseleave handler
87
+ this.#removeEventFunc = converter.debounce((e) => {
88
+ this.#mouseLeaveEvent = this.$.eventManager.removeEvent(this.#mouseLeaveEvent);
89
+
90
+ if (e && containerEl.contains(e.relatedTarget)) {
91
+ this.#addCtrlLeaveEvent();
92
+ } else {
93
+ this.#hideHover();
94
+ }
95
+ }, 0);
96
+
97
+ // SelectMenu
98
+ this.#hoverSelectMenu = new SelectMenu(this.$, {
99
+ position: 'bottom-right',
100
+ dir: this.$.options.get('_rtl') ? 'rtl' : 'ltr',
101
+ maxHeight: '214px',
102
+ minWidth: '132px',
103
+ closeMethod: this.#removeEventFunc,
104
+ });
105
+
106
+ this.#hoverSelectMenu.on(this.#hoverButton, this.#onHoverSelect.bind(this));
107
+ this.#buildHoverMenu('');
108
+
109
+ // selectMenu
110
+ this.$.eventManager.addEvent(this.#hoverButton, 'click', (e) => {
111
+ e.preventDefault();
112
+ e.stopPropagation();
113
+ if (this.#hoverSelectMenu.isOpen) {
114
+ this.#hoverSelectMenu.close();
115
+ } else {
116
+ const currentLang = this.#getPreLang(this.#hoverCurrentPre);
117
+ this.#buildHoverMenu(currentLang);
118
+ const items = this.#hoverSelectMenu.items;
119
+ const idx = currentLang ? items.indexOf(currentLang) : 0;
120
+ this.#hoverSelectMenu.open(null, idx >= 0 ? `[data-index="${idx}"]` : null);
121
+ }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * @hook Editor.EventManager
127
+ * @type {SunEditor.Hook.Event.OnMouseMove}
128
+ */
129
+ onMouseMove({ event }) {
130
+ if (!this.#hoverController) return;
131
+ const eventTarget = dom.query.getEventTarget(event);
132
+ const pre = eventTarget.closest('pre');
133
+
134
+ if (pre && !this.#isHoverOpen() && this.$.ui.opendControllers.length === 0) {
135
+ this.#showHover(pre);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * @hook Editor.EventManager
141
+ * @type {SunEditor.Hook.Event.Active}
142
+ */
143
+ active(element, target) {
144
+ if (/^PRE$/i.test(element?.nodeName)) {
145
+ dom.utils.addClass(target, 'active');
146
+ return true;
147
+ }
148
+
149
+ dom.utils.removeClass(target, 'active');
150
+ return false;
151
+ }
152
+
153
+ /**
154
+ * @override
155
+ * @type {PluginCommand['action']}
156
+ */
157
+ action(target) {
158
+ const lang = target?.getAttribute('data-value') || '';
159
+ const selNode = this.$.selection.getNode();
160
+ const currentPre = dom.query.getParentElement(selNode, (el) => /^PRE$/i.test(el.nodeName));
161
+
162
+ if (currentPre && !lang) {
163
+ // toggle off: convert <pre> to default line
164
+ this.$.format.setLine(dom.utils.createElement(this.$.options.get('defaultLine')));
165
+ } else {
166
+ // toggle on or change language
167
+ if (!currentPre) {
168
+ this.$.format.setBrLine(this.#preTag.cloneNode(false));
169
+ }
170
+
171
+ if (lang) {
172
+ const pre = dom.query.getParentElement(this.$.selection.getNode(), (el) => /^PRE$/i.test(el.nodeName));
173
+ if (pre) this.#setLang(pre, lang);
174
+ }
175
+ }
176
+
177
+ this.$.menu.dropdownOff();
178
+ this.$.focusManager.focus();
179
+ this.$.history.push(false);
180
+ }
181
+
182
+ /**
183
+ * @impl Dropdown
184
+ * @type {PluginDropdown['on']}
185
+ */
186
+ on() {
187
+ if (!this.#langItems) return;
188
+ const currentLang = this.#getPreLang(this.$.selection.getNode());
189
+
190
+ for (let i = 0, len = this.#langItems.length; i < len; i++) {
191
+ const item = this.#langItems[i];
192
+ dom.utils.toggleClass(item, 'active', item.getAttribute('data-value') === currentLang);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * @description Shows the hover language selector over the given pre element.
198
+ * @param {HTMLElement} preElement
199
+ */
200
+ #showHover(preElement) {
201
+ if (this.#hoverCurrentPre === preElement && this.#hoverController.isOpen) return;
202
+
203
+ if (this.#hoverCurrentPre && this.#hoverCurrentPre !== preElement) {
204
+ dom.utils.removeClass(this.#hoverCurrentPre, 'se-pre-code-focus');
205
+ }
206
+ this.#hoverCurrentPre = preElement;
207
+ dom.utils.addClass(preElement, 'se-pre-code-focus');
208
+
209
+ this.#hoverController.open(preElement, null, { passive: true, addOffset: { right: preElement.offsetWidth } });
210
+ this.#updateHoverButtonText(this.#getPreLang(preElement));
211
+
212
+ this.#addPreLeaveEvent();
213
+ }
214
+
215
+ #hideHover() {
216
+ if (this.#hoverSelectMenu?.isOpen) return;
217
+ this.#closeHover();
218
+ }
219
+
220
+ #closeHover() {
221
+ if (this.#hoverSelectMenu?.isOpen) this.#hoverSelectMenu.close();
222
+ dom.utils.removeClass(this.#hoverCurrentPre, 'se-pre-code-focus');
223
+ this.#hoverController.close(true);
224
+ }
225
+
226
+ /** @hook Module.Controller */
227
+ controllerClose() {
228
+ if (this.#hoverCurrentPre) {
229
+ dom.utils.removeClass(this.#hoverCurrentPre, 'se-pre-code-focus');
230
+ this.#hoverCurrentPre = null;
231
+ }
232
+ }
233
+
234
+ #onHoverSelect(langValue) {
235
+ if (!this.#hoverCurrentPre) return;
236
+ this.#setLang(this.#hoverCurrentPre, langValue);
237
+ this.#updateHoverButtonText(langValue);
238
+ this.#hoverSelectMenu.close();
239
+ this.#hideHover();
240
+ this.$.focusManager.focus();
241
+ this.$.history.push(false);
242
+ }
243
+
244
+ #addPreLeaveEvent() {
245
+ this.#mouseLeaveEvent ??= this.$.eventManager.addEvent(this.#hoverCurrentPre, 'mouseleave', this.#removeEventFunc);
246
+ }
247
+
248
+ #addCtrlLeaveEvent() {
249
+ this.#mouseLeaveEvent ??= this.$.eventManager.addEvent(this.#hoverController.form, 'mouseleave', this.#removeEventFunc);
250
+ }
251
+
252
+ #buildHoverMenu(currentLang) {
253
+ const noneLabel = this.$.lang.codeLanguage_none || 'None';
254
+ const hasExtra = currentLang && !this.#langs.includes(currentLang);
255
+ const items = hasExtra ? ['', currentLang, ...this.#langs] : ['', ...this.#langs];
256
+ const menus = hasExtra ? [noneLabel, currentLang, ...this.#langs] : [noneLabel, ...this.#langs];
257
+ this.#hoverSelectMenu.create(items, menus);
258
+ }
259
+
260
+ #updateHoverButtonText(lang) {
261
+ this.#hoverButton.innerHTML = /* html */ `<span class="se-code-lang-icon">&lt;/&gt;</span><span class="se-code-lang-text">${lang || this.$.lang.codeLanguage || 'Language'}</span>`;
262
+ }
263
+
264
+ #isHoverOpen() {
265
+ return this.#hoverSelectMenu?.isOpen || this.#hoverController?.isOpen;
266
+ }
267
+
268
+ /**
269
+ * @description Get the language from a pre element's class.
270
+ * @param {?Node} preOrChild - The pre element or a node inside it
271
+ * @returns {string}
272
+ */
273
+ #getPreLang(preOrChild) {
274
+ const pre = preOrChild?.nodeName === 'PRE' ? preOrChild : dom.query.getParentElement(preOrChild, (el) => /^PRE$/i.test(el.nodeName));
275
+ if (!pre) return '';
276
+ return /** @type {HTMLElement} */ (pre).className.match(/language-(\S+)/)?.[1] || '';
277
+ }
278
+
279
+ /**
280
+ * @description Set language class on a pre element.
281
+ * @param {HTMLElement} pre
282
+ * @param {string} lang
283
+ */
284
+ #setLang(pre, lang) {
285
+ pre.className = pre.className.replace(/\s*language-\S+/g, '').trim();
286
+ if (lang) {
287
+ dom.utils.addClass(pre, 'language-' + lang);
288
+ pre.setAttribute('data-se-lang', lang);
289
+ } else {
290
+ pre.removeAttribute('data-se-lang');
291
+ }
292
+ }
293
+
294
+ /**
295
+ * @description Cleans up resources.
296
+ */
297
+ destroy() {
298
+ if (this.#hoverCurrentPre) {
299
+ dom.utils.removeClass(this.#hoverCurrentPre, 'se-pre-code-focus');
300
+ }
301
+ this.#hoverController?.form?.parentNode?.removeChild(this.#hoverController.form);
302
+ this.#hoverCurrentPre = null;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * @param {SunEditor.Deps} $
308
+ * @param {string[]} langs
309
+ * @returns {HTMLElement}
310
+ */
311
+ function CreateDropdownHTML($, langs) {
312
+ const noneLabel = $.lang.codeLanguage_none || 'None';
313
+ let list = '<div class="se-list-inner"><ul class="se-list-basic">';
314
+
315
+ list += `<li><button type="button" class="se-btn se-btn-list" data-command="codeBlock" data-value="" title="${noneLabel}">${noneLabel}</button></li>`;
316
+ for (const lang of langs) {
317
+ list += `<li><button type="button" class="se-btn se-btn-list" data-command="codeBlock" data-value="${lang}" title="${lang}">${lang}</button></li>`;
318
+ }
319
+
320
+ list += '</ul></div>';
321
+ return dom.utils.createElement('DIV', { class: 'se-dropdown se-list-layer se-list-code-block' }, list);
322
+ }
323
+
324
+ export default CodeBlock;
@@ -63,12 +63,17 @@ class ExportPDF extends PluginCommand {
63
63
 
64
64
  try {
65
65
  const standardWW = this.$.frameContext.get('documentTypePageMirror') || this.$.frameContext.get('wysiwygFrame');
66
- const editableDiv = dom.utils.createElement('div', { class: standardWW.className }, standardWW.innerHTML);
66
+
67
+ // Strip theme class so getComputedStyle resolves default (light) colors for borders, shadows, etc.
68
+ const themeClass = (this.$.options.get('_themeClass') || '').trim();
69
+ const wwClassName = themeClass ? standardWW.className.replace(themeClass, '').trim() : standardWW.className;
70
+ const editableDiv = dom.utils.createElement('div', { class: wwClassName }, standardWW.innerHTML);
67
71
  ww = dom.utils.createElement('div', { style: `position: absolute; top: -10000px; left: -10000px; width: 21cm; columns: 21cm; height: auto;` }, editableDiv);
68
72
 
69
73
  const innerPadding = _w.getComputedStyle(standardWW).padding;
70
74
  const inlineWW = dom.utils.applyInlineStylesAll(editableDiv, true, this.$.options.get('allUsedStyles'));
71
75
  inlineWW.style.padding = inlineWW.style.paddingTop = inlineWW.style.paddingBottom = inlineWW.style.paddingLeft = inlineWW.style.paddingRight = '0';
76
+
72
77
  ww.innerHTML = `
73
78
  <style>
74
79
  @page {
@@ -109,8 +114,15 @@ class ExportPDF extends PluginCommand {
109
114
  const xhr = await this.apiManager.asyncCall({ data: JSON.stringify(data) });
110
115
 
111
116
  if (xhr.status !== 200) {
112
- const res = !xhr.responseText ? xhr : JSON.parse(xhr.responseText);
113
- throw Error(`[SUNEDITOR.plugins.exportPDF.error] ${res.errorMessage}`);
117
+ let errorMessage;
118
+
119
+ try {
120
+ errorMessage = JSON.parse(xhr.responseText).errorMessage;
121
+ } catch {
122
+ // ignore
123
+ }
124
+
125
+ throw Error(`[SUNEDITOR.plugins.exportPDF.error] ${errorMessage || xhr.statusText}`);
114
126
  }
115
127
 
116
128
  const blob = new Blob([xhr.response], { type: 'application/pdf' });
@@ -12,7 +12,10 @@ const { NO_EVENT } = env;
12
12
  * @property {number} [uploadSizeLimit] - Total upload size limit in bytes
13
13
  * @property {number} [uploadSingleSizeLimit] - Single file size limit in bytes
14
14
  * @property {boolean} [allowMultiple=false] - Allow multiple file uploads
15
- * @property {string} [acceptedFormats="*"] - Accepted file formats (e.g., 'image/*, .pdf')
15
+ * @property {string} [acceptedFormats="*"] - Accepted file formats.
16
+ * ```js
17
+ * { acceptedFormats: 'image/*, .pdf, .docx' }
18
+ * ```
16
19
  * @property {string} [as="box"] - Specify the default form of the file component as `box` or `link`
17
20
  * @property {Array<string>} [controls] - Additional controls to be added to the figure
18
21
  * @property {SunEditor.ComponentInsertType} [insertBehavior] - Component insertion behavior for selection and cursor placement.
@@ -4,9 +4,13 @@ import { dom } from '../../helper';
4
4
 
5
5
  /**
6
6
  * @typedef {Object} BackgroundColorPluginOptions
7
- * @property {Array<string|{value: string, name: string}>} [items] - Color list
7
+ * @property {Array<string|{value: string, name: string}>} [items] - Color list.
8
+ * Use HEX strings or objects with `value`/`name` for labeled colors.
8
9
  * @property {number} [splitNum] - Number of colors per line
9
10
  * @property {boolean} [disableHEXInput] - Disable HEX input
11
+ * ```js
12
+ * { items: ['#ff0000', '#00ff00', { value: '#0000ff', name: 'Blue' }], splitNum: 6 }
13
+ * ```
10
14
  */
11
15
 
12
16
  /**
@@ -7,7 +7,13 @@ import { dom } from '../../helper';
7
7
 
8
8
  /**
9
9
  * @typedef {Object} BlockStylePluginOptions
10
- * @property {Array<"p"|"div"|"blockquote"|"pre"|"h1"|"h2"|"h3"|"h4"|"h5"|"h6"|string|BlockStyleItem>} [items] - Format list
10
+ * @property {Array<"p"|"div"|"blockquote"|"pre"|"h1"|"h2"|"h3"|"h4"|"h5"|"h6"|string|BlockStyleItem>} [items] - Format list.
11
+ * Use string shortcuts for built-in tags, or `BlockStyleItem` objects for custom block styles.
12
+ * - `command` — `"line"`: single line block, `"br-line"`: br-separated block, `"block"`: container block.
13
+ * ```js
14
+ * // string shortcuts + custom item
15
+ * ['p', 'h1', 'h2', 'blockquote', { tag: 'div', command: 'block', name: 'Custom Block', class: 'my-block' }]
16
+ * ```
11
17
  */
12
18
 
13
19
  /**
@@ -133,7 +139,7 @@ class BlockStyle extends PluginDropdown {
133
139
  * @returns {HTMLElement}
134
140
  */
135
141
  function CreateHTML({ lang }, items) {
136
- const defaultFormats = ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
142
+ const defaultFormats = ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
137
143
  const formatList = !items || items.length === 0 ? defaultFormats : items;
138
144
 
139
145
  let list = /*html*/ `