@collabchron/notiq 0.2.0

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 (188) hide show
  1. package/README.md +71 -0
  2. package/components.json +21 -0
  3. package/eslint.config.mjs +16 -0
  4. package/next.config.ts +12 -0
  5. package/package.json +108 -0
  6. package/postcss.config.mjs +5 -0
  7. package/public/file.svg +1 -0
  8. package/public/globe.svg +1 -0
  9. package/public/images/icons/plus.svg +10 -0
  10. package/public/next.svg +1 -0
  11. package/public/vercel.svg +1 -0
  12. package/public/window.svg +1 -0
  13. package/src/app/actions.ts +2 -0
  14. package/src/app/api/ai/route.ts +175 -0
  15. package/src/app/api/edgestore/[...edgestore]/route.ts +28 -0
  16. package/src/app/favicon.ico +0 -0
  17. package/src/app/globals.css +205 -0
  18. package/src/app/layout.tsx +38 -0
  19. package/src/app/page.tsx +12 -0
  20. package/src/components/editor/Core.tsx +220 -0
  21. package/src/components/editor/hooks/instructions-messages.ts +300 -0
  22. package/src/components/editor/hooks/use-mobile.ts +19 -0
  23. package/src/components/editor/hooks/useReport.ts +67 -0
  24. package/src/components/editor/hooks/useResizeObservert.ts +22 -0
  25. package/src/components/editor/index.tsx +39 -0
  26. package/src/components/editor/lexical-on-change.tsx +28 -0
  27. package/src/components/editor/nodes/CollapsibleNode/CollapsibleContainerNode.ts +92 -0
  28. package/src/components/editor/nodes/CollapsibleNode/CollapsibleContentNode.ts +65 -0
  29. package/src/components/editor/nodes/CollapsibleNode/CollapsibleTitleNode.ts +105 -0
  30. package/src/components/editor/nodes/EquationNode/EquationComponent.tsx +143 -0
  31. package/src/components/editor/nodes/EquationNode/EquationNode.tsx +170 -0
  32. package/src/components/editor/nodes/ExcalidrawNode/ExcalidrawComponent.tsx +228 -0
  33. package/src/components/editor/nodes/ExcalidrawNode/ExcalidrawImage.tsx +137 -0
  34. package/src/components/editor/nodes/ExcalidrawNode/ImageResizer.tsx +317 -0
  35. package/src/components/editor/nodes/ExcalidrawNode/index.tsx +204 -0
  36. package/src/components/editor/nodes/FigmaNode/FigmaNode.tsx +134 -0
  37. package/src/components/editor/nodes/Hint/HintComponet.tsx +221 -0
  38. package/src/components/editor/nodes/Hint/index.tsx +190 -0
  39. package/src/components/editor/nodes/ImageNode/index.tsx +328 -0
  40. package/src/components/editor/nodes/InlineImageNode/InlineImageComponent.tsx +383 -0
  41. package/src/components/editor/nodes/InlineImageNode/InlineImageNode.css +94 -0
  42. package/src/components/editor/nodes/InlineImageNode/InlineImageNode.tsx +309 -0
  43. package/src/components/editor/nodes/LayoutNode/LayoutContainerNode.ts +146 -0
  44. package/src/components/editor/nodes/LayoutNode/LayoutItemNode.ts +79 -0
  45. package/src/components/editor/nodes/PollNode/index.tsx +204 -0
  46. package/src/components/editor/nodes/Stepper/index.tsx +260 -0
  47. package/src/components/editor/nodes/TweetNode/index.tsx +214 -0
  48. package/src/components/editor/nodes/index.ts +81 -0
  49. package/src/components/editor/plugins/AutoEmbedPlugin/index.tsx +350 -0
  50. package/src/components/editor/plugins/AutoLinkPlugin/index.tsx +56 -0
  51. package/src/components/editor/plugins/CodeActionMenuPlugin/components/CopyButton.tsx +70 -0
  52. package/src/components/editor/plugins/CodeActionMenuPlugin/components/PrettierButton.tsx +192 -0
  53. package/src/components/editor/plugins/CodeActionMenuPlugin/index.tsx +217 -0
  54. package/src/components/editor/plugins/CodeActionMenuPlugin/utils.ts +26 -0
  55. package/src/components/editor/plugins/CodeHighlightPlugin/index.ts +21 -0
  56. package/src/components/editor/plugins/CollapsiblePlugin/Collapsible.css +76 -0
  57. package/src/components/editor/plugins/CollapsiblePlugin/index.ts +228 -0
  58. package/src/components/editor/plugins/DragDropPastePlugin/index.tsx +44 -0
  59. package/src/components/editor/plugins/DraggableBlockPlugin/index.tsx +52 -0
  60. package/src/components/editor/plugins/EquationsPlugin/index.tsx +85 -0
  61. package/src/components/editor/plugins/ExcalidrawPlugin/index.tsx +98 -0
  62. package/src/components/editor/plugins/FigmaPlugin/index.tsx +42 -0
  63. package/src/components/editor/plugins/FloatingLinkEditorPlugin/index.tsx +445 -0
  64. package/src/components/editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +275 -0
  65. package/src/components/editor/plugins/ImagesPlugin/index.tsx +222 -0
  66. package/src/components/editor/plugins/InlineImagePlugin/index.tsx +351 -0
  67. package/src/components/editor/plugins/LayoutPlugin/index.tsx +238 -0
  68. package/src/components/editor/plugins/LinkPlugin/index.tsx +36 -0
  69. package/src/components/editor/plugins/LinkWithMetaData/index.tsx +271 -0
  70. package/src/components/editor/plugins/MarkdownShortcutPlugin/index.tsx +11 -0
  71. package/src/components/editor/plugins/MarkdownTransformers/index.tsx +304 -0
  72. package/src/components/editor/plugins/PollPlugin/index.tsx +49 -0
  73. package/src/components/editor/plugins/ShortcutsPlugin/index.tsx +180 -0
  74. package/src/components/editor/plugins/ShortcutsPlugin/shortcuts.ts +253 -0
  75. package/src/components/editor/plugins/SlashCommand/index.tsx +621 -0
  76. package/src/components/editor/plugins/SpeechToTextPlugin/index.ts +127 -0
  77. package/src/components/editor/plugins/TabFocusPlugin/index.ts +58 -0
  78. package/src/components/editor/plugins/TableCellActionMenuPlugin/index.tsx +759 -0
  79. package/src/components/editor/plugins/TableCellResizer/index.tsx +438 -0
  80. package/src/components/editor/plugins/TableHoverActionsPlugin/index.tsx +314 -0
  81. package/src/components/editor/plugins/TablePlugin/index.tsx +99 -0
  82. package/src/components/editor/plugins/ToolbarPlugin/index.tsx +522 -0
  83. package/src/components/editor/plugins/TwitterPlugin/index.ts +35 -0
  84. package/src/components/editor/plugins/YouTubeNode/index.tsx +179 -0
  85. package/src/components/editor/plugins/YouTubePlugin/index.ts +41 -0
  86. package/src/components/editor/themes/editor-theme.ts +113 -0
  87. package/src/components/editor/themes/theme.css +377 -0
  88. package/src/components/editor/utils/ai.ts +291 -0
  89. package/src/components/editor/utils/canUseDOM.ts +12 -0
  90. package/src/components/editor/utils/editorFormatting.ts +282 -0
  91. package/src/components/editor/utils/environment.ts +50 -0
  92. package/src/components/editor/utils/extract-data.ts +166 -0
  93. package/src/components/editor/utils/getAllLexicalChildren.ts +13 -0
  94. package/src/components/editor/utils/getDOMRangeRect.ts +27 -0
  95. package/src/components/editor/utils/getSelectedNode.ts +27 -0
  96. package/src/components/editor/utils/gif.ts +29 -0
  97. package/src/components/editor/utils/invariant.ts +15 -0
  98. package/src/components/editor/utils/setFloatingElemPosition.ts +51 -0
  99. package/src/components/editor/utils/setFloatingElemPositionForLinkEditor.ts +40 -0
  100. package/src/components/editor/utils/setNodePlaceholderFromSelection/getNodePlaceholder.ts +51 -0
  101. package/src/components/editor/utils/setNodePlaceholderFromSelection/setNodePlaceholderFromSelection.ts +15 -0
  102. package/src/components/editor/utils/setNodePlaceholderFromSelection/setPlaceholderOnSelection.ts +114 -0
  103. package/src/components/editor/utils/setNodePlaceholderFromSelection/styles.css +6 -0
  104. package/src/components/editor/utils/url.ts +109 -0
  105. package/src/components/editor/utils/useLayoutEffect.ts +13 -0
  106. package/src/components/providers/QueryProvider.tsx +15 -0
  107. package/src/components/providers/SharedHistoryContext.tsx +28 -0
  108. package/src/components/providers/ToolbarContext.tsx +123 -0
  109. package/src/components/providers/theme-provider.tsx +11 -0
  110. package/src/components/theme/ModeToggle.tsx +40 -0
  111. package/src/components/ui/FileInput.tsx +40 -0
  112. package/src/components/ui/Input.css +32 -0
  113. package/src/components/ui/Select.css +42 -0
  114. package/src/components/ui/Select.tsx +36 -0
  115. package/src/components/ui/TextInput.tsx +48 -0
  116. package/src/components/ui/ai/ai-button.tsx +574 -0
  117. package/src/components/ui/ai/border.tsx +99 -0
  118. package/src/components/ui/ai/placeholder-input-vanish.tsx +282 -0
  119. package/src/components/ui/button.tsx +89 -0
  120. package/src/components/ui/card.tsx +76 -0
  121. package/src/components/ui/checkbox.tsx +30 -0
  122. package/src/components/ui/command.tsx +153 -0
  123. package/src/components/ui/dialog/Dialog.css +25 -0
  124. package/src/components/ui/dialog/Dialog.tsx +34 -0
  125. package/src/components/ui/dialog.tsx +122 -0
  126. package/src/components/ui/drop-downs/background-color.tsx +183 -0
  127. package/src/components/ui/drop-downs/block-format.tsx +159 -0
  128. package/src/components/ui/drop-downs/code.tsx +42 -0
  129. package/src/components/ui/drop-downs/color.tsx +177 -0
  130. package/src/components/ui/drop-downs/font-size.tsx +138 -0
  131. package/src/components/ui/drop-downs/font.tsx +155 -0
  132. package/src/components/ui/drop-downs/index.tsx +122 -0
  133. package/src/components/ui/drop-downs/insert-node.tsx +213 -0
  134. package/src/components/ui/drop-downs/text-align.tsx +123 -0
  135. package/src/components/ui/drop-downs/text-format.tsx +104 -0
  136. package/src/components/ui/dropdown-menu.tsx +201 -0
  137. package/src/components/ui/equation/EquationEditor.css +38 -0
  138. package/src/components/ui/equation/EquationEditor.tsx +56 -0
  139. package/src/components/ui/equation/KatexEquationAlterer.css +41 -0
  140. package/src/components/ui/equation/KatexEquationAlterer.tsx +83 -0
  141. package/src/components/ui/equation/KatexRenderer.tsx +66 -0
  142. package/src/components/ui/excalidraw/ExcalidrawModal.css +64 -0
  143. package/src/components/ui/excalidraw/ExcalidrawModal.tsx +234 -0
  144. package/src/components/ui/excalidraw/Modal.css +62 -0
  145. package/src/components/ui/excalidraw/Modal.tsx +110 -0
  146. package/src/components/ui/hover-card.tsx +29 -0
  147. package/src/components/ui/image/error-image.tsx +17 -0
  148. package/src/components/ui/image/file-upload.tsx +240 -0
  149. package/src/components/ui/image/image-resizer.tsx +297 -0
  150. package/src/components/ui/image/image-toolbar.tsx +264 -0
  151. package/src/components/ui/image/index.tsx +408 -0
  152. package/src/components/ui/image/lazy-image.tsx +68 -0
  153. package/src/components/ui/image/lazy-video.tsx +71 -0
  154. package/src/components/ui/input.tsx +22 -0
  155. package/src/components/ui/models/custom-dialog.tsx +320 -0
  156. package/src/components/ui/models/insert-gif.tsx +90 -0
  157. package/src/components/ui/models/insert-image.tsx +52 -0
  158. package/src/components/ui/models/insert-poll.tsx +29 -0
  159. package/src/components/ui/models/insert-table.tsx +62 -0
  160. package/src/components/ui/models/use-model.tsx +91 -0
  161. package/src/components/ui/poll/poll-component.tsx +304 -0
  162. package/src/components/ui/popover.tsx +33 -0
  163. package/src/components/ui/progress.tsx +28 -0
  164. package/src/components/ui/scroll-area.tsx +48 -0
  165. package/src/components/ui/separator.tsx +31 -0
  166. package/src/components/ui/skeleton.tsx +15 -0
  167. package/src/components/ui/sonner.tsx +31 -0
  168. package/src/components/ui/stepper/step.tsx +179 -0
  169. package/src/components/ui/stepper/stepper.tsx +89 -0
  170. package/src/components/ui/textarea.tsx +22 -0
  171. package/src/components/ui/toggle.tsx +71 -0
  172. package/src/components/ui/tooltip.tsx +32 -0
  173. package/src/components/ui/write/text-format-floting-toolbar.tsx +346 -0
  174. package/src/lib/edgestore.ts +9 -0
  175. package/src/lib/pinecone-client.ts +0 -0
  176. package/src/lib/utils.ts +6 -0
  177. package/src/utils/docSerialization.ts +77 -0
  178. package/src/utils/emoji-list.ts +16615 -0
  179. package/src/utils/getDOMRangeRect.ts +27 -0
  180. package/src/utils/getSelectedNode.ts +27 -0
  181. package/src/utils/getThemeSelector.ts +25 -0
  182. package/src/utils/isMobileWidth.ts +7 -0
  183. package/src/utils/joinClasses.ts +13 -0
  184. package/src/utils/setFloatingElemPosition.ts +74 -0
  185. package/src/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
  186. package/src/utils/swipe.ts +127 -0
  187. package/src/utils/url.ts +38 -0
  188. package/tsconfig.json +27 -0
