@meta-1/editor 0.0.27

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 (130) hide show
  1. package/README.md +458 -0
  2. package/package.json +100 -0
  3. package/src/editor/constants.tsx +66 -0
  4. package/src/editor/container.css +46 -0
  5. package/src/editor/control/character-count/index.tsx +39 -0
  6. package/src/editor/control/drag-handle/index.tsx +85 -0
  7. package/src/editor/control/drag-handle/use.content.actions.ts +71 -0
  8. package/src/editor/control/drag-handle/use.data.ts +29 -0
  9. package/src/editor/control/drag-handle/use.handle.id.ts +6 -0
  10. package/src/editor/control/index.tsx +35 -0
  11. package/src/editor/editor.css +626 -0
  12. package/src/editor/extension/block-quote-figure/BlockquoteFigure.ts +73 -0
  13. package/src/editor/extension/block-quote-figure/Quote/Quote.ts +31 -0
  14. package/src/editor/extension/block-quote-figure/Quote/index.ts +1 -0
  15. package/src/editor/extension/block-quote-figure/QuoteCaption/QuoteCaption.ts +54 -0
  16. package/src/editor/extension/block-quote-figure/QuoteCaption/index.ts +1 -0
  17. package/src/editor/extension/block-quote-figure/index.ts +1 -0
  18. package/src/editor/extension/document/index.ts +5 -0
  19. package/src/editor/extension/figcaption/Figcaption.ts +90 -0
  20. package/src/editor/extension/figcaption/index.ts +1 -0
  21. package/src/editor/extension/figure/Figure.ts +62 -0
  22. package/src/editor/extension/figure/index.ts +1 -0
  23. package/src/editor/extension/font-size/FontSize.ts +64 -0
  24. package/src/editor/extension/font-size/index.ts +1 -0
  25. package/src/editor/extension/global-drag-handle/clipboard-serializer.ts +28 -0
  26. package/src/editor/extension/global-drag-handle/index.ts +377 -0
  27. package/src/editor/extension/heading/index.ts +13 -0
  28. package/src/editor/extension/horizontal-rule/HorizontalRule.ts +10 -0
  29. package/src/editor/extension/horizontal-rule/index.ts +1 -0
  30. package/src/editor/extension/image/index.ts +5 -0
  31. package/src/editor/extension/image-block/ImageBlock.ts +103 -0
  32. package/src/editor/extension/image-block/components/ImageBlockMenu.tsx +100 -0
  33. package/src/editor/extension/image-block/components/ImageBlockView.tsx +47 -0
  34. package/src/editor/extension/image-block/components/ImageBlockWidth.tsx +40 -0
  35. package/src/editor/extension/image-block/index.ts +1 -0
  36. package/src/editor/extension/image-upload/ImageUpload.ts +58 -0
  37. package/src/editor/extension/image-upload/index.ts +1 -0
  38. package/src/editor/extension/image-upload/view/ImageUpload.tsx +27 -0
  39. package/src/editor/extension/image-upload/view/ImageUploader.tsx +64 -0
  40. package/src/editor/extension/image-upload/view/hooks.ts +109 -0
  41. package/src/editor/extension/image-upload/view/index.tsx +1 -0
  42. package/src/editor/extension/index.ts +30 -0
  43. package/src/editor/extension/link/Link.ts +39 -0
  44. package/src/editor/extension/link/index.ts +1 -0
  45. package/src/editor/extension/multi-column/Column.ts +33 -0
  46. package/src/editor/extension/multi-column/Columns.ts +65 -0
  47. package/src/editor/extension/multi-column/index.ts +2 -0
  48. package/src/editor/extension/multi-column/menus/ColumnsMenu.tsx +82 -0
  49. package/src/editor/extension/multi-column/menus/index.ts +1 -0
  50. package/src/editor/extension/selection/Selection.ts +36 -0
  51. package/src/editor/extension/selection/index.ts +1 -0
  52. package/src/editor/extension/slash-command/MenuList.tsx +145 -0
  53. package/src/editor/extension/slash-command/groups.ts +153 -0
  54. package/src/editor/extension/slash-command/index.ts +277 -0
  55. package/src/editor/extension/slash-command/types.ts +25 -0
  56. package/src/editor/extension/table/Cell.ts +126 -0
  57. package/src/editor/extension/table/Header.ts +89 -0
  58. package/src/editor/extension/table/Row.ts +8 -0
  59. package/src/editor/extension/table/Table.ts +9 -0
  60. package/src/editor/extension/table/index.ts +4 -0
  61. package/src/editor/extension/table/menus/TableColumn/index.tsx +73 -0
  62. package/src/editor/extension/table/menus/TableColumn/utils.ts +38 -0
  63. package/src/editor/extension/table/menus/TableRow/index.tsx +74 -0
  64. package/src/editor/extension/table/menus/TableRow/utils.ts +38 -0
  65. package/src/editor/extension/table/menus/index.tsx +2 -0
  66. package/src/editor/extension/table/utils.ts +258 -0
  67. package/src/editor/extension/task-item/index.ts +1 -0
  68. package/src/editor/extension/task-item/task-item.ts +225 -0
  69. package/src/editor/extension/task-list/index.ts +1 -0
  70. package/src/editor/extension/task-list/task-list.ts +81 -0
  71. package/src/editor/extension/trailing-node/index.ts +1 -0
  72. package/src/editor/extension/trailing-node/trailing-node.ts +70 -0
  73. package/src/editor/extension/unique-id/index.ts +1 -0
  74. package/src/editor/extension/unique-id/uniqueId.ts +123 -0
  75. package/src/editor/hooks.ts +264 -0
  76. package/src/editor/index.tsx +53 -0
  77. package/src/editor/menus/LinkMenu/LinkMenu.tsx +75 -0
  78. package/src/editor/menus/LinkMenu/index.tsx +1 -0
  79. package/src/editor/menus/TextMenu/TextMenu.tsx +193 -0
  80. package/src/editor/menus/TextMenu/components/AIDropdown.tsx +140 -0
  81. package/src/editor/menus/TextMenu/components/ContentTypePicker.tsx +76 -0
  82. package/src/editor/menus/TextMenu/components/EditLinkPopover.tsx +25 -0
  83. package/src/editor/menus/TextMenu/components/FontFamilyPicker.tsx +84 -0
  84. package/src/editor/menus/TextMenu/components/FontSizePicker.tsx +56 -0
  85. package/src/editor/menus/TextMenu/hooks/useTextmenuCommands.ts +96 -0
  86. package/src/editor/menus/TextMenu/hooks/useTextmenuContentTypes.ts +86 -0
  87. package/src/editor/menus/TextMenu/hooks/useTextmenuStates.ts +50 -0
  88. package/src/editor/menus/TextMenu/index.tsx +2 -0
  89. package/src/editor/menus/types.ts +21 -0
  90. package/src/editor/panels/Colorpicker/ColorButton.tsx +35 -0
  91. package/src/editor/panels/Colorpicker/Colorpicker.tsx +67 -0
  92. package/src/editor/panels/Colorpicker/index.tsx +2 -0
  93. package/src/editor/panels/LinkEditorPanel/LinkEditorPanel.tsx +76 -0
  94. package/src/editor/panels/LinkEditorPanel/index.tsx +1 -0
  95. package/src/editor/panels/LinkPreviewPanel/LinkPreviewPanel.tsx +32 -0
  96. package/src/editor/panels/LinkPreviewPanel/index.tsx +1 -0
  97. package/src/editor/panels/index.tsx +3 -0
  98. package/src/editor/types.tsx +38 -0
  99. package/src/editor/ui/Button/Button.tsx +70 -0
  100. package/src/editor/ui/Button/index.tsx +2 -0
  101. package/src/editor/ui/Dropdown/Dropdown.tsx +39 -0
  102. package/src/editor/ui/Dropdown/index.tsx +1 -0
  103. package/src/editor/ui/Icon.tsx +21 -0
  104. package/src/editor/ui/Loader/Loader.tsx +39 -0
  105. package/src/editor/ui/Loader/index.ts +1 -0
  106. package/src/editor/ui/Loader/types.ts +7 -0
  107. package/src/editor/ui/Panel/index.tsx +109 -0
  108. package/src/editor/ui/PopoverMenu.tsx +127 -0
  109. package/src/editor/ui/Spinner/Spinner.tsx +10 -0
  110. package/src/editor/ui/Spinner/index.tsx +1 -0
  111. package/src/editor/ui/Surface.tsx +27 -0
  112. package/src/editor/ui/Textarea/Textarea.tsx +20 -0
  113. package/src/editor/ui/Textarea/index.tsx +1 -0
  114. package/src/editor/ui/Toggle/Toggle.tsx +39 -0
  115. package/src/editor/ui/Toggle/index.tsx +1 -0
  116. package/src/editor/ui/Toolbar.tsx +107 -0
  117. package/src/editor/ui/Tooltip/index.tsx +77 -0
  118. package/src/editor/ui/Tooltip/types.ts +17 -0
  119. package/src/editor/utils/cssVar.ts +14 -0
  120. package/src/editor/utils/getRenderContainer.ts +39 -0
  121. package/src/editor/utils/index.ts +16 -0
  122. package/src/editor/utils/isCustomNodeSelected.ts +47 -0
  123. package/src/editor/utils/isTextSelected.ts +25 -0
  124. package/src/editor/utils/locale.ts +5 -0
  125. package/src/editor/viewer/index.tsx +26 -0
  126. package/src/globals.css +1 -0
  127. package/src/index.ts +7 -0
  128. package/src/locales/en-us.ts +133 -0
  129. package/src/locales/zh-cn.ts +133 -0
  130. package/src/locales/zh-tw.ts +133 -0
