jodit 4.18.6 → 4.18.8

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 (76) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/es2015/jodit.css +1 -1
  3. package/es2015/jodit.fat.min.js +9 -9
  4. package/es2015/jodit.js +265 -36
  5. package/es2015/jodit.min.js +9 -9
  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 +9 -9
  13. package/es2018/jodit.min.js +7 -7
  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 +11 -11
  18. package/es2021/jodit.js +264 -36
  19. package/es2021/jodit.min.js +7 -7
  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 +8 -8
  28. package/es2021.en/jodit.js +264 -36
  29. package/es2021.en/jodit.min.js +7 -7
  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 +280 -35
  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 +4 -2
  51. package/esm/core/constants.js +1 -1
  52. package/esm/core/decorators/component/component.js +12 -0
  53. package/esm/core/ui/popup/popup.js +14 -9
  54. package/esm/jodit.js +5 -1
  55. package/esm/modules/dialog/dialog.js +18 -3
  56. package/esm/modules/file-browser/ui/files/files.js +17 -2
  57. package/esm/modules/file-browser/ui/tree/tree.js +17 -2
  58. package/esm/plugins/backspace/cases/check-join-neighbors.js +81 -0
  59. package/esm/plugins/backspace/cases/check-remove-char.d.ts +2 -1
  60. package/esm/plugins/backspace/cases/check-remove-char.js +31 -9
  61. package/esm/plugins/backspace/cases/check-unwrap-list-item.d.ts +26 -0
  62. package/esm/plugins/backspace/cases/check-unwrap-list-item.js +56 -0
  63. package/esm/plugins/backspace/cases/index.js +2 -0
  64. package/esm/plugins/backspace/config.d.ts +4 -2
  65. package/esm/plugins/backspace/config.js +6 -3
  66. package/esm/plugins/enter/enter.js +1 -1
  67. package/esm/plugins/enter/helpers/check-br.js +7 -2
  68. package/esm/plugins/image-properties/image-properties.d.ts +5 -0
  69. package/esm/plugins/image-properties/image-properties.js +30 -5
  70. package/esm/plugins/mobile/mobile.js +7 -4
  71. package/package.json +1 -1
  72. package/types/config.d.ts +4 -2
  73. package/types/plugins/backspace/cases/check-remove-char.d.ts +2 -1
  74. package/types/plugins/backspace/cases/check-unwrap-list-item.d.ts +26 -0
  75. package/types/plugins/backspace/config.d.ts +4 -2
  76. package/types/plugins/image-properties/image-properties.d.ts +5 -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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
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.18.6
4
+ * Version: v4.18.8
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
package/esm/config.d.ts CHANGED
@@ -1034,6 +1034,8 @@ interface Config {
1034
1034
  interface Config {
1035
1035
  /**
1036
1036
  * Keyboard hotkey mappings for delete and backspace operations (character, word, sentence).
1037
+ * On macOS the defaults leave out Cmd+Backspace, Ctrl+Backspace, Ctrl+Option+Backspace
1038
+ * and Cmd+Option+Backspace, so the browser applies the system's own bindings to them.
1037
1039
  */
1038
1040
  delete: {
1039
1041
  hotkeys: {
@@ -1049,8 +1051,8 @@ interface Config {
1049
1051
  * key, so the plugin no longer applies that particular behavior.
1050
1052
  * Available keys: `remove-unbreakable`, `remove-not-editable`,
1051
1053
  * `remove-char`, `table-cell`, `remove-empty-parent`,
1052
- * `remove-empty-neighbor`, `join-two-lists`, `join-neighbors`,
1053
- * `unwrap-first-list-item`.
1054
+ * `remove-empty-neighbor`, `join-two-lists`, `unwrap-list-item`,
1055
+ * `join-neighbors`, `unwrap-first-list-item`.
1054
1056
  *
1055
1057
  * ```javascript
1056
1058
  * Jodit.make('#editor', {
@@ -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.18.6";
6
+ export const APP_VERSION = "4.18.8";
7
7
  // prettier-ignore
8
8
  export const ES = "es2020";
9
9
  export const IS_ES_MODERN = true;
@@ -6,6 +6,8 @@
6
6
  import { IS_PROD } from "../../constants.js";
7
7
  import { getClassName } from "../../helpers/utils/get-class-name.js";
8
8
  const componentRegistry = new Map();
9
+ const decoratedClasses = new WeakSet();
10
+ const warnedClasses = new WeakSet();
9
11
  /**
10
12
  * Decorate components and set status isReady after constructor
11
13
  * @param constructorFunction - Component constructor class
@@ -24,8 +26,18 @@ export function component(constructorFunction) {
24
26
  }
25
27
  this.setStatus('ready');
26
28
  }
29
+ else if (!IS_PROD &&
30
+ !decoratedClasses.has(this.constructor) &&
31
+ !warnedClasses.has(this.constructor)) {
32
+ // Only the outermost class's decorator sets `ready`, so a subclass
33
+ // without its own decorator stays in `beforeInit`. See #1685
34
+ warnedClasses.add(this.constructor);
35
+ console.warn(`Component "${this.constructor.name || getClassName(this)}" extends a @component class without being decorated itself, so it never becomes ready and its @watch, @debounce, @throttle and @hook handlers never run`);
36
+ }
27
37
  }
28
38
  }
39
+ decoratedClasses.add(constructorFunction);
40
+ decoratedClasses.add(newConstructorFunction);
29
41
  const name = getClassName(constructorFunction.prototype);
30
42
  if (componentRegistry.has(name) && !IS_PROD) {
31
43
  throw new Error(`Component with name "${name}" is already registered`);
@@ -13,8 +13,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
13
13
  r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
14
14
  return c > 3 && r && Object.defineProperty(target, key, r), r;
15
15
  };
16
+ var Popup_1;
16
17
  import { Component } from "../../component/component.js";
17
- import { autobind, throttle } from "../../decorators/index.js";
18
+ import { autobind, component, throttle } from "../../decorators/index.js";
18
19
  import { Dom } from "../../dom/dom.js";
19
20
  import { eventEmitter, getContainer } from "../../global.js";
20
21
  import { attr, css, getFixedPositionOffset, isFunction, isString, kebabCase, markOwner, position, ucfirst } from "../../helpers/index.js";
@@ -29,7 +30,7 @@ const EVENTS_FOR_AUTOCLOSE = [
29
30
  'backSpaceAfterDelete',
30
31
  'beforeCommandDelete'
31
32
  ];
32
- export class Popup extends UIGroup {
33
+ let Popup = Popup_1 = class Popup extends UIGroup {
33
34
  className() {
34
35
  return 'Popup';
35
36
  }
@@ -39,7 +40,7 @@ export class Popup extends UIGroup {
39
40
  Dom.append(content, childContainer);
40
41
  }
41
42
  updateParentElement(target) {
42
- if (target !== this && Component.isInstanceOf(target, Popup)) {
43
+ if (target !== this && Component.isInstanceOf(target, Popup_1)) {
43
44
  this.__childrenPopups.forEach(popup => {
44
45
  if (!target.closest(popup) && popup.isOpened) {
45
46
  popup.close();
@@ -89,7 +90,7 @@ export class Popup extends UIGroup {
89
90
  Dom.append(parentContainer, this.container);
90
91
  }
91
92
  else {
92
- const popupContainer = getContainer(this.jodit, Popup);
93
+ const popupContainer = getContainer(this.jodit, Popup_1);
93
94
  if (parentContainer !== this.container.parentElement) {
94
95
  Dom.append(popupContainer, this.container);
95
96
  }
@@ -207,13 +208,13 @@ export class Popup extends UIGroup {
207
208
  };
208
209
  const getMatchStrategy = (inBox) => {
209
210
  let strategy;
210
- if (Popup.boxInView(getPointByStrategy(defaultStrategy), inBox)) {
211
+ if (Popup_1.boxInView(getPointByStrategy(defaultStrategy), inBox)) {
211
212
  strategy = defaultStrategy;
212
213
  }
213
214
  else {
214
215
  strategy =
215
216
  list.find((key) => {
216
- if (Popup.boxInView(getPointByStrategy(key), inBox)) {
217
+ if (Popup_1.boxInView(getPointByStrategy(key), inBox)) {
217
218
  return key;
218
219
  }
219
220
  return;
@@ -224,7 +225,7 @@ export class Popup extends UIGroup {
224
225
  // Try to find match position inside Jodit.container
225
226
  let strategy = getMatchStrategy(position(this.j.container, this.j));
226
227
  // If not found or is not inside window view
227
- if (!strategy || !Popup.boxInView(getPointByStrategy(strategy), view)) {
228
+ if (!strategy || !Popup_1.boxInView(getPointByStrategy(strategy), view)) {
228
229
  // Find match strategy inside window view
229
230
  strategy = getMatchStrategy(view) || strategy || defaultStrategy;
230
231
  }
@@ -280,7 +281,7 @@ export class Popup extends UIGroup {
280
281
  if (!e.target) {
281
282
  return false;
282
283
  }
283
- const box = UIElement.closestElement(e.target, Popup);
284
+ const box = UIElement.closestElement(e.target, Popup_1);
284
285
  return Boolean(box && (this === box || box.closest(this)));
285
286
  }
286
287
  __addGlobalListeners() {
@@ -353,7 +354,7 @@ export class Popup extends UIGroup {
353
354
  this.close();
354
355
  return super.destruct();
355
356
  }
356
- }
357
+ };
357
358
  __decorate([
358
359
  autobind
359
360
  ], Popup.prototype, "updatePosition", null);
@@ -367,3 +368,7 @@ __decorate([
367
368
  __decorate([
368
369
  autobind
369
370
  ], Popup.prototype, "__closeOnOutsideClick", null);
371
+ Popup = Popup_1 = __decorate([
372
+ component
373
+ ], Popup);
374
+ export { Popup };
package/esm/jodit.js CHANGED
@@ -557,7 +557,7 @@ let Jodit = Jodit_1 = class Jodit extends ViewWithToolbar {
557
557
  const hotkeys = this.o.commandToHotkeys[commandName] ||
558
558
  this.o.commandToHotkeys[commandNameOriginal] ||
559
559
  command.hotkeys;
560
- if (hotkeys) {
560
+ if (hotkeys && hotkeys.length) {
561
561
  this.registerHotkeyToCommand(hotkeys, commandName, options === null || options === void 0 ? void 0 : options.stopPropagation);
562
562
  }
563
563
  }
@@ -567,6 +567,10 @@ let Jodit = Jodit_1 = class Jodit extends ViewWithToolbar {
567
567
  * Register hotkey for command
568
568
  */
569
569
  registerHotkeyToCommand(hotkeys, commandName, shouldStop = true) {
570
+ // An empty list (e.g. `commandToHotkeys: { cmd: [] }`) turns the key off
571
+ if (!hotkeys || !hotkeys.length) {
572
+ return;
573
+ }
570
574
  const shortcuts = asArray(hotkeys)
571
575
  .map(normalizeKeyAliases)
572
576
  .map(hotkey => hotkey + '.hotkey')
@@ -28,6 +28,10 @@ import { Icon } from "../../core/ui/index.js";
28
28
  import { View } from "../../core/view/view.js";
29
29
  import { ViewWithToolbar } from "../../core/view/view-with-toolbar.js";
30
30
  import { Config } from "../../config.js";
31
+ /**
32
+ * A position from 0 up to `max`, or 0 when the panel is larger than the window
33
+ */
34
+ const clampToWindow = (value, max) => Math.max(0, Math.min(value, max));
31
35
  Config.prototype.dialog = {
32
36
  namespace: '',
33
37
  extraButtons: [],
@@ -126,6 +130,11 @@ let Dialog = Dialog_1 = class Dialog extends ViewWithToolbar {
126
130
  this.startY = e.clientY;
127
131
  this.startPoint.x = css(this.dialog, 'left');
128
132
  this.startPoint.y = css(this.dialog, 'top');
133
+ // Rounded up: at the right edge a fraction of a pixel less room would
134
+ // wrap the header of a panel that has no width of its own
135
+ const { width, height } = this.dialog.getBoundingClientRect();
136
+ this.startPoint.w = Math.ceil(width);
137
+ this.startPoint.h = Math.ceil(height);
129
138
  this.setMaxZIndex();
130
139
  if (e.cancelable) {
131
140
  e.preventDefault();
@@ -142,7 +151,13 @@ let Dialog = Dialog_1 = class Dialog extends ViewWithToolbar {
142
151
  }
143
152
  __onMouseMove(e) {
144
153
  if (this.draggable && this.o.draggable) {
145
- this.setPosition(this.startPoint.x + e.clientX - this.startX, this.startPoint.y + e.clientY - this.startY);
154
+ const { documentElement } = this.od;
155
+ // Keep the whole panel in the window at the size it had when the
156
+ // drag began. A panel with no width of its own would otherwise
157
+ // shrink as it neared the right edge, and then leave the window
158
+ this.setPosition(clampToWindow(this.startPoint.x + e.clientX - this.startX, (documentElement.clientWidth || this.ow.innerWidth) -
159
+ this.startPoint.w), clampToWindow(this.startPoint.y + e.clientY - this.startY, (documentElement.clientHeight || this.ow.innerHeight) -
160
+ this.startPoint.h));
146
161
  if (this.e) {
147
162
  /**
148
163
  * Fired when dialog box is moved
@@ -291,8 +306,8 @@ let Dialog = Dialog_1 = class Dialog extends ViewWithToolbar {
291
306
  }
292
307
  // strings on purpose: `css()` would truncate fractional pixels
293
308
  css(this.dialog, {
294
- left: (x || left) + 'px',
295
- top: (y || top) + 'px'
309
+ left: (x !== null && x !== void 0 ? x : left) + 'px',
310
+ top: (y !== null && y !== void 0 ? y : top) + 'px'
296
311
  });
297
312
  return this;
298
313
  }
@@ -3,9 +3,24 @@
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
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
7
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
9
+ r = Reflect.decorate(decorators, target, key, desc);
10
+ else
11
+ for (var i = decorators.length - 1; i >= 0; i--)
12
+ if (d = decorators[i])
13
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
14
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
15
+ };
16
+ import { component } from "../../../../core/decorators/index.js";
6
17
  import { UIGroup } from "../../../../core/ui/index.js";
7
- export class FileBrowserFiles extends UIGroup {
18
+ let FileBrowserFiles = class FileBrowserFiles extends UIGroup {
8
19
  className() {
9
20
  return 'FileBrowserFiles';
10
21
  }
11
- }
22
+ };
23
+ FileBrowserFiles = __decorate([
24
+ component
25
+ ], FileBrowserFiles);
26
+ export { FileBrowserFiles };
@@ -3,9 +3,24 @@
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
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
7
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
9
+ r = Reflect.decorate(decorators, target, key, desc);
10
+ else
11
+ for (var i = decorators.length - 1; i >= 0; i--)
12
+ if (d = decorators[i])
13
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
14
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
15
+ };
16
+ import { component } from "../../../../core/decorators/index.js";
6
17
  import { UIGroup } from "../../../../core/ui/index.js";
7
- export class FileBrowserTree extends UIGroup {
18
+ let FileBrowserTree = class FileBrowserTree extends UIGroup {
8
19
  className() {
9
20
  return 'FileBrowserTree';
10
21
  }
11
- }
22
+ };
23
+ FileBrowserTree = __decorate([
24
+ component
25
+ ], FileBrowserTree);
26
+ export { FileBrowserTree };
@@ -11,6 +11,9 @@ import { getMoveFilter } from "../helpers.js";
11
11
  * @private
12
12
  */
13
13
  export function checkJoinNeighbors(jodit, fakeNode, backspace) {
14
+ if (checkJoinBareText(jodit, fakeNode, backspace)) {
15
+ return true;
16
+ }
14
17
  let nextBox = fakeNode, mainClosestBox = nextBox;
15
18
  // Find the main big closest element
16
19
  while (nextBox &&
@@ -44,6 +47,7 @@ export function checkJoinNeighbors(jodit, fakeNode, backspace) {
44
47
  if (sibling &&
45
48
  (checkPullNextLine(jodit, fakeNode, box, sibling, backspace) ||
46
49
  checkMoveListContent(jodit, box, sibling, backspace) ||
50
+ checkJoinInlineSibling(jodit, box, sibling, backspace) ||
47
51
  moveContentAndRemoveEmpty(jodit, box, findEdgeBlock(resolveTableSibling(sibling, backspace), backspace, jodit.editor), backspace))) {
48
52
  jodit.s.setCursorBefore(fakeNode);
49
53
  return true;
@@ -143,6 +147,83 @@ function checkPullNextLine(jodit, fakeNode, box, sibling, backspace) {
143
147
  }
144
148
  return true;
145
149
  }
150
+ /**
151
+ * A block next to bare text in the same parent joins that text, as the
152
+ * browsers do: `<li>one<p>|two</p></li>` gives `<li>one|two</li>`, and
153
+ * `<li><p>one|</p>two</li>` gives `<li><p>one|two</p></li>`
154
+ * @private
155
+ */
156
+ function checkJoinInlineSibling(jodit, box, sibling, backspace) {
157
+ if (Dom.isBlock(sibling) ||
158
+ !Dom.isBlock(box) ||
159
+ Dom.isList(box) ||
160
+ // The caret's block is nested deeper than the text: `text<div><p>|x</p></div>`
161
+ box.parentNode !== sibling.parentNode ||
162
+ Array.from(box.childNodes).some(node => Dom.isBlock(node))) {
163
+ return false;
164
+ }
165
+ if (backspace) {
166
+ // Text after the block keeps its own line
167
+ const next = Dom.findNotEmptySibling(box, false);
168
+ if (next && !Dom.isBlock(next) && !Dom.isTag(next, 'br')) {
169
+ Dom.after(box, jodit.createInside.element('br'));
170
+ }
171
+ Dom.unwrap(box);
172
+ return true;
173
+ }
174
+ const filter = getMoveFilter(jodit);
175
+ for (let node = sibling; node && !Dom.isBlock(node);) {
176
+ const next = node.nextSibling;
177
+ if (Dom.isTag(node, 'br')) {
178
+ Dom.safeRemove(node);
179
+ break;
180
+ }
181
+ filter(node) ? Dom.append(box, node) : Dom.safeRemove(node);
182
+ node = next;
183
+ }
184
+ return true;
185
+ }
186
+ /**
187
+ * Bare text next to a block in the same parent, as the browsers join them:
188
+ * Backspace in `<div><p>a</p>|b</div>` gives `<div><p>a|b</p></div>`, Delete
189
+ * in `<div>a|<p>b</p>c</div>` gives `<div>a|b<br>c</div>`
190
+ */
191
+ function checkJoinBareText(jodit, fakeNode, backspace) {
192
+ const block = Dom.findNotEmptySibling(fakeNode, backspace);
193
+ if (!Dom.isElement(block) ||
194
+ !Dom.isBlock(block) ||
195
+ Dom.isList(block) ||
196
+ Dom.isTag(block, 'table') ||
197
+ Dom.isCell(block) ||
198
+ !Dom.isContentEditable(block, jodit.editor) ||
199
+ Array.from(block.childNodes).some(node => Dom.isBlock(node))) {
200
+ return false;
201
+ }
202
+ const filter = getMoveFilter(jodit);
203
+ if (backspace) {
204
+ // The caret's line goes to the end of the block before it
205
+ for (let node = fakeNode; node && !Dom.isBlock(node);) {
206
+ const next = node.nextSibling;
207
+ if (Dom.isTag(node, 'br')) {
208
+ Dom.safeRemove(node);
209
+ break;
210
+ }
211
+ filter(node) ? Dom.append(block, node) : Dom.safeRemove(node);
212
+ node = next;
213
+ }
214
+ }
215
+ else {
216
+ // The block's content joins the caret's line, and text after the
217
+ // block keeps its own line
218
+ const next = Dom.findNotEmptySibling(block, false);
219
+ if (next && !Dom.isBlock(next) && !Dom.isTag(next, 'br')) {
220
+ Dom.after(block, jodit.createInside.element('br'));
221
+ }
222
+ Dom.unwrap(block);
223
+ }
224
+ jodit.s.setCursorBefore(fakeNode);
225
+ return true;
226
+ }
146
227
  function checkMoveListContent(jodit, mainClosestBox, sibling, backspace) {
147
228
  // Process UL/LI/OL cases
148
229
  const siblingIsList = Dom.isTag(sibling, LIST_TAGS);
@@ -19,6 +19,7 @@ import type { DeleteMode } from "../interface";
19
19
  * ```html
20
20
  * t|st
21
21
  * ```
22
+ * With `stopBeforeSpace`, nothing is removed when the next char is a space.
22
23
  * @private
23
24
  */
24
- export declare function checkRemoveChar(jodit: IJodit, fakeNode: Node, backspace: boolean, mode: DeleteMode): boolean;
25
+ export declare function checkRemoveChar(jodit: IJodit, fakeNode: Node, backspace: boolean, mode: DeleteMode, stopBeforeSpace?: boolean): boolean;
@@ -18,10 +18,11 @@ import { findMostNestedNeighbor } from "../helpers.js";
18
18
  * ```html
19
19
  * t|st
20
20
  * ```
21
+ * With `stopBeforeSpace`, nothing is removed when the next char is a space.
21
22
  * @private
22
23
  */
23
24
  // eslint-disable-next-line complexity
24
- export function checkRemoveChar(jodit, fakeNode, backspace, mode) {
25
+ export function checkRemoveChar(jodit, fakeNode, backspace, mode, stopBeforeSpace = false) {
25
26
  var _a, _b;
26
27
  const step = backspace ? -1 : 1;
27
28
  const anotherSibling = Dom.sibling(fakeNode, !backspace);
@@ -40,6 +41,10 @@ export function checkRemoveChar(jodit, fakeNode, backspace, mode) {
40
41
  break;
41
42
  }
42
43
  if ((_a = sibling.nodeValue) === null || _a === void 0 ? void 0 : _a.length) {
44
+ if (stopBeforeSpace &&
45
+ isSpace(nextChar(sibling.nodeValue, backspace))) {
46
+ break;
47
+ }
43
48
  removed = tryRemoveChar(sibling, backspace, step, anotherSibling);
44
49
  if (!sibling.nodeValue.length &&
45
50
  Dom.isInlineBlock(sibling.parentNode)) {
@@ -79,12 +84,13 @@ function getNextInlineSibling(sibling, backspace, root) {
79
84
  let nextSibling = Dom.sibling(sibling, backspace);
80
85
  if (!nextSibling && sibling.parentNode && sibling.parentNode !== root) {
81
86
  nextSibling = findMostNestedNeighbor(sibling, !backspace, root, true);
82
- // Do not cross a list-item boundary. At the start of a list item the
83
- // neighbor can be the text of the parent/previous item; deleting a char
84
- // there is wrong — the list cases must handle the outdent/merge (#1277).
87
+ // Do not cross a block boundary. At the start of a list item the
88
+ // neighbor can be the text of the parent/previous item (#1277), and at
89
+ // the start of a paragraph the text before it in the same parent
90
+ // (#1693); deleting a char there is wrong — the join cases handle them.
85
91
  if (nextSibling &&
86
- Dom.closest(sibling, 'li', root) !==
87
- Dom.closest(nextSibling, 'li', root)) {
92
+ Dom.closest(sibling, Dom.isBlock, root) !==
93
+ Dom.closest(nextSibling, Dom.isBlock, root)) {
88
94
  return null;
89
95
  }
90
96
  }
@@ -160,8 +166,24 @@ function replaceSpaceOnNBSP(anotherSibling, backspace, value) {
160
166
  }
161
167
  function checkRepeatRemoveCharAction(backspace, sibling, fakeNode, mode, removed, jodit) {
162
168
  call(backspace ? Dom.after : Dom.before, sibling, fakeNode);
163
- if (mode === 'sentence' ||
164
- (mode === 'word' && removed !== ' ' && removed !== NBSP_SPACE)) {
165
- checkRemoveChar(jodit, fakeNode, backspace, mode);
169
+ const wordChar = mode === 'word' && !isSpace(removed);
170
+ // Backspace by word removes the spaces before the caret, then the word, and keeps the space in front of it
171
+ if (mode === 'sentence' || wordChar || (mode === 'word' && backspace)) {
172
+ checkRemoveChar(jodit, fakeNode, backspace, mode, wordChar && backspace);
166
173
  }
167
174
  }
175
+ /**
176
+ * The char tryRemoveChar would remove next
177
+ */
178
+ function nextChar(value, backspace) {
179
+ const chars = toArray(value);
180
+ const step = backspace ? -1 : 1;
181
+ let index = backspace ? chars.length - 1 : 0;
182
+ while (chars[index] === INVISIBLE_SPACE) {
183
+ index += step;
184
+ }
185
+ return chars[index];
186
+ }
187
+ function isSpace(char) {
188
+ return char === ' ' || char === NBSP_SPACE;
189
+ }
@@ -0,0 +1,26 @@
1
+ /*!
2
+ * Jodit Editor (https://xdsoft.net/jodit/)
3
+ * Released under MIT see LICENSE.txt in the project root for license information.
4
+ * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
+ */
6
+ /**
7
+ * @module plugins/backspace
8
+ */
9
+ import type { IJodit } from "../../../types/index";
10
+ /**
11
+ * Backspace at the start of a later list item takes the bullet off, as Word
12
+ * does: the item becomes a paragraph where it was, and the list is split
13
+ * around it. A split `<ol>` carries its numbering on with `start`.
14
+ *
15
+ * @example
16
+ * ```html
17
+ * <ol><li>first</li><li>|second</li><li>third</li></ol>
18
+ * ```
19
+ * Result
20
+ * ```html
21
+ * <ol><li>first</li></ol><p>|second</p><ol start="2"><li>third</li></ol>
22
+ * ```
23
+ *
24
+ * @private
25
+ */
26
+ export declare function checkUnwrapListItem(jodit: IJodit, fakeNode: Node, backspace: boolean): boolean;
@@ -0,0 +1,56 @@
1
+ /*!
2
+ * Jodit Editor (https://xdsoft.net/jodit/)
3
+ * Released under MIT see LICENSE.txt in the project root for license information.
4
+ * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
+ */
6
+ import { Dom } from "../../../core/dom/dom.js";
7
+ /**
8
+ * Backspace at the start of a later list item takes the bullet off, as Word
9
+ * does: the item becomes a paragraph where it was, and the list is split
10
+ * around it. A split `<ol>` carries its numbering on with `start`.
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <ol><li>first</li><li>|second</li><li>third</li></ol>
15
+ * ```
16
+ * Result
17
+ * ```html
18
+ * <ol><li>first</li></ol><p>|second</p><ol start="2"><li>third</li></ol>
19
+ * ```
20
+ *
21
+ * @private
22
+ */
23
+ export function checkUnwrapListItem(jodit, fakeNode, backspace) {
24
+ if (!backspace) {
25
+ return false;
26
+ }
27
+ const li = Dom.closest(fakeNode, 'li', jodit.editor);
28
+ const list = li ? li.parentElement : null;
29
+ if (!li ||
30
+ !Dom.isList(list) ||
31
+ !li.previousElementSibling ||
32
+ Dom.isEmpty(li) ||
33
+ Array.from(li.childNodes).some(node => Dom.isBlock(node)) ||
34
+ !jodit.s.cursorInTheEdge(true, li)) {
35
+ return false;
36
+ }
37
+ const p = jodit.createInside.element(jodit.o.enterBlock);
38
+ Dom.after(list, p);
39
+ if (li.nextElementSibling) {
40
+ const rest = list.cloneNode(false);
41
+ // Two lists cannot share one id
42
+ rest.removeAttribute('id');
43
+ while (li.nextSibling) {
44
+ Dom.append(rest, li.nextSibling);
45
+ }
46
+ if (Dom.isTag(list, 'ol')) {
47
+ const start = parseInt(list.getAttribute('start') || '1', 10) || 1;
48
+ rest.setAttribute('start', String(start + list.children.length - 1));
49
+ }
50
+ Dom.after(p, rest);
51
+ }
52
+ Dom.moveContent(li, p);
53
+ Dom.safeRemove(li);
54
+ jodit.s.setCursorBefore(fakeNode);
55
+ return true;
56
+ }
@@ -12,6 +12,7 @@ import { checkRemoveEmptyParent } from "./check-remove-empty-parent.js";
12
12
  import { checkRemoveUnbreakableElement } from "./check-remove-unbreakable-element.js";
13
13
  import { checkTableCell } from "./check-table-cell.js";
14
14
  import { checkUnwrapFirstListItem } from "./check-unwrap-first-list-item.js";
15
+ import { checkUnwrapListItem } from "./check-unwrap-list-item.js";
15
16
  /**
16
17
  * Ordered delete/backspace cases with stable keys. The first one returning
17
18
  * `true` wins. The keys are stable across minified builds (function names are
@@ -26,6 +27,7 @@ export const casesMap = [
26
27
  ['remove-empty-parent', checkRemoveEmptyParent],
27
28
  ['remove-empty-neighbor', checkRemoveEmptyNeighbor],
28
29
  ['join-two-lists', checkJoinTwoLists],
30
+ ['unwrap-list-item', checkUnwrapListItem],
29
31
  ['join-neighbors', checkJoinNeighbors],
30
32
  ['unwrap-first-list-item', checkUnwrapFirstListItem]
31
33
  ];