onchain-lexical-instance 0.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.
@@ -0,0 +1,231 @@
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
+
9
+ import {ListNode, ListNodeTagType, ListType} from '@lexical/list';
10
+ import {
11
+ $applyNodeReplacement,
12
+ $createTextNode,
13
+ $isElementNode,
14
+ DOMConversionMap,
15
+ DOMConversionOutput,
16
+ isHTMLElement,
17
+ LexicalNode,
18
+ LexicalUpdateJSON,
19
+ SerializedElementNode,
20
+ Spread,
21
+ } from 'lexical';
22
+ import invariant from 'shared/invariant';
23
+
24
+ import {
25
+ mergeNextSiblingListIfSameType,
26
+ updateChildrenListItemValue,
27
+ } from './formatList';
28
+ import {
29
+ $createInstanceListItemNode,
30
+ $isInstanceListItemNode,
31
+ InstanceListItemNode,
32
+ } from './item';
33
+
34
+ export type SerializedInstanceListNode = Spread<
35
+ {
36
+ listType: ListType;
37
+ start: number;
38
+ tag: ListNodeTagType;
39
+ },
40
+ SerializedElementNode
41
+ >;
42
+
43
+ /** @noInheritDoc */
44
+ export class InstanceListNode extends ListNode {
45
+ static getType(): string {
46
+ return 'List';
47
+ }
48
+
49
+ static clone(node: InstanceListNode): InstanceListNode {
50
+ const listType = node.__listType || TAG_TO_LIST_TYPE[node.__tag];
51
+
52
+ return new InstanceListNode(listType, node.__start, node.__key);
53
+ }
54
+
55
+ static transform(): (node: LexicalNode) => void {
56
+ return (node: LexicalNode) => {
57
+ invariant($isInstanceListNode(node), 'node is not a InstanceListNode');
58
+ mergeNextSiblingListIfSameType(node);
59
+ updateChildrenListItemValue(node);
60
+ };
61
+ }
62
+
63
+ static importDOM(): DOMConversionMap | null {
64
+ return {
65
+ ol: () => ({
66
+ conversion: $convertInstanceListNode,
67
+ priority: 0,
68
+ }),
69
+ ul: () => ({
70
+ conversion: $convertInstanceListNode,
71
+ priority: 0,
72
+ }),
73
+ };
74
+ }
75
+
76
+ static importJSON(serializedNode: SerializedInstanceListNode) {
77
+ return $createInstanceListNode().updateFromJSON(serializedNode);
78
+ }
79
+
80
+ updateFromJSON(
81
+ serializedNode: LexicalUpdateJSON<SerializedInstanceListNode>,
82
+ ): this {
83
+ return super
84
+ .updateFromJSON(serializedNode)
85
+ .setListType(serializedNode.listType)
86
+ .setStart(serializedNode.start);
87
+ }
88
+
89
+ exportJSON(): SerializedInstanceListNode {
90
+ return {
91
+ ...super.exportJSON(),
92
+ listType: this.getListType(),
93
+ start: this.getStart(),
94
+ tag: this.getTag(),
95
+ };
96
+ }
97
+
98
+ splice(
99
+ start: number,
100
+ deleteCount: number,
101
+ nodesToInsert: LexicalNode[],
102
+ ): this {
103
+ let listItemNodesToInsert = nodesToInsert;
104
+ for (let i = 0; i < nodesToInsert.length; i++) {
105
+ const node = nodesToInsert[i];
106
+ if (!$isInstanceListItemNode(node)) {
107
+ if (listItemNodesToInsert === nodesToInsert) {
108
+ listItemNodesToInsert = [...nodesToInsert];
109
+ }
110
+ listItemNodesToInsert[i] = $createInstanceListItemNode().append(
111
+ $isElementNode(node) &&
112
+ !($isInstanceListNode(node) || node.isInline())
113
+ ? $createTextNode(node.getTextContent())
114
+ : node,
115
+ );
116
+ }
117
+ }
118
+ return super.splice(start, deleteCount, listItemNodesToInsert);
119
+ }
120
+
121
+ extractWithChild(child: LexicalNode): boolean {
122
+ return $isInstanceListItemNode(child);
123
+ }
124
+ }
125
+
126
+ /*
127
+ * This function normalizes the children of a InstanceListNode after the conversion from HTML,
128
+ * ensuring that they are all ListItemNodes and contain either a single nested InstanceListNode
129
+ * or some other inline content.
130
+ */
131
+ function $normalizeChildren(
132
+ nodes: Array<LexicalNode>,
133
+ ): Array<InstanceListItemNode> {
134
+ const normalizedListItems: Array<InstanceListItemNode> = [];
135
+ for (let i = 0; i < nodes.length; i++) {
136
+ const node = nodes[i];
137
+ if ($isInstanceListItemNode(node)) {
138
+ normalizedListItems.push(node);
139
+ const children = node.getChildren();
140
+ if (children.length > 1) {
141
+ children.forEach((child) => {
142
+ if ($isInstanceListNode(child)) {
143
+ normalizedListItems.push($wrapInInstanceListItem(child));
144
+ }
145
+ });
146
+ }
147
+ } else {
148
+ normalizedListItems.push($wrapInInstanceListItem(node));
149
+ }
150
+ }
151
+ return normalizedListItems;
152
+ }
153
+
154
+ function isDomChecklist(domNode: HTMLElement) {
155
+ if (
156
+ domNode.getAttribute('__lexicallisttype') === 'check' ||
157
+ // is github checklist
158
+ domNode.classList.contains('contains-task-list')
159
+ ) {
160
+ return true;
161
+ }
162
+ // if children are checklist items, the node is a checklist ul. Applicable for googledoc checklist pasting.
163
+ for (const child of domNode.childNodes) {
164
+ if (isHTMLElement(child) && child.hasAttribute('aria-checked')) {
165
+ return true;
166
+ }
167
+ }
168
+ return false;
169
+ }
170
+
171
+ function $convertInstanceListNode(domNode: HTMLElement): DOMConversionOutput {
172
+ const nodeName = domNode.nodeName.toLowerCase();
173
+ let node = null;
174
+ if (nodeName === 'ol') {
175
+ // @ts-ignore
176
+ const start = domNode.start;
177
+ node = $createInstanceListNode('number', start);
178
+ } else if (nodeName === 'ul') {
179
+ if (isDomChecklist(domNode)) {
180
+ node = $createInstanceListNode('check');
181
+ } else {
182
+ node = $createInstanceListNode('bullet');
183
+ }
184
+ }
185
+
186
+ return {
187
+ after: $normalizeChildren,
188
+ node,
189
+ };
190
+ }
191
+
192
+ const TAG_TO_LIST_TYPE: Record<string, ListType> = {
193
+ ol: 'number',
194
+ ul: 'bullet',
195
+ };
196
+
197
+ /**
198
+ * Wraps a node into a ListItemNode.
199
+ * @param node - The node to be wrapped into a ListItemNode
200
+ * @returns The ListItemNode which the passed node is wrapped in.
201
+ */
202
+ export function $wrapInInstanceListItem(
203
+ node: LexicalNode,
204
+ ): InstanceListItemNode {
205
+ const listItemWrapper = $createInstanceListItemNode();
206
+ return listItemWrapper.append(node);
207
+ }
208
+
209
+ /**
210
+ * Creates a InstanceListNode of listType.
211
+ * @param listType - The type of list to be created. Can be 'number', 'bullet', or 'check'.
212
+ * @param start - Where an ordered list starts its count, start = 1 if left undefined.
213
+ * @returns The new InstanceListNode
214
+ */
215
+ export function $createInstanceListNode(
216
+ listType: ListType = 'number',
217
+ start = 1,
218
+ ): InstanceListNode {
219
+ return $applyNodeReplacement(new InstanceListNode(listType, start));
220
+ }
221
+
222
+ /**
223
+ * Checks to see if the node is a InstanceListNode.
224
+ * @param node - The node to be checked.
225
+ * @returns true if the node is a InstanceListNode, false otherwise.
226
+ */
227
+ export function $isInstanceListNode(
228
+ node: LexicalNode | null | undefined,
229
+ ): node is InstanceListNode {
230
+ return node instanceof InstanceListNode;
231
+ }
@@ -0,0 +1,316 @@
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
+
9
+ import {$isListItemNode, $isListNode, ListItemNode} from '@lexical/list';
10
+ import {$setBlocksType} from '@lexical/selection';
11
+ import {
12
+ $applyNodeReplacement,
13
+ $getSelection,
14
+ $isElementNode,
15
+ BaseSelection,
16
+ COMMAND_PRIORITY_NORMAL,
17
+ DOMConversionMap,
18
+ DOMConversionOutput,
19
+ ElementNode,
20
+ INSERT_PARAGRAPH_COMMAND,
21
+ LexicalEditor,
22
+ LexicalNode,
23
+ LexicalUpdateJSON,
24
+ RangeSelection,
25
+ SerializedElementNode,
26
+ Spread,
27
+ } from 'lexical';
28
+ import invariant from 'shared/invariant';
29
+
30
+ import {
31
+ $createInstanceParagraphNode,
32
+ InstanceParagraphNode,
33
+ } from '../paragraph';
34
+ import {$createInstanceListNode} from '.';
35
+ import {
36
+ $handleIndent,
37
+ $handleListInsertParagraph,
38
+ $handleOutdent,
39
+ } from './formatList';
40
+
41
+ export type SerializedInstanceListItemNode = Spread<
42
+ {
43
+ checked: boolean | undefined;
44
+ value: number;
45
+ },
46
+ SerializedElementNode
47
+ >;
48
+
49
+ /** @noInheritDoc */
50
+ export class InstanceListItemNode extends ListItemNode {
51
+ static getType(): string {
52
+ return 'ListItem';
53
+ }
54
+
55
+ static clone(node: InstanceListItemNode): InstanceListItemNode {
56
+ return new InstanceListItemNode(node.__value, node.__checked, node.__key);
57
+ }
58
+
59
+ static importDOM(): DOMConversionMap | null {
60
+ return {
61
+ li: () => ({
62
+ conversion: $convertListItemElement,
63
+ priority: 0,
64
+ }),
65
+ };
66
+ }
67
+
68
+ static importJSON(serializedNode: SerializedInstanceListItemNode) {
69
+ return $createInstanceListItemNode().updateFromJSON(serializedNode);
70
+ }
71
+
72
+ updateFromJSON(
73
+ serializedNode: LexicalUpdateJSON<SerializedInstanceListItemNode>,
74
+ ): this {
75
+ return super
76
+ .updateFromJSON(serializedNode)
77
+ .setValue(serializedNode.value)
78
+ .setChecked(serializedNode.checked);
79
+ }
80
+
81
+ exportJSON(): SerializedInstanceListItemNode {
82
+ return {
83
+ ...super.exportJSON(),
84
+ checked: this.getChecked(),
85
+ value: this.getValue(),
86
+ };
87
+ }
88
+
89
+ replace<N extends LexicalNode>(
90
+ replaceWithNode: N,
91
+ includeChildren?: boolean,
92
+ ): N {
93
+ if ($isListItemNode(replaceWithNode)) {
94
+ return super.replace(replaceWithNode);
95
+ }
96
+ this.setIndent(0);
97
+ const list = this.getParentOrThrow();
98
+ if (!$isListNode(list)) {
99
+ return replaceWithNode;
100
+ }
101
+ if (list.__first === this.getKey()) {
102
+ list.insertBefore(replaceWithNode);
103
+ } else if (list.__last === this.getKey()) {
104
+ list.insertAfter(replaceWithNode);
105
+ } else {
106
+ // Split the list
107
+ const newList = $createInstanceListNode(list.getListType());
108
+ let nextSibling = this.getNextSibling();
109
+ while (nextSibling) {
110
+ const nodeToAppend = nextSibling;
111
+ nextSibling = nextSibling.getNextSibling();
112
+ newList.append(nodeToAppend);
113
+ }
114
+ list.insertAfter(replaceWithNode);
115
+ replaceWithNode.insertAfter(newList);
116
+ }
117
+ if (includeChildren) {
118
+ invariant(
119
+ $isElementNode(replaceWithNode),
120
+ 'includeChildren should only be true for ElementNodes',
121
+ );
122
+ this.getChildren().forEach((child: LexicalNode) => {
123
+ replaceWithNode.append(child);
124
+ });
125
+ }
126
+ super.remove();
127
+ if (list.getChildrenSize() === 0) {
128
+ list.remove();
129
+ }
130
+ return replaceWithNode;
131
+ }
132
+
133
+ insertAfter(node: LexicalNode, restoreSelection = true): LexicalNode {
134
+ const listNode = this.getParentOrThrow();
135
+
136
+ if (!$isListNode(listNode)) {
137
+ invariant(
138
+ false,
139
+ 'insertAfter: list node is not parent of list item node',
140
+ );
141
+ }
142
+
143
+ if ($isListItemNode(node)) {
144
+ return super.insertAfter(node, restoreSelection);
145
+ }
146
+
147
+ const siblings = this.getNextSiblings();
148
+
149
+ // Split the lists and insert the node in between them
150
+ listNode.insertAfter(node, restoreSelection);
151
+
152
+ if (siblings.length !== 0) {
153
+ const newListNode = $createInstanceListNode(listNode.getListType());
154
+
155
+ siblings.forEach((sibling) => newListNode.append(sibling));
156
+
157
+ node.insertAfter(newListNode, restoreSelection);
158
+ }
159
+
160
+ return node;
161
+ }
162
+
163
+ setIndent(indent: number): this {
164
+ invariant(typeof indent === 'number', 'Invalid indent value.');
165
+ indent = Math.floor(indent);
166
+ invariant(indent >= 0, 'Indent value must be non-negative.');
167
+ let currentIndent = this.getIndent();
168
+ while (currentIndent !== indent) {
169
+ if (currentIndent < indent) {
170
+ $handleIndent(this);
171
+ currentIndent++;
172
+ } else {
173
+ $handleOutdent(this);
174
+ currentIndent--;
175
+ }
176
+ }
177
+
178
+ return this;
179
+ }
180
+
181
+ /** 删除行时会判断是否是最后一行,从而替换父级数据,
182
+ * 调用 this.remove 时需要根据具体需求调用,如果只是单纯的删除行,
183
+ * 请调用 super.remove
184
+ */
185
+ remove(preserveEmptyParent?: boolean): void {
186
+ const selection = $getSelection();
187
+ if (selection) {
188
+ this.collapseAtStart(selection);
189
+ } else {
190
+ super.remove(preserveEmptyParent);
191
+ }
192
+ }
193
+
194
+ insertNewAfter(
195
+ selection: RangeSelection,
196
+ restoreSelection = true,
197
+ ): InstanceListItemNode | InstanceParagraphNode {
198
+ const [node] = selection.getNodes();
199
+ if ($isInstanceListItemNode(node) && node.getChildren().length === 0) {
200
+ // 列表内无数据时清空格式
201
+ let newElement: InstanceParagraphNode | null = null;
202
+ $setBlocksType(
203
+ selection,
204
+ () => (newElement = $createInstanceParagraphNode()),
205
+ );
206
+ return newElement!;
207
+ } else {
208
+ const newElement = $createInstanceListItemNode()
209
+ .updateFromJSON(this.exportJSON())
210
+ .setChecked(this.getChecked() ? false : undefined);
211
+ this.insertAfter(newElement, restoreSelection);
212
+ return newElement;
213
+ }
214
+ }
215
+
216
+ collapseAtStart(selection: BaseSelection): true {
217
+ const paragraph = $createInstanceParagraphNode();
218
+ const children = this.getChildren();
219
+ children.forEach((child) => paragraph.append(child));
220
+ const listNode = this.getParentOrThrow();
221
+ const listNodeParent = listNode.getParentOrThrow();
222
+ const isIndented = $isListItemNode(listNodeParent);
223
+
224
+ if (listNode.getChildrenSize() === 1) {
225
+ if (isIndented) {
226
+ // 如果列表节点是嵌套的,我们只需将其移除即可,
227
+ // 实际上是取消缩进。
228
+ listNode.remove();
229
+ listNodeParent.select();
230
+ } else {
231
+ listNode.replace(paragraph);
232
+ // 如果我们选择了列表项,我们需要将其移动到段落
233
+ const [anchor, focus] = selection.getStartEndPoints() || [];
234
+ const key = paragraph.getKey();
235
+
236
+ if (anchor && anchor.type === 'element' && anchor.getNode().is(this)) {
237
+ anchor.set(key, anchor.offset, 'element');
238
+ }
239
+
240
+ if (focus && focus.type === 'element' && focus.getNode().is(this)) {
241
+ focus.set(key, focus.offset, 'element');
242
+ }
243
+ }
244
+ } else {
245
+ super.remove();
246
+ }
247
+ return true;
248
+ }
249
+
250
+ createParentElementNode(): ElementNode {
251
+ return $createInstanceListNode('bullet');
252
+ }
253
+ }
254
+
255
+ function $convertListItemElement(domNode: HTMLElement): DOMConversionOutput {
256
+ const isGitHubCheckList = domNode.classList.contains('task-list-item');
257
+ if (isGitHubCheckList) {
258
+ for (const child of domNode.children) {
259
+ if (child.tagName === 'INPUT') {
260
+ return $convertCheckboxInput(child);
261
+ }
262
+ }
263
+ }
264
+
265
+ const ariaCheckedAttr = domNode.getAttribute('aria-checked');
266
+ const checked =
267
+ ariaCheckedAttr === 'true'
268
+ ? true
269
+ : ariaCheckedAttr === 'false'
270
+ ? false
271
+ : undefined;
272
+ return {node: $createInstanceListItemNode(checked)};
273
+ }
274
+
275
+ function $convertCheckboxInput(domNode: Element): DOMConversionOutput {
276
+ const isCheckboxInput = domNode.getAttribute('type') === 'checkbox';
277
+ if (!isCheckboxInput) {
278
+ return {node: null};
279
+ }
280
+ const checked = domNode.hasAttribute('checked');
281
+ return {node: $createInstanceListItemNode(checked)};
282
+ }
283
+
284
+ /**
285
+ * Creates a new List Item node, passing true/false will convert it to a checkbox input.
286
+ * @param checked - Is the List Item a checkbox and, if so, is it checked? undefined/null: not a checkbox, true/false is a checkbox and checked/unchecked, respectively.
287
+ * @returns The new List Item.
288
+ */
289
+ export function $createInstanceListItemNode(
290
+ checked?: boolean,
291
+ ): InstanceListItemNode {
292
+ return $applyNodeReplacement(new InstanceListItemNode(undefined, checked));
293
+ }
294
+
295
+ /**
296
+ * Checks to see if the node is a InstanceListItemNode.
297
+ * @param node - The node to be checked.
298
+ * @returns true if the node is a InstanceListItemNode, false otherwise.
299
+ */
300
+ export function $isInstanceListItemNode(
301
+ node: LexicalNode | null | undefined,
302
+ ): node is InstanceListItemNode {
303
+ return node instanceof InstanceListItemNode;
304
+ }
305
+
306
+ export function $registerInstanceListItemInsertParagraph(
307
+ editor: LexicalEditor,
308
+ ) {
309
+ return editor.registerCommand(
310
+ INSERT_PARAGRAPH_COMMAND,
311
+ (...e) => {
312
+ return $handleListInsertParagraph();
313
+ },
314
+ COMMAND_PRIORITY_NORMAL,
315
+ );
316
+ }