@bizapps/design-system-testing 1.6.0 → 1.7.2
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/editor.cjs +434 -202
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +94 -2
- package/dist/editor.d.ts +94 -2
- package/dist/editor.js +431 -205
- package/dist/editor.js.map +1 -1
- package/package.json +17 -17
package/dist/editor.cjs
CHANGED
|
@@ -11,8 +11,16 @@ var classVarianceAuthority = require('class-variance-authority');
|
|
|
11
11
|
var lucideReact = require('lucide-react');
|
|
12
12
|
var react$2 = require('motion/react');
|
|
13
13
|
var reactDropzone = require('react-dropzone');
|
|
14
|
-
var
|
|
14
|
+
var Highlight = require('@tiptap/extension-highlight');
|
|
15
|
+
var extensionTable = require('@tiptap/extension-table');
|
|
16
|
+
var StarterKit = require('@tiptap/starter-kit');
|
|
15
17
|
var extensionImage = require('@tiptap/extension-image');
|
|
18
|
+
var menus = require('@tiptap/react/menus');
|
|
19
|
+
|
|
20
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
21
|
+
|
|
22
|
+
var Highlight__default = /*#__PURE__*/_interopDefault(Highlight);
|
|
23
|
+
var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
|
|
16
24
|
|
|
17
25
|
// src/components/compounds/RichTextEditor/rich-text-editor.tsx
|
|
18
26
|
function cn(...inputs) {
|
|
@@ -816,6 +824,430 @@ function InsertImageDialog({
|
|
|
816
824
|
] })
|
|
817
825
|
] }) });
|
|
818
826
|
}
|
|
827
|
+
var DEFAULT_MIN_WIDTH = 120;
|
|
828
|
+
var DEFAULT_START_WIDTH = 400;
|
|
829
|
+
var DEFAULT_RATIO = 4 / 3;
|
|
830
|
+
var KEYBOARD_STEP = 20;
|
|
831
|
+
function ResizableImageView({ node, updateAttributes, selected, extension }) {
|
|
832
|
+
const { src, alt, width, height } = node.attrs;
|
|
833
|
+
const sizeRef = react.useRef({ width, height });
|
|
834
|
+
sizeRef.current = { width, height };
|
|
835
|
+
const { resizeLabel, minWidth = DEFAULT_MIN_WIDTH } = extension.options;
|
|
836
|
+
const dragRef = react.useRef(null);
|
|
837
|
+
const [dragging, setDragging] = react.useState(false);
|
|
838
|
+
const resizeTo = react.useCallback(
|
|
839
|
+
(nextWidth, ratio, maxWidth) => {
|
|
840
|
+
const clamped = Math.max(minWidth, Math.min(Math.round(nextWidth), maxWidth));
|
|
841
|
+
updateAttributes({ width: clamped, height: Math.round(clamped / ratio) });
|
|
842
|
+
},
|
|
843
|
+
[minWidth, updateAttributes]
|
|
844
|
+
);
|
|
845
|
+
const measure = react.useCallback((handle) => {
|
|
846
|
+
const image = handle.parentElement?.querySelector("img") ?? null;
|
|
847
|
+
const current = sizeRef.current;
|
|
848
|
+
const editable = handle.closest(".ProseMirror");
|
|
849
|
+
const available = editable?.clientWidth ?? 0;
|
|
850
|
+
const naturalWidth = image?.naturalWidth ?? 0;
|
|
851
|
+
const naturalHeight = image?.naturalHeight ?? 0;
|
|
852
|
+
const ratio = current.width > 0 && current.height > 0 ? current.width / current.height : naturalWidth > 0 && naturalHeight > 0 ? naturalWidth / naturalHeight : DEFAULT_RATIO;
|
|
853
|
+
const unbounded = current.width > 0 ? current.width : naturalWidth || DEFAULT_START_WIDTH;
|
|
854
|
+
const startWidth = available > 0 ? Math.min(unbounded, available) : unbounded;
|
|
855
|
+
return {
|
|
856
|
+
width: startWidth,
|
|
857
|
+
ratio,
|
|
858
|
+
// jsdom reports clientWidth as 0, so a zero measure means "no
|
|
859
|
+
// constraint" — the real clamp only applies in a browser.
|
|
860
|
+
maxWidth: available > 0 ? available : Infinity
|
|
861
|
+
};
|
|
862
|
+
}, []);
|
|
863
|
+
const imageRef = react.useRef(null);
|
|
864
|
+
const fittedSrcRef = react.useRef(null);
|
|
865
|
+
react.useEffect(() => {
|
|
866
|
+
const img = imageRef.current;
|
|
867
|
+
if (!img) return;
|
|
868
|
+
if (sizeRef.current.width > 0 || fittedSrcRef.current === src) return;
|
|
869
|
+
const fit = () => {
|
|
870
|
+
const available = img.closest(".ProseMirror")?.clientWidth ?? 0;
|
|
871
|
+
if (!img.naturalWidth || !img.naturalHeight || available <= 0) return;
|
|
872
|
+
fittedSrcRef.current = src;
|
|
873
|
+
const nextWidth = Math.min(img.naturalWidth, available);
|
|
874
|
+
updateAttributes({
|
|
875
|
+
width: nextWidth,
|
|
876
|
+
height: Math.round(nextWidth / (img.naturalWidth / img.naturalHeight))
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
if (img.complete) {
|
|
880
|
+
fit();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
img.addEventListener("load", fit, { once: true });
|
|
884
|
+
return () => img.removeEventListener("load", fit);
|
|
885
|
+
}, [src, updateAttributes]);
|
|
886
|
+
const onPointerDown = react.useCallback(
|
|
887
|
+
(event) => {
|
|
888
|
+
event.preventDefault();
|
|
889
|
+
event.stopPropagation();
|
|
890
|
+
const start = measure(event.currentTarget);
|
|
891
|
+
dragRef.current = { startX: event.clientX, ...start };
|
|
892
|
+
setDragging(true);
|
|
893
|
+
const onMove = (moveEvent) => {
|
|
894
|
+
const drag = dragRef.current;
|
|
895
|
+
if (!drag) return;
|
|
896
|
+
resizeTo(drag.width + (moveEvent.clientX - drag.startX), drag.ratio, drag.maxWidth);
|
|
897
|
+
};
|
|
898
|
+
const onUp = () => {
|
|
899
|
+
dragRef.current = null;
|
|
900
|
+
setDragging(false);
|
|
901
|
+
window.removeEventListener("pointermove", onMove);
|
|
902
|
+
window.removeEventListener("pointerup", onUp);
|
|
903
|
+
};
|
|
904
|
+
window.addEventListener("pointermove", onMove);
|
|
905
|
+
window.addEventListener("pointerup", onUp);
|
|
906
|
+
},
|
|
907
|
+
[measure, resizeTo]
|
|
908
|
+
);
|
|
909
|
+
const onKeyDown = react.useCallback(
|
|
910
|
+
(event) => {
|
|
911
|
+
const step = event.key === "ArrowRight" ? KEYBOARD_STEP : event.key === "ArrowLeft" ? -KEYBOARD_STEP : 0;
|
|
912
|
+
if (step === 0) return;
|
|
913
|
+
event.preventDefault();
|
|
914
|
+
const start = measure(event.currentTarget);
|
|
915
|
+
resizeTo(start.width + step, start.ratio, start.maxWidth);
|
|
916
|
+
},
|
|
917
|
+
[measure, resizeTo]
|
|
918
|
+
);
|
|
919
|
+
const sizeStyle = width > 0 ? { width, height: height > 0 ? height : void 0, maxWidth: "100%" } : void 0;
|
|
920
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
921
|
+
react$1.NodeViewWrapper,
|
|
922
|
+
{
|
|
923
|
+
"data-slot": "resizable-image",
|
|
924
|
+
className: "group relative inline-block max-w-full",
|
|
925
|
+
children: [
|
|
926
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
927
|
+
"img",
|
|
928
|
+
{
|
|
929
|
+
ref: imageRef,
|
|
930
|
+
src,
|
|
931
|
+
alt,
|
|
932
|
+
className: "max-w-full rounded-md",
|
|
933
|
+
style: sizeStyle,
|
|
934
|
+
draggable: false
|
|
935
|
+
}
|
|
936
|
+
),
|
|
937
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
938
|
+
"div",
|
|
939
|
+
{
|
|
940
|
+
role: "button",
|
|
941
|
+
tabIndex: 0,
|
|
942
|
+
"aria-label": resizeLabel,
|
|
943
|
+
title: resizeLabel,
|
|
944
|
+
"data-slot": "resizable-image-handle",
|
|
945
|
+
onPointerDown,
|
|
946
|
+
onKeyDown,
|
|
947
|
+
className: cn(
|
|
948
|
+
"border-primary absolute -right-1 -bottom-1 h-4 w-4 cursor-se-resize touch-none rounded-full border-2 bg-white transition-opacity",
|
|
949
|
+
"focus-visible:ring-ring/50 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:outline-none",
|
|
950
|
+
dragging || selected ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
)
|
|
954
|
+
]
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
function createResizableImage(options) {
|
|
959
|
+
return extensionImage.Image.extend({
|
|
960
|
+
addOptions() {
|
|
961
|
+
return { ...this.parent(), ...options };
|
|
962
|
+
},
|
|
963
|
+
// The size attributes belong to the extension, not to the consumer: the
|
|
964
|
+
// stock Image extension keeps width/height too, but they default to null
|
|
965
|
+
// and render as HTML attributes, which pins the image to a fixed size.
|
|
966
|
+
//
|
|
967
|
+
// A numeric default with `rendered: false` is what makes resizing work
|
|
968
|
+
// at all — Tiptap v3 gates DOM emission on `rendered`, so the size stays
|
|
969
|
+
// out of the markup and lives only in the document JSON, where a
|
|
970
|
+
// serializer can read it. Without these declarations the schema drops
|
|
971
|
+
// every updateAttributes({ width }) call and the handle does nothing.
|
|
972
|
+
addAttributes() {
|
|
973
|
+
return {
|
|
974
|
+
...this.parent?.(),
|
|
975
|
+
width: { default: 0, rendered: false },
|
|
976
|
+
height: { default: 0, rendered: false }
|
|
977
|
+
};
|
|
978
|
+
},
|
|
979
|
+
addNodeView() {
|
|
980
|
+
return react$1.ReactNodeViewRenderer(ResizableImageView);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function ListMenu({ editor, triggerLabel, icon, options }) {
|
|
985
|
+
const active = options.find((option) => editor?.isActive(option.node));
|
|
986
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
987
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
988
|
+
Button,
|
|
989
|
+
{
|
|
990
|
+
type: "button",
|
|
991
|
+
size: "sm",
|
|
992
|
+
variant: active ? "secondary" : "ghost",
|
|
993
|
+
disabled: !editor,
|
|
994
|
+
title: active ? active.label : triggerLabel,
|
|
995
|
+
"aria-label": active ? active.label : triggerLabel,
|
|
996
|
+
"data-slot": "list-menu-trigger",
|
|
997
|
+
children: [
|
|
998
|
+
active ? active.icon : icon,
|
|
999
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-3 w-3 opacity-60" })
|
|
1000
|
+
]
|
|
1001
|
+
}
|
|
1002
|
+
) }),
|
|
1003
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "start", children: options.map((option) => {
|
|
1004
|
+
const isActive = editor?.isActive(option.node) ?? false;
|
|
1005
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1006
|
+
DropdownMenuItem,
|
|
1007
|
+
{
|
|
1008
|
+
disabled: !editor,
|
|
1009
|
+
className: "cursor-pointer",
|
|
1010
|
+
onSelect: () => editor && option.run(editor),
|
|
1011
|
+
children: [
|
|
1012
|
+
option.icon,
|
|
1013
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: option.label }),
|
|
1014
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1015
|
+
lucideReact.Check,
|
|
1016
|
+
{
|
|
1017
|
+
"aria-hidden": "true",
|
|
1018
|
+
className: cn("h-4 w-4", isActive ? "opacity-100" : "opacity-0")
|
|
1019
|
+
}
|
|
1020
|
+
)
|
|
1021
|
+
]
|
|
1022
|
+
},
|
|
1023
|
+
option.node
|
|
1024
|
+
);
|
|
1025
|
+
}) })
|
|
1026
|
+
] });
|
|
1027
|
+
}
|
|
1028
|
+
function useRichTextEditor({
|
|
1029
|
+
labels,
|
|
1030
|
+
onUpdate,
|
|
1031
|
+
content,
|
|
1032
|
+
onInsertLink,
|
|
1033
|
+
onInsertImage,
|
|
1034
|
+
extraExtensions = [],
|
|
1035
|
+
extraToolbar = [],
|
|
1036
|
+
contentClassName,
|
|
1037
|
+
allowBase64Images = false,
|
|
1038
|
+
editorProps
|
|
1039
|
+
}) {
|
|
1040
|
+
const [linkNeedsText, setLinkNeedsText] = react.useState(false);
|
|
1041
|
+
const editor = react$1.useEditor({
|
|
1042
|
+
extensions: [
|
|
1043
|
+
// openOnClick false: a plain click has to place the caret so the
|
|
1044
|
+
// author can edit link text. Left at its default, Tiptap navigates
|
|
1045
|
+
// the current tab and throws the author out of the editor.
|
|
1046
|
+
StarterKit__default.default.configure({ link: { openOnClick: false } }),
|
|
1047
|
+
Highlight__default.default,
|
|
1048
|
+
extensionTable.TableKit.configure({ table: { resizable: true } }),
|
|
1049
|
+
createResizableImage({ resizeLabel: labels.resizeImage }).configure({
|
|
1050
|
+
inline: false,
|
|
1051
|
+
allowBase64: allowBase64Images
|
|
1052
|
+
}),
|
|
1053
|
+
...extraExtensions
|
|
1054
|
+
],
|
|
1055
|
+
content,
|
|
1056
|
+
immediatelyRender: false,
|
|
1057
|
+
// Required for the toolbar's pressed state and the image node view to
|
|
1058
|
+
// repaint as the selection and attributes change.
|
|
1059
|
+
shouldRerenderOnTransaction: true,
|
|
1060
|
+
editorProps: {
|
|
1061
|
+
...editorProps,
|
|
1062
|
+
attributes: {
|
|
1063
|
+
// ProseMirror's editable div needs its own accessible name: a
|
|
1064
|
+
// visible <Label> beside it is decoration, and a screen reader
|
|
1065
|
+
// would announce an unnamed box.
|
|
1066
|
+
role: "textbox",
|
|
1067
|
+
"aria-label": labels.editor,
|
|
1068
|
+
class: contentClassName ? `${EDITOR_CONTENT_STYLE} ${contentClassName}` : EDITOR_CONTENT_STYLE
|
|
1069
|
+
},
|
|
1070
|
+
// Ctrl/Cmd-click opens the link in a new tab and leaves the author
|
|
1071
|
+
// where they were: the shortcut is a deliberate gesture, and keeping
|
|
1072
|
+
// the editor open means no unsaved work is at risk. A plain click
|
|
1073
|
+
// falls through to ProseMirror and just moves the caret.
|
|
1074
|
+
handleClick(_view, _pos, event) {
|
|
1075
|
+
if (!event.ctrlKey && !event.metaKey) return false;
|
|
1076
|
+
const href = event.target?.closest("a")?.getAttribute("href");
|
|
1077
|
+
if (!href) return false;
|
|
1078
|
+
event.preventDefault();
|
|
1079
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
onUpdate: onUpdate ? ({ editor: current }) => onUpdate(current.getJSON()) : void 0
|
|
1084
|
+
});
|
|
1085
|
+
const currentLinkHref = react.useCallback(
|
|
1086
|
+
() => editor?.getAttributes("link").href ?? "",
|
|
1087
|
+
[editor]
|
|
1088
|
+
);
|
|
1089
|
+
const openLink = react.useCallback(() => {
|
|
1090
|
+
if (editor) {
|
|
1091
|
+
const { from, to } = editor.state.selection;
|
|
1092
|
+
setLinkNeedsText(from === to && !editor.isActive("link"));
|
|
1093
|
+
}
|
|
1094
|
+
onInsertLink?.();
|
|
1095
|
+
}, [editor, onInsertLink]);
|
|
1096
|
+
const applyLink = react.useCallback(
|
|
1097
|
+
(href, text) => {
|
|
1098
|
+
if (!editor) return;
|
|
1099
|
+
const url = href.trim();
|
|
1100
|
+
if (linkNeedsText) {
|
|
1101
|
+
if (url && text?.trim()) {
|
|
1102
|
+
editor.chain().focus().insertContent({
|
|
1103
|
+
type: "text",
|
|
1104
|
+
text: text.trim(),
|
|
1105
|
+
marks: [{ type: "link", attrs: { href: url } }]
|
|
1106
|
+
}).run();
|
|
1107
|
+
}
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (url) editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
1111
|
+
else editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
1112
|
+
},
|
|
1113
|
+
[editor, linkNeedsText]
|
|
1114
|
+
);
|
|
1115
|
+
const toolbar = react.useMemo(() => {
|
|
1116
|
+
const builtIn = [
|
|
1117
|
+
{
|
|
1118
|
+
kind: "button",
|
|
1119
|
+
id: "bold",
|
|
1120
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Bold, { className: "h-4 w-4" }),
|
|
1121
|
+
label: labels.bold,
|
|
1122
|
+
active: { name: "bold" },
|
|
1123
|
+
run: (e) => e.chain().focus().toggleBold().run()
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
kind: "button",
|
|
1127
|
+
id: "italic",
|
|
1128
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Italic, { className: "h-4 w-4" }),
|
|
1129
|
+
label: labels.italic,
|
|
1130
|
+
active: { name: "italic" },
|
|
1131
|
+
run: (e) => e.chain().focus().toggleItalic().run()
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
kind: "button",
|
|
1135
|
+
id: "heading",
|
|
1136
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading2, { className: "h-4 w-4" }),
|
|
1137
|
+
label: labels.heading,
|
|
1138
|
+
active: { name: "heading", attrs: { level: 2 } },
|
|
1139
|
+
run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run()
|
|
1140
|
+
},
|
|
1141
|
+
{ kind: "separator" },
|
|
1142
|
+
{
|
|
1143
|
+
kind: "button",
|
|
1144
|
+
id: "highlight",
|
|
1145
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Highlighter, { className: "h-4 w-4" }),
|
|
1146
|
+
label: labels.highlight,
|
|
1147
|
+
active: { name: "highlight" },
|
|
1148
|
+
run: (e) => e.chain().focus().toggleHighlight().run()
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
kind: "button",
|
|
1152
|
+
id: "underline",
|
|
1153
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Underline, { className: "h-4 w-4" }),
|
|
1154
|
+
label: labels.underline,
|
|
1155
|
+
active: { name: "underline" },
|
|
1156
|
+
run: (e) => e.chain().focus().toggleMark("underline").run()
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
kind: "button",
|
|
1160
|
+
id: "strike",
|
|
1161
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Strikethrough, { className: "h-4 w-4" }),
|
|
1162
|
+
label: labels.strikethrough,
|
|
1163
|
+
active: { name: "strike" },
|
|
1164
|
+
run: (e) => e.chain().focus().toggleMark("strike").run()
|
|
1165
|
+
},
|
|
1166
|
+
{ kind: "separator" },
|
|
1167
|
+
{
|
|
1168
|
+
kind: "button",
|
|
1169
|
+
id: "quote",
|
|
1170
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Quote, { className: "h-4 w-4" }),
|
|
1171
|
+
label: labels.quote,
|
|
1172
|
+
active: { name: "blockquote" },
|
|
1173
|
+
run: (e) => e.chain().focus().toggleBlockquote().run()
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
kind: "custom",
|
|
1177
|
+
id: "lists",
|
|
1178
|
+
render: (current) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1179
|
+
ListMenu,
|
|
1180
|
+
{
|
|
1181
|
+
editor: current,
|
|
1182
|
+
triggerLabel: labels.lists.trigger,
|
|
1183
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { className: "h-4 w-4" }),
|
|
1184
|
+
options: [
|
|
1185
|
+
{
|
|
1186
|
+
node: "bulletList",
|
|
1187
|
+
label: labels.lists.bullet,
|
|
1188
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { className: "h-4 w-4" }),
|
|
1189
|
+
run: (e) => e.chain().focus().toggleBulletList().run()
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
node: "orderedList",
|
|
1193
|
+
label: labels.lists.ordered,
|
|
1194
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ListOrdered, { className: "h-4 w-4" }),
|
|
1195
|
+
run: (e) => e.chain().focus().toggleOrderedList().run()
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
}
|
|
1199
|
+
)
|
|
1200
|
+
},
|
|
1201
|
+
{ kind: "separator" },
|
|
1202
|
+
{
|
|
1203
|
+
kind: "button",
|
|
1204
|
+
id: "link",
|
|
1205
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link, { className: "h-4 w-4" }),
|
|
1206
|
+
label: labels.insertLink,
|
|
1207
|
+
active: { name: "link" },
|
|
1208
|
+
run: () => openLink()
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
kind: "button",
|
|
1212
|
+
id: "image",
|
|
1213
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "h-4 w-4" }),
|
|
1214
|
+
label: labels.insertImage,
|
|
1215
|
+
// No `active`: this opens a dialog, it is never a pressed state.
|
|
1216
|
+
run: () => onInsertImage?.()
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
kind: "custom",
|
|
1220
|
+
id: "table",
|
|
1221
|
+
render: (current) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1222
|
+
TableMenu,
|
|
1223
|
+
{
|
|
1224
|
+
editor: current,
|
|
1225
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Table, { className: "h-4 w-4" }),
|
|
1226
|
+
labels: labels.table
|
|
1227
|
+
}
|
|
1228
|
+
)
|
|
1229
|
+
}
|
|
1230
|
+
];
|
|
1231
|
+
let items = builtIn;
|
|
1232
|
+
const appended = [];
|
|
1233
|
+
for (const group of extraToolbar) {
|
|
1234
|
+
if (!group.after) {
|
|
1235
|
+
appended.push(...group.items);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const at = items.findIndex(
|
|
1239
|
+
(item) => item.kind !== "separator" && item.id === group.after
|
|
1240
|
+
);
|
|
1241
|
+
if (at === -1) {
|
|
1242
|
+
appended.push(...group.items);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
items = [...items.slice(0, at + 1), ...group.items, ...items.slice(at + 1)];
|
|
1246
|
+
}
|
|
1247
|
+
return [...items, ...appended];
|
|
1248
|
+
}, [labels, extraToolbar, onInsertImage, openLink]);
|
|
1249
|
+
return { editor, toolbar, linkNeedsText, currentLinkHref, applyLink };
|
|
1250
|
+
}
|
|
819
1251
|
function ColorMenu({ editor, labels, icon, swatches }) {
|
|
820
1252
|
const active = swatches.find((swatch) => editor?.isActive(swatch.mark));
|
|
821
1253
|
function apply(mark) {
|
|
@@ -885,50 +1317,6 @@ function ColorMenu({ editor, labels, icon, swatches }) {
|
|
|
885
1317
|
) })
|
|
886
1318
|
] });
|
|
887
1319
|
}
|
|
888
|
-
function ListMenu({ editor, triggerLabel, icon, options }) {
|
|
889
|
-
const active = options.find((option) => editor?.isActive(option.node));
|
|
890
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
891
|
-
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
892
|
-
Button,
|
|
893
|
-
{
|
|
894
|
-
type: "button",
|
|
895
|
-
size: "sm",
|
|
896
|
-
variant: active ? "secondary" : "ghost",
|
|
897
|
-
disabled: !editor,
|
|
898
|
-
title: active ? active.label : triggerLabel,
|
|
899
|
-
"aria-label": active ? active.label : triggerLabel,
|
|
900
|
-
"data-slot": "list-menu-trigger",
|
|
901
|
-
children: [
|
|
902
|
-
active ? active.icon : icon,
|
|
903
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-3 w-3 opacity-60" })
|
|
904
|
-
]
|
|
905
|
-
}
|
|
906
|
-
) }),
|
|
907
|
-
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "start", children: options.map((option) => {
|
|
908
|
-
const isActive = editor?.isActive(option.node) ?? false;
|
|
909
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
910
|
-
DropdownMenuItem,
|
|
911
|
-
{
|
|
912
|
-
disabled: !editor,
|
|
913
|
-
className: "cursor-pointer",
|
|
914
|
-
onSelect: () => editor && option.run(editor),
|
|
915
|
-
children: [
|
|
916
|
-
option.icon,
|
|
917
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: option.label }),
|
|
918
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
919
|
-
lucideReact.Check,
|
|
920
|
-
{
|
|
921
|
-
"aria-hidden": "true",
|
|
922
|
-
className: cn("h-4 w-4", isActive ? "opacity-100" : "opacity-0")
|
|
923
|
-
}
|
|
924
|
-
)
|
|
925
|
-
]
|
|
926
|
-
},
|
|
927
|
-
option.node
|
|
928
|
-
);
|
|
929
|
-
}) })
|
|
930
|
-
] });
|
|
931
|
-
}
|
|
932
1320
|
function InsertLinkDialog({
|
|
933
1321
|
open,
|
|
934
1322
|
onOpenChange,
|
|
@@ -1070,163 +1458,6 @@ function LinkBubbleMenu({ editor, labels, onEdit, onOpen }) {
|
|
|
1070
1458
|
if (!editor) return null;
|
|
1071
1459
|
return /* @__PURE__ */ jsxRuntime.jsx(menus.BubbleMenu, { editor, shouldShow, options, children: /* @__PURE__ */ jsxRuntime.jsx(LinkActions, { editor, labels, onEdit, onOpen }) });
|
|
1072
1460
|
}
|
|
1073
|
-
var DEFAULT_MIN_WIDTH = 120;
|
|
1074
|
-
var DEFAULT_START_WIDTH = 400;
|
|
1075
|
-
var DEFAULT_RATIO = 4 / 3;
|
|
1076
|
-
var KEYBOARD_STEP = 20;
|
|
1077
|
-
function ResizableImageView({ node, updateAttributes, selected, extension }) {
|
|
1078
|
-
const { src, alt, width, height } = node.attrs;
|
|
1079
|
-
const sizeRef = react.useRef({ width, height });
|
|
1080
|
-
sizeRef.current = { width, height };
|
|
1081
|
-
const { resizeLabel, minWidth = DEFAULT_MIN_WIDTH } = extension.options;
|
|
1082
|
-
const dragRef = react.useRef(null);
|
|
1083
|
-
const [dragging, setDragging] = react.useState(false);
|
|
1084
|
-
const resizeTo = react.useCallback(
|
|
1085
|
-
(nextWidth, ratio, maxWidth) => {
|
|
1086
|
-
const clamped = Math.max(minWidth, Math.min(Math.round(nextWidth), maxWidth));
|
|
1087
|
-
updateAttributes({ width: clamped, height: Math.round(clamped / ratio) });
|
|
1088
|
-
},
|
|
1089
|
-
[minWidth, updateAttributes]
|
|
1090
|
-
);
|
|
1091
|
-
const measure = react.useCallback((handle) => {
|
|
1092
|
-
const image = handle.parentElement?.querySelector("img") ?? null;
|
|
1093
|
-
const current = sizeRef.current;
|
|
1094
|
-
const editable = handle.closest(".ProseMirror");
|
|
1095
|
-
const available = editable?.clientWidth ?? 0;
|
|
1096
|
-
const naturalWidth = image?.naturalWidth ?? 0;
|
|
1097
|
-
const naturalHeight = image?.naturalHeight ?? 0;
|
|
1098
|
-
const ratio = current.width > 0 && current.height > 0 ? current.width / current.height : naturalWidth > 0 && naturalHeight > 0 ? naturalWidth / naturalHeight : DEFAULT_RATIO;
|
|
1099
|
-
const unbounded = current.width > 0 ? current.width : naturalWidth || DEFAULT_START_WIDTH;
|
|
1100
|
-
const startWidth = available > 0 ? Math.min(unbounded, available) : unbounded;
|
|
1101
|
-
return {
|
|
1102
|
-
width: startWidth,
|
|
1103
|
-
ratio,
|
|
1104
|
-
// jsdom reports clientWidth as 0, so a zero measure means "no
|
|
1105
|
-
// constraint" — the real clamp only applies in a browser.
|
|
1106
|
-
maxWidth: available > 0 ? available : Infinity
|
|
1107
|
-
};
|
|
1108
|
-
}, []);
|
|
1109
|
-
const imageRef = react.useRef(null);
|
|
1110
|
-
const fittedSrcRef = react.useRef(null);
|
|
1111
|
-
react.useEffect(() => {
|
|
1112
|
-
const img = imageRef.current;
|
|
1113
|
-
if (!img) return;
|
|
1114
|
-
if (sizeRef.current.width > 0 || fittedSrcRef.current === src) return;
|
|
1115
|
-
const fit = () => {
|
|
1116
|
-
const available = img.closest(".ProseMirror")?.clientWidth ?? 0;
|
|
1117
|
-
if (!img.naturalWidth || !img.naturalHeight || available <= 0) return;
|
|
1118
|
-
fittedSrcRef.current = src;
|
|
1119
|
-
const nextWidth = Math.min(img.naturalWidth, available);
|
|
1120
|
-
updateAttributes({
|
|
1121
|
-
width: nextWidth,
|
|
1122
|
-
height: Math.round(nextWidth / (img.naturalWidth / img.naturalHeight))
|
|
1123
|
-
});
|
|
1124
|
-
};
|
|
1125
|
-
if (img.complete) {
|
|
1126
|
-
fit();
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
img.addEventListener("load", fit, { once: true });
|
|
1130
|
-
return () => img.removeEventListener("load", fit);
|
|
1131
|
-
}, [src, updateAttributes]);
|
|
1132
|
-
const onPointerDown = react.useCallback(
|
|
1133
|
-
(event) => {
|
|
1134
|
-
event.preventDefault();
|
|
1135
|
-
event.stopPropagation();
|
|
1136
|
-
const start = measure(event.currentTarget);
|
|
1137
|
-
dragRef.current = { startX: event.clientX, ...start };
|
|
1138
|
-
setDragging(true);
|
|
1139
|
-
const onMove = (moveEvent) => {
|
|
1140
|
-
const drag = dragRef.current;
|
|
1141
|
-
if (!drag) return;
|
|
1142
|
-
resizeTo(drag.width + (moveEvent.clientX - drag.startX), drag.ratio, drag.maxWidth);
|
|
1143
|
-
};
|
|
1144
|
-
const onUp = () => {
|
|
1145
|
-
dragRef.current = null;
|
|
1146
|
-
setDragging(false);
|
|
1147
|
-
window.removeEventListener("pointermove", onMove);
|
|
1148
|
-
window.removeEventListener("pointerup", onUp);
|
|
1149
|
-
};
|
|
1150
|
-
window.addEventListener("pointermove", onMove);
|
|
1151
|
-
window.addEventListener("pointerup", onUp);
|
|
1152
|
-
},
|
|
1153
|
-
[measure, resizeTo]
|
|
1154
|
-
);
|
|
1155
|
-
const onKeyDown = react.useCallback(
|
|
1156
|
-
(event) => {
|
|
1157
|
-
const step = event.key === "ArrowRight" ? KEYBOARD_STEP : event.key === "ArrowLeft" ? -KEYBOARD_STEP : 0;
|
|
1158
|
-
if (step === 0) return;
|
|
1159
|
-
event.preventDefault();
|
|
1160
|
-
const start = measure(event.currentTarget);
|
|
1161
|
-
resizeTo(start.width + step, start.ratio, start.maxWidth);
|
|
1162
|
-
},
|
|
1163
|
-
[measure, resizeTo]
|
|
1164
|
-
);
|
|
1165
|
-
const sizeStyle = width > 0 ? { width, height: height > 0 ? height : void 0, maxWidth: "100%" } : void 0;
|
|
1166
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1167
|
-
react$1.NodeViewWrapper,
|
|
1168
|
-
{
|
|
1169
|
-
"data-slot": "resizable-image",
|
|
1170
|
-
className: "group relative inline-block max-w-full",
|
|
1171
|
-
children: [
|
|
1172
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1173
|
-
"img",
|
|
1174
|
-
{
|
|
1175
|
-
ref: imageRef,
|
|
1176
|
-
src,
|
|
1177
|
-
alt,
|
|
1178
|
-
className: "max-w-full rounded-md",
|
|
1179
|
-
style: sizeStyle,
|
|
1180
|
-
draggable: false
|
|
1181
|
-
}
|
|
1182
|
-
),
|
|
1183
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1184
|
-
"div",
|
|
1185
|
-
{
|
|
1186
|
-
role: "button",
|
|
1187
|
-
tabIndex: 0,
|
|
1188
|
-
"aria-label": resizeLabel,
|
|
1189
|
-
title: resizeLabel,
|
|
1190
|
-
"data-slot": "resizable-image-handle",
|
|
1191
|
-
onPointerDown,
|
|
1192
|
-
onKeyDown,
|
|
1193
|
-
className: cn(
|
|
1194
|
-
"border-primary absolute -right-1 -bottom-1 h-4 w-4 cursor-se-resize touch-none rounded-full border-2 bg-white transition-opacity",
|
|
1195
|
-
"focus-visible:ring-ring/50 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:outline-none",
|
|
1196
|
-
dragging || selected ? "opacity-100" : "opacity-0 group-hover:opacity-100"
|
|
1197
|
-
)
|
|
1198
|
-
}
|
|
1199
|
-
)
|
|
1200
|
-
]
|
|
1201
|
-
}
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
function createResizableImage(options) {
|
|
1205
|
-
return extensionImage.Image.extend({
|
|
1206
|
-
addOptions() {
|
|
1207
|
-
return { ...this.parent(), ...options };
|
|
1208
|
-
},
|
|
1209
|
-
// The size attributes belong to the extension, not to the consumer: the
|
|
1210
|
-
// stock Image extension keeps width/height too, but they default to null
|
|
1211
|
-
// and render as HTML attributes, which pins the image to a fixed size.
|
|
1212
|
-
//
|
|
1213
|
-
// A numeric default with `rendered: false` is what makes resizing work
|
|
1214
|
-
// at all — Tiptap v3 gates DOM emission on `rendered`, so the size stays
|
|
1215
|
-
// out of the markup and lives only in the document JSON, where a
|
|
1216
|
-
// serializer can read it. Without these declarations the schema drops
|
|
1217
|
-
// every updateAttributes({ width }) call and the handle does nothing.
|
|
1218
|
-
addAttributes() {
|
|
1219
|
-
return {
|
|
1220
|
-
...this.parent?.(),
|
|
1221
|
-
width: { default: 0, rendered: false },
|
|
1222
|
-
height: { default: 0, rendered: false }
|
|
1223
|
-
};
|
|
1224
|
-
},
|
|
1225
|
-
addNodeView() {
|
|
1226
|
-
return react$1.ReactNodeViewRenderer(ResizableImageView);
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
1461
|
|
|
1231
1462
|
exports.ColorMenu = ColorMenu;
|
|
1232
1463
|
exports.EDITOR_CONTENT_STYLE = EDITOR_CONTENT_STYLE;
|
|
@@ -1238,5 +1469,6 @@ exports.RichTextEditor = RichTextEditor;
|
|
|
1238
1469
|
exports.RichTextToolbar = RichTextToolbar;
|
|
1239
1470
|
exports.TableMenu = TableMenu;
|
|
1240
1471
|
exports.createResizableImage = createResizableImage;
|
|
1472
|
+
exports.useRichTextEditor = useRichTextEditor;
|
|
1241
1473
|
//# sourceMappingURL=editor.cjs.map
|
|
1242
1474
|
//# sourceMappingURL=editor.cjs.map
|