@@ -0,0 +1,204 @@
1
+ import {
2
+ DecoratorNode,
3
+ DOMConversionMap,
4
+ DOMConversionOutput,
5
+ DOMExportOutput,
6
+ LexicalNode,
7
+ NodeKey,
8
+ SerializedLexicalNode,
9
+ Spread,
10
+ } from "lexical";
11
+ import * as React from "react";
12
+ import { Suspense } from "react";
13
+
14
+ export type Options = ReadonlyArray<Option>;
15
+
16
+ export type Option = Readonly<{
17
+ text: string;
18
+ uid: string;
19
+ votes: Array<string>;
20
+ }>;
21
+
22
+ const PollComponent = React.lazy(
23
+ () => import("@/components/ui/poll/poll-component")
24
+ );
25
+
26
+ function createUID(): string {
27
+ return Math.random()
28
+ .toString(36)
29
+ .replace(/[^a-z]+/g, "")
30
+ .substr(0, 5);
31
+ }
32
+
33
+ export function createPollOption(text = ""): Option {
34
+ return {
35
+ text,
36
+ uid: createUID(),
37
+ votes: [],
38
+ };
39
+ }
40
+
41
+ function cloneOption(
42
+ option: Option,
43
+ text: string,
44
+ votes?: Array<string>
45
+ ): Option {
46
+ return {
47
+ text,
48
+ uid: option.uid,
49
+ votes: votes || Array.from(option.votes),
50
+ };
51
+ }
52
+
53
+ export type SerializedPollNode = Spread<
54
+ {
55
+ question: string;
56
+ options: Options;
57
+ },
58
+ SerializedLexicalNode
59
+ >;
60
+
61
+ function $convertPollElement(domNode: HTMLElement): DOMConversionOutput | null {
62
+ const question = domNode.getAttribute("data-lexical-poll-question");
63
+ const options = domNode.getAttribute("data-lexical-poll-options");
64
+ if (question !== null && options !== null) {
65
+ const node = $createPollNode(question, JSON.parse(options));
66
+ return { node };
67
+ }
68
+ return null;
69
+ }
70
+
71
+ export class PollNode extends DecoratorNode<React.JSX.Element> {
72
+ __question: string;
73
+ __options: Options;
74
+
75
+ static getType(): string {
76
+ return "poll";
77
+ }
78
+
79
+ static clone(node: PollNode): PollNode {
80
+ return new PollNode(node.__question, node.__options, node.__key);
81
+ }
82
+
83
+ static importJSON(serializedNode: SerializedPollNode): PollNode {
84
+ return $createPollNode(
85
+ serializedNode.question,
86
+ serializedNode.options
87
+ ).updateFromJSON(serializedNode);
88
+ }
89
+
90
+ constructor(question: string, options: Options, key?: NodeKey) {
91
+ super(key);
92
+ this.__question = question;
93
+ this.__options = options;
94
+ }
95
+
96
+ exportJSON(): SerializedPollNode {
97
+ return {
98
+ ...super.exportJSON(),
99
+ options: this.__options,
100
+ question: this.__question,
101
+ };
102
+ }
103
+
104
+ setQuestion(newQuestion: string): void {
105
+ const self = this.getWritable();
106
+ self.__question = newQuestion;
107
+ }
108
+
109
+ addOption(option: Option): void {
110
+ const self = this.getWritable();
111
+ const options = Array.from(self.__options);
112
+ options.push(option);
113
+ self.__options = options;
114
+ }
115
+
116
+ deleteOption(option: Option): void {
117
+ const self = this.getWritable();
118
+ const options = Array.from(self.__options);
119
+ const index = options.indexOf(option);
120
+ options.splice(index, 1);
121
+ self.__options = options;
122
+ }
123
+
124
+ setOptionText(option: Option, text: string): void {
125
+ const self = this.getWritable();
126
+ const clonedOption = cloneOption(option, text);
127
+ const options = Array.from(self.__options);
128
+ const index = options.indexOf(option);
129
+ options[index] = clonedOption;
130
+ self.__options = options;
131
+ }
132
+
133
+ toggleVote(option: Option, clientID: string): void {
134
+ const self = this.getWritable();
135
+ const newOptions = self.__options.map((opt) => {
136
+ if (opt.uid === option.uid) {
137
+ const newVotes = opt.votes.includes(clientID)
138
+ ? opt.votes.filter((id) => id !== clientID)
139
+ : [...opt.votes, clientID];
140
+ return { ...opt, votes: newVotes };
141
+ }
142
+ return { ...opt };
143
+ });
144
+
145
+ self.__options = newOptions;
146
+ this.markDirty();
147
+ }
148
+
149
+ static importDOM(): DOMConversionMap | null {
150
+ return {
151
+ span: (domNode: HTMLElement) => {
152
+ if (!domNode.hasAttribute("data-lexical-poll-question")) {
153
+ return null;
154
+ }
155
+ return {
156
+ conversion: $convertPollElement,
157
+ priority: 2,
158
+ };
159
+ },
160
+ };
161
+ }
162
+
163
+ exportDOM(): DOMExportOutput {
164
+ const element = document.createElement("span");
165
+ element.setAttribute("data-lexical-poll-question", this.__question);
166
+ element.setAttribute(
167
+ "data-lexical-poll-options",
168
+ JSON.stringify(this.__options)
169
+ );
170
+ return { element };
171
+ }
172
+
173
+ createDOM(): HTMLElement {
174
+ const elem = document.createElement("span");
175
+ elem.style.display = "inline-block";
176
+ return elem;
177
+ }
178
+
179
+ updateDOM(): false {
180
+ return false;
181
+ }
182
+
183
+ decorate(): React.JSX.Element {
184
+ return (
185
+ <Suspense fallback={null}>
186
+ <PollComponent
187
+ question={this.__question}
188
+ options={this.__options}
189
+ nodeKey={this.__key}
190
+ />
191
+ </Suspense>
192
+ );
193
+ }
194
+ }
195
+
196
+ export function $createPollNode(question: string, options: Options): PollNode {
197
+ return new PollNode(question, options);
198
+ }
199
+
200
+ export function $isPollNode(
201
+ node: LexicalNode | null | undefined
202
+ ): node is PollNode {
203
+ return node instanceof PollNode;
204
+ }
@@ -0,0 +1,260 @@
1
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
2
+ import {
3
+ SerializedLexicalNode,
4
+ SerializedEditor,
5
+ Spread,
6
+ DecoratorNode,
7
+ NodeKey,
8
+ LexicalNode,
9
+ createEditor,
10
+ LexicalCommand,
11
+ createCommand,
12
+ $getSelection,
13
+ $isRangeSelection,
14
+ COMMAND_PRIORITY_LOW,
15
+ LexicalEditor,
16
+ EditorState,
17
+ } from "lexical";
18
+ import React, { Suspense, useEffect } from "react";
19
+ const StepperComponent = React.lazy(
20
+ () => import("@/components/ui/stepper/stepper")
21
+ );
22
+
23
+ export interface StepType {
24
+ title: string;
25
+ id: number;
26
+ content: LexicalEditor;
27
+ }
28
+
29
+ export type StepsType = StepType[];
30
+
31
+ export type SerializedStepper = Spread<
32
+ {
33
+ type: "stepper";
34
+ steps: {
35
+ title: string;
36
+ id: string;
37
+ content: SerializedEditor;
38
+ }[];
39
+ },
40
+ SerializedLexicalNode
41
+ >;
42
+
43
+ export const initialEditorState = {
44
+ root: {
45
+ children: [
46
+ {
47
+ children: [
48
+ {
49
+ detail: 0,
50
+ format: 0,
51
+ mode: "normal",
52
+ style: "",
53
+ text: "type the content here..",
54
+ type: "text",
55
+ version: 1,
56
+ },
57
+ ],
58
+ direction: null,
59
+ format: "",
60
+ indent: 0,
61
+ type: "paragraph",
62
+ version: 1,
63
+ textFormat: 0,
64
+ textStyle: "",
65
+ },
66
+ ],
67
+ direction: null,
68
+ format: "",
69
+ indent: 0,
70
+ type: "root",
71
+ version: 1,
72
+ },
73
+ };
74
+
75
+ export class StepperNode extends DecoratorNode<React.ReactElement> {
76
+ __steps: StepsType;
77
+
78
+ constructor(steps: StepsType, key?: NodeKey) {
79
+ super(key);
80
+ this.__steps = steps.map((step) => {
81
+ if (!step.content) {
82
+ const newEditor = createEditor();
83
+ const parsedEditorState = newEditor.parseEditorState(
84
+ JSON.stringify(initialEditorState)
85
+ );
86
+ newEditor.setEditorState(parsedEditorState);
87
+ return { ...step, content: newEditor };
88
+ }
89
+ return step;
90
+ });
91
+ }
92
+
93
+ static getType(): string {
94
+ return "stepper";
95
+ }
96
+
97
+ addStep(step: Omit<StepType, "content">) {
98
+ const self = this.getWritable();
99
+ const newEditor = createEditor();
100
+ const parsedEditorState = newEditor.parseEditorState(
101
+ JSON.stringify(initialEditorState)
102
+ );
103
+ newEditor.setEditorState(parsedEditorState);
104
+ const newStep: StepType = {
105
+ ...step,
106
+ content: newEditor,
107
+ };
108
+ self.__steps.push(newStep);
109
+ return self;
110
+ }
111
+
112
+ insertStepAtIndex(step: Omit<StepType, "content">, index: number) {
113
+ if (index < 0 || index > this.__steps.length) {
114
+ throw new Error("Invalid index for inserting step.");
115
+ }
116
+ const self = this.getWritable();
117
+ const newEditor = createEditor();
118
+ const parsedEditorState = newEditor.parseEditorState(
119
+ JSON.stringify(initialEditorState)
120
+ );
121
+ newEditor.setEditorState(parsedEditorState);
122
+ const newStep: StepType = {
123
+ ...step,
124
+ content: newEditor,
125
+ };
126
+ self.__steps.splice(index, 0, newStep);
127
+ return self;
128
+ }
129
+ deleteStep(id: number) {
130
+ const self = this.getWritable();
131
+ self.__steps = self.__steps.filter(step => step.id !== id);
132
+ return self;
133
+ }
134
+ updateTitle(id: number, title: string) {
135
+ const self = this.getWritable();
136
+ const step = self.__steps.find(s => s.id === id);
137
+ if (step) step.title = title;
138
+ return self;
139
+ }
140
+
141
+
142
+ replaceSteps(steps: StepsType) {
143
+ const self = this.getWritable();
144
+ self.__steps = steps.map((step) => {
145
+ if (step.content) return step;
146
+ const newEditor = createEditor();
147
+ const parsedEditorState = newEditor.parseEditorState(
148
+ JSON.stringify(initialEditorState)
149
+ );
150
+ newEditor.setEditorState(parsedEditorState);
151
+ return { ...step, content: newEditor };
152
+ });
153
+ return self;
154
+ }
155
+
156
+ reorderSteps(newSteps: StepsType) {
157
+ const self = this.getWritable();
158
+ self.__steps = newSteps;
159
+ return self;
160
+ }
161
+
162
+
163
+ static clone(node: StepperNode): StepperNode {
164
+ return new StepperNode(node.__steps, node.__key);
165
+ }
166
+
167
+ static importJSON(serializedNode: SerializedStepper): StepperNode {
168
+ const steps: StepType[] = serializedNode.steps.map((serializedStep) => {
169
+ const newEditor = createEditor();
170
+ const editorState = newEditor.parseEditorState(
171
+ serializedStep.content.editorState
172
+ );
173
+ newEditor.setEditorState(editorState);
174
+ return {
175
+ title: serializedStep.title,
176
+ id: parseInt(serializedStep.id, 10),
177
+ content: newEditor,
178
+ };
179
+ });
180
+ return new StepperNode(steps);
181
+ }
182
+
183
+ exportJSON(): SerializedStepper {
184
+ return {
185
+ ...super.exportJSON(),
186
+ steps: this.__steps.map((step) => ({
187
+ title: step.title,
188
+ id: step.id.toString(),
189
+ content: {
190
+ editorState: step.content.getEditorState().toJSON(),
191
+ },
192
+ })),
193
+ type: "stepper",
194
+ };
195
+ }
196
+
197
+ createDOM(): HTMLElement {
198
+ const element = document.createElement("div");
199
+ element.className = `stepper-${this.__type}`;
200
+ return element;
201
+ }
202
+
203
+ updateDOM(): false {
204
+ return false;
205
+ }
206
+
207
+ decorate(): React.ReactElement {
208
+ return (
209
+ <Suspense fallback={null}>
210
+ <div>
211
+ <StepperComponent steps={this.__steps} nodeKey={this.getKey()} />
212
+ </div>
213
+ </Suspense>
214
+ );
215
+ }
216
+ }
217
+
218
+ export function $isStepperNode(
219
+ node: LexicalNode | null | undefined
220
+ ): node is StepperNode {
221
+ return node instanceof StepperNode;
222
+ }
223
+
224
+ export function $createStepperNode(steps: StepsType): StepperNode {
225
+ return new StepperNode(steps);
226
+ }
227
+
228
+ export const INSERT_STEPPER_COMMAND: LexicalCommand<StepsType> =
229
+ createCommand();
230
+
231
+ export function $insertStepperNode(steps: StepsType) {
232
+ const selection = $getSelection();
233
+ if ($isRangeSelection(selection)) {
234
+ const stepperNode = $createStepperNode(steps);
235
+ selection.insertNodes([stepperNode]);
236
+ }
237
+ }
238
+
239
+ export default function StepperPlugin() {
240
+ const [editor] = useLexicalComposerContext();
241
+ useEffect(() => {
242
+ if (!editor.hasNodes([StepperNode])) {
243
+ throw new Error("StepperNode: StepperNode not registered on editor");
244
+ }
245
+ }, [editor]);
246
+ useEffect(() => {
247
+ return editor.registerCommand(
248
+ INSERT_STEPPER_COMMAND,
249
+ (payload: StepsType) => {
250
+ editor.update(() => {
251
+ $insertStepperNode(payload);
252
+ });
253
+ return true;
254
+ },
255
+ COMMAND_PRIORITY_LOW
256
+ );
257
+ }, [editor]);
258
+
259
+ return null;
260
+ }
@@ -0,0 +1,214 @@
1
+ import type {
2
+ DOMConversionMap,
3
+ DOMConversionOutput,
4
+ DOMExportOutput,
5
+ EditorConfig,
6
+ ElementFormatType,
7
+ LexicalEditor,
8
+ LexicalNode,
9
+ NodeKey,
10
+ Spread,
11
+ } from "lexical";
12
+
13
+ import { BlockWithAlignableContents } from "@lexical/react/LexicalBlockWithAlignableContents";
14
+ import {
15
+ DecoratorBlockNode,
16
+ SerializedDecoratorBlockNode,
17
+ } from "@lexical/react/LexicalDecoratorBlockNode";
18
+ import * as React from "react";
19
+ import { useCallback, useEffect, useRef, useState } from "react";
20
+ import { Skeleton } from "@/components/ui/skeleton";
21
+
22
+ const WIDGET_SCRIPT_URL = "https://platform.twitter.com/widgets.js";
23
+
24
+ type TweetComponentProps = Readonly<{
25
+ className: Readonly<{
26
+ base: string;
27
+ focus: string;
28
+ }>;
29
+ format: ElementFormatType | null;
30
+ loadingComponent?: React.JSX.Element | string;
31
+ nodeKey: NodeKey;
32
+ onError?: (error: string) => void;
33
+ onLoad?: () => void;
34
+ tweetID: string;
35
+ }>;
36
+
37
+ function $convertTweetElement(
38
+ domNode: HTMLDivElement
39
+ ): DOMConversionOutput | null {
40
+ const id = domNode.getAttribute("data-lexical-tweet-id");
41
+ if (id) {
42
+ const node = $createTweetNode(id);
43
+ return { node };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ let isTwitterScriptLoading = true;
49
+
50
+ function TweetComponent({
51
+ className,
52
+ format,
53
+ nodeKey,
54
+ onError,
55
+ onLoad,
56
+ tweetID,
57
+ }: TweetComponentProps) {
58
+ const containerRef = useRef<HTMLDivElement | null>(null);
59
+
60
+ const previousTweetIDRef = useRef<string>("");
61
+ const [isTweetLoading, setIsTweetLoading] = useState(false);
62
+
63
+ const createTweet = useCallback(async () => {
64
+ try {
65
+ // @ts-expect-error Twitter is attached to the window.
66
+ await window.twttr.widgets.createTweet(tweetID, containerRef.current);
67
+
68
+ setIsTweetLoading(false);
69
+ isTwitterScriptLoading = false;
70
+
71
+ if (onLoad) {
72
+ onLoad();
73
+ }
74
+ } catch (error) {
75
+ if (onError) {
76
+ onError(String(error));
77
+ }
78
+ }
79
+ }, [onError, onLoad, tweetID]);
80
+
81
+ useEffect(() => {
82
+ if (tweetID !== previousTweetIDRef.current) {
83
+ setIsTweetLoading(true);
84
+
85
+ if (isTwitterScriptLoading) {
86
+ const script = document.createElement("script");
87
+ script.src = WIDGET_SCRIPT_URL;
88
+ script.async = true;
89
+ document.body?.appendChild(script);
90
+ script.onload = createTweet;
91
+ if (onError) {
92
+ script.onerror = onError as OnErrorEventHandler;
93
+ }
94
+ } else {
95
+ createTweet();
96
+ }
97
+
98
+ if (previousTweetIDRef) {
99
+ previousTweetIDRef.current = tweetID;
100
+ }
101
+ }
102
+ }, [createTweet, onError, tweetID]);
103
+
104
+ return (
105
+ <BlockWithAlignableContents
106
+ className={className}
107
+ format={format}
108
+ nodeKey={nodeKey}
109
+ >
110
+ {isTweetLoading ? (
111
+ <Skeleton className="max-h-[200px] w-full h-full max-w-[400px]" />
112
+ ) : null}
113
+ <div
114
+ style={{
115
+ display: "inline-block",
116
+ width: "100%",
117
+ }}
118
+ ref={containerRef}
119
+ />
120
+ </BlockWithAlignableContents>
121
+ );
122
+ }
123
+
124
+ export type SerializedTweetNode = Spread<
125
+ {
126
+ id: string;
127
+ },
128
+ SerializedDecoratorBlockNode
129
+ >;
130
+
131
+ export class TweetNode extends DecoratorBlockNode {
132
+ __id: string;
133
+
134
+ static getType(): string {
135
+ return "tweet";
136
+ }
137
+
138
+ static clone(node: TweetNode): TweetNode {
139
+ return new TweetNode(node.__id, node.__format, node.__key);
140
+ }
141
+
142
+ static importJSON(serializedNode: SerializedTweetNode): TweetNode {
143
+ return $createTweetNode(serializedNode.id).updateFromJSON(serializedNode);
144
+ }
145
+
146
+ exportJSON(): SerializedTweetNode {
147
+ return {
148
+ ...super.exportJSON(),
149
+ id: this.getId(),
150
+ };
151
+ }
152
+
153
+ static importDOM(): DOMConversionMap<HTMLDivElement> | null {
154
+ return {
155
+ div: (domNode: HTMLDivElement) => {
156
+ if (!domNode.hasAttribute("data-lexical-tweet-id")) {
157
+ return null;
158
+ }
159
+ return {
160
+ conversion: $convertTweetElement,
161
+ priority: 2,
162
+ };
163
+ },
164
+ };
165
+ }
166
+
167
+ exportDOM(): DOMExportOutput {
168
+ const element = document.createElement("div");
169
+ element.setAttribute("data-lexical-tweet-id", this.__id);
170
+ const text = document.createTextNode(this.getTextContent());
171
+ element.append(text);
172
+ return { element };
173
+ }
174
+
175
+ constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
176
+ super(format, key);
177
+ this.__id = id;
178
+ }
179
+
180
+ getId(): string {
181
+ return this.__id;
182
+ }
183
+
184
+ getTextContent(): string {
185
+ return `https://x.com/i/web/status/${this.__id}`;
186
+ }
187
+
188
+ decorate(editor: LexicalEditor, config: EditorConfig): React.JSX.Element {
189
+ const embedBlockTheme = config.theme.embedBlock || {};
190
+ const className = {
191
+ base: embedBlockTheme.base || "",
192
+ focus: embedBlockTheme.focus || "",
193
+ };
194
+ return (
195
+ <TweetComponent
196
+ className={className}
197
+ format={this.__format}
198
+ loadingComponent="Loading..."
199
+ nodeKey={this.getKey()}
200
+ tweetID={this.__id}
201
+ />
202
+ );
203
+ }
204
+ }
205
+
206
+ export function $createTweetNode(tweetID: string): TweetNode {
207
+ return new TweetNode(tweetID);
208
+ }
209
+
210
+ export function $isTweetNode(
211
+ node: TweetNode | LexicalNode | null | undefined
212
+ ): node is TweetNode {
213
+ return node instanceof TweetNode;
214
+ }