@railway/inkwell 0.1.4 → 1.0.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/README.md +22 -13
- package/dist/index.cjs +2143 -568
- package/dist/index.d.cts +385 -282
- package/dist/index.d.ts +385 -282
- package/dist/index.js +2142 -571
- package/package.json +7 -8
- package/src/styles.css +474 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { withReact, ReactEditor, Slate, Editable } from 'slate-react';
|
|
1
|
+
import { forwardRef, useMemo, useRef, useState, useCallback, useEffect, useImperativeHandle, useLayoutEffect, Fragment, useId, createElement } from 'react';
|
|
2
|
+
import { createEditor, Transforms, Editor, Node, Range, Element, Path } from 'slate';
|
|
3
|
+
import { withHistory, HistoryEditor } from 'slate-history';
|
|
4
|
+
import { withReact, ReactEditor, Slate, Editable, useSelected } from 'slate-react';
|
|
6
5
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
7
6
|
import rehypeHighlight from 'rehype-highlight';
|
|
8
7
|
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
|
@@ -12,7 +11,7 @@ import remarkParse from 'remark-parse';
|
|
|
12
11
|
import remarkRehype from 'remark-rehype';
|
|
13
12
|
import { unified } from 'unified';
|
|
14
13
|
import { toString } from 'mdast-util-to-string';
|
|
15
|
-
import { visit } from 'unist-util-visit';
|
|
14
|
+
import { visit, SKIP } from 'unist-util-visit';
|
|
16
15
|
import rehypeParse from 'rehype-parse';
|
|
17
16
|
import rehypeRemark from 'rehype-remark';
|
|
18
17
|
import remarkStringify from 'remark-stringify';
|
|
@@ -103,9 +102,12 @@ function BubbleMenuWidget({
|
|
|
103
102
|
}
|
|
104
103
|
const rect = sel.getRangeAt(0).getBoundingClientRect();
|
|
105
104
|
const editorRect = editorRef.current.getBoundingClientRect();
|
|
105
|
+
const topInEditor = rect.top - editorRect.top;
|
|
106
|
+
const hasRoomAbove = topInEditor >= 48;
|
|
106
107
|
setPosition({
|
|
107
|
-
top: rect.
|
|
108
|
-
left: rect.left - editorRect.left + rect.width / 2
|
|
108
|
+
top: hasRoomAbove ? topInEditor : rect.bottom - editorRect.top,
|
|
109
|
+
left: rect.left - editorRect.left + rect.width / 2,
|
|
110
|
+
placement: hasRoomAbove ? "above" : "below"
|
|
109
111
|
});
|
|
110
112
|
}, [editorRef]);
|
|
111
113
|
useEffect(() => {
|
|
@@ -129,9 +131,9 @@ function BubbleMenuWidget({
|
|
|
129
131
|
position: "absolute",
|
|
130
132
|
top: position.top,
|
|
131
133
|
left: position.left,
|
|
132
|
-
transform: "translateX(-50%) translateY(-100%)",
|
|
133
|
-
marginTop: -8,
|
|
134
|
-
zIndex:
|
|
134
|
+
transform: position.placement === "above" ? "translateX(-50%) translateY(-100%)" : "translateX(-50%)",
|
|
135
|
+
marginTop: position.placement === "above" ? -8 : 8,
|
|
136
|
+
zIndex: 1100
|
|
135
137
|
},
|
|
136
138
|
onMouseDown: (e) => e.preventDefault(),
|
|
137
139
|
children: /* @__PURE__ */ jsx("div", { className: cls("inner"), children: items.map((item) => /* @__PURE__ */ jsx(item.render, { wrapSelection }, item.key)) })
|
|
@@ -202,17 +204,13 @@ function remarkNoTables() {
|
|
|
202
204
|
|
|
203
205
|
// src/lib/render-html.ts
|
|
204
206
|
var processorCache = /* @__PURE__ */ new Map();
|
|
205
|
-
function
|
|
206
|
-
const key = rehypePlugins ? JSON.stringify(
|
|
207
|
-
rehypePlugins.map((p) => Array.isArray(p) ? p[1] : "default")
|
|
208
|
-
) : "default";
|
|
209
|
-
const cached = processorCache.get(key);
|
|
210
|
-
if (cached) return cached;
|
|
207
|
+
function createProcessor(rehypePlugins) {
|
|
211
208
|
const processor2 = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype);
|
|
212
209
|
const plugins = rehypePlugins ?? [[rehypeHighlight, { detect: true }]];
|
|
213
210
|
for (const plugin of plugins) {
|
|
214
211
|
if (Array.isArray(plugin)) {
|
|
215
|
-
|
|
212
|
+
const [rehypePlugin, ...options] = plugin;
|
|
213
|
+
processor2.use(rehypePlugin, ...options);
|
|
216
214
|
} else {
|
|
217
215
|
processor2.use(plugin);
|
|
218
216
|
}
|
|
@@ -227,7 +225,14 @@ function getProcessor(rehypePlugins) {
|
|
|
227
225
|
}
|
|
228
226
|
});
|
|
229
227
|
processor2.use(rehypeStringify);
|
|
230
|
-
|
|
228
|
+
return processor2;
|
|
229
|
+
}
|
|
230
|
+
function getProcessor(rehypePlugins) {
|
|
231
|
+
if (rehypePlugins) return createProcessor(rehypePlugins);
|
|
232
|
+
const cached = processorCache.get("default");
|
|
233
|
+
if (cached) return cached;
|
|
234
|
+
const processor2 = createProcessor();
|
|
235
|
+
processorCache.set("default", processor2);
|
|
231
236
|
return processor2;
|
|
232
237
|
}
|
|
233
238
|
function escapeBareBq(markdown) {
|
|
@@ -246,7 +251,7 @@ function computeDecorations(entry, editor, rehypePlugins) {
|
|
|
246
251
|
if (element.type === "code-line") {
|
|
247
252
|
return computeCodeDecorations(entry, editor, rehypePlugins);
|
|
248
253
|
}
|
|
249
|
-
if (element.type === "paragraph" || element.type === "blockquote" || element.type === "
|
|
254
|
+
if (element.type === "paragraph" || element.type === "blockquote" || element.type === "heading") {
|
|
250
255
|
return computeInlineDecorations(entry);
|
|
251
256
|
}
|
|
252
257
|
if (element.type === "code-fence") {
|
|
@@ -500,6 +505,37 @@ function parseHljsRanges(html, elementPath) {
|
|
|
500
505
|
}
|
|
501
506
|
return ranges;
|
|
502
507
|
}
|
|
508
|
+
|
|
509
|
+
// src/editor/slate/features.ts
|
|
510
|
+
var DEFAULT_FEATURES = {
|
|
511
|
+
heading1: true,
|
|
512
|
+
heading2: true,
|
|
513
|
+
heading3: true,
|
|
514
|
+
heading4: true,
|
|
515
|
+
heading5: true,
|
|
516
|
+
heading6: true,
|
|
517
|
+
blockquotes: true,
|
|
518
|
+
codeBlocks: true,
|
|
519
|
+
images: true
|
|
520
|
+
};
|
|
521
|
+
var resolveFeatures = (features) => {
|
|
522
|
+
if (!features) return DEFAULT_FEATURES;
|
|
523
|
+
const maybeResolved = features;
|
|
524
|
+
const headings = features.headings;
|
|
525
|
+
const headingOverrides = typeof headings === "object" && headings !== null ? headings : null;
|
|
526
|
+
const allHeadings = typeof headings === "boolean" ? headings : true;
|
|
527
|
+
return {
|
|
528
|
+
heading1: maybeResolved.heading1 ?? headingOverrides?.h1 ?? allHeadings,
|
|
529
|
+
heading2: maybeResolved.heading2 ?? headingOverrides?.h2 ?? allHeadings,
|
|
530
|
+
heading3: maybeResolved.heading3 ?? headingOverrides?.h3 ?? allHeadings,
|
|
531
|
+
heading4: maybeResolved.heading4 ?? headingOverrides?.h4 ?? allHeadings,
|
|
532
|
+
heading5: maybeResolved.heading5 ?? headingOverrides?.h5 ?? allHeadings,
|
|
533
|
+
heading6: maybeResolved.heading6 ?? headingOverrides?.h6 ?? allHeadings,
|
|
534
|
+
blockquotes: features.blockquotes ?? true,
|
|
535
|
+
codeBlocks: features.codeBlocks ?? true,
|
|
536
|
+
images: features.images ?? true
|
|
537
|
+
};
|
|
538
|
+
};
|
|
503
539
|
function generateId() {
|
|
504
540
|
return crypto.randomUUID();
|
|
505
541
|
}
|
|
@@ -534,24 +570,21 @@ function withNodeId(editor) {
|
|
|
534
570
|
|
|
535
571
|
// src/editor/slate/deserialize.ts
|
|
536
572
|
var HEADING_RE = /^(#{1,6}) /;
|
|
537
|
-
|
|
538
|
-
|
|
573
|
+
var IMAGE_RE = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
|
|
574
|
+
function deserialize(content, features) {
|
|
575
|
+
if (!content) {
|
|
539
576
|
return [{ type: "paragraph", id: generateId(), children: [{ text: "" }] }];
|
|
540
577
|
}
|
|
578
|
+
const cfg = resolveFeatures(features);
|
|
541
579
|
const headingEnabled = [
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
580
|
+
cfg.heading1,
|
|
581
|
+
cfg.heading2,
|
|
582
|
+
cfg.heading3,
|
|
583
|
+
cfg.heading4,
|
|
584
|
+
cfg.heading5,
|
|
585
|
+
cfg.heading6
|
|
548
586
|
];
|
|
549
|
-
const
|
|
550
|
-
lists: decorations?.lists ?? true,
|
|
551
|
-
blockquotes: decorations?.blockquotes ?? true,
|
|
552
|
-
codeBlocks: decorations?.codeBlocks ?? true
|
|
553
|
-
};
|
|
554
|
-
const lines = markdown.split("\n");
|
|
587
|
+
const lines = content.split("\n");
|
|
555
588
|
const result = [];
|
|
556
589
|
let inCodeBlock = false;
|
|
557
590
|
let paragraphLines = [];
|
|
@@ -564,19 +597,22 @@ function deserialize(markdown, decorations) {
|
|
|
564
597
|
type: "heading",
|
|
565
598
|
id: generateId(),
|
|
566
599
|
level: hMatch[1].length,
|
|
567
|
-
children: [{ text: line
|
|
600
|
+
children: [{ text: line }]
|
|
568
601
|
});
|
|
569
602
|
continue;
|
|
570
603
|
}
|
|
571
|
-
|
|
604
|
+
const imageMatch = cfg.images ? IMAGE_RE.exec(line) : null;
|
|
605
|
+
if (imageMatch) {
|
|
572
606
|
result.push({
|
|
573
|
-
type: "
|
|
607
|
+
type: "image",
|
|
574
608
|
id: generateId(),
|
|
575
|
-
|
|
609
|
+
alt: imageMatch[1],
|
|
610
|
+
url: imageMatch[2],
|
|
611
|
+
children: [{ text: line }]
|
|
576
612
|
});
|
|
577
|
-
} else if (cfg.
|
|
613
|
+
} else if (cfg.blockquotes && /^> /.test(line)) {
|
|
578
614
|
result.push({
|
|
579
|
-
type: "
|
|
615
|
+
type: "blockquote",
|
|
580
616
|
id: generateId(),
|
|
581
617
|
children: [{ text: line }]
|
|
582
618
|
});
|
|
@@ -632,6 +668,41 @@ function deserialize(markdown, decorations) {
|
|
|
632
668
|
flushParagraph();
|
|
633
669
|
return result.length > 0 ? result : [{ type: "paragraph", id: generateId(), children: [{ text: "" }] }];
|
|
634
670
|
}
|
|
671
|
+
|
|
672
|
+
// src/lib/safe-url.ts
|
|
673
|
+
function isSafeImageUrl(rawUrl) {
|
|
674
|
+
if (typeof rawUrl !== "string") return false;
|
|
675
|
+
if (Array.from(rawUrl).some((char) => {
|
|
676
|
+
const code = char.charCodeAt(0);
|
|
677
|
+
return code <= 31 || code === 127;
|
|
678
|
+
})) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
const url = rawUrl.trim();
|
|
682
|
+
if (url.length === 0) return false;
|
|
683
|
+
if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) {
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
const lower = url.toLowerCase();
|
|
690
|
+
if (lower.startsWith("http://") || lower.startsWith("https://")) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
if (lower.startsWith("blob:")) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
if (/^data:image\/(png|jpeg|jpg|gif|webp);/.test(lower)) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
function sanitizeImageUrl(rawUrl) {
|
|
702
|
+
if (!rawUrl) return void 0;
|
|
703
|
+
const url = rawUrl.trim();
|
|
704
|
+
return isSafeImageUrl(rawUrl) ? url : void 0;
|
|
705
|
+
}
|
|
635
706
|
function RenderElement({
|
|
636
707
|
attributes,
|
|
637
708
|
children,
|
|
@@ -654,12 +725,39 @@ function RenderElement({
|
|
|
654
725
|
return /* @__PURE__ */ jsx("p", { ...attributes, className: editorClass("code-line"), children });
|
|
655
726
|
case "blockquote":
|
|
656
727
|
return /* @__PURE__ */ jsx("p", { ...attributes, className: editorClass("blockquote"), children });
|
|
657
|
-
case "
|
|
658
|
-
return /* @__PURE__ */ jsx(
|
|
728
|
+
case "image":
|
|
729
|
+
return /* @__PURE__ */ jsx(ImageElement, { attributes, element: el, children });
|
|
659
730
|
default:
|
|
660
731
|
return /* @__PURE__ */ jsx("p", { ...attributes, children });
|
|
661
732
|
}
|
|
662
733
|
}
|
|
734
|
+
function ImageElement({
|
|
735
|
+
attributes,
|
|
736
|
+
element,
|
|
737
|
+
children
|
|
738
|
+
}) {
|
|
739
|
+
const selected = useSelected();
|
|
740
|
+
return /* @__PURE__ */ jsxs(
|
|
741
|
+
"div",
|
|
742
|
+
{
|
|
743
|
+
...attributes,
|
|
744
|
+
className: editorClass("image"),
|
|
745
|
+
"data-selected": selected || void 0,
|
|
746
|
+
children: [
|
|
747
|
+
/* @__PURE__ */ jsx(
|
|
748
|
+
"img",
|
|
749
|
+
{
|
|
750
|
+
src: sanitizeImageUrl(element.url),
|
|
751
|
+
alt: element.alt ?? "",
|
|
752
|
+
contentEditable: false,
|
|
753
|
+
draggable: false
|
|
754
|
+
}
|
|
755
|
+
),
|
|
756
|
+
children
|
|
757
|
+
]
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
}
|
|
663
761
|
function RenderLeaf({ attributes, children, leaf }) {
|
|
664
762
|
const l = leaf;
|
|
665
763
|
if (l.boldMarker || l.italicMarker || l.strikeMarker) {
|
|
@@ -676,53 +774,19 @@ function RenderLeaf({ attributes, children, leaf }) {
|
|
|
676
774
|
if (l.hljs) {
|
|
677
775
|
content = /* @__PURE__ */ jsx("span", { className: l.hljs, children: content });
|
|
678
776
|
}
|
|
679
|
-
if (l.remoteCursor) {
|
|
680
|
-
content = /* @__PURE__ */ jsx(
|
|
681
|
-
"span",
|
|
682
|
-
{
|
|
683
|
-
className: editorClass("remote-cursor"),
|
|
684
|
-
style: { backgroundColor: `${l.remoteCursor}30` },
|
|
685
|
-
children: content
|
|
686
|
-
}
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
if (l.remoteCursorCaret) {
|
|
690
|
-
content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
691
|
-
/* @__PURE__ */ jsx(
|
|
692
|
-
"span",
|
|
693
|
-
{
|
|
694
|
-
className: editorClass("remote-caret"),
|
|
695
|
-
style: { borderColor: l.remoteCursor },
|
|
696
|
-
contentEditable: false
|
|
697
|
-
}
|
|
698
|
-
),
|
|
699
|
-
content
|
|
700
|
-
] });
|
|
701
|
-
}
|
|
702
777
|
return /* @__PURE__ */ jsx("span", { ...attributes, children: content });
|
|
703
778
|
}
|
|
779
|
+
var LIST_LIKE_PARAGRAPH_RE = /^\s*(?:[-*+]|\d+\.)(?:\s|$)/;
|
|
780
|
+
var isListLikeParagraph = (entry) => entry.type === "paragraph" && LIST_LIKE_PARAGRAPH_RE.test(entry.text);
|
|
704
781
|
function serialize(nodes) {
|
|
705
782
|
const entries = [];
|
|
706
783
|
for (const node of nodes) {
|
|
707
784
|
const text = Node.string(node);
|
|
708
785
|
const type = node.type;
|
|
709
|
-
if (type === "
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
entries.push({ text:
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
if (type === "blockquote") {
|
|
716
|
-
const lines = text.split("\n").filter((l) => l.trim() !== "");
|
|
717
|
-
if (lines.length === 0) {
|
|
718
|
-
entries.push({ text: "> ", type });
|
|
719
|
-
} else {
|
|
720
|
-
const prefixed = lines.map((line) => {
|
|
721
|
-
const escaped = line.replace(/^(>+)/g, (m) => "\\>".repeat(m.length));
|
|
722
|
-
return "> " + escaped;
|
|
723
|
-
}).join("\n>\n");
|
|
724
|
-
entries.push({ text: prefixed, type });
|
|
725
|
-
}
|
|
786
|
+
if (type === "image" && !text) {
|
|
787
|
+
const url = node.url ?? "";
|
|
788
|
+
const alt = node.alt ?? "";
|
|
789
|
+
entries.push({ text: ``, type });
|
|
726
790
|
continue;
|
|
727
791
|
}
|
|
728
792
|
if (type === "paragraph" && !text.trim()) {
|
|
@@ -735,18 +799,44 @@ function serialize(nodes) {
|
|
|
735
799
|
let result = "";
|
|
736
800
|
for (let i = 0; i < entries.length; i++) {
|
|
737
801
|
if (i > 0) {
|
|
738
|
-
const prev = entries[i - 1]
|
|
739
|
-
const curr = entries[i]
|
|
740
|
-
const sameGroup = prev === "blockquote" && curr === "blockquote" || prev
|
|
802
|
+
const prev = entries[i - 1];
|
|
803
|
+
const curr = entries[i];
|
|
804
|
+
const sameGroup = prev.type === "blockquote" && curr.type === "blockquote" || codeTypes.has(prev.type) && codeTypes.has(curr.type) || isListLikeParagraph(prev) && isListLikeParagraph(curr);
|
|
741
805
|
result += sameGroup ? "\n" : "\n\n";
|
|
742
806
|
}
|
|
743
807
|
result += entries[i].text;
|
|
744
808
|
}
|
|
745
|
-
return result.replace(/\n{3,}/g, "\n\n").
|
|
809
|
+
return result.replace(/\n{3,}/g, "\n\n").replace(/^\n+|\n+$/g, "");
|
|
746
810
|
}
|
|
747
811
|
var HEADING_RE2 = /^#{1,6}$/;
|
|
748
|
-
|
|
749
|
-
|
|
812
|
+
var UNORDERED_LIST_CONTINUE_RE = /^(\s*)([-*+]) \S/;
|
|
813
|
+
var UNORDERED_LIST_EMPTY_RE = /^(\s*)([-*+]) ?$/;
|
|
814
|
+
var HEADING_LINE_RE = /^(#{1,6})\s/;
|
|
815
|
+
function classifyLine(text, deco) {
|
|
816
|
+
const headingMatch = HEADING_LINE_RE.exec(text);
|
|
817
|
+
if (headingMatch) {
|
|
818
|
+
const level = headingMatch[1].length;
|
|
819
|
+
const key = `heading${level}`;
|
|
820
|
+
if (deco[key]) return { type: "heading", level };
|
|
821
|
+
}
|
|
822
|
+
if (deco.blockquotes && /^>\s/.test(text)) {
|
|
823
|
+
return { type: "blockquote" };
|
|
824
|
+
}
|
|
825
|
+
return { type: "paragraph" };
|
|
826
|
+
}
|
|
827
|
+
function withMarkdown(editor, featuresRef) {
|
|
828
|
+
const {
|
|
829
|
+
insertBreak,
|
|
830
|
+
insertData,
|
|
831
|
+
insertText,
|
|
832
|
+
isVoid,
|
|
833
|
+
normalizeNode,
|
|
834
|
+
setFragmentData
|
|
835
|
+
} = editor;
|
|
836
|
+
editor.isVoid = (element) => {
|
|
837
|
+
if (element.type === "image") return true;
|
|
838
|
+
return isVoid(element);
|
|
839
|
+
};
|
|
750
840
|
editor.insertBreak = () => {
|
|
751
841
|
const { selection } = editor;
|
|
752
842
|
if (!selection) return insertBreak();
|
|
@@ -757,7 +847,7 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
757
847
|
const [node, path] = match;
|
|
758
848
|
const element = node;
|
|
759
849
|
const text = Node.string(node);
|
|
760
|
-
const deco =
|
|
850
|
+
const deco = featuresRef.current;
|
|
761
851
|
if (deco.codeBlocks && element.type === "paragraph" && text.startsWith("```")) {
|
|
762
852
|
Transforms.setNodes(editor, {
|
|
763
853
|
type: "code-fence"
|
|
@@ -794,7 +884,13 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
794
884
|
}
|
|
795
885
|
if (element.type === "blockquote") {
|
|
796
886
|
const text2 = Node.string(node);
|
|
797
|
-
if (
|
|
887
|
+
if (/^>\s*$/.test(text2)) {
|
|
888
|
+
Transforms.delete(editor, {
|
|
889
|
+
at: {
|
|
890
|
+
anchor: Editor.start(editor, path),
|
|
891
|
+
focus: Editor.end(editor, path)
|
|
892
|
+
}
|
|
893
|
+
});
|
|
798
894
|
Transforms.setNodes(editor, {
|
|
799
895
|
type: "paragraph"
|
|
800
896
|
});
|
|
@@ -812,22 +908,124 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
812
908
|
return;
|
|
813
909
|
}
|
|
814
910
|
if (element.type === "heading") {
|
|
815
|
-
if (!text.trim()) {
|
|
911
|
+
if (!text.trim() || /^#{1,6}\s*$/.test(text)) {
|
|
912
|
+
Transforms.delete(editor, {
|
|
913
|
+
at: {
|
|
914
|
+
anchor: Editor.start(editor, path),
|
|
915
|
+
focus: Editor.end(editor, path)
|
|
916
|
+
}
|
|
917
|
+
});
|
|
816
918
|
Transforms.setNodes(editor, {
|
|
817
919
|
type: "paragraph"
|
|
818
920
|
});
|
|
819
921
|
Transforms.unsetNodes(editor, "level");
|
|
820
922
|
return;
|
|
821
923
|
}
|
|
822
|
-
|
|
924
|
+
if (!Range.isCollapsed(selection)) {
|
|
925
|
+
Transforms.delete(editor);
|
|
926
|
+
}
|
|
927
|
+
const point = editor.selection?.anchor;
|
|
928
|
+
const cursorOffset = point?.offset ?? text.length;
|
|
929
|
+
const endPoint = Editor.end(editor, path);
|
|
930
|
+
const tail = point ? Editor.string(editor, { anchor: point, focus: endPoint }) : "";
|
|
931
|
+
if (point && tail.length > 0) {
|
|
932
|
+
Transforms.delete(editor, { at: { anchor: point, focus: endPoint } });
|
|
933
|
+
}
|
|
934
|
+
const head = text.slice(0, cursorOffset);
|
|
935
|
+
const headClass = classifyLine(head, deco);
|
|
936
|
+
if (headClass.type === "heading" && headClass.level !== void 0) {
|
|
937
|
+
Transforms.setNodes(editor, {
|
|
938
|
+
type: "heading",
|
|
939
|
+
level: headClass.level
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
Transforms.setNodes(editor, {
|
|
943
|
+
type: headClass.type
|
|
944
|
+
});
|
|
945
|
+
Transforms.unsetNodes(editor, "level");
|
|
946
|
+
}
|
|
947
|
+
const tailClass = classifyLine(tail, deco);
|
|
948
|
+
const newNode = tailClass.type === "heading" && tailClass.level !== void 0 ? {
|
|
949
|
+
type: "heading",
|
|
950
|
+
id: generateId(),
|
|
951
|
+
level: tailClass.level,
|
|
952
|
+
children: [{ text: tail }]
|
|
953
|
+
} : {
|
|
823
954
|
type: "paragraph",
|
|
824
955
|
id: generateId(),
|
|
825
|
-
children: [{ text:
|
|
956
|
+
children: [{ text: tail }]
|
|
826
957
|
};
|
|
827
|
-
Transforms.insertNodes(editor,
|
|
958
|
+
Transforms.insertNodes(editor, newNode, { at: Path.next(path) });
|
|
828
959
|
Transforms.select(editor, Editor.start(editor, Path.next(path)));
|
|
829
960
|
return;
|
|
830
961
|
}
|
|
962
|
+
if (element.type === "paragraph") {
|
|
963
|
+
const emptyMatch = UNORDERED_LIST_EMPTY_RE.exec(text);
|
|
964
|
+
if (emptyMatch) {
|
|
965
|
+
const [, indent, marker] = emptyMatch;
|
|
966
|
+
Transforms.delete(editor, {
|
|
967
|
+
at: {
|
|
968
|
+
anchor: Editor.start(editor, path),
|
|
969
|
+
focus: Editor.end(editor, path)
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
if (indent.length >= 2) {
|
|
973
|
+
editor.insertText(`${indent.slice(2)}${marker} `);
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const continueMatch = UNORDERED_LIST_CONTINUE_RE.exec(text);
|
|
978
|
+
if (continueMatch) {
|
|
979
|
+
const [, indent, marker] = continueMatch;
|
|
980
|
+
const prefix = `${indent}${marker} `;
|
|
981
|
+
if (!Range.isCollapsed(selection)) {
|
|
982
|
+
Transforms.delete(editor);
|
|
983
|
+
}
|
|
984
|
+
const point = editor.selection?.anchor;
|
|
985
|
+
const cursorOffset = point?.offset ?? text.length;
|
|
986
|
+
if (cursorOffset < prefix.length) {
|
|
987
|
+
const newParagraph2 = {
|
|
988
|
+
type: "paragraph",
|
|
989
|
+
id: generateId(),
|
|
990
|
+
children: [{ text: prefix }]
|
|
991
|
+
};
|
|
992
|
+
Transforms.insertNodes(editor, newParagraph2, {
|
|
993
|
+
at: Path.next(path)
|
|
994
|
+
});
|
|
995
|
+
Transforms.select(editor, Editor.end(editor, Path.next(path)));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (cursorOffset === prefix.length) {
|
|
999
|
+
const newParagraph2 = {
|
|
1000
|
+
type: "paragraph",
|
|
1001
|
+
id: generateId(),
|
|
1002
|
+
children: [{ text: prefix }]
|
|
1003
|
+
};
|
|
1004
|
+
Transforms.insertNodes(editor, newParagraph2, { at: path });
|
|
1005
|
+
Transforms.select(editor, {
|
|
1006
|
+
path: [...Path.next(path), 0],
|
|
1007
|
+
offset: prefix.length
|
|
1008
|
+
});
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const endPoint = Editor.end(editor, path);
|
|
1012
|
+
const tail = point ? Editor.string(editor, { anchor: point, focus: endPoint }) : "";
|
|
1013
|
+
if (point && tail.length > 0) {
|
|
1014
|
+
Transforms.delete(editor, { at: { anchor: point, focus: endPoint } });
|
|
1015
|
+
}
|
|
1016
|
+
const newParagraph = {
|
|
1017
|
+
type: "paragraph",
|
|
1018
|
+
id: generateId(),
|
|
1019
|
+
children: [{ text: `${prefix}${tail}` }]
|
|
1020
|
+
};
|
|
1021
|
+
Transforms.insertNodes(editor, newParagraph, { at: Path.next(path) });
|
|
1022
|
+
Transforms.select(editor, {
|
|
1023
|
+
path: [...Path.next(path), 0],
|
|
1024
|
+
offset: prefix.length
|
|
1025
|
+
});
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
831
1029
|
if (element.type === "code-line") {
|
|
832
1030
|
const newLine = {
|
|
833
1031
|
type: "code-line",
|
|
@@ -838,28 +1036,14 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
838
1036
|
Transforms.select(editor, Editor.start(editor, Path.next(path)));
|
|
839
1037
|
return;
|
|
840
1038
|
}
|
|
841
|
-
if (element.type === "
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
Transforms.delete(editor, {
|
|
845
|
-
at: {
|
|
846
|
-
anchor: Editor.start(editor, path),
|
|
847
|
-
focus: Editor.end(editor, path)
|
|
848
|
-
}
|
|
849
|
-
});
|
|
850
|
-
Transforms.setNodes(editor, {
|
|
851
|
-
type: "paragraph"
|
|
852
|
-
});
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
const marker = text2.match(/^([-*+] )/)?.[1] || "- ";
|
|
856
|
-
const newItem = {
|
|
857
|
-
type: "list-item",
|
|
1039
|
+
if (element.type === "image") {
|
|
1040
|
+
const newParagraph = {
|
|
1041
|
+
type: "paragraph",
|
|
858
1042
|
id: generateId(),
|
|
859
|
-
children: [{ text:
|
|
1043
|
+
children: [{ text: "" }]
|
|
860
1044
|
};
|
|
861
|
-
Transforms.insertNodes(editor,
|
|
862
|
-
Transforms.select(editor, Editor.
|
|
1045
|
+
Transforms.insertNodes(editor, newParagraph, { at: Path.next(path) });
|
|
1046
|
+
Transforms.select(editor, Editor.start(editor, Path.next(path)));
|
|
863
1047
|
return;
|
|
864
1048
|
}
|
|
865
1049
|
insertBreak();
|
|
@@ -878,7 +1062,7 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
878
1062
|
const newBq = {
|
|
879
1063
|
type: "blockquote",
|
|
880
1064
|
id: generateId(),
|
|
881
|
-
children: [{ text: "" }]
|
|
1065
|
+
children: [{ text: "> " }]
|
|
882
1066
|
};
|
|
883
1067
|
Transforms.insertNodes(editor, newBq, { at: Path.next(path) });
|
|
884
1068
|
Transforms.select(editor, Editor.start(editor, Path.next(path)));
|
|
@@ -907,7 +1091,7 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
907
1091
|
const [node, path] = match;
|
|
908
1092
|
const element = node;
|
|
909
1093
|
const currentText = Node.string(node);
|
|
910
|
-
const deco =
|
|
1094
|
+
const deco = featuresRef.current;
|
|
911
1095
|
if (element.type === "code-line" && currentText === "```" && text !== "" && text !== "\n") {
|
|
912
1096
|
Transforms.setNodes(editor, {
|
|
913
1097
|
type: "code-fence"
|
|
@@ -922,12 +1106,7 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
922
1106
|
return;
|
|
923
1107
|
}
|
|
924
1108
|
if (deco.blockquotes && element.type === "paragraph" && text === " " && currentText === ">") {
|
|
925
|
-
|
|
926
|
-
at: {
|
|
927
|
-
anchor: Editor.start(editor, path),
|
|
928
|
-
focus: Editor.end(editor, path)
|
|
929
|
-
}
|
|
930
|
-
});
|
|
1109
|
+
insertText(text);
|
|
931
1110
|
Transforms.setNodes(editor, {
|
|
932
1111
|
type: "blockquote"
|
|
933
1112
|
});
|
|
@@ -937,25 +1116,13 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
937
1116
|
const headingKey = `heading${headingLevel}`;
|
|
938
1117
|
if (element.type === "paragraph" && text === " " && HEADING_RE2.test(currentText) && deco[headingKey]) {
|
|
939
1118
|
const level = headingLevel;
|
|
940
|
-
|
|
941
|
-
at: {
|
|
942
|
-
anchor: Editor.start(editor, path),
|
|
943
|
-
focus: Editor.end(editor, path)
|
|
944
|
-
}
|
|
945
|
-
});
|
|
1119
|
+
insertText(text);
|
|
946
1120
|
Transforms.setNodes(editor, {
|
|
947
1121
|
type: "heading",
|
|
948
1122
|
level
|
|
949
1123
|
});
|
|
950
1124
|
return;
|
|
951
1125
|
}
|
|
952
|
-
if (deco.lists && element.type === "paragraph" && text === " " && (currentText === "-" || currentText === "*" || currentText === "+")) {
|
|
953
|
-
insertText(text);
|
|
954
|
-
Transforms.setNodes(editor, {
|
|
955
|
-
type: "list-item"
|
|
956
|
-
});
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
1126
|
if (element.type === "code-fence") {
|
|
960
1127
|
const prevIdx = path[0] - 1;
|
|
961
1128
|
if (prevIdx >= 0) {
|
|
@@ -979,494 +1146,1816 @@ function withMarkdown(editor, decorationsRef) {
|
|
|
979
1146
|
editor.insertData = (data) => {
|
|
980
1147
|
const text = data.getData("text/plain");
|
|
981
1148
|
if (text) {
|
|
982
|
-
const nodes = deserialize(text,
|
|
1149
|
+
const nodes = deserialize(text, featuresRef.current);
|
|
983
1150
|
Transforms.insertNodes(editor, nodes);
|
|
984
1151
|
return;
|
|
985
1152
|
}
|
|
986
1153
|
insertData(data);
|
|
987
1154
|
};
|
|
1155
|
+
editor.setFragmentData = (data, originEvent) => {
|
|
1156
|
+
try {
|
|
1157
|
+
setFragmentData(data, originEvent);
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
const fragment = editor.getFragment();
|
|
1161
|
+
if (fragment.length > 0) {
|
|
1162
|
+
data.setData("text/plain", serialize(fragment));
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
editor.normalizeNode = (entry) => {
|
|
1166
|
+
const [node, path] = entry;
|
|
1167
|
+
if (Element.isElement(node)) {
|
|
1168
|
+
const element = node;
|
|
1169
|
+
const textDriven = element.type === "paragraph" || element.type === "heading" || element.type === "blockquote";
|
|
1170
|
+
if (textDriven) {
|
|
1171
|
+
const text = Node.string(node);
|
|
1172
|
+
const cls2 = classifyLine(text, featuresRef.current);
|
|
1173
|
+
if (cls2.type === "heading" && cls2.level !== void 0) {
|
|
1174
|
+
if (element.type !== "heading" || element.level !== cls2.level) {
|
|
1175
|
+
Transforms.setNodes(
|
|
1176
|
+
editor,
|
|
1177
|
+
{
|
|
1178
|
+
type: "heading",
|
|
1179
|
+
level: cls2.level
|
|
1180
|
+
},
|
|
1181
|
+
{ at: path }
|
|
1182
|
+
);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
} else if (cls2.type === "blockquote") {
|
|
1186
|
+
if (element.type !== "blockquote") {
|
|
1187
|
+
Transforms.setNodes(
|
|
1188
|
+
editor,
|
|
1189
|
+
{ type: "blockquote" },
|
|
1190
|
+
{ at: path }
|
|
1191
|
+
);
|
|
1192
|
+
if (element.level !== void 0) {
|
|
1193
|
+
Transforms.unsetNodes(editor, "level", { at: path });
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
if (element.type !== "paragraph") {
|
|
1199
|
+
Transforms.setNodes(
|
|
1200
|
+
editor,
|
|
1201
|
+
{ type: "paragraph" },
|
|
1202
|
+
{ at: path }
|
|
1203
|
+
);
|
|
1204
|
+
if (element.level !== void 0) {
|
|
1205
|
+
Transforms.unsetNodes(editor, "level", { at: path });
|
|
1206
|
+
}
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
normalizeNode(entry);
|
|
1213
|
+
};
|
|
988
1214
|
return editor;
|
|
989
1215
|
}
|
|
990
1216
|
var IS_SERVER = typeof window === "undefined";
|
|
991
|
-
var
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
heading5: true,
|
|
997
|
-
heading6: true,
|
|
998
|
-
lists: true,
|
|
999
|
-
blockquotes: true,
|
|
1000
|
-
codeBlocks: true
|
|
1001
|
-
};
|
|
1002
|
-
function InkwellEditor({
|
|
1003
|
-
content,
|
|
1004
|
-
onChange,
|
|
1005
|
-
className,
|
|
1006
|
-
placeholder,
|
|
1007
|
-
plugins: userPlugins = [],
|
|
1008
|
-
rehypePlugins,
|
|
1009
|
-
decorations,
|
|
1010
|
-
collaboration,
|
|
1011
|
-
bubbleMenu = true
|
|
1217
|
+
var EMPTY_PLUGINS = [];
|
|
1218
|
+
function CharacterCount({
|
|
1219
|
+
count,
|
|
1220
|
+
limit,
|
|
1221
|
+
over
|
|
1012
1222
|
}) {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const { sharedType, awareness, user } = collaboration;
|
|
1028
|
-
const yjsEditor = withYjs(base, sharedType, { autoConnect: false });
|
|
1029
|
-
const cursorEditor = withCursors(yjsEditor, awareness, {
|
|
1030
|
-
data: user
|
|
1031
|
-
});
|
|
1032
|
-
const historyEditor = withYHistory(cursorEditor);
|
|
1033
|
-
return withMarkdown(historyEditor, decorationsRef);
|
|
1223
|
+
const label = `${count} of ${limit} characters${over ? ", over limit" : ""}`;
|
|
1224
|
+
return /* @__PURE__ */ jsxs(
|
|
1225
|
+
"div",
|
|
1226
|
+
{
|
|
1227
|
+
className: `inkwell-editor-character-count${over ? " inkwell-editor-character-count-over" : ""}`,
|
|
1228
|
+
role: over ? "status" : void 0,
|
|
1229
|
+
"aria-live": over ? "polite" : "off",
|
|
1230
|
+
"aria-atomic": over ? true : void 0,
|
|
1231
|
+
"aria-label": label,
|
|
1232
|
+
children: [
|
|
1233
|
+
count,
|
|
1234
|
+
" / ",
|
|
1235
|
+
limit
|
|
1236
|
+
]
|
|
1034
1237
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}, [editor, collaboration]);
|
|
1045
|
-
const [cursorVersion, setCursorVersion] = useState(0);
|
|
1046
|
-
useEffect(() => {
|
|
1047
|
-
if (!collaboration || !CursorEditor.isCursorEditor(editor)) return;
|
|
1048
|
-
const handleChange2 = () => setCursorVersion((v) => v + 1);
|
|
1049
|
-
CursorEditor.on(editor, "change", handleChange2);
|
|
1050
|
-
return () => {
|
|
1051
|
-
CursorEditor.off(editor, "change", handleChange2);
|
|
1052
|
-
};
|
|
1053
|
-
}, [editor, collaboration]);
|
|
1054
|
-
const remoteCursorRanges = useMemo(() => {
|
|
1055
|
-
if (!collaboration || !CursorEditor.isCursorEditor(editor)) return [];
|
|
1056
|
-
const ranges = [];
|
|
1057
|
-
const states = CursorEditor.cursorStates(editor);
|
|
1058
|
-
for (const [, state] of Object.entries(states)) {
|
|
1059
|
-
if (!state.relativeSelection) continue;
|
|
1060
|
-
const data = state.data;
|
|
1061
|
-
if (!data) continue;
|
|
1062
|
-
try {
|
|
1063
|
-
const { anchor, focus } = state.relativeSelection;
|
|
1064
|
-
const anchorPoint = relativePositionToSlatePoint(
|
|
1065
|
-
collaboration.sharedType,
|
|
1066
|
-
editor,
|
|
1067
|
-
anchor
|
|
1068
|
-
);
|
|
1069
|
-
const focusPoint = relativePositionToSlatePoint(
|
|
1070
|
-
collaboration.sharedType,
|
|
1071
|
-
editor,
|
|
1072
|
-
focus
|
|
1073
|
-
);
|
|
1074
|
-
if (!anchorPoint || !focusPoint) continue;
|
|
1075
|
-
const range = { anchor: anchorPoint, focus: focusPoint };
|
|
1076
|
-
if (Range.isCollapsed(range)) {
|
|
1077
|
-
ranges.push({
|
|
1078
|
-
...range,
|
|
1079
|
-
remoteCursor: data.color,
|
|
1080
|
-
remoteCursorCaret: true
|
|
1081
|
-
});
|
|
1082
|
-
} else {
|
|
1083
|
-
ranges.push({
|
|
1084
|
-
...range,
|
|
1085
|
-
remoteCursor: data.color
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
} catch {
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
var ORDERED_LIST_MARKER_RE = /^\s*\d+\.(?:\s|$)/;
|
|
1241
|
+
var UNORDERED_LIST_MARKER_RE = /^(\s*)([-*+])(?:\s|$)/;
|
|
1242
|
+
function replaceEditorChildren(editor, nodes, withoutSaving) {
|
|
1243
|
+
const replace = () => {
|
|
1244
|
+
Editor.withoutNormalizing(editor, () => {
|
|
1245
|
+
for (let index = editor.children.length - 1; index >= 0; index--) {
|
|
1246
|
+
Transforms.removeNodes(editor, { at: [index] });
|
|
1089
1247
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
()
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1248
|
+
Transforms.insertNodes(editor, nodes, { at: [0] });
|
|
1249
|
+
});
|
|
1250
|
+
};
|
|
1251
|
+
if (HistoryEditor.isHistoryEditor(editor)) {
|
|
1252
|
+
HistoryEditor.withoutSaving(editor, replace);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
replace();
|
|
1256
|
+
}
|
|
1257
|
+
var InkwellEditor = forwardRef(function InkwellEditor2(props, ref) {
|
|
1258
|
+
if (IS_SERVER) return null;
|
|
1259
|
+
return /* @__PURE__ */ jsx(InkwellEditorClient, { ...props, ref });
|
|
1260
|
+
});
|
|
1261
|
+
var InkwellEditorClient = forwardRef(
|
|
1262
|
+
function InkwellEditorClient2({
|
|
1263
|
+
content = "",
|
|
1264
|
+
onChange,
|
|
1265
|
+
onStateChange,
|
|
1266
|
+
className,
|
|
1267
|
+
classNames,
|
|
1268
|
+
styles,
|
|
1269
|
+
placeholder,
|
|
1270
|
+
editable = true,
|
|
1271
|
+
plugins: userPlugins = EMPTY_PLUGINS,
|
|
1272
|
+
rehypePlugins,
|
|
1273
|
+
features,
|
|
1274
|
+
bubbleMenu = true,
|
|
1275
|
+
characterLimit,
|
|
1276
|
+
onCharacterCount,
|
|
1277
|
+
submitOnEnter = false,
|
|
1278
|
+
onSubmit
|
|
1279
|
+
}, ref) {
|
|
1280
|
+
const resolvedFeatures = useMemo(
|
|
1281
|
+
() => resolveFeatures(features),
|
|
1282
|
+
[features]
|
|
1283
|
+
);
|
|
1284
|
+
const featuresRef = useRef(resolvedFeatures);
|
|
1285
|
+
featuresRef.current = resolvedFeatures;
|
|
1286
|
+
const plugins = useMemo(() => {
|
|
1287
|
+
const builtIn = bubbleMenu ? [createBubbleMenuPlugin()] : [];
|
|
1288
|
+
const userNames = new Set(userPlugins.map((p) => p.name));
|
|
1289
|
+
const survivingBuiltIns = builtIn.filter((p) => !userNames.has(p.name));
|
|
1290
|
+
return [...survivingBuiltIns, ...userPlugins];
|
|
1291
|
+
}, [userPlugins, bubbleMenu]);
|
|
1292
|
+
const editor = useMemo(() => {
|
|
1293
|
+
const base = withNodeId(withReact(createEditor()));
|
|
1294
|
+
return withMarkdown(withHistory(base), featuresRef);
|
|
1295
|
+
}, []);
|
|
1296
|
+
const initialValue = useMemo(
|
|
1297
|
+
() => deserialize(content, resolvedFeatures),
|
|
1298
|
+
[]
|
|
1299
|
+
);
|
|
1300
|
+
const lastContent = useRef(content);
|
|
1301
|
+
const isInternalChange = useRef(false);
|
|
1302
|
+
const suppressImperativeOnChange = useRef(false);
|
|
1303
|
+
const [characterCount, setCharacterCount] = useState(
|
|
1304
|
+
() => serialize(initialValue).length
|
|
1305
|
+
);
|
|
1306
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
1307
|
+
const focusStateFrameRef = useRef(null);
|
|
1308
|
+
const [stateVersion, setStateVersion] = useState(0);
|
|
1309
|
+
const bumpStateVersion = useCallback(() => {
|
|
1310
|
+
setStateVersion((version) => version + 1);
|
|
1311
|
+
}, []);
|
|
1312
|
+
const scheduleFocusedState = useCallback((nextFocused) => {
|
|
1313
|
+
if (focusStateFrameRef.current !== null) {
|
|
1314
|
+
cancelAnimationFrame(focusStateFrameRef.current);
|
|
1099
1315
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
if (content === lastContent.current) return;
|
|
1112
|
-
const newValue = deserialize(content, resolvedDecorations);
|
|
1113
|
-
editor.children = newValue;
|
|
1114
|
-
Transforms.select(editor, Editor.start(editor, []));
|
|
1115
|
-
editor.onChange();
|
|
1116
|
-
lastContent.current = content;
|
|
1117
|
-
}, [content, editor, collaboration]);
|
|
1118
|
-
const handleChange = useCallback(
|
|
1119
|
-
(value) => {
|
|
1120
|
-
const isAstChange = editor.operations.some(
|
|
1121
|
-
(op) => op.type !== "set_selection"
|
|
1122
|
-
);
|
|
1123
|
-
if (!isAstChange) return;
|
|
1124
|
-
if (!onChange) return;
|
|
1125
|
-
const md = serialize(value);
|
|
1126
|
-
if (collaboration) {
|
|
1127
|
-
onChange(md);
|
|
1128
|
-
} else {
|
|
1129
|
-
if (md !== lastContent.current) {
|
|
1130
|
-
lastContent.current = md;
|
|
1131
|
-
isInternalChange.current = true;
|
|
1132
|
-
onChange(md);
|
|
1316
|
+
focusStateFrameRef.current = requestAnimationFrame(() => {
|
|
1317
|
+
focusStateFrameRef.current = null;
|
|
1318
|
+
setIsFocused(nextFocused);
|
|
1319
|
+
});
|
|
1320
|
+
}, []);
|
|
1321
|
+
useEffect(
|
|
1322
|
+
() => () => {
|
|
1323
|
+
if (focusStateFrameRef.current !== null) {
|
|
1324
|
+
cancelAnimationFrame(focusStateFrameRef.current);
|
|
1133
1325
|
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1326
|
+
},
|
|
1327
|
+
[]
|
|
1328
|
+
);
|
|
1329
|
+
const updateCharacterCount = useCallback(() => {
|
|
1330
|
+
const length = serialize(editor.children).length;
|
|
1331
|
+
setCharacterCount(length);
|
|
1332
|
+
onCharacterCount?.(length, characterLimit);
|
|
1333
|
+
return length;
|
|
1334
|
+
}, [editor, onCharacterCount, characterLimit]);
|
|
1335
|
+
const serializeContent = useCallback(
|
|
1336
|
+
() => serialize(editor.children),
|
|
1337
|
+
[editor]
|
|
1338
|
+
);
|
|
1339
|
+
const pluginEditorRef = useRef(null);
|
|
1340
|
+
const handleChange = useCallback(
|
|
1341
|
+
(value) => {
|
|
1342
|
+
const isAstChange = editor.operations.some(
|
|
1343
|
+
(op) => op.type !== "set_selection"
|
|
1344
|
+
);
|
|
1345
|
+
if (!isAstChange) return;
|
|
1346
|
+
updateCharacterCount();
|
|
1347
|
+
bumpStateVersion();
|
|
1348
|
+
const currentPluginEditor = pluginEditorRef.current;
|
|
1349
|
+
if (currentPluginEditor) {
|
|
1350
|
+
for (const plugin of plugins) {
|
|
1351
|
+
plugin.onEditorChange?.(currentPluginEditor);
|
|
1157
1352
|
}
|
|
1158
1353
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
try {
|
|
1173
|
-
const domSelection = window.getSelection();
|
|
1174
|
-
if (!domSelection || domSelection.rangeCount === 0)
|
|
1175
|
-
return { top: 0, left: 0 };
|
|
1176
|
-
const range = domSelection.getRangeAt(0);
|
|
1177
|
-
let rect = range.getBoundingClientRect();
|
|
1178
|
-
if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
|
|
1179
|
-
const node = domSelection.anchorNode instanceof HTMLElement ? domSelection.anchorNode : domSelection.anchorNode.parentElement;
|
|
1180
|
-
if (node) rect = node.getBoundingClientRect();
|
|
1181
|
-
}
|
|
1182
|
-
const wrapperEl = wrapperRef.current;
|
|
1183
|
-
if (!wrapperEl) return { top: 0, left: 0 };
|
|
1184
|
-
const wrapperRect = wrapperEl.getBoundingClientRect();
|
|
1354
|
+
const nextContent = serialize(value);
|
|
1355
|
+
if (nextContent !== lastContent.current) {
|
|
1356
|
+
lastContent.current = nextContent;
|
|
1357
|
+
isInternalChange.current = true;
|
|
1358
|
+
onChange?.(nextContent);
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
[bumpStateVersion, editor, onChange, plugins, updateCharacterCount]
|
|
1362
|
+
);
|
|
1363
|
+
const overLimit = characterLimit !== void 0 && characterCount > characterLimit;
|
|
1364
|
+
const hasCharacterLimit = characterLimit !== void 0;
|
|
1365
|
+
const getEditorState = useCallback(() => {
|
|
1366
|
+
const content2 = serializeContent();
|
|
1185
1367
|
return {
|
|
1186
|
-
|
|
1187
|
-
|
|
1368
|
+
content: content2,
|
|
1369
|
+
isEmpty: content2.trim().length === 0,
|
|
1370
|
+
isFocused,
|
|
1371
|
+
isEditable: editable,
|
|
1372
|
+
characterCount,
|
|
1373
|
+
characterLimit,
|
|
1374
|
+
overLimit
|
|
1188
1375
|
};
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
editor,
|
|
1202
|
-
selectedText.slice(before.length, -after.length || void 0)
|
|
1203
|
-
);
|
|
1376
|
+
}, [
|
|
1377
|
+
characterCount,
|
|
1378
|
+
characterLimit,
|
|
1379
|
+
editable,
|
|
1380
|
+
editor,
|
|
1381
|
+
isFocused,
|
|
1382
|
+
overLimit,
|
|
1383
|
+
serializeContent
|
|
1384
|
+
]);
|
|
1385
|
+
useEffect(() => {
|
|
1386
|
+
if (isInternalChange.current) {
|
|
1387
|
+
isInternalChange.current = false;
|
|
1204
1388
|
return;
|
|
1205
1389
|
}
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1390
|
+
if (content === lastContent.current) return;
|
|
1391
|
+
const newValue = deserialize(content, resolvedFeatures);
|
|
1392
|
+
replaceEditorChildren(editor, newValue);
|
|
1393
|
+
Transforms.select(editor, Editor.start(editor, []));
|
|
1394
|
+
lastContent.current = content;
|
|
1395
|
+
updateCharacterCount();
|
|
1396
|
+
bumpStateVersion();
|
|
1397
|
+
editor.onChange();
|
|
1398
|
+
}, [
|
|
1399
|
+
bumpStateVersion,
|
|
1400
|
+
content,
|
|
1401
|
+
editor,
|
|
1402
|
+
resolvedFeatures,
|
|
1403
|
+
updateCharacterCount
|
|
1404
|
+
]);
|
|
1405
|
+
useEffect(() => {
|
|
1406
|
+
onStateChange?.(getEditorState());
|
|
1407
|
+
}, [getEditorState, onStateChange, stateVersion]);
|
|
1408
|
+
const decorate = useCallback(
|
|
1409
|
+
(entry) => {
|
|
1410
|
+
const ranges = computeDecorations(entry, editor, rehypePlugins);
|
|
1411
|
+
return ranges;
|
|
1412
|
+
},
|
|
1413
|
+
[editor, rehypePlugins]
|
|
1414
|
+
);
|
|
1415
|
+
const [activePlugin, setActivePluginState] = useState(
|
|
1416
|
+
null
|
|
1417
|
+
);
|
|
1418
|
+
const [activePluginQuery, setActivePluginQuery] = useState("");
|
|
1419
|
+
const activePluginQueryRef = useRef("");
|
|
1420
|
+
const activePluginRef = useRef(null);
|
|
1421
|
+
activePluginRef.current = activePlugin;
|
|
1422
|
+
const pluginPositionRef = useRef({
|
|
1423
|
+
top: 0,
|
|
1424
|
+
left: 0
|
|
1425
|
+
});
|
|
1426
|
+
const forwardedKeyListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
1427
|
+
const wrapperRef = useRef(null);
|
|
1428
|
+
const editorElRef = useRef(null);
|
|
1429
|
+
const subscribeForwardedKeyFor = useCallback(
|
|
1430
|
+
(pluginName) => (listener) => {
|
|
1431
|
+
const map = forwardedKeyListenersRef.current;
|
|
1432
|
+
let listeners = map.get(pluginName);
|
|
1433
|
+
if (!listeners) {
|
|
1434
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1435
|
+
map.set(pluginName, listeners);
|
|
1436
|
+
}
|
|
1437
|
+
listeners.add(listener);
|
|
1438
|
+
return () => {
|
|
1439
|
+
listeners?.delete(listener);
|
|
1440
|
+
if (listeners && listeners.size === 0) map.delete(pluginName);
|
|
1441
|
+
};
|
|
1442
|
+
},
|
|
1443
|
+
[]
|
|
1444
|
+
);
|
|
1445
|
+
const emitForwardedKey = useCallback((pluginName, key) => {
|
|
1446
|
+
const listeners = forwardedKeyListenersRef.current.get(pluginName);
|
|
1447
|
+
if (!listeners || listeners.size === 0) return;
|
|
1448
|
+
for (const listener of listeners) listener(key);
|
|
1449
|
+
}, []);
|
|
1450
|
+
const canonicalizeEmptyEditor = useCallback(() => {
|
|
1451
|
+
if (Node.string(editor).trim().length !== 0) return;
|
|
1452
|
+
const onlyChild = editor.children[0];
|
|
1453
|
+
const isCanonicalEmptyParagraph = editor.children.length === 1 && onlyChild?.type === "paragraph" && Node.string(onlyChild).length === 0;
|
|
1454
|
+
if (isCanonicalEmptyParagraph) return;
|
|
1455
|
+
HistoryEditor.withoutSaving(editor, () => {
|
|
1456
|
+
Editor.withoutNormalizing(editor, () => {
|
|
1457
|
+
for (let index = editor.children.length - 1; index >= 0; index--) {
|
|
1458
|
+
Transforms.removeNodes(editor, { at: [index] });
|
|
1459
|
+
}
|
|
1460
|
+
Transforms.insertNodes(editor, {
|
|
1461
|
+
type: "paragraph",
|
|
1462
|
+
id: generateId(),
|
|
1463
|
+
children: [{ text: "" }]
|
|
1464
|
+
});
|
|
1217
1465
|
});
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1466
|
+
});
|
|
1467
|
+
}, [editor]);
|
|
1468
|
+
const selectEditor = useCallback(
|
|
1469
|
+
(at = "end") => {
|
|
1470
|
+
try {
|
|
1471
|
+
Transforms.select(
|
|
1472
|
+
editor,
|
|
1473
|
+
at === "start" ? Editor.start(editor, []) : Editor.end(editor, [])
|
|
1474
|
+
);
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
},
|
|
1478
|
+
[editor]
|
|
1479
|
+
);
|
|
1480
|
+
const focusEditor = useCallback(
|
|
1481
|
+
(options) => {
|
|
1482
|
+
ReactEditor.focus(editor);
|
|
1483
|
+
if (options?.at) selectEditor(options.at);
|
|
1484
|
+
},
|
|
1485
|
+
[editor, selectEditor]
|
|
1486
|
+
);
|
|
1487
|
+
const replaceContent = useCallback(
|
|
1488
|
+
(content2, options) => {
|
|
1489
|
+
const select = options?.select ?? "start";
|
|
1490
|
+
const newValue = deserialize(content2, resolvedFeatures);
|
|
1491
|
+
suppressImperativeOnChange.current = true;
|
|
1492
|
+
replaceEditorChildren(editor, newValue);
|
|
1493
|
+
updateCharacterCount();
|
|
1494
|
+
bumpStateVersion();
|
|
1495
|
+
if (select !== "preserve") selectEditor(select);
|
|
1496
|
+
const nextContent = serializeContent();
|
|
1497
|
+
lastContent.current = nextContent;
|
|
1498
|
+
editor.onChange();
|
|
1499
|
+
queueMicrotask(() => {
|
|
1500
|
+
suppressImperativeOnChange.current = false;
|
|
1221
1501
|
});
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1502
|
+
},
|
|
1503
|
+
[
|
|
1504
|
+
bumpStateVersion,
|
|
1505
|
+
editor,
|
|
1506
|
+
resolvedFeatures,
|
|
1507
|
+
selectEditor,
|
|
1508
|
+
serializeContent,
|
|
1509
|
+
updateCharacterCount
|
|
1510
|
+
]
|
|
1511
|
+
);
|
|
1512
|
+
useImperativeHandle(
|
|
1513
|
+
ref,
|
|
1514
|
+
() => ({
|
|
1515
|
+
getState: getEditorState,
|
|
1516
|
+
focus: focusEditor,
|
|
1517
|
+
clear: (options) => replaceContent("", options),
|
|
1518
|
+
setContent: replaceContent,
|
|
1519
|
+
insertContent: (content2) => {
|
|
1520
|
+
focusEditor();
|
|
1521
|
+
const nodes = deserialize(content2, resolvedFeatures);
|
|
1522
|
+
Transforms.insertFragment(editor, nodes);
|
|
1523
|
+
}
|
|
1524
|
+
}),
|
|
1525
|
+
[
|
|
1526
|
+
editor,
|
|
1527
|
+
focusEditor,
|
|
1528
|
+
getEditorState,
|
|
1529
|
+
replaceContent,
|
|
1530
|
+
resolvedFeatures,
|
|
1531
|
+
serializeContent
|
|
1532
|
+
]
|
|
1533
|
+
);
|
|
1534
|
+
const getCursorPosition = useCallback(() => {
|
|
1535
|
+
try {
|
|
1536
|
+
const domSelection = window.getSelection();
|
|
1537
|
+
if (!domSelection || domSelection.rangeCount === 0)
|
|
1538
|
+
return { top: 0, left: 0 };
|
|
1539
|
+
const range = domSelection.getRangeAt(0);
|
|
1540
|
+
let rect = range.getBoundingClientRect();
|
|
1541
|
+
if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
|
|
1542
|
+
const node = domSelection.anchorNode instanceof HTMLElement ? domSelection.anchorNode : domSelection.anchorNode.parentElement;
|
|
1543
|
+
if (node) rect = node.getBoundingClientRect();
|
|
1544
|
+
}
|
|
1545
|
+
const wrapperEl = wrapperRef.current;
|
|
1546
|
+
if (!wrapperEl) return { top: 0, left: 0 };
|
|
1547
|
+
const wrapperRect = wrapperEl.getBoundingClientRect();
|
|
1548
|
+
return {
|
|
1549
|
+
top: rect.bottom - wrapperRect.top + 4,
|
|
1550
|
+
left: rect.left - wrapperRect.left
|
|
1551
|
+
};
|
|
1552
|
+
} catch {
|
|
1553
|
+
return { top: 0, left: 0 };
|
|
1554
|
+
}
|
|
1555
|
+
}, []);
|
|
1556
|
+
const wrapSelection = useCallback(
|
|
1557
|
+
(before, after) => {
|
|
1558
|
+
const { selection } = editor;
|
|
1559
|
+
if (!selection) return;
|
|
1560
|
+
const selectedText = Editor.string(editor, selection);
|
|
1561
|
+
if (selectedText.startsWith(before) && selectedText.endsWith(after) && selectedText.length >= before.length + after.length) {
|
|
1225
1562
|
Transforms.delete(editor);
|
|
1226
|
-
Transforms.insertText(
|
|
1563
|
+
Transforms.insertText(
|
|
1564
|
+
editor,
|
|
1565
|
+
selectedText.slice(before.length, -after.length || void 0)
|
|
1566
|
+
);
|
|
1227
1567
|
return;
|
|
1228
1568
|
}
|
|
1569
|
+
const { anchor, focus } = selection;
|
|
1570
|
+
const [start, end] = Range.isForward(selection) ? [anchor, focus] : [focus, anchor];
|
|
1571
|
+
const beforeStart = {
|
|
1572
|
+
path: start.path,
|
|
1573
|
+
offset: Math.max(0, start.offset - before.length)
|
|
1574
|
+
};
|
|
1575
|
+
const afterEnd = { path: end.path, offset: end.offset + after.length };
|
|
1576
|
+
try {
|
|
1577
|
+
const textBefore = Editor.string(editor, {
|
|
1578
|
+
anchor: beforeStart,
|
|
1579
|
+
focus: start
|
|
1580
|
+
});
|
|
1581
|
+
const textAfter = Editor.string(editor, {
|
|
1582
|
+
anchor: end,
|
|
1583
|
+
focus: afterEnd
|
|
1584
|
+
});
|
|
1585
|
+
if (textBefore === before && textAfter === after) {
|
|
1586
|
+
const expandedRange = { anchor: beforeStart, focus: afterEnd };
|
|
1587
|
+
Transforms.select(editor, expandedRange);
|
|
1588
|
+
Transforms.delete(editor);
|
|
1589
|
+
Transforms.insertText(editor, selectedText);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
} catch {
|
|
1593
|
+
}
|
|
1594
|
+
Transforms.delete(editor);
|
|
1595
|
+
Transforms.insertText(editor, `${before}${selectedText}${after}`);
|
|
1596
|
+
},
|
|
1597
|
+
[editor]
|
|
1598
|
+
);
|
|
1599
|
+
const insertTextAtCursor = useCallback(
|
|
1600
|
+
(text) => {
|
|
1601
|
+
ReactEditor.focus(editor);
|
|
1602
|
+
const nodes = deserialize(text, resolvedFeatures);
|
|
1603
|
+
Transforms.insertFragment(editor, nodes);
|
|
1604
|
+
},
|
|
1605
|
+
[editor, resolvedFeatures]
|
|
1606
|
+
);
|
|
1607
|
+
const getCurrentBlockPath = useCallback(() => {
|
|
1608
|
+
const { selection } = editor;
|
|
1609
|
+
if (!selection || !Range.isCollapsed(selection)) return null;
|
|
1610
|
+
return selection.anchor.path.slice(0, 1);
|
|
1611
|
+
}, [editor]);
|
|
1612
|
+
const getRangeContent = useCallback(
|
|
1613
|
+
(range) => {
|
|
1614
|
+
try {
|
|
1615
|
+
return Editor.string(editor, range);
|
|
1616
|
+
} catch {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
},
|
|
1620
|
+
[editor]
|
|
1621
|
+
);
|
|
1622
|
+
const getCurrentBlockContent = useCallback(() => {
|
|
1623
|
+
const path = getCurrentBlockPath();
|
|
1624
|
+
if (!path) return null;
|
|
1625
|
+
try {
|
|
1626
|
+
return Editor.string(editor, path);
|
|
1229
1627
|
} catch {
|
|
1628
|
+
return null;
|
|
1230
1629
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1630
|
+
}, [editor, getCurrentBlockPath]);
|
|
1631
|
+
const getCurrentBlockContentBeforeCursor = useCallback(() => {
|
|
1632
|
+
const { selection } = editor;
|
|
1633
|
+
if (!selection || !Range.isCollapsed(selection)) return null;
|
|
1634
|
+
const anchor = selection.anchor;
|
|
1635
|
+
return getRangeContent({
|
|
1636
|
+
anchor: { path: anchor.path, offset: 0 },
|
|
1637
|
+
focus: anchor
|
|
1638
|
+
});
|
|
1639
|
+
}, [editor, getRangeContent]);
|
|
1640
|
+
const replaceCurrentBlockContent = useCallback(
|
|
1641
|
+
(nextContent) => {
|
|
1642
|
+
const path = getCurrentBlockPath();
|
|
1643
|
+
if (!path) return;
|
|
1644
|
+
const start = Editor.start(editor, path);
|
|
1645
|
+
const end = Editor.end(editor, path);
|
|
1646
|
+
Transforms.select(editor, { anchor: start, focus: end });
|
|
1647
|
+
Transforms.insertText(editor, nextContent);
|
|
1648
|
+
Transforms.select(editor, Editor.end(editor, path));
|
|
1649
|
+
},
|
|
1650
|
+
[editor, getCurrentBlockPath]
|
|
1651
|
+
);
|
|
1652
|
+
const clearCurrentBlock = useCallback(() => {
|
|
1653
|
+
const path = getCurrentBlockPath();
|
|
1654
|
+
if (!path) return;
|
|
1655
|
+
try {
|
|
1656
|
+
const start = Editor.start(editor, path);
|
|
1657
|
+
const end = Editor.end(editor, path);
|
|
1658
|
+
Transforms.select(editor, { anchor: start, focus: end });
|
|
1659
|
+
Transforms.delete(editor);
|
|
1660
|
+
editor.onChange();
|
|
1661
|
+
} catch {
|
|
1662
|
+
}
|
|
1663
|
+
}, [editor, getCurrentBlockPath]);
|
|
1664
|
+
const insertImage = useCallback(
|
|
1665
|
+
(image) => {
|
|
1666
|
+
const id = image.id ?? generateId();
|
|
1667
|
+
Transforms.insertNodes(editor, {
|
|
1668
|
+
type: "image",
|
|
1669
|
+
id,
|
|
1670
|
+
url: image.url,
|
|
1671
|
+
alt: image.alt,
|
|
1672
|
+
children: [{ text: "" }]
|
|
1673
|
+
});
|
|
1674
|
+
return id;
|
|
1675
|
+
},
|
|
1676
|
+
[editor]
|
|
1677
|
+
);
|
|
1678
|
+
const updateImage = useCallback(
|
|
1679
|
+
(id, image) => {
|
|
1680
|
+
for (const [node, path] of Node.nodes(editor)) {
|
|
1681
|
+
if (Editor.isEditor(node) || !("type" in node) || node.type !== "image" || node.id !== id) {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
Transforms.setNodes(editor, image, { at: path });
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
[editor]
|
|
1689
|
+
);
|
|
1690
|
+
const removeImage = useCallback(
|
|
1691
|
+
(id) => {
|
|
1692
|
+
for (const [node, path] of Node.nodes(editor)) {
|
|
1693
|
+
if (Editor.isEditor(node) || !("type" in node) || node.type !== "image" || node.id !== id) {
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
Transforms.removeNodes(editor, { at: path });
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
},
|
|
1700
|
+
[editor]
|
|
1701
|
+
);
|
|
1702
|
+
const pluginEditorImplRef = useRef(null);
|
|
1703
|
+
const getPluginEditorImpl = useCallback(() => {
|
|
1704
|
+
const impl = pluginEditorImplRef.current;
|
|
1705
|
+
if (!impl) throw new Error("Inkwell plugin editor is not ready");
|
|
1706
|
+
return impl;
|
|
1707
|
+
}, []);
|
|
1708
|
+
const pluginEditor = useMemo(
|
|
1709
|
+
() => ({
|
|
1710
|
+
getState: () => getPluginEditorImpl().getState(),
|
|
1711
|
+
isEmpty: () => getPluginEditorImpl().isEmpty(),
|
|
1712
|
+
focus: (options) => getPluginEditorImpl().focus(options),
|
|
1713
|
+
clear: (options) => getPluginEditorImpl().clear(options),
|
|
1714
|
+
setContent: (content2, options) => getPluginEditorImpl().setContent(content2, options),
|
|
1715
|
+
insertContent: (content2) => getPluginEditorImpl().insertContent(content2),
|
|
1716
|
+
getContentBeforeCursor: () => getPluginEditorImpl().getContentBeforeCursor(),
|
|
1717
|
+
getCurrentBlockContent: () => getPluginEditorImpl().getCurrentBlockContent(),
|
|
1718
|
+
getCurrentBlockContentBeforeCursor: () => getPluginEditorImpl().getCurrentBlockContentBeforeCursor(),
|
|
1719
|
+
replaceCurrentBlockContent: (content2) => getPluginEditorImpl().replaceCurrentBlockContent(content2),
|
|
1720
|
+
clearCurrentBlock: () => getPluginEditorImpl().clearCurrentBlock(),
|
|
1721
|
+
wrapSelection: (before, after) => getPluginEditorImpl().wrapSelection(before, after),
|
|
1722
|
+
insertImage: (image) => getPluginEditorImpl().insertImage(image),
|
|
1723
|
+
updateImage: (id, image) => getPluginEditorImpl().updateImage(id, image),
|
|
1724
|
+
removeImage: (id) => getPluginEditorImpl().removeImage(id)
|
|
1725
|
+
}),
|
|
1726
|
+
[getPluginEditorImpl]
|
|
1727
|
+
);
|
|
1728
|
+
pluginEditorImplRef.current = {
|
|
1729
|
+
getState: getEditorState,
|
|
1730
|
+
isEmpty: () => serializeContent().trim().length === 0,
|
|
1731
|
+
focus: focusEditor,
|
|
1732
|
+
clear: (options) => replaceContent("", options),
|
|
1733
|
+
setContent: replaceContent,
|
|
1734
|
+
insertContent: insertTextAtCursor,
|
|
1735
|
+
getContentBeforeCursor: () => {
|
|
1736
|
+
const { selection } = editor;
|
|
1737
|
+
if (!selection || !Range.isCollapsed(selection)) return null;
|
|
1738
|
+
return getRangeContent({
|
|
1739
|
+
anchor: Editor.start(editor, []),
|
|
1740
|
+
focus: selection.anchor
|
|
1741
|
+
});
|
|
1742
|
+
},
|
|
1743
|
+
getCurrentBlockContent,
|
|
1744
|
+
getCurrentBlockContentBeforeCursor,
|
|
1745
|
+
replaceCurrentBlockContent,
|
|
1746
|
+
clearCurrentBlock,
|
|
1747
|
+
wrapSelection,
|
|
1748
|
+
insertImage,
|
|
1749
|
+
updateImage,
|
|
1750
|
+
removeImage
|
|
1751
|
+
};
|
|
1752
|
+
useEffect(() => {
|
|
1753
|
+
const { insertData } = editor;
|
|
1754
|
+
editor.insertData = (data) => {
|
|
1755
|
+
const baseContext = {
|
|
1756
|
+
editor: pluginEditor,
|
|
1757
|
+
insertData
|
|
1758
|
+
};
|
|
1759
|
+
for (const plugin of plugins) {
|
|
1760
|
+
if (plugin.onInsertData?.(data, baseContext)) return;
|
|
1761
|
+
}
|
|
1762
|
+
insertData(data);
|
|
1763
|
+
};
|
|
1764
|
+
const cleanups = [];
|
|
1765
|
+
for (const plugin of plugins) {
|
|
1766
|
+
if (!plugin.setup) continue;
|
|
1767
|
+
const cleanup = plugin.setup(pluginEditor);
|
|
1768
|
+
if (typeof cleanup === "function") cleanups.push(cleanup);
|
|
1769
|
+
}
|
|
1770
|
+
return () => {
|
|
1771
|
+
editor.insertData = insertData;
|
|
1772
|
+
for (let i = cleanups.length - 1; i >= 0; i--) cleanups[i]();
|
|
1773
|
+
};
|
|
1774
|
+
}, [editor, plugins, pluginEditor, wrapSelection]);
|
|
1775
|
+
pluginEditorRef.current = pluginEditor;
|
|
1776
|
+
const dismissPlugin = useCallback(() => {
|
|
1777
|
+
activePluginRef.current = null;
|
|
1778
|
+
setActivePluginState(null);
|
|
1779
|
+
activePluginQueryRef.current = "";
|
|
1780
|
+
setActivePluginQuery("");
|
|
1238
1781
|
ReactEditor.focus(editor);
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1782
|
+
}, [editor]);
|
|
1783
|
+
const activatePlugin = useCallback(
|
|
1784
|
+
(plugin, options) => {
|
|
1785
|
+
const initialQuery = options?.query ?? "";
|
|
1786
|
+
activePluginQueryRef.current = initialQuery;
|
|
1787
|
+
setActivePluginQuery(initialQuery);
|
|
1788
|
+
pluginPositionRef.current = getCursorPosition();
|
|
1789
|
+
activePluginRef.current = plugin;
|
|
1790
|
+
setActivePluginState(plugin);
|
|
1791
|
+
},
|
|
1792
|
+
[getCursorPosition]
|
|
1793
|
+
);
|
|
1794
|
+
const handlePluginSelect = useCallback(
|
|
1795
|
+
(text) => {
|
|
1796
|
+
const activation = activePlugin?.activation;
|
|
1797
|
+
const triggerKey = activation?.type === "trigger" ? activation.key : void 0;
|
|
1798
|
+
const isCharTrigger = triggerKey && !triggerKey.includes("+");
|
|
1799
|
+
const queryLength = activePluginQueryRef.current.length;
|
|
1800
|
+
dismissPlugin();
|
|
1801
|
+
requestAnimationFrame(() => {
|
|
1802
|
+
ReactEditor.focus(editor);
|
|
1803
|
+
if (isCharTrigger) {
|
|
1804
|
+
Transforms.delete(editor, {
|
|
1805
|
+
distance: 1 + queryLength,
|
|
1806
|
+
unit: "character",
|
|
1807
|
+
reverse: true
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
insertTextAtCursor(text);
|
|
1811
|
+
});
|
|
1812
|
+
},
|
|
1813
|
+
[dismissPlugin, insertTextAtCursor, activePlugin, editor]
|
|
1814
|
+
);
|
|
1815
|
+
const isActivatablePlugin = (plugin) => (plugin.activation?.type ?? "always") !== "always";
|
|
1816
|
+
const makePluginProps = (plugin) => ({
|
|
1817
|
+
active: isActivatablePlugin(plugin) ? activePlugin === plugin : true,
|
|
1818
|
+
query: activePlugin === plugin ? activePluginQuery : "",
|
|
1819
|
+
onSelect: handlePluginSelect,
|
|
1820
|
+
onDismiss: dismissPlugin,
|
|
1821
|
+
position: pluginPositionRef.current,
|
|
1822
|
+
editorRef: editorElRef,
|
|
1823
|
+
editor: pluginEditor,
|
|
1824
|
+
wrapSelection,
|
|
1825
|
+
subscribeForwardedKey: subscribeForwardedKeyFor(plugin.name)
|
|
1826
|
+
});
|
|
1827
|
+
const makeKeyDownContext = useCallback(
|
|
1828
|
+
(plugin) => ({
|
|
1829
|
+
editor: pluginEditor,
|
|
1830
|
+
wrapSelection,
|
|
1831
|
+
activate: (options) => activatePlugin(plugin, options),
|
|
1832
|
+
dismiss: dismissPlugin
|
|
1833
|
+
}),
|
|
1834
|
+
[activatePlugin, dismissPlugin, pluginEditor, wrapSelection]
|
|
1835
|
+
);
|
|
1836
|
+
const handleKeyDown = useCallback(
|
|
1837
|
+
(event) => {
|
|
1838
|
+
if (activePlugin) {
|
|
1839
|
+
const activeResult = activePlugin.onActiveKeyDown?.(
|
|
1840
|
+
event,
|
|
1841
|
+
makeKeyDownContext(activePlugin)
|
|
1842
|
+
);
|
|
1843
|
+
if (activeResult === false) {
|
|
1844
|
+
dismissPlugin();
|
|
1845
|
+
} else {
|
|
1846
|
+
if (event.defaultPrevented) return;
|
|
1847
|
+
if (event.key === "Escape") {
|
|
1848
|
+
event.preventDefault();
|
|
1849
|
+
dismissPlugin();
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
const isPrintable = !event.metaKey && !event.ctrlKey && !event.altKey && event.key.length === 1;
|
|
1853
|
+
const shouldForward = event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === "Backspace" || isPrintable;
|
|
1854
|
+
if (shouldForward) {
|
|
1855
|
+
if (event.key === "Enter") {
|
|
1856
|
+
event.preventDefault();
|
|
1857
|
+
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1858
|
+
event.preventDefault();
|
|
1859
|
+
} else if (event.key === "Backspace") {
|
|
1860
|
+
if (activePluginQueryRef.current.length === 0) {
|
|
1861
|
+
dismissPlugin();
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const nextQuery = activePluginQueryRef.current.slice(0, -1);
|
|
1865
|
+
activePluginQueryRef.current = nextQuery;
|
|
1866
|
+
setActivePluginQuery(nextQuery);
|
|
1867
|
+
} else if (isPrintable) {
|
|
1868
|
+
const nextQuery = `${activePluginQueryRef.current}${event.key}`;
|
|
1869
|
+
activePluginQueryRef.current = nextQuery;
|
|
1870
|
+
setActivePluginQuery(nextQuery);
|
|
1871
|
+
}
|
|
1872
|
+
emitForwardedKey(activePlugin.name, event.key);
|
|
1873
|
+
}
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1261
1876
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1877
|
+
for (const plugin of plugins) {
|
|
1878
|
+
plugin.onKeyDown?.(event, makeKeyDownContext(plugin));
|
|
1879
|
+
if (event.defaultPrevented) return;
|
|
1880
|
+
if (activePluginRef.current) return;
|
|
1881
|
+
}
|
|
1882
|
+
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
|
|
1883
|
+
event.preventDefault();
|
|
1884
|
+
onSubmit?.(serializeContent());
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
for (const plugin of plugins) {
|
|
1888
|
+
const activation = plugin.activation;
|
|
1889
|
+
if (activation?.type !== "trigger") continue;
|
|
1890
|
+
const parts = activation.key.toLowerCase().split("+").map((s) => s.trim());
|
|
1891
|
+
const key = parts[parts.length - 1];
|
|
1892
|
+
const mods = new Set(parts.slice(0, -1));
|
|
1893
|
+
const hasModifiers = mods.size > 0;
|
|
1894
|
+
const needCtrl = mods.has("control") || mods.has("ctrl");
|
|
1895
|
+
const needMeta = mods.has("meta") || mods.has("cmd") || mods.has("command");
|
|
1896
|
+
const needAlt = mods.has("alt");
|
|
1897
|
+
const needShift = mods.has("shift");
|
|
1898
|
+
const keyMatch = event.key.toLowerCase() === key;
|
|
1899
|
+
const modMatch = hasModifiers ? event.ctrlKey === needCtrl && event.metaKey === needMeta && event.altKey === needAlt && event.shiftKey === needShift : !event.ctrlKey && !event.metaKey;
|
|
1900
|
+
if (keyMatch && modMatch) {
|
|
1901
|
+
if (plugin.shouldTrigger && !plugin.shouldTrigger(event, makeKeyDownContext(plugin))) {
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
if (hasModifiers) event.preventDefault();
|
|
1905
|
+
activatePlugin(plugin);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (event.key === "Tab" && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
1910
|
+
const { selection } = editor;
|
|
1911
|
+
if (selection) {
|
|
1912
|
+
const [match] = Editor.nodes(editor, {
|
|
1913
|
+
match: (n) => Element.isElement(n)
|
|
1914
|
+
});
|
|
1915
|
+
if (match) {
|
|
1916
|
+
const [node, path] = match;
|
|
1917
|
+
const element = node;
|
|
1918
|
+
if (element.type === "paragraph") {
|
|
1919
|
+
const text = Node.string(node);
|
|
1920
|
+
if (ORDERED_LIST_MARKER_RE.test(text)) {
|
|
1921
|
+
event.preventDefault();
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (UNORDERED_LIST_MARKER_RE.test(text)) {
|
|
1925
|
+
event.preventDefault();
|
|
1926
|
+
const savedSelection = editor.selection;
|
|
1927
|
+
Transforms.insertText(editor, " ", {
|
|
1928
|
+
at: { path: [...path, 0], offset: 0 }
|
|
1929
|
+
});
|
|
1930
|
+
if (savedSelection) {
|
|
1931
|
+
Transforms.select(editor, {
|
|
1932
|
+
anchor: {
|
|
1933
|
+
path: savedSelection.anchor.path,
|
|
1934
|
+
offset: savedSelection.anchor.offset + 2
|
|
1935
|
+
},
|
|
1936
|
+
focus: {
|
|
1937
|
+
path: savedSelection.focus.path,
|
|
1938
|
+
offset: savedSelection.focus.offset + 2
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (event.key === "a" && (event.metaKey || event.ctrlKey) && !Node.string(editor).trim()) {
|
|
1949
|
+
event.preventDefault();
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
[
|
|
1954
|
+
plugins,
|
|
1955
|
+
activePlugin,
|
|
1956
|
+
editor,
|
|
1957
|
+
getCursorPosition,
|
|
1958
|
+
dismissPlugin,
|
|
1959
|
+
makeKeyDownContext,
|
|
1960
|
+
activatePlugin,
|
|
1961
|
+
emitForwardedKey,
|
|
1962
|
+
submitOnEnter,
|
|
1963
|
+
onSubmit,
|
|
1964
|
+
serializeContent
|
|
1965
|
+
]
|
|
1966
|
+
);
|
|
1967
|
+
const pluginPlaceholder = plugins.reduce(
|
|
1968
|
+
(value, plugin) => {
|
|
1969
|
+
if (value) return value;
|
|
1970
|
+
const nextPlaceholder = plugin.getPlaceholder?.(pluginEditor) ?? null;
|
|
1971
|
+
if (!nextPlaceholder) return null;
|
|
1972
|
+
return typeof nextPlaceholder === "string" ? { text: nextPlaceholder } : nextPlaceholder;
|
|
1973
|
+
},
|
|
1974
|
+
null
|
|
1975
|
+
);
|
|
1976
|
+
const basePlaceholder = pluginPlaceholder?.text ?? placeholder ?? "Start writing...";
|
|
1977
|
+
const resolvedPlaceholder = pluginPlaceholder?.hint ? `${pluginPlaceholder.hint} ${basePlaceholder}` : basePlaceholder;
|
|
1978
|
+
useLayoutEffect(() => {
|
|
1979
|
+
if (!pluginPlaceholder) return;
|
|
1980
|
+
if (Node.string(editor).trim().length !== 0) return;
|
|
1981
|
+
canonicalizeEmptyEditor();
|
|
1982
|
+
selectEditor("start");
|
|
1983
|
+
}, [
|
|
1984
|
+
canonicalizeEmptyEditor,
|
|
1985
|
+
editor,
|
|
1986
|
+
pluginPlaceholder,
|
|
1987
|
+
selectEditor,
|
|
1988
|
+
stateVersion
|
|
1989
|
+
]);
|
|
1990
|
+
return /* @__PURE__ */ jsxs(
|
|
1991
|
+
"div",
|
|
1992
|
+
{
|
|
1993
|
+
ref: wrapperRef,
|
|
1994
|
+
className: `inkwell-editor-wrapper${hasCharacterLimit ? " inkwell-editor-has-character-limit" : ""}${overLimit ? " inkwell-editor-over-limit" : ""}${className ? ` ${className}` : ""}${classNames?.root ? ` ${classNames.root}` : ""}`,
|
|
1995
|
+
style: styles?.root,
|
|
1996
|
+
children: [
|
|
1997
|
+
hasCharacterLimit && /* @__PURE__ */ jsx(
|
|
1998
|
+
CharacterCount,
|
|
1999
|
+
{
|
|
2000
|
+
count: characterCount,
|
|
2001
|
+
limit: characterLimit,
|
|
2002
|
+
over: overLimit
|
|
2003
|
+
}
|
|
2004
|
+
),
|
|
2005
|
+
activePlugin && /* @__PURE__ */ jsx(
|
|
2006
|
+
"div",
|
|
2007
|
+
{
|
|
2008
|
+
className: "inkwell-plugin-backdrop",
|
|
2009
|
+
style: {
|
|
2010
|
+
position: "fixed",
|
|
2011
|
+
inset: 0,
|
|
2012
|
+
zIndex: 999,
|
|
2013
|
+
background: "transparent"
|
|
2014
|
+
},
|
|
2015
|
+
onMouseDown: dismissPlugin
|
|
2016
|
+
}
|
|
2017
|
+
),
|
|
2018
|
+
plugins.map((plugin) => {
|
|
2019
|
+
const props = makePluginProps(plugin);
|
|
2020
|
+
if (!props.active || !plugin.render) return null;
|
|
2021
|
+
return /* @__PURE__ */ jsx(Fragment, { children: plugin.render(props) }, plugin.name);
|
|
2022
|
+
}),
|
|
2023
|
+
/* @__PURE__ */ jsx(
|
|
2024
|
+
Slate,
|
|
2025
|
+
{
|
|
2026
|
+
editor,
|
|
2027
|
+
initialValue,
|
|
2028
|
+
onChange: handleChange,
|
|
2029
|
+
children: /* @__PURE__ */ jsx(
|
|
2030
|
+
Editable,
|
|
2031
|
+
{
|
|
2032
|
+
ref: editorElRef,
|
|
2033
|
+
className: `inkwell-editor${classNames?.editor ? ` ${classNames.editor}` : ""}`,
|
|
2034
|
+
style: styles?.editor,
|
|
2035
|
+
renderElement: RenderElement,
|
|
2036
|
+
renderLeaf: RenderLeaf,
|
|
2037
|
+
decorate,
|
|
2038
|
+
placeholder: resolvedPlaceholder,
|
|
2039
|
+
spellCheck: true,
|
|
2040
|
+
role: "textbox",
|
|
2041
|
+
"aria-multiline": true,
|
|
2042
|
+
"aria-placeholder": resolvedPlaceholder,
|
|
2043
|
+
"data-placeholder": resolvedPlaceholder,
|
|
2044
|
+
readOnly: !editable,
|
|
2045
|
+
onFocus: () => scheduleFocusedState(true),
|
|
2046
|
+
onBlur: () => scheduleFocusedState(false),
|
|
2047
|
+
onKeyDown: handleKeyDown
|
|
2048
|
+
}
|
|
2049
|
+
)
|
|
2050
|
+
}
|
|
2051
|
+
)
|
|
2052
|
+
]
|
|
2053
|
+
}
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
);
|
|
2057
|
+
|
|
2058
|
+
// src/plugins/attachments/index.tsx
|
|
2059
|
+
function mimeMatches(mime, accept) {
|
|
2060
|
+
if (!mime) return false;
|
|
2061
|
+
const patterns = accept.split(",").map((p) => p.trim()).filter(Boolean);
|
|
2062
|
+
return patterns.some((pattern) => {
|
|
2063
|
+
if (pattern.endsWith("/*")) {
|
|
2064
|
+
const prefix = pattern.slice(0, -1);
|
|
2065
|
+
return mime.startsWith(prefix);
|
|
2066
|
+
}
|
|
2067
|
+
return mime === pattern;
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
function isImageFile(file) {
|
|
2071
|
+
return file.type.startsWith("image/");
|
|
2072
|
+
}
|
|
2073
|
+
function extractFiles(data) {
|
|
2074
|
+
if (data.files && data.files.length > 0) return Array.from(data.files);
|
|
2075
|
+
if (!data.items) return [];
|
|
2076
|
+
const files = [];
|
|
2077
|
+
for (const item of Array.from(data.items)) {
|
|
2078
|
+
if (item.kind !== "file") continue;
|
|
2079
|
+
const file = item.getAsFile();
|
|
2080
|
+
if (file) files.push(file);
|
|
2081
|
+
}
|
|
2082
|
+
return files;
|
|
2083
|
+
}
|
|
2084
|
+
function filesOnlyDataTransfer(files) {
|
|
2085
|
+
return {
|
|
2086
|
+
types: files.length > 0 ? ["Files"] : [],
|
|
2087
|
+
files,
|
|
2088
|
+
items: void 0,
|
|
2089
|
+
getData: () => "",
|
|
2090
|
+
setData: () => {
|
|
1264
2091
|
},
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
2092
|
+
clearData: () => {
|
|
2093
|
+
},
|
|
2094
|
+
setDragImage: () => {
|
|
2095
|
+
},
|
|
2096
|
+
dropEffect: "none",
|
|
2097
|
+
effectAllowed: "all"
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function extractHtmlImages(data) {
|
|
2101
|
+
const html = data.getData("text/html");
|
|
2102
|
+
if (!html) return [];
|
|
2103
|
+
const template = document.createElement("template");
|
|
2104
|
+
template.innerHTML = html;
|
|
2105
|
+
const images = [];
|
|
2106
|
+
for (const img of Array.from(template.content.querySelectorAll("img"))) {
|
|
2107
|
+
const url = sanitizeImageUrl(img.getAttribute("src"));
|
|
2108
|
+
if (!url) continue;
|
|
2109
|
+
images.push({ url, alt: img.getAttribute("alt") ?? "" });
|
|
2110
|
+
}
|
|
2111
|
+
return images;
|
|
2112
|
+
}
|
|
2113
|
+
var insertUploadedImage = (editor, file, options) => {
|
|
2114
|
+
const placeholder = options.uploadingPlaceholder?.(file) ?? "Uploading\u2026";
|
|
2115
|
+
const id = editor.insertImage({ url: "", alt: placeholder });
|
|
2116
|
+
Promise.resolve().then(() => options.onUpload(file)).then((result) => {
|
|
2117
|
+
const url = typeof result === "string" ? result : result.url;
|
|
2118
|
+
const safeUrl = sanitizeImageUrl(url);
|
|
2119
|
+
if (!safeUrl) {
|
|
2120
|
+
editor.removeImage(id);
|
|
2121
|
+
options.onError?.(new Error("Unsafe upload URL"), file);
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
const alt = typeof result === "string" ? file.name : result.alt ?? file.name;
|
|
2125
|
+
editor.updateImage(id, { url: safeUrl, alt });
|
|
2126
|
+
}).catch((err) => {
|
|
2127
|
+
editor.removeImage(id);
|
|
2128
|
+
options.onError?.(err, file);
|
|
1275
2129
|
});
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
2130
|
+
};
|
|
2131
|
+
var insertUploadedAttachment = (file, options) => {
|
|
2132
|
+
const onAttachmentAdd = options.onAttachmentAdd;
|
|
2133
|
+
if (!onAttachmentAdd) return;
|
|
2134
|
+
Promise.resolve().then(() => options.onUpload(file)).then((result) => {
|
|
2135
|
+
const url = typeof result === "string" ? result : result.url;
|
|
2136
|
+
if (!isSafeImageUrl(url)) {
|
|
2137
|
+
options.onError?.(new Error("Unsafe upload URL"), file);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const extra = typeof result === "string" ? {} : Object.fromEntries(
|
|
2141
|
+
Object.entries(result).filter(
|
|
2142
|
+
([k]) => k !== "url" && k !== "alt"
|
|
2143
|
+
)
|
|
2144
|
+
);
|
|
2145
|
+
onAttachmentAdd({
|
|
2146
|
+
...extra,
|
|
2147
|
+
url: url.trim(),
|
|
2148
|
+
filename: file.name || "attachment",
|
|
2149
|
+
mime: file.type,
|
|
2150
|
+
size: file.size
|
|
2151
|
+
});
|
|
2152
|
+
}).catch((err) => {
|
|
2153
|
+
options.onError?.(err, file);
|
|
2154
|
+
});
|
|
2155
|
+
};
|
|
2156
|
+
function createAttachmentsPlugin(options) {
|
|
2157
|
+
const { accept } = options;
|
|
2158
|
+
return {
|
|
2159
|
+
name: "attachments",
|
|
2160
|
+
onInsertData(data, { editor, insertData }) {
|
|
2161
|
+
const files = extractFiles(data);
|
|
2162
|
+
const matching = accept ? files.filter((f) => mimeMatches(f.type, accept)) : files;
|
|
2163
|
+
const handled = matching.filter(
|
|
2164
|
+
(f) => isImageFile(f) || options.onAttachmentAdd !== void 0
|
|
2165
|
+
);
|
|
2166
|
+
if (handled.length === 0) {
|
|
2167
|
+
const htmlImages = extractHtmlImages(data);
|
|
2168
|
+
if (htmlImages.length === 0) return false;
|
|
2169
|
+
for (const image of htmlImages) {
|
|
2170
|
+
editor.insertImage(image);
|
|
1282
2171
|
}
|
|
1283
|
-
return;
|
|
2172
|
+
return true;
|
|
1284
2173
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
2174
|
+
const unhandled = files.filter((f) => !handled.includes(f));
|
|
2175
|
+
if (unhandled.length > 0) {
|
|
2176
|
+
insertData(filesOnlyDataTransfer(unhandled));
|
|
1288
2177
|
}
|
|
1289
|
-
for (const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const mods = new Set(parts.slice(0, -1));
|
|
1295
|
-
const hasModifiers = mods.size > 0;
|
|
1296
|
-
const needCtrl = mods.has("control") || mods.has("ctrl");
|
|
1297
|
-
const needMeta = mods.has("meta") || mods.has("cmd") || mods.has("command");
|
|
1298
|
-
const needAlt = mods.has("alt");
|
|
1299
|
-
const needShift = mods.has("shift");
|
|
1300
|
-
const keyMatch = event.key.toLowerCase() === key;
|
|
1301
|
-
const modMatch = hasModifiers ? event.ctrlKey === needCtrl && event.metaKey === needMeta && event.altKey === needAlt && event.shiftKey === needShift : !event.ctrlKey && !event.metaKey;
|
|
1302
|
-
if (keyMatch && modMatch) {
|
|
1303
|
-
if (hasModifiers) event.preventDefault();
|
|
1304
|
-
pluginPositionRef.current = getCursorPosition();
|
|
1305
|
-
setActivePlugin(plugin);
|
|
1306
|
-
return;
|
|
2178
|
+
for (const file of handled) {
|
|
2179
|
+
if (isImageFile(file)) {
|
|
2180
|
+
insertUploadedImage(editor, file, options);
|
|
2181
|
+
} else {
|
|
2182
|
+
insertUploadedAttachment(file, options);
|
|
1307
2183
|
}
|
|
1308
2184
|
}
|
|
1309
|
-
|
|
2185
|
+
return true;
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/plugins/completions/index.tsx
|
|
2191
|
+
var isPlainTypingKey = (event) => {
|
|
2192
|
+
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
2193
|
+
};
|
|
2194
|
+
var lastAcceptedCompletionByEditor = /* @__PURE__ */ new WeakMap();
|
|
2195
|
+
var setLastAcceptedCompletion = (editor, pluginName, completion) => {
|
|
2196
|
+
const completions = lastAcceptedCompletionByEditor.get(editor) ?? /* @__PURE__ */ new Map();
|
|
2197
|
+
completions.set(pluginName, completion);
|
|
2198
|
+
lastAcceptedCompletionByEditor.set(editor, completions);
|
|
2199
|
+
};
|
|
2200
|
+
var takeLastAcceptedCompletion = (editor, pluginName) => {
|
|
2201
|
+
const completions = lastAcceptedCompletionByEditor.get(editor);
|
|
2202
|
+
if (!completions) return null;
|
|
2203
|
+
const completion = completions.get(pluginName) ?? null;
|
|
2204
|
+
completions.delete(pluginName);
|
|
2205
|
+
if (completions.size === 0) {
|
|
2206
|
+
lastAcceptedCompletionByEditor.delete(editor);
|
|
2207
|
+
}
|
|
2208
|
+
return completion;
|
|
2209
|
+
};
|
|
2210
|
+
function createCompletionsPlugin({
|
|
2211
|
+
name = "completions",
|
|
2212
|
+
getCompletion,
|
|
2213
|
+
isLoading,
|
|
2214
|
+
loadingText = "Loading suggestion\u2026",
|
|
2215
|
+
acceptHint = "[tab \u21B9]",
|
|
2216
|
+
onAccept,
|
|
2217
|
+
onDismiss,
|
|
2218
|
+
onRestore,
|
|
2219
|
+
restoreOnUndo = true
|
|
2220
|
+
}) {
|
|
2221
|
+
return {
|
|
2222
|
+
name,
|
|
2223
|
+
getPlaceholder: (editor) => {
|
|
2224
|
+
if (!editor.isEmpty()) return null;
|
|
2225
|
+
const text = getCompletion() ?? (isLoading?.() ? loadingText : null);
|
|
2226
|
+
if (!text) return null;
|
|
2227
|
+
return { text, hint: acceptHint };
|
|
2228
|
+
},
|
|
2229
|
+
onKeyDown: (event, { editor }) => {
|
|
2230
|
+
const completion = getCompletion();
|
|
2231
|
+
if (!completion) return;
|
|
2232
|
+
if (!editor.isEmpty()) return;
|
|
2233
|
+
if (event.key === "Tab") {
|
|
1310
2234
|
event.preventDefault();
|
|
2235
|
+
setLastAcceptedCompletion(editor, name, completion);
|
|
2236
|
+
editor.insertContent(completion);
|
|
2237
|
+
onAccept?.(completion);
|
|
1311
2238
|
return;
|
|
1312
2239
|
}
|
|
2240
|
+
if (event.key === "Escape") {
|
|
2241
|
+
event.preventDefault();
|
|
2242
|
+
onDismiss?.(completion);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (isPlainTypingKey(event)) {
|
|
2246
|
+
onDismiss?.(completion);
|
|
2247
|
+
}
|
|
1313
2248
|
},
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
editor,
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
wrapSelection
|
|
1321
|
-
]
|
|
1322
|
-
);
|
|
1323
|
-
return /* @__PURE__ */ jsxs(
|
|
1324
|
-
"div",
|
|
1325
|
-
{
|
|
1326
|
-
ref: wrapperRef,
|
|
1327
|
-
className: `inkwell-editor-wrapper ${className ?? ""}`,
|
|
1328
|
-
children: [
|
|
1329
|
-
activePlugin && /* @__PURE__ */ jsx(
|
|
1330
|
-
"div",
|
|
1331
|
-
{
|
|
1332
|
-
className: "inkwell-plugin-backdrop",
|
|
1333
|
-
style: {
|
|
1334
|
-
position: "fixed",
|
|
1335
|
-
inset: 0,
|
|
1336
|
-
zIndex: 999,
|
|
1337
|
-
background: "transparent"
|
|
1338
|
-
},
|
|
1339
|
-
onMouseDown: dismissPlugin
|
|
1340
|
-
}
|
|
1341
|
-
),
|
|
1342
|
-
plugins.map((plugin) => {
|
|
1343
|
-
const props = makePluginProps(plugin);
|
|
1344
|
-
if (!props.active) return null;
|
|
1345
|
-
return /* @__PURE__ */ jsx(Fragment, { children: plugin.render(props) }, plugin.name);
|
|
1346
|
-
}),
|
|
1347
|
-
/* @__PURE__ */ jsx(
|
|
1348
|
-
Slate,
|
|
1349
|
-
{
|
|
1350
|
-
editor,
|
|
1351
|
-
initialValue,
|
|
1352
|
-
onChange: handleChange,
|
|
1353
|
-
children: /* @__PURE__ */ jsx(
|
|
1354
|
-
Editable,
|
|
1355
|
-
{
|
|
1356
|
-
ref: editorElRef,
|
|
1357
|
-
className: "inkwell-editor",
|
|
1358
|
-
renderElement: RenderElement,
|
|
1359
|
-
renderLeaf: RenderLeaf,
|
|
1360
|
-
decorate,
|
|
1361
|
-
placeholder: placeholder ?? "Start writing...",
|
|
1362
|
-
spellCheck: true,
|
|
1363
|
-
role: "textbox",
|
|
1364
|
-
"aria-multiline": true,
|
|
1365
|
-
"aria-placeholder": placeholder,
|
|
1366
|
-
"data-placeholder": placeholder ?? "Start writing...",
|
|
1367
|
-
onKeyDown: handleKeyDown
|
|
1368
|
-
}
|
|
1369
|
-
)
|
|
1370
|
-
}
|
|
1371
|
-
)
|
|
1372
|
-
]
|
|
2249
|
+
onEditorChange: (editor) => {
|
|
2250
|
+
if (!restoreOnUndo) return;
|
|
2251
|
+
if (!editor.isEmpty()) return;
|
|
2252
|
+
const lastAcceptedCompletion = takeLastAcceptedCompletion(editor, name);
|
|
2253
|
+
if (!lastAcceptedCompletion) return;
|
|
2254
|
+
onRestore?.(lastAcceptedCompletion);
|
|
1373
2255
|
}
|
|
1374
|
-
|
|
2256
|
+
};
|
|
1375
2257
|
}
|
|
1376
|
-
var
|
|
1377
|
-
|
|
1378
|
-
|
|
2258
|
+
var BASE = "inkwell-plugin-picker";
|
|
2259
|
+
var pluginPickerClass = {
|
|
2260
|
+
popup: `${BASE}-popup`,
|
|
2261
|
+
picker: `${BASE}`,
|
|
2262
|
+
search: `${BASE}-search`,
|
|
2263
|
+
item: `${BASE}-item`,
|
|
2264
|
+
itemActive: `${BASE}-item-active`,
|
|
2265
|
+
empty: `${BASE}-empty`,
|
|
2266
|
+
title: `${BASE}-title`,
|
|
2267
|
+
subtitle: `${BASE}-subtitle`,
|
|
2268
|
+
preview: `${BASE}-preview`
|
|
2269
|
+
};
|
|
2270
|
+
function PluginMenuPrimitive({
|
|
2271
|
+
items,
|
|
2272
|
+
search,
|
|
2273
|
+
getKey,
|
|
2274
|
+
renderItem,
|
|
2275
|
+
itemToText,
|
|
2276
|
+
placeholder,
|
|
2277
|
+
emptyMessage = "No results",
|
|
1379
2278
|
onSelect,
|
|
1380
|
-
onDismiss
|
|
2279
|
+
onDismiss,
|
|
2280
|
+
position,
|
|
2281
|
+
subscribeForwardedKey
|
|
1381
2282
|
}) {
|
|
1382
2283
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1383
|
-
const [
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
);
|
|
1387
|
-
const
|
|
1388
|
-
|
|
2284
|
+
const [query, setQuery] = useState("");
|
|
2285
|
+
const [asyncResults, setAsyncResults] = useState([]);
|
|
2286
|
+
const selectedIndexRef = useRef(0);
|
|
2287
|
+
const resultsRef = useRef(items ?? []);
|
|
2288
|
+
const updateSelectedIndex = useCallback((next) => {
|
|
2289
|
+
selectedIndexRef.current = next;
|
|
2290
|
+
setSelectedIndex(next);
|
|
1389
2291
|
}, []);
|
|
1390
|
-
const
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
2292
|
+
const syncResults = useMemo(() => {
|
|
2293
|
+
if (search) return null;
|
|
2294
|
+
const all = items ?? [];
|
|
2295
|
+
return all.filter(
|
|
2296
|
+
(item) => getKey(item).toLowerCase().includes(query.toLowerCase())
|
|
2297
|
+
);
|
|
2298
|
+
}, [getKey, items, query, search]);
|
|
2299
|
+
const results = syncResults ?? asyncResults;
|
|
2300
|
+
useEffect(() => {
|
|
2301
|
+
if (!syncResults) return;
|
|
2302
|
+
resultsRef.current = syncResults;
|
|
2303
|
+
if (selectedIndexRef.current >= syncResults.length) {
|
|
2304
|
+
updateSelectedIndex(0);
|
|
1393
2305
|
}
|
|
1394
|
-
}, []);
|
|
1395
|
-
|
|
1396
|
-
(
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2306
|
+
}, [syncResults, updateSelectedIndex]);
|
|
2307
|
+
useEffect(() => {
|
|
2308
|
+
if (!search) return;
|
|
2309
|
+
let cancelled = false;
|
|
2310
|
+
Promise.resolve(search(query)).then((next) => {
|
|
2311
|
+
if (cancelled) return;
|
|
2312
|
+
resultsRef.current = next;
|
|
2313
|
+
setAsyncResults(next);
|
|
2314
|
+
updateSelectedIndex(0);
|
|
2315
|
+
});
|
|
2316
|
+
return () => {
|
|
2317
|
+
cancelled = true;
|
|
2318
|
+
};
|
|
2319
|
+
}, [query, search, updateSelectedIndex]);
|
|
2320
|
+
const commitItem = useCallback(
|
|
2321
|
+
(item) => onSelect(itemToText(item)),
|
|
2322
|
+
[itemToText, onSelect]
|
|
2323
|
+
);
|
|
2324
|
+
const commitSelected = useCallback(() => {
|
|
2325
|
+
const item = resultsRef.current[selectedIndexRef.current];
|
|
2326
|
+
if (item) commitItem(item);
|
|
2327
|
+
}, [commitItem]);
|
|
2328
|
+
const handlePluginKey = useCallback(
|
|
2329
|
+
(key) => {
|
|
2330
|
+
switch (key) {
|
|
2331
|
+
case "Backspace":
|
|
2332
|
+
if (query.length === 0) {
|
|
2333
|
+
onSelect("");
|
|
2334
|
+
} else {
|
|
2335
|
+
setQuery((prev) => prev.slice(0, -1));
|
|
2336
|
+
}
|
|
1401
2337
|
break;
|
|
1402
|
-
case "
|
|
1403
|
-
|
|
1404
|
-
|
|
2338
|
+
case "ArrowDown": {
|
|
2339
|
+
const length = resultsRef.current.length;
|
|
2340
|
+
if (length === 0) break;
|
|
2341
|
+
updateSelectedIndex(
|
|
2342
|
+
selectedIndexRef.current < length - 1 ? selectedIndexRef.current + 1 : 0
|
|
2343
|
+
);
|
|
1405
2344
|
break;
|
|
2345
|
+
}
|
|
2346
|
+
case "ArrowUp": {
|
|
2347
|
+
const length = resultsRef.current.length;
|
|
2348
|
+
if (length === 0) break;
|
|
2349
|
+
updateSelectedIndex(
|
|
2350
|
+
selectedIndexRef.current > 0 ? selectedIndexRef.current - 1 : length - 1
|
|
2351
|
+
);
|
|
2352
|
+
break;
|
|
2353
|
+
}
|
|
1406
2354
|
case "Enter":
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}
|
|
2355
|
+
commitSelected();
|
|
2356
|
+
break;
|
|
2357
|
+
default:
|
|
2358
|
+
if (key.length === 1) setQuery((prev) => `${prev}${key}`);
|
|
2359
|
+
break;
|
|
2360
|
+
}
|
|
2361
|
+
},
|
|
2362
|
+
[commitSelected, onSelect, query.length, updateSelectedIndex]
|
|
2363
|
+
);
|
|
2364
|
+
useEffect(
|
|
2365
|
+
() => subscribeForwardedKey(handlePluginKey),
|
|
2366
|
+
[handlePluginKey, subscribeForwardedKey]
|
|
2367
|
+
);
|
|
2368
|
+
const handleKeyDown = useCallback(
|
|
2369
|
+
(event) => {
|
|
2370
|
+
event.stopPropagation();
|
|
2371
|
+
switch (event.key) {
|
|
2372
|
+
case "ArrowDown":
|
|
2373
|
+
case "ArrowUp":
|
|
2374
|
+
case "Enter":
|
|
2375
|
+
case "Backspace":
|
|
2376
|
+
event.preventDefault();
|
|
2377
|
+
handlePluginKey(event.key);
|
|
1411
2378
|
break;
|
|
1412
2379
|
case "Escape":
|
|
1413
|
-
|
|
2380
|
+
event.preventDefault();
|
|
1414
2381
|
onDismiss();
|
|
1415
2382
|
break;
|
|
1416
2383
|
}
|
|
1417
2384
|
},
|
|
1418
|
-
[
|
|
2385
|
+
[handlePluginKey, onDismiss]
|
|
1419
2386
|
);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
"
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
2387
|
+
const activeItemRef = useCallback((el) => {
|
|
2388
|
+
if (el && typeof el.scrollIntoView === "function") {
|
|
2389
|
+
el.scrollIntoView({ block: "nearest" });
|
|
2390
|
+
}
|
|
2391
|
+
}, []);
|
|
2392
|
+
const reactId = useId().replace(/:/g, "");
|
|
2393
|
+
const listboxId = `${pluginPickerClass.picker}-${reactId}-listbox`;
|
|
2394
|
+
const activeOptionId = `${listboxId}-option-${selectedIndex}`;
|
|
2395
|
+
const renderedResults = useMemo(
|
|
2396
|
+
() => results.map((item, index) => {
|
|
2397
|
+
const active = index === selectedIndex;
|
|
2398
|
+
return /* @__PURE__ */ jsx(
|
|
2399
|
+
"div",
|
|
2400
|
+
{
|
|
2401
|
+
ref: active ? activeItemRef : void 0,
|
|
2402
|
+
id: `${listboxId}-option-${index}`,
|
|
2403
|
+
role: "option",
|
|
2404
|
+
"aria-selected": active,
|
|
2405
|
+
className: `${pluginPickerClass.item} ${active ? pluginPickerClass.itemActive : ""}`,
|
|
2406
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
2407
|
+
onMouseEnter: () => updateSelectedIndex(index),
|
|
2408
|
+
onClick: () => commitItem(item),
|
|
2409
|
+
children: renderItem(item, active)
|
|
1431
2410
|
},
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
2411
|
+
getKey(item)
|
|
2412
|
+
);
|
|
2413
|
+
}),
|
|
2414
|
+
[
|
|
2415
|
+
activeItemRef,
|
|
2416
|
+
commitItem,
|
|
2417
|
+
getKey,
|
|
2418
|
+
listboxId,
|
|
2419
|
+
renderItem,
|
|
2420
|
+
results,
|
|
2421
|
+
selectedIndex,
|
|
2422
|
+
updateSelectedIndex
|
|
2423
|
+
]
|
|
2424
|
+
);
|
|
2425
|
+
return /* @__PURE__ */ jsx(
|
|
2426
|
+
"div",
|
|
2427
|
+
{
|
|
2428
|
+
className: pluginPickerClass.popup,
|
|
2429
|
+
style: {
|
|
2430
|
+
position: "absolute",
|
|
2431
|
+
top: position.top,
|
|
2432
|
+
left: position.left,
|
|
2433
|
+
zIndex: 1001
|
|
1448
2434
|
},
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
2435
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
2436
|
+
children: /* @__PURE__ */ jsxs(
|
|
2437
|
+
"div",
|
|
2438
|
+
{
|
|
2439
|
+
className: pluginPickerClass.picker,
|
|
2440
|
+
onKeyDown: handleKeyDown,
|
|
2441
|
+
role: "combobox",
|
|
2442
|
+
"aria-expanded": "true",
|
|
2443
|
+
"aria-haspopup": "listbox",
|
|
2444
|
+
"aria-controls": listboxId,
|
|
2445
|
+
"aria-activedescendant": results.length > 0 ? activeOptionId : void 0,
|
|
2446
|
+
children: [
|
|
2447
|
+
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, "aria-label": placeholder, children: query || placeholder }),
|
|
2448
|
+
results.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, role: "status", children: emptyMessage }) : /* @__PURE__ */ jsx("div", { id: listboxId, role: "listbox", children: renderedResults })
|
|
2449
|
+
]
|
|
2450
|
+
}
|
|
2451
|
+
)
|
|
2452
|
+
}
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
var defaultEmojis = [
|
|
2456
|
+
{ emoji: "\u{1F600}", name: "grinning", shortcodes: ["smile"], tags: ["happy"] },
|
|
2457
|
+
{ emoji: "\u{1F604}", name: "smile", shortcodes: ["smiley"], tags: ["happy"] },
|
|
2458
|
+
{ emoji: "\u{1F602}", name: "joy", shortcodes: ["laugh"], tags: ["tears"] },
|
|
2459
|
+
{ emoji: "\u{1F923}", name: "rofl", shortcodes: ["rolling_on_the_floor_laughing"] },
|
|
2460
|
+
{ emoji: "\u{1F60A}", name: "blush", shortcodes: ["smiling"] },
|
|
2461
|
+
{ emoji: "\u{1F642}", name: "slight_smile", shortcodes: ["slightly_smiling_face"] },
|
|
2462
|
+
{ emoji: "\u{1F609}", name: "wink" },
|
|
2463
|
+
{ emoji: "\u{1F60D}", name: "heart_eyes", tags: ["love"] },
|
|
2464
|
+
{ emoji: "\u{1F618}", name: "kissing_heart" },
|
|
2465
|
+
{ emoji: "\u{1F60E}", name: "sunglasses", shortcodes: ["cool"] },
|
|
2466
|
+
{ emoji: "\u{1F914}", name: "thinking", shortcodes: ["thinking_face"] },
|
|
2467
|
+
{ emoji: "\u{1F615}", name: "confused", shortcodes: ["slash"] },
|
|
2468
|
+
{ emoji: "\u{1F62D}", name: "sob", tags: ["cry"] },
|
|
2469
|
+
{ emoji: "\u{1F621}", name: "rage", shortcodes: ["angry"] },
|
|
2470
|
+
{ emoji: "\u{1F44D}", name: "thumbsup", shortcodes: ["+1", "like"] },
|
|
2471
|
+
{ emoji: "\u{1F44E}", name: "thumbsdown", shortcodes: ["-1", "dislike"] },
|
|
2472
|
+
{ emoji: "\u{1F44F}", name: "clap" },
|
|
2473
|
+
{ emoji: "\u{1F64C}", name: "raised_hands" },
|
|
2474
|
+
{ emoji: "\u{1F64F}", name: "pray", shortcodes: ["thanks"] },
|
|
2475
|
+
{ emoji: "\u{1F4AA}", name: "muscle", shortcodes: ["strong"] },
|
|
2476
|
+
{ emoji: "\u{1F440}", name: "eyes" },
|
|
2477
|
+
{ emoji: "\u{1F4AF}", name: "100" },
|
|
2478
|
+
{ emoji: "\u{1F525}", name: "fire" },
|
|
2479
|
+
{ emoji: "\u2728", name: "sparkles" },
|
|
2480
|
+
{ emoji: "\u{1F389}", name: "tada", shortcodes: ["party"] },
|
|
2481
|
+
{ emoji: "\u{1F680}", name: "rocket", tags: ["ship", "launch"] },
|
|
2482
|
+
{ emoji: "\u2705", name: "white_check_mark", shortcodes: ["check", "done"] },
|
|
2483
|
+
{ emoji: "\u274C", name: "x", shortcodes: ["cross"] },
|
|
2484
|
+
{ emoji: "\u26A0\uFE0F", name: "warning" },
|
|
2485
|
+
{ emoji: "\u{1F6A8}", name: "rotating_light", shortcodes: ["alert"] },
|
|
2486
|
+
{ emoji: "\u{1F41B}", name: "bug" },
|
|
2487
|
+
{ emoji: "\u{1F4A1}", name: "bulb", shortcodes: ["idea"] },
|
|
2488
|
+
{ emoji: "\u{1F9F5}", name: "yarn" },
|
|
2489
|
+
{ emoji: "\u{1F6E0}\uFE0F", name: "tools", shortcodes: ["wrench"] },
|
|
2490
|
+
{ emoji: "\u{1F4CC}", name: "pushpin", shortcodes: ["pin"] },
|
|
2491
|
+
{ emoji: "\u{1F4DD}", name: "memo", shortcodes: ["note"] },
|
|
2492
|
+
{ emoji: "\u{1F4CE}", name: "paperclip", shortcodes: ["attachment"] },
|
|
2493
|
+
{ emoji: "\u{1F512}", name: "lock" },
|
|
2494
|
+
{ emoji: "\u{1F513}", name: "unlock" },
|
|
2495
|
+
{ emoji: "\u{1F4AC}", name: "speech_balloon", shortcodes: ["comment"] },
|
|
2496
|
+
{ emoji: "\u2764\uFE0F", name: "heart", shortcodes: ["love"] },
|
|
2497
|
+
{ emoji: "\u{1F494}", name: "broken_heart" },
|
|
2498
|
+
{ emoji: "\u2601\uFE0F", name: "cloud" },
|
|
2499
|
+
{ emoji: "\u{1F682}", name: "train", tags: ["railway"] }
|
|
2500
|
+
];
|
|
2501
|
+
var itemText = (emoji) => [emoji.name, ...emoji.shortcodes ?? [], ...emoji.tags ?? []].join(" ");
|
|
2502
|
+
var defaultSearch = (emojis, query) => {
|
|
2503
|
+
const q = query.toLowerCase();
|
|
2504
|
+
const slashQuery = q === "/" ? "confused" : q;
|
|
2505
|
+
return emojis.filter(
|
|
2506
|
+
(emoji) => !slashQuery || itemText(emoji).toLowerCase().includes(slashQuery)
|
|
2507
|
+
).slice(0, 20);
|
|
2508
|
+
};
|
|
2509
|
+
var DefaultEmojiItem = ({ item }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2510
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: item.emoji }),
|
|
2511
|
+
/* @__PURE__ */ jsxs("span", { className: pluginPickerClass.title, children: [
|
|
2512
|
+
":",
|
|
2513
|
+
item.name,
|
|
2514
|
+
":"
|
|
2515
|
+
] }),
|
|
2516
|
+
item.shortcodes?.[0] ? /* @__PURE__ */ jsxs("span", { className: pluginPickerClass.subtitle, children: [
|
|
2517
|
+
":",
|
|
2518
|
+
item.shortcodes[0],
|
|
2519
|
+
":"
|
|
2520
|
+
] }) : null
|
|
2521
|
+
] });
|
|
2522
|
+
function createEmojiPlugin({
|
|
2523
|
+
name = "emoji",
|
|
2524
|
+
trigger = ":",
|
|
2525
|
+
emojis,
|
|
2526
|
+
search,
|
|
2527
|
+
renderItem,
|
|
2528
|
+
emptyMessage = "No emoji found"
|
|
2529
|
+
} = {}) {
|
|
2530
|
+
const resolvedEmojis = emojis ?? defaultEmojis;
|
|
2531
|
+
return {
|
|
2532
|
+
name,
|
|
2533
|
+
shouldTrigger: (event, { editor }) => {
|
|
2534
|
+
if (event.key !== trigger || event.ctrlKey || event.metaKey || event.altKey) {
|
|
2535
|
+
return false;
|
|
2536
|
+
}
|
|
2537
|
+
const beforeCursor = editor.getContentBeforeCursor();
|
|
2538
|
+
if (beforeCursor === null) return false;
|
|
2539
|
+
const previous = beforeCursor.at(-1) ?? "";
|
|
2540
|
+
return previous === "" || /\s|[([{]/.test(previous);
|
|
2541
|
+
},
|
|
2542
|
+
activation: { type: "trigger", key: trigger },
|
|
2543
|
+
onActiveKeyDown: (event) => {
|
|
2544
|
+
if (event.key.length !== 1) return;
|
|
2545
|
+
if (/[\p{L}\p{N}_+-]/u.test(event.key)) return;
|
|
2546
|
+
return false;
|
|
2547
|
+
},
|
|
2548
|
+
render: (props) => {
|
|
2549
|
+
if (!props.active) return null;
|
|
2550
|
+
return /* @__PURE__ */ jsx(
|
|
2551
|
+
PluginMenuPrimitive,
|
|
2552
|
+
{
|
|
2553
|
+
...props,
|
|
2554
|
+
pluginName: name,
|
|
2555
|
+
items: search ? void 0 : emojis,
|
|
2556
|
+
search: search ?? ((query) => defaultSearch(resolvedEmojis, query)),
|
|
2557
|
+
getKey: (item) => item.name,
|
|
2558
|
+
renderItem: renderItem ?? ((item, _active) => /* @__PURE__ */ jsx(DefaultEmojiItem, { item })),
|
|
2559
|
+
itemToText: (item) => item.emoji,
|
|
2560
|
+
placeholder: "Search emoji...",
|
|
2561
|
+
emptyMessage
|
|
2562
|
+
}
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
1452
2566
|
}
|
|
1453
|
-
function
|
|
1454
|
-
const
|
|
2567
|
+
function createMentionsPlugin(options) {
|
|
2568
|
+
const name = options.name ?? "mentions";
|
|
2569
|
+
const trigger = options.trigger ?? "@";
|
|
2570
|
+
const marker = options.marker ?? "mention";
|
|
2571
|
+
const itemToText = (item) => options.onSelect ? options.onSelect(item) : `@${marker}[${item.id}]`;
|
|
1455
2572
|
return {
|
|
1456
|
-
name
|
|
1457
|
-
|
|
2573
|
+
name,
|
|
2574
|
+
activation: { type: "trigger", key: trigger },
|
|
2575
|
+
// Dismiss the picker when the user types whitespace or punctuation —
|
|
2576
|
+
// matches the emoji plugin so `@john<space>` flows back into the
|
|
2577
|
+
// document instead of growing the query indefinitely.
|
|
2578
|
+
onActiveKeyDown: (event) => {
|
|
2579
|
+
if (event.key.length !== 1) return;
|
|
2580
|
+
if (/[\p{L}\p{N}_-]/u.test(event.key)) return;
|
|
2581
|
+
return false;
|
|
2582
|
+
},
|
|
1458
2583
|
render: (props) => /* @__PURE__ */ jsx(
|
|
2584
|
+
PluginMenuPrimitive,
|
|
2585
|
+
{
|
|
2586
|
+
pluginName: name,
|
|
2587
|
+
placeholder: "Search...",
|
|
2588
|
+
search: options.search,
|
|
2589
|
+
getKey: (item) => item.id,
|
|
2590
|
+
renderItem: options.renderItem,
|
|
2591
|
+
itemToText,
|
|
2592
|
+
emptyMessage: options.emptyMessage ?? "No results",
|
|
2593
|
+
...props
|
|
2594
|
+
}
|
|
2595
|
+
)
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
var fuzzyMatch = (query, text) => {
|
|
2599
|
+
const q = query.toLowerCase();
|
|
2600
|
+
const t = text.toLowerCase();
|
|
2601
|
+
return t.includes(q) || t.startsWith(q);
|
|
2602
|
+
};
|
|
2603
|
+
var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
2604
|
+
commands,
|
|
2605
|
+
emptyMessage,
|
|
2606
|
+
onReadyChange,
|
|
2607
|
+
onExecute,
|
|
2608
|
+
onDismiss,
|
|
2609
|
+
position,
|
|
2610
|
+
getEditor
|
|
2611
|
+
}, ref) {
|
|
2612
|
+
const [mode, setMode] = useState("commands");
|
|
2613
|
+
const [query, setQuery] = useState("");
|
|
2614
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
2615
|
+
const [selectedCommand, setSelectedCommand] = useState(null);
|
|
2616
|
+
const [selectedArg, setSelectedArg] = useState(null);
|
|
2617
|
+
const [argChoices, setArgChoices] = useState([]);
|
|
2618
|
+
const [loadingArgs, setLoadingArgs] = useState(false);
|
|
2619
|
+
useEffect(() => {
|
|
2620
|
+
onReadyChange?.(mode === "ready");
|
|
2621
|
+
}, [mode, onReadyChange]);
|
|
2622
|
+
useEffect(() => {
|
|
2623
|
+
let cancelled = false;
|
|
2624
|
+
const loadChoices = async () => {
|
|
2625
|
+
if (mode !== "args" || !selectedCommand) {
|
|
2626
|
+
setArgChoices([]);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const firstArg = selectedCommand.arg;
|
|
2630
|
+
if (!firstArg) {
|
|
2631
|
+
setArgChoices([]);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (firstArg.choices) {
|
|
2635
|
+
setArgChoices(firstArg.choices);
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
if (!firstArg.fetchChoices) {
|
|
2639
|
+
setArgChoices([]);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
setLoadingArgs(true);
|
|
2643
|
+
try {
|
|
2644
|
+
const choices = await firstArg.fetchChoices();
|
|
2645
|
+
if (!cancelled) setArgChoices(choices);
|
|
2646
|
+
} finally {
|
|
2647
|
+
if (!cancelled) setLoadingArgs(false);
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
void loadChoices();
|
|
2651
|
+
return () => {
|
|
2652
|
+
cancelled = true;
|
|
2653
|
+
};
|
|
2654
|
+
}, [mode, selectedCommand]);
|
|
2655
|
+
const commandItems = useMemo(
|
|
2656
|
+
() => commands.filter((command) => {
|
|
2657
|
+
if (!query) return true;
|
|
2658
|
+
return fuzzyMatch(query, command.name) || command.aliases?.some((alias) => fuzzyMatch(query, alias));
|
|
2659
|
+
}).map((command) => {
|
|
2660
|
+
const disabledReason = command.disabled?.();
|
|
2661
|
+
return {
|
|
2662
|
+
type: "command",
|
|
2663
|
+
value: command.name,
|
|
2664
|
+
label: `/${command.name}`,
|
|
2665
|
+
description: command.description,
|
|
2666
|
+
disabled: !!disabledReason,
|
|
2667
|
+
disabledReason: disabledReason || void 0,
|
|
2668
|
+
command
|
|
2669
|
+
};
|
|
2670
|
+
}),
|
|
2671
|
+
[commands, query]
|
|
2672
|
+
);
|
|
2673
|
+
const argItems = useMemo(
|
|
2674
|
+
() => argChoices.filter((choice) => !query || fuzzyMatch(query, choice.label)).map((choice) => ({
|
|
2675
|
+
type: "arg",
|
|
2676
|
+
value: choice.value,
|
|
2677
|
+
label: choice.label,
|
|
2678
|
+
description: choice.disabled ? "(current)" : selectedCommand?.arg?.description ?? "",
|
|
2679
|
+
disabled: choice.disabled
|
|
2680
|
+
})),
|
|
2681
|
+
[argChoices, query, selectedCommand]
|
|
2682
|
+
);
|
|
2683
|
+
const items = mode === "args" ? argItems : commandItems;
|
|
2684
|
+
const reactId = useId().replace(/:/g, "");
|
|
2685
|
+
const listboxId = `${pluginPickerClass.picker}-${reactId}-slash-listbox`;
|
|
2686
|
+
const activeOptionId = `${listboxId}-option-${selectedIndex}`;
|
|
2687
|
+
useEffect(() => {
|
|
2688
|
+
const firstEnabled = items.findIndex((item) => !item.disabled);
|
|
2689
|
+
setSelectedIndex(firstEnabled >= 0 ? firstEnabled : 0);
|
|
2690
|
+
}, [items.length, mode, query]);
|
|
2691
|
+
const writeSlashLine = useCallback(
|
|
2692
|
+
(line) => {
|
|
2693
|
+
const editor = getEditor();
|
|
2694
|
+
if (!editor) return;
|
|
2695
|
+
editor.replaceCurrentBlockContent(line);
|
|
2696
|
+
},
|
|
2697
|
+
[getEditor]
|
|
2698
|
+
);
|
|
2699
|
+
const handleSelect = useCallback(
|
|
2700
|
+
(item) => {
|
|
2701
|
+
if (item.disabled) return;
|
|
2702
|
+
if (item.type === "command") {
|
|
2703
|
+
const command = item.command;
|
|
2704
|
+
if (!command) return;
|
|
2705
|
+
const hasArg = !!command.arg;
|
|
2706
|
+
const nextLine = `/${command.name}${hasArg ? " " : ""}`;
|
|
2707
|
+
writeSlashLine(nextLine);
|
|
2708
|
+
setSelectedCommand(command);
|
|
2709
|
+
setSelectedArg(null);
|
|
2710
|
+
setQuery("");
|
|
2711
|
+
setMode(hasArg ? "args" : "ready");
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
if (!selectedCommand) return;
|
|
2715
|
+
const firstArg = selectedCommand.arg;
|
|
2716
|
+
writeSlashLine(`/${selectedCommand.name} ${item.label}`);
|
|
2717
|
+
setSelectedArg(
|
|
2718
|
+
firstArg ? { name: firstArg.name, value: item.value } : null
|
|
2719
|
+
);
|
|
2720
|
+
setQuery("");
|
|
2721
|
+
setMode("ready");
|
|
2722
|
+
},
|
|
2723
|
+
[selectedCommand, writeSlashLine]
|
|
2724
|
+
);
|
|
2725
|
+
useImperativeHandle(
|
|
2726
|
+
ref,
|
|
2727
|
+
() => ({
|
|
2728
|
+
isReady: () => mode === "ready",
|
|
2729
|
+
appendQuery: (value) => setQuery((current) => `${current}${value}`),
|
|
2730
|
+
removeQueryChar: () => setQuery((current) => current.length > 0 ? current.slice(0, -1) : ""),
|
|
2731
|
+
move: (direction) => setSelectedIndex((current) => {
|
|
2732
|
+
if (items.length === 0) return current;
|
|
2733
|
+
for (let step = 1; step <= items.length; step++) {
|
|
2734
|
+
const next = (current + direction * step + items.length) % items.length;
|
|
2735
|
+
if (!items[next]?.disabled) return next;
|
|
2736
|
+
}
|
|
2737
|
+
return current;
|
|
2738
|
+
}),
|
|
2739
|
+
selectActive: () => {
|
|
2740
|
+
const item = items[selectedIndex];
|
|
2741
|
+
if (item) handleSelect(item);
|
|
2742
|
+
},
|
|
2743
|
+
execute: () => {
|
|
2744
|
+
if (!selectedCommand) return;
|
|
2745
|
+
const editor = getEditor();
|
|
2746
|
+
const raw = editor?.getCurrentBlockContent() ?? `/${selectedCommand.name}`;
|
|
2747
|
+
onExecute?.({
|
|
2748
|
+
name: selectedCommand.name,
|
|
2749
|
+
args: selectedArg ? { [selectedArg.name]: selectedArg.value } : {},
|
|
2750
|
+
raw
|
|
2751
|
+
});
|
|
2752
|
+
},
|
|
2753
|
+
reset: () => {
|
|
2754
|
+
setMode("commands");
|
|
2755
|
+
setQuery("");
|
|
2756
|
+
setSelectedCommand(null);
|
|
2757
|
+
setSelectedArg(null);
|
|
2758
|
+
setSelectedIndex(0);
|
|
2759
|
+
setArgChoices([]);
|
|
2760
|
+
}
|
|
2761
|
+
}),
|
|
2762
|
+
[
|
|
2763
|
+
getEditor,
|
|
2764
|
+
handleSelect,
|
|
2765
|
+
items,
|
|
2766
|
+
mode,
|
|
2767
|
+
onExecute,
|
|
2768
|
+
selectedArg,
|
|
2769
|
+
selectedCommand,
|
|
2770
|
+
selectedIndex
|
|
2771
|
+
]
|
|
2772
|
+
);
|
|
2773
|
+
if (mode === "ready" && selectedCommand) {
|
|
2774
|
+
return /* @__PURE__ */ jsx(
|
|
1459
2775
|
"div",
|
|
1460
2776
|
{
|
|
1461
|
-
className:
|
|
2777
|
+
className: pluginPickerClass.popup,
|
|
1462
2778
|
style: {
|
|
1463
2779
|
position: "absolute",
|
|
1464
|
-
top:
|
|
1465
|
-
left:
|
|
2780
|
+
top: position.top,
|
|
2781
|
+
left: position.left,
|
|
1466
2782
|
zIndex: 1001
|
|
1467
2783
|
},
|
|
1468
|
-
|
|
1469
|
-
|
|
2784
|
+
children: /* @__PURE__ */ jsx("div", { className: pluginPickerClass.picker, children: /* @__PURE__ */ jsx("div", { className: "inkwell-plugin-slash-commands-execute", children: "Enter to execute \xB7 Esc to cancel" }) })
|
|
2785
|
+
}
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
return /* @__PURE__ */ jsx(
|
|
2789
|
+
"div",
|
|
2790
|
+
{
|
|
2791
|
+
className: pluginPickerClass.popup,
|
|
2792
|
+
style: {
|
|
2793
|
+
position: "absolute",
|
|
2794
|
+
top: position.top,
|
|
2795
|
+
left: position.left,
|
|
2796
|
+
zIndex: 1001
|
|
2797
|
+
},
|
|
2798
|
+
children: /* @__PURE__ */ jsxs("div", { className: pluginPickerClass.picker, children: [
|
|
2799
|
+
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, children: mode === "commands" ? `/${query}` : `${selectedCommand ? `/${selectedCommand.name} ` : ""}${query}` }),
|
|
2800
|
+
loadingArgs && items.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: "Loading..." }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: emptyMessage }) : /* @__PURE__ */ jsx(
|
|
2801
|
+
"div",
|
|
2802
|
+
{
|
|
2803
|
+
id: listboxId,
|
|
2804
|
+
role: "listbox",
|
|
2805
|
+
"aria-activedescendant": activeOptionId,
|
|
2806
|
+
children: items.map((item, index) => {
|
|
2807
|
+
const active = index === selectedIndex;
|
|
2808
|
+
return /* @__PURE__ */ jsxs(
|
|
2809
|
+
"div",
|
|
2810
|
+
{
|
|
2811
|
+
id: `${listboxId}-option-${index}`,
|
|
2812
|
+
role: "option",
|
|
2813
|
+
"aria-selected": active,
|
|
2814
|
+
className: `${pluginPickerClass.item} ${active ? pluginPickerClass.itemActive : ""}`,
|
|
2815
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
2816
|
+
onMouseEnter: () => {
|
|
2817
|
+
if (!item.disabled) setSelectedIndex(index);
|
|
2818
|
+
},
|
|
2819
|
+
onClick: () => handleSelect(item),
|
|
2820
|
+
"aria-disabled": item.disabled,
|
|
2821
|
+
children: [
|
|
2822
|
+
/* @__PURE__ */ jsx("span", { className: pluginPickerClass.title, children: item.label }),
|
|
2823
|
+
/* @__PURE__ */ jsx("span", { className: pluginPickerClass.subtitle, children: item.disabledReason ?? item.description })
|
|
2824
|
+
]
|
|
2825
|
+
},
|
|
2826
|
+
`${item.type}-${item.value}`
|
|
2827
|
+
);
|
|
2828
|
+
})
|
|
2829
|
+
}
|
|
2830
|
+
),
|
|
2831
|
+
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: "\u2191\u2193 navigate \xB7 Tab/Enter select \xB7 Esc close" })
|
|
2832
|
+
] })
|
|
2833
|
+
}
|
|
2834
|
+
);
|
|
2835
|
+
});
|
|
2836
|
+
var createSlashCommandsPlugin = ({
|
|
2837
|
+
name = "slash-commands",
|
|
2838
|
+
commands,
|
|
2839
|
+
onReadyChange,
|
|
2840
|
+
onExecute,
|
|
2841
|
+
emptyMessage = "No commands found"
|
|
2842
|
+
}) => {
|
|
2843
|
+
let editorRef = null;
|
|
2844
|
+
const menuRef = { current: null };
|
|
2845
|
+
const enterReadyOrExecuteCleanup = (editor, ctx, action) => {
|
|
2846
|
+
action();
|
|
2847
|
+
ctx.dismiss();
|
|
2848
|
+
requestAnimationFrame(() => editor.clearCurrentBlock());
|
|
2849
|
+
};
|
|
2850
|
+
return {
|
|
2851
|
+
name,
|
|
2852
|
+
activation: { type: "manual" },
|
|
2853
|
+
render: (props) => {
|
|
2854
|
+
if (!props.active) return null;
|
|
2855
|
+
return /* @__PURE__ */ jsx(
|
|
2856
|
+
SlashCommandMenuInner,
|
|
2857
|
+
{
|
|
2858
|
+
...props,
|
|
2859
|
+
ref: menuRef,
|
|
2860
|
+
commands,
|
|
2861
|
+
emptyMessage,
|
|
2862
|
+
onReadyChange,
|
|
2863
|
+
onExecute,
|
|
2864
|
+
getEditor: () => editorRef
|
|
2865
|
+
}
|
|
2866
|
+
);
|
|
2867
|
+
},
|
|
2868
|
+
onKeyDown: (event, ctx) => {
|
|
2869
|
+
editorRef = ctx.editor;
|
|
2870
|
+
if (event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
2871
|
+
const beforeCursor = ctx.editor.getCurrentBlockContentBeforeCursor();
|
|
2872
|
+
const blockText = ctx.editor.getCurrentBlockContent();
|
|
2873
|
+
if (beforeCursor !== null && beforeCursor.trim() === "" && blockText !== null && blockText.trim() === "") {
|
|
2874
|
+
event.preventDefault();
|
|
2875
|
+
ctx.editor.insertContent("/");
|
|
2876
|
+
ctx.activate();
|
|
2877
|
+
menuRef.current?.reset();
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
},
|
|
2881
|
+
onActiveKeyDown: (event, ctx) => {
|
|
2882
|
+
editorRef = ctx.editor;
|
|
2883
|
+
const menu = menuRef.current;
|
|
2884
|
+
if (!menu) return;
|
|
2885
|
+
if (event.key === "Escape") {
|
|
2886
|
+
event.preventDefault();
|
|
2887
|
+
if (menu.isReady()) {
|
|
2888
|
+
enterReadyOrExecuteCleanup(ctx.editor, ctx, () => {
|
|
2889
|
+
});
|
|
2890
|
+
} else {
|
|
2891
|
+
ctx.dismiss();
|
|
2892
|
+
menu.reset();
|
|
2893
|
+
}
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
if (menu.isReady() && event.key === "Enter") {
|
|
2897
|
+
event.preventDefault();
|
|
2898
|
+
enterReadyOrExecuteCleanup(ctx.editor, ctx, () => {
|
|
2899
|
+
menu.execute();
|
|
2900
|
+
});
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
if (event.key === "ArrowDown") {
|
|
2904
|
+
event.preventDefault();
|
|
2905
|
+
menu.move(1);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
if (event.key === "ArrowUp") {
|
|
2909
|
+
event.preventDefault();
|
|
2910
|
+
menu.move(-1);
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
if (event.key === "Tab" || event.key === "Enter") {
|
|
2914
|
+
event.preventDefault();
|
|
2915
|
+
menu.selectActive();
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
if (event.key === "Backspace") {
|
|
2919
|
+
const beforeCursor = ctx.editor.getCurrentBlockContentBeforeCursor();
|
|
2920
|
+
if (beforeCursor === "/") {
|
|
2921
|
+
ctx.dismiss();
|
|
2922
|
+
menu.reset();
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
menu.removeQueryChar();
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey && event.key.length === 1) {
|
|
2929
|
+
menu.appendQuery(event.key);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
};
|
|
2934
|
+
function renderSnippet(snippet) {
|
|
2935
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2936
|
+
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.title, children: snippet.title }),
|
|
2937
|
+
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.preview, children: snippet.content.length > 80 ? `${snippet.content.slice(0, 80)}...` : snippet.content })
|
|
2938
|
+
] });
|
|
2939
|
+
}
|
|
2940
|
+
function createSnippetsPlugin({
|
|
2941
|
+
snippets,
|
|
2942
|
+
name = "snippets",
|
|
2943
|
+
trigger = "["
|
|
2944
|
+
}) {
|
|
2945
|
+
return {
|
|
2946
|
+
name,
|
|
2947
|
+
activation: { type: "trigger", key: trigger },
|
|
2948
|
+
render: (props) => /* @__PURE__ */ jsx(
|
|
2949
|
+
PluginMenuPrimitive,
|
|
2950
|
+
{
|
|
2951
|
+
pluginName: name,
|
|
2952
|
+
items: snippets,
|
|
2953
|
+
getKey: (snippet) => snippet.title,
|
|
2954
|
+
renderItem: renderSnippet,
|
|
2955
|
+
itemToText: (snippet) => snippet.content,
|
|
2956
|
+
placeholder: "Search snippets...",
|
|
2957
|
+
emptyMessage: "No snippets found",
|
|
2958
|
+
...props
|
|
1470
2959
|
}
|
|
1471
2960
|
)
|
|
1472
2961
|
};
|
|
@@ -1485,7 +2974,7 @@ var processor = unified().use(rehypeParse, { fragment: true }).use(rehypeRemark)
|
|
|
1485
2974
|
}
|
|
1486
2975
|
}
|
|
1487
2976
|
});
|
|
1488
|
-
function
|
|
2977
|
+
function htmlToMarkdown(html) {
|
|
1489
2978
|
return String(processor.processSync(html)).trim();
|
|
1490
2979
|
}
|
|
1491
2980
|
function CopyCodeBlock({
|
|
@@ -1546,14 +3035,76 @@ function CopyCodeBlock({
|
|
|
1546
3035
|
/* @__PURE__ */ jsx("pre", { ref: preRef, ...props, children })
|
|
1547
3036
|
] });
|
|
1548
3037
|
}
|
|
1549
|
-
|
|
3038
|
+
var MENTION_TAG_PREFIX = "inkwell-mention-";
|
|
3039
|
+
function rehypeMentions(mentions) {
|
|
3040
|
+
return () => (tree) => {
|
|
3041
|
+
if (mentions.length === 0) return;
|
|
3042
|
+
visit(tree, "text", (node, index, parent) => {
|
|
3043
|
+
if (typeof node.value !== "string" || !parent || index == null) return;
|
|
3044
|
+
const text = node.value;
|
|
3045
|
+
const hits = [];
|
|
3046
|
+
for (let i = 0; i < mentions.length; i++) {
|
|
3047
|
+
const re = new RegExp(
|
|
3048
|
+
mentions[i].pattern.source,
|
|
3049
|
+
mentions[i].pattern.flags.includes("g") ? mentions[i].pattern.flags : `${mentions[i].pattern.flags}g`
|
|
3050
|
+
);
|
|
3051
|
+
let m;
|
|
3052
|
+
while ((m = re.exec(text)) !== null) {
|
|
3053
|
+
hits.push({
|
|
3054
|
+
start: m.index,
|
|
3055
|
+
end: m.index + m[0].length,
|
|
3056
|
+
mentionIdx: i,
|
|
3057
|
+
matchText: m[0]
|
|
3058
|
+
});
|
|
3059
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
if (hits.length === 0) return;
|
|
3063
|
+
hits.sort((a, b) => a.start - b.start || a.mentionIdx - b.mentionIdx);
|
|
3064
|
+
const nonOverlapping = [];
|
|
3065
|
+
let cursor = 0;
|
|
3066
|
+
for (const hit of hits) {
|
|
3067
|
+
if (hit.start < cursor) continue;
|
|
3068
|
+
nonOverlapping.push(hit);
|
|
3069
|
+
cursor = hit.end;
|
|
3070
|
+
}
|
|
3071
|
+
const replacements = [];
|
|
3072
|
+
let offset = 0;
|
|
3073
|
+
for (const hit of nonOverlapping) {
|
|
3074
|
+
if (hit.start > offset) {
|
|
3075
|
+
replacements.push({
|
|
3076
|
+
type: "text",
|
|
3077
|
+
value: text.slice(offset, hit.start)
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
replacements.push({
|
|
3081
|
+
type: "element",
|
|
3082
|
+
tagName: `${MENTION_TAG_PREFIX}${hit.mentionIdx}`,
|
|
3083
|
+
properties: { "data-match": hit.matchText },
|
|
3084
|
+
children: []
|
|
3085
|
+
});
|
|
3086
|
+
offset = hit.end;
|
|
3087
|
+
}
|
|
3088
|
+
if (offset < text.length) {
|
|
3089
|
+
replacements.push({
|
|
3090
|
+
type: "text",
|
|
3091
|
+
value: text.slice(offset)
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
parent.children.splice(index, 1, ...replacements);
|
|
3095
|
+
return [SKIP, index + replacements.length];
|
|
3096
|
+
});
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
function createProcessor2(options = {}) {
|
|
1550
3100
|
const proc = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype);
|
|
1551
3101
|
const plugins = options.rehypePlugins ?? [
|
|
1552
3102
|
[rehypeHighlight, { detect: true }]
|
|
1553
3103
|
];
|
|
1554
3104
|
for (const plugin of plugins) {
|
|
1555
3105
|
if (Array.isArray(plugin)) {
|
|
1556
|
-
|
|
3106
|
+
const [rehypePlugin, ...options2] = plugin;
|
|
3107
|
+
proc.use(rehypePlugin, ...options2);
|
|
1557
3108
|
} else {
|
|
1558
3109
|
proc.use(plugin);
|
|
1559
3110
|
}
|
|
@@ -1567,21 +3118,37 @@ function createProcessor(options = {}) {
|
|
|
1567
3118
|
span: ["className"]
|
|
1568
3119
|
}
|
|
1569
3120
|
});
|
|
3121
|
+
const mentionConfigs = options.mentions ?? [];
|
|
3122
|
+
if (mentionConfigs.length > 0) {
|
|
3123
|
+
proc.use(rehypeMentions(mentionConfigs));
|
|
3124
|
+
}
|
|
3125
|
+
const mentionComponents = {};
|
|
3126
|
+
mentionConfigs.forEach((cfg, i) => {
|
|
3127
|
+
const tag = `${MENTION_TAG_PREFIX}${i}`;
|
|
3128
|
+
mentionComponents[tag] = (props) => {
|
|
3129
|
+
const matchText = props["data-match"] ?? "";
|
|
3130
|
+
const exec = new RegExp(cfg.pattern.source, cfg.pattern.flags).exec(
|
|
3131
|
+
matchText
|
|
3132
|
+
);
|
|
3133
|
+
if (!exec) return matchText;
|
|
3134
|
+
return cfg.resolve(exec);
|
|
3135
|
+
};
|
|
3136
|
+
});
|
|
1570
3137
|
proc.use(rehypeReact, {
|
|
1571
3138
|
createElement,
|
|
1572
3139
|
Fragment: Fragment,
|
|
1573
3140
|
jsx: jsx,
|
|
1574
3141
|
jsxs: jsxs,
|
|
1575
|
-
components: options.components ?? {}
|
|
3142
|
+
components: { ...mentionComponents, ...options.components ?? {} }
|
|
1576
3143
|
});
|
|
1577
3144
|
return proc;
|
|
1578
3145
|
}
|
|
1579
3146
|
function escapeBareBq2(markdown) {
|
|
1580
3147
|
return markdown.replace(/^>(?=\S)/gm, "\\>");
|
|
1581
3148
|
}
|
|
1582
|
-
function parseMarkdown(
|
|
1583
|
-
const processor2 =
|
|
1584
|
-
const file = processor2.processSync(escapeBareBq2(
|
|
3149
|
+
function parseMarkdown(content, options = {}) {
|
|
3150
|
+
const processor2 = createProcessor2(options);
|
|
3151
|
+
const file = processor2.processSync(escapeBareBq2(content));
|
|
1585
3152
|
return file.result;
|
|
1586
3153
|
}
|
|
1587
3154
|
function InkwellRenderer({
|
|
@@ -1589,17 +3156,21 @@ function InkwellRenderer({
|
|
|
1589
3156
|
className,
|
|
1590
3157
|
components,
|
|
1591
3158
|
rehypePlugins,
|
|
1592
|
-
|
|
3159
|
+
mentions
|
|
1593
3160
|
}) {
|
|
1594
|
-
const mergedComponents = useMemo(
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
3161
|
+
const mergedComponents = useMemo(
|
|
3162
|
+
() => ({ pre: CopyCodeBlock, ...components }),
|
|
3163
|
+
[components]
|
|
3164
|
+
);
|
|
1598
3165
|
const rendered = useMemo(
|
|
1599
|
-
() => parseMarkdown(content,
|
|
1600
|
-
|
|
3166
|
+
() => parseMarkdown(content, {
|
|
3167
|
+
components: mergedComponents,
|
|
3168
|
+
rehypePlugins,
|
|
3169
|
+
mentions
|
|
3170
|
+
}),
|
|
3171
|
+
[content, mergedComponents, rehypePlugins, mentions]
|
|
1601
3172
|
);
|
|
1602
3173
|
return /* @__PURE__ */ jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
|
|
1603
3174
|
}
|
|
1604
3175
|
|
|
1605
|
-
export { InkwellEditor, InkwellRenderer, createBubbleMenuPlugin, createSnippetsPlugin, defaultBubbleMenuItems,
|
|
3176
|
+
export { InkwellEditor, InkwellRenderer, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
|