jodit 4.18.10 → 4.18.12

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 (73) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/es2015/jodit.css +1 -1
  3. package/es2015/jodit.fat.min.js +5 -5
  4. package/es2015/jodit.js +281 -76
  5. package/es2015/jodit.min.js +5 -5
  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 +5 -5
  13. package/es2018/jodit.min.js +5 -5
  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 +7 -7
  18. package/es2021/jodit.js +278 -76
  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 +7 -7
  28. package/es2021.en/jodit.js +278 -76
  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 +353 -77
  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 +2 -2
  48. package/es5/polyfills.js +3 -3
  49. package/es5/polyfills.min.js +2 -2
  50. package/esm/core/async/async.js +2 -1
  51. package/esm/core/constants.js +1 -1
  52. package/esm/core/helpers/size/get-fixed-position-offset.d.ts +4 -2
  53. package/esm/core/helpers/size/get-fixed-position-offset.js +14 -2
  54. package/esm/core/selection/style/api/list/toggle-ordered-list.d.ts +1 -1
  55. package/esm/core/selection/style/api/list/toggle-ordered-list.js +30 -4
  56. package/esm/core/selection/style/transactions.js +1 -1
  57. package/esm/plugins/add-new-line/add-new-line.d.ts +7 -0
  58. package/esm/plugins/add-new-line/add-new-line.js +45 -0
  59. package/esm/plugins/backspace/backspace.d.ts +7 -0
  60. package/esm/plugins/backspace/backspace.js +45 -2
  61. package/esm/plugins/backspace/cases/check-join-neighbors.js +69 -1
  62. package/esm/plugins/backspace/cases/check-remove-char.js +3 -1
  63. package/esm/plugins/backspace/cases/check-remove-content-not-editable.d.ts +4 -1
  64. package/esm/plugins/backspace/cases/check-remove-content-not-editable.js +39 -11
  65. package/esm/plugins/wrap-nodes/wrap-nodes.js +19 -2
  66. package/esm/types/async.d.ts +7 -0
  67. package/package.json +1 -1
  68. package/types/core/helpers/size/get-fixed-position-offset.d.ts +4 -2
  69. package/types/core/selection/style/api/list/toggle-ordered-list.d.ts +1 -1
  70. package/types/plugins/add-new-line/add-new-line.d.ts +7 -0
  71. package/types/plugins/backspace/backspace.d.ts +7 -0
  72. package/types/plugins/backspace/cases/check-remove-content-not-editable.d.ts +4 -1
  73. package/types/types/async.d.ts +7 -0
@@ -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 { INSEPARABLE_TAGS, LIST_TAGS } from "../../../core/constants.js";
6
+ import { INSEPARABLE_TAGS, LIST_TAGS, NO_EMPTY_TAGS } from "../../../core/constants.js";
7
7
  import { Dom } from "../../../core/dom/dom.js";
8
8
  import { getMoveFilter } from "../helpers.js";
