@lexical/text 0.13.1 → 0.14.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,288 @@
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
+ import { $isElementNode, $isTextNode, $getRoot, $isDecoratorNode, $isParagraphNode, TextNode, $createTextNode } from 'lexical';
8
+
9
+ /** @module @lexical/text */
10
+ /**
11
+ * Finds a TextNode with a size larger than targetCharacters and returns
12
+ * the node along with the remaining length of the text.
13
+ * @param root - The RootNode.
14
+ * @param targetCharacters - The number of characters whose TextNode must be larger than.
15
+ * @returns The TextNode and the intersections offset, or null if no TextNode is found.
16
+ */
17
+ function $findTextIntersectionFromCharacters(root, targetCharacters) {
18
+ let node = root.getFirstChild();
19
+ let currentCharacters = 0;
20
+ mainLoop: while (node !== null) {
21
+ if ($isElementNode(node)) {
22
+ const child = node.getFirstChild();
23
+ if (child !== null) {
24
+ node = child;
25
+ continue;
26
+ }
27
+ } else if ($isTextNode(node)) {
28
+ const characters = node.getTextContentSize();
29
+ if (currentCharacters + characters > targetCharacters) {
30
+ return {
31
+ node,
32
+ offset: targetCharacters - currentCharacters
33
+ };
34
+ }
35
+ currentCharacters += characters;
36
+ }
37
+ const sibling = node.getNextSibling();
38
+ if (sibling !== null) {
39
+ node = sibling;
40
+ continue;
41
+ }
42
+ let parent = node.getParent();
43
+ while (parent !== null) {
44
+ const parentSibling = parent.getNextSibling();
45
+ if (parentSibling !== null) {
46
+ node = parentSibling;
47
+ continue mainLoop;
48
+ }
49
+ parent = parent.getParent();
50
+ }
51
+ break;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Determines if the root has any text content and can trim any whitespace if it does.
58
+ * @param isEditorComposing - Is the editor in composition mode due to an active Input Method Editor?
59
+ * @param trim - Should the root text have its whitespaced trimmed? Defaults to true.
60
+ * @returns true if text content is empty, false if there is text or isEditorComposing is true.
61
+ */
62
+ function $isRootTextContentEmpty(isEditorComposing, trim = true) {
63
+ if (isEditorComposing) {
64
+ return false;
65
+ }
66
+ let text = $rootTextContent();
67
+ if (trim) {
68
+ text = text.trim();
69
+ }
70
+ return text === '';
71
+ }
72
+
73
+ /**
74
+ * Returns a function that executes {@link $isRootTextContentEmpty}
75
+ * @param isEditorComposing - Is the editor in composition mode due to an active Input Method Editor?
76
+ * @param trim - Should the root text have its whitespaced trimmed? Defaults to true.
77
+ * @returns A function that executes $isRootTextContentEmpty based on arguments.
78
+ */
79
+ function $isRootTextContentEmptyCurry(isEditorComposing, trim) {
80
+ return () => $isRootTextContentEmpty(isEditorComposing, trim);
81
+ }
82
+
83
+ /**
84
+ * Returns the root's text content.
85
+ * @returns The root's text content.
86
+ */
87
+ function $rootTextContent() {
88
+ const root = $getRoot();
89
+ return root.getTextContent();
90
+ }
91
+
92
+ /**
93
+ * Determines if the input should show the placeholder. If anything is in
94
+ * in the root the placeholder should not be shown.
95
+ * @param isComposing - Is the editor in composition mode due to an active Input Method Editor?
96
+ * @returns true if the input should show the placeholder, false otherwise.
97
+ */
98
+ function $canShowPlaceholder(isComposing) {
99
+ if (!$isRootTextContentEmpty(isComposing, false)) {
100
+ return false;
101
+ }
102
+ const root = $getRoot();
103
+ const children = root.getChildren();
104
+ const childrenLength = children.length;
105
+ if (childrenLength > 1) {
106
+ return false;
107
+ }
108
+ for (let i = 0; i < childrenLength; i++) {
109
+ const topBlock = children[i];
110
+ if ($isDecoratorNode(topBlock)) {
111
+ return false;
112
+ }
113
+ if ($isElementNode(topBlock)) {
114
+ if (!$isParagraphNode(topBlock)) {
115
+ return false;
116
+ }
117
+ if (topBlock.__indent !== 0) {
118
+ return false;
119
+ }
120
+ const topBlockChildren = topBlock.getChildren();
121
+ const topBlockChildrenLength = topBlockChildren.length;
122
+ for (let s = 0; s < topBlockChildrenLength; s++) {
123
+ const child = topBlockChildren[i];
124
+ if (!$isTextNode(child)) {
125
+ return false;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Returns a function that executes {@link $canShowPlaceholder}
135
+ * @param isEditorComposing - Is the editor in composition mode due to an active Input Method Editor?
136
+ * @returns A function that executes $canShowPlaceholder with arguments.
137
+ */
138
+ function $canShowPlaceholderCurry(isEditorComposing) {
139
+ return () => $canShowPlaceholder(isEditorComposing);
140
+ }
141
+ /**
142
+ * Returns a tuple that can be rested (...) into mergeRegister to clean up
143
+ * node transforms listeners that transforms text into another node, eg. a HashtagNode.
144
+ * @example
145
+ * ```ts
146
+ * useEffect(() => {
147
+ return mergeRegister(
148
+ ...registerLexicalTextEntity(editor, getMatch, targetNode, createNode),
149
+ );
150
+ }, [createNode, editor, getMatch, targetNode]);
151
+ * ```
152
+ * Where targetNode is the type of node containing the text you want to transform (like a text input),
153
+ * then getMatch uses a regex to find a matching text and creates the proper node to include the matching text.
154
+ * @param editor - The lexical editor.
155
+ * @param getMatch - Finds a matching string that satisfies a regex expression.
156
+ * @param targetNode - The node type that contains text to match with. eg. HashtagNode
157
+ * @param createNode - A function that creates a new node to contain the matched text. eg createHashtagNode
158
+ * @returns An array containing the plain text and reverse node transform listeners.
159
+ */
160
+ function registerLexicalTextEntity(editor, getMatch, targetNode, createNode) {
161
+ const isTargetNode = node => {
162
+ return node instanceof targetNode;
163
+ };
164
+ const replaceWithSimpleText = node => {
165
+ const textNode = $createTextNode(node.getTextContent());
166
+ textNode.setFormat(node.getFormat());
167
+ node.replace(textNode);
168
+ };
169
+ const getMode = node => {
170
+ return node.getLatest().__mode;
171
+ };
172
+ const textNodeTransform = node => {
173
+ if (!node.isSimpleText()) {
174
+ return;
175
+ }
176
+ const prevSibling = node.getPreviousSibling();
177
+ let text = node.getTextContent();
178
+ let currentNode = node;
179
+ let match;
180
+ if ($isTextNode(prevSibling)) {
181
+ const previousText = prevSibling.getTextContent();
182
+ const combinedText = previousText + text;
183
+ const prevMatch = getMatch(combinedText);
184
+ if (isTargetNode(prevSibling)) {
185
+ if (prevMatch === null || getMode(prevSibling) !== 0) {
186
+ replaceWithSimpleText(prevSibling);
187
+ return;
188
+ } else {
189
+ const diff = prevMatch.end - previousText.length;
190
+ if (diff > 0) {
191
+ const concatText = text.slice(0, diff);
192
+ const newTextContent = previousText + concatText;
193
+ prevSibling.select();
194
+ prevSibling.setTextContent(newTextContent);
195
+ if (diff === text.length) {
196
+ node.remove();
197
+ } else {
198
+ const remainingText = text.slice(diff);
199
+ node.setTextContent(remainingText);
200
+ }
201
+ return;
202
+ }
203
+ }
204
+ } else if (prevMatch === null || prevMatch.start < previousText.length) {
205
+ return;
206
+ }
207
+ }
208
+
209
+ // eslint-disable-next-line no-constant-condition
210
+ while (true) {
211
+ match = getMatch(text);
212
+ let nextText = match === null ? '' : text.slice(match.end);
213
+ text = nextText;
214
+ if (nextText === '') {
215
+ const nextSibling = currentNode.getNextSibling();
216
+ if ($isTextNode(nextSibling)) {
217
+ nextText = currentNode.getTextContent() + nextSibling.getTextContent();
218
+ const nextMatch = getMatch(nextText);
219
+ if (nextMatch === null) {
220
+ if (isTargetNode(nextSibling)) {
221
+ replaceWithSimpleText(nextSibling);
222
+ } else {
223
+ nextSibling.markDirty();
224
+ }
225
+ return;
226
+ } else if (nextMatch.start !== 0) {
227
+ return;
228
+ }
229
+ }
230
+ } else {
231
+ const nextMatch = getMatch(nextText);
232
+ if (nextMatch !== null && nextMatch.start === 0) {
233
+ return;
234
+ }
235
+ }
236
+ if (match === null) {
237
+ return;
238
+ }
239
+ if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) {
240
+ continue;
241
+ }
242
+ let nodeToReplace;
243
+ if (match.start === 0) {
244
+ [nodeToReplace, currentNode] = currentNode.splitText(match.end);
245
+ } else {
246
+ [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end);
247
+ }
248
+ const replacementNode = createNode(nodeToReplace);
249
+ replacementNode.setFormat(nodeToReplace.getFormat());
250
+ nodeToReplace.replace(replacementNode);
251
+ if (currentNode == null) {
252
+ return;
253
+ }
254
+ }
255
+ };
256
+ const reverseNodeTransform = node => {
257
+ const text = node.getTextContent();
258
+ const match = getMatch(text);
259
+ if (match === null || match.start !== 0) {
260
+ replaceWithSimpleText(node);
261
+ return;
262
+ }
263
+ if (text.length > match.end) {
264
+ // This will split out the rest of the text as simple text
265
+ node.splitText(match.end);
266
+ return;
267
+ }
268
+ const prevSibling = node.getPreviousSibling();
269
+ if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {
270
+ replaceWithSimpleText(prevSibling);
271
+ replaceWithSimpleText(node);
272
+ }
273
+ const nextSibling = node.getNextSibling();
274
+ if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
275
+ replaceWithSimpleText(nextSibling);
276
+
277
+ // This may have already been converted in the previous block
278
+ if (isTargetNode(node)) {
279
+ replaceWithSimpleText(node);
280
+ }
281
+ }
282
+ };
283
+ const removePlainTextTransform = editor.registerNodeTransform(TextNode, textNodeTransform);
284
+ const removeReverseNodeTransform = editor.registerNodeTransform(targetNode, reverseNodeTransform);
285
+ return [removePlainTextTransform, removeReverseNodeTransform];
286
+ }
287
+
288
+ export { $canShowPlaceholder, $canShowPlaceholderCurry, $findTextIntersectionFromCharacters, $isRootTextContentEmpty, $isRootTextContentEmptyCurry, $rootTextContent, registerLexicalTextEntity };
@@ -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
+ import * as modDev from './LexicalText.dev.esm.js';
8
+ import * as modProd from './LexicalText.prod.esm.js';
9
+ const mod = process.env.NODE_ENV === 'development' ? modDev : modProd;
10
+ export const $canShowPlaceholder = mod.$canShowPlaceholder;
11
+ export const $canShowPlaceholderCurry = mod.$canShowPlaceholderCurry;
12
+ export const $findTextIntersectionFromCharacters = mod.$findTextIntersectionFromCharacters;
13
+ export const $isRootTextContentEmpty = mod.$isRootTextContentEmpty;
14
+ export const $isRootTextContentEmptyCurry = mod.$isRootTextContentEmptyCurry;
15
+ export const $rootTextContent = mod.$rootTextContent;
16
+ export const registerLexicalTextEntity = mod.registerLexicalTextEntity;
package/LexicalText.js CHANGED
@@ -5,5 +5,5 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  'use strict'
8
- const LexicalText = process.env.NODE_ENV === 'development' ? require('./LexicalText.dev.js') : require('./LexicalText.prod.js')
8
+ const LexicalText = process.env.NODE_ENV === 'development' ? require('./LexicalText.dev.js') : require('./LexicalText.prod.js');
9
9
  module.exports = LexicalText;
@@ -0,0 +1,7 @@
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
+ import{$isElementNode as t,$isTextNode as e,$getRoot as n,$isDecoratorNode as r,$isParagraphNode as i,TextNode as o,$createTextNode as l}from"lexical";function s(n,r){let i=n.getFirstChild(),o=0;t:for(;null!==i;){if(t(i)){const t=i.getFirstChild();if(null!==t){i=t;continue}}else if(e(i)){const t=i.getTextContentSize();if(o+t>r)return{node:i,offset:r-o};o+=t}const n=i.getNextSibling();if(null!==n){i=n;continue}let l=i.getParent();for(;null!==l;){const t=l.getNextSibling();if(null!==t){i=t;continue t}l=l.getParent()}break}return null}function u(t,e=!0){if(t)return!1;let n=c();return e&&(n=n.trim()),""===n}function f(t,e){return()=>u(t,e)}function c(){return n().getTextContent()}function g(o){if(!u(o,!1))return!1;const l=n().getChildren(),s=l.length;if(s>1)return!1;for(let n=0;n<s;n++){const o=l[n];if(r(o))return!1;if(t(o)){if(!i(o))return!1;if(0!==o.__indent)return!1;const t=o.getChildren(),r=t.length;for(let i=0;i<r;i++){const r=t[n];if(!e(r))return!1}}}return!0}function x(t){return()=>g(t)}function a(t,n,r,i){const s=t=>t instanceof r,u=t=>{const e=l(t.getTextContent());e.setFormat(t.getFormat()),t.replace(e)};return[t.registerNodeTransform(o,(t=>{if(!t.isSimpleText())return;const r=t.getPreviousSibling();let o,l=t.getTextContent(),f=t;if(e(r)){const e=r.getTextContent(),i=n(e+l);if(s(r)){if(null===i||0!==(t=>t.getLatest().__mode)(r))return void u(r);{const n=i.end-e.length;if(n>0){const i=e+l.slice(0,n);if(r.select(),r.setTextContent(i),n===l.length)t.remove();else{const e=l.slice(n);t.setTextContent(e)}return}}}else if(null===i||i.start<e.length)return}for(;;){o=n(l);let t,c=null===o?"":l.slice(o.end);if(l=c,""===c){const t=f.getNextSibling();if(e(t)){c=f.getTextContent()+t.getTextContent();const e=n(c);if(null===e)return void(s(t)?u(t):t.markDirty());if(0!==e.start)return}}else{const t=n(c);if(null!==t&&0===t.start)return}if(null===o)return;if(0===o.start&&e(r)&&r.isTextEntity())continue;0===o.start?[t,f]=f.splitText(o.end):[,t,f]=f.splitText(o.start,o.end);const g=i(t);if(g.setFormat(t.getFormat()),t.replace(g),null==f)return}})),t.registerNodeTransform(r,(t=>{const r=t.getTextContent(),i=n(r);if(null===i||0!==i.start)return void u(t);if(r.length>i.end)return void t.splitText(i.end);const o=t.getPreviousSibling();e(o)&&o.isTextEntity()&&(u(o),u(t));const l=t.getNextSibling();e(l)&&l.isTextEntity()&&(u(l),s(t)&&u(t))}))]}export{g as $canShowPlaceholder,x as $canShowPlaceholderCurry,s as $findTextIntersectionFromCharacters,u as $isRootTextContentEmpty,f as $isRootTextContentEmptyCurry,c as $rootTextContent,a as registerLexicalTextEntity};
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # `@lexical/text`
2
2
 
3
+ [![See API Documentation](https://lexical.dev/img/see-api-documentation.svg)](https://lexical.dev/docs/api/modules/lexical_text)
4
+
3
5
  This package contains utilities and helpers for handling Lexical text.
package/package.json CHANGED
@@ -9,14 +9,16 @@
9
9
  "text"
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "0.13.1",
12
+ "version": "0.14.2",
13
13
  "main": "LexicalText.js",
14
14
  "peerDependencies": {
15
- "lexical": "0.13.1"
15
+ "lexical": "0.14.2"
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
19
19
  "url": "https://github.com/facebook/lexical",
20
20
  "directory": "packages/lexical-text"
21
- }
21
+ },
22
+ "module": "LexicalText.esm.js",
23
+ "sideEffects": false
22
24
  }