jodit 4.12.22 → 4.12.24

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 (97) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/es2015/jodit.css +1 -1
  3. package/es2015/jodit.fat.min.js +11 -11
  4. package/es2015/jodit.js +353 -86
  5. package/es2015/jodit.min.js +11 -11
  6. package/es2015/plugins/debug/debug.css +1 -1
  7. package/es2015/plugins/debug/debug.js +1 -1
  8. package/es2015/plugins/debug/debug.min.js +1 -1
  9. package/es2015/plugins/speech-recognize/speech-recognize.css +1 -1
  10. package/es2015/plugins/speech-recognize/speech-recognize.js +1 -1
  11. package/es2015/plugins/speech-recognize/speech-recognize.min.js +1 -1
  12. package/es2018/jodit.fat.min.js +11 -11
  13. package/es2018/jodit.min.js +15 -15
  14. package/es2018/plugins/debug/debug.min.js +1 -1
  15. package/es2018/plugins/speech-recognize/speech-recognize.min.js +1 -1
  16. package/es2021/jodit.css +1 -1
  17. package/es2021/jodit.fat.min.js +13 -13
  18. package/es2021/jodit.js +343 -85
  19. package/es2021/jodit.min.js +13 -13
  20. package/es2021/plugins/debug/debug.css +1 -1
  21. package/es2021/plugins/debug/debug.js +1 -1
  22. package/es2021/plugins/debug/debug.min.js +1 -1
  23. package/es2021/plugins/speech-recognize/speech-recognize.css +1 -1
  24. package/es2021/plugins/speech-recognize/speech-recognize.js +1 -1
  25. package/es2021/plugins/speech-recognize/speech-recognize.min.js +1 -1
  26. package/es2021.en/jodit.css +1 -1
  27. package/es2021.en/jodit.fat.min.js +14 -14
  28. package/es2021.en/jodit.js +321 -63
  29. package/es2021.en/jodit.min.js +13 -13
  30. package/es2021.en/plugins/debug/debug.css +1 -1
  31. package/es2021.en/plugins/debug/debug.js +1 -1
  32. package/es2021.en/plugins/debug/debug.min.js +1 -1
  33. package/es2021.en/plugins/speech-recognize/speech-recognize.css +1 -1
  34. package/es2021.en/plugins/speech-recognize/speech-recognize.js +1 -1
  35. package/es2021.en/plugins/speech-recognize/speech-recognize.min.js +1 -1
  36. package/es5/jodit.css +2 -2
  37. package/es5/jodit.fat.min.js +2 -2
  38. package/es5/jodit.js +443 -153
  39. package/es5/jodit.min.css +2 -2
  40. package/es5/jodit.min.js +2 -2
  41. package/es5/plugins/debug/debug.css +1 -1
  42. package/es5/plugins/debug/debug.js +1 -1
  43. package/es5/plugins/debug/debug.min.js +1 -1
  44. package/es5/plugins/speech-recognize/speech-recognize.css +1 -1
  45. package/es5/plugins/speech-recognize/speech-recognize.js +1 -1
  46. package/es5/plugins/speech-recognize/speech-recognize.min.js +1 -1
  47. package/es5/polyfills.fat.min.js +1 -1
  48. package/es5/polyfills.js +1 -1
  49. package/es5/polyfills.min.js +1 -1
  50. package/esm/config.d.ts +30 -0
  51. package/esm/core/component/view-component.js +8 -0
  52. package/esm/core/constants.js +1 -1
  53. package/esm/core/helpers/checker/is-html-from-word.d.ts +1 -1
  54. package/esm/core/helpers/checker/is-html-from-word.js +12 -1
  55. package/esm/core/selection/style/api/wrap.js +18 -3
  56. package/esm/core/ui/popup/popup.js +4 -1
  57. package/esm/jodit.js +12 -0
  58. package/esm/modules/image-editor/image-editor.js +5 -0
  59. package/esm/modules/toolbar/button/content.d.ts +6 -0
  60. package/esm/modules/toolbar/button/content.js +26 -1
  61. package/esm/modules/uploader/helpers/send-files.js +9 -0
  62. package/esm/plugins/add-new-line/add-new-line.js +4 -1
  63. package/esm/plugins/ai-assistant/ai-assistant.js +7 -1
  64. package/esm/plugins/backspace/backspace.js +7 -3
  65. package/esm/plugins/backspace/cases/check-join-neighbors.js +15 -1
  66. package/esm/plugins/backspace/cases/index.d.ts +19 -3
  67. package/esm/plugins/backspace/cases/index.js +18 -13
  68. package/esm/plugins/backspace/config.d.ts +15 -0
  69. package/esm/plugins/clean-html/clean-html.d.ts +8 -0
  70. package/esm/plugins/clean-html/clean-html.js +16 -0
  71. package/esm/plugins/clean-html/config.d.ts +9 -0
  72. package/esm/plugins/clean-html/config.js +1 -0
  73. package/esm/plugins/color/color.js +21 -21
  74. package/esm/plugins/font/config.js +19 -0
  75. package/esm/plugins/inline-popup/inline-popup.d.ts +7 -0
  76. package/esm/plugins/inline-popup/inline-popup.js +18 -2
  77. package/esm/plugins/link/config.d.ts +6 -0
  78. package/esm/plugins/link/config.js +1 -0
  79. package/esm/plugins/link/link.js +8 -1
  80. package/esm/plugins/link/template.js +10 -1
  81. package/esm/plugins/paste/config.js +11 -2
  82. package/esm/plugins/search/ui/search.js +22 -12
  83. package/esm/plugins/select-cells/select-cells.d.ts +1 -1
  84. package/esm/plugins/select-cells/select-cells.js +39 -8
  85. package/esm/types/uploader.d.ts +23 -0
  86. package/package.json +1 -1
  87. package/types/config.d.ts +30 -0
  88. package/types/core/helpers/checker/is-html-from-word.d.ts +1 -1
  89. package/types/modules/toolbar/button/content.d.ts +6 -0
  90. package/types/plugins/backspace/cases/index.d.ts +19 -3
  91. package/types/plugins/backspace/config.d.ts +15 -0
  92. package/types/plugins/clean-html/clean-html.d.ts +8 -0
  93. package/types/plugins/clean-html/config.d.ts +9 -0
  94. package/types/plugins/inline-popup/inline-popup.d.ts +7 -0
  95. package/types/plugins/link/config.d.ts +6 -0
  96. package/types/plugins/select-cells/select-cells.d.ts +1 -1
  97. package/types/types/uploader.d.ts +23 -0