9
9
  /**
@@ -35,6 +35,9 @@ function joinNeighbors(jodit, fakeNode, backspace, moved) {
35
35
  return false;
36
36
  }
37
37
  const sibling = Dom.findNotEmptySibling(mainClosestBox, backspace);
38
+ if (moveInlinesInside(jodit, fakeNode, sibling)) {
39
+ return joinNeighbors(jodit, fakeNode, backspace, moved);
40
+ }
38
41
  // A line break before the cursor is a single unit: remove it instead of
39
42
  // merging the current element into the (void) `<br>`, which used to move
40
43
  // the whole element's content into the `<br>` and delete it (#1282).
@@ -49,6 +52,11 @@ function joinNeighbors(jodit, fakeNode, backspace, moved) {
49
52
  jodit.s.setCursorBefore(fakeNode);
50
53
  return true;
51
54
  }
55
+ // A non-editable sibling is removed by remove-not-editable, never joined
56
+ if (Dom.isElement(sibling) &&
57
+ !Dom.isContentEditable(sibling, jodit.editor)) {
58
+ return false;
59
+ }
52
60
  const box = findCaretBox(fakeNode, mainClosestBox);
53
61
  // One pass only: the line now sits beside the next one, so the
54
62
  // cases below join them. The DOM has changed either way
@@ -86,6 +94,66 @@ function findCaretBox(fakeNode, mainClosestBox) {
86
94
  ? block.parentElement
87
95
  : block;
88
96
  }
97
+ /**
98
+ * With an inline element round the caret's line or the one it joins moved
99
+ * inside its blocks, the cases join the lines as they join any two, and its
100
+ * styles stay with its text. One round both lines is left as it is.
101
+ */
102
+ function moveInlinesInside(jodit, fakeNode, sibling) {
103
+ if (!sibling) {
104
+ return false;
105
+ }
106
+ const around = Dom.furthest(fakeNode, node => Dom.isElement(node) &&
107
+ !Dom.isBlock(node) &&
108
+ !Dom.isOrContains(node, sibling), jodit.editor);
109
+ return (moveInlineInside(around, jodit.editor) ||
110
+ moveInlineInside(sibling, jodit.editor));
111
+ }
112
+ /**
113
+ * Old content can have an inline element round blocks. This moves it inside
114
+ * them, and gives the text, images and line breaks beside them a copy of
115
+ * their own: `<span style="…"><p>one</p><br><p>two</p></span>` becomes
116
+ * `<p><span style="…">one</span></p><span style="…"><br></span><p><span style="…">two</span></p>`
117
+ */
118
+ function moveInlineInside(node, root) {
119
+ if (!Dom.isElement(node) || Dom.isBlock(node)) {
120
+ return false;
121
+ }
122
+ // Embedded content, such as the `<iframe>` in a video's wrapper, counts
123
+ // as a block in `Dom.isBlock`, but stays with the text beside it
124
+ const isBlock = (child) => Dom.isBlock(child) && !Dom.isTag(child, NO_EMPTY_TAGS);
125
+ // The inner one first: `<span><strong><p>one</p></strong></span>`
126
+ Array.from(node.childNodes).forEach(child => moveInlineInside(child, root));
127
+ const children = Array.from(node.childNodes);
128
+ if (!children.some(isBlock)) {
129
+ return false;
130
+ }
131
+ let run = null;
132
+ for (const child of children) {
133
+ if (!isBlock(child)) {
134
+ // Blank text between blocks stays where it is
135
+ if (!run && !Dom.isEmptyTextNode(child)) {
136
+ run = node.cloneNode(false);
137
+ Dom.before(child, run);
138
+ }
139
+ run && Dom.append(run, child);
140
+ continue;
141
+ }
142
+ run = null;
143
+ // Nothing goes inside a rule or a non-editable block
144
+ if (Dom.isTag(child, INSEPARABLE_TAGS) ||
145
+ !Dom.isContentEditable(child, root)) {
146
+ continue;
147
+ }
148
+ const inner = node.cloneNode(false);
149
+ Dom.moveContent(child, inner);
150
+ Dom.append(child, inner);
151
+ // A list or a table: on down to its items or cells
152
+ moveInlineInside(inner, root);
153
+ }
154
+ Dom.unwrap(node);
155
+ return true;
156
+ }
89
157
  /**
90
158
  * The innermost block on the sibling's edge facing the caret, so the
91
159
  * content is joined to text: `<div><p>one</p></div><p>|two</p>` gives
@@ -37,7 +37,9 @@ export function checkRemoveChar(jodit, fakeNode, backspace, mode, stopBeforeSpac
37
37
  while (Dom.isInlineBlock(sibling)) {
38
38
  sibling = (backspace ? sibling === null || sibling === void 0 ? void 0 : sibling.lastChild : sibling === null || sibling === void 0 ? void 0 : sibling.firstChild);
39
39
  }
40
- if (!sibling) {
40
+ // A block inside an inline element, such as the `<p>` in
41
+ // `<span><p>…</p></span>`, isn't a char: the join cases handle it
42
+ if (!sibling || Dom.isBlock(sibling)) {
41
43
  break;
42
44
  }
43
45
  if ((_a = sibling.nodeValue) === null || _a === void 0 ? void 0 : _a.length) {
@@ -8,7 +8,10 @@
8
8
  */
