@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.
- package/components/shared/icons.js +1 -0
- package/components/ui-editor-footnote-editor/ui-editor-footnote-editor.html +57 -0
- package/components/ui-editor-footnote-editor/ui-editor-footnote-editor.js +38 -0
- package/components/ui-icon/ui-icon.html +1 -0
- package/dist/index.js +37 -36
- package/editor/chunks/extensions.js +342 -20
- package/editor/expressmark.js +1 -1
- package/editor/extensions/footnote.d.ts +40 -7
- package/editor/extensions/index.d.ts +1 -1
- package/editor/extensions/index.js +2 -2
- package/editor/footnote-editing.d.ts +46 -0
- package/editor/icons.d.ts +1 -0
- package/editor/index.js +4 -3
- package/package.json +1 -1
- package/styles/ui-editor-footnote-editor.css +1 -0
- package/vanilla/UiEditorFootnoteEditor.d.ts +30 -0
- package/vanilla/UiEditorFootnoteEditor.js +101 -0
- package/vanilla/UiIcon.js +1 -1
- package/vanilla/index.js +1 -0
- package/vue/UiEditorFootnoteEditor.d.ts +57 -0
- package/vue/UiEditorFootnoteEditor.js +2 -0
- package/vue/UiEditorFootnoteEditor.vue +304 -0
- package/vue/UiIcon.vue +4 -0
- package/vue/chunks/UiEditorFootnoteEditor.js +306 -0
- package/vue/chunks/UiEditorImageToolbar.js +1 -1
- package/vue/chunks/UiEditorLinkForm.js +1 -1
- package/vue/chunks/UiIcon.js +15 -1
- package/vue/components.css +579 -579
- package/vue/editor/index.js +607 -7
- package/vue/editor/primitives.d.ts +3 -1
- package/vue/index.d.ts +1 -0
- package/vue/index.js +5 -4
package/vue/editor/index.js
CHANGED
|
@@ -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 };
|