1// Browser-only helpers for the guest posting tool: clipboard, CSV download and a viewport check. No storage, no network. 2export async function copy(text) { await navigator.clipboard.writeText(text); } 3export function download(csv, name) { 4 const url = URL.createObjectURL(new Blob(["", csv], { type: "text/csv;charset=utf-8" })); 5 const link = document.createElement("a"); 6 link.href = url; 7 link.download = name || "guest-post-sites.csv"; 8 document.body.append(link); 9 link.click(); 10 link.remove(); 11 setTimeout(() => URL.revokeObjectURL(url), 1000); 12} 13// Writes the selection into the address in place (no reload, no history entry) and returns the full link. 14// Only the sheet's own keys are replaced; anything else already in the address stays. 15export function link(keys, pairs) { 16 const url = new URL(location.href); 17 for (const key of keys) url.searchParams.delete(key); 18 for (const [key, value] of pairs) url.searchParams.set(key, value); 19 history.replaceState(history.state, "", url); 20 return url.href; 21} 22// The sheet is two tables: the header (in .gp-head-wrap, no scrollbar) and the rows (in .gp-grid-wrap, which 23// scrolls). mount() keeps the header following the rows sideways; align() gives both tables the same column 24// widths, each column as wide as the wider of its header cell and its widest row cell. 25const state = new WeakMap(); 26export function mount(sheet) { 27 const head = sheet.querySelector(".gp-head-wrap"), body = sheet.querySelector(".gp-grid-wrap"); 28 if (!head || !body || state.has(sheet)) return; 29 body.addEventListener("scroll", () => { head.scrollLeft = body.scrollLeft; }, { passive: true }); 30 const observer = new ResizeObserver(() => align(sheet)); 31 observer.observe(body); 32 state.set(sheet, observer); 33 align(sheet); 34} 35export function align(sheet) { 36 const head = sheet.querySelector(".gp-head-wrap"), body = sheet.querySelector(".gp-grid-wrap"); 37 const headTable = head?.querySelector("table"), bodyTable = body?.querySelector("table"); 38 if (!headTable || !bodyTable) return; 39 const headCells = [...headTable.querySelectorAll("tr.gp-columns > th")]; 40 const headCols = [...headTable.querySelectorAll("colgroup col")], bodyCols = [...bodyTable.querySelectorAll("colgroup col")]; 41 if (headCols.length !== headCells.length || bodyCols.length !== headCells.length) return; 42 // The header wrap ends where the rows end, before the vertical scrollbar, so both tables are laid out in the same width. 43 head.style.paddingRight = (body.offsetWidth - body.clientWidth) + "px"; 44 // Natural widths first (no fixed layout), then the wider of the two per column is set on both. 45 for (const table of [headTable, bodyTable]) { table.style.tableLayout = ""; table.style.width = ""; } 46 for (const col of [...headCols, ...bodyCols]) col.style.width = ""; 47 const rows = [...bodyTable.querySelectorAll("tbody > tr:not(.gp-empty)")].slice(0, 60); 48 const widths = headCells.map((cell, i) => Math.ceil(Math.max(cell.offsetWidth, ...rows.map(r => r.children[i]?.offsetWidth ?? 0)))); 49 const total = widths.reduce((a, b) => a + b, 0); 50 if (total === 0) return; 51 // Full screen is the one place the sheet stretches to the edge; otherwise the columns keep their own widths. 52 const full = document.fullscreenElement === sheet || sheet.classList.contains("is-fullwindow"); 53 const width = full ? Math.max(total, body.clientWidth) : total; 54 if (width > total) { const extra = width - total; widths.forEach((w, i) => { widths[i] = w + extra * w / total; }); } 55 for (const cols of [headCols, bodyCols]) cols.forEach((col, i) => { col.style.width = widths[i] + "px"; }); 56 for (const table of [headTable, bodyTable]) { table.style.tableLayout = "fixed"; table.style.width = width + "px"; } 57 head.scrollLeft = body.scrollLeft; 58} 59// Full screen for the sheet element. Where the browser refuses real full screen (some embedded views do), the 60// sheet fills the window instead. Returns whether it is on; tells .NET when it ends by other means (Esc included). 61export async function fullscreen(sheet, dotnet) { 62 const fill = "is-fullwindow"; 63 if (document.fullscreenElement === sheet) { await document.exitFullscreen(); return false; } 64 if (sheet.classList.contains(fill)) { sheet.classList.remove(fill); align(sheet); return false; } 65 if (!sheet.dataset.fullscreenWatched) { 66 sheet.dataset.fullscreenWatched = "1"; 67 sheet.addEventListener("fullscreenchange", () => dotnet.invokeMethodAsync("FullScreenChanged", document.fullscreenElement === sheet)); 68 document.addEventListener("keydown", e => { 69 if (e.key === "Escape" && sheet.classList.contains(fill)) { sheet.classList.remove(fill); dotnet.invokeMethodAsync("FullScreenChanged", false); } 70 }); 71 } 72 try { await sheet.requestFullscreen(); } 73 catch { sheet.classList.add(fill); } 74 align(sheet); 75 return true; 76}
77// The zoom, in percent, at which every column of the sheet is in view. 78export function fit() { 79 const wrap = document.querySelector(".gp-grid-wrap"), sheet = wrap?.querySelector(".gp-grid"); 80 if (!wrap || !sheet) return 100; 81 // A zoomed-out sheet is stretched to fill the frame, so its real width is measured at normal size. 82 const zoom = sheet.style.zoom, width = sheet.style.width, layout = sheet.style.tableLayout; 83 sheet.style.zoom = "1"; sheet.style.width = ""; sheet.style.tableLayout = ""; 84 const natural = sheet.offsetWidth; 85 sheet.style.zoom = zoom; sheet.style.width = width; sheet.style.tableLayout = layout; 86 return natural > 0 ? Math.floor(wrap.clientWidth * 100 / natural) : 100; 87} 88// Esc closes the site card. Registered once; .NET decides whether anything is open. 89let escapeWatched = false; 90export function watchEscape(dotnet) { 91 if (escapeWatched) return; 92 escapeWatched = true; 93 document.addEventListener("keydown", e => { if (e.key === "Escape") dotnet.invokeMethodAsync("EscapePressed"); }); 94} 95// A new page starts at its first row. 96export function top() { const sheet = document.querySelector(".gp-grid-wrap"); if (sheet) sheet.scrollTop = 0; } 97// Matches the stylesheet's single-column breakpoint, where the filter panel starts closed. 98export function narrow() { return matchMedia("(max-width: 800px)").matches; }
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.