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
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
  };
@@ -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,9 +124,13 @@ 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
130
134
  * @example
131
135
  * // Open controller on a target element with default options
132
136
  * this.controller.open(target);
@@ -137,9 +141,9 @@ class Controller {
137
141
  * // Open on a Range target (e.g., text selection)
138
142
  * this.controller.open(this.$.selection.getRange());
139
143
  */
140
- open(target, positionTarget, { isWWTarget, initMethod, disabled, addOffset } = {}) {
144
+ open(target, positionTarget, { isWWTarget, passive, initMethod, disabled, addOffset } = {}) {
141
145
  if (_DragHandle.get('__overInfo') === ON_OVER_COMPONENT) {
142
- return;
146
+ passive = true;
143
147
  }
144
148
 
145
149
  if (!target) {
@@ -151,35 +155,38 @@ class Controller {
151
155
  this.form.removeAttribute('data-se-hidden-by-children');
152
156
  this.#__hiddenByParents__.clear();
153
157
 
154
- if (this.#$.store.mode.isBalloon) this.#$.toolbar.hide();
155
- 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();
156
161
 
157
- if (!this.#$.store.get('hasFocus')) {
158
- if (disabled ?? this.disabled) {
159
- this.#$.ui.setControllerOnDisabledButtons(true);
160
- } else {
161
- 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
+ }
162
168
  }
163
169
  }
164
170
 
165
171
  this.currentPositionTarget = positionTarget || target;
166
172
  this.isWWTarget = isWWTarget ?? this.isWWTarget;
167
173
  if (typeof initMethod === 'function') this.#initMethod = initMethod;
168
- this.#$.ui.currentControllerName = this.kind;
174
+ if (!passive) this.#$.ui.currentControllerName = this.kind;
169
175
 
170
- this.#addOffset = { left: 0, top: 0 };
171
- if (addOffset) this.#addOffset = { ...this.#addOffset, ...addOffset };
176
+ this.#addOffset = { left: 0, right: 0, top: 0, ...addOffset };
172
177
 
173
- const parents = this.isOutsideForm ? this.parentsForm : [];
174
- this.#$.ui.opendControllers?.forEach((e) => {
175
- if (!parents.includes(e.form)) e.form.style.zIndex = INDEX_1;
176
- });
177
-
178
- if (this.parentsHide) {
179
- this.parentsForm.forEach((e) => {
180
- e.style.display = 'none';
181
- 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;
182
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
+ }
183
190
  }
184
191
 
185
192
  this.#addGlobalEvent();
@@ -189,7 +196,7 @@ class Controller {
189
196
 
190
197
  const isRangeTarget = this.#$.instanceCheck.isRange(target);
191
198
  this.currentTarget = /** @type {HTMLElement} */ (isRangeTarget ? null : target);
192
- this.#controllerOn(this.form, target, isRangeTarget);
199
+ this.#controllerOn(this.form, target, isRangeTarget, passive);
193
200
  _w.setTimeout(() => _DragHandle.set('__overInfo', false), 0);
194
201
  }
195
202
 
@@ -216,7 +223,7 @@ class Controller {
216
223
  this.isOpen = false;
217
224
  this.#preventClose = false;
218
225
  this.__offset = {};
219
- this.#addOffset = { left: 0, top: 0 };
226
+ this.#addOffset = ADD_OFFSET_VALUE;
220
227
 
221
228
  this.#removeGlobalEvent();
222
229
 
@@ -347,8 +354,9 @@ class Controller {
347
354
  * @param {HTMLFormElement} form Controller element
348
355
  * @param {Node|Range} target Controller target element
349
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.)
350
358
  */
351
- async #controllerOn(form, target, isRangeTarget) {
359
+ async #controllerOn(form, target, isRangeTarget, passive) {
352
360
  /** @type {ControllerInfo} */
353
361
  const info = {
354
362
  position: this.position,
@@ -363,20 +371,21 @@ class Controller {
363
371
 
364
372
  form.style.display = 'block';
365
373
  if (this.#$.contextProvider.shadowRoot) {
366
- this.#shadowRootEventForm = form;
367
- this.#shadowRootEventListener = (e) => e.stopPropagation();
368
- form.addEventListener('mousedown', this.#shadowRootEventListener);
374
+ this.#bindShadowRootEvent = this.#$.eventManager.addEvent(form, 'mousedown', (e) => e.stopPropagation());
369
375
  }
370
376
 
371
- this.#$.ui.onControllerContext();
377
+ if (!passive) {
378
+ this.#$.ui.onControllerContext();
379
+ this.#$.store.set('controlActive', true);
380
+ }
372
381
 
373
382
  if (!this.isOpen) {
374
383
  this.#$.ui.opendControllers.push(info);
375
384
  }
376
385
 
377
- this.isOpen = true;
378
386
  this.#$.store.set('_preventBlur', true);
379
- this.#$.store.set('controlActive', true);
387
+
388
+ this.isOpen = true;
380
389
 
381
390
  this.host.controllerOn?.(form, target);
382
391
  this.#$.eventManager.triggerEvent('onShowController', { caller: this.kind, frameContext: this.#$.frameContext, info });
@@ -401,10 +410,7 @@ class Controller {
401
410
  _w.setTimeout(() => {
402
411
  this.#$.store.set('controlActive', false);
403
412
  }, 0);
404
- if (this.#shadowRootEventForm) {
405
- this.#shadowRootEventForm.removeEventListener('mousedown', this.#shadowRootEventListener);
406
- this.#shadowRootEventForm = this.#shadowRootEventListener = null;
407
- }
413
+ this.#bindShadowRootEvent &&= this.#$.eventManager.removeEvent(this.#bindShadowRootEvent);
408
414
  }
409
415
 
410
416
  /**
@@ -500,7 +506,7 @@ class Controller {
500
506
 
501
507
  /**
502
508
  * @description Checks if the given target is within a form or controller.
503
- * @param {Node} target The target element.
509
+ * @param {Element} target The target element.
504
510
  * @returns {boolean} `true` if the target is inside a form or controller.
505
511
  */
506
512
  #checkForm(target) {
@@ -532,6 +538,11 @@ class Controller {
532
538
  e.stopPropagation();
533
539
  e.preventDefault();
534
540
 
541
+ if (target.getAttribute('data-command') === 'close') {
542
+ this.close();
543
+ return;
544
+ }
545
+
535
546
  this.host.controllerAction(target);
536
547
  }
537
548
 
@@ -144,9 +144,9 @@ class ApiManager {
144
144
  this.#$.ui.hideLoading();
145
145
  }
146
146
  } else {
147
+ console.error(`[SUNEDITOR.ApiManager[${this.kind}].upload.serverException]`, xhr);
147
148
  try {
148
- const res = !xhr.responseText ? xhr : JSON.parse(xhr.responseText);
149
- reject(res);
149
+ reject(_parseErrorResponse(xhr));
150
150
  } finally {
151
151
  this.#$.ui.hideLoading();
152
152
  }
@@ -203,12 +203,12 @@ class ApiManager {
203
203
  // exception
204
204
  console.error(`[SUNEDITOR.ApiManager[${this.kind}].upload.serverException]`, xmlHttp);
205
205
  try {
206
- const res = !xmlHttp.responseText ? xmlHttp : JSON.parse(xmlHttp.responseText);
206
+ const res = _parseErrorResponse(xmlHttp);
207
207
  let message = '';
208
208
  if (typeof errorCallBack === 'function') {
209
209
  message = await errorCallBack(res, xmlHttp);
210
210
  }
211
- 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))}`;
212
212
  this.#$.ui.alertOpen(err, 'error');
213
213
  } catch (error) {
214
214
  throw Error(`[SUNEDITOR.ApiManager[${this.kind}].upload.errorCallBack.fail] ${error.message}`);
@@ -220,4 +220,27 @@ class ApiManager {
220
220
  }
221
221
  }
222
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
+
223
246
  export default ApiManager;
@@ -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;
@@ -16,6 +16,8 @@ const MENU_MIN_HEIGHT = 38;
16
16
  * @property {number} [splitNum=0] Optional split number for horizontal positioning; defines how many items per row
17
17
  * @property {() => void} [openMethod] Optional method to call when the menu is opened
18
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"`).
19
21
  */
20
22
 
21
23
  /**
@@ -64,6 +66,8 @@ class SelectMenu {
64
66
  this.horizontal = !!this.splitNum;
65
67
  this.openMethod = params.openMethod;
66
68
  this.closeMethod = params.closeMethod;
69
+ this.maxHeight = params.maxHeight || '';
70
+ this.minWidth = params.minWidth || '';
67
71
 
68
72
  this.#dirPosition = /^(left|right)$/.test(this.position) ? (this.position === 'left' ? 'right' : 'left') : this.position;
69
73
  this.#dirSubPosition = /^(left|right)$/.test(this.subPosition) ? (this.subPosition === 'left' ? 'right' : 'left') : this.subPosition;
@@ -117,14 +121,20 @@ class SelectMenu {
117
121
  this.#refer = /** @type {HTMLElement} */ (referElement);
118
122
  this.#keydownTarget = dom.check.isInputElement(referElement) ? referElement : this.#$.frameContext.get('_ww');
119
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
+
120
129
  this.form = dom.utils.createElement(
121
130
  'DIV',
122
131
  {
123
132
  class: 'se-select-menu' + (attr.class ? ' ' + attr.class : ''),
124
133
  style: attr.style || '',
125
134
  },
126
- '<div class="se-list-inner"></div>',
135
+ '<div class="se-list-inner"' + (innerStyle ? ' style="' + innerStyle + '"' : '') + '></div>',
127
136
  );
137
+
128
138
  referElement.parentNode.insertBefore(this.form, referElement);
129
139
  }
130
140
 
@@ -407,11 +417,12 @@ class SelectMenu {
407
417
  */
408
418
  #addEvents() {
409
419
  this.#removeEvents();
410
- this.#events = this.#eventHandlers;
411
- this.form.addEventListener('mousedown', this.#events.mousedown);
412
- this.form.addEventListener('mousemove', this.#events.mousemove);
413
- this.form.addEventListener('click', this.#events.click);
414
- 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
+ };
415
426
  }
416
427
 
417
428
  /**
@@ -419,10 +430,10 @@ class SelectMenu {
419
430
  */
420
431
  #removeEvents() {
421
432
  if (!this.#events) return;
422
- this.form.removeEventListener('mousedown', this.#events.mousedown);
423
- this.form.removeEventListener('mousemove', this.#events.mousemove);
424
- this.form.removeEventListener('click', this.#events.click);
425
- 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);
426
437
  this.#events = null;
427
438
  }
428
439
 
@@ -547,7 +558,7 @@ class SelectMenu {
547
558
  #CloseListener_mousedown(e) {
548
559
  const eventTarget = dom.query.getEventTarget(e);
549
560
  if (this.form.contains(eventTarget)) return;
550
- if (e.target !== this.#refer) {
561
+ if (!this.#refer.contains(eventTarget)) {
551
562
  this.close();
552
563
  } else if (!dom.check.isInputElement(eventTarget)) {
553
564
  this.#bindClose_click = this.#$.eventManager.addGlobalEvent('click', this.#globalEventHandlers.click, true);