jodit 4.16.1 → 4.17.1

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 (71) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/es2015/jodit.css +1 -1
  3. package/es2015/jodit.fat.min.js +7 -7
  4. package/es2015/jodit.js +173 -34
  5. package/es2015/jodit.min.js +7 -7
  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 +7 -7
  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 +8 -8
  18. package/es2021/jodit.js +165 -33
  19. package/es2021/jodit.min.js +8 -8
  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 +165 -33
  29. package/es2021.en/jodit.min.js +8 -8
  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 +182 -34
  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 +40 -1
  51. package/esm/core/constants.js +1 -1
  52. package/esm/core/helpers/html/safe-html.d.ts +1 -1
  53. package/esm/core/helpers/html/safe-html.js +8 -1
  54. package/esm/modules/widget/file-selector/file-selector.d.ts +10 -1
  55. package/esm/modules/widget/file-selector/file-selector.js +30 -18
  56. package/esm/plugins/drag-and-drop/drag-and-drop.js +16 -4
  57. package/esm/plugins/file/file.d.ts +24 -1
  58. package/esm/plugins/file/file.js +17 -1
  59. package/esm/plugins/inline-popup/config/items/img.js +22 -1
  60. package/esm/plugins/source/editor/engines/ace.js +2 -1
  61. package/esm/plugins/source/source.d.ts +17 -0
  62. package/esm/plugins/source/source.js +60 -8
  63. package/esm/plugins/video/config.d.ts +21 -1
  64. package/esm/plugins/video/config.js +22 -2
  65. package/package.json +1 -1
  66. package/types/config.d.ts +40 -1
  67. package/types/core/helpers/html/safe-html.d.ts +1 -1
  68. package/types/modules/widget/file-selector/file-selector.d.ts +10 -1
  69. package/types/plugins/file/file.d.ts +24 -1
  70. package/types/plugins/source/source.d.ts +17 -0
  71. package/types/plugins/video/config.d.ts +21 -1
@@ -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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
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.16.1
4
+ * Version: v4.17.1
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
package/esm/config.d.ts CHANGED
@@ -8,9 +8,10 @@ import type { HTMLTagNames } from "./types/index";
8
8
  import type { AiAssistantSettings } from "./plugins/ai-assistant/interface";
9
9
  import type { IDictionary } from "./types/index";
10
10
  import type { Nullable } from "./types/index";
11
+ import type { IJodit } from "./types/index";
12
+ import type { IUploaderData } from "./types/index";
11
13
  import type { ImagePropertiesOptions } from "./plugins/image-properties/interface";
12
14
  import type { IControlType } from "./types/index";
13
- import type { IJodit } from "./types/index";
14
15
  import type { formTemplate } from "./plugins/link/template";
15
16
  import type { IUIForm } from "./types/index";
16
17
  import type { IUIOption } from "./types/index";
@@ -1264,6 +1265,27 @@ interface Config {
1264
1265
  blockLimits: IDictionary<1>;
1265
1266
  };
1266
1267
  }
1268
+ interface Config {
1269
+ file: {
1270
+ /**
1271
+ * Inserts the files uploaded from the file popup's Upload tab. By default
1272
+ * each one goes in as a link, an image as well, the way
1273
+ * `uploader.defaultHandlerSuccess` inserts a file that isn't an image.
1274
+ * ```javascript
1275
+ * Jodit.make('#editor', {
1276
+ * file: {
1277
+ * defaultHandlerSuccess(data) {
1278
+ * data.files.forEach(file => {
1279
+ * this.s.insertHTML(`<a class="file" href="${data.baseurl + file}">${file}</a>`);
1280
+ * });
1281
+ * }
1282
+ * }
1283
+ * });
1284
+ * ```
1285
+ */
1286
+ defaultHandlerSuccess?: (this: IJodit, data: IUploaderData) => void;
1287
+ };
1288
+ }
1267
1289
  interface Config {
1268
1290
  autofocus: boolean;
1269
1291
  cursorAfterAutofocus: 'start' | 'end';
@@ -1971,6 +1993,23 @@ interface Config {
1971
1993
  * Default height for video iframe. Default: 345
1972
1994
  */
1973
1995
  defaultHeight?: number;
1996
+ /**
1997
+ * Inserts the videos uploaded from the popup's Upload tab, which is shown
1998
+ * when `uploader.url` or `uploader.customUploadFunction` is set. By default
1999
+ * each one goes in as `<video controls src="…">`.
2000
+ * ```javascript
2001
+ * Jodit.make('#editor', {
2002
+ * video: {
2003
+ * defaultHandlerSuccess(data) {
2004
+ * data.files.forEach(file => {
2005
+ * this.s.insertHTML(`<video controls preload="metadata" src="${data.baseurl + file}"></video>`);
2006
+ * });
2007
+ * }
2008
+ * }
2009
+ * });
2010
+ * ```
2011
+ */
2012
+ defaultHandlerSuccess?: (this: IJodit, data: IUploaderData) => void;
1974
2013
  };
1975
2014
  }
