suneditor 3.0.0-beta.2 → 3.0.0-beta.20

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 (177) hide show
  1. package/CONTRIBUTING.md +186 -184
  2. package/LICENSE +21 -21
  3. package/README.md +157 -180
  4. package/dist/suneditor.min.css +1 -1
  5. package/dist/suneditor.min.js +1 -1
  6. package/package.json +126 -123
  7. package/src/assets/design/color.css +131 -121
  8. package/src/assets/design/index.css +3 -3
  9. package/src/assets/design/size.css +37 -35
  10. package/src/assets/design/typography.css +37 -37
  11. package/src/assets/icons/defaultIcons.js +247 -232
  12. package/src/assets/suneditor-contents.css +779 -778
  13. package/src/assets/suneditor.css +43 -35
  14. package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
  15. package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
  16. package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
  17. package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
  18. package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
  19. package/src/core/base/eventManager.js +1550 -1484
  20. package/src/core/base/history.js +355 -355
  21. package/src/core/class/char.js +163 -162
  22. package/src/core/class/component.js +856 -842
  23. package/src/core/class/format.js +3433 -3422
  24. package/src/core/class/html.js +1927 -1890
  25. package/src/core/class/menu.js +357 -346
  26. package/src/core/class/nodeTransform.js +424 -424
  27. package/src/core/class/offset.js +858 -891
  28. package/src/core/class/selection.js +710 -620
  29. package/src/core/class/shortcuts.js +98 -98
  30. package/src/core/class/toolbar.js +438 -430
  31. package/src/core/class/ui.js +424 -422
  32. package/src/core/class/viewer.js +750 -750
  33. package/src/core/editor.js +1810 -1708
  34. package/src/core/section/actives.js +268 -241
  35. package/src/core/section/constructor.js +1348 -1661
  36. package/src/core/section/context.js +102 -102
  37. package/src/core/section/documentType.js +582 -561
  38. package/src/core/section/options.js +367 -0
  39. package/src/core/util/instanceCheck.js +59 -0
  40. package/src/editorInjector/_classes.js +36 -36
  41. package/src/editorInjector/_core.js +92 -92
  42. package/src/editorInjector/index.js +75 -75
  43. package/src/events.js +634 -622
  44. package/src/helper/clipboard.js +59 -59
  45. package/src/helper/converter.js +586 -564
  46. package/src/helper/dom/domCheck.js +304 -304
  47. package/src/helper/dom/domQuery.js +677 -669
  48. package/src/helper/dom/domUtils.js +618 -557
  49. package/src/helper/dom/index.js +12 -12
  50. package/src/helper/env.js +249 -240
  51. package/src/helper/index.js +25 -25
  52. package/src/helper/keyCodeMap.js +183 -183
  53. package/src/helper/numbers.js +72 -72
  54. package/src/helper/unicode.js +47 -47
  55. package/src/langs/ckb.js +231 -231
  56. package/src/langs/cs.js +231 -231
  57. package/src/langs/da.js +231 -231
  58. package/src/langs/de.js +231 -231
  59. package/src/langs/en.js +230 -230
  60. package/src/langs/es.js +231 -231
  61. package/src/langs/fa.js +231 -231
  62. package/src/langs/fr.js +231 -231
  63. package/src/langs/he.js +231 -231
  64. package/src/langs/hu.js +230 -230
  65. package/src/langs/index.js +28 -28
  66. package/src/langs/it.js +231 -231
  67. package/src/langs/ja.js +230 -230
  68. package/src/langs/km.js +230 -230
  69. package/src/langs/ko.js +230 -230
  70. package/src/langs/lv.js +231 -231
  71. package/src/langs/nl.js +231 -231
  72. package/src/langs/pl.js +231 -231
  73. package/src/langs/pt_br.js +231 -231
  74. package/src/langs/ro.js +231 -231
  75. package/src/langs/ru.js +231 -231
  76. package/src/langs/se.js +231 -231
  77. package/src/langs/tr.js +231 -231
  78. package/src/langs/uk.js +231 -231
  79. package/src/langs/ur.js +231 -231
  80. package/src/langs/zh_cn.js +231 -231
  81. package/src/modules/ApiManager.js +191 -191
  82. package/src/modules/Browser.js +669 -667
  83. package/src/modules/ColorPicker.js +364 -362
  84. package/src/modules/Controller.js +474 -454
  85. package/src/modules/Figure.js +1620 -1617
  86. package/src/modules/FileManager.js +359 -359
  87. package/src/modules/HueSlider.js +577 -565
  88. package/src/modules/Modal.js +346 -346
  89. package/src/modules/ModalAnchorEditor.js +643 -643
  90. package/src/modules/SelectMenu.js +549 -549
  91. package/src/modules/_DragHandle.js +17 -17
  92. package/src/modules/index.js +14 -14
  93. package/src/plugins/browser/audioGallery.js +83 -83
  94. package/src/plugins/browser/fileBrowser.js +103 -103
  95. package/src/plugins/browser/fileGallery.js +83 -83
  96. package/src/plugins/browser/imageGallery.js +81 -81
  97. package/src/plugins/browser/videoGallery.js +103 -103
  98. package/src/plugins/command/blockquote.js +61 -60
  99. package/src/plugins/command/exportPDF.js +134 -134
  100. package/src/plugins/command/fileUpload.js +456 -456
  101. package/src/plugins/command/list_bulleted.js +149 -148
  102. package/src/plugins/command/list_numbered.js +152 -151
  103. package/src/plugins/dropdown/align.js +157 -155
  104. package/src/plugins/dropdown/backgroundColor.js +108 -104
  105. package/src/plugins/dropdown/font.js +141 -137
  106. package/src/plugins/dropdown/fontColor.js +109 -105
  107. package/src/plugins/dropdown/formatBlock.js +170 -178
  108. package/src/plugins/dropdown/hr.js +152 -152
  109. package/src/plugins/dropdown/layout.js +83 -83
  110. package/src/plugins/dropdown/lineHeight.js +131 -130
  111. package/src/plugins/dropdown/list.js +123 -122
  112. package/src/plugins/dropdown/paragraphStyle.js +138 -138
  113. package/src/plugins/dropdown/table.js +4110 -4000
  114. package/src/plugins/dropdown/template.js +83 -83
  115. package/src/plugins/dropdown/textStyle.js +149 -149
  116. package/src/plugins/field/mention.js +242 -242
  117. package/src/plugins/index.js +120 -120
  118. package/src/plugins/input/fontSize.js +414 -410
  119. package/src/plugins/input/pageNavigator.js +71 -70
  120. package/src/plugins/modal/audio.js +677 -677
  121. package/src/plugins/modal/drawing.js +537 -531
  122. package/src/plugins/modal/embed.js +886 -886
  123. package/src/plugins/modal/image.js +1377 -1376
  124. package/src/plugins/modal/link.js +248 -240
  125. package/src/plugins/modal/math.js +563 -563
  126. package/src/plugins/modal/video.js +1226 -1226
  127. package/src/plugins/popup/anchor.js +224 -222
  128. package/src/suneditor.js +114 -107
  129. package/src/themes/dark.css +132 -122
  130. package/src/typedef.js +132 -130
  131. package/types/assets/icons/defaultIcons.d.ts +8 -0
  132. package/types/core/base/eventManager.d.ts +29 -4
  133. package/types/core/class/char.d.ts +2 -1
  134. package/types/core/class/component.d.ts +1 -2
  135. package/types/core/class/format.d.ts +8 -1
  136. package/types/core/class/html.d.ts +8 -0
  137. package/types/core/class/menu.d.ts +8 -0
  138. package/types/core/class/offset.d.ts +24 -26
  139. package/types/core/class/selection.d.ts +2 -0
  140. package/types/core/class/toolbar.d.ts +6 -0
  141. package/types/core/class/ui.d.ts +1 -1
  142. package/types/core/editor.d.ts +34 -12
  143. package/types/core/section/constructor.d.ts +5 -638
  144. package/types/core/section/documentType.d.ts +12 -2
  145. package/types/core/section/options.d.ts +740 -0
  146. package/types/core/util/instanceCheck.d.ts +50 -0
  147. package/types/editorInjector/_core.d.ts +5 -5
  148. package/types/editorInjector/index.d.ts +2 -2
  149. package/types/events.d.ts +2 -0
  150. package/types/helper/converter.d.ts +9 -0
  151. package/types/helper/dom/domQuery.d.ts +5 -5
  152. package/types/helper/dom/domUtils.d.ts +8 -0
  153. package/types/helper/env.d.ts +6 -1
  154. package/types/helper/index.d.ts +4 -1
  155. package/types/index.d.ts +122 -120
  156. package/types/langs/_Lang.d.ts +194 -194
  157. package/types/modules/ColorPicker.d.ts +5 -1
  158. package/types/modules/Controller.d.ts +8 -4
  159. package/types/modules/Figure.d.ts +2 -1
  160. package/types/modules/HueSlider.d.ts +4 -1
  161. package/types/modules/SelectMenu.d.ts +1 -1
  162. package/types/plugins/command/blockquote.d.ts +1 -0
  163. package/types/plugins/command/list_bulleted.d.ts +1 -0
  164. package/types/plugins/command/list_numbered.d.ts +1 -0
  165. package/types/plugins/dropdown/align.d.ts +1 -0
  166. package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
  167. package/types/plugins/dropdown/font.d.ts +1 -0
  168. package/types/plugins/dropdown/fontColor.d.ts +1 -0
  169. package/types/plugins/dropdown/formatBlock.d.ts +3 -2
  170. package/types/plugins/dropdown/lineHeight.d.ts +1 -0
  171. package/types/plugins/dropdown/list.d.ts +1 -0
  172. package/types/plugins/dropdown/table.d.ts +6 -0
  173. package/types/plugins/input/fontSize.d.ts +1 -0
  174. package/types/plugins/modal/drawing.d.ts +4 -0
  175. package/types/plugins/modal/link.d.ts +32 -15
  176. package/types/suneditor.d.ts +13 -9
  177. package/types/typedef.d.ts +8 -0
