suneditor 3.0.0-rc.5 → 3.0.1
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/README.md +3 -2
- package/dist/suneditor-contents.min.css +1 -1
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +3 -3
- package/src/assets/design/color.css +14 -2
- package/src/assets/design/typography.css +5 -0
- package/src/assets/icons/defaultIcons.js +22 -4
- package/src/assets/suneditor-contents.css +1 -1
- package/src/assets/suneditor.css +312 -18
- package/src/core/config/eventManager.js +6 -9
- package/src/core/editor.js +1 -1
- package/src/core/event/actions/index.js +5 -0
- package/src/core/event/effects/keydown.registry.js +25 -0
- package/src/core/event/eventOrchestrator.js +69 -2
- package/src/core/event/handlers/handler_ww_mouse.js +1 -0
- package/src/core/event/rules/keydown.rule.backspace.js +9 -1
- package/src/core/kernel/coreKernel.js +4 -0
- package/src/core/kernel/store.js +2 -0
- package/src/core/logic/dom/html.js +110 -11
- package/src/core/logic/dom/offset.js +89 -35
- package/src/core/logic/dom/selection.js +46 -19
- package/src/core/logic/panel/finder.js +982 -0
- package/src/core/logic/panel/menu.js +8 -6
- package/src/core/logic/panel/toolbar.js +112 -19
- package/src/core/logic/panel/viewer.js +214 -43
- package/src/core/logic/shell/_commandExecutor.js +7 -1
- package/src/core/logic/shell/commandDispatcher.js +1 -1
- package/src/core/logic/shell/component.js +5 -7
- package/src/core/logic/shell/history.js +24 -0
- package/src/core/logic/shell/shortcuts.js +5 -3
- package/src/core/logic/shell/ui.js +25 -26
- package/src/core/schema/frameContext.js +15 -1
- package/src/core/schema/options.js +75 -16
- package/src/core/section/constructor.js +62 -21
- package/src/core/section/documentType.js +1 -1
- package/src/events.js +12 -0
- package/src/helper/clipboard.js +1 -1
- package/src/helper/dom/domUtils.js +5 -14
- package/src/helper/index.js +3 -0
- package/src/helper/markdown.js +876 -0
- package/src/langs/ckb.js +9 -0
- package/src/langs/cs.js +9 -0
- package/src/langs/da.js +9 -0
- package/src/langs/de.js +9 -0
- package/src/langs/en.js +9 -0
- package/src/langs/es.js +9 -0
- package/src/langs/fa.js +9 -0
- package/src/langs/fr.js +9 -0
- package/src/langs/he.js +9 -0
- package/src/langs/hu.js +9 -0
- package/src/langs/it.js +9 -0
- package/src/langs/ja.js +9 -0
- package/src/langs/km.js +9 -0
- package/src/langs/ko.js +9 -0
- package/src/langs/lv.js +9 -0
- package/src/langs/nl.js +9 -0
- package/src/langs/pl.js +9 -0
- package/src/langs/pt_br.js +9 -0
- package/src/langs/ro.js +9 -0
- package/src/langs/ru.js +9 -0
- package/src/langs/se.js +9 -0
- package/src/langs/tr.js +9 -0
- package/src/langs/uk.js +9 -0
- package/src/langs/ur.js +9 -0
- package/src/langs/zh_cn.js +9 -0
- package/src/modules/contract/Controller.js +50 -39
- package/src/modules/manager/ApiManager.js +27 -4
- package/src/modules/manager/FileManager.js +1 -1
- package/src/modules/ui/SelectMenu.js +22 -11
- package/src/plugins/command/codeBlock.js +324 -0
- package/src/plugins/command/exportPDF.js +15 -3
- package/src/plugins/dropdown/blockStyle.js +1 -1
- package/src/plugins/dropdown/paragraphStyle.js +1 -2
- package/src/plugins/dropdown/table/render/table.html.js +1 -1
- package/src/plugins/dropdown/table/services/table.grid.js +16 -8
- package/src/plugins/dropdown/table/services/table.style.js +5 -9
- package/src/plugins/index.js +3 -0
- package/src/plugins/input/fontSize.js +4 -2
- package/src/plugins/modal/audio.js +2 -1
- package/src/plugins/modal/image/index.js +2 -1
- package/src/plugins/modal/math.js +2 -1
- package/src/plugins/modal/video/index.js +2 -1
- package/src/themes/cobalt.css +13 -4
- package/src/themes/cream.css +44 -35
- package/src/themes/dark.css +13 -4
- package/src/themes/midnight.css +13 -4
- package/src/typedef.js +4 -4
- package/types/assets/icons/defaultIcons.d.ts +12 -1
- package/types/core/config/eventManager.d.ts +6 -8
- package/types/core/event/actions/index.d.ts +1 -0
- package/types/core/event/effects/keydown.registry.d.ts +2 -0
- package/types/core/event/eventOrchestrator.d.ts +2 -1
- package/types/core/kernel/coreKernel.d.ts +5 -0
- package/types/core/kernel/store.d.ts +5 -0
- package/types/core/logic/dom/offset.d.ts +16 -3
- package/types/core/logic/dom/selection.d.ts +3 -3
- package/types/core/logic/panel/finder.d.ts +83 -0
- package/types/core/logic/panel/toolbar.d.ts +14 -1
- package/types/core/logic/panel/viewer.d.ts +22 -2
- package/types/core/logic/shell/shortcuts.d.ts +1 -1
- package/types/core/schema/frameContext.d.ts +22 -0
- package/types/core/schema/options.d.ts +153 -31
- package/types/events.d.ts +11 -0
- package/types/helper/dom/domUtils.d.ts +2 -2
- package/types/helper/index.d.ts +5 -0
- package/types/helper/markdown.d.ts +27 -0
- package/types/langs/_Lang.d.ts +9 -0
- package/types/modules/contract/Controller.d.ts +8 -1
- package/types/modules/ui/SelectMenu.d.ts +12 -0
- package/types/plugins/command/codeBlock.d.ts +53 -0
- package/types/plugins/index.d.ts +3 -0
- package/types/plugins/input/fontSize.d.ts +6 -2
- package/types/plugins/modal/audio.d.ts +4 -2
- package/types/plugins/modal/image/index.d.ts +3 -1
- package/types/plugins/modal/math.d.ts +3 -1
- package/types/plugins/modal/video/index.d.ts +3 -1
- package/types/typedef.d.ts +5 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dom,
|
|
1
|
+
import { dom, env } from '../../helper';
|
|
2
2
|
import { _DragHandle } from '../../modules/ui';
|
|
3
3
|
|
|
4
4
|
const { _w, _d, NO_EVENT } = env;
|
|
@@ -54,9 +54,6 @@ class EventManager {
|
|
|
54
54
|
|
|
55
55
|
return NO_EVENT;
|
|
56
56
|
};
|
|
57
|
-
|
|
58
|
-
/** @type {HTMLInputElement} */
|
|
59
|
-
this.__focusTemp = contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
/**
|
|
@@ -64,7 +61,7 @@ class EventManager {
|
|
|
64
61
|
* - Only events registered with this method are unregistered or re-registered when methods such as 'setOptions', 'destroy' are called.
|
|
65
62
|
* @param {*} target Target element
|
|
66
63
|
* @param {string} type Event type
|
|
67
|
-
* @param {
|
|
64
|
+
* @param {*} listener Event handler
|
|
68
65
|
* @param {boolean|AddEventListenerOptions} [useCapture] Event useCapture option
|
|
69
66
|
* @return {?SunEditor.Event.Info} Registered event information
|
|
70
67
|
*/
|
|
@@ -85,7 +82,7 @@ class EventManager {
|
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
return {
|
|
88
|
-
target
|
|
85
|
+
target,
|
|
89
86
|
type,
|
|
90
87
|
listener,
|
|
91
88
|
useCapture,
|
|
@@ -106,7 +103,7 @@ class EventManager {
|
|
|
106
103
|
const useCapture = params.useCapture;
|
|
107
104
|
|
|
108
105
|
if (!target) return;
|
|
109
|
-
if (
|
|
106
|
+
if (target === _w || target === _d || typeof target.length !== 'number' || target.nodeType || (!Array.isArray(target) && target.length < 1)) target = [target];
|
|
110
107
|
if (target.length === 0) return;
|
|
111
108
|
|
|
112
109
|
for (let i = 0, len = target.length; i < len; i++) {
|
|
@@ -120,7 +117,7 @@ class EventManager {
|
|
|
120
117
|
* @description Add an event to document.
|
|
121
118
|
* - When created as an Iframe, the same event is added to the document in the Iframe.
|
|
122
119
|
* @param {string} type Event type
|
|
123
|
-
* @param {
|
|
120
|
+
* @param {*} listener Event listener
|
|
124
121
|
* @param {boolean|AddEventListenerOptions} [useCapture] Use event capture
|
|
125
122
|
* @return {SunEditor.Event.GlobalInfo} Registered event information
|
|
126
123
|
*/
|
|
@@ -140,7 +137,7 @@ class EventManager {
|
|
|
140
137
|
* @description Remove events from document.
|
|
141
138
|
* - When created as an Iframe, the event of the document inside the Iframe is also removed.
|
|
142
139
|
* @param {string|SunEditor.Event.GlobalInfo} type Event type or (Event info = this.addGlobalEvent())
|
|
143
|
-
* @param {
|
|
140
|
+
* @param {*} [listener] Event listener
|
|
144
141
|
* @param {boolean|AddEventListenerOptions} [useCapture] Use event capture
|
|
145
142
|
* @returns {undefined|null} Success: `null`, Not found: `undefined`
|
|
146
143
|
*/
|
package/src/core/editor.js
CHANGED
|
@@ -224,7 +224,7 @@ class Editor {
|
|
|
224
224
|
*/
|
|
225
225
|
#init(options) {
|
|
226
226
|
this.$.pluginManager.init(options);
|
|
227
|
-
this.$.shortcuts.
|
|
227
|
+
this.$.shortcuts._registerShortcuts();
|
|
228
228
|
this.$.commandDispatcher._initCommandButtons();
|
|
229
229
|
this.$.ui.init();
|
|
230
230
|
}
|
|
@@ -91,6 +91,11 @@ export const A = {
|
|
|
91
91
|
* @returns {Action}
|
|
92
92
|
*/
|
|
93
93
|
backspaceFormatMaintain: (formatEl) => ({ t: 'backspace.format.maintain', p: { formatEl } }),
|
|
94
|
+
/**
|
|
95
|
+
* @param {Element} formatEl - brLine element (e.g. PRE) to strip
|
|
96
|
+
* @returns {Action}
|
|
97
|
+
*/
|
|
98
|
+
backspaceBrLineStrip: (formatEl) => ({ t: 'backspace.brline.strip', p: { formatEl } }),
|
|
94
99
|
/**
|
|
95
100
|
* @param {Node} selectionNode
|
|
96
101
|
* @param {Range} range
|
|
@@ -30,6 +30,31 @@ export default {
|
|
|
30
30
|
|
|
31
31
|
/** [backspace] */
|
|
32
32
|
|
|
33
|
+
/** @action backspaceBrLineStrip — extract first line from brLine (PRE) */
|
|
34
|
+
'backspace.brline.strip': ({ ctx, ports }, { formatEl }) => {
|
|
35
|
+
const defaultTag = ctx.options.get('defaultLine');
|
|
36
|
+
const parent = formatEl.parentNode;
|
|
37
|
+
const newLine = dom.utils.createElement(defaultTag);
|
|
38
|
+
|
|
39
|
+
while (formatEl.firstChild && !dom.check.isBreak(formatEl.firstChild)) {
|
|
40
|
+
newLine.appendChild(formatEl.firstChild);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (formatEl.firstChild && dom.check.isBreak(formatEl.firstChild)) {
|
|
44
|
+
formatEl.removeChild(formatEl.firstChild);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!newLine.firstChild) newLine.innerHTML = '<br>';
|
|
48
|
+
parent.insertBefore(newLine, formatEl);
|
|
49
|
+
|
|
50
|
+
if (!formatEl.firstChild || (!formatEl.textContent.trim() && !formatEl.querySelector('br'))) {
|
|
51
|
+
parent.removeChild(formatEl);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const focusNode = newLine.firstChild;
|
|
55
|
+
ports.selection.setRange(focusNode, 0, focusNode, 0);
|
|
56
|
+
},
|
|
57
|
+
|
|
33
58
|
/** @action backspaceFormatMaintain */
|
|
34
59
|
'backspace.format.maintain': ({ ctx }, { formatEl }) => {
|
|
35
60
|
if (formatEl.nodeName.toUpperCase() === ctx.options.get('defaultLine').toUpperCase()) {
|
|
@@ -106,6 +106,9 @@ class EventOrchestrator extends KernelInjector {
|
|
|
106
106
|
this.__eventDoc = null;
|
|
107
107
|
/** @type {string} */
|
|
108
108
|
this.__secopy = null;
|
|
109
|
+
|
|
110
|
+
/** @type {HTMLInputElement} */
|
|
111
|
+
this.__focusTemp = this.#contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -343,6 +346,9 @@ class EventOrchestrator extends KernelInjector {
|
|
|
343
346
|
entries.forEach((e) => {
|
|
344
347
|
this.#ui._emitResizeEvent(this.$.frameRoots.get(e.target.getAttribute('data-root-key')), -1, e);
|
|
345
348
|
});
|
|
349
|
+
if (this.#store.mode.isInline && this.#store.mode.isBottom) {
|
|
350
|
+
this.#toolbar._resetSticky();
|
|
351
|
+
}
|
|
346
352
|
}, 0);
|
|
347
353
|
});
|
|
348
354
|
}
|
|
@@ -443,10 +449,35 @@ class EventOrchestrator extends KernelInjector {
|
|
|
443
449
|
this.#eventManager.addEvent(codeArea, 'keyup', cvAuthHeight, false);
|
|
444
450
|
this.#eventManager.addEvent(codeArea, 'paste', cvAuthHeight, false);
|
|
445
451
|
|
|
452
|
+
/** code view tab key */
|
|
453
|
+
if (!this.#options.get('tabDisable')) {
|
|
454
|
+
this.#eventManager.addEvent(codeArea, 'keydown', InsertTab, false);
|
|
455
|
+
}
|
|
456
|
+
|
|
446
457
|
/** code view numbers */
|
|
447
458
|
if (codeNumbers) this.#eventManager.addEvent(codeArea, 'scroll', this.$.viewer._scrollLineNumbers.bind(codeArea, codeNumbers), false);
|
|
448
459
|
}
|
|
449
460
|
|
|
461
|
+
/** markdown view area */
|
|
462
|
+
const markdownArea = fc.get('markdown');
|
|
463
|
+
if (markdownArea) {
|
|
464
|
+
this.#eventManager.addEvent(markdownArea, 'mousedown', this.#OnFocus_markdown.bind(this, fc), false);
|
|
465
|
+
|
|
466
|
+
const mdNumbers = fc.get('markdownNumbers');
|
|
467
|
+
const mdAutoHeight = this.$.viewer._markdownViewAutoHeight.bind(this.$.viewer, markdownArea, mdNumbers, this.$.frameOptions.get('height') === 'auto');
|
|
468
|
+
|
|
469
|
+
this.#eventManager.addEvent(markdownArea, 'keydown', mdAutoHeight, false);
|
|
470
|
+
this.#eventManager.addEvent(markdownArea, 'keyup', mdAutoHeight, false);
|
|
471
|
+
this.#eventManager.addEvent(markdownArea, 'paste', mdAutoHeight, false);
|
|
472
|
+
|
|
473
|
+
/** markdown view tab key */
|
|
474
|
+
if (!this.#options.get('tabDisable')) {
|
|
475
|
+
this.#eventManager.addEvent(markdownArea, 'keydown', InsertTab, false);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (mdNumbers) this.#eventManager.addEvent(markdownArea, 'scroll', this.$.viewer._scrollMarkdownLineNumbers.bind(markdownArea, mdNumbers), false);
|
|
479
|
+
}
|
|
480
|
+
|
|
450
481
|
if (fc.has('statusbar')) this.__addStatusbarEvent(fc, fc.get('options'));
|
|
451
482
|
|
|
452
483
|
const OnScrollAbs = this.#OnScroll_Abs.bind(this);
|
|
@@ -636,7 +667,16 @@ class EventOrchestrator extends KernelInjector {
|
|
|
636
667
|
* @param {FocusEvent} event - Focus event object
|
|
637
668
|
*/
|
|
638
669
|
__postBlurEvent(frameContext, event) {
|
|
639
|
-
if (this.#store.mode.isInline
|
|
670
|
+
if (this.#store.mode.isInline) {
|
|
671
|
+
// Defer hide — prevents race with deferred focus show (setTimeout in #OnFocus_wysiwyg)
|
|
672
|
+
_w.setTimeout(() => {
|
|
673
|
+
if (!this.#store.get('hasFocus')) {
|
|
674
|
+
this._hideToolbar();
|
|
675
|
+
}
|
|
676
|
+
}, 0);
|
|
677
|
+
} else if (this.#store.mode.isBalloon) {
|
|
678
|
+
this._hideToolbar();
|
|
679
|
+
}
|
|
640
680
|
if (this.#store.mode.isSubBalloon) this._hideToolbar_sub();
|
|
641
681
|
|
|
642
682
|
// user event
|
|
@@ -699,7 +739,9 @@ class EventOrchestrator extends KernelInjector {
|
|
|
699
739
|
this.#ui._iframeAutoHeight(fc);
|
|
700
740
|
|
|
701
741
|
if (this.#toolbar.isSticky) {
|
|
702
|
-
this.#
|
|
742
|
+
if (!this.#toolbar.isCSSSticky) {
|
|
743
|
+
this.#context.get('toolbar_main').style.width = fc.get('topArea').offsetWidth - 2 + 'px';
|
|
744
|
+
}
|
|
703
745
|
this.#toolbar._resetSticky();
|
|
704
746
|
}
|
|
705
747
|
}
|
|
@@ -939,6 +981,31 @@ class EventOrchestrator extends KernelInjector {
|
|
|
939
981
|
dom.utils.addClass(this.$.commandDispatcher.targets.get('codeView'), 'active');
|
|
940
982
|
this.#ui._toggleCodeViewButtons(true);
|
|
941
983
|
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* @param {SunEditor.FrameContext} frameContext - frame context object
|
|
987
|
+
*/
|
|
988
|
+
#OnFocus_markdown(frameContext) {
|
|
989
|
+
this.$.facade.changeFrameContext(frameContext.get('key'));
|
|
990
|
+
dom.utils.addClass(this.$.commandDispatcher.targets.get('markdownView'), 'active');
|
|
991
|
+
this.#ui._toggleCodeViewButtons(true);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* @description Inserts a tab character at the cursor position in a textarea
|
|
997
|
+
* @param {KeyboardEvent} e
|
|
998
|
+
*/
|
|
999
|
+
function InsertTab(e) {
|
|
1000
|
+
if (e.key !== 'Tab') return;
|
|
1001
|
+
e.preventDefault();
|
|
1002
|
+
|
|
1003
|
+
const textarea = /** @type {HTMLTextAreaElement} */ (e.target);
|
|
1004
|
+
const start = textarea.selectionStart;
|
|
1005
|
+
const end = textarea.selectionEnd;
|
|
1006
|
+
|
|
1007
|
+
textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end);
|
|
1008
|
+
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
|
942
1009
|
}
|
|
943
1010
|
|
|
944
1011
|
export default EventOrchestrator;
|
|
@@ -89,6 +89,7 @@ export async function OnClick_wysiwyg(fc, e) {
|
|
|
89
89
|
// plugin event
|
|
90
90
|
if ((await this._callPluginEventAsync('onClick', { frameContext: fc, event: e })) === false) return;
|
|
91
91
|
|
|
92
|
+
// component
|
|
92
93
|
const componentInfo = this.$.component.get(eventTarget);
|
|
93
94
|
if (componentInfo) {
|
|
94
95
|
e.preventDefault();
|
|
@@ -59,7 +59,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
59
59
|
!selectionNode.previousSibling &&
|
|
60
60
|
!dom.check.isListCell(formatEl) &&
|
|
61
61
|
format.isLine(formatEl) &&
|
|
62
|
-
(!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl))
|
|
62
|
+
(!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl) || dom.check.isWysiwygFrame(formatEl.parentNode))
|
|
63
63
|
) {
|
|
64
64
|
// closure range
|
|
65
65
|
if (format.isClosureBlock(formatEl.parentNode)) {
|
|
@@ -67,6 +67,14 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// brLine (pre): strip tag to default line(s)
|
|
71
|
+
if (format.isBrLine(formatEl) && dom.check.isWysiwygFrame(formatEl.parentNode)) {
|
|
72
|
+
actions.push(A.preventStop());
|
|
73
|
+
actions.push(A.backspaceBrLineStrip(formatEl));
|
|
74
|
+
actions.push(A.historyPush(true));
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
// maintain default format
|
|
71
79
|
if (dom.check.isWysiwygFrame(formatEl.parentNode) && formatEl.childNodes.length <= 1 && (!formatEl.firstChild || dom.check.isZeroWidth(formatEl.textContent))) {
|
|
72
80
|
actions.push(A.preventStop());
|
|
@@ -30,6 +30,7 @@ import History from '../logic/shell/history';
|
|
|
30
30
|
import Toolbar from '../logic/panel/toolbar';
|
|
31
31
|
import Menu from '../logic/panel/menu';
|
|
32
32
|
import Viewer from '../logic/panel/viewer';
|
|
33
|
+
import Finder from '../logic/panel/finder';
|
|
33
34
|
|
|
34
35
|
// L4: event
|
|
35
36
|
import EventOrchestrator from '../event/eventOrchestrator';
|
|
@@ -78,6 +79,7 @@ import EventOrchestrator from '../event/eventOrchestrator';
|
|
|
78
79
|
* @property {import('../logic/panel/toolbar').default} subToolbar - L3: Sub-toolbar renderer
|
|
79
80
|
* @property {import('../logic/panel/menu').default} menu - L3: Menu renderer
|
|
80
81
|
* @property {import('../logic/panel/viewer').default} viewer - L3: View mode handler
|
|
82
|
+
* @property {import('../logic/panel/finder').default} finder - L3: Finder handler
|
|
81
83
|
*/
|
|
82
84
|
|
|
83
85
|
/**
|
|
@@ -218,6 +220,7 @@ class CoreKernel {
|
|
|
218
220
|
}
|
|
219
221
|
this.#logic.set('menu', new Menu(this));
|
|
220
222
|
this.#logic.set('viewer', new Viewer(this));
|
|
223
|
+
this.#logic.set('finder', new Finder(this));
|
|
221
224
|
|
|
222
225
|
// history (last — closure captures all L3 modules above)
|
|
223
226
|
this.#logic.set('history', History(this));
|
|
@@ -268,6 +271,7 @@ class CoreKernel {
|
|
|
268
271
|
subToolbar: this.#logic.get('subToolbar'),
|
|
269
272
|
menu: this.#logic.get('menu'),
|
|
270
273
|
viewer: this.#logic.get('viewer'),
|
|
274
|
+
finder: this.#logic.get('finder'),
|
|
271
275
|
});
|
|
272
276
|
}
|
|
273
277
|
|
package/src/core/kernel/store.js
CHANGED
|
@@ -28,6 +28,7 @@ import { numbers } from '../../helper';
|
|
|
28
28
|
* @property {boolean} isBalloonAlways - Whether the toolbar is in `balloon-always` mode (always visible as floating).
|
|
29
29
|
* @property {boolean} isSubBalloon - Whether the sub-toolbar is in `balloon` mode.
|
|
30
30
|
* @property {boolean} isSubBalloonAlways - Whether the sub-toolbar is in `balloon-always` mode.
|
|
31
|
+
* @property {boolean} isBottom - Whether the toolbar is placed at the bottom of the editor (`classic:bottom`, `inline:bottom`).
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -64,6 +65,7 @@ class Store {
|
|
|
64
65
|
isBalloonAlways: /balloon-always/i.test(mode),
|
|
65
66
|
isSubBalloon: /balloon/i.test(subMode),
|
|
66
67
|
isSubBalloonAlways: /balloon-always/i.test(subMode),
|
|
68
|
+
isBottom: !!options.get('_toolbar_bottom'),
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
this.#state = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dom, converter, numbers, unicode, clipboard, env } from '../../../helper';
|
|
1
|
+
import { dom, converter, markdown, numbers, unicode, clipboard, env } from '../../../helper';
|
|
2
2
|
|
|
3
3
|
const { _d } = env;
|
|
4
4
|
const REQUIRED_DATA_ATTRS = 'data-se-[^\\s]+';
|
|
@@ -293,7 +293,7 @@ class HTML {
|
|
|
293
293
|
for (const [key, value] of Object.entries(attrs)) {
|
|
294
294
|
// Block event handler attributes and validate src protocol
|
|
295
295
|
if (/^on/i.test(key)) continue;
|
|
296
|
-
if (key === 'src' && !
|
|
296
|
+
if (key === 'src' && !_isSafeURL(String(value))) continue;
|
|
297
297
|
iframe.setAttribute(key, value);
|
|
298
298
|
}
|
|
299
299
|
|
|
@@ -1149,6 +1149,9 @@ class HTML {
|
|
|
1149
1149
|
emptyCells[j].innerHTML = '<br>';
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
|
+
// output: wrap code blocks <pre class="language-xxx"> → <pre><code class="language-xxx">
|
|
1153
|
+
this.#wrapPreCode(renderHTML);
|
|
1154
|
+
|
|
1152
1155
|
const content = renderHTML.innerHTML;
|
|
1153
1156
|
if (this.#frameOptions.get('iframe_fullPage')) {
|
|
1154
1157
|
if (includeFullPage) {
|
|
@@ -1188,7 +1191,10 @@ class HTML {
|
|
|
1188
1191
|
for (let i = 0; i < rootKey.length; i++) {
|
|
1189
1192
|
this.#$.facade.changeFrameContext(rootKey[i]);
|
|
1190
1193
|
|
|
1191
|
-
if (
|
|
1194
|
+
if (this.#frameContext.get('isMarkdownView')) {
|
|
1195
|
+
const json = converter.htmlToJson(convertValue);
|
|
1196
|
+
this.#frameContext.get('markdown').value = markdown.jsonToMarkdown(json);
|
|
1197
|
+
} else if (!this.#frameContext.get('isCodeView')) {
|
|
1192
1198
|
this.#frameContext.get('wysiwyg').innerHTML = convertValue;
|
|
1193
1199
|
this.#$.pluginManager.resetFileInfo();
|
|
1194
1200
|
this.#$.history.push(false, rootKey[i]);
|
|
@@ -1215,7 +1221,10 @@ class HTML {
|
|
|
1215
1221
|
this.#$.facade.changeFrameContext(rootKey[i]);
|
|
1216
1222
|
const convertValue = this.clean(html, { forceFormat: true, whitelist: null, blacklist: null });
|
|
1217
1223
|
|
|
1218
|
-
if (
|
|
1224
|
+
if (this.#frameContext.get('isMarkdownView')) {
|
|
1225
|
+
const json = converter.htmlToJson(convertValue);
|
|
1226
|
+
this.#frameContext.get('markdown').value += '\n' + markdown.jsonToMarkdown(json);
|
|
1227
|
+
} else if (!this.#frameContext.get('isCodeView')) {
|
|
1219
1228
|
const temp = dom.utils.createElement('DIV', null, convertValue);
|
|
1220
1229
|
const children = Array.from(temp.children);
|
|
1221
1230
|
for (let j = 0, jLen = children.length; j < jLen; j++) {
|
|
@@ -1521,6 +1530,9 @@ class HTML {
|
|
|
1521
1530
|
}
|
|
1522
1531
|
}
|
|
1523
1532
|
|
|
1533
|
+
// code block: unwrap <pre><code class="language-xxx"> → <pre class="language-xxx">
|
|
1534
|
+
if (current.nodeName === 'PRE') this.#unwrapPreCode(current);
|
|
1535
|
+
|
|
1524
1536
|
const nrtag = !dom.query.getParentElement(current, dom.check.isExcludeFormat);
|
|
1525
1537
|
|
|
1526
1538
|
// formatFilter
|
|
@@ -2000,13 +2012,58 @@ class HTML {
|
|
|
2000
2012
|
this.#cleanStyleRegExpMap.clear();
|
|
2001
2013
|
}
|
|
2002
2014
|
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* @description Input: unwrap `<pre><code class="language-xxx">` → `<pre class="language-xxx">`
|
|
2018
|
+
* @param {HTMLElement} pre
|
|
2019
|
+
*/
|
|
2020
|
+
#unwrapPreCode(pre) {
|
|
2021
|
+
if (pre.children.length !== 1 || pre.firstElementChild?.nodeName !== 'CODE') return;
|
|
2022
|
+
|
|
2023
|
+
const codeEl = pre.firstElementChild;
|
|
2024
|
+
const langMatch = (codeEl.className || '').match(/language-(\S+)/);
|
|
2025
|
+
|
|
2026
|
+
if (langMatch) {
|
|
2027
|
+
dom.utils.addClass(pre, 'language-' + langMatch[1]);
|
|
2028
|
+
pre.setAttribute('data-se-lang', langMatch[1]);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
while (codeEl.firstChild) {
|
|
2032
|
+
pre.insertBefore(codeEl.firstChild, codeEl);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
pre.removeChild(codeEl);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* @description Output: wrap `<pre class="language-xxx">` → `<pre><code class="language-xxx">`
|
|
2040
|
+
* @param {HTMLElement} container
|
|
2041
|
+
*/
|
|
2042
|
+
#wrapPreCode(container) {
|
|
2043
|
+
const preEls = container.querySelectorAll('pre[class*="language-"]');
|
|
2044
|
+
|
|
2045
|
+
for (let j = 0, jlen = preEls.length, pre, lang, codeEl; j < jlen; j++) {
|
|
2046
|
+
pre = preEls[j];
|
|
2047
|
+
lang = (pre.className.match(/language-(\S+)/) || [])[1];
|
|
2048
|
+
if (!lang) continue;
|
|
2049
|
+
|
|
2050
|
+
codeEl = dom.utils.createElement('CODE', { class: 'language-' + lang });
|
|
2051
|
+
while (pre.firstChild) {
|
|
2052
|
+
codeEl.appendChild(pre.firstChild);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
pre.appendChild(codeEl);
|
|
2056
|
+
pre.className = pre.className.replace(/\s*language-\S+/g, '').trim();
|
|
2057
|
+
pre.removeAttribute('data-se-lang');
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2003
2060
|
}
|
|
2004
2061
|
|
|
2005
2062
|
/** Module-level regex constants */
|
|
2006
2063
|
// #CleanElements
|
|
2007
2064
|
const _RE_XML_NS_TAG = /^<[a-z0-9]+:[a-z0-9]+/i;
|
|
2008
2065
|
const _RE_TAG_NAME = /(?!<)[a-zA-Z0-9-]+/;
|
|
2009
|
-
const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(")[^"]*\1/gi;
|
|
2066
|
+
const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(?:(["'])[^"']*\1|\S+)/gi;
|
|
2010
2067
|
const _RE_STYLE_EQ = /style=/i;
|
|
2011
2068
|
const _RE_STYLE_ATTR = /style\s*=\s*(?:"|')[^"']*(?:"|')/;
|
|
2012
2069
|
const _RE_LEADING_SPACE = /^\s/;
|
|
@@ -2029,20 +2086,62 @@ const _RE_SPAN = /^span$/i;
|
|
|
2029
2086
|
|
|
2030
2087
|
// _isSafeAttribute
|
|
2031
2088
|
const _SAFE_URL_PROTOCOL = /^(?:https?|ftps?|mailto|tel|blob|sms|geo|webcal|callto):|^[#/]|^data:image\//i;
|
|
2032
|
-
const _URL_ATTR_PATTERN = /^(?:href|src)\s
|
|
2033
|
-
const _RE_ATTR_VALUE = /=\s*(?:"|'
|
|
2034
|
-
const _RE_WHITESPACE = /[\s\r\n\t]+/g;
|
|
2035
|
-
const _RE_HTML_ENTITY = /&(#x?[0-9a-f]+|[a-z]+);/gi;
|
|
2089
|
+
const _URL_ATTR_PATTERN = /^(?:href|src)\s*=/i;
|
|
2090
|
+
const _RE_ATTR_VALUE = /=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/;
|
|
2036
2091
|
const _RE_COLON = /:/i;
|
|
2037
2092
|
|
|
2093
|
+
/**
|
|
2094
|
+
* @description Normalize a URL by decoding all HTML entities, URL-encoded characters,
|
|
2095
|
+
* and stripping whitespace/control characters. Used to detect obfuscated dangerous protocols.
|
|
2096
|
+
* e.g. `java	script:`, `java%09script:`, `javascript:`
|
|
2097
|
+
* @param {string} url Raw URL string
|
|
2098
|
+
* @returns {string} Normalized URL
|
|
2099
|
+
*/
|
|
2100
|
+
function _normalizeURL(url) {
|
|
2101
|
+
// Decode HTML entities (	 	 etc.) — repeat to handle nested encoding (e.g. &#x6a; → j → j)
|
|
2102
|
+
let prev,
|
|
2103
|
+
limit = 5;
|
|
2104
|
+
do {
|
|
2105
|
+
prev = url;
|
|
2106
|
+
url = url.replace(/&(#x([0-9a-f]+)|#([0-9]+)|([a-z]+));/gi, (_, __, hex, dec) => {
|
|
2107
|
+
if (hex) return String.fromCharCode(parseInt(hex, 16));
|
|
2108
|
+
if (dec) return String.fromCharCode(parseInt(dec, 10));
|
|
2109
|
+
return '';
|
|
2110
|
+
});
|
|
2111
|
+
} while (url !== prev && --limit);
|
|
2112
|
+
|
|
2113
|
+
// Decode URL-encoded characters (%09, %0a, etc.)
|
|
2114
|
+
try {
|
|
2115
|
+
url = decodeURIComponent(url);
|
|
2116
|
+
} catch {
|
|
2117
|
+
// malformed URI — keep as is
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// Strip all whitespace and control characters (U+0000–U+0020)
|
|
2121
|
+
// eslint-disable-next-line no-control-regex -- intentional: strip control chars used to bypass protocol detection
|
|
2122
|
+
url = url.replace(/[\u0000-\u0020]+/g, '');
|
|
2123
|
+
|
|
2124
|
+
return url;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* @description Check if a URL is safe (matches the allowed protocol whitelist).
|
|
2129
|
+
* @param {string} url Raw URL string
|
|
2130
|
+
* @returns {boolean}
|
|
2131
|
+
*/
|
|
2132
|
+
function _isSafeURL(url) {
|
|
2133
|
+
const normalized = _normalizeURL(url);
|
|
2134
|
+
return _SAFE_URL_PROTOCOL.test(normalized) || !_RE_COLON.test(normalized);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2038
2137
|
function _isSafeAttribute(attr) {
|
|
2039
2138
|
if (!_URL_ATTR_PATTERN.test(attr)) return true;
|
|
2040
2139
|
|
|
2041
2140
|
const urlMatch = attr.match(_RE_ATTR_VALUE);
|
|
2042
2141
|
if (!urlMatch) return true;
|
|
2043
2142
|
|
|
2044
|
-
const url = urlMatch[1]
|
|
2045
|
-
return
|
|
2143
|
+
const url = urlMatch[1] ?? urlMatch[2] ?? urlMatch[3] ?? '';
|
|
2144
|
+
return _isSafeURL(url);
|
|
2046
2145
|
}
|
|
2047
2146
|
|
|
2048
2147
|
/**
|