jodit 4.12.22 → 4.12.24
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 +33 -0
- package/es2015/jodit.css +1 -1
- package/es2015/jodit.fat.min.js +11 -11
- package/es2015/jodit.js +353 -86
- package/es2015/jodit.min.js +11 -11
- 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 +11 -11
- package/es2018/jodit.min.js +15 -15
- 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 +13 -13
- package/es2021/jodit.js +343 -85
- package/es2021/jodit.min.js +13 -13
- 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 +14 -14
- package/es2021.en/jodit.js +321 -63
- package/es2021.en/jodit.min.js +13 -13
- 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 +443 -153
- 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 +30 -0
- package/esm/core/component/view-component.js +8 -0
- package/esm/core/constants.js +1 -1
- package/esm/core/helpers/checker/is-html-from-word.d.ts +1 -1
- package/esm/core/helpers/checker/is-html-from-word.js +12 -1
- package/esm/core/selection/style/api/wrap.js +18 -3
- package/esm/core/ui/popup/popup.js +4 -1
- package/esm/jodit.js +12 -0
- package/esm/modules/image-editor/image-editor.js +5 -0
- package/esm/modules/toolbar/button/content.d.ts +6 -0
- package/esm/modules/toolbar/button/content.js +26 -1
- package/esm/modules/uploader/helpers/send-files.js +9 -0
- package/esm/plugins/add-new-line/add-new-line.js +4 -1
- package/esm/plugins/ai-assistant/ai-assistant.js +7 -1
- package/esm/plugins/backspace/backspace.js +7 -3
- package/esm/plugins/backspace/cases/check-join-neighbors.js +15 -1
- package/esm/plugins/backspace/cases/index.d.ts +19 -3
- package/esm/plugins/backspace/cases/index.js +18 -13
- package/esm/plugins/backspace/config.d.ts +15 -0
- package/esm/plugins/clean-html/clean-html.d.ts +8 -0
- package/esm/plugins/clean-html/clean-html.js +16 -0
- package/esm/plugins/clean-html/config.d.ts +9 -0
- package/esm/plugins/clean-html/config.js +1 -0
- package/esm/plugins/color/color.js +21 -21
- package/esm/plugins/font/config.js +19 -0
- package/esm/plugins/inline-popup/inline-popup.d.ts +7 -0
- package/esm/plugins/inline-popup/inline-popup.js +18 -2
- package/esm/plugins/link/config.d.ts +6 -0
- package/esm/plugins/link/config.js +1 -0
- package/esm/plugins/link/link.js +8 -1
- package/esm/plugins/link/template.js +10 -1
- package/esm/plugins/paste/config.js +11 -2
- package/esm/plugins/search/ui/search.js +22 -12
- package/esm/plugins/select-cells/select-cells.d.ts +1 -1
- package/esm/plugins/select-cells/select-cells.js +39 -8
- package/esm/types/uploader.d.ts +23 -0
- package/package.json +1 -1
- package/types/config.d.ts +30 -0
- package/types/core/helpers/checker/is-html-from-word.d.ts +1 -1
- package/types/modules/toolbar/button/content.d.ts +6 -0
- package/types/plugins/backspace/cases/index.d.ts +19 -3
- package/types/plugins/backspace/config.d.ts +15 -0
- package/types/plugins/clean-html/clean-html.d.ts +8 -0
- package/types/plugins/clean-html/config.d.ts +9 -0
- package/types/plugins/inline-popup/inline-popup.d.ts +7 -0
- package/types/plugins/link/config.d.ts +6 -0
- package/types/plugins/select-cells/select-cells.d.ts +1 -1
- package/types/types/uploader.d.ts +23 -0
package/es5/polyfills.fat.min.js
CHANGED
package/es5/polyfills.js
CHANGED
package/es5/polyfills.min.js
CHANGED
package/esm/config.d.ts
CHANGED
|
@@ -956,6 +956,21 @@ interface Config {
|
|
|
956
956
|
backspaceWord: string[];
|
|
957
957
|
backspaceSentence: string[];
|
|
958
958
|
};
|
|
959
|
+
/**
|
|
960
|
+
* Disable specific Backspace/Delete cleanup cases by their stable
|
|
961
|
+
* key, so the plugin no longer applies that particular behavior.
|
|
962
|
+
* Available keys: `remove-unbreakable`, `remove-not-editable`,
|
|
963
|
+
* `remove-char`, `table-cell`, `remove-empty-parent`,
|
|
964
|
+
* `remove-empty-neighbor`, `join-two-lists`, `join-neighbors`,
|
|
965
|
+
* `unwrap-first-list-item`.
|
|
966
|
+
*
|
|
967
|
+
* ```javascript
|
|
968
|
+
* Jodit.make('#editor', {
|
|
969
|
+
* delete: { disableCases: new Set(['remove-empty-parent']) }
|
|
970
|
+
* });
|
|
971
|
+
* ```
|
|
972
|
+
*/
|
|
973
|
+
disableCases?: Set<string>;
|
|
959
974
|
};
|
|
960
975
|
}
|
|
961
976
|
interface Config {
|
|
@@ -973,6 +988,15 @@ interface Config {
|
|
|
973
988
|
* Remove empty elements
|
|
974
989
|
*/
|
|
975
990
|
removeEmptyElements: boolean;
|
|
991
|
+
/**
|
|
992
|
+
* Return an empty string from `editor.value` (and the synced source
|
|
993
|
+
* element) when the editor holds only a single empty block — e.g.
|
|
994
|
+
* `<p><br></p>` left after the user deletes all the content.
|
|
995
|
+
* `contenteditable` keeps that caret container in the DOM, so by
|
|
996
|
+
* default the value getter returns it as-is; enable this to collapse
|
|
997
|
+
* it to `''` for form submission.
|
|
998
|
+
*/
|
|
999
|
+
collapseEmptyValueToEmptyString: boolean;
|
|
976
1000
|
/**
|
|
977
1001
|
* Replace old tags to new eg. <i> to <em>, <b> to <strong>
|
|
978
1002
|
*/
|
|
@@ -1371,6 +1395,12 @@ interface Config {
|
|
|
1371
1395
|
* Default value for the `Open in new tab` checkbox when inserting a new link.
|
|
1372
1396
|
*/
|
|
1373
1397
|
openInNewTabCheckboxDefaultChecked: boolean;
|
|
1398
|
+
/**
|
|
1399
|
+
* Show an `aria-label` text input in the link dialog so an
|
|
1400
|
+
* accessible name can be set on the `<a>` (useful when several
|
|
1401
|
+
* links share the same visible text, e.g. "here"). Default: false.
|
|
1402
|
+
*/
|
|
1403
|
+
ariaLabelInput: boolean;
|
|
1374
1404
|
/**
|
|
1375
1405
|
* Use an input text to ask the classname or a select or not ask
|
|
1376
1406
|
*/
|
|
@@ -22,6 +22,14 @@ export class ViewComponent extends Component {
|
|
|
22
22
|
*/
|
|
23
23
|
setParentView(jodit) {
|
|
24
24
|
this.jodit = jodit;
|
|
25
|
+
// Inherit the owner window from the parent view — for an editor
|
|
26
|
+
// created with a custom `ownerWindow` (e.g. inside an iframe) the
|
|
27
|
+
// component default (the global `window`) is wrong: outside-click
|
|
28
|
+
// handlers of dropdowns/popups listened to the wrong window. See
|
|
29
|
+
// https://github.com/xdan/jodit/issues/965
|
|
30
|
+
if (jodit.ow) {
|
|
31
|
+
this.ownerWindow = jodit.ow;
|
|
32
|
+
}
|
|
25
33
|
jodit.components.add(this);
|
|
26
34
|
return this;
|
|
27
35
|
}
|
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.24";
|
|
7
7
|
// prettier-ignore
|
|
8
8
|
export const ES = "es2020";
|
|
9
9
|
export const IS_ES_MODERN = true;
|
|
@@ -7,10 +7,21 @@
|
|
|
7
7
|
* @module helpers/checker
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* Detect if string is HTML from MS Word or
|
|
10
|
+
* Detect if string is HTML from MS Word, Excel or LibreOffice/OpenOffice
|
|
11
11
|
*/
|
|
12
12
|
export function isHtmlFromWord(data) {
|
|
13
13
|
return (data.search(/<meta.*?Microsoft Excel\s[\d].*?>/) !== -1 ||
|
|
14
14
|
data.search(/<meta.*?Microsoft Word\s[\d].*?>/) !== -1 ||
|
|
15
|
+
// `<meta name=ProgId content=Word.Document>` — attribute values are
|
|
16
|
+
// unquoted in the raw Word clipboard fragment
|
|
17
|
+
data.search(/<meta[^>]*?ProgId[^>]*?(Word|Excel)\./i) !== -1 ||
|
|
18
|
+
// LibreOffice/OpenOffice Writer & Calc
|
|
19
|
+
data.search(/<meta[^>]*?(LibreOffice|OpenOffice)/i) !== -1 ||
|
|
20
|
+
// Office namespaces on the root element of the clipboard fragment
|
|
21
|
+
data.search(/urn:schemas-microsoft-com:office:(word|excel)/) !== -1 ||
|
|
22
|
+
// `class=MsoNormal` and friends (unquoted/quoted)
|
|
23
|
+
data.search(/<\w[^>]*\sclass=("|')?Mso/) !== -1 ||
|
|
24
|
+
// the raw Word clipboard uses SINGLE quotes: style='mso-…'
|
|
25
|
+
data.search(/style='[^']*mso-/) !== -1 ||
|
|
15
26
|
(data.search(/style="[^"]*mso-/) !== -1 && data.search(/<font/) !== -1));
|
|
16
27
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Dom } from "../../../dom/index.js";
|
|
7
7
|
import { attr } from "../../../helpers/utils/attr.js";
|
|
8
|
+
import { css } from "../../../helpers/utils/css.js";
|
|
8
9
|
import { wrapList } from "./list/wrap-list.js";
|
|
9
10
|
import { wrapUnwrappedText } from "./wrap-unwrapped-text.js";
|
|
10
11
|
/**
|
|
@@ -13,9 +14,23 @@ import { wrapUnwrappedText } from "./wrap-unwrapped-text.js";
|
|
|
13
14
|
*/
|
|
14
15
|
export function wrap(commitStyle, font, jodit) {
|
|
15
16
|
const wrapper = findOrCreateWrapper(commitStyle, font, jodit);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (commitStyle.elementIsList) {
|
|
18
|
+
return wrapList(commitStyle, wrapper, jodit);
|
|
19
|
+
}
|
|
20
|
+
const newWrapper = Dom.replace(wrapper, commitStyle.element, jodit.createInside, true);
|
|
21
|
+
if (commitStyle.elementIsBlock) {
|
|
22
|
+
// Inline font styles left over from pasted content visually override
|
|
23
|
+
// the new block format — e.g. an h2 with `font-weight: normal;
|
|
24
|
+
// font-size: 24px` does not look like a heading at all, so the
|
|
25
|
+
// command seems to do nothing. See
|
|
26
|
+
// https://github.com/xdan/jodit/issues/1063
|
|
27
|
+
css(newWrapper, 'fontSize', null);
|
|
28
|
+
css(newWrapper, 'fontWeight', null);
|
|
29
|
+
if (!attr(newWrapper, 'style')) {
|
|
30
|
+
attr(newWrapper, 'style', null);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return newWrapper;
|
|
19
34
|
}
|
|
20
35
|
const WRAP_NODES = new Set([
|
|
21
36
|
'td',
|
|
@@ -137,8 +137,11 @@ export class Popup extends UIGroup {
|
|
|
137
137
|
* Calculate static bound for point
|
|
138
138
|
*/
|
|
139
139
|
getKeepBound(getBound) {
|
|
140
|
+
var _a;
|
|
140
141
|
const oldBound = getBound();
|
|
141
|
-
|
|
142
|
+
// Inside Shadow DOM `document.elementFromPoint` returns the shadow
|
|
143
|
+
// host, so the lookup must start from the shadow root
|
|
144
|
+
const elmUnderCursor = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.od).elementFromPoint(oldBound.left, oldBound.top);
|
|
142
145
|
if (!elmUnderCursor) {
|
|
143
146
|
return getBound;
|
|
144
147
|
}
|
package/esm/jodit.js
CHANGED
|
@@ -1073,6 +1073,18 @@ let Jodit = Jodit_1 = class Jodit extends ViewWithToolbar {
|
|
|
1073
1073
|
(opt.uploader.url || opt.uploader.insertImageAsBase64URI)) {
|
|
1074
1074
|
this.uploader.bind(this.editor);
|
|
1075
1075
|
}
|
|
1076
|
+
else if (!opt.enableDragAndDropFileToEditor) {
|
|
1077
|
+
// Without a bound uploader nobody cancels a file drop, and
|
|
1078
|
+
// Firefox inserts the dropped image natively — the option
|
|
1079
|
+
// must mean "do nothing". See
|
|
1080
|
+
// https://github.com/xdan/jodit/issues/1077
|
|
1081
|
+
this.e.on(editor, 'drop', (e) => {
|
|
1082
|
+
var _a, _b;
|
|
1083
|
+
if ((_b = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b.length) {
|
|
1084
|
+
e.preventDefault();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1076
1088
|
// in initEditor - the editor could change
|
|
1077
1089
|
if (!this.__elementToPlace.get(this.editor)) {
|
|
1078
1090
|
this.__elementToPlace.set(this.editor, currentPlace);
|
|
@@ -439,12 +439,17 @@ let ImageEditor = ImageEditor_1 = class ImageEditor extends ViewComponent {
|
|
|
439
439
|
self.j.alert('The name should not be empty');
|
|
440
440
|
return false;
|
|
441
441
|
}
|
|
442
|
+
self.j.e.fire('afterImageEditorSave', data, name);
|
|
442
443
|
self.onSave(name, data, self.hide, (e) => {
|
|
443
444
|
self.j.alert(e.message);
|
|
444
445
|
});
|
|
445
446
|
});
|
|
446
447
|
break;
|
|
447
448
|
case self.buttons.save:
|
|
449
|
+
// Notify listeners that a crop/resize was applied,
|
|
450
|
+
// passing the action box (action + box dimensions).
|
|
451
|
+
// See https://github.com/xdan/jodit/issues/820
|
|
452
|
+
self.j.e.fire('afterImageEditorSave', data);
|
|
448
453
|
self.onSave(undefined, data, self.hide, (e) => {
|
|
449
454
|
self.j.alert(e.message);
|
|
450
455
|
});
|
|
@@ -15,6 +15,12 @@ export declare class ToolbarContent<T extends IViewBased = IViewBased> extends U
|
|
|
15
15
|
className(): string;
|
|
16
16
|
/** @override */
|
|
17
17
|
update(): void;
|
|
18
|
+
/**
|
|
19
|
+
* The content is arbitrary HTML — propagate the disabled state to the
|
|
20
|
+
* nested form controls (e.g. the file input of the Upload button),
|
|
21
|
+
* otherwise they stay interactive.
|
|
22
|
+
*/
|
|
23
|
+
protected onChangeDisabled(): void;
|
|
18
24
|
/** @override */
|
|
19
25
|
protected createContainer(): HTMLElement;
|
|
20
26
|
constructor(jodit: T, control: IControlTypeContent, target?: Nullable<HTMLElement>);
|
|
@@ -24,13 +24,38 @@ let ToolbarContent = class ToolbarContent extends UIButton {
|
|
|
24
24
|
}
|
|
25
25
|
/** @override */
|
|
26
26
|
update() {
|
|
27
|
-
|
|
27
|
+
var _a, _b, _c;
|
|
28
|
+
const { control } = this;
|
|
29
|
+
const content = control.getContent(this.j, this);
|
|
28
30
|
if (isString(content) || content.parentNode !== this.container) {
|
|
29
31
|
Dom.detach(this.container);
|
|
30
32
|
this.container.appendChild(isString(content) ? this.j.create.fromHTML(content) : content);
|
|
31
33
|
}
|
|
34
|
+
// Content controls never went through the ToolbarButton status
|
|
35
|
+
// calculation, so `isDisabled`/`isActive`/`update` declared on the
|
|
36
|
+
// control were silently ignored (e.g. the FileBrowser Upload button
|
|
37
|
+
// ignored the backend permissions). See
|
|
38
|
+
// https://github.com/xdan/jodit/issues/1094
|
|
39
|
+
this.state.disabled = Boolean((_a = control.isDisabled) === null || _a === void 0 ? void 0 : _a.call(control, this.j, this));
|
|
40
|
+
this.state.activated = Boolean((_b = control.isActive) === null || _b === void 0 ? void 0 : _b.call(control, this.j, this));
|
|
41
|
+
(_c = control.update) === null || _c === void 0 ? void 0 : _c.call(control, this.j, this);
|
|
42
|
+
// The first update() runs before the state watchers are attached, so
|
|
43
|
+
// apply the calculated state explicitly (the calls are idempotent)
|
|
44
|
+
this.onChangeDisabled();
|
|
45
|
+
this.onChangeActivated();
|
|
32
46
|
super.update();
|
|
33
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The content is arbitrary HTML — propagate the disabled state to the
|
|
50
|
+
* nested form controls (e.g. the file input of the Upload button),
|
|
51
|
+
* otherwise they stay interactive.
|
|
52
|
+
*/
|
|
53
|
+
onChangeDisabled() {
|
|
54
|
+
super.onChangeDisabled();
|
|
55
|
+
this.container
|
|
56
|
+
.querySelectorAll('input,button,select,textarea')
|
|
57
|
+
.forEach(elm => attr(elm, 'disabled', this.state.disabled || null));
|
|
58
|
+
}
|
|
34
59
|
/** @override */
|
|
35
60
|
createContainer() {
|
|
36
61
|
return this.j.c.span(this.componentName);
|
|
@@ -17,6 +17,15 @@ export function sendFiles(uploader, files, handlerSuccess, handlerError, process
|
|
|
17
17
|
if (!fileList.length) {
|
|
18
18
|
return Promise.reject(error('Need files'));
|
|
19
19
|
}
|
|
20
|
+
// Client-side validation hook — returning `false` aborts the upload.
|
|
21
|
+
// See https://github.com/xdan/jodit/issues/1329
|
|
22
|
+
if (isFunction(o.beforeUpload)) {
|
|
23
|
+
if (o.beforeUpload.call(uploader, fileList) === false) {
|
|
24
|
+
const err = error('Upload canceled');
|
|
25
|
+
(handlerError || o.defaultHandlerError).call(uploader, err);
|
|
26
|
+
return Promise.reject(err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
const promises = [];
|
|
21
30
|
if (o.insertImageAsBase64URI) {
|
|
22
31
|
readImagesWithReader(fileList, o.imagesExtensions, promises, uploader, handlerSuccess, o.defaultHandlerSuccess);
|
|
@@ -151,8 +151,11 @@ export class addNewLine extends Plugin {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
__onMouseMove(e) {
|
|
154
|
+
var _a;
|
|
154
155
|
const editor = this.j;
|
|
155
|
-
|
|
156
|
+
// Inside Shadow DOM `document.elementFromPoint` returns the shadow
|
|
157
|
+
// host, so the lookup must start from the shadow root
|
|
158
|
+
let currentElement = ((_a = editor.o.shadowRoot) !== null && _a !== void 0 ? _a : editor.ed).elementFromPoint(e.clientX, e.clientY);
|
|
156
159
|
if (!Dom.isHTMLElement(currentElement) ||
|
|
157
160
|
!Dom.isOrContains(editor.editor, currentElement)) {
|
|
158
161
|
return;
|
|
@@ -40,7 +40,13 @@ export class aiAssistant extends Plugin {
|
|
|
40
40
|
return new UiAiAssistant(jodit, {
|
|
41
41
|
onInsertAfter(html) {
|
|
42
42
|
jodit.s.focus();
|
|
43
|
-
|
|
43
|
+
// `current()` returns a node of the selection START, so with
|
|
44
|
+
// several selected paragraphs the result landed after the
|
|
45
|
+
// first one — collapse the range to the END of the selection
|
|
46
|
+
// instead. See https://github.com/xdan/jodit/issues/1263
|
|
47
|
+
const range = jodit.s.range;
|
|
48
|
+
range.collapse(false);
|
|
49
|
+
jodit.s.selectRange(range);
|
|
44
50
|
jodit.s.insertHTML(html);
|
|
45
51
|
__dialog.close();
|
|
46
52
|
},
|
|
@@ -11,7 +11,7 @@ import { Plugin } from "../../core/plugin/index.js";
|
|
|
11
11
|
import { moveNodeInsideStart } from "../../core/selection/helpers/index.js";
|
|
12
12
|
import "./config.js";
|
|
13
13
|
import { checkNotCollapsed } from "./cases/check-not-collapsed.js";
|
|
14
|
-
import {
|
|
14
|
+
import { casesMap } from "./cases/index.js";
|
|
15
15
|
export class backspace extends Plugin {
|
|
16
16
|
afterInit(jodit) {
|
|
17
17
|
jodit
|
|
@@ -71,11 +71,15 @@ export class backspace extends Plugin {
|
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
moveNodeInsideStart(jodit, fakeNode, backspace);
|
|
74
|
-
|
|
74
|
+
const disabled = jodit.o.delete.disableCases;
|
|
75
|
+
if (casesMap.some(([key, func]) => {
|
|
76
|
+
if (disabled && disabled.has(key)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
75
79
|
if (isFunction(func) &&
|
|
76
80
|
func(jodit, fakeNode, backspace, mode)) {
|
|
77
81
|
if (!IS_PROD) {
|
|
78
|
-
console.info('Remove case:',
|
|
82
|
+
console.info('Remove case:', key);
|
|
79
83
|
}
|
|
80
84
|
return true;
|
|
81
85
|
}
|
|
@@ -32,13 +32,27 @@ export function checkJoinNeighbors(jodit, fakeNode, backspace) {
|
|
|
32
32
|
}
|
|
33
33
|
if (sibling &&
|
|
34
34
|
(checkMoveListContent(jodit, mainClosestBox, sibling, backspace) ||
|
|
35
|
-
moveContentAndRemoveEmpty(jodit, mainClosestBox, sibling, backspace))) {
|
|
35
|
+
moveContentAndRemoveEmpty(jodit, mainClosestBox, resolveTableSibling(sibling, backspace), backspace))) {
|
|
36
36
|
jodit.s.setCursorBefore(fakeNode);
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Content cannot be merged into the `<table>` element itself — it would land
|
|
44
|
+
* between the table sections (after `</tbody>`), which is invalid HTML and
|
|
45
|
+
* gets foster-parented out of the table on the next parse. Merge into the
|
|
46
|
+
* edge cell instead. See https://github.com/xdan/jodit/issues/1064
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
function resolveTableSibling(sibling, backspace) {
|
|
50
|
+
if (!Dom.isTag(sibling, 'table')) {
|
|
51
|
+
return sibling;
|
|
52
|
+
}
|
|
53
|
+
const cells = sibling.querySelectorAll('td,th');
|
|
54
|
+
return cells.length ? cells[backspace ? cells.length - 1 : 0] : null;
|
|
55
|
+
}
|
|
42
56
|
function checkMoveListContent(jodit, mainClosestBox, sibling, backspace) {
|
|
43
57
|
// Process UL/LI/OL cases
|
|
44
58
|
const siblingIsList = Dom.isTag(sibling, LIST_TAGS);
|
|
@@ -3,9 +3,25 @@
|
|
|
3
3
|
* Released under MIT see LICENSE.txt in the project root for license information.
|
|
4
4
|
* Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @module plugins/backspace
|
|
8
|
+
*/
|
|
9
|
+
import type { IJodit } from "../../../types/index";
|
|
10
|
+
import type { DeleteMode } from "../interface";
|
|
11
|
+
type CaseFn = (jodit: IJodit, fakeNode: any, backspace: boolean, mode: DeleteMode) => void | boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Ordered delete/backspace cases with stable keys. The first one returning
|
|
14
|
+
* `true` wins. The keys are stable across minified builds (function names are
|
|
15
|
+
* mangled by terser) so they can be referenced by `delete.disableCases`.
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
export declare const casesMap: ReadonlyArray<readonly [
|
|
19
|
+
string,
|
|
20
|
+
CaseFn
|
|
21
|
+
]>;
|
|
8
22
|
/**
|
|
9
23
|
* @private
|
|
24
|
+
* @deprecated Use `casesMap` to also get the stable case keys.
|
|
10
25
|
*/
|
|
11
|
-
export declare const cases:
|
|
26
|
+
export declare const cases: CaseFn[];
|
|
27
|
+
export {};
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
* Released under MIT see LICENSE.txt in the project root for license information.
|
|
4
4
|
* Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
7
|
-
* @module plugins/backspace
|
|
8
|
-
*/
|
|
9
6
|
import { checkJoinNeighbors } from "./check-join-neighbors.js";
|
|
10
7
|
import { checkJoinTwoLists } from "./check-join-two-lists.js";
|
|
11
8
|
import { checkRemoveChar } from "./check-remove-char.js";
|
|
@@ -16,16 +13,24 @@ import { checkRemoveUnbreakableElement } from "./check-remove-unbreakable-elemen
|
|
|
16
13
|
import { checkTableCell } from "./check-table-cell.js";
|
|
17
14
|
import { checkUnwrapFirstListItem } from "./check-unwrap-first-list-item.js";
|
|
18
15
|
/**
|
|
16
|
+
* Ordered delete/backspace cases with stable keys. The first one returning
|
|
17
|
+
* `true` wins. The keys are stable across minified builds (function names are
|
|
18
|
+
* mangled by terser) so they can be referenced by `delete.disableCases`.
|
|
19
19
|
* @private
|
|
20
20
|
*/
|
|
21
|
-
export const
|
|
22
|
-
checkRemoveUnbreakableElement,
|
|
23
|
-
checkRemoveContentNotEditable,
|
|
24
|
-
checkRemoveChar,
|
|
25
|
-
checkTableCell,
|
|
26
|
-
checkRemoveEmptyParent,
|
|
27
|
-
checkRemoveEmptyNeighbor,
|
|
28
|
-
checkJoinTwoLists,
|
|
29
|
-
checkJoinNeighbors,
|
|
30
|
-
checkUnwrapFirstListItem
|
|
21
|
+
export const casesMap = [
|
|
22
|
+
['remove-unbreakable', checkRemoveUnbreakableElement],
|
|
23
|
+
['remove-not-editable', checkRemoveContentNotEditable],
|
|
24
|
+
['remove-char', checkRemoveChar],
|
|
25
|
+
['table-cell', checkTableCell],
|
|
26
|
+
['remove-empty-parent', checkRemoveEmptyParent],
|
|
27
|
+
['remove-empty-neighbor', checkRemoveEmptyNeighbor],
|
|
28
|
+
['join-two-lists', checkJoinTwoLists],
|
|
29
|
+
['join-neighbors', checkJoinNeighbors],
|
|
30
|
+
['unwrap-first-list-item', checkUnwrapFirstListItem]
|
|
31
31
|
];
|
|
32
|
+
/**
|
|
33
|
+
* @private
|
|
34
|
+
* @deprecated Use `casesMap` to also get the stable case keys.
|
|
35
|
+
*/
|
|
36
|
+
export const cases = casesMap.map(([, fn]) => fn);
|
|
@@ -17,6 +17,21 @@ declare module 'jodit/config' {
|
|
|
17
17
|
backspaceWord: string[];
|
|
18
18
|
backspaceSentence: string[];
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Disable specific Backspace/Delete cleanup cases by their stable
|
|
22
|
+
* key, so the plugin no longer applies that particular behavior.
|
|
23
|
+
* Available keys: `remove-unbreakable`, `remove-not-editable`,
|
|
24
|
+
* `remove-char`, `table-cell`, `remove-empty-parent`,
|
|
25
|
+
* `remove-empty-neighbor`, `join-two-lists`, `join-neighbors`,
|
|
26
|
+
* `unwrap-first-list-item`.
|
|
27
|
+
*
|
|
28
|
+
* ```javascript
|
|
29
|
+
* Jodit.make('#editor', {
|
|
30
|
+
* delete: { disableCases: new Set(['remove-empty-parent']) }
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
disableCases?: Set<string>;
|
|
20
35
|
};
|
|
21
36
|
}
|
|
22
37
|
}
|
|
@@ -34,6 +34,14 @@ export declare class cleanHtml extends Plugin {
|
|
|
34
34
|
protected onBeforeSetNativeEditorValue(data: {
|
|
35
35
|
value: string;
|
|
36
36
|
}): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Collapse a value that holds only a single empty block (e.g.
|
|
39
|
+
* `<p><br></p>` left after deleting all content) to an empty string —
|
|
40
|
+
* opt-in via `cleanHTML.collapseEmptyValueToEmptyString`. See #1149
|
|
41
|
+
*/
|
|
42
|
+
protected onAfterGetValueFromEditor(data: {
|
|
43
|
+
value: string;
|
|
44
|
+
}): void;
|
|
37
45
|
protected onSafeHTML(sandBox: HTMLElement): void;
|
|
38
46
|
/** @override */
|
|
39
47
|
protected beforeDestruct(): void;
|
|
@@ -100,6 +100,19 @@ export class cleanHtml extends Plugin {
|
|
|
100
100
|
Dom.safeRemove(iframe);
|
|
101
101
|
return false;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Collapse a value that holds only a single empty block (e.g.
|
|
105
|
+
* `<p><br></p>` left after deleting all content) to an empty string —
|
|
106
|
+
* opt-in via `cleanHTML.collapseEmptyValueToEmptyString`. See #1149
|
|
107
|
+
*/
|
|
108
|
+
onAfterGetValueFromEditor(data) {
|
|
109
|
+
if (!this.j.o.cleanHTML.collapseEmptyValueToEmptyString) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (/^<([a-z][a-z0-9]*)\b[^>]*>(?:<br\/?>)?<\/\1>$/i.test(data.value.trim())) {
|
|
113
|
+
data.value = '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
103
116
|
onSafeHTML(sandBox) {
|
|
104
117
|
const sanitizer = this.j.o.cleanHTML.sanitizer;
|
|
105
118
|
if (sanitizer) {
|
|
@@ -124,6 +137,9 @@ __decorate([
|
|
|
124
137
|
__decorate([
|
|
125
138
|
watch(':beforeSetNativeEditorValue')
|
|
126
139
|
], cleanHtml.prototype, "onBeforeSetNativeEditorValue", null);
|
|
140
|
+
__decorate([
|
|
141
|
+
watch(':afterGetValueFromEditor')
|
|
142
|
+
], cleanHtml.prototype, "onAfterGetValueFromEditor", null);
|
|
127
143
|
__decorate([
|
|
128
144
|
watch(':safeHTML')
|
|
129
145
|
], cleanHtml.prototype, "onSafeHTML", null);
|
|
@@ -23,6 +23,15 @@ declare module 'jodit/config' {
|
|
|
23
23
|
* Remove empty elements
|
|
24
24
|
*/
|
|
25
25
|
removeEmptyElements: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Return an empty string from `editor.value` (and the synced source
|
|
28
|
+
* element) when the editor holds only a single empty block — e.g.
|
|
29
|
+
* `<p><br></p>` left after the user deletes all the content.
|
|
30
|
+
* `contenteditable` keeps that caret container in the DOM, so by
|
|
31
|
+
* default the value getter returns it as-is; enable this to collapse
|
|
32
|
+
* it to `''` for form submission.
|
|
33
|
+
*/
|
|
34
|
+
collapseEmptyValueToEmptyString: boolean;
|
|
26
35
|
/**
|
|
27
36
|
* Replace old tags to new eg. <i> to <em>, <b> to <strong>
|
|
28
37
|
*/
|