@thangph2146/lexical-editor 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.
Files changed (183) hide show
  1. package/dist/editor-x/editor.cjs +33121 -0
  2. package/dist/editor-x/editor.cjs.map +1 -0
  3. package/dist/editor-x/editor.css +2854 -0
  4. package/dist/editor-x/editor.css.map +1 -0
  5. package/dist/editor-x/editor.d.cts +12 -0
  6. package/dist/editor-x/editor.d.ts +12 -0
  7. package/dist/editor-x/editor.js +33095 -0
  8. package/dist/editor-x/editor.js.map +1 -0
  9. package/dist/index.cjs +33210 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +2854 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.cts +15 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +33183 -0
  16. package/dist/index.js.map +1 -0
  17. package/package.json +84 -0
  18. package/src/components/lexical-editor.tsx +123 -0
  19. package/src/context/editor-container-context.tsx +29 -0
  20. package/src/context/priority-image-context.tsx +7 -0
  21. package/src/context/toolbar-context.tsx +60 -0
  22. package/src/context/uploads-context.tsx +53 -0
  23. package/src/editor-hooks/use-debounce.ts +80 -0
  24. package/src/editor-hooks/use-modal.tsx +64 -0
  25. package/src/editor-hooks/use-report.ts +57 -0
  26. package/src/editor-hooks/use-update-toolbar.ts +41 -0
  27. package/src/editor-ui/broken-image.tsx +18 -0
  28. package/src/editor-ui/caption-composer.tsx +45 -0
  29. package/src/editor-ui/code-button.tsx +75 -0
  30. package/src/editor-ui/color-picker.tsx +2010 -0
  31. package/src/editor-ui/content-editable.tsx +37 -0
  32. package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
  33. package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
  34. package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
  35. package/src/editor-ui/image-component.tsx +321 -0
  36. package/src/editor-ui/image-placeholder.tsx +57 -0
  37. package/src/editor-ui/image-resizer.tsx +499 -0
  38. package/src/editor-ui/image-sizing.ts +120 -0
  39. package/src/editor-ui/lazy-image.tsx +136 -0
  40. package/src/editor-x/editor.tsx +117 -0
  41. package/src/editor-x/nodes.ts +79 -0
  42. package/src/editor-x/plugins.tsx +380 -0
  43. package/src/hooks/use-click-outside.ts +27 -0
  44. package/src/hooks/use-element-size.ts +54 -0
  45. package/src/hooks/use-header-height.ts +95 -0
  46. package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
  47. package/src/index.ts +4 -0
  48. package/src/lib/logger.ts +6 -0
  49. package/src/lib/utils.ts +19 -0
  50. package/src/nodes/autocomplete-node.tsx +94 -0
  51. package/src/nodes/embeds/tweet-node.tsx +224 -0
  52. package/src/nodes/embeds/youtube-node.tsx +519 -0
  53. package/src/nodes/emoji-node.tsx +83 -0
  54. package/src/nodes/image-node.tsx +328 -0
  55. package/src/nodes/keyword-node.tsx +58 -0
  56. package/src/nodes/layout-container-node.tsx +128 -0
  57. package/src/nodes/layout-item-node.tsx +118 -0
  58. package/src/nodes/list-with-color-node.tsx +160 -0
  59. package/src/nodes/mention-node.ts +122 -0
  60. package/src/plugins/actions/actions-plugin.tsx +3 -0
  61. package/src/plugins/actions/character-limit-plugin.tsx +27 -0
  62. package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
  63. package/src/plugins/actions/counter-character-plugin.tsx +80 -0
  64. package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
  65. package/src/plugins/actions/import-export-plugin.tsx +61 -0
  66. package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
  67. package/src/plugins/actions/max-length-plugin.tsx +59 -0
  68. package/src/plugins/actions/share-content-plugin.tsx +72 -0
  69. package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
  70. package/src/plugins/actions/tree-view-plugin.tsx +63 -0
  71. package/src/plugins/align-plugin.tsx +86 -0
  72. package/src/plugins/auto-link-plugin.tsx +34 -0
  73. package/src/plugins/autocomplete-plugin.tsx +2574 -0
  74. package/src/plugins/code-action-menu-plugin.tsx +240 -0
  75. package/src/plugins/code-highlight-plugin.tsx +22 -0
  76. package/src/plugins/component-picker-menu-plugin.tsx +427 -0
  77. package/src/plugins/context-menu-plugin.tsx +311 -0
  78. package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
  79. package/src/plugins/draggable-block-plugin.tsx +50 -0
  80. package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
  81. package/src/plugins/embeds/twitter-plugin.tsx +45 -0
  82. package/src/plugins/embeds/youtube-plugin.tsx +84 -0
  83. package/src/plugins/emoji-picker-plugin.tsx +206 -0
  84. package/src/plugins/emojis-plugin.tsx +84 -0
  85. package/src/plugins/floating-link-editor-plugin.tsx +791 -0
  86. package/src/plugins/floating-text-format-plugin.tsx +710 -0
  87. package/src/plugins/images-plugin.tsx +671 -0
  88. package/src/plugins/keywords-plugin.tsx +59 -0
  89. package/src/plugins/layout-plugin.tsx +658 -0
  90. package/src/plugins/link-plugin.tsx +18 -0
  91. package/src/plugins/list-color-plugin.tsx +178 -0
  92. package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
  93. package/src/plugins/mentions-plugin.tsx +714 -0
  94. package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
  95. package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
  96. package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
  97. package/src/plugins/picker/code-picker-plugin.tsx +30 -0
  98. package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
  99. package/src/plugins/picker/component-picker-option.tsx +47 -0
  100. package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
  101. package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
  102. package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
  103. package/src/plugins/picker/image-picker-plugin.tsx +16 -0
  104. package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
  105. package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
  106. package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
  107. package/src/plugins/picker/table-picker-plugin.tsx +56 -0
  108. package/src/plugins/tab-focus-plugin.tsx +66 -0
  109. package/src/plugins/table-column-resizer-plugin.tsx +309 -0
  110. package/src/plugins/table-plugin.tsx +299 -0
  111. package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
  112. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
  113. package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
  114. package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
  115. package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
  116. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
  117. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
  118. package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
  119. package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
  120. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
  121. package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
  122. package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
  123. package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
  124. package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
  125. package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
  126. package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
  127. package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
  128. package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
  129. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
  130. package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
  131. package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
  132. package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
  133. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
  134. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
  135. package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
  136. package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
  137. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
  138. package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
  139. package/src/plugins/typing-pref-plugin.tsx +118 -0
  140. package/src/shared/can-use-dom.ts +4 -0
  141. package/src/shared/environment.ts +47 -0
  142. package/src/shared/invariant.ts +16 -0
  143. package/src/shared/use-layout-effect.ts +12 -0
  144. package/src/themes/_mixins.scss +107 -0
  145. package/src/themes/_variables.scss +33 -0
  146. package/src/themes/editor-theme.scss +622 -0
  147. package/src/themes/editor-theme.ts +118 -0
  148. package/src/themes/plugins.scss +1180 -0
  149. package/src/themes/ui-components.scss +936 -0
  150. package/src/transformers/markdown-emoji-transformer.ts +20 -0
  151. package/src/transformers/markdown-hr-transformer.ts +28 -0
  152. package/src/transformers/markdown-image-transformer.ts +31 -0
  153. package/src/transformers/markdown-list-transformer.ts +51 -0
  154. package/src/transformers/markdown-table-transformer.ts +200 -0
  155. package/src/transformers/markdown-tweet-transformer.ts +26 -0
  156. package/src/ui/button-group.tsx +10 -0
  157. package/src/ui/button.tsx +29 -0
  158. package/src/ui/collapsible.tsx +67 -0
  159. package/src/ui/command.tsx +48 -0
  160. package/src/ui/dialog.tsx +146 -0
  161. package/src/ui/flex.tsx +38 -0
  162. package/src/ui/input.tsx +20 -0
  163. package/src/ui/label.tsx +20 -0
  164. package/src/ui/popover.tsx +128 -0
  165. package/src/ui/scroll-area.tsx +17 -0
  166. package/src/ui/select.tsx +171 -0
  167. package/src/ui/separator.tsx +20 -0
  168. package/src/ui/slider.tsx +14 -0
  169. package/src/ui/slot.tsx +3 -0
  170. package/src/ui/tabs.tsx +87 -0
  171. package/src/ui/toggle-group.tsx +109 -0
  172. package/src/ui/toggle.tsx +28 -0
  173. package/src/ui/tooltip.tsx +28 -0
  174. package/src/ui/typography.tsx +44 -0
  175. package/src/utils/doc-serialization.ts +68 -0
  176. package/src/utils/emoji-list.ts +16604 -0
  177. package/src/utils/get-dom-range-rect.ts +20 -0
  178. package/src/utils/get-selected-node.ts +20 -0
  179. package/src/utils/is-mobile-width.ts +0 -0
  180. package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
  181. package/src/utils/set-floating-elem-position.ts +44 -0
  182. package/src/utils/swipe.ts +119 -0
  183. package/src/utils/url.ts +32 -0
