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