@threadlabs/looma 0.28.20 → 0.28.22
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/editor/chunks/extensions.js +199 -142
- package/editor/extensions/task-item.d.ts +7 -0
- package/editor/image-uploads.d.ts +49 -0
- package/package.json +1 -1
- package/vue/editor/LoomaEditor.d.ts +3 -18
- package/vue/editor/index.js +79 -33
|
@@ -1732,144 +1732,11 @@ var TaskList = Node$1.create({
|
|
|
1732
1732
|
}
|
|
1733
1733
|
});
|
|
1734
1734
|
//#endregion
|
|
1735
|
-
//#region ../../node_modules/.pnpm/@tiptap+extension-task-item@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2__@tiptap+pm@2.27.2/node_modules/@tiptap/extension-task-item/dist/index.js
|
|
1736
|
-
/**
|
|
1737
|
-
* Matches a task item to a - [ ] on input.
|
|
1738
|
-
*/
|
|
1739
|
-
var inputRegex$4 = /^\s*(\[([( |x])?\])\s$/;
|
|
1740
|
-
/**
|
|
1741
|
-
* This extension allows you to create task items.
|
|
1742
|
-
* @see https://www.tiptap.dev/api/nodes/task-item
|
|
1743
|
-
*/
|
|
1744
|
-
var TaskItem = Node$1.create({
|
|
1745
|
-
name: "taskItem",
|
|
1746
|
-
addOptions() {
|
|
1747
|
-
return {
|
|
1748
|
-
nested: false,
|
|
1749
|
-
HTMLAttributes: {},
|
|
1750
|
-
taskListTypeName: "taskList",
|
|
1751
|
-
a11y: void 0
|
|
1752
|
-
};
|
|
1753
|
-
},
|
|
1754
|
-
content() {
|
|
1755
|
-
return this.options.nested ? "paragraph block*" : "paragraph+";
|
|
1756
|
-
},
|
|
1757
|
-
defining: true,
|
|
1758
|
-
addAttributes() {
|
|
1759
|
-
return { checked: {
|
|
1760
|
-
default: false,
|
|
1761
|
-
keepOnSplit: false,
|
|
1762
|
-
parseHTML: (element) => {
|
|
1763
|
-
const dataChecked = element.getAttribute("data-checked");
|
|
1764
|
-
return dataChecked === "" || dataChecked === "true";
|
|
1765
|
-
},
|
|
1766
|
-
renderHTML: (attributes) => ({ "data-checked": attributes.checked })
|
|
1767
|
-
} };
|
|
1768
|
-
},
|
|
1769
|
-
parseHTML() {
|
|
1770
|
-
return [{
|
|
1771
|
-
tag: `li[data-type="${this.name}"]`,
|
|
1772
|
-
priority: 51
|
|
1773
|
-
}];
|
|
1774
|
-
},
|
|
1775
|
-
renderHTML({ node, HTMLAttributes }) {
|
|
1776
|
-
return [
|
|
1777
|
-
"li",
|
|
1778
|
-
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }),
|
|
1779
|
-
[
|
|
1780
|
-
"label",
|
|
1781
|
-
["input", {
|
|
1782
|
-
type: "checkbox",
|
|
1783
|
-
checked: node.attrs.checked ? "checked" : null
|
|
1784
|
-
}],
|
|
1785
|
-
["span"]
|
|
1786
|
-
],
|
|
1787
|
-
["div", 0]
|
|
1788
|
-
];
|
|
1789
|
-
},
|
|
1790
|
-
addKeyboardShortcuts() {
|
|
1791
|
-
const shortcuts = {
|
|
1792
|
-
Enter: () => this.editor.commands.splitListItem(this.name),
|
|
1793
|
-
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
|
|
1794
|
-
};
|
|
1795
|
-
if (!this.options.nested) return shortcuts;
|
|
1796
|
-
return {
|
|
1797
|
-
...shortcuts,
|
|
1798
|
-
Tab: () => this.editor.commands.sinkListItem(this.name)
|
|
1799
|
-
};
|
|
1800
|
-
},
|
|
1801
|
-
addNodeView() {
|
|
1802
|
-
return ({ node, HTMLAttributes, getPos, editor }) => {
|
|
1803
|
-
const listItem = document.createElement("li");
|
|
1804
|
-
const checkboxWrapper = document.createElement("label");
|
|
1805
|
-
const checkboxStyler = document.createElement("span");
|
|
1806
|
-
const checkbox = document.createElement("input");
|
|
1807
|
-
const content = document.createElement("div");
|
|
1808
|
-
const updateA11Y = () => {
|
|
1809
|
-
var _a, _b;
|
|
1810
|
-
checkbox.ariaLabel = ((_b = (_a = this.options.a11y) === null || _a === void 0 ? void 0 : _a.checkboxLabel) === null || _b === void 0 ? void 0 : _b.call(_a, node, checkbox.checked)) || `Task item checkbox for ${node.textContent || "empty task item"}`;
|
|
1811
|
-
};
|
|
1812
|
-
updateA11Y();
|
|
1813
|
-
checkboxWrapper.contentEditable = "false";
|
|
1814
|
-
checkbox.type = "checkbox";
|
|
1815
|
-
checkbox.addEventListener("mousedown", (event) => event.preventDefault());
|
|
1816
|
-
checkbox.addEventListener("change", (event) => {
|
|
1817
|
-
if (!editor.isEditable && !this.options.onReadOnlyChecked) {
|
|
1818
|
-
checkbox.checked = !checkbox.checked;
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
const { checked } = event.target;
|
|
1822
|
-
if (editor.isEditable && typeof getPos === "function") editor.chain().focus(void 0, { scrollIntoView: false }).command(({ tr }) => {
|
|
1823
|
-
const position = getPos();
|
|
1824
|
-
if (typeof position !== "number") return false;
|
|
1825
|
-
const currentNode = tr.doc.nodeAt(position);
|
|
1826
|
-
tr.setNodeMarkup(position, void 0, {
|
|
1827
|
-
...currentNode === null || currentNode === void 0 ? void 0 : currentNode.attrs,
|
|
1828
|
-
checked
|
|
1829
|
-
});
|
|
1830
|
-
return true;
|
|
1831
|
-
}).run();
|
|
1832
|
-
if (!editor.isEditable && this.options.onReadOnlyChecked) {
|
|
1833
|
-
if (!this.options.onReadOnlyChecked(node, checked)) checkbox.checked = !checkbox.checked;
|
|
1834
|
-
}
|
|
1835
|
-
});
|
|
1836
|
-
Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {
|
|
1837
|
-
listItem.setAttribute(key, value);
|
|
1838
|
-
});
|
|
1839
|
-
listItem.dataset.checked = node.attrs.checked;
|
|
1840
|
-
checkbox.checked = node.attrs.checked;
|
|
1841
|
-
checkboxWrapper.append(checkbox, checkboxStyler);
|
|
1842
|
-
listItem.append(checkboxWrapper, content);
|
|
1843
|
-
Object.entries(HTMLAttributes).forEach(([key, value]) => {
|
|
1844
|
-
listItem.setAttribute(key, value);
|
|
1845
|
-
});
|
|
1846
|
-
return {
|
|
1847
|
-
dom: listItem,
|
|
1848
|
-
contentDOM: content,
|
|
1849
|
-
update: (updatedNode) => {
|
|
1850
|
-
if (updatedNode.type !== this.type) return false;
|
|
1851
|
-
listItem.dataset.checked = updatedNode.attrs.checked;
|
|
1852
|
-
checkbox.checked = updatedNode.attrs.checked;
|
|
1853
|
-
updateA11Y();
|
|
1854
|
-
return true;
|
|
1855
|
-
}
|
|
1856
|
-
};
|
|
1857
|
-
};
|
|
1858
|
-
},
|
|
1859
|
-
addInputRules() {
|
|
1860
|
-
return [wrappingInputRule({
|
|
1861
|
-
find: inputRegex$4,
|
|
1862
|
-
type: this.type,
|
|
1863
|
-
getAttributes: (match) => ({ checked: match[match.length - 1] === "x" })
|
|
1864
|
-
})];
|
|
1865
|
-
}
|
|
1866
|
-
});
|
|
1867
|
-
//#endregion
|
|
1868
1735
|
//#region ../../node_modules/.pnpm/@tiptap+extension-blockquote@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-blockquote/dist/index.js
|
|
1869
1736
|
/**
|
|
1870
1737
|
* Matches a blockquote to a `>` as input.
|
|
1871
1738
|
*/
|
|
1872
|
-
var inputRegex$
|
|
1739
|
+
var inputRegex$4 = /^\s*>\s$/;
|
|
1873
1740
|
/**
|
|
1874
1741
|
* This extension allows you to create blockquotes.
|
|
1875
1742
|
* @see https://tiptap.dev/api/nodes/blockquote
|
|
@@ -1910,7 +1777,7 @@ var Blockquote = Node$1.create({
|
|
|
1910
1777
|
},
|
|
1911
1778
|
addInputRules() {
|
|
1912
1779
|
return [wrappingInputRule({
|
|
1913
|
-
find: inputRegex$
|
|
1780
|
+
find: inputRegex$4,
|
|
1914
1781
|
type: this.type
|
|
1915
1782
|
})];
|
|
1916
1783
|
}
|
|
@@ -3861,7 +3728,7 @@ var Link = Mark.create({
|
|
|
3861
3728
|
/**
|
|
3862
3729
|
* Matches an image to a  on input.
|
|
3863
3730
|
*/
|
|
3864
|
-
var inputRegex$
|
|
3731
|
+
var inputRegex$3 = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/;
|
|
3865
3732
|
/**
|
|
3866
3733
|
* This extension allows you to insert images.
|
|
3867
3734
|
* @see https://www.tiptap.dev/api/nodes/image
|
|
@@ -3905,7 +3772,7 @@ var Image = Node$1.create({
|
|
|
3905
3772
|
},
|
|
3906
3773
|
addInputRules() {
|
|
3907
3774
|
return [nodeInputRule({
|
|
3908
|
-
find: inputRegex$
|
|
3775
|
+
find: inputRegex$3,
|
|
3909
3776
|
type: this.type,
|
|
3910
3777
|
getAttributes: (match) => {
|
|
3911
3778
|
const [, , alt, src, title] = match;
|
|
@@ -3962,7 +3829,7 @@ var LoomaImage = Image.extend({ addAttributes() {
|
|
|
3962
3829
|
/**
|
|
3963
3830
|
* Matches a highlight to a ==highlight== on input.
|
|
3964
3831
|
*/
|
|
3965
|
-
var inputRegex$
|
|
3832
|
+
var inputRegex$2 = /(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))$/;
|
|
3966
3833
|
/**
|
|
3967
3834
|
* Matches a highlight to a ==highlight== on paste.
|
|
3968
3835
|
*/
|
|
@@ -4021,7 +3888,7 @@ var Highlight = Mark.create({
|
|
|
4021
3888
|
},
|
|
4022
3889
|
addInputRules() {
|
|
4023
3890
|
return [markInputRule({
|
|
4024
|
-
find: inputRegex$
|
|
3891
|
+
find: inputRegex$2,
|
|
4025
3892
|
type: this.type
|
|
4026
3893
|
})];
|
|
4027
3894
|
},
|
|
@@ -4244,7 +4111,7 @@ var Placeholder = Extension.create({
|
|
|
4244
4111
|
* This ensures that any text between backticks is formatted as code,
|
|
4245
4112
|
* regardless of the surrounding characters (exception being another backtick).
|
|
4246
4113
|
*/
|
|
4247
|
-
var inputRegex = /(^|[^`])`([^`]+)`(?!`)/;
|
|
4114
|
+
var inputRegex$1 = /(^|[^`])`([^`]+)`(?!`)/;
|
|
4248
4115
|
/**
|
|
4249
4116
|
* Matches inline code while pasting.
|
|
4250
4117
|
*/
|
|
@@ -4289,7 +4156,7 @@ var Code = Mark.create({
|
|
|
4289
4156
|
},
|
|
4290
4157
|
addInputRules() {
|
|
4291
4158
|
return [markInputRule({
|
|
4292
|
-
find: inputRegex,
|
|
4159
|
+
find: inputRegex$1,
|
|
4293
4160
|
type: this.type
|
|
4294
4161
|
})];
|
|
4295
4162
|
},
|
|
@@ -6431,6 +6298,196 @@ var LoomaBlockSelection = Extension.create({
|
|
|
6431
6298
|
}
|
|
6432
6299
|
});
|
|
6433
6300
|
//#endregion
|
|
6301
|
+
//#region ../../node_modules/.pnpm/@tiptap+extension-task-item@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2__@tiptap+pm@2.27.2/node_modules/@tiptap/extension-task-item/dist/index.js
|
|
6302
|
+
/**
|
|
6303
|
+
* Matches a task item to a - [ ] on input.
|
|
6304
|
+
*/
|
|
6305
|
+
var inputRegex = /^\s*(\[([( |x])?\])\s$/;
|
|
6306
|
+
/**
|
|
6307
|
+
* This extension allows you to create task items.
|
|
6308
|
+
* @see https://www.tiptap.dev/api/nodes/task-item
|
|
6309
|
+
*/
|
|
6310
|
+
var TaskItem = Node$1.create({
|
|
6311
|
+
name: "taskItem",
|
|
6312
|
+
addOptions() {
|
|
6313
|
+
return {
|
|
6314
|
+
nested: false,
|
|
6315
|
+
HTMLAttributes: {},
|
|
6316
|
+
taskListTypeName: "taskList",
|
|
6317
|
+
a11y: void 0
|
|
6318
|
+
};
|
|
6319
|
+
},
|
|
6320
|
+
content() {
|
|
6321
|
+
return this.options.nested ? "paragraph block*" : "paragraph+";
|
|
6322
|
+
},
|
|
6323
|
+
defining: true,
|
|
6324
|
+
addAttributes() {
|
|
6325
|
+
return { checked: {
|
|
6326
|
+
default: false,
|
|
6327
|
+
keepOnSplit: false,
|
|
6328
|
+
parseHTML: (element) => {
|
|
6329
|
+
const dataChecked = element.getAttribute("data-checked");
|
|
6330
|
+
return dataChecked === "" || dataChecked === "true";
|
|
6331
|
+
},
|
|
6332
|
+
renderHTML: (attributes) => ({ "data-checked": attributes.checked })
|
|
6333
|
+
} };
|
|
6334
|
+
},
|
|
6335
|
+
parseHTML() {
|
|
6336
|
+
return [{
|
|
6337
|
+
tag: `li[data-type="${this.name}"]`,
|
|
6338
|
+
priority: 51
|
|
6339
|
+
}];
|
|
6340
|
+
},
|
|
6341
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
6342
|
+
return [
|
|
6343
|
+
"li",
|
|
6344
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }),
|
|
6345
|
+
[
|
|
6346
|
+
"label",
|
|
6347
|
+
["input", {
|
|
6348
|
+
type: "checkbox",
|
|
6349
|
+
checked: node.attrs.checked ? "checked" : null
|
|
6350
|
+
}],
|
|
6351
|
+
["span"]
|
|
6352
|
+
],
|
|
6353
|
+
["div", 0]
|
|
6354
|
+
];
|
|
6355
|
+
},
|
|
6356
|
+
addKeyboardShortcuts() {
|
|
6357
|
+
const shortcuts = {
|
|
6358
|
+
Enter: () => this.editor.commands.splitListItem(this.name),
|
|
6359
|
+
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
|
|
6360
|
+
};
|
|
6361
|
+
if (!this.options.nested) return shortcuts;
|
|
6362
|
+
return {
|
|
6363
|
+
...shortcuts,
|
|
6364
|
+
Tab: () => this.editor.commands.sinkListItem(this.name)
|
|
6365
|
+
};
|
|
6366
|
+
},
|
|
6367
|
+
addNodeView() {
|
|
6368
|
+
return ({ node, HTMLAttributes, getPos, editor }) => {
|
|
6369
|
+
const listItem = document.createElement("li");
|
|
6370
|
+
const checkboxWrapper = document.createElement("label");
|
|
6371
|
+
const checkboxStyler = document.createElement("span");
|
|
6372
|
+
const checkbox = document.createElement("input");
|
|
6373
|
+
const content = document.createElement("div");
|
|
6374
|
+
const updateA11Y = () => {
|
|
6375
|
+
var _a, _b;
|
|
6376
|
+
checkbox.ariaLabel = ((_b = (_a = this.options.a11y) === null || _a === void 0 ? void 0 : _a.checkboxLabel) === null || _b === void 0 ? void 0 : _b.call(_a, node, checkbox.checked)) || `Task item checkbox for ${node.textContent || "empty task item"}`;
|
|
6377
|
+
};
|
|
6378
|
+
updateA11Y();
|
|
6379
|
+
checkboxWrapper.contentEditable = "false";
|
|
6380
|
+
checkbox.type = "checkbox";
|
|
6381
|
+
checkbox.addEventListener("mousedown", (event) => event.preventDefault());
|
|
6382
|
+
checkbox.addEventListener("change", (event) => {
|
|
6383
|
+
if (!editor.isEditable && !this.options.onReadOnlyChecked) {
|
|
6384
|
+
checkbox.checked = !checkbox.checked;
|
|
6385
|
+
return;
|
|
6386
|
+
}
|
|
6387
|
+
const { checked } = event.target;
|
|
6388
|
+
if (editor.isEditable && typeof getPos === "function") editor.chain().focus(void 0, { scrollIntoView: false }).command(({ tr }) => {
|
|
6389
|
+
const position = getPos();
|
|
6390
|
+
if (typeof position !== "number") return false;
|
|
6391
|
+
const currentNode = tr.doc.nodeAt(position);
|
|
6392
|
+
tr.setNodeMarkup(position, void 0, {
|
|
6393
|
+
...currentNode === null || currentNode === void 0 ? void 0 : currentNode.attrs,
|
|
6394
|
+
checked
|
|
6395
|
+
});
|
|
6396
|
+
return true;
|
|
6397
|
+
}).run();
|
|
6398
|
+
if (!editor.isEditable && this.options.onReadOnlyChecked) {
|
|
6399
|
+
if (!this.options.onReadOnlyChecked(node, checked)) checkbox.checked = !checkbox.checked;
|
|
6400
|
+
}
|
|
6401
|
+
});
|
|
6402
|
+
Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {
|
|
6403
|
+
listItem.setAttribute(key, value);
|
|
6404
|
+
});
|
|
6405
|
+
listItem.dataset.checked = node.attrs.checked;
|
|
6406
|
+
checkbox.checked = node.attrs.checked;
|
|
6407
|
+
checkboxWrapper.append(checkbox, checkboxStyler);
|
|
6408
|
+
listItem.append(checkboxWrapper, content);
|
|
6409
|
+
Object.entries(HTMLAttributes).forEach(([key, value]) => {
|
|
6410
|
+
listItem.setAttribute(key, value);
|
|
6411
|
+
});
|
|
6412
|
+
return {
|
|
6413
|
+
dom: listItem,
|
|
6414
|
+
contentDOM: content,
|
|
6415
|
+
update: (updatedNode) => {
|
|
6416
|
+
if (updatedNode.type !== this.type) return false;
|
|
6417
|
+
listItem.dataset.checked = updatedNode.attrs.checked;
|
|
6418
|
+
checkbox.checked = updatedNode.attrs.checked;
|
|
6419
|
+
updateA11Y();
|
|
6420
|
+
return true;
|
|
6421
|
+
}
|
|
6422
|
+
};
|
|
6423
|
+
};
|
|
6424
|
+
},
|
|
6425
|
+
addInputRules() {
|
|
6426
|
+
return [wrappingInputRule({
|
|
6427
|
+
find: inputRegex,
|
|
6428
|
+
type: this.type,
|
|
6429
|
+
getAttributes: (match) => ({ checked: match[match.length - 1] === "x" })
|
|
6430
|
+
})];
|
|
6431
|
+
}
|
|
6432
|
+
});
|
|
6433
|
+
//#endregion
|
|
6434
|
+
//#region src/editor/extensions/task-item.ts
|
|
6435
|
+
var taskSequence = 0;
|
|
6436
|
+
/** A task item's accessible name: its text, or "Task" while it has none. */
|
|
6437
|
+
var taskName = (node) => node.textContent.trim() || "Task";
|
|
6438
|
+
/**
|
|
6439
|
+
* Tiptap's task item with each checkbox named by its item's text, so a screen reader announces
|
|
6440
|
+
* the task rather than an unnamed checkbox. In the editor the checkbox points at the item's own
|
|
6441
|
+
* content with `aria-labelledby`, so the name follows every edit; saved HTML has no element to point
|
|
6442
|
+
* at, so its checkbox carries the text as `aria-label`.
|
|
6443
|
+
*/
|
|
6444
|
+
var LoomaTaskItem = TaskItem.extend({
|
|
6445
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
6446
|
+
return [
|
|
6447
|
+
"li",
|
|
6448
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }),
|
|
6449
|
+
[
|
|
6450
|
+
"label",
|
|
6451
|
+
["input", {
|
|
6452
|
+
type: "checkbox",
|
|
6453
|
+
checked: node.attrs.checked ? "checked" : null,
|
|
6454
|
+
"aria-label": taskName(node)
|
|
6455
|
+
}],
|
|
6456
|
+
["span"]
|
|
6457
|
+
],
|
|
6458
|
+
["div", 0]
|
|
6459
|
+
];
|
|
6460
|
+
},
|
|
6461
|
+
addNodeView() {
|
|
6462
|
+
const parent = this.parent();
|
|
6463
|
+
return (props) => {
|
|
6464
|
+
const view = parent(props);
|
|
6465
|
+
const checkbox = view.dom instanceof Element ? view.dom.querySelector("input[type=\"checkbox\"]") : null;
|
|
6466
|
+
const content = view.contentDOM;
|
|
6467
|
+
if (!checkbox || !(content instanceof HTMLElement)) return view;
|
|
6468
|
+
content.id ||= `looma-task-${++taskSequence}`;
|
|
6469
|
+
const name = (node) => {
|
|
6470
|
+
if (node.textContent.trim()) {
|
|
6471
|
+
checkbox.setAttribute("aria-labelledby", content.id);
|
|
6472
|
+
checkbox.removeAttribute("aria-label");
|
|
6473
|
+
} else {
|
|
6474
|
+
checkbox.removeAttribute("aria-labelledby");
|
|
6475
|
+
checkbox.setAttribute("aria-label", "Task");
|
|
6476
|
+
}
|
|
6477
|
+
};
|
|
6478
|
+
name(props.node);
|
|
6479
|
+
return {
|
|
6480
|
+
...view,
|
|
6481
|
+
update: (...args) => {
|
|
6482
|
+
const kept = view.update?.(...args) ?? false;
|
|
6483
|
+
if (kept) name(args[0]);
|
|
6484
|
+
return kept;
|
|
6485
|
+
}
|
|
6486
|
+
};
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
});
|
|
6490
|
+
//#endregion
|
|
6434
6491
|
//#region src/editor/extensions/list-behavior.ts
|
|
6435
6492
|
/**
|
|
6436
6493
|
* Looma list behavior extension:
|
|
@@ -10900,7 +10957,7 @@ function getDefaultEditorExtensions(options = {}) {
|
|
|
10900
10957
|
OrderedList,
|
|
10901
10958
|
ListItem,
|
|
10902
10959
|
TaskList,
|
|
10903
|
-
|
|
10960
|
+
LoomaTaskItem.configure({ nested: false }),
|
|
10904
10961
|
Blockquote,
|
|
10905
10962
|
calloutNodeView ? LoomaCallout.extend({ addNodeView: () => calloutNodeView }) : LoomaCallout,
|
|
10906
10963
|
LoomaChip,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiptap's task item with each checkbox named by its item's text, so a screen reader announces
|
|
3
|
+
* the task rather than an unnamed checkbox. In the editor the checkbox points at the item's own
|
|
4
|
+
* content with `aria-labelledby`, so the name follows every edit; saved HTML has no element to point
|
|
5
|
+
* at, so its checkbox carries the text as `aria-label`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LoomaTaskItem: import("@tiptap/core").Node<import("@tiptap/extension-task-item").TaskItemOptions, any>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import { type LoomaImageDescriptor } from "./image-delivery.js";
|
|
3
|
+
/**
|
|
4
|
+
* Host upload result normalized into the editor's durable image descriptor.
|
|
5
|
+
* `url` becomes the stored `src`; optional dimensions preserve layout before
|
|
6
|
+
* the image loads and `responsive` opts into host-provided rendition policy.
|
|
7
|
+
*/
|
|
8
|
+
export interface LoomaImageUploadResult extends Omit<LoomaImageDescriptor, "src"> {
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Application-owned upload boundary used by paste, drop, and file selection.
|
|
13
|
+
*
|
|
14
|
+
* @ownership The application owns network requests, persistence, and returned
|
|
15
|
+
* URLs. Looma owns only editor insertion after the promise fulfills.
|
|
16
|
+
* @failure Rejection leaves the document unchanged and is surfaced through the
|
|
17
|
+
* editor's `uploadError` event so the host chooses retry and user messaging.
|
|
18
|
+
*/
|
|
19
|
+
export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUploadResult>;
|
|
20
|
+
export interface LoomaImageUploadsState {
|
|
21
|
+
readonly uploading: boolean;
|
|
22
|
+
/** The file whose upload failed last, which `retry()` sends again. */
|
|
23
|
+
readonly failed: File | null;
|
|
24
|
+
}
|
|
25
|
+
export interface LoomaImageUploadsOptions {
|
|
26
|
+
editor: Editor;
|
|
27
|
+
/** Read on each upload, so a host can change it while the editor runs. */
|
|
28
|
+
upload: () => LoomaImageUploader | null | undefined;
|
|
29
|
+
onError: (error: unknown, file: File) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The editor's image uploads: each file goes to the host's uploader in turn, and the image it
|
|
33
|
+
* returns is inserted at the selection.
|
|
34
|
+
*
|
|
35
|
+
* @ownership The host owns the upload and its URL; this owns the uploading state, the failed file,
|
|
36
|
+
* and inserting the result.
|
|
37
|
+
* @failure A rejected upload leaves the document unchanged, records the file for `retry()`, and is
|
|
38
|
+
* reported through `onError`.
|
|
39
|
+
*/
|
|
40
|
+
export interface LoomaImageUploads {
|
|
41
|
+
readonly state: LoomaImageUploadsState;
|
|
42
|
+
/** Calls `listener` after every state change; returns the unsubscribe function. */
|
|
43
|
+
subscribe(listener: () => void): () => void;
|
|
44
|
+
/** Uploads each file in turn and inserts its image; does nothing without an uploader. */
|
|
45
|
+
insert(files: Iterable<File>): Promise<void>;
|
|
46
|
+
/** Sends the failed file again. */
|
|
47
|
+
retry(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare function createImageUploads(options: LoomaImageUploadsOptions): LoomaImageUploads;
|
package/package.json
CHANGED
|
@@ -3,25 +3,10 @@ import "./looma-editor.css";
|
|
|
3
3
|
import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
|
|
4
4
|
import { type PropType, type VNode } from "vue";
|
|
5
5
|
import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaMentionProvider, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
|
|
6
|
-
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageSourceResolver, type
|
|
6
|
+
import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageSourceResolver, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
|
|
7
|
+
import { type LoomaImageUploader } from "../../editor/image-uploads.js";
|
|
7
8
|
import { type LoomaLinkResolve, type LoomaLinkSearch } from "../../editor/link-editing.js";
|
|
8
|
-
|
|
9
|
-
* Host upload result normalized into the editor's durable image descriptor.
|
|
10
|
-
* `url` becomes the stored `src`; optional dimensions preserve layout before
|
|
11
|
-
* the image loads and `responsive` opts into host-provided rendition policy.
|
|
12
|
-
*/
|
|
13
|
-
export interface LoomaImageUploadResult extends Omit<LoomaImageDescriptor, "src"> {
|
|
14
|
-
url: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Application-owned upload boundary used by paste, drop, and file selection.
|
|
18
|
-
*
|
|
19
|
-
* @ownership The application owns network requests, persistence, and returned
|
|
20
|
-
* URLs. Looma owns only editor insertion after the promise fulfills.
|
|
21
|
-
* @failure Rejection leaves the document unchanged and is surfaced through the
|
|
22
|
-
* editor's `uploadError` event so the host chooses retry and user messaging.
|
|
23
|
-
*/
|
|
24
|
-
export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUploadResult>;
|
|
9
|
+
export type { LoomaImageUploader, LoomaImageUploadResult } from "../../editor/image-uploads.js";
|
|
25
10
|
/**
|
|
26
11
|
* Chooses whether formatting controls follow a selection, occupy persistent
|
|
27
12
|
* editor chrome, or open from an app-owned button (`popover`, with a text-only
|
package/vue/editor/index.js
CHANGED
|
@@ -1138,6 +1138,64 @@ function runBlockAction(editor, action) {
|
|
|
1138
1138
|
return true;
|
|
1139
1139
|
}
|
|
1140
1140
|
//#endregion
|
|
1141
|
+
//#region src/editor/image-uploads.ts
|
|
1142
|
+
function createImageUploads(options) {
|
|
1143
|
+
const { editor } = options;
|
|
1144
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1145
|
+
let state = {
|
|
1146
|
+
uploading: false,
|
|
1147
|
+
failed: null
|
|
1148
|
+
};
|
|
1149
|
+
const set = (patch) => {
|
|
1150
|
+
state = {
|
|
1151
|
+
...state,
|
|
1152
|
+
...patch
|
|
1153
|
+
};
|
|
1154
|
+
listeners.forEach((listener) => listener());
|
|
1155
|
+
};
|
|
1156
|
+
const insertOne = async (file) => {
|
|
1157
|
+
const upload = options.upload();
|
|
1158
|
+
if (!upload || editor.isDestroyed) return;
|
|
1159
|
+
set({ uploading: true });
|
|
1160
|
+
try {
|
|
1161
|
+
const result = await upload(file);
|
|
1162
|
+
const image = typeof result === "string" ? { url: result } : result;
|
|
1163
|
+
const width = normalizeImageDimension(image.width);
|
|
1164
|
+
const height = normalizeImageDimension(image.height);
|
|
1165
|
+
if (!editor.isDestroyed) editor.chain().focus().insertContent(imageUploadContent({
|
|
1166
|
+
src: image.url,
|
|
1167
|
+
alt: image.alt ?? file.name,
|
|
1168
|
+
...width ? { width } : {},
|
|
1169
|
+
...height ? { height } : {},
|
|
1170
|
+
...image.responsive === true ? { responsive: true } : {}
|
|
1171
|
+
})).run();
|
|
1172
|
+
set({ failed: null });
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
set({ failed: file });
|
|
1175
|
+
options.onError(error, file);
|
|
1176
|
+
} finally {
|
|
1177
|
+
set({ uploading: false });
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
return {
|
|
1181
|
+
get state() {
|
|
1182
|
+
return state;
|
|
1183
|
+
},
|
|
1184
|
+
subscribe(listener) {
|
|
1185
|
+
listeners.add(listener);
|
|
1186
|
+
return () => {
|
|
1187
|
+
listeners.delete(listener);
|
|
1188
|
+
};
|
|
1189
|
+
},
|
|
1190
|
+
async insert(files) {
|
|
1191
|
+
for (const file of files) await insertOne(file);
|
|
1192
|
+
},
|
|
1193
|
+
async retry() {
|
|
1194
|
+
if (state.failed) await insertOne(state.failed);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
//#endregion
|
|
1141
1199
|
//#region src/editor/formatting-selection.ts
|
|
1142
1200
|
var HOLD_MS = 500;
|
|
1143
1201
|
var HOLD_SLOP_PX = 8;
|
|
@@ -3025,8 +3083,11 @@ var LoomaEditor = defineComponent({
|
|
|
3025
3083
|
const mobileToolbarShell = ref(null);
|
|
3026
3084
|
const codeLanguageShell = ref(null);
|
|
3027
3085
|
const dragOver = ref(false);
|
|
3028
|
-
|
|
3029
|
-
const
|
|
3086
|
+
let uploads;
|
|
3087
|
+
const uploadState = shallowRef({
|
|
3088
|
+
uploading: false,
|
|
3089
|
+
failed: null
|
|
3090
|
+
});
|
|
3030
3091
|
const tablePickerOpen = ref(false);
|
|
3031
3092
|
const mobileToolbarStyle = ref({});
|
|
3032
3093
|
const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
|
|
@@ -3263,6 +3324,15 @@ var LoomaEditor = defineComponent({
|
|
|
3263
3324
|
linkState.value = engine.state;
|
|
3264
3325
|
});
|
|
3265
3326
|
linkEditing = engine;
|
|
3327
|
+
const imageUploads = createImageUploads({
|
|
3328
|
+
editor: session.editor,
|
|
3329
|
+
upload: () => props.uploadImage,
|
|
3330
|
+
onError: (error, file) => emit("uploadError", error, file)
|
|
3331
|
+
});
|
|
3332
|
+
imageUploads.subscribe(() => {
|
|
3333
|
+
uploadState.value = imageUploads.state;
|
|
3334
|
+
});
|
|
3335
|
+
uploads = imageUploads;
|
|
3266
3336
|
const instance = session.editor;
|
|
3267
3337
|
formatting = createFormattingSelection({
|
|
3268
3338
|
editor: instance,
|
|
@@ -3867,41 +3937,18 @@ var LoomaEditor = defineComponent({
|
|
|
3867
3937
|
if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
|
|
3868
3938
|
if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
|
|
3869
3939
|
});
|
|
3870
|
-
const insertImage = async (file) => {
|
|
3871
|
-
if (!props.uploadImage || !editor.value) return;
|
|
3872
|
-
uploading.value = true;
|
|
3873
|
-
try {
|
|
3874
|
-
const result = await props.uploadImage(file);
|
|
3875
|
-
const image = typeof result === "string" ? { url: result } : result;
|
|
3876
|
-
const width = normalizeImageDimension(image.width);
|
|
3877
|
-
const height = normalizeImageDimension(image.height);
|
|
3878
|
-
editor.value.chain().focus().insertContent(imageUploadContent({
|
|
3879
|
-
src: image.url,
|
|
3880
|
-
alt: image.alt ?? file.name,
|
|
3881
|
-
...width ? { width } : {},
|
|
3882
|
-
...height ? { height } : {},
|
|
3883
|
-
...image.responsive === true ? { responsive: true } : {}
|
|
3884
|
-
})).run();
|
|
3885
|
-
failedUpload.value = null;
|
|
3886
|
-
} catch (error) {
|
|
3887
|
-
failedUpload.value = { file };
|
|
3888
|
-
emit("uploadError", error, file);
|
|
3889
|
-
} finally {
|
|
3890
|
-
uploading.value = false;
|
|
3891
|
-
}
|
|
3892
|
-
};
|
|
3893
3940
|
const onFileChange = async (event) => {
|
|
3894
3941
|
const input = event.target;
|
|
3895
3942
|
const files = Array.from(input.files ?? []);
|
|
3896
3943
|
input.value = "";
|
|
3897
|
-
|
|
3944
|
+
await uploads?.insert(files);
|
|
3898
3945
|
};
|
|
3899
3946
|
const onDrop = async (event) => {
|
|
3900
3947
|
event.preventDefault();
|
|
3901
3948
|
dragOver.value = false;
|
|
3902
3949
|
if (!props.editable) return;
|
|
3903
3950
|
const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
3904
|
-
|
|
3951
|
+
await uploads?.insert(files);
|
|
3905
3952
|
};
|
|
3906
3953
|
const onContextMenu = (event) => {
|
|
3907
3954
|
const instance = editor.value;
|
|
@@ -4158,7 +4205,7 @@ var LoomaEditor = defineComponent({
|
|
|
4158
4205
|
variant: tablePickerOpen.value ? "solid" : "ghost",
|
|
4159
4206
|
"aria-expanded": tablePickerOpen.value ? "true" : "false"
|
|
4160
4207
|
}, () => loomaIcon("table")),
|
|
4161
|
-
commandButton(
|
|
4208
|
+
commandButton(uploadState.value.uploading ? "Uploading image" : "Insert image", "image", false, uploadState.value.uploading || !props.uploadImage, () => fileInput.value?.click()),
|
|
4162
4209
|
h("span", {
|
|
4163
4210
|
class: "divider",
|
|
4164
4211
|
"aria-hidden": "true"
|
|
@@ -4349,17 +4396,16 @@ var LoomaEditor = defineComponent({
|
|
|
4349
4396
|
"aria-hidden": "true",
|
|
4350
4397
|
onChange: onFileChange
|
|
4351
4398
|
}),
|
|
4352
|
-
|
|
4399
|
+
uploadState.value.failed ? h("div", {
|
|
4353
4400
|
class: "looma-editor__upload-error",
|
|
4354
4401
|
role: "alert"
|
|
4355
4402
|
}, [h("span", "Couldn’t upload image."), h("button", {
|
|
4356
4403
|
type: "button",
|
|
4357
|
-
disabled:
|
|
4404
|
+
disabled: uploadState.value.uploading,
|
|
4358
4405
|
onClick: () => {
|
|
4359
|
-
|
|
4360
|
-
if (attempt) insertImage(attempt.file);
|
|
4406
|
+
uploads?.retry();
|
|
4361
4407
|
}
|
|
4362
|
-
},
|
|
4408
|
+
}, uploadState.value.uploading ? "Retrying…" : "Retry")]) : null,
|
|
4363
4409
|
dragOver.value ? h("div", {
|
|
4364
4410
|
class: "looma-editor__drop-overlay",
|
|
4365
4411
|
"aria-hidden": "true"
|