@threadlabs/looma 0.31.0 → 0.31.2

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.
Files changed (36) hide show
  1. package/components/shared/icons.js +1 -0
  2. package/components/shared/tooltip.js +7 -0
  3. package/components/ui-editor-footnote-editor/ui-editor-footnote-editor.html +57 -0
  4. package/components/ui-editor-footnote-editor/ui-editor-footnote-editor.js +38 -0
  5. package/components/ui-icon/ui-icon.html +1 -0
  6. package/components/ui-tooltip/ui-tooltip.js +4 -1
  7. package/dist/index.js +37 -36
  8. package/editor/chunks/extensions.js +363 -20
  9. package/editor/expressmark.js +1 -1
  10. package/editor/extensions/footnote.d.ts +40 -7
  11. package/editor/extensions/index.d.ts +1 -1
  12. package/editor/extensions/index.js +2 -2
  13. package/editor/footnote-editing.d.ts +46 -0
  14. package/editor/icons.d.ts +1 -0
  15. package/editor/index.js +4 -3
  16. package/editor/table-ui.d.ts +2 -11
  17. package/editor/toolbar-tooltip.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/styles/ui-editor-footnote-editor.css +1 -0
  20. package/vanilla/UiEditorFootnoteEditor.d.ts +30 -0
  21. package/vanilla/UiEditorFootnoteEditor.js +101 -0
  22. package/vanilla/UiIcon.js +1 -1
  23. package/vanilla/index.js +1 -0
  24. package/vue/UiEditorFootnoteEditor.d.ts +57 -0
  25. package/vue/UiEditorFootnoteEditor.js +2 -0
  26. package/vue/UiEditorFootnoteEditor.vue +304 -0
  27. package/vue/UiIcon.vue +4 -0
  28. package/vue/chunks/UiEditorFootnoteEditor.js +306 -0
  29. package/vue/chunks/UiEditorImageToolbar.js +1 -1
  30. package/vue/chunks/UiEditorLinkForm.js +1 -1
  31. package/vue/chunks/UiIcon.js +15 -1
  32. package/vue/components.css +590 -575
  33. package/vue/editor/index.js +643 -45
  34. package/vue/editor/primitives.d.ts +3 -1
  35. package/vue/index.d.ts +1 -0
  36. package/vue/index.js +5 -4
@@ -1,16 +1,16 @@
1
- import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, onUpdated, reactive, ref, shallowRef, useId, watch } from "vue";
1
+ import { Teleport, computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, onUpdated, reactive, ref, shallowRef, useId, watch } from "vue";
2
2
  import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeTableOfContentsConfig } from "@threadlabs/looma/editor";
3
- import { Button, Card, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Popover, Stack, StatusMessage, Text, Tooltip } from "@threadlabs/looma/vue";
4
- import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
3
+ import { Button, Card, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorFootnoteEditor, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Popover, Stack, StatusMessage, Text, Tooltip } from "@threadlabs/looma/vue";
4
+ import { Editor, Extension, InputRule, Node as Node$1, PasteRule, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
5
5
  import { Editor as Editor$1 } from "@tiptap/vue-3";
6
- import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
6
+ import { AllSelection, NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
7
7
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
8
  import { createLowlight } from "lowlight";
9
9
  import { RequestEvent, componentRegistry } from "@nextwebwg/html-next/runtime";
10
10
  import "@tiptap/pm/tables";
11
+ import { DOMParser, DOMSerializer, Fragment as Fragment$1, Slice } from "@tiptap/pm/model";
11
12
  import "@tiptap/pm/transform";
12
13
  import { closeHistory } from "@tiptap/pm/history";
13
- import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
14
14
  import { IndexedList, NONCONFORMING, READY_INPUT, UNRESOLVED_INPUT, acceptProps, activateModule, armElement, attachGeneratedController, awaitInput, buildTemplate, checkBoolean, checkConstrained, checkFormat, checkInteger, checkNumber, checkObject, checkSelected, checkString, clearRegion, computedCycle, defineComponent as defineComponent$1, detailCheck, dispatchDeclared, dispose, evaluateInput, fillSlot, formatOf, generatedInputs, hostState, inputRead, keywordFormat, manageProps, notifyPropertySet, project, readIndex, readMember, readonlyView, readsChanged, rec, replaceRoot, textCall, toAttribute, toText, trackContainer, truthy, writeAttribute, writeUrlAttribute } from "@nextwebwg/html-next/generated-runtime";
15
15
  export * from "@threadlabs/looma/editor";
16
16
  //#region \0rolldown/runtime.js
@@ -43,7 +43,8 @@ var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
43
43
  EditorLinkForm: "ui-editor-link-form",
44
44
  EditorBlockActions: "ui-editor-block-actions",
45
45
  EditorCodeLanguage: "ui-editor-code-language",
46
- EditorChipEditor: "ui-editor-chip-editor"
46
+ EditorChipEditor: "ui-editor-chip-editor",
47
+ EditorFootnoteEditor: "ui-editor-footnote-editor"
47
48
  };
48
49
  //#endregion
49
50
  //#region src/components/shared/overlay.js
@@ -1678,6 +1679,528 @@ function createChipEditing(options) {
1678
1679
  };
1679
1680
  }
1680
1681
  //#endregion