1976
2015
  interface Config {
@@ -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.16.1";
6
+ export const APP_VERSION = "4.17.1";
7
7
  // prettier-ignore
8
8
  export const ES = "es2020";
9
9
  export const IS_ES_MODERN = true;
@@ -13,4 +13,4 @@ export type safeOptions = {
13
13
  * Removes dangerous constructs from HTML
14
14
  */
15
15
  export declare function safeHTML(box: HTMLElement | DocumentFragment, options: safeOptions): void;
16
- export declare function sanitizeHTMLElement(elm: Element | DocumentFragment, { safeJavaScriptLink, removeOnError }?: safeOptions): boolean;
16
+ export declare function sanitizeHTMLElement(elm: Element | DocumentFragment, { safeJavaScriptLink, removeOnError, removeEventAttributes }?: safeOptions): boolean;
@@ -145,7 +145,7 @@ function isDangerousUrl(value, tagName) {
145
145
  }
146
146
  return (/^data:image\/svg/.test(normalized) && DOCUMENT_EMBED_TAGS.has(tagName));
147
147
  }
148
- export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } = {
148
+ export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError, removeEventAttributes } = {
149
149
  safeJavaScriptLink: true,
150
150
  removeOnError: true
151
151
  }) {
@@ -153,6 +153,13 @@ export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } =
153
153
  return false;
154
154
  }
155
155
  let effected = false;
