@blocklet/editor 2.5.2 → 2.5.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/lib/ext/CharacterLimitPlugin/index.js +1 -1
- package/lib/vendor/LexicalCharacterLimitPlugin/LexicalCharacterLimitPlugin.d.ts +15 -0
- package/lib/vendor/LexicalCharacterLimitPlugin/LexicalCharacterLimitPlugin.js +45 -0
- package/lib/vendor/LexicalCharacterLimitPlugin/index.d.ts +1 -0
- package/lib/vendor/LexicalCharacterLimitPlugin/index.js +1 -0
- package/lib/vendor/LexicalCharacterLimitPlugin/useCharacterLimit.d.ts +16 -0
- package/lib/vendor/LexicalCharacterLimitPlugin/useCharacterLimit.js +208 -0
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
-
import { CharacterLimitPlugin as LexicalCharacterLimitPlugin } from '@lexical/react/LexicalCharacterLimitPlugin';
|
|
4
3
|
import { Box } from '@mui/material';
|
|
4
|
+
import { CharacterLimitPlugin as LexicalCharacterLimitPlugin } from '../../vendor/LexicalCharacterLimitPlugin';
|
|
5
5
|
import { MaxLengthPlugin } from '../../main/plugins/MaxLengthPlugin';
|
|
6
6
|
export function CharacterLimitPlugin({ maxLength, charLimitIndicatorStyle, alignLeft = false, }) {
|
|
7
7
|
const [editor] = useLexicalComposerContext();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type { JSX } from 'react';
|
|
9
|
+
export declare function CharacterLimitPlugin({ charset, maxLength, renderer, }: {
|
|
10
|
+
charset: 'UTF-8' | 'UTF-16';
|
|
11
|
+
maxLength: number;
|
|
12
|
+
renderer?: ({ remainingCharacters }: {
|
|
13
|
+
remainingCharacters: number;
|
|
14
|
+
}) => JSX.Element;
|
|
15
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { useCharacterLimit } from './useCharacterLimit';
|
|
5
|
+
const CHARACTER_LIMIT = 5;
|
|
6
|
+
let textEncoderInstance = null;
|
|
7
|
+
function textEncoder() {
|
|
8
|
+
if (window.TextEncoder === undefined) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (textEncoderInstance === null) {
|
|
12
|
+
textEncoderInstance = new window.TextEncoder();
|
|
13
|
+
}
|
|
14
|
+
return textEncoderInstance;
|
|
15
|
+
}
|
|
16
|
+
function utf8Length(text) {
|
|
17
|
+
const currentTextEncoder = textEncoder();
|
|
18
|
+
if (currentTextEncoder === null) {
|
|
19
|
+
// http://stackoverflow.com/a/5515960/210370
|
|
20
|
+
const m = encodeURIComponent(text).match(/%[89ABab]/g);
|
|
21
|
+
return text.length + (m ? m.length : 0);
|
|
22
|
+
}
|
|
23
|
+
return currentTextEncoder.encode(text).length;
|
|
24
|
+
}
|
|
25
|
+
function DefaultRenderer({ remainingCharacters }) {
|
|
26
|
+
return (_jsx("span", { className: `characters-limit ${remainingCharacters < 0 ? 'characters-limit-exceeded' : ''}`, children: remainingCharacters }));
|
|
27
|
+
}
|
|
28
|
+
export function CharacterLimitPlugin({ charset = 'UTF-16', maxLength = CHARACTER_LIMIT, renderer = DefaultRenderer, }) {
|
|
29
|
+
const [editor] = useLexicalComposerContext();
|
|
30
|
+
const [remainingCharacters, setRemainingCharacters] = useState(maxLength);
|
|
31
|
+
const characterLimitProps = useMemo(() => ({
|
|
32
|
+
remainingCharacters: setRemainingCharacters,
|
|
33
|
+
strlen: (text) => {
|
|
34
|
+
if (charset === 'UTF-8') {
|
|
35
|
+
return utf8Length(text);
|
|
36
|
+
}
|
|
37
|
+
if (charset === 'UTF-16') {
|
|
38
|
+
return text.length;
|
|
39
|
+
}
|
|
40
|
+
throw new Error('Unrecognized charset');
|
|
41
|
+
},
|
|
42
|
+
}), [charset]);
|
|
43
|
+
useCharacterLimit(editor, maxLength, characterLimitProps);
|
|
44
|
+
return renderer({ remainingCharacters });
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LexicalCharacterLimitPlugin';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LexicalCharacterLimitPlugin';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type { LexicalEditor } from 'lexical';
|
|
9
|
+
import { OverflowNode } from '@lexical/overflow';
|
|
10
|
+
type OptionalProps = {
|
|
11
|
+
remainingCharacters?: (characters: number) => void;
|
|
12
|
+
strlen?: (input: string) => number;
|
|
13
|
+
};
|
|
14
|
+
export declare function useCharacterLimit(editor: LexicalEditor, maxCharacters: number, optional?: OptionalProps): void;
|
|
15
|
+
export declare function $mergePrevious(overflowNode: OverflowNode): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { $createOverflowNode, $isOverflowNode, OverflowNode } from '@lexical/overflow';
|
|
9
|
+
import { $rootTextContent } from '@lexical/text';
|
|
10
|
+
import { $dfs, $unwrapNode, mergeRegister } from '@lexical/utils';
|
|
11
|
+
import { $getSelection, $isElementNode, $isLeafNode, $isRangeSelection, $isTextNode, $setSelection, COMMAND_PRIORITY_LOW, DELETE_CHARACTER_COMMAND, } from 'lexical';
|
|
12
|
+
import { useEffect } from 'react';
|
|
13
|
+
import invariant from '../../shared/invariant';
|
|
14
|
+
export function useCharacterLimit(editor, maxCharacters, optional = Object.freeze({})) {
|
|
15
|
+
const { strlen = (input) => input.length,
|
|
16
|
+
// UTF-16
|
|
17
|
+
remainingCharacters = () => { }, } = optional;
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!editor.hasNodes([OverflowNode])) {
|
|
20
|
+
invariant(false, 'useCharacterLimit: OverflowNode not registered on editor');
|
|
21
|
+
}
|
|
22
|
+
}, [editor]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let text = editor.getEditorState().read($rootTextContent);
|
|
25
|
+
let lastComputedTextLength = 0;
|
|
26
|
+
return mergeRegister(editor.registerTextContentListener((currentText) => {
|
|
27
|
+
text = currentText;
|
|
28
|
+
}), editor.registerUpdateListener((payload) => {
|
|
29
|
+
if (!payload)
|
|
30
|
+
return;
|
|
31
|
+
const { dirtyLeaves, dirtyElements } = payload;
|
|
32
|
+
const isComposing = editor.isComposing();
|
|
33
|
+
const hasContentChanges = dirtyLeaves.size > 0 || dirtyElements.size > 0;
|
|
34
|
+
if (isComposing || !hasContentChanges) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const textLength = strlen(text);
|
|
38
|
+
const textLengthAboveThreshold = textLength > maxCharacters || (lastComputedTextLength !== null && lastComputedTextLength > maxCharacters);
|
|
39
|
+
const diff = maxCharacters - textLength;
|
|
40
|
+
remainingCharacters(diff);
|
|
41
|
+
if (lastComputedTextLength === null || textLengthAboveThreshold) {
|
|
42
|
+
const offset = findOffset(text, maxCharacters, strlen);
|
|
43
|
+
editor.update(() => {
|
|
44
|
+
$wrapOverflowedNodes(offset);
|
|
45
|
+
}, {
|
|
46
|
+
tag: 'history-merge',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
lastComputedTextLength = textLength;
|
|
50
|
+
}), editor.registerCommand(DELETE_CHARACTER_COMMAND, (isBackward) => {
|
|
51
|
+
const selection = $getSelection();
|
|
52
|
+
if (!$isRangeSelection(selection)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const anchorNode = selection.anchor.getNode();
|
|
56
|
+
const overflow = anchorNode.getParent();
|
|
57
|
+
const overflowParent = overflow ? overflow.getParent() : null;
|
|
58
|
+
const parentNext = overflowParent ? overflowParent.getNextSibling() : null;
|
|
59
|
+
selection.deleteCharacter(isBackward);
|
|
60
|
+
if (overflowParent && overflowParent.isEmpty()) {
|
|
61
|
+
overflowParent.remove();
|
|
62
|
+
}
|
|
63
|
+
else if ($isElementNode(parentNext) && parentNext.isEmpty()) {
|
|
64
|
+
parentNext.remove();
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}, COMMAND_PRIORITY_LOW));
|
|
68
|
+
}, [editor, maxCharacters, remainingCharacters, strlen]);
|
|
69
|
+
}
|
|
70
|
+
function findOffset(text, maxCharacters, strlen) {
|
|
71
|
+
const { Segmenter } = Intl;
|
|
72
|
+
let offsetUtf16 = 0;
|
|
73
|
+
let offset = 0;
|
|
74
|
+
if (typeof Segmenter === 'function') {
|
|
75
|
+
const segmenter = new Segmenter();
|
|
76
|
+
const graphemes = segmenter.segment(text);
|
|
77
|
+
for (const { segment: grapheme } of graphemes) {
|
|
78
|
+
const nextOffset = offset + strlen(grapheme);
|
|
79
|
+
if (nextOffset > maxCharacters) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
offset = nextOffset;
|
|
83
|
+
offsetUtf16 += grapheme.length;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const codepoints = Array.from(text);
|
|
88
|
+
const codepointsLength = codepoints.length;
|
|
89
|
+
for (let i = 0; i < codepointsLength; i++) {
|
|
90
|
+
const codepoint = codepoints[i];
|
|
91
|
+
const nextOffset = offset + strlen(codepoint);
|
|
92
|
+
if (nextOffset > maxCharacters) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
offset = nextOffset;
|
|
96
|
+
offsetUtf16 += codepoint.length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return offsetUtf16;
|
|
100
|
+
}
|
|
101
|
+
function $wrapOverflowedNodes(offset) {
|
|
102
|
+
const dfsNodes = $dfs().filter(Boolean);
|
|
103
|
+
const dfsNodesLength = dfsNodes.length;
|
|
104
|
+
let accumulatedLength = 0;
|
|
105
|
+
for (let i = 0; i < dfsNodesLength; i += 1) {
|
|
106
|
+
const { node } = dfsNodes[i];
|
|
107
|
+
if ($isOverflowNode(node)) {
|
|
108
|
+
const previousLength = accumulatedLength;
|
|
109
|
+
const nextLength = accumulatedLength + node.getTextContentSize();
|
|
110
|
+
if (nextLength <= offset) {
|
|
111
|
+
const parent = node.getParent();
|
|
112
|
+
const previousSibling = node.getPreviousSibling();
|
|
113
|
+
const nextSibling = node.getNextSibling();
|
|
114
|
+
$unwrapNode(node);
|
|
115
|
+
const selection = $getSelection();
|
|
116
|
+
// Restore selection when the overflow children are removed
|
|
117
|
+
if ($isRangeSelection(selection) &&
|
|
118
|
+
(!selection.anchor.getNode().isAttached() || !selection.focus.getNode().isAttached())) {
|
|
119
|
+
if ($isTextNode(previousSibling)) {
|
|
120
|
+
previousSibling.select();
|
|
121
|
+
}
|
|
122
|
+
else if ($isTextNode(nextSibling)) {
|
|
123
|
+
nextSibling.select();
|
|
124
|
+
}
|
|
125
|
+
else if (parent !== null) {
|
|
126
|
+
parent.select();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (previousLength < offset) {
|
|
131
|
+
const descendant = node.getFirstDescendant();
|
|
132
|
+
const descendantLength = descendant !== null ? descendant.getTextContentSize() : 0;
|
|
133
|
+
const previousPlusDescendantLength = previousLength + descendantLength;
|
|
134
|
+
// For simple text we can redimension the overflow into a smaller and more accurate
|
|
135
|
+
// container
|
|
136
|
+
const firstDescendantIsSimpleText = $isTextNode(descendant) && descendant.isSimpleText();
|
|
137
|
+
const firstDescendantDoesNotOverflow = previousPlusDescendantLength <= offset;
|
|
138
|
+
if (firstDescendantIsSimpleText || firstDescendantDoesNotOverflow) {
|
|
139
|
+
$unwrapNode(node);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if ($isLeafNode(node)) {
|
|
144
|
+
const previousAccumulatedLength = accumulatedLength;
|
|
145
|
+
accumulatedLength += node.getTextContentSize();
|
|
146
|
+
if (accumulatedLength > offset && !$isOverflowNode(node.getParent())) {
|
|
147
|
+
const previousSelection = $getSelection();
|
|
148
|
+
let overflowNode;
|
|
149
|
+
// For simple text we can improve the limit accuracy by splitting the TextNode
|
|
150
|
+
// on the split point
|
|
151
|
+
if (previousAccumulatedLength < offset && $isTextNode(node) && node.isSimpleText()) {
|
|
152
|
+
const [, overflowedText] = node.splitText(offset - previousAccumulatedLength);
|
|
153
|
+
overflowNode = $wrapNode(overflowedText);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
overflowNode = $wrapNode(node);
|
|
157
|
+
}
|
|
158
|
+
if (previousSelection !== null) {
|
|
159
|
+
$setSelection(previousSelection);
|
|
160
|
+
}
|
|
161
|
+
$mergePrevious(overflowNode);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function $wrapNode(node) {
|
|
167
|
+
const overflowNode = $createOverflowNode();
|
|
168
|
+
node.replace(overflowNode);
|
|
169
|
+
overflowNode.append(node);
|
|
170
|
+
return overflowNode;
|
|
171
|
+
}
|
|
172
|
+
export function $mergePrevious(overflowNode) {
|
|
173
|
+
const previousNode = overflowNode.getPreviousSibling();
|
|
174
|
+
if (!$isOverflowNode(previousNode)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const firstChild = overflowNode.getFirstChild();
|
|
178
|
+
const previousNodeChildren = previousNode.getChildren();
|
|
179
|
+
const previousNodeChildrenLength = previousNodeChildren.length;
|
|
180
|
+
if (firstChild === null) {
|
|
181
|
+
overflowNode.append(...previousNodeChildren);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
for (let i = 0; i < previousNodeChildrenLength; i++) {
|
|
185
|
+
firstChild.insertBefore(previousNodeChildren[i]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const selection = $getSelection();
|
|
189
|
+
if ($isRangeSelection(selection)) {
|
|
190
|
+
const { anchor } = selection;
|
|
191
|
+
const anchorNode = anchor.getNode();
|
|
192
|
+
const { focus } = selection;
|
|
193
|
+
const focusNode = anchor.getNode();
|
|
194
|
+
if (anchorNode.is(previousNode)) {
|
|
195
|
+
anchor.set(overflowNode.getKey(), anchor.offset, 'element');
|
|
196
|
+
}
|
|
197
|
+
else if (anchorNode.is(overflowNode)) {
|
|
198
|
+
anchor.set(overflowNode.getKey(), previousNodeChildrenLength + anchor.offset, 'element');
|
|
199
|
+
}
|
|
200
|
+
if (focusNode.is(previousNode)) {
|
|
201
|
+
focus.set(overflowNode.getKey(), focus.offset, 'element');
|
|
202
|
+
}
|
|
203
|
+
else if (focusNode.is(overflowNode)) {
|
|
204
|
+
focus.set(overflowNode.getKey(), previousNodeChildrenLength + focus.offset, 'element');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
previousNode.remove();
|
|
208
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"ufo": "^1.5.4",
|
|
74
74
|
"url-join": "^4.0.1",
|
|
75
75
|
"zustand": "^4.5.5",
|
|
76
|
-
"@blocklet/pdf": "2.5.
|
|
76
|
+
"@blocklet/pdf": "2.5.4"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@babel/core": "^7.25.2",
|