@threadlabs/looma 0.31.0 → 0.31.1

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.
@@ -1,16 +1,16 @@
1
1
  import { 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;
@@ -6212,6 +6735,20 @@ function createUiIcon(options = {}, html) {
6212
6735
  "y2": "12"
6213
6736
  }
6214
6737
  ],
6738
+ "superscript": [
6739
+ {
6740
+ "tag": "path",
6741
+ "d": "m4 19 8-8"
6742
+ },
6743
+ {
6744
+ "tag": "path",
6745
+ "d": "m12 19-8-8"
6746
+ },
6747
+ {
6748
+ "tag": "path",
6749
+ "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"
6750
+ }
6751
+ ],
6215
6752
  "table": [{
6216
6753
  "tag": "path",
6217
6754
  "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"
@@ -14210,6 +14747,14 @@ var LoomaEditor = defineComponent({
14210
14747
  const chipInput = () => chipEditorRoot.value?.querySelector("[data-chip-label]");
14211
14748
  let chipEditing;
14212
14749
  const chipState = shallowRef(LOOMA_CHIP_EDITING_IDLE);
14750
+ const footnoteAnchorId = `looma-editor-footnote-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14751
+ const footnoteEditorRoot = ref(null);
14752
+ const setFootnoteEditorRoot = (instance) => {
14753
+ const element = instance?.$el;
14754
+ footnoteEditorRoot.value = element instanceof HTMLElement ? element : null;
14755
+ };
14756
+ let footnoteEditing;
14757
+ const footnoteState = shallowRef(LOOMA_FOOTNOTE_EDITING_IDLE);
14213
14758
  const toolbarScope = `looma-editor-tool-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14214
14759
  const toolbarTooltip = createToolbarTooltip((state) => {
14215
14760
  tooltip.value = state;
@@ -14296,6 +14841,17 @@ var LoomaEditor = defineComponent({
14296
14841
  chipEditing.element.id = chipAnchorId;
14297
14842
  nextTick(() => chipInput()?.focus());
14298
14843
  };
14844
+ const openFootnoteEditor = (position) => {
14845
+ if (!footnoteEditing?.open(position)) return;
14846
+ root.value?.querySelector(`#${footnoteAnchorId}`)?.removeAttribute("id");
14847
+ footnoteEditing.element.id = footnoteAnchorId;
14848
+ nextTick(() => footnoteEditorRoot.value?.querySelector("[data-footnote-note]")?.focus());
14849
+ };
14850
+ const closeFootnote = (focusReference = false) => {
14851
+ const element = footnoteEditing?.element;
14852
+ footnoteEditing?.close();
14853
+ if (focusReference) element?.focus();
14854
+ };
14299
14855
  const closeChip = (focusChip = false) => {
14300
14856
  const element = chipEditing?.element;
14301
14857
  chipEditing?.close();
@@ -14525,6 +15081,14 @@ var LoomaEditor = defineComponent({
14525
15081
  chipState.value = chips.state;
14526
15082
  });
14527
15083
  chipEditing = chips;
15084
+ const footnotes = createFootnoteEditing({
15085
+ editor: session.editor,
15086
+ isEditable: () => props.editable
15087
+ });
15088
+ footnotes.subscribe(() => {
15089
+ footnoteState.value = footnotes.state;
15090
+ });
15091
+ footnoteEditing = footnotes;
14528
15092
  const imageUploads = createImageUploads({
14529
15093
  editor: session.editor,
14530
15094
  upload: () => (file, init) => ask("image-upload", { file }, init),
@@ -14551,6 +15115,7 @@ var LoomaEditor = defineComponent({
14551
15115
  const content = edited ? previous.editor.getJSON() : props.modelValue;
14552
15116
  linkEditing?.destroy();
14553
15117
  chipEditing?.close();
15118
+ footnoteEditing?.close();
14554
15119
  formatting?.destroy();
14555
15120
  uploads?.cancel();
14556
15121
  previous.destroy();
@@ -14673,10 +15238,11 @@ var LoomaEditor = defineComponent({
14673
15238
  editor.value?.setEditable(editable, false);
14674
15239
  if (!editable) formattingPopup?.hide();
14675
15240
  if (editor.value) imageDelivery.reset(editor.value);
14676
- root.value?.querySelectorAll("[data-looma-chip]").forEach((chip) => {
15241
+ root.value?.querySelectorAll("[data-looma-chip], [data-looma-footnote-ref]").forEach((chip) => {
14677
15242
  chip.setAttribute("aria-disabled", String(!editable));
14678
15243
  });
14679
15244
  if (!editable) chipEditing?.close();
15245
+ if (!editable) footnoteEditing?.close();
14680
15246
  });
14681
15247
  watch([
14682
15248
  () => linkState.value.surface === "popover",
@@ -15023,7 +15589,18 @@ var LoomaEditor = defineComponent({
15023
15589
  const element = event.target instanceof Element ? event.target.closest("[data-looma-chip]") : null;
15024
15590
  return element && root.value?.contains(element) ? element : null;
15025
15591
  };
15592
+ const footnoteElementForEvent = (event) => {
15593
+ const element = event.target instanceof Element ? event.target.closest("[data-looma-footnote-ref]") : null;
15594
+ return element && root.value?.contains(element) ? element : null;
15595
+ };
15026
15596
  const onChipClickCapture = (event) => {
15597
+ const reference = footnoteElementForEvent(event);
15598
+ if (reference && editor.value && props.editable) {
15599
+ event.preventDefault();
15600
+ event.stopPropagation();
15601
+ openFootnoteEditor(editor.value.view.posAtDOM(reference, 0));
15602
+ return;
15603
+ }
15027
15604
  const chip = chipElementForEvent(event);
15028
15605
  if (chip && editor.value && props.editable) {
15029
15606
  event.preventDefault();
@@ -15041,6 +15618,12 @@ var LoomaEditor = defineComponent({
15041
15618
  };
15042
15619
  const onImageKeyDown = (event) => {
15043
15620
  if (event.key !== "Enter" && event.key !== " ") return;
15621
+ const reference = footnoteElementForEvent(event);
15622
+ if (reference && editor.value && props.editable) {
15623
+ event.preventDefault();
15624
+ openFootnoteEditor(editor.value.view.posAtDOM(reference, 0));
15625
+ return;
15626
+ }
15044
15627
  const chip = chipElementForEvent(event);
15045
15628
  if (chip && editor.value && props.editable) {
15046
15629
  event.preventDefault();
@@ -15576,6 +16159,23 @@ var LoomaEditor = defineComponent({
15576
16159
  onColor: (event) => chipEditing?.setColor(event.detail.color),
15577
16160
  onDone: () => closeChip(true)
15578
16161
  })),
16162
+ h(Popover, {
16163
+ size: "sm",
16164
+ open: footnoteState.value.open,
16165
+ for: footnoteAnchorId,
16166
+ placement: mobile.value ? "top-start" : "bottom-start",
16167
+ onClose: () => footnoteEditing?.close()
16168
+ }, () => h(EditorFootnoteEditor, {
16169
+ ref: setFootnoteEditorRoot,
16170
+ label: footnoteState.value.label,
16171
+ note: footnoteState.value.note,
16172
+ editable: footnoteState.value.editable,
16173
+ error: footnoteState.value.error,
16174
+ onText: (event) => footnoteEditing?.setNote(event.detail.text),
16175
+ onLabel: (event) => footnoteEditing?.rename(event.detail.label),
16176
+ onGo: () => footnoteEditing?.goToNote(),
16177
+ onDone: () => closeFootnote(true)
16178
+ })),
15579
16179
  h(Popover, {
15580
16180
  class: "looma-editor__link-popover",
15581
16181
  size: "sm",
@@ -15739,4 +16339,4 @@ var LoomaEditor = defineComponent({
15739
16339
  }
15740
16340
  });
15741
16341
  //#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 };
16342
+ export { EDITOR_ADAPTER_COMPONENT_TAG_MAP, EditorBlockActions, EditorChipEditor, EditorCodeLanguage, EditorFootnoteEditor, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, LoomaEditor };