@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.cjs
CHANGED
|
@@ -1275,8 +1275,50 @@ var Popover = class {
|
|
|
1275
1275
|
this.mentionPicker = null;
|
|
1276
1276
|
/** Dismiss handlers wired to document for the open picker; cleared in closeMentionPicker. */
|
|
1277
1277
|
this.mentionPickerDismiss = null;
|
|
1278
|
+
/** All mentionable users fetched for the open picker — pre-filter list. */
|
|
1279
|
+
this.mentionPickerUsers = [];
|
|
1280
|
+
/** Currently visible subset of `mentionPickerUsers` after the user's filter. */
|
|
1281
|
+
this.mentionPickerFiltered = [];
|
|
1282
|
+
/** Highlighted index in `mentionPickerFiltered`. Up/Down arrows mutate this. */
|
|
1283
|
+
this.mentionPickerSelected = 0;
|
|
1284
|
+
/**
|
|
1285
|
+
* 'manual' = opened from the @ mention button (separate search input).
|
|
1286
|
+
* 'editor' = opened by typing `@` in the editor; filter is derived
|
|
1287
|
+
* from the text after `@` in the editor itself, no search input.
|
|
1288
|
+
*/
|
|
1289
|
+
this.mentionPickerMode = "manual";
|
|
1290
|
+
/**
|
|
1291
|
+
* In editor mode: a Range covering `@<query>` in the editor's text.
|
|
1292
|
+
* Used to replace that span with the mention chip on commit, and to
|
|
1293
|
+
* recompute the bounding rect when re-positioning after typing.
|
|
1294
|
+
*/
|
|
1295
|
+
this.mentionPickerEditorRange = null;
|
|
1278
1296
|
this.handleEditorKey = (e) => {
|
|
1279
1297
|
const editor = e.currentTarget;
|
|
1298
|
+
if (this.mentionPicker && this.mentionPickerMode === "editor") {
|
|
1299
|
+
if (e.key === "ArrowDown") {
|
|
1300
|
+
e.preventDefault();
|
|
1301
|
+
this.moveMentionPickerSelection(1);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (e.key === "ArrowUp") {
|
|
1305
|
+
e.preventDefault();
|
|
1306
|
+
this.moveMentionPickerSelection(-1);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
1310
|
+
if (this.mentionPickerFiltered.length > 0) {
|
|
1311
|
+
e.preventDefault();
|
|
1312
|
+
this.commitMentionPickerSelection();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (e.key === "Escape") {
|
|
1317
|
+
e.preventDefault();
|
|
1318
|
+
this.closeMentionPicker();
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1280
1322
|
if (e.key === "#") {
|
|
1281
1323
|
e.preventDefault();
|
|
1282
1324
|
const savedRange = this.saveRange();
|
|
@@ -1299,6 +1341,21 @@ var Popover = class {
|
|
|
1299
1341
|
this.callbacks.onClose();
|
|
1300
1342
|
}
|
|
1301
1343
|
};
|
|
1344
|
+
/**
|
|
1345
|
+
* Fires after every keystroke in the editor (post-insertion). Looks
|
|
1346
|
+
* for an `@<query>` pattern adjacent to the caret. If found, opens
|
|
1347
|
+
* (or refreshes) the mention picker in editor mode. If the trigger
|
|
1348
|
+
* disappears (user typed a space, backspaced over @, etc.), closes
|
|
1349
|
+
* the picker.
|
|
1350
|
+
*/
|
|
1351
|
+
this.handleEditorInput = () => {
|
|
1352
|
+
const trigger = this.detectMentionTrigger();
|
|
1353
|
+
if (trigger) {
|
|
1354
|
+
void this.openOrUpdateEditorMentionPicker(trigger.range, trigger.query);
|
|
1355
|
+
} else if (this.mentionPicker && this.mentionPickerMode === "editor") {
|
|
1356
|
+
this.closeMentionPicker();
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1302
1359
|
this.handleEditorPaste = (e) => {
|
|
1303
1360
|
if (this.callbacks.canUploadAttachments()) {
|
|
1304
1361
|
const items = e.clipboardData?.items ?? null;
|
|
@@ -1559,6 +1616,7 @@ var Popover = class {
|
|
|
1559
1616
|
const editor = this.el.querySelector(".vz-textarea");
|
|
1560
1617
|
editor.focus();
|
|
1561
1618
|
editor.addEventListener("keydown", this.handleEditorKey);
|
|
1619
|
+
editor.addEventListener("input", this.handleEditorInput);
|
|
1562
1620
|
editor.addEventListener("paste", this.handleEditorPaste);
|
|
1563
1621
|
const wrap = this.el.querySelector(".vz-editor-wrap");
|
|
1564
1622
|
if (wrap && this.callbacks.canUploadAttachments()) {
|
|
@@ -1588,6 +1646,37 @@ var Popover = class {
|
|
|
1588
1646
|
<div class="vz-anchor-list">${chips}</div>
|
|
1589
1647
|
`;
|
|
1590
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Walk back from the caret looking for the most recent `@` that
|
|
1651
|
+
* starts a mention trigger — must be at start-of-text-node or
|
|
1652
|
+
* preceded by whitespace, and there must be no whitespace between
|
|
1653
|
+
* `@` and the caret. Returns the Range covering `@<query>` plus
|
|
1654
|
+
* the bare query string.
|
|
1655
|
+
*/
|
|
1656
|
+
detectMentionTrigger() {
|
|
1657
|
+
const sel = window.getSelection();
|
|
1658
|
+
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
|
|
1659
|
+
const caret = sel.getRangeAt(0);
|
|
1660
|
+
const node = caret.startContainer;
|
|
1661
|
+
if (node.nodeType !== Node.TEXT_NODE) return null;
|
|
1662
|
+
const text = node.textContent ?? "";
|
|
1663
|
+
const offset = caret.startOffset;
|
|
1664
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
1665
|
+
const ch = text[i];
|
|
1666
|
+
if (ch === "@") {
|
|
1667
|
+
const prev = i > 0 ? text[i - 1] : "";
|
|
1668
|
+
if (prev !== "" && !/\s/.test(prev)) return null;
|
|
1669
|
+
const query = text.slice(i + 1, offset);
|
|
1670
|
+
if (/\s/.test(query)) return null;
|
|
1671
|
+
const range = document.createRange();
|
|
1672
|
+
range.setStart(node, i);
|
|
1673
|
+
range.setEnd(node, offset);
|
|
1674
|
+
return { range, query };
|
|
1675
|
+
}
|
|
1676
|
+
if (/\s/.test(ch)) return null;
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1591
1680
|
async uploadAndAppend(file) {
|
|
1592
1681
|
if (!this.callbacks.canUploadAttachments()) return;
|
|
1593
1682
|
const placeholderId = "upload-" + Math.random().toString(36).slice(2, 9);
|
|
@@ -1718,54 +1807,195 @@ var Popover = class {
|
|
|
1718
1807
|
return parts.join("").trim();
|
|
1719
1808
|
}
|
|
1720
1809
|
/**
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
* in another tab are picked up without a popover reopen. Falls back
|
|
1727
|
-
* to silent no-op if `onMentionSearch` isn't wired (non-cloud
|
|
1728
|
-
* adapters) or returns an empty list.
|
|
1810
|
+
* Manual-mode picker: opens from the @ mention button. Renders a
|
|
1811
|
+
* search input above the list; the user types into the input to
|
|
1812
|
+
* filter, arrows + Enter to pick. The selection inserts a mention
|
|
1813
|
+
* chip at the editor's current caret (no `@<query>` replacement —
|
|
1814
|
+
* the user didn't type one).
|
|
1729
1815
|
*/
|
|
1730
1816
|
async openMentionPicker(anchor) {
|
|
1731
1817
|
this.closeMentionPicker();
|
|
1732
1818
|
if (!this.callbacks.onMentionSearch) return;
|
|
1819
|
+
this.mentionPickerMode = "manual";
|
|
1820
|
+
this.mentionPickerEditorRange = null;
|
|
1821
|
+
const picker = this.createMentionPickerShell();
|
|
1822
|
+
this.positionMentionPickerByElement(picker, anchor);
|
|
1823
|
+
const users = await this.fetchMentionables();
|
|
1824
|
+
if (this.mentionPicker !== picker) return;
|
|
1825
|
+
this.mentionPickerUsers = users;
|
|
1826
|
+
this.renderMentionPickerBody("");
|
|
1827
|
+
this.positionMentionPickerByElement(picker, anchor);
|
|
1828
|
+
const input = picker.querySelector(".vz-mention-search");
|
|
1829
|
+
input?.focus();
|
|
1830
|
+
this.wireMentionPickerDismiss();
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Editor-mode picker: opens because the user typed `@` in the
|
|
1834
|
+
* editor. No search input — the editor IS the search; the query is
|
|
1835
|
+
* the text after `@`. On commit, replaces the `@<query>` range with
|
|
1836
|
+
* the chip.
|
|
1837
|
+
*/
|
|
1838
|
+
async openOrUpdateEditorMentionPicker(range, query) {
|
|
1839
|
+
this.mentionPickerEditorRange = range;
|
|
1840
|
+
if (!this.mentionPicker || this.mentionPickerMode !== "editor") {
|
|
1841
|
+
this.closeMentionPicker();
|
|
1842
|
+
this.mentionPickerMode = "editor";
|
|
1843
|
+
const picker = this.createMentionPickerShell({ showSearch: false });
|
|
1844
|
+
this.positionMentionPickerByRange(picker, range);
|
|
1845
|
+
const users = await this.fetchMentionables();
|
|
1846
|
+
if (this.mentionPicker !== picker) return;
|
|
1847
|
+
this.mentionPickerUsers = users;
|
|
1848
|
+
this.renderMentionPickerBody(query);
|
|
1849
|
+
this.positionMentionPickerByRange(picker, range);
|
|
1850
|
+
this.wireMentionPickerDismiss();
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
this.renderMentionPickerBody(query);
|
|
1854
|
+
this.positionMentionPickerByRange(this.mentionPicker, range);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Build the picker DOM scaffold and attach it to the popover. Does
|
|
1858
|
+
* NOT fetch users or render the body — caller's responsibility.
|
|
1859
|
+
* Search input is hidden in editor mode where the user already has
|
|
1860
|
+
* a perfectly good text-entry surface (the editor itself).
|
|
1861
|
+
*/
|
|
1862
|
+
createMentionPickerShell({ showSearch = true } = {}) {
|
|
1733
1863
|
const picker = document.createElement("div");
|
|
1734
1864
|
picker.className = "vz-mention-picker";
|
|
1735
1865
|
picker.setAttribute("data-vz-ignore", "");
|
|
1736
|
-
picker.innerHTML =
|
|
1866
|
+
picker.innerHTML = `
|
|
1867
|
+
${showSearch ? `<input class="vz-mention-search" type="text" placeholder="Search teammates\u2026" autocomplete="off" spellcheck="false" />` : ""}
|
|
1868
|
+
<div class="vz-mention-list" data-vz="mention-list">
|
|
1869
|
+
<div class="vz-mention-loading">Loading\u2026</div>
|
|
1870
|
+
</div>
|
|
1871
|
+
`;
|
|
1737
1872
|
this.el.appendChild(picker);
|
|
1738
1873
|
this.mentionPicker = picker;
|
|
1739
|
-
this.
|
|
1740
|
-
|
|
1874
|
+
this.mentionPickerFiltered = [];
|
|
1875
|
+
this.mentionPickerSelected = 0;
|
|
1876
|
+
picker.addEventListener("mousemove", (e) => {
|
|
1877
|
+
const btn = e.target?.closest(".vz-mention-item");
|
|
1878
|
+
if (!btn) return;
|
|
1879
|
+
const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
|
|
1880
|
+
if (idx >= 0 && idx !== this.mentionPickerSelected) {
|
|
1881
|
+
this.mentionPickerSelected = idx;
|
|
1882
|
+
this.refreshMentionPickerHighlight();
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
picker.addEventListener("click", (e) => {
|
|
1886
|
+
const btn = e.target?.closest(".vz-mention-item");
|
|
1887
|
+
if (!btn) return;
|
|
1888
|
+
e.preventDefault();
|
|
1889
|
+
e.stopPropagation();
|
|
1890
|
+
const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
|
|
1891
|
+
if (idx >= 0) {
|
|
1892
|
+
this.mentionPickerSelected = idx;
|
|
1893
|
+
this.commitMentionPickerSelection();
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
if (showSearch) {
|
|
1897
|
+
const input = picker.querySelector(".vz-mention-search");
|
|
1898
|
+
input?.addEventListener("input", () => this.renderMentionPickerBody(input.value));
|
|
1899
|
+
input?.addEventListener("keydown", (e) => {
|
|
1900
|
+
if (e.key === "ArrowDown") {
|
|
1901
|
+
e.preventDefault();
|
|
1902
|
+
this.moveMentionPickerSelection(1);
|
|
1903
|
+
} else if (e.key === "ArrowUp") {
|
|
1904
|
+
e.preventDefault();
|
|
1905
|
+
this.moveMentionPickerSelection(-1);
|
|
1906
|
+
} else if (e.key === "Enter" || e.key === "Tab") {
|
|
1907
|
+
if (this.mentionPickerFiltered.length > 0) {
|
|
1908
|
+
e.preventDefault();
|
|
1909
|
+
this.commitMentionPickerSelection();
|
|
1910
|
+
}
|
|
1911
|
+
} else if (e.key === "Escape") {
|
|
1912
|
+
e.preventDefault();
|
|
1913
|
+
this.closeMentionPicker();
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
return picker;
|
|
1918
|
+
}
|
|
1919
|
+
/** Fetch the mentionable list. Swallows errors → []. */
|
|
1920
|
+
async fetchMentionables() {
|
|
1921
|
+
if (!this.callbacks.onMentionSearch) return [];
|
|
1741
1922
|
try {
|
|
1742
|
-
|
|
1923
|
+
return await this.callbacks.onMentionSearch();
|
|
1743
1924
|
} catch {
|
|
1925
|
+
return [];
|
|
1744
1926
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Re-filter the cached users by `query`, render the list, reset
|
|
1930
|
+
* selected to 0. Called on every search input change and on every
|
|
1931
|
+
* editor input event while in editor mode.
|
|
1932
|
+
*/
|
|
1933
|
+
renderMentionPickerBody(query) {
|
|
1934
|
+
const picker = this.mentionPicker;
|
|
1935
|
+
if (!picker) return;
|
|
1936
|
+
const list = picker.querySelector('[data-vz="mention-list"]');
|
|
1937
|
+
if (!list) return;
|
|
1938
|
+
const q = query.toLowerCase().trim();
|
|
1939
|
+
this.mentionPickerFiltered = q ? this.mentionPickerUsers.filter((u) => u.name.toLowerCase().includes(q)) : this.mentionPickerUsers.slice();
|
|
1940
|
+
this.mentionPickerSelected = 0;
|
|
1941
|
+
if (this.mentionPickerFiltered.length === 0) {
|
|
1942
|
+
list.innerHTML = `<div class="vz-mention-empty">${this.mentionPickerUsers.length === 0 ? "No teammates to mention yet." : "No teammates match."}</div>`;
|
|
1750
1943
|
return;
|
|
1751
1944
|
}
|
|
1752
|
-
|
|
1945
|
+
list.innerHTML = this.mentionPickerFiltered.map((u, i) => {
|
|
1753
1946
|
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>`;
|
|
1754
|
-
return `<button class="vz-mention-item"
|
|
1947
|
+
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>`;
|
|
1755
1948
|
}).join("");
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
this.
|
|
1949
|
+
}
|
|
1950
|
+
moveMentionPickerSelection(delta) {
|
|
1951
|
+
if (this.mentionPickerFiltered.length === 0) return;
|
|
1952
|
+
const n = this.mentionPickerFiltered.length;
|
|
1953
|
+
this.mentionPickerSelected = (this.mentionPickerSelected + delta + n) % n;
|
|
1954
|
+
this.refreshMentionPickerHighlight();
|
|
1955
|
+
}
|
|
1956
|
+
refreshMentionPickerHighlight() {
|
|
1957
|
+
const picker = this.mentionPicker;
|
|
1958
|
+
if (!picker) return;
|
|
1959
|
+
const items = picker.querySelectorAll(".vz-mention-item");
|
|
1960
|
+
items.forEach((el, i) => el.classList.toggle("is-selected", i === this.mentionPickerSelected));
|
|
1961
|
+
items[this.mentionPickerSelected]?.scrollIntoView({ block: "nearest" });
|
|
1962
|
+
}
|
|
1963
|
+
commitMentionPickerSelection() {
|
|
1964
|
+
const user = this.mentionPickerFiltered[this.mentionPickerSelected];
|
|
1965
|
+
if (!user) return;
|
|
1966
|
+
if (this.mentionPickerMode === "editor" && this.mentionPickerEditorRange) {
|
|
1967
|
+
this.replaceRangeWithMentionChip(this.mentionPickerEditorRange, user);
|
|
1968
|
+
} else {
|
|
1969
|
+
this.insertMentionChip(user);
|
|
1970
|
+
}
|
|
1971
|
+
this.closeMentionPicker();
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Swap a Range covering `@<query>` for a mention chip + trailing
|
|
1975
|
+
* space, then place the caret after the space so the user can keep
|
|
1976
|
+
* typing. Used only in editor-trigger mode.
|
|
1977
|
+
*/
|
|
1978
|
+
replaceRangeWithMentionChip(range, user) {
|
|
1979
|
+
const editor = this.el.querySelector(".vz-textarea");
|
|
1980
|
+
if (!editor) return;
|
|
1981
|
+
const chip = document.createElement("span");
|
|
1982
|
+
chip.className = "vz-chip vz-chip-mention";
|
|
1983
|
+
chip.contentEditable = "false";
|
|
1984
|
+
chip.setAttribute("data-mention-id", user.id);
|
|
1985
|
+
chip.textContent = "@" + user.name;
|
|
1986
|
+
range.deleteContents();
|
|
1987
|
+
range.insertNode(chip);
|
|
1988
|
+
const space = document.createTextNode(" ");
|
|
1989
|
+
chip.after(space);
|
|
1990
|
+
const sel = window.getSelection();
|
|
1991
|
+
if (sel) {
|
|
1992
|
+
const after = document.createRange();
|
|
1993
|
+
after.setStartAfter(space);
|
|
1994
|
+
after.collapse(true);
|
|
1995
|
+
sel.removeAllRanges();
|
|
1996
|
+
sel.addRange(after);
|
|
1997
|
+
}
|
|
1998
|
+
editor.focus();
|
|
1769
1999
|
}
|
|
1770
2000
|
/** Wires Esc + outside-click handlers to close the open picker. */
|
|
1771
2001
|
wireMentionPickerDismiss() {
|
|
@@ -1779,6 +2009,10 @@ var Popover = class {
|
|
|
1779
2009
|
const onOutside = (e) => {
|
|
1780
2010
|
if (!this.mentionPicker) return;
|
|
1781
2011
|
if (this.mentionPicker.contains(e.target)) return;
|
|
2012
|
+
if (this.mentionPickerMode === "editor") {
|
|
2013
|
+
const editor = this.el.querySelector(".vz-textarea");
|
|
2014
|
+
if (editor && editor.contains(e.target)) return;
|
|
2015
|
+
}
|
|
1782
2016
|
this.closeMentionPicker();
|
|
1783
2017
|
};
|
|
1784
2018
|
document.addEventListener("keydown", onKey, true);
|
|
@@ -1797,18 +2031,23 @@ var Popover = class {
|
|
|
1797
2031
|
this.mentionPicker.remove();
|
|
1798
2032
|
this.mentionPicker = null;
|
|
1799
2033
|
}
|
|
2034
|
+
this.mentionPickerFiltered = [];
|
|
2035
|
+
this.mentionPickerSelected = 0;
|
|
2036
|
+
this.mentionPickerEditorRange = null;
|
|
1800
2037
|
}
|
|
1801
|
-
/**
|
|
1802
|
-
|
|
1803
|
-
* popover's right edge so we don't overflow the popover frame.
|
|
1804
|
-
*/
|
|
1805
|
-
positionMentionPicker(picker, anchor) {
|
|
2038
|
+
/** Position the picker below the given anchor (the @ mention button). */
|
|
2039
|
+
positionMentionPickerByElement(picker, anchor) {
|
|
1806
2040
|
const popRect = this.el.getBoundingClientRect();
|
|
1807
2041
|
const btnRect = anchor.getBoundingClientRect();
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2042
|
+
picker.style.left = `${btnRect.left - popRect.left}px`;
|
|
2043
|
+
picker.style.top = `${btnRect.bottom - popRect.top + 4}px`;
|
|
2044
|
+
}
|
|
2045
|
+
/** Position the picker below the `@<query>` text range in the editor. */
|
|
2046
|
+
positionMentionPickerByRange(picker, range) {
|
|
2047
|
+
const popRect = this.el.getBoundingClientRect();
|
|
2048
|
+
const rect = range.getBoundingClientRect();
|
|
2049
|
+
picker.style.left = `${rect.left - popRect.left}px`;
|
|
2050
|
+
picker.style.top = `${rect.bottom - popRect.top + 4}px`;
|
|
1812
2051
|
}
|
|
1813
2052
|
/**
|
|
1814
2053
|
* Insert a mention chip for the given user into the editor at the
|
|
@@ -2687,6 +2926,26 @@ var STYLES = `
|
|
|
2687
2926
|
flex-direction: column;
|
|
2688
2927
|
gap: 1px;
|
|
2689
2928
|
}
|
|
2929
|
+
.vz-mention-search {
|
|
2930
|
+
width: 100%;
|
|
2931
|
+
padding: 7px 9px;
|
|
2932
|
+
margin-bottom: 4px;
|
|
2933
|
+
background: rgba(255,255,255,0.04);
|
|
2934
|
+
border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
|
|
2935
|
+
border-radius: 6px;
|
|
2936
|
+
color: #fafafa;
|
|
2937
|
+
font-size: 13px;
|
|
2938
|
+
outline: none;
|
|
2939
|
+
box-sizing: border-box;
|
|
2940
|
+
}
|
|
2941
|
+
.vz-mention-search:focus { border-color: var(--vz-accent, #ff8b6e); }
|
|
2942
|
+
.vz-mention-search::placeholder { color: var(--vz-text-muted); }
|
|
2943
|
+
.vz-mention-list {
|
|
2944
|
+
display: flex;
|
|
2945
|
+
flex-direction: column;
|
|
2946
|
+
gap: 1px;
|
|
2947
|
+
min-height: 0;
|
|
2948
|
+
}
|
|
2690
2949
|
.vz-mention-loading,
|
|
2691
2950
|
.vz-mention-empty {
|
|
2692
2951
|
padding: 10px 12px;
|
|
@@ -2708,9 +2967,9 @@ var STYLES = `
|
|
|
2708
2967
|
cursor: pointer;
|
|
2709
2968
|
transition: background 80ms;
|
|
2710
2969
|
}
|
|
2711
|
-
.vz-mention-item
|
|
2970
|
+
.vz-mention-item.is-selected,
|
|
2712
2971
|
.vz-mention-item:focus-visible {
|
|
2713
|
-
background: var(--vz-bg-hover, rgba(255,255,255,0.
|
|
2972
|
+
background: var(--vz-bg-hover, rgba(255,255,255,0.08));
|
|
2714
2973
|
outline: none;
|
|
2715
2974
|
}
|
|
2716
2975
|
.vz-mention-avatar {
|
|
@@ -3013,7 +3272,9 @@ var STYLES = `
|
|
|
3013
3272
|
.vz-ref:hover { background: color-mix(in srgb, var(--vz-accent) 30%, var(--vz-bg-hover)); transform: translateY(-1px); }
|
|
3014
3273
|
.vz-ref::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--vz-accent); flex-shrink: 0; }
|
|
3015
3274
|
|
|
3016
|
-
/* Inline @mention chip (rendered inside displayed comments)
|
|
3275
|
+
/* Inline @mention chip (rendered inside displayed comments). user-select
|
|
3276
|
+
* prevents the user from selecting the chip's text and editing it; the
|
|
3277
|
+
* displayed comment is intentionally read-only after save. */
|
|
3017
3278
|
.vz-mention {
|
|
3018
3279
|
display: inline-flex; align-items: center;
|
|
3019
3280
|
padding: 1px 8px 1px 8px;
|
|
@@ -3023,13 +3284,21 @@ var STYLES = `
|
|
|
3023
3284
|
font-size: 12px; font-weight: 600;
|
|
3024
3285
|
vertical-align: baseline;
|
|
3025
3286
|
margin: 0 1px;
|
|
3287
|
+
user-select: none;
|
|
3288
|
+
-webkit-user-select: none;
|
|
3289
|
+
cursor: default;
|
|
3026
3290
|
}
|
|
3027
3291
|
|
|
3028
|
-
/* Mention chip inside the editor (contenteditable=false token)
|
|
3292
|
+
/* Mention chip inside the editor (contenteditable=false token). Same
|
|
3293
|
+
* non-selection rules as .vz-mention so the chip stays atomic even
|
|
3294
|
+
* inside the editable surface \u2014 caret can land before/after but not
|
|
3295
|
+
* inside, no partial-text edit. */
|
|
3029
3296
|
.vz-chip-mention {
|
|
3030
3297
|
background: color-mix(in srgb, var(--vz-accent) 16%, var(--vz-bg-hover));
|
|
3031
3298
|
color: var(--vz-accent);
|
|
3032
3299
|
font-weight: 600;
|
|
3300
|
+
user-select: none;
|
|
3301
|
+
-webkit-user-select: none;
|
|
3033
3302
|
}
|
|
3034
3303
|
.vz-chip-mention::before { display: none; }
|
|
3035
3304
|
|
|
@@ -3723,10 +3992,29 @@ var Vizu = class {
|
|
|
3723
3992
|
replies: [...this.comments[idx].replies ?? [], reply]
|
|
3724
3993
|
};
|
|
3725
3994
|
this.comments[idx] = updated;
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3995
|
+
try {
|
|
3996
|
+
if (this.storage.addReply) {
|
|
3997
|
+
await this.storage.addReply(this.opts.namespace, commentId, reply);
|
|
3998
|
+
} else {
|
|
3999
|
+
await this.storage.updateComment(this.opts.namespace, commentId, { replies: updated.replies });
|
|
4000
|
+
}
|
|
4001
|
+
} catch (err) {
|
|
4002
|
+
const cur = this.comments[idx];
|
|
4003
|
+
this.comments[idx] = {
|
|
4004
|
+
...cur,
|
|
4005
|
+
replies: (cur.replies ?? []).filter((r) => r.id !== reply.id)
|
|
4006
|
+
};
|
|
4007
|
+
const code = err?.code ?? "";
|
|
4008
|
+
const status = err?.status;
|
|
4009
|
+
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.";
|
|
4010
|
+
this.pill?.toast(msg);
|
|
4011
|
+
this.refreshUi();
|
|
4012
|
+
if (this.popover?.isOpen()) {
|
|
4013
|
+
const anchors = this.popover.getAnchors();
|
|
4014
|
+
if (anchors[0]) this.popover.update(this.commentsForElement(anchors[0]));
|
|
4015
|
+
}
|
|
4016
|
+
if (typeof console !== "undefined") console.warn("[vizu] addReply failed", err);
|
|
4017
|
+
return null;
|
|
3730
4018
|
}
|
|
3731
4019
|
this.bus.emit("comment:updated", { comment: updated });
|
|
3732
4020
|
this.refreshUi();
|