@@ -15,28 +15,28 @@ export function color(editor) {
15
15
  group: 'color'
16
16
  });
17
17
  const callback = (command, second, third) => {
18
+ var _a;
18
19
  const colorHEX = normalizeColor(third);
19
- switch (command) {
20
- case 'background':
21
- editor.s.commitStyle({
22
- attributes: {
23
- style: {
24
- backgroundColor: !colorHEX
25
- ? ''
26
- : colorHEX
27
- }
28
- }
29
- });
30
- break;
31
- case 'forecolor':
32
- editor.s.commitStyle({
33
- attributes: {
34
- style: {
35
- color: !colorHEX ? '' : colorHEX
36
- }
37
- }
38
- });
39
- break;
20
+ const value = !colorHEX ? '' : colorHEX;
21
+ const style = command === 'background'
22
+ ? { backgroundColor: value }
23
+ : { color: value };
24
+ // Cells selected with the `select-cells` plugin drop or collapse the
25
+ // native range, so `commitStyle` would paint a pending caret format
26
+ // outside the table instead of the selection the user sees. Apply the
27
+ // style to the content of every selected cell instead. See #1250
28
+ const selectedCells = editor
29
+ .getInstance('Table', editor.o)
30
+ .getAllSelectedCells();
31
+ if (selectedCells.length && editor.s.isCollapsed()) {
32
+ selectedCells.forEach(cell => {
33
+ editor.s.select(cell, true);
34
+ editor.s.commitStyle({ attributes: { style } });
35
+ });
36
+ (_a = editor.s.sel) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
37
+ }
38
+ else {
39
+ editor.s.commitStyle({ attributes: { style } });
40
40
  }
41
41
  editor.synchronizeValues();
42
42
  return false;
@@ -117,5 +117,24 @@ Config.prototype.controls.font = {
117
117
  .replace(/[^a-z0-9-]+/g, ',');
118
118
  }
119
119
  },
120
+ // When no font is explicitly set, the computed font-family equals the
121
+ // editor's own default (e.g. `-apple-system, …`). Return '' so the
122
+ // button shows the `Default` list entry instead of that raw stack.
123
+ // See #1370
124
+ value: (editor) => {
125
+ const current = editor.s.current();
126
+ if (!current) {
127
+ return;
128
+ }
129
+ const box = Dom.closest(current, Dom.isElement, editor.editor);
130
+ if (!box) {
131
+ return;
132
+ }
133
+ const value = css(box, 'font-family').toString();
134
+ if (value === css(editor.editor, 'font-family').toString()) {
135
+ return '';
136
+ }
137
+ return value;
138
+ },
120
139
  tooltip: 'Font family'
121
140
  };
