jodit 4.18.2 → 4.18.3

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 (95) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/es2015/jodit.css +7 -6
  3. package/es2015/jodit.fat.min.js +10 -10
  4. package/es2015/jodit.js +475 -183
  5. package/es2015/jodit.min.js +10 -10
  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 +9 -9
  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 +7 -6
  17. package/es2021/jodit.fat.min.js +16 -16
  18. package/es2021/jodit.js +415 -129
  19. package/es2021/jodit.min.js +16 -16
  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 +7 -6
  27. package/es2021.en/jodit.fat.min.js +25 -25
  28. package/es2021.en/jodit.js +415 -129
  29. package/es2021.en/jodit.min.js +16 -16
  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 +8 -7
  37. package/es5/jodit.fat.min.js +2 -2
  38. package/es5/jodit.js +536 -209
  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/helpers/utils/index.d.ts +1 -0
  53. package/esm/core/helpers/utils/index.js +1 -0
  54. package/esm/core/helpers/utils/prepare-media.d.ts +19 -0
  55. package/esm/core/helpers/utils/prepare-media.js +31 -0
  56. package/esm/core/selection/style/api/wrap-unwrapped-text.js +3 -2
  57. package/esm/modules/history/history.d.ts +17 -2
  58. package/esm/modules/history/history.js +41 -19
  59. package/esm/modules/table/table.js +7 -2
  60. package/esm/modules/widget/file-selector/file-selector.js +2 -2
  61. package/esm/plugins/backspace/backspace.js +4 -0
  62. package/esm/plugins/clean-html/clean-html.d.ts +7 -0
  63. package/esm/plugins/clean-html/clean-html.js +17 -0
  64. package/esm/plugins/clean-html/helpers/visitor/filters/replace-old-tags.js +26 -1
  65. package/esm/plugins/dtd/before-insert/check-block-nesting.d.ts +2 -1
  66. package/esm/plugins/dtd/before-insert/check-block-nesting.js +26 -5
  67. package/esm/plugins/enter/helpers/move-cursor-out-from-specal-tags.js +8 -0
  68. package/esm/plugins/focus/focus.js +3 -0
  69. package/esm/plugins/hr/hr.js +21 -5
  70. package/esm/plugins/iframe/config.js +8 -3
  71. package/esm/plugins/inline-popup/inline-popup.d.ts +5 -0
  72. package/esm/plugins/inline-popup/inline-popup.js +26 -2
  73. package/esm/plugins/link/link.js +5 -2
  74. package/esm/plugins/media/config.d.ts +2 -1
  75. package/esm/plugins/media/media.js +21 -2
  76. package/esm/plugins/paste/helpers.d.ts +8 -0
  77. package/esm/plugins/paste/helpers.js +18 -1
  78. package/esm/plugins/paste/paste.js +2 -2
  79. package/esm/plugins/resizer/resizer.js +8 -3
  80. package/esm/plugins/select-cells/select-cells.d.ts +10 -0
  81. package/esm/plugins/select-cells/select-cells.js +54 -6
  82. package/esm/plugins/table/config.js +51 -1
  83. package/esm/plugins/table-keyboard-navigation/table-keyboard-navigation.js +1 -1
  84. package/esm/plugins/video/config.js +6 -22
  85. package/package.json +1 -1
  86. package/types/config.d.ts +4 -2
  87. package/types/core/helpers/utils/index.d.ts +1 -0
  88. package/types/core/helpers/utils/prepare-media.d.ts +19 -0
  89. package/types/modules/history/history.d.ts +17 -2
  90. package/types/plugins/clean-html/clean-html.d.ts +7 -0
  91. package/types/plugins/dtd/before-insert/check-block-nesting.d.ts +2 -1
  92. package/types/plugins/inline-popup/inline-popup.d.ts +5 -0
  93. package/types/plugins/media/config.d.ts +2 -1
  94. package/types/plugins/paste/helpers.d.ts +8 -0
  95. package/types/plugins/select-cells/select-cells.d.ts +10 -0
@@ -20,18 +20,34 @@ export function hr(editor) {
20
20
  group: 'insert'
21
21
  });
