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
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
package/es5/polyfills.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * jodit - Jodit is an awesome and useful wysiwyg editor with filebrowser
3
3
  * Author: Chupurnov <chupurnov@gmail.com> (https://xdsoft.net/jodit/)
4
- * Version: v4.12.22
4
+ * Version: v4.12.24
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
package/esm/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
  */
@@ -22,6 +22,14 @@ export class ViewComponent extends Component {
22
22
  */
23
23
  setParentView(jodit) {
24
24
  this.jodit = jodit;
25
+ // Inherit the owner window from the parent view — for an editor
26
+ // created with a custom `ownerWindow` (e.g. inside an iframe) the
27
+ // component default (the global `window`) is wrong: outside-click
28
+ // handlers of dropdowns/popups listened to the wrong window. See
29
+ // https://github.com/xdan/jodit/issues/965
30
+ if (jodit.ow) {
31
+ this.ownerWindow = jodit.ow;
32
+ }
25
33
  jodit.components.add(this);
26
34
  return this;
27
35
  }
@@ -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
- export const APP_VERSION = "4.12.22";
6
+ export const APP_VERSION = "4.12.24";
7
7
  // prettier-ignore
8
8
  export const ES = "es2020";
9
9
  export const IS_ES_MODERN = true;
@@ -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;
@@ -7,10 +7,21 @@
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 function isHtmlFromWord(data) {
13
13
  return (data.search(/<meta.*?Microsoft Excel\s[\d].*?>/) !== -1 ||
14
14
  data.search(/<meta.*?Microsoft Word\s[\d].*?>/) !== -1 ||
15
+ // `<meta name=ProgId content=Word.Document>` — attribute values are
16
+ // unquoted in the raw Word clipboard fragment
17
+ data.search(/<meta[^>]*?ProgId[^>]*?(Word|Excel)\./i) !== -1 ||
18
+ // LibreOffice/OpenOffice Writer & Calc
19
+ data.search(/<meta[^>]*?(LibreOffice|OpenOffice)/i) !== -1 ||
20
+ // Office namespaces on the root element of the clipboard fragment
21
+ data.search(/urn:schemas-microsoft-com:office:(word|excel)/) !== -1 ||
22
+ // `class=MsoNormal` and friends (unquoted/quoted)
23
+ data.search(/<\w[^>]*\sclass=("|')?Mso/) !== -1 ||
24
+ // the raw Word clipboard uses SINGLE quotes: style='mso-…'
25
+ data.search(/style='[^']*mso-/) !== -1 ||
15
26
  (data.search(/style="[^"]*mso-/) !== -1 && data.search(/<font/) !== -1));
16
27
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Dom } from "../../../dom/index.js";
7
7
  import { attr } from "../../../helpers/utils/attr.js";
8
+ import { css } from "../../../helpers/utils/css.js";
8
9
  import { wrapList } from "./list/wrap-list.js";
9
10
  import { wrapUnwrappedText } from "./wrap-unwrapped-text.js";
10
11
  /**
@@ -13,9 +14,23 @@ import { wrapUnwrappedText } from "./wrap-unwrapped-text.js";
13
14
  */
14
15
  export function wrap(commitStyle, font, jodit) {
15
16
  const wrapper = findOrCreateWrapper(commitStyle, font, jodit);
16
- return commitStyle.elementIsList
17
- ? wrapList(commitStyle, wrapper, jodit)
18
- : Dom.replace(wrapper, commitStyle.element, jodit.createInside, true);
17
+ if (commitStyle.elementIsList) {
18
+ return wrapList(commitStyle, wrapper, jodit);
19
+ }
20
+ const newWrapper = Dom.replace(wrapper, commitStyle.element, jodit.createInside, true);
21
+ if (commitStyle.elementIsBlock) {
22
+ // Inline font styles left over from pasted content visually override
23
+ // the new block format — e.g. an h2 with `font-weight: normal;
24
+ // font-size: 24px` does not look like a heading at all, so the
25
+ // command seems to do nothing. See
26
+ // https://github.com/xdan/jodit/issues/1063
27
+ css(newWrapper, 'fontSize', null);
28
+ css(newWrapper, 'fontWeight', null);
29
+ if (!attr(newWrapper, 'style')) {
30
+ attr(newWrapper, 'style', null);
31
+ }
32
+ }
33
+ return newWrapper;
19
34
  }
20
35
  const WRAP_NODES = new Set([
21
36
  'td',
@@ -137,8 +137,11 @@ export class Popup extends UIGroup {
137
137
  * Calculate static bound for point
138
138
  */
139
139
  getKeepBound(getBound) {
140
+ var _a;
140
141
  const oldBound = getBound();
141
- const elmUnderCursor = this.od.elementFromPoint(oldBound.left, oldBound.top);
142
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
143
+ // host, so the lookup must start from the shadow root
144
+ const elmUnderCursor = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.od).elementFromPoint(oldBound.left, oldBound.top);
142
145
  if (!elmUnderCursor) {
143
146
  return getBound;
144
147
  }
package/esm/jodit.js CHANGED
@@ -1073,6 +1073,18 @@ let Jodit = Jodit_1 = class Jodit extends ViewWithToolbar {
1073
1073
  (opt.uploader.url || opt.uploader.insertImageAsBase64URI)) {
1074
1074
  this.uploader.bind(this.editor);
1075
1075
  }
1076
+ else if (!opt.enableDragAndDropFileToEditor) {
1077
+ // Without a bound uploader nobody cancels a file drop, and
1078
+ // Firefox inserts the dropped image natively — the option
1079
+ // must mean "do nothing". See
1080
+ // https://github.com/xdan/jodit/issues/1077
1081
+ this.e.on(editor, 'drop', (e) => {
1082
+ var _a, _b;
1083
+ if ((_b = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b.length) {
1084
+ e.preventDefault();
1085
+ }
1086
+ });
1087
+ }
1076
1088
  // in initEditor - the editor could change
1077
1089
  if (!this.__elementToPlace.get(this.editor)) {
1078
1090
  this.__elementToPlace.set(this.editor, currentPlace);
@@ -439,12 +439,17 @@ let ImageEditor = ImageEditor_1 = class ImageEditor extends ViewComponent {
439
439
  self.j.alert('The name should not be empty');
440
440
  return false;
441
441
  }
442
+ self.j.e.fire('afterImageEditorSave', data, name);
442
443
  self.onSave(name, data, self.hide, (e) => {
443
444
  self.j.alert(e.message);
444
445
  });
445
446
  });
446
447
  break;
447
448
  case self.buttons.save:
449
+ // Notify listeners that a crop/resize was applied,
450
+ // passing the action box (action + box dimensions).
451
+ // See https://github.com/xdan/jodit/issues/820
452
+ self.j.e.fire('afterImageEditorSave', data);
448
453
  self.onSave(undefined, data, self.hide, (e) => {
449
454
  self.j.alert(e.message);
450
455
  });
@@ -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>);
@@ -24,13 +24,38 @@ let ToolbarContent = class ToolbarContent extends UIButton {
24
24
  }
25
25
  /** @override */
26
26
  update() {
27
- const content = this.control.getContent(this.j, this);
27
+ var _a, _b, _c;
28
+ const { control } = this;
29
+ const content = control.getContent(this.j, this);
28
30
  if (isString(content) || content.parentNode !== this.container) {
29
31
  Dom.detach(this.container);
30
32
  this.container.appendChild(isString(content) ? this.j.create.fromHTML(content) : content);
31
33
  }
34
+ // Content controls never went through the ToolbarButton status
35
+ // calculation, so `isDisabled`/`isActive`/`update` declared on the
36
+ // control were silently ignored (e.g. the FileBrowser Upload button
37
+ // ignored the backend permissions). See
38
+ // https://github.com/xdan/jodit/issues/1094
39
+ this.state.disabled = Boolean((_a = control.isDisabled) === null || _a === void 0 ? void 0 : _a.call(control, this.j, this));
40
+ this.state.activated = Boolean((_b = control.isActive) === null || _b === void 0 ? void 0 : _b.call(control, this.j, this));
41
+ (_c = control.update) === null || _c === void 0 ? void 0 : _c.call(control, this.j, this);
42
+ // The first update() runs before the state watchers are attached, so
43
+ // apply the calculated state explicitly (the calls are idempotent)
44
+ this.onChangeDisabled();
45
+ this.onChangeActivated();
32
46
  super.update();
33
47
  }
48
+ /**
49
+ * The content is arbitrary HTML — propagate the disabled state to the
50
+ * nested form controls (e.g. the file input of the Upload button),
51
+ * otherwise they stay interactive.
52
+ */
53
+ onChangeDisabled() {
54
+ super.onChangeDisabled();
55
+ this.container
56
+ .querySelectorAll('input,button,select,textarea')
57
+ .forEach(elm => attr(elm, 'disabled', this.state.disabled || null));
58
+ }
34
59
  /** @override */
35
60
  createContainer() {
36
61
  return this.j.c.span(this.componentName);
@@ -17,6 +17,15 @@ export function sendFiles(uploader, files, handlerSuccess, handlerError, process
17
17
  if (!fileList.length) {
18
18
  return Promise.reject(error('Need files'));
19
19
  }
20
+ // Client-side validation hook — returning `false` aborts the upload.
21
+ // See https://github.com/xdan/jodit/issues/1329
22
+ if (isFunction(o.beforeUpload)) {
23
+ if (o.beforeUpload.call(uploader, fileList) === false) {
24
+ const err = error('Upload canceled');
25
+ (handlerError || o.defaultHandlerError).call(uploader, err);
26
+ return Promise.reject(err);
27
+ }
28
+ }
20
29
  const promises = [];
21
30
  if (o.insertImageAsBase64URI) {
22
31
  readImagesWithReader(fileList, o.imagesExtensions, promises, uploader, handlerSuccess, o.defaultHandlerSuccess);
@@ -151,8 +151,11 @@ export class addNewLine extends Plugin {
151
151
  }
152
152
  }
153
153
  __onMouseMove(e) {
154
+ var _a;
154
155
  const editor = this.j;
155
- let currentElement = editor.ed.elementFromPoint(e.clientX, e.clientY);
156
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
157
+ // host, so the lookup must start from the shadow root
158
+ let currentElement = ((_a = editor.o.shadowRoot) !== null && _a !== void 0 ? _a : editor.ed).elementFromPoint(e.clientX, e.clientY);
156
159
  if (!Dom.isHTMLElement(currentElement) ||
157
160
  !Dom.isOrContains(editor.editor, currentElement)) {
158
161
  return;
@@ -40,7 +40,13 @@ export class aiAssistant extends Plugin {
40
40
  return new UiAiAssistant(jodit, {
41
41
  onInsertAfter(html) {
42
42
  jodit.s.focus();
43
- jodit.s.setCursorAfter(jodit.s.current());
43
+ // `current()` returns a node of the selection START, so with
44
+ // several selected paragraphs the result landed after the
45
+ // first one — collapse the range to the END of the selection
46
+ // instead. See https://github.com/xdan/jodit/issues/1263
47
+ const range = jodit.s.range;
48
+ range.collapse(false);
49
+ jodit.s.selectRange(range);
44
50
  jodit.s.insertHTML(html);
45
51
  __dialog.close();
46
52
  },
@@ -11,7 +11,7 @@ import { Plugin } from "../../core/plugin/index.js";
11
11
  import { moveNodeInsideStart } from "../../core/selection/helpers/index.js";
12
12
  import "./config.js";
13
13
  import { checkNotCollapsed } from "./cases/check-not-collapsed.js";
14
- import { cases } from "./cases/index.js";
14
+ import { casesMap } from "./cases/index.js";
15
15
  export class backspace extends Plugin {
16
16
  afterInit(jodit) {
17
17
  jodit
@@ -71,11 +71,15 @@ export class backspace extends Plugin {
71
71
  return false;
72
72
  }
73
73
  moveNodeInsideStart(jodit, fakeNode, backspace);
74
- if (cases.some((func) => {
74
+ const disabled = jodit.o.delete.disableCases;
75
+ if (casesMap.some(([key, func]) => {
76
+ if (disabled && disabled.has(key)) {
77
+ return;
78
+ }
75
79
  if (isFunction(func) &&
76
80
  func(jodit, fakeNode, backspace, mode)) {
77
81
  if (!IS_PROD) {
78
- console.info('Remove case:', func.name);
82
+ console.info('Remove case:', key);
79
83
  }
80
84
  return true;
81
85
  }
@@ -32,13 +32,27 @@ export function checkJoinNeighbors(jodit, fakeNode, backspace) {
32
32
  }
33
33
  if (sibling &&
34
34
  (checkMoveListContent(jodit, mainClosestBox, sibling, backspace) ||
35
- moveContentAndRemoveEmpty(jodit, mainClosestBox, sibling, backspace))) {
35
+ moveContentAndRemoveEmpty(jodit, mainClosestBox, resolveTableSibling(sibling, backspace), backspace))) {
36
36
  jodit.s.setCursorBefore(fakeNode);
37
37
  return true;
38
38
  }
39
39
  }
40
40
  return false;
41
41
  }
42
+ /**
43
+ * Content cannot be merged into the `<table>` element itself — it would land
44
+ * between the table sections (after `</tbody>`), which is invalid HTML and
45
+ * gets foster-parented out of the table on the next parse. Merge into the
46
+ * edge cell instead. See https://github.com/xdan/jodit/issues/1064
47
+ * @private
48
+ */
49
+ function resolveTableSibling(sibling, backspace) {
50
+ if (!Dom.isTag(sibling, 'table')) {
51
+ return sibling;
52
+ }
53
+ const cells = sibling.querySelectorAll('td,th');
54
+ return cells.length ? cells[backspace ? cells.length - 1 : 0] : null;
55
+ }
42
56
  function checkMoveListContent(jodit, mainClosestBox, sibling, backspace) {
43
57
  // Process UL/LI/OL cases
44
58
  const siblingIsList = Dom.isTag(sibling, LIST_TAGS);
@@ -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 {};
@@ -3,9 +3,6 @@
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
- /**
7
- * @module plugins/backspace
8
- */
9
6
  import { checkJoinNeighbors } from "./check-join-neighbors.js";
10
7
  import { checkJoinTwoLists } from "./check-join-two-lists.js";
11
8
  import { checkRemoveChar } from "./check-remove-char.js";
@@ -16,16 +13,24 @@ import { checkRemoveUnbreakableElement } from "./check-remove-unbreakable-elemen
16
13
  import { checkTableCell } from "./check-table-cell.js";
17
14
  import { checkUnwrapFirstListItem } from "./check-unwrap-first-list-item.js";
18
15
  /**
16
+ * Ordered delete/backspace cases with stable keys. The first one returning
17
+ * `true` wins. The keys are stable across minified builds (function names are
18
+ * mangled by terser) so they can be referenced by `delete.disableCases`.
19
19
  * @private
20
20
  */
21
- export const cases = [
22
- checkRemoveUnbreakableElement,
23
- checkRemoveContentNotEditable,
24
- checkRemoveChar,
25
- checkTableCell,
26
- checkRemoveEmptyParent,
27
- checkRemoveEmptyNeighbor,
28
- checkJoinTwoLists,
29
- checkJoinNeighbors,
30
- checkUnwrapFirstListItem
21
+ export const casesMap = [
22
+ ['remove-unbreakable', checkRemoveUnbreakableElement],
23
+ ['remove-not-editable', checkRemoveContentNotEditable],
24
+ ['remove-char', checkRemoveChar],
25
+ ['table-cell', checkTableCell],
26
+ ['remove-empty-parent', checkRemoveEmptyParent],
27
+ ['remove-empty-neighbor', checkRemoveEmptyNeighbor],
28
+ ['join-two-lists', checkJoinTwoLists],
29
+ ['join-neighbors', checkJoinNeighbors],
30
+ ['unwrap-first-list-item', checkUnwrapFirstListItem]
31
31
  ];
32
+ /**
33
+ * @private
34
+ * @deprecated Use `casesMap` to also get the stable case keys.
35
+ */
36
+ export const cases = casesMap.map(([, fn]) => fn);
@@ -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;
@@ -100,6 +100,19 @@ export class cleanHtml extends Plugin {
100
100
  Dom.safeRemove(iframe);
101
101
  return false;
102
102
  }
103
+ /**
104
+ * Collapse a value that holds only a single empty block (e.g.
105
+ * `<p><br></p>` left after deleting all content) to an empty string —
106
+ * opt-in via `cleanHTML.collapseEmptyValueToEmptyString`. See #1149
107
+ */
108
+ onAfterGetValueFromEditor(data) {
109
+ if (!this.j.o.cleanHTML.collapseEmptyValueToEmptyString) {
110
+ return;
111
+ }
112
+ if (/^<([a-z][a-z0-9]*)\b[^>]*>(?:<br\/?>)?<\/\1>$/i.test(data.value.trim())) {
113
+ data.value = '';
114
+ }
115
+ }
103
116
  onSafeHTML(sandBox) {
104
117
  const sanitizer = this.j.o.cleanHTML.sanitizer;
105
118
  if (sanitizer) {
@@ -124,6 +137,9 @@ __decorate([
124
137
  __decorate([
125
138
  watch(':beforeSetNativeEditorValue')
126
139
  ], cleanHtml.prototype, "onBeforeSetNativeEditorValue", null);
140
+ __decorate([
141
+ watch(':afterGetValueFromEditor')
142
+ ], cleanHtml.prototype, "onAfterGetValueFromEditor", null);
127
143
  __decorate([
128
144
  watch(':safeHTML')
129
145
  ], cleanHtml.prototype, "onSafeHTML", null);
@@ -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
  */
@@ -10,6 +10,7 @@ Config.prototype.cleanHTML = {
10
10
  timeout: 300,
11
11
  removeEmptyElements: true,
12
12
  fillEmptyParagraph: true,
13
+ collapseEmptyValueToEmptyString: false,
13
14
  replaceNBSP: true,
14
15
  replaceOldTags: {
15
16
  i: 'em',