@@ -1,643 +1,643 @@
1
- import EditorInjector from '../editorInjector';
2
- import SelectMenu from './SelectMenu';
3
- import FileManager from './FileManager';
4
- import { dom, numbers, env, unicode } from '../helper';
5
- const { NO_EVENT } = env;
6
-
7
- /**
8
- * @typedef {{default?: string, check_new_window?: string, check_bookmark?: string}} RELAttr
9
- */
10
-
11
- /**
12
- * @typedef {Object} ModalAnchorEditorParams
13
- * @property {boolean} [title=false] - Modal title display.
14
- * @property {boolean} [textToDisplay=''] - Create Text to display input.
15
- * @property {boolean} [openNewWindow=false] - Default checked value of the "Open in new window" checkbox.
16
- * @property {boolean} [noAutoPrefix=false] - If true, disables the automatic prefixing of the host URL to the value of the link.
17
- * @property {Array<string>} [relList=[]] - The "rel" attribute list of anchor tag.
18
- * @property {RELAttr} [defaultRel={}] - Default "rel" attributes of anchor tag.
19
- * @property {string=} uploadUrl - File upload URL.
20
- * @property {Object<string, string>=} uploadHeaders - File upload headers.
21
- * @property {number=} uploadSizeLimit - File upload size limit.
22
- * @property {number=} uploadSingleSizeLimit - File upload single size limit.
23
- * @property {string=} acceptedFormats - File upload accepted formats.
24
- * @property {boolean=} enableFileUpload - If true, enables file upload.
25
- * @example "REL" structure
26
- {
27
- default: 'nofollow', // Default rel
28
- check_new_window: 'noreferrer noopener', // When "open new window" is checked
29
- check_bookmark: 'bookmark' // When "bookmark" is checked
30
- }
31
- If true, disables the automatic prefixing of the host URL to the value of the link.
32
- */
33
-
34
- /**
35
- * @class
36
- * @description Modal form Anchor tag editor
37
- * - Use it by inserting it into Modal in a plugin that uses Modal.
38
- */
39
- class ModalAnchorEditor extends EditorInjector {
40
- /**
41
- * @constructor
42
- * @param {*} inst The instance object that called the constructor.
43
- * @param {Node} modalForm The modal form element
44
- * @param {ModalAnchorEditorParams} params ModalAnchorEditor options
45
- */
46
- constructor(inst, modalForm, params) {
47
- // plugin bisic properties
48
- super(inst.editor);
49
-
50
- // params
51
- this.openNewWindow = !!params.openNewWindow;
52
- this.relList = Array.isArray(params.relList) ? params.relList : [];
53
- this.defaultRel = params.defaultRel || {};
54
- this.noAutoPrefix = !!params.noAutoPrefix;
55
- // file upload
56
- if (params.enableFileUpload) {
57
- this.uploadUrl = typeof params.uploadUrl === 'string' ? params.uploadUrl : null;
58
- this.uploadHeaders = params.uploadHeaders || null;
59
- this.uploadSizeLimit = numbers.get(params.uploadSizeLimit, 0) || null;
60
- this.uploadSingleSizeLimit = numbers.get(params.uploadSingleSizeLimit, 0) || null;
61
- this.input = dom.utils.createElement('input', { type: 'file', accept: params.acceptedFormats || '*' });
62
- this.eventManager.addEvent(this.input, 'change', this.#OnChangeFile.bind(this));
63
- // file manager
64
- this.fileManager = new FileManager(this, {
65
- query: 'a[download]:not([data-se-file-download])',
66
- loadHandler: this.events.onFileLoad,
67
- eventHandler: this.events.onFileAction
68
- });
69
- }
70
-
71
- // create HTML
72
- const forms = CreatetModalForm(inst.editor, params, this.relList);
73
-
74
- // members
75
- this.kink = inst.constructor.key || inst.constructor.name;
76
- this.inst = inst;
77
- this.modalForm = /** @type {HTMLElement} */ (modalForm);
78
- this.host = (this._w.location.origin + this._w.location.pathname).replace(/\/$/, '');
79
-
80
- /** @type {HTMLInputElement} */
81
- this.urlInput = forms.querySelector('.se-input-url');
82
- /** @type {HTMLInputElement} */
83
- this.displayInput = forms.querySelector('._se_display_text');
84
- /** @type {HTMLInputElement} */
85
- this.titleInput = forms.querySelector('._se_title');
86
- /** @type {HTMLInputElement} */
87
- this.newWindowCheck = forms.querySelector('._se_anchor_check');
88
- /** @type {HTMLInputElement} */
89
- this.downloadCheck = forms.querySelector('._se_anchor_download');
90
- /** @type {HTMLElement} */
91
- this.download = forms.querySelector('._se_anchor_download_icon');
92
- /** @type {HTMLElement} */
93
- this.preview = forms.querySelector('.se-link-preview');
94
- /** @type {HTMLElement} */
95
- this.bookmark = forms.querySelector('._se_anchor_bookmark_icon');
96
- /** @type {HTMLButtonElement} */
97
- this.bookmarkButton = forms.querySelector('._se_bookmark_button');
98
-
99
- this.currentRel = [];
100
- this.currentTarget = null;
101
- this.linkValue = '';
102
- this._change = false;
103
- this._isRel = this.relList.length > 0;
104
- // members - rel
105
- if (this._isRel) {
106
- /** @type {HTMLButtonElement} */
107
- this.relButton = forms.querySelector('.se-anchor-rel-btn');
108
- /** @type {HTMLElement} */
109
- this.relPreview = forms.querySelector('.se-anchor-rel-preview');
110
-
111
- const relList = this.relList;
112
- const defaultRel = (this.defaultRel.default || '').split(' ');
113
- const list = [];
114
- for (let i = 0, len = relList.length, rel; i < len; i++) {
115
- rel = relList[i];
116
- list.push(
117
- dom.utils.createElement(
118
- 'BUTTON',
119
- {
120
- type: 'button',
121
- class: 'se-btn-list' + (defaultRel.includes(rel) ? ' se-checked' : ''),
122
- 'data-command': rel,
123
- title: rel,
124
- 'aria-label': rel
125
- },
126
- rel + '<span class="se-svg">' + this.icons.checked + '</span>'
127
- )
128
- );
129
- }
130
- this.selectMenu_rel = new SelectMenu(this, { checkList: true, position: 'right-middle', dir: 'ltr' });
131
- this.selectMenu_rel.on(this.relButton, this.#SetRelItem.bind(this));
132
- this.selectMenu_rel.create(list);
133
- this.eventManager.addEvent(this.relButton, 'click', this.#OnClick_relbutton.bind(this));
134
- }
135
-
136
- // init
137
- this.modalForm.querySelector('.se-anchor-editor').appendChild(forms);
138
- this.selectMenu_bookmark = new SelectMenu(this, { checkList: false, position: 'bottom-left', dir: 'ltr' });
139
- this.selectMenu_bookmark.on(this.urlInput, this.#SetHeaderBookmark.bind(this));
140
- this.eventManager.addEvent(this.newWindowCheck, 'change', this.#OnChange_newWindowCheck.bind(this));
141
- this.eventManager.addEvent(this.downloadCheck, 'change', this.#OnChange_downloadCheck.bind(this));
142
- this.eventManager.addEvent(this.displayInput, 'input', this.#OnChange_displayInput.bind(this));
143
- this.eventManager.addEvent(this.urlInput, 'input', this.#OnChange_urlInput.bind(this));
144
- this.eventManager.addEvent(this.urlInput, 'focus', this.#OnFocus_urlInput.bind(this));
145
- this.eventManager.addEvent(this.bookmarkButton, 'click', this.#OnClick_bookmarkButton.bind(this));
146
- this.eventManager.addEvent(forms.querySelector('._se_upload_button'), 'click', () => this.input.click());
147
- }
148
- /**
149
- * @description Initialize.
150
- * - Sets the current anchor element to be edited.
151
- * @param {Node} element Modal target element
152
- */
153
- set(element) {
154
- this.currentTarget = /** @type {HTMLAnchorElement} */ (element);
155
- }
156
-
157
- /**
158
- * @description Opens the anchor editor modal and populates it with data.
159
- * @param {boolean} isUpdate - Indicates whether an existing anchor is being updated (`true`) or a new one is being created (`false`).
160
- */
161
- on(isUpdate) {
162
- if (!isUpdate) {
163
- this.init();
164
- this.displayInput.value = this.selection.get().toString().trim();
165
- this.newWindowCheck.checked = this.openNewWindow;
166
- this.titleInput.value = '';
167
- } else if (this.currentTarget) {
168
- const href = this.currentTarget.href;
169
- this.linkValue = this.preview.textContent = this.urlInput.value = this._selfPathBookmark(href) ? href.substring(href.lastIndexOf('#')) : href;
170
- this.displayInput.value = this.currentTarget.textContent;
171
- this.titleInput.value = this.currentTarget.title;
172
- this.newWindowCheck.checked = /_blank/i.test(this.currentTarget.target) ? true : false;
173
- this.downloadCheck.checked = !!this.currentTarget.download;
174
- }
175
-
176
- this._setRel(isUpdate && this.currentTarget ? this.currentTarget.rel : this.defaultRel.default || '');
177
- this._setLinkPreview(this.linkValue);
178
- }
179
-
180
- /**
181
- * @description Creates an anchor (`<a>`) element with the specified attributes.
182
- * @param {boolean} notText - If `true`, the anchor will not contain text content.
183
- * @returns {HTMLElement|null} - The newly created anchor element, or `null` if the URL is empty.
184
- */
185
- create(notText) {
186
- if (this.linkValue.length === 0) return null;
187
-
188
- const url = this.linkValue;
189
- const displayText = this.displayInput.value.length === 0 ? url : this.displayInput.value;
190
-
191
- const oA = /** @type {HTMLAnchorElement} */ (this.currentTarget || dom.utils.createElement('A'));
192
- this._updateAnchor(oA, url, displayText, this.titleInput.value, notText);
193
- this.linkValue = this.preview.textContent = this.urlInput.value = this.displayInput.value = '';
194
-
195
- return oA;
196
- }
197
-
198
- /**
199
- * @description Resets the ModalAnchorEditor to its initial state.
200
- */
201
- init() {
202
- this.currentTarget = null;
203
- this.linkValue = this.preview.textContent = this.urlInput.value = '';
204
- this.displayInput.value = '';
205
- this.newWindowCheck.checked = false;
206
- this.downloadCheck.checked = false;
207
- this._change = false;
208
- this._setRel(this.defaultRel.default || '');
209
- }
210
-
211
- /**
212
- * @private
213
- * @description Updates the anchor element with new attributes.
214
- * @param {HTMLAnchorElement} anchor - The anchor (`<a>`) element to update.
215
- * @param {string} url - The URL for the anchor's `href` attribute.
216
- * @param {string} displayText - The text to be displayed inside the anchor.
217
- * @param {string} title - The tooltip text (title attribute).
218
- * @param {boolean} notText - If `true`, the anchor will not contain text content.
219
- */
220
- _updateAnchor(anchor, url, displayText, title, notText) {
221
- // download
222
- if (!this._selfPathBookmark(url) && this.downloadCheck.checked) {
223
- anchor.setAttribute('download', displayText || url);
224
- } else {
225
- anchor.removeAttribute('download');
226
- }
227
-
228
- // new window
229
- if (this.newWindowCheck.checked) anchor.target = '_blank';
230
- else anchor.removeAttribute('target');
231
-
232
- // rel
233
- const rel = this.currentRel.join(' ');
234
- if (!rel) anchor.removeAttribute('rel');
235
- else anchor.rel = rel;
236
-
237
- // set url
238
- anchor.href = url;
239
- if (title) anchor.title = title;
240
- else anchor.removeAttribute('title');
241
-
242
- if (notText) {
243
- if (anchor.children.length === 0) anchor.textContent = '';
244
- } else {
245
- anchor.textContent = displayText;
246
- }
247
- }
248
-
249
- /**
250
- * @private
251
- * @description Checks if the given path is an internal bookmark.
252
- * @param {string} path - The URL or anchor link.
253
- * @returns {boolean} - `true` if the path is an internal bookmark, otherwise `false`.
254
- */
255
- _selfPathBookmark(path) {
256
- const href = this._w.location.href.replace(/\/$/, '');
257
- return path.indexOf('#') === 0 || (path.indexOf(href) === 0 && path.indexOf('#') === (!href.includes('#') ? href.length : href.substring(0, href.indexOf('#')).length));
258
- }
259
-
260
- /**
261
- * @private
262
- * @description Updates the `rel` attribute list in the modal and preview.
263
- * @param {string} relAttr - The `rel` attribute string to set.
264
- */
265
- _setRel(relAttr) {
266
- if (!this._isRel) return;
267
-
268
- const rels = (this.currentRel = !relAttr ? [] : relAttr.split(' '));
269
- const checkedRel = this.selectMenu_rel.form.querySelectorAll('button');
270
- for (let i = 0, len = checkedRel.length, cmd; i < len; i++) {
271
- cmd = checkedRel[i].getAttribute('data-command');
272
- if (rels.includes(cmd)) {
273
- dom.utils.addClass(checkedRel[i], 'se-checked');
274
- } else {
275
- dom.utils.removeClass(checkedRel[i], 'se-checked');
276
- }
277
- }
278
-
279
- this.relPreview.title = this.relPreview.textContent = rels.join(' ');
280
- if (rels.length > 0) {
281
- dom.utils.addClass(this.relButton, 'on');
282
- } else {
283
- dom.utils.removeClass(this.relButton, 'on');
284
- }
285
- }
286
-
287
- /**
288
- * @private
289
- * @description Generates a list of bookmark headers within the editor.
290
- * @param {string} urlValue - The current URL input value.
291
- */
292
- _createBookmarkList(urlValue) {
293
- const headers = dom.query.getListChildren(this.editor.frameContext.get('wysiwyg'), (current) => /h[1-6]/i.test(current.nodeName) || (dom.check.isAnchor(current) && !!current.id));
294
- if (headers.length === 0) return;
295
-
296
- const valueRegExp = new RegExp(`^${urlValue.replace(/^#/, '')}`, 'i');
297
- const list = [];
298
- const menus = [];
299
- for (let i = 0, len = headers.length, v; i < len; i++) {
300
- v = headers[i];
301
- if (!valueRegExp.test(v.textContent)) continue;
302
- list.push(v);
303
- menus.push(dom.check.isAnchor(v) ? `<div><span class="se-text-prefix-icon">${this.icons.bookmark_anchor}</span>${v.id}</div>` : `<div style="${v.style.cssText}">${v.textContent}</div>`);
304
- }
305
-
306
- if (list.length === 0) {
307
- this.selectMenu_bookmark.close();
308
- } else {
309
- this.selectMenu_bookmark.create(list, menus);
310
- this.selectMenu_bookmark.open(this.options.get('_rtl') ? 'bottom-right' : '');
311
- }
312
- }
313
-
314
- /**
315
- * @private
316
- * @description Updates the preview of the anchor link.
317
- * @param {string} value - The current URL value.
318
- */
319
- _setLinkPreview(value) {
320
- const preview = this.preview;
321
- const protocol = this.options.get('defaultUrlProtocol');
322
- const noPrefix = this.noAutoPrefix;
323
- const reservedProtocol = /^(mailto:|tel:|sms:|https*:\/\/|#)/.test(value) || value.indexOf(protocol) === 0;
324
- const sameProtocol = !protocol ? false : RegExp('^' + unicode.escapeStringRegexp(value.substring(0, protocol.length))).test(protocol);
325
-
326
- value =
327
- this.linkValue =
328
- preview.textContent =
329
- !value ? '' : noPrefix ? value : protocol && !reservedProtocol && !sameProtocol ? protocol + value : reservedProtocol ? value : /^www\./.test(value) ? 'http://' + value : this.host + (/^\//.test(value) ? '' : '/') + value;
330
-
331
- if (this._selfPathBookmark(value)) {
332
- this.bookmark.style.display = 'block';
333
- dom.utils.addClass(this.bookmarkButton, 'active');
334
- } else {
335
- this.bookmark.style.display = 'none';
336
- dom.utils.removeClass(this.bookmarkButton, 'active');
337
- }
338
-
339
- if (!this._selfPathBookmark(value) && this.downloadCheck.checked) {
340
- this.download.style.display = 'block';
341
- } else {
342
- this.download.style.display = 'none';
343
- }
344
- }
345
-
346
- /**
347
- * @private
348
- * @description Merges the given `rel` attribute value with the current list.
349
- * @param {string} relAttr - The `rel` attribute to merge.
350
- * @returns {string} - The updated `rel` attribute string.
351
- */
352
- _relMerge(relAttr) {
353
- const current = this.currentRel;
354
- if (!relAttr) return current.join(' ');
355
-
356
- if (/^only:/.test(relAttr)) {
357
- relAttr = relAttr.replace(/^only:/, '').trim();
358
- this.currentRel = relAttr.split(' ');
359
- return relAttr;
360
- }
361
-
362
- const rels = relAttr.split(' ');
363
- for (let i = 0, len = rels.length; i < len; i++) {
364
- if (!current.includes(rels[i])) current.push(rels[i]);
365
- }
366
-
367
- return current.join(' ');
368
- }
369
-
370
- /**
371
- * @private
372
- * @description Removes the specified `rel` attribute from the current list.
373
- * @param {string} relAttr - The `rel` attribute to remove.
374
- * @returns {string} - The updated `rel` attribute string.
375
- */
376
- _relDelete(relAttr) {
377
- if (!relAttr) return this.currentRel.join(' ');
378
- if (/^only:/.test(relAttr)) relAttr = relAttr.replace(/^only:/, '').trim();
379
-
380
- const rels = this.currentRel.join(' ').replace(RegExp(relAttr + '\\s*'), '');
381
- this.currentRel = rels.split(' ');
382
- return rels;
383
- }
384
-
385
- /**
386
- * @private
387
- * @description Registers a newly uploaded file and sets its URL in the modal form.
388
- * @param {Object<string, *>} response - The response object from the file upload request.
389
- */
390
- _register(response) {
391
- const file = response.result[0];
392
- this.linkValue = this.preview.textContent = this.urlInput.value = file.url;
393
- this.displayInput.value = file.name;
394
- this.downloadCheck.checked = true;
395
- this.download.style.display = 'block';
396
- }
397
-
398
- /**
399
- * @private
400
- * @description Handles file upload errors.
401
- * @param {Object<string, *>} response - The error response object.
402
- * @returns {Promise<void>}
403
- */
404
- async _error(response) {
405
- const message = await this.triggerEvent('onFileUploadError', { error: response });
406
- if (message === false) return;
407
- const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
408
- this.ui.alertOpen(err, 'error');
409
- console.error('[SUNEDITOR.plugin.fileUpload.error]', err);
410
- }
411
-
412
- /**
413
- * @description Handles the callback after a file upload completes.
414
- * @param {XMLHttpRequest} xmlHttp - The XMLHttpRequest object containing the response.
415
- */
416
- _uploadCallBack(xmlHttp) {
417
- const response = JSON.parse(xmlHttp.responseText);
418
- if (response.errorMessage) {
419
- this._error(response);
420
- } else {
421
- this._register(response);
422
- }
423
- }
424
-
425
- /**
426
- * @description Handles file input change events.
427
- * @param {InputEvent} e - The change event object.
428
- */
429
- async #OnChangeFile(e) {
430
- /** @type {HTMLInputElement} */
431
- const eventTarget = dom.query.getEventTarget(e);
432
- const files = eventTarget.files;
433
- if (!files[0]) return;
434
-
435
- const fileInfo = {
436
- url: this.uploadUrl,
437
- uploadHeaders: this.uploadHeaders,
438
- files
439
- };
440
-
441
- const handler = async function (infos, newInfos) {
442
- infos = newInfos || infos;
443
- const xmlHttp = await this.fileManager.asyncUpload(infos.url, infos.uploadHeaders, infos.files);
444
- this._uploadCallBack(xmlHttp);
445
- }.bind(this, fileInfo);
446
- // se-ts-ignore
447
- void this._uploadCallBack;
448
-
449
- const result = await this.triggerEvent('onFileUploadBefore', {
450
- info: fileInfo,
451
- handler
452
- });
453
-
454
- if (result === undefined) return true;
455
- if (result === false) return false;
456
- if (result !== null && typeof result === 'object') handler(result);
457
-
458
- if (result === true || result === NO_EVENT) handler(null);
459
- }
460
-
461
- /**
462
- * @description Opens the `rel` attribute selection menu.
463
- */
464
- #OnClick_relbutton() {
465
- this.selectMenu_rel.open(this.options.get('_rtl') ? 'left-middle' : '');
466
- }
467
-
468
- /**
469
- * @description Sets the selected bookmark as the URL.
470
- * @param {HTMLElement} item - The selected bookmark element.
471
- */
472
- #SetHeaderBookmark(item) {
473
- const id = item.id || 'h_' + Math.random().toString().replace(/.+\./, '');
474
- item.id = id;
475
- this.urlInput.value = '#' + id;
476
-
477
- this._setLinkPreview(this.urlInput.value);
478
- this.selectMenu_bookmark.close();
479
- this.urlInput.focus();
480
- }
481
-
482
- /**
483
- * @param {HTMLElement} item - The selected `rel` attribute element.
484
- */
485
- #SetRelItem(item) {
486
- const cmd = item.getAttribute('data-command');
487
- if (!cmd) return;
488
-
489
- const current = this.currentRel;
490
- const index = current.indexOf(cmd);
491
- if (index === -1) current.push(cmd);
492
- else current.splice(index, 1);
493
-
494
- this.relPreview.title = this.relPreview.textContent = current.join(', ');
495
- }
496
-
497
- /**
498
- * @param {InputEvent} e - Event object
499
- */
500
- #OnChange_displayInput(e) {
501
- /** @type {HTMLInputElement} */
502
- const eventTarget = dom.query.getEventTarget(e);
503
- this._change = !!eventTarget.value.trim();
504
- }
505
-
506
- /**
507
- * @param {InputEvent} e - Event object
508
- */
509
- #OnChange_urlInput(e) {
510
- /** @type {HTMLInputElement} */
511
- const eventTarget = dom.query.getEventTarget(e);
512
- const value = eventTarget.value.trim();
513
- this._setLinkPreview(value);
514
- if (this._selfPathBookmark(value)) this._createBookmarkList(value);
515
- else this.selectMenu_bookmark.close();
516
- }
517
-
518
- #OnFocus_urlInput() {
519
- const value = this.urlInput.value;
520
- if (this._selfPathBookmark(value)) this._createBookmarkList(value);
521
- }
522
-
523
- #OnClick_bookmarkButton() {
524
- let url = this.urlInput.value;
525
- if (this._selfPathBookmark(url)) {
526
- url = url.substring(1);
527
- this.bookmark.style.display = 'none';
528
- dom.utils.removeClass(this.bookmarkButton, 'active');
529
- } else {
530
- url = '#' + url;
531
- this.bookmark.style.display = 'block';
532
- dom.utils.addClass(this.bookmarkButton, 'active');
533
- this.downloadCheck.checked = false;
534
- this.download.style.display = 'none';
535
- this._createBookmarkList(url);
536
- }
537
-
538
- this.urlInput.value = url;
539
- this._setLinkPreview(url);
540
- this.urlInput.focus();
541
- }
542
-
543
- /**
544
- * @param {InputEvent} e - Event object
545
- */
546
- #OnChange_newWindowCheck(e) {
547
- if (typeof this.defaultRel.check_new_window !== 'string') return;
548
- /** @type {HTMLInputElement} */
549
- const eventTarget = dom.query.getEventTarget(e);
550
- if (eventTarget.checked) {
551
- this._setRel(this._relMerge(this.defaultRel.check_new_window));
552
- } else {
553
- this._setRel(this._relDelete(this.defaultRel.check_new_window));
554
- }
555
- }
556
-
557
- /**
558
- * @param {InputEvent} e - Event object
559
- */
560
- #OnChange_downloadCheck(e) {
561
- /** @type {HTMLInputElement} */
562
- const eventTarget = dom.query.getEventTarget(e);
563
- if (eventTarget.checked) {
564
- this.download.style.display = 'block';
565
- this.bookmark.style.display = 'none';
566
- dom.utils.removeClass(this.bookmarkButton, 'active');
567
- this.linkValue = this.preview.textContent = this.urlInput.value = this.urlInput.value.replace(/^#+/, '');
568
- if (typeof this.defaultRel.check_bookmark === 'string') {
569
- this._setRel(this._relMerge(this.defaultRel.check_bookmark));
570
- }
571
- } else {
572
- this.download.style.display = 'none';
573
- if (typeof this.defaultRel.check_bookmark === 'string') {
574
- this._setRel(this._relDelete(this.defaultRel.check_bookmark));
575
- }
576
- }
577
- }
578
- }
579
-
580
- /**
581
- * @private
582
- * @param {__se__EditorCore} editor - Editor instance
583
- * @param {ModalAnchorEditorParams} params - ModalAnchorEditor options
584
- * @param {Array<string>} relList - REL attribute list
585
- * @returns {HTMLElement} - Modal form element
586
- */
587
- function CreatetModalForm(editor, params, relList) {
588
- const lang = editor.lang;
589
- const icons = editor.icons;
590
- const textDisplayShow = params.textToDisplay ? '' : 'style="display: none;"';
591
- const titleShow = params.title ? '' : 'style="display: none;"';
592
-
593
- let html = /*html*/ `
594
- <div class="se-modal-body">
595
- <div class="se-modal-form">
596
- <label>${lang.link_modal_url}</label>
597
- <div class="se-modal-form-files">
598
- <input data-focus class="se-input-form se-input-url" type="text" placeholder="${editor.options.get('protocol') || ''}" />
599
- ${
600
- params.enableFileUpload
601
- ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button _se_upload_button" aria-label="${lang.fileUpload}">
602
- ${icons.file_upload}
603
- ${dom.utils.createTooltipInner(lang.fileUpload)}
604
- </button>`
605
- : ''
606
- }
607
- <button type="button" class="se-btn se-tooltip se-modal-files-edge-button _se_bookmark_button" aria-label="${lang.link_modal_bookmark}">
608
- ${icons.bookmark}
609
- ${dom.utils.createTooltipInner(lang.link_modal_bookmark)}
610
- </button>
611
- </div>
612
- <div class="se-anchor-preview-form">
613
- <span class="se-svg se-anchor-preview-icon _se_anchor_bookmark_icon">${icons.bookmark}</span>
614
- <span class="se-svg se-anchor-preview-icon _se_anchor_download_icon">${icons.download}</span>
615
- <pre class="se-link-preview"></pre>
616
- </div>
617
- <label ${textDisplayShow}>${lang.link_modal_text}</label>
618
- <input class="se-input-form _se_display_text" type="text" ${textDisplayShow} />
619
- <label ${titleShow}>${lang.title}</label>
620
- <input class="se-input-form _se_title" type="text" ${titleShow} />
621
- </div>
622
- <div class="se-modal-form-footer">
623
- <label><input type="checkbox" class="se-modal-btn-check _se_anchor_check" />&nbsp;${lang.link_modal_newWindowCheck}</label>
624
- <label><input type="checkbox" class="se-modal-btn-check _se_anchor_download" />&nbsp;${lang.link_modal_downloadLinkCheck}</label>`;
625
-
626
- if (relList.length > 0) {
627
- html += /*html*/ `
628
- <div class="se-anchor-rel">
629
- <button type="button" class="se-btn se-tooltip se-anchor-rel-btn" title="${lang.link_modal_relAttribute}" aria-label="${lang.link_modal_relAttribute}">
630
- ${icons.link_rel}
631
- ${dom.utils.createTooltipInner(lang.link_modal_relAttribute)}
632
- </button>
633
- <div class="se-anchor-rel-wrapper"><pre class="se-link-preview se-anchor-rel-preview"></pre></div>
634
- </div>
635
- </div>`;
636
- }
637
-
638
- html += '</div></div>';
639
-
640
- return dom.utils.createElement('DIV', null, html);
641
- }
642
-
643
- export default ModalAnchorEditor;
1
+ import EditorInjector from '../editorInjector';
2
+ import SelectMenu from './SelectMenu';
3
+ import FileManager from './FileManager';
4
+ import { dom, numbers, env, unicode } from '../helper';
5
+ const { NO_EVENT } = env;
6
+
7
+ /**
8
+ * @typedef {{default?: string, check_new_window?: string, check_bookmark?: string}} RELAttr
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} ModalAnchorEditorParams
13
+ * @property {boolean} [title=false] - Modal title display.
14
+ * @property {boolean} [textToDisplay=''] - Create Text to display input.
15
+ * @property {boolean} [openNewWindow=false] - Default checked value of the "Open in new window" checkbox.
16
+ * @property {boolean} [noAutoPrefix=false] - If true, disables the automatic prefixing of the host URL to the value of the link.
17
+ * @property {Array<string>} [relList=[]] - The "rel" attribute list of anchor tag.
18
+ * @property {RELAttr} [defaultRel={}] - Default "rel" attributes of anchor tag.
19
+ * @property {string=} uploadUrl - File upload URL.
20
+ * @property {Object<string, string>=} uploadHeaders - File upload headers.
21
+ * @property {number=} uploadSizeLimit - File upload size limit.
22
+ * @property {number=} uploadSingleSizeLimit - File upload single size limit.
23
+ * @property {string=} acceptedFormats - File upload accepted formats.
24
+ * @property {boolean=} enableFileUpload - If true, enables file upload.
25
+ * @example "REL" structure
26
+ {
27
+ default: 'nofollow', // Default rel
28
+ check_new_window: 'noreferrer noopener', // When "open new window" is checked
29
+ check_bookmark: 'bookmark' // When "bookmark" is checked
30
+ }
31
+ If true, disables the automatic prefixing of the host URL to the value of the link.
32
+ */
33
+
34
+ /**
35
+ * @class
36
+ * @description Modal form Anchor tag editor
37
+ * - Use it by inserting it into Modal in a plugin that uses Modal.
38
+ */
39
+ class ModalAnchorEditor extends EditorInjector {
40
+ /**
41
+ * @constructor
42
+ * @param {*} inst The instance object that called the constructor.
43
+ * @param {Node} modalForm The modal form element
44
+ * @param {ModalAnchorEditorParams} params ModalAnchorEditor options
45
+ */
46
+ constructor(inst, modalForm, params) {
47
+ // plugin bisic properties
48
+ super(inst.editor);
49
+
50
+ // params
51
+ this.openNewWindow = !!params.openNewWindow;
52
+ this.relList = Array.isArray(params.relList) ? params.relList : [];
53
+ this.defaultRel = params.defaultRel || {};
54
+ this.noAutoPrefix = !!params.noAutoPrefix;
55
+ // file upload
56
+ if (params.enableFileUpload) {
57
+ this.uploadUrl = typeof params.uploadUrl === 'string' ? params.uploadUrl : null;
58
+ this.uploadHeaders = params.uploadHeaders || null;
59
+ this.uploadSizeLimit = numbers.get(params.uploadSizeLimit, 0) || null;
60
+ this.uploadSingleSizeLimit = numbers.get(params.uploadSingleSizeLimit, 0) || null;
61
+ this.input = dom.utils.createElement('input', { type: 'file', accept: params.acceptedFormats || '*' });
62
+ this.eventManager.addEvent(this.input, 'change', this.#OnChangeFile.bind(this));
63
+ // file manager
64
+ this.fileManager = new FileManager(this, {
65
+ query: 'a[download]:not([data-se-file-download])',
66
+ loadHandler: this.events.onFileLoad,
67
+ eventHandler: this.events.onFileAction
68
+ });
69
+ }
70
+
71
+ // create HTML
72
+ const forms = CreatetModalForm(inst.editor, params, this.relList);
73
+
74
+ // members
75
+ this.kink = inst.constructor.key || inst.constructor.name;
76
+ this.inst = inst;
77
+ this.modalForm = /** @type {HTMLElement} */ (modalForm);
78
+ this.host = (this._w.location.origin + this._w.location.pathname).replace(/\/$/, '');
79
+
80
+ /** @type {HTMLInputElement} */
81
+ this.urlInput = forms.querySelector('.se-input-url');
82
+ /** @type {HTMLInputElement} */
83
+ this.displayInput = forms.querySelector('._se_display_text');
84
+ /** @type {HTMLInputElement} */
85
+ this.titleInput = forms.querySelector('._se_title');
86
+ /** @type {HTMLInputElement} */
87
+ this.newWindowCheck = forms.querySelector('._se_anchor_check');
88
+ /** @type {HTMLInputElement} */
89
+ this.downloadCheck = forms.querySelector('._se_anchor_download');
90
+ /** @type {HTMLElement} */
91
+ this.download = forms.querySelector('._se_anchor_download_icon');
92
+ /** @type {HTMLElement} */
93
+ this.preview = forms.querySelector('.se-link-preview');
94
+ /** @type {HTMLElement} */
95
+ this.bookmark = forms.querySelector('._se_anchor_bookmark_icon');
96
+ /** @type {HTMLButtonElement} */
97
+ this.bookmarkButton = forms.querySelector('._se_bookmark_button');
98
+
99
+ this.currentRel = [];
100
+ this.currentTarget = null;
101
+ this.linkValue = '';
102
+ this._change = false;
103
+ this._isRel = this.relList.length > 0;
104
+ // members - rel
105
+ if (this._isRel) {
106
+ /** @type {HTMLButtonElement} */
107
+ this.relButton = forms.querySelector('.se-anchor-rel-btn');
108
+ /** @type {HTMLElement} */
109
+ this.relPreview = forms.querySelector('.se-anchor-rel-preview');
110
+
111
+ const relList = this.relList;
112
+ const defaultRel = (this.defaultRel.default || '').split(' ');
113
+ const list = [];
114
+ for (let i = 0, len = relList.length, rel; i < len; i++) {
115
+ rel = relList[i];
116
+ list.push(
117
+ dom.utils.createElement(
118
+ 'BUTTON',
119
+ {
120
+ type: 'button',
121
+ class: 'se-btn-list' + (defaultRel.includes(rel) ? ' se-checked' : ''),
122
+ 'data-command': rel,
123
+ title: rel,
124
+ 'aria-label': rel
125
+ },
126
+ rel + '<span class="se-svg">' + this.icons.checked + '</span>'
127
+ )
128
+ );
129
+ }
130
+ this.selectMenu_rel = new SelectMenu(this, { checkList: true, position: 'right-middle', dir: 'ltr' });
131
+ this.selectMenu_rel.on(this.relButton, this.#SetRelItem.bind(this));
132
+ this.selectMenu_rel.create(list);
133
+ this.eventManager.addEvent(this.relButton, 'click', this.#OnClick_relbutton.bind(this));
134
+ }
135
+
136
+ // init
137
+ this.modalForm.querySelector('.se-anchor-editor').appendChild(forms);
138
+ this.selectMenu_bookmark = new SelectMenu(this, { checkList: false, position: 'bottom-left', dir: 'ltr' });
139
+ this.selectMenu_bookmark.on(this.urlInput, this.#SetHeaderBookmark.bind(this));
140
+ this.eventManager.addEvent(this.newWindowCheck, 'change', this.#OnChange_newWindowCheck.bind(this));
141
+ this.eventManager.addEvent(this.downloadCheck, 'change', this.#OnChange_downloadCheck.bind(this));
142
+ this.eventManager.addEvent(this.displayInput, 'input', this.#OnChange_displayInput.bind(this));
143
+ this.eventManager.addEvent(this.urlInput, 'input', this.#OnChange_urlInput.bind(this));
144
+ this.eventManager.addEvent(this.urlInput, 'focus', this.#OnFocus_urlInput.bind(this));
145
+ this.eventManager.addEvent(this.bookmarkButton, 'click', this.#OnClick_bookmarkButton.bind(this));
146
+ this.eventManager.addEvent(forms.querySelector('._se_upload_button'), 'click', () => this.input.click());
147
+ }
148
+ /**
149
+ * @description Initialize.
150
+ * - Sets the current anchor element to be edited.
151
+ * @param {Node} element Modal target element
152
+ */
153
+ set(element) {
154
+ this.currentTarget = /** @type {HTMLAnchorElement} */ (element);
155
+ }
156
+
157
+ /**
158
+ * @description Opens the anchor editor modal and populates it with data.
159
+ * @param {boolean} isUpdate - Indicates whether an existing anchor is being updated (`true`) or a new one is being created (`false`).
160
+ */
161
+ on(isUpdate) {
162
+ if (!isUpdate) {
163
+ this.init();
164
+ this.displayInput.value = this.selection.get().toString().trim();
165
+ this.newWindowCheck.checked = this.openNewWindow;
166
+ this.titleInput.value = '';
167
+ } else if (this.currentTarget) {
168
+ const href = this.currentTarget.href;
169
+ this.linkValue = this.preview.textContent = this.urlInput.value = this._selfPathBookmark(href) ? href.substring(href.lastIndexOf('#')) : href;
170
+ this.displayInput.value = this.currentTarget.textContent;
171
+ this.titleInput.value = this.currentTarget.title;
172
+ this.newWindowCheck.checked = /_blank/i.test(this.currentTarget.target) ? true : false;
173
+ this.downloadCheck.checked = !!this.currentTarget.download;
174
+ }
175
+
176
+ this._setRel(isUpdate && this.currentTarget ? this.currentTarget.rel : this.defaultRel.default || '');
177
+ this._setLinkPreview(this.linkValue);
178
+ }
179
+
180
+ /**
181
+ * @description Creates an anchor (`<a>`) element with the specified attributes.
182
+ * @param {boolean} notText - If `true`, the anchor will not contain text content.
183
+ * @returns {HTMLElement|null} - The newly created anchor element, or `null` if the URL is empty.
184
+ */
185
+ create(notText) {
186
+ if (this.linkValue.length === 0) return null;
187
+
188
+ const url = this.linkValue;
189
+ const displayText = this.displayInput.value.length === 0 ? url : this.displayInput.value;
190
+
191
+ const oA = /** @type {HTMLAnchorElement} */ (this.currentTarget || dom.utils.createElement('A'));
192
+ this._updateAnchor(oA, url, displayText, this.titleInput.value, notText);
193
+ this.linkValue = this.preview.textContent = this.urlInput.value = this.displayInput.value = '';
194
+
195
+ return oA;
196
+ }
197
+
198
+ /**
199
+ * @description Resets the ModalAnchorEditor to its initial state.
200
+ */
201
+ init() {
202
+ this.currentTarget = null;
203
+ this.linkValue = this.preview.textContent = this.urlInput.value = '';
204
+ this.displayInput.value = '';
205
+ this.newWindowCheck.checked = false;
206
+ this.downloadCheck.checked = false;
207
+ this._change = false;
208
+ this._setRel(this.defaultRel.default || '');
209
+ }
210
+
211
+ /**
212
+ * @private
213
+ * @description Updates the anchor element with new attributes.
214
+ * @param {HTMLAnchorElement} anchor - The anchor (`<a>`) element to update.
215
+ * @param {string} url - The URL for the anchor's `href` attribute.
216
+ * @param {string} displayText - The text to be displayed inside the anchor.
217
+ * @param {string} title - The tooltip text (title attribute).
218
+ * @param {boolean} notText - If `true`, the anchor will not contain text content.
219
+ */
220
+ _updateAnchor(anchor, url, displayText, title, notText) {
221
+ // download
222
+ if (!this._selfPathBookmark(url) && this.downloadCheck.checked) {
223
+ anchor.setAttribute('download', displayText || url);
224
+ } else {
225
+ anchor.removeAttribute('download');
226
+ }
227
+
228
+ // new window
229
+ if (this.newWindowCheck.checked) anchor.target = '_blank';
230
+ else anchor.removeAttribute('target');
231
+
232
+ // rel
233
+ const rel = this.currentRel.join(' ');
234
+ if (!rel) anchor.removeAttribute('rel');
235
+ else anchor.rel = rel;
236
+
237
+ // set url
238
+ anchor.href = url;
239
+ if (title) anchor.title = title;
240
+ else anchor.removeAttribute('title');
241
+
242
+ if (notText) {
243
+ if (anchor.children.length === 0) anchor.textContent = '';
244
+ } else {
245
+ anchor.textContent = displayText;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @private
251
+ * @description Checks if the given path is an internal bookmark.
252
+ * @param {string} path - The URL or anchor link.
253
+ * @returns {boolean} - `true` if the path is an internal bookmark, otherwise `false`.
254
+ */
255
+ _selfPathBookmark(path) {
256
+ const href = this._w.location.href.replace(/\/$/, '');
257
+ return path.indexOf('#') === 0 || (path.indexOf(href) === 0 && path.indexOf('#') === (!href.includes('#') ? href.length : href.substring(0, href.indexOf('#')).length));
258
+ }
259
+
260
+ /**
261
+ * @private
262
+ * @description Updates the `rel` attribute list in the modal and preview.
263
+ * @param {string} relAttr - The `rel` attribute string to set.
264
+ */
265
+ _setRel(relAttr) {
266
+ if (!this._isRel) return;
267
+
268
+ const rels = (this.currentRel = !relAttr ? [] : relAttr.split(' '));
269
+ const checkedRel = this.selectMenu_rel.form.querySelectorAll('button');
270
+ for (let i = 0, len = checkedRel.length, cmd; i < len; i++) {
271
+ cmd = checkedRel[i].getAttribute('data-command');
272
+ if (rels.includes(cmd)) {
273
+ dom.utils.addClass(checkedRel[i], 'se-checked');
274
+ } else {
275
+ dom.utils.removeClass(checkedRel[i], 'se-checked');
276
+ }
277
+ }
278
+
279
+ this.relPreview.title = this.relPreview.textContent = rels.join(' ');
280
+ if (rels.length > 0) {
281
+ dom.utils.addClass(this.relButton, 'on');
282
+ } else {
283
+ dom.utils.removeClass(this.relButton, 'on');
284
+ }
285
+ }
286
+
287
+ /**
288
+ * @private
289
+ * @description Generates a list of bookmark headers within the editor.
290
+ * @param {string} urlValue - The current URL input value.
291
+ */
292
+ _createBookmarkList(urlValue) {
293
+ const headers = dom.query.getListChildren(this.editor.frameContext.get('wysiwyg'), (current) => /h[1-6]/i.test(current.nodeName) || (dom.check.isAnchor(current) && !!current.id));
294
+ if (headers.length === 0) return;
295
+
296
+ const valueRegExp = new RegExp(`^${urlValue.replace(/^#/, '')}`, 'i');
297
+ const list = [];
298
+ const menus = [];
299
+ for (let i = 0, len = headers.length, v; i < len; i++) {
300
+ v = headers[i];
301
+ if (!valueRegExp.test(v.textContent)) continue;
302
+ list.push(v);
303
+ menus.push(dom.check.isAnchor(v) ? `<div><span class="se-text-prefix-icon">${this.icons.bookmark_anchor}</span>${v.id}</div>` : `<div style="${v.style.cssText}">${v.textContent}</div>`);
304
+ }
305
+
306
+ if (list.length === 0) {
307
+ this.selectMenu_bookmark.close();
308
+ } else {
309
+ this.selectMenu_bookmark.create(list, menus);
310
+ this.selectMenu_bookmark.open(this.options.get('_rtl') ? 'bottom-right' : '');
311
+ }
312
+ }
313
+
314
+ /**
315
+ * @private
316
+ * @description Updates the preview of the anchor link.
317
+ * @param {string} value - The current URL value.
318
+ */
319
+ _setLinkPreview(value) {
320
+ const preview = this.preview;
321
+ const protocol = this.options.get('defaultUrlProtocol');
322
+ const noPrefix = this.noAutoPrefix;
323
+ const reservedProtocol = /^(mailto:|tel:|sms:|https*:\/\/|#)/.test(value) || value.indexOf(protocol) === 0;
324
+ const sameProtocol = !protocol ? false : RegExp('^' + unicode.escapeStringRegexp(value.substring(0, protocol.length))).test(protocol);
325
+
326
+ value =
327
+ this.linkValue =
328
+ preview.textContent =
329
+ !value ? '' : noPrefix ? value : protocol && !reservedProtocol && !sameProtocol ? protocol + value : reservedProtocol ? value : /^www\./.test(value) ? 'http://' + value : this.host + (/^\//.test(value) ? '' : '/') + value;
330
+
331
+ if (this._selfPathBookmark(value)) {
332
+ this.bookmark.style.display = 'block';
333
+ dom.utils.addClass(this.bookmarkButton, 'active');
334
+ } else {
335
+ this.bookmark.style.display = 'none';
336
+ dom.utils.removeClass(this.bookmarkButton, 'active');
337
+ }
338
+
339
+ if (!this._selfPathBookmark(value) && this.downloadCheck.checked) {
340
+ this.download.style.display = 'block';
341
+ } else {
342
+ this.download.style.display = 'none';
343
+ }
344
+ }
345
+
346
+ /**
347
+ * @private
348
+ * @description Merges the given `rel` attribute value with the current list.
349
+ * @param {string} relAttr - The `rel` attribute to merge.
350
+ * @returns {string} - The updated `rel` attribute string.
351
+ */
352
+ _relMerge(relAttr) {
353
+ const current = this.currentRel;
354
+ if (!relAttr) return current.join(' ');
355
+
356
+ if (/^only:/.test(relAttr)) {
357
+ relAttr = relAttr.replace(/^only:/, '').trim();
358
+ this.currentRel = relAttr.split(' ');
359
+ return relAttr;
360
+ }
361
+
362
+ const rels = relAttr.split(' ');
363
+ for (let i = 0, len = rels.length; i < len; i++) {
364
+ if (!current.includes(rels[i])) current.push(rels[i]);
365
+ }
366
+
367
+ return current.join(' ');
368
+ }
369
+
370
+ /**
371
+ * @private
372
+ * @description Removes the specified `rel` attribute from the current list.
373
+ * @param {string} relAttr - The `rel` attribute to remove.
374
+ * @returns {string} - The updated `rel` attribute string.
375
+ */
376
+ _relDelete(relAttr) {
377
+ if (!relAttr) return this.currentRel.join(' ');
378
+ if (/^only:/.test(relAttr)) relAttr = relAttr.replace(/^only:/, '').trim();
379
+
380
+ const rels = this.currentRel.join(' ').replace(RegExp(relAttr + '\\s*'), '');
381
+ this.currentRel = rels.split(' ');
382
+ return rels;
383
+ }
384
+
385
+ /**
386
+ * @private
387
+ * @description Registers a newly uploaded file and sets its URL in the modal form.
388
+ * @param {Object<string, *>} response - The response object from the file upload request.
389
+ */
390
+ _register(response) {
391
+ const file = response.result[0];
392
+ this.linkValue = this.preview.textContent = this.urlInput.value = file.url;
393
+ this.displayInput.value = file.name;
394
+ this.downloadCheck.checked = true;
395
+ this.download.style.display = 'block';
396
+ }
397
+
398
+ /**
399
+ * @private
400
+ * @description Handles file upload errors.
401
+ * @param {Object<string, *>} response - The error response object.
402
+ * @returns {Promise<void>}
403
+ */
404
+ async _error(response) {
405
+ const message = await this.triggerEvent('onFileUploadError', { error: response });
406
+ if (message === false) return;
407
+ const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
408
+ this.ui.alertOpen(err, 'error');
409
+ console.error('[SUNEDITOR.plugin.fileUpload.error]', err);
410
+ }
411
+
412
+ /**
413
+ * @description Handles the callback after a file upload completes.
414
+ * @param {XMLHttpRequest} xmlHttp - The XMLHttpRequest object containing the response.
415
+ */
416
+ _uploadCallBack(xmlHttp) {
417
+ const response = JSON.parse(xmlHttp.responseText);
418
+ if (response.errorMessage) {
419
+ this._error(response);
420
+ } else {
421
+ this._register(response);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * @description Handles file input change events.
427
+ * @param {InputEvent} e - The change event object.
428
+ */
429
+ async #OnChangeFile(e) {
430
+ /** @type {HTMLInputElement} */
431
+ const eventTarget = dom.query.getEventTarget(e);
432
+ const files = eventTarget.files;
433
+ if (!files[0]) return;
434
+
435
+ const fileInfo = {
436
+ url: this.uploadUrl,
437
+ uploadHeaders: this.uploadHeaders,
438
+ files
439
+ };
440
+
441
+ const handler = async function (infos, newInfos) {
442
+ infos = newInfos || infos;
443
+ const xmlHttp = await this.fileManager.asyncUpload(infos.url, infos.uploadHeaders, infos.files);
444
+ this._uploadCallBack(xmlHttp);
445
+ }.bind(this, fileInfo);
446
+ // se-ts-ignore
447
+ void this._uploadCallBack;
448
+
449
+ const result = await this.triggerEvent('onFileUploadBefore', {
450
+ info: fileInfo,
451
+ handler
452
+ });
453
+
454
+ if (result === undefined) return true;
455
+ if (result === false) return false;
456
+ if (result !== null && typeof result === 'object') handler(result);
457
+
458
+ if (result === true || result === NO_EVENT) handler(null);
459
+ }
460
+
461
+ /**
462
+ * @description Opens the `rel` attribute selection menu.
463
+ */
464
+ #OnClick_relbutton() {
465
+ this.selectMenu_rel.open(this.options.get('_rtl') ? 'left-middle' : '');
466
+ }
467
+
468
+ /**
469
+ * @description Sets the selected bookmark as the URL.
470
+ * @param {HTMLElement} item - The selected bookmark element.
471
+ */
472
+ #SetHeaderBookmark(item) {
473
+ const id = item.id || 'h_' + Math.random().toString().replace(/.+\./, '');
474
+ item.id = id;
475
+ this.urlInput.value = '#' + id;
476
+
477
+ this._setLinkPreview(this.urlInput.value);
478
+ this.selectMenu_bookmark.close();
479
+ this.urlInput.focus();
480
+ }
481
+
482
+ /**
483
+ * @param {HTMLElement} item - The selected `rel` attribute element.
484
+ */
485
+ #SetRelItem(item) {
486
+ const cmd = item.getAttribute('data-command');
487
+ if (!cmd) return;
488
+
489
+ const current = this.currentRel;
490
+ const index = current.indexOf(cmd);
491
+ if (index === -1) current.push(cmd);
492
+ else current.splice(index, 1);
493
+
494
+ this.relPreview.title = this.relPreview.textContent = current.join(', ');
495
+ }
496
+
497
+ /**
498
+ * @param {InputEvent} e - Event object
499
+ */
500
+ #OnChange_displayInput(e) {
501
+ /** @type {HTMLInputElement} */
502
+ const eventTarget = dom.query.getEventTarget(e);
503
+ this._change = !!eventTarget.value.trim();
504
+ }
505
+
506
+ /**
507
+ * @param {InputEvent} e - Event object
508
+ */
509
+ #OnChange_urlInput(e) {
510
+ /** @type {HTMLInputElement} */
511
+ const eventTarget = dom.query.getEventTarget(e);
512
+ const value = eventTarget.value.trim();
513
+ this._setLinkPreview(value);
514
+ if (this._selfPathBookmark(value)) this._createBookmarkList(value);
515
+ else this.selectMenu_bookmark.close();
516
+ }
517
+
518
+ #OnFocus_urlInput() {
519
+ const value = this.urlInput.value;
520
+ if (this._selfPathBookmark(value)) this._createBookmarkList(value);
521
+ }
522
+
523
+ #OnClick_bookmarkButton() {
524
+ let url = this.urlInput.value;
525
+ if (this._selfPathBookmark(url)) {
526
+ url = url.substring(1);
527
+ this.bookmark.style.display = 'none';
528
+ dom.utils.removeClass(this.bookmarkButton, 'active');
529
+ } else {
530
+ url = '#' + url;
531
+ this.bookmark.style.display = 'block';
532
+ dom.utils.addClass(this.bookmarkButton, 'active');
533
+ this.downloadCheck.checked = false;
534
+ this.download.style.display = 'none';
535
+ this._createBookmarkList(url);
536
+ }
537
+
538
+ this.urlInput.value = url;
539
+ this._setLinkPreview(url);
540
+ this.urlInput.focus();
541
+ }
542
+
543
+ /**
544
+ * @param {InputEvent} e - Event object
545
+ */
546
+ #OnChange_newWindowCheck(e) {
547
+ if (typeof this.defaultRel.check_new_window !== 'string') return;
548
+ /** @type {HTMLInputElement} */
549
+ const eventTarget = dom.query.getEventTarget(e);
550
+ if (eventTarget.checked) {
551
+ this._setRel(this._relMerge(this.defaultRel.check_new_window));
552
+ } else {
553
+ this._setRel(this._relDelete(this.defaultRel.check_new_window));
554
+ }
555
+ }
556
+
557
+ /**
558
+ * @param {InputEvent} e - Event object
559
+ */
560
+ #OnChange_downloadCheck(e) {
561
+ /** @type {HTMLInputElement} */
562
+ const eventTarget = dom.query.getEventTarget(e);
563
+ if (eventTarget.checked) {
564
+ this.download.style.display = 'block';
565
+ this.bookmark.style.display = 'none';
566
+ dom.utils.removeClass(this.bookmarkButton, 'active');
567
+ this.linkValue = this.preview.textContent = this.urlInput.value = this.urlInput.value.replace(/^#+/, '');
568
+ if (typeof this.defaultRel.check_bookmark === 'string') {
569
+ this._setRel(this._relMerge(this.defaultRel.check_bookmark));
570
+ }
571
+ } else {
572
+ this.download.style.display = 'none';
573
+ if (typeof this.defaultRel.check_bookmark === 'string') {
574
+ this._setRel(this._relDelete(this.defaultRel.check_bookmark));
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ /**
581
+ * @private
582
+ * @param {__se__EditorCore} editor - Editor instance
583
+ * @param {ModalAnchorEditorParams} params - ModalAnchorEditor options
584
+ * @param {Array<string>} relList - REL attribute list
585
+ * @returns {HTMLElement} - Modal form element
586
+ */
587
+ function CreatetModalForm(editor, params, relList) {
588
+ const lang = editor.lang;
589
+ const icons = editor.icons;
590
+ const textDisplayShow = params.textToDisplay ? '' : 'style="display: none;"';
591
+ const titleShow = params.title ? '' : 'style="display: none;"';
592
+
593
+ let html = /*html*/ `
594
+ <div class="se-modal-body">
595
+ <div class="se-modal-form">
596
+ <label>${lang.link_modal_url}</label>
597
+ <div class="se-modal-form-files">
598
+ <input data-focus class="se-input-form se-input-url" type="text" placeholder="${editor.options.get('protocol') || ''}" />
599
+ ${
600
+ params.enableFileUpload
601
+ ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button _se_upload_button" aria-label="${lang.fileUpload}">
602
+ ${icons.file_upload}
603
+ ${dom.utils.createTooltipInner(lang.fileUpload)}
604
+ </button>`
605
+ : ''
606
+ }
607
+ <button type="button" class="se-btn se-tooltip se-modal-files-edge-button _se_bookmark_button" aria-label="${lang.link_modal_bookmark}">
608
+ ${icons.bookmark}
609
+ ${dom.utils.createTooltipInner(lang.link_modal_bookmark)}
610
+ </button>
611
+ </div>
612
+ <div class="se-anchor-preview-form">
613
+ <span class="se-svg se-anchor-preview-icon _se_anchor_bookmark_icon">${icons.bookmark}</span>
614
+ <span class="se-svg se-anchor-preview-icon _se_anchor_download_icon">${icons.download}</span>
615
+ <pre class="se-link-preview"></pre>
616
+ </div>
617
+ <label ${textDisplayShow}>${lang.link_modal_text}</label>
618
+ <input class="se-input-form _se_display_text" type="text" ${textDisplayShow} />
619
+ <label ${titleShow}>${lang.title}</label>
620
+ <input class="se-input-form _se_title" type="text" ${titleShow} />
621
+ </div>
622
+ <div class="se-modal-form-footer">
623
+ <label><input type="checkbox" class="se-modal-btn-check _se_anchor_check" />&nbsp;${lang.link_modal_newWindowCheck}</label>
624
+ <label><input type="checkbox" class="se-modal-btn-check _se_anchor_download" />&nbsp;${lang.link_modal_downloadLinkCheck}</label>`;
625
+
626
+ if (relList.length > 0) {
627
+ html += /*html*/ `
628
+ <div class="se-anchor-rel">
629
+ <button type="button" class="se-btn se-tooltip se-anchor-rel-btn" title="${lang.link_modal_relAttribute}" aria-label="${lang.link_modal_relAttribute}">
630
+ ${icons.link_rel}
631
+ ${dom.utils.createTooltipInner(lang.link_modal_relAttribute)}
632
+ </button>
633
+ <div class="se-anchor-rel-wrapper"><pre class="se-link-preview se-anchor-rel-preview"></pre></div>
634
+ </div>
635
+ </div>`;
636
+ }
637
+
638
+ html += '</div></div>';
639
+
640
+ return dom.utils.createElement('DIV', null, html);
641
+ }
642
+
643
+ export default ModalAnchorEditor;