@sckyzo/cosmos-react 0.5.0 → 0.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/CHANGELOG.md +65 -0
- package/README.md +2 -2
- package/dist/editor/index.cjs +421 -66
- package/dist/editor/index.cjs.map +1 -1
- package/dist/editor/index.d.cts +38 -12
- package/dist/editor/index.d.ts +38 -12
- package/dist/editor/index.js +423 -69
- package/dist/editor/index.js.map +1 -1
- package/dist/index.cjs +5474 -1602
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1413 -167
- package/dist/index.d.ts +1413 -167
- package/dist/index.js +5452 -1604
- package/dist/index.js.map +1 -1
- package/dist/qr/index.cjs +62 -0
- package/dist/qr/index.cjs.map +1 -0
- package/dist/qr/index.d.cts +43 -0
- package/dist/qr/index.d.ts +43 -0
- package/dist/qr/index.js +60 -0
- package/dist/qr/index.js.map +1 -0
- package/package.json +39 -11
package/dist/editor/index.js
CHANGED
|
@@ -6,11 +6,14 @@ import { useEditor, EditorContent, useEditorState } from '@tiptap/react';
|
|
|
6
6
|
import { BubbleMenu } from '@tiptap/react/menus';
|
|
7
7
|
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
|
8
8
|
import StarterKit from '@tiptap/starter-kit';
|
|
9
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
9
10
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
10
|
-
import { Redo2, Undo2, Link, Code, Quote, ListOrdered, List,
|
|
11
|
-
import {
|
|
11
|
+
import { Pilcrow, Heading1, Heading2, Heading3, Redo2, Undo2, Image as Image$1, Link, TextAlignStart, TextAlignCenter, TextAlignEnd, SquareCode, Code, Quote, ListOrdered, List, Strikethrough, Underline, Italic, Bold } from 'lucide-react';
|
|
12
|
+
import { Image } from '@tiptap/extension-image';
|
|
13
|
+
import { DOMParser } from '@tiptap/pm/model';
|
|
14
|
+
import { Plugin, PluginKey, NodeSelection, Selection } from '@tiptap/pm/state';
|
|
12
15
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
13
|
-
import { Popover, Tooltip, Button } from '@sckyzo/cosmos-react';
|
|
16
|
+
import { Menu, Popover, Tooltip, Button, Checkbox } from '@sckyzo/cosmos-react';
|
|
14
17
|
|
|
15
18
|
// src/editor/RichTextEditor.tsx
|
|
16
19
|
function useControllableState({
|
|
@@ -124,26 +127,86 @@ var RequiredMark = ({ srText }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
|
124
127
|
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-red-700 dark:text-red-400", children: "*" }),
|
|
125
128
|
srText && /* @__PURE__ */ jsx("span", { className: "sr-only", children: srText })
|
|
126
129
|
] });
|
|
130
|
+
var isSafeSource = (src) => typeof src === "string" && isHttpUrl(src);
|
|
131
|
+
var RichTextImage = Image.extend({
|
|
132
|
+
// Written out in full: `extend` calls `addOptions` again, and a `configure` before it is lost.
|
|
133
|
+
addOptions() {
|
|
134
|
+
return { inline: true, allowBase64: false, HTMLAttributes: {}, resize: false };
|
|
135
|
+
},
|
|
136
|
+
addAttributes() {
|
|
137
|
+
return {
|
|
138
|
+
src: {
|
|
139
|
+
default: null,
|
|
140
|
+
renderHTML: ({ src }) => isSafeSource(src) ? { src } : {}
|
|
141
|
+
},
|
|
142
|
+
// `alt=""` is kept, as it says the image is decorative: no `alt` says nothing.
|
|
143
|
+
alt: { default: null }
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
parseHTML() {
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
tag: "img[src]",
|
|
150
|
+
getAttrs: (element) => isSafeSource(element.getAttribute("src")) ? null : false
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
},
|
|
154
|
+
addInputRules() {
|
|
155
|
+
return [];
|
|
156
|
+
},
|
|
157
|
+
addProseMirrorPlugins() {
|
|
158
|
+
const { schema } = this.editor;
|
|
159
|
+
const rules = DOMParser.fromSchema(schema).rules.filter(
|
|
160
|
+
(rule) => !("node" in rule && rule.node === this.name)
|
|
161
|
+
);
|
|
162
|
+
return [new Plugin({ props: { clipboardParser: new DOMParser(schema, rules) } })];
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
var selectedImage = (editor) => {
|
|
166
|
+
const { selection } = editor.state;
|
|
167
|
+
if (!(selection instanceof NodeSelection) || selection.node.type.name !== RichTextImage.name) {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
const { src, alt } = selection.node.attrs;
|
|
171
|
+
return { src: src ?? "", alt };
|
|
172
|
+
};
|
|
173
|
+
var imagePosition = (editor) => {
|
|
174
|
+
const end = Selection.near(editor.state.selection.$to, -1).$to;
|
|
175
|
+
const image = editor.schema.nodes[RichTextImage.name];
|
|
176
|
+
return image && end.parent.canReplaceWith(end.index(), end.index(), image) ? end.pos : void 0;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/editor/actions.ts
|
|
127
180
|
var RICH_TEXT_ACTIONS = [
|
|
128
181
|
"bold",
|
|
129
182
|
"italic",
|
|
183
|
+
"underline",
|
|
130
184
|
"strike",
|
|
131
185
|
"heading",
|
|
132
186
|
"bulletList",
|
|
133
187
|
"orderedList",
|
|
134
188
|
"blockquote",
|
|
135
189
|
"code",
|
|
190
|
+
"codeBlock",
|
|
191
|
+
"align",
|
|
136
192
|
"link",
|
|
193
|
+
"image",
|
|
137
194
|
"undo",
|
|
138
195
|
"redo"
|
|
139
196
|
];
|
|
140
197
|
var BUBBLE_ACTIONS = [
|
|
141
198
|
"bold",
|
|
142
199
|
"italic",
|
|
200
|
+
"underline",
|
|
143
201
|
"strike",
|
|
144
202
|
"code",
|
|
145
203
|
"link"
|
|
146
204
|
];
|
|
205
|
+
var HEADINGS = [
|
|
206
|
+
{ id: "heading1", level: 1, icon: Heading1 },
|
|
207
|
+
{ id: "heading2", level: 2, icon: Heading2 },
|
|
208
|
+
{ id: "heading3", level: 3, icon: Heading3 }
|
|
209
|
+
];
|
|
147
210
|
var ACTION_SPECS = {
|
|
148
211
|
bold: {
|
|
149
212
|
icon: Bold,
|
|
@@ -159,6 +222,13 @@ var ACTION_SPECS = {
|
|
|
159
222
|
isActive: (editor) => editor.isActive("italic"),
|
|
160
223
|
run: (editor) => editor.chain().toggleItalic().run()
|
|
161
224
|
},
|
|
225
|
+
underline: {
|
|
226
|
+
icon: Underline,
|
|
227
|
+
label: "Underline",
|
|
228
|
+
shortcut: ["Mod", "U"],
|
|
229
|
+
isActive: (editor) => editor.isActive("underline"),
|
|
230
|
+
run: (editor) => editor.chain().toggleUnderline().run()
|
|
231
|
+
},
|
|
162
232
|
strike: {
|
|
163
233
|
icon: Strikethrough,
|
|
164
234
|
label: "Strikethrough",
|
|
@@ -169,9 +239,27 @@ var ACTION_SPECS = {
|
|
|
169
239
|
heading: {
|
|
170
240
|
icon: Heading2,
|
|
171
241
|
label: "Heading",
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
242
|
+
isActive: (editor) => editor.isActive("heading"),
|
|
243
|
+
menu: {
|
|
244
|
+
rest: "paragraph",
|
|
245
|
+
current: (editor) => HEADINGS.find(({ level }) => editor.isActive("heading", { level }))?.id ?? "paragraph",
|
|
246
|
+
choices: [
|
|
247
|
+
{
|
|
248
|
+
id: "paragraph",
|
|
249
|
+
label: "Paragraph",
|
|
250
|
+
icon: Pilcrow,
|
|
251
|
+
shortcut: ["Mod", "Alt", "0"],
|
|
252
|
+
run: (editor) => editor.chain().focus().setParagraph().run()
|
|
253
|
+
},
|
|
254
|
+
...HEADINGS.map(({ id, level, icon }) => ({
|
|
255
|
+
id,
|
|
256
|
+
label: `Heading ${level}`,
|
|
257
|
+
icon,
|
|
258
|
+
shortcut: ["Mod", "Alt", String(level)],
|
|
259
|
+
run: (editor) => editor.chain().focus().setHeading({ level }).run()
|
|
260
|
+
}))
|
|
261
|
+
]
|
|
262
|
+
}
|
|
175
263
|
},
|
|
176
264
|
bulletList: {
|
|
177
265
|
icon: List,
|
|
@@ -204,11 +292,60 @@ var ACTION_SPECS = {
|
|
|
204
292
|
isActive: (editor) => editor.isActive("code"),
|
|
205
293
|
run: (editor) => editor.chain().toggleCode().run()
|
|
206
294
|
},
|
|
295
|
+
codeBlock: {
|
|
296
|
+
icon: SquareCode,
|
|
297
|
+
label: "Code block",
|
|
298
|
+
shortcut: ["Mod", "Alt", "C"],
|
|
299
|
+
isActive: (editor) => editor.isActive("codeBlock"),
|
|
300
|
+
run: (editor) => editor.chain().toggleCodeBlock().run()
|
|
301
|
+
},
|
|
302
|
+
align: {
|
|
303
|
+
icon: TextAlignStart,
|
|
304
|
+
mirror: true,
|
|
305
|
+
label: "Text alignment",
|
|
306
|
+
isActive: (editor) => editor.isActive({ textAlign: "center" }) || editor.isActive({ textAlign: "end" }),
|
|
307
|
+
menu: {
|
|
308
|
+
rest: "alignStart",
|
|
309
|
+
current: (editor) => editor.isActive({ textAlign: "center" }) ? "alignCenter" : editor.isActive({ textAlign: "end" }) ? "alignEnd" : "alignStart",
|
|
310
|
+
choices: [
|
|
311
|
+
{
|
|
312
|
+
id: "alignStart",
|
|
313
|
+
label: "Align start",
|
|
314
|
+
icon: TextAlignStart,
|
|
315
|
+
mirror: true,
|
|
316
|
+
// The start is what a block has when nothing is set.
|
|
317
|
+
run: (editor) => editor.chain().focus().unsetTextAlign().run()
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "alignCenter",
|
|
321
|
+
label: "Align center",
|
|
322
|
+
icon: TextAlignCenter,
|
|
323
|
+
run: (editor) => editor.chain().focus().setTextAlign("center").run()
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "alignEnd",
|
|
327
|
+
label: "Align end",
|
|
328
|
+
icon: TextAlignEnd,
|
|
329
|
+
mirror: true,
|
|
330
|
+
run: (editor) => editor.chain().focus().setTextAlign("end").run()
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
},
|
|
207
335
|
link: {
|
|
208
336
|
icon: Link,
|
|
209
337
|
label: "Link",
|
|
210
338
|
shortcut: ["Mod", "K"],
|
|
211
|
-
isActive: (editor) => editor.isActive("link")
|
|
339
|
+
isActive: (editor) => editor.isActive("link"),
|
|
340
|
+
form: "link"
|
|
341
|
+
},
|
|
342
|
+
image: {
|
|
343
|
+
icon: Image$1,
|
|
344
|
+
label: "Image",
|
|
345
|
+
isActive: (editor) => selectedImage(editor) !== void 0,
|
|
346
|
+
// An image lives in a line of text, and a code block has none.
|
|
347
|
+
isIdle: (editor) => selectedImage(editor) === void 0 && imagePosition(editor) === void 0,
|
|
348
|
+
form: "image"
|
|
212
349
|
},
|
|
213
350
|
undo: {
|
|
214
351
|
icon: Undo2,
|
|
@@ -281,6 +418,19 @@ var BlockDirection = Extension.create({
|
|
|
281
418
|
];
|
|
282
419
|
}
|
|
283
420
|
});
|
|
421
|
+
|
|
422
|
+
// src/editor/documentJson.ts
|
|
423
|
+
var withoutDefaultAlignment = (node) => {
|
|
424
|
+
const { attrs, content, ...rest } = node;
|
|
425
|
+
const result = { ...rest };
|
|
426
|
+
if (attrs) {
|
|
427
|
+
const { textAlign, ...others } = attrs;
|
|
428
|
+
const kept = textAlign == null ? others : { ...others, textAlign };
|
|
429
|
+
if (Object.keys(kept).length > 0) result.attrs = kept;
|
|
430
|
+
}
|
|
431
|
+
if (content) result.content = content.map(withoutDefaultAlignment);
|
|
432
|
+
return result;
|
|
433
|
+
};
|
|
284
434
|
var directionOf2 = (element, read = (el) => getComputedStyle(el)) => element && read(element).direction === "rtl" ? "rtl" : "ltr";
|
|
285
435
|
|
|
286
436
|
// src/ui/mergeRefs.ts
|
|
@@ -304,6 +454,139 @@ function mergeRefs(...refs) {
|
|
|
304
454
|
};
|
|
305
455
|
}
|
|
306
456
|
|
|
457
|
+
// src/editor/imageUrl.ts
|
|
458
|
+
var normalizeImageUrl = (input) => {
|
|
459
|
+
const value = input.trim();
|
|
460
|
+
return isHttpUrl(value) ? value : void 0;
|
|
461
|
+
};
|
|
462
|
+
var LABEL = "text-xs font-medium text-gray-700 dark:text-gray-300";
|
|
463
|
+
var ERROR = "text-xs text-red-700 dark:text-red-400";
|
|
464
|
+
function ImageForm({ editor, labels, onDone, className }) {
|
|
465
|
+
const urlId = useId();
|
|
466
|
+
const altId = useId();
|
|
467
|
+
const urlErrorId = useId();
|
|
468
|
+
const altErrorId = useId();
|
|
469
|
+
const urlRef = useRef(null);
|
|
470
|
+
const altRef = useRef(null);
|
|
471
|
+
const [selected] = useState(() => selectedImage(editor));
|
|
472
|
+
const [src, setSrc] = useState(selected?.src ?? "");
|
|
473
|
+
const [alt, setAlt] = useState(selected?.alt ?? "");
|
|
474
|
+
const [decorative, setDecorative] = useState(selected?.alt === "");
|
|
475
|
+
const [invalid, setInvalid] = useState(null);
|
|
476
|
+
const apply = () => {
|
|
477
|
+
const address = normalizeImageUrl(src);
|
|
478
|
+
if (!address) {
|
|
479
|
+
setInvalid("url");
|
|
480
|
+
urlRef.current?.focus();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const description = decorative ? "" : alt.trim();
|
|
484
|
+
if (!decorative && !description) {
|
|
485
|
+
setInvalid("alt");
|
|
486
|
+
altRef.current?.focus();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const attrs = { src: address, alt: description };
|
|
490
|
+
const chain = editor.chain().focus();
|
|
491
|
+
const at = imagePosition(editor);
|
|
492
|
+
if (selected) chain.updateAttributes("image", attrs);
|
|
493
|
+
else if (at !== void 0) chain.insertContentAt(at, { type: "image", attrs });
|
|
494
|
+
chain.run();
|
|
495
|
+
onDone();
|
|
496
|
+
};
|
|
497
|
+
const onKeyDown = (e) => {
|
|
498
|
+
if (e.key === "Enter") {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
apply();
|
|
501
|
+
} else if (e.key === "Escape") {
|
|
502
|
+
e.preventDefault();
|
|
503
|
+
e.stopPropagation();
|
|
504
|
+
onDone();
|
|
505
|
+
editor.commands.focus();
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const input = (side) => clsx(
|
|
509
|
+
"block w-full",
|
|
510
|
+
fieldClass({ size: "sm", invalid: invalid === side, fixedHeight: false }),
|
|
511
|
+
FIELD_PAD_X.sm,
|
|
512
|
+
FIELD_PAD_Y.sm,
|
|
513
|
+
FIELD_PLACEHOLDER
|
|
514
|
+
);
|
|
515
|
+
return /* @__PURE__ */ jsxs(
|
|
516
|
+
"div",
|
|
517
|
+
{
|
|
518
|
+
role: "group",
|
|
519
|
+
"aria-label": labels.imageDialog,
|
|
520
|
+
className: clsx("flex flex-col gap-3", className),
|
|
521
|
+
children: [
|
|
522
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
523
|
+
/* @__PURE__ */ jsx("label", { htmlFor: urlId, className: LABEL, children: labels.imageUrl }),
|
|
524
|
+
/* @__PURE__ */ jsx(
|
|
525
|
+
"input",
|
|
526
|
+
{
|
|
527
|
+
ref: urlRef,
|
|
528
|
+
id: urlId,
|
|
529
|
+
type: "text",
|
|
530
|
+
inputMode: "url",
|
|
531
|
+
autoFocus: true,
|
|
532
|
+
autoComplete: "off",
|
|
533
|
+
spellCheck: false,
|
|
534
|
+
dir: "ltr",
|
|
535
|
+
"aria-invalid": invalid === "url" || void 0,
|
|
536
|
+
"aria-describedby": invalid === "url" ? urlErrorId : void 0,
|
|
537
|
+
placeholder: "https://",
|
|
538
|
+
value: src,
|
|
539
|
+
onChange: (e) => {
|
|
540
|
+
setSrc(e.target.value);
|
|
541
|
+
setInvalid(null);
|
|
542
|
+
},
|
|
543
|
+
onKeyDown,
|
|
544
|
+
className: input("url")
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
invalid === "url" && /* @__PURE__ */ jsx("span", { id: urlErrorId, className: ERROR, children: labels.imageInvalid })
|
|
548
|
+
] }),
|
|
549
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
550
|
+
/* @__PURE__ */ jsx("label", { htmlFor: altId, className: LABEL, children: labels.imageAlt }),
|
|
551
|
+
/* @__PURE__ */ jsx(
|
|
552
|
+
"input",
|
|
553
|
+
{
|
|
554
|
+
ref: altRef,
|
|
555
|
+
id: altId,
|
|
556
|
+
type: "text",
|
|
557
|
+
autoComplete: "off",
|
|
558
|
+
disabled: decorative,
|
|
559
|
+
"aria-invalid": invalid === "alt" || void 0,
|
|
560
|
+
"aria-describedby": invalid === "alt" ? altErrorId : void 0,
|
|
561
|
+
value: alt,
|
|
562
|
+
onChange: (e) => {
|
|
563
|
+
setAlt(e.target.value);
|
|
564
|
+
setInvalid(null);
|
|
565
|
+
},
|
|
566
|
+
onKeyDown,
|
|
567
|
+
className: clsx(input("alt"), "disabled:cursor-not-allowed disabled:opacity-50")
|
|
568
|
+
}
|
|
569
|
+
),
|
|
570
|
+
invalid === "alt" && /* @__PURE__ */ jsx("span", { id: altErrorId, className: ERROR, children: labels.imageAltRequired })
|
|
571
|
+
] }),
|
|
572
|
+
/* @__PURE__ */ jsx(
|
|
573
|
+
Checkbox,
|
|
574
|
+
{
|
|
575
|
+
size: "sm",
|
|
576
|
+
label: labels.imageDecorative,
|
|
577
|
+
checked: decorative,
|
|
578
|
+
onChange: (checked) => {
|
|
579
|
+
setDecorative(checked);
|
|
580
|
+
setInvalid((was) => was === "alt" ? null : was);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
),
|
|
584
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsx(Button, { type: "button", size: "xs", onClick: apply, children: labels.imageApply }) })
|
|
585
|
+
]
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
307
590
|
// src/editor/linkUrl.ts
|
|
308
591
|
var BARE_HOST = /^(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/?#]\S*)?$/i;
|
|
309
592
|
var EMAIL = /^[^\s@<>"'\\:/]+@(?:[a-z0-9-]+\.)+[a-z]{2,}$/i;
|
|
@@ -400,9 +683,9 @@ function FormattingToolbar({
|
|
|
400
683
|
actions,
|
|
401
684
|
labels,
|
|
402
685
|
disabled,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
686
|
+
form,
|
|
687
|
+
onFormChange,
|
|
688
|
+
formMode,
|
|
406
689
|
onEscape,
|
|
407
690
|
className,
|
|
408
691
|
ref
|
|
@@ -416,7 +699,8 @@ function FormattingToolbar({
|
|
|
416
699
|
action,
|
|
417
700
|
{
|
|
418
701
|
active: ACTION_SPECS[action].isActive(current),
|
|
419
|
-
idle: ACTION_SPECS[action].isIdle?.(current) ?? false
|
|
702
|
+
idle: ACTION_SPECS[action].isIdle?.(current) ?? false,
|
|
703
|
+
choice: ACTION_SPECS[action].menu?.current(current)
|
|
420
704
|
}
|
|
421
705
|
])
|
|
422
706
|
)
|
|
@@ -425,6 +709,7 @@ function FormattingToolbar({
|
|
|
425
709
|
const tabStop = Math.min(stop, actions.length - 1);
|
|
426
710
|
const buttons = () => Array.from(root.current?.querySelectorAll("button[data-rt-action]") ?? []);
|
|
427
711
|
const onKeyDown = (event) => {
|
|
712
|
+
if (!(event.target instanceof Node) || !root.current?.contains(event.target)) return;
|
|
428
713
|
if (event.key === "Escape" && onEscape) {
|
|
429
714
|
event.preventDefault();
|
|
430
715
|
onEscape();
|
|
@@ -439,15 +724,15 @@ function FormattingToolbar({
|
|
|
439
724
|
event.preventDefault();
|
|
440
725
|
list[next]?.focus();
|
|
441
726
|
};
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
{
|
|
727
|
+
const field = (which) => {
|
|
728
|
+
const props = {
|
|
445
729
|
editor,
|
|
446
730
|
labels,
|
|
447
|
-
onDone: () =>
|
|
448
|
-
className:
|
|
449
|
-
}
|
|
450
|
-
|
|
731
|
+
onDone: () => onFormChange(null),
|
|
732
|
+
className: formMode === "inline" ? "w-64 p-2" : void 0
|
|
733
|
+
};
|
|
734
|
+
return which === "link" ? /* @__PURE__ */ jsx(LinkForm, { ...props }) : /* @__PURE__ */ jsx(ImageForm, { ...props });
|
|
735
|
+
};
|
|
451
736
|
return /* @__PURE__ */ jsx(
|
|
452
737
|
"div",
|
|
453
738
|
{
|
|
@@ -457,15 +742,16 @@ function FormattingToolbar({
|
|
|
457
742
|
"aria-orientation": "horizontal",
|
|
458
743
|
onKeyDown,
|
|
459
744
|
className: clsx("flex flex-wrap items-center gap-0.5", className),
|
|
460
|
-
children:
|
|
745
|
+
children: formMode === "inline" && form ? field(form) : actions.map((action, index) => {
|
|
461
746
|
const spec = ACTION_SPECS[action];
|
|
462
|
-
const { active, idle } = state[action];
|
|
463
|
-
const Icon = spec.icon;
|
|
747
|
+
const { active, idle, choice } = state[action];
|
|
464
748
|
const inert = disabled || idle;
|
|
749
|
+
const shown = active && spec.menu ? spec.menu.choices.find(({ id }) => id === choice) : void 0;
|
|
750
|
+
const Icon = shown?.icon ?? spec.icon;
|
|
465
751
|
const common = {
|
|
466
752
|
"data-rt-action": action,
|
|
467
753
|
"aria-label": labels[action],
|
|
468
|
-
"aria-keyshortcuts": ariaShortcut(spec.shortcut),
|
|
754
|
+
"aria-keyshortcuts": spec.shortcut && ariaShortcut(spec.shortcut),
|
|
469
755
|
"aria-pressed": active,
|
|
470
756
|
"aria-disabled": inert || void 0,
|
|
471
757
|
tabIndex: index === tabStop ? 0 : -1,
|
|
@@ -474,43 +760,82 @@ function FormattingToolbar({
|
|
|
474
760
|
onMouseDown: (e) => e.preventDefault(),
|
|
475
761
|
className: clsx(BUTTON, active ? PRESSED : IDLE)
|
|
476
762
|
};
|
|
477
|
-
const icon = /* @__PURE__ */ jsx(
|
|
763
|
+
const icon = /* @__PURE__ */ jsx(
|
|
764
|
+
Icon,
|
|
765
|
+
{
|
|
766
|
+
"aria-hidden": true,
|
|
767
|
+
className: clsx(
|
|
768
|
+
"h-4 w-4",
|
|
769
|
+
(shown ? shown.mirror : spec.mirror) && "rtl:-scale-x-100"
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
);
|
|
478
773
|
const tooltip = (child) => /* @__PURE__ */ jsx(
|
|
479
774
|
Tooltip,
|
|
480
775
|
{
|
|
481
776
|
content: labels[action],
|
|
482
|
-
shortcut: shortcutKeys(spec.shortcut),
|
|
777
|
+
shortcut: spec.shortcut && shortcutKeys(spec.shortcut),
|
|
483
778
|
position: "top",
|
|
484
779
|
children: child
|
|
485
780
|
},
|
|
486
781
|
action
|
|
487
782
|
);
|
|
488
|
-
if (
|
|
489
|
-
return /* @__PURE__ */ jsxs(
|
|
490
|
-
tooltip(
|
|
491
|
-
|
|
492
|
-
|
|
783
|
+
if (spec.menu) {
|
|
784
|
+
return /* @__PURE__ */ jsxs(Menu, { children: [
|
|
785
|
+
tooltip(
|
|
786
|
+
/* @__PURE__ */ jsx(
|
|
787
|
+
Menu.Trigger,
|
|
788
|
+
{
|
|
789
|
+
...common,
|
|
790
|
+
onClick: (e) => inert && e.preventDefault(),
|
|
791
|
+
onKeyDown: (e) => {
|
|
792
|
+
if (inert && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
children: icon
|
|
797
|
+
}
|
|
798
|
+
)
|
|
799
|
+
),
|
|
800
|
+
/* @__PURE__ */ jsx(Menu.Content, { width: "w-44", children: spec.menu.choices.map((item) => /* @__PURE__ */ jsx(
|
|
801
|
+
Menu.RadioItem,
|
|
493
802
|
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
)
|
|
803
|
+
checked: item.id === choice,
|
|
804
|
+
"aria-keyshortcuts": item.shortcut && ariaShortcut(item.shortcut),
|
|
805
|
+
onSelect: () => item.run(editor),
|
|
806
|
+
children: labels[item.id]
|
|
807
|
+
},
|
|
808
|
+
item.id
|
|
809
|
+
)) })
|
|
501
810
|
] }, action);
|
|
502
811
|
}
|
|
503
|
-
if (
|
|
812
|
+
if (spec.form && formMode === "popover") {
|
|
813
|
+
return /* @__PURE__ */ jsxs(
|
|
814
|
+
Popover,
|
|
815
|
+
{
|
|
816
|
+
open: form === spec.form && !inert,
|
|
817
|
+
onOpenChange: (open) => onFormChange(open ? spec.form ?? null : null),
|
|
818
|
+
children: [
|
|
819
|
+
tooltip(/* @__PURE__ */ jsx(Popover.Trigger, { ...common, children: icon })),
|
|
820
|
+
/* @__PURE__ */ jsx(
|
|
821
|
+
Popover.Content,
|
|
822
|
+
{
|
|
823
|
+
"aria-label": spec.form === "image" ? labels.imageDialog : labels.linkDialog,
|
|
824
|
+
position: "bottom",
|
|
825
|
+
width: spec.form === "image" ? "w-80" : "w-72",
|
|
826
|
+
padding: "p-3",
|
|
827
|
+
children: field(spec.form)
|
|
828
|
+
}
|
|
829
|
+
)
|
|
830
|
+
]
|
|
831
|
+
},
|
|
832
|
+
action
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (spec.form) {
|
|
836
|
+
const opens = spec.form;
|
|
504
837
|
return tooltip(
|
|
505
|
-
/* @__PURE__ */ jsx(
|
|
506
|
-
"button",
|
|
507
|
-
{
|
|
508
|
-
type: "button",
|
|
509
|
-
...common,
|
|
510
|
-
onClick: () => !disabled && onLinkOpenChange(true),
|
|
511
|
-
children: icon
|
|
512
|
-
}
|
|
513
|
-
)
|
|
838
|
+
/* @__PURE__ */ jsx("button", { type: "button", ...common, onClick: () => !inert && onFormChange(opens), children: icon })
|
|
514
839
|
);
|
|
515
840
|
}
|
|
516
841
|
return tooltip(
|
|
@@ -528,23 +853,40 @@ var DEFAULTS = {
|
|
|
528
853
|
linkDialog: "Edit link",
|
|
529
854
|
linkApply: "Apply",
|
|
530
855
|
linkRemove: "Remove link",
|
|
531
|
-
linkInvalid: "Enter an http(s), mailto or relative link."
|
|
856
|
+
linkInvalid: "Enter an http(s), mailto or relative link.",
|
|
857
|
+
imageDialog: "Image details",
|
|
858
|
+
imageUrl: "Image URL",
|
|
859
|
+
imageAlt: "Description (alt text)",
|
|
860
|
+
imageDecorative: "Decorative image, no description",
|
|
861
|
+
imageApply: "Apply",
|
|
862
|
+
imageInvalid: "Enter an http(s) image address.",
|
|
863
|
+
imageAltRequired: "Describe the image, or mark it decorative."
|
|
532
864
|
};
|
|
865
|
+
var NAMED_DEFAULTS = Object.fromEntries([
|
|
866
|
+
...Object.entries(ACTION_SPECS).map(([action, spec]) => [action, spec.label]),
|
|
867
|
+
...Object.values(ACTION_SPECS).flatMap(
|
|
868
|
+
({ menu }) => (menu?.choices ?? []).map((choice) => [choice.id, choice.label])
|
|
869
|
+
)
|
|
870
|
+
]);
|
|
533
871
|
var resolveLabels = (labels = {}) => {
|
|
534
|
-
const
|
|
535
|
-
Object.keys(
|
|
536
|
-
action,
|
|
537
|
-
labels[action] ?? ACTION_SPECS[action].label
|
|
538
|
-
])
|
|
872
|
+
const named = Object.fromEntries(
|
|
873
|
+
Object.keys(NAMED_DEFAULTS).map((key2) => [key2, labels[key2] ?? NAMED_DEFAULTS[key2]])
|
|
539
874
|
);
|
|
540
875
|
return {
|
|
541
|
-
...
|
|
876
|
+
...named,
|
|
542
877
|
toolbar: labels.toolbar ?? DEFAULTS.toolbar,
|
|
543
878
|
linkUrl: labels.linkUrl ?? DEFAULTS.linkUrl,
|
|
544
879
|
linkDialog: labels.linkDialog ?? DEFAULTS.linkDialog,
|
|
545
880
|
linkApply: labels.linkApply ?? DEFAULTS.linkApply,
|
|
546
881
|
linkRemove: labels.linkRemove ?? DEFAULTS.linkRemove,
|
|
547
|
-
linkInvalid: labels.linkInvalid ?? DEFAULTS.linkInvalid
|
|
882
|
+
linkInvalid: labels.linkInvalid ?? DEFAULTS.linkInvalid,
|
|
883
|
+
imageDialog: labels.imageDialog ?? DEFAULTS.imageDialog,
|
|
884
|
+
imageUrl: labels.imageUrl ?? DEFAULTS.imageUrl,
|
|
885
|
+
imageAlt: labels.imageAlt ?? DEFAULTS.imageAlt,
|
|
886
|
+
imageDecorative: labels.imageDecorative ?? DEFAULTS.imageDecorative,
|
|
887
|
+
imageApply: labels.imageApply ?? DEFAULTS.imageApply,
|
|
888
|
+
imageInvalid: labels.imageInvalid ?? DEFAULTS.imageInvalid,
|
|
889
|
+
imageAltRequired: labels.imageAltRequired ?? DEFAULTS.imageAltRequired
|
|
548
890
|
};
|
|
549
891
|
};
|
|
550
892
|
var NO_PROPS = { format: "html", setCurrent: () => {
|
|
@@ -559,7 +901,7 @@ var createStore = (initial) => {
|
|
|
559
901
|
};
|
|
560
902
|
};
|
|
561
903
|
var EMPTY_DOC = { type: "doc", content: [{ type: "paragraph" }] };
|
|
562
|
-
var readContent = (editor, format) => format === "json" ? editor.getJSON() : editor.isEmpty ? "" : editor.getHTML();
|
|
904
|
+
var readContent = (editor, format) => format === "json" ? withoutDefaultAlignment(editor.getJSON()) : editor.isEmpty ? "" : editor.getHTML();
|
|
563
905
|
var countCharacters = (editor) => editor.storage.characterCount?.characters() ?? 0;
|
|
564
906
|
var sameContent = (a, b) => typeof a === "string" || typeof b === "string" ? a === b : JSON.stringify(a) === JSON.stringify(b);
|
|
565
907
|
var LinkShortcut = Extension.create({
|
|
@@ -569,12 +911,14 @@ var LinkShortcut = Extension.create({
|
|
|
569
911
|
return { "Mod-k": () => this.options.open() };
|
|
570
912
|
}
|
|
571
913
|
});
|
|
914
|
+
var BlockAlign = TextAlign.extend({ addKeyboardShortcuts: () => ({}) });
|
|
572
915
|
var CONTENT_CLASS = [
|
|
573
916
|
"min-w-0 outline-hidden break-words",
|
|
574
917
|
"[&>*+*]:mt-2",
|
|
575
918
|
// Each block follows the direction of what is typed in it, as a Textarea's paragraphs do
|
|
576
919
|
// (`FIELD_PLAINTEXT`); lists and quotes take a `dir` from `BlockDirection`.
|
|
577
|
-
"[&_p]:[unicode-bidi:plaintext] [&_h2]:[unicode-bidi:plaintext] [&_h3]:[unicode-bidi:plaintext]",
|
|
920
|
+
"[&_p]:[unicode-bidi:plaintext] [&_h1]:[unicode-bidi:plaintext] [&_h2]:[unicode-bidi:plaintext] [&_h3]:[unicode-bidi:plaintext]",
|
|
921
|
+
"[&_h1]:text-xl [&_h1]:leading-tight [&_h1]:font-semibold",
|
|
578
922
|
"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold",
|
|
579
923
|
"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold",
|
|
580
924
|
"[&_ul]:list-disc [&_ul]:ps-5 [&_ol]:list-decimal [&_ol]:ps-5 [&_li>p]:my-0",
|
|
@@ -582,6 +926,13 @@ var CONTENT_CLASS = [
|
|
|
582
926
|
"dark:[&_blockquote]:border-gray-600 dark:[&_blockquote]:text-gray-300",
|
|
583
927
|
"[&_code]:rounded-sm [&_code]:bg-gray-100 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.9em]",
|
|
584
928
|
"dark:[&_code]:bg-gray-800",
|
|
929
|
+
// A code block reads left to right in any page, as `CodeBlock` does; its `code` drops the pill.
|
|
930
|
+
"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-gray-100 [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-[0.9em] [&_pre]:leading-snug",
|
|
931
|
+
"dark:[&_pre]:bg-gray-800 [&_pre]:[direction:ltr] [&_pre]:text-start",
|
|
932
|
+
"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-[1em]",
|
|
933
|
+
// An image is in the line: it never grows past it, and a selected one shows it.
|
|
934
|
+
"[&_img[src]]:inline-block [&_img[src]]:h-auto [&_img[src]]:max-w-full [&_img[src]]:align-middle",
|
|
935
|
+
"[&_img.ProseMirror-selectednode]:outline-brand-500 [&_img.ProseMirror-selectednode]:outline-2",
|
|
585
936
|
"[&_a]:text-brand-600 dark:[&_a]:text-brand-400 [&_a]:underline [&_a]:underline-offset-2",
|
|
586
937
|
"[&_p.is-editor-empty:first-child]:before:pointer-events-none [&_p.is-editor-empty:first-child]:before:float-start [&_p.is-editor-empty:first-child]:before:h-0",
|
|
587
938
|
"[&_p.is-editor-empty:first-child]:before:text-gray-500 dark:[&_p.is-editor-empty:first-child]:before:text-gray-400",
|
|
@@ -626,7 +977,7 @@ function RichTextEditor({
|
|
|
626
977
|
onChange: (next) => onChange?.(next),
|
|
627
978
|
name: "RichTextEditor.value"
|
|
628
979
|
});
|
|
629
|
-
const [
|
|
980
|
+
const [form, setForm] = useState(null);
|
|
630
981
|
const [characters, setCharacters] = useState(0);
|
|
631
982
|
const [latest] = useState(() => createStore(NO_PROPS));
|
|
632
983
|
const [echoes] = useState(() => createStore([]));
|
|
@@ -637,7 +988,7 @@ function RichTextEditor({
|
|
|
637
988
|
setCurrent,
|
|
638
989
|
openLink: () => {
|
|
639
990
|
if (!hasLinkField || !editable) return false;
|
|
640
|
-
|
|
991
|
+
setForm("link");
|
|
641
992
|
return true;
|
|
642
993
|
}
|
|
643
994
|
});
|
|
@@ -652,11 +1003,9 @@ function RichTextEditor({
|
|
|
652
1003
|
content: current,
|
|
653
1004
|
extensions: [
|
|
654
1005
|
StarterKit.configure({
|
|
655
|
-
heading: { levels: [2, 3] },
|
|
1006
|
+
heading: { levels: [1, 2, 3] },
|
|
656
1007
|
// What the editor doesn't offer is out of the schema, so no paste or value brings it in.
|
|
657
|
-
codeBlock: false,
|
|
658
1008
|
horizontalRule: false,
|
|
659
|
-
underline: false,
|
|
660
1009
|
trailingNode: false,
|
|
661
1010
|
link: {
|
|
662
1011
|
openOnClick: false,
|
|
@@ -665,6 +1014,10 @@ function RichTextEditor({
|
|
|
665
1014
|
isAllowedUri: (url) => isSafeLinkUrl(url)
|
|
666
1015
|
}
|
|
667
1016
|
}),
|
|
1017
|
+
// Center and end only, as the sides of the line: a pasted `left`, `right` or `justify` is
|
|
1018
|
+
// dropped, and the start is a block with nothing set.
|
|
1019
|
+
BlockAlign.configure({ types: ["paragraph", "heading"], alignments: ["center", "end"] }),
|
|
1020
|
+
RichTextImage,
|
|
668
1021
|
Placeholder.configure({ placeholder: () => latest.get().placeholder ?? "" }),
|
|
669
1022
|
...maxLength !== void 0 ? [CharacterCount.configure({ limit: maxLength })] : [],
|
|
670
1023
|
LinkShortcut.configure({ open: () => latest.get().openLink() }),
|
|
@@ -784,9 +1137,9 @@ function RichTextEditor({
|
|
|
784
1137
|
actions,
|
|
785
1138
|
labels,
|
|
786
1139
|
disabled,
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1140
|
+
form,
|
|
1141
|
+
onFormChange: setForm,
|
|
1142
|
+
formMode: "popover",
|
|
790
1143
|
className: "border-b border-gray-200 p-1 dark:border-gray-700"
|
|
791
1144
|
}
|
|
792
1145
|
),
|
|
@@ -800,7 +1153,7 @@ function RichTextEditor({
|
|
|
800
1153
|
strategy: "fixed",
|
|
801
1154
|
placement: "top",
|
|
802
1155
|
offset: 8,
|
|
803
|
-
onHide: () =>
|
|
1156
|
+
onHide: () => setForm(null)
|
|
804
1157
|
},
|
|
805
1158
|
className: "z-[9999]",
|
|
806
1159
|
children: /* @__PURE__ */ jsx(
|
|
@@ -811,11 +1164,11 @@ function RichTextEditor({
|
|
|
811
1164
|
actions,
|
|
812
1165
|
labels,
|
|
813
1166
|
disabled,
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1167
|
+
form,
|
|
1168
|
+
onFormChange: setForm,
|
|
1169
|
+
formMode: "inline",
|
|
817
1170
|
onEscape: () => {
|
|
818
|
-
|
|
1171
|
+
setForm(null);
|
|
819
1172
|
editor.commands.focus();
|
|
820
1173
|
},
|
|
821
1174
|
className: "rounded-lg border border-gray-200 bg-white p-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"
|
|
@@ -861,6 +1214,7 @@ function RichTextEditor({
|
|
|
861
1214
|
{
|
|
862
1215
|
type: "hidden",
|
|
863
1216
|
name,
|
|
1217
|
+
disabled,
|
|
864
1218
|
value: typeof current === "string" ? current : JSON.stringify(current)
|
|
865
1219
|
}
|
|
866
1220
|
)
|