jodit 4.18.2 → 4.18.4

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 (226) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/es2015/jodit.css +66 -6
  3. package/es2015/jodit.fat.min.css +1 -1
  4. package/es2015/jodit.fat.min.js +36 -11
  5. package/es2015/jodit.js +1435 -327
  6. package/es2015/jodit.min.css +1 -1
  7. package/es2015/jodit.min.js +41 -16
  8. package/es2015/plugins/debug/debug.css +1 -1
  9. package/es2015/plugins/debug/debug.js +1 -1
  10. package/es2015/plugins/debug/debug.min.js +1 -1
  11. package/es2015/plugins/speech-recognize/speech-recognize.css +1 -1
  12. package/es2015/plugins/speech-recognize/speech-recognize.js +1 -1
  13. package/es2015/plugins/speech-recognize/speech-recognize.min.js +1 -1
  14. package/es2018/jodit.fat.min.css +1 -1
  15. package/es2018/jodit.fat.min.js +36 -11
  16. package/es2018/jodit.min.css +1 -1
  17. package/es2018/jodit.min.js +36 -11
  18. package/es2018/plugins/debug/debug.min.js +1 -1
  19. package/es2018/plugins/speech-recognize/speech-recognize.min.js +1 -1
  20. package/es2021/jodit.css +66 -6
  21. package/es2021/jodit.fat.min.css +1 -1
  22. package/es2021/jodit.fat.min.js +45 -20
  23. package/es2021/jodit.js +1365 -271
  24. package/es2021/jodit.min.css +1 -1
  25. package/es2021/jodit.min.js +45 -20
  26. package/es2021/plugins/debug/debug.css +1 -1
  27. package/es2021/plugins/debug/debug.js +1 -1
  28. package/es2021/plugins/debug/debug.min.js +1 -1
  29. package/es2021/plugins/speech-recognize/speech-recognize.css +1 -1
  30. package/es2021/plugins/speech-recognize/speech-recognize.js +1 -1
  31. package/es2021/plugins/speech-recognize/speech-recognize.min.js +1 -1
  32. package/es2021.en/jodit.css +66 -6
  33. package/es2021.en/jodit.fat.min.css +1 -1
  34. package/es2021.en/jodit.fat.min.js +51 -26
  35. package/es2021.en/jodit.js +1339 -245
  36. package/es2021.en/jodit.min.css +1 -1
  37. package/es2021.en/jodit.min.js +51 -26
  38. package/es2021.en/plugins/debug/debug.css +1 -1
  39. package/es2021.en/plugins/debug/debug.js +1 -1
  40. package/es2021.en/plugins/debug/debug.min.js +1 -1
  41. package/es2021.en/plugins/speech-recognize/speech-recognize.css +1 -1
  42. package/es2021.en/plugins/speech-recognize/speech-recognize.js +1 -1
  43. package/es2021.en/plugins/speech-recognize/speech-recognize.min.js +1 -1
  44. package/es5/jodit.css +82 -7
  45. package/es5/jodit.fat.min.css +1 -1
  46. package/es5/jodit.fat.min.js +2 -2
  47. package/es5/jodit.js +1629 -354
  48. package/es5/jodit.min.css +3 -3
  49. package/es5/jodit.min.js +2 -2
  50. package/es5/plugins/debug/debug.css +1 -1
  51. package/es5/plugins/debug/debug.js +1 -1
  52. package/es5/plugins/debug/debug.min.js +1 -1
  53. package/es5/plugins/speech-recognize/speech-recognize.css +1 -1
  54. package/es5/plugins/speech-recognize/speech-recognize.js +1 -1
  55. package/es5/plugins/speech-recognize/speech-recognize.min.js +1 -1
  56. package/es5/polyfills.fat.min.js +1 -1
  57. package/es5/polyfills.js +1 -1
  58. package/es5/polyfills.min.js +1 -1
  59. package/esm/config.d.ts +29 -28
  60. package/esm/config.js +19 -21
  61. package/esm/core/constants.js +1 -1
  62. package/esm/core/helpers/utils/align.d.ts +1 -1
  63. package/esm/core/helpers/utils/align.js +2 -3
  64. package/esm/core/helpers/utils/index.d.ts +1 -0
  65. package/esm/core/helpers/utils/index.js +1 -0
  66. package/esm/core/helpers/utils/prepare-media.d.ts +19 -0
  67. package/esm/core/helpers/utils/prepare-media.js +31 -0
  68. package/esm/core/selection/style/api/wrap-unwrapped-text.js +3 -2
  69. package/esm/jodit.js +14 -4
  70. package/esm/langs/ar.d.ts +7 -0
  71. package/esm/langs/ar.js +8 -1
  72. package/esm/langs/az.d.ts +7 -0
  73. package/esm/langs/az.js +8 -1
  74. package/esm/langs/cs_cz.d.ts +7 -0
  75. package/esm/langs/cs_cz.js +9 -1
  76. package/esm/langs/de.d.ts +7 -0
  77. package/esm/langs/de.js +8 -1
  78. package/esm/langs/es.d.ts +7 -0
  79. package/esm/langs/es.js +8 -1
  80. package/esm/langs/fa.d.ts +7 -0
  81. package/esm/langs/fa.js +8 -1
  82. package/esm/langs/fi.d.ts +7 -0
  83. package/esm/langs/fi.js +8 -1
  84. package/esm/langs/fr.d.ts +7 -0
  85. package/esm/langs/fr.js +8 -1
  86. package/esm/langs/he.d.ts +7 -0
  87. package/esm/langs/he.js +8 -1
  88. package/esm/langs/hu.d.ts +7 -0
  89. package/esm/langs/hu.js +8 -1
  90. package/esm/langs/id.d.ts +7 -0
  91. package/esm/langs/id.js +8 -1
  92. package/esm/langs/it.d.ts +7 -0
  93. package/esm/langs/it.js +8 -1
  94. package/esm/langs/ja.d.ts +7 -0
  95. package/esm/langs/ja.js +8 -1
  96. package/esm/langs/ko.d.ts +7 -0
  97. package/esm/langs/ko.js +8 -1
  98. package/esm/langs/mn.d.ts +7 -0
  99. package/esm/langs/mn.js +8 -1
  100. package/esm/langs/nl.d.ts +7 -0
  101. package/esm/langs/nl.js +8 -1
  102. package/esm/langs/no.d.ts +7 -0
  103. package/esm/langs/no.js +8 -1
  104. package/esm/langs/pl.d.ts +7 -0
  105. package/esm/langs/pl.js +8 -1
  106. package/esm/langs/pt_br.d.ts +7 -0
  107. package/esm/langs/pt_br.js +8 -1
  108. package/esm/langs/ru.d.ts +7 -0
  109. package/esm/langs/ru.js +8 -1
  110. package/esm/langs/sk.d.ts +7 -0
  111. package/esm/langs/sk.js +8 -1
  112. package/esm/langs/sv.d.ts +7 -0
  113. package/esm/langs/sv.js +8 -1
  114. package/esm/langs/tr.d.ts +7 -0
  115. package/esm/langs/tr.js +8 -1
  116. package/esm/langs/ua.d.ts +7 -0
  117. package/esm/langs/ua.js +8 -1
  118. package/esm/langs/zh_cn.d.ts +7 -0
  119. package/esm/langs/zh_cn.js +8 -1
  120. package/esm/langs/zh_tw.d.ts +7 -0
  121. package/esm/langs/zh_tw.js +8 -1
  122. package/esm/modules/history/history.d.ts +17 -2
  123. package/esm/modules/history/history.js +41 -19
  124. package/esm/modules/table/table.js +7 -2
  125. package/esm/modules/widget/file-selector/file-selector.js +2 -2
  126. package/esm/plugins/all.d.ts +1 -0
  127. package/esm/plugins/all.js +1 -0
  128. package/esm/plugins/backspace/backspace.js +4 -0
  129. package/esm/plugins/clean-html/clean-html.d.ts +7 -0
  130. package/esm/plugins/clean-html/clean-html.js +17 -0
  131. package/esm/plugins/clean-html/helpers/visitor/filters/replace-old-tags.js +26 -1
  132. package/esm/plugins/dtd/before-insert/check-block-nesting.d.ts +2 -1
  133. package/esm/plugins/dtd/before-insert/check-block-nesting.js +26 -5
  134. package/esm/plugins/enter/helpers/move-cursor-out-from-specal-tags.js +8 -0
  135. package/esm/plugins/focus/focus.js +3 -0
  136. package/esm/plugins/hr/hr.js +21 -5
  137. package/esm/plugins/iframe/config.js +8 -3
  138. package/esm/plugins/image-processor/image-processor.d.ts +4 -1
  139. package/esm/plugins/image-processor/image-processor.js +9 -3
  140. package/esm/plugins/image-properties/readers/align.d.ts +2 -2
  141. package/esm/plugins/index.d.ts +1 -0
  142. package/esm/plugins/inline-popup/config/items/iframe.d.ts +9 -1
  143. package/esm/plugins/inline-popup/config/items/iframe.js +15 -0
  144. package/esm/plugins/inline-popup/inline-popup.d.ts +5 -0
  145. package/esm/plugins/inline-popup/inline-popup.js +26 -2
  146. package/esm/plugins/link/link.js +5 -2
  147. package/esm/plugins/media/config.d.ts +2 -1
  148. package/esm/plugins/media/media.js +37 -9
  149. package/esm/plugins/paste/helpers.d.ts +8 -0
  150. package/esm/plugins/paste/helpers.js +18 -1
  151. package/esm/plugins/paste/paste.js +2 -2
  152. package/esm/plugins/resizer/config.d.ts +5 -3
  153. package/esm/plugins/resizer/config.js +8 -2
  154. package/esm/plugins/resizer/resizer.d.ts +10 -0
  155. package/esm/plugins/resizer/resizer.js +72 -18
  156. package/esm/plugins/select-cells/select-cells.d.ts +10 -0
  157. package/esm/plugins/select-cells/select-cells.js +54 -6
  158. package/esm/plugins/table/config.js +51 -1
  159. package/esm/plugins/table-keyboard-navigation/table-keyboard-navigation.js +1 -1
  160. package/esm/plugins/video/config.d.ts +6 -0
  161. package/esm/plugins/video/config.js +78 -82
  162. package/esm/plugins/video-properties/interface.d.ts +31 -0
  163. package/esm/plugins/video-properties/interface.js +6 -0
  164. package/esm/plugins/video-properties/readers.d.ts +26 -0
  165. package/esm/plugins/video-properties/readers.js +65 -0
  166. package/esm/plugins/video-properties/replace.d.ts +15 -0
  167. package/esm/plugins/video-properties/replace.js +97 -0
  168. package/esm/plugins/video-properties/ui/ui-video-form.d.ts +41 -0
  169. package/esm/plugins/video-properties/ui/ui-video-form.js +228 -0
  170. package/esm/plugins/video-properties/video-properties.d.ts +37 -0
  171. package/esm/plugins/video-properties/video-properties.js +175 -0
  172. package/esm/plugins/video-properties/writers.d.ts +15 -0
  173. package/esm/plugins/video-properties/writers.js +73 -0
  174. package/esm/types/types.d.ts +1 -1
  175. package/package.json +1 -1
  176. package/types/config.d.ts +29 -28
  177. package/types/core/helpers/utils/align.d.ts +1 -1
  178. package/types/core/helpers/utils/index.d.ts +1 -0
  179. package/types/core/helpers/utils/prepare-media.d.ts +19 -0
  180. package/types/langs/ar.d.ts +7 -0
  181. package/types/langs/az.d.ts +7 -0
  182. package/types/langs/cs_cz.d.ts +7 -0
  183. package/types/langs/de.d.ts +7 -0
  184. package/types/langs/es.d.ts +7 -0
  185. package/types/langs/fa.d.ts +7 -0
  186. package/types/langs/fi.d.ts +7 -0
  187. package/types/langs/fr.d.ts +7 -0
  188. package/types/langs/he.d.ts +7 -0
  189. package/types/langs/hu.d.ts +7 -0
  190. package/types/langs/id.d.ts +7 -0
  191. package/types/langs/it.d.ts +7 -0
  192. package/types/langs/ja.d.ts +7 -0
  193. package/types/langs/ko.d.ts +7 -0
  194. package/types/langs/mn.d.ts +7 -0
  195. package/types/langs/nl.d.ts +7 -0
  196. package/types/langs/no.d.ts +7 -0
  197. package/types/langs/pl.d.ts +7 -0
  198. package/types/langs/pt_br.d.ts +7 -0
  199. package/types/langs/ru.d.ts +7 -0
  200. package/types/langs/sk.d.ts +7 -0
  201. package/types/langs/sv.d.ts +7 -0
  202. package/types/langs/tr.d.ts +7 -0
  203. package/types/langs/ua.d.ts +7 -0
  204. package/types/langs/zh_cn.d.ts +7 -0
  205. package/types/langs/zh_tw.d.ts +7 -0
  206. package/types/modules/history/history.d.ts +17 -2
  207. package/types/plugins/clean-html/clean-html.d.ts +7 -0
  208. package/types/plugins/dtd/before-insert/check-block-nesting.d.ts +2 -1
  209. package/types/plugins/image-processor/image-processor.d.ts +4 -1
  210. package/types/plugins/image-properties/readers/align.d.ts +2 -2
  211. package/types/plugins/index.d.ts +1 -0
  212. package/types/plugins/inline-popup/config/items/iframe.d.ts +9 -1
  213. package/types/plugins/inline-popup/inline-popup.d.ts +5 -0
  214. package/types/plugins/media/config.d.ts +2 -1
  215. package/types/plugins/paste/helpers.d.ts +8 -0
  216. package/types/plugins/resizer/config.d.ts +5 -3
  217. package/types/plugins/resizer/resizer.d.ts +10 -0
  218. package/types/plugins/select-cells/select-cells.d.ts +10 -0
  219. package/types/plugins/video/config.d.ts +6 -0
  220. package/types/plugins/video-properties/interface.d.ts +31 -0
  221. package/types/plugins/video-properties/readers.d.ts +26 -0
  222. package/types/plugins/video-properties/replace.d.ts +15 -0
  223. package/types/plugins/video-properties/ui/ui-video-form.d.ts +41 -0
  224. package/types/plugins/video-properties/video-properties.d.ts +37 -0
  225. package/types/plugins/video-properties/writers.d.ts +15 -0
  226. package/types/types/types.d.ts +1 -1