@@ -0,0 +1,328 @@
1
+ import * as React from "react"
2
+ import { JSX, Suspense } from "react"
3
+ import type {
4
+ DOMConversionMap,
5
+ DOMConversionOutput,
6
+ DOMExportOutput,
7
+ EditorConfig,
8
+ EditorState,
9
+ LexicalEditor,
10
+ LexicalNode,
11
+ NodeKey,
12
+ SerializedEditor,
13
+ SerializedLexicalNode,
14
+ Spread,
15
+ } from "lexical"
16
+ import {
17
+ $applyNodeReplacement,
18
+ $getRoot,
19
+ createEditor,
20
+ DecoratorNode,
21
+ ParagraphNode,
22
+ RootNode,
23
+ TextNode,
24
+ } from "lexical"
25
+
26
+ const ImageComponent = React.lazy(() => import("../editor-ui/image-component"))
27
+
28
+ const WHITESPACE_REGEX = /[\u200B\u00A0\s]+/g
29
+
30
+ export interface ImagePayload {
31
+ altText: string
32
+ caption?: LexicalEditor
33
+ height?: number
34
+ key?: NodeKey
35
+ maxWidth?: number
36
+ showCaption?: boolean
37
+ src: string
38
+ width?: number
39
+ captionsEnabled?: boolean
40
+ fullWidth?: boolean
41
+ }
42
+
43
+ function editorStateHasContent(editorState: EditorState): boolean {
44
+ return editorState.read(() => {
45
+ const root = $getRoot()
46
+ const text = root.getTextContent().replace(WHITESPACE_REGEX, "")
47
+ return text.length > 0
48
+ })
49
+ }
50
+
51
+ function captionHasContent(editor: LexicalEditor): boolean {
52
+ return editorStateHasContent(editor.getEditorState())
53
+ }
54
+
55
+ function clearCaption(editor: LexicalEditor): void {
56
+ editor.update(() => {
57
+ const root = $getRoot()
58
+ root.clear()
59
+ })
60
+ }
61
+
62
+ function isGoogleDocCheckboxImg(img: HTMLImageElement): boolean {
63
+ return (
64
+ img.parentElement != null &&
65
+ img.parentElement.tagName === "LI" &&
66
+ img.previousSibling === null &&
67
+ img.getAttribute("aria-roledescription") === "checkbox"
68
+ )
69
+ }
70
+
71
+ function $convertImageElement(domNode: Node): null | DOMConversionOutput {
72
+ const img = domNode as HTMLImageElement
73
+ if (img.src.startsWith("file:///") || isGoogleDocCheckboxImg(img)) {
74
+ return null
75
+ }
76
+ const { alt: altText, src, width, height } = img
77
+ const node = $createImageNode({ altText, height, src, width })
78
+ return { node }
79
+ }
80
+
81
+ export type SerializedImageNode = Spread<
82
+ {
83
+ altText: string
84
+ caption: SerializedEditor
85
+ height?: number
86
+ maxWidth: number
87
+ showCaption: boolean
88
+ src: string
89
+ width?: number
90
+ fullWidth?: boolean
91
+ },
92
+ SerializedLexicalNode
93
+ >
94
+
95
+ export class ImageNode extends DecoratorNode<JSX.Element> {
96
+ __src: string
97
+ __altText: string
98
+ __width: "inherit" | number
99
+ __height: "inherit" | number
100
+ __maxWidth: number
101
+ __showCaption: boolean
102
+ __caption: LexicalEditor
103
+ // Captions cannot yet be used within editor cells
104
+ __captionsEnabled: boolean
105
+ __fullWidth: boolean
106
+
107
+ static getType(): string {
108
+ return "image"
109
+ }
110
+
111
+ static clone(node: ImageNode): ImageNode {
112
+ return new ImageNode(
113
+ node.__src,
114
+ node.__altText,
115
+ node.__maxWidth,
116
+ node.__width,
117
+ node.__height,
118
+ node.__showCaption,
119
+ node.__caption,
120
+ node.__captionsEnabled,
121
+ node.__fullWidth,
122
+ node.__key
123
+ )
124
+ }
125
+
126
+ static importJSON(serializedNode: SerializedImageNode): ImageNode {
127
+ const { altText, height, width, maxWidth, caption, src, showCaption, fullWidth } =
128
+ serializedNode
129
+
130
+ const node = $createImageNode({
131
+ altText,
132
+ height,
133
+ maxWidth,
134
+ showCaption: showCaption === true,
135
+ src,
136
+ width,
137
+ fullWidth,
138
+ })
139
+
140
+ if (showCaption === true && caption?.editorState) {
141
+ const parsedState = node.__caption.parseEditorState(caption.editorState)
142
+
143
+ if (editorStateHasContent(parsedState)) {
144
+ node.__caption.setEditorState(parsedState)
145
+ return node
146
+ }
147
+ }
148
+
149
+ node.__showCaption = false
150
+ clearCaption(node.__caption)
151
+ return node
152
+ }
153
+
154
+ exportDOM(): DOMExportOutput {
155
+ const element = document.createElement("img")
156
+ element.setAttribute("src", this.__src)
157
+ element.setAttribute("alt", this.__altText)
158
+ if (typeof this.__width === "number") {
159
+ element.setAttribute("width", this.__width.toString())
160
+ }
161
+ if (typeof this.__height === "number") {
162
+ element.setAttribute("height", this.__height.toString())
163
+ }
164
+ return { element }
165
+ }
166
+
167
+ static importDOM(): DOMConversionMap | null {
168
+ return {
169
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
170
+ img: (node: Node) => ({
171
+ conversion: $convertImageElement,
172
+ priority: 0,
173
+ }),
174
+ }
175
+ }
176
+
177
+ constructor(
178
+ src: string,
179
+ altText: string,
180
+ maxWidth: number,
181
+ width?: "inherit" | number,
182
+ height?: "inherit" | number,
183
+ showCaption?: boolean,
184
+ caption?: LexicalEditor,
185
+ captionsEnabled?: boolean,
186
+ fullWidth?: boolean,
187
+ key?: NodeKey
188
+ ) {
189
+ super(key)
190
+ this.__src = src
191
+ this.__altText = altText
192
+ this.__maxWidth = maxWidth
193
+ this.__width = width || "inherit"
194
+ this.__height = height || "inherit"
195
+ this.__showCaption = showCaption || false
196
+ this.__caption =
197
+ caption ||
198
+ createEditor({
199
+ namespace: "ImageCaption",
200
+ nodes: [RootNode, TextNode, ParagraphNode],
201
+ })
202
+ this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined
203
+ // Default image to full width if not specified for consistency with YouTube
204
+ this.__fullWidth = fullWidth === undefined ? true : !!fullWidth
205
+ }
206
+
207
+ exportJSON(): SerializedImageNode {
208
+ return {
209
+ altText: this.getAltText(),
210
+ caption: this.__caption.toJSON(),
211
+ height: this.__height === "inherit" ? 0 : this.__height,
212
+ maxWidth: this.__maxWidth,
213
+ showCaption: this.__showCaption && captionHasContent(this.__caption),
214
+ src: this.getSrc(),
215
+ type: "image",
216
+ version: 1,
217
+ width: this.__width === "inherit" ? 0 : this.__width,
218
+ fullWidth: this.__fullWidth || undefined,
219
+ }
220
+ }
221
+
222
+ setWidthAndHeight(
223
+ width: "inherit" | number,
224
+ height: "inherit" | number
225
+ ): void {
226
+ const writable = this.getWritable()
227
+ writable.__width = width
228
+ writable.__height = height
229
+ }
230
+
231
+ setSrc(src: string): void {
232
+ const writable = this.getWritable()
233
+ writable.__src = src
234
+ }
235
+
236
+ setAltText(altText: string): void {
237
+ const writable = this.getWritable()
238
+ writable.__altText = altText
239
+ }
240
+
241
+ setShowCaption(showCaption: boolean): void {
242
+ const writable = this.getWritable()
243
+ writable.__showCaption = showCaption
244
+ }
245
+
246
+ setFullWidth(fullWidth: boolean): void {
247
+ const writable = this.getWritable()
248
+ writable.__fullWidth = fullWidth
249
+ }
250
+
251
+ // View
252
+
253
+ createDOM(config: EditorConfig): HTMLElement {
254
+ const span = document.createElement("span")
255
+ const theme = config.theme
256
+ const className = theme.image
257
+ if (className !== undefined) {
258
+ span.className = className
259
+ }
260
+ return span
261
+ }
262
+
263
+ updateDOM(): false {
264
+ return false
265
+ }
266
+
267
+ getSrc(): string {
268
+ return this.__src
269
+ }
270
+
271
+ getAltText(): string {
272
+ return this.__altText
273
+ }
274
+
275
+ decorate(): JSX.Element {
276
+ return (
277
+ <Suspense fallback={null}>
278
+ <ImageComponent
279
+ src={this.__src}
280
+ altText={this.__altText}
281
+ width={this.__width}
282
+ height={this.__height}
283
+ maxWidth={this.__maxWidth}
284
+ nodeKey={this.getKey()}
285
+ showCaption={this.__showCaption}
286
+ caption={this.__caption}
287
+ captionsEnabled={this.__captionsEnabled}
288
+ fullWidth={this.__fullWidth}
289
+ resizable={true}
290
+ />
291
+ </Suspense>
292
+ )
293
+ }
294
+ }
295
+
296
+ export function $createImageNode({
297
+ altText,
298
+ height,
299
+ maxWidth = 500,
300
+ captionsEnabled,
301
+ src,
302
+ width,
303
+ showCaption,
304
+ caption,
305
+ fullWidth,
306
+ key,
307
+ }: ImagePayload): ImageNode {
308
+ return $applyNodeReplacement(
309
+ new ImageNode(
310
+ src,
311
+ altText,
312
+ maxWidth,
313
+ width,
314
+ height,
315
+ showCaption,
316
+ caption,
317
+ captionsEnabled,
318
+ fullWidth,
319
+ key
320
+ )
321
+ )
322
+ }
323
+
324
+ export function $isImageNode(
325
+ node: LexicalNode | null | undefined
326
+ ): node is ImageNode {
327
+ return node instanceof ImageNode
328
+ }
@@ -0,0 +1,58 @@
1
+ import type { EditorConfig, LexicalNode, SerializedTextNode } from "lexical"
2
+ import { TextNode } from "lexical"
3
+
4
+ export type SerializedKeywordNode = SerializedTextNode
5
+
6
+ export class KeywordNode extends TextNode {
7
+ static getType(): string {
8
+ return "keyword"
9
+ }
10
+
11
+ static clone(node: KeywordNode): KeywordNode {
12
+ return new KeywordNode(node.__text, node.__key)
13
+ }
14
+
15
+ static importJSON(serializedNode: SerializedKeywordNode): KeywordNode {
16
+ const node = $createKeywordNode(serializedNode.text)
17
+ node.setFormat(serializedNode.format)
18
+ node.setDetail(serializedNode.detail)
19
+ node.setMode(serializedNode.mode)
20
+ node.setStyle(serializedNode.style)
21
+ return node
22
+ }
23
+
24
+ exportJSON(): SerializedKeywordNode {
25
+ return {
26
+ ...super.exportJSON(),
27
+ type: "keyword",
28
+ version: 1,
29
+ }
30
+ }
31
+
32
+ createDOM(config: EditorConfig): HTMLElement {
33
+ const dom = super.createDOM(config)
34
+ dom.style.cursor = "default"
35
+ dom.className = "keyword text-purple-900 font-bold"
36
+ return dom
37
+ }
38
+
39
+ canInsertTextBefore(): boolean {
40
+ return false
41
+ }
42
+
43
+ canInsertTextAfter(): boolean {
44
+ return false
45
+ }
46
+
47
+ isTextEntity(): true {
48
+ return true
49
+ }
50
+ }
51
+
52
+ export function $createKeywordNode(keyword: string): KeywordNode {
53
+ return new KeywordNode(keyword)
54
+ }
55
+
56
+ export function $isKeywordNode(node: LexicalNode | null | undefined): boolean {
57
+ return node instanceof KeywordNode
58
+ }
@@ -0,0 +1,128 @@
1
+ import { addClassNamesToElement } from "@lexical/utils"
2
+ import type {
3
+ DOMConversionMap,
4
+ DOMConversionOutput,
5
+ DOMExportOutput,
6
+ EditorConfig,
7
+ LexicalNode,
8
+ NodeKey,
9
+ SerializedElementNode,
10
+ Spread,
11
+ } from "lexical"
12
+ import { ElementNode } from "lexical"
13
+
14
+ export type SerializedLayoutContainerNode = Spread<
15
+ {
16
+ templateColumns: string
17
+ },
18
+ SerializedElementNode
19
+ >
20
+
21
+ function $convertLayoutContainerElement(
22
+ domNode: HTMLElement
23
+ ): DOMConversionOutput | null {
24
+ const styleAttributes = window.getComputedStyle(domNode)
25
+ const templateColumns = styleAttributes.getPropertyValue(
26
+ "grid-template-columns"
27
+ )
28
+ if (templateColumns) {
29
+ const node = $createLayoutContainerNode(templateColumns)
30
+ return { node }
31
+ }
32
+ return null
33
+ }
34
+
35
+ export class LayoutContainerNode extends ElementNode {
36
+ __templateColumns: string
37
+
38
+ constructor(templateColumns: string, key?: NodeKey) {
39
+ super(key)
40
+ this.__templateColumns = templateColumns
41
+ }
42
+
43
+ static getType(): string {
44
+ return "layout-container"
45
+ }
46
+
47
+ static clone(node: LayoutContainerNode): LayoutContainerNode {
48
+ return new LayoutContainerNode(node.__templateColumns, node.__key)
49
+ }
50
+
51
+ createDOM(config: EditorConfig): HTMLElement {
52
+ const dom = document.createElement("div")
53
+ dom.style.gridTemplateColumns = this.__templateColumns
54
+ if (typeof config.theme.layoutContainer === "string") {
55
+ addClassNamesToElement(dom, config.theme.layoutContainer)
56
+ }
57
+ return dom
58
+ }
59
+
60
+ exportDOM(): DOMExportOutput {
61
+ const element = document.createElement("div")
62
+ element.style.gridTemplateColumns = this.__templateColumns
63
+ element.setAttribute("data-lexical-layout-container", "true")
64
+ return { element }
65
+ }
66
+
67
+ updateDOM(prevNode: LayoutContainerNode, dom: HTMLElement): boolean {
68
+ if (prevNode.__templateColumns !== this.__templateColumns) {
69
+ dom.style.gridTemplateColumns = this.__templateColumns
70
+ }
71
+ return false
72
+ }
73
+
74
+ static importDOM(): DOMConversionMap | null {
75
+ return {
76
+ div: (domNode: HTMLElement) => {
77
+ if (!domNode.hasAttribute("data-lexical-layout-container")) {
78
+ return null
79
+ }
80
+ return {
81
+ conversion: $convertLayoutContainerElement,
82
+ priority: 2,
83
+ }
84
+ },
85
+ }
86
+ }
87
+
88
+ static importJSON(json: SerializedLayoutContainerNode): LayoutContainerNode {
89
+ return $createLayoutContainerNode(json.templateColumns)
90
+ }
91
+
92
+ isShadowRoot(): boolean {
93
+ return true
94
+ }
95
+
96
+ canBeEmpty(): boolean {
97
+ return false
98
+ }
99
+
100
+ exportJSON(): SerializedLayoutContainerNode {
101
+ return {
102
+ ...super.exportJSON(),
103
+ templateColumns: this.__templateColumns,
104
+ type: "layout-container",
105
+ version: 1,
106
+ }
107
+ }
108
+
109
+ getTemplateColumns(): string {
110
+ return this.getLatest().__templateColumns
111
+ }
112
+
113
+ setTemplateColumns(templateColumns: string) {
114
+ this.getWritable().__templateColumns = templateColumns
115
+ }
116
+ }
117
+
118
+ export function $createLayoutContainerNode(
119
+ templateColumns: string
120
+ ): LayoutContainerNode {
121
+ return new LayoutContainerNode(templateColumns)
122
+ }
123
+
124
+ export function $isLayoutContainerNode(
125
+ node: LexicalNode | null | undefined
126
+ ): node is LayoutContainerNode {
127
+ return node instanceof LayoutContainerNode
128
+ }
@@ -0,0 +1,118 @@
1
+ import { addClassNamesToElement } from "@lexical/utils"
2
+ import type {
3
+ DOMConversionMap,
4
+ EditorConfig,
5
+ LexicalNode,
6
+ SerializedElementNode,
7
+ } from "lexical"
8
+ import { ElementNode } from "lexical"
9
+
10
+ export type SerializedLayoutItemNode = SerializedElementNode & {
11
+ style?: string
12
+ }
13
+
14
+ function withPaddingFallback(style: string): string {
15
+ const normalizedStyle = style.trim()
16
+ if (/padding\s*:/i.test(normalizedStyle)) {
17
+ return normalizedStyle
18
+ }
19
+ if (!normalizedStyle) {
20
+ return "padding: 12px;"
21
+ }
22
+ return `${normalizedStyle.replace(/;?$/, ";")} padding: 12px;`
23
+ }
24
+
25
+ function applyLayoutItemStyle(dom: HTMLElement, style: string): void {
26
+ dom.style.cssText = style
27
+ const padding = dom.style.padding || "12px"
28
+ dom.style.setProperty("padding", padding, "important")
29
+ }
30
+
31
+ function applyLayoutItemAttributes(dom: HTMLElement): void {
32
+ dom.setAttribute("data-lexical-layout-item", "true")
33
+ }
34
+
35
+ function getLayoutItemThemeClass(config: EditorConfig): string {
36
+ return typeof config.theme.layoutItem === "string"
37
+ ? config.theme.layoutItem
38
+ : "border border-dashed"
39
+ }
40
+
41
+ export class LayoutItemNode extends ElementNode {
42
+ static getType(): string {
43
+ return "layout-item"
44
+ }
45
+
46
+ static clone(node: LayoutItemNode): LayoutItemNode {
47
+ return new LayoutItemNode(node.__key)
48
+ }
49
+
50
+ createDOM(config: EditorConfig): HTMLElement {
51
+ const dom = document.createElement("div")
52
+ addClassNamesToElement(dom, getLayoutItemThemeClass(config))
53
+ applyLayoutItemAttributes(dom)
54
+ applyLayoutItemStyle(dom, withPaddingFallback(this.getStyle()))
55
+ return dom
56
+ }
57
+
58
+ updateDOM(prevNode: LayoutItemNode, dom: HTMLElement, config: EditorConfig): boolean {
59
+ const expectedClass = getLayoutItemThemeClass(config)
60
+ if (dom.className !== expectedClass) {
61
+ dom.className = ""
62
+ addClassNamesToElement(dom, expectedClass)
63
+ }
64
+ applyLayoutItemAttributes(dom)
65
+
66
+ const prevStyle = withPaddingFallback(prevNode.getStyle())
67
+ const nextStyle = withPaddingFallback(this.getStyle())
68
+ if (prevStyle !== nextStyle) {
69
+ applyLayoutItemStyle(dom, nextStyle)
70
+ }
71
+ return false
72
+ }
73
+
74
+ static importDOM(): DOMConversionMap | null {
75
+ return {}
76
+ }
77
+
78
+ static importJSON(json: SerializedLayoutItemNode): LayoutItemNode {
79
+ const node = $createLayoutItemNode()
80
+ if (json.format) {
81
+ node.setFormat(json.format)
82
+ }
83
+ if (json.indent) {
84
+ node.setIndent(json.indent)
85
+ }
86
+ if ("direction" in json && json.direction !== null) {
87
+ node.setDirection(json.direction)
88
+ }
89
+ if ("style" in json && typeof json.style === "string") {
90
+ node.setStyle(json.style)
91
+ }
92
+ return node
93
+ }
94
+
95
+ isShadowRoot(): boolean {
96
+ return true
97
+ }
98
+
99
+ exportJSON(): SerializedLayoutItemNode {
100
+ const style = this.getStyle()
101
+ return {
102
+ ...super.exportJSON(),
103
+ ...(style ? { style } : {}),
104
+ type: "layout-item",
105
+ version: 1,
106
+ }
107
+ }
108
+ }
109
+
110
+ export function $createLayoutItemNode(): LayoutItemNode {
111
+ return new LayoutItemNode()
112
+ }
113
+
114
+ export function $isLayoutItemNode(
115
+ node: LexicalNode | null | undefined
116
+ ): node is LayoutItemNode {
117
+ return node instanceof LayoutItemNode
118
+ }