22
22
  editor.registerCommand('insertHorizontalRule', () => {
23
+ var _a;
23
24
  const elm = editor.createInside.element('hr');
24
25
  editor.s.insertNode(elm, false, false);
25
26
  const block = Dom.closest(elm.parentElement, Dom.isBlock, editor.editor);
26
- if (block && Dom.isEmpty(block) && block !== editor.editor) {
27
+ // A block that holds other blocks, such as a list item or a table
28
+ // cell, keeps the line
29
+ if (block &&
30
+ Dom.isEmpty(block) &&
31
+ block !== editor.editor &&
32
+ !((_a = editor.o.dtd) === null || _a === void 0 ? void 0 : _a.blockLimits[block.nodeName.toLowerCase()])) {
27
33
  Dom.after(block, elm);
28
34
  Dom.safeRemove(block);
29
35
  }
30
- let p = Dom.next(elm, Dom.isBlock, editor.editor, false);
31
- if (!p) {
32
- p = editor.createInside.element(editor.o.enter);
33
- Dom.after(elm, p);
36
+ const next = Dom.findNotEmptySibling(elm, false);
37
+ if (Dom.isBlock(next)) {
38
+ editor.s.setCursorIn(next, true);
39
+ return false;
34
40
  }
41
+ // In a list item or a table cell the text goes on after the line, as
42
+ // it does with the browsers' own command
43
+ if (next ||
44
+ (elm.parentNode !== editor.editor &&
45
+ !Dom.isBlock(Dom.findNotEmptySibling(elm, true)))) {
46
+ editor.s.setCursorAfter(elm);
47
+ return false;
48
+ }
49
+ const p = editor.createInside.element(editor.o.enter);
50
+ Dom.after(elm, p);
35
51
  editor.s.setCursorIn(p);
36
52
  return false;
37
53
  });
@@ -51,13 +51,15 @@ Config.prototype.iframeStyle =
51
51
  'p{' +
52
52
  'margin-top:0;' +
53
53
  '}' +
54
- '.jodit_editor .jodit_iframe_wrapper{' +
54
+ '[data-jodit_iframe_wrapper]{' +
55
55
  'display: block;' +
56
- 'clear: both;' +
57
56
  'user-select: none;' +
58
57
  'position: relative;' +
59
58
  '}' +
60
- '.jodit_editor .jodit_iframe_wrapper:after {' +
59
+ '[data-jodit_iframe_wrapper] iframe{' +
60
+ 'position: relative;' +
61
+ '}' +
62
+ '[data-jodit_iframe_wrapper]:after {' +
61
63
  'position:absolute;' +
62
64
  'content:"";' +
63
65
  'z-index:1;' +
@@ -69,6 +71,9 @@ Config.prototype.iframeStyle =
69
71
  'display: block;' +
70
72
  'background: rgba(0, 0, 0, 0);' +
71
73
  '} ' +
74
+ '[data-jodit_iframe_wrapper][data-jodit-wrapper_active=true] iframe{' +
75
+ 'z-index: 2;' +
76
+ '}' +
72
77
  '.jodit-wysiwyg_image-selected::selection,' +
73
78
  '.jodit-wysiwyg_image-selected *:not(img)::selection{' +
74
79
  'background:transparent' +
@@ -20,6 +20,11 @@ export declare class inlinePopup extends Plugin {
20
20
  private get popup();
21
21
  private get toolbar();
22
22
  private onClick;
23
+ /**
24
+ * The box the popup opens beside: the target's, with room for the
25
+ * resizer's handles when the target gets them, so it doesn't cover them
26
+ */
27
+ private __bound;
23
28
  /**
24
29
  * Show inline popup with some toolbar
25
30
  *
@@ -16,7 +16,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
16
16
  import { autobind, cache, debounce, wait, watch } from "../../core/decorators/index.js";
17
17
  import { Dom } from "../../core/dom/index.js";
18
18
  import { pluginSystem } from "../../core/global.js";
19
- import { camelCase, getMediaWrapper, isArray, isFunction, isString, keys, position, splitArray, toArray } from "../../core/helpers/index.js";
19
+ import { camelCase, getMediaWrapper, getWrappedMedia, isArray, isFunction, isString, keys, position, splitArray, toArray } from "../../core/helpers/index.js";
20
20
  import { Plugin } from "../../core/plugin/index.js";
21
21
  import { UIElement } from "../../core/ui/index.js";
22
22
  import { Popup } from "../../core/ui/popup/index.js";
@@ -53,10 +53,34 @@ export class inlinePopup extends Plugin {
53
53
  ? 'jodit-media'
54
54
  : target === null || target === void 0 ? void 0 : target.nodeName.toLowerCase();
55
55
  if (target && type && this.canShowPopupForType(type)) {
56
- this.showPopup(() => position(target, this.j), type, target);
56
+ const isMedia = type === 'jodit-media';
57
+ // Beside the media in a wrapper kept in the content, which can fill
58
+ // the line
59
+ const bound = (mediaWrapper && getWrappedMedia(this.j, mediaWrapper)) ||
60
+ target;
61
+ this.showPopup(() => this.__bound(bound, isMedia), type, target);
57
62
  return false;
58
63
  }
59
64
  }
65
+ /**
66
+ * The box the popup opens beside: the target's, with room for the
67
+ * resizer's handles when the target gets them, so it doesn't cover them
68
+ */
69
+ __bound(target, isMedia) {
70
+ const bound = position(target, this.j);
71
+ const resizable = this.j.o.allowResizeTags;
72
+ if (!isMedia && !(resizable && Dom.isTag(target, resizable))) {
73
+ return bound;
74
+ }
75
+ const room = parseInt(this.j.ow
76
+ .getComputedStyle(this.j.container)
77
+ .getPropertyValue('--jd-resizer-handle-size'), 10) / 2 || 0;
78
+ return {
79
+ ...bound,
80
+ top: bound.top - room,
81
+ height: bound.height + room * 2
82
+ };
83
+ }
60
84
  /**
61
85
  * Show inline popup with some toolbar
62
86
  *
@@ -16,7 +16,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
16
16
  import { autobind } from "../../core/decorators/index.js";
17
17
  import { Dom } from "../../core/dom/index.js";
18
18
  import { pluginSystem } from "../../core/global.js";
19
- import { attr, call, convertMediaUrlToVideoEmbed, isString, isURL, refs, stripTags } from "../../core/helpers/index.js";
19
+ import { attr, call, convertMediaUrlToVideoEmbed, isString, isURL, prepareEmbedCode, refs, stripTags } from "../../core/helpers/index.js";
20
20
  import { Plugin } from "../../core/plugin/index.js";
21
21
  import "./config.js";
22
22
  /**
@@ -85,7 +85,10 @@ export class link extends Plugin {
85
85
  height: (_d = jodit.o.video) === null || _d === void 0 ? void 0 : _d.defaultHeight
86
86
  });
87
87
  if (embed !== html) {
88
- return jodit.createInside.fromHTML(embed);
88
+ const media = prepareEmbedCode(jodit, embed);
89
+ return isString(media)
90
+ ? jodit.createInside.fromHTML(media)
91
+ : media;
89
92
  }
90
93
  }
91
94
  if (jodit.s.isCollapsed()) {
@@ -28,7 +28,8 @@ declare module 'jodit/config' {
28
28
  * - is wrapped in it instead of the editor-only `mediaFakeTag` when its tag
29
29
  * is one of `mediaBlocks`, or left in it when it's already in one
30
30
  * - goes in inside it when the video popup inserts it, as an upload, a
31
- * link's embed or embed code that is one element
31
+ * link's embed or embed code that is one element, and when a pasted
32
+ * video link is turned into its embed
32
33
  * - opens the media popup for its wrapper when clicked, whose Align and
33
34
  * Delete act on the wrapper
34
35
  *
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as consts from "../../core/constants.js";
7
7
  import { Dom } from "../../core/dom/dom.js";
8
- import { pluginSystem } from "../../core/global.js";
8
+ import { getContainer, pluginSystem } from "../../core/global.js";
9
9
  import { $$, attr, dataBind, getMediaWrapper, wrapMedia } from "../../core/helpers/utils/index.js";
10
10
  import { css, cssInline } from "../../core/helpers/utils/css.js";
11
11
  import "./config.js";
@@ -49,12 +49,31 @@ export function media(editor) {
49
49
  else {
50
50
  return;
51
51
  }
52
+ // Selected, as an image is, so Delete and Backspace remove it
52
53
  editor.e
53
54
  .off(element, 'mousedown.select touchstart.select')
54
55
  .on(element, 'mousedown.select touchstart.select', () => {
55
- editor.s.setCursorAfter(element);
56
+ editor.s.select(element);
56
57
  });
57
58
  };
59
+ // Media in a wrapper kept in the content let clicks through to it, as
60
+ // jodit-media covers its media, so a click selects the media rather than
61
+ // playing it. An inline wrapper holds it as an inline-block, unless its
62
+ // classes or style give it a display of their own
63
+ const wrappers = Object.values(mediaWrappers);
64
+ if (wrappers.length) {
65
+ editor.e.on('afterInit', () => {
66
+ const selectors = (list) => [...new Set(list)].join(',');
67
+ const scoped = ({ tag, className }) => `.jodit-wysiwyg ${tag}.${className}`;
68
+ const inline = selectors(wrappers
69
+ .filter(({ tag }) => consts.IS_INLINE.test(tag))
70
+ .map(scoped));
71
+ const media = selectors(wrappers.flatMap(wrapper => mediaBlocks.map(tag => `${scoped(wrapper)}>${tag}`)));
72
+ getContainer(editor, 'media-wrappers', 'style', true).innerHTML =
73
+ (inline ? `:where(${inline}){display:inline-block}` : '') +
74
+ `${media}{pointer-events:none}`;
75
+ });
76
+ }
58
77
  if (mediaInFakeBlock || Object.keys(mediaWrappers).length) {
59
78
  editor.e
60
79
  .on('afterGetValueFromEditor', (data) => {
@@ -8,6 +8,14 @@
8
8
  */
9
9
  import type { IDialog, IJodit, InsertMode, IUIOption, Nullable } from "../../types/index";
10
10
  import type { PasteEvent } from "./interface";
11
+ /**
12
+ * Google Docs wraps the copied content in
13
+ * `<b style="font-weight:normal;" id="docs-internal-guid-…">`. Kept, the
14
+ * wrapper holds the pasted blocks, and without its style it makes the whole
15
+ * paste bold
16
+ * @private
17
+ */
18
+ export declare function removeGoogleDocsWrapper(html: string): string;
11
19
  /**
12
20
  * One insert point for clipboard plugins
13
21
  * @private
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { TEXT_PLAIN } from "../../core/constants.js";
7
7
  import { Dom } from "../../core/dom/dom.js";
8
+ import { toArray } from "../../core/helpers/array/to-array.js";
8
9
  import { isArray, isNumber, isString, isVoid } from "../../core/helpers/checker/index.js";
9
10
  import { Button } from "../../core/ui/button/button/button.js";
10
11
  /**
@@ -23,6 +24,22 @@ function removeExtraFragments(html) {
23
24
  }
24
25
  return html;
25
26
  }
27
+ /**
28
+ * Google Docs wraps the copied content in
29
+ * `<b style="font-weight:normal;" id="docs-internal-guid-…">`. Kept, the
30
+ * wrapper holds the pasted blocks, and without its style it makes the whole
31
+ * paste bold
32
+ * @private
33
+ */
34
+ export function removeGoogleDocsWrapper(html) {
35
+ if (!/<b\s[^>]*docs-internal-guid-/i.test(html)) {
36
+ return html;
37
+ }
38
+ // An inert document: images don't load and handlers don't run
39
+ const { body } = new DOMParser().parseFromString(html, 'text/html');
40
+ toArray(body.querySelectorAll('b[id^="docs-internal-guid-"]')).forEach(Dom.unwrap);
41
+ return body.innerHTML;
42
+ }
26
43
  /**
27
44
  * @private
28
45
  */
@@ -46,7 +63,7 @@ export function pasteInsertHtml(e, editor, html) {
46
63
  html = result;
47
64
  }
48
65
  if (isString(html)) {
49
- html = removeExtraFragments(html);
66
+ html = removeExtraFragments(removeGoogleDocsWrapper(html));
50
67
  }
51
68
  editor.s.insertHTML(html);
52
69
  }
@@ -20,7 +20,7 @@ import { pluginSystem } from "../../core/global.js";
20
20
  import { cleanFromWord, getDataTransfer, htmlspecialchars, isHTML, isString, LimitedStack, nl2br, stripTags, trim } from "../../core/helpers/index.js";
21
21
  import { Plugin } from "../../core/plugin/plugin.js";
22
22
  import "./config.js";
23
- import { askInsertTypeDialog, getAllTypes, pasteInsertHtml } from "./helpers.js";
23
+ import { askInsertTypeDialog, getAllTypes, pasteInsertHtml, removeGoogleDocsWrapper } from "./helpers.js";
24
24
  /**
25
25
  * Ask before paste HTML source
26
26
  */
@@ -143,7 +143,7 @@ export class paste extends Plugin {
143
143
  this.j.buffer.set(CLIPBOARD_ID, html);
144
144
  switch (action) {
145
145
  case INSERT_CLEAR_HTML:
146
- html = cleanFromWord(html);
146
+ html = cleanFromWord(removeGoogleDocsWrapper(html));
147
147
  break;
148
148
  case INSERT_ONLY_TEXT:
149
149
  html = stripTags(html, this.j.ed, new Set(this.j.o.pasteExcludeStripTags), this.j.o.nl2brInPlainText);
@@ -160,7 +160,8 @@ export class resizer extends Plugin {
160
160
  .off(editor.ow, '.resizer')
161
161
  .on(editor.editor, 'keydown.resizer', (e) => {
162
162
  if (this.isShown &&
163
- e.key === consts.KEY_DELETE &&
163
+ (e.key === consts.KEY_DELETE ||
164
+ e.key === consts.KEY_BACKSPACE) &&
164
165
  this.element &&
165
166
  !Dom.isTag(this.element, 'table')) {
166
167
  this.onDelete(e);
@@ -396,8 +397,12 @@ export class resizer extends Plugin {
396
397
  })
397
398
  .off(element, 'changesize')
398
399
  .on(element, 'changesize', () => {
399
- attr(iframe, 'width', element.offsetWidth + 'px');
400
- attr(iframe, 'height', element.offsetHeight + 'px');
400
+ // Only a resize sets the embed's size, and as the plain numbers
401
+ // the attributes take, so a click leaves `width="100%"` alone
402
+ if (this.isResizeMode) {
403
+ attr(iframe, 'width', element.offsetWidth);
404
+ attr(iframe, 'height', element.offsetHeight);
405
+ }
401
406
  });
402
407
  }
403
408
  this.j.e.on(element, 'dragstart', this.hide);
@@ -52,10 +52,20 @@ export declare class selectCells extends Plugin {
52
52
  * Execute custom commands for table
53
53
  */
54
54
  private onExecCommand;
55
+ /**
56
+ * The cell that takes the caret when rows or columns are deleted: the next
57
+ * one in the same column (or row) that is not deleted, else the previous one
58
+ */
59
+ private __nearestCell;
55
60
  /**
56
61
  * Add some align after native command
57
62
  */
58
63
  private onAfterCommand;
64
+ /**
65
+ * Remove the table and put the caret where it stood: at the start of what
66
+ * follows it, else at the end of what precedes it
67
+ */
68
+ private __removeTable;
59
69
  /** @override */
60
70
  protected beforeDestruct(jodit: IJodit): void;
61
71
  }
@@ -17,7 +17,7 @@ import { KEY_TAB } from "../../core/constants.js";
17
17
  import { autobind, watch } from "../../core/decorators/index.js";
18
18
  import { Dom } from "../../core/dom/dom.js";
19
19
  import { pluginSystem } from "../../core/global.js";
20
- import { $$, alignElement, position } from "../../core/helpers/index.js";
20
+ import { $$, alignElement, position, toArray } from "../../core/helpers/index.js";
21
21
  import { Plugin } from "../../core/plugin/index.js";
22
22
  import { Table } from "../../modules/table/table.js";
23
23
  import "./config.js";
@@ -316,15 +316,23 @@ export class selectCells extends Plugin {
316
316
  this.__tableModule.mergeSelected(table);
317
317
  break;
318
318
  case 'empty':
319
- cells.forEach(td => Dom.detach(td));
319
+ cells.forEach(td => {
320
+ Dom.detach(td);
321
+ Dom.append(td, this.j.createInside.element('br'));
322
+ });
320
323
  break;
321
324
  case 'bin':
322
- Dom.safeRemove(table);
325
+ this.__removeTable(table);
323
326
  break;
324
327
  case 'binrow':
325
- new Set(cells.map(td => td.parentNode)).forEach(row => {
326
- this.__tableModule.removeRow(table, row.rowIndex);
327
- });
328
+ {
329
+ const rows = new Set(cells.map(td => td.parentNode));
330
+ const next = this.__nearestCell(table, cell, toArray(rows).map(row => row.rowIndex), true);
331
+ rows.forEach(row => {
332
+ this.__tableModule.removeRow(table, row.rowIndex);
333
+ });
334
+ next && this.j.s.setCursorIn(next);
335
+ }
328
336
  break;
329
337
  case 'bincolumn':
330
338
  {
@@ -337,11 +345,13 @@ export class selectCells extends Plugin {
337
345
  columnsSet.add(col);
338
346
  }
339
347
  });
348
+ const next = this.__nearestCell(table, cell, columns, false);
340
349
  columns
341
350
  .sort((a, b) => b - a)
342
351
  .forEach(col => {
343
352
  this.__tableModule.removeColumn(table, col);
344
353
  });
354
+ next && this.j.s.setCursorIn(next);
345
355
  }
346
356
  break;
347
357
  case 'addcolumnafter':
@@ -353,10 +363,39 @@ export class selectCells extends Plugin {
353
363
  this.__tableModule.appendRow(table, cell.parentNode, command === 'addrowafter');
354
364
  break;
355
365
  }
366
+ if (/^bin(row|column)$/.test(command) &&
367
+ !this.__tableModule.getColumnsCount(table)) {
368
+ this.__removeTable(table);
369
+ }
356
370
  }
357
371
  return false;
358
372
  }
359
373
  }
374
+ /**
375
+ * The cell that takes the caret when rows or columns are deleted: the next
376
+ * one in the same column (or row) that is not deleted, else the previous one
377
+ */
378
+ __nearestCell(table, cell, deleted, isRow) {
379
+ const box = this.__tableModule.formalMatrix(table);
380
+ const [row, column] = this.__tableModule.formalCoordinate(table, cell);
381
+ const start = isRow ? row : column;
382
+ const count = isRow ? box.length : box[row].length;
383
+ const find = (i) => (!deleted.includes(i) && (isRow ? box[i][column] : box[row][i])) ||
384
+ null;
385
+ for (let i = start + 1; i < count; i += 1) {
386
+ const next = find(i);
387
+ if (next) {
388
+ return next;
389
+ }
390
+ }
391
+ for (let i = start - 1; i >= 0; i -= 1) {
392
+ const prev = find(i);
393
+ if (prev) {
394
+ return prev;
395
+ }
396
+ }
397
+ return null;
398
+ }
360
399
  /**
361
400
  * Add some align after native command
362
401
  */
@@ -367,6 +406,15 @@ export class selectCells extends Plugin {
367
406
  .forEach(elm => alignElement(command, elm));
368
407
  }
369
408
  }
409
+ /**
410
+ * Remove the table and put the caret where it stood: at the start of what
411
+ * follows it, else at the end of what precedes it
412
+ */
413
+ __removeTable(table) {
414
+ const parent = table.parentNode, next = Dom.findNotEmptySibling(table, false), prev = Dom.findNotEmptySibling(table, true);
415
+ Dom.safeRemove(table);
416
+ this.j.s.setCursorIn(next || prev || parent, Boolean(next));
417
+ }
370
418
  /** @override */
371
419
  beforeDestruct(jodit) {
372
420
  this.__onRemoveSelection();
@@ -16,6 +16,44 @@ Config.prototype.table = {
16
16
  useExtraClassesOptions: false
17
17
  };
18
18
  Icon.set('table', tableIcon);
19
+ /**
20
+ * Elements in `dtd.blockLimits` that hold only rows, cells or list items, so
21
+ * a table can't go directly inside them
22
+ */
23
+ const ROW_AND_ITEM_HOLDERS = new Set([
24
+ 'table',
25
+ 'thead',
26
+ 'tbody',
27
+ 'tfoot',
28
+ 'tr',
29
+ 'ul',
30
+ 'ol',
31
+ 'dl'
32
+ ]);
33
+ /**
34
+ * The nearest block around the cursor that can hold the new table: an element
35
+ * in `dtd.blockLimits`, such as a table cell, a list item or a `<div>`. The
36
+ * cursor's own block of the `enter` tag is a line, not a container, as in the
37
+ * dtd plugin's `checkBlockNesting`
38
+ */
39
+ function findTableContainer(editor, current) {
40
+ const line = Dom.closest(current, Dom.isBlock, editor.editor);
41
+ const container = Dom.closest(current, node => {
42
+ var _a;
43
+ if (!Dom.isElement(node) ||
44
+ (node === line && Dom.isTag(node, editor.o.enter))) {
45
+ return false;
46
+ }
47
+ const tag = node.nodeName.toLowerCase();
48
+ return (Boolean((_a = editor.o.dtd) === null || _a === void 0 ? void 0 : _a.blockLimits[tag]) &&
49
+ !ROW_AND_ITEM_HOLDERS.has(tag));
50
+ }, editor.editor);
51
+ // With the cursor between two rows, `current` is the next row's text
52
+ return container &&
53
+ Dom.isOrContains(container, editor.s.range.startContainer)
54
+ ? container
55
+ : null;
56
+ }
19
57
  Config.prototype.controls.table = {
20
58
  data: {
21
59
  cols: 10,
@@ -129,7 +167,13 @@ Config.prototype.controls.table = {
129
167
  editor.s.removeMarkers();
130
168
  editor.editor.normalize();
131
169
  editor.history.snapshot.restore(snapshot);
132
- const block = Dom.furthest(editor.s.current(), Dom.isBlock, editor.editor);
170
+ const current = editor.s.current();
171
+ // The table goes into the cell, list item or box holding the
172
+ // cursor, and only a block inside it is split
173
+ const container = findTableContainer(editor, current);
174
+ const block = container === current
175
+ ? null
176
+ : Dom.furthest(current, Dom.isBlock, container !== null && container !== void 0 ? container : editor.editor);
133
177
  if (block && Dom.isEmpty(block)) {
134
178
  Dom.replace(block, table, undefined, false, true);
135
179
  }
@@ -155,6 +199,12 @@ Config.prototype.controls.table = {
155
199
  }
156
200
  }
157
201
  }
202
+ else if (container &&
203
+ !Dom.isEmpty(container) &&
204
+ !editor.o.table.splitBlockOnInsertTable) {
205
+ // With no block to go after, the table goes after the text
206
+ Dom.append(container, table);
207
+ }
158
208
  else {
159
209
  editor.s.insertNode(table, false);
160
210
  }
@@ -72,7 +72,7 @@ export function tableKeyboardNavigation(editor) {
72
72
  editor.s.setCursorBefore(first);
73
73
  }
74
74
  else {
75
- if (key === consts.KEY_TAB) {
75
+ if (key === consts.KEY_TAB && !Dom.isEmpty(next)) {
76
76
  editor.s.select(next, true);
77
77
  }
78
78
  else {
@@ -3,23 +3,13 @@
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 { call, convertMediaUrlToVideoEmbed, getAlignClasses, hAlignElement, wrapMedia } from "../../core/helpers/index.js";
6
+ import { call, convertMediaUrlToVideoEmbed, prepareEmbedCode, prepareMedia } from "../../core/helpers/index.js";
7
7
  import { Button } from "../../core/ui/button/index.js";
8
8
  import { UIBlock, UIForm, UIInput, UITextArea } from "../../core/ui/form/index.js";
9
9
  import { Icon } from "../../core/ui/icon.js";
10
10
  import { Config } from "../../config.js";
11
11
  import { TabsWidget, UploadTab } from "../../modules/widget/index.js";
12
12
  import videoIcon from "./video.svg.js";
13
- // Media the popup inserts go in their wrapper (see `mediaWrappers`), aligned
14
- // with `mediaDefaultAlign`
15
- function prepareMedia(jodit, element) {
16
- const media = wrapMedia(jodit, element);
17
- const { mediaDefaultAlign } = jodit.o;
18
- if (mediaDefaultAlign && mediaDefaultAlign !== 'normal') {
19
- hAlignElement(media, mediaDefaultAlign, getAlignClasses(jodit, media));
20
- }
21
- return media;
22
- }
23
13
  function insertVideos(data) {
24
14
  var _a;
25
15
  (_a = data.files) === null || _a === void 0 ? void 0 : _a.forEach(file => {
@@ -29,16 +19,6 @@ function insertVideos(data) {
29
19
  })));
30
20
  });
31
21
  }
32
- // Embed code that is one element is prepared as media; other code goes in as
33
- // it is
34
- function prepareEmbedCode(jodit, code) {
35
- const box = jodit.createInside.div();
36
- box.innerHTML = code.trim();
37
- const element = box.firstElementChild;
38
- return element && box.childNodes.length === 1
39
- ? prepareMedia(jodit, element)
40
- : code;
41
- }
42
22
  Config.prototype.video = {
43
23
  parseUrlToVideoEmbed: convertMediaUrlToVideoEmbed,
44
24
  defaultWidth: 400,
@@ -107,7 +87,11 @@ Config.prototype.controls.video = {
107
87
  formCode.onSubmit(data => {
108
88
  insertCode(data.code);
109
89
  });
110
- return TabsWidget(jodit, tabs);
90
+ const box = TabsWidget(jodit, tabs);
91
+ // Its tabs are sized to their labels, as the file selector's are, since
92
+ // "Upload" doesn't fit a third of the popup
93
+ box.classList.add('jodit-file-selector');
94
+ return box;
111
95
  },
112
96
  tags: ['iframe'],
113
97
  tooltip: 'Insert youtube/vimeo video'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jodit",
3
- "version": "4.18.2",
3
+ "version": "4.18.3",
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
@@ -989,7 +989,8 @@ interface Config {
989
989
  */
990
990
  maxHistoryLength: number;
991
991
  /**
992
- * Delay on every change
992
+ * How long a step stays open after its first change, in
993
+ * milliseconds. The changes made in that time are undone together.
993
994
  */
994
995
  timeout: number;
995
996
  };
@@ -1597,7 +1598,8 @@ interface Config {
1597
1598
  * - is wrapped in it instead of the editor-only `mediaFakeTag` when its tag
1598
1599
  * is one of `mediaBlocks`, or left in it when it's already in one
1599
1600
  * - goes in inside it when the video popup inserts it, as an upload, a
1600
- * link's embed or embed code that is one element
1601
+ * link's embed or embed code that is one element, and when a pasted
1602
+ * video link is turned into its embed
1601
1603
  * - opens the media popup for its wrapper when clicked, whose Align and
1602
1604
  * Delete act on the wrapper
1603
1605
  *
@@ -28,6 +28,7 @@ export * from "./is-unsafe-proto-key";
28
28
  export * from "./mark-deprecated";
29
29
  export * from "./media-wrapper";
30
30
  export * from "./parse-query";
31
+ export * from "./prepare-media";
31
32
  export * from "./print";
32
33
  export * from "./reset";
33
34
  export * from "./scroll-into-view";
@@ -0,0 +1,19 @@
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 helpers/utils
8
+ */
9
+ import type { IJodit } from "../../../types/index";
10
+ /**
11
+ * Puts new media in its wrapper (see `mediaWrappers`) and aligns it with
12
+ * `mediaDefaultAlign`. Returns the wrapper, or `element` when there is none
13
+ */
14
+ export declare function prepareMedia(jodit: IJodit, element: HTMLElement): HTMLElement;
15
+ /**
16
+ * Embed code that is one element is prepared as media (see `prepareMedia`);
17
+ * other code is returned as it is
18
+ */
19
+ export declare function prepareEmbedCode(jodit: IJodit, code: string): string | HTMLElement;