@@ -0,0 +1,54 @@
1
+ import { Node } from "@tiptap/core";
2
+
3
+ export const QuoteCaption = Node.create({
4
+ name: "quoteCaption",
5
+
6
+ group: "block",
7
+
8
+ content: "text*",
9
+
10
+ defining: true,
11
+
12
+ isolating: true,
13
+
14
+ parseHTML() {
15
+ return [
16
+ {
17
+ tag: "figcaption",
18
+ },
19
+ ];
20
+ },
21
+
22
+ renderHTML({ HTMLAttributes }) {
23
+ return ["figcaption", HTMLAttributes, 0];
24
+ },
25
+
26
+ addKeyboardShortcuts() {
27
+ return {
28
+ // On Enter at the end of line, create new paragraph and focus
29
+ Enter: ({ editor }) => {
30
+ const {
31
+ state: {
32
+ selection: { $from, empty },
33
+ },
34
+ } = editor;
35
+
36
+ if (!empty || $from.parent.type !== this.type) {
37
+ return false;
38
+ }
39
+
40
+ const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
41
+
42
+ if (!isAtEnd) {
43
+ return false;
44
+ }
45
+
46
+ const pos = editor.state.selection.$from.end();
47
+
48
+ return editor.chain().focus(pos).insertContentAt(pos, { type: "paragraph" }).run();
49
+ },
50
+ };
51
+ },
52
+ });
53
+
54
+ export default QuoteCaption;
@@ -0,0 +1 @@
1
+ export { QuoteCaption } from "./QuoteCaption";
@@ -0,0 +1 @@
1
+ export { BlockquoteFigure } from "./BlockquoteFigure";
@@ -0,0 +1,5 @@
1
+ import { Document as TiptapDocument } from "@tiptap/extension-document";
2
+
3
+ export const Document = TiptapDocument.extend({
4
+ content: "(block|columns)+",
5
+ });
@@ -0,0 +1,90 @@
1
+ import { mergeAttributes, Node } from "@tiptap/core";
2
+
3
+ import { Image } from "../image";
4
+
5
+ export const Figcaption = Node.create({
6
+ name: "figcaption",
7
+
8
+ addOptions() {
9
+ return {
10
+ HTMLAttributes: {},
11
+ };
12
+ },
13
+
14
+ content: "inline*",
15
+
16
+ selectable: false,
17
+
18
+ draggable: false,
19
+
20
+ marks: "link",
21
+
22
+ parseHTML() {
23
+ return [
24
+ {
25
+ tag: "figcaption",
26
+ },
27
+ ];
28
+ },
29
+
30
+ addKeyboardShortcuts() {
31
+ return {
32
+ // On Enter at the end of line, create new paragraph and focus
33
+ Enter: ({ editor }) => {
34
+ const {
35
+ state: {
36
+ selection: { $from, empty },
37
+ },
38
+ } = editor;
39
+
40
+ if (!empty || $from.parent.type !== this.type) {
41
+ return false;
42
+ }
43
+
44
+ const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
45
+
46
+ if (!isAtEnd) {
47
+ return false;
48
+ }
49
+
50
+ const pos = editor.state.selection.$from.end();
51
+
52
+ return editor.chain().focus(pos).insertContentAt(pos, { type: "paragraph" }).run();
53
+ },
54
+
55
+ // On Backspace at the beginning of line,
56
+ // dont delete content of image before
57
+ Backspace: ({ editor }) => {
58
+ const {
59
+ state: {
60
+ selection: { $from, empty },
61
+ },
62
+ } = editor;
63
+
64
+ if (!empty || $from.parent.type !== this.type) {
65
+ return false;
66
+ }
67
+
68
+ const isAtStart = $from.parentOffset === 0;
69
+
70
+ if (!isAtStart) {
71
+ return false;
72
+ }
73
+
74
+ // if the node before is of type image, don't do anything
75
+ const nodeBefore = editor.state.doc.nodeAt($from.pos - 2);
76
+ if (nodeBefore?.type.name === Image.name) {
77
+ return true;
78
+ }
79
+
80
+ return false;
81
+ },
82
+ };
83
+ },
84
+
85
+ renderHTML({ HTMLAttributes }) {
86
+ return ["figcaption", mergeAttributes(HTMLAttributes), 0];
87
+ },
88
+ });
89
+
90
+ export default Figcaption;
@@ -0,0 +1 @@
1
+ export { Figcaption } from "./Figcaption";
@@ -0,0 +1,62 @@
1
+ import { mergeAttributes, Node } from "@tiptap/core";
2
+ import { Plugin } from "@tiptap/pm/state";
3
+
4
+ export const Figure = Node.create({
5
+ name: "figure",
6
+
7
+ addOptions() {
8
+ return {
9
+ HTMLAttributes: {},
10
+ };
11
+ },
12
+
13
+ group: "block",
14
+
15
+ content: "block figcaption",
16
+
17
+ draggable: true,
18
+
19
+ defining: true,
20
+
21
+ selectable: true,
22
+
23
+ parseHTML() {
24
+ return [
25
+ {
26
+ tag: `figure[data-type="${this.name}"]`,
27
+ },
28
+ ];
29
+ },
30
+
31
+ renderHTML({ HTMLAttributes }) {
32
+ return ["figure", mergeAttributes(HTMLAttributes, { "data-type": this.name }), 0];
33
+ },
34
+
35
+ addProseMirrorPlugins() {
36
+ return [
37
+ new Plugin({
38
+ props: {
39
+ handleDOMEvents: {
40
+ // Prevent dragging child nodes from figure
41
+ dragstart: (view, event) => {
42
+ if (!event.target) {
43
+ return false;
44
+ }
45
+
46
+ const pos = view.posAtDOM(event.target as HTMLElement, 0);
47
+ const $pos = view.state.doc.resolve(pos);
48
+
49
+ if ($pos.parent.type.name === this.type.name) {
50
+ event.preventDefault();
51
+ }
52
+
53
+ return false;
54
+ },
55
+ },
56
+ },
57
+ }),
58
+ ];
59
+ },
60
+ });
61
+
62
+ export default Figure;
@@ -0,0 +1 @@
1
+ export { Figure } from "./Figure";
@@ -0,0 +1,64 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import "@tiptap/extension-text-style";
3
+
4
+ declare module "@tiptap/core" {
5
+ interface Commands<ReturnType> {
6
+ fontSize: {
7
+ setFontSize: (size: string) => ReturnType;
8
+ unsetFontSize: () => ReturnType;
9
+ };
10
+ }
11
+ }
12
+
13
+ export const FontSize = Extension.create({
14
+ name: "fontSize",
15
+
16
+ addOptions() {
17
+ return {
18
+ types: ["textStyle"],
19
+ };
20
+ },
21
+
22
+ addGlobalAttributes() {
23
+ return [
24
+ {
25
+ types: ["paragraph"],
26
+ attributes: {
27
+ class: {},
28
+ },
29
+ },
30
+ {
31
+ types: this.options.types,
32
+ attributes: {
33
+ fontSize: {
34
+ parseHTML: (element) => element.style.fontSize.replace(/['"]+/g, ""),
35
+ renderHTML: (attributes) => {
36
+ if (!attributes.fontSize) {
37
+ return {};
38
+ }
39
+
40
+ return {
41
+ style: `font-size: ${attributes.fontSize}`,
42
+ };
43
+ },
44
+ },
45
+ },
46
+ },
47
+ ];
48
+ },
49
+
50
+ addCommands() {
51
+ return {
52
+ setFontSize:
53
+ (fontSize: string) =>
54
+ ({ chain }) =>
55
+ chain().setMark("textStyle", { fontSize }).run(),
56
+ unsetFontSize:
57
+ () =>
58
+ ({ chain }) =>
59
+ chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run(),
60
+ };
61
+ },
62
+ });
63
+
64
+ export default FontSize;
@@ -0,0 +1 @@
1
+ export { FontSize } from "./FontSize";
@@ -0,0 +1,28 @@
1
+ import type { Slice } from "@tiptap/pm/model";
2
+ import type { EditorView } from "@tiptap/pm/view";
3
+ import * as pmView from "@tiptap/pm/view";
4
+
5
+ function getPmView() {
6
+ try {
7
+ return pmView;
8
+ } catch (_error) {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ export function serializeForClipboard(view: EditorView, slice: Slice) {
14
+ // Newer Tiptap/ProseMirror
15
+ if (view && typeof view.serializeForClipboard === "function") {
16
+ return view.serializeForClipboard(slice);
17
+ }
18
+
19
+ // Older version fallback
20
+ const proseMirrorView = getPmView();
21
+ // @ts-expect-error
22
+ if (proseMirrorView && typeof proseMirrorView?.__serializeForClipboard === "function") {
23
+ // @ts-expect-error
24
+ return proseMirrorView.__serializeForClipboard(view, slice);
25
+ }
26
+
27
+ throw new Error("No supported clipboard serialization method found.");
28
+ }
@@ -0,0 +1,377 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Fragment, type Node, type ResolvedPos, Slice } from "@tiptap/pm/model";
3
+ import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
4
+ import type { EditorView } from "@tiptap/pm/view";
5
+
6
+ import { serializeForClipboard } from "./clipboard-serializer";
7
+
8
+ export interface GlobalDragHandleOptions {
9
+ /**
10
+ * The width of the drag handle
11
+ */
12
+ dragHandleWidth: number;
13
+
14
+ /**
15
+ * The treshold for scrolling
16
+ */
17
+ scrollTreshold: number;
18
+
19
+ /*
20
+ * The css selector to query for the drag handle. (eg: '.custom-handle').
21
+ * If handle element is found, that element will be used as drag handle. If not, a default handle will be created
22
+ */
23
+ dragHandleSelector?: string;
24
+
25
+ onNodeChange?: (node: ResolvedPos, nodePos: number) => void;
26
+ onShow?: () => void;
27
+ onHide?: () => void;
28
+ uniqueId?: string;
29
+ }
30
+
31
+ function absoluteRect(node: Element) {
32
+ const data = node.getBoundingClientRect();
33
+ const modal = node.closest('[role="dialog"]');
34
+
35
+ if (modal && window.getComputedStyle(modal).transform !== "none") {
36
+ const modalRect = modal.getBoundingClientRect();
37
+
38
+ return {
39
+ top: data.top - modalRect.top,
40
+ left: data.left - modalRect.left,
41
+ width: data.width,
42
+ };
43
+ }
44
+ return {
45
+ top: data.top,
46
+ left: data.left,
47
+ width: data.width,
48
+ };
49
+ }
50
+
51
+ function nodeDOMAtCoords(coords: { x: number; y: number }) {
52
+ return document
53
+ .elementsFromPoint(coords.x, coords.y)
54
+ .find(
55
+ (elem: Element) =>
56
+ elem.parentElement?.matches?.(".ProseMirror") ||
57
+ elem.matches(
58
+ ["li", "p:not(:first-child)", "pre", "blockquote", "h1, h2, h3, h4, h5, h6", "div.react-renderer"].join(", "),
59
+ ),
60
+ );
61
+ }
62
+
63
+ function nodePosAtDOM(node: Element, view: EditorView, options: GlobalDragHandleOptions) {
64
+ const boundingRect = node.getBoundingClientRect();
65
+
66
+ return view.posAtCoords({
67
+ left: boundingRect.left + 50 + options.dragHandleWidth,
68
+ top: boundingRect.top + 1,
69
+ })?.inside;
70
+ }
71
+
72
+ function calcNodePos(pos: number, view: EditorView) {
73
+ const $pos = view.state.doc.resolve(pos);
74
+ if ($pos.depth > 1) return $pos.before($pos.depth);
75
+ return pos;
76
+ }
77
+
78
+ function rootNodePos(pos: number, view: EditorView) {
79
+ const $pos = view.state.doc.resolve(pos);
80
+ if ($pos.depth < 1) return pos;
81
+ return $pos.before(1) + 1;
82
+ }
83
+
84
+ const getRootNode = (pos: number, view: EditorView, node: Element, uniqueId: string) => {
85
+ const root = view.state.doc.resolve(pos);
86
+ if (root.node().type.name === "doc") {
87
+ let id = (node as HTMLElement).dataset[uniqueId];
88
+ if (node.classList.contains("react-renderer")) {
89
+ const item = node.firstElementChild as HTMLElement;
90
+ id = item?.dataset[uniqueId];
91
+ }
92
+ if (id) {
93
+ let node: Node | null = null;
94
+ root.node().content.descendants((n) => {
95
+ if (n.attrs[uniqueId] === id) {
96
+ node = n;
97
+ }
98
+ });
99
+ return node ? (node as Node).resolve(0) : root;
100
+ }
101
+ }
102
+ return root;
103
+ };
104
+
105
+ export function DragHandlePlugin(options: GlobalDragHandleOptions & { pluginKey: string }) {
106
+ let listType = "";
107
+
108
+ function handleDragStart(event: DragEvent, view: EditorView) {
109
+ // view.focus();
110
+
111
+ if (!event.dataTransfer) return;
112
+
113
+ const node = nodeDOMAtCoords({
114
+ x: event.clientX + 50 + options.dragHandleWidth,
115
+ y: event.clientY,
116
+ });
117
+
118
+ if (!(node instanceof Element)) return;
119
+
120
+ let draggedNodePos = nodePosAtDOM(node, view, options);
121
+ if (draggedNodePos == null || draggedNodePos < 0) return;
122
+ const dragNode = view.state.doc.resolve(draggedNodePos);
123
+ if (
124
+ ["tableCell", "tableRow"].includes(dragNode.node().type.name) ||
125
+ node.matches("div.tableWrapper, li, blockquote")
126
+ ) {
127
+ draggedNodePos = rootNodePos(draggedNodePos, view) - 1;
128
+ } else {
129
+ draggedNodePos = calcNodePos(draggedNodePos, view);
130
+ }
131
+
132
+ const { from, to } = view.state.selection;
133
+ const diff = from - to;
134
+
135
+ const fromSelectionPos = calcNodePos(from, view);
136
+ let differentNodeSelected = false;
137
+
138
+ const nodePos = view.state.doc.resolve(fromSelectionPos);
139
+
140
+ // Check if nodePos points to the top level node
141
+ if (nodePos.node().type.name === "doc") differentNodeSelected = true;
142
+ else {
143
+ const nodeSelection = NodeSelection.create(view.state.doc, nodePos.before());
144
+ // Check if the node where the drag event started is part of the current selection
145
+ differentNodeSelected = !(
146
+ draggedNodePos + 1 >= nodeSelection.$from.pos && draggedNodePos <= nodeSelection.$to.pos
147
+ );
148
+ }
149
+
150
+ if (!differentNodeSelected && diff !== 0 && !(view.state.selection instanceof NodeSelection)) {
151
+ const endSelection = NodeSelection.create(view.state.doc, to - 1);
152
+ const multiNodeSelection = TextSelection.create(view.state.doc, draggedNodePos, endSelection.$to.pos);
153
+ view.dispatch(view.state.tr.setSelection(multiNodeSelection));
154
+ } else {
155
+ const nodeSelection = NodeSelection.create(view.state.doc, draggedNodePos);
156
+ view.dispatch(view.state.tr.setSelection(nodeSelection));
157
+ }
158
+
159
+ // If the selected node is a list item, we need to save the type of the wrapping list e.g. OL or UL
160
+ if (view.state.selection instanceof NodeSelection && view.state.selection.node.type.name === "listItem") {
161
+ listType = node.parentElement?.tagName || "";
162
+ }
163
+
164
+ const slice = view.state.selection.content();
165
+ const { dom, text } = serializeForClipboard(view, slice);
166
+
167
+ event.dataTransfer.clearData();
168
+ event.dataTransfer.setData("text/html", dom.innerHTML);
169
+ event.dataTransfer.setData("text/plain", text);
170
+ event.dataTransfer.effectAllowed = "copyMove";
171
+
172
+ event.dataTransfer.setDragImage(node, 0, 0);
173
+
174
+ view.dragging = { slice, move: event.ctrlKey };
175
+ }
176
+
177
+ let dragHandleElement: HTMLElement | null = null;
178
+
179
+ function hideDragHandle() {
180
+ if (dragHandleElement) {
181
+ dragHandleElement.classList.add("hidden");
182
+ options.onHide?.();
183
+ }
184
+ }
185
+
186
+ function showDragHandle() {
187
+ if (dragHandleElement) {
188
+ dragHandleElement.classList.remove("hidden");
189
+ options.onShow?.();
190
+ }
191
+ }
192
+
193
+ return new Plugin({
194
+ key: new PluginKey(options.pluginKey),
195
+ view: (view) => {
196
+ const handleBySelector = options.dragHandleSelector
197
+ ? document.querySelector<HTMLElement>(options.dragHandleSelector)
198
+ : null;
199
+ dragHandleElement = handleBySelector ?? document.createElement("div");
200
+ dragHandleElement.draggable = true;
201
+ dragHandleElement.dataset.dragHandle = "";
202
+ dragHandleElement.classList.add("drag-handle");
203
+
204
+ function onDragHandleDragStart(e: DragEvent) {
205
+ view.dom.classList.add("dragging");
206
+ handleDragStart(e, view);
207
+ }
208
+
209
+ dragHandleElement.addEventListener("dragstart", onDragHandleDragStart);
210
+
211
+ function onDragHandleDragEnd() {
212
+ view.dom.classList.remove("dragging");
213
+ }
214
+
215
+ dragHandleElement.addEventListener("dragend", onDragHandleDragEnd);
216
+
217
+ function onDragHandleDrag(e: DragEvent) {
218
+ hideDragHandle();
219
+ const scrollY = window.scrollY;
220
+ if (e.clientY < options.scrollTreshold) {
221
+ window.scrollTo({ top: scrollY - 30, behavior: "smooth" });
222
+ } else if (window.innerHeight - e.clientY < options.scrollTreshold) {
223
+ window.scrollTo({ top: scrollY + 30, behavior: "smooth" });
224
+ }
225
+ }
226
+
227
+ dragHandleElement.addEventListener("drag", onDragHandleDrag);
228
+
229
+ hideDragHandle();
230
+
231
+ if (!handleBySelector) {
232
+ view?.dom?.parentElement?.appendChild(dragHandleElement);
233
+ }
234
+
235
+ return {
236
+ destroy: () => {
237
+ if (!handleBySelector) {
238
+ dragHandleElement?.remove?.();
239
+ }
240
+ dragHandleElement?.removeEventListener("drag", onDragHandleDrag);
241
+ dragHandleElement?.removeEventListener("dragstart", onDragHandleDragStart);
242
+ dragHandleElement = null;
243
+ },
244
+ };
245
+ },
246
+ props: {
247
+ handleDOMEvents: {
248
+ mousemove: (view, event) => {
249
+ if (!view.editable) {
250
+ return;
251
+ }
252
+
253
+ const node = nodeDOMAtCoords({
254
+ x: event.clientX + 50 + options.dragHandleWidth,
255
+ y: event.clientY,
256
+ });
257
+
258
+ const notDragging = node?.closest(".not-draggable");
259
+
260
+ if (!(node instanceof Element) || node.matches("ul, ol") || notDragging) {
261
+ hideDragHandle();
262
+ return;
263
+ }
264
+
265
+ const compStyle = window.getComputedStyle(node);
266
+ const parsedLineHeight = Number.parseInt(compStyle.lineHeight, 10);
267
+ const lineHeight = Number.isNaN(parsedLineHeight)
268
+ ? Number.parseInt(compStyle.fontSize, 10) * 1.2
269
+ : parsedLineHeight;
270
+ const paddingTop = Number.parseInt(compStyle.paddingTop, 10);
271
+
272
+ const boundingRect = node.getBoundingClientRect();
273
+ const draggedNodePos = view.posAtCoords({
274
+ left: boundingRect.left,
275
+ top: boundingRect.top,
276
+ });
277
+ if (draggedNodePos == null) return;
278
+ const rootPos = rootNodePos(draggedNodePos.pos, view);
279
+ const root = getRootNode(rootPos, view, node, options.uniqueId!);
280
+ options.onNodeChange?.(root, rootPos);
281
+
282
+ const rootNode = view.domAtPos(root.pos).node as Element;
283
+ let rect = absoluteRect(rootNode);
284
+ if (node.matches("div.react-renderer, div[data-type=horizontalRule]")) {
285
+ rect = absoluteRect(node);
286
+ }
287
+
288
+ rect.top += paddingTop;
289
+ rect.width = options.dragHandleWidth;
290
+ if (node.matches("div[data-type=horizontalRule]")) {
291
+ rect.top -= (boundingRect.height + 8) / 2;
292
+ }
293
+ if (root.node().type.name === "codeBlock") {
294
+ rect.top -= lineHeight + Number.parseInt(compStyle.fontSize, 10);
295
+ }
296
+ if (!dragHandleElement) return;
297
+
298
+ const editorRect = view.dom.getBoundingClientRect();
299
+ dragHandleElement.style.left = `${rect.left - rect.width}px`;
300
+ dragHandleElement.style.top = `${rect.top - editorRect.top}px`;
301
+ showDragHandle();
302
+ },
303
+ keydown: () => {
304
+ hideDragHandle();
305
+ },
306
+ mousewheel: () => {
307
+ hideDragHandle();
308
+ },
309
+ drop: (view, event) => {
310
+ view.dom.classList.remove("dragging");
311
+ hideDragHandle();
312
+ let droppedNode: Node | null = null;
313
+ const dropPos = view.posAtCoords({
314
+ left: event.clientX,
315
+ top: event.clientY,
316
+ });
317
+
318
+ if (!dropPos) return;
319
+
320
+ if (view.state.selection instanceof NodeSelection) {
321
+ droppedNode = view.state.selection.node;
322
+ }
323
+ if (!droppedNode) return;
324
+
325
+ const resolvedPos = view.state.doc.resolve(dropPos.pos);
326
+
327
+ const isDroppedInsideList = resolvedPos.parent.type.name === "listItem";
328
+
329
+ // If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
330
+ if (
331
+ view.state.selection instanceof NodeSelection &&
332
+ view.state.selection.node.type.name === "listItem" &&
333
+ !isDroppedInsideList &&
334
+ listType === "OL"
335
+ ) {
336
+ const text = droppedNode.textContent;
337
+ if (!text) return;
338
+ const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
339
+ const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
340
+ const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
341
+ const slice = new Slice(Fragment.from(newList), 0, 0);
342
+ view.dragging = { slice, move: event.ctrlKey };
343
+ }
344
+ },
345
+ },
346
+ },
347
+ });
348
+ }
349
+
350
+ const GlobalDragHandle = Extension.create({
351
+ name: "globalDragHandle",
352
+
353
+ addOptions() {
354
+ return {
355
+ dragHandleWidth: 20,
356
+ scrollTreshold: 100,
357
+ uniqueId: "id",
358
+ };
359
+ },
360
+
361
+ addProseMirrorPlugins() {
362
+ return [
363
+ DragHandlePlugin({
364
+ pluginKey: "globalDragHandle",
365
+ dragHandleWidth: this.options.dragHandleWidth,
366
+ scrollTreshold: this.options.scrollTreshold,
367
+ dragHandleSelector: this.options.dragHandleSelector,
368
+ onNodeChange: this.options.onNodeChange,
369
+ onShow: this.options.onShow,
370
+ onHide: this.options.onHide,
371
+ uniqueId: this.options.uniqueId,
372
+ }),
373
+ ];
374
+ },
375
+ });
376
+
377
+ export default GlobalDragHandle;