jodit 4.12.17 → 4.12.20

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 (74) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/es2015/jodit.css +1 -1
  3. package/es2015/jodit.fat.min.js +7 -7
  4. package/es2015/jodit.js +338 -79
  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 +9 -9
  18. package/es2021/jodit.js +335 -79
  19. package/es2021/jodit.min.js +9 -9
  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 +10 -10
  28. package/es2021.en/jodit.js +335 -79
  29. package/es2021.en/jodit.min.js +9 -9
  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 +367 -85
  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/core/constants.js +1 -1
  51. package/esm/core/helpers/html/apply-styles.js +11 -0
  52. package/esm/core/helpers/html/clean-from-word.js +9 -0
  53. package/esm/core/helpers/html/safe-html.js +71 -19
  54. package/esm/core/helpers/html/strip-tags.d.ts +1 -1
  55. package/esm/core/helpers/html/strip-tags.js +7 -3
  56. package/esm/core/helpers/utils/config-proto.js +15 -0
  57. package/esm/core/helpers/utils/convert-media-url-to-video-embed.js +41 -19
  58. package/esm/jodit.js +20 -0
  59. package/esm/modules/uploader/config.js +11 -1
  60. package/esm/plugins/clean-html/helpers/visitor/filters/try-remove-node.js +8 -1
  61. package/esm/plugins/color/config.js +12 -3
  62. package/esm/plugins/drag-and-drop-element/drag-and-drop-element.d.ts +21 -0
  63. package/esm/plugins/drag-and-drop-element/drag-and-drop-element.js +48 -3
  64. package/esm/plugins/enter/enter.js +11 -6
  65. package/esm/plugins/hotkeys/config.js +1 -1
  66. package/esm/plugins/indent/config.js +20 -6
  67. package/esm/plugins/paste/paste.js +6 -1
  68. package/esm/plugins/paste-from-word/paste-from-word.js +1 -1
  69. package/esm/plugins/select/select.d.ts +8 -0
  70. package/esm/plugins/select/select.js +37 -0
  71. package/package.json +1 -1
  72. package/types/core/helpers/html/strip-tags.d.ts +1 -1
  73. package/types/plugins/drag-and-drop-element/drag-and-drop-element.d.ts +21 -0
  74. package/types/plugins/select/select.d.ts +8 -0
@@ -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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
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.12.17
4
+ * Version: v4.12.20
5
5
  * Url: https://xdsoft.net/jodit/
6
6
  * License(s): MIT
7
7
  */
@@ -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.12.17";
6
+ export const APP_VERSION = "4.12.20";
7
7
  // prettier-ignore
8
8
  export const ES = "es2020";
9
9
  export const IS_ES_MODERN = true;
@@ -47,6 +47,17 @@ export function applyStyles(html) {
47
47
  iframeDoc.open();
48
48
  iframeDoc.write(html);
49
49
  iframeDoc.close();
50
+ // Word marks its auto-generated list markers (the literal
51
+ // bullet/number, e.g. `1.` or `·`) with `mso-list:Ignore`.
52
+ // They are display-only and must not be imported, otherwise
53
+ // the marker text leaks into the content. Drop them before any
54
+ // style normalization strips the `mso-list` hint. See #948
55
+ Dom.each(iframeDoc.body, (node) => {
56
+ if (Dom.isElement(node) &&
57
+ /mso-list:\s*ignore/i.test(node.getAttribute('style') || '')) {
58
+ Dom.safeRemove(node);
59
+ }
60
+ });
50
61
  try {
51
62
  for (let i = 0; i < iframeDoc.styleSheets.length; i += 1) {
52
63
  const rules = iframeDoc.styleSheets[i].cssRules;
@@ -43,6 +43,15 @@ export function cleanFromWord(html) {
43
43
  Dom.unwrap(node);
44
44
  break;
45
45
  default:
46
+ // Word marks its auto-generated list markers
47
+ // (the literal bullet/number, e.g. `1.` or `·`)
48
+ // with `mso-list:Ignore`. They are display-only
49
+ // and must not be imported, otherwise the marker
50
+ // text leaks into the content. See #948
51
+ if (/mso-list:\s*ignore/i.test(node.getAttribute('style') || '')) {
52
+ marks.push(node);
53
+ break;
54
+ }
46
55
  toArray(node.attributes).forEach((attr) => {
47
56
  if ([
48
57
  'src',
@@ -7,7 +7,7 @@
7
7
  * @module helpers/html
8
8
  */
9
9
  import { Dom } from "../../dom/dom.js";
10
- import { $$, attr } from "../utils/index.js";
10
+ import { attr } from "../utils/index.js";
11
11
  /**
12
12
  * Removes dangerous constructs from HTML
13
13
  */
@@ -17,21 +17,22 @@ export function safeHTML(box, options) {
17
17
  return;
18
18
  }
19
19
  const removeEvents = (_a = options.removeEventAttributes) !== null && _a !== void 0 ? _a : options.removeOnError;
20
- if (removeEvents) {
21
- removeAllEventAttributes(box);
22
- $$('*', box).forEach(elm => removeAllEventAttributes(elm));
23
- }
24
- else if (options.removeOnError) {
25
- sanitizeHTMLElement(box, options);
26
- $$('[onerror]', box).forEach(elm => sanitizeHTMLElement(elm, options));
27
- }
28
- if (options.safeJavaScriptLink) {
29
- sanitizeHTMLElement(box, options);
30
- $$('a[href^="javascript"]', box).forEach(elm => sanitizeHTMLElement(elm, options));
31
- }
32
- if (options.safeLinksTarget) {
33
- $$('a[target="_blank"]', box).forEach(elm => {
34
- const rel = elm.getAttribute('rel') || '';
20
+ // Single synchronous traversal of the subtree. Besides removing event
21
+ // handlers and `javascript:` links, `sanitizeHTMLElement` neutralises
22
+ // executable `iframe[srcdoc]`, `data:text/html` / SVG `data:` document
23
+ // sources and dangerous schemes in every URL-bearing attribute.
24
+ const process = (node) => {
25
+ if (!Dom.isElement(node)) {
26
+ return;
27
+ }
28
+ if (removeEvents) {
29
+ removeAllEventAttributes(node);
30
+ }
31
+ sanitizeHTMLElement(node, options);
32
+ if (options.safeLinksTarget &&
33
+ node.nodeName === 'A' &&
34
+ node.getAttribute('target') === '_blank') {
35
+ const rel = node.getAttribute('rel') || '';
35
36
  const parts = rel.split(/\s+/).filter(Boolean);
36
37
  if (!parts.includes('noopener')) {
37
38
  parts.push('noopener');
@@ -39,9 +40,11 @@ export function safeHTML(box, options) {
39
40
  if (!parts.includes('noreferrer')) {
40
41
  parts.push('noreferrer');
41
42
  }
42
- attr(elm, 'rel', parts.join(' '));
43
- });
44
- }
43
+ attr(node, 'rel', parts.join(' '));
44
+ }
45
+ };
46
+ process(box);
47
+ Dom.each(box, process);
45
48
  }
46
49
  /**
47
50
  * Remove all on* event handler attributes from an element
@@ -63,6 +66,39 @@ function removeAllEventAttributes(elm) {
63
66
  }
64
67
  return effected;
65
68
  }
69
+ /**
70
+ * URL-bearing attributes (besides `href`) that can load or execute content.
71
+ */
72
+ const URL_ATTRIBUTES = [
73
+ 'src',
74
+ 'data',
75
+ 'action',
76
+ 'formaction',
77
+ 'poster',
78
+ 'background',
79
+ 'xlink:href'
80
+ ];
81
+ /**
82
+ * Tags that load their URL as a *document* (scripts inside run). An SVG data
83
+ * URL is only an XSS vector here — as an `<img>` source it renders inertly.
84
+ */
85
+ const DOCUMENT_EMBED_TAGS = new Set(['iframe', 'frame', 'object', 'embed']);
86
+ /**
87
+ * Detects executable / script-bearing URL schemes. The attribute value is
88
+ * already HTML-entity-decoded by `getAttribute`, so only whitespace and
89
+ * control characters (which browsers ignore inside a scheme) need stripping.
90
+ */
91
+ function isDangerousUrl(value, tagName) {
92
+ // eslint-disable-next-line no-control-regex
93
+ const normalized = value.replace(/[\u0000-\u0020]+/g, '').toLowerCase();
94
+ if (/^(?:javascript|vbscript|livescript|mocha):/.test(normalized)) {
95
+ return true;
96
+ }
97
+ if (/^data:(?:text\/html|application\/xhtml)/.test(normalized)) {
98
+ return true;
99
+ }
100
+ return (/^data:image\/svg/.test(normalized) && DOCUMENT_EMBED_TAGS.has(tagName));
101
+ }
66
102
  export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } = {
67
103
  safeJavaScriptLink: true,
68
104
  removeOnError: true
@@ -80,5 +116,21 @@ export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } =
80
116
  attr(elm, 'href', location.protocol + '//' + href);
81
117
  effected = true;
82
118
  }
119
+ if (safeJavaScriptLink) {
120
+ // `srcdoc` runs its content as a full HTML document — drop it entirely.
121
+ if (elm.hasAttribute('srcdoc')) {
122
+ attr(elm, 'srcdoc', null);
123
+ effected = true;
124
+ }
125
+ // Strip executable schemes from any other URL-bearing attribute.
126
+ const tagName = elm.nodeName.toLowerCase();
127
+ for (const name of URL_ATTRIBUTES) {
128
+ const value = elm.getAttribute(name);
129
+ if (value && isDangerousUrl(value, tagName)) {
130
+ attr(elm, name, null);
131
+ effected = true;
132
+ }
133
+ }
134
+ }
83
135
  return effected;
84
136
  }
@@ -10,4 +10,4 @@ import type { HTMLTagNames, Nullable } from "../../../types/index";
10
10
  /**
11
11
  * Extract plain text from HTML text
12
12
  */
13
- export declare function stripTags(html: string | Node, doc?: Document, exclude?: Nullable<Set<HTMLTagNames>>): string;
13
+ export declare function stripTags(html: string | Node, doc?: Document, exclude?: Nullable<Set<HTMLTagNames>>, blockBr?: boolean): string;
@@ -24,7 +24,7 @@ const ALONE_TAGS = new Set(['br', 'hr', 'input']);
24
24
  /**
25
25
  * Extract plain text from HTML text
26
26
  */
27
- export function stripTags(html, doc = document, exclude = null) {
27
+ export function stripTags(html, doc = document, exclude = null, blockBr = false) {
28
28
  const tmp = doc.createElement('div');
29
29
  if (isString(html)) {
30
30
  tmp.innerHTML = html;
@@ -40,7 +40,7 @@ export function stripTags(html, doc = document, exclude = null) {
40
40
  if (exclude && Dom.isTag(p, exclude)) {
41
41
  const tag = p.nodeName.toLowerCase();
42
42
  const text = !Dom.isTag(p, ALONE_TAGS)
43
- ? `%%%jodit-${tag}%%%${stripTags(p.innerHTML, doc, exclude)}%%%/jodit-${tag}%%%`
43
+ ? `%%%jodit-${tag}%%%${stripTags(p.innerHTML, doc, exclude, blockBr)}%%%/jodit-${tag}%%%`
44
44
  : `%%%jodit-single-${tag}%%%`;
45
45
  Dom.before(p, doc.createTextNode(text));
46
46
  Dom.safeRemove(p);
@@ -58,7 +58,11 @@ export function stripTags(html, doc = document, exclude = null) {
58
58
  return;
59
59
  }
60
60
  if (nx) {
61
- pr.insertBefore(doc.createTextNode(' '), nx);
61
+ // By default blocks are joined with a single space (single-line
62
+ // plain text). When `blockBr` is set, separate them with a line
63
+ // break instead, so paragraph structure survives — e.g. the
64
+ // "Insert only Text" paste option. See #1232
65
+ pr.insertBefore(doc.createTextNode(blockBr ? '%%%jodit-single-br%%%' : ' '), nx);
62
66
  }
63
67
  });
64
68
  return restoreTags(trim(tmp.innerText));
@@ -10,6 +10,15 @@ import { isVoid } from "../checker/is-void.js";
10
10
  import { Config } from "../../../config.js";
11
11
  import { isAtom } from "./extend.js";
12
12
  import { keys } from "./utils.js";
13
+ /**
14
+ * Keys that must never be copied from a (potentially untrusted) config object —
15
+ * assigning them during a recursive merge can reach and mutate
16
+ * `Object.prototype` (prototype pollution, CWE-1321).
17
+ */
18
+ const UNSAFE_PROTO_KEYS = ['__proto__', 'constructor', 'prototype'];
19
+ function isUnsafeProtoKey(key) {
20
+ return UNSAFE_PROTO_KEYS.indexOf(key) !== -1;
21
+ }
13
22
  /**
14
23
  * @example
15
24
  * ```js
@@ -59,6 +68,9 @@ export function ConfigProto(options, proto, deep = 0) {
59
68
  }
60
69
  const newOpt = {};
61
70
  Object.keys(options).forEach(key => {
71
+ if (isUnsafeProtoKey(key)) {
72
+ return;
73
+ }
62
74
  const opt = options[key], protoKey = proto ? proto[key] : null;
63
75
  if (isPlainObject(opt) && isPlainObject(protoKey) && !isAtom(opt)) {
64
76
  newOpt[key] = ConfigProto(opt, protoKey, deep + 1);
@@ -119,6 +131,9 @@ export function ConfigFlatten(obj) {
119
131
  */
120
132
  export function ConfigMerge(target, source) {
121
133
  Object.keys(source).forEach(key => {
134
+ if (isUnsafeProtoKey(key)) {
135
+ return;
136
+ }
122
137
  const srcVal = source[key];
123
138
  const tgtVal = target[key];
124
139
  if (isPlainObject(srcVal) && isPlainObject(tgtVal) && !isAtom(srcVal)) {
@@ -17,7 +17,6 @@ export const convertMediaUrlToVideoEmbed = (url, { width = 400, height = 345 } =
17
17
  return url;
18
18
  }
19
19
  const parser = globalDocument.createElement('a');
20
- const pattern1 = /(?:http?s?:\/\/)?(?:www\.)?(?:vimeo\.com)\/?(.+)/g;
21
20
  parser.href = url;
22
21
  if (!width) {
23
22
  width = 400;
@@ -28,27 +27,50 @@ export const convertMediaUrlToVideoEmbed = (url, { width = 400, height = 345 } =
28
27
  const protocol = parser.protocol || '';
29
28
  switch (parser.hostname) {
30
29
  case 'www.vimeo.com':
31
- case 'vimeo.com':
32
- return pattern1.test(url)
33
- ? url.replace(pattern1, '<iframe width="' +
34
- width +
35
- '" height="' +
36
- height +
37
- '" src="' +
38
- protocol +
39
- '//player.vimeo.com/video/$1" frameborder="0" allowfullscreen></iframe>')
40
- : url;
30
+ case 'vimeo.com': {
31
+ // The numeric video id can be preceded by `channels/<name>/` or
32
+ // `groups/<name>/videos/` and followed by tracking params (e.g.
33
+ // `?share=copy`). Unlisted videos keep a hash right after the id
34
+ // (`vimeo.com/<id>/<hash>`). Extract the id (+ hash) from the path
35
+ // so all of those forms produce a valid embed. See #1209
36
+ const segments = parser.pathname.split('/').filter(Boolean);
37
+ const idIndex = segments.findIndex(s => /^\d+$/.test(s));
38
+ if (idIndex === -1) {
39
+ return url;
40
+ }
41
+ let path = segments[idIndex];
42
+ const hash = segments[idIndex + 1];
43
+ if (hash && idIndex === 0) {
44
+ path += '/' + hash;
45
+ }
46
+ return ('<iframe width="' +
47
+ width +
48
+ '" height="' +
49
+ height +
50
+ '" src="' +
51
+ protocol +
52
+ '//player.vimeo.com/video/' +
53
+ path +
54
+ '" frameborder="0" allowfullscreen></iframe>');
55
+ }
41
56
  case 'youtube.com':
42
57
  case 'www.youtube.com':
58
+ case 'm.youtube.com':
59
+ case 'music.youtube.com':
43
60
  case 'youtu.be':
44
61
  case 'www.youtu.be': {
45
- const query = parser.search
46
- ? parseQuery(parser.search)
47
- : { v: parser.pathname.substring(1) };
48
- if (/^embed\/.*/.test(query.v)) {
49
- query.v = query.v.substring(6);
50
- }
51
- return query.v
62
+ const query = parser.search ? parseQuery(parser.search) : {};
63
+ // `youtube.com/watch` keeps the video id in the `v` query
64
+ // parameter, while the short `youtu.be/<id>` links and the
65
+ // `/embed/`, `/shorts/`, `/live/` paths keep it in the pathname.
66
+ // Modern share urls add tracking params (e.g. `?si=`, `?t=`), so
67
+ // the pathname must still be used as a fallback when there is no
68
+ // `v`. See #1209
69
+ let v = query.v || parser.pathname.substring(1);
70
+ v = v
71
+ .replace(/^(watch|embed|shorts|live|v)\//, '')
72
+ .replace(/\/$/, '');
73
+ return v
52
74
  ? '<iframe width="' +
53
75
  width +
54
76
  '" height="' +
@@ -56,7 +78,7 @@ export const convertMediaUrlToVideoEmbed = (url, { width = 400, height = 345 } =
56
78
  '" src="' +
57
79
  protocol +
58
80
  '//www.youtube.com/embed/' +
59
- query.v +
81
+ v +
60
82
  '" frameborder="0" allowfullscreen></iframe>'
61
83
  : url;
62
84
  }
package/esm/jodit.js CHANGED
@@ -1210,6 +1210,26 @@ let Jodit = Jodit_1 = class Jodit extends ViewWithToolbar {
1210
1210
  }
1211
1211
  this.synchronizeValues();
1212
1212
  }
1213
+ })
1214
+ .on(this.ow, 'mouseup', (event) => {
1215
+ if (this.o.readonly || this.__isSilentChange) {
1216
+ return;
1217
+ }
1218
+ // When a selection is started inside the editor and the
1219
+ // mouse button is released outside of it, the editable
1220
+ // area never receives the `mouseup` event, so the toolbar
1221
+ // state (active buttons) is not recalculated. Re-fire the
1222
+ // event manually for that case while the selection still
1223
+ // belongs to the editor. See #1251
1224
+ const target = event.target;
1225
+ const insideEditor = Boolean(target &&
1226
+ isNumber(target.nodeType) &&
1227
+ editor.contains(target));
1228
+ if (insideEditor || !this.s.isInsideArea) {
1229
+ return;
1230
+ }
1231
+ this.e.fire('changeSelection');
1232
+ this.synchronizeValues();
1213
1233
  });
1214
1234
  }
1215
1235
  fetch(url, options) {
@@ -14,7 +14,17 @@ Config.prototype.enableDragAndDropFileToEditor = true;
14
14
  Config.prototype.uploader = {
15
15
  url: '',
16
16
  insertImageAsBase64URI: false,
17
- imagesExtensions: ['jpg', 'png', 'jpeg', 'gif'],
17
+ imagesExtensions: [
18
+ 'jpg',
19
+ 'jpeg',
20
+ 'png',
21
+ 'gif',
22
+ 'webp',
23
+ 'bmp',
24
+ 'svg',
25
+ 'tiff',
26
+ 'avif'
27
+ ],
18
28
  headers: null,
19
29
  data: null,
20
30
  filesVariableName(i) {
@@ -31,9 +31,16 @@ function isRemovableNode(jodit, node, current, allow, deny) {
31
31
  if (!jodit.o.cleanHTML.removeEmptyElements) {
32
32
  return false;
33
33
  }
34
+ // Never drop an empty inline element that currently holds the caret — it is
35
+ // a pending-format marker the user is about to type into (#1291). `current`
36
+ // is captured before a click moves the caret, so also check the live caret.
37
+ const liveCaret = jodit.s.isCollapsed()
38
+ ? jodit.s.range.startContainer
39
+ : null;
34
40
  return (Dom.isElement(node) &&
35
41
  node.nodeName.match(IS_INLINE) != null &&
36
42
  !Dom.isTemporary(node) &&
37
43
  trimInv(node.innerHTML).length === 0 &&
38
- (current == null || !Dom.isOrContains(node, current)));
44
+ (current == null || !Dom.isOrContains(node, current)) &&
45
+ (liveCaret == null || !Dom.isOrContains(node, liveCaret)));
39
46
  }
@@ -52,8 +52,9 @@ Config.prototype.controls.brush = {
52
52
  const update = (key, value) => {
53
53
  if (value && value !== css(editor.editor, key).toString()) {
54
54
  button.state.icon.fill = value;
55
- return;
55
+ return true;
56
56
  }
57
+ return false;
57
58
  };
58
59
  if (color) {
59
60
  const mode = dataBind(button, 'color');
@@ -63,8 +64,16 @@ Config.prototype.controls.brush = {
63
64
  const current = editor.s.current();
64
65
  if (current && !button.state.disabled) {
65
66
  const currentBpx = Dom.closest(current, Dom.isElement, editor.editor) || editor.editor;
66
- update('color', css(currentBpx, 'color').toString());
67
- update('background-color', css(currentBpx, 'background-color').toString());
67
+ // The icon's fill mirrors the current text/background color so the
68
+ // button reflects the formatting under the caret. Both calls run so
69
+ // that a background color (the second call) wins over the text color
70
+ // when both are set. Keep the computed fill instead of resetting it
71
+ // below. See #195, #182
72
+ const hasColor = update('color', css(currentBpx, 'color').toString());
73
+ const hasBackground = update('background-color', css(currentBpx, 'background-color').toString());
74
+ if (hasColor || hasBackground) {
75
+ return;
76
+ }
68
77
  }
69
78
  button.state.icon.fill = '';
70
79
  button.state.activated = false;
@@ -21,10 +21,31 @@ export declare class dragAndDropElement extends Plugin {
21
21
  private state;
22
22
  /** @override */
23
23
  protected afterInit(): void;
24
+ /**
25
+ * Start dragging a specific element programmatically.
26
+ *
27
+ * Allows a separate UI element (for example a drag handle/anchor shown next
28
+ * to a block) to initiate the drag without the user pressing directly on the
29
+ * draggable element itself.
30
+ *
31
+ * @example
32
+ * ```js
33
+ * handle.addEventListener('mousedown', e => {
34
+ * editor.e.fire('startDragElement', preBlock, e);
35
+ * });
36
+ * ```
37
+ */
38
+ private onStartDragElement;
24
39
  /**
25
40
  * Drag start handler
26
41
  */
27
42
  private onDragStart;
43
+ /**
44
+ * Prepare the ghost element and switch to the waiting state.
45
+ * Shared by the native mousedown handler and the programmatic
46
+ * `startDragElement` event handler.
47
+ */
48
+ private startDragging;
28
49
  /**
29
50
  * Mouse move handler handler
30
51
  */
@@ -49,11 +49,34 @@ export class dragAndDropElement extends Plugin {
49
49
  .filter(Boolean)
50
50
  .map(item => item.toLowerCase())
51
51
  : [];
52
+ // Allow another plugin (e.g. a drag handle/anchor) to start dragging
53
+ // an element programmatically, regardless of the `draggableTags` list.
54
+ this.j.e.on('startDragElement', this.onStartDragElement);
52
55
  if (!this.dragList.length) {
53
56
  return;
54
57
  }
55
58
  this.j.e.on('mousedown dragstart', this.onDragStart);
56
59
  }
60
+ /**
61
+ * Start dragging a specific element programmatically.
62
+ *
63
+ * Allows a separate UI element (for example a drag handle/anchor shown next
64
+ * to a block) to initiate the drag without the user pressing directly on the
65
+ * draggable element itself.
66
+ *
67
+ * @example
68
+ * ```js
69
+ * handle.addEventListener('mousedown', e => {
70
+ * editor.e.fire('startDragElement', preBlock, e);
71
+ * });
72
+ * ```
73
+ */
74
+ onStartDragElement(element, event) {
75
+ if (this.isInDestruct || this.state > DragState.IDLE || !element) {
76
+ return;
77
+ }
78
+ this.startDragging(element, event);
79
+ }
57
80
  /**
58
81
  * Drag start handler
59
82
  */
@@ -79,11 +102,19 @@ export class dragAndDropElement extends Plugin {
79
102
  lastTarget.parentElement.lastChild === lastTarget) {
80
103
  lastTarget = lastTarget.parentElement;
81
104
  }
105
+ this.startDragging(lastTarget, event);
106
+ }
107
+ /**
108
+ * Prepare the ghost element and switch to the waiting state.
109
+ * Shared by the native mousedown handler and the programmatic
110
+ * `startDragElement` event handler.
111
+ */
112
+ startDragging(target, event) {
82
113
  this.startX = event.clientX;
83
114
  this.startY = event.clientY;
84
115
  this.isCopyMode = ctrlKey(event); // we can move only element from editor
85
- this.draggable = lastTarget.cloneNode(true);
86
- dataBind(this.draggable, 'target', lastTarget);
116
+ this.draggable = target.cloneNode(true);
117
+ dataBind(this.draggable, 'target', target);
87
118
  this.state = DragState.WAIT_DRAGGING;
88
119
  this.addDragListeners();
89
120
  }
@@ -157,6 +188,15 @@ export class dragAndDropElement extends Plugin {
157
188
  }
158
189
  const { parentElement } = fragment;
159
190
  this.j.s.insertNode(fragment, true, false);
191
+ // Dropping a non-editable block (e.g. a `<pre>` code sample) can leave an
192
+ // invisible filler text node beside it. Remove it so the drop does not
193
+ // introduce a stray empty line (which clean-html would otherwise strip
194
+ // later, causing a flash).
195
+ [fragment.previousSibling, fragment.nextSibling].forEach(node => {
196
+ if (Dom.isEmptyTextNode(node)) {
197
+ Dom.safeRemove(node);
198
+ }
199
+ });
160
200
  if (parentElement &&
161
201
  Dom.isEmpty(parentElement) &&
162
202
  !Dom.isCell(parentElement)) {
@@ -188,10 +228,15 @@ export class dragAndDropElement extends Plugin {
188
228
  /** @override */
189
229
  beforeDestruct() {
190
230
  this.onDragEnd();
191
- this.j.e.off('mousedown dragstart', this.onDragStart);
231
+ this.j.e
232
+ .off('mousedown dragstart', this.onDragStart)
233
+ .off('startDragElement', this.onStartDragElement);
192
234
  this.removeDragListeners();
193
235
  }
194
236
  }
237
+ __decorate([
238
+ autobind
239
+ ], dragAndDropElement.prototype, "onStartDragElement", null);
195
240
  __decorate([
196
241
  autobind
197
242
  ], dragAndDropElement.prototype, "onDragStart", null);