9
9
  import type { IJodit } from "../../../types/index";
10
10
  /**
11
- * Checks if a non-editable element can be deleted
11
+ * Checks if a non-editable element can be deleted. A non-editable block goes
12
+ * whole, and the lines either side of it then join, as the spec's tests do:
13
+ * `<ul><li>one|</li></ul><div contenteditable="false">x</div><p>two</p>`
14
+ * gives `<ul><li>one|two</li></ul>`
12
15
  * @private
13
16
  */
14
17
  export declare function checkRemoveContentNotEditable(jodit: IJodit, fakeNode: Text, backspace: boolean): boolean;
@@ -6,24 +6,52 @@
6
6
  import { Dom } from "../../../core/dom/index.js";
7
7
  import { call } from "../../../core/helpers/index.js";
8
8
  import { moveNodeInsideStart } from "../../../core/selection/helpers/index.js";
9
+ import { checkJoinNeighbors } from "./check-join-neighbors.js";
9
10
  /**
10
- * Checks if a non-editable element can be deleted
11
+ * Checks if a non-editable element can be deleted. A non-editable block goes
12
+ * whole, and the lines either side of it then join, as the spec's tests do:
13
+ * `<ul><li>one|</li></ul><div contenteditable="false">x</div><p>two</p>`
14
+ * gives `<ul><li>one|two</li></ul>`
11
15
  * @private
12
16
  */
