jodit 4.17.6 → 4.18.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 (102) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/es2015/jodit.css +87 -3
  3. package/es2015/jodit.fat.min.css +1 -1
  4. package/es2015/jodit.fat.min.js +8 -8
  5. package/es2015/jodit.js +808 -132
  6. package/es2015/jodit.min.css +1 -1
  7. package/es2015/jodit.min.js +16 -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 +8 -8
  16. package/es2018/jodit.min.css +1 -1
  17. package/es2018/jodit.min.js +8 -8
  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 +87 -3
  21. package/es2021/jodit.fat.min.css +1 -1
  22. package/es2021/jodit.fat.min.js +14 -14
  23. package/es2021/jodit.js +788 -132
  24. package/es2021/jodit.min.css +1 -1
  25. package/es2021/jodit.min.js +14 -14
  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 +87 -3
  33. package/es2021.en/jodit.fat.min.css +1 -1
  34. package/es2021.en/jodit.fat.min.js +26 -26
  35. package/es2021.en/jodit.js +788 -132
  36. package/es2021.en/jodit.min.css +1 -1
  37. package/es2021.en/jodit.min.js +12 -12
  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 +92 -6
  45. package/es5/jodit.fat.min.css +1 -1
  46. package/es5/jodit.fat.min.js +2 -2
  47. package/es5/jodit.js +1073 -192
  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 +65 -2
  60. package/esm/config.js +29 -2
  61. package/esm/core/constants.js +1 -1
  62. package/esm/core/helpers/utils/align.d.ts +11 -5
  63. package/esm/core/helpers/utils/align.js +28 -8
  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/media-wrapper.d.ts +29 -0
  67. package/esm/core/helpers/utils/media-wrapper.js +63 -0
  68. package/esm/core/selection/selection.js +3 -3
  69. package/esm/modules/toolbar/button/button.d.ts +18 -0
  70. package/esm/modules/toolbar/button/button.js +75 -5
  71. package/esm/plugins/dtd/after-insert/remove-extra-br.js +6 -1
  72. package/esm/plugins/dtd/before-insert/check-block-nesting.js +16 -1
  73. package/esm/plugins/dtd/config.js +3 -0
  74. package/esm/plugins/font/config.d.ts +12 -0
  75. package/esm/plugins/font/config.js +25 -10
  76. package/esm/plugins/font/font-picker.d.ts +51 -0
  77. package/esm/plugins/font/font-picker.js +238 -0
  78. package/esm/plugins/font/font.js +12 -0
  79. package/esm/plugins/font/google-fonts.d.ts +65 -0
  80. package/esm/plugins/font/google-fonts.js +143 -0
  81. package/esm/plugins/image-properties/readers/index.js +2 -1
  82. package/esm/plugins/image-properties/writers/index.js +2 -2
  83. package/esm/plugins/inline-popup/config/items/img.js +10 -8
  84. package/esm/plugins/inline-popup/inline-popup.js +13 -6
  85. package/esm/plugins/media/config.d.ts +26 -1
  86. package/esm/plugins/media/config.js +1 -3
  87. package/esm/plugins/media/media.js +12 -4
  88. package/esm/plugins/video/config.js +24 -4
  89. package/esm/types/toolbar.d.ts +2 -0
  90. package/esm/types/types.d.ts +13 -1
  91. package/package.json +1 -1
  92. package/types/config.d.ts +65 -2
  93. package/types/core/helpers/utils/align.d.ts +11 -5
  94. package/types/core/helpers/utils/index.d.ts +1 -0
  95. package/types/core/helpers/utils/media-wrapper.d.ts +29 -0
  96. package/types/modules/toolbar/button/button.d.ts +18 -0
  97. package/types/plugins/font/config.d.ts +12 -0
  98. package/types/plugins/font/font-picker.d.ts +51 -0
  99. package/types/plugins/font/google-fonts.d.ts +65 -0
  100. package/types/plugins/media/config.d.ts +26 -1
  101. package/types/types/toolbar.d.ts +2 -0
  102. package/types/types/types.d.ts +13 -1