@@ -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";
@@ -26,6 +26,7 @@ export function media(editor) {
26
26
  (_a = getMediaWrapper(editor, element)) !== null && _a !== void 0 ? _a : wrapMedia(editor, element);
27
27
  }
28
28
  else if (mediaInFakeBlock) {
29
+ const media = element;
29
30
  const wrapper = editor.createInside.element(mediaFakeTag, {
30
31
  'data-jodit-temp': 1,
31
32
  contenteditable: false,
@@ -33,13 +34,21 @@ export function media(editor) {
33
34
  [`data-${keyFake}`]: 1
34
35
  });
35
36
  attr(wrapper, 'style', attr(element, 'style'));
36
- css(wrapper, {
37
- display: cssInline(element, 'display') === 'inline-block'
38
- ? 'inline-block'
39
- : 'block',
40
- width: element.offsetWidth,
41
- height: element.offsetHeight
42
- });
37
+ // The wrapper fits the media, whose size changes as its metadata loads
38
+ // and as it's resized. A percentage width is of the line, so it takes that
39
+ const fit = () => {
40
+ css(wrapper, {
41
+ display: cssInline(media, 'display') === 'inline-block'
42
+ ? 'inline-block'
43
+ : 'block',
44
+ width: cssInline(media, 'width').endsWith('%')
45
+ ? ''
46
+ : 'fit-content',
47
+ height: ''
48
+ });
49
+ };
50
+ fit();
51
+ editor.e.on(media, 'changesize', fit);
43
52
  if (element.parentNode) {
44
53
  Dom.before(element, wrapper);
45
54
  }
@@ -49,12 +58,31 @@ export function media(editor) {
49
58
  else {
50
59
  return;
51
60
  }
61
+ // Selected, as an image is, so Delete and Backspace remove it
52
62
  editor.e
53
63
  .off(element, 'mousedown.select touchstart.select')
54
64
  .on(element, 'mousedown.select touchstart.select', () => {
55
- editor.s.setCursorAfter(element);
65
+ editor.s.select(element);
56
66
  });
57
67
  };
68
+ // Media in a wrapper kept in the content let clicks through to it, as
69
+ // jodit-media covers its media, so a click selects the media rather than
70
+ // playing it. An inline wrapper holds it as an inline-block, unless its
71
+ // classes or style give it a display of their own
72
+ const wrappers = Object.values(mediaWrappers);
73
+ if (wrappers.length) {
74
+ editor.e.on('afterInit', () => {
75
+ const selectors = (list) => [...new Set(list)].join(',');
76
+ const scoped = ({ tag, className }) => `.jodit-wysiwyg ${tag}.${className}`;
77
+ const inline = selectors(wrappers
78
+ .filter(({ tag }) => consts.IS_INLINE.test(tag))
79
+ .map(scoped));
80
+ const media = selectors(wrappers.flatMap(wrapper => mediaBlocks.map(tag => `${scoped(wrapper)}>${tag}`)));
81
+ getContainer(editor, 'media-wrappers', 'style', true).innerHTML =
82
+ (inline ? `:where(${inline}){display:inline-block}` : '') +
83
+ `${media}{pointer-events:none}`;
84
+ });
85
+ }
58
86
  if (mediaInFakeBlock || Object.keys(mediaWrappers).length) {
59
87
  editor.e
60
88
  .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);
@@ -10,7 +10,7 @@ import type { HTMLTagNames } from "../../types/index";
10
10
  declare module 'jodit/config' {
11
11
  interface Config {
12
12
  /**
13
- * Set of HTML tag names whose elements can be resized by the user via drag handles (e.g. images, iframes, tables)
13
+ * Set of HTML tag names whose elements can be resized by the user via drag handles (e.g. images, iframes, tables, videos)
14
14
  */
15
15
  allowResizeTags: Set<HTMLTagNames>;
16
16
  resizer: {
@@ -23,14 +23,16 @@ declare module 'jodit/config' {
23
23
  * Save width and height proportions when resizing
24
24
  * ```js
25
25
  * Jodit.make('#editor', {
26
- * allowResizeTags: ['img', 'iframe', 'table', 'jodit'],
26
+ * allowResizeTags: new Set(['img', 'iframe', 'table', 'jodit', 'video']),
27
27
  * resizer: {
28
28
  * useAspectRatio: false, // don't save,
29
- * useAspectRatio: ['img'], // save only for images (default value)
29
+ * useAspectRatio: new Set(['img']), // save only for images
30
30
  * useAspectRatio: true // save for all
31
31
  * }
32
32
  * });
33
33
  * ```
34
+ * Images and videos keep it by default. A video with no height of its
35
+ * own keeps it by being given only a width.
34
36
  */
35
37
  useAspectRatio: boolean | Set<HTMLTagNames>;
36
38
  /**
@@ -4,12 +4,18 @@
4
4
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
5
  */
6
6
  import { Config } from "../../config.js";
7
- Config.prototype.allowResizeTags = new Set(['img', 'iframe', 'table', 'jodit']);
7
+ Config.prototype.allowResizeTags = new Set([
8
+ 'img',
9
+ 'iframe',
10
+ 'table',
11
+ 'jodit',
12
+ 'video'
13
+ ]);
8
14
  Config.prototype.resizer = {
9
15
  showSize: true,
10
16
  hideSizeTimeout: 1000,
11
17
  forImageChangeAttributes: true,
12
18
  min_width: 10,
13
19
  min_height: 10,
14
- useAspectRatio: new Set(['img'])
20
+ useAspectRatio: new Set(['img', 'video'])
15
21
  };
@@ -26,6 +26,7 @@ export declare class resizer extends Plugin {
26
26
  private height;
27
27
  private ratio;
28
28
  private rect;
29
+ private layer;
29
30
  private sizeViewer;
30
31
  /** @override */
31
32
  protected afterInit(editor: IJodit): void;
@@ -33,6 +34,11 @@ export declare class resizer extends Plugin {
33
34
  * Click in the editor area
34
35
  */
35
36
  protected onEditorClick(e: MouseEvent): void;
37
+ /**
38
+ * What a click on `node` resizes: the media in it when it's a media
39
+ * wrapper (`jodit-media`, or one from `mediaWrappers`), or `node` itself
40
+ */
41
+ private __resizable;
36
42
  protected __afterInsertImage(image: HTMLImageElement): void;
37
43
  private addEventListeners;
38
44
  private onStartResizing;
@@ -46,6 +52,10 @@ export declare class resizer extends Plugin {
46
52
  private onClickOutside;
47
53
  private getWorkplacePosition;
48
54
  private applySize;
55
+ /**
56
+ * The framed element can go with a wrapper round it, keeping its parent
57
+ */
58
+ private get isElementInEditor();
49
59
  private onDelete;
50
60
  private __onChangeEditor;
51
61
  /**
@@ -18,7 +18,7 @@ import { IS_ES_NEXT, IS_IE, KEY_ALT } from "../../core/constants.js";
18
18
  import { autobind, watch } from "../../core/decorators/index.js";
19
19
  import { Dom } from "../../core/dom/dom.js";
20
20
  import { eventEmitter, pluginSystem } from "../../core/global.js";
21
- import { $$, attr, css, dataBind, innerWidth, markOwner, offset } from "../../core/helpers/index.js";
21
+ import { $$, attr, css, dataBind, getWrappedMedia, innerWidth, markOwner, offset } from "../../core/helpers/index.js";
22
22
  import { cssInline } from "../../core/helpers/utils/css.js";
23
23
  import { Plugin } from "../../core/plugin/plugin.js";
24
24
  import "./config.js";
@@ -36,6 +36,15 @@ function attrPixelSize(element, key) {
36
36
  }
37
37
  return parseFloat(value);
38
38
  }
39
+ /**
40
+ * A video with no height of its own takes it from its width and its ratio,
41
+ * so resizing it keeps the ratio without setting a height.
42
+ */
43
+ function heightFollowsWidth(element) {
44
+ return (Dom.isTag(element, 'video') &&
45
+ !attr(element, 'height') &&
46
+ !cssInline(element, 'height'));
47
+ }
39
48
  /**
40
49
  * The module creates a supporting frame for resizing of the elements img and table
41
50
  */
@@ -58,6 +67,8 @@ export class resizer extends Plugin {
58
67
  <div class="jodit-resizer__bottom-left"></div>
59
68
  <span>100x100</span>
60
69
  </div>`);
70
+ // Clips the frame to the workplace, without the frame making it scroll
71
+ this.layer = this.j.c.div('jodit-resizer-layer');
61
72
  this.sizeViewer = this.rect.getElementsByTagName('span')[0];
62
73
  this.pointerX = 0;
63
74
  this.pointerY = 0;
@@ -72,6 +83,10 @@ export class resizer extends Plugin {
72
83
  if (Dom.isTag(this.element, 'img') && !this.element.complete) {
73
84
  this.j.e.one(this.element, 'load', this.updateSize);
74
85
  }
86
+ if (Dom.isTag(this.element, 'video') &&
87
+ this.element.readyState < HTMLMediaElement.HAVE_METADATA) {
88
+ this.j.e.one(this.element, 'loadedmetadata', this.updateSize);
89
+ }
75
90
  }
76
91
  };
77
92
  this.updateSize = () => {
@@ -135,14 +150,33 @@ export class resizer extends Plugin {
135
150
  let node = e.target;
136
151
  const { editor, options: { allowResizeTags } } = this.j;
137
152
  while (node && node !== editor) {
138
- if (Dom.isTag(node, allowResizeTags)) {
139
- this.__bind(node);
140
- this.onClickElement(node);
153
+ const element = this.__resizable(node);
154
+ if (Dom.isTag(element, allowResizeTags)) {
155
+ this.__bind(element);
156
+ this.onClickElement(element);
141
157
  return;
142
158
  }
143
159
  node = node.parentNode;
144
160
  }
145
161
  }
162
+ /**
163
+ * What a click on `node` resizes: the media in it when it's a media
164
+ * wrapper (`jodit-media`, or one from `mediaWrappers`), or `node` itself
165
+ */
166
+ __resizable(node) {
167
+ var _a;
168
+ const media = (_a = getWrappedMedia(this.j, node)) !== null && _a !== void 0 ? _a : (Dom.isHTMLElement(node) && attr(node, 'data-jodit_fake_wrapper')
169
+ ? node.firstElementChild
170
+ : null);
171
+ if (!media) {
172
+ return node;
173
+ }
174
+ // An embed is resized through its own wrapper
175
+ return Dom.isHTMLElement(media.parentNode) &&
176
+ attr(media.parentNode, 'data-jodit_iframe_wrapper')
177
+ ? media.parentNode
178
+ : media;
179
+ }
146
180
  __afterInsertImage(image) {
147
181
  if (this.j.o.resizer.forImageChangeAttributes) {
148
182
  return;
@@ -160,7 +194,8 @@ export class resizer extends Plugin {
160
194
  .off(editor.ow, '.resizer')
161
195
  .on(editor.editor, 'keydown.resizer', (e) => {
162
196
  if (this.isShown &&
163
- e.key === consts.KEY_DELETE &&
197
+ (e.key === consts.KEY_DELETE ||
198
+ e.key === consts.KEY_BACKSPACE) &&
164
199
  this.element &&
165
200
  !Dom.isTag(this.element, 'table')) {
166
201
  this.onDelete(e);
@@ -168,17 +203,15 @@ export class resizer extends Plugin {
168
203
  })
169
204
  .on(editor.ow, 'resize.resizer', this.updateSize)
170
205
  .on('resize.resizer', this.updateSize)
171
- .on([editor.ow, editor.editor], 'scroll.resizer', () => {
172
- if (this.isShown && !this.isResizeMode) {
173
- this.hide();
174
- }
175
- })
206
+ // The frame is outside the content, so it's moved to follow the
207
+ // element as the content scrolls
208
+ .on([editor.ow, editor.editor], 'scroll.resizer', this.updateSize)
176
209
  .on(editor.ow, 'keydown.resizer', this.onKeyDown)
177
210
  .on(editor.ow, 'keyup.resizer', this.onKeyUp)
178
211
  .on(editor.ow, 'mouseup.resizer touchend.resizer', this.onClickOutside);
179
212
  }
180
213
  onStartResizing(resizeHandle, e) {
181
- if (!this.element || !this.element.parentNode) {
214
+ if (!this.element || !this.isElementInEditor) {
182
215
  this.hide();
183
216
  return false;
184
217
  }
@@ -232,8 +265,9 @@ export class resizer extends Plugin {
232
265
  const className = this.handle.className;
233
266
  let new_w, new_h;
234
267
  const uar = this.j.o.resizer.useAspectRatio;
235
- if (!this.isAltMode &&
236
- (uar === true || (uar && Dom.isTag(this.element, uar)))) {
268
+ const keepRatio = !this.isAltMode &&
269
+ (uar === true || Boolean(uar && Dom.isTag(this.element, uar)));
270
+ if (keepRatio) {
237
271
  if (diff_x) {
238
272
  new_w =
239
273
  this.width +
@@ -265,7 +299,8 @@ export class resizer extends Plugin {
265
299
  this.applySize(this.element, 'width', '100%');
266
300
  }
267
301
  }
268
- if (new_h > this.j.o.resizer.min_height) {
302
+ if (new_h > this.j.o.resizer.min_height &&
303
+ !(keepRatio && heightFollowsWidth(this.element))) {
269
304
  this.applySize(this.element, 'height', new_h);
270
305
  }
271
306
  this.updateSize();
@@ -311,10 +346,20 @@ export class resizer extends Plugin {
311
346
  css(element, key, value);
312
347
  }
313
348
  }
349
+ /**
350
+ * The framed element can go with a wrapper round it, keeping its parent
351
+ */
352
+ get isElementInEditor() {
353
+ return Boolean(this.element && this.j.editor.contains(this.element));
354
+ }
314
355
  onDelete(e) {
315
356
  if (!this.element) {
316
357
  return;
317
358
  }
359
+ if (!this.isElementInEditor) {
360
+ this.hide();
361
+ return;
362
+ }
318
363
  if (this.element.tagName !== 'JODIT') {
319
364
  this.j.s.select(this.element);
320
365
  }
@@ -326,7 +371,7 @@ export class resizer extends Plugin {
326
371
  }
327
372
  __onChangeEditor() {
328
373
  if (this.isShown) {
329
- if (!this.element || !this.element.parentNode) {
374
+ if (!this.isElementInEditor) {
330
375
  this.hide();
331
376
  }
332
377
  else {
@@ -396,8 +441,12 @@ export class resizer extends Plugin {
396
441
  })
397
442
  .off(element, 'changesize')
398
443
  .on(element, 'changesize', () => {
399
- attr(iframe, 'width', element.offsetWidth + 'px');
400
- attr(iframe, 'height', element.offsetHeight + 'px');
444
+ // Only a resize sets the embed's size, and as the plain numbers
445
+ // the attributes take, so a click leaves `width="100%"` alone
446
+ if (this.isResizeMode) {
447
+ attr(iframe, 'width', element.offsetWidth);
448
+ attr(iframe, 'height', element.offsetHeight);
449
+ }
401
450
  });
402
451
  }
403
452
  this.j.e.on(element, 'dragstart', this.hide);
@@ -434,9 +483,13 @@ export class resizer extends Plugin {
434
483
  return;
435
484
  }
436
485
  this.isShown = true;
486
+ if (this.layer.parentNode !== this.j.workplace) {
487
+ markOwner(this.j, this.layer);
488
+ Dom.append(this.j.workplace, this.layer);
489
+ }
437
490
  if (!this.rect.parentNode) {
438
491
  markOwner(this.j, this.rect);
439
- Dom.append(this.j.workplace, this.rect);
492
+ Dom.append(this.layer, this.rect);
440
493
  }
441
494
  if (this.j.isFullSize) {
442
495
  css(this.rect, 'zIndex', css(this.j.container, 'zIndex').toString());
@@ -457,6 +510,7 @@ export class resizer extends Plugin {
457
510
  }
458
511
  beforeDestruct(jodit) {
459
512
  this.hide();
513
+ Dom.safeRemove(this.layer);
460
514
  eventEmitter.off('hideHelpers', this.hide);
461
515
  jodit.e.off(this.j.ow, '.resizer').off('.resizer');
462
516
  }
@@ -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
  }