jodit 4.18.1 → 4.18.3
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 +30 -0
- package/es2015/jodit.css +7 -6
- package/es2015/jodit.fat.min.js +10 -10
- package/es2015/jodit.js +492 -189
- package/es2015/jodit.min.js +10 -10
- 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 +9 -9
- package/es2018/jodit.min.js +9 -9
- package/es2018/plugins/debug/debug.min.js +1 -1
- package/es2018/plugins/speech-recognize/speech-recognize.min.js +1 -1
- package/es2021/jodit.css +7 -6
- package/es2021/jodit.fat.min.js +16 -16
- package/es2021/jodit.js +432 -135
- package/es2021/jodit.min.js +16 -16
- 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 +7 -6
- package/es2021.en/jodit.fat.min.js +16 -16
- package/es2021.en/jodit.js +432 -135
- package/es2021.en/jodit.min.js +16 -16
- 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 +8 -7
- package/es5/jodit.fat.min.js +2 -2
- package/es5/jodit.js +555 -215
- 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/config.d.ts +4 -2
- package/esm/core/constants.js +1 -1
- package/esm/core/helpers/utils/index.d.ts +1 -0
- package/esm/core/helpers/utils/index.js +1 -0
- package/esm/core/helpers/utils/prepare-media.d.ts +19 -0
- package/esm/core/helpers/utils/prepare-media.js +31 -0
- package/esm/core/selection/style/api/wrap-unwrapped-text.js +3 -2
- package/esm/modules/history/history.d.ts +17 -2
- package/esm/modules/history/history.js +41 -19
- package/esm/modules/table/table.js +7 -2
- package/esm/modules/widget/file-selector/file-selector.js +2 -2
- package/esm/plugins/backspace/backspace.js +4 -0
- package/esm/plugins/clean-html/clean-html.d.ts +9 -1
- package/esm/plugins/clean-html/clean-html.js +41 -7
- package/esm/plugins/clean-html/helpers/visitor/filters/replace-old-tags.js +26 -1
- package/esm/plugins/dtd/before-insert/check-block-nesting.d.ts +2 -1
- package/esm/plugins/dtd/before-insert/check-block-nesting.js +26 -5
- package/esm/plugins/enter/helpers/move-cursor-out-from-specal-tags.js +8 -0
- package/esm/plugins/focus/focus.js +3 -0
- package/esm/plugins/hr/hr.js +21 -5
- package/esm/plugins/iframe/config.js +8 -3
- package/esm/plugins/inline-popup/inline-popup.d.ts +5 -0
- package/esm/plugins/inline-popup/inline-popup.js +26 -2
- package/esm/plugins/link/link.js +5 -2
- package/esm/plugins/media/config.d.ts +2 -1
- package/esm/plugins/media/media.js +21 -2
- package/esm/plugins/paste/helpers.d.ts +8 -0
- package/esm/plugins/paste/helpers.js +18 -1
- package/esm/plugins/paste/paste.js +2 -2
- package/esm/plugins/resizer/resizer.js +8 -3
- package/esm/plugins/select-cells/select-cells.d.ts +10 -0
- package/esm/plugins/select-cells/select-cells.js +54 -6
- package/esm/plugins/table/config.js +51 -1
- package/esm/plugins/table-keyboard-navigation/table-keyboard-navigation.js +1 -1
- package/esm/plugins/video/config.js +6 -22
- package/package.json +1 -1
- package/types/config.d.ts +4 -2
- package/types/core/helpers/utils/index.d.ts +1 -0
- package/types/core/helpers/utils/prepare-media.d.ts +19 -0
- package/types/modules/history/history.d.ts +17 -2
- package/types/plugins/clean-html/clean-html.d.ts +9 -1
- package/types/plugins/dtd/before-insert/check-block-nesting.d.ts +2 -1
- package/types/plugins/inline-popup/inline-popup.d.ts +5 -0
- package/types/plugins/media/config.d.ts +2 -1
- package/types/plugins/paste/helpers.d.ts +8 -0
- package/types/plugins/select-cells/select-cells.d.ts +10 -0
|
@@ -4,9 +4,33 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Moves the cursor out of a block that can't hold the inserted block: before
|
|
9
|
+
* or after it when the cursor is at its edge, or between its two halves when
|
|
10
|
+
* the cursor is inside its content. An empty block is removed
|
|
11
|
+
*/
|
|
12
|
+
function moveCursorOutOf(jodit, parent) {
|
|
13
|
+
var _a;
|
|
14
|
+
const { s } = jodit;
|
|
15
|
+
if (Dom.isEmpty(parent)) {
|
|
16
|
+
s.setCursorAfter(parent);
|
|
17
|
+
Dom.safeRemove(parent);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (s.cursorOnTheRight(parent)) {
|
|
21
|
+
s.setCursorAfter(parent);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (s.cursorOnTheLeft(parent)) {
|
|
25
|
+
s.setCursorBefore(parent);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
s.setCursorAfter((_a = s.splitSelection(parent)) !== null && _a !== void 0 ? _a : parent);
|
|
29
|
+
}
|
|
7
30
|
/**
|
|
8
31
|
* Checks whether the insertion of an element at the current location is allowed,
|
|
9
|
-
* if it is not allowed, it deletes an empty block element
|
|
32
|
+
* if it is not allowed, it deletes an empty block element, moves the cursor
|
|
33
|
+
* before or after it, or splits it at the cursor
|
|
10
34
|
* @internal
|
|
11
35
|
*/
|
|
12
36
|
export function checkBlockNesting(jodit, node) {
|
|
@@ -42,10 +66,7 @@ export function checkBlockNesting(jodit, node) {
|
|
|
42
66
|
parent = current;
|
|
43
67
|
}
|
|
44
68
|
if (parent) {
|
|
45
|
-
jodit
|
|
46
|
-
if (Dom.isEmpty(parent)) {
|
|
47
|
-
Dom.safeRemove(parent);
|
|
48
|
-
}
|
|
69
|
+
moveCursorOutOf(jodit, parent);
|
|
49
70
|
}
|
|
50
71
|
}
|
|
51
72
|
}
|
|
@@ -18,5 +18,13 @@ export function moveCursorOutFromSpecialTags(jodit, fake, tags) {
|
|
|
18
18
|
else if (s.cursorOnTheLeft(link, fake)) {
|
|
19
19
|
Dom.before(link, fake);
|
|
20
20
|
}
|
|
21
|
+
else {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// The block is split at the caret, so the caret leaves the link too:
|
|
25
|
+
// beside the fake, since Safari mislays a caret inside a removed node
|
|
26
|
+
const range = s.createRange();
|
|
27
|
+
range.setStartBefore(fake);
|
|
28
|
+
s.selectRange(range);
|
|
21
29
|
}
|
|
22
30
|
}
|
|
@@ -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 { INSEPARABLE_TAGS } from "../../core/constants.js";
|
|
6
7
|
import { Dom } from "../../core/dom/index.js";
|
|
7
8
|
import { pluginSystem } from "../../core/global.js";
|
|
8
9
|
import { Config } from "../../config.js";
|
|
@@ -77,9 +78,11 @@ export function focus(editor) {
|
|
|
77
78
|
editor.e
|
|
78
79
|
.off(editor.editor, 'mousedown.autofocus')
|
|
79
80
|
.on(editor.editor, 'mousedown.autofocus', (e) => {
|
|
81
|
+
// An empty block takes the caret, but a video, say, can't hold one
|
|
80
82
|
if (editor.isEditorMode() &&
|
|
81
83
|
e.target &&
|
|
82
84
|
Dom.isBlock(e.target) &&
|
|
85
|
+
!Dom.isTag(e.target, INSEPARABLE_TAGS) &&
|
|
83
86
|
!e.target.childNodes.length) {
|
|
84
87
|
if (editor.editor === e.target) {
|
|
85
88
|
editor.s.focus();
|
package/esm/plugins/hr/hr.js
CHANGED
|
@@ -20,18 +20,34 @@ export function hr(editor) {
|
|
|
20
20
|
group: 'insert'
|
|
21
21
|
});
|
|
22
22
|
editor.registerCommand('insertHorizontalRule', () => {
|
|
23
|
+
var _a;
|
|
23
24
|
const elm = editor.createInside.element('hr');
|
|
24
25
|
editor.s.insertNode(elm, false, false);
|
|
25
26
|
const block = Dom.closest(elm.parentElement, Dom.isBlock, editor.editor);
|
|
26
|
-
|
|
27
|
+
// A block that holds other blocks, such as a list item or a table
|
|
28
|
+
// cell, keeps the line
|
|
29
|
+
if (block &&
|
|
30
|
+
Dom.isEmpty(block) &&
|
|
31
|
+
block !== editor.editor &&
|
|
32
|
+
!((_a = editor.o.dtd) === null || _a === void 0 ? void 0 : _a.blockLimits[block.nodeName.toLowerCase()])) {
|
|
27
33
|
Dom.after(block, elm);
|
|
28
34
|
Dom.safeRemove(block);
|
|
29
35
|
}
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const next = Dom.findNotEmptySibling(elm, false);
|
|
37
|
+
if (Dom.isBlock(next)) {
|
|
38
|
+
editor.s.setCursorIn(next, true);
|
|
39
|
+
return false;
|
|
34
40
|
}
|
|
41
|
+
// In a list item or a table cell the text goes on after the line, as
|
|
42
|
+
// it does with the browsers' own command
|
|
43
|
+
if (next ||
|
|
44
|
+
(elm.parentNode !== editor.editor &&
|
|
45
|
+
!Dom.isBlock(Dom.findNotEmptySibling(elm, true)))) {
|
|
46
|
+
editor.s.setCursorAfter(elm);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const p = editor.createInside.element(editor.o.enter);
|
|
50
|
+
Dom.after(elm, p);
|
|
35
51
|
editor.s.setCursorIn(p);
|
|
36
52
|
return false;
|
|
37
53
|
});
|
|
@@ -51,13 +51,15 @@ Config.prototype.iframeStyle =
|
|
|
51
51
|
'p{' +
|
|
52
52
|
'margin-top:0;' +
|
|
53
53
|
'}' +
|
|
54
|
-
'
|
|
54
|
+
'[data-jodit_iframe_wrapper]{' +
|
|
55
55
|
'display: block;' +
|
|
56
|
-
'clear: both;' +
|
|
57
56
|
'user-select: none;' +
|
|
58
57
|
'position: relative;' +
|
|
59
58
|
'}' +
|
|
60
|
-
'
|
|
59
|
+
'[data-jodit_iframe_wrapper] iframe{' +
|
|
60
|
+
'position: relative;' +
|
|
61
|
+
'}' +
|
|
62
|
+
'[data-jodit_iframe_wrapper]:after {' +
|
|
61
63
|
'position:absolute;' +
|
|
62
64
|
'content:"";' +
|
|
63
65
|
'z-index:1;' +
|
|
@@ -69,6 +71,9 @@ Config.prototype.iframeStyle =
|
|
|
69
71
|
'display: block;' +
|
|
70
72
|
'background: rgba(0, 0, 0, 0);' +
|
|
71
73
|
'} ' +
|
|
74
|
+
'[data-jodit_iframe_wrapper][data-jodit-wrapper_active=true] iframe{' +
|
|
75
|
+
'z-index: 2;' +
|
|
76
|
+
'}' +
|
|
72
77
|
'.jodit-wysiwyg_image-selected::selection,' +
|
|
73
78
|
'.jodit-wysiwyg_image-selected *:not(img)::selection{' +
|
|
74
79
|
'background:transparent' +
|
|
@@ -20,6 +20,11 @@ export declare class inlinePopup extends Plugin {
|
|
|
20
20
|
private get popup();
|
|
21
21
|
private get toolbar();
|
|
22
22
|
private onClick;
|
|
23
|
+
/**
|
|
24
|
+
* The box the popup opens beside: the target's, with room for the
|
|
25
|
+
* resizer's handles when the target gets them, so it doesn't cover them
|
|
26
|
+
*/
|
|
27
|
+
private __bound;
|
|
23
28
|
/**
|
|
24
29
|
* Show inline popup with some toolbar
|
|
25
30
|
*
|
|
@@ -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, getMediaWrapper, isArray, isFunction, isString, keys, position, splitArray, toArray } from "../../core/helpers/index.js";
|
|
19
|
+
import { camelCase, getMediaWrapper, getWrappedMedia, 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";
|
|
@@ -53,10 +53,34 @@ export class inlinePopup extends Plugin {
|
|
|
53
53
|
? 'jodit-media'
|
|
54
54
|
: target === null || target === void 0 ? void 0 : target.nodeName.toLowerCase();
|
|
55
55
|
if (target && type && this.canShowPopupForType(type)) {
|
|
56
|
-
|
|
56
|
+
const isMedia = type === 'jodit-media';
|
|
57
|
+
// Beside the media in a wrapper kept in the content, which can fill
|
|
58
|
+
// the line
|
|
59
|
+
const bound = (mediaWrapper && getWrappedMedia(this.j, mediaWrapper)) ||
|
|
60
|
+
target;
|
|
61
|
+
this.showPopup(() => this.__bound(bound, isMedia), type, target);
|
|
57
62
|
return false;
|
|
58
63
|
}
|
|
59
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The box the popup opens beside: the target's, with room for the
|
|
67
|
+
* resizer's handles when the target gets them, so it doesn't cover them
|
|
68
|
+
*/
|
|
69
|
+
__bound(target, isMedia) {
|
|
70
|
+
const bound = position(target, this.j);
|
|
71
|
+
const resizable = this.j.o.allowResizeTags;
|
|
72
|
+
if (!isMedia && !(resizable && Dom.isTag(target, resizable))) {
|
|
73
|
+
return bound;
|
|
74
|
+
}
|
|
75
|
+
const room = parseInt(this.j.ow
|
|
76
|
+
.getComputedStyle(this.j.container)
|
|
77
|
+
.getPropertyValue('--jd-resizer-handle-size'), 10) / 2 || 0;
|
|
78
|
+
return {
|
|
79
|
+
...bound,
|
|
80
|
+
top: bound.top - room,
|
|
81
|
+
height: bound.height + room * 2
|
|
82
|
+
};
|
|
83
|
+
}
|
|
60
84
|
/**
|
|
61
85
|
* Show inline popup with some toolbar
|
|
62
86
|
*
|
package/esm/plugins/link/link.js
CHANGED
|
@@ -16,7 +16,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
16
16
|
import { autobind } from "../../core/decorators/index.js";
|
|
17
17
|
import { Dom } from "../../core/dom/index.js";
|
|
18
18
|
import { pluginSystem } from "../../core/global.js";
|
|
19
|
-
import { attr, call, convertMediaUrlToVideoEmbed, isString, isURL, refs, stripTags } from "../../core/helpers/index.js";
|
|
19
|
+
import { attr, call, convertMediaUrlToVideoEmbed, isString, isURL, prepareEmbedCode, refs, stripTags } from "../../core/helpers/index.js";
|
|
20
20
|
import { Plugin } from "../../core/plugin/index.js";
|
|
21
21
|
import "./config.js";
|
|
22
22
|
/**
|
|
@@ -85,7 +85,10 @@ export class link extends Plugin {
|
|
|
85
85
|
height: (_d = jodit.o.video) === null || _d === void 0 ? void 0 : _d.defaultHeight
|
|
86
86
|
});
|
|
87
87
|
if (embed !== html) {
|
|
88
|
-
|
|
88
|
+
const media = prepareEmbedCode(jodit, embed);
|
|
89
|
+
return isString(media)
|
|
90
|
+
? jodit.createInside.fromHTML(media)
|
|
91
|
+
: media;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
if (jodit.s.isCollapsed()) {
|
|
@@ -28,7 +28,8 @@ declare module 'jodit/config' {
|
|
|
28
28
|
* - is wrapped in it instead of the editor-only `mediaFakeTag` when its tag
|
|
29
29
|
* is one of `mediaBlocks`, or left in it when it's already in one
|
|
30
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
|
|
31
|
+
* link's embed or embed code that is one element, and when a pasted
|
|
32
|
+
* video link is turned into its embed
|
|
32
33
|
* - opens the media popup for its wrapper when clicked, whose Align and
|
|
33
34
|
* Delete act on the wrapper
|
|
34
35
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as consts from "../../core/constants.js";
|
|
7
7
|
import { Dom } from "../../core/dom/dom.js";
|
|
8
|
-
import { pluginSystem } from "../../core/global.js";
|
|
8
|
+
import { getContainer, pluginSystem } from "../../core/global.js";
|
|
9
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";
|
|
@@ -49,12 +49,31 @@ export function media(editor) {
|
|
|
49
49
|
else {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
// Selected, as an image is, so Delete and Backspace remove it
|
|
52
53
|
editor.e
|
|
53
54
|
.off(element, 'mousedown.select touchstart.select')
|
|
54
55
|
.on(element, 'mousedown.select touchstart.select', () => {
|
|
55
|
-
editor.s.
|
|
56
|
+
editor.s.select(element);
|
|
56
57
|
});
|
|
57
58
|
};
|
|
59
|
+
// Media in a wrapper kept in the content let clicks through to it, as
|
|
60
|
+
// jodit-media covers its media, so a click selects the media rather than
|
|
61
|
+
// playing it. An inline wrapper holds it as an inline-block, unless its
|
|
62
|
+
// classes or style give it a display of their own
|
|
63
|
+
const wrappers = Object.values(mediaWrappers);
|
|
64
|
+
if (wrappers.length) {
|
|
65
|
+
editor.e.on('afterInit', () => {
|
|
66
|
+
const selectors = (list) => [...new Set(list)].join(',');
|
|
67
|
+
const scoped = ({ tag, className }) => `.jodit-wysiwyg ${tag}.${className}`;
|
|
68
|
+
const inline = selectors(wrappers
|
|
69
|
+
.filter(({ tag }) => consts.IS_INLINE.test(tag))
|
|
70
|
+
.map(scoped));
|
|
71
|
+
const media = selectors(wrappers.flatMap(wrapper => mediaBlocks.map(tag => `${scoped(wrapper)}>${tag}`)));
|
|
72
|
+
getContainer(editor, 'media-wrappers', 'style', true).innerHTML =
|
|
73
|
+
(inline ? `:where(${inline}){display:inline-block}` : '') +
|
|
74
|
+
`${media}{pointer-events:none}`;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
58
77
|
if (mediaInFakeBlock || Object.keys(mediaWrappers).length) {
|
|
59
78
|
editor.e
|
|
60
79
|
.on('afterGetValueFromEditor', (data) => {
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { IDialog, IJodit, InsertMode, IUIOption, Nullable } from "../../types/index";
|
|
10
10
|
import type { PasteEvent } from "./interface";
|
|
11
|
+
/**
|
|
12
|
+
* Google Docs wraps the copied content in
|
|
13
|
+
* `<b style="font-weight:normal;" id="docs-internal-guid-…">`. Kept, the
|
|
14
|
+
* wrapper holds the pasted blocks, and without its style it makes the whole
|
|
15
|
+
* paste bold
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
export declare function removeGoogleDocsWrapper(html: string): string;
|
|
11
19
|
/**
|
|
12
20
|
* One insert point for clipboard plugins
|
|
13
21
|
* @private
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { TEXT_PLAIN } from "../../core/constants.js";
|
|
7
7
|
import { Dom } from "../../core/dom/dom.js";
|
|
8
|
+
import { toArray } from "../../core/helpers/array/to-array.js";
|
|
8
9
|
import { isArray, isNumber, isString, isVoid } from "../../core/helpers/checker/index.js";
|
|
9
10
|
import { Button } from "../../core/ui/button/button/button.js";
|
|
10
11
|
/**
|
|
@@ -23,6 +24,22 @@ function removeExtraFragments(html) {
|
|
|
23
24
|
}
|
|
24
25
|
return html;
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Google Docs wraps the copied content in
|
|
29
|
+
* `<b style="font-weight:normal;" id="docs-internal-guid-…">`. Kept, the
|
|
30
|
+
* wrapper holds the pasted blocks, and without its style it makes the whole
|
|
31
|
+
* paste bold
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
export function removeGoogleDocsWrapper(html) {
|
|
35
|
+
if (!/<b\s[^>]*docs-internal-guid-/i.test(html)) {
|
|
36
|
+
return html;
|
|
37
|
+
}
|
|
38
|
+
// An inert document: images don't load and handlers don't run
|
|
39
|
+
const { body } = new DOMParser().parseFromString(html, 'text/html');
|
|
40
|
+
toArray(body.querySelectorAll('b[id^="docs-internal-guid-"]')).forEach(Dom.unwrap);
|
|
41
|
+
return body.innerHTML;
|
|
42
|
+
}
|
|
26
43
|
/**
|
|
27
44
|
* @private
|
|
28
45
|
*/
|
|
@@ -46,7 +63,7 @@ export function pasteInsertHtml(e, editor, html) {
|
|
|
46
63
|
html = result;
|
|
47
64
|
}
|
|
48
65
|
if (isString(html)) {
|
|
49
|
-
html = removeExtraFragments(html);
|
|
66
|
+
html = removeExtraFragments(removeGoogleDocsWrapper(html));
|
|
50
67
|
}
|
|
51
68
|
editor.s.insertHTML(html);
|
|
52
69
|
}
|
|
@@ -20,7 +20,7 @@ import { pluginSystem } from "../../core/global.js";
|
|
|
20
20
|
import { cleanFromWord, getDataTransfer, htmlspecialchars, isHTML, isString, LimitedStack, nl2br, stripTags, trim } from "../../core/helpers/index.js";
|
|
21
21
|
import { Plugin } from "../../core/plugin/plugin.js";
|
|
22
22
|
import "./config.js";
|
|
23
|
-
import { askInsertTypeDialog, getAllTypes, pasteInsertHtml } from "./helpers.js";
|
|
23
|
+
import { askInsertTypeDialog, getAllTypes, pasteInsertHtml, removeGoogleDocsWrapper } from "./helpers.js";
|
|
24
24
|
/**
|
|
25
25
|
* Ask before paste HTML source
|
|
26
26
|
*/
|
|
@@ -143,7 +143,7 @@ export class paste extends Plugin {
|
|
|
143
143
|
this.j.buffer.set(CLIPBOARD_ID, html);
|
|
144
144
|
switch (action) {
|
|
145
145
|
case INSERT_CLEAR_HTML:
|
|
146
|
-
html = cleanFromWord(html);
|
|
146
|
+
html = cleanFromWord(removeGoogleDocsWrapper(html));
|
|
147
147
|
break;
|
|
148
148
|
case INSERT_ONLY_TEXT:
|
|
149
149
|
html = stripTags(html, this.j.ed, new Set(this.j.o.pasteExcludeStripTags), this.j.o.nl2brInPlainText);
|
|
@@ -160,7 +160,8 @@ export class resizer extends Plugin {
|
|
|
160
160
|
.off(editor.ow, '.resizer')
|
|
161
161
|
.on(editor.editor, 'keydown.resizer', (e) => {
|
|
162
162
|
if (this.isShown &&
|
|
163
|
-
e.key === consts.KEY_DELETE
|
|
163
|
+
(e.key === consts.KEY_DELETE ||
|
|
164
|
+
e.key === consts.KEY_BACKSPACE) &&
|
|
164
165
|
this.element &&
|
|
165
166
|
!Dom.isTag(this.element, 'table')) {
|
|
166
167
|
this.onDelete(e);
|
|
@@ -396,8 +397,12 @@ export class resizer extends Plugin {
|
|
|
396
397
|
})
|
|
397
398
|
.off(element, 'changesize')
|
|
398
399
|
.on(element, 'changesize', () => {
|
|
399
|
-
|
|
400
|
-
|
|
400
|
+
// Only a resize sets the embed's size, and as the plain numbers
|
|
401
|
+
// the attributes take, so a click leaves `width="100%"` alone
|
|
402
|
+
if (this.isResizeMode) {
|
|
403
|
+
attr(iframe, 'width', element.offsetWidth);
|
|
404
|
+
attr(iframe, 'height', element.offsetHeight);
|
|
405
|
+
}
|
|
401
406
|
});
|
|
402
407
|
}
|
|
403
408
|
this.j.e.on(element, 'dragstart', this.hide);
|
|
@@ -52,10 +52,20 @@ export declare class selectCells extends Plugin {
|
|
|
52
52
|
* Execute custom commands for table
|
|
53
53
|
*/
|
|
54
54
|
private onExecCommand;
|
|
55
|
+
/**
|
|
56
|
+
* The cell that takes the caret when rows or columns are deleted: the next
|
|
57
|
+
* one in the same column (or row) that is not deleted, else the previous one
|
|
58
|
+
*/
|
|
59
|
+
private __nearestCell;
|
|
55
60
|
/**
|
|
56
61
|
* Add some align after native command
|
|
57
62
|
*/
|
|
58
63
|
private onAfterCommand;
|
|
64
|
+
/**
|
|
65
|
+
* Remove the table and put the caret where it stood: at the start of what
|
|
66
|
+
* follows it, else at the end of what precedes it
|
|
67
|
+
*/
|
|
68
|
+
private __removeTable;
|
|
59
69
|
/** @override */
|
|
60
70
|
protected beforeDestruct(jodit: IJodit): void;
|
|
61
71
|
}
|
|
@@ -17,7 +17,7 @@ import { KEY_TAB } from "../../core/constants.js";
|
|
|
17
17
|
import { autobind, watch } from "../../core/decorators/index.js";
|
|
18
18
|
import { Dom } from "../../core/dom/dom.js";
|
|
19
19
|
import { pluginSystem } from "../../core/global.js";
|
|
20
|
-
import { $$, alignElement, position } from "../../core/helpers/index.js";
|
|
20
|
+
import { $$, alignElement, position, toArray } from "../../core/helpers/index.js";
|
|
21
21
|
import { Plugin } from "../../core/plugin/index.js";
|
|
22
22
|
import { Table } from "../../modules/table/table.js";
|
|
23
23
|
import "./config.js";
|
|
@@ -316,15 +316,23 @@ export class selectCells extends Plugin {
|
|
|
316
316
|
this.__tableModule.mergeSelected(table);
|
|
317
317
|
break;
|
|
318
318
|
case 'empty':
|
|
319
|
-
cells.forEach(td =>
|
|
319
|
+
cells.forEach(td => {
|
|
320
|
+
Dom.detach(td);
|
|
321
|
+
Dom.append(td, this.j.createInside.element('br'));
|
|
322
|
+
});
|
|
320
323
|
break;
|
|
321
324
|
case 'bin':
|
|
322
|
-
|
|
325
|
+
this.__removeTable(table);
|
|
323
326
|
break;
|
|
324
327
|
case 'binrow':
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
+
{
|
|
329
|
+
const rows = new Set(cells.map(td => td.parentNode));
|
|
330
|
+
const next = this.__nearestCell(table, cell, toArray(rows).map(row => row.rowIndex), true);
|
|
331
|
+
rows.forEach(row => {
|
|
332
|
+
this.__tableModule.removeRow(table, row.rowIndex);
|
|
333
|
+
});
|
|
334
|
+
next && this.j.s.setCursorIn(next);
|
|
335
|
+
}
|
|
328
336
|
break;
|
|
329
337
|
case 'bincolumn':
|
|
330
338
|
{
|
|
@@ -337,11 +345,13 @@ export class selectCells extends Plugin {
|
|
|
337
345
|
columnsSet.add(col);
|
|
338
346
|
}
|
|
339
347
|
});
|
|
348
|
+
const next = this.__nearestCell(table, cell, columns, false);
|
|
340
349
|
columns
|
|
341
350
|
.sort((a, b) => b - a)
|
|
342
351
|
.forEach(col => {
|
|
343
352
|
this.__tableModule.removeColumn(table, col);
|
|
344
353
|
});
|
|
354
|
+
next && this.j.s.setCursorIn(next);
|
|
345
355
|
}
|
|
346
356
|
break;
|
|
347
357
|
case 'addcolumnafter':
|
|
@@ -353,10 +363,39 @@ export class selectCells extends Plugin {
|
|
|
353
363
|
this.__tableModule.appendRow(table, cell.parentNode, command === 'addrowafter');
|
|
354
364
|
break;
|
|
355
365
|
}
|
|
366
|
+
if (/^bin(row|column)$/.test(command) &&
|
|
367
|
+
!this.__tableModule.getColumnsCount(table)) {
|
|
368
|
+
this.__removeTable(table);
|
|
369
|
+
}
|
|
356
370
|
}
|
|
357
371
|
return false;
|
|
358
372
|
}
|
|
359
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* The cell that takes the caret when rows or columns are deleted: the next
|
|
376
|
+
* one in the same column (or row) that is not deleted, else the previous one
|
|
377
|
+
*/
|
|
378
|
+
__nearestCell(table, cell, deleted, isRow) {
|
|
379
|
+
const box = this.__tableModule.formalMatrix(table);
|
|
380
|
+
const [row, column] = this.__tableModule.formalCoordinate(table, cell);
|
|
381
|
+
const start = isRow ? row : column;
|
|
382
|
+
const count = isRow ? box.length : box[row].length;
|
|
383
|
+
const find = (i) => (!deleted.includes(i) && (isRow ? box[i][column] : box[row][i])) ||
|
|
384
|
+
null;
|
|
385
|
+
for (let i = start + 1; i < count; i += 1) {
|
|
386
|
+
const next = find(i);
|
|
387
|
+
if (next) {
|
|
388
|
+
return next;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (let i = start - 1; i >= 0; i -= 1) {
|
|
392
|
+
const prev = find(i);
|
|
393
|
+
if (prev) {
|
|
394
|
+
return prev;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
360
399
|
/**
|
|
361
400
|
* Add some align after native command
|
|
362
401
|
*/
|
|
@@ -367,6 +406,15 @@ export class selectCells extends Plugin {
|
|
|
367
406
|
.forEach(elm => alignElement(command, elm));
|
|
368
407
|
}
|
|
369
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Remove the table and put the caret where it stood: at the start of what
|
|
411
|
+
* follows it, else at the end of what precedes it
|
|
412
|
+
*/
|
|
413
|
+
__removeTable(table) {
|
|
414
|
+
const parent = table.parentNode, next = Dom.findNotEmptySibling(table, false), prev = Dom.findNotEmptySibling(table, true);
|
|
415
|
+
Dom.safeRemove(table);
|
|
416
|
+
this.j.s.setCursorIn(next || prev || parent, Boolean(next));
|
|
417
|
+
}
|
|
370
418
|
/** @override */
|
|
371
419
|
beforeDestruct(jodit) {
|
|
372
420
|
this.__onRemoveSelection();
|
|
@@ -16,6 +16,44 @@ Config.prototype.table = {
|
|
|
16
16
|
useExtraClassesOptions: false
|
|
17
17
|
};
|
|
18
18
|
Icon.set('table', tableIcon);
|
|
19
|
+
/**
|
|
20
|
+
* Elements in `dtd.blockLimits` that hold only rows, cells or list items, so
|
|
21
|
+
* a table can't go directly inside them
|
|
22
|
+
*/
|
|
23
|
+
const ROW_AND_ITEM_HOLDERS = new Set([
|
|
24
|
+
'table',
|
|
25
|
+
'thead',
|
|
26
|
+
'tbody',
|
|
27
|
+
'tfoot',
|
|
28
|
+
'tr',
|
|
29
|
+
'ul',
|
|
30
|
+
'ol',
|
|
31
|
+
'dl'
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* The nearest block around the cursor that can hold the new table: an element
|
|
35
|
+
* in `dtd.blockLimits`, such as a table cell, a list item or a `<div>`. The
|
|
36
|
+
* cursor's own block of the `enter` tag is a line, not a container, as in the
|
|
37
|
+
* dtd plugin's `checkBlockNesting`
|
|
38
|
+
*/
|
|
39
|
+
function findTableContainer(editor, current) {
|
|
40
|
+
const line = Dom.closest(current, Dom.isBlock, editor.editor);
|
|
41
|
+
const container = Dom.closest(current, node => {
|
|
42
|
+
var _a;
|
|
43
|
+
if (!Dom.isElement(node) ||
|
|
44
|
+
(node === line && Dom.isTag(node, editor.o.enter))) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const tag = node.nodeName.toLowerCase();
|
|
48
|
+
return (Boolean((_a = editor.o.dtd) === null || _a === void 0 ? void 0 : _a.blockLimits[tag]) &&
|
|
49
|
+
!ROW_AND_ITEM_HOLDERS.has(tag));
|
|
50
|
+
}, editor.editor);
|
|
51
|
+
// With the cursor between two rows, `current` is the next row's text
|
|
52
|
+
return container &&
|
|
53
|
+
Dom.isOrContains(container, editor.s.range.startContainer)
|
|
54
|
+
? container
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
19
57
|
Config.prototype.controls.table = {
|
|
20
58
|
data: {
|
|
21
59
|
cols: 10,
|
|
@@ -129,7 +167,13 @@ Config.prototype.controls.table = {
|
|
|
129
167
|
editor.s.removeMarkers();
|
|
130
168
|
editor.editor.normalize();
|
|
131
169
|
editor.history.snapshot.restore(snapshot);
|
|
132
|
-
const
|
|
170
|
+
const current = editor.s.current();
|
|
171
|
+
// The table goes into the cell, list item or box holding the
|
|
172
|
+
// cursor, and only a block inside it is split
|
|
173
|
+
const container = findTableContainer(editor, current);
|
|
174
|
+
const block = container === current
|
|
175
|
+
? null
|
|
176
|
+
: Dom.furthest(current, Dom.isBlock, container !== null && container !== void 0 ? container : editor.editor);
|
|
133
177
|
if (block && Dom.isEmpty(block)) {
|
|
134
178
|
Dom.replace(block, table, undefined, false, true);
|
|
135
179
|
}
|
|
@@ -155,6 +199,12 @@ Config.prototype.controls.table = {
|
|
|
155
199
|
}
|
|
156
200
|
}
|
|
157
201
|
}
|
|
202
|
+
else if (container &&
|
|
203
|
+
!Dom.isEmpty(container) &&
|
|
204
|
+
!editor.o.table.splitBlockOnInsertTable) {
|
|
205
|
+
// With no block to go after, the table goes after the text
|
|
206
|
+
Dom.append(container, table);
|
|
207
|
+
}
|
|
158
208
|
else {
|
|
159
209
|
editor.s.insertNode(table, false);
|
|
160
210
|
}
|
|
@@ -3,23 +3,13 @@
|
|
|
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,
|
|
6
|
+
import { call, convertMediaUrlToVideoEmbed, prepareEmbedCode, prepareMedia } 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
|
-
}
|
|
23
13
|
function insertVideos(data) {
|
|
24
14
|
var _a;
|
|
25
15
|
(_a = data.files) === null || _a === void 0 ? void 0 : _a.forEach(file => {
|
|
@@ -29,16 +19,6 @@ function insertVideos(data) {
|
|
|
29
19
|
})));
|
|
30
20
|
});
|
|
31
21
|
}
|
|
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
|
-
}
|
|
42
22
|
Config.prototype.video = {
|
|
43
23
|
parseUrlToVideoEmbed: convertMediaUrlToVideoEmbed,
|
|
44
24
|
defaultWidth: 400,
|
|
@@ -107,7 +87,11 @@ Config.prototype.controls.video = {
|
|
|
107
87
|
formCode.onSubmit(data => {
|
|
108
88
|
insertCode(data.code);
|
|
109
89
|
});
|
|
110
|
-
|
|
90
|
+
const box = TabsWidget(jodit, tabs);
|
|
91
|
+
// Its tabs are sized to their labels, as the file selector's are, since
|
|
92
|
+
// "Upload" doesn't fit a third of the popup
|
|
93
|
+
box.classList.add('jodit-file-selector');
|
|
94
|
+
return box;
|
|
111
95
|
},
|
|
112
96
|
tags: ['iframe'],
|
|
113
97
|
tooltip: 'Insert youtube/vimeo video'
|