1682
+ //#region src/editor/extensions/footnote.ts
1683
+ var labelFrom = (href, prefix) => href?.startsWith(`#${prefix}`) ? href.slice(prefix.length + 1) : null;
1684
+ /** A footnote label as Looma Markdown reads it in `[^label]`: no whitespace and no `]`. */
1685
+ var LOOMA_FOOTNOTE_LABEL = /^[^\s\]]+$/;
1686
+ var REFERENCE = "loomaFootnoteReference";
1687
+ var NOTES = "loomaFootnotes";
1688
+ var NOTE = "loomaFootnote";
1689
+ var isLabel = (label) => LOOMA_FOOTNOTE_LABEL.test(label);
1690
+ /** Every label a reference or note in `doc` uses. */
1691
+ function usedLabels(doc) {
1692
+ const labels = /* @__PURE__ */ new Set();
1693
+ doc.descendants((node) => {
1694
+ if (node.type.name === REFERENCE || node.type.name === NOTE) labels.add(String(node.attrs.label));
1695
+ });
1696
+ return labels;
1697
+ }
1698
+ /**
1699
+ * The first note for `label` in `doc` and its position.
1700
+ * @failure Returns null when no note has that label.
1701
+ */
1702
+ function findLoomaFootnote(doc, label) {
1703
+ let found = null;
1704
+ doc.descendants((node, position) => {
1705
+ if (found) return false;
1706
+ if (node.type.name === NOTE && node.attrs.label === label) {
1707
+ found = {
1708
+ node,
1709
+ position
1710
+ };
1711
+ return false;
1712
+ }
1713
+ });
1714
+ return found;
1715
+ }
1716
+ function mapReferences(node, change) {
1717
+ if (node.type.name === REFERENCE) return change(node);
1718
+ if (node.isLeaf) return node;
1719
+ const children = [];
1720
+ node.content.forEach((child) => children.push(mapReferences(child, change)));
1721
+ return node.copy(Fragment$1.from(children));
1722
+ }
1723
+ function noteAt(doc, position) {
1724
+ const $position = doc.resolve(position);
1725
+ for (let depth = $position.depth; depth > 0; depth--) {
1726
+ const node = $position.node(depth);
1727
+ if (node.type.name === NOTE) return {
1728
+ node,
1729
+ start: $position.before(depth)
1730
+ };
1731
+ }
1732
+ return null;
1733
+ }
1734
+ /**
1735
+ * Keeps footnotes on Looma Markdown's rule, so the saved HTML is the HTML Looma Markdown renders for
1736
+ * the same document. Notes are numbered by first reference, in the text and then in the notes, and
1737
+ * every reference carries its note's number and its place among that note's references. The notes
1738
+ * live in one `loomaFootnotes` section at the end of the document, in number order. A note no
1739
+ * reference points to leaves the document; a reference with no note gets one back, the note it last
1740
+ * had in this editor (so cutting a reference and pasting it keeps its note) or an empty one.
1741
+ * @invariant Changing nothing but text inside a note never rebuilds the section.
1742
+ */
1743
+ function footnoteTransaction(state, known) {
1744
+ const { doc, schema } = state;
1745
+ const sections = [];
1746
+ const notes = /* @__PURE__ */ new Map();
1747
+ const body = [];
1748
+ doc.descendants((node, position) => {
1749
+ if (node.type.name === NOTES) {
1750
+ sections.push({
1751
+ node,
1752
+ position
1753
+ });
1754
+ node.forEach((note) => {
1755
+ if (!notes.has(note.attrs.label)) notes.set(note.attrs.label, note);
1756
+ });
1757
+ return false;
1758
+ }
1759
+ if (node.type.name === REFERENCE) body.push({
1760
+ node,
1761
+ position
1762
+ });
1763
+ });
1764
+ if (!sections.length && !body.length) return null;
1765
+ const order = [];
1766
+ const counts = /* @__PURE__ */ new Map();
1767
+ const numbered = (reference) => {
1768
+ const label = String(reference.attrs.label);
1769
+ if (!counts.has(label)) order.push(label);
1770
+ counts.set(label, (counts.get(label) ?? 0) + 1);
1771
+ const attrs = {
1772
+ ...reference.attrs,
1773
+ number: order.indexOf(label) + 1,
1774
+ occurrence: counts.get(label)
1775
+ };
1776
+ return attrs.number === reference.attrs.number && attrs.occurrence === reference.attrs.occurrence ? reference : reference.type.create(attrs, null, reference.marks);
1777
+ };
1778
+ const transaction = state.tr;
1779
+ for (const { node, position } of body) {
1780
+ const next = numbered(node);
1781
+ if (next !== node) transaction.setNodeMarkup(position, void 0, next.attrs);
1782
+ }
1783
+ const built = [];
1784
+ for (let index = 0; index < order.length; index++) {
1785
+ const label = order[index];
1786
+ const note = notes.get(label) ?? known.get(label) ?? schema.nodes[NOTE].create({ label }, schema.nodes.paragraph.create());
1787
+ built.push(mapReferences(note, numbered));
1788
+ }
1789
+ const wanted = built.map((note) => note.attrs.count === counts.get(note.attrs.label) ? note : note.type.create({
1790
+ ...note.attrs,
1791
+ count: counts.get(note.attrs.label)
1792
+ }, note.content));
1793
+ for (const note of [...notes.values(), ...wanted]) known.set(note.attrs.label, note);
1794
+ const section = wanted.length ? schema.nodes[NOTES].create(null, wanted) : null;
1795
+ const last = doc.lastChild?.type.name === NOTES ? {
1796
+ node: doc.lastChild,
1797
+ position: doc.content.size - doc.lastChild.nodeSize
1798
+ } : null;
1799
+ if (!(section && last && sections.length === 1 && last.node.eq(section)) && (sections.length || section)) {
1800
+ const { selection } = state;
1801
+ const inside = noteAt(doc, selection.from);
1802
+ const keep = inside && selection.to <= inside.start + inside.node.nodeSize ? {
1803
+ label: inside.node.attrs.label,
1804
+ start: inside.start
1805
+ } : null;
1806
+ for (const { node, position } of [...sections].reverse()) if (node !== last?.node) transaction.delete(position, position + node.nodeSize);
1807
+ if (last) {
1808
+ const position = transaction.mapping.map(last.position);
1809
+ if (section) transaction.replaceWith(position, position + last.node.nodeSize, section);
1810
+ else transaction.delete(position, position + last.node.nodeSize);
1811
+ } else if (section) transaction.insert(transaction.doc.content.size, section);
1812
+ const moved = keep && findLoomaFootnote(transaction.doc, keep.label);
1813
+ if (keep && moved) {
1814
+ const json = selection.toJSON();
1815
+ json.anchor += moved.position - keep.start;
1816
+ if (json.head !== void 0) json.head += moved.position - keep.start;
1817
+ try {
1818
+ transaction.setSelection(Selection.fromJSON(transaction.doc, json));
1819
+ } catch {}
1820
+ }
1821
+ }
1822
+ return transaction.docChanged ? transaction : null;
1823
+ }
1824
+ function appendBackLinks(item, label, number, count) {
1825
+ const document = item.ownerDocument;
1826
+ let paragraph = item.lastElementChild;
1827
+ if (paragraph?.nodeName !== "P") item.append(paragraph = document.createElement("p"));
1828
+ for (let occurrence = 1; occurrence <= count; occurrence++) {
1829
+ const link = document.createElement("a");
1830
+ link.setAttribute("href", `#fnref:${label}${occurrence > 1 ? `:${occurrence}` : ""}`);
1831
+ link.setAttribute("data-footnote-backref", "");
1832
+ link.setAttribute("aria-label", `Back to reference ${number}${occurrence > 1 ? `-${occurrence}` : ""}`);
1833
+ link.append("↩");
1834
+ if (occurrence > 1) link.append(Object.assign(document.createElement("sup"), { textContent: String(occurrence) }));
1835
+ paragraph.append(" ", link);
1836
+ }
1837
+ }
1838
+ var FootnoteSerializer = class extends DOMSerializer {
1839
+ serializeFragment(fragment, options = {}, target) {
1840
+ const dom = super.serializeFragment(fragment, options, target);
1841
+ fragment.forEach((node, _offset, index) => {
1842
+ if (node.type.name === NOTE) appendBackLinks(dom.childNodes[index], String(node.attrs.label), index + 1, Number(node.attrs.count) || 1);
1843
+ });
1844
+ return dom;
1845
+ }
1846
+ };
1847
+ function installSerializer(schema) {
1848
+ if (!(schema.cached.domSerializer instanceof FootnoteSerializer)) schema.cached.domSerializer = new FootnoteSerializer(DOMSerializer.nodesFromSchema(schema), DOMSerializer.marksFromSchema(schema));
1849
+ }
1850
+ Node$1.create({
1851
+ name: REFERENCE,
1852
+ group: "inline",
1853
+ inline: true,
1854
+ atom: true,
1855
+ selectable: true,
1856
+ addAttributes() {
1857
+ const link = (element) => element.querySelector(":scope > a[data-footnote-ref]");
1858
+ const label = (element) => labelFrom(link(element)?.getAttribute("href"), "fn:") ?? "";
1859
+ return {
1860
+ label: {
1861
+ default: "",
1862
+ parseHTML: label,
1863
+ renderHTML: () => ({})
1864
+ },
1865
+ number: {
1866
+ default: null,
1867
+ parseHTML: (element) => Number(element.textContent) || null,
1868
+ renderHTML: () => ({})
1869
+ },
1870
+ occurrence: {
1871
+ default: 1,
1872
+ parseHTML: (element) => Number(link(element)?.id.slice(`fnref:${label(element)}:`.length)) || 1,
1873
+ renderHTML: () => ({})
1874
+ }
1875
+ };
1876
+ },
1877
+ parseHTML() {
1878
+ return [{
1879
+ tag: "sup",
1880
+ priority: 1100,
1881
+ getAttrs: (element) => element.querySelector(":scope > a[data-footnote-ref]") ? null : false
1882
+ }];
1883
+ },
1884
+ renderHTML({ node }) {
1885
+ const { label, number, occurrence } = node.attrs;
1886
+ return ["sup", [
1887
+ "a",
1888
+ {
1889
+ href: `#fn:${label}`,
1890
+ id: `fnref:${label}${occurrence > 1 ? `:${occurrence}` : ""}`,
1891
+ "data-footnote-ref": "",
1892
+ "aria-describedby": "footnote:label"
1893
+ },
1894
+ String(number ?? label)
1895
+ ]];
1896
+ },
1897
+ renderText({ node }) {
1898
+ return `[^${node.attrs.label}]`;
1899
+ },
1900
+ addInputRules() {
1901
+ return [new InputRule({
1902
+ find: /\[\^([^\s\]]+)\]$/,
1903
+ handler: ({ state, range, match }) => {
1904
+ state.tr.replaceWith(range.from, range.to, this.type.create({ label: match[1] }));
1905
+ }
1906
+ })];
1907
+ },
1908
+ addPasteRules() {
1909
+ return [new PasteRule({
1910
+ find: /\[\^([^\s\]]+)\]/g,
1911
+ handler: ({ state, range, match }) => {
1912
+ state.tr.replaceWith(range.from, range.to, this.type.create({ label: match[1] }));
1913
+ }
1914
+ })];
1915
+ },
1916
+ addNodeView() {
1917
+ return ({ node, editor }) => {
1918
+ const dom = document.createElement("sup");
1919
+ dom.dataset.loomaFootnoteRef = "";
1920
+ dom.contentEditable = "false";
1921
+ dom.setAttribute("role", "button");
1922
+ dom.setAttribute("aria-haspopup", "dialog");
1923
+ dom.setAttribute("aria-disabled", String(!editor.isEditable));
1924
+ dom.tabIndex = 0;
1925
+ const number = dom.appendChild(document.createElement("a"));
1926
+ const update = (next) => {
1927
+ if (next.type !== node.type) return false;
1928
+ const shown = String(next.attrs.number ?? next.attrs.label);
1929
+ dom.dataset.label = String(next.attrs.label);
1930
+ if (number.textContent !== shown) number.textContent = shown;
1931
+ dom.setAttribute("aria-label", `Footnote ${shown}`);
1932
+ return true;
1933
+ };
1934
+ update(node);
1935
+ return {
1936
+ dom,
1937
+ update,
1938
+ stopEvent: (event) => event.type === "click" || event.type === "keydown"
1939
+ };
1940
+ };
1941
+ }
1942
+ });
1943
+ Node$1.create({
1944
+ name: NOTES,
1945
+ group: "block",
1946
+ content: `${NOTE}+`,
1947
+ isolating: true,
1948
+ parseHTML() {
1949
+ return [{
1950
+ tag: "section[data-footnotes]",
1951
+ contentElement: "ol"
1952
+ }];
1953
+ },
1954
+ renderHTML() {
1955
+ return [
1956
+ "section",
1957
+ {
1958
+ "data-footnotes": "",
1959
+ class: "footnotes"
1960
+ },
1961
+ [
1962
+ "h2",
1963
+ {
1964
+ id: "footnote:label",
1965
+ class: "sr-only"
1966
+ },
1967
+ "Footnotes"
1968
+ ],
1969
+ ["ol", 0]
1970
+ ];
1971
+ },
1972
+ onBeforeCreate() {
1973
+ installSerializer(this.editor.schema);
1974
+ },
1975
+ onCreate() {
1976
+ const transaction = footnoteTransaction(this.editor.state, this.storage.known);
1977
+ if (transaction) this.editor.view.dispatch(transaction.setMeta("addToHistory", false));
1978
+ },
1979
+ addStorage: () => ({ known: /* @__PURE__ */ new Map() }),
1980
+ addProseMirrorPlugins() {
1981
+ return [new Plugin({
1982
+ appendTransaction: (transactions, _oldState, state) => transactions.some((transaction) => transaction.docChanged) ? footnoteTransaction(state, this.storage.known) : null,
1983
+ props: { transformPasted: (slice) => {
1984
+ const kept = [];
1985
+ slice.content.forEach((node) => {
1986
+ if (node.type.name === NOTES) node.forEach((note) => {
1987
+ this.storage.known.set(note.attrs.label, note);
1988
+ });
1989
+ else kept.push(node);
1990
+ });
1991
+ if (!kept.length || kept.length === slice.content.childCount) return slice;
1992
+ const content = Fragment$1.from(kept);
1993
+ const open = Slice.maxOpen(content);
1994
+ return new Slice(content, kept[0] === slice.content.firstChild ? slice.openStart : open.openStart, kept.at(-1) === slice.content.lastChild ? slice.openEnd : open.openEnd);
1995
+ } }
1996
+ })];
1997
+ },
1998
+ addCommands() {
1999
+ return {
2000
+ insertLoomaFootnote: (label) => ({ state, tr, dispatch }) => {
2001
+ const name = label ?? (() => {
2002
+ const used = usedLabels(state.doc);
2003
+ let next = 1;
2004
+ while (used.has(String(next))) next++;
2005
+ return String(next);
2006
+ })();
2007
+ const { $from, $to } = state.selection;
2008
+ if (!isLabel(name) || !$from.parent.canReplaceWith($from.index(), $to.index(), state.schema.nodes[REFERENCE])) return false;
2009
+ if (!dispatch) return true;
2010
+ tr.replaceSelectionWith(state.schema.nodes[REFERENCE].create({ label: name }));
2011
+ if (!findLoomaFootnote(tr.doc, name)) {
2012
+ const note = state.schema.nodes[NOTE].create({ label: name }, state.schema.nodes.paragraph.create());
2013
+ if (tr.doc.lastChild?.type.name === NOTES) tr.insert(tr.doc.content.size - 1, note);
2014
+ else tr.insert(tr.doc.content.size, state.schema.nodes[NOTES].create(null, note));
2015
+ tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size - 3));
2016
+ }
2017
+ tr.scrollIntoView();
2018
+ return true;
2019
+ },
2020
+ renameLoomaFootnote: (from, to) => ({ state, tr, dispatch }) => {
2021
+ if (!isLabel(to) || usedLabels(state.doc).has(to) || !findLoomaFootnote(state.doc, from)) return false;
2022
+ if (dispatch) state.doc.descendants((node, position) => {
2023
+ if ((node.type.name === REFERENCE || node.type.name === NOTE) && node.attrs.label === from) tr.setNodeMarkup(position, void 0, {
2024
+ ...node.attrs,
2025
+ label: to
2026
+ });
2027
+ });
2028
+ return true;
2029
+ },
2030
+ goToLoomaFootnote: () => ({ state, tr, dispatch }) => {
2031
+ const { selection } = state;
2032
+ const reference = selection instanceof NodeSelection ? selection.node : selection.empty ? selection.$from.nodeBefore : null;
2033
+ if (reference?.type.name === REFERENCE) {
2034
+ const note = findLoomaFootnote(state.doc, reference.attrs.label);
2035
+ if (!note) return false;
2036
+ if (dispatch) tr.setSelection(Selection.near(state.doc.resolve(note.position + note.node.nodeSize - 1), -1)).scrollIntoView();
2037
+ return true;
2038
+ }
2039
+ const note = noteAt(state.doc, selection.from);
2040
+ let first = -1;
2041
+ if (note) state.doc.descendants((node, position) => {
2042
+ if (first >= 0) return false;
2043
+ if (node.type.name === REFERENCE && node.attrs.label === note.node.attrs.label) first = position;
2044
+ });
2045
+ if (first < 0) return false;
2046
+ if (dispatch) tr.setSelection(TextSelection.create(state.doc, first + 1)).scrollIntoView();
2047
+ return true;
2048
+ }
2049
+ };
2050
+ },
2051
+ addKeyboardShortcuts() {
2052
+ return {
2053
+ "Mod-Alt-f": () => this.editor.commands.insertLoomaFootnote(),
2054
+ "Mod-Alt-n": () => this.editor.commands.goToLoomaFootnote()
2055
+ };
2056
+ }
2057
+ });
2058
+ Node$1.create({
2059
+ name: NOTE,
2060
+ content: "block+",
2061
+ defining: true,
2062
+ addAttributes() {
2063
+ return {
2064
+ label: {
2065
+ default: "",
2066
+ parseHTML: (element) => labelFrom(`#${element.id}`, "fn:") ?? "",
2067
+ renderHTML: () => ({})
2068
+ },
2069
+ count: {
2070
+ default: 1,
2071
+ parseHTML: (element) => element.querySelectorAll("a[data-footnote-backref]").length || 1,
2072
+ renderHTML: () => ({})
2073
+ }
2074
+ };
2075
+ },
2076
+ parseHTML() {
2077
+ return [{
2078
+ tag: "li",
2079
+ priority: 1100,
2080
+ context: `${NOTES}/`
2081
+ }, {
2082
+ tag: "a[data-footnote-backref]",
2083
+ priority: 1100,
2084
+ ignore: true
2085
+ }];
2086
+ },
2087
+ renderHTML({ node }) {
2088
+ return [
2089
+ "li",
2090
+ { id: `fn:${node.attrs.label}` },
2091
+ 0
2092
+ ];
2093
+ }
2094
+ });
2095
+ //#endregion
2096
+ //#region src/editor/footnote-editing.ts
2097
+ var LOOMA_FOOTNOTE_EDITING_IDLE = Object.freeze({
2098
+ open: false,
2099
+ label: "",
2100
+ note: "",
2101
+ editable: false,
2102
+ error: ""
2103
+ });
2104
+ var plain = (note) => {
2105
+ const paragraph = note.firstChild;
2106
+ if (note.childCount !== 1 || paragraph?.type.name !== "paragraph") return false;
2107
+ let text = true;
2108
+ paragraph.forEach((child) => {
2109
+ if (!child.isText || child.marks.length) text = false;
2110
+ });
2111
+ return text;
2112
+ };
2113
+ function createFootnoteEditing(options) {
2114
+ const { editor } = options;
2115
+ const listeners = /* @__PURE__ */ new Set();
2116
+ let state = LOOMA_FOOTNOTE_EDITING_IDLE;
2117
+ let element = null;
2118
+ const set = (next) => {
2119
+ state = next;
2120
+ listeners.forEach((listener) => listener());
2121
+ };
2122
+ const close = () => {
2123
+ element = null;
2124
+ if (state.open) set({
2125
+ ...state,
2126
+ open: false
2127
+ });
2128
+ };
2129
+ const note = () => state.open && !editor.isDestroyed && element?.isConnected ? findLoomaFootnote(editor.state.doc, state.label) : null;
2130
+ return {
2131
+ get state() {
2132
+ return state;
2133
+ },
2134
+ get element() {
2135
+ return element;
2136
+ },
2137
+ subscribe(listener) {
2138
+ listeners.add(listener);
2139
+ return () => {
2140
+ listeners.delete(listener);
2141
+ };
2142
+ },
2143
+ open(position) {
2144
+ if (!options.isEditable() || editor.isDestroyed) return false;
2145
+ const node = editor.state.doc.nodeAt(position);
2146
+ const dom = editor.view.nodeDOM(position);
2147
+ if (node?.type.name !== "loomaFootnoteReference" || !(dom instanceof HTMLElement)) return false;
2148
+ const label = String(node.attrs.label);
2149
+ const found = findLoomaFootnote(editor.state.doc, label);
2150
+ if (!found) return false;
2151
+ element = dom;
2152
+ set({
2153
+ open: true,
2154
+ label,
2155
+ note: found.node.textBetween(0, found.node.content.size, "\n"),
2156
+ editable: plain(found.node),
2157
+ error: ""
2158
+ });
2159
+ return true;
2160
+ },
2161
+ setNote(text) {
2162
+ const found = note();
2163
+ if (!found) return close();
2164
+ if (!state.editable) return;
2165
+ const value = text.replace(/\s*\n\s*/g, " ");
2166
+ const start = found.position + 2;
2167
+ const end = start + found.node.firstChild.content.size;
2168
+ editor.view.dispatch(editor.state.tr.replaceWith(start, end, value ? editor.schema.text(value) : Fragment$1.empty));
2169
+ set({
2170
+ ...state,
2171
+ note: value
2172
+ });
2173
+ },
2174
+ rename(label) {
2175
+ if (!note()) return close();
2176
+ if (label === state.label) return state.error ? set({
2177
+ ...state,
2178
+ error: ""
2179
+ }) : void 0;
2180
+ if (!LOOMA_FOOTNOTE_LABEL.test(label)) return set({
2181
+ ...state,
2182
+ error: "A label is one word, without ]."
2183
+ });
2184
+ if (!editor.commands.renameLoomaFootnote(state.label, label)) return set({
2185
+ ...state,
2186
+ error: "Another footnote uses this label."
2187
+ });
2188
+ set({
2189
+ ...state,
2190
+ label,
2191
+ error: ""
2192
+ });
2193
+ },
2194
+ goToNote() {
2195
+ const reference = element;
2196
+ close();
2197
+ if (!reference?.isConnected || editor.isDestroyed) return;
2198
+ editor.chain().focus().setNodeSelection(editor.view.posAtDOM(reference, 0)).goToLoomaFootnote().run();
2199
+ },
2200
+ close
2201
+ };
2202
+ }
2203
+ //#endregion
1681
2204
  //#region src/editor/host-actions.ts