@@ -0,0 +1,143 @@
1
+ /*!
2
+ * Jodit Editor (https://xdsoft.net/jodit/)
3
+ * Released under MIT see LICENSE.txt in the project root for license information.
4
+ * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
+ */
6
+ import { ViewComponent } from "../../core/component/view-component.js";
7
+ import { Dom } from "../../core/dom/dom.js";
8
+ import { Ajax } from "../../core/request/ajax.js";
9
+ const CSS_URL = 'https://fonts.googleapis.com/css2';
10
+ // `cursive` and `fantasy` stand in badly for a handwriting or display font
11
+ const GENERIC_FAMILIES = {
12
+ serif: 'serif',
13
+ monospace: 'monospace'
14
+ };
15
+ const libraries = new Map();
16
+ /**
17
+ * The fonts of the library at `url`, fetched once for every editor on the page
18
+ */
19
+ function library(jodit, url) {
20
+ let fonts = libraries.get(url);
21
+ if (!fonts) {
22
+ const ajax = new Ajax({ url, method: 'GET' }, jodit.o.defaultAjaxOptions);
23
+ fonts = ajax
24
+ .send()
25
+ .then(response => response.json())
26
+ // A font without a regular style can't be asked for by its name alone
27
+ .then(({ items }) => items.filter(font => font.variants.includes('regular')))
28
+ .catch(() => {
29
+ libraries.delete(url);
30
+ return [];
31
+ })
32
+ .finally(() => ajax.destruct());
33
+ libraries.set(url, fonts);
34
+ }
35
+ return fonts;
36
+ }
37
+ /**
38
+ * The first family of a `font-family` value, unquoted and in lower case for
39
+ * comparing
40
+ */
41
+ export function firstFamily(fontFamily) {
42
+ return fontFamily.split(',')[0].replace(/["']/g, '').trim().toLowerCase();
43
+ }
44
+ /**
45
+ * Loads fonts from Google Fonts: the whole of each font the content uses into
46
+ * the editor's document, and just the letters of its name for the font list
47
+ */
48
+ export class GoogleFonts extends ViewComponent {
49
+ className() {
50
+ return 'GoogleFonts';
51
+ }
52
+ constructor(jodit) {
53
+ super(jodit);
54
+ /**
55
+ * The library by lower-case family, empty until it arrives
56
+ */
57
+ this.fonts = new Map();
58
+ this.__links = [];
59
+ this.__loaded = new WeakMap();
60
+ this.__previewed = new Set();
61
+ this.ready = library(jodit, jodit.o.font.googleFonts.url).then(fonts => {
62
+ fonts.forEach(font => this.fonts.set(font.family.toLowerCase(), font));
63
+ });
64
+ }
65
+ /**
66
+ * The library's font named first in a `font-family` value
67
+ */
68
+ find(fontFamily) {
69
+ return this.fonts.get(firstFamily(fontFamily));
70
+ }
71
+ /**
72
+ * The `font-family` value that applies a font
73
+ */
74
+ value(font) {
75
+ var _a;
76
+ return `'${font.family}', ${(_a = GENERIC_FAMILIES[font.category]) !== null && _a !== void 0 ? _a : 'sans-serif'}`;
77
+ }
78
+ /**
79
+ * Loads the regular, italic, bold and bold italic styles that a font has
80
+ * into the editor's document
81
+ */
82
+ load(font) {
83
+ const loaded = this.__loadedInto(this.j.ed);
84
+ if (loaded.has(font.family)) {
85
+ return;
86
+ }
87
+ loaded.add(font.family);
88
+ const styles = [
89
+ ['regular', '0,400'],
90
+ ['700', '0,700'],
91
+ ['italic', '1,400'],
92
+ ['700italic', '1,700']
93
+ ]
94
+ .filter(([variant]) => font.variants.includes(variant))
95
+ .map(([, style]) => style);
96
+ this.__append(this.j.ed, `family=${this.__family(font)}:ital,wght@${styles.join(';')}`);
97
+ }
98
+ /**
99
+ * Loads every font of the library that the content uses
100
+ */
101
+ loadUsed() {
102
+ this.j.editor
103
+ .querySelectorAll('[style*="font-family"]')
104
+ .forEach(element => {
105
+ const font = this.find(element.style.fontFamily);
106
+ font && this.load(font);
107
+ });
108
+ }
109
+ /**
110
+ * Loads into the page just the letters of each font's name, so the font
111
+ * list can show the name in its font
112
+ */
113
+ preview(fonts) {
114
+ const loaded = this.__loadedInto(this.j.od);
115
+ fonts = fonts.filter(font => !this.__previewed.has(font.family) && !loaded.has(font.family));
116
+ if (!fonts.length) {
117
+ return;
118
+ }
119
+ fonts.forEach(font => this.__previewed.add(font.family));
120
+ const families = fonts.map(font => `family=${this.__family(font)}`), letters = new Set(fonts.map(font => font.family).join(''));
121
+ this.__append(this.j.od, `${families.join('&')}&text=${encodeURIComponent([...letters].join(''))}`);
122
+ }
123
+ __loadedInto(doc) {
124
+ var _a;
125
+ const loaded = (_a = this.__loaded.get(doc)) !== null && _a !== void 0 ? _a : new Set();
126
+ this.__loaded.set(doc, loaded);
127
+ return loaded;
128
+ }
129
+ __family(font) {
130
+ return encodeURIComponent(font.family).replace(/%20/g, '+');
131
+ }
132
+ __append(doc, query) {
133
+ const link = doc.createElement('link');
134
+ link.rel = 'stylesheet';
135
+ link.href = `${CSS_URL}?${query}&display=swap`;
136
+ doc.head.appendChild(link);
137
+ this.__links.push(link);
138
+ }
139
+ destruct() {
140
+ this.__links.forEach(link => Dom.safeRemove(link));
141
+ super.destruct();
142
+ }
143
+ }
@@ -3,6 +3,7 @@
3
3
  * Released under MIT see LICENSE.txt in the project root for license information.
4
4
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
5
  */
6
+ import { getAlignClasses } from "../../../core/helpers/utils/align.js";
6
7
  import { attr } from "../../../core/helpers/utils/attr.js";
7
8
  import { cssInline } from "../../../core/helpers/utils/css.js";
8
9
  import { readAlign } from "./align.js";
@@ -15,7 +16,7 @@ import { readSizes } from "./size.js";
15
16
  */
16
17
  export async function readValuesFromImage(j, state) {
17
18
  const { sourceImage: image, values } = state;
18
- readAlign(image, values, j.o.imageAlignClasses);
19
+ readAlign(image, values, getAlignClasses(j, image));
19
20
  // Border radius
20
21
  values.borderRadius =
21
22
  parseInt(cssInline(image, 'border-radius') || '0', 10) || 0;
@@ -4,7 +4,7 @@
4
4
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
5
5
  */
6
6
  import { Dom } from "../../../core/dom/dom.js";
7
- import { attr, hAlignElement } from "../../../core/helpers/utils/index.js";
7
+ import { attr, getAlignClasses, hAlignElement } from "../../../core/helpers/utils/index.js";
8
8
  import { css } from "../../../core/helpers/utils/css.js";
9
9
  import { applyLink } from "./link.js";
10
10
  import { applyMargin } from "./margin.js";
@@ -49,6 +49,6 @@ export function applyValuesToImage(j, state, image) {
49
49
  attr(image, 'id', id || null);
50
50
  }
51
51
  if (opt.image.editAlign) {
52
- hAlignElement(image, align, j.o.imageAlignClasses);
52
+ hAlignElement(image, align, getAlignClasses(j, image));
53
53
  }
54
54
  }
@@ -5,11 +5,9 @@
5
5
  */
6
6
  import { Dom } from "../../../../core/dom/index.js";
7
7
  import { isString } from "../../../../core/helpers/checker/is-string.js";
8
- import { getHAlign, hAlignElement } from "../../../../core/helpers/utils/align.js";
8
+ import { getAlignClasses, getHAlign, hAlignElement } from "../../../../core/helpers/utils/align.js";
9
9
  import { css } from "../../../../core/helpers/utils/css.js";
10
- // Images align with `imageAlignClasses`; the wrappers of iframes and media
11
- // keep inline styles
12
- const alignClasses = (editor, elm) => Dom.isTag(elm, 'img') ? editor.o.imageAlignClasses : null;
10
+ import { getMediaWrapper } from "../../../../core/helpers/utils/media-wrapper.js";
13
11
  // A Normal image sits where the text of its line is aligned (#1531)
14
12
  const lineAlign = (editor, elm) => {
15
13
  const block = (elm && Dom.closest(elm, Dom.isBlock, editor.editor)) || editor.editor;
@@ -30,7 +28,7 @@ const lineAlign = (editor, elm) => {
30
28
  return 'left';
31
29
  }
32
30
  };
33
- const currentAlign = (editor, elm) => (elm ? getHAlign(elm, alignClasses(editor, elm)) : 'normal');
31
+ const currentAlign = (editor, elm) => elm ? getHAlign(elm, getAlignClasses(editor, elm)) : 'normal';
34
32
  export const align = {
35
33
  name: 'left',
36
34
  childTemplate: (_, __, value) => value,
@@ -48,7 +46,10 @@ export const align = {
48
46
  currentAlign(editor, button.target) === value.toLowerCase());
49
47
  },
50
48
  exec: (editor, elm, { control }) => {
51
- if (!Dom.isTag(elm, new Set(['img', 'jodit', 'jodit-media']))) {
49
+ // A media wrapper kept in the content (`mediaWrappers`) is aligned itself
50
+ if (!Dom.isHTMLElement(elm) ||
51
+ (!Dom.isTag(elm, new Set(['img', 'jodit', 'jodit-media'])) &&
52
+ getMediaWrapper(editor, elm) !== elm)) {
52
53
  return;
53
54
  }
54
55
  const command = control.args && isString(control.args[0])
@@ -57,10 +58,11 @@ export const align = {
57
58
  if (!command) {
58
59
  return false;
59
60
  }
60
- hAlignElement(elm, command, alignClasses(editor, elm));
61
+ hAlignElement(elm, command, getAlignClasses(editor, elm));
61
62
  if (Dom.isTag(elm, new Set(['jodit', 'jodit-media'])) &&
62
63
  elm.firstElementChild) {
63
- hAlignElement(elm.firstElementChild, command);
64
+ const media = elm.firstElementChild;
65
+ hAlignElement(media, command, getAlignClasses(editor, media));
64
66
  }
65
67
  editor.synchronizeValues();
66
68
  editor.e.fire('recalcPositionPopup');
@@ -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, isArray, isFunction, isString, keys, position, splitArray, toArray } from "../../core/helpers/index.js";
19
+ import { camelCase, getMediaWrapper, 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";
@@ -45,11 +45,15 @@ export class inlinePopup extends Plugin {
45
45
  return makeCollection(this.jodit, this.popup);
46
46
  }
47
47
  onClick(node) {
48
- const elements = this.elmsList, target = Dom.isTag(node, 'img')
48
+ const elements = this.elmsList, mediaWrapper = getMediaWrapper(this.j, node), target = mediaWrapper !== null && mediaWrapper !== void 0 ? mediaWrapper : (Dom.isTag(node, 'img')
49
49
  ? node
50
- : Dom.closest(node, elements, this.j.editor);
51
- if (target && this.canShowPopupForType(target.nodeName.toLowerCase())) {
52
- this.showPopup(() => position(target, this.j), target.nodeName.toLowerCase(), target);
50
+ : Dom.closest(node, elements, this.j.editor));
51
+ // A media wrapper kept in the content gets the media popup
52
+ const type = mediaWrapper
53
+ ? 'jodit-media'
54
+ : target === null || target === void 0 ? void 0 : target.nodeName.toLowerCase();
55
+ if (target && type && this.canShowPopupForType(type)) {
56
+ this.showPopup(() => position(target, this.j), type, target);
53
57
  return false;
54
58
  }
55
59
  }
@@ -263,7 +267,10 @@ export class inlinePopup extends Plugin {
263
267
  this.removeListenersForElements();
264
268
  }
265
269
  _eventsList() {
266
- const el = this.elmsList;
270
+ var _a;
271
+ // Clicks on a media wrapper kept in the content (`mediaWrappers`)
272
+ const wrapperTags = Object.values((_a = this.j.o.mediaWrappers) !== null && _a !== void 0 ? _a : {}).map(({ tag }) => tag.toLowerCase());
273
+ const el = [...new Set(this.elmsList.concat(wrapperTags))];
267
274
  return el
268
275
  .map(e => camelCase(`click_${e}`))
269
276
  .concat(el.map(e => camelCase(`touchstart_${e}`)))
@@ -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/media
8
+ */
9
+ import type { IDictionary, MediaWrapper } from "../../types/index";
6
10
  declare module 'jodit/config' {
7
11
  interface Config {
8
12
  /**
@@ -17,6 +21,27 @@ declare module 'jodit/config' {
17
21
  * Media tags
18
22
  */
19
23
  mediaBlocks: string[];
24
+ /**
25
+ * Wrappers that media keep in the content, by the media's tag name, such as
26
+ * Froala's `<span class="fr-video">` around video, audio and embeds. An
27
+ * element with a wrapper for its tag:
28
+ * - is wrapped in it instead of the editor-only `mediaFakeTag` when its tag
29
+ * is one of `mediaBlocks`, or left in it when it's already in one
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
32
+ * - opens the media popup for its wrapper when clicked, whose Align and
33
+ * Delete act on the wrapper
34
+ *
35
+ * A wrapper is given `contenteditable="false"` and `draggable="true"`,
36
+ * which stay in the value.
37
+ * ```javascript
38
+ * const frVideo = { tag: 'span', className: 'fr-video' };
39
+ *
40
+ * Jodit.make('#editor', {
41
+ * mediaWrappers: { video: frVideo, audio: frVideo, iframe: frVideo }
42
+ * });
43
+ * ```
44
+ */
45
+ mediaWrappers: IDictionary<MediaWrapper>;
20
46
  }
21
47
  }
22
- export {};
@@ -3,10 +3,8 @@
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/media
8
- */
9
6
  import { Config } from "../../config.js";
10
7
  Config.prototype.mediaFakeTag = 'jodit-media';
11
8
  Config.prototype.mediaInFakeBlock = true;
12
9
  Config.prototype.mediaBlocks = ['video', 'audio'];
10
+ Config.prototype.mediaWrappers = {};
@@ -6,7 +6,7 @@
6
6
  import * as consts from "../../core/constants.js";
7
7
  import { Dom } from "../../core/dom/dom.js";
8
8
  import { pluginSystem } from "../../core/global.js";
9
- import { $$, attr, dataBind } from "../../core/helpers/utils/index.js";
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";
12
12
  /**
@@ -14,13 +14,18 @@ import "./config.js";
14
14
  */
15
15
  export function media(editor) {
16
16
  const keyFake = 'jodit_fake_wrapper';
17
- const { mediaFakeTag, mediaBlocks, mediaInFakeBlock } = editor.options;
17
+ const { mediaFakeTag, mediaBlocks, mediaInFakeBlock, mediaWrappers } = editor.options;
18
18
  const wrap = (element) => {
19
+ var _a;
19
20
  if (element.parentNode &&
20
21
  attr(element.parentNode, 'data-jodit_iframe_wrapper')) {
21
22
  element = element.parentNode;
22
23
  }
23
- else {
24
+ else if (mediaWrappers[element.nodeName.toLowerCase()]) {
25
+ element =
26
+ (_a = getMediaWrapper(editor, element)) !== null && _a !== void 0 ? _a : wrapMedia(editor, element);
27
+ }
28
+ else if (mediaInFakeBlock) {
24
29
  const wrapper = editor.createInside.element(mediaFakeTag, {
25
30
  'data-jodit-temp': 1,
26
31
  contenteditable: false,
@@ -41,13 +46,16 @@ export function media(editor) {
41
46
  Dom.append(wrapper, element);
42
47
  element = wrapper;
43
48
  }
49
+ else {
50
+ return;
51
+ }
44
52
  editor.e
45
53
  .off(element, 'mousedown.select touchstart.select')
46
54
  .on(element, 'mousedown.select touchstart.select', () => {
47
55
  editor.s.setCursorAfter(element);
48
56
  });
49
57
  };
50
- if (mediaInFakeBlock) {
58
+ if (mediaInFakeBlock || Object.keys(mediaWrappers).length) {
51
59
  editor.e
52
60
  .on('afterGetValueFromEditor', (data) => {
53
61
  const rxp = new RegExp(`<${mediaFakeTag}[^>]+data-${keyFake}[^>]+>([^]+?)</${mediaFakeTag}>`, 'ig');
@@ -3,22 +3,42 @@
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 } from "../../core/helpers/index.js";
6
+ import { call, convertMediaUrlToVideoEmbed, getAlignClasses, hAlignElement, wrapMedia } 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
+ }
13
23
  function insertVideos(data) {
14
24
  var _a;
15
25
  (_a = data.files) === null || _a === void 0 ? void 0 : _a.forEach(file => {
16
- this.s.insertNode(this.createInside.element('video', {
26
+ this.s.insertNode(prepareMedia(this, this.createInside.element('video', {
17
27
  controls: '',
18
28
  src: data.baseurl + file
19
- }));
29
+ })));
20
30
  });
21
31
  }
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
+ }
22
42
  Config.prototype.video = {
23
43
  parseUrlToVideoEmbed: convertMediaUrlToVideoEmbed,
24
44
  defaultWidth: 400,
@@ -54,7 +74,7 @@ Config.prototype.controls.video = {
54
74
  ])
55
75
  ]), tabs = [], insertCode = (code) => {
56
76
  jodit.s.restore();
57
- jodit.s.insertHTML(code);
77
+ jodit.s.insertHTML(prepareEmbedCode(jodit, code));
58
78
  close();
59
79
  };
60
80
  jodit.s.save();
@@ -193,6 +193,8 @@ interface IControlType<T extends IViewBased = IJodit | IViewBased | IFileBrowser
193
193
  childTemplate?: (jodit: T, key: string, value: string, button: IToolbarButton) => string;
194
194
  /**
195
195
  * After click on the button it will show popup element which consist value that this function returned
196
+ * For a control with a `list`, the returned content replaces the list's buttons in its dropdown,
197
+ * and `false` keeps them
196
198
  * @example
197
199
  * ```javascript
198
200
  * var editor = Jodit.make('.editor', {
@@ -187,10 +187,22 @@ export interface ImageEditorOptions {
187
187
  export type InsertMode = 'insert_as_html' | 'insert_as_text' | 'insert_clear_html' | 'insert_only_text';
188
188
  export type ImageAction = 'resize' | 'crop';
189
189
  export type ImageHAlign = 'normal' | 'left' | 'right' | 'center' | '';
190
+ /**
191
+ * Class names for each alignment, see `alignClasses`. An alignment can list
192
+ * several sets of class names to recognise; aligning writes the first
193
+ */
194
+ export type HAlignClasses = Partial<Record<Exclude<ImageHAlign, ''>, string | string[]>>;
190
195
  /**
191
196
  * Class names for each image alignment, see `imageAlignClasses`
192
197
  */
193
- export type ImageAlignClasses = Partial<Record<Exclude<ImageHAlign, ''>, string>>;
198
+ export type ImageAlignClasses = HAlignClasses;
199
+ /**
200
+ * A wrapper that media keep in the content, see `mediaWrappers`
201
+ */
202
+ export interface MediaWrapper {
203
+ tag: string;
204
+ className: string;
205
+ }
194
206
  export interface ImageBox {
195
207
  w: number;
196
208
  h: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jodit",
3
- "version": "4.17.6",
3
+ "version": "4.18.1",
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
@@ -15,6 +15,7 @@ import type { IControlType } from "./types/index";
15
15
  import type { formTemplate } from "./plugins/link/template";
16
16
  import type { IUIForm } from "./types/index";
17
17
  import type { IUIOption } from "./types/index";
18
+ import type { MediaWrapper } from "./types/index";
18
19
  import type { ButtonsOption } from "./types/index";
19
20
  import type { InsertMode } from "./types/index";
20
21
  import type { FuzzySearch } from "./types/index";
@@ -22,6 +23,7 @@ import type { ISourceEditor } from "./types/index";
22
23
  import type { ISpeechRecognizeConstructor } from "./plugins/speech-recognize/interface";
23
24
  import type { Attributes } from "./types/index";
24
25
  import type { Controls } from "./types/index";
26
+ import type { HAlignClasses } from "./types/index";
25
27
  import type { IAsyncStorageOptions } from "./types/index";
26
28
  import type { IExtraPlugin } from "./types/index";
27
29
  import type { ImageAlignClasses } from "./types/index";
@@ -642,19 +644,46 @@ declare class Config implements IViewOptions {
642
644
  /**
643
645
  * Class names to align images with, instead of an inline `float` or
644
646
  * `margin`. Aligning an image removes every class listed here and adds the
645
- * chosen alignment's, which can be several separated by spaces.
647
+ * chosen alignment's, which can be several separated by spaces. An
648
+ * alignment can also list several such sets, to recognise each of them;
649
+ * aligning writes the first. `alignClasses.img` takes its place when set.
646
650
  *
647
651
  * ```javascript
648
652
  * Jodit.make('#editor', {
649
653
  * imageAlignClasses: {
650
654
  * left: 'align-left',
651
655
  * right: 'align-right',
652
- * center: 'align-center'
656
+ * center: ['align-center', 'centered']
653
657
  * }
654
658
  * });
655
659
  * ```
656
660
  */
657
661
  imageAlignClasses: Nullable<ImageAlignClasses>;
662
+ /**
663
+ * Class names to align elements with, by tag name, as `imageAlignClasses`
664
+ * does for images: `img`, and `video`, `audio` and `iframe` in the media
665
+ * popup. A media wrapper (see `mediaWrappers`), and Jodit's own `jodit` and
666
+ * `jodit-media` wrappers, are aligned with the classes for the media in
667
+ * them.
668
+ *
669
+ * ```javascript
670
+ * const media = {
671
+ * left: 'media-left',
672
+ * right: 'media-right',
673
+ * center: 'media-center'
674
+ * };
675
+ *
676
+ * Jodit.make('#editor', {
677
+ * alignClasses: { video: media, iframe: media }
678
+ * });
679
+ * ```
680
+ */
681
+ alignClasses: IDictionary<HAlignClasses>;
682
+ /**
683
+ * Alignment given to the media the video popup inserts. `'normal'` leaves
684
+ * them unaligned
685
+ */
686
+ mediaDefaultAlign: ImageHAlign;
658
687
  /**
659
688
  * Do not display these buttons that are on the list
660
689
  *
@@ -1293,6 +1322,18 @@ interface Config {
1293
1322
  }
1294
1323
  interface Config {
1295
1324
  defaultFontSizePoints: 'px' | 'pt';
1325
+ font: {
1326
+ googleFonts: {
1327
+ /**
1328
+ * Adds the Google Fonts library to the font family dropdown, which then
1329
+ * also lists the theme's fonts and the fonts the content uses. The
1330
+ * address answers with the JSON of the Google Fonts Developer API:
1331
+ * the API itself (`https://www.googleapis.com/webfonts/v1/webfonts?key=…`)
1332
+ * or a proxy of it. Empty leaves the dropdown as it is
1333
+ */
1334
+ url: string;
1335
+ };
1336
+ };
1296
1337
  }
1297
1338
  interface Config {
1298
1339
  /**
@@ -1549,6 +1590,28 @@ interface Config {
1549
1590
  * Media tags
1550
1591
  */
1551
1592
  mediaBlocks: string[];
1593
+ /**
1594
+ * Wrappers that media keep in the content, by the media's tag name, such as
1595
+ * Froala's `<span class="fr-video">` around video, audio and embeds. An
1596
+ * element with a wrapper for its tag:
1597
+ * - is wrapped in it instead of the editor-only `mediaFakeTag` when its tag
1598
+ * is one of `mediaBlocks`, or left in it when it's already in one
1599
+ * - 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
+ * - opens the media popup for its wrapper when clicked, whose Align and
1602
+ * Delete act on the wrapper
1603
+ *
1604
+ * A wrapper is given `contenteditable="false"` and `draggable="true"`,
1605
+ * which stay in the value.
1606
+ * ```javascript
1607
+ * const frVideo = { tag: 'span', className: 'fr-video' };
1608
+ *
1609
+ * Jodit.make('#editor', {
1610
+ * mediaWrappers: { video: frVideo, audio: frVideo, iframe: frVideo }
1611
+ * });
1612
+ * ```
1613
+ */
1614
+ mediaWrappers: IDictionary<MediaWrapper>;
1552
1615
  }
1553
1616
  interface Config {
1554
1617
  /**
@@ -6,16 +6,22 @@
6
6
  /**
7
7
  * @module helpers/utils
8
8
  */
9
- import type { ImageAlignClasses, ImageHAlign, Nullable } from "../../../types/index";
9
+ import type { HAlignClasses, IJodit, ImageHAlign, Nullable } from "../../../types/index";
10
10
  /**
11
- * Align image, with `classes` (see `imageAlignClasses`) instead of inline
12
- * styles when they are given
11
+ * The class names to align `elm` with: `alignClasses` for its tag, or for the
12
+ * media in it when it wraps media (a `mediaWrappers` wrapper, `jodit` or
13
+ * `jodit-media`), falling back to `imageAlignClasses` for an image
13
14
  */
14
- export declare function hAlignElement(image: HTMLElement, align: ImageHAlign, classes?: Nullable<ImageAlignClasses>): void;
15
+ export declare function getAlignClasses(jodit: IJodit, elm: Nullable<HTMLElement>): Nullable<HAlignClasses>;
16
+ /**
17
+ * Align image, with `classes` (see `alignClasses`) instead of inline styles
18
+ * when they are given
19
+ */
20
+ export declare function hAlignElement(image: HTMLElement, align: ImageHAlign, classes?: Nullable<HAlignClasses>): void;
15
21
  /**
16
22
  * The alignment `hAlignElement` has set on an element
17
23
  */
18
- export declare function getHAlign(elm: HTMLElement, classes?: Nullable<ImageAlignClasses>): ImageHAlign;
24
+ export declare function getHAlign(elm: HTMLElement, classes?: Nullable<HAlignClasses>): ImageHAlign;
19
25
  /**
20
26
  * Remove text-align style for all selected children
21
27
  */
@@ -26,6 +26,7 @@ export * from "./get-class-name";
26
26
  export * from "./human-size-to-bytes";
27
27
  export * from "./is-unsafe-proto-key";
28
28
  export * from "./mark-deprecated";
29
+ export * from "./media-wrapper";
29
30
  export * from "./parse-query";
30
31
  export * from "./print";
31
32
  export * from "./reset";
@@ -0,0 +1,29 @@
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, Nullable } from "../../../types/index";
10
+ /**
11
+ * The media in `elm` when `elm` is a wrapper media keep in the content (see
12
+ * `mediaWrappers`): an element of a type `elm` is the wrapper for
13
+ */
14
+ export declare function getWrappedMedia(jodit: IJodit, elm: Nullable<Node>): Nullable<HTMLElement>;
15
+ /**
16
+ * The wrapper media keep in the content (see `mediaWrappers`) that is or
17
+ * holds `node`
18
+ */
19
+ export declare function getMediaWrapper(jodit: IJodit, node: Nullable<Node>): Nullable<HTMLElement>;
20
+ /**
21
+ * An empty wrapper for media of type `tag` to keep in the content (see
22
+ * `mediaWrappers`), or null when there is none for it
23
+ */
24
+ export declare function createMediaWrapper(jodit: IJodit, tag: string): Nullable<HTMLElement>;
25
+ /**
26
+ * Puts `element` in the wrapper for its type (see `mediaWrappers`), and
27
+ * returns the wrapper, or `element` when there is none for it
28
+ */
29
+ export declare function wrapMedia(jodit: IJodit, element: HTMLElement): HTMLElement;