neuphlo-editor 2.5.0 → 2.6.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.
- package/dist/Markdown-BAW0cb-U.d.cts +153 -0
- package/dist/Markdown-BAW0cb-U.d.ts +153 -0
- package/dist/{chunk-4L7FHW5M.js → chunk-ABL4D65Z.js} +113 -47
- package/dist/chunk-ABL4D65Z.js.map +1 -0
- package/dist/{chunk-BVFTQKY7.js → chunk-Z2DR2FK3.js} +245 -50
- package/dist/chunk-Z2DR2FK3.js.map +1 -0
- package/dist/headless/components/editor-bubble-item.d.ts +9 -0
- package/dist/headless/components/editor-bubble-item.js +29 -0
- package/dist/headless/components/editor-bubble.d.ts +9 -0
- package/dist/headless/components/editor-bubble.js +9 -0
- package/dist/headless/components/editor-command-item.d.ts +3 -0
- package/dist/headless/components/editor-command-item.js +27 -0
- package/dist/headless/components/editor-command.d.ts +3 -0
- package/dist/headless/components/editor-command.js +136 -0
- package/dist/headless/components/editor.d.ts +24 -0
- package/dist/headless/components/editor.js +13 -0
- package/dist/headless/extensions/AISuggestion/AISuggestion.d.ts +82 -0
- package/dist/headless/extensions/AISuggestion/AISuggestion.js +244 -0
- package/dist/headless/extensions/AISuggestion/index.d.ts +2 -0
- package/dist/headless/extensions/AISuggestion/index.js +1 -0
- package/dist/headless/extensions/CodeBlock/CodeBlock.d.ts +1 -0
- package/dist/headless/extensions/CodeBlock/CodeBlock.js +6 -0
- package/dist/headless/extensions/CodeBlock/index.d.ts +1 -0
- package/dist/headless/extensions/CodeBlock/index.js +1 -0
- package/dist/headless/extensions/DragHandle/DragHandle.d.ts +9 -0
- package/dist/headless/extensions/DragHandle/DragHandle.js +47 -0
- package/dist/headless/extensions/DragHandle/index.d.ts +2 -0
- package/dist/headless/extensions/DragHandle/index.js +1 -0
- package/dist/headless/extensions/Image/Image.d.ts +6 -0
- package/dist/headless/extensions/Image/Image.js +197 -0
- package/dist/headless/extensions/ImageBlock/ImageBlock.d.ts +23 -0
- package/dist/headless/extensions/ImageBlock/ImageBlock.js +242 -0
- package/dist/headless/extensions/ImageBlock/index.d.ts +2 -0
- package/dist/headless/extensions/ImageBlock/index.js +1 -0
- package/dist/headless/extensions/Link/Link.d.ts +2 -0
- package/dist/headless/extensions/Link/Link.js +44 -0
- package/dist/headless/extensions/Link/index.d.ts +1 -0
- package/dist/headless/extensions/Link/index.js +1 -0
- package/dist/headless/extensions/Markdown.d.ts +9 -0
- package/dist/headless/extensions/Markdown.js +138 -0
- package/dist/headless/extensions/MarkdownPaste.d.ts +3 -0
- package/dist/headless/extensions/MarkdownPaste.js +168 -0
- package/dist/headless/extensions/Mention/index.d.ts +4 -0
- package/dist/headless/extensions/Mention/index.js +3 -0
- package/dist/headless/extensions/Mention/mention-command.d.ts +8 -0
- package/dist/headless/extensions/Mention/mention-command.js +165 -0
- package/dist/headless/extensions/Mention/mention-list.d.ts +8 -0
- package/dist/headless/extensions/Mention/mention-list.js +150 -0
- package/dist/headless/extensions/Mention/mention-node-view.d.ts +2 -0
- package/dist/headless/extensions/Mention/mention-node-view.js +92 -0
- package/dist/headless/extensions/Mention/mention.d.ts +46 -0
- package/dist/headless/extensions/Mention/mention.js +218 -0
- package/dist/headless/extensions/Table/index.d.ts +2 -0
- package/dist/headless/extensions/Table/index.js +2 -0
- package/dist/headless/extensions/VideoBlock/VideoBlock.d.ts +17 -0
- package/dist/headless/extensions/VideoBlock/VideoBlock.js +73 -0
- package/dist/headless/extensions/VideoBlock/index.d.ts +2 -0
- package/dist/headless/extensions/VideoBlock/index.js +1 -0
- package/dist/headless/extensions/extension-kit.d.ts +29 -0
- package/dist/headless/extensions/extension-kit.js +132 -0
- package/dist/headless/extensions/index.d.ts +16 -0
- package/dist/headless/extensions/index.js +12 -0
- package/dist/headless/extensions/slash-command.d.ts +24 -0
- package/dist/headless/extensions/slash-command.js +99 -0
- package/dist/headless/index.cjs +143 -2
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +3 -2
- package/dist/headless/index.d.ts +3 -2
- package/dist/headless/index.js +5 -1
- package/dist/headless/utils/atoms.d.ts +13 -0
- package/dist/headless/utils/atoms.js +5 -0
- package/dist/headless/utils/store.d.ts +2 -0
- package/dist/headless/utils/store.js +4 -0
- package/dist/highlight.css +77 -1
- package/dist/react/Editor.d.ts +43 -0
- package/dist/react/Editor.js +119 -0
- package/dist/react/TableOfContents.d.ts +7 -0
- package/dist/react/TableOfContents.js +96 -0
- package/dist/react/index.cjs +156 -9
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +5 -2
- package/dist/react/index.d.ts +5 -2
- package/dist/react/index.js +14 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/menus/DragHandle/BlockActionMenu.d.ts +7 -0
- package/dist/react/menus/DragHandle/BlockActionMenu.js +111 -0
- package/dist/react/menus/DragHandle/DragHandleMenu.d.ts +7 -0
- package/dist/react/menus/DragHandle/DragHandleMenu.js +97 -0
- package/dist/react/menus/ImageBlock/ImageBlockLoading.d.ts +2 -0
- package/dist/react/menus/ImageBlock/ImageBlockLoading.js +6 -0
- package/dist/react/menus/ImageBlock/ImageBlockMenu.d.ts +8 -0
- package/dist/react/menus/ImageBlock/ImageBlockMenu.js +75 -0
- package/dist/react/menus/ImageBlock/ImageBlockView.d.ts +10 -0
- package/dist/react/menus/ImageBlock/ImageBlockView.js +60 -0
- package/dist/react/menus/ImageBlock/ImageBlockWidth.d.ts +6 -0
- package/dist/react/menus/ImageBlock/ImageBlockWidth.js +29 -0
- package/dist/react/menus/ImageBlock/ImageResizeHandle.d.ts +8 -0
- package/dist/react/menus/ImageBlock/ImageResizeHandle.js +37 -0
- package/dist/react/menus/ImageBlock/ImageUploader.d.ts +7 -0
- package/dist/react/menus/ImageBlock/ImageUploader.js +73 -0
- package/dist/react/menus/ImageBlock/index.d.ts +4 -0
- package/dist/react/menus/ImageBlock/index.js +4 -0
- package/dist/react/menus/ImageMenu.d.ts +10 -0
- package/dist/react/menus/ImageMenu.js +77 -0
- package/dist/react/menus/ImagePopover.d.ts +8 -0
- package/dist/react/menus/ImagePopover.js +146 -0
- package/dist/react/menus/LinkMenu.d.ts +2 -0
- package/dist/react/menus/LinkMenu.js +69 -0
- package/dist/react/menus/MenuList.d.ts +10 -0
- package/dist/react/menus/MenuList.js +94 -0
- package/dist/react/menus/SlashMenu.d.ts +5 -0
- package/dist/react/menus/SlashMenu.js +156 -0
- package/dist/react/menus/TableMenu.d.ts +5 -0
- package/dist/react/menus/TableMenu.js +462 -0
- package/dist/react/menus/TaskItem/TaskItemView.d.ts +12 -0
- package/dist/react/menus/TaskItem/TaskItemView.js +11 -0
- package/dist/react/menus/TextMenu.d.ts +10 -0
- package/dist/react/menus/TextMenu.js +139 -0
- package/dist/react/menus/VideoBlock/VideoBlockMenu.d.ts +7 -0
- package/dist/react/menus/VideoBlock/VideoBlockMenu.js +72 -0
- package/dist/react/menus/VideoBlock/VideoBlockView.d.ts +10 -0
- package/dist/react/menus/VideoBlock/VideoBlockView.js +87 -0
- package/dist/react/menus/VideoBlock/index.d.ts +2 -0
- package/dist/react/menus/VideoBlock/index.js +2 -0
- package/dist/react/menus/index.d.ts +15 -0
- package/dist/react/menus/index.js +9 -0
- package/package.json +3 -1
- package/dist/chunk-2DWEJI45.js +0 -1296
- package/dist/chunk-2DWEJI45.js.map +0 -1
- package/dist/chunk-3CLWAYRP.js +0 -1387
- package/dist/chunk-3CLWAYRP.js.map +0 -1
- package/dist/chunk-457ETWB6.js +0 -1351
- package/dist/chunk-457ETWB6.js.map +0 -1
- package/dist/chunk-4L7FHW5M.js.map +0 -1
- package/dist/chunk-4MV3UUZN.js +0 -1073
- package/dist/chunk-4MV3UUZN.js.map +0 -1
- package/dist/chunk-62DYB7FY.js +0 -1305
- package/dist/chunk-62DYB7FY.js.map +0 -1
- package/dist/chunk-BVFTQKY7.js.map +0 -1
- package/dist/chunk-DWGPGRTQ.js +0 -1302
- package/dist/chunk-DWGPGRTQ.js.map +0 -1
- package/dist/chunk-EG7NQJRA.js +0 -1324
- package/dist/chunk-EG7NQJRA.js.map +0 -1
- package/dist/chunk-FLLPFFI5.js +0 -1296
- package/dist/chunk-FLLPFFI5.js.map +0 -1
- package/dist/chunk-FVQHB6VC.js +0 -1128
- package/dist/chunk-FVQHB6VC.js.map +0 -1
- package/dist/chunk-G5E32MLB.js +0 -1352
- package/dist/chunk-G5E32MLB.js.map +0 -1
- package/dist/chunk-GXJGZHKR.js +0 -1326
- package/dist/chunk-GXJGZHKR.js.map +0 -1
- package/dist/chunk-KCPPTLGY.js +0 -1299
- package/dist/chunk-KCPPTLGY.js.map +0 -1
- package/dist/chunk-LHG2NX6C.js +0 -1123
- package/dist/chunk-LHG2NX6C.js.map +0 -1
- package/dist/chunk-MSNTVITF.js +0 -1352
- package/dist/chunk-MSNTVITF.js.map +0 -1
- package/dist/chunk-N4RDYJ5Z.js +0 -1351
- package/dist/chunk-N4RDYJ5Z.js.map +0 -1
- package/dist/chunk-OCNM37WJ.js +0 -1289
- package/dist/chunk-OCNM37WJ.js.map +0 -1
- package/dist/chunk-P3YFYEUB.js +0 -1351
- package/dist/chunk-P3YFYEUB.js.map +0 -1
- package/dist/chunk-RW6QBMJB.js +0 -1300
- package/dist/chunk-RW6QBMJB.js.map +0 -1
- package/dist/chunk-SJQQVB3M.js +0 -1353
- package/dist/chunk-SJQQVB3M.js.map +0 -1
- package/dist/chunk-SOXTEP7H.js +0 -6705
- package/dist/chunk-SOXTEP7H.js.map +0 -1
- package/dist/chunk-VPI26I4P.js +0 -1350
- package/dist/chunk-VPI26I4P.js.map +0 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/react";
|
|
2
|
+
export type BlockActionMenuProps = {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function BlockActionMenu({ editor, onClose }: BlockActionMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export default BlockActionMenu;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconCopy, IconTrash, IconArrowUp, IconArrowDown, IconClipboard, } from "@tabler/icons-react";
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
export function BlockActionMenu({ editor, onClose }) {
|
|
5
|
+
const executeAndClose = useCallback((fn) => {
|
|
6
|
+
fn();
|
|
7
|
+
onClose();
|
|
8
|
+
}, [onClose]);
|
|
9
|
+
const handleDelete = useCallback(() => {
|
|
10
|
+
executeAndClose(() => {
|
|
11
|
+
editor.commands.deleteSelection();
|
|
12
|
+
});
|
|
13
|
+
}, [editor, executeAndClose]);
|
|
14
|
+
const handleDuplicate = useCallback(() => {
|
|
15
|
+
executeAndClose(() => {
|
|
16
|
+
const { state } = editor;
|
|
17
|
+
const { selection } = state;
|
|
18
|
+
const { $anchor } = selection;
|
|
19
|
+
// Find the top-level block
|
|
20
|
+
const depth = $anchor.depth > 0 ? 1 : 0;
|
|
21
|
+
const start = $anchor.start(depth);
|
|
22
|
+
const end = $anchor.end(depth);
|
|
23
|
+
const node = state.doc.nodeAt(start - 1);
|
|
24
|
+
if (node) {
|
|
25
|
+
const insertPos = end + 1;
|
|
26
|
+
editor
|
|
27
|
+
.chain()
|
|
28
|
+
.focus()
|
|
29
|
+
.insertContentAt(insertPos, node.toJSON())
|
|
30
|
+
.run();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}, [editor, executeAndClose]);
|
|
34
|
+
const handleMoveUp = useCallback(() => {
|
|
35
|
+
executeAndClose(() => {
|
|
36
|
+
const { state } = editor;
|
|
37
|
+
const { selection } = state;
|
|
38
|
+
const { $anchor } = selection;
|
|
39
|
+
const depth = $anchor.depth > 0 ? 1 : 0;
|
|
40
|
+
const blockStart = $anchor.start(depth) - 1;
|
|
41
|
+
if (blockStart <= 0)
|
|
42
|
+
return; // Already at top
|
|
43
|
+
const node = state.doc.nodeAt(blockStart);
|
|
44
|
+
if (!node)
|
|
45
|
+
return;
|
|
46
|
+
// Find previous sibling
|
|
47
|
+
const $pos = state.doc.resolve(blockStart);
|
|
48
|
+
const index = $pos.index($pos.depth);
|
|
49
|
+
if (index === 0)
|
|
50
|
+
return; // First child
|
|
51
|
+
const prevNode = $pos.node($pos.depth).child(index - 1);
|
|
52
|
+
const prevStart = blockStart - prevNode.nodeSize;
|
|
53
|
+
editor.chain().focus()
|
|
54
|
+
.command(({ tr }) => {
|
|
55
|
+
const currentSlice = state.doc.slice(blockStart, blockStart + node.nodeSize);
|
|
56
|
+
tr.delete(blockStart, blockStart + node.nodeSize);
|
|
57
|
+
tr.insert(prevStart, currentSlice.content);
|
|
58
|
+
return true;
|
|
59
|
+
})
|
|
60
|
+
.run();
|
|
61
|
+
});
|
|
62
|
+
}, [editor, executeAndClose]);
|
|
63
|
+
const handleMoveDown = useCallback(() => {
|
|
64
|
+
executeAndClose(() => {
|
|
65
|
+
const { state } = editor;
|
|
66
|
+
const { selection } = state;
|
|
67
|
+
const { $anchor } = selection;
|
|
68
|
+
const depth = $anchor.depth > 0 ? 1 : 0;
|
|
69
|
+
const blockStart = $anchor.start(depth) - 1;
|
|
70
|
+
const node = state.doc.nodeAt(blockStart);
|
|
71
|
+
if (!node)
|
|
72
|
+
return;
|
|
73
|
+
const blockEnd = blockStart + node.nodeSize;
|
|
74
|
+
// Check if there's a next sibling
|
|
75
|
+
const $pos = state.doc.resolve(blockStart);
|
|
76
|
+
const parent = $pos.node($pos.depth);
|
|
77
|
+
const index = $pos.index($pos.depth);
|
|
78
|
+
if (index >= parent.childCount - 1)
|
|
79
|
+
return; // Last child
|
|
80
|
+
const nextNode = parent.child(index + 1);
|
|
81
|
+
const nextEnd = blockEnd + nextNode.nodeSize;
|
|
82
|
+
editor.chain().focus()
|
|
83
|
+
.command(({ tr }) => {
|
|
84
|
+
const currentSlice = state.doc.slice(blockStart, blockEnd);
|
|
85
|
+
tr.delete(blockStart, blockEnd);
|
|
86
|
+
const insertPos = blockStart + nextNode.nodeSize;
|
|
87
|
+
tr.insert(insertPos, currentSlice.content);
|
|
88
|
+
return true;
|
|
89
|
+
})
|
|
90
|
+
.run();
|
|
91
|
+
});
|
|
92
|
+
}, [editor, executeAndClose]);
|
|
93
|
+
const handleCopyToClipboard = useCallback(() => {
|
|
94
|
+
executeAndClose(() => {
|
|
95
|
+
const { state } = editor;
|
|
96
|
+
const { selection } = state;
|
|
97
|
+
const { $anchor } = selection;
|
|
98
|
+
const depth = $anchor.depth > 0 ? 1 : 0;
|
|
99
|
+
const start = $anchor.start(depth) - 1;
|
|
100
|
+
const node = state.doc.nodeAt(start);
|
|
101
|
+
if (node) {
|
|
102
|
+
const text = node.textContent;
|
|
103
|
+
navigator.clipboard.writeText(text).catch(() => {
|
|
104
|
+
// Fallback: silently fail
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}, [editor, executeAndClose]);
|
|
109
|
+
return (_jsx("div", { className: "nph-block-action-menu nph-command", children: _jsxs("div", { className: "nph-command__list", style: { maxHeight: "none" }, children: [_jsxs("button", { type: "button", className: "nph-command__item", onClick: handleDelete, onMouseDown: (e) => e.preventDefault(), children: [_jsx(IconTrash, { size: 16 }), _jsx("span", { children: "Delete" })] }), _jsxs("button", { type: "button", className: "nph-command__item", onClick: handleDuplicate, onMouseDown: (e) => e.preventDefault(), children: [_jsx(IconCopy, { size: 16 }), _jsx("span", { children: "Duplicate" })] }), _jsxs("button", { type: "button", className: "nph-command__item", onClick: handleCopyToClipboard, onMouseDown: (e) => e.preventDefault(), children: [_jsx(IconClipboard, { size: 16 }), _jsx("span", { children: "Copy to clipboard" })] }), _jsxs("button", { type: "button", className: "nph-command__item", onClick: handleMoveUp, onMouseDown: (e) => e.preventDefault(), children: [_jsx(IconArrowUp, { size: 16 }), _jsx("span", { children: "Move up" })] }), _jsxs("button", { type: "button", className: "nph-command__item", onClick: handleMoveDown, onMouseDown: (e) => e.preventDefault(), children: [_jsx(IconArrowDown, { size: 16 }), _jsx("span", { children: "Move down" })] })] }) }));
|
|
110
|
+
}
|
|
111
|
+
export default BlockActionMenu;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function DragHandleMenu(): import("react/jsx-runtime").JSX.Element | null;
|
|
2
|
+
/**
|
|
3
|
+
* Creates the drag handle DOM element with + and grip buttons.
|
|
4
|
+
* Called by the DragHandle extension's render() option.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDragHandleElement(onAddBlock: () => void, onGripClick: () => void): HTMLElement;
|
|
7
|
+
export default DragHandleMenu;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCurrentEditor } from "@tiptap/react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { BlockActionMenu } from "./BlockActionMenu";
|
|
5
|
+
export function DragHandleMenu() {
|
|
6
|
+
const { editor } = useCurrentEditor();
|
|
7
|
+
const [hoveredNode, setHoveredNode] = useState(null);
|
|
8
|
+
const [showActions, setShowActions] = useState(false);
|
|
9
|
+
const menuRef = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!editor)
|
|
12
|
+
return;
|
|
13
|
+
const handler = ({ node }) => {
|
|
14
|
+
setHoveredNode(node);
|
|
15
|
+
if (!node)
|
|
16
|
+
setShowActions(false);
|
|
17
|
+
};
|
|
18
|
+
// The drag handle extension calls onNodeChange
|
|
19
|
+
// We use the editor's storage to communicate
|
|
20
|
+
const ext = editor.extensionManager.extensions.find((e) => e.name === "dragHandle");
|
|
21
|
+
if (ext) {
|
|
22
|
+
const origOnNodeChange = ext.options.onNodeChange;
|
|
23
|
+
ext.options.onNodeChange = (opts) => {
|
|
24
|
+
origOnNodeChange?.(opts);
|
|
25
|
+
handler(opts);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
// Cleanup
|
|
30
|
+
};
|
|
31
|
+
}, [editor]);
|
|
32
|
+
const handleAddBlock = useCallback(() => {
|
|
33
|
+
if (!editor)
|
|
34
|
+
return;
|
|
35
|
+
// Get current position and insert a new paragraph after the current block
|
|
36
|
+
const { state } = editor;
|
|
37
|
+
const { selection } = state;
|
|
38
|
+
const pos = selection.$anchor.end();
|
|
39
|
+
editor
|
|
40
|
+
.chain()
|
|
41
|
+
.focus()
|
|
42
|
+
.insertContentAt(pos + 1, { type: "paragraph" })
|
|
43
|
+
.focus(pos + 2)
|
|
44
|
+
.run();
|
|
45
|
+
}, [editor]);
|
|
46
|
+
const handleGripClick = useCallback(() => {
|
|
47
|
+
setShowActions((prev) => !prev);
|
|
48
|
+
}, []);
|
|
49
|
+
// Close action menu on click outside
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!showActions)
|
|
52
|
+
return;
|
|
53
|
+
const handleClick = (e) => {
|
|
54
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
55
|
+
setShowActions(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener("pointerdown", handleClick);
|
|
59
|
+
return () => document.removeEventListener("pointerdown", handleClick);
|
|
60
|
+
}, [showActions]);
|
|
61
|
+
if (!editor)
|
|
62
|
+
return null;
|
|
63
|
+
return (_jsx("div", { ref: menuRef, style: { position: "relative" }, children: showActions && editor && (_jsx(BlockActionMenu, { editor: editor, onClose: () => setShowActions(false) })) }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates the drag handle DOM element with + and grip buttons.
|
|
67
|
+
* Called by the DragHandle extension's render() option.
|
|
68
|
+
*/
|
|
69
|
+
export function createDragHandleElement(onAddBlock, onGripClick) {
|
|
70
|
+
const container = document.createElement("div");
|
|
71
|
+
container.className = "nph-drag-handle";
|
|
72
|
+
const plusBtn = document.createElement("button");
|
|
73
|
+
plusBtn.className = "nph-drag-handle__btn";
|
|
74
|
+
plusBtn.type = "button";
|
|
75
|
+
plusBtn.setAttribute("aria-label", "Add block");
|
|
76
|
+
plusBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>`;
|
|
77
|
+
plusBtn.addEventListener("click", (e) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
onAddBlock();
|
|
81
|
+
});
|
|
82
|
+
const gripBtn = document.createElement("button");
|
|
83
|
+
gripBtn.className = "nph-drag-handle__btn nph-drag-handle__grip";
|
|
84
|
+
gripBtn.type = "button";
|
|
85
|
+
gripBtn.setAttribute("aria-label", "Drag to reorder");
|
|
86
|
+
gripBtn.setAttribute("draggable", "true");
|
|
87
|
+
gripBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
|
88
|
+
gripBtn.addEventListener("click", (e) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
onGripClick();
|
|
92
|
+
});
|
|
93
|
+
container.appendChild(plusBtn);
|
|
94
|
+
container.appendChild(gripBtn);
|
|
95
|
+
return container;
|
|
96
|
+
}
|
|
97
|
+
export default DragHandleMenu;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconLoader2 } from "@tabler/icons-react";
|
|
3
|
+
export const ImageBlockLoading = () => {
|
|
4
|
+
return (_jsxs("div", { className: "nph-image-block-loading", children: [_jsx("div", { className: "nph-image-block-loading__overlay", children: _jsxs("div", { className: "nph-image-block-loading__content", children: [_jsx(IconLoader2, { size: 24, className: "nph-image-block-loading__spinner" }), _jsx("p", { className: "nph-image-block-loading__text", children: "Uploading image..." })] }) }), _jsx("div", { className: "nph-image-block-loading__placeholder" })] }));
|
|
5
|
+
};
|
|
6
|
+
export default ImageBlockLoading;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Editor } from "@tiptap/react";
|
|
2
|
+
export type ImageBlockMenuProps = {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
getPos: () => number;
|
|
5
|
+
appendTo?: React.RefObject<HTMLElement | null>;
|
|
6
|
+
};
|
|
7
|
+
export declare const ImageBlockMenu: ({ editor, getPos, appendTo }: ImageBlockMenuProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
export default ImageBlockMenu;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NodeSelection } from "@tiptap/pm/state";
|
|
3
|
+
import { useEditorState } from "@tiptap/react";
|
|
4
|
+
import { useCallback, useRef } from "react";
|
|
5
|
+
import { ImageBlockWidth } from "./ImageBlockWidth";
|
|
6
|
+
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconTrash, } from "@tabler/icons-react";
|
|
7
|
+
export const ImageBlockMenu = ({ editor, getPos, appendTo }) => {
|
|
8
|
+
const menuRef = useRef(null);
|
|
9
|
+
const { isVisible, align, width } = useEditorState({
|
|
10
|
+
editor,
|
|
11
|
+
selector: (ctx) => {
|
|
12
|
+
if (!ctx.editor)
|
|
13
|
+
return { isVisible: false, align: "center", width: 100 };
|
|
14
|
+
const { state } = ctx.editor;
|
|
15
|
+
const { selection } = state;
|
|
16
|
+
if (!ctx.editor.isEditable)
|
|
17
|
+
return { isVisible: false, align: "center", width: 100 };
|
|
18
|
+
if (!ctx.editor.isFocused)
|
|
19
|
+
return { isVisible: false, align: "center", width: 100 };
|
|
20
|
+
const isNodeSel = selection instanceof NodeSelection;
|
|
21
|
+
const isThisNode = isNodeSel && selection.from === getPos();
|
|
22
|
+
const visible = isThisNode;
|
|
23
|
+
let currentAlign = "center";
|
|
24
|
+
let currentWidth = 100;
|
|
25
|
+
if (visible) {
|
|
26
|
+
const attrs = ctx.editor.getAttributes("imageBlock");
|
|
27
|
+
currentAlign = attrs.align || "center";
|
|
28
|
+
const widthStr = attrs.width || "100%";
|
|
29
|
+
currentWidth = parseInt(widthStr) || 100;
|
|
30
|
+
}
|
|
31
|
+
return { isVisible: visible, align: currentAlign, width: currentWidth };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const onAlignImageLeft = useCallback(() => {
|
|
35
|
+
editor
|
|
36
|
+
.chain()
|
|
37
|
+
.focus(undefined, { scrollIntoView: false })
|
|
38
|
+
.setImageBlockAlign("left")
|
|
39
|
+
.run();
|
|
40
|
+
}, [editor]);
|
|
41
|
+
const onAlignImageCenter = useCallback(() => {
|
|
42
|
+
editor
|
|
43
|
+
.chain()
|
|
44
|
+
.focus(undefined, { scrollIntoView: false })
|
|
45
|
+
.setImageBlockAlign("center")
|
|
46
|
+
.run();
|
|
47
|
+
}, [editor]);
|
|
48
|
+
const onAlignImageRight = useCallback(() => {
|
|
49
|
+
editor
|
|
50
|
+
.chain()
|
|
51
|
+
.focus(undefined, { scrollIntoView: false })
|
|
52
|
+
.setImageBlockAlign("right")
|
|
53
|
+
.run();
|
|
54
|
+
}, [editor]);
|
|
55
|
+
const onWidthChange = useCallback((value) => {
|
|
56
|
+
editor
|
|
57
|
+
.chain()
|
|
58
|
+
.focus(undefined, { scrollIntoView: false })
|
|
59
|
+
.setImageBlockWidth(value)
|
|
60
|
+
.run();
|
|
61
|
+
}, [editor]);
|
|
62
|
+
const onRemoveImage = useCallback(() => {
|
|
63
|
+
editor.chain().focus(undefined, { scrollIntoView: false }).deleteSelection().run();
|
|
64
|
+
}, [editor]);
|
|
65
|
+
if (!isVisible)
|
|
66
|
+
return null;
|
|
67
|
+
return (_jsxs("div", { className: "bubble-menu", ref: menuRef, style: {
|
|
68
|
+
position: "absolute",
|
|
69
|
+
top: "-40px",
|
|
70
|
+
left: "50%",
|
|
71
|
+
transform: "translateX(-50%)",
|
|
72
|
+
zIndex: "var(--nph-z, 50)",
|
|
73
|
+
}, children: [_jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "left" ? " is-active" : ""}`, title: "Align left", onMouseDown: (e) => e.preventDefault(), onClick: onAlignImageLeft, children: _jsx(IconAlignLeft, { size: 16 }) }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "center" ? " is-active" : ""}`, title: "Align center", onMouseDown: (e) => e.preventDefault(), onClick: onAlignImageCenter, children: _jsx(IconAlignCenter, { size: 16 }) }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "right" ? " is-active" : ""}`, title: "Align right", onMouseDown: (e) => e.preventDefault(), onClick: onAlignImageRight, children: _jsx(IconAlignRight, { size: 16 }) }), _jsx("div", { className: "nph-link-popover__divider", style: { margin: "0 4px" } }), _jsx(ImageBlockWidth, { onChange: onWidthChange, value: width }), _jsx("div", { className: "nph-link-popover__divider", style: { margin: "0 4px" } }), _jsx("button", { type: "button", className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", title: "Remove image", onMouseDown: (e) => e.preventDefault(), onClick: onRemoveImage, children: _jsx(IconTrash, { size: 16 }) })] }));
|
|
74
|
+
};
|
|
75
|
+
export default ImageBlockMenu;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Node } from "@tiptap/pm/model";
|
|
2
|
+
import { Editor } from "@tiptap/react";
|
|
3
|
+
interface ImageBlockViewProps {
|
|
4
|
+
editor: Editor;
|
|
5
|
+
getPos: () => number;
|
|
6
|
+
node: Node;
|
|
7
|
+
updateAttributes: (attrs: Record<string, any>) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const ImageBlockView: (props: ImageBlockViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export default ImageBlockView;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NodeViewWrapper } from "@tiptap/react";
|
|
3
|
+
import { useCallback, useRef, useState } from "react";
|
|
4
|
+
import { IconPhotoOff } from "@tabler/icons-react";
|
|
5
|
+
import { ImageBlockMenu } from "./ImageBlockMenu";
|
|
6
|
+
import { ImageUploader } from "./ImageUploader";
|
|
7
|
+
import { ImageBlockLoading } from "./ImageBlockLoading";
|
|
8
|
+
import { ImageResizeHandle } from "./ImageResizeHandle";
|
|
9
|
+
export const ImageBlockView = (props) => {
|
|
10
|
+
const { editor, getPos, node, updateAttributes } = props;
|
|
11
|
+
const imageWrapperRef = useRef(null);
|
|
12
|
+
const [imageError, setImageError] = useState(false);
|
|
13
|
+
const { src, width, align, alt, loading } = node.attrs;
|
|
14
|
+
const handleUpload = useCallback((url) => {
|
|
15
|
+
updateAttributes({ src: url, loading: false });
|
|
16
|
+
}, [updateAttributes]);
|
|
17
|
+
const onClick = useCallback(() => {
|
|
18
|
+
editor.commands.setNodeSelection(getPos());
|
|
19
|
+
}, [getPos, editor.commands]);
|
|
20
|
+
const handleResize = useCallback((widthPercent) => {
|
|
21
|
+
updateAttributes({ width: `${widthPercent}%` });
|
|
22
|
+
}, [updateAttributes]);
|
|
23
|
+
// Calculate wrapper style based on alignment and width
|
|
24
|
+
const getWrapperStyle = () => {
|
|
25
|
+
const baseStyle = {
|
|
26
|
+
width: width || "100%",
|
|
27
|
+
maxWidth: "100%",
|
|
28
|
+
};
|
|
29
|
+
if (align === "left") {
|
|
30
|
+
return { ...baseStyle, marginLeft: 0, marginRight: "auto" };
|
|
31
|
+
}
|
|
32
|
+
else if (align === "right") {
|
|
33
|
+
return { ...baseStyle, marginLeft: "auto", marginRight: 0 };
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return { ...baseStyle, marginLeft: "auto", marginRight: "auto" };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
// The inner content wrapper for positioning the menu
|
|
40
|
+
const getContentStyle = () => ({
|
|
41
|
+
position: "relative",
|
|
42
|
+
});
|
|
43
|
+
// Show uploader if no src (only when editable)
|
|
44
|
+
if (!src || src === "") {
|
|
45
|
+
if (!editor.isEditable)
|
|
46
|
+
return _jsx(NodeViewWrapper, {});
|
|
47
|
+
return (_jsx(NodeViewWrapper, { style: { width: "100%", marginTop: "0.5rem", marginBottom: "0.5rem" }, children: _jsx("div", { ref: imageWrapperRef, children: _jsx(ImageUploader, { onUpload: handleUpload, editor: editor }) }) }));
|
|
48
|
+
}
|
|
49
|
+
// Show loading placeholder
|
|
50
|
+
if (loading) {
|
|
51
|
+
return (_jsx(NodeViewWrapper, { style: getWrapperStyle(), children: _jsx("div", { ref: imageWrapperRef, children: _jsx(ImageBlockLoading, {}) }) }));
|
|
52
|
+
}
|
|
53
|
+
// Show error state for broken images
|
|
54
|
+
if (imageError) {
|
|
55
|
+
return (_jsx(NodeViewWrapper, { style: getWrapperStyle(), children: _jsxs("div", { contentEditable: false, ref: imageWrapperRef, style: getContentStyle(), children: [_jsxs("div", { className: "nph-image-block-error", onClick: onClick, children: [_jsx(IconPhotoOff, { size: 32 }), _jsx("span", { children: "Image could not be loaded" })] }), editor.isEditable && _jsx(ImageBlockMenu, { editor: editor, getPos: getPos, appendTo: imageWrapperRef })] }) }));
|
|
56
|
+
}
|
|
57
|
+
// Show the actual image
|
|
58
|
+
return (_jsx(NodeViewWrapper, { style: getWrapperStyle(), children: _jsxs("div", { contentEditable: false, ref: imageWrapperRef, style: getContentStyle(), children: [editor.isEditable ? (_jsx(ImageResizeHandle, { onResize: handleResize, currentWidth: width, children: _jsx("img", { src: src, alt: alt || "", onClick: onClick, onError: () => setImageError(true), className: "nph-image-block" }) })) : (_jsx("img", { src: src, alt: alt || "", onError: () => setImageError(true), className: "nph-image-block" })), editor.isEditable && _jsx(ImageBlockMenu, { editor: editor, getPos: getPos, appendTo: imageWrapperRef })] }) }));
|
|
59
|
+
};
|
|
60
|
+
export default ImageBlockView;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ImageBlockWidthProps = {
|
|
2
|
+
onChange: (value: number) => void;
|
|
3
|
+
value: number;
|
|
4
|
+
};
|
|
5
|
+
export declare const ImageBlockWidth: import("react").MemoExoticComponent<({ onChange, value }: ImageBlockWidthProps) => import("react/jsx-runtime").JSX.Element>;
|
|
6
|
+
export default ImageBlockWidth;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useEffect, useState } from "react";
|
|
3
|
+
export const ImageBlockWidth = memo(({ onChange, value }) => {
|
|
4
|
+
const [currentValue, setCurrentValue] = useState(value);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
setCurrentValue(value);
|
|
7
|
+
}, [value]);
|
|
8
|
+
const handleChange = useCallback((e) => {
|
|
9
|
+
const nextValue = parseInt(e.target.value);
|
|
10
|
+
onChange(nextValue);
|
|
11
|
+
setCurrentValue(nextValue);
|
|
12
|
+
}, [onChange]);
|
|
13
|
+
return (_jsxs("div", { style: {
|
|
14
|
+
display: "flex",
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
gap: 8,
|
|
17
|
+
minWidth: 140,
|
|
18
|
+
}, children: [_jsxs("span", { style: { fontSize: "12px", whiteSpace: "nowrap", width: 40 }, children: [currentValue, "%"] }), _jsx("input", { type: "range", min: "25", max: "100", step: "25", value: currentValue, onChange: handleChange, onMouseDown: (e) => e.stopPropagation(), style: {
|
|
19
|
+
flex: 1,
|
|
20
|
+
height: 3,
|
|
21
|
+
borderRadius: 9999,
|
|
22
|
+
appearance: "none",
|
|
23
|
+
background: "linear-gradient(to right, #333 0%, #333 " + currentValue + "%, #ddd " + currentValue + "%, #ddd 100%)",
|
|
24
|
+
outline: "none",
|
|
25
|
+
cursor: "pointer",
|
|
26
|
+
}, className: "nph-image-width-slider" })] }));
|
|
27
|
+
});
|
|
28
|
+
ImageBlockWidth.displayName = "ImageBlockWidth";
|
|
29
|
+
export default ImageBlockWidth;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ImageResizeHandleProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
onResize: (widthPercent: number) => void;
|
|
5
|
+
currentWidth?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function ImageResizeHandle({ children, onResize, currentWidth, }: ImageResizeHandleProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default ImageResizeHandle;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
export function ImageResizeHandle({ children, onResize, currentWidth, }) {
|
|
4
|
+
const containerRef = useRef(null);
|
|
5
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
6
|
+
const handleMouseDown = useCallback((e, side) => {
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
e.stopPropagation();
|
|
9
|
+
setIsResizing(true);
|
|
10
|
+
const startX = e.clientX;
|
|
11
|
+
const containerEl = containerRef.current;
|
|
12
|
+
if (!containerEl)
|
|
13
|
+
return;
|
|
14
|
+
const editorEl = containerEl.closest(".nph-editor") || containerEl.parentElement;
|
|
15
|
+
if (!editorEl)
|
|
16
|
+
return;
|
|
17
|
+
const editorWidth = editorEl.getBoundingClientRect().width;
|
|
18
|
+
const startWidth = containerEl.getBoundingClientRect().width;
|
|
19
|
+
const startPercent = (startWidth / editorWidth) * 100;
|
|
20
|
+
const handleMouseMove = (moveEvent) => {
|
|
21
|
+
const deltaX = moveEvent.clientX - startX;
|
|
22
|
+
const direction = side === "right" ? 1 : -1;
|
|
23
|
+
const deltaPercent = (deltaX * direction / editorWidth) * 100 * 2; // *2 because resizing from center
|
|
24
|
+
const newPercent = Math.max(10, Math.min(100, startPercent + deltaPercent));
|
|
25
|
+
onResize(Math.round(newPercent));
|
|
26
|
+
};
|
|
27
|
+
const handleMouseUp = () => {
|
|
28
|
+
setIsResizing(false);
|
|
29
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
30
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
33
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
34
|
+
}, [onResize]);
|
|
35
|
+
return (_jsxs("div", { ref: containerRef, className: `nph-resize-wrapper${isResizing ? " nph-resize-wrapper--active" : ""}`, children: [children, _jsx("div", { className: "nph-resize-handle nph-resize-handle--left", onMouseDown: (e) => handleMouseDown(e, "left") }), _jsx("div", { className: "nph-resize-handle nph-resize-handle--right", onMouseDown: (e) => handleMouseDown(e, "right") })] }));
|
|
36
|
+
}
|
|
37
|
+
export default ImageResizeHandle;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Editor } from "@tiptap/react";
|
|
2
|
+
export type ImageUploaderProps = {
|
|
3
|
+
onUpload: (url: string) => void;
|
|
4
|
+
editor: Editor;
|
|
5
|
+
};
|
|
6
|
+
export declare const ImageUploader: ({ onUpload, editor }: ImageUploaderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export default ImageUploader;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconPhoto, IconUpload } from "@tabler/icons-react";
|
|
3
|
+
import { useCallback, useRef, useState } from "react";
|
|
4
|
+
import { ImageBlockLoading } from "./ImageBlockLoading";
|
|
5
|
+
export const ImageUploader = ({ onUpload, editor }) => {
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const [draggedInside, setDraggedInside] = useState(false);
|
|
8
|
+
const fileInputRef = useRef(null);
|
|
9
|
+
const imageExtension = editor.extensionManager.extensions.find((ext) => ext.name === "imageBlock");
|
|
10
|
+
const browseAssets = imageExtension?.options?.browseAssets;
|
|
11
|
+
const uploadFile = useCallback(async (file) => {
|
|
12
|
+
setLoading(true);
|
|
13
|
+
try {
|
|
14
|
+
const uploadImage = imageExtension?.options?.uploadImage;
|
|
15
|
+
if (uploadImage) {
|
|
16
|
+
const url = await uploadImage(file);
|
|
17
|
+
onUpload(url);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.error("No uploadImage handler provided");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error("Failed to upload image:", error);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
}, [imageExtension, onUpload]);
|
|
30
|
+
const handleUploadClick = useCallback(() => {
|
|
31
|
+
fileInputRef.current?.click();
|
|
32
|
+
}, []);
|
|
33
|
+
const onFileChange = useCallback((e) => {
|
|
34
|
+
const file = e.target.files?.[0];
|
|
35
|
+
if (file) {
|
|
36
|
+
uploadFile(file);
|
|
37
|
+
}
|
|
38
|
+
}, [uploadFile]);
|
|
39
|
+
const onDrop = useCallback((e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
setDraggedInside(false);
|
|
43
|
+
const file = e.dataTransfer.files[0];
|
|
44
|
+
if (file && /image/i.test(file.type)) {
|
|
45
|
+
uploadFile(file);
|
|
46
|
+
}
|
|
47
|
+
}, [uploadFile]);
|
|
48
|
+
const onDragEnter = useCallback((e) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
setDraggedInside(true);
|
|
52
|
+
}, []);
|
|
53
|
+
const onDragLeave = useCallback((e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
setDraggedInside(false);
|
|
57
|
+
}, []);
|
|
58
|
+
if (loading) {
|
|
59
|
+
return _jsx(ImageBlockLoading, {});
|
|
60
|
+
}
|
|
61
|
+
// When browseAssets is provided, show a clean asset browser CTA
|
|
62
|
+
if (browseAssets) {
|
|
63
|
+
return (_jsx("div", { className: "nph-image-uploader nph-image-uploader--browse-only", contentEditable: false, onClick: () => browseAssets(onUpload), role: "button", tabIndex: 0, onKeyDown: (e) => {
|
|
64
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
browseAssets(onUpload);
|
|
67
|
+
}
|
|
68
|
+
}, children: _jsxs("div", { className: "nph-image-uploader__browse-cta", children: [_jsx("div", { className: "nph-image-uploader__browse-icon-wrapper", children: _jsx(IconPhoto, { size: 28 }) }), _jsxs("div", { className: "nph-image-uploader__browse-text", children: [_jsx("span", { className: "nph-image-uploader__browse-title", children: "Choose from assets" }), _jsx("span", { className: "nph-image-uploader__browse-subtitle", children: "Select an image from your library" })] })] }) }));
|
|
69
|
+
}
|
|
70
|
+
// Fallback: standard drag-and-drop / upload flow
|
|
71
|
+
return (_jsxs("div", { className: `nph-image-uploader${draggedInside ? " nph-image-uploader--dragging" : ""}`, onDrop: onDrop, onDragOver: onDragEnter, onDragLeave: onDragLeave, contentEditable: false, children: [_jsx(IconPhoto, { size: 48, className: "nph-image-uploader__icon" }), _jsxs("div", { className: "nph-image-uploader__content", children: [_jsx("div", { className: "nph-image-uploader__text", children: draggedInside ? "Drop image here" : "Drag and drop or" }), _jsx("div", { className: "nph-image-uploader__actions", children: _jsxs("button", { type: "button", disabled: draggedInside, onClick: handleUploadClick, className: "nph-btn nph-btn-ghost nph-btn-sm nph-image-uploader__button", children: [_jsx(IconUpload, { size: 16 }), "Upload an image"] }) })] }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".jpg,.jpeg,.png,.webp,.gif", onChange: onFileChange, className: "nph-image-uploader__input" })] }));
|
|
72
|
+
};
|
|
73
|
+
export default ImageUploader;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Editor as TiptapEditor } from "@tiptap/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
type ExtraRenderer = (editor: TiptapEditor) => ReactNode;
|
|
4
|
+
export type ImageMenuProps = {
|
|
5
|
+
className?: string;
|
|
6
|
+
leadingExtras?: ExtraRenderer[];
|
|
7
|
+
trailingExtras?: ExtraRenderer[];
|
|
8
|
+
};
|
|
9
|
+
export declare function ImageMenu({ className, leadingExtras, trailingExtras, }: ImageMenuProps): import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export default ImageMenu;
|