13
17
  export function checkRemoveContentNotEditable(jodit, fakeNode, backspace) {
14
- let neighbor = Dom.findSibling(fakeNode, backspace);
15
- if (!neighbor &&
16
- fakeNode.parentElement &&
17
- fakeNode.parentElement !== jodit.editor) {
18
- neighbor = Dom.findSibling(fakeNode.parentElement, backspace);
18
+ let node = fakeNode;
19
+ let neighbor = Dom.findSibling(node, backspace);
20
+ // Past the edges of the blocks the caret is at, but not into a table cell
21
+ while (!neighbor &&
22
+ node.parentElement &&
23
+ node.parentElement !== jodit.editor &&
24
+ !Dom.isCell(node.parentElement)) {
25
+ node = node.parentElement;
26
+ neighbor = Dom.findSibling(node, backspace);
27
+ // At the start of a list item, Backspace takes the bullet off first,
28
+ // unless the item before it is a non-editable one to remove
29
+ if (backspace &&
30
+ Dom.isLeaf(node) &&
31
+ !(Dom.isLeaf(neighbor) && isNotEditable(jodit, neighbor))) {
32
+ return false;
33
+ }
19
34
  }
20
- if (Dom.isElement(neighbor) &&
21
- !Dom.isContentEditable(neighbor, jodit.editor)) {
22
- call(backspace ? Dom.before : Dom.after, neighbor, fakeNode);
23
- Dom.safeRemove(neighbor);
24
- moveNodeInsideStart(jodit, fakeNode, backspace);
35
+ if (isNotEditable(jodit, neighbor)) {
36
+ if (Dom.isBlock(neighbor)) {
37
+ // Backspace after a removed list item leaves the items apart: the
38
+ // next press takes the bullet off, as for any later item
39
+ const joinLines = !(backspace && Dom.isLeaf(neighbor));
40
+ Dom.safeRemove(neighbor);
41
+ if (joinLines) {
42
+ checkJoinNeighbors(jodit, fakeNode, backspace);
43
+ }
44
+ }
45
+ else {
46
+ call(backspace ? Dom.before : Dom.after, neighbor, fakeNode);
47
+ Dom.safeRemove(neighbor);
48
+ moveNodeInsideStart(jodit, fakeNode, backspace);
49
+ }
25
50
  call(backspace ? jodit.s.setCursorBefore : jodit.s.setCursorAfter, fakeNode);
26
51
  return true;
27
52
  }
28
53
  return false;
29
54
  }
55
+ function isNotEditable(jodit, node) {
56
+ return Dom.isElement(node) && !Dom.isContentEditable(node, jodit.editor);
57
+ }
@@ -13,6 +13,7 @@ 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
+ import { NO_EMPTY_TAGS } from "../../core/constants.js";
16
17
  import { autobind } from "../../core/decorators/index.js";
17
18
  import { Dom } from "../../core/dom/index.js";
18
19
  import { pluginSystem } from "../../core/global.js";
@@ -45,7 +46,9 @@ class wrapNodes extends Plugin {
45
46
  * Some element which needs to append in block
46
47
  */
47
48
  this.isNotWrapped = (n) => Dom.isElement(n) &&
48
- !(Dom.isBlock(n) || Dom.isTag(n, this.j.o.wrapNodes.exclude));
49
+ !(Dom.isBlock(n) ||
50
+ Dom.isTag(n, this.j.o.wrapNodes.exclude) ||
51
+ holdsBlock(n));
49
52
  }
50
53
  /** @override **/
51
54
  afterInit(jodit) {
@@ -81,7 +84,9 @@ class wrapNodes extends Plugin {
81
84
  isChanged = true;
82
85
  const box = jodit.createInside.element(jodit.o.enter);
83
86
  Dom.before(child, box);
84
- while (child && this.isSuitable(child)) {
87
+ // `exclude` keeps a <br> from starting a box, but one between loose lines stays with them
88
+ while (child &&
89
+ (this.isSuitable(child) || Dom.isTag(child, 'br'))) {
85
90
  const next = child.nextSibling;
86
91
  Dom.append(box, child);
87
92
  child = next;
@@ -124,6 +129,18 @@ __decorate([
124
129
  __decorate([
125
130
  autobind
126
131
  ], wrapNodes.prototype, "preprocessInput", null);
132
+ /**
133
+ * A block can't go in a paragraph, so neither can an element that holds one,
134
+ * such as a `<span>` round a `<p>`. Embedded content such as an `<iframe>` can.
135
+ */
136
+ function holdsBlock(elm) {
137
+ for (const node of Dom.eachGen(elm)) {
138
+ if (Dom.isBlock(node) && !Dom.isTag(node, NO_EMPTY_TAGS)) {
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
127
144
  function checkAloneListLeaf(child, jodit) {
128
145
  let result = child;
129
146
  let next = child;
@@ -20,6 +20,13 @@ interface RejectablePromise<T> extends Promise<T> {
20
20
  }
21
21
  export interface IAsync extends IDestructible {
22
22
  delay(timeout: number | IAsyncParams): Promise<void>;
23
+ /**
24
+ * Calls `callback` after `timeout` milliseconds, unless the view is
25
+ * destroyed first. A timeout of 0, or none, calls it at once, before
26
+ * returning, where `window.setTimeout(callback, 0)` would wait for the
27
+ * current event to finish. To run after the browser has handled an event,
28
+ * such as moving the caret on a keydown, use a timeout of at least 1.
29
+ */
23
30
  setTimeout<T = any>(callback: (...args: T[]) => void, timeout: number | IAsyncParams, ...args: T[]): number;
24
31
  updateTimeout(label: string, timeout: number): Nullable<number>;
25
32
  clearTimeout(timer: number): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jodit",
3
- "version": "4.18.10",
3
+ "version": "4.18.12",
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",
@@ -15,7 +15,9 @@ import type { IPoint } from "../../../types/index";
15
15
  * ancestor's top-left corner. The returned offset should be subtracted from
16
16
  * the desired viewport coordinates before applying them.
17
17
  *
18
- * Returns `{ x: 0, y: 0 }` when no such ancestor exists (the common case), so
19
- * call sites keep their previous behaviour unchanged.
18
+ * Without such an ancestor the containing block is the layout viewport. Its
19
+ * offset is `{ x: 0, y: 0 }` in most browsers, but WebKit measures client
20
+ * rects from the visual viewport, so once the page is pinch-zoomed and panned
21
+ * the layout viewport starts above and to the left of the client origin.
20
22
  */
21
23
  export declare function getFixedPositionOffset(elm: HTMLElement): IPoint;
@@ -8,4 +8,4 @@ import type { CommitMode, ICommitStyle, IJodit } from "../../../../../types/inde
8
8
  * Replaces `ul->ol` or `ol->ul`, apply styles to the list, or remove a list item from it
9
9
  * @private
10
10
  */
11
- export declare function toggleOrderedList(commitStyle: ICommitStyle, li: HTMLElement, jodit: IJodit, mode: CommitMode): CommitMode;
11
+ export declare function toggleOrderedList(commitStyle: ICommitStyle, li: HTMLElement, jodit: IJodit, mode: CommitMode, collapsed?: boolean): CommitMode;
@@ -30,6 +30,13 @@ export declare class addNewLine extends Plugin {
30
30
  protected afterInit(editor: IJodit): void;
31
31
  private __addEventListeners;
32
32
  private __onClickLine;
33
+ /**
34
+ * A paragraph holding only an embed has no place for the caret, so the
35
+ * arrow keys can't get past one that ends the content, or before one that
36
+ * starts it. When an arrow key leaves the caret where it was and there is
37
+ * content beyond it, a new line goes there, as when the line is clicked.
38
+ */
39
+ protected onArrowKey(e: KeyboardEvent): void;
33
40
  protected onDblClickEditor(e: MouseEvent): void;
34
41
  private __onMouseMove;
35
42
  /** @override */
@@ -19,6 +19,13 @@ export declare class backspace extends Plugin {
19
19
  * Listener BackSpace or Delete button
20
20
  */
21
21
  private onDelete;
22
+ /**
23
+ * The browser types at the end of the text before the caret on its line,
24
+ * in that text's formatting, but not inside a link. Moving the caret there
25
+ * too makes a paste and the toolbar agree with what's typed:
26
+ * `<p>one</p><p><b>|two</b></p>` joins to `<p>one|<b>two</b></p>`
27
+ */
28
+ private moveCaretToTextBefore;
22
29
  /**
23
30
  * Remove node and replace cursor position out of it
24
31
  */
@@ -8,7 +8,10 @@
8
8
  */
9
9
  import type { IJodit } from "../../../types/index";
10
10
  /**
11
- * Checks if a non-editable element can be deleted
11
+ * Checks if a non-editable element can be deleted. A non-editable block goes
12
+ * whole, and the lines either side of it then join, as the spec's tests do:
13
+ * `<ul><li>one|</li></ul><div contenteditable="false">x</div><p>two</p>`
14
+ * gives `<ul><li>one|two</li></ul>`
12
15
  * @private
13
16
  */
14
17
  export declare function checkRemoveContentNotEditable(jodit: IJodit, fakeNode: Text, backspace: boolean): boolean;
@@ -20,6 +20,13 @@ interface RejectablePromise<T> extends Promise<T> {
20
20
  }
21
21
  export interface IAsync extends IDestructible {
22
22
  delay(timeout: number | IAsyncParams): Promise<void>;
23
+ /**
24
+ * Calls `callback` after `timeout` milliseconds, unless the view is
25
+ * destroyed first. A timeout of 0, or none, calls it at once, before
26
+ * returning, where `window.setTimeout(callback, 0)` would wait for the
27
+ * current event to finish. To run after the browser has handled an event,
28
+ * such as moving the caret on a keydown, use a timeout of at least 1.
29
+ */
23
30
  setTimeout<T = any>(callback: (...args: T[]) => void, timeout: number | IAsyncParams, ...args: T[]): number;
24
31
  updateTimeout(label: string, timeout: number): Nullable<number>;
25
32
  clearTimeout(timer: number): void;