156
+ // Every `on*` handler, not only `onerror`: the clean-html background
157
+ // pass calls this directly, and markup that reached the editable area
158
+ // without `safeHTML` (a drop, a script) kept its handlers otherwise.
159
+ // See GHSA-jhhp-r3r7-v2cg
160
+ if (removeEventAttributes && removeAllEventAttributes(elm)) {
161
+ effected = true;
162
+ }
156
163
  if (removeOnError && elm.hasAttribute('onerror')) {
157
164
  attr(elm, 'onerror', null);
158
165
  effected = true;
@@ -8,7 +8,8 @@
8
8
  * @packageDocumentation
9
9
  * @module modules/widget/file-selector
10
10
  */
11
- import type { IFileBrowserCallBackData, IJodit } from "../../../types/index";
11
+ import type { IFileBrowserCallBackData, IJodit, IUploaderData } from "../../../types/index";
12
+ import { type TabOption } from "../tabs/tabs";
12
13
  interface ImageSelectorCallbacks {
13
14
  /**
14
15
  * Function that will be called when the user enters the URL of the tab image and alternative text for images
@@ -34,4 +35,12 @@ interface ImageSelectorCallbacks {
34
35
  * functions with different parameters
35
36
  */
36
37
  export declare const FileSelectorWidget: (editor: IJodit, callbacks: ImageSelectorCallbacks, elm: HTMLElement | null, close: () => void, isImage?: boolean) => HTMLDivElement;
38
+ /**
39
+ * The Upload tab: a box that uploads the files dropped on it or chosen in its
40
+ * file input, then passes the uploader's answer to `onSuccess` and closes the popup
41
+ */
42
+ export declare const UploadTab: (editor: IJodit, onSuccess: (this: IJodit, data: IUploaderData) => void, { label, accept }: {
43
+ label: string;
44
+ accept: string;
45
+ }) => TabOption;
37
46
  export {};
@@ -25,28 +25,17 @@ export const FileSelectorWidget = (editor, callbacks, elm, close, isImage = true
25
25
  (options.url ||
26
26
  options.insertImageAsBase64URI ||
27
27
  options.customUploadFunction)) {
28
- const dragBox = editor.c.fromHTML('<div class="jodit-drag-and-drop__file-box">' +
29
- `<strong>${editor.i18n(isImage ? 'Drop image' : 'Drop file')}</strong>` +
30
- `<span><br>${editor.i18n('or click')}</span>` +
31
- `<input type="file" accept="${isImage ? 'image/*' : '*'}" tabindex="-1" dir="auto" multiple=""/>` +
32
- '</div>');
33
- editor.uploader.bind(dragBox, resp => {
34
- const handler = isFunction(callbacks.upload)
35
- ? callbacks.upload
28
+ const { upload } = callbacks;
29
+ tabs.push(UploadTab(editor, resp => {
30
+ const handler = isFunction(upload)
31
+ ? upload
36
32
  : options.defaultHandlerSuccess;
37
33
  if (isFunction(handler)) {
38
34
  handler.call(editor, resp);
39
35
  }
40
- editor.e.fire('closeAllPopups');
41
- }, error => {
42
- editor.message.error(error.message);
43
- editor.e.fire('closeAllPopups');
44
- });
45
- tabs.push({
46
- icon: 'upload',
47
- name: 'Upload',
48
- content: dragBox
49
- });
36
+ }, isImage
37
+ ? { label: 'Drop image', accept: 'image/*' }
38
+ : { label: 'Drop file', accept: '*' }));
50
39
  }
51
40
  if (callbacks.filebrowser) {
52
41
  if (editor.o.filebrowser.ajax.url || editor.o.filebrowser.items.url) {
@@ -115,6 +104,29 @@ export const FileSelectorWidget = (editor, callbacks, elm, close, isImage = true
115
104
  box.classList.add('jodit-file-selector');
116
105
  return box;
117
106
  };
107
+ /**
108
+ * The Upload tab: a box that uploads the files dropped on it or chosen in its
109
+ * file input, then passes the uploader's answer to `onSuccess` and closes the popup
110
+ */
111
+ export const UploadTab = (editor, onSuccess, { label, accept }) => {
112
+ const dragBox = editor.c.fromHTML('<div class="jodit-drag-and-drop__file-box">' +
113
+ `<strong>${editor.i18n(label)}</strong>` +
114
+ `<span><br>${editor.i18n('or click')}</span>` +
115
+ `<input type="file" accept="${accept}" tabindex="-1" dir="auto" multiple=""/>` +
116
+ '</div>');
117
+ editor.uploader.bind(dragBox, resp => {
118
+ onSuccess.call(editor, resp);
119
+ editor.e.fire('closeAllPopups');
120
+ }, error => {
121
+ editor.message.error(error.message);
122
+ editor.e.fire('closeAllPopups');
123
+ });
124
+ return {
125
+ icon: 'upload',
126
+ name: 'Upload',
127
+ content: dragBox
128
+ };
129
+ };
118
130
  function val(elm, name, value) {
119
131
  const child = Dom.first(elm, node => Dom.isTag(node, 'input') && attr(node, 'name') === name);
120
132
  if (!child) {
@@ -38,7 +38,13 @@ export class dragAndDrop extends Plugin {
38
38
  }
39
39
  /** @override */
40
40
  afterInit() {
41
- this.j.e.on([window, this.j.ed, this.j.editor], 'dragstart.DragAndDrop', this.onDragStart);
41
+ this.j.e
42
+ .on([window, this.j.ed, this.j.editor], 'dragstart.DragAndDrop', this.onDragStart)
43
+ // Always, not only after a `dragstart` seen in this window: a drop
44
+ // from another window or application never produces one here, and
45
+ // the browser then inserted its HTML natively, unsanitized. Routed
46
+ // through the paste plugin instead. See GHSA-jhhp-r3r7-v2cg
47
+ .on('drop.DragAndDrop', this.onDrop);
42
48
  }
43
49
  onDragStart(event) {
44
50
  let target = event.target;
@@ -69,13 +75,11 @@ export class dragAndDrop extends Plugin {
69
75
  addDragListeners() {
70
76
  this.j.e
71
77
  .on('dragover', this.onDrag)
72
- .on('drop.DragAndDrop', this.onDrop)
73
78
  .on(window, 'dragend.DragAndDrop drop.DragAndDrop mouseup.DragAndDrop', this.onDragEnd);
74
79
  }
75
80
  removeDragListeners() {
76
81
  this.j.e
77
82
  .off('dragover', this.onDrag)
78
- .off('drop.DragAndDrop', this.onDrop)
79
83
  .off(window, 'dragend.DragAndDrop drop.DragAndDrop mouseup.DragAndDrop', this.onDragEnd);
80
84
  }
81
85
  onDrag(event) {
@@ -144,7 +148,15 @@ export class dragAndDrop extends Plugin {
144
148
  }
145
149
  }
146
150
  else if (this.getText(event)) {
147
- fragment = this.j.createInside.fromHTML(this.getText(event));
151
+ // Dropped markup takes the same sanitizing pass as pasted and
152
+ // assigned content. See GHSA-jhhp-r3r7-v2cg
153
+ const box = this.j.createInside.div();
154
+ box.innerHTML = this.getText(event);
155
+ this.j.e.fire('safeHTML', box);
156
+ fragment =
157
+ box.firstChild !== box.lastChild || !box.firstChild
158
+ ? box
159
+ : box.firstChild;
148
160
  }
149
161
  return fragment;
150
162
  }
@@ -8,5 +8,28 @@
8
8
  * @packageDocumentation
9
9
  * @module plugins/file
10
10
  */
11
- import type { IJodit } from "../../types/index";
11
+ import type { IJodit, IUploaderData } from "../../types/index";
12
+ declare module 'jodit/config' {
13
+ interface Config {
14
+ file: {
15
+ /**
16
+ * Inserts the files uploaded from the file popup's Upload tab. By default
17
+ * each one goes in as a link, an image as well, the way
18
+ * `uploader.defaultHandlerSuccess` inserts a file that isn't an image.
19
+ * ```javascript
20
+ * Jodit.make('#editor', {
21
+ * file: {
22
+ * defaultHandlerSuccess(data) {
23
+ * data.files.forEach(file => {
24
+ * this.s.insertHTML(`<a class="file" href="${data.baseurl + file}">${file}</a>`);
25
+ * });
26
+ * }
27
+ * }
28
+ * });
29
+ * ```
30
+ */
31
+ defaultHandlerSuccess?: (this: IJodit, data: IUploaderData) => void;
32
+ };
33
+ }
34
+ }
12
35
  export declare function file(editor: IJodit): void;
@@ -8,6 +8,19 @@ import { pluginSystem } from "../../core/global.js";
8
8
  import { attr } from "../../core/helpers/utils/attr.js";
9
9
  import { Config } from "../../config.js";
10
10
  import { FileSelectorWidget } from "../../modules/widget/index.js";
11
+ function insertLinks(data) {
12
+ var _a;
13
+ (_a = data.files) === null || _a === void 0 ? void 0 : _a.forEach(file => {
14
+ const link = this.createInside.element('a', {
15
+ href: data.baseurl + file
16
+ });
17
+ link.textContent = this.o.uploader.getDisplayName.call(this.uploader, data.baseurl, file);
18
+ this.s.insertNode(link);
19
+ });
20
+ }
21
+ Config.prototype.file = {
22
+ defaultHandlerSuccess: insertLinks
23
+ };
11
24
  Config.prototype.controls.file = {
12
25
  popup: (editor, current, close) => {
13
26
  const insert = (url, title = '') => {
@@ -27,7 +40,10 @@ Config.prototype.controls.file = {
27
40
  data.files.forEach(file => insert(data.baseurl + file));
28
41
  close();
29
42
  },
30
- upload: true,
43
+ upload: (data) => {
44
+ var _a, _b;
45
+ ((_b = (_a = editor.o.file) === null || _a === void 0 ? void 0 : _a.defaultHandlerSuccess) !== null && _b !== void 0 ? _b : insertLinks).call(editor, data);
46
+ },
31
47
  url: (url, text) => {
32
48
  if (sourceAnchor) {
33
49
  attr(sourceAnchor, 'href', url);
@@ -10,6 +10,26 @@ import { css } from "../../../../core/helpers/utils/css.js";
10
10
  // Images align with `imageAlignClasses`; the wrappers of iframes and media
11
11
  // keep inline styles
12
12
  const alignClasses = (editor, elm) => Dom.isTag(elm, 'img') ? editor.o.imageAlignClasses : null;
13
+ // A Normal image sits where the text of its line is aligned (#1531)
14
+ const lineAlign = (editor, elm) => {
15
+ const block = (elm && Dom.closest(elm, Dom.isBlock, editor.editor)) || editor.editor;
16
+ const rtl = css(block, 'direction') === 'rtl';
17
+ switch (css(block, 'text-align')) {
18
+ case 'center':
19
+ case '-webkit-center':
20
+ return 'center';
21
+ case 'justify':
22
+ return 'justify';
23
+ case 'right':
24
+ return 'right';
25
+ case 'end':
26
+ return rtl ? 'left' : 'right';
27
+ case 'start':
28
+ return rtl ? 'right' : 'left';
29
+ default:
30
+ return 'left';
31
+ }
32
+ };
13
33
  const currentAlign = (editor, elm) => (elm ? getHAlign(elm, alignClasses(editor, elm)) : 'normal');
14
34
  export const align = {
15
35
  name: 'left',
@@ -17,7 +37,8 @@ export const align = {
17
37
  list: ['Left', 'Right', 'Center', 'Normal'],
18
38
  update: (editor, button) => {
19
39
  const current = currentAlign(editor, button.target);
20
- button.state.icon.name = current === 'normal' ? 'left' : current;
40
+ button.state.icon.name =
41
+ current === 'normal' ? lineAlign(editor, button.target) : current;
21
42
  },
22
43
  isActive: (editor, button) => currentAlign(editor, button.target) !== 'normal',
23
44
  isChildActive: (editor, button) => {
@@ -146,7 +146,8 @@ export class AceEditor extends SourceEditor {
146
146
  }
147
147
  return;
148
148
  }
149
- this.fromWYSIWYG();
149
+ // The source plugin carries the fallback editor's text over when this
150
+ // one is ready; refilling from WYSIWYG here would drop unsynced typing
150
151
  tryInitAceEditor();
151
152
  };
152
153
  editor.e.on('afterSetMode', initAceEditor);
@@ -21,6 +21,11 @@ export declare class source extends Plugin {
21
21
  private mirrorContainer;
22
22
  private __lock;
23
23
  private __oldMirrorValue;
24
+ /**
25
+ * What the source view was last filled with from WYSIWYG, read back from
26
+ * the editor; while the view still holds it, nothing has been typed there
27
+ */
28
+ private __filledValue;
24
29
  private tempMarkerStart;
25
30
  private tempMarkerStartReg;
26
31
  private tempMarkerEnd;
@@ -50,6 +55,18 @@ export declare class source extends Plugin {
50
55
  afterInit(editor: IJodit): void;
51
56
  private syncValueFromWYSIWYG;
52
57
  private initSourceEditor;
58
+ /**
59
+ * Replaces the source editor (the textarea shown while ACE loads) with
60
+ * the one that just became ready, carrying its text, selection and focus
61
+ * over. Filling the new editor from WYSIWYG instead would drop what was
62
+ * typed since the last, debounced, sync and normalise half-typed markup.
63
+ */
64
+ private takeOver;
65
+ /**
66
+ * The beautifier arrived after the source view was opened: format the
67
+ * view if nothing has been typed into it, otherwise leave the typing alone
68
+ */
69
+ private beautifyUntouched;
53
70
  /** @override */
54
71
  beforeDestruct(): void;
55
72
  }
@@ -37,6 +37,11 @@ export class source extends Plugin {
37
37
  ];
38
38
  this.__lock = false;
39
39
  this.__oldMirrorValue = '';
40
+ /**
41
+ * What the source view was last filled with from WYSIWYG, read back from
42
+ * the editor; while the view still holds it, nothing has been typed there
43
+ */
44
+ this.__filledValue = '';
40
45
  this.tempMarkerStart = '{start-jodit-selection}';
41
46
  this.tempMarkerStartReg = /{start-jodit-selection}/g;
42
47
  this.tempMarkerEnd = '{end-jodit-selection}';
@@ -69,6 +74,7 @@ export class source extends Plugin {
69
74
  if (new_value !== this.getMirrorValue()) {
70
75
  this.setMirrorValue(new_value);
71
76
  }
77
+ this.__filledValue = this.getMirrorValue();
72
78
  this.__lock = false;
73
79
  }
74
80
  }
@@ -271,9 +277,9 @@ export class source extends Plugin {
271
277
  return;
272
278
  }
273
279
  loading = true;
274
- // It formats the source view from the next time it opens:
275
- // filling it again now would replace what was typed there
276
- loadNext(editor, editor.o.beautifyHTMLCDNUrlsJS).then(addEventListener, () => null);
280
+ loadNext(editor, editor.o.beautifyHTMLCDNUrlsJS).then(() => {
281
+ addEventListener() && this.beautifyUntouched();
282
+ }, () => null);
277
283
  };
278
284
  editor.e.on('afterSetMode.source', loadBeautify);
279
285
  loadBeautify();
@@ -294,11 +300,9 @@ export class source extends Plugin {
294
300
  if (editor.o.sourceEditor !== 'area') {
295
301
  const sourceEditor = createSourceEditor(editor.o.sourceEditor, editor, this.mirrorContainer, this.toWYSIWYG, this.fromWYSIWYG);
296
302
  sourceEditor.onReadyAlways(() => {
297
- var _a, _b;
298
- (_a = this.sourceEditor) === null || _a === void 0 ? void 0 : _a.destruct();
299
- this.sourceEditor = sourceEditor;
300
- this.syncValueFromWYSIWYG(true);
301
- (_b = editor.events) === null || _b === void 0 ? void 0 : _b.fire('sourceEditorReady', editor);
303
+ var _a;
304
+ this.takeOver(sourceEditor);
305
+ (_a = editor.events) === null || _a === void 0 ? void 0 : _a.fire('sourceEditorReady', editor);
302
306
  });
303
307
  }
304
308
  else {
@@ -309,6 +313,54 @@ export class source extends Plugin {
309
313
  });
310
314
  }
311
315
  }
316
+ /**
317
+ * Replaces the source editor (the textarea shown while ACE loads) with
318
+ * the one that just became ready, carrying its text, selection and focus
319
+ * over. Filling the new editor from WYSIWYG instead would drop what was
320
+ * typed since the last, debounced, sync and normalise half-typed markup.
321
+ */
322
+ takeOver(sourceEditor) {
323
+ const previous = this.sourceEditor;
324
+ if (!previous || previous === sourceEditor) {
325
+ this.sourceEditor = sourceEditor;
326
+ this.syncValueFromWYSIWYG(true);
327
+ return;
328
+ }
329
+ const value = previous.getValue(), start = previous.getSelectionStart(), end = previous.getSelectionEnd(), focused = previous.isFocused, untouched = value === this.__filledValue;
330
+ previous.destruct();
331
+ this.sourceEditor = sourceEditor;
332
+ if (untouched) {
333
+ this.syncValueFromWYSIWYG(true);
334
+ }
335
+ else {
336
+ sourceEditor.setValue(value);
337
+ this.toWYSIWYG();
338
+ }
339
+ if (start >= 0) {
340
+ sourceEditor.setSelectionRange(start, end);
341
+ }
342
+ focused && sourceEditor.focus();
343
+ }
344
+ /**
345
+ * The beautifier arrived after the source view was opened: format the
346
+ * view if nothing has been typed into it, otherwise leave the typing alone
347
+ */
348
+ beautifyUntouched() {
349
+ var _a, _b;
350
+ const value = this.getMirrorValue();
351
+ if (this.j.o.editHTMLDocumentMode ||
352
+ !value ||
353
+ value !== this.__filledValue) {
354
+ return;
355
+ }
356
+ const html = this.j.e.fire('beautifyHTML', value);
357
+ if (isString(html) && html !== value) {
358
+ const focused = (_a = this.sourceEditor) === null || _a === void 0 ? void 0 : _a.isFocused;
359
+ this.setMirrorValue(html);
360
+ this.__filledValue = this.getMirrorValue();
361
+ focused && ((_b = this.sourceEditor) === null || _b === void 0 ? void 0 : _b.focus());
362
+ }
363
+ }
312
364
  /** @override */
313
365
  beforeDestruct() {
314
366
  if (this.sourceEditor) {
@@ -3,6 +3,10 @@
3
3
  * Released under MIT see LICENSE.txt in the project root for license information.
4
4
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
5
  */
6
+ /**
7
+ * @module plugins/video
8
+ */
9
+ import type { IJodit, IUploaderData } from "../../types/index";
6
10
  declare module 'jodit/config' {
7
11
  interface Config {
8
12
  video: {
@@ -29,7 +33,23 @@ declare module 'jodit/config' {
29
33
  * Default height for video iframe. Default: 345
30
34
  */
31
35
  defaultHeight?: number;
36
+ /**
37
+ * Inserts the videos uploaded from the popup's Upload tab, which is shown
38
+ * when `uploader.url` or `uploader.customUploadFunction` is set. By default
39
+ * each one goes in as `<video controls src="…">`.
40
+ * ```javascript
41
+ * Jodit.make('#editor', {
42
+ * video: {
43
+ * defaultHandlerSuccess(data) {
44
+ * data.files.forEach(file => {
45
+ * this.s.insertHTML(`<video controls preload="metadata" src="${data.baseurl + file}"></video>`);
46
+ * });
47
+ * }
48
+ * }
49
+ * });
50
+ * ```
51
+ */
52
+ defaultHandlerSuccess?: (this: IJodit, data: IUploaderData) => void;
32
53
  };
33
54
  }
34
55
  }
35
- export {};
@@ -8,12 +8,22 @@ 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
- import { TabsWidget } from "../../modules/widget/index.js";
11
+ import { TabsWidget, UploadTab } from "../../modules/widget/index.js";
12
12
  import videoIcon from "./video.svg.js";
13
+ function insertVideos(data) {
14
+ var _a;
15
+ (_a = data.files) === null || _a === void 0 ? void 0 : _a.forEach(file => {
16
+ this.s.insertNode(this.createInside.element('video', {
17
+ controls: '',
18
+ src: data.baseurl + file
19
+ }));
20
+ });
21
+ }
13
22
  Config.prototype.video = {
14
23
  parseUrlToVideoEmbed: convertMediaUrlToVideoEmbed,
15
24
  defaultWidth: 400,
16
- defaultHeight: 345
25
+ defaultHeight: 345,
26
+ defaultHandlerSuccess: insertVideos
17
27
  };
18
28
  Icon.set('video', videoIcon);
19
29
  Config.prototype.controls.video = {
@@ -48,6 +58,16 @@ Config.prototype.controls.video = {
48
58
  close();
49
59
  };
50
60
  jodit.s.save();
61
+ const { uploader } = jodit.o;
62
+ // Base64 alone would put whole video files into the content
63
+ if (uploader.showTabInFileSelector !== false &&
64
+ (uploader.url || uploader.customUploadFunction)) {
65
+ tabs.push(UploadTab(jodit, data => {
66
+ var _a, _b;
67
+ jodit.s.restore();
68
+ ((_b = (_a = jodit.o.video) === null || _a === void 0 ? void 0 : _a.defaultHandlerSuccess) !== null && _b !== void 0 ? _b : insertVideos).call(jodit, data);
69
+ }, { label: 'Drop file', accept: 'video/*' }));
70
+ }
51
71
  tabs.push({
52
72
  icon: 'link',
53
73
  name: 'Link',