@@ -50,6 +50,13 @@ export declare class inlinePopup extends Plugin {
50
50
  private __reopenSelectionPopup;
51
51
  private __onDocumentMouseDown;
52
52
  private __onCloseAllPopups;
53
+ /**
54
+ * The selection rect comes from the editor document — in iframe mode its
55
+ * coordinates are iframe-local, while the popup lives in the host
56
+ * document, so the iframe offset must be added. See
57
+ * https://github.com/xdan/jodit/issues/1058
58
+ */
59
+ private __selectionBound;
53
60
  private snapRange;
54
61
  private onSelectionStart;
55
62
  private onSelectionEnd;
@@ -176,10 +176,26 @@ export class inlinePopup extends Plugin {
176
176
  this.j.async.setTimeout(() => {
177
177
  const sel = this.j.s.sel;
178
178
  if (sel && !sel.isCollapsed) {
179
- this.showPopup(() => this.j.s.range.getBoundingClientRect(), 'selection');
179
+ this.showPopup(() => this.__selectionBound(), 'selection');
180
180
  }
181
181
  }, 1);
182
182
  }
183
+ /**
184
+ * The selection rect comes from the editor document — in iframe mode its
185
+ * coordinates are iframe-local, while the popup lives in the host
186
+ * document, so the iframe offset must be added. See
187
+ * https://github.com/xdan/jodit/issues/1058
188
+ */
189
+ __selectionBound() {
190
+ const rect = this.j.s.range.getBoundingClientRect();
191
+ let { left, top } = rect;
192
+ if (this.j.iframe) {
193
+ const offset = position(this.j.iframe, this.j, true);
194
+ left += offset.left;
195
+ top += offset.top;
196
+ }
197
+ return { left, top, width: rect.width, height: rect.height };
198
+ }
183
199
  onSelectionStart() {
184
200
  this.snapRange = this.j.s.range.cloneRange();
185
201
  }
@@ -219,7 +235,7 @@ export class inlinePopup extends Plugin {
219
235
  if (!node) {
220
236
  return;
221
237
  }
222
- this.showPopup(() => range.getBoundingClientRect(), type);
238
+ this.showPopup(() => this.__selectionBound(), type);
223
239
  }
224
240
  /**
225
241
  * In not collapsed selection - only one image
@@ -39,6 +39,12 @@ declare module 'jodit/config' {
39
39
  * Default value for the `Open in new tab` checkbox when inserting a new link.
40
40
  */
41
41
  openInNewTabCheckboxDefaultChecked: boolean;
42
+ /**
43
+ * Show an `aria-label` text input in the link dialog so an
44
+ * accessible name can be set on the `<a>` (useful when several
45
+ * links share the same visible text, e.g. "here"). Default: false.
46
+ */
47
+ ariaLabelInput: boolean;
42
48
  /**
43
49
  * Use an input text to ask the classname or a select or not ask
44
50
  */
@@ -17,6 +17,7 @@ Config.prototype.link = {
17
17
  noFollowCheckbox: true,
18
18
  openInNewTabCheckbox: true,
19
19
  openInNewTabCheckboxDefaultChecked: false,
20
+ ariaLabelInput: false,
20
21
  modeClassName: 'input',
21
22
  selectMultipleClassName: true,
22
23
  preventReadOnlyNavigation: true,
@@ -121,7 +121,7 @@ export class link extends Plugin {
121
121
  const currentElement = current;
122
122
  const isImageContent = Dom.isImage(currentElement);
123
123
  let { content_input } = elements;
124
- const { className_input } = elements, { className_select } = elements;
124
+ const { className_input } = elements, { className_select } = elements, { aria_label_input } = elements;
125
125
  if (!content_input) {
126
126
  content_input = jodit.c.element('input', {
127
127
  type: 'hidden',
@@ -147,6 +147,9 @@ export class link extends Plugin {
147
147
  if (!isImageContent && current) {
148
148
  content_input.value = getSelectionText();
149
149
  }
150
+ if (aria_label_input) {
151
+ aria_label_input.value = link ? attr(link, 'aria-label') || '' : '';
152
+ }
150
153
  if (link) {
151
154
  url_input.value = attr(link, 'href') || '';
152
155
  if (modeClassName) {
@@ -237,6 +240,10 @@ export class link extends Plugin {
237
240
  }
238
241
  attr(a, 'rel', relParts.length ? relParts.join(' ') : null);
239
242
  }
243
+ if (aria_label_input) {
244
+ const ariaLabel = aria_label_input.value.trim();
245
+ attr(a, 'aria-label', ariaLabel || null);
246
+ }
240
247
  jodit.e.fire('applyLink', jodit, a, form);
241
248
  });
242
249
  jodit.synchronizeValues();
@@ -6,7 +6,7 @@
6
6
  import { UIButton } from "../../core/ui/button/index.js";
7
7
  import { UIBlock, UICheckbox, UIForm, UIInput, UISelect } from "../../core/ui/form/index.js";
8
8
  export const formTemplate = (editor) => {
9
- const { openInNewTabCheckbox, noFollowCheckbox, modeClassName, selectSizeClassName, selectMultipleClassName, selectOptionsClassName } = editor.o.link;
9
+ const { openInNewTabCheckbox, noFollowCheckbox, ariaLabelInput, modeClassName, selectSizeClassName, selectMultipleClassName, selectOptionsClassName } = editor.o.link;
10
10
  return new UIForm(editor, [
11
11
  new UIBlock(editor, [
12
12
  new UIInput(editor, {
@@ -18,6 +18,15 @@ export const formTemplate = (editor) => {
18
18
  required: true
19
19
  })
20
20
  ]),
21
+ ariaLabelInput
22
+ ? new UIBlock(editor, [
23
+ new UIInput(editor, {
24
+ name: 'ariaLabel',
25
+ ref: 'aria_label_input',
26
+ label: 'Aria label'
27
+ })
28
+ ])
29
+ : null,
21
30
  new UIBlock(editor, [
22
31
  new UIInput(editor, {
23
32
  name: 'content',
@@ -3,7 +3,7 @@
3
3
  * Released under MIT see LICENSE.txt in the project root for license information.
4
4
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
5
  */
6
- import { CLIPBOARD_ID, INSERT_AS_HTML, INSERT_AS_TEXT, INSERT_ONLY_TEXT, IS_PROD, TEXT_PLAIN } from "../../core/constants.js";
6
+ import { CLIPBOARD_ID, INSERT_AS_HTML, INSERT_AS_TEXT, INSERT_ONLY_TEXT, IS_PROD, TEXT_HTML, TEXT_PLAIN } from "../../core/constants.js";
7
7
  import { Config } from "../../config.js";
8
8
  import { pasteInsertHtml } from "./helpers.js";
9
9
  Config.prototype.askBeforePasteHTML = true;
@@ -21,6 +21,7 @@ const psKey = 'pasteStorage';
21
21
  Config.prototype.controls.paste = {
22
22
  tooltip: 'Paste from clipboard',
23
23
  async exec(editor, _, { control }) {
24
+ var _a;
24
25
  if (control.name === psKey) {
25
26
  editor.execCommand('showPasteStorage');
26
27
  return;
@@ -31,7 +32,15 @@ Config.prototype.controls.paste = {
31
32
  try {
32
33
  const items = await navigator.clipboard.read();
33
34
  if (items && items.length) {
34
- const textBlob = await items[0].getType(TEXT_PLAIN);
35
+ const item = items[0];
36
+ // Prefer the HTML flavor so the button behaves like the
37
+ // Ctrl+V shortcut (which receives text/html from the
38
+ // native paste event). See
39
+ // https://github.com/xdan/jodit/issues/1061
40
+ const type = ((_a = item.types) === null || _a === void 0 ? void 0 : _a.includes(TEXT_HTML))
41
+ ? TEXT_HTML
42
+ : TEXT_PLAIN;
43
+ const textBlob = await item.getType(type);
35
44
  text = await new Response(textBlob).text();
36
45
  }
37
46
  error = false;
@@ -101,20 +101,30 @@ let UISearch = class UISearch extends UIElement {
101
101
  .on(this.queryInput, 'input', () => {
102
102
  this.setMod('empty-query', !trim(this.queryInput.value).length);
103
103
  })
104
- .on(this.queryInput, 'keydown', this.j.async.debounce(async (e) => {
105
- switch (e.key) {
106
- case consts.KEY_ENTER:
104
+ .on(this.queryInput, 'keydown', (() => {
105
+ const debounced = this.j.async.debounce(async (e) => {
106
+ switch (e.key) {
107
+ case consts.KEY_ENTER:
108
+ if (await jodit.e.fire('searchNext')) {
109
+ this.close();
110
+ }
111
+ break;
112
+ default:
113
+ jodit.e.fire(this, 'needUpdateCounters');
114
+ break;
115
+ }
116
+ }, this.j.defaultTimeout);
117
+ return (e) => {
118
+ // Must be canceled synchronously — inside the debounced
119
+ // handler the browser has already submitted the parent
120
+ // form. See https://github.com/xdan/jodit/issues/918
121
+ if (e.key === consts.KEY_ENTER) {
107
122
  e.preventDefault();
108
123
  e.stopImmediatePropagation();
109
- if (await jodit.e.fire('searchNext')) {
110
- this.close();
111
- }
112
- break;
113
- default:
114
- jodit.e.fire(this, 'needUpdateCounters');
115
- break;
116
- }
117
- }, this.j.defaultTimeout));
124
+ }
125
+ debounced(e);
126
+ };
127
+ })());
118
128
  }
119
129
  onEditorKeyDown(e) {
120
130
  if (!this.isOpened) {
@@ -29,7 +29,7 @@ export declare class selectCells extends Plugin {
29
29
  /**
30
30
  * Mouse click inside the table
31
31
  */
32
- protected onStartSelection(cell: HTMLTableCellElement): void | false;
32
+ protected onStartSelection(cell: HTMLTableCellElement, event?: MouseEvent): void | false;
33
33
  protected onOutsideClick(): void;
34
34
  protected onChange(): void;
35
35
  /**
@@ -79,15 +79,42 @@ export class selectCells extends Plugin {
79
79
  /**
80
80
  * Mouse click inside the table
81
81
  */
82
- onStartSelection(cell) {
82
+ onStartSelection(cell, event) {
83
+ var _a;
83
84
  if (this.j.o.readonly) {
84
85
  return;
85
86
  }
87
+ const table = Dom.closest(cell, 'table', this.j.editor);
88
+ // Ctrl/Cmd + click toggles a single cell into the existing selection
89
+ // instead of resetting it — non-contiguous multi-cell selection.
90
+ // See https://github.com/xdan/jodit/issues/1163
91
+ if (((event === null || event === void 0 ? void 0 : event.ctrlKey) || (event === null || event === void 0 ? void 0 : event.metaKey)) &&
92
+ cell !== this.j.editor &&
93
+ table &&
94
+ Dom.isCell(cell)) {
95
+ if (this.__tableModule.getAllSelectedCells().includes(cell)) {
96
+ this.__tableModule.removeSelection(cell);
97
+ }
98
+ else {
99
+ if (!cell.firstChild) {
100
+ cell.appendChild(this.j.createInside.element('br'));
101
+ }
102
+ this.__tableModule.addSelection(cell);
103
+ }
104
+ this.__selectedCell = cell;
105
+ (_a = this.j.s.sel) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
106
+ if (this.__tableModule.getAllSelectedCells().length) {
107
+ this.j.e.fire('showPopup', table, () => position(cell, this.j), 'cells');
108
+ }
109
+ else {
110
+ this.j.e.fire('hidePopup', 'cells');
111
+ }
112
+ return false;
113
+ }
86
114
  this.unselectCells();
87
115
  if (cell === this.j.editor) {
88
116
  return;
89
117
  }
90
- const table = Dom.closest(cell, 'table', this.j.editor);
91
118
  if (!cell || !table) {
92
119
  return;
93
120
  }
@@ -120,14 +147,16 @@ export class selectCells extends Plugin {
120
147
  * Mouse move inside the table
121
148
  */
122
149
  __onMove(table, e) {
123
- var _a;
150
+ var _a, _b;
124
151
  if (this.j.o.readonly && !this.j.isLocked) {
125
152
  return;
126
153
  }
127
154
  if (this.j.isLockedNotBy(key)) {
128
155
  return;
129
156
  }
130
- const node = this.j.ed.elementFromPoint(e.clientX, e.clientY);
157
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
158
+ // host, so the lookup must start from the shadow root
159
+ const node = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.j.ed).elementFromPoint(e.clientX, e.clientY);
131
160
  if (!node) {
132
161
  return;
133
162
  }
@@ -150,7 +179,7 @@ export class selectCells extends Plugin {
150
179
  }
151
180
  const cellsCount = this.__tableModule.getAllSelectedCells().length;
152
181
  if (cellsCount > 1) {
153
- (_a = this.j.s.sel) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
182
+ (_b = this.j.s.sel) === null || _b === void 0 ? void 0 : _b.removeAllRanges();
154
183
  }
155
184
  this.j.e.fire('hidePopup');
156
185
  e.stopPropagation();
@@ -184,13 +213,15 @@ export class selectCells extends Plugin {
184
213
  * Stop a selection process
185
214
  */
186
215
  __onStopSelection(table, e) {
187
- var _a, _b;
216
+ var _a, _b, _c;
188
217
  if (!this.__selectedCell) {
189
218
  return;
190
219
  }
191
220
  this.__isSelectionMode = false;
192
221
  this.j.unlock();
193
- const node = this.j.ed.elementFromPoint(e.clientX, e.clientY);
222
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
223
+ // host, so the lookup must start from the shadow root
224
+ const node = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.j.ed).elementFromPoint(e.clientX, e.clientY);
194
225
  if (!node) {
195
226
  return;
196
227
  }
@@ -206,7 +237,7 @@ export class selectCells extends Plugin {
206
237
  cell,
207
238
  this.__selectedCell
208
239
  ]), box = this.__tableModule.formalMatrix(table);
209
- const max = (_a = box[bound[1][0]]) === null || _a === void 0 ? void 0 : _a[bound[1][1]], min = (_b = box[bound[0][0]]) === null || _b === void 0 ? void 0 : _b[bound[0][1]];
240
+ const max = (_b = box[bound[1][0]]) === null || _b === void 0 ? void 0 : _b[bound[1][1]], min = (_c = box[bound[0][0]]) === null || _c === void 0 ? void 0 : _c[bound[0][1]];
210
241
  // `getSelectedBound` keeps its `Infinity` sentinel when none of the
211
242
  // selected cells belong to this table's matrix — e.g. after a
212
243
  // drag-and-drop that moved/removed the cells, leaving a stale anchor and
@@ -80,6 +80,29 @@ export interface IUploaderOptions<T> {
80
80
  getDisplayName(this: T, baseurl: string, filename: string): string;
81
81
  pathVariableName: string;
82
82
  withCredentials: boolean;
83
+ /**
84
+ * Called with the list of files right before they are uploaded (or read as
85
+ * base64). Return `false` to abort the whole upload — useful for client
86
+ * side validation (size, type, count). Throwing an Error also aborts and
87
+ * routes the message through the uploader error handler.
88
+ *
89
+ * ```javascript
90
+ * Jodit.make('#editor', {
91
+ * uploader: {
92
+ * url: '...',
93
+ * beforeUpload(files) {
94
+ * for (const file of files) {
95
+ * if (file.size > 2 * 1024 * 1024) {
96
+ * this.jodit.message.error('Max 2MB');
97
+ * return false;
98
+ * }
99
+ * }
100
+ * }
101
+ * }
102
+ * });
103
+ * ```
104
+ */
105
+ beforeUpload?: (this: T, files: File[]) => boolean | void;
83
106
  prepareData: (this: T, formData: FormData) => any;
84
107
  buildData?: (this: T, formData: any) => BuildDataResult;
85
108
  queryBuild?: (obj: string | IDictionary<string | object> | FormData, prefix?: string) => string | FormData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jodit",
3
- "version": "4.12.22",
3
+ "version": "4.12.24",
4
4
  "description": "Jodit is an awesome and useful wysiwyg editor with filebrowser",
5
5
  "main": "esm/index.js",
6
6
  "types": "types/index.d.ts",
package/types/config.d.ts CHANGED
@@ -956,6 +956,21 @@ interface Config {
956
956
  backspaceWord: string[];
957
957
  backspaceSentence: string[];
958
958
  };
959
+ /**
960
+ * Disable specific Backspace/Delete cleanup cases by their stable
961
+ * key, so the plugin no longer applies that particular behavior.
962
+ * Available keys: `remove-unbreakable`, `remove-not-editable`,
963
+ * `remove-char`, `table-cell`, `remove-empty-parent`,
964
+ * `remove-empty-neighbor`, `join-two-lists`, `join-neighbors`,
965
+ * `unwrap-first-list-item`.
966
+ *
967
+ * ```javascript
968
+ * Jodit.make('#editor', {
969
+ * delete: { disableCases: new Set(['remove-empty-parent']) }
970
+ * });
971
+ * ```
972
+ */
973
+ disableCases?: Set<string>;
959
974
  };
960
975
  }
961
976
  interface Config {
@@ -973,6 +988,15 @@ interface Config {
973
988
  * Remove empty elements
974
989
  */
975
990
  removeEmptyElements: boolean;
991
+ /**
992
+ * Return an empty string from `editor.value` (and the synced source
993
+ * element) when the editor holds only a single empty block — e.g.
994
+ * `<p><br></p>` left after the user deletes all the content.
995
+ * `contenteditable` keeps that caret container in the DOM, so by
996
+ * default the value getter returns it as-is; enable this to collapse
997
+ * it to `''` for form submission.
998
+ */
999
+ collapseEmptyValueToEmptyString: boolean;
976
1000
  /**
977
1001
  * Replace old tags to new eg. <i> to <em>, <b> to <strong>
978
1002
  */
@@ -1371,6 +1395,12 @@ interface Config {
1371
1395
  * Default value for the `Open in new tab` checkbox when inserting a new link.
1372
1396
  */
1373
1397
  openInNewTabCheckboxDefaultChecked: boolean;
1398
+ /**
1399
+ * Show an `aria-label` text input in the link dialog so an
1400
+ * accessible name can be set on the `<a>` (useful when several
1401
+ * links share the same visible text, e.g. "here"). Default: false.
1402
+ */
1403
+ ariaLabelInput: boolean;
1374
1404
  /**
1375
1405
  * Use an input text to ask the classname or a select or not ask
1376
1406
  */
@@ -7,6 +7,6 @@
7
7
  * @module helpers/checker
8
8
  */
9
9
  /**
10
- * Detect if string is HTML from MS Word or Excel
10
+ * Detect if string is HTML from MS Word, Excel or LibreOffice/OpenOffice
11
11
  */
12
12
  export declare function isHtmlFromWord(data: string): boolean;
@@ -15,6 +15,12 @@ export declare class ToolbarContent<T extends IViewBased = IViewBased> extends U
15
15
  className(): string;
16
16
  /** @override */
17
17
  update(): void;
18
+ /**
19
+ * The content is arbitrary HTML — propagate the disabled state to the
20
+ * nested form controls (e.g. the file input of the Upload button),
21
+ * otherwise they stay interactive.
22
+ */
23
+ protected onChangeDisabled(): void;
18
24
  /** @override */
19
25
  protected createContainer(): HTMLElement;
20
26
  constructor(jodit: T, control: IControlTypeContent, target?: Nullable<HTMLElement>);
@@ -3,9 +3,25 @@
3
3
  * Released under MIT see LICENSE.txt in the project root for license information.
4
4
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
5
  */
6
- import { checkRemoveChar } from "./check-remove-char";
7
- import { checkRemoveContentNotEditable } from "./check-remove-content-not-editable";
6
+ /**
7
+ * @module plugins/backspace
8
+ */
9
+ import type { IJodit } from "../../../types/index";
10
+ import type { DeleteMode } from "../interface";
11
+ type CaseFn = (jodit: IJodit, fakeNode: any, backspace: boolean, mode: DeleteMode) => void | boolean;
12
+ /**
13
+ * Ordered delete/backspace cases with stable keys. The first one returning
14
+ * `true` wins. The keys are stable across minified builds (function names are
15
+ * mangled by terser) so they can be referenced by `delete.disableCases`.
16
+ * @private
17
+ */
18
+ export declare const casesMap: ReadonlyArray<readonly [
19
+ string,
20
+ CaseFn
21
+ ]>;
8
22
  /**
9
23
  * @private
24
+ * @deprecated Use `casesMap` to also get the stable case keys.
10
25
  */
11
- export declare const cases: (typeof checkRemoveContentNotEditable | typeof checkRemoveChar)[];
26
+ export declare const cases: CaseFn[];
27
+ export {};
@@ -17,6 +17,21 @@ declare module 'jodit/config' {
17
17
  backspaceWord: string[];
18
18
  backspaceSentence: string[];
19
19
  };
20
+ /**
21
+ * Disable specific Backspace/Delete cleanup cases by their stable
22
+ * key, so the plugin no longer applies that particular behavior.
23
+ * Available keys: `remove-unbreakable`, `remove-not-editable`,
24
+ * `remove-char`, `table-cell`, `remove-empty-parent`,
25
+ * `remove-empty-neighbor`, `join-two-lists`, `join-neighbors`,
26
+ * `unwrap-first-list-item`.
27
+ *
28
+ * ```javascript
29
+ * Jodit.make('#editor', {
30
+ * delete: { disableCases: new Set(['remove-empty-parent']) }
31
+ * });
32
+ * ```
33
+ */
34
+ disableCases?: Set<string>;
20
35
  };
21
36
  }
22
37
  }
@@ -34,6 +34,14 @@ export declare class cleanHtml extends Plugin {
34
34
  protected onBeforeSetNativeEditorValue(data: {
35
35
  value: string;
36
36
  }): boolean;
37
+ /**
38
+ * Collapse a value that holds only a single empty block (e.g.
39
+ * `<p><br></p>` left after deleting all content) to an empty string —
40
+ * opt-in via `cleanHTML.collapseEmptyValueToEmptyString`. See #1149
41
+ */
42
+ protected onAfterGetValueFromEditor(data: {
43
+ value: string;
44
+ }): void;
37
45
  protected onSafeHTML(sandBox: HTMLElement): void;
38
46
  /** @override */
39
47
  protected beforeDestruct(): void;
@@ -23,6 +23,15 @@ declare module 'jodit/config' {
23
23
  * Remove empty elements
24
24
  */
25
25
  removeEmptyElements: boolean;
26
+ /**
27
+ * Return an empty string from `editor.value` (and the synced source
28
+ * element) when the editor holds only a single empty block — e.g.
29
+ * `<p><br></p>` left after the user deletes all the content.
30
+ * `contenteditable` keeps that caret container in the DOM, so by
31
+ * default the value getter returns it as-is; enable this to collapse
32
+ * it to `''` for form submission.
33
+ */
34
+ collapseEmptyValueToEmptyString: boolean;
26
35
  /**
27
36
  * Replace old tags to new eg. <i> to <em>, <b> to <strong>
28
37
  */
@@ -50,6 +50,13 @@ export declare class inlinePopup extends Plugin {
50
50
  private __reopenSelectionPopup;
51
51
  private __onDocumentMouseDown;
52
52
  private __onCloseAllPopups;
53
+ /**
54
+ * The selection rect comes from the editor document — in iframe mode its
55
+ * coordinates are iframe-local, while the popup lives in the host
56
+ * document, so the iframe offset must be added. See
57
+ * https://github.com/xdan/jodit/issues/1058
58
+ */
59
+ private __selectionBound;
53
60
  private snapRange;
54
61
  private onSelectionStart;
55
62
  private onSelectionEnd;
@@ -39,6 +39,12 @@ declare module 'jodit/config' {
39
39
  * Default value for the `Open in new tab` checkbox when inserting a new link.
40
40
  */
41
41
  openInNewTabCheckboxDefaultChecked: boolean;
42
+ /**
43
+ * Show an `aria-label` text input in the link dialog so an
44
+ * accessible name can be set on the `<a>` (useful when several
45
+ * links share the same visible text, e.g. "here"). Default: false.
46
+ */
47
+ ariaLabelInput: boolean;
42
48
  /**
43
49
  * Use an input text to ask the classname or a select or not ask
44
50
  */
@@ -29,7 +29,7 @@ export declare class selectCells extends Plugin {
29
29
  /**
30
30
  * Mouse click inside the table
31
31
  */
32
- protected onStartSelection(cell: HTMLTableCellElement): void | false;
32
+ protected onStartSelection(cell: HTMLTableCellElement, event?: MouseEvent): void | false;
33
33
  protected onOutsideClick(): void;
34
34
  protected onChange(): void;
35
35
  /**