@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.
- package/README.md +458 -0
- package/package.json +100 -0
- package/src/editor/constants.tsx +66 -0
- package/src/editor/container.css +46 -0
- package/src/editor/control/character-count/index.tsx +39 -0
- package/src/editor/control/drag-handle/index.tsx +85 -0
- package/src/editor/control/drag-handle/use.content.actions.ts +71 -0
- package/src/editor/control/drag-handle/use.data.ts +29 -0
- package/src/editor/control/drag-handle/use.handle.id.ts +6 -0
- package/src/editor/control/index.tsx +35 -0
- package/src/editor/editor.css +626 -0
- package/src/editor/extension/block-quote-figure/BlockquoteFigure.ts +73 -0
- package/src/editor/extension/block-quote-figure/Quote/Quote.ts +31 -0
- package/src/editor/extension/block-quote-figure/Quote/index.ts +1 -0
- package/src/editor/extension/block-quote-figure/QuoteCaption/QuoteCaption.ts +54 -0
- package/src/editor/extension/block-quote-figure/QuoteCaption/index.ts +1 -0
- package/src/editor/extension/block-quote-figure/index.ts +1 -0
- package/src/editor/extension/document/index.ts +5 -0
- package/src/editor/extension/figcaption/Figcaption.ts +90 -0
- package/src/editor/extension/figcaption/index.ts +1 -0
- package/src/editor/extension/figure/Figure.ts +62 -0
- package/src/editor/extension/figure/index.ts +1 -0
- package/src/editor/extension/font-size/FontSize.ts +64 -0
- package/src/editor/extension/font-size/index.ts +1 -0
- package/src/editor/extension/global-drag-handle/clipboard-serializer.ts +28 -0
- package/src/editor/extension/global-drag-handle/index.ts +377 -0
- package/src/editor/extension/heading/index.ts +13 -0
- package/src/editor/extension/horizontal-rule/HorizontalRule.ts +10 -0
- package/src/editor/extension/horizontal-rule/index.ts +1 -0
- package/src/editor/extension/image/index.ts +5 -0
- package/src/editor/extension/image-block/ImageBlock.ts +103 -0
- package/src/editor/extension/image-block/components/ImageBlockMenu.tsx +100 -0
- package/src/editor/extension/image-block/components/ImageBlockView.tsx +47 -0
- package/src/editor/extension/image-block/components/ImageBlockWidth.tsx +40 -0
- package/src/editor/extension/image-block/index.ts +1 -0
- package/src/editor/extension/image-upload/ImageUpload.ts +58 -0
- package/src/editor/extension/image-upload/index.ts +1 -0
- package/src/editor/extension/image-upload/view/ImageUpload.tsx +27 -0
- package/src/editor/extension/image-upload/view/ImageUploader.tsx +64 -0
- package/src/editor/extension/image-upload/view/hooks.ts +109 -0
- package/src/editor/extension/image-upload/view/index.tsx +1 -0
- package/src/editor/extension/index.ts +30 -0
- package/src/editor/extension/link/Link.ts +39 -0
- package/src/editor/extension/link/index.ts +1 -0
- package/src/editor/extension/multi-column/Column.ts +33 -0
- package/src/editor/extension/multi-column/Columns.ts +65 -0
- package/src/editor/extension/multi-column/index.ts +2 -0
- package/src/editor/extension/multi-column/menus/ColumnsMenu.tsx +82 -0
- package/src/editor/extension/multi-column/menus/index.ts +1 -0
- package/src/editor/extension/selection/Selection.ts +36 -0
- package/src/editor/extension/selection/index.ts +1 -0
- package/src/editor/extension/slash-command/MenuList.tsx +145 -0
- package/src/editor/extension/slash-command/groups.ts +153 -0
- package/src/editor/extension/slash-command/index.ts +277 -0
- package/src/editor/extension/slash-command/types.ts +25 -0
- package/src/editor/extension/table/Cell.ts +126 -0
- package/src/editor/extension/table/Header.ts +89 -0
- package/src/editor/extension/table/Row.ts +8 -0
- package/src/editor/extension/table/Table.ts +9 -0
- package/src/editor/extension/table/index.ts +4 -0
- package/src/editor/extension/table/menus/TableColumn/index.tsx +73 -0
- package/src/editor/extension/table/menus/TableColumn/utils.ts +38 -0
- package/src/editor/extension/table/menus/TableRow/index.tsx +74 -0
- package/src/editor/extension/table/menus/TableRow/utils.ts +38 -0
- package/src/editor/extension/table/menus/index.tsx +2 -0
- package/src/editor/extension/table/utils.ts +258 -0
- package/src/editor/extension/task-item/index.ts +1 -0
- package/src/editor/extension/task-item/task-item.ts +225 -0
- package/src/editor/extension/task-list/index.ts +1 -0
- package/src/editor/extension/task-list/task-list.ts +81 -0
- package/src/editor/extension/trailing-node/index.ts +1 -0
- package/src/editor/extension/trailing-node/trailing-node.ts +70 -0
- package/src/editor/extension/unique-id/index.ts +1 -0
- package/src/editor/extension/unique-id/uniqueId.ts +123 -0
- package/src/editor/hooks.ts +264 -0
- package/src/editor/index.tsx +53 -0
- package/src/editor/menus/LinkMenu/LinkMenu.tsx +75 -0
- package/src/editor/menus/LinkMenu/index.tsx +1 -0
- package/src/editor/menus/TextMenu/TextMenu.tsx +193 -0
- package/src/editor/menus/TextMenu/components/AIDropdown.tsx +140 -0
- package/src/editor/menus/TextMenu/components/ContentTypePicker.tsx +76 -0
- package/src/editor/menus/TextMenu/components/EditLinkPopover.tsx +25 -0
- package/src/editor/menus/TextMenu/components/FontFamilyPicker.tsx +84 -0
- package/src/editor/menus/TextMenu/components/FontSizePicker.tsx +56 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuCommands.ts +96 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuContentTypes.ts +86 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuStates.ts +50 -0
- package/src/editor/menus/TextMenu/index.tsx +2 -0
- package/src/editor/menus/types.ts +21 -0
- package/src/editor/panels/Colorpicker/ColorButton.tsx +35 -0
- package/src/editor/panels/Colorpicker/Colorpicker.tsx +67 -0
- package/src/editor/panels/Colorpicker/index.tsx +2 -0
- package/src/editor/panels/LinkEditorPanel/LinkEditorPanel.tsx +76 -0
- package/src/editor/panels/LinkEditorPanel/index.tsx +1 -0
- package/src/editor/panels/LinkPreviewPanel/LinkPreviewPanel.tsx +32 -0
- package/src/editor/panels/LinkPreviewPanel/index.tsx +1 -0
- package/src/editor/panels/index.tsx +3 -0
- package/src/editor/types.tsx +38 -0
- package/src/editor/ui/Button/Button.tsx +70 -0
- package/src/editor/ui/Button/index.tsx +2 -0
- package/src/editor/ui/Dropdown/Dropdown.tsx +39 -0
- package/src/editor/ui/Dropdown/index.tsx +1 -0
- package/src/editor/ui/Icon.tsx +21 -0
- package/src/editor/ui/Loader/Loader.tsx +39 -0
- package/src/editor/ui/Loader/index.ts +1 -0
- package/src/editor/ui/Loader/types.ts +7 -0
- package/src/editor/ui/Panel/index.tsx +109 -0
- package/src/editor/ui/PopoverMenu.tsx +127 -0
- package/src/editor/ui/Spinner/Spinner.tsx +10 -0
- package/src/editor/ui/Spinner/index.tsx +1 -0
- package/src/editor/ui/Surface.tsx +27 -0
- package/src/editor/ui/Textarea/Textarea.tsx +20 -0
- package/src/editor/ui/Textarea/index.tsx +1 -0
- package/src/editor/ui/Toggle/Toggle.tsx +39 -0
- package/src/editor/ui/Toggle/index.tsx +1 -0
- package/src/editor/ui/Toolbar.tsx +107 -0
- package/src/editor/ui/Tooltip/index.tsx +77 -0
- package/src/editor/ui/Tooltip/types.ts +17 -0
- package/src/editor/utils/cssVar.ts +14 -0
- package/src/editor/utils/getRenderContainer.ts +39 -0
- package/src/editor/utils/index.ts +16 -0
- package/src/editor/utils/isCustomNodeSelected.ts +47 -0
- package/src/editor/utils/isTextSelected.ts +25 -0
- package/src/editor/utils/locale.ts +5 -0
- package/src/editor/viewer/index.tsx +26 -0
- package/src/globals.css +1 -0
- package/src/index.ts +7 -0
- package/src/locales/en-us.ts +133 -0
- package/src/locales/zh-cn.ts +133 -0
- 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,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;
|