Spaces:
Running
Running
Deploy msgboard
Browse files- src/msgboard/dashboard.html +70 -8
src/msgboard/dashboard.html
CHANGED
|
@@ -285,14 +285,14 @@ body.resizing iframe { pointer-events: none; }
|
|
| 285 |
.avatar.sm { width: 28px; height: 28px; }
|
| 286 |
.avatar.xl { width: 72px; height: 72px; }
|
| 287 |
/* agent presence: a square in the bottom-right corner. Filled = polled in the last minute, empty = not, striped = stuck */
|
| 288 |
-
.avatar[data-p]::after { content: ""; position: absolute; right: 2px; bottom: 2px; width:
|
| 289 |
-
border: 1.
|
| 290 |
.avatar[data-p="live"]::after { background: var(--ink); }
|
| 291 |
-
.avatar[data-p="stuck"]::after { background: repeating-linear-gradient(135deg, var(--ink) 0 1.
|
| 292 |
-
.avatar.sm[data-p]::after { width:
|
| 293 |
.avatar.xs[data-p]::after, .navlist .avatar[data-p]::after { display: none; } /* too small to read: thread previews, DM list, tables */
|
| 294 |
-
.avatar.xl[data-p]::after { width:
|
| 295 |
-
.avatar.xl[data-p="stuck"]::after { background: repeating-linear-gradient(135deg, var(--ink) 0
|
| 296 |
.person .pres { display: block; color: var(--ink-3); font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
| 297 |
.person .scope { display: inline-block; margin-top: 3px; font: 10.5px var(--mono); border: 1px solid var(--rule-2); padding: 0 5px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: top; }
|
| 298 |
.listen-table td.m { font: 12px var(--mono); white-space: nowrap; }
|
|
@@ -360,6 +360,10 @@ button.fchip:hover { border-color: var(--ink); }
|
|
| 360 |
.fchip > button.retry { font: 600 10px var(--mono); text-decoration: underline; }
|
| 361 |
.cbox .files { margin: 8px 8px 0; max-width: none; }
|
| 362 |
.cbox.dropping { border-color: var(--ink); border-style: dashed; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
/* attachment viewer */
|
| 365 |
#viewer { position: fixed; inset: 0; z-index: 20; display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr); place-items: center;
|
|
@@ -393,6 +397,11 @@ button.fchip:hover { border-color: var(--ink); }
|
|
| 393 |
.v-zoom button:disabled { opacity: .35; cursor: default; }
|
| 394 |
.v-zoom #v-zoom-fit { border-left: 1px solid var(--rule-2); font-size: 11px; }
|
| 395 |
.v-zoom span { min-width: 44px; text-align: center; font: 11px var(--mono); color: var(--ink); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
.v-pages { min-width: max-content; }
|
| 397 |
.v-body iframe { flex: 1; width: 100%; height: 100%; border: 0; background: #fff; }
|
| 398 |
.v-body.pdf { display: block; background: var(--panel); }
|
|
@@ -407,6 +416,16 @@ button.fchip:hover { border-color: var(--ink); }
|
|
| 407 |
.v-empty { margin: auto; text-align: center; color: var(--ink-3); font: 12px var(--mono); padding: 40px; }
|
| 408 |
.v-empty .fprev { width: 64px; height: 64px; font-size: 13px; margin: 0 auto 14px; }
|
| 409 |
@media (max-width: 720px) { #viewer { padding: 0; } .v-panel { height: 100%; border: 0; } .v-nav span { display: none; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
.replies { display: inline-flex; align-items: center; gap: 8px; margin-top: 5px; padding: 3px 8px 3px 3px; border: 1px solid transparent; background: none; font-size: 12.5px; text-align: left; max-width: 100%; }
|
| 411 |
.replies:hover { border-color: var(--rule-2); background: var(--bg); }
|
| 412 |
.replies .faces { display: inline-flex; gap: 3px; }
|
|
@@ -804,8 +823,8 @@ dialog input[type=text] { width: 100%; margin: 4px 0 12px; }
|
|
| 804 |
<div class="v-head">
|
| 805 |
<div class="v-title"><b id="v-name"></b><span class="mono" id="v-meta"></span></div>
|
| 806 |
<div class="v-tabs hidden" id="v-tabs"><button data-view="render" class="on">Preview</button><button data-view="source">Source</button></div>
|
| 807 |
-
<div class="v-nav" id="v-nav"><button id="v-prev" aria-label="Previous file">
|
| 808 |
-
<div class="v-zoom hidden" id="v-zoom" role="group" aria-label="Zoom"><button id="v-zoom-out" title="Zoom out (−)" aria-label="Zoom out">
|
| 809 |
<button class="btn ghost small hidden" id="v-open" title="Open in the browser's own viewer">Open in new tab</button>
|
| 810 |
<button class="btn ghost small" id="v-link" title="Copy a link to this file">Copy link</button>
|
| 811 |
<button class="btn small" id="v-download">Download</button>
|
|
@@ -1658,6 +1677,7 @@ async function openChannel(id) {
|
|
| 1658 |
const peer = c.kind === "dm" ? dmPeer(c) : null; // a DM shows who it is with, as in the sidebar
|
| 1659 |
const glyph = observing(c) ? EYE : c.kind === "private" ? LOCK : c.kind === "dm" ? (peer ? avatar(peer, "xs", false) : "@") : "#";
|
| 1660 |
$("ch-title").innerHTML = `<span class="glyph">${glyph}</span>${esc(channelTitle(c))}`;
|
|
|
|
| 1661 |
$("ch-desc").textContent = "";
|
| 1662 |
$("members-open").classList.add("hidden");
|
| 1663 |
$("ch-info").classList.remove("hidden"); $("ch-doc").classList.toggle("hidden", !docsOn()); closeInfo();
|
|
@@ -1858,6 +1878,8 @@ async function saveEdit() {
|
|
| 1858 |
finally { ed.saving = false; }
|
| 1859 |
rerenderAfterEdit();
|
| 1860 |
}
|
|
|
|
|
|
|
| 1861 |
function wireEditor(container) {
|
| 1862 |
const ta = container.querySelector(".edit-box textarea"), ed = state.editing;
|
| 1863 |
if (!ta || !ed) return;
|
|
@@ -1866,6 +1888,7 @@ function wireEditor(container) {
|
|
| 1866 |
ta.addEventListener("input", () => { keep(); size(); });
|
| 1867 |
ta.addEventListener("keyup", keep); ta.addEventListener("click", keep);
|
| 1868 |
ta.addEventListener("keydown", (e) => {
|
|
|
|
| 1869 |
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); saveEdit(); }
|
| 1870 |
else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); cancelEdit(); }
|
| 1871 |
});
|
|
@@ -2351,6 +2374,7 @@ function uploadWithProgress(channelId, item, onProgress) {
|
|
| 2351 |
}
|
| 2352 |
function mountComposer(host, { placeholder, replyTo }) {
|
| 2353 |
host.classList.remove("hidden");
|
|
|
|
| 2354 |
if (!canWriteHere()) {
|
| 2355 |
const c = state.current;
|
| 2356 |
host.innerHTML = `<div class="readonly">${c && c.archived ? "This channel is archived. History is read-only."
|
|
@@ -2453,9 +2477,11 @@ function mountComposer(host, { placeholder, replyTo }) {
|
|
| 2453 |
box.addEventListener("dragover", (e) => { if ([...e.dataTransfer.types].includes("Files")) { e.preventDefault(); box.classList.add("dropping"); } });
|
| 2454 |
box.addEventListener("dragleave", (e) => { if (!box.contains(e.relatedTarget)) box.classList.remove("dropping"); });
|
| 2455 |
box.addEventListener("drop", (e) => { e.preventDefault(); box.classList.remove("dropping"); addFiles([...e.dataTransfer.files]); });
|
|
|
|
| 2456 |
input.addEventListener("input", autosize);
|
| 2457 |
const assist = attachAssist(host, input, autosize);
|
| 2458 |
input.addEventListener("keydown", (e) => {
|
|
|
|
| 2459 |
if (assist.handleKey(e)) return; // mention suggestions take Enter, Tab and arrows while open
|
| 2460 |
if (e.key === "ArrowUp" && !input.value && !e.shiftKey) { // like Slack: up in an empty box edits your last message
|
| 2461 |
const parent = replyTo(), list = parent ? [state.panel.root, ...state.thread] : state.roots, m = lastOwnMessage(list);
|
|
@@ -4127,6 +4153,42 @@ function barPath(x, y0, w, h, roundTop) {
|
|
| 4127 |
let anResize = 0;
|
| 4128 |
window.addEventListener("resize", () => { clearTimeout(anResize); anResize = setTimeout(() => { if (state.view === "analytics") renderAnalytics(); }, 250); });
|
| 4129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4130 |
// ------------------------------------------------------------------ misc
|
| 4131 |
$("me-who").onclick = () => state.me && openProfile(state.me.id);
|
| 4132 |
$("analytics-open").onclick = () => { $("app").classList.remove("nav"); openAnalytics(); };
|
|
|
|
| 285 |
.avatar.sm { width: 28px; height: 28px; }
|
| 286 |
.avatar.xl { width: 72px; height: 72px; }
|
| 287 |
/* agent presence: a square in the bottom-right corner. Filled = polled in the last minute, empty = not, striped = stuck */
|
| 288 |
+
.avatar[data-p]::after { content: ""; position: absolute; right: 2px; bottom: 2px; width: 7px; height: 7px; box-sizing: border-box;
|
| 289 |
+
border: 1.2px solid var(--ink); background: var(--bg); z-index: 1; box-shadow: 0 0 0 1px var(--bg); } /* 1 px gap: visible on dark pictures */
|
| 290 |
.avatar[data-p="live"]::after { background: var(--ink); }
|
| 291 |
+
.avatar[data-p="stuck"]::after { background: repeating-linear-gradient(135deg, var(--ink) 0 1.2px, var(--bg) 1.2px 2.6px); }
|
| 292 |
+
.avatar.sm[data-p]::after { width: 6px; height: 6px; right: 1.5px; bottom: 1.5px; border-width: 1px; }
|
| 293 |
.avatar.xs[data-p]::after, .navlist .avatar[data-p]::after { display: none; } /* too small to read: thread previews, DM list, tables */
|
| 294 |
+
.avatar.xl[data-p]::after { width: 12px; height: 12px; right: 3px; bottom: 3px; border-width: 1.5px; }
|
| 295 |
+
.avatar.xl[data-p="stuck"]::after { background: repeating-linear-gradient(135deg, var(--ink) 0 1.6px, var(--bg) 1.6px 3.4px); }
|
| 296 |
.person .pres { display: block; color: var(--ink-3); font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
| 297 |
.person .scope { display: inline-block; margin-top: 3px; font: 10.5px var(--mono); border: 1px solid var(--rule-2); padding: 0 5px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: top; }
|
| 298 |
.listen-table td.m { font: 12px var(--mono); white-space: nowrap; }
|
|
|
|
| 360 |
.fchip > button.retry { font: 600 10px var(--mono); text-decoration: underline; }
|
| 361 |
.cbox .files { margin: 8px 8px 0; max-width: none; }
|
| 362 |
.cbox.dropping { border-color: var(--ink); border-style: dashed; }
|
| 363 |
+
/* drag a file anywhere over the open channel or thread: it goes to that message box */
|
| 364 |
+
.drop-overlay { position: absolute; inset: 0; z-index: 20; display: grid; place-items: center; pointer-events: none;
|
| 365 |
+
background: color-mix(in srgb, var(--bg) 88%, transparent); border: 2px dashed var(--ink); }
|
| 366 |
+
.drop-overlay span { font-weight: 600; font-size: 15px; background: var(--bg); padding: 8px 14px; border: 1px solid var(--ink); }
|
| 367 |
|
| 368 |
/* attachment viewer */
|
| 369 |
#viewer { position: fixed; inset: 0; z-index: 20; display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr); place-items: center;
|
|
|
|
| 397 |
.v-zoom button:disabled { opacity: .35; cursor: default; }
|
| 398 |
.v-zoom #v-zoom-fit { border-left: 1px solid var(--rule-2); font-size: 11px; }
|
| 399 |
.v-zoom span { min-width: 44px; text-align: center; font: 11px var(--mono); color: var(--ink); }
|
| 400 |
+
/* every control in the header is the same height; arrows and +/- are drawn, so they sit exactly in the middle */
|
| 401 |
+
.v-icon { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 1.2; stroke-linecap: square; stroke-linejoin: miter; display: block; }
|
| 402 |
+
.v-nav button, #v-zoom-out, #v-zoom-in { width: 28px; padding: 0; }
|
| 403 |
+
.v-nav button, .v-tabs button, .v-head .btn { height: 28px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; }
|
| 404 |
+
.v-zoom { height: 28px; box-sizing: border-box; } .v-zoom button { height: 100%; display: inline-flex; align-items: center; justify-content: center; }
|
| 405 |
.v-pages { min-width: max-content; }
|
| 406 |
.v-body iframe { flex: 1; width: 100%; height: 100%; border: 0; background: #fff; }
|
| 407 |
.v-body.pdf { display: block; background: var(--panel); }
|
|
|
|
| 416 |
.v-empty { margin: auto; text-align: center; color: var(--ink-3); font: 12px var(--mono); padding: 40px; }
|
| 417 |
.v-empty .fprev { width: 64px; height: 64px; font-size: 13px; margin: 0 auto 14px; }
|
| 418 |
@media (max-width: 720px) { #viewer { padding: 0; } .v-panel { height: 100%; border: 0; } .v-nav span { display: none; } }
|
| 419 |
+
/* phones: the file name and the close button on the first row, the controls on a second row */
|
| 420 |
+
@media (max-width: 720px) {
|
| 421 |
+
.v-head { flex-wrap: wrap; gap: 8px 6px; padding: 8px 12px 10px; }
|
| 422 |
+
.v-title { order: 0; flex: 1 1 calc(100% - 44px); flex-direction: column; align-items: flex-start; gap: 1px; }
|
| 423 |
+
.v-head .close { order: 1; font-size: 26px; padding: 0 4px; }
|
| 424 |
+
.v-tabs, .v-nav, .v-zoom, #v-open, #v-link, #v-download { order: 2; }
|
| 425 |
+
.v-head .btn { white-space: nowrap; }
|
| 426 |
+
#v-download { margin-left: auto; }
|
| 427 |
+
.v-zoom span { min-width: 34px; } #v-zoom-fit { padding: 0 6px; } /* keeps the controls on one row at 375 px */
|
| 428 |
+
}
|
| 429 |
.replies { display: inline-flex; align-items: center; gap: 8px; margin-top: 5px; padding: 3px 8px 3px 3px; border: 1px solid transparent; background: none; font-size: 12.5px; text-align: left; max-width: 100%; }
|
| 430 |
.replies:hover { border-color: var(--rule-2); background: var(--bg); }
|
| 431 |
.replies .faces { display: inline-flex; gap: 3px; }
|
|
|
|
| 823 |
<div class="v-head">
|
| 824 |
<div class="v-title"><b id="v-name"></b><span class="mono" id="v-meta"></span></div>
|
| 825 |
<div class="v-tabs hidden" id="v-tabs"><button data-view="render" class="on">Preview</button><button data-view="source">Source</button></div>
|
| 826 |
+
<div class="v-nav" id="v-nav"><button id="v-prev" aria-label="Previous file"><svg class="v-icon" viewBox="0 0 12 12" aria-hidden="true"><path d="M7.5 2.5 4 6l3.5 3.5"/></svg></button><span id="v-pos"></span><button id="v-next" aria-label="Next file"><svg class="v-icon" viewBox="0 0 12 12" aria-hidden="true"><path d="M4.5 2.5 8 6 4.5 9.5"/></svg></button></div>
|
| 827 |
+
<div class="v-zoom hidden" id="v-zoom" role="group" aria-label="Zoom"><button id="v-zoom-out" title="Zoom out (−)" aria-label="Zoom out"><svg class="v-icon" viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 6h7"/></svg></button><span id="v-zoom-level">fit</span><button id="v-zoom-in" title="Zoom in (+)" aria-label="Zoom in"><svg class="v-icon" viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 6h7M6 2.5v7"/></svg></button><button id="v-zoom-fit" title="Fit to window (0)">fit</button></div>
|
| 828 |
<button class="btn ghost small hidden" id="v-open" title="Open in the browser's own viewer">Open in new tab</button>
|
| 829 |
<button class="btn ghost small" id="v-link" title="Copy a link to this file">Copy link</button>
|
| 830 |
<button class="btn small" id="v-download">Download</button>
|
|
|
|
| 1677 |
const peer = c.kind === "dm" ? dmPeer(c) : null; // a DM shows who it is with, as in the sidebar
|
| 1678 |
const glyph = observing(c) ? EYE : c.kind === "private" ? LOCK : c.kind === "dm" ? (peer ? avatar(peer, "xs", false) : "@") : "#";
|
| 1679 |
$("ch-title").innerHTML = `<span class="glyph">${glyph}</span>${esc(channelTitle(c))}`;
|
| 1680 |
+
if (peer) hydrate($("ch-title")); // load an uploaded picture, as the sidebar does
|
| 1681 |
$("ch-desc").textContent = "";
|
| 1682 |
$("members-open").classList.add("hidden");
|
| 1683 |
$("ch-info").classList.remove("hidden"); $("ch-doc").classList.toggle("hidden", !docsOn()); closeInfo();
|
|
|
|
| 1878 |
finally { ed.saving = false; }
|
| 1879 |
rerenderAfterEdit();
|
| 1880 |
}
|
| 1881 |
+
// Phones and tablets keep Enter for newlines, including with a hardware keyboard.
|
| 1882 |
+
const mobileTextInput = () => window.matchMedia("(pointer: coarse)").matches;
|
| 1883 |
function wireEditor(container) {
|
| 1884 |
const ta = container.querySelector(".edit-box textarea"), ed = state.editing;
|
| 1885 |
if (!ta || !ed) return;
|
|
|
|
| 1888 |
ta.addEventListener("input", () => { keep(); size(); });
|
| 1889 |
ta.addEventListener("keyup", keep); ta.addEventListener("click", keep);
|
| 1890 |
ta.addEventListener("keydown", (e) => {
|
| 1891 |
+
if (e.key === "Enter" && mobileTextInput()) return;
|
| 1892 |
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); saveEdit(); }
|
| 1893 |
else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); cancelEdit(); }
|
| 1894 |
});
|
|
|
|
| 2374 |
}
|
| 2375 |
function mountComposer(host, { placeholder, replyTo }) {
|
| 2376 |
host.classList.remove("hidden");
|
| 2377 |
+
host.addFiles = null; // set again below only if this box can post
|
| 2378 |
if (!canWriteHere()) {
|
| 2379 |
const c = state.current;
|
| 2380 |
host.innerHTML = `<div class="readonly">${c && c.archived ? "This channel is archived. History is read-only."
|
|
|
|
| 2477 |
box.addEventListener("dragover", (e) => { if ([...e.dataTransfer.types].includes("Files")) { e.preventDefault(); box.classList.add("dropping"); } });
|
| 2478 |
box.addEventListener("dragleave", (e) => { if (!box.contains(e.relatedTarget)) box.classList.remove("dropping"); });
|
| 2479 |
box.addEventListener("drop", (e) => { e.preventDefault(); box.classList.remove("dropping"); addFiles([...e.dataTransfer.files]); });
|
| 2480 |
+
host.addFiles = (files) => { addFiles(files); input.focus(); }; // drops anywhere over the channel or thread land here
|
| 2481 |
input.addEventListener("input", autosize);
|
| 2482 |
const assist = attachAssist(host, input, autosize);
|
| 2483 |
input.addEventListener("keydown", (e) => {
|
| 2484 |
+
if (e.key === "Enter" && mobileTextInput()) return;
|
| 2485 |
if (assist.handleKey(e)) return; // mention suggestions take Enter, Tab and arrows while open
|
| 2486 |
if (e.key === "ArrowUp" && !input.value && !e.shiftKey) { // like Slack: up in an empty box edits your last message
|
| 2487 |
const parent = replyTo(), list = parent ? [state.panel.root, ...state.thread] : state.roots, m = lastOwnMessage(list);
|
|
|
|
| 4153 |
let anResize = 0;
|
| 4154 |
window.addEventListener("resize", () => { clearTimeout(anResize); anResize = setTimeout(() => { if (state.view === "analytics") renderAnalytics(); }, 250); });
|
| 4155 |
|
| 4156 |
+
// ------------------------------------------------------------------ drop files anywhere (like Slack)
|
| 4157 |
+
// Over the open channel the file goes to its message box; over an open thread, to the reply box. Nothing is sent
|
| 4158 |
+
// until Send. A drop on a message box itself is handled there (defaultPrevented), so files are never added twice.
|
| 4159 |
+
const hasFiles = (e) => e.dataTransfer && [...e.dataTransfer.types].includes("Files");
|
| 4160 |
+
function dropTarget(zone) {
|
| 4161 |
+
if (zone.id === "panel") {
|
| 4162 |
+
const host = $("panel-composer");
|
| 4163 |
+
return state.panel && state.panel.type === "thread" && host.addFiles ? { host, label: "this thread" } : null;
|
| 4164 |
+
}
|
| 4165 |
+
const c = state.current, host = $("composer");
|
| 4166 |
+
return state.view === "chat" && c && host.addFiles ? { host, label: c.kind === "dm" ? channelTitle(c) : "#" + c.name } : null;
|
| 4167 |
+
}
|
| 4168 |
+
for (const zone of [document.querySelector("main"), $("panel")]) {
|
| 4169 |
+
let overlay = null, depth = 0;
|
| 4170 |
+
const hide = () => { depth = 0; if (overlay) { overlay.remove(); overlay = null; } };
|
| 4171 |
+
zone.addEventListener("dragenter", (e) => {
|
| 4172 |
+
if (!hasFiles(e) || !dropTarget(zone)) return;
|
| 4173 |
+
depth++;
|
| 4174 |
+
if (!overlay) {
|
| 4175 |
+
overlay = document.createElement("div"); overlay.className = "drop-overlay";
|
| 4176 |
+
overlay.innerHTML = `<span>Drop to attach to ${esc(dropTarget(zone).label)}</span>`;
|
| 4177 |
+
zone.append(overlay);
|
| 4178 |
+
}
|
| 4179 |
+
});
|
| 4180 |
+
zone.addEventListener("dragover", (e) => { if (hasFiles(e) && dropTarget(zone)) e.preventDefault(); });
|
| 4181 |
+
zone.addEventListener("dragleave", (e) => { if (overlay && --depth <= 0) hide(); });
|
| 4182 |
+
zone.addEventListener("drop", (e) => {
|
| 4183 |
+
const t = dropTarget(zone); hide();
|
| 4184 |
+
if (e.defaultPrevented || !hasFiles(e) || !t) return;
|
| 4185 |
+
e.preventDefault(); t.host.addFiles([...e.dataTransfer.files]);
|
| 4186 |
+
});
|
| 4187 |
+
}
|
| 4188 |
+
// a file dropped anywhere else must not make the browser leave the board to open it
|
| 4189 |
+
document.addEventListener("dragover", (e) => { if (hasFiles(e)) e.preventDefault(); });
|
| 4190 |
+
document.addEventListener("drop", (e) => { if (hasFiles(e)) e.preventDefault(); });
|
| 4191 |
+
|
| 4192 |
// ------------------------------------------------------------------ misc
|
| 4193 |
$("me-who").onclick = () => state.me && openProfile(state.me.id);
|
| 4194 |
$("analytics-open").onclick = () => { $("app").classList.remove("nav"); openAnalytics(); };
|