jodit 4.12.18 → 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 +21 -1
- package/es2015/jodit.css +1 -1
- package/es2015/jodit.fat.min.js +6 -6
- package/es2015/jodit.js +195 -44
- package/es2015/jodit.min.js +6 -6
- 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 +6 -6
- package/es2018/jodit.min.js +6 -6
- 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 +8 -8
- package/es2021/jodit.js +194 -44
- package/es2021/jodit.min.js +8 -8
- 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 +8 -8
- package/es2021.en/jodit.js +194 -44
- package/es2021.en/jodit.min.js +8 -8
- 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 +214 -50
- 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/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/color/config.js +12 -3
- 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/package.json +1 -1
- package/types/core/helpers/html/strip-tags.d.ts +1 -1
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));
|
|
@@ -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) {
|
|
@@ -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;
|
|
@@ -7,6 +7,6 @@ import { Config } from "../../config.js";
|
|
|
7
7
|
Config.prototype.commandToHotkeys = {
|
|
8
8
|
removeFormat: ['ctrl+shift+m', 'cmd+shift+m'],
|
|
9
9
|
insertOrderedList: ['ctrl+shift+7', 'cmd+shift+7'],
|
|
10
|
-
insertUnorderedList: ['ctrl+shift+8, cmd+shift+8'],
|
|
10
|
+
insertUnorderedList: ['ctrl+shift+8', 'cmd+shift+8'],
|
|
11
11
|
selectall: ['ctrl+a', 'cmd+a']
|
|
12
12
|
};
|
|
@@ -15,15 +15,29 @@ Config.prototype.controls.indent = {
|
|
|
15
15
|
};
|
|
16
16
|
Config.prototype.controls.outdent = {
|
|
17
17
|
isDisabled: (editor) => {
|
|
18
|
+
var _a;
|
|
18
19
|
const current = editor.s.current();
|
|
19
|
-
if (current) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
if (!current) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
// A list item whose list is nested inside another list item can be
|
|
24
|
+
// outdented (un-nested) by the `tab` plugin, even without an inline
|
|
25
|
+
// indent margin. Keep the button enabled in that case. See #1247
|
|
26
|
+
if ((_a = editor.o.tab) === null || _a === void 0 ? void 0 : _a.tabInsideLiInsertNewList) {
|
|
27
|
+
const li = Dom.closest(current, 'li', editor.editor);
|
|
28
|
+
if (li) {
|
|
29
|
+
const list = Dom.closest(li, ['ul', 'ol'], editor.editor);
|
|
30
|
+
if (list && Dom.closest(list, 'li', editor.editor)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
25
33
|
}
|
|
26
34
|
}
|
|
35
|
+
const currentBox = Dom.closest(current, Dom.isBlock, editor.editor);
|
|
36
|
+
if (currentBox) {
|
|
37
|
+
const arrow = getKey(editor.o.direction, currentBox);
|
|
38
|
+
return (!currentBox.style[arrow] ||
|
|
39
|
+
parseInt(currentBox.style[arrow], 10) <= 0);
|
|
40
|
+
}
|
|
27
41
|
return true;
|
|
28
42
|
},
|
|
29
43
|
tooltip: 'Decrease Indent'
|
|
@@ -148,10 +148,15 @@ export class paste extends Plugin {
|
|
|
148
148
|
html = cleanFromWord(html);
|
|
149
149
|
break;
|
|
150
150
|
case INSERT_ONLY_TEXT:
|
|
151
|
-
html = stripTags(html, this.j.ed, new Set(this.j.o.pasteExcludeStripTags));
|
|
151
|
+
html = stripTags(html, this.j.ed, new Set(this.j.o.pasteExcludeStripTags), this.j.o.nl2brInPlainText);
|
|
152
152
|
break;
|
|
153
153
|
case INSERT_AS_TEXT:
|
|
154
154
|
html = htmlspecialchars(html);
|
|
155
|
+
// Keep the source line breaks instead of letting the raw
|
|
156
|
+
// newlines collapse into spaces when rendered. See #1093
|
|
157
|
+
if (this.j.o.nl2brInPlainText) {
|
|
158
|
+
html = nl2br(html);
|
|
159
|
+
}
|
|
155
160
|
break;
|
|
156
161
|
default: {
|
|
157
162
|
const newHTML = this.j.e.fire('onCustomPasteHTMLOption', action, html, e);
|
|
@@ -61,7 +61,7 @@ export class pasteFromWord extends Plugin {
|
|
|
61
61
|
break;
|
|
62
62
|
}
|
|
63
63
|
case INSERT_ONLY_TEXT: {
|
|
64
|
-
html = stripTags(cleanFromWord(html));
|
|
64
|
+
html = stripTags(cleanFromWord(html), this.j.ed, new Set(this.j.o.pasteExcludeStripTags), this.j.o.nl2brInPlainText);
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
67
67
|
}
|
package/package.json
CHANGED
|
@@ -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;
|