@cyberyozh/text-editor 0.1.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 (209) hide show
  1. package/README.md +102 -0
  2. package/dist/editor.css +2069 -0
  3. package/dist/editor.css.map +1 -0
  4. package/dist/editor.d.mts +45 -0
  5. package/dist/editor.d.ts +45 -0
  6. package/dist/editor.js +6155 -0
  7. package/dist/editor.js.map +1 -0
  8. package/dist/editor.mjs +6118 -0
  9. package/dist/editor.mjs.map +1 -0
  10. package/dist/render/client.d.mts +24 -0
  11. package/dist/render/client.d.ts +24 -0
  12. package/dist/render/client.js +98 -0
  13. package/dist/render/client.js.map +1 -0
  14. package/dist/render/client.mjs +74 -0
  15. package/dist/render/client.mjs.map +1 -0
  16. package/dist/render.d.mts +58 -0
  17. package/dist/render.d.ts +58 -0
  18. package/dist/render.js +2521 -0
  19. package/dist/render.js.map +1 -0
  20. package/dist/render.mjs +2484 -0
  21. package/dist/render.mjs.map +1 -0
  22. package/dist/styles/index.css +2068 -0
  23. package/package.json +92 -0
  24. package/src/editor/commands/items.ts +374 -0
  25. package/src/editor/commands/suggestion.tsx +146 -0
  26. package/src/editor/components/button/button.css +56 -0
  27. package/src/editor/components/button/button.tsx +35 -0
  28. package/src/editor/components/button/index.ts +1 -0
  29. package/src/editor/components/common-modal/common-modal.css +85 -0
  30. package/src/editor/components/common-modal/common-modal.tsx +58 -0
  31. package/src/editor/components/common-modal/index.ts +1 -0
  32. package/src/editor/components/index.ts +3 -0
  33. package/src/editor/components/input/index.ts +1 -0
  34. package/src/editor/components/input/input.css +43 -0
  35. package/src/editor/components/input/input.tsx +22 -0
  36. package/src/editor/context/dialog-command.tsx +47 -0
  37. package/src/editor/context/index.ts +2 -0
  38. package/src/editor/context/translations-context.tsx +28 -0
  39. package/src/editor/controls/bubble-menu-content.tsx +394 -0
  40. package/src/editor/controls/callout-menu.tsx +169 -0
  41. package/src/editor/controls/character-count.tsx +36 -0
  42. package/src/editor/controls/commands-list.tsx +117 -0
  43. package/src/editor/controls/drag-handle.tsx +98 -0
  44. package/src/editor/controls/emoji-picker.tsx +62 -0
  45. package/src/editor/controls/index.ts +5 -0
  46. package/src/editor/controls/table-menu.tsx +322 -0
  47. package/src/editor/dialogs/add-katex-dialog/add-katex-dialog.css +115 -0
  48. package/src/editor/dialogs/add-katex-dialog/add-katex-dialog.tsx +116 -0
  49. package/src/editor/dialogs/add-katex-dialog/index.ts +1 -0
  50. package/src/editor/dialogs/add-link-dialog/add-link-dialog.css +105 -0
  51. package/src/editor/dialogs/add-link-dialog/add-link-dialog.tsx +177 -0
  52. package/src/editor/dialogs/add-link-dialog/index.ts +1 -0
  53. package/src/editor/dialogs/add-video-dialog/add-video-dialog.css +146 -0
  54. package/src/editor/dialogs/add-video-dialog/add-video-dialog.tsx +157 -0
  55. package/src/editor/dialogs/add-video-dialog/index.ts +1 -0
  56. package/src/editor/dialogs/image-dialog/image-dialog.css +101 -0
  57. package/src/editor/dialogs/image-dialog/image-dialog.tsx +276 -0
  58. package/src/editor/dialogs/image-dialog/index.ts +1 -0
  59. package/src/editor/dialogs/index.ts +4 -0
  60. package/src/editor/editor.tsx +247 -0
  61. package/src/editor/handlers/create-image-upload-handler.ts +24 -0
  62. package/src/editor/handlers/dialog-handlers.ts +143 -0
  63. package/src/editor/handlers/index.ts +8 -0
  64. package/src/editor/index.ts +3 -0
  65. package/src/extensions/base/callout.ts +138 -0
  66. package/src/extensions/base/code-block.ts +24 -0
  67. package/src/extensions/base/compatibility.ts +493 -0
  68. package/src/extensions/base/copyable-text.ts +56 -0
  69. package/src/extensions/base/custom-quote.ts +174 -0
  70. package/src/extensions/base/figure.ts +141 -0
  71. package/src/extensions/base/index.ts +19 -0
  72. package/src/extensions/base/link.ts +18 -0
  73. package/src/extensions/base/myth-reality.ts +45 -0
  74. package/src/extensions/base/prompt.ts +109 -0
  75. package/src/extensions/base/video.ts +29 -0
  76. package/src/extensions/editor/callout.ts +43 -0
  77. package/src/extensions/editor/code-block.ts +9 -0
  78. package/src/extensions/editor/commands.ts +43 -0
  79. package/src/extensions/editor/copyable-text.ts +16 -0
  80. package/src/extensions/editor/custom-quote.ts +54 -0
  81. package/src/extensions/editor/figure.ts +20 -0
  82. package/src/extensions/editor/get-editor-extensions.ts +113 -0
  83. package/src/extensions/editor/index.ts +25 -0
  84. package/src/extensions/editor/prompt.ts +43 -0
  85. package/src/extensions/editor/video.ts +23 -0
  86. package/src/extensions/render/get-render-extensions.ts +74 -0
  87. package/src/extensions/render/index.ts +6 -0
  88. package/src/extensions/views/components/callout.tsx +70 -0
  89. package/src/extensions/views/components/code-block.tsx +73 -0
  90. package/src/extensions/views/components/copy-button.tsx +52 -0
  91. package/src/extensions/views/components/copyable-text.tsx +11 -0
  92. package/src/extensions/views/components/custom-quote.tsx +64 -0
  93. package/src/extensions/views/components/figure.tsx +63 -0
  94. package/src/extensions/views/components/index.ts +7 -0
  95. package/src/extensions/views/components/prompt.tsx +43 -0
  96. package/src/extensions/views/components/video.tsx +40 -0
  97. package/src/extensions/views/node-views/callout.tsx +51 -0
  98. package/src/extensions/views/node-views/code-block.tsx +30 -0
  99. package/src/extensions/views/node-views/copyable-text.tsx +10 -0
  100. package/src/extensions/views/node-views/custom-quote.tsx +66 -0
  101. package/src/extensions/views/node-views/figure.tsx +38 -0
  102. package/src/extensions/views/node-views/index.ts +7 -0
  103. package/src/extensions/views/node-views/prompt.tsx +35 -0
  104. package/src/extensions/views/node-views/video.tsx +19 -0
  105. package/src/icons/Check.tsx +12 -0
  106. package/src/icons/Copy.tsx +15 -0
  107. package/src/icons/DragIcon.tsx +10 -0
  108. package/src/icons/Edit.tsx +12 -0
  109. package/src/icons/Quote.tsx +10 -0
  110. package/src/icons/X.tsx +13 -0
  111. package/src/icons/color/TiptapCalloutError.tsx +25 -0
  112. package/src/icons/color/TiptapCalloutInfo.tsx +18 -0
  113. package/src/icons/color/TiptapCalloutSuccess.tsx +49 -0
  114. package/src/icons/color/TiptapCalloutWarning.tsx +28 -0
  115. package/src/icons/color/index.ts +4 -0
  116. package/src/icons/index.ts +60 -0
  117. package/src/icons/menu/AddImage.tsx +10 -0
  118. package/src/icons/menu/AlignCenter.tsx +10 -0
  119. package/src/icons/menu/AlignLeft.tsx +10 -0
  120. package/src/icons/menu/AlignRight.tsx +10 -0
  121. package/src/icons/menu/ArrowDown.tsx +13 -0
  122. package/src/icons/menu/ArrowLeft.tsx +13 -0
  123. package/src/icons/menu/ArrowRight.tsx +13 -0
  124. package/src/icons/menu/ArrowUp.tsx +13 -0
  125. package/src/icons/menu/Blockquote.tsx +7 -0
  126. package/src/icons/menu/Bold.tsx +10 -0
  127. package/src/icons/menu/BulletList.tsx +10 -0
  128. package/src/icons/menu/CalloutError.tsx +10 -0
  129. package/src/icons/menu/CalloutInfo.tsx +12 -0
  130. package/src/icons/menu/CalloutSuccess.tsx +10 -0
  131. package/src/icons/menu/CalloutWarning.tsx +12 -0
  132. package/src/icons/menu/ClearFormatting.tsx +10 -0
  133. package/src/icons/menu/Code.tsx +10 -0
  134. package/src/icons/menu/CodeBlock.tsx +10 -0
  135. package/src/icons/menu/Copyable.tsx +10 -0
  136. package/src/icons/menu/CustomQuote.tsx +17 -0
  137. package/src/icons/menu/DeleteColumn.tsx +17 -0
  138. package/src/icons/menu/DeleteRow.tsx +17 -0
  139. package/src/icons/menu/Details.tsx +10 -0
  140. package/src/icons/menu/H2.tsx +10 -0
  141. package/src/icons/menu/H3.tsx +10 -0
  142. package/src/icons/menu/H4.tsx +10 -0
  143. package/src/icons/menu/H5.tsx +10 -0
  144. package/src/icons/menu/H6.tsx +10 -0
  145. package/src/icons/menu/Highlight.tsx +13 -0
  146. package/src/icons/menu/HorizontalRule.tsx +7 -0
  147. package/src/icons/menu/Italic.tsx +10 -0
  148. package/src/icons/menu/Link.tsx +10 -0
  149. package/src/icons/menu/LinkOff.tsx +10 -0
  150. package/src/icons/menu/Math.tsx +10 -0
  151. package/src/icons/menu/MergeCells.tsx +10 -0
  152. package/src/icons/menu/MythReality.tsx +10 -0
  153. package/src/icons/menu/OrderedList.tsx +10 -0
  154. package/src/icons/menu/Prompt.tsx +18 -0
  155. package/src/icons/menu/Redo.tsx +14 -0
  156. package/src/icons/menu/SplitCells.tsx +7 -0
  157. package/src/icons/menu/Strikethrough.tsx +10 -0
  158. package/src/icons/menu/Subscript.tsx +10 -0
  159. package/src/icons/menu/Superscript.tsx +10 -0
  160. package/src/icons/menu/Table.tsx +14 -0
  161. package/src/icons/menu/TableColumnHeading.tsx +7 -0
  162. package/src/icons/menu/TableRowHeading.tsx +10 -0
  163. package/src/icons/menu/TextColor.tsx +13 -0
  164. package/src/icons/menu/Underline.tsx +10 -0
  165. package/src/icons/menu/Undo.tsx +14 -0
  166. package/src/icons/menu/Video.tsx +14 -0
  167. package/src/icons/menu/index.ts +49 -0
  168. package/src/render/index.ts +22 -0
  169. package/src/render/static-renderer-config.tsx +206 -0
  170. package/src/render/tiptap-interactivity-provider.tsx +78 -0
  171. package/src/render/tiptap-render.tsx +102 -0
  172. package/src/styles/components.css +10 -0
  173. package/src/styles/editor/base.css +39 -0
  174. package/src/styles/editor/callout-menu.css +81 -0
  175. package/src/styles/editor/controls.css +57 -0
  176. package/src/styles/editor/emoji-picker.css +113 -0
  177. package/src/styles/editor/index.css +9 -0
  178. package/src/styles/editor/menus.css +16 -0
  179. package/src/styles/editor/states.css +43 -0
  180. package/src/styles/editor/typography.css +165 -0
  181. package/src/styles/editor/utilities.css +71 -0
  182. package/src/styles/editor/variables.css +70 -0
  183. package/src/styles/extensions/callout.css +155 -0
  184. package/src/styles/extensions/code-block.css +109 -0
  185. package/src/styles/extensions/commands-list.css +83 -0
  186. package/src/styles/extensions/custom-quote.css +103 -0
  187. package/src/styles/extensions/details.css +87 -0
  188. package/src/styles/extensions/figure.css +66 -0
  189. package/src/styles/extensions/index.css +11 -0
  190. package/src/styles/extensions/math.css +32 -0
  191. package/src/styles/extensions/myth-reality.css +59 -0
  192. package/src/styles/extensions/prompt.css +75 -0
  193. package/src/styles/extensions/table.css +253 -0
  194. package/src/styles/extensions/video.css +53 -0
  195. package/src/styles/styles.css +3 -0
  196. package/src/translations/en.json +158 -0
  197. package/src/translations/ru.json +158 -0
  198. package/src/types/image-handler.ts +33 -0
  199. package/src/types/index.ts +1 -0
  200. package/src/utils/copy-to-clipboard.ts +23 -0
  201. package/src/utils/custom-emoji.ts +174 -0
  202. package/src/utils/detect-video-hosting.ts +106 -0
  203. package/src/utils/get-shared-extensions.ts +45 -0
  204. package/src/utils/get-translations.ts +25 -0
  205. package/src/utils/handle-exit-node.ts +61 -0
  206. package/src/utils/image-src-resolver.ts +74 -0
  207. package/src/utils/index.ts +2 -0
  208. package/src/utils/migrate-legacy-callouts.ts +59 -0
  209. package/src/utils/pluralize.ts +38 -0
@@ -0,0 +1,169 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ import { type Editor } from "@tiptap/react";
4
+
5
+ import { useTiptapTranslations } from "../context";
6
+
7
+ const CALLOUT_COLORS = [
8
+ { bgColor: "#F3F9F6", textColor: "#015629" },
9
+ { bgColor: "#FFFED8", textColor: "#7c5f00" },
10
+ { bgColor: "#FFF4E3", textColor: "#774900" },
11
+ { bgColor: "#FFF1F1", textColor: "#6B0000" },
12
+ { bgColor: "#FFEBFC", textColor: "#940388" },
13
+ { bgColor: "#F6F0FF", textColor: "#582f95" },
14
+ { bgColor: "#EBEFFF", textColor: "#002584" },
15
+ { bgColor: "#F7F7F8", textColor: "#222025" },
16
+ ];
17
+
18
+ interface Props {
19
+ isOpen: boolean;
20
+ onClose: () => void;
21
+ onColorSelect: (bgColor: string, textColor: string) => void;
22
+ editor: Editor;
23
+ nodePos: number | null;
24
+ }
25
+
26
+ export const CalloutMenu = ({ isOpen, onClose, onColorSelect, editor, nodePos }: Props) => {
27
+ const translations = useTiptapTranslations();
28
+ const menuRef = useRef<HTMLDivElement>(null);
29
+ const colorInputRef = useRef<HTMLInputElement>(null);
30
+
31
+ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
32
+
33
+ const getCurrentColors = () => {
34
+ if (nodePos !== null) {
35
+ const node = editor.view.state.doc.nodeAt(nodePos);
36
+ return {
37
+ bgColor: node?.attrs?.bgColor || "#F7F7F8",
38
+ textColor: node?.attrs?.textColor || "#222025",
39
+ };
40
+ }
41
+ return { bgColor: "#F7F7F8", textColor: "#222025" };
42
+ };
43
+
44
+ const [customColor, setCustomColor] = useState(getCurrentColors().bgColor);
45
+
46
+ useEffect(() => {
47
+ const handleClickOutside = (event: MouseEvent) => {
48
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
49
+ onClose();
50
+ }
51
+ };
52
+
53
+ if (isOpen) {
54
+ document.addEventListener("mousedown", handleClickOutside);
55
+ }
56
+
57
+ return () => {
58
+ document.removeEventListener("mousedown", handleClickOutside);
59
+ };
60
+ }, [isOpen, onClose]);
61
+
62
+ useEffect(() => {
63
+ if (isOpen) {
64
+ setIsColorPickerOpen(false);
65
+ setCustomColor(getCurrentColors().bgColor);
66
+ }
67
+ }, [isOpen, nodePos]);
68
+
69
+ if (!isOpen) return null;
70
+
71
+ return (
72
+ <div
73
+ ref={menuRef}
74
+ className="tiptap__drag-handle-color-menu"
75
+ style={{ opacity: isColorPickerOpen ? 0 : 1 }}
76
+ >
77
+ <div className="tiptap__color-menu-title">{translations.blocks.calloutSelectStyle}</div>
78
+ <div className="tiptap__color-menu-row">
79
+ {CALLOUT_COLORS.map(({ bgColor, textColor }) => (
80
+ <button
81
+ key={bgColor}
82
+ className="tiptap__color-option"
83
+ type="button"
84
+ onClick={() => onColorSelect(bgColor, textColor)}
85
+ >
86
+ <div
87
+ className="tiptap__color-swatch"
88
+ style={{
89
+ backgroundColor: bgColor,
90
+ color: textColor,
91
+ }}
92
+ >
93
+ А
94
+ </div>
95
+ </button>
96
+ ))}
97
+ </div>
98
+ <div className="tiptap__color-menu-custom">
99
+ <button
100
+ type="button"
101
+ className="tiptap__color-option"
102
+ onClick={() => {
103
+ colorInputRef.current?.click();
104
+ setIsColorPickerOpen(true);
105
+ }}
106
+ >
107
+ <div
108
+ className="tiptap__color-swatch"
109
+ style={{
110
+ backgroundColor: customColor,
111
+ color: (() => {
112
+ const r = parseInt(customColor.slice(1, 3), 16);
113
+ const g = parseInt(customColor.slice(3, 5), 16);
114
+ const b = parseInt(customColor.slice(5, 7), 16);
115
+ const brightness = (r * 299 + g * 587 + b * 114) / 1000;
116
+ return brightness > 128 ? "#000000" : "#FFFFFF";
117
+ })(),
118
+ }}
119
+ >
120
+ А
121
+ </div>
122
+ </button>
123
+ <span className="tiptap__color-menu-custom-label">
124
+ {translations.blocks.calloutCustomColor}
125
+ </span>
126
+ <input
127
+ ref={colorInputRef}
128
+ type="color"
129
+ value={customColor}
130
+ onInput={(e) => {
131
+ const bgColor = (e.target as HTMLInputElement).value;
132
+ setCustomColor(bgColor);
133
+
134
+ if (nodePos !== null) {
135
+ const node = editor.view.state.doc.nodeAt(nodePos);
136
+ if (node) {
137
+ const from = nodePos + 1;
138
+ const to = nodePos + node.nodeSize - 1;
139
+ const r = parseInt(bgColor.slice(1, 3), 16);
140
+ const g = parseInt(bgColor.slice(3, 5), 16);
141
+ const b = parseInt(bgColor.slice(5, 7), 16);
142
+ const brightness = (r * 299 + g * 587 + b * 114) / 1000;
143
+ const textColor = brightness > 128 ? "#000000" : "#FFFFFF";
144
+
145
+ editor
146
+ .chain()
147
+ .setTextSelection({ from, to })
148
+ .setColor(textColor)
149
+ .setNodeSelection(nodePos)
150
+ .updateAttributes("callout", { bgColor })
151
+ .run();
152
+ }
153
+ }
154
+ }}
155
+ onBlur={() => setIsColorPickerOpen(false)}
156
+ style={{
157
+ position: "absolute",
158
+ top: 0,
159
+ left: 0,
160
+ width: "1px",
161
+ height: "1px",
162
+ opacity: 0,
163
+ pointerEvents: "none",
164
+ }}
165
+ />
166
+ </div>
167
+ </div>
168
+ );
169
+ };
@@ -0,0 +1,36 @@
1
+ import { useEditorState, type Editor } from "@tiptap/react";
2
+
3
+ import { pluralize } from "../../utils";
4
+ import { useTiptapTranslations } from "../context";
5
+
6
+ interface Props {
7
+ editor: Editor;
8
+ }
9
+
10
+ export const CharacterCount = ({ editor }: Props) => {
11
+ const translations = useTiptapTranslations();
12
+ const { charactersCount, wordsCount } = useEditorState({
13
+ editor,
14
+ selector: (context) => ({
15
+ charactersCount: context.editor.storage.characterCount.characters(),
16
+ wordsCount: context.editor.storage.characterCount.words(),
17
+ }),
18
+ });
19
+
20
+ return (
21
+ <div className="tiptap__character-count">
22
+ <span className="tiptap__character-count-item">
23
+ {charactersCount}{" "}
24
+ {pluralize(
25
+ charactersCount,
26
+ translations.characterCount.characters as [string, string, string]
27
+ )}
28
+ </span>
29
+ <span className="tiptap__character-count-separator">/</span>
30
+ <span className="tiptap__character-count-item">
31
+ {wordsCount}{" "}
32
+ {pluralize(wordsCount, translations.characterCount.words as [string, string, string])}
33
+ </span>
34
+ </div>
35
+ );
36
+ };
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useEffect, useImperativeHandle, useRef, useState, forwardRef } from "react";
4
+
5
+ import type { Editor } from "@tiptap/core";
6
+
7
+ import { useDialogCommand } from "../context/dialog-command";
8
+ import { useTiptapTranslations } from "../context";
9
+
10
+ export interface CommandRange {
11
+ from: number;
12
+ to: number;
13
+ }
14
+
15
+ export interface CommandItem {
16
+ title: string;
17
+ icon?: React.ReactNode;
18
+ command: (props: { editor: Editor; range: CommandRange }) => void;
19
+ dialogCommand?: "image" | "video" | "link" | "katex";
20
+ }
21
+
22
+ interface CommandsListProps {
23
+ items: CommandItem[];
24
+ command: (item: CommandItem) => void;
25
+ }
26
+
27
+ export const CommandsList = forwardRef<
28
+ { onKeyDown: (props: { event: KeyboardEvent }) => boolean },
29
+ CommandsListProps
30
+ >(({ items, command }, ref) => {
31
+ const [selectedIndex, setSelectedIndex] = useState(0);
32
+ const listRef = useRef<HTMLDivElement>(null);
33
+ const { openDialogCommand } = useDialogCommand();
34
+ const translations = useTiptapTranslations();
35
+
36
+ useEffect(() => {
37
+ setSelectedIndex(0);
38
+ }, [items.length]);
39
+
40
+ useEffect(() => {
41
+ // Scroll to active item when selection changes
42
+ const activeButton = listRef.current?.querySelector(".commands-list-item.active");
43
+ if (activeButton) {
44
+ activeButton.scrollIntoView({
45
+ behavior: "smooth",
46
+ block: "nearest",
47
+ });
48
+ }
49
+ }, [selectedIndex]);
50
+
51
+ useImperativeHandle(
52
+ ref,
53
+ () => ({
54
+ onKeyDown: ({ event }: { event: KeyboardEvent }) => {
55
+ if (event.key === "ArrowUp") {
56
+ setSelectedIndex((i) => (i - 1 + items.length) % items.length);
57
+ return true;
58
+ }
59
+
60
+ if (event.key === "ArrowDown") {
61
+ setSelectedIndex((i) => (i + 1) % items.length);
62
+ return true;
63
+ }
64
+
65
+ if (event.key === "Enter") {
66
+ selectItem(selectedIndex);
67
+ return true;
68
+ }
69
+
70
+ return false;
71
+ },
72
+ }),
73
+ [items, selectedIndex]
74
+ );
75
+
76
+ const selectItem = (index: number) => {
77
+ const item = items[index];
78
+ if (item) {
79
+ if (item.dialogCommand) {
80
+ openDialogCommand(item.dialogCommand);
81
+ } else {
82
+ command(item);
83
+ }
84
+ }
85
+ };
86
+
87
+ if (items.length === 0) {
88
+ return (
89
+ <div className="commands-list-container">
90
+ <div className="commands-list-empty">
91
+ <p>{translations.editor.noCommandsFound}</p>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ return (
98
+ <div
99
+ ref={listRef}
100
+ className="commands-list-container"
101
+ >
102
+ {items.map((item, index) => (
103
+ <button
104
+ key={index}
105
+ className={`commands-list-item ${index === selectedIndex ? " active" : ""}`}
106
+ onClick={() => selectItem(index)}
107
+ onMouseEnter={() => setSelectedIndex(index)}
108
+ >
109
+ {item.icon && <span className="commands-list-icon">{item.icon}</span>}
110
+ <span className="commands-list-title">{item.title}</span>
111
+ </button>
112
+ ))}
113
+ </div>
114
+ );
115
+ });
116
+
117
+ CommandsList.displayName = "CommandsList";
@@ -0,0 +1,98 @@
1
+ import { useRef, useState } from "react";
2
+
3
+ import { DragHandle as TiptapDragHandle } from "@tiptap/extension-drag-handle-react";
4
+ import { type Editor } from "@tiptap/react";
5
+
6
+ import { TiptapDragIcon } from "../../icons";
7
+ import { CalloutMenu } from "./callout-menu";
8
+
9
+ interface Props {
10
+ editor: Editor;
11
+ }
12
+
13
+ export const DragHandle = ({ editor }: Props) => {
14
+ const currentNodePosRef = useRef<number | null>(null);
15
+ const [showCalloutMenu, setShowCalloutMenu] = useState(false);
16
+ const [isCalloutNode, setIsCalloutNode] = useState(false);
17
+
18
+ const handleNodeChange = ({ pos }: { pos?: number }) => {
19
+ if (pos !== undefined && pos >= 0) {
20
+ const prevPos = currentNodePosRef.current;
21
+ currentNodePosRef.current = pos;
22
+
23
+ try {
24
+ const node = editor.view.state.doc.nodeAt(pos);
25
+ const isCallout = node?.type.name === "callout";
26
+ setIsCalloutNode(isCallout);
27
+
28
+ if (prevPos !== null && prevPos !== pos) {
29
+ setShowCalloutMenu(false);
30
+ }
31
+ } catch (error) {
32
+ setIsCalloutNode(false);
33
+ }
34
+ } else {
35
+ setShowCalloutMenu(false);
36
+ }
37
+ };
38
+
39
+ const handleClick = () => {
40
+ if (currentNodePosRef.current !== null) {
41
+ if (isCalloutNode) {
42
+ setShowCalloutMenu(!showCalloutMenu);
43
+ }
44
+ editor.commands.setNodeSelection(currentNodePosRef.current);
45
+ editor.view.focus();
46
+ }
47
+ };
48
+
49
+ const handleColorSelect = (bgColor: string, textColor: string) => {
50
+ if (currentNodePosRef.current !== null) {
51
+ const pos = currentNodePosRef.current;
52
+ const node = editor.view.state.doc.nodeAt(pos);
53
+
54
+ if (node) {
55
+ const from = pos + 1;
56
+ const to = pos + node.nodeSize - 1;
57
+
58
+ editor
59
+ .chain()
60
+ .setTextSelection({ from, to })
61
+ .setColor(textColor)
62
+ .setNodeSelection(pos)
63
+ .updateAttributes("callout", { bgColor })
64
+ .run();
65
+ }
66
+
67
+ setShowCalloutMenu(false);
68
+ }
69
+ };
70
+
71
+ return (
72
+ <TiptapDragHandle
73
+ editor={editor}
74
+ className="tiptap__drag-handle"
75
+ onNodeChange={handleNodeChange}
76
+ >
77
+ <div style={{ position: "relative" }}>
78
+ <button
79
+ className="tiptap__drag-handle-button"
80
+ type="button"
81
+ onClick={handleClick}
82
+ >
83
+ <TiptapDragIcon />
84
+ </button>
85
+
86
+ {isCalloutNode && (
87
+ <CalloutMenu
88
+ isOpen={showCalloutMenu}
89
+ onClose={() => setShowCalloutMenu(false)}
90
+ onColorSelect={handleColorSelect}
91
+ editor={editor}
92
+ nodePos={currentNodePosRef.current}
93
+ />
94
+ )}
95
+ </div>
96
+ </TiptapDragHandle>
97
+ );
98
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef } from "react";
2
+ import EmojiPicker from "emoji-picker-react";
3
+ import {
4
+ calloutCustomEmojis,
5
+ favoriteCustomEmojis,
6
+ favoriteEmojiIdMap,
7
+ } from "../../utils/custom-emoji";
8
+ import { useTiptapTranslations } from "../context/translations-context";
9
+
10
+ interface Props {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ onEmojiSelect: (emoji: string) => void;
14
+ }
15
+
16
+ export const EmojiPickerComponent = ({ isOpen, onClose, onEmojiSelect }: Props) => {
17
+ const pickerRef = useRef<HTMLDivElement>(null);
18
+ const translations = useTiptapTranslations();
19
+
20
+ useEffect(() => {
21
+ const handleClickOutside = (event: MouseEvent) => {
22
+ if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
23
+ onClose();
24
+ }
25
+ };
26
+
27
+ if (isOpen) {
28
+ document.addEventListener("mousedown", handleClickOutside);
29
+ }
30
+
31
+ return () => {
32
+ document.removeEventListener("mousedown", handleClickOutside);
33
+ };
34
+ }, [isOpen, onClose]);
35
+
36
+ if (!isOpen) return null;
37
+
38
+ return (
39
+ <div
40
+ ref={pickerRef}
41
+ className="tiptap__emoji-picker-wrapper"
42
+ >
43
+ <EmojiPicker
44
+ skinTonesDisabled={true}
45
+ onEmojiClick={(emojiData) => {
46
+ let emojiToUse: string;
47
+ if (emojiData.isCustom) {
48
+ const emojiId = emojiData.unified;
49
+ emojiToUse = favoriteEmojiIdMap[emojiId] || emojiId;
50
+ } else {
51
+ emojiToUse = emojiData.emoji;
52
+ }
53
+ onEmojiSelect(emojiToUse);
54
+ onClose();
55
+ }}
56
+ previewConfig={{ showPreview: false }}
57
+ searchPlaceHolder={translations.editor.emojiSearchPlaceholder}
58
+ customEmojis={[...favoriteCustomEmojis, ...calloutCustomEmojis]}
59
+ />
60
+ </div>
61
+ );
62
+ };
@@ -0,0 +1,5 @@
1
+ export { BubbleMenuContent } from './bubble-menu-content';
2
+ export { DragHandle } from './drag-handle';
3
+ export { CommandsList } from './commands-list';
4
+ export { CharacterCount } from './character-count';
5
+ export { TableMenu } from './table-menu';