1682
2205
  /** Characters of context on each side of a selected passage. */
1683
2206
  var CONTEXT = 48;
@@ -1768,25 +2291,10 @@ function tableControls(state, selectionInTable, narrow) {
1768
2291
  capabilities: state.capabilities
1769
2292
  };
1770
2293
  }
1771
- /** Space, in pixels, the toolbar needs above a table; with less it sits below. */
1772
- var ROOM_ABOVE$1 = 76;
1773
- /**
1774
- * Centers the table toolbar on the table, at most 420px wide and 12px inside the viewport's sides,
1775
- * above the table when there is room and below it otherwise.
1776
- */
1777
- function tableToolbarPlacement(table, viewportWidth) {
1778
- const width = Math.min(420, viewportWidth - 24);
1779
- const left = Math.min(Math.max(12, table.left + (table.width - width) / 2), Math.max(12, viewportWidth - width - 12));
1780
- return {
1781
- top: `${table.top > ROOM_ABOVE$1 ? table.top - 52 : table.bottom + 12}px`,
1782
- left: `${left}px`,
1783
- maxWidth: `${width}px`
1784
- };
1785
- }
1786
- /** The overlay covers the table exactly. */
1787
- var tableOverlayPlacement = (table) => ({
1788
- top: `${table.top}px`,
1789
- left: `${table.left}px`,
2294
+ /** Inline controls cover the table in the editor's coordinate system. */
2295
+ var tableOverlayPlacement = (table, editor) => ({
2296
+ top: `${table.top - editor.top}px`,
2297
+ left: `${table.left - editor.left}px`,
1790
2298
  width: `${table.width}px`,
1791
2299
  height: `${table.height}px`
1792
2300
  });
@@ -2591,8 +3099,17 @@ function formatEditorShortcut(keys, apple) {
2591
3099
  ].filter(Boolean).join("+");
2592
3100
  }
2593
3101
  var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
3102
+ //#endregion
3103
+ //#region src/components/shared/tooltip.js
3104
+ /** Resolve the inherited pointer-intent delay; CSS time values and millisecond numbers are accepted. */
3105
+ function tooltipShowDelay$1(element, fallback = 500) {
3106
+ if (!element) return fallback;
3107
+ const value = element.ownerDocument.defaultView.getComputedStyle(element).getPropertyValue("--ui-tooltip-show-delay").trim();
3108
+ const match = /^([0-9]+(?:\.[0-9]+)?)(ms|s)?$/.exec(value);
3109
+ return match ? Number(match[1]) * (match[2] === "s" ? 1e3 : 1) : fallback;
3110
+ }
2594
3111
  /** Calls `onChange` after every state change. */
2595
- function createToolbarTooltip(onChange) {
3112
+ function createToolbarTooltip(onChange, delay = () => 400) {
2596
3113
  let state = {
2597
3114
  for: "",
2598
3115
  label: "",
@@ -2627,7 +3144,7 @@ function createToolbarTooltip(onChange) {
2627
3144
  if (!open) timer = setTimeout(() => {
2628
3145
  timer = void 0;
2629
3146
  set({ open: true });
2630
- }, 400);
3147
+ }, delay());
2631
3148
  },
2632
3149
  hide() {
2633
3150
  clear();
@@ -6212,6 +6729,20 @@ function createUiIcon(options = {}, html) {
6212
6729
  "y2": "12"
6213
6730
  }
6214
6731
  ],
6732
+ "superscript": [
6733
+ {
6734
+ "tag": "path",
6735
+ "d": "m4 19 8-8"
6736
+ },
6737
+ {
6738
+ "tag": "path",
6739
+ "d": "m12 19-8-8"
6740
+ },
6741
+ {
6742
+ "tag": "path",
6743
+ "d": "M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06"
6744
+ }
6745
+ ],
6215
6746
  "table": [{
6216
6747
  "tag": "path",
6217
6748
  "d": "M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"
@@ -9993,6 +10524,15 @@ defineComponent$1("ui-text", createUiText, [
9993
10524
  "wrap"
9994
10525
  ]);
9995
10526
  //#endregion
10527
+ //#region components/shared/tooltip.js
10528
+ /** Resolve the inherited pointer-intent delay; CSS time values and millisecond numbers are accepted. */
10529
+ function tooltipShowDelay(element, fallback = 500) {
10530
+ if (!element) return fallback;
10531
+ const value = element.ownerDocument.defaultView.getComputedStyle(element).getPropertyValue("--ui-tooltip-show-delay").trim();
10532
+ const match = /^([0-9]+(?:\.[0-9]+)?)(ms|s)?$/.exec(value);
10533
+ return match ? Number(match[1]) * (match[2] === "s" ? 1e3 : 1) : fallback;
10534
+ }
10535
+ //#endregion
9996
10536
  //#region components/ui-tooltip/ui-tooltip.js
9997
10537
  var ui_tooltip_exports = /* @__PURE__ */ __exportAll({ connect: () => connect$4 });
9998
10538
  var warmTooltips = /* @__PURE__ */ new WeakMap();
@@ -10069,7 +10609,7 @@ function connect$4(host) {
10069
10609
  showTimer = setTimeout(() => {
10070
10610
  showTimer = null;
10071
10611
  setOpen(true, "pointer");
10072
- }, warm.active > 0 || warm.until > Date.now() ? 0 : Math.max(0, Number(host.props.showDelay.value ?? 500)));
10612
+ }, warm.active > 0 || warm.until > Date.now() ? 0 : Math.max(0, host.props.showDelay.inputValue != null ? Number(host.props.showDelay.value) : tooltipShowDelay(element)));
10073
10613
  };
10074
10614
  const onLeave = () => {
10075
10615
  clearShow();
@@ -10139,6 +10679,7 @@ function connect$4(host) {
10139
10679
  if (how === "hover") {
10140
10680
  trigger.addEventListener("pointerenter", onEnter);
10141
10681
  trigger.addEventListener("pointerleave", onLeave);
10682
+ if (trigger.matches(":hover")) onEnter({ pointerType: "mouse" });
10142
10683
  }
10143
10684
  trigger.addEventListener("focusin", onFocusin);
10144
10685
  trigger.addEventListener("focusout", onFocusout);
@@ -14210,10 +14751,18 @@ var LoomaEditor = defineComponent({
14210
14751
  const chipInput = () => chipEditorRoot.value?.querySelector("[data-chip-label]");
14211
14752
  let chipEditing;
14212
14753
  const chipState = shallowRef(LOOMA_CHIP_EDITING_IDLE);
14754
+ const footnoteAnchorId = `looma-editor-footnote-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14755
+ const footnoteEditorRoot = ref(null);
14756
+ const setFootnoteEditorRoot = (instance) => {
14757
+ const element = instance?.$el;
14758
+ footnoteEditorRoot.value = element instanceof HTMLElement ? element : null;
14759
+ };
14760
+ let footnoteEditing;
14761
+ const footnoteState = shallowRef(LOOMA_FOOTNOTE_EDITING_IDLE);
14213
14762
  const toolbarScope = `looma-editor-tool-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14214
14763
  const toolbarTooltip = createToolbarTooltip((state) => {
14215
14764
  tooltip.value = state;
14216
- });
14765
+ }, () => tooltipShowDelay$1(root.value));
14217
14766
  const tooltip = shallowRef(toolbarTooltip.state);
14218
14767
  const resizeHintFor = ref("");
14219
14768
  let resizeHintSequence = 0;
@@ -14285,7 +14834,7 @@ var LoomaEditor = defineComponent({
14285
14834
  rows: 0,
14286
14835
  cols: 0,
14287
14836
  geometry: null,
14288
- toolbarStyle: {},
14837
+ toolbarTarget: null,
14289
14838
  overlayStyle: {},
14290
14839
  menuStyle: {},
14291
14840
  capabilities: LOOMA_TABLE_CONTROLS_CLOSED.capabilities
@@ -14296,6 +14845,17 @@ var LoomaEditor = defineComponent({
14296
14845
  chipEditing.element.id = chipAnchorId;
14297
14846
  nextTick(() => chipInput()?.focus());
14298
14847
  };
14848
+ const openFootnoteEditor = (position) => {
14849
+ if (!footnoteEditing?.open(position)) return;
14850
+ root.value?.querySelector(`#${footnoteAnchorId}`)?.removeAttribute("id");
14851
+ footnoteEditing.element.id = footnoteAnchorId;
14852
+ nextTick(() => footnoteEditorRoot.value?.querySelector("[data-footnote-note]")?.focus());
14853
+ };
14854
+ const closeFootnote = (focusReference = false) => {
14855
+ const element = footnoteEditing?.element;
14856
+ footnoteEditing?.close();
14857
+ if (focusReference) element?.focus();
14858
+ };
14299
14859
  const closeChip = (focusChip = false) => {
14300
14860
  const element = chipEditing?.element;
14301
14861
  chipEditing?.close();
@@ -14525,6 +15085,14 @@ var LoomaEditor = defineComponent({
14525
15085
  chipState.value = chips.state;
14526
15086
  });
14527
15087
  chipEditing = chips;
15088
+ const footnotes = createFootnoteEditing({
15089
+ editor: session.editor,
15090
+ isEditable: () => props.editable
15091
+ });
15092
+ footnotes.subscribe(() => {
15093
+ footnoteState.value = footnotes.state;
15094
+ });
15095
+ footnoteEditing = footnotes;
14528
15096
  const imageUploads = createImageUploads({
14529
15097
  editor: session.editor,
14530
15098
  upload: () => (file, init) => ask("image-upload", { file }, init),
@@ -14551,6 +15119,7 @@ var LoomaEditor = defineComponent({
14551
15119
  const content = edited ? previous.editor.getJSON() : props.modelValue;
14552
15120
  linkEditing?.destroy();
14553
15121
  chipEditing?.close();
15122
+ footnoteEditing?.close();
14554
15123
  formatting?.destroy();
14555
15124
  uploads?.cancel();
14556
15125
  previous.destroy();
@@ -14673,10 +15242,11 @@ var LoomaEditor = defineComponent({
14673
15242
  editor.value?.setEditable(editable, false);
14674
15243
  if (!editable) formattingPopup?.hide();
14675
15244
  if (editor.value) imageDelivery.reset(editor.value);
14676
- root.value?.querySelectorAll("[data-looma-chip]").forEach((chip) => {
15245
+ root.value?.querySelectorAll("[data-looma-chip], [data-looma-footnote-ref]").forEach((chip) => {
14677
15246
  chip.setAttribute("aria-disabled", String(!editable));
14678
15247
  });
14679
15248
  if (!editable) chipEditing?.close();
15249
+ if (!editable) footnoteEditing?.close();
14680
15250
  });
14681
15251
  watch([
14682
15252
  () => linkState.value.surface === "popover",
@@ -14796,8 +15366,8 @@ var LoomaEditor = defineComponent({
14796
15366
  tableUi.rows = Math.max(0, geometry.rowBoundaries.length - 1);
14797
15367
  tableUi.cols = Math.max(0, geometry.columnBoundaries.length - 1);
14798
15368
  tableUi.geometry = geometry;
14799
- tableUi.toolbarStyle = tableToolbarPlacement(rect, window.innerWidth);
14800
- tableUi.overlayStyle = tableOverlayPlacement(rect);
15369
+ tableUi.toolbarTarget = table.closest("[data-looma-table-block]")?.querySelector("[data-looma-table-toolbar-host]") ?? null;
15370
+ tableUi.overlayStyle = tableOverlayPlacement(rect, root.value.getBoundingClientRect());
14801
15371
  };
14802
15372
  const onEditorFocus = () => {
14803
15373
  editorFocused.value = true;
@@ -14863,13 +15433,10 @@ var LoomaEditor = defineComponent({
14863
15433
  presentation?.show();
14864
15434
  };
14865
15435
  };
14866
- const tableToolbarRef = popupRef(tableToolbarShell);
14867
15436
  const codeLanguageRef = popupRef(codeLanguageShell, true);
14868
15437
  const mobileToolbarRef = popupRef(mobileToolbarShell, true);
14869
- const presentTableOverlay = popupRef(tableOverlayShell);
14870
15438
  const tableOverlayRef = (node) => {
14871
- presentTableOverlay(node);
14872
- if (node instanceof HTMLElement) node.hidden = !tableUi.overlayOpen;
15439
+ tableOverlayShell.value = node instanceof HTMLElement ? node : null;
14873
15440
  };
14874
15441
  const onDocumentPointerDown = (event) => {
14875
15442
  const path = typeof event.composedPath === "function" ? event.composedPath() : [];
@@ -15023,7 +15590,18 @@ var LoomaEditor = defineComponent({
15023
15590
  const element = event.target instanceof Element ? event.target.closest("[data-looma-chip]") : null;
15024
15591
  return element && root.value?.contains(element) ? element : null;
15025
15592
  };
15593
+ const footnoteElementForEvent = (event) => {
15594
+ const element = event.target instanceof Element ? event.target.closest("[data-looma-footnote-ref]") : null;
15595
+ return element && root.value?.contains(element) ? element : null;
15596
+ };
15026
15597
  const onChipClickCapture = (event) => {
15598
+ const reference = footnoteElementForEvent(event);
15599
+ if (reference && editor.value && props.editable) {
15600
+ event.preventDefault();
15601
+ event.stopPropagation();
15602
+ openFootnoteEditor(editor.value.view.posAtDOM(reference, 0));
15603
+ return;
15604
+ }
15027
15605
  const chip = chipElementForEvent(event);
15028
15606
  if (chip && editor.value && props.editable) {
15029
15607
  event.preventDefault();
@@ -15041,6 +15619,12 @@ var LoomaEditor = defineComponent({
15041
15619
  };
15042
15620
  const onImageKeyDown = (event) => {
15043
15621
  if (event.key !== "Enter" && event.key !== " ") return;
15622
+ const reference = footnoteElementForEvent(event);
15623
+ if (reference && editor.value && props.editable) {
15624
+ event.preventDefault();
15625
+ openFootnoteEditor(editor.value.view.posAtDOM(reference, 0));
15626
+ return;
15627
+ }
15044
15628
  const chip = chipElementForEvent(event);
15045
15629
  if (chip && editor.value && props.editable) {
15046
15630
  event.preventDefault();
@@ -15465,8 +16049,6 @@ var LoomaEditor = defineComponent({
15465
16049
  }, [
15466
16050
  resizeHintFor.value ? h(Tooltip, {
15467
16051
  for: resizeHintFor.value,
15468
- open: true,
15469
- trigger: "focus",
15470
16052
  placement: "top",
15471
16053
  inverse: true
15472
16054
  }, () => "Drag to resize column") : null,
@@ -15576,6 +16158,23 @@ var LoomaEditor = defineComponent({
15576
16158
  onColor: (event) => chipEditing?.setColor(event.detail.color),
15577
16159
  onDone: () => closeChip(true)
15578
16160
  })),
16161
+ h(Popover, {
16162
+ size: "sm",
16163
+ open: footnoteState.value.open,
16164
+ for: footnoteAnchorId,
16165
+ placement: mobile.value ? "top-start" : "bottom-start",
16166
+ onClose: () => footnoteEditing?.close()
16167
+ }, () => h(EditorFootnoteEditor, {
16168
+ ref: setFootnoteEditorRoot,
16169
+ label: footnoteState.value.label,
16170
+ note: footnoteState.value.note,
16171
+ editable: footnoteState.value.editable,
16172
+ error: footnoteState.value.error,
16173
+ onText: (event) => footnoteEditing?.setNote(event.detail.text),
16174
+ onLabel: (event) => footnoteEditing?.rename(event.detail.label),
16175
+ onGo: () => footnoteEditing?.goToNote(),
16176
+ onDone: () => closeFootnote(true)
16177
+ })),
15579
16178
  h(Popover, {
15580
16179
  class: "looma-editor__link-popover",
15581
16180
  size: "sm",
@@ -15663,12 +16262,11 @@ var LoomaEditor = defineComponent({
15663
16262
  mention.select?.(event.detail.index);
15664
16263
  }
15665
16264
  }) : null,
15666
- tableUi.toolbarOpen ? h("div", {
16265
+ tableUi.toolbarOpen && tableUi.toolbarTarget ? h(Teleport, { to: tableUi.toolbarTarget }, [h("div", {
15667
16266
  key: "looma-table-toolbar-shell",
15668
- ref: tableToolbarRef,
15669
- class: "looma-editor__table-toolbar-shell",
15670
- style: tableUi.toolbarStyle
15671
- }, [h(EditorTableToolbar, tableProps)]) : null,
16267
+ ref: tableToolbarShell,
16268
+ class: "looma-editor__table-toolbar-shell"
16269
+ }, [h(EditorTableToolbar, tableProps)])]) : null,
15672
16270
  instance && props.editable && !mobile.value ? h("div", {
15673
16271
  key: "looma-table-overlay-shell",
15674
16272
  ref: tableOverlayRef,
@@ -15739,4 +16337,4 @@ var LoomaEditor = defineComponent({
15739
16337
  }
15740
16338
  });
15741
16339
  //#endregion
15742
- export { EDITOR_ADAPTER_COMPONENT_TAG_MAP, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, LoomaEditor };
16340
+ export { EDITOR_ADAPTER_COMPONENT_TAG_MAP, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorFootnoteEditor, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, LoomaEditor };