@unhingged/vizu-core 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto.cjs +337 -49
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +337 -49
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +337 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +337 -49
- package/dist/index.js.map +1 -1
- package/dist/vizu.min.js +98 -63
- package/dist/vizu.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1239,8 +1239,50 @@ var Popover = class {
|
|
|
1239
1239
|
this.mentionPicker = null;
|
|
1240
1240
|
/** Dismiss handlers wired to document for the open picker; cleared in closeMentionPicker. */
|
|
1241
1241
|
this.mentionPickerDismiss = null;
|
|
1242
|
+
/** All mentionable users fetched for the open picker — pre-filter list. */
|
|
1243
|
+
this.mentionPickerUsers = [];
|
|
1244
|
+
/** Currently visible subset of `mentionPickerUsers` after the user's filter. */
|
|
1245
|
+
this.mentionPickerFiltered = [];
|
|
1246
|
+
/** Highlighted index in `mentionPickerFiltered`. Up/Down arrows mutate this. */
|
|
1247
|
+
this.mentionPickerSelected = 0;
|
|
1248
|
+
/**
|
|
1249
|
+
* 'manual' = opened from the @ mention button (separate search input).
|
|
1250
|
+
* 'editor' = opened by typing `@` in the editor; filter is derived
|
|
1251
|
+
* from the text after `@` in the editor itself, no search input.
|
|
1252
|
+
*/
|
|
1253
|
+
this.mentionPickerMode = "manual";
|
|
1254
|
+
/**
|
|
1255
|
+
* In editor mode: a Range covering `@<query>` in the editor's text.
|
|
1256
|
+
* Used to replace that span with the mention chip on commit, and to
|
|
1257
|
+
* recompute the bounding rect when re-positioning after typing.
|
|
1258
|
+
*/
|
|
1259
|
+
this.mentionPickerEditorRange = null;
|
|
1242
1260
|
this.handleEditorKey = (e) => {
|
|
1243
1261
|
const editor = e.currentTarget;
|
|
1262
|
+
if (this.mentionPicker && this.mentionPickerMode === "editor") {
|
|
1263
|
+
if (e.key === "ArrowDown") {
|
|
1264
|
+
e.preventDefault();
|
|
1265
|
+
this.moveMentionPickerSelection(1);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (e.key === "ArrowUp") {
|
|
1269
|
+
e.preventDefault();
|
|
1270
|
+
this.moveMentionPickerSelection(-1);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
1274
|
+
if (this.mentionPickerFiltered.length > 0) {
|
|
1275
|
+
e.preventDefault();
|
|
1276
|
+
this.commitMentionPickerSelection();
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (e.key === "Escape") {
|
|
1281
|
+
e.preventDefault();
|
|
1282
|
+
this.closeMentionPicker();
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1244
1286
|
if (e.key === "#") {
|
|
1245
1287
|
e.preventDefault();
|
|
1246
1288
|
const savedRange = this.saveRange();
|
|
@@ -1263,6 +1305,21 @@ var Popover = class {
|
|
|
1263
1305
|
this.callbacks.onClose();
|
|
1264
1306
|
}
|
|
1265
1307
|
};
|
|
1308
|
+
/**
|
|
1309
|
+
* Fires after every keystroke in the editor (post-insertion). Looks
|
|
1310
|
+
* for an `@<query>` pattern adjacent to the caret. If found, opens
|
|
1311
|
+
* (or refreshes) the mention picker in editor mode. If the trigger
|
|
1312
|
+
* disappears (user typed a space, backspaced over @, etc.), closes
|
|
1313
|
+
* the picker.
|
|
1314
|
+
*/
|
|
1315
|
+
this.handleEditorInput = () => {
|
|
1316
|
+
const trigger = this.detectMentionTrigger();
|
|
1317
|
+
if (trigger) {
|
|
1318
|
+
void this.openOrUpdateEditorMentionPicker(trigger.range, trigger.query);
|
|
1319
|
+
} else if (this.mentionPicker && this.mentionPickerMode === "editor") {
|
|
1320
|
+
this.closeMentionPicker();
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1266
1323
|
this.handleEditorPaste = (e) => {
|
|
1267
1324
|
if (this.callbacks.canUploadAttachments()) {
|
|
1268
1325
|
const items = e.clipboardData?.items ?? null;
|
|
@@ -1523,6 +1580,7 @@ var Popover = class {
|
|
|
1523
1580
|
const editor = this.el.querySelector(".vz-textarea");
|
|
1524
1581
|
editor.focus();
|
|
1525
1582
|
editor.addEventListener("keydown", this.handleEditorKey);
|
|
1583
|
+
editor.addEventListener("input", this.handleEditorInput);
|
|
1526
1584
|
editor.addEventListener("paste", this.handleEditorPaste);
|
|
1527
1585
|
const wrap = this.el.querySelector(".vz-editor-wrap");
|
|
1528
1586
|
if (wrap && this.callbacks.canUploadAttachments()) {
|
|
@@ -1552,6 +1610,37 @@ var Popover = class {
|
|
|
1552
1610
|
<div class="vz-anchor-list">${chips}</div>
|
|
1553
1611
|
`;
|
|
1554
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Walk back from the caret looking for the most recent `@` that
|
|
1615
|
+
* starts a mention trigger — must be at start-of-text-node or
|
|
1616
|
+
* preceded by whitespace, and there must be no whitespace between
|
|
1617
|
+
* `@` and the caret. Returns the Range covering `@<query>` plus
|
|
1618
|
+
* the bare query string.
|
|
1619
|
+
*/
|
|
1620
|
+
detectMentionTrigger() {
|
|
1621
|
+
const sel = window.getSelection();
|
|
1622
|
+
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
|
|
1623
|
+
const caret = sel.getRangeAt(0);
|
|
1624
|
+
const node = caret.startContainer;
|
|
1625
|
+
if (node.nodeType !== Node.TEXT_NODE) return null;
|
|
1626
|
+
const text = node.textContent ?? "";
|
|
1627
|
+
const offset = caret.startOffset;
|
|
1628
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
1629
|
+
const ch = text[i];
|
|
1630
|
+
if (ch === "@") {
|
|
1631
|
+
const prev = i > 0 ? text[i - 1] : "";
|
|
1632
|
+
if (prev !== "" && !/\s/.test(prev)) return null;
|
|
1633
|
+
const query = text.slice(i + 1, offset);
|
|
1634
|
+
if (/\s/.test(query)) return null;
|
|
1635
|
+
const range = document.createRange();
|
|
1636
|
+
range.setStart(node, i);
|
|
1637
|
+
range.setEnd(node, offset);
|
|
1638
|
+
return { range, query };
|
|
1639
|
+
}
|
|
1640
|
+
if (/\s/.test(ch)) return null;
|
|
1641
|
+
}
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1555
1644
|
async uploadAndAppend(file) {
|
|
1556
1645
|
if (!this.callbacks.canUploadAttachments()) return;
|
|
1557
1646
|
const placeholderId = "upload-" + Math.random().toString(36).slice(2, 9);
|
|
@@ -1682,54 +1771,195 @@ var Popover = class {
|
|
|
1682
1771
|
return parts.join("").trim();
|
|
1683
1772
|
}
|
|
1684
1773
|
/**
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
* in another tab are picked up without a popover reopen. Falls back
|
|
1691
|
-
* to silent no-op if `onMentionSearch` isn't wired (non-cloud
|
|
1692
|
-
* adapters) or returns an empty list.
|
|
1774
|
+
* Manual-mode picker: opens from the @ mention button. Renders a
|
|
1775
|
+
* search input above the list; the user types into the input to
|
|
1776
|
+
* filter, arrows + Enter to pick. The selection inserts a mention
|
|
1777
|
+
* chip at the editor's current caret (no `@<query>` replacement —
|
|
1778
|
+
* the user didn't type one).
|
|
1693
1779
|
*/
|
|
1694
1780
|
async openMentionPicker(anchor) {
|
|
1695
1781
|
this.closeMentionPicker();
|
|
1696
1782
|
if (!this.callbacks.onMentionSearch) return;
|
|
1783
|
+
this.mentionPickerMode = "manual";
|
|
1784
|
+
this.mentionPickerEditorRange = null;
|
|
1785
|
+
const picker = this.createMentionPickerShell();
|
|
1786
|
+
this.positionMentionPickerByElement(picker, anchor);
|
|
1787
|
+
const users = await this.fetchMentionables();
|
|
1788
|
+
if (this.mentionPicker !== picker) return;
|
|
1789
|
+
this.mentionPickerUsers = users;
|
|
1790
|
+
this.renderMentionPickerBody("");
|
|
1791
|
+
this.positionMentionPickerByElement(picker, anchor);
|
|
1792
|
+
const input = picker.querySelector(".vz-mention-search");
|
|
1793
|
+
input?.focus();
|
|
1794
|
+
this.wireMentionPickerDismiss();
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Editor-mode picker: opens because the user typed `@` in the
|
|
1798
|
+
* editor. No search input — the editor IS the search; the query is
|
|
1799
|
+
* the text after `@`. On commit, replaces the `@<query>` range with
|
|
1800
|
+
* the chip.
|
|
1801
|
+
*/
|
|
1802
|
+
async openOrUpdateEditorMentionPicker(range, query) {
|
|
1803
|
+
this.mentionPickerEditorRange = range;
|
|
1804
|
+
if (!this.mentionPicker || this.mentionPickerMode !== "editor") {
|
|
1805
|
+
this.closeMentionPicker();
|
|
1806
|
+
this.mentionPickerMode = "editor";
|
|
1807
|
+
const picker = this.createMentionPickerShell({ showSearch: false });
|
|
1808
|
+
this.positionMentionPickerByRange(picker, range);
|
|
1809
|
+
const users = await this.fetchMentionables();
|
|
1810
|
+
if (this.mentionPicker !== picker) return;
|
|
1811
|
+
this.mentionPickerUsers = users;
|
|
1812
|
+
this.renderMentionPickerBody(query);
|
|
1813
|
+
this.positionMentionPickerByRange(picker, range);
|
|
1814
|
+
this.wireMentionPickerDismiss();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
this.renderMentionPickerBody(query);
|
|
1818
|
+
this.positionMentionPickerByRange(this.mentionPicker, range);
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Build the picker DOM scaffold and attach it to the popover. Does
|
|
1822
|
+
* NOT fetch users or render the body — caller's responsibility.
|
|
1823
|
+
* Search input is hidden in editor mode where the user already has
|
|
1824
|
+
* a perfectly good text-entry surface (the editor itself).
|
|
1825
|
+
*/
|
|
1826
|
+
createMentionPickerShell({ showSearch = true } = {}) {
|
|
1697
1827
|
const picker = document.createElement("div");
|
|
1698
1828
|
picker.className = "vz-mention-picker";
|
|
1699
1829
|
picker.setAttribute("data-vz-ignore", "");
|
|
1700
|
-
picker.innerHTML =
|
|
1830
|
+
picker.innerHTML = `
|
|
1831
|
+
${showSearch ? `<input class="vz-mention-search" type="text" placeholder="Search teammates\u2026" autocomplete="off" spellcheck="false" />` : ""}
|
|
1832
|
+
<div class="vz-mention-list" data-vz="mention-list">
|
|
1833
|
+
<div class="vz-mention-loading">Loading\u2026</div>
|
|
1834
|
+
</div>
|
|
1835
|
+
`;
|
|
1701
1836
|
this.el.appendChild(picker);
|
|
1702
1837
|
this.mentionPicker = picker;
|
|
1703
|
-
this.
|
|
1704
|
-
|
|
1838
|
+
this.mentionPickerFiltered = [];
|
|
1839
|
+
this.mentionPickerSelected = 0;
|
|
1840
|
+
picker.addEventListener("mousemove", (e) => {
|
|
1841
|
+
const btn = e.target?.closest(".vz-mention-item");
|
|
1842
|
+
if (!btn) return;
|
|
1843
|
+
const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
|
|
1844
|
+
if (idx >= 0 && idx !== this.mentionPickerSelected) {
|
|
1845
|
+
this.mentionPickerSelected = idx;
|
|
1846
|
+
this.refreshMentionPickerHighlight();
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
picker.addEventListener("click", (e) => {
|
|
1850
|
+
const btn = e.target?.closest(".vz-mention-item");
|
|
1851
|
+
if (!btn) return;
|
|
1852
|
+
e.preventDefault();
|
|
1853
|
+
e.stopPropagation();
|
|
1854
|
+
const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
|
|
1855
|
+
if (idx >= 0) {
|
|
1856
|
+
this.mentionPickerSelected = idx;
|
|
1857
|
+
this.commitMentionPickerSelection();
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
if (showSearch) {
|
|
1861
|
+
const input = picker.querySelector(".vz-mention-search");
|
|
1862
|
+
input?.addEventListener("input", () => this.renderMentionPickerBody(input.value));
|
|
1863
|
+
input?.addEventListener("keydown", (e) => {
|
|
1864
|
+
if (e.key === "ArrowDown") {
|
|
1865
|
+
e.preventDefault();
|
|
1866
|
+
this.moveMentionPickerSelection(1);
|
|
1867
|
+
} else if (e.key === "ArrowUp") {
|
|
1868
|
+
e.preventDefault();
|
|
1869
|
+
this.moveMentionPickerSelection(-1);
|
|
1870
|
+
} else if (e.key === "Enter" || e.key === "Tab") {
|
|
1871
|
+
if (this.mentionPickerFiltered.length > 0) {
|
|
1872
|
+
e.preventDefault();
|
|
1873
|
+
this.commitMentionPickerSelection();
|
|
1874
|
+
}
|
|
1875
|
+
} else if (e.key === "Escape") {
|
|
1876
|
+
e.preventDefault();
|
|
1877
|
+
this.closeMentionPicker();
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
return picker;
|
|
1882
|
+
}
|
|
1883
|
+
/** Fetch the mentionable list. Swallows errors → []. */
|
|
1884
|
+
async fetchMentionables() {
|
|
1885
|
+
if (!this.callbacks.onMentionSearch) return [];
|
|
1705
1886
|
try {
|
|
1706
|
-
|
|
1887
|
+
return await this.callbacks.onMentionSearch();
|
|
1707
1888
|
} catch {
|
|
1889
|
+
return [];
|
|
1708
1890
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Re-filter the cached users by `query`, render the list, reset
|
|
1894
|
+
* selected to 0. Called on every search input change and on every
|
|
1895
|
+
* editor input event while in editor mode.
|
|
1896
|
+
*/
|
|
1897
|
+
renderMentionPickerBody(query) {
|
|
1898
|
+
const picker = this.mentionPicker;
|
|
1899
|
+
if (!picker) return;
|
|
1900
|
+
const list = picker.querySelector('[data-vz="mention-list"]');
|
|
1901
|
+
if (!list) return;
|
|
1902
|
+
const q = query.toLowerCase().trim();
|
|
1903
|
+
this.mentionPickerFiltered = q ? this.mentionPickerUsers.filter((u) => u.name.toLowerCase().includes(q)) : this.mentionPickerUsers.slice();
|
|
1904
|
+
this.mentionPickerSelected = 0;
|
|
1905
|
+
if (this.mentionPickerFiltered.length === 0) {
|
|
1906
|
+
list.innerHTML = `<div class="vz-mention-empty">${this.mentionPickerUsers.length === 0 ? "No teammates to mention yet." : "No teammates match."}</div>`;
|
|
1714
1907
|
return;
|
|
1715
1908
|
}
|
|
1716
|
-
|
|
1909
|
+
list.innerHTML = this.mentionPickerFiltered.map((u, i) => {
|
|
1717
1910
|
const avatar = u.avatarUrl ? `<img class="vz-mention-avatar" src="${escapeHtml(u.avatarUrl)}" alt="" />` : `<span class="vz-mention-avatar vz-mention-avatar-initials">${escapeHtml(initials(u.name) || "?")}</span>`;
|
|
1718
|
-
return `<button class="vz-mention-item"
|
|
1911
|
+
return `<button class="vz-mention-item${i === 0 ? " is-selected" : ""}" type="button" data-mention-index="${i}">${avatar}<span class="vz-mention-item-name">${escapeHtml(u.name)}</span></button>`;
|
|
1719
1912
|
}).join("");
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
this.
|
|
1913
|
+
}
|
|
1914
|
+
moveMentionPickerSelection(delta) {
|
|
1915
|
+
if (this.mentionPickerFiltered.length === 0) return;
|
|
1916
|
+
const n = this.mentionPickerFiltered.length;
|
|
1917
|
+
this.mentionPickerSelected = (this.mentionPickerSelected + delta + n) % n;
|
|
1918
|
+
this.refreshMentionPickerHighlight();
|
|
1919
|
+
}
|
|
1920
|
+
refreshMentionPickerHighlight() {
|
|
1921
|
+
const picker = this.mentionPicker;
|
|
1922
|
+
if (!picker) return;
|
|
1923
|
+
const items = picker.querySelectorAll(".vz-mention-item");
|
|
1924
|
+
items.forEach((el, i) => el.classList.toggle("is-selected", i === this.mentionPickerSelected));
|
|
1925
|
+
items[this.mentionPickerSelected]?.scrollIntoView({ block: "nearest" });
|
|
1926
|
+
}
|
|
1927
|
+
commitMentionPickerSelection() {
|
|
1928
|
+
const user = this.mentionPickerFiltered[this.mentionPickerSelected];
|
|
1929
|
+
if (!user) return;
|
|
1930
|
+
if (this.mentionPickerMode === "editor" && this.mentionPickerEditorRange) {
|
|
1931
|
+
this.replaceRangeWithMentionChip(this.mentionPickerEditorRange, user);
|
|
1932
|
+
} else {
|
|
1933
|
+
this.insertMentionChip(user);
|
|
1934
|
+
}
|
|
1935
|
+
this.closeMentionPicker();
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Swap a Range covering `@<query>` for a mention chip + trailing
|
|
1939
|
+
* space, then place the caret after the space so the user can keep
|
|
1940
|
+
* typing. Used only in editor-trigger mode.
|
|
1941
|
+
*/
|
|
1942
|
+
replaceRangeWithMentionChip(range, user) {
|
|
1943
|
+
const editor = this.el.querySelector(".vz-textarea");
|
|
1944
|
+
if (!editor) return;
|
|
1945
|
+
const chip = document.createElement("span");
|
|
1946
|
+
chip.className = "vz-chip vz-chip-mention";
|
|
1947
|
+
chip.contentEditable = "false";
|
|
1948
|
+
chip.setAttribute("data-mention-id", user.id);
|
|
1949
|
+
chip.textContent = "@" + user.name;
|
|
1950
|
+
range.deleteContents();
|
|
1951
|
+
range.insertNode(chip);
|
|
1952
|
+
const space = document.createTextNode(" ");
|
|
1953
|
+
chip.after(space);
|
|
1954
|
+
const sel = window.getSelection();
|
|
1955
|
+
if (sel) {
|
|
1956
|
+
const after = document.createRange();
|
|
1957
|
+
after.setStartAfter(space);
|
|
1958
|
+
after.collapse(true);
|
|
1959
|
+
sel.removeAllRanges();
|
|
1960
|
+
sel.addRange(after);
|
|
1961
|
+
}
|
|
1962
|
+
editor.focus();
|
|
1733
1963
|
}
|
|
1734
1964
|
/** Wires Esc + outside-click handlers to close the open picker. */
|
|
1735
1965
|
wireMentionPickerDismiss() {
|
|
@@ -1743,6 +1973,10 @@ var Popover = class {
|
|
|
1743
1973
|
const onOutside = (e) => {
|
|
1744
1974
|
if (!this.mentionPicker) return;
|
|
1745
1975
|
if (this.mentionPicker.contains(e.target)) return;
|
|
1976
|
+
if (this.mentionPickerMode === "editor") {
|
|
1977
|
+
const editor = this.el.querySelector(".vz-textarea");
|
|
1978
|
+
if (editor && editor.contains(e.target)) return;
|
|
1979
|
+
}
|
|
1746
1980
|
this.closeMentionPicker();
|
|
1747
1981
|
};
|
|
1748
1982
|
document.addEventListener("keydown", onKey, true);
|
|
@@ -1761,18 +1995,23 @@ var Popover = class {
|
|
|
1761
1995
|
this.mentionPicker.remove();
|
|
1762
1996
|
this.mentionPicker = null;
|
|
1763
1997
|
}
|
|
1998
|
+
this.mentionPickerFiltered = [];
|
|
1999
|
+
this.mentionPickerSelected = 0;
|
|
2000
|
+
this.mentionPickerEditorRange = null;
|
|
1764
2001
|
}
|
|
1765
|
-
/**
|
|
1766
|
-
|
|
1767
|
-
* popover's right edge so we don't overflow the popover frame.
|
|
1768
|
-
*/
|
|
1769
|
-
positionMentionPicker(picker, anchor) {
|
|
2002
|
+
/** Position the picker below the given anchor (the @ mention button). */
|
|
2003
|
+
positionMentionPickerByElement(picker, anchor) {
|
|
1770
2004
|
const popRect = this.el.getBoundingClientRect();
|
|
1771
2005
|
const btnRect = anchor.getBoundingClientRect();
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2006
|
+
picker.style.left = `${btnRect.left - popRect.left}px`;
|
|
2007
|
+
picker.style.top = `${btnRect.bottom - popRect.top + 4}px`;
|
|
2008
|
+
}
|
|
2009
|
+
/** Position the picker below the `@<query>` text range in the editor. */
|
|
2010
|
+
positionMentionPickerByRange(picker, range) {
|
|
2011
|
+
const popRect = this.el.getBoundingClientRect();
|
|
2012
|
+
const rect = range.getBoundingClientRect();
|
|
2013
|
+
picker.style.left = `${rect.left - popRect.left}px`;
|
|
2014
|
+
picker.style.top = `${rect.bottom - popRect.top + 4}px`;
|
|
1776
2015
|
}
|
|
1777
2016
|
/**
|
|
1778
2017
|
* Insert a mention chip for the given user into the editor at the
|
|
@@ -2651,6 +2890,26 @@ var STYLES = `
|
|
|
2651
2890
|
flex-direction: column;
|
|
2652
2891
|
gap: 1px;
|
|
2653
2892
|
}
|
|
2893
|
+
.vz-mention-search {
|
|
2894
|
+
width: 100%;
|
|
2895
|
+
padding: 7px 9px;
|
|
2896
|
+
margin-bottom: 4px;
|
|
2897
|
+
background: rgba(255,255,255,0.04);
|
|
2898
|
+
border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
|
|
2899
|
+
border-radius: 6px;
|
|
2900
|
+
color: #fafafa;
|
|
2901
|
+
font-size: 13px;
|
|
2902
|
+
outline: none;
|
|
2903
|
+
box-sizing: border-box;
|
|
2904
|
+
}
|
|
2905
|
+
.vz-mention-search:focus { border-color: var(--vz-accent, #ff8b6e); }
|
|
2906
|
+
.vz-mention-search::placeholder { color: var(--vz-text-muted); }
|
|
2907
|
+
.vz-mention-list {
|
|
2908
|
+
display: flex;
|
|
2909
|
+
flex-direction: column;
|
|
2910
|
+
gap: 1px;
|
|
2911
|
+
min-height: 0;
|
|
2912
|
+
}
|
|
2654
2913
|
.vz-mention-loading,
|
|
2655
2914
|
.vz-mention-empty {
|
|
2656
2915
|
padding: 10px 12px;
|
|
@@ -2672,9 +2931,9 @@ var STYLES = `
|
|
|
2672
2931
|
cursor: pointer;
|
|
2673
2932
|
transition: background 80ms;
|
|
2674
2933
|
}
|
|
2675
|
-
.vz-mention-item
|
|
2934
|
+
.vz-mention-item.is-selected,
|
|
2676
2935
|
.vz-mention-item:focus-visible {
|
|
2677
|
-
background: var(--vz-bg-hover, rgba(255,255,255,0.
|
|
2936
|
+
background: var(--vz-bg-hover, rgba(255,255,255,0.08));
|
|
2678
2937
|
outline: none;
|
|
2679
2938
|
}
|
|
2680
2939
|
.vz-mention-avatar {
|
|
@@ -2977,7 +3236,9 @@ var STYLES = `
|
|
|
2977
3236
|
.vz-ref:hover { background: color-mix(in srgb, var(--vz-accent) 30%, var(--vz-bg-hover)); transform: translateY(-1px); }
|
|
2978
3237
|
.vz-ref::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--vz-accent); flex-shrink: 0; }
|
|
2979
3238
|
|
|
2980
|
-
/* Inline @mention chip (rendered inside displayed comments)
|
|
3239
|
+
/* Inline @mention chip (rendered inside displayed comments). user-select
|
|
3240
|
+
* prevents the user from selecting the chip's text and editing it; the
|
|
3241
|
+
* displayed comment is intentionally read-only after save. */
|
|
2981
3242
|
.vz-mention {
|
|
2982
3243
|
display: inline-flex; align-items: center;
|
|
2983
3244
|
padding: 1px 8px 1px 8px;
|
|
@@ -2987,13 +3248,21 @@ var STYLES = `
|
|
|
2987
3248
|
font-size: 12px; font-weight: 600;
|
|
2988
3249
|
vertical-align: baseline;
|
|
2989
3250
|
margin: 0 1px;
|
|
3251
|
+
user-select: none;
|
|
3252
|
+
-webkit-user-select: none;
|
|
3253
|
+
cursor: default;
|
|
2990
3254
|
}
|
|
2991
3255
|
|
|
2992
|
-
/* Mention chip inside the editor (contenteditable=false token)
|
|
3256
|
+
/* Mention chip inside the editor (contenteditable=false token). Same
|
|
3257
|
+
* non-selection rules as .vz-mention so the chip stays atomic even
|
|
3258
|
+
* inside the editable surface \u2014 caret can land before/after but not
|
|
3259
|
+
* inside, no partial-text edit. */
|
|
2993
3260
|
.vz-chip-mention {
|
|
2994
3261
|
background: color-mix(in srgb, var(--vz-accent) 16%, var(--vz-bg-hover));
|
|
2995
3262
|
color: var(--vz-accent);
|
|
2996
3263
|
font-weight: 600;
|
|
3264
|
+
user-select: none;
|
|
3265
|
+
-webkit-user-select: none;
|
|
2997
3266
|
}
|
|
2998
3267
|
.vz-chip-mention::before { display: none; }
|
|
2999
3268
|
|
|
@@ -3687,10 +3956,29 @@ var Vizu = class {
|
|
|
3687
3956
|
replies: [...this.comments[idx].replies ?? [], reply]
|
|
3688
3957
|
};
|
|
3689
3958
|
this.comments[idx] = updated;
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3959
|
+
try {
|
|
3960
|
+
if (this.storage.addReply) {
|
|
3961
|
+
await this.storage.addReply(this.opts.namespace, commentId, reply);
|
|
3962
|
+
} else {
|
|
3963
|
+
await this.storage.updateComment(this.opts.namespace, commentId, { replies: updated.replies });
|
|
3964
|
+
}
|
|
3965
|
+
} catch (err) {
|
|
3966
|
+
const cur = this.comments[idx];
|
|
3967
|
+
this.comments[idx] = {
|
|
3968
|
+
...cur,
|
|
3969
|
+
replies: (cur.replies ?? []).filter((r) => r.id !== reply.id)
|
|
3970
|
+
};
|
|
3971
|
+
const code = err?.code ?? "";
|
|
3972
|
+
const status = err?.status;
|
|
3973
|
+
const msg = status === 403 || code === "forbidden" ? "Reply failed \u2014 you don't have permission." : status === 401 || code === "auth_required" ? "Reply failed \u2014 please sign in again." : "Reply failed. Please try again.";
|
|
3974
|
+
this.pill?.toast(msg);
|
|
3975
|
+
this.refreshUi();
|
|
3976
|
+
if (this.popover?.isOpen()) {
|
|
3977
|
+
const anchors = this.popover.getAnchors();
|
|
3978
|
+
if (anchors[0]) this.popover.update(this.commentsForElement(anchors[0]));
|
|
3979
|
+
}
|
|
3980
|
+
if (typeof console !== "undefined") console.warn("[vizu] addReply failed", err);
|
|
3981
|
+
return null;
|
|
3694
3982
|
}
|
|
3695
3983
|
this.bus.emit("comment:updated", { comment: updated });
|
|
3696
3984
|
this.refreshUi();
|