suneditor 3.0.2 → 3.0.4
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/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +1 -1
- package/src/assets/suneditor.css +36 -5
- package/src/core/config/optionProvider.js +1 -1
- package/src/core/editor.js +2 -0
- package/src/core/event/eventOrchestrator.js +32 -3
- package/src/core/event/ports.js +2 -2
- package/src/core/logic/dom/char.js +27 -4
- package/src/core/logic/dom/html.js +8 -0
- package/src/core/logic/dom/offset.js +1 -1
- package/src/core/logic/dom/selection.js +1 -1
- package/src/core/logic/panel/finder.js +1 -1
- package/src/core/logic/panel/toolbar.js +49 -7
- package/src/core/logic/panel/viewer.js +1 -1
- package/src/core/schema/frameContext.js +6 -0
- package/src/core/schema/options.js +28 -3
- package/src/core/section/constructor.js +46 -3
- package/src/modules/manager/ApiManager.js +1 -0
- package/types/core/event/ports.d.ts +1 -1
- package/types/core/logic/dom/char.d.ts +10 -1
- package/types/core/schema/frameContext.d.ts +10 -0
- package/types/core/schema/options.d.ts +72 -5
- package/types/core/section/constructor.d.ts +3 -1
package/package.json
CHANGED
package/src/assets/suneditor.css
CHANGED
|
@@ -916,13 +916,13 @@
|
|
|
916
916
|
.sun-editor .se-toolbar.se-toolbar-bottom {
|
|
917
917
|
top: auto;
|
|
918
918
|
bottom: 0;
|
|
919
|
-
outline-offset: -1px;
|
|
920
919
|
}
|
|
921
920
|
|
|
922
921
|
/* Bottom toolbar: more layer opens above buttons */
|
|
923
922
|
.sun-editor .se-toolbar.se-toolbar-bottom .se-btn-tray {
|
|
924
923
|
display: flex;
|
|
925
924
|
flex-wrap: wrap;
|
|
925
|
+
padding: 4px 3px;
|
|
926
926
|
}
|
|
927
927
|
|
|
928
928
|
.sun-editor .se-toolbar.se-toolbar-bottom .se-btn-tray > .se-toolbar-more-layer {
|
|
@@ -934,6 +934,7 @@
|
|
|
934
934
|
.sun-editor .se-toolbar.se-toolbar-bottom .se-toolbar-more-layer .se-more-layer {
|
|
935
935
|
border-top: none;
|
|
936
936
|
border-bottom: 1px solid var(--se-main-divider-color);
|
|
937
|
+
margin-bottom: 4px;
|
|
937
938
|
}
|
|
938
939
|
|
|
939
940
|
/* JS fallback sticky for bottom */
|
|
@@ -1065,11 +1066,11 @@
|
|
|
1065
1066
|
}
|
|
1066
1067
|
|
|
1067
1068
|
.sun-editor .se-btn-select.se-btn-tool-font {
|
|
1068
|
-
width:
|
|
1069
|
+
width: 128px;
|
|
1069
1070
|
}
|
|
1070
1071
|
|
|
1071
1072
|
.sun-editor .se-btn-select.se-btn-tool-format {
|
|
1072
|
-
width:
|
|
1073
|
+
width: 96px;
|
|
1073
1074
|
}
|
|
1074
1075
|
|
|
1075
1076
|
.sun-editor .se-btn-select.se-btn-tool-font-size .se-txt {
|
|
@@ -1879,7 +1880,27 @@
|
|
|
1879
1880
|
background: transparent;
|
|
1880
1881
|
}
|
|
1881
1882
|
|
|
1883
|
+
/** status bar - wordCounter */
|
|
1884
|
+
.sun-editor .se-status-bar .se-word-counter-wrapper {
|
|
1885
|
+
position: relative;
|
|
1886
|
+
width: auto;
|
|
1887
|
+
height: auto;
|
|
1888
|
+
margin: 0;
|
|
1889
|
+
padding: 0;
|
|
1890
|
+
color: var(--se-statusbar-font-color);
|
|
1891
|
+
font-size: var(--se-statusbar-font-size);
|
|
1892
|
+
background: transparent;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
.sun-editor .se-status-bar .se-word-counter-wrapper .se-word-label {
|
|
1896
|
+
margin-right: 4px;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1882
1899
|
/** status bar - charCounter */
|
|
1900
|
+
.sun-editor .se-status-bar .se-char-counter-wrapper.se-with-word-counter {
|
|
1901
|
+
margin-left: 0.6em;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1883
1904
|
.sun-editor .se-status-bar .se-char-counter-wrapper {
|
|
1884
1905
|
flex: none;
|
|
1885
1906
|
position: relative;
|
|
@@ -1889,7 +1910,7 @@
|
|
|
1889
1910
|
margin: 0;
|
|
1890
1911
|
padding: 0;
|
|
1891
1912
|
color: var(--se-statusbar-font-color);
|
|
1892
|
-
font-size: var(--se-
|
|
1913
|
+
font-size: var(--se-statusbar-font-size);
|
|
1893
1914
|
background: transparent;
|
|
1894
1915
|
}
|
|
1895
1916
|
|
|
@@ -3883,10 +3904,20 @@
|
|
|
3883
3904
|
}
|
|
3884
3905
|
|
|
3885
3906
|
/* statusbar */
|
|
3886
|
-
.sun-editor.se-rtl .se-status-bar
|
|
3907
|
+
.sun-editor.se-rtl .se-status-bar {
|
|
3887
3908
|
direction: rtl;
|
|
3888
3909
|
}
|
|
3889
3910
|
|
|
3911
|
+
.sun-editor.se-rtl .se-status-bar .se-word-counter-wrapper .se-word-label {
|
|
3912
|
+
margin-right: 0;
|
|
3913
|
+
margin-left: 4px;
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
.sun-editor.se-rtl .se-status-bar .se-char-counter-wrapper.se-with-word-counter {
|
|
3917
|
+
margin-left: 0;
|
|
3918
|
+
margin-right: 0.6em;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3890
3921
|
/* button--- */
|
|
3891
3922
|
/* button - select text */
|
|
3892
3923
|
.sun-editor.se-rtl .se-btn-select .se-txt {
|
|
@@ -331,7 +331,7 @@ export default class OptionProvider {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
#GetResetDiffKey(key) {
|
|
334
|
-
if (/^statusbar|^charCounter/.test(key)) return 'statusbar-changed';
|
|
334
|
+
if (/^statusbar|^charCounter|^wordCounter/.test(key)) return 'statusbar-changed';
|
|
335
335
|
return key;
|
|
336
336
|
}
|
|
337
337
|
|
package/src/core/editor.js
CHANGED
|
@@ -204,6 +204,8 @@ class Editor {
|
|
|
204
204
|
|
|
205
205
|
// char counter
|
|
206
206
|
if (e.has('charCounter')) e.get('charCounter').textContent = String(this.$.char.getLength());
|
|
207
|
+
// word counter
|
|
208
|
+
if (e.has('wordCounter')) e.get('wordCounter').textContent = String(this.$.char.getWordCount());
|
|
207
209
|
|
|
208
210
|
// document type init
|
|
209
211
|
if (this.$.options.get('type') === 'document') {
|
|
@@ -138,6 +138,21 @@ class EventOrchestrator extends KernelInjector {
|
|
|
138
138
|
}, 250);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* @description Toggle toolbar-balloon with delay (debounced for selectionchange).
|
|
143
|
+
*/
|
|
144
|
+
#toggleToolbarBalloonDelay() {
|
|
145
|
+
if (this.#balloonDelay) {
|
|
146
|
+
_w.clearTimeout(this.#balloonDelay);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.#balloonDelay = _w.setTimeout(() => {
|
|
150
|
+
_w.clearTimeout(this.#balloonDelay);
|
|
151
|
+
this.#balloonDelay = null;
|
|
152
|
+
this._toggleToolbarBalloon();
|
|
153
|
+
}, 250);
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
/**
|
|
142
157
|
* @internal
|
|
143
158
|
* @description Show or hide the toolbar-balloon.
|
|
@@ -654,6 +669,9 @@ class EventOrchestrator extends KernelInjector {
|
|
|
654
669
|
if (this.#store.mode.isInline || this.#store.mode.isBalloonAlways) this.#toolbar.show();
|
|
655
670
|
if (this.#store.mode.isSubBalloonAlways) this.$.subToolbar.show();
|
|
656
671
|
|
|
672
|
+
// sticky
|
|
673
|
+
this.#toolbar._resetSticky();
|
|
674
|
+
|
|
657
675
|
// user event
|
|
658
676
|
this.#eventManager.triggerEvent('onFocus', { frameContext, event });
|
|
659
677
|
// plugin event
|
|
@@ -909,17 +927,23 @@ class EventOrchestrator extends KernelInjector {
|
|
|
909
927
|
}
|
|
910
928
|
|
|
911
929
|
#OnResize_viewport() {
|
|
912
|
-
if (isMobile && this.#options.get('
|
|
930
|
+
if (isMobile && this.#options.get('_toolbar_sticky') > -1) {
|
|
913
931
|
this.#toolbar._resetSticky();
|
|
914
932
|
this.#menu.__restoreMenuPosition();
|
|
915
933
|
}
|
|
916
934
|
|
|
917
935
|
this.#scrollContainer();
|
|
936
|
+
|
|
937
|
+
const prevHeight = this.#store.get('currentViewportHeight');
|
|
918
938
|
this.__setViewportSize();
|
|
939
|
+
|
|
940
|
+
if (isMobile && prevHeight > 0 && prevHeight - _w.visualViewport.height > 100 && this.#store.get('hasFocus')) {
|
|
941
|
+
this.$.selection.scrollTo(this.$.selection.getRange(), { behavior: 'auto', block: 'nearest', inline: 'nearest' });
|
|
942
|
+
}
|
|
919
943
|
}
|
|
920
944
|
|
|
921
945
|
#OnScroll_window() {
|
|
922
|
-
if (this.#options.get('
|
|
946
|
+
if (this.#options.get('_toolbar_sticky') > -1) {
|
|
923
947
|
this.#toolbar._resetSticky();
|
|
924
948
|
}
|
|
925
949
|
|
|
@@ -938,7 +962,7 @@ class EventOrchestrator extends KernelInjector {
|
|
|
938
962
|
}
|
|
939
963
|
|
|
940
964
|
#OnMobileScroll_viewport() {
|
|
941
|
-
if (this.#options.get('
|
|
965
|
+
if (this.#options.get('_toolbar_sticky') > -1) {
|
|
942
966
|
this.#toolbar._resetSticky();
|
|
943
967
|
this.#menu.__restoreMenuPosition();
|
|
944
968
|
}
|
|
@@ -959,6 +983,11 @@ class EventOrchestrator extends KernelInjector {
|
|
|
959
983
|
this.$.selection.init();
|
|
960
984
|
this.applyTagEffect();
|
|
961
985
|
|
|
986
|
+
// balloon toolbar - touch devices
|
|
987
|
+
if (isTouchDevice && (this.#store.mode.isBalloon || this.#store.mode.isSubBalloon)) {
|
|
988
|
+
this.#toggleToolbarBalloonDelay();
|
|
989
|
+
}
|
|
990
|
+
|
|
962
991
|
// document type
|
|
963
992
|
if (root.has('documentType_use_header')) {
|
|
964
993
|
const el = dom.query.getParentElement(this.$.selection.selectionNode, this.$.format.isLine.bind(this.$.format));
|
package/src/core/event/ports.js
CHANGED
|
@@ -158,14 +158,14 @@ export function makePorts(inst, { _styleNodes }) {
|
|
|
158
158
|
|
|
159
159
|
// === enter event specific ===
|
|
160
160
|
/**
|
|
161
|
-
* @description Scrolls the editor view to the caret position after pressing `Enter`.
|
|
161
|
+
* @description Scrolls the editor view to the caret position after pressing `Enter`.
|
|
162
162
|
* @param {Range} range Range object
|
|
163
163
|
*/
|
|
164
164
|
enterScrollTo(range) {
|
|
165
165
|
ui._iframeAutoHeight(frameContext);
|
|
166
166
|
|
|
167
167
|
// scroll to
|
|
168
|
-
if (isMobile && inst.scrollparents.length > 0) return;
|
|
168
|
+
// if (isMobile && inst.scrollparents.length > 0) return;
|
|
169
169
|
selection.scrollTo(range, { behavior: 'auto', block: 'nearest', inline: 'nearest' });
|
|
170
170
|
},
|
|
171
171
|
/**
|
|
@@ -92,15 +92,38 @@ class Char {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* @description
|
|
95
|
+
* @description Get the number of words in the content.
|
|
96
|
+
* - If [content] is `undefined`, get the current editor's word count.
|
|
97
|
+
* @param {string} [content] Content to count. (default: wysiwyg textContent)
|
|
98
|
+
* @returns {number}
|
|
99
|
+
* const currentWords = editor.$.char.getWordCount();
|
|
100
|
+
* const textWords = editor.$.char.getWordCount('Hello World');
|
|
101
|
+
*/
|
|
102
|
+
getWordCount(content) {
|
|
103
|
+
if (typeof content !== 'string') {
|
|
104
|
+
content = this.#frameContext.get('wysiwyg').innerText;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const trimmed = content.trim();
|
|
108
|
+
if (!trimmed) return 0;
|
|
109
|
+
|
|
110
|
+
return trimmed.split(/\s+/).length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @description Set the char count and word count to counter element textContent.
|
|
96
115
|
* @param {?SunEditor.FrameContext} [fc] Frame context
|
|
97
116
|
*/
|
|
98
117
|
display(fc) {
|
|
99
|
-
const
|
|
100
|
-
|
|
118
|
+
const ctx = fc || this.#frameContext;
|
|
119
|
+
const charCounter = ctx.get('charCounter');
|
|
120
|
+
const wordCounter = ctx.get('wordCounter');
|
|
121
|
+
|
|
122
|
+
if (charCounter || wordCounter) {
|
|
101
123
|
// Defer count update — DOM content may still be mutating from the current input/paste action
|
|
102
124
|
_w.setTimeout(() => {
|
|
103
|
-
charCounter.textContent = String(this.getLength());
|
|
125
|
+
if (charCounter) charCounter.textContent = String(this.getLength());
|
|
126
|
+
if (wordCounter) wordCounter.textContent = String(this.getWordCount());
|
|
104
127
|
}, 0);
|
|
105
128
|
}
|
|
106
129
|
}
|
|
@@ -2134,6 +2134,14 @@ function _isSafeURL(url) {
|
|
|
2134
2134
|
return _SAFE_URL_PROTOCOL.test(normalized) || !_RE_COLON.test(normalized);
|
|
2135
2135
|
}
|
|
2136
2136
|
|
|
2137
|
+
/**
|
|
2138
|
+
* @description Checks whether an HTML attribute string is safe from XSS injection.
|
|
2139
|
+
* - Non-URL attributes (anything other than `href` or `src`) are always safe.
|
|
2140
|
+
* - For URL attributes, extracts the URL value and delegates to {@link _isSafeURL}
|
|
2141
|
+
* to verify the protocol against the allowed whitelist.
|
|
2142
|
+
* @param {string} attr A single attribute string (e.g. `href="https://example.com"`, `class="foo"`)
|
|
2143
|
+
* @returns {boolean} `true` if the attribute is safe, `false` if it contains a dangerous URL protocol.
|
|
2144
|
+
*/
|
|
2137
2145
|
function _isSafeAttribute(attr) {
|
|
2138
2146
|
if (!_URL_ATTR_PATTERN.test(attr)) return true;
|
|
2139
2147
|
|
|
@@ -503,7 +503,7 @@ class Offset {
|
|
|
503
503
|
const th = this.#context.get('toolbar_main').offsetHeight;
|
|
504
504
|
const containerToolbar = this.#options.get('toolbar_container');
|
|
505
505
|
const headLess = this.#store.mode.isBalloon || this.#store.mode.isInline || containerToolbar;
|
|
506
|
-
const toolbarH = (containerToolbar && globalTop - wScrollY - th > 0) || (!this.#$.toolbar.isSticky && headLess) ? 0 : th + (this.#$.toolbar.isSticky ? this.#options.get('
|
|
506
|
+
const toolbarH = (containerToolbar && globalTop - wScrollY - th > 0) || (!this.#$.toolbar.isSticky && headLess) ? 0 : th + (this.#$.toolbar.isSticky ? this.#options.get('_toolbar_sticky') : 0);
|
|
507
507
|
const statusBarH = this.#frameContext.get('statusbar')?.offsetHeight || 0;
|
|
508
508
|
|
|
509
509
|
// check margin
|
|
@@ -425,7 +425,7 @@ class Selection_ {
|
|
|
425
425
|
const isBottom = this.#store.mode.isBottom;
|
|
426
426
|
const realToolbarHeight = this.#context.get('toolbar_main').offsetHeight;
|
|
427
427
|
const toolbarHeight = this.#$.toolbar.isSticky ? realToolbarHeight : 0;
|
|
428
|
-
const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('
|
|
428
|
+
const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('_toolbar_sticky') : toolbarHeight;
|
|
429
429
|
const statusbarHeight = this.#frameContext.get('statusbar')?.offsetHeight || 0;
|
|
430
430
|
|
|
431
431
|
if (this.#hasScrollParents) {
|
|
@@ -154,7 +154,7 @@ class Finder {
|
|
|
154
154
|
*/
|
|
155
155
|
#updateStickyTop() {
|
|
156
156
|
if (!this.#isOpen || !this.#panel) return;
|
|
157
|
-
const stickyTop = this.#$.options.get('
|
|
157
|
+
const stickyTop = this.#$.options.get('_toolbar_sticky');
|
|
158
158
|
if (this.#store.mode.isBottom) {
|
|
159
159
|
this.#panel.style.top = 'auto';
|
|
160
160
|
this.#panel.style.bottom = stickyTop >= 0 ? stickyTop + this.#$.context.get('toolbar_main').offsetHeight + 'px' : '0px';
|
|
@@ -28,6 +28,7 @@ class Toolbar {
|
|
|
28
28
|
|
|
29
29
|
#useCSSSticky = false;
|
|
30
30
|
#_isStickyFlag = false;
|
|
31
|
+
#_cssStickyShifted = false;
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @constructor
|
|
@@ -88,7 +89,7 @@ class Toolbar {
|
|
|
88
89
|
|
|
89
90
|
// CSS sticky: non-balloon, non-inline, non-container, sticky enabled
|
|
90
91
|
const isStickyPosible = !this.isSub && !balloon && !inline;
|
|
91
|
-
const stickyTop = this.#options.get('
|
|
92
|
+
const stickyTop = this.#options.get('_toolbar_sticky');
|
|
92
93
|
|
|
93
94
|
this.#useCSSSticky = isStickyPosible && stickyTop >= 0 && !this.#options.get('toolbar_container') && typeof CSS !== 'undefined' && CSS.supports('position', 'sticky');
|
|
94
95
|
|
|
@@ -119,14 +120,15 @@ class Toolbar {
|
|
|
119
120
|
*/
|
|
120
121
|
get isSticky() {
|
|
121
122
|
if (this.isSub) return false;
|
|
122
|
-
const stickyTop = this.#options.get('
|
|
123
|
+
const stickyTop = this.#options.get('_toolbar_sticky');
|
|
123
124
|
if (stickyTop < 0) return false;
|
|
124
125
|
|
|
125
126
|
if (this.#useCSSSticky) {
|
|
126
127
|
const toolbar = this.#context.get(this.keyName.main);
|
|
127
128
|
if (!toolbar || toolbar.offsetWidth === 0 || toolbar.style.display === 'none') return false;
|
|
128
129
|
if (this.isBottomMode) {
|
|
129
|
-
|
|
130
|
+
const viewportHeight = this.#isViewPortSize ? _w.visualViewport.height : _w.innerHeight;
|
|
131
|
+
return toolbar.getBoundingClientRect().bottom >= viewportHeight - stickyTop - 1;
|
|
130
132
|
}
|
|
131
133
|
return toolbar.getBoundingClientRect().top <= stickyTop + 1;
|
|
132
134
|
}
|
|
@@ -254,13 +256,16 @@ class Toolbar {
|
|
|
254
256
|
* @description Reset the sticky toolbar position based on the editor state.
|
|
255
257
|
*/
|
|
256
258
|
_resetSticky() {
|
|
257
|
-
if (this.#useCSSSticky)
|
|
259
|
+
if (this.#useCSSSticky) {
|
|
260
|
+
this.#resetCSSStickyOffset();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
258
263
|
|
|
259
264
|
const wrapper = this.#frameContext.get('wrapper');
|
|
260
265
|
if (!wrapper) return;
|
|
261
266
|
|
|
262
267
|
const toolbar = this.#context.get(this.keyName.main);
|
|
263
|
-
const stickyTop = this.#options.get('
|
|
268
|
+
const stickyTop = this.#options.get('_toolbar_sticky');
|
|
264
269
|
if (this.#frameContext.get('isFullScreen') || toolbar.offsetWidth === 0 || stickyTop < 0) return;
|
|
265
270
|
|
|
266
271
|
const currentScrollY = this.#isViewPortSize ? _w.visualViewport.pageTop : _w.scrollY;
|
|
@@ -485,11 +490,11 @@ class Toolbar {
|
|
|
485
490
|
}
|
|
486
491
|
|
|
487
492
|
if (this.isBottomMode) {
|
|
488
|
-
const toolbarBottomPosition = this.#options.get('
|
|
493
|
+
const toolbarBottomPosition = this.#options.get('_toolbar_sticky') + this.#getViewportTop();
|
|
489
494
|
toolbar.style.bottom = `${toolbarBottomPosition}px`;
|
|
490
495
|
toolbar.style.top = 'auto';
|
|
491
496
|
} else {
|
|
492
|
-
const toolbarTopPosition = this.#options.get('
|
|
497
|
+
const toolbarTopPosition = this.#options.get('_toolbar_sticky') + inlineOffset + this.#getViewportTop();
|
|
493
498
|
toolbar.style.top = `${toolbarTopPosition}px`;
|
|
494
499
|
}
|
|
495
500
|
toolbar.style.width = this.isInlineMode ? this.inlineToolbarAttr.width : toolbar.offsetWidth + 'px';
|
|
@@ -508,6 +513,43 @@ class Toolbar {
|
|
|
508
513
|
return 0;
|
|
509
514
|
}
|
|
510
515
|
|
|
516
|
+
/**
|
|
517
|
+
* @description Adjust CSS sticky toolbar position when the mobile virtual keyboard changes the visual viewport.
|
|
518
|
+
*/
|
|
519
|
+
#resetCSSStickyOffset() {
|
|
520
|
+
if (!this.#isViewPortSize) return;
|
|
521
|
+
// When the editor is inside a scrollable container (e.g., modal),
|
|
522
|
+
// position:sticky is relative to that container, not the viewport.
|
|
523
|
+
if (this.#kernel._eventOrchestrator.scrollparents.length > 0) return;
|
|
524
|
+
|
|
525
|
+
const viewportOffset = Math.round(_w.visualViewport.offsetTop);
|
|
526
|
+
if (viewportOffset === 0 && !this.#_cssStickyShifted) return;
|
|
527
|
+
|
|
528
|
+
const toolbar = this.#context.get(this.keyName.main);
|
|
529
|
+
const stickyOffset = this.#options.get('_toolbar_sticky_offset');
|
|
530
|
+
|
|
531
|
+
if (viewportOffset > 0) {
|
|
532
|
+
this.#_cssStickyShifted = true;
|
|
533
|
+
if (this.isBottomMode) {
|
|
534
|
+
const viewportBottom = Math.round(_w.innerHeight - _w.visualViewport.height - viewportOffset);
|
|
535
|
+
toolbar.style.bottom = stickyOffset + viewportBottom + 'px';
|
|
536
|
+
toolbar.style.top = 'auto';
|
|
537
|
+
} else {
|
|
538
|
+
toolbar.style.top = stickyOffset + viewportOffset + 'px';
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
// restore original CSS sticky value
|
|
542
|
+
this.#_cssStickyShifted = false;
|
|
543
|
+
const stickyTop = this.#options.get('_toolbar_sticky');
|
|
544
|
+
if (this.isBottomMode) {
|
|
545
|
+
toolbar.style.bottom = stickyTop + 'px';
|
|
546
|
+
toolbar.style.top = 'auto';
|
|
547
|
+
} else {
|
|
548
|
+
toolbar.style.top = stickyTop > 0 ? stickyTop + 'px' : '';
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
511
553
|
/**
|
|
512
554
|
* @description Disable `sticky` toolbar mode.
|
|
513
555
|
*/
|
|
@@ -38,6 +38,8 @@ import { get as getNumber } from '../../helper/numbers';
|
|
|
38
38
|
* @property {HTMLElement} navigation - Navigation element (e.g., for outline or bookmarks).
|
|
39
39
|
* @property {HTMLElement} charWrapper - Wrapper for the character counter element.
|
|
40
40
|
* @property {HTMLElement} charCounter - Element showing the character counter.
|
|
41
|
+
* @property {HTMLElement} wordWrapper - Wrapper for the word counter element.
|
|
42
|
+
* @property {HTMLElement} wordCounter - Element showing the word counter.
|
|
41
43
|
* @property {Window} [_ww] - The window object of the WYSIWYG frame (iframe window).
|
|
42
44
|
* @property {Document} [_wd] - The document object of the WYSIWYG frame (iframe document).
|
|
43
45
|
*
|
|
@@ -171,4 +173,8 @@ export function UpdateStatusbarContext(statusbar, mapper) {
|
|
|
171
173
|
navigation ? mapper.set('navigation', navigation) : mapper.delete('navigation');
|
|
172
174
|
charWrapper ? mapper.set('charWrapper', charWrapper) : mapper.delete('charWrapper');
|
|
173
175
|
charCounter ? mapper.set('charCounter', charCounter) : mapper.delete('charCounter');
|
|
176
|
+
const wordWrapper = statusbar ? statusbar.querySelector('.se-word-counter-wrapper') : null;
|
|
177
|
+
const wordCounter = statusbar ? statusbar.querySelector('.se-word-counter-wrapper .se-word-counter') : null;
|
|
178
|
+
wordWrapper ? mapper.set('wordWrapper', wordWrapper) : mapper.delete('wordWrapper');
|
|
179
|
+
wordCounter ? mapper.set('wordCounter', wordCounter) : mapper.delete('wordCounter');
|
|
174
180
|
}
|
|
@@ -150,6 +150,13 @@ export const DEFAULTS = {
|
|
|
150
150
|
* - `char`: Characters length.
|
|
151
151
|
* - `byte`: Binary data size of characters.
|
|
152
152
|
* - `byte-html`: Binary data size of the full HTML string.
|
|
153
|
+
*
|
|
154
|
+
* === Word Counter ===
|
|
155
|
+
* @property {boolean} [wordCounter=false] - Shows the number of words in the editor.
|
|
156
|
+
* @property {?string} [wordCounter_label=null] - Text to be displayed in the `wordCounter` area of the bottom bar.
|
|
157
|
+
* ```js
|
|
158
|
+
* { wordCounter_label: 'Words :' }
|
|
159
|
+
* ```
|
|
153
160
|
*/
|
|
154
161
|
|
|
155
162
|
/** ================================================================================================================================ */
|
|
@@ -460,7 +467,22 @@ export const DEFAULTS = {
|
|
|
460
467
|
* @property {boolean} [tabDisable=false] - Disables tab key input.
|
|
461
468
|
* @property {string} [toolbar_width="auto"] - Toolbar width.
|
|
462
469
|
* @property {?HTMLElement} [toolbar_container] - Container element for the toolbar.
|
|
463
|
-
* @property {number} [toolbar_sticky=0] - Enables sticky toolbar
|
|
470
|
+
* @property {number|{top: number, offset: number}} [toolbar_sticky=0] - Enables sticky toolbar.
|
|
471
|
+
* - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
|
|
472
|
+
* - `{top, offset}`: `top` is the sticky position when the page header is visible.
|
|
473
|
+
* - `offset` is the sticky position when a virtual keyboard shifts the viewport (e.g., on tablets, touch devices).
|
|
474
|
+
* - When the virtual keyboard is active, `offset` replaces `top` so the toolbar doesn't leave a gap
|
|
475
|
+
* - for a page header that has scrolled out of view. Default `offset` is `0`.
|
|
476
|
+
* ```js
|
|
477
|
+
* // Basic usage — sticky at top with 0px offset
|
|
478
|
+
* toolbar_sticky: 0
|
|
479
|
+
*
|
|
480
|
+
* // Account for a 92px fixed/sticky site header
|
|
481
|
+
* toolbar_sticky: 92
|
|
482
|
+
*
|
|
483
|
+
* // 92px header on desktop, but 0px when virtual keyboard pushes the viewport
|
|
484
|
+
* toolbar_sticky: { top: 92, offset: 0 }
|
|
485
|
+
* ```
|
|
464
486
|
* @property {boolean} [toolbar_hide=false] - Hides toolbar initially.
|
|
465
487
|
* @property {Object} [subToolbar={}] - Sub-toolbar configuration. A secondary toolbar that appears on text selection.
|
|
466
488
|
* @property {SunEditor.UI.ButtonList} [subToolbar.buttonList] - List of Sub-toolbar buttons, grouped by sub-arrays.
|
|
@@ -666,6 +688,8 @@ export const OPTION_FRAME_FIXED_FLAG = {
|
|
|
666
688
|
charCounter_max: true,
|
|
667
689
|
charCounter_label: true,
|
|
668
690
|
charCounter_type: true,
|
|
691
|
+
wordCounter: true,
|
|
692
|
+
wordCounter_label: true,
|
|
669
693
|
};
|
|
670
694
|
|
|
671
695
|
/**
|
|
@@ -754,7 +778,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
754
778
|
};
|
|
755
779
|
|
|
756
780
|
/**
|
|
757
|
-
* @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '
|
|
781
|
+
* @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '_toolbar_sticky' | '_toolbar_sticky_offset' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
|
|
758
782
|
*/
|
|
759
783
|
|
|
760
784
|
/**
|
|
@@ -776,7 +800,8 @@ export const OPTION_FIXED_FLAG = {
|
|
|
776
800
|
* @property {{ reg: RegExp, str: string }} formatBlock
|
|
777
801
|
* @property {string} toolbar_width
|
|
778
802
|
* @property {HTMLElement|null} toolbar_container
|
|
779
|
-
* @property {number}
|
|
803
|
+
* @property {number} _toolbar_sticky
|
|
804
|
+
* @property {number} _toolbar_sticky_offset
|
|
780
805
|
* @property {StrictModeOptions} strictMode
|
|
781
806
|
* @property {string[]} lineAttrReset
|
|
782
807
|
*/
|
|
@@ -616,7 +616,20 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
616
616
|
/** Toolbar */
|
|
617
617
|
o.set('toolbar_width', options.toolbar_width ? (numbers.is(options.toolbar_width) ? options.toolbar_width + 'px' : options.toolbar_width) : 'auto');
|
|
618
618
|
o.set('toolbar_container', options.toolbar_container && !/inline/i.test(o.get('mode')) ? (typeof options.toolbar_container === 'string' ? _d.querySelector(options.toolbar_container) : options.toolbar_container) : null);
|
|
619
|
-
|
|
619
|
+
|
|
620
|
+
const _stickyOpt = options.toolbar_sticky;
|
|
621
|
+
const _isBalloon = /balloon/i.test(o.get('mode'));
|
|
622
|
+
if (_isBalloon) {
|
|
623
|
+
o.set('_toolbar_sticky', -1);
|
|
624
|
+
o.set('_toolbar_sticky_offset', 0);
|
|
625
|
+
} else if (_stickyOpt !== null && typeof _stickyOpt === 'object') {
|
|
626
|
+
o.set('_toolbar_sticky', numbers.get(_stickyOpt.top, 0));
|
|
627
|
+
o.set('_toolbar_sticky_offset', numbers.get(_stickyOpt.offset, 0));
|
|
628
|
+
} else {
|
|
629
|
+
o.set('_toolbar_sticky', _stickyOpt === undefined ? 0 : numbers.is(_stickyOpt) ? _stickyOpt : -1);
|
|
630
|
+
o.set('_toolbar_sticky_offset', 0);
|
|
631
|
+
}
|
|
632
|
+
|
|
620
633
|
o.set('toolbar_hide', !!options.toolbar_hide);
|
|
621
634
|
|
|
622
635
|
/** subToolbar */
|
|
@@ -759,12 +772,14 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
759
772
|
* @description Create a context object for the editor frame.
|
|
760
773
|
* @param {SunEditor.FrameOptions} targetOptions - `editor.frameOptions`
|
|
761
774
|
* @param {HTMLElement} statusbar - statusbar element
|
|
762
|
-
* @returns {{statusbar: HTMLElement, navigation: HTMLElement, charWrapper: HTMLElement, charCounter: HTMLElement}}
|
|
775
|
+
* @returns {{statusbar: HTMLElement, navigation: HTMLElement, charWrapper: HTMLElement, charCounter: HTMLElement, wordWrapper: HTMLElement, wordCounter: HTMLElement}}
|
|
763
776
|
*/
|
|
764
777
|
export function CreateStatusbar(targetOptions, statusbar) {
|
|
765
778
|
let navigation = null;
|
|
766
779
|
let charWrapper = null;
|
|
767
780
|
let charCounter = null;
|
|
781
|
+
let wordWrapper = null;
|
|
782
|
+
let wordCounter = null;
|
|
768
783
|
|
|
769
784
|
if (targetOptions.get('statusbar')) {
|
|
770
785
|
statusbar ||= dom.utils.createElement('DIV', { class: 'se-status-bar sun-editor-common' });
|
|
@@ -773,10 +788,31 @@ export function CreateStatusbar(targetOptions, statusbar) {
|
|
|
773
788
|
navigation = statusbar.querySelector('.se-navigation') || dom.utils.createElement('DIV', { class: 'se-navigation sun-editor-common' });
|
|
774
789
|
statusbar.appendChild(navigation);
|
|
775
790
|
|
|
776
|
-
/**
|
|
791
|
+
/** word counter (left) */
|
|
792
|
+
if (targetOptions.get('wordCounter')) {
|
|
793
|
+
wordWrapper = statusbar.querySelector('.se-word-counter-wrapper') || dom.utils.createElement('DIV', { class: 'se-word-counter-wrapper' });
|
|
794
|
+
|
|
795
|
+
if (targetOptions.get('wordCounter_label')) {
|
|
796
|
+
const wordLabel = wordWrapper.querySelector('.se-word-label') || dom.utils.createElement('SPAN', { class: 'se-word-label' });
|
|
797
|
+
wordLabel.textContent = targetOptions.get('wordCounter_label');
|
|
798
|
+
wordWrapper.appendChild(wordLabel);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
wordCounter = wordWrapper.querySelector('.se-word-counter') || dom.utils.createElement('SPAN', { class: 'se-word-counter' });
|
|
802
|
+
wordCounter.textContent = '0';
|
|
803
|
+
wordWrapper.appendChild(wordCounter);
|
|
804
|
+
|
|
805
|
+
statusbar.appendChild(wordWrapper);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** char counter (right) */
|
|
777
809
|
if (targetOptions.get('charCounter')) {
|
|
778
810
|
charWrapper = statusbar.querySelector('.se-char-counter-wrapper') || dom.utils.createElement('DIV', { class: 'se-char-counter-wrapper' });
|
|
779
811
|
|
|
812
|
+
if (targetOptions.get('wordCounter') && charWrapper.className.indexOf('se-with-word-counter') === -1) {
|
|
813
|
+
charWrapper.className += ' se-with-word-counter';
|
|
814
|
+
}
|
|
815
|
+
|
|
780
816
|
if (targetOptions.get('charCounter_label')) {
|
|
781
817
|
const charLabel = charWrapper.querySelector('.se-char-label') || dom.utils.createElement('SPAN', { class: 'se-char-label' });
|
|
782
818
|
charLabel.textContent = targetOptions.get('charCounter_label');
|
|
@@ -802,6 +838,8 @@ export function CreateStatusbar(targetOptions, statusbar) {
|
|
|
802
838
|
navigation: /** @type {HTMLElement} */ (navigation),
|
|
803
839
|
charWrapper: /** @type {HTMLElement} */ (charWrapper),
|
|
804
840
|
charCounter: /** @type {HTMLElement} */ (charCounter),
|
|
841
|
+
wordWrapper: /** @type {HTMLElement} */ (wordWrapper),
|
|
842
|
+
wordCounter: /** @type {HTMLElement} */ (wordCounter),
|
|
805
843
|
};
|
|
806
844
|
}
|
|
807
845
|
|
|
@@ -839,6 +877,8 @@ function InitFrameOptions(o, origin) {
|
|
|
839
877
|
const charCounter_max = barContainer || o.charCounter_max === undefined ? origin.charCounter_max : o.charCounter_max;
|
|
840
878
|
const charCounter_label = barContainer || o.charCounter_label === undefined ? origin.charCounter_label : o.charCounter_label;
|
|
841
879
|
const charCounter_type = barContainer || o.charCounter_type === undefined ? origin.charCounter_type : o.charCounter_type;
|
|
880
|
+
const wordCounter = barContainer || o.wordCounter === undefined ? origin.wordCounter : o.wordCounter;
|
|
881
|
+
const wordCounter_label = barContainer || o.wordCounter_label === undefined ? origin.wordCounter_label : o.wordCounter_label;
|
|
842
882
|
|
|
843
883
|
// value
|
|
844
884
|
fo.set('value', value);
|
|
@@ -868,6 +908,9 @@ function InitFrameOptions(o, origin) {
|
|
|
868
908
|
fo.set('charCounter_max', numbers.is(charCounter_max) && charCounter_max > -1 ? charCounter_max * 1 : null);
|
|
869
909
|
fo.set('charCounter_label', typeof charCounter_label === 'string' ? charCounter_label.trim() : null);
|
|
870
910
|
fo.set('charCounter_type', typeof charCounter_type === 'string' ? charCounter_type : 'char');
|
|
911
|
+
// status bar - word count
|
|
912
|
+
fo.set('wordCounter', typeof wordCounter === 'boolean' ? wordCounter : false);
|
|
913
|
+
fo.set('wordCounter_label', typeof wordCounter_label === 'string' ? wordCounter_label.trim() : null);
|
|
871
914
|
|
|
872
915
|
return fo;
|
|
873
916
|
}
|
|
@@ -166,7 +166,7 @@ export function makePorts(
|
|
|
166
166
|
formatAttrsTempCache: (attrs: any) => any;
|
|
167
167
|
setOnShortcutKey: (v: any) => any;
|
|
168
168
|
/**
|
|
169
|
-
* @description Scrolls the editor view to the caret position after pressing `Enter`.
|
|
169
|
+
* @description Scrolls the editor view to the caret position after pressing `Enter`.
|
|
170
170
|
* @param {Range} range Range object
|
|
171
171
|
*/
|
|
172
172
|
enterScrollTo(range: Range): void;
|
|
@@ -36,7 +36,16 @@ declare class Char {
|
|
|
36
36
|
*/
|
|
37
37
|
getByteLength(text: string): number;
|
|
38
38
|
/**
|
|
39
|
-
* @description
|
|
39
|
+
* @description Get the number of words in the content.
|
|
40
|
+
* - If [content] is `undefined`, get the current editor's word count.
|
|
41
|
+
* @param {string} [content] Content to count. (default: wysiwyg textContent)
|
|
42
|
+
* @returns {number}
|
|
43
|
+
* const currentWords = editor.$.char.getWordCount();
|
|
44
|
+
* const textWords = editor.$.char.getWordCount('Hello World');
|
|
45
|
+
*/
|
|
46
|
+
getWordCount(content?: string): number;
|
|
47
|
+
/**
|
|
48
|
+
* @description Set the char count and word count to counter element textContent.
|
|
40
49
|
* @param {?SunEditor.FrameContext} [fc] Frame context
|
|
41
50
|
*/
|
|
42
51
|
display(fc?: SunEditor.FrameContext | null): void;
|