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.
- package/CHANGELOG.md +36 -0
- package/es2015/jodit.css +1 -1
- package/es2015/jodit.fat.min.js +7 -7
- package/es2015/jodit.js +338 -79
- package/es2015/jodit.min.js +7 -7
- package/es2015/plugins/debug/debug.css +1 -1
- package/es2015/plugins/debug/debug.js +1 -1
- package/es2015/plugins/debug/debug.min.js +1 -1
- package/es2015/plugins/speech-recognize/speech-recognize.css +1 -1
- package/es2015/plugins/speech-recognize/speech-recognize.js +1 -1
- package/es2015/plugins/speech-recognize/speech-recognize.min.js +1 -1
- package/es2018/jodit.fat.min.js +7 -7
- package/es2018/jodit.min.js +7 -7
- package/es2018/plugins/debug/debug.min.js +1 -1
- package/es2018/plugins/speech-recognize/speech-recognize.min.js +1 -1
- package/es2021/jodit.css +1 -1
- package/es2021/jodit.fat.min.js +9 -9
- package/es2021/jodit.js +335 -79
- package/es2021/jodit.min.js +9 -9
- package/es2021/plugins/debug/debug.css +1 -1
- package/es2021/plugins/debug/debug.js +1 -1
- package/es2021/plugins/debug/debug.min.js +1 -1
- package/es2021/plugins/speech-recognize/speech-recognize.css +1 -1
- package/es2021/plugins/speech-recognize/speech-recognize.js +1 -1
- package/es2021/plugins/speech-recognize/speech-recognize.min.js +1 -1
- package/es2021.en/jodit.css +1 -1
- package/es2021.en/jodit.fat.min.js +10 -10
- package/es2021.en/jodit.js +335 -79
- package/es2021.en/jodit.min.js +9 -9
- package/es2021.en/plugins/debug/debug.css +1 -1
- package/es2021.en/plugins/debug/debug.js +1 -1
- package/es2021.en/plugins/debug/debug.min.js +1 -1
- package/es2021.en/plugins/speech-recognize/speech-recognize.css +1 -1
- package/es2021.en/plugins/speech-recognize/speech-recognize.js +1 -1
- package/es2021.en/plugins/speech-recognize/speech-recognize.min.js +1 -1
- package/es5/jodit.css +2 -2
- package/es5/jodit.fat.min.js +2 -2
- package/es5/jodit.js +367 -85
- package/es5/jodit.min.css +2 -2
- package/es5/jodit.min.js +2 -2
- package/es5/plugins/debug/debug.css +1 -1
- package/es5/plugins/debug/debug.js +1 -1
- package/es5/plugins/debug/debug.min.js +1 -1
- package/es5/plugins/speech-recognize/speech-recognize.css +1 -1
- package/es5/plugins/speech-recognize/speech-recognize.js +1 -1
- package/es5/plugins/speech-recognize/speech-recognize.min.js +1 -1
- package/es5/polyfills.fat.min.js +1 -1
- package/es5/polyfills.js +1 -1
- package/es5/polyfills.min.js +1 -1
- package/esm/core/constants.js +1 -1
- package/esm/core/helpers/html/apply-styles.js +11 -0
- package/esm/core/helpers/html/clean-from-word.js +9 -0
- package/esm/core/helpers/html/safe-html.js +71 -19
- package/esm/core/helpers/html/strip-tags.d.ts +1 -1
- package/esm/core/helpers/html/strip-tags.js +7 -3
- package/esm/core/helpers/utils/config-proto.js +15 -0
- package/esm/core/helpers/utils/convert-media-url-to-video-embed.js +41 -19
- package/esm/jodit.js +20 -0
- package/esm/modules/uploader/config.js +11 -1
- package/esm/plugins/clean-html/helpers/visitor/filters/try-remove-node.js +8 -1
- package/esm/plugins/color/config.js +12 -3
- package/esm/plugins/drag-and-drop-element/drag-and-drop-element.d.ts +21 -0
- package/esm/plugins/drag-and-drop-element/drag-and-drop-element.js +48 -3
- package/esm/plugins/enter/enter.js +11 -6
- package/esm/plugins/hotkeys/config.js +1 -1
- package/esm/plugins/indent/config.js +20 -6
- package/esm/plugins/paste/paste.js +6 -1
- package/esm/plugins/paste-from-word/paste-from-word.js +1 -1
- package/esm/plugins/select/select.d.ts +8 -0
- package/esm/plugins/select/select.js +37 -0
- package/package.json +1 -1
- package/types/core/helpers/html/strip-tags.d.ts +1 -1
- package/types/plugins/drag-and-drop-element/drag-and-drop-element.d.ts +21 -0
- package/types/plugins/select/select.d.ts +8 -0
package/es5/polyfills.fat.min.js
CHANGED
package/es5/polyfills.js
CHANGED
package/es5/polyfills.min.js
CHANGED
package/esm/core/constants.js
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
67
|
-
|
|
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 =
|
|
86
|
-
dataBind(this.draggable, 'target',
|
|
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
|
|
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);
|