@estiva-app/ui 0.66.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BlockNodes.d.ts +12 -1
- package/dist/BlockNodes.d.ts.map +1 -1
- package/dist/EditorTableControls.d.ts +38 -0
- package/dist/EditorTableControls.d.ts.map +1 -0
- package/dist/LinkChips.d.ts +82 -0
- package/dist/LinkChips.d.ts.map +1 -0
- package/dist/NavItem.d.ts +11 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/ReferenceCards.d.ts +77 -0
- package/dist/ReferenceCards.d.ts.map +1 -0
- package/dist/ReferenceMenu.d.ts +18 -0
- package/dist/ReferenceMenu.d.ts.map +1 -0
- package/dist/RichText.d.ts +9 -2
- package/dist/RichText.d.ts.map +1 -1
- package/dist/SelectionToolbar.d.ts +9 -1
- package/dist/SelectionToolbar.d.ts.map +1 -1
- package/dist/editor.d.ts +4 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +750 -27
- package/dist/editor.js.map +4 -4
- package/dist/index.js +62 -33
- package/dist/index.js.map +4 -4
- package/package.json +2 -1
- package/registry.json +414 -4
- package/src/BlockNodes.test.tsx +2 -1
- package/src/BlockNodes.tsx +51 -9
- package/src/EditorTableControls.mdx +78 -0
- package/src/EditorTableControls.stories.tsx +63 -0
- package/src/EditorTableControls.test.tsx +288 -0
- package/src/EditorTableControls.tsx +255 -0
- package/src/LinkChips.mdx +57 -0
- package/src/LinkChips.stories.tsx +67 -0
- package/src/LinkChips.test.tsx +178 -0
- package/src/LinkChips.ts +243 -0
- package/src/NavItem.mdx +10 -0
- package/src/NavItem.stories.tsx +23 -1
- package/src/NavItem.test.tsx +36 -0
- package/src/NavItem.tsx +40 -16
- package/src/ReferenceCards.mdx +79 -0
- package/src/ReferenceCards.stories.tsx +64 -0
- package/src/ReferenceCards.test.tsx +143 -0
- package/src/ReferenceCards.ts +225 -0
- package/src/ReferenceMenu.mdx +75 -0
- package/src/ReferenceMenu.stories.tsx +59 -0
- package/src/ReferenceMenu.test.tsx +289 -0
- package/src/ReferenceMenu.tsx +346 -0
- package/src/RichText.test.tsx +17 -0
- package/src/RichText.tsx +36 -4
- package/src/SelectionToolbar.tsx +10 -2
- package/src/editor.ts +14 -0
- package/src/pages.test.ts +3 -0
- package/src/registry/registry.test.ts +7 -7
- package/src/registry/siblings.test.ts +2 -2
- package/stories/Choosing.mdx +4 -0
package/dist/editor.js
CHANGED
|
@@ -83,7 +83,7 @@ var ICONS = {
|
|
|
83
83
|
var SETTLE_MS = 250;
|
|
84
84
|
var GAP = 8;
|
|
85
85
|
var POPOVER_GAP = 4;
|
|
86
|
-
function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false }) {
|
|
86
|
+
function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false, children }) {
|
|
87
87
|
const [anchor, setAnchor] = useState(null);
|
|
88
88
|
const [mode, setMode] = useState("marks");
|
|
89
89
|
const [url, setUrl] = useState("");
|
|
@@ -273,7 +273,8 @@ function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], lin
|
|
|
273
273
|
onClick: openField,
|
|
274
274
|
children: /* @__PURE__ */ jsx(IconLink, { size: 16, stroke: 1.5 })
|
|
275
275
|
}
|
|
276
|
-
)
|
|
276
|
+
),
|
|
277
|
+
children
|
|
277
278
|
] })
|
|
278
279
|
)
|
|
279
280
|
}
|
|
@@ -580,6 +581,155 @@ function EditorBlockHandle({ editor, turnInto = [], actions, children, className
|
|
|
580
581
|
return /* @__PURE__ */ jsx3(BlockHandle, { editing, actions, className, children });
|
|
581
582
|
}
|
|
582
583
|
|
|
584
|
+
// src/EditorTableControls.tsx
|
|
585
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
|
|
586
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
587
|
+
import { addColumnAfter, addColumnBefore, addRowAfter, addRowBefore, deleteColumn, deleteRow, deleteTable, toggleHeader } from "@tiptap/pm/tables";
|
|
588
|
+
import { IconColumnInsertLeft, IconColumnInsertRight, IconGripHorizontal, IconGripVertical, IconPlus, IconRowInsertBottom, IconRowInsertTop, IconTrash as IconTrash2 } from "@tabler/icons-react";
|
|
589
|
+
import { cn } from "@estiva-app/ui";
|
|
590
|
+
import { IconButton } from "@estiva-app/ui";
|
|
591
|
+
|
|
592
|
+
// src/looks.ts
|
|
593
|
+
var FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
|
|
594
|
+
|
|
595
|
+
// src/EditorTableControls.tsx
|
|
596
|
+
import { Menu, MenuItem as MenuItem2, MenuSeparator } from "@estiva-app/ui";
|
|
597
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
598
|
+
function inCell(editor, cell, command) {
|
|
599
|
+
const { view } = editor;
|
|
600
|
+
if (!view.dom.contains(cell)) return false;
|
|
601
|
+
const at = view.posAtDOM(cell, 0);
|
|
602
|
+
view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(at))));
|
|
603
|
+
const done = command(view.state, view.dispatch, view);
|
|
604
|
+
editor.commands.focus();
|
|
605
|
+
return done;
|
|
606
|
+
}
|
|
607
|
+
function isHeaderRow(row) {
|
|
608
|
+
return row.cells.length > 0 && Array.from(row.cells).every((c) => c.tagName === "TH");
|
|
609
|
+
}
|
|
610
|
+
function deleteTableRow(editor, cell) {
|
|
611
|
+
const row = cell.closest("tr");
|
|
612
|
+
const table = cell.closest("table");
|
|
613
|
+
if (!row || !table || table.rows.length <= 1) return inCell(editor, cell, deleteTable);
|
|
614
|
+
const header = isHeaderRow(row);
|
|
615
|
+
return inCell(editor, cell, header ? (state, dispatch, view) => deleteRow(state, dispatch) && (!view || toggleHeader("row")(view.state, view.dispatch)) : deleteRow);
|
|
616
|
+
}
|
|
617
|
+
function deleteTableColumn(editor, cell) {
|
|
618
|
+
const row = cell.closest("tr");
|
|
619
|
+
return inCell(editor, cell, row && row.cells.length > 1 ? deleteColumn : deleteTable);
|
|
620
|
+
}
|
|
621
|
+
function appendTableRow(editor, table) {
|
|
622
|
+
const last = table.rows[table.rows.length - 1]?.cells[0];
|
|
623
|
+
return !!last && inCell(editor, last, addRowAfter);
|
|
624
|
+
}
|
|
625
|
+
function appendTableColumn(editor, table) {
|
|
626
|
+
const first = table.rows[0];
|
|
627
|
+
const last = first?.cells[first.cells.length - 1];
|
|
628
|
+
return !!last && inCell(editor, last, addColumnAfter);
|
|
629
|
+
}
|
|
630
|
+
function place(frame, { table, row, cell }) {
|
|
631
|
+
const at = frame.getBoundingClientRect();
|
|
632
|
+
const t = table.getBoundingClientRect();
|
|
633
|
+
const r = row.getBoundingClientRect();
|
|
634
|
+
const c = cell.getBoundingClientRect();
|
|
635
|
+
return {
|
|
636
|
+
tableBox: { top: t.top - at.top, left: t.left - at.left, width: t.width, height: t.height },
|
|
637
|
+
rowBox: { top: r.top - at.top, height: r.height },
|
|
638
|
+
columnBox: { left: c.left - at.left, width: c.width }
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
var ICON = { size: 16, stroke: 1.5, className: "text-text-secondary" };
|
|
642
|
+
var keepFocus = (e) => e.preventDefault();
|
|
643
|
+
var HANDLE_BOX = cn(FLOATING_SURFACE_CLASSES, "shadow-none");
|
|
644
|
+
function EditorTableControls({ editor, children, className }) {
|
|
645
|
+
const frame = useRef3(null);
|
|
646
|
+
const [hovered, setHovered] = useState2(null);
|
|
647
|
+
const [menu, setMenu] = useState2(null);
|
|
648
|
+
const editable = !!editor && editor.isEditable;
|
|
649
|
+
const follow = useCallback2(
|
|
650
|
+
(target) => {
|
|
651
|
+
if (!editor || !frame.current || !(target instanceof Element)) return;
|
|
652
|
+
if (target.closest("[data-table-control]")) return;
|
|
653
|
+
const cell = target.closest("td, th");
|
|
654
|
+
const row = cell?.closest("tr");
|
|
655
|
+
const table = row?.closest("table");
|
|
656
|
+
if (!cell || !row || !table || !editor.view.dom.contains(table)) {
|
|
657
|
+
setHovered(null);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const next = { table, row, cell, ...place(frame.current, { table, row, cell }) };
|
|
661
|
+
setHovered((was) => was && was.cell === cell && JSON.stringify([was.tableBox, was.rowBox, was.columnBox]) === JSON.stringify([next.tableBox, next.rowBox, next.columnBox]) ? was : next);
|
|
662
|
+
},
|
|
663
|
+
[editor]
|
|
664
|
+
);
|
|
665
|
+
useEffect2(() => {
|
|
666
|
+
if (!editor) return;
|
|
667
|
+
const changed = ({ transaction }) => {
|
|
668
|
+
if (transaction.docChanged && !menu) setHovered(null);
|
|
669
|
+
};
|
|
670
|
+
editor.on("transaction", changed);
|
|
671
|
+
return () => {
|
|
672
|
+
editor.off("transaction", changed);
|
|
673
|
+
};
|
|
674
|
+
}, [editor, menu]);
|
|
675
|
+
const shown = editable ? hovered : null;
|
|
676
|
+
const run = (act) => () => {
|
|
677
|
+
if (editor) act();
|
|
678
|
+
setMenu(null);
|
|
679
|
+
setHovered(null);
|
|
680
|
+
};
|
|
681
|
+
return /* @__PURE__ */ jsxs3(
|
|
682
|
+
"div",
|
|
683
|
+
{
|
|
684
|
+
ref: frame,
|
|
685
|
+
className: cn("relative", className),
|
|
686
|
+
onMouseMove: (e) => {
|
|
687
|
+
if (!menu) follow(e.target);
|
|
688
|
+
},
|
|
689
|
+
onMouseLeave: () => {
|
|
690
|
+
if (!menu) setHovered(null);
|
|
691
|
+
},
|
|
692
|
+
children: [
|
|
693
|
+
children,
|
|
694
|
+
shown && editor && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
695
|
+
/* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute -translate-x-1/2 -translate-y-1/2"), style: { left: shown.tableBox.left, top: shown.rowBox.top + shown.rowBox.height / 2 }, children: /* @__PURE__ */ jsxs3(
|
|
696
|
+
Menu,
|
|
697
|
+
{
|
|
698
|
+
open: menu === "row",
|
|
699
|
+
onOpenChange: (open) => setMenu(open ? "row" : null),
|
|
700
|
+
trigger: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Row menu", tooltip: "Row", className: "h-6 w-4 p-0", children: /* @__PURE__ */ jsx4(IconGripVertical, { size: 16, stroke: 1.5 }) }),
|
|
701
|
+
children: [
|
|
702
|
+
!isHeaderRow(shown.row) && /* @__PURE__ */ jsx4(MenuItem2, { label: "Insert above", leading: /* @__PURE__ */ jsx4(IconRowInsertTop, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowBefore)) }),
|
|
703
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Insert below", leading: /* @__PURE__ */ jsx4(IconRowInsertBottom, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addRowAfter)) }),
|
|
704
|
+
/* @__PURE__ */ jsx4(MenuSeparator, {}),
|
|
705
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => deleteTableRow(editor, shown.cell)) }),
|
|
706
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Delete table", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => inCell(editor, shown.cell, deleteTable)) })
|
|
707
|
+
]
|
|
708
|
+
}
|
|
709
|
+
) }),
|
|
710
|
+
/* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute -translate-x-1/2 -translate-y-1/2"), style: { left: shown.columnBox.left + shown.columnBox.width / 2, top: shown.tableBox.top }, children: /* @__PURE__ */ jsxs3(
|
|
711
|
+
Menu,
|
|
712
|
+
{
|
|
713
|
+
open: menu === "column",
|
|
714
|
+
onOpenChange: (open) => setMenu(open ? "column" : null),
|
|
715
|
+
trigger: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Column menu", tooltip: "Column", className: "h-4 w-6 p-0", children: /* @__PURE__ */ jsx4(IconGripHorizontal, { size: 16, stroke: 1.5 }) }),
|
|
716
|
+
children: [
|
|
717
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Insert left", leading: /* @__PURE__ */ jsx4(IconColumnInsertLeft, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnBefore)) }),
|
|
718
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Insert right", leading: /* @__PURE__ */ jsx4(IconColumnInsertRight, { ...ICON }), onClick: run(() => inCell(editor, shown.cell, addColumnAfter)) }),
|
|
719
|
+
/* @__PURE__ */ jsx4(MenuSeparator, {}),
|
|
720
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Delete", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => deleteTableColumn(editor, shown.cell)) }),
|
|
721
|
+
/* @__PURE__ */ jsx4(MenuItem2, { label: "Delete table", destructive: true, leading: /* @__PURE__ */ jsx4(IconTrash2, { size: 16, stroke: 1.5, className: "text-error-default" }), onClick: run(() => inCell(editor, shown.cell, deleteTable)) })
|
|
722
|
+
]
|
|
723
|
+
}
|
|
724
|
+
) }),
|
|
725
|
+
shown.row === shown.table.rows[shown.table.rows.length - 1] && /* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute flex h-4"), style: { left: shown.tableBox.left, top: shown.tableBox.top + shown.tableBox.height, width: shown.tableBox.width }, children: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Add row", tooltip: "Add row", className: "h-full w-full p-0", onMouseDown: keepFocus, onClick: run(() => appendTableRow(editor, shown.table)), children: /* @__PURE__ */ jsx4(IconPlus, { size: 12, stroke: 1.5 }) }) }),
|
|
726
|
+
shown.cell === shown.row.cells[shown.row.cells.length - 1] && /* @__PURE__ */ jsx4("div", { "data-table-control": true, className: cn(HANDLE_BOX, "absolute flex w-4"), style: { left: shown.tableBox.left + shown.tableBox.width, top: shown.tableBox.top, height: shown.tableBox.height }, children: /* @__PURE__ */ jsx4(IconButton, { "aria-label": "Add column", tooltip: "Add column", className: "h-full w-full p-0", onMouseDown: keepFocus, onClick: run(() => appendTableColumn(editor, shown.table)), children: /* @__PURE__ */ jsx4(IconPlus, { size: 12, stroke: 1.5 }) }) })
|
|
727
|
+
] })
|
|
728
|
+
]
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
583
733
|
// src/ComposerTriggers.tsx
|
|
584
734
|
import { Extension as Extension3 } from "@tiptap/core";
|
|
585
735
|
import { PluginKey as PluginKey3 } from "@tiptap/pm/state";
|
|
@@ -589,7 +739,7 @@ import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
|
|
|
589
739
|
import { Avatar } from "@estiva-app/ui";
|
|
590
740
|
import { EnterHint } from "@estiva-app/ui";
|
|
591
741
|
import { inlineChipClassName } from "@estiva-app/ui";
|
|
592
|
-
import { jsx as
|
|
742
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
593
743
|
var PEOPLE_LIMIT = 6;
|
|
594
744
|
function filterPeople(people, query) {
|
|
595
745
|
const q = query.toLowerCase();
|
|
@@ -607,8 +757,8 @@ function peopleMenu(urgent) {
|
|
|
607
757
|
row: (person) => ({
|
|
608
758
|
label: person.name,
|
|
609
759
|
description: person.description,
|
|
610
|
-
leading: person.avatar ?? /* @__PURE__ */
|
|
611
|
-
hint: /* @__PURE__ */
|
|
760
|
+
leading: person.avatar ?? /* @__PURE__ */ jsx5(Avatar, { size: 32, alt: person.name }),
|
|
761
|
+
hint: /* @__PURE__ */ jsx5(EnterHint, {}),
|
|
612
762
|
size: "tall"
|
|
613
763
|
})
|
|
614
764
|
};
|
|
@@ -735,9 +885,9 @@ var CaptionedReference = Mention.extend({
|
|
|
735
885
|
addNodeView() {
|
|
736
886
|
const { icon, iconFor } = this.options;
|
|
737
887
|
return ReactNodeViewRenderer(
|
|
738
|
-
({ node }) => /* @__PURE__ */
|
|
739
|
-
/* @__PURE__ */
|
|
740
|
-
/* @__PURE__ */
|
|
888
|
+
({ node }) => /* @__PURE__ */ jsxs4(NodeViewWrapper, { as: "span", className: inlineChipClassName("neutral", "max-w-[24ch] cursor-default"), children: [
|
|
889
|
+
/* @__PURE__ */ jsx5("span", { className: "flex items-center justify-center w-4 h-4 shrink-0 text-text-secondary", children: iconFor?.(node.attrs.uri ?? null) ?? icon }),
|
|
890
|
+
/* @__PURE__ */ jsx5("span", { className: "truncate", children: captionOf(node.attrs.label, node.attrs.snippet) })
|
|
741
891
|
] }),
|
|
742
892
|
{ as: "span" }
|
|
743
893
|
);
|
|
@@ -759,7 +909,7 @@ function referenceMenu({ ariaLabel, sectionLabel, icon, iconFor }) {
|
|
|
759
909
|
return groups;
|
|
760
910
|
},
|
|
761
911
|
itemKey: (item) => `reference-${item.id}`,
|
|
762
|
-
row: (item) => ({ leading: item.icon ?? iconFor?.(item.uri) ?? icon, label: captionOf(item.label, item.snippet), description: item.description, hint: /* @__PURE__ */
|
|
912
|
+
row: (item) => ({ leading: item.icon ?? iconFor?.(item.uri) ?? icon, label: captionOf(item.label, item.snippet), description: item.description, hint: /* @__PURE__ */ jsx5(EnterHint, {}) })
|
|
763
913
|
};
|
|
764
914
|
}
|
|
765
915
|
var listed = (options, query) => options.sections ? options.sections(query).flatMap((section) => section.items.map((item) => ({ ...item, section: section.label }))) : filterReferences(options.items(), query);
|
|
@@ -913,14 +1063,540 @@ var SlashCommands = Extension3.create({
|
|
|
913
1063
|
});
|
|
914
1064
|
|
|
915
1065
|
// src/BlockNodes.tsx
|
|
916
|
-
import { Extension as
|
|
1066
|
+
import { Extension as Extension7, InputRule, Node as Node2 } from "@tiptap/core";
|
|
917
1067
|
import { DOMSerializer } from "@tiptap/pm/model";
|
|
918
|
-
import { Plugin as
|
|
1068
|
+
import { Plugin as Plugin6, PluginKey as PluginKey7 } from "@tiptap/pm/state";
|
|
919
1069
|
import { Mapping, ReplaceAroundStep } from "@tiptap/pm/transform";
|
|
920
1070
|
import { NodeViewWrapper as NodeViewWrapper2, ReactNodeViewRenderer as ReactNodeViewRenderer2, ReactRenderer as ReactRenderer2 } from "@tiptap/react";
|
|
921
1071
|
import { Checkbox } from "@estiva-app/ui";
|
|
922
1072
|
import { calloutLook } from "@estiva-app/ui";
|
|
923
|
-
|
|
1073
|
+
|
|
1074
|
+
// src/ReferenceMenu.tsx
|
|
1075
|
+
import { useCallback as useCallback3 } from "react";
|
|
1076
|
+
import { Extension as Extension6 } from "@tiptap/core";
|
|
1077
|
+
import { useEditorState } from "@tiptap/react";
|
|
1078
|
+
import { NodeSelection as NodeSelection2, Plugin as Plugin5, PluginKey as PluginKey6, TextSelection as TextSelection2 } from "@tiptap/pm/state";
|
|
1079
|
+
import { ReplaceStep } from "@tiptap/pm/transform";
|
|
1080
|
+
import { IconCheck } from "@tabler/icons-react";
|
|
1081
|
+
|
|
1082
|
+
// src/LinkChips.ts
|
|
1083
|
+
import { Extension as Extension4 } from "@tiptap/core";
|
|
1084
|
+
import { Plugin as Plugin3, PluginKey as PluginKey4 } from "@tiptap/pm/state";
|
|
1085
|
+
var key = new PluginKey4("linkChips");
|
|
1086
|
+
var CHIPS_MADE = "linkChipsMade";
|
|
1087
|
+
var PASTES_KEPT = 8;
|
|
1088
|
+
var LINK_OR_URL = /(\[[^[\]\n]+\]\((?:https?:\/\/|mailto:)[^\s)]+\))|(https?:\/\/[^\s<>"'`]+)/g;
|
|
1089
|
+
var TRAILING = /* @__PURE__ */ new Set([".", ",", ";", ":", "!", "?", ")", "]"]);
|
|
1090
|
+
function urlRuns(text) {
|
|
1091
|
+
if (!text.includes("://")) return [];
|
|
1092
|
+
const out = [];
|
|
1093
|
+
for (const m of text.matchAll(LINK_OR_URL)) {
|
|
1094
|
+
if (m[1]) continue;
|
|
1095
|
+
const start = m.index;
|
|
1096
|
+
if (text.slice(Math.max(0, start - 2), start) === "](" || text[start - 1] === "[") continue;
|
|
1097
|
+
let end = m[0].length;
|
|
1098
|
+
while (end > 0 && TRAILING.has(m[0][end - 1])) end--;
|
|
1099
|
+
out.push({ url: m[0].slice(0, end), from: start, to: start + end, end: start + m[0].length });
|
|
1100
|
+
}
|
|
1101
|
+
return out;
|
|
1102
|
+
}
|
|
1103
|
+
function findLinkChips(doc, selection, referenceOf, pasted = []) {
|
|
1104
|
+
const found = { chips: [], fills: [], open: [] };
|
|
1105
|
+
const open = /* @__PURE__ */ new Set();
|
|
1106
|
+
doc.descendants((node, pos) => {
|
|
1107
|
+
if (node.type.spec.code) return false;
|
|
1108
|
+
if (node.type.name === "reference") {
|
|
1109
|
+
const href = typeof node.attrs.href === "string" ? node.attrs.href : "";
|
|
1110
|
+
if (href && !node.attrs.uri) {
|
|
1111
|
+
const uri = referenceOf?.(href);
|
|
1112
|
+
if (uri) found.fills.push({ pos, uri });
|
|
1113
|
+
else open.add(href);
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
if (!node.isText || !node.text) return true;
|
|
1118
|
+
if (node.marks.some((m) => m.type.spec.code || m.type.name === "code" || m.type.name === "link")) return false;
|
|
1119
|
+
for (const run of urlRuns(node.text)) {
|
|
1120
|
+
const from = pos + run.from;
|
|
1121
|
+
const to = pos + run.to;
|
|
1122
|
+
const wasPasted = pasted.some((range) => range.from <= from && pos + run.end <= range.to);
|
|
1123
|
+
const typing = selection && selection.to > from && selection.from <= pos + run.end;
|
|
1124
|
+
if (typing && !(selection.empty && wasPasted)) continue;
|
|
1125
|
+
const uri = referenceOf?.(run.url);
|
|
1126
|
+
if (uri) found.chips.push({ from, to, url: run.url, uri, marks: node.marks.map((m) => m.type.name), pasted: wasPasted });
|
|
1127
|
+
else open.add(run.url);
|
|
1128
|
+
}
|
|
1129
|
+
return false;
|
|
1130
|
+
});
|
|
1131
|
+
found.open = [...open];
|
|
1132
|
+
return found;
|
|
1133
|
+
}
|
|
1134
|
+
function apply(state, found) {
|
|
1135
|
+
if (!found.chips.length && !found.fills.length) return null;
|
|
1136
|
+
const type = state.schema.nodes.reference;
|
|
1137
|
+
if (!type) return null;
|
|
1138
|
+
const tr = state.tr;
|
|
1139
|
+
for (const { pos, uri } of found.fills) {
|
|
1140
|
+
const node = tr.doc.nodeAt(pos);
|
|
1141
|
+
if (node) tr.setNodeMarkup(pos, void 0, { ...node.attrs, uri });
|
|
1142
|
+
}
|
|
1143
|
+
for (const chip of [...found.chips].reverse()) {
|
|
1144
|
+
tr.replaceWith(chip.from, chip.to, type.create({ uri: chip.uri, href: chip.url, marks: chip.marks }));
|
|
1145
|
+
}
|
|
1146
|
+
return tr.setMeta("addToHistory", false).setMeta(CHIPS_MADE, { pasted: found.chips.some((chip) => chip.pasted) });
|
|
1147
|
+
}
|
|
1148
|
+
function refreshLinkChips(editor) {
|
|
1149
|
+
if (editor.isDestroyed) return;
|
|
1150
|
+
const chips = key.getState(editor.state);
|
|
1151
|
+
if (!chips) return;
|
|
1152
|
+
const tr = apply(editor.state, findLinkChips(editor.state.doc, editor.state.selection, chips.options.referenceOf, chips.pasted));
|
|
1153
|
+
if (tr) editor.view.dispatch(tr);
|
|
1154
|
+
}
|
|
1155
|
+
var LinkChips = Extension4.create({
|
|
1156
|
+
name: "linkChips",
|
|
1157
|
+
addOptions() {
|
|
1158
|
+
return { referenceOf: null, ask: null, limit: 24 };
|
|
1159
|
+
},
|
|
1160
|
+
addProseMirrorPlugins() {
|
|
1161
|
+
const options = this.options;
|
|
1162
|
+
const editor = this.editor;
|
|
1163
|
+
return [
|
|
1164
|
+
new Plugin3({
|
|
1165
|
+
key,
|
|
1166
|
+
// The options, where `refreshLinkChips` can read them from the state alone, and where the last pastes landed.
|
|
1167
|
+
state: {
|
|
1168
|
+
init: () => ({ options, pasted: [] }),
|
|
1169
|
+
apply: (tr, value, before, after) => {
|
|
1170
|
+
if (!tr.docChanged) return value;
|
|
1171
|
+
const pasted = value.pasted.map((range) => ({ from: tr.mapping.map(range.from, 1), to: tr.mapping.map(range.to, -1), size: range.to - range.from })).filter((range) => range.from < range.to && range.to - range.from === range.size).map(({ from, to }) => ({ from, to }));
|
|
1172
|
+
if (tr.getMeta("uiEvent") === "paste") {
|
|
1173
|
+
const from = tr.mapping.map(before.selection.from, -1);
|
|
1174
|
+
if (from < after.selection.to) pasted.push({ from, to: after.selection.to });
|
|
1175
|
+
}
|
|
1176
|
+
return { options, pasted: pasted.slice(-PASTES_KEPT) };
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
appendTransaction: (trs, _old, state) => {
|
|
1180
|
+
if (!trs.some((tr) => tr.docChanged || tr.selectionSet)) return null;
|
|
1181
|
+
return apply(state, findLinkChips(state.doc, state.selection, options.referenceOf, key.getState(state)?.pasted));
|
|
1182
|
+
},
|
|
1183
|
+
view: (view) => {
|
|
1184
|
+
const asked = /* @__PURE__ */ new Set();
|
|
1185
|
+
const look = (state) => {
|
|
1186
|
+
if (!options.ask) return;
|
|
1187
|
+
const { open } = findLinkChips(state.doc, state.selection, options.referenceOf, key.getState(state)?.pasted);
|
|
1188
|
+
const fresh = open.filter((url) => !asked.has(url)).slice(0, Math.max(0, options.limit - asked.size));
|
|
1189
|
+
if (!fresh.length) return;
|
|
1190
|
+
for (const url of fresh) asked.add(url);
|
|
1191
|
+
const forget = () => {
|
|
1192
|
+
for (const url of fresh) asked.delete(url);
|
|
1193
|
+
};
|
|
1194
|
+
let answer;
|
|
1195
|
+
try {
|
|
1196
|
+
answer = options.ask(fresh);
|
|
1197
|
+
} catch {
|
|
1198
|
+
forget();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
void Promise.resolve(answer).then(() => refreshLinkChips(editor), forget);
|
|
1202
|
+
};
|
|
1203
|
+
look(view.state);
|
|
1204
|
+
queueMicrotask(() => {
|
|
1205
|
+
try {
|
|
1206
|
+
refreshLinkChips(editor);
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
return {
|
|
1211
|
+
update: (next, prev) => {
|
|
1212
|
+
if (next.state.doc !== prev.doc || !next.state.selection.eq(prev.selection)) look(next.state);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
})
|
|
1217
|
+
];
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// src/ReferenceMenu.tsx
|
|
1222
|
+
import { MenuItem as MenuItem3, MenuSection, MenuSeparator as MenuSeparator2 } from "@estiva-app/ui";
|
|
1223
|
+
import { Popover as Popover2 } from "@estiva-app/ui";
|
|
1224
|
+
|
|
1225
|
+
// src/ReferenceCards.ts
|
|
1226
|
+
import { Extension as Extension5 } from "@tiptap/core";
|
|
1227
|
+
import { NodeSelection, Plugin as Plugin4, PluginKey as PluginKey5 } from "@tiptap/pm/state";
|
|
1228
|
+
import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
|
|
1229
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
1230
|
+
function layoutOf(raw) {
|
|
1231
|
+
if (typeof raw !== "string" || raw === "" || raw === "none") return void 0;
|
|
1232
|
+
return raw === "detailed" ? "detailed" : "compact";
|
|
1233
|
+
}
|
|
1234
|
+
function referenceCardPlacements(doc) {
|
|
1235
|
+
const out = [];
|
|
1236
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1237
|
+
doc.descendants((node, pos, parent) => {
|
|
1238
|
+
if (!node.isTextblock) return true;
|
|
1239
|
+
const cards = [];
|
|
1240
|
+
node.forEach((child) => {
|
|
1241
|
+
if (child.type.name !== "reference") return;
|
|
1242
|
+
const layout = layoutOf(child.attrs.embed);
|
|
1243
|
+
if (layout && child.attrs.uri) cards.push({ uri: String(child.attrs.uri), layout });
|
|
1244
|
+
});
|
|
1245
|
+
if (cards.length) {
|
|
1246
|
+
const id = node.attrs.blockId ?? parent?.attrs.blockId;
|
|
1247
|
+
const base = id ? `b:${String(id)}` : `p:${pos}`;
|
|
1248
|
+
const n = seen.get(base) ?? 0;
|
|
1249
|
+
seen.set(base, n + 1);
|
|
1250
|
+
out.push({ key: n ? `${base}:${n}` : base, pos: pos + node.nodeSize - 1, cards });
|
|
1251
|
+
}
|
|
1252
|
+
return false;
|
|
1253
|
+
});
|
|
1254
|
+
return out;
|
|
1255
|
+
}
|
|
1256
|
+
var key2 = new PluginKey5("referenceCards");
|
|
1257
|
+
function elementFor(storage, slotKey) {
|
|
1258
|
+
let element = storage.elements.get(slotKey);
|
|
1259
|
+
if (!element) {
|
|
1260
|
+
element = document.createElement("div");
|
|
1261
|
+
element.setAttribute("data-reference-cards", slotKey);
|
|
1262
|
+
element.setAttribute("contenteditable", "false");
|
|
1263
|
+
storage.elements.set(slotKey, element);
|
|
1264
|
+
}
|
|
1265
|
+
return element;
|
|
1266
|
+
}
|
|
1267
|
+
function storageOf(editor) {
|
|
1268
|
+
return editor.storage.referenceCards;
|
|
1269
|
+
}
|
|
1270
|
+
var ReferenceCards = Extension5.create({
|
|
1271
|
+
name: "referenceCards",
|
|
1272
|
+
addStorage() {
|
|
1273
|
+
return { elements: /* @__PURE__ */ new Map() };
|
|
1274
|
+
},
|
|
1275
|
+
addProseMirrorPlugins() {
|
|
1276
|
+
const storage = this.storage;
|
|
1277
|
+
const decorate = (doc) => DecorationSet2.create(
|
|
1278
|
+
doc,
|
|
1279
|
+
referenceCardPlacements(doc).map(
|
|
1280
|
+
(p) => Decoration2.widget(p.pos, () => elementFor(storage, p.key), {
|
|
1281
|
+
key: p.key,
|
|
1282
|
+
side: 1,
|
|
1283
|
+
ignoreSelection: true,
|
|
1284
|
+
// A click on a card is the card's, not a caret placement in the line.
|
|
1285
|
+
stopEvent: () => true
|
|
1286
|
+
})
|
|
1287
|
+
)
|
|
1288
|
+
);
|
|
1289
|
+
return [
|
|
1290
|
+
new Plugin4({
|
|
1291
|
+
key: key2,
|
|
1292
|
+
state: {
|
|
1293
|
+
init: (_, state) => decorate(state.doc),
|
|
1294
|
+
apply: (tr, set) => tr.docChanged ? decorate(tr.doc) : set
|
|
1295
|
+
},
|
|
1296
|
+
props: { decorations: (state) => key2.getState(state) }
|
|
1297
|
+
})
|
|
1298
|
+
];
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
function useReferenceCards(editor) {
|
|
1302
|
+
const [slots, setSlots] = useState3([]);
|
|
1303
|
+
useEffect3(() => {
|
|
1304
|
+
if (!editor) return;
|
|
1305
|
+
const storage = storageOf(editor);
|
|
1306
|
+
if (!storage) return;
|
|
1307
|
+
let last = "";
|
|
1308
|
+
const read = () => {
|
|
1309
|
+
if (editor.isDestroyed) return;
|
|
1310
|
+
const next = referenceCardPlacements(editor.state.doc).map((p) => ({ key: p.key, element: elementFor(storage, p.key), cards: p.cards }));
|
|
1311
|
+
const signature = JSON.stringify(next.map((s) => [s.key, s.cards]));
|
|
1312
|
+
if (signature === last) return;
|
|
1313
|
+
last = signature;
|
|
1314
|
+
const live = new Set(next.map((s) => s.key));
|
|
1315
|
+
for (const k of storage.elements.keys()) if (!live.has(k)) storage.elements.delete(k);
|
|
1316
|
+
setSlots(next);
|
|
1317
|
+
};
|
|
1318
|
+
read();
|
|
1319
|
+
editor.on("transaction", read);
|
|
1320
|
+
return () => {
|
|
1321
|
+
editor.off("transaction", read);
|
|
1322
|
+
};
|
|
1323
|
+
}, [editor]);
|
|
1324
|
+
return slots;
|
|
1325
|
+
}
|
|
1326
|
+
function referenceAtSelection(state) {
|
|
1327
|
+
const { selection } = state;
|
|
1328
|
+
let found = null;
|
|
1329
|
+
if (selection instanceof NodeSelection) {
|
|
1330
|
+
if (selection.node.type.name === "reference") found = { pos: selection.from, node: selection.node };
|
|
1331
|
+
} else if (!selection.empty) {
|
|
1332
|
+
let only = true;
|
|
1333
|
+
state.doc.nodesBetween(selection.from, selection.to, (node2, pos2) => {
|
|
1334
|
+
if (node2.type.name === "reference") {
|
|
1335
|
+
if (found) only = false;
|
|
1336
|
+
found = { pos: pos2, node: node2 };
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
if (node2.isText) {
|
|
1340
|
+
const from = Math.max(selection.from, pos2) - pos2;
|
|
1341
|
+
const to = Math.min(selection.to, pos2 + node2.nodeSize) - pos2;
|
|
1342
|
+
if ((node2.text ?? "").slice(from, to).trim() !== "") only = false;
|
|
1343
|
+
}
|
|
1344
|
+
return true;
|
|
1345
|
+
});
|
|
1346
|
+
if (!only) found = null;
|
|
1347
|
+
}
|
|
1348
|
+
if (!found) return null;
|
|
1349
|
+
const { pos, node } = found;
|
|
1350
|
+
if (!node.attrs.uri) return null;
|
|
1351
|
+
return { pos, uri: String(node.attrs.uri ?? ""), layout: layoutOf(node.attrs.embed) ?? "none" };
|
|
1352
|
+
}
|
|
1353
|
+
function setReferenceCard(editor, layout) {
|
|
1354
|
+
const at = referenceAtSelection(editor.state);
|
|
1355
|
+
return at ? setReferenceCardAt(editor, at.pos, layout) : false;
|
|
1356
|
+
}
|
|
1357
|
+
function setReferenceCardAt(editor, pos, layout) {
|
|
1358
|
+
const node = editor.state.doc.nodeAt(pos);
|
|
1359
|
+
if (!node || node.type.name !== "reference" || !node.attrs.uri) return false;
|
|
1360
|
+
const wasNode = editor.state.selection instanceof NodeSelection && editor.state.selection.from === pos;
|
|
1361
|
+
const tr = editor.state.tr.setNodeMarkup(pos, void 0, { ...node.attrs, embed: layout === "none" ? null : layout });
|
|
1362
|
+
if (wasNode) tr.setSelection(NodeSelection.create(tr.doc, pos));
|
|
1363
|
+
editor.view.dispatch(tr);
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/ReferenceMenu.tsx
|
|
1368
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1369
|
+
var key3 = new PluginKey6("referenceMenu");
|
|
1370
|
+
var LAYOUTS = [
|
|
1371
|
+
{ layout: "none", label: "None" },
|
|
1372
|
+
{ layout: "compact", label: "Compact" },
|
|
1373
|
+
{ layout: "detailed", label: "Detailed" }
|
|
1374
|
+
];
|
|
1375
|
+
function chipTarget(view, pos) {
|
|
1376
|
+
const dom = view.nodeDOM(pos);
|
|
1377
|
+
return dom instanceof HTMLElement ? dom.querySelector("a[href], button") : null;
|
|
1378
|
+
}
|
|
1379
|
+
function webHref(view, pos) {
|
|
1380
|
+
const href = view.state.doc.nodeAt(pos)?.attrs.href;
|
|
1381
|
+
return typeof href === "string" && /^https?:\/\//i.test(href) ? href : "";
|
|
1382
|
+
}
|
|
1383
|
+
function rowsOf(view, pos) {
|
|
1384
|
+
const open = chipTarget(view, pos) || webHref(view, pos) ? ["open"] : [];
|
|
1385
|
+
return view.state.doc.nodeAt(pos)?.attrs.uri ? [...open, ...LAYOUTS.map((card) => card.layout)] : open;
|
|
1386
|
+
}
|
|
1387
|
+
function chipAt(state, pos) {
|
|
1388
|
+
const node = state.doc.nodeAt(pos);
|
|
1389
|
+
return node && node.type.name === "reference" ? node : null;
|
|
1390
|
+
}
|
|
1391
|
+
function justPlaced(tr, state) {
|
|
1392
|
+
if (!tr.docChanged || tr.getMeta("history$") || tr.getMeta("preventUpdate")) return null;
|
|
1393
|
+
const whole = tr.steps.some((step) => step instanceof ReplaceStep && step.from === 0 && step.to === tr.before.content.size);
|
|
1394
|
+
if (whole && tr.getMeta("uiEvent") !== "paste") return null;
|
|
1395
|
+
const made = tr.getMeta(CHIPS_MADE);
|
|
1396
|
+
if (made && !made.pasted) return null;
|
|
1397
|
+
const { selection } = state;
|
|
1398
|
+
if (!selection.empty) return null;
|
|
1399
|
+
let at = selection.from;
|
|
1400
|
+
let before = selection.$from.nodeBefore;
|
|
1401
|
+
if (!made && before?.isText && before.text === " ") {
|
|
1402
|
+
at -= 1;
|
|
1403
|
+
before = state.doc.resolve(at).nodeBefore;
|
|
1404
|
+
}
|
|
1405
|
+
if (!before || before.type.name !== "reference" || !before.attrs.uri) return null;
|
|
1406
|
+
const pos = at - before.nodeSize;
|
|
1407
|
+
const old = tr.mapping.invert().map(pos);
|
|
1408
|
+
const was = tr.before.nodeAt(old);
|
|
1409
|
+
if (was && was.type.name === "reference" && was.attrs.uri === before.attrs.uri && tr.mapping.map(old, 1) === pos) return null;
|
|
1410
|
+
return pos;
|
|
1411
|
+
}
|
|
1412
|
+
function dispatchMenu(view, next) {
|
|
1413
|
+
view.dispatch(view.state.tr.setMeta(key3, next));
|
|
1414
|
+
}
|
|
1415
|
+
var passing = false;
|
|
1416
|
+
function openChip(view, pos) {
|
|
1417
|
+
const target = chipTarget(view, pos);
|
|
1418
|
+
if (target) {
|
|
1419
|
+
passing = true;
|
|
1420
|
+
try {
|
|
1421
|
+
target.click();
|
|
1422
|
+
} finally {
|
|
1423
|
+
passing = false;
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const href = webHref(view, pos);
|
|
1428
|
+
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
|
1429
|
+
}
|
|
1430
|
+
function choose(editor, pos, row) {
|
|
1431
|
+
dispatchMenu(editor.view, null);
|
|
1432
|
+
if (row === "open") openChip(editor.view, pos);
|
|
1433
|
+
else if (row) setReferenceCardAt(editor, pos, row);
|
|
1434
|
+
}
|
|
1435
|
+
function isReferenceMenuOpen(state) {
|
|
1436
|
+
return !!key3.getState(state);
|
|
1437
|
+
}
|
|
1438
|
+
function referenceMenuClick(editor, pos) {
|
|
1439
|
+
if (passing || editor.isDestroyed || !editor.isEditable || key3.getState(editor.state) === void 0) return false;
|
|
1440
|
+
if (!chipAt(editor.state, pos) || rowsOf(editor.view, pos).length === 0) return false;
|
|
1441
|
+
editor.view.dispatch(caretAfter(editor.state, pos).setMeta(key3, { pos, active: -1 }));
|
|
1442
|
+
return true;
|
|
1443
|
+
}
|
|
1444
|
+
function caretAfter(state, pos) {
|
|
1445
|
+
const node = state.doc.nodeAt(pos);
|
|
1446
|
+
return state.tr.setSelection(TextSelection2.create(state.doc, pos + (node?.nodeSize ?? 1)));
|
|
1447
|
+
}
|
|
1448
|
+
var ReferenceMenu = Extension6.create({
|
|
1449
|
+
name: "referenceMenu",
|
|
1450
|
+
// Before the editor's own keys, so Enter on a highlighted row is the row's, not a new line.
|
|
1451
|
+
priority: 1e3,
|
|
1452
|
+
addProseMirrorPlugins() {
|
|
1453
|
+
const editor = this.editor;
|
|
1454
|
+
return [
|
|
1455
|
+
new Plugin5({
|
|
1456
|
+
key: key3,
|
|
1457
|
+
state: {
|
|
1458
|
+
init: () => null,
|
|
1459
|
+
apply: (tr, value, before, after) => {
|
|
1460
|
+
const meta = tr.getMeta(key3);
|
|
1461
|
+
if (meta !== void 0) return meta;
|
|
1462
|
+
const placed = justPlaced(tr, after);
|
|
1463
|
+
if (placed !== null) return { pos: placed, active: -1 };
|
|
1464
|
+
if (!value) return null;
|
|
1465
|
+
const pos = tr.mapping.map(value.pos);
|
|
1466
|
+
if (!chipAt(after, pos)) return null;
|
|
1467
|
+
if (tr.docChanged && !tr.getMeta("appendedTransaction") && tr.getMeta(CHIPS_MADE) === void 0) return null;
|
|
1468
|
+
const onChip = after.selection instanceof NodeSelection2 && after.selection.from === pos;
|
|
1469
|
+
if (tr.selectionSet && !after.selection.eq(before.selection) && !onChip) return null;
|
|
1470
|
+
return pos === value.pos ? value : { ...value, pos };
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
props: {
|
|
1474
|
+
handleDOMEvents: {
|
|
1475
|
+
// Focus gone, the keys no longer reach the menu, and a card chosen now would miss the save on leaving (code review).
|
|
1476
|
+
blur: (view) => {
|
|
1477
|
+
if (key3.getState(view.state)) dispatchMenu(view, null);
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
handleKeyDown: (view, event) => {
|
|
1482
|
+
if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
1483
|
+
const open = key3.getState(view.state);
|
|
1484
|
+
if (!open) {
|
|
1485
|
+
const { selection } = view.state;
|
|
1486
|
+
if (event.key !== "Enter" || !view.editable || !(selection instanceof NodeSelection2) || selection.node.type.name !== "reference") return false;
|
|
1487
|
+
if (rowsOf(view, selection.from).length === 0) return false;
|
|
1488
|
+
view.dispatch(caretAfter(view.state, selection.from).setMeta(key3, { pos: selection.from, active: 0 }));
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
const rows = rowsOf(view, open.pos);
|
|
1492
|
+
if (event.key === "Escape") {
|
|
1493
|
+
dispatchMenu(view, null);
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
if (rows.length === 0) return false;
|
|
1497
|
+
if (event.key === "ArrowDown") {
|
|
1498
|
+
dispatchMenu(view, { ...open, active: (open.active + 1) % rows.length });
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
if (event.key === "ArrowUp") {
|
|
1502
|
+
dispatchMenu(view, { ...open, active: open.active <= 0 ? rows.length - 1 : open.active - 1 });
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
if (event.key === "Enter" && open.active >= 0) {
|
|
1506
|
+
choose(editor, open.pos, rows[open.active]);
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
})
|
|
1513
|
+
];
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
function ReferenceMenuView({ editor }) {
|
|
1517
|
+
const shown = useEditorState({
|
|
1518
|
+
editor,
|
|
1519
|
+
selector: ({ editor: ed }) => {
|
|
1520
|
+
if (!ed || ed.isDestroyed) return null;
|
|
1521
|
+
const open = key3.getState(ed.state);
|
|
1522
|
+
if (!open) return null;
|
|
1523
|
+
const node = chipAt(ed.state, open.pos);
|
|
1524
|
+
if (!node) return null;
|
|
1525
|
+
const embed = node.attrs.embed;
|
|
1526
|
+
const layout = embed === "detailed" ? "detailed" : typeof embed === "string" && embed !== "" && embed !== "none" ? "compact" : "none";
|
|
1527
|
+
return { pos: open.pos, active: open.active, named: !!node.attrs.uri, layout };
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
const keepFocus2 = useCallback3((list) => {
|
|
1531
|
+
const panel = list?.closest('[role="dialog"]');
|
|
1532
|
+
if (!panel) return;
|
|
1533
|
+
const keep = (event) => event.preventDefault();
|
|
1534
|
+
panel.addEventListener("mousedown", keep);
|
|
1535
|
+
return () => panel.removeEventListener("mousedown", keep);
|
|
1536
|
+
}, []);
|
|
1537
|
+
const live = shown && editor && !editor.isDestroyed ? editor : null;
|
|
1538
|
+
const dom = live && shown ? live.view.nodeDOM(shown.pos) : null;
|
|
1539
|
+
const anchor = dom instanceof HTMLElement ? dom : null;
|
|
1540
|
+
const rows = live && shown ? rowsOf(live.view, shown.pos) : [];
|
|
1541
|
+
const opens = rows[0] === "open";
|
|
1542
|
+
const run = (row) => {
|
|
1543
|
+
if (editor && shown) choose(editor, shown.pos, row);
|
|
1544
|
+
};
|
|
1545
|
+
return (
|
|
1546
|
+
// finalFocus={false}: focus never left the text.
|
|
1547
|
+
/* @__PURE__ */ jsx6(
|
|
1548
|
+
Popover2,
|
|
1549
|
+
{
|
|
1550
|
+
anchor,
|
|
1551
|
+
open: !!shown && !!anchor && rows.length > 0,
|
|
1552
|
+
onOpenChange: (next) => {
|
|
1553
|
+
if (!next && editor && !editor.isDestroyed && key3.getState(editor.state)) dispatchMenu(editor.view, null);
|
|
1554
|
+
},
|
|
1555
|
+
side: "bottom",
|
|
1556
|
+
align: "left",
|
|
1557
|
+
ariaLabel: "Link",
|
|
1558
|
+
finalFocus: false,
|
|
1559
|
+
children: shown && /* @__PURE__ */ jsxs5("div", { ref: keepFocus2, role: "menu", "aria-label": "Link", className: "flex flex-col gap-0.5", children: [
|
|
1560
|
+
opens && /* @__PURE__ */ jsx6(
|
|
1561
|
+
MenuItem3,
|
|
1562
|
+
{
|
|
1563
|
+
role: "menuitem",
|
|
1564
|
+
tabIndex: -1,
|
|
1565
|
+
label: "Open",
|
|
1566
|
+
selected: shown.active === 0,
|
|
1567
|
+
onMouseDown: (event) => {
|
|
1568
|
+
event.preventDefault();
|
|
1569
|
+
run("open");
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
),
|
|
1573
|
+
shown.named && /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
1574
|
+
opens && /* @__PURE__ */ jsx6(MenuSeparator2, {}),
|
|
1575
|
+
/* @__PURE__ */ jsx6("div", { role: "group", "aria-label": "Card", children: /* @__PURE__ */ jsx6(MenuSection, { label: "Card", children: LAYOUTS.map(({ layout, label }, i) => /* @__PURE__ */ jsx6(
|
|
1576
|
+
MenuItem3,
|
|
1577
|
+
{
|
|
1578
|
+
role: "menuitemradio",
|
|
1579
|
+
"aria-checked": shown.layout === layout,
|
|
1580
|
+
tabIndex: -1,
|
|
1581
|
+
label,
|
|
1582
|
+
selected: shown.active === i + (opens ? 1 : 0),
|
|
1583
|
+
trailing: shown.layout === layout ? /* @__PURE__ */ jsx6(IconCheck, { size: 16, "aria-hidden": true }) : void 0,
|
|
1584
|
+
onMouseDown: (event) => {
|
|
1585
|
+
event.preventDefault();
|
|
1586
|
+
run(layout);
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
layout
|
|
1590
|
+
)) }) })
|
|
1591
|
+
] })
|
|
1592
|
+
] })
|
|
1593
|
+
}
|
|
1594
|
+
)
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/BlockNodes.tsx
|
|
1599
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
924
1600
|
var BLOCK_TYPES = [
|
|
925
1601
|
"paragraph",
|
|
926
1602
|
"heading",
|
|
@@ -1078,7 +1754,7 @@ function bringsInBlocks(transactions) {
|
|
|
1078
1754
|
})
|
|
1079
1755
|
);
|
|
1080
1756
|
}
|
|
1081
|
-
var BlockId =
|
|
1757
|
+
var BlockId = Extension7.create({
|
|
1082
1758
|
name: "blockId",
|
|
1083
1759
|
onCreate() {
|
|
1084
1760
|
const { state, view } = this.editor;
|
|
@@ -1090,8 +1766,8 @@ var BlockId = Extension4.create({
|
|
|
1090
1766
|
},
|
|
1091
1767
|
addProseMirrorPlugins() {
|
|
1092
1768
|
return [
|
|
1093
|
-
new
|
|
1094
|
-
key: new
|
|
1769
|
+
new Plugin6({
|
|
1770
|
+
key: new PluginKey7("blockIdSplit"),
|
|
1095
1771
|
appendTransaction: (transactions, old, state) => {
|
|
1096
1772
|
if (!bringsInBlocks(transactions)) return null;
|
|
1097
1773
|
const mapping = new Mapping();
|
|
@@ -1125,6 +1801,7 @@ var BlockId = Extension4.create({
|
|
|
1125
1801
|
];
|
|
1126
1802
|
}
|
|
1127
1803
|
});
|
|
1804
|
+
var webUrl = (raw) => raw && /^https?:\/\/\S+$/i.test(raw) ? raw : null;
|
|
1128
1805
|
var ReferenceNode = Node2.create({
|
|
1129
1806
|
name: "reference",
|
|
1130
1807
|
group: "inline",
|
|
@@ -1137,20 +1814,48 @@ var ReferenceNode = Node2.create({
|
|
|
1137
1814
|
},
|
|
1138
1815
|
addAttributes() {
|
|
1139
1816
|
return {
|
|
1140
|
-
|
|
1141
|
-
|
|
1817
|
+
// A pasted URL chip keeps only its URL: what it names is the editor's to resolve (`LinkChips`), never
|
|
1818
|
+
// the clipboard's, which could show one object while saving another URL (security review).
|
|
1819
|
+
uri: { default: "", parseHTML: (element) => webUrl(element.getAttribute("data-href")) ? "" : element.getAttribute("data-reference") ?? "" },
|
|
1820
|
+
marks: { default: [] },
|
|
1821
|
+
embed: { default: null, parseHTML: (element) => element.getAttribute("data-embed") },
|
|
1822
|
+
extra: { default: null, parseHTML: () => null },
|
|
1823
|
+
// An http(s) URL only: `LinkChips` makes no other, and a pasted `javascript:` must not reach a saved link.
|
|
1824
|
+
href: { default: null, parseHTML: (element) => webUrl(element.getAttribute("data-href")) }
|
|
1142
1825
|
};
|
|
1143
1826
|
},
|
|
1144
1827
|
parseHTML() {
|
|
1145
1828
|
return [{ tag: "span[data-reference]" }];
|
|
1146
1829
|
},
|
|
1147
1830
|
renderHTML({ node }) {
|
|
1148
|
-
|
|
1831
|
+
const embed = typeof node.attrs.embed === "string" ? { "data-embed": node.attrs.embed } : {};
|
|
1832
|
+
const href = typeof node.attrs.href === "string" && node.attrs.href ? { "data-href": node.attrs.href } : {};
|
|
1833
|
+
return ["span", { "data-reference": String(node.attrs.uri), ...embed, ...href }, String(node.attrs.href || node.attrs.uri)];
|
|
1834
|
+
},
|
|
1835
|
+
renderText({ node }) {
|
|
1836
|
+
return String(node.attrs.href || node.attrs.uri);
|
|
1149
1837
|
},
|
|
1150
1838
|
addNodeView() {
|
|
1151
1839
|
const View = this.options.view;
|
|
1152
1840
|
if (!View) return null;
|
|
1153
|
-
return ReactNodeViewRenderer2(({ node
|
|
1841
|
+
return ReactNodeViewRenderer2(({ node, editor, getPos }) => {
|
|
1842
|
+
const uri = String(node.attrs.uri ?? "");
|
|
1843
|
+
const href = typeof node.attrs.href === "string" && node.attrs.href ? node.attrs.href : void 0;
|
|
1844
|
+
return /* @__PURE__ */ jsx7(
|
|
1845
|
+
NodeViewWrapper2,
|
|
1846
|
+
{
|
|
1847
|
+
as: "span",
|
|
1848
|
+
className: "inline",
|
|
1849
|
+
onClickCapture: (event) => {
|
|
1850
|
+
const pos = getPos();
|
|
1851
|
+
if (typeof pos !== "number" || !referenceMenuClick(editor, pos)) return;
|
|
1852
|
+
event.preventDefault();
|
|
1853
|
+
event.stopPropagation();
|
|
1854
|
+
},
|
|
1855
|
+
children: uri ? /* @__PURE__ */ jsx7(View, { uri, ...href ? { href } : {} }) : /* @__PURE__ */ jsx7("span", { children: href })
|
|
1856
|
+
}
|
|
1857
|
+
);
|
|
1858
|
+
});
|
|
1154
1859
|
}
|
|
1155
1860
|
});
|
|
1156
1861
|
var UnknownBlock = Node2.create({
|
|
@@ -1178,7 +1883,7 @@ var UnknownBlock = Node2.create({
|
|
|
1178
1883
|
function UnknownBlockView({ node }) {
|
|
1179
1884
|
const source = node.attrs.source;
|
|
1180
1885
|
const text = String(node.attrs.text ?? "");
|
|
1181
|
-
return /* @__PURE__ */
|
|
1886
|
+
return /* @__PURE__ */ jsx7(
|
|
1182
1887
|
NodeViewWrapper2,
|
|
1183
1888
|
{
|
|
1184
1889
|
"data-block-id": source?.id,
|
|
@@ -1218,7 +1923,7 @@ var AttachmentNode = Node2.create({
|
|
|
1218
1923
|
addNodeView() {
|
|
1219
1924
|
const View = this.options.view;
|
|
1220
1925
|
if (!View) return null;
|
|
1221
|
-
return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */
|
|
1926
|
+
return ReactNodeViewRenderer2(({ node }) => /* @__PURE__ */ jsx7(NodeViewWrapper2, { className: "my-2", children: /* @__PURE__ */ jsx7(View, { attrs: node.attrs }) }));
|
|
1222
1927
|
}
|
|
1223
1928
|
});
|
|
1224
1929
|
var TICK = "checklistTick";
|
|
@@ -1236,7 +1941,7 @@ function itemAtCaret(state) {
|
|
|
1236
1941
|
if (item.type.name !== "listItem" || $from.index(-1) !== 0) return null;
|
|
1237
1942
|
return { node: item, pos: $from.before(-1) };
|
|
1238
1943
|
}
|
|
1239
|
-
var sameAttrs = (a, b) => Object.keys({ ...a.attrs, ...b.attrs }).every((
|
|
1944
|
+
var sameAttrs = (a, b) => Object.keys({ ...a.attrs, ...b.attrs }).every((key4) => a.attrs[key4] === b.attrs[key4]);
|
|
1240
1945
|
function listItemView(editor, node, view, getPos) {
|
|
1241
1946
|
const toDOM = node.type.spec.toDOM;
|
|
1242
1947
|
const { dom, contentDOM } = toDOM ? DOMSerializer.renderSpec(document, toDOM(node)) : { dom: document.createElement("li"), contentDOM: void 0 };
|
|
@@ -1287,7 +1992,7 @@ function listItemView(editor, node, view, getPos) {
|
|
|
1287
1992
|
}
|
|
1288
1993
|
};
|
|
1289
1994
|
}
|
|
1290
|
-
var Checklist =
|
|
1995
|
+
var Checklist = Extension7.create({
|
|
1291
1996
|
name: "checklist",
|
|
1292
1997
|
// Before the list item's own Enter and Backspace.
|
|
1293
1998
|
priority: 101,
|
|
@@ -1352,14 +2057,14 @@ var Checklist = Extension4.create({
|
|
|
1352
2057
|
addProseMirrorPlugins() {
|
|
1353
2058
|
const editor = this.editor;
|
|
1354
2059
|
return [
|
|
1355
|
-
new
|
|
1356
|
-
key: new
|
|
2060
|
+
new Plugin6({
|
|
2061
|
+
key: new PluginKey7("checklist"),
|
|
1357
2062
|
props: { nodeViews: { listItem: (node, view, getPos) => listItemView(editor, node, view, getPos) } }
|
|
1358
2063
|
})
|
|
1359
2064
|
];
|
|
1360
2065
|
}
|
|
1361
2066
|
});
|
|
1362
|
-
var Callout =
|
|
2067
|
+
var Callout = Extension7.create({
|
|
1363
2068
|
name: "callout",
|
|
1364
2069
|
addGlobalAttributes() {
|
|
1365
2070
|
return [
|
|
@@ -1385,25 +2090,36 @@ export {
|
|
|
1385
2090
|
Checklist,
|
|
1386
2091
|
CodeBlockColours,
|
|
1387
2092
|
EditorBlockHandle,
|
|
2093
|
+
EditorTableControls,
|
|
1388
2094
|
FORMATS,
|
|
1389
2095
|
KeptSelection,
|
|
2096
|
+
LinkChips,
|
|
1390
2097
|
PastedImages,
|
|
1391
2098
|
PersonMention,
|
|
2099
|
+
ReferenceCards,
|
|
2100
|
+
ReferenceMenu,
|
|
2101
|
+
ReferenceMenuView,
|
|
1392
2102
|
ReferenceNode,
|
|
1393
2103
|
ReferenceTrigger,
|
|
1394
2104
|
SelectionToolbar,
|
|
1395
2105
|
SlashCommands,
|
|
1396
2106
|
UnknownBlock,
|
|
1397
2107
|
UrgentPersonMention,
|
|
2108
|
+
appendTableColumn,
|
|
2109
|
+
appendTableRow,
|
|
1398
2110
|
applyFormat,
|
|
1399
2111
|
blockDropCursor,
|
|
1400
2112
|
deleteBlock,
|
|
2113
|
+
deleteTableColumn,
|
|
2114
|
+
deleteTableRow,
|
|
1401
2115
|
duplicateBlock,
|
|
1402
2116
|
filterPeople,
|
|
1403
2117
|
filterReferences,
|
|
1404
2118
|
filterSlashSections,
|
|
2119
|
+
findLinkChips,
|
|
1405
2120
|
formatSection,
|
|
1406
2121
|
isChecklistTick,
|
|
2122
|
+
isReferenceMenuOpen,
|
|
1407
2123
|
isSuggestionActive,
|
|
1408
2124
|
isSuggestionOpen,
|
|
1409
2125
|
moveBlock,
|
|
@@ -1411,10 +2127,17 @@ export {
|
|
|
1411
2127
|
pastedImageName,
|
|
1412
2128
|
pastedImages,
|
|
1413
2129
|
peopleMenu,
|
|
2130
|
+
referenceAtSelection,
|
|
2131
|
+
referenceCardPlacements,
|
|
1414
2132
|
referenceMenu,
|
|
2133
|
+
referenceMenuClick,
|
|
2134
|
+
refreshLinkChips,
|
|
2135
|
+
setReferenceCard,
|
|
2136
|
+
setReferenceCardAt,
|
|
1415
2137
|
startBlockDrag,
|
|
1416
2138
|
suggestionPopup,
|
|
1417
2139
|
typeTrigger,
|
|
1418
|
-
urgentPubkeys
|
|
2140
|
+
urgentPubkeys,
|
|
2141
|
+
useReferenceCards
|
|
1419
2142
|
};
|
|
1420
2143
|
//# sourceMappingURL=editor.js.map
|