@thebes/cadmea 1.2.0 → 1.3.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/RichTextEditor-CPQTvhQD.js +190 -0
- package/dist/RichTextEditor-CPQTvhQD.js.map +1 -0
- package/dist/RichTextEditor-ComcBFfl.js +334 -0
- package/dist/RichTextEditor-ComcBFfl.js.map +1 -0
- package/dist/index/index.js +5 -2
- package/dist/index/index.js.map +1 -1
- package/dist/index/server.js +5 -2
- package/dist/index/server.js.map +1 -1
- package/dist/tanstack-start/index.js +5 -2
- package/dist/tanstack-start/index.js.map +1 -1
- package/dist/tanstack-start/server.js +5 -2
- package/dist/tanstack-start/server.js.map +1 -1
- package/package.json +2 -1
- package/dist/RichTextEditor-BPilh7Pw.js +0 -36
- package/dist/RichTextEditor-BPilh7Pw.js.map +0 -1
- package/dist/RichTextEditor-DcLqdFY7.js +0 -15
- package/dist/RichTextEditor-DcLqdFY7.js.map +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createComponent, escape, ssr, ssrAttribute, ssrStyleProperty } from "solid-js/web";
|
|
2
|
+
import { For, Show, createSignal, onCleanup, onMount } from "solid-js";
|
|
3
|
+
import "@tiptap/core";
|
|
4
|
+
import "@tiptap/extension-image";
|
|
5
|
+
import "@tiptap/starter-kit";
|
|
6
|
+
//#region src/RichTextEditor.tsx
|
|
7
|
+
var _tmpl$ = "<span class=\"font-bold\">B</span>", _tmpl$2 = "<span class=\"italic\">I</span>", _tmpl$3 = "<span class=\"underline\">U</span>", _tmpl$4 = "<span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\"></span>", _tmpl$5 = [
|
|
8
|
+
"<div role=\"menu\" aria-label=\"Insert block\" class=\"bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow\" style=\"",
|
|
9
|
+
"\">",
|
|
10
|
+
"</div>"
|
|
11
|
+
], _tmpl$6 = "<input type=\"file\" accept=\"image/*\" class=\"hidden\">", _tmpl$7 = [
|
|
12
|
+
"<div class=\"border-base-300 rounded-box border\"><div class=\"border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1\">",
|
|
13
|
+
"",
|
|
14
|
+
"",
|
|
15
|
+
"",
|
|
16
|
+
"<span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\"></span>",
|
|
17
|
+
"",
|
|
18
|
+
"",
|
|
19
|
+
"",
|
|
20
|
+
"",
|
|
21
|
+
"",
|
|
22
|
+
"",
|
|
23
|
+
"</div><div class=\"relative\"><div",
|
|
24
|
+
" class=\"min-h-32 p-3\"></div>",
|
|
25
|
+
"</div>",
|
|
26
|
+
"</div>"
|
|
27
|
+
], _tmpl$8 = [
|
|
28
|
+
"<button type=\"button\" role=\"menuitem\" class=\"",
|
|
29
|
+
"\">",
|
|
30
|
+
"</button>"
|
|
31
|
+
], _tmpl$9 = [
|
|
32
|
+
"<button type=\"button\"",
|
|
33
|
+
" class=\"",
|
|
34
|
+
"\">",
|
|
35
|
+
"</button>"
|
|
36
|
+
];
|
|
37
|
+
function filterSlashItems(items, query) {
|
|
38
|
+
const q = query.toLowerCase();
|
|
39
|
+
if (!q) return items;
|
|
40
|
+
return items.filter((it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q));
|
|
41
|
+
}
|
|
42
|
+
function RichTextEditor(props) {
|
|
43
|
+
const [tick, setTick] = createSignal(0);
|
|
44
|
+
const [slash, setSlash] = createSignal(null);
|
|
45
|
+
const [activeIdx, setActiveIdx] = createSignal(0);
|
|
46
|
+
const slashItems = () => {
|
|
47
|
+
const items = [
|
|
48
|
+
{
|
|
49
|
+
label: "Heading 2",
|
|
50
|
+
keywords: "h2 title heading",
|
|
51
|
+
run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run()
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "Heading 3",
|
|
55
|
+
keywords: "h3 subtitle heading",
|
|
56
|
+
run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run()
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "Bullet list",
|
|
60
|
+
keywords: "ul unordered bullets",
|
|
61
|
+
run: (e) => e.chain().focus().toggleBulletList().run()
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Numbered list",
|
|
65
|
+
keywords: "ol ordered numbers",
|
|
66
|
+
run: (e) => e.chain().focus().toggleOrderedList().run()
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "Quote",
|
|
70
|
+
keywords: "blockquote citation",
|
|
71
|
+
run: (e) => e.chain().focus().toggleBlockquote().run()
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: "Divider",
|
|
75
|
+
keywords: "hr rule separator line",
|
|
76
|
+
run: (e) => e.chain().focus().setHorizontalRule().run()
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
if (props.onUploadFile) items.push({
|
|
80
|
+
label: "Image",
|
|
81
|
+
keywords: "img photo picture upload",
|
|
82
|
+
run: () => void 0
|
|
83
|
+
});
|
|
84
|
+
return items;
|
|
85
|
+
};
|
|
86
|
+
const filteredSlash = () => {
|
|
87
|
+
const s = slash();
|
|
88
|
+
return s ? filterSlashItems(slashItems(), s.query) : [];
|
|
89
|
+
};
|
|
90
|
+
onMount(() => {});
|
|
91
|
+
onCleanup(() => void 0);
|
|
92
|
+
const isActive = (name, attrs) => {
|
|
93
|
+
tick();
|
|
94
|
+
return Boolean(void 0);
|
|
95
|
+
};
|
|
96
|
+
function setLink() {}
|
|
97
|
+
return ssr(_tmpl$7, escape(createComponent(ToolbarButton, {
|
|
98
|
+
label: "Bold",
|
|
99
|
+
onClick: () => void 0,
|
|
100
|
+
active: () => isActive("bold"),
|
|
101
|
+
get children() {
|
|
102
|
+
return ssr(_tmpl$);
|
|
103
|
+
}
|
|
104
|
+
})), escape(createComponent(ToolbarButton, {
|
|
105
|
+
label: "Italic",
|
|
106
|
+
onClick: () => void 0,
|
|
107
|
+
active: () => isActive("italic"),
|
|
108
|
+
get children() {
|
|
109
|
+
return ssr(_tmpl$2);
|
|
110
|
+
}
|
|
111
|
+
})), escape(createComponent(ToolbarButton, {
|
|
112
|
+
label: "Underline",
|
|
113
|
+
onClick: () => void 0,
|
|
114
|
+
active: () => isActive("underline"),
|
|
115
|
+
get children() {
|
|
116
|
+
return ssr(_tmpl$3);
|
|
117
|
+
}
|
|
118
|
+
})), escape(createComponent(ToolbarButton, {
|
|
119
|
+
label: "Link",
|
|
120
|
+
onClick: setLink,
|
|
121
|
+
active: () => isActive("link"),
|
|
122
|
+
children: "Link"
|
|
123
|
+
})), escape(createComponent(ToolbarButton, {
|
|
124
|
+
label: "Heading 2",
|
|
125
|
+
onClick: () => void 0,
|
|
126
|
+
active: () => isActive("heading", { level: 2 }),
|
|
127
|
+
children: "H2"
|
|
128
|
+
})), escape(createComponent(ToolbarButton, {
|
|
129
|
+
label: "Heading 3",
|
|
130
|
+
onClick: () => void 0,
|
|
131
|
+
active: () => isActive("heading", { level: 3 }),
|
|
132
|
+
children: "H3"
|
|
133
|
+
})), escape(createComponent(ToolbarButton, {
|
|
134
|
+
label: "Bullet list",
|
|
135
|
+
onClick: () => void 0,
|
|
136
|
+
active: () => isActive("bulletList"),
|
|
137
|
+
children: "•"
|
|
138
|
+
})), escape(createComponent(ToolbarButton, {
|
|
139
|
+
label: "Numbered list",
|
|
140
|
+
onClick: () => void 0,
|
|
141
|
+
active: () => isActive("orderedList"),
|
|
142
|
+
children: "1."
|
|
143
|
+
})), escape(createComponent(ToolbarButton, {
|
|
144
|
+
label: "Quote",
|
|
145
|
+
onClick: () => void 0,
|
|
146
|
+
active: () => isActive("blockquote"),
|
|
147
|
+
children: "❝"
|
|
148
|
+
})), escape(createComponent(ToolbarButton, {
|
|
149
|
+
label: "Divider",
|
|
150
|
+
onClick: () => void 0,
|
|
151
|
+
children: "—"
|
|
152
|
+
})), escape(createComponent(Show, {
|
|
153
|
+
get when() {
|
|
154
|
+
return props.onUploadFile;
|
|
155
|
+
},
|
|
156
|
+
get children() {
|
|
157
|
+
return [ssr(_tmpl$4), createComponent(ToolbarButton, {
|
|
158
|
+
label: "Image",
|
|
159
|
+
onClick: () => void 0,
|
|
160
|
+
children: "Image"
|
|
161
|
+
})];
|
|
162
|
+
}
|
|
163
|
+
})), ssrAttribute("id", escape(props.id, true), false), escape(createComponent(Show, {
|
|
164
|
+
get when() {
|
|
165
|
+
return slash() && filteredSlash().length > 0;
|
|
166
|
+
},
|
|
167
|
+
get children() {
|
|
168
|
+
return ssr(_tmpl$5, ssrStyleProperty("left:", `${escape(slash()?.left ?? 0, true)}px`) + ssrStyleProperty(";top:", `${escape(slash()?.top ?? 0, true) + 4}px`), escape(createComponent(For, {
|
|
169
|
+
get each() {
|
|
170
|
+
return filteredSlash();
|
|
171
|
+
},
|
|
172
|
+
children: (item, i) => ssr(_tmpl$8, `rounded px-3 py-1.5 text-left text-sm ${i() === activeIdx() ? "bg-base-200" : ""}`, escape(item.label))
|
|
173
|
+
})));
|
|
174
|
+
}
|
|
175
|
+
})), escape(createComponent(Show, {
|
|
176
|
+
get when() {
|
|
177
|
+
return props.onUploadFile;
|
|
178
|
+
},
|
|
179
|
+
get children() {
|
|
180
|
+
return ssr(_tmpl$6);
|
|
181
|
+
}
|
|
182
|
+
})));
|
|
183
|
+
}
|
|
184
|
+
function ToolbarButton(props) {
|
|
185
|
+
return ssr(_tmpl$9, ssrAttribute("aria-label", escape(props.label, true), false) + ssrAttribute("aria-pressed", escape(props.active?.() ?? false, true), false) + ssrAttribute("title", escape(props.label, true), false), `btn btn-ghost btn-xs ${props.active?.() ?? false ? "btn-active" : ""}`, escape(props.children));
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { RichTextEditor };
|
|
189
|
+
|
|
190
|
+
//# sourceMappingURL=RichTextEditor-CPQTvhQD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RichTextEditor-CPQTvhQD.js","names":["Editor","Image","StarterKit","createSignal","For","JSX","onCleanup","onMount","Show","RichTextEditorProps","id","content","onChange","doc","onUploadFile","file","File","Promise","url","SlashItem","label","keywords","run","editor","matchSlashQuery","textBeforeCursor","m","match","filterSlashItems","items","query","q","toLowerCase","filter","it","includes","SlashState","from","to","left","top","RichTextEditor","props","container","HTMLDivElement","fileInput","HTMLInputElement","tick","setTick","bump","t","slash","setSlash","activeIdx","setActiveIdx","slashItems","e","chain","focus","toggleHeading","level","toggleBulletList","toggleOrderedList","toggleBlockquote","setHorizontalRule","push","click","filteredSlash","s","detectSlash","state","sel","selection","empty","$from","textBefore","parent","textBetween","parentOffset","undefined","start","coords","view","coordsAtPos","rect","getBoundingClientRect","bottom","runSlashItem","item","deleteRange","element","extensions","configure","link","openOnClick","editorProps","attributes","class","handleKeyDown","_view","event","key","i","Math","min","length","max","onUpdate","current","getJSON","onSelectionUpdate","destroy","isActive","name","attrs","Record","Boolean","setLink","prev","getAttributes","href","window","prompt","extendMarkRange","unsetLink","handleImageFile","Event","currentTarget","files","setImage","src","value","_$ssr","_tmpl$7","_$escape","_$createComponent","ToolbarButton","onClick","toggleBold","active","children","_tmpl$","toggleItalic","_tmpl$2","toggleUnderline","_tmpl$3","when","_tmpl$4","_$ssrAttribute","_tmpl$5","_$ssrStyleProperty","each","_tmpl$8","_tmpl$6","Element","_tmpl$9"],"sources":["../src/RichTextEditor.tsx"],"sourcesContent":["import { Editor } from \"@tiptap/core\";\nimport Image from \"@tiptap/extension-image\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n createSignal,\n For,\n type JSX,\n onCleanup,\n onMount,\n Show,\n} from \"solid-js\";\n\nexport interface RichTextEditorProps {\n id?: string;\n /** TipTap's native JSON document shape — stored as-is, no transform layer. */\n content?: object;\n onChange: (doc: object) => void;\n /**\n * Resolves a picked image file to a stored URL (same contract as the form's\n * upload fields). When provided, the toolbar and slash menu expose an\n * \"Image\" insert; omitted, image insertion is hidden.\n */\n onUploadFile?: (file: File) => Promise<{ url: string }>;\n}\n\ninterface SlashItem {\n label: string;\n /** Extra search terms so e.g. \"ul\" finds \"Bullet list\". */\n keywords: string;\n run: (editor: Editor) => void;\n}\n\n// Pure: a slash menu opens only when the current block's text up to the\n// cursor is a bare `/` followed by an optional word (Notion/Ghost-style, at\n// the start of an empty-ish block). Returns the query (may be \"\") or null.\n// Exported for unit testing without a live ProseMirror view.\nexport function matchSlashQuery(textBeforeCursor: string): string | null {\n const m = textBeforeCursor.match(/^\\/(\\w*)$/);\n return m ? m[1] : null;\n}\n\n// Pure: filter slash items by label or keywords against a (lowercased) query.\nexport function filterSlashItems(\n items: SlashItem[],\n query: string,\n): SlashItem[] {\n const q = query.toLowerCase();\n if (!q) return items;\n return items.filter(\n (it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q),\n );\n}\n\ninterface SlashState {\n from: number;\n to: number;\n query: string;\n left: number;\n top: number;\n}\n\n// No official Solid binding for TipTap exists, so this wraps @tiptap/core's\n// vanilla `Editor` class directly in Solid's onMount/onCleanup lifecycle —\n// per CLAUDE.md's preference for the framework-agnostic core API over an\n// unofficial community port. A persistent formatting toolbar (discoverable\n// for non-technical clients) plus a `/` slash menu for inserting blocks make\n// this a Ghost-like writing surface rather than a bare textarea. `content`\n// is only read once at mount, matching how the form's other fields init from\n// `initialValues` rather than reacting to later prop changes.\nexport function RichTextEditor(props: RichTextEditorProps) {\n let container: HTMLDivElement | undefined;\n let fileInput: HTMLInputElement | undefined;\n let editor: Editor | undefined;\n\n // Tiptap's Editor isn't reactive; bump a signal on every transaction so the\n // toolbar's active states (bold on/off, current heading…) re-render.\n const [tick, setTick] = createSignal(0);\n const bump = () => setTick((t) => t + 1);\n const [slash, setSlash] = createSignal<SlashState | null>(null);\n const [activeIdx, setActiveIdx] = createSignal(0);\n\n const slashItems = (): SlashItem[] => {\n const items: SlashItem[] = [\n {\n label: \"Heading 2\",\n keywords: \"h2 title heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),\n },\n {\n label: \"Heading 3\",\n keywords: \"h3 subtitle heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),\n },\n {\n label: \"Bullet list\",\n keywords: \"ul unordered bullets\",\n run: (e) => e.chain().focus().toggleBulletList().run(),\n },\n {\n label: \"Numbered list\",\n keywords: \"ol ordered numbers\",\n run: (e) => e.chain().focus().toggleOrderedList().run(),\n },\n {\n label: \"Quote\",\n keywords: \"blockquote citation\",\n run: (e) => e.chain().focus().toggleBlockquote().run(),\n },\n {\n label: \"Divider\",\n keywords: \"hr rule separator line\",\n run: (e) => e.chain().focus().setHorizontalRule().run(),\n },\n ];\n if (props.onUploadFile) {\n items.push({\n label: \"Image\",\n keywords: \"img photo picture upload\",\n run: () => fileInput?.click(),\n });\n }\n return items;\n };\n\n const filteredSlash = () => {\n const s = slash();\n return s ? filterSlashItems(slashItems(), s.query) : [];\n };\n\n // Re-evaluate whether a slash menu should be open after every selection or\n // doc change.\n function detectSlash() {\n if (!editor) return;\n const { state } = editor;\n const sel = state.selection;\n if (!sel.empty) {\n setSlash(null);\n return;\n }\n const $from = sel.$from;\n const textBefore = $from.parent.textBetween(\n 0,\n $from.parentOffset,\n undefined,\n \"\",\n );\n const query = matchSlashQuery(textBefore);\n if (query === null) {\n setSlash(null);\n return;\n }\n const to = sel.from;\n const from = $from.start();\n let left = 0;\n let top = 0;\n try {\n const coords = editor.view.coordsAtPos(to);\n const rect = container?.getBoundingClientRect();\n left = coords.left - (rect?.left ?? 0);\n top = coords.bottom - (rect?.top ?? 0);\n } catch {\n // coordsAtPos throws if layout isn't ready (e.g. jsdom) — fall back to\n // the top-left of the editor; the menu is still usable.\n }\n setSlash({ from, to, query, left, top });\n setActiveIdx(0);\n }\n\n function runSlashItem(item: SlashItem) {\n const s = slash();\n if (!s || !editor) return;\n // Drop the typed \"/query\" first, then run the block command at that spot.\n editor.chain().focus().deleteRange({ from: s.from, to: s.to }).run();\n item.run(editor);\n setSlash(null);\n }\n\n onMount(() => {\n if (!container) return;\n editor = new Editor({\n element: container,\n extensions: [\n StarterKit.configure({ link: { openOnClick: false } }),\n Image,\n ],\n content: props.content ?? \"\",\n editorProps: {\n attributes: { class: \"prose-site max-w-none focus:outline-none\" },\n // Drive slash-menu keyboard nav while it's open; let TipTap handle\n // everything else.\n handleKeyDown: (_view, event) => {\n if (!slash()) return false;\n const items = filteredSlash();\n if (event.key === \"ArrowDown\") {\n setActiveIdx((i) => Math.min(i + 1, items.length - 1));\n return true;\n }\n if (event.key === \"ArrowUp\") {\n setActiveIdx((i) => Math.max(i - 1, 0));\n return true;\n }\n if (event.key === \"Enter\") {\n const item = items[activeIdx()];\n if (item) {\n runSlashItem(item);\n return true;\n }\n }\n if (event.key === \"Escape\") {\n setSlash(null);\n return true;\n }\n return false;\n },\n },\n onUpdate: ({ editor: current }) => {\n props.onChange(current.getJSON());\n bump();\n detectSlash();\n },\n onSelectionUpdate: () => {\n bump();\n detectSlash();\n },\n });\n });\n\n onCleanup(() => editor?.destroy());\n\n // Reading `tick()` here subscribes the caller (each toolbar button's\n // `active` accessor) to editor transactions, so active states re-render.\n const isActive = (name: string, attrs?: Record<string, unknown>): boolean => {\n tick();\n return Boolean(editor?.isActive(name, attrs));\n };\n\n function setLink() {\n if (!editor) return;\n const prev = editor.getAttributes(\"link\").href as string | undefined;\n const url = window.prompt(\"Link URL\", prev ?? \"https://\");\n if (url === null) return;\n if (url === \"\") {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n return;\n }\n editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run();\n }\n\n async function handleImageFile(\n e: Event & { currentTarget: HTMLInputElement },\n ) {\n const file = e.currentTarget.files?.[0];\n if (!file || !props.onUploadFile || !editor) return;\n try {\n const { url } = await props.onUploadFile(file);\n editor.chain().focus().setImage({ src: url }).run();\n } finally {\n e.currentTarget.value = \"\";\n }\n }\n\n return (\n <div class=\"border-base-300 rounded-box border\">\n <div class=\"border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1\">\n <ToolbarButton\n label=\"Bold\"\n onClick={() => editor?.chain().focus().toggleBold().run()}\n active={() => isActive(\"bold\")}\n >\n <span class=\"font-bold\">B</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Italic\"\n onClick={() => editor?.chain().focus().toggleItalic().run()}\n active={() => isActive(\"italic\")}\n >\n <span class=\"italic\">I</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Underline\"\n onClick={() => editor?.chain().focus().toggleUnderline().run()}\n active={() => isActive(\"underline\")}\n >\n <span class=\"underline\">U</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Link\"\n onClick={setLink}\n active={() => isActive(\"link\")}\n >\n Link\n </ToolbarButton>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton\n label=\"Heading 2\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 2 }).run()\n }\n active={() => isActive(\"heading\", { level: 2 })}\n >\n H2\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 3\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 3 }).run()\n }\n active={() => isActive(\"heading\", { level: 3 })}\n >\n H3\n </ToolbarButton>\n <ToolbarButton\n label=\"Bullet list\"\n onClick={() => editor?.chain().focus().toggleBulletList().run()}\n active={() => isActive(\"bulletList\")}\n >\n •\n </ToolbarButton>\n <ToolbarButton\n label=\"Numbered list\"\n onClick={() => editor?.chain().focus().toggleOrderedList().run()}\n active={() => isActive(\"orderedList\")}\n >\n 1.\n </ToolbarButton>\n <ToolbarButton\n label=\"Quote\"\n onClick={() => editor?.chain().focus().toggleBlockquote().run()}\n active={() => isActive(\"blockquote\")}\n >\n ❝\n </ToolbarButton>\n <ToolbarButton\n label=\"Divider\"\n onClick={() => editor?.chain().focus().setHorizontalRule().run()}\n >\n —\n </ToolbarButton>\n <Show when={props.onUploadFile}>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton label=\"Image\" onClick={() => fileInput?.click()}>\n Image\n </ToolbarButton>\n </Show>\n </div>\n\n <div class=\"relative\">\n <div\n id={props.id}\n class=\"min-h-32 p-3\"\n ref={(el) => (container = el)}\n />\n <Show when={slash() && filteredSlash().length > 0}>\n <div\n role=\"menu\"\n aria-label=\"Insert block\"\n class=\"bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow\"\n style={{\n left: `${slash()?.left ?? 0}px`,\n top: `${(slash()?.top ?? 0) + 4}px`,\n }}\n >\n <For each={filteredSlash()}>\n {(item, i) => (\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"rounded px-3 py-1.5 text-left text-sm\"\n classList={{ \"bg-base-200\": i() === activeIdx() }}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => runSlashItem(item)}\n >\n {item.label}\n </button>\n )}\n </For>\n </div>\n </Show>\n </div>\n\n <Show when={props.onUploadFile}>\n <input\n ref={(el) => (fileInput = el)}\n type=\"file\"\n accept=\"image/*\"\n class=\"hidden\"\n onChange={handleImageFile}\n />\n </Show>\n </div>\n );\n}\n\n// A toolbar button. `active` is an accessor so Solid re-tracks it on every\n// editor transaction (via the `tick` signal isActive reads).\nfunction ToolbarButton(props: {\n label: string;\n active?: () => boolean;\n onClick: () => void;\n children: JSX.Element;\n}): JSX.Element {\n return (\n <button\n type=\"button\"\n aria-label={props.label}\n aria-pressed={props.active?.() ?? false}\n title={props.label}\n class=\"btn btn-ghost btn-xs\"\n classList={{ \"btn-active\": props.active?.() ?? false }}\n // Keep the editor selection while clicking the toolbar.\n onMouseDown={(e) => e.preventDefault()}\n onClick={props.onClick}\n >\n {props.children}\n </button>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { addEventListener, createComponent, delegateEvents, effect, insert, memo, setAttribute, setStyleProperty, template, use } from "solid-js/web";
|
|
2
|
+
import { For, Show, createSignal, onCleanup, onMount } from "solid-js";
|
|
3
|
+
import { Editor } from "@tiptap/core";
|
|
4
|
+
import Image from "@tiptap/extension-image";
|
|
5
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
6
|
+
//#region src/RichTextEditor.tsx
|
|
7
|
+
var _tmpl$ = /*#__PURE__*/ template(`<span class=font-bold>B`), _tmpl$2 = /*#__PURE__*/ template(`<span class=italic>I`), _tmpl$3 = /*#__PURE__*/ template(`<span class=underline>U`), _tmpl$4 = /*#__PURE__*/ template(`<span class="bg-base-300 mx-1 h-4 w-px"aria-hidden=true>`), _tmpl$5 = /*#__PURE__*/ template(`<div role=menu aria-label="Insert block"class="bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow">`), _tmpl$6 = /*#__PURE__*/ template(`<input type=file accept=image/* class=hidden>`), _tmpl$7 = /*#__PURE__*/ template(`<div class="border-base-300 rounded-box border"><div class="border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1"><span class="bg-base-300 mx-1 h-4 w-px"aria-hidden=true></span></div><div class=relative><div class="min-h-32 p-3">`), _tmpl$8 = /*#__PURE__*/ template(`<button type=button role=menuitem class="rounded px-3 py-1.5 text-left text-sm">`), _tmpl$9 = /*#__PURE__*/ template(`<button type=button class="btn btn-ghost btn-xs">`);
|
|
8
|
+
function matchSlashQuery(textBeforeCursor) {
|
|
9
|
+
const m = textBeforeCursor.match(/^\/(\w*)$/);
|
|
10
|
+
return m ? m[1] : null;
|
|
11
|
+
}
|
|
12
|
+
function filterSlashItems(items, query) {
|
|
13
|
+
const q = query.toLowerCase();
|
|
14
|
+
if (!q) return items;
|
|
15
|
+
return items.filter((it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q));
|
|
16
|
+
}
|
|
17
|
+
function RichTextEditor(props) {
|
|
18
|
+
let container;
|
|
19
|
+
let fileInput;
|
|
20
|
+
let editor;
|
|
21
|
+
const [tick, setTick] = createSignal(0);
|
|
22
|
+
const bump = () => setTick((t) => t + 1);
|
|
23
|
+
const [slash, setSlash] = createSignal(null);
|
|
24
|
+
const [activeIdx, setActiveIdx] = createSignal(0);
|
|
25
|
+
const slashItems = () => {
|
|
26
|
+
const items = [
|
|
27
|
+
{
|
|
28
|
+
label: "Heading 2",
|
|
29
|
+
keywords: "h2 title heading",
|
|
30
|
+
run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run()
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: "Heading 3",
|
|
34
|
+
keywords: "h3 subtitle heading",
|
|
35
|
+
run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run()
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "Bullet list",
|
|
39
|
+
keywords: "ul unordered bullets",
|
|
40
|
+
run: (e) => e.chain().focus().toggleBulletList().run()
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Numbered list",
|
|
44
|
+
keywords: "ol ordered numbers",
|
|
45
|
+
run: (e) => e.chain().focus().toggleOrderedList().run()
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "Quote",
|
|
49
|
+
keywords: "blockquote citation",
|
|
50
|
+
run: (e) => e.chain().focus().toggleBlockquote().run()
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Divider",
|
|
54
|
+
keywords: "hr rule separator line",
|
|
55
|
+
run: (e) => e.chain().focus().setHorizontalRule().run()
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
if (props.onUploadFile) items.push({
|
|
59
|
+
label: "Image",
|
|
60
|
+
keywords: "img photo picture upload",
|
|
61
|
+
run: () => fileInput?.click()
|
|
62
|
+
});
|
|
63
|
+
return items;
|
|
64
|
+
};
|
|
65
|
+
const filteredSlash = () => {
|
|
66
|
+
const s = slash();
|
|
67
|
+
return s ? filterSlashItems(slashItems(), s.query) : [];
|
|
68
|
+
};
|
|
69
|
+
function detectSlash() {
|
|
70
|
+
if (!editor) return;
|
|
71
|
+
const { state } = editor;
|
|
72
|
+
const sel = state.selection;
|
|
73
|
+
if (!sel.empty) {
|
|
74
|
+
setSlash(null);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const $from = sel.$from;
|
|
78
|
+
const query = matchSlashQuery($from.parent.textBetween(0, $from.parentOffset, void 0, ""));
|
|
79
|
+
if (query === null) {
|
|
80
|
+
setSlash(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const to = sel.from;
|
|
84
|
+
const from = $from.start();
|
|
85
|
+
let left = 0;
|
|
86
|
+
let top = 0;
|
|
87
|
+
try {
|
|
88
|
+
const coords = editor.view.coordsAtPos(to);
|
|
89
|
+
const rect = container?.getBoundingClientRect();
|
|
90
|
+
left = coords.left - (rect?.left ?? 0);
|
|
91
|
+
top = coords.bottom - (rect?.top ?? 0);
|
|
92
|
+
} catch {}
|
|
93
|
+
setSlash({
|
|
94
|
+
from,
|
|
95
|
+
to,
|
|
96
|
+
query,
|
|
97
|
+
left,
|
|
98
|
+
top
|
|
99
|
+
});
|
|
100
|
+
setActiveIdx(0);
|
|
101
|
+
}
|
|
102
|
+
function runSlashItem(item) {
|
|
103
|
+
const s = slash();
|
|
104
|
+
if (!s || !editor) return;
|
|
105
|
+
editor.chain().focus().deleteRange({
|
|
106
|
+
from: s.from,
|
|
107
|
+
to: s.to
|
|
108
|
+
}).run();
|
|
109
|
+
item.run(editor);
|
|
110
|
+
setSlash(null);
|
|
111
|
+
}
|
|
112
|
+
onMount(() => {
|
|
113
|
+
if (!container) return;
|
|
114
|
+
editor = new Editor({
|
|
115
|
+
element: container,
|
|
116
|
+
extensions: [StarterKit.configure({ link: { openOnClick: false } }), Image],
|
|
117
|
+
content: props.content ?? "",
|
|
118
|
+
editorProps: {
|
|
119
|
+
attributes: { class: "prose-site max-w-none focus:outline-none" },
|
|
120
|
+
handleKeyDown: (_view, event) => {
|
|
121
|
+
if (!slash()) return false;
|
|
122
|
+
const items = filteredSlash();
|
|
123
|
+
if (event.key === "ArrowDown") {
|
|
124
|
+
setActiveIdx((i) => Math.min(i + 1, items.length - 1));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (event.key === "ArrowUp") {
|
|
128
|
+
setActiveIdx((i) => Math.max(i - 1, 0));
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (event.key === "Enter") {
|
|
132
|
+
const item = items[activeIdx()];
|
|
133
|
+
if (item) {
|
|
134
|
+
runSlashItem(item);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (event.key === "Escape") {
|
|
139
|
+
setSlash(null);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
onUpdate: ({ editor: current }) => {
|
|
146
|
+
props.onChange(current.getJSON());
|
|
147
|
+
bump();
|
|
148
|
+
detectSlash();
|
|
149
|
+
},
|
|
150
|
+
onSelectionUpdate: () => {
|
|
151
|
+
bump();
|
|
152
|
+
detectSlash();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
onCleanup(() => editor?.destroy());
|
|
157
|
+
const isActive = (name, attrs) => {
|
|
158
|
+
tick();
|
|
159
|
+
return Boolean(editor?.isActive(name, attrs));
|
|
160
|
+
};
|
|
161
|
+
function setLink() {
|
|
162
|
+
if (!editor) return;
|
|
163
|
+
const prev = editor.getAttributes("link").href;
|
|
164
|
+
const url = window.prompt("Link URL", prev ?? "https://");
|
|
165
|
+
if (url === null) return;
|
|
166
|
+
if (url === "") {
|
|
167
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
171
|
+
}
|
|
172
|
+
async function handleImageFile(e) {
|
|
173
|
+
const file = e.currentTarget.files?.[0];
|
|
174
|
+
if (!file || !props.onUploadFile || !editor) return;
|
|
175
|
+
try {
|
|
176
|
+
const { url } = await props.onUploadFile(file);
|
|
177
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
178
|
+
} finally {
|
|
179
|
+
e.currentTarget.value = "";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return (() => {
|
|
183
|
+
var _el$ = _tmpl$7(), _el$2 = _el$.firstChild, _el$6 = _el$2.firstChild, _el$8 = _el$2.nextSibling, _el$9 = _el$8.firstChild;
|
|
184
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
185
|
+
label: "Bold",
|
|
186
|
+
onClick: () => editor?.chain().focus().toggleBold().run(),
|
|
187
|
+
active: () => isActive("bold"),
|
|
188
|
+
get children() {
|
|
189
|
+
return _tmpl$();
|
|
190
|
+
}
|
|
191
|
+
}), _el$6);
|
|
192
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
193
|
+
label: "Italic",
|
|
194
|
+
onClick: () => editor?.chain().focus().toggleItalic().run(),
|
|
195
|
+
active: () => isActive("italic"),
|
|
196
|
+
get children() {
|
|
197
|
+
return _tmpl$2();
|
|
198
|
+
}
|
|
199
|
+
}), _el$6);
|
|
200
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
201
|
+
label: "Underline",
|
|
202
|
+
onClick: () => editor?.chain().focus().toggleUnderline().run(),
|
|
203
|
+
active: () => isActive("underline"),
|
|
204
|
+
get children() {
|
|
205
|
+
return _tmpl$3();
|
|
206
|
+
}
|
|
207
|
+
}), _el$6);
|
|
208
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
209
|
+
label: "Link",
|
|
210
|
+
onClick: setLink,
|
|
211
|
+
active: () => isActive("link"),
|
|
212
|
+
children: "Link"
|
|
213
|
+
}), _el$6);
|
|
214
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
215
|
+
label: "Heading 2",
|
|
216
|
+
onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
217
|
+
active: () => isActive("heading", { level: 2 }),
|
|
218
|
+
children: "H2"
|
|
219
|
+
}), null);
|
|
220
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
221
|
+
label: "Heading 3",
|
|
222
|
+
onClick: () => editor?.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
223
|
+
active: () => isActive("heading", { level: 3 }),
|
|
224
|
+
children: "H3"
|
|
225
|
+
}), null);
|
|
226
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
227
|
+
label: "Bullet list",
|
|
228
|
+
onClick: () => editor?.chain().focus().toggleBulletList().run(),
|
|
229
|
+
active: () => isActive("bulletList"),
|
|
230
|
+
children: "•"
|
|
231
|
+
}), null);
|
|
232
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
233
|
+
label: "Numbered list",
|
|
234
|
+
onClick: () => editor?.chain().focus().toggleOrderedList().run(),
|
|
235
|
+
active: () => isActive("orderedList"),
|
|
236
|
+
children: "1."
|
|
237
|
+
}), null);
|
|
238
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
239
|
+
label: "Quote",
|
|
240
|
+
onClick: () => editor?.chain().focus().toggleBlockquote().run(),
|
|
241
|
+
active: () => isActive("blockquote"),
|
|
242
|
+
children: "❝"
|
|
243
|
+
}), null);
|
|
244
|
+
insert(_el$2, createComponent(ToolbarButton, {
|
|
245
|
+
label: "Divider",
|
|
246
|
+
onClick: () => editor?.chain().focus().setHorizontalRule().run(),
|
|
247
|
+
children: "—"
|
|
248
|
+
}), null);
|
|
249
|
+
insert(_el$2, createComponent(Show, {
|
|
250
|
+
get when() {
|
|
251
|
+
return props.onUploadFile;
|
|
252
|
+
},
|
|
253
|
+
get children() {
|
|
254
|
+
return [_tmpl$4(), createComponent(ToolbarButton, {
|
|
255
|
+
label: "Image",
|
|
256
|
+
onClick: () => fileInput?.click(),
|
|
257
|
+
children: "Image"
|
|
258
|
+
})];
|
|
259
|
+
}
|
|
260
|
+
}), null);
|
|
261
|
+
use((el) => container = el, _el$9);
|
|
262
|
+
insert(_el$8, createComponent(Show, {
|
|
263
|
+
get when() {
|
|
264
|
+
return memo(() => !!slash())() && filteredSlash().length > 0;
|
|
265
|
+
},
|
|
266
|
+
get children() {
|
|
267
|
+
var _el$0 = _tmpl$5();
|
|
268
|
+
insert(_el$0, createComponent(For, {
|
|
269
|
+
get each() {
|
|
270
|
+
return filteredSlash();
|
|
271
|
+
},
|
|
272
|
+
children: (item, i) => (() => {
|
|
273
|
+
var _el$10 = _tmpl$8();
|
|
274
|
+
_el$10.$$click = () => runSlashItem(item);
|
|
275
|
+
_el$10.$$mousedown = (e) => e.preventDefault();
|
|
276
|
+
insert(_el$10, () => item.label);
|
|
277
|
+
effect(() => _el$10.classList.toggle("bg-base-200", !!(i() === activeIdx())));
|
|
278
|
+
return _el$10;
|
|
279
|
+
})()
|
|
280
|
+
}));
|
|
281
|
+
effect((_p$) => {
|
|
282
|
+
var _v$ = `${slash()?.left ?? 0}px`, _v$2 = `${(slash()?.top ?? 0) + 4}px`;
|
|
283
|
+
_v$ !== _p$.e && setStyleProperty(_el$0, "left", _p$.e = _v$);
|
|
284
|
+
_v$2 !== _p$.t && setStyleProperty(_el$0, "top", _p$.t = _v$2);
|
|
285
|
+
return _p$;
|
|
286
|
+
}, {
|
|
287
|
+
e: void 0,
|
|
288
|
+
t: void 0
|
|
289
|
+
});
|
|
290
|
+
return _el$0;
|
|
291
|
+
}
|
|
292
|
+
}), null);
|
|
293
|
+
insert(_el$, createComponent(Show, {
|
|
294
|
+
get when() {
|
|
295
|
+
return props.onUploadFile;
|
|
296
|
+
},
|
|
297
|
+
get children() {
|
|
298
|
+
var _el$1 = _tmpl$6();
|
|
299
|
+
_el$1.addEventListener("change", handleImageFile);
|
|
300
|
+
use((el) => fileInput = el, _el$1);
|
|
301
|
+
return _el$1;
|
|
302
|
+
}
|
|
303
|
+
}), null);
|
|
304
|
+
effect(() => setAttribute(_el$9, "id", props.id));
|
|
305
|
+
return _el$;
|
|
306
|
+
})();
|
|
307
|
+
}
|
|
308
|
+
function ToolbarButton(props) {
|
|
309
|
+
return (() => {
|
|
310
|
+
var _el$11 = _tmpl$9();
|
|
311
|
+
addEventListener(_el$11, "click", props.onClick, true);
|
|
312
|
+
_el$11.$$mousedown = (e) => e.preventDefault();
|
|
313
|
+
insert(_el$11, () => props.children);
|
|
314
|
+
effect((_p$) => {
|
|
315
|
+
var _v$3 = props.label, _v$4 = props.active?.() ?? false, _v$5 = props.label, _v$6 = !!(props.active?.() ?? false);
|
|
316
|
+
_v$3 !== _p$.e && setAttribute(_el$11, "aria-label", _p$.e = _v$3);
|
|
317
|
+
_v$4 !== _p$.t && setAttribute(_el$11, "aria-pressed", _p$.t = _v$4);
|
|
318
|
+
_v$5 !== _p$.a && setAttribute(_el$11, "title", _p$.a = _v$5);
|
|
319
|
+
_v$6 !== _p$.o && _el$11.classList.toggle("btn-active", _p$.o = _v$6);
|
|
320
|
+
return _p$;
|
|
321
|
+
}, {
|
|
322
|
+
e: void 0,
|
|
323
|
+
t: void 0,
|
|
324
|
+
a: void 0,
|
|
325
|
+
o: void 0
|
|
326
|
+
});
|
|
327
|
+
return _el$11;
|
|
328
|
+
})();
|
|
329
|
+
}
|
|
330
|
+
delegateEvents(["mousedown", "click"]);
|
|
331
|
+
//#endregion
|
|
332
|
+
export { RichTextEditor };
|
|
333
|
+
|
|
334
|
+
//# sourceMappingURL=RichTextEditor-ComcBFfl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RichTextEditor-ComcBFfl.js","names":["Editor","Image","StarterKit","createSignal","For","JSX","onCleanup","onMount","Show","RichTextEditorProps","id","content","onChange","doc","onUploadFile","file","File","Promise","url","SlashItem","label","keywords","run","editor","matchSlashQuery","textBeforeCursor","m","match","filterSlashItems","items","query","q","toLowerCase","filter","it","includes","SlashState","from","to","left","top","RichTextEditor","props","container","HTMLDivElement","fileInput","HTMLInputElement","tick","setTick","bump","t","slash","setSlash","activeIdx","setActiveIdx","slashItems","e","chain","focus","toggleHeading","level","toggleBulletList","toggleOrderedList","toggleBlockquote","setHorizontalRule","push","click","filteredSlash","s","detectSlash","state","sel","selection","empty","$from","textBefore","parent","textBetween","parentOffset","undefined","start","coords","view","coordsAtPos","rect","getBoundingClientRect","bottom","runSlashItem","item","deleteRange","element","extensions","configure","link","openOnClick","editorProps","attributes","class","handleKeyDown","_view","event","key","i","Math","min","length","max","onUpdate","current","getJSON","onSelectionUpdate","destroy","isActive","name","attrs","Record","Boolean","setLink","prev","getAttributes","href","window","prompt","extendMarkRange","unsetLink","handleImageFile","Event","currentTarget","files","setImage","src","value","_el$","_tmpl$7","_el$2","firstChild","_el$6","_el$8","nextSibling","_el$9","_$insert","_$createComponent","ToolbarButton","onClick","toggleBold","active","children","_tmpl$","toggleItalic","_tmpl$2","toggleUnderline","_tmpl$3","when","_tmpl$4","_$use","el","_$memo","_el$0","_tmpl$5","each","_el$10","_tmpl$8","$$click","$$mousedown","preventDefault","_$effect","classList","toggle","_p$","_v$","_v$2","_$setStyleProperty","_el$1","_tmpl$6","addEventListener","_$setAttribute","Element","_el$11","_tmpl$9","_$addEventListener","_v$3","_v$4","_v$5","_v$6","a","o","_$delegateEvents"],"sources":["../src/RichTextEditor.tsx"],"sourcesContent":["import { Editor } from \"@tiptap/core\";\nimport Image from \"@tiptap/extension-image\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n createSignal,\n For,\n type JSX,\n onCleanup,\n onMount,\n Show,\n} from \"solid-js\";\n\nexport interface RichTextEditorProps {\n id?: string;\n /** TipTap's native JSON document shape — stored as-is, no transform layer. */\n content?: object;\n onChange: (doc: object) => void;\n /**\n * Resolves a picked image file to a stored URL (same contract as the form's\n * upload fields). When provided, the toolbar and slash menu expose an\n * \"Image\" insert; omitted, image insertion is hidden.\n */\n onUploadFile?: (file: File) => Promise<{ url: string }>;\n}\n\ninterface SlashItem {\n label: string;\n /** Extra search terms so e.g. \"ul\" finds \"Bullet list\". */\n keywords: string;\n run: (editor: Editor) => void;\n}\n\n// Pure: a slash menu opens only when the current block's text up to the\n// cursor is a bare `/` followed by an optional word (Notion/Ghost-style, at\n// the start of an empty-ish block). Returns the query (may be \"\") or null.\n// Exported for unit testing without a live ProseMirror view.\nexport function matchSlashQuery(textBeforeCursor: string): string | null {\n const m = textBeforeCursor.match(/^\\/(\\w*)$/);\n return m ? m[1] : null;\n}\n\n// Pure: filter slash items by label or keywords against a (lowercased) query.\nexport function filterSlashItems(\n items: SlashItem[],\n query: string,\n): SlashItem[] {\n const q = query.toLowerCase();\n if (!q) return items;\n return items.filter(\n (it) => it.label.toLowerCase().includes(q) || it.keywords.includes(q),\n );\n}\n\ninterface SlashState {\n from: number;\n to: number;\n query: string;\n left: number;\n top: number;\n}\n\n// No official Solid binding for TipTap exists, so this wraps @tiptap/core's\n// vanilla `Editor` class directly in Solid's onMount/onCleanup lifecycle —\n// per CLAUDE.md's preference for the framework-agnostic core API over an\n// unofficial community port. A persistent formatting toolbar (discoverable\n// for non-technical clients) plus a `/` slash menu for inserting blocks make\n// this a Ghost-like writing surface rather than a bare textarea. `content`\n// is only read once at mount, matching how the form's other fields init from\n// `initialValues` rather than reacting to later prop changes.\nexport function RichTextEditor(props: RichTextEditorProps) {\n let container: HTMLDivElement | undefined;\n let fileInput: HTMLInputElement | undefined;\n let editor: Editor | undefined;\n\n // Tiptap's Editor isn't reactive; bump a signal on every transaction so the\n // toolbar's active states (bold on/off, current heading…) re-render.\n const [tick, setTick] = createSignal(0);\n const bump = () => setTick((t) => t + 1);\n const [slash, setSlash] = createSignal<SlashState | null>(null);\n const [activeIdx, setActiveIdx] = createSignal(0);\n\n const slashItems = (): SlashItem[] => {\n const items: SlashItem[] = [\n {\n label: \"Heading 2\",\n keywords: \"h2 title heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),\n },\n {\n label: \"Heading 3\",\n keywords: \"h3 subtitle heading\",\n run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),\n },\n {\n label: \"Bullet list\",\n keywords: \"ul unordered bullets\",\n run: (e) => e.chain().focus().toggleBulletList().run(),\n },\n {\n label: \"Numbered list\",\n keywords: \"ol ordered numbers\",\n run: (e) => e.chain().focus().toggleOrderedList().run(),\n },\n {\n label: \"Quote\",\n keywords: \"blockquote citation\",\n run: (e) => e.chain().focus().toggleBlockquote().run(),\n },\n {\n label: \"Divider\",\n keywords: \"hr rule separator line\",\n run: (e) => e.chain().focus().setHorizontalRule().run(),\n },\n ];\n if (props.onUploadFile) {\n items.push({\n label: \"Image\",\n keywords: \"img photo picture upload\",\n run: () => fileInput?.click(),\n });\n }\n return items;\n };\n\n const filteredSlash = () => {\n const s = slash();\n return s ? filterSlashItems(slashItems(), s.query) : [];\n };\n\n // Re-evaluate whether a slash menu should be open after every selection or\n // doc change.\n function detectSlash() {\n if (!editor) return;\n const { state } = editor;\n const sel = state.selection;\n if (!sel.empty) {\n setSlash(null);\n return;\n }\n const $from = sel.$from;\n const textBefore = $from.parent.textBetween(\n 0,\n $from.parentOffset,\n undefined,\n \"\",\n );\n const query = matchSlashQuery(textBefore);\n if (query === null) {\n setSlash(null);\n return;\n }\n const to = sel.from;\n const from = $from.start();\n let left = 0;\n let top = 0;\n try {\n const coords = editor.view.coordsAtPos(to);\n const rect = container?.getBoundingClientRect();\n left = coords.left - (rect?.left ?? 0);\n top = coords.bottom - (rect?.top ?? 0);\n } catch {\n // coordsAtPos throws if layout isn't ready (e.g. jsdom) — fall back to\n // the top-left of the editor; the menu is still usable.\n }\n setSlash({ from, to, query, left, top });\n setActiveIdx(0);\n }\n\n function runSlashItem(item: SlashItem) {\n const s = slash();\n if (!s || !editor) return;\n // Drop the typed \"/query\" first, then run the block command at that spot.\n editor.chain().focus().deleteRange({ from: s.from, to: s.to }).run();\n item.run(editor);\n setSlash(null);\n }\n\n onMount(() => {\n if (!container) return;\n editor = new Editor({\n element: container,\n extensions: [\n StarterKit.configure({ link: { openOnClick: false } }),\n Image,\n ],\n content: props.content ?? \"\",\n editorProps: {\n attributes: { class: \"prose-site max-w-none focus:outline-none\" },\n // Drive slash-menu keyboard nav while it's open; let TipTap handle\n // everything else.\n handleKeyDown: (_view, event) => {\n if (!slash()) return false;\n const items = filteredSlash();\n if (event.key === \"ArrowDown\") {\n setActiveIdx((i) => Math.min(i + 1, items.length - 1));\n return true;\n }\n if (event.key === \"ArrowUp\") {\n setActiveIdx((i) => Math.max(i - 1, 0));\n return true;\n }\n if (event.key === \"Enter\") {\n const item = items[activeIdx()];\n if (item) {\n runSlashItem(item);\n return true;\n }\n }\n if (event.key === \"Escape\") {\n setSlash(null);\n return true;\n }\n return false;\n },\n },\n onUpdate: ({ editor: current }) => {\n props.onChange(current.getJSON());\n bump();\n detectSlash();\n },\n onSelectionUpdate: () => {\n bump();\n detectSlash();\n },\n });\n });\n\n onCleanup(() => editor?.destroy());\n\n // Reading `tick()` here subscribes the caller (each toolbar button's\n // `active` accessor) to editor transactions, so active states re-render.\n const isActive = (name: string, attrs?: Record<string, unknown>): boolean => {\n tick();\n return Boolean(editor?.isActive(name, attrs));\n };\n\n function setLink() {\n if (!editor) return;\n const prev = editor.getAttributes(\"link\").href as string | undefined;\n const url = window.prompt(\"Link URL\", prev ?? \"https://\");\n if (url === null) return;\n if (url === \"\") {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n return;\n }\n editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run();\n }\n\n async function handleImageFile(\n e: Event & { currentTarget: HTMLInputElement },\n ) {\n const file = e.currentTarget.files?.[0];\n if (!file || !props.onUploadFile || !editor) return;\n try {\n const { url } = await props.onUploadFile(file);\n editor.chain().focus().setImage({ src: url }).run();\n } finally {\n e.currentTarget.value = \"\";\n }\n }\n\n return (\n <div class=\"border-base-300 rounded-box border\">\n <div class=\"border-base-300 flex flex-wrap items-center gap-0.5 border-b p-1\">\n <ToolbarButton\n label=\"Bold\"\n onClick={() => editor?.chain().focus().toggleBold().run()}\n active={() => isActive(\"bold\")}\n >\n <span class=\"font-bold\">B</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Italic\"\n onClick={() => editor?.chain().focus().toggleItalic().run()}\n active={() => isActive(\"italic\")}\n >\n <span class=\"italic\">I</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Underline\"\n onClick={() => editor?.chain().focus().toggleUnderline().run()}\n active={() => isActive(\"underline\")}\n >\n <span class=\"underline\">U</span>\n </ToolbarButton>\n <ToolbarButton\n label=\"Link\"\n onClick={setLink}\n active={() => isActive(\"link\")}\n >\n Link\n </ToolbarButton>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton\n label=\"Heading 2\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 2 }).run()\n }\n active={() => isActive(\"heading\", { level: 2 })}\n >\n H2\n </ToolbarButton>\n <ToolbarButton\n label=\"Heading 3\"\n onClick={() =>\n editor?.chain().focus().toggleHeading({ level: 3 }).run()\n }\n active={() => isActive(\"heading\", { level: 3 })}\n >\n H3\n </ToolbarButton>\n <ToolbarButton\n label=\"Bullet list\"\n onClick={() => editor?.chain().focus().toggleBulletList().run()}\n active={() => isActive(\"bulletList\")}\n >\n •\n </ToolbarButton>\n <ToolbarButton\n label=\"Numbered list\"\n onClick={() => editor?.chain().focus().toggleOrderedList().run()}\n active={() => isActive(\"orderedList\")}\n >\n 1.\n </ToolbarButton>\n <ToolbarButton\n label=\"Quote\"\n onClick={() => editor?.chain().focus().toggleBlockquote().run()}\n active={() => isActive(\"blockquote\")}\n >\n ❝\n </ToolbarButton>\n <ToolbarButton\n label=\"Divider\"\n onClick={() => editor?.chain().focus().setHorizontalRule().run()}\n >\n —\n </ToolbarButton>\n <Show when={props.onUploadFile}>\n <span class=\"bg-base-300 mx-1 h-4 w-px\" aria-hidden=\"true\" />\n <ToolbarButton label=\"Image\" onClick={() => fileInput?.click()}>\n Image\n </ToolbarButton>\n </Show>\n </div>\n\n <div class=\"relative\">\n <div\n id={props.id}\n class=\"min-h-32 p-3\"\n ref={(el) => (container = el)}\n />\n <Show when={slash() && filteredSlash().length > 0}>\n <div\n role=\"menu\"\n aria-label=\"Insert block\"\n class=\"bg-base-100 border-base-300 rounded-box absolute z-10 flex min-w-44 flex-col border p-1 shadow\"\n style={{\n left: `${slash()?.left ?? 0}px`,\n top: `${(slash()?.top ?? 0) + 4}px`,\n }}\n >\n <For each={filteredSlash()}>\n {(item, i) => (\n <button\n type=\"button\"\n role=\"menuitem\"\n class=\"rounded px-3 py-1.5 text-left text-sm\"\n classList={{ \"bg-base-200\": i() === activeIdx() }}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => runSlashItem(item)}\n >\n {item.label}\n </button>\n )}\n </For>\n </div>\n </Show>\n </div>\n\n <Show when={props.onUploadFile}>\n <input\n ref={(el) => (fileInput = el)}\n type=\"file\"\n accept=\"image/*\"\n class=\"hidden\"\n onChange={handleImageFile}\n />\n </Show>\n </div>\n );\n}\n\n// A toolbar button. `active` is an accessor so Solid re-tracks it on every\n// editor transaction (via the `tick` signal isActive reads).\nfunction ToolbarButton(props: {\n label: string;\n active?: () => boolean;\n onClick: () => void;\n children: JSX.Element;\n}): JSX.Element {\n return (\n <button\n type=\"button\"\n aria-label={props.label}\n aria-pressed={props.active?.() ?? false}\n title={props.label}\n class=\"btn btn-ghost btn-xs\"\n classList={{ \"btn-active\": props.active?.() ?? false }}\n // Keep the editor selection while clicking the toolbar.\n onMouseDown={(e) => e.preventDefault()}\n onClick={props.onClick}\n >\n {props.children}\n </button>\n );\n}\n"],"mappings":"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"}
|
package/dist/index/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { For, Index, Show, Suspense, createEffect, createMemo, createSignal, laz
|
|
|
5
5
|
import { createSolidTable, flexRender, getCoreRowModel } from "@tanstack/solid-table";
|
|
6
6
|
//#region src/CollectionEdit.tsx
|
|
7
7
|
var _tmpl$$4 = /*#__PURE__*/ template(`<p class="text-sm text-error"role=alert>`), _tmpl$2$3 = /*#__PURE__*/ template(`<span class="loading loading-spinner loading-sm">`), _tmpl$3$3 = /*#__PURE__*/ template(`<button type=button class="btn flex-1">`), _tmpl$4$3 = /*#__PURE__*/ template(`<button type=button class="btn btn-primary flex-1">`), _tmpl$5$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline flex-1">`), _tmpl$6$2 = /*#__PURE__*/ template(`<form class="flex flex-col gap-4"><div class="bg-base-100 sticky bottom-0 flex gap-2 border-t py-3">`), _tmpl$7$1 = /*#__PURE__*/ template(`<fieldset class="border-base-300 rounded-box border p-4"><legend class="px-2 text-sm font-semibold">`), _tmpl$8$1 = /*#__PURE__*/ template(`<div class="grid grid-cols-1 gap-4 md:grid-cols-2">`), _tmpl$9$1 = /*#__PURE__*/ template(`<span class=text-error> *`), _tmpl$0$1 = /*#__PURE__*/ template(`<p class="text-base-content/60 mb-1 text-xs">`), _tmpl$1$1 = /*#__PURE__*/ template(`<p class="text-error mt-1 text-sm"role=alert>`), _tmpl$10$1 = /*#__PURE__*/ template(`<div><label class=label>`), _tmpl$11$1 = /*#__PURE__*/ template(`<input class=input type=text>`), _tmpl$12$1 = /*#__PURE__*/ template(`<select class=select>`), _tmpl$13$1 = /*#__PURE__*/ template(`<option>`), _tmpl$14 = /*#__PURE__*/ template(`<input class=input type=number>`), _tmpl$15 = /*#__PURE__*/ template(`<input class=input type=text readonly>`), _tmpl$16 = /*#__PURE__*/ template(`<input class=checkbox type=checkbox>`), _tmpl$17 = /*#__PURE__*/ template(`<p class="text-sm opacity-70 break-all">`), _tmpl$18 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$19 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><input class=file-input type=file>`), _tmpl$20 = /*#__PURE__*/ template(`<div class="mb-1 flex flex-wrap gap-1">`), _tmpl$21 = /*#__PURE__*/ template(`<button type=button aria-label=Clear class="absolute top-2 right-2 cursor-pointer opacity-60">×`), _tmpl$22 = /*#__PURE__*/ template(`<div role=listbox class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex max-h-56 w-full flex-col overflow-auto border p-1 shadow">`), _tmpl$23 = /*#__PURE__*/ template(`<div class=relative><input type=text role=combobox autocomplete=off class=input>`), _tmpl$24 = /*#__PURE__*/ template(`<span class="badge badge-primary gap-1"><button type=button class=cursor-pointer>×`), _tmpl$25 = /*#__PURE__*/ template(`<button type=button role=option class="rounded px-3 py-2 text-left">`), _tmpl$26 = /*#__PURE__*/ template(`<div role=menu class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex flex-col border p-1 shadow">`), _tmpl$27 = /*#__PURE__*/ template(`<div class="relative self-start"><button type=button class="btn btn-outline btn-sm"aria-haspopup=menu>Add block`), _tmpl$28 = /*#__PURE__*/ template(`<div class="form-control md:col-span-2"><div class="label font-medium"></div><div class="flex flex-col gap-3">`), _tmpl$29 = /*#__PURE__*/ template(`<span class="text-base-content/60 truncate text-sm">`), _tmpl$30 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2">`), _tmpl$31 = /*#__PURE__*/ template(`<div class="card bg-base-200 flex flex-col gap-2 p-3"><div class="flex items-center gap-2"><button type=button class="btn btn-ghost btn-sm gap-2"><span aria-hidden=true></span><span class=font-semibold></span></button><div class="ml-auto flex gap-1"><button type=button class="btn btn-ghost btn-xs"aria-label="Move up">↑</button><button type=button class="btn btn-ghost btn-xs"aria-label="Move down">↓</button><button type=button class="btn btn-ghost btn-xs"aria-label=Duplicate>⧉</button><button type=button class="btn btn-ghost btn-xs text-error"aria-label=Remove>Remove`), _tmpl$32 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline btn-sm self-start">Add `), _tmpl$33 = /*#__PURE__*/ template(`<i aria-hidden=true>`), _tmpl$34 = /*#__PURE__*/ template(`<button type=button role=menuitem class="flex items-center gap-2 rounded px-3 py-2 text-left">`);
|
|
8
|
-
const RichTextEditor = lazy(() => import("../RichTextEditor-
|
|
8
|
+
const RichTextEditor = lazy(() => import("../RichTextEditor-ComcBFfl.js").then((mod) => ({ default: mod.RichTextEditor })));
|
|
9
9
|
function editableFields(config) {
|
|
10
10
|
return Object.entries(config.fields).filter(([key]) => key !== "id");
|
|
11
11
|
}
|
|
@@ -379,7 +379,10 @@ function renderControl(ctx, name, field, fieldApi) {
|
|
|
379
379
|
get content() {
|
|
380
380
|
return fieldApi().state.value;
|
|
381
381
|
},
|
|
382
|
-
onChange: (doc) => change(doc)
|
|
382
|
+
onChange: (doc) => change(doc),
|
|
383
|
+
get onUploadFile() {
|
|
384
|
+
return ctx.onUploadFile;
|
|
385
|
+
}
|
|
383
386
|
});
|
|
384
387
|
}
|
|
385
388
|
});
|