// ==UserScript== // @name Hashtag Bundler for X/Twitter and Bluesky // @namespace https://codymkw.nekoweb.org/ // @version 2.6.3 // @description A floating hashtag organizer for X/Twitter and Bluesky. Create reusable hashtag bundles, collapse or expand the panel, copy or insert tags into the composer, combine bundles into new sets, and export or import everything as JSON. The panel remembers its collapsed state, combined bundles open in a dedicated popup, and actions like Insert or Copy show confirmation feedback. Fully supports both individual and combined bundles with a simple, fast UI. // @match https://twitter.com/* // @match https://x.com/* // @match https://bsky.app/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // ------------------------------ // Host / Storage setup // ------------------------------ let PANEL_ID, STORAGE_KEY; const host = location.hostname; if (host === "twitter.com" || host === "x.com") { PANEL_ID = "twitter-hashtag-panel"; STORAGE_KEY = "twitterHashtagBundles"; } else if (host === "bsky.app") { PANEL_ID = "bluesky-hashtag-panel"; STORAGE_KEY = "blueskyHashtagBundles"; } else return; const COMBINED_KEY = STORAGE_KEY + "_combined"; const COLLAPSED_KEY = STORAGE_KEY + "_collapsed"; // ------------------------------ // Storage helpers // ------------------------------ const loadData = () => { try { return JSON.parse(GM_getValue(STORAGE_KEY, "{}") || "{}"); } catch { return {}; } }; const saveData = (v) => GM_setValue(STORAGE_KEY, JSON.stringify(v)); const loadCombined = () => { try { return JSON.parse(GM_getValue(COMBINED_KEY, "{}") || "{}"); } catch { return {}; } }; const saveCombined = (v) => GM_setValue(COMBINED_KEY, JSON.stringify(v)); const loadCollapsed = () => { try { return JSON.parse(GM_getValue(COLLAPSED_KEY, "false")); } catch { return false; } }; const saveCollapsed = (v) => GM_setValue(COLLAPSED_KEY, JSON.stringify(Boolean(v))); // ------------------------------ // DOM helper // ------------------------------ const el = (tag, props = {}, kids = []) => { const e = document.createElement(tag); for (const k in props) { if (k === "style" && typeof props[k] === "object") Object.assign(e.style, props[k]); else if (k === "html") e.innerHTML = props[k]; else e[k] = props[k]; } kids.forEach(c => e.appendChild(c)); return e; }; // ------------------------------ // Auto Combined Name — Option C1 // ------------------------------ const makeAutoName = (sources) => { const getInitials = (s) => { const parts = s.split(/[\W_]+/).filter(Boolean); if (!parts.length) return s.slice(0, 3).toUpperCase(); return parts.map(p => p[0].toUpperCase()).slice(0, 4).join(''); }; const safeSourceKey = (s) => s.replace(/[^\w\-]/g, "_").replace(/_+/g, "_"); const initials = sources.map(getInitials).join("_"); const safeNames = sources.map(safeSourceKey); const ts = Date.now(); const shortTs = String(ts).slice(-5); return { key: `Combined_${safeNames.join("_")}_${ts}`, label: `Combined_${initials}_${shortTs}` }; }; // ------------------------------ // Composer detection + insert append helpers (kept from v2.6.2) // ------------------------------ function getComposerTarget() { let node = document.querySelector('div[data-testid="tweetTextarea_0"]'); if (node) { const inner = node.querySelector('div[contenteditable="true"], [contenteditable="true"]'); if (inner) return { type: 'contenteditable', node: inner }; if (node.getAttribute && node.getAttribute('contenteditable') === 'true') return { type: 'contenteditable', node }; } const generic = document.querySelector('div[contenteditable="true"], [contenteditable="true"]'); if (generic) return { type: 'contenteditable', node: generic }; const ta = document.querySelector('textarea'); if (ta) return { type: 'textarea', node: ta }; return null; } function moveCaretToEnd(contentEditableNode) { try { const range = document.createRange(); range.selectNodeContents(contentEditableNode); range.collapse(false); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } catch (err) { /* ignore */ } } function insertTextAtEnd(text) { const target = getComposerTarget(); if (!target) return false; if (target.type === 'textarea') { const ta = target.node; try { ta.focus(); } catch {} ta.value = ta.value + text; try { ta.dispatchEvent(new Event('input', { bubbles: true })); } catch {} return true; } else if (target.type === 'contenteditable') { const node = target.node; moveCaretToEnd(node); try { node.focus(); } catch (e) {} try { const success = document.execCommand('insertText', false, text); if (success) return true; node.textContent = node.textContent + text; return true; } catch (err) { try { node.textContent = node.textContent + text; return true; } catch (ee) { return false; } } } return false; } // ------------------------------ // Confirm text-only helper (3s) // ------------------------------ const confirmButton = (btn, originalText, confirmText) => { try { btn.textContent = confirmText; } catch {} setTimeout(() => { try { btn.textContent = originalText; } catch {} }, 3000); }; // ------------------------------ // Panel creation & rendering (v2.6.3) // ------------------------------ const createPanel = () => { if (document.getElementById(PANEL_ID)) return; const panel = el("div", { id: PANEL_ID, style: ` position:fixed;right:20px;bottom:20px;width:340px; background:#1f1f1f;color:white;border-radius:12px;padding:10px; font-family:sans-serif;box-shadow:0 4px 14px rgba(0,0,0,0.5); z-index:999999; ` }); // header const header = el("div", { style: "display:flex;justify-content:space-between;align-items:center;cursor:pointer;margin-bottom:8px;" }); const title = el("span", { textContent: "Hashtag Bundles", style: "font-weight:700;" }); const arrow = el("span", { textContent: "▼", style: "font-size:14px;" }); header.append(title, arrow); // container const container = el("div", { id: "bundle-container", style: "max-height:520px;overflow-y:auto;padding-right:6px;" }); panel.append(header, container); document.body.append(panel); // persistent collapse let collapsed = loadCollapsed(); container.style.display = collapsed ? "none" : "block"; arrow.textContent = collapsed ? "▲" : "▼"; header.onclick = () => { collapsed = !collapsed; saveCollapsed(collapsed); container.style.display = collapsed ? "none" : "block"; arrow.textContent = collapsed ? "▲" : "▼"; }; render(); // main render function function render() { container.innerHTML = ""; const data = loadData(); const combined = loadCombined(); // New Bundle const newBtn = el("button", { textContent: "➕ New Bundle", style: "width:100%;padding:8px;margin-bottom:8px;border:none;border-radius:6px;background:#5865F2;color:white;font-weight:700;cursor:pointer;" }); newBtn.onclick = () => { const name = prompt("Bundle name?"); if (!name) return; const tags = prompt("Enter hashtags separated by spaces:"); if (!tags) return; data[name] = tags.split(/\s+/).filter(Boolean).map(t => t.startsWith("#") ? t : "#" + t); saveData(data); render(); }; container.append(newBtn); // Combine Bundles const combineBtn = el("button", { textContent: "🔗 Combine Bundles", style: "width:100%;padding:8px;margin-bottom:8px;border:none;border-radius:6px;background:#9b59b6;color:white;font-weight:700;cursor:pointer;" }); combineBtn.onclick = openCreateCombinedModal; container.append(combineBtn); // Show Combined Bundles const showCB = el("button", { textContent: "📂 Show Combined Bundles", style: "width:100%;padding:8px;margin-bottom:8px;border:none;border-radius:6px;background:#8e44ad;color:white;font-weight:700;cursor:pointer;" }); showCB.onclick = openCombinedPopup; container.append(showCB); // Export / Import and Reset All Data (A: under Export/Import) const ioWrap = el("div", { style: "display:flex;gap:8px;margin:10px 0;" }); const exportBtn = el("button", { textContent: "Export", style: "flex:1;padding:8px;border:none;border-radius:6px;background:#4CAF50;color:white;font-weight:700;cursor:pointer;" }); exportBtn.onclick = () => { const payload = { bundles: loadData(), combined: loadCombined() }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = STORAGE_KEY + ".json"; a.click(); }; const importBtn = el("button", { textContent: "Import", style: "flex:1;padding:8px;border:none;border-radius:6px;background:#2196F3;color:white;font-weight:700;cursor:pointer;" }); importBtn.onclick = () => { const f = document.createElement("input"); f.type = "file"; f.accept = "application/json"; f.onchange = e => { const file = e.target.files?.[0]; if (!file) return; const r = new FileReader(); r.onload = ev => { try { const j = JSON.parse(ev.target.result); const newBundles = j.bundles || {}; const newCombined = j.combined || {}; saveData({ ...loadData(), ...newBundles }); saveCombined({ ...loadCombined(), ...newCombined }); render(); alert("Import complete."); } catch { alert("Invalid JSON."); } }; r.readAsText(file); }; f.click(); }; ioWrap.append(exportBtn, importBtn); container.append(ioWrap); // Reset All Data button (under Export/Import) const resetBtn = el("button", { textContent: "⚠️ Reset All Data", style: "width:100%;padding:8px;margin-bottom:8px;border:none;border-radius:6px;background:#c0392b;color:white;font-weight:700;cursor:pointer;" }); resetBtn.onclick = () => { if (!confirm("This will permanently delete ALL bundles and combined bundles. Continue?")) return; try { GM_deleteValue(STORAGE_KEY); GM_deleteValue(COMBINED_KEY); GM_deleteValue(COLLAPSED_KEY); } catch (e) { // fallback: clear by setting empty objects saveData({}); saveCombined({}); saveCollapsed(false); } render(); alert("All data cleared."); }; container.append(resetBtn); // Dropdown: "Your Bundles" (L2) const selectLabel = el("div", { textContent: "Your Bundles", style: "font-weight:700;margin:6px 0 4px 0;" }); const select = el("select", { id: "bundle-select", style: "width:100%;padding:8px;border-radius:6px;background:#262626;color:#fff;border:none;margin-bottom:8px;" }); const defaultOpt = el("option", { textContent: "-- Select a bundle --", value: "" }); select.append(defaultOpt); const bundleNames = Object.keys(data).sort((a, b) => a.localeCompare(b)); bundleNames.forEach(n => { const opt = el("option", { textContent: n, value: n }); select.append(opt); }); container.append(selectLabel, select); // Detail pane for selected bundle (shows tags + buttons) const detail = el("div", { id: "bundle-detail", style: "background:#262626;border-radius:8px;padding:10px;margin-bottom:8px;display:none;" }); container.append(detail); function renderDetail(bundleName) { detail.innerHTML = ""; if (!bundleName) { detail.style.display = "none"; return; } const tagsArr = data[bundleName] || []; const nameEl = el("div", { textContent: bundleName, style: "font-weight:700;margin-bottom:6px;" }); const tagsEl = el("div", { textContent: tagsArr.join(" "), style: "color:#ddd;margin-bottom:8px;word-break:break-word;" }); const btnRow = el("div", { style: "display:flex;gap:8px;" }); const mkbtn = (txt, bg, fn) => el("button", { textContent: txt, onclick: fn, style: `flex:1;padding:8px;border:none;border-radius:6px;background:${bg};color:white;font-weight:700;cursor:pointer;` }); // Insert (keeps existing behavior) const insertBtn = mkbtn("Insert", "#2196F3", () => { const text = tagsArr.join(" ") + " "; const ok = insertTextAtEnd(text); if (!ok) return alert("Insert failed (composer not found)."); confirmButton(insertBtn, "Insert", "Inserted!"); }); // Copy const copyBtn = mkbtn("Copy", "#4CAF50", async () => { await navigator.clipboard.writeText(tagsArr.join(" ")).catch(() => alert("Copy error")); confirmButton(copyBtn, "Copy", "Copied!"); }); // Edit const editBtn = mkbtn("Edit", "#555", () => { const tags = prompt("Edit hashtags:", tagsArr.join(" ")); if (!tags) return; data[bundleName] = tags.split(/\s+/).filter(Boolean).map(t => t.startsWith("#") ? t : "#" + t); saveData(data); // refresh select and detail const opts = select.querySelectorAll("option"); opts.forEach(o => { if (o.value === bundleName) o.textContent = bundleName; }); renderDetail(bundleName); }); // Delete const delBtn = mkbtn("Delete", "#c0392b", () => { if (!confirm(`Delete "${bundleName}"?`)) return; delete data[bundleName]; saveData(data); // remove option from select and hide detail const opt = select.querySelector(`option[value="${CSS.escape(bundleName)}"]`); if (opt) opt.remove(); select.value = ""; renderDetail(""); // also re-render the panel to update combined previews etc render(); }); // On Buffer user requested no insert in Buffer, but for main script we keep insert — user asked to remove only in Buffer. // Append buttons (insert, copy, edit, delete) btnRow.append(insertBtn, copyBtn, editBtn, delBtn); detail.append(nameEl, tagsEl, btnRow); detail.style.display = "block"; } // When selection changes select.onchange = () => { const val = select.value; renderDetail(val); }; // Add an option to show combined quick-access? no — combined stays in popup per design // If no bundles, show message if (!bundleNames.length) { const msg = el("div", { textContent: "No bundles yet. Use New Bundle to create one.", style: "color:#999;margin:8px 0;" }); container.append(msg); } // Render combined summary small (optional) const combinedSummary = el("div", { style: "font-size:12px;color:#bbb;margin-top:6px;" }); const combinedCount = Object.keys(combined).length; combinedSummary.textContent = `${combinedCount} combined bundle${combinedCount === 1 ? "" : "s"}.`; container.append(combinedSummary); } }; // ------------------------------ // Create Combined Bundle Modal (E1) // ------------------------------ const openCreateCombinedModal = () => { const data = loadData(); const bundles = Object.keys(data).sort((a, b) => a.localeCompare(b)); if (!bundles.length) return alert("No bundles available."); if (document.getElementById("combine-backdrop")) return; const backdrop = el("div", { id: "combine-backdrop", style: "position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.4);z-index:999999;" }); document.body.append(backdrop); const modal = el("div", { style: "position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);background:#1f1f1f;color:white;width:360px;padding:14px;border-radius:12px;z-index:1000000;box-shadow:0 6px 24px rgba(0,0,0,0.6);" }); backdrop.append(modal); backdrop.onclick = (e) => { if (e.target === backdrop) close(); }; modal.onclick = (e) => e.stopPropagation(); modal.append(el("div", { textContent: "Combine Bundles", style: "font-weight:700;margin-bottom:10px;font-size:15px;" })); modal.append(el("div", { textContent: "Select bundles (order of checking determines tag order):", style: "color:#ccc;margin-bottom:8px;font-size:13px;" })); let order = []; const list = el("div", { style: "display:flex;flex-direction:column;gap:6px;margin-bottom:10px;max-height:220px;overflow:auto;" }); bundles.forEach(n => { const row = el("div", { style: "display:flex;align-items:center;gap:8px;background:#262626;padding:8px;border-radius:6px;cursor:pointer;" }); const cb = el("input", { type: "checkbox" }); cb.onclick = (ev) => ev.stopPropagation(); cb.onchange = () => { if (cb.checked) order.push(n); else order = order.filter(x => x !== n); updatePreview(); }; const lbl = el("span", { textContent: n, style: "font-weight:600;cursor:pointer;" }); lbl.onclick = () => { cb.checked = !cb.checked; cb.onchange(); }; row.append(cb, lbl); list.append(row); }); modal.append(list); modal.append(el("div", { textContent: "Preview:", style: "color:#ccc;margin-bottom:6px;" })); const preview = el("div", { style: "padding:8px;background:#262626;border-radius:6px;color:#ddd;min-height:28px;margin-bottom:12px;word-break:break-word;" }); modal.append(preview); function updatePreview() { const tags = [...new Set(order.flatMap(n => data[n] || []))]; preview.textContent = tags.join(" ") || "(none)"; } const createBtn = el("button", { textContent: "Create Combined Bundle", style: "width:100%;padding:8px;background:#27ae60;color:white;border:none;border-radius:6px;font-weight:700;cursor:pointer;margin-bottom:8px;" }); createBtn.onclick = () => { if (!order.length) return alert("Select at least one bundle."); const auto = makeAutoName(order); const tags = [...new Set(order.flatMap(n => data[n] || []))]; const combined = loadCombined(); combined[auto.key] = { label: auto.label, fullKey: auto.key, sources: [...order], tags }; saveCombined(combined); alert("Created combined bundle."); close(); }; const cancelBtn = el("button", { textContent: "Cancel", style: "width:100%;padding:8px;background:#7f8c8d;color:white;border:none;border-radius:6px;font-weight:700;cursor:pointer;" }); cancelBtn.onclick = close; modal.append(createBtn, cancelBtn); updatePreview(); function close() { const b = document.getElementById("combine-backdrop"); if (b) b.remove(); } }; // ------------------------------ // Edit Combined Modal (E1) // ------------------------------ const openEditCombinedModal = (key) => { const combined = loadCombined(); const data = loadData(); const combo = combined[key]; if (!combo) return alert("Combined bundle not found."); const bundles = Object.keys(data).sort(); if (!bundles.length) return alert("No bundles exist."); if (document.getElementById("edit-backdrop")) return; const backdrop = el("div", { id: "edit-backdrop", style: "position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.4);z-index:999999;" }); document.body.append(backdrop); const modal = el("div", { style: "position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);background:#1f1f1f;color:white;width:360px;padding:14px;border-radius:12px;z-index:1000000;box-shadow:0 6px 24px rgba(0,0,0,0.6);" }); backdrop.append(modal); backdrop.onclick = (e) => { if (e.target === backdrop) close(); }; modal.onclick = (e) => e.stopPropagation(); modal.append(el("div", { textContent: "Edit Combined Bundle", style: "font-weight:700;margin-bottom:10px;font-size:15px;" })); modal.append(el("div", { textContent: `Current label: ${combo.label}`, style: "color:#bbb;margin-bottom:10px;font-size:13px;" })); let order = [...combo.sources]; const list = el("div", { style: "display:flex;flex-direction:column;gap:6px;margin-bottom:10px;max-height:220px;overflow:auto;" }); bundles.forEach(n => { const row = el("div", { style: "display:flex;align-items:center;gap:8px;background:#262626;padding:8px;border-radius:6px;cursor:pointer;" }); const cb = el("input", { type: "checkbox" }); cb.checked = order.includes(n); cb.onclick = (e) => e.stopPropagation(); cb.onchange = () => { if (cb.checked) { if (!order.includes(n)) order.push(n); } else { order = order.filter(x => x !== n); } updatePreview(); }; const lbl = el("span", { textContent: n, style: "font-weight:600;cursor:pointer;" }); lbl.onclick = () => { cb.checked = !cb.checked; cb.onchange(); }; row.append(cb, lbl); list.append(row); }); modal.append(list); modal.append(el("div", { textContent: "Preview:", style: "color:#ccc;margin-bottom:6px;" })); const preview = el("div", { style: "padding:8px;background:#262626;border-radius:6px;color:#ddd;min-height:28px;margin-bottom:12px;word-break:break-word;" }); modal.append(preview); function updatePreview() { const tags = [...new Set(order.flatMap(n => data[n] || []))]; preview.textContent = tags.join(" ") || "(none)"; } updatePreview(); const saveBtn = el("button", { textContent: "Save Changes", style: "width:100%;padding:8px;background:#27ae60;color:white;border:none;border-radius:6px;font-weight:700;cursor:pointer;margin-bottom:8px;" }); saveBtn.onclick = () => { if (!order.length) return alert("Select at least one bundle."); const oldKey = key; const oldSources = combo.sources.join("|"); const newSources = order.join("|"); let newKey = key; let newLabel = combo.label; if (oldSources !== newSources) { const auto = makeAutoName(order); newKey = auto.key; newLabel = auto.label; delete combined[oldKey]; } const newTags = [...new Set(order.flatMap(n => data[n] || []))]; combined[newKey] = { label: newLabel, fullKey: newKey, sources: [...order], tags: newTags }; saveCombined(combined); alert("Updated."); close(); }; const cancelBtn = el("button", { textContent: "Cancel", style: "width:100%;padding:8px;background:#7f8c8d;color:white;border:none;border-radius:6px;font-weight:700;cursor:pointer;" }); cancelBtn.onclick = close; modal.append(saveBtn, cancelBtn); function close() { const b = document.getElementById("edit-backdrop"); if (b) b.remove(); } }; // ------------------------------ // Combined Popup (with circle close) // ------------------------------ const openCombinedPopup = () => { const combined = loadCombined(); const keys = Object.keys(combined).sort((a, b) => { const la = (combined[a].label || a).toLowerCase(); const lb = (combined[b].label || b).toLowerCase(); return la.localeCompare(lb); }); if (!keys.length) return alert("No combined bundles exist."); if (document.getElementById("combined-backdrop")) return; const backdrop = el("div", { id: "combined-backdrop", style: "position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.4);z-index:999999;" }); document.body.append(backdrop); const popup = el("div", { id: "combined-popup", style: "position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);background:#1f1f1f;color:white;width:420px;padding:18px;border-radius:12px;z-index:1000000;max-height:560px;overflow-y:auto;box-shadow:0 6px 24px rgba(0,0,0,0.6);" }); backdrop.append(popup); backdrop.onclick = (e) => { if (e.target === backdrop) close(); }; popup.onclick = (e) => e.stopPropagation(); popup.append(el("div", { textContent: "Combined Bundles", style: "font-weight:700;margin-bottom:12px;font-size:15px;" })); // circle close button (22px) const closeBtn = el("div", { html: "❌", style: "position:absolute;right:12px;top:12px;font-size:22px;cursor:pointer;color:#fff;opacity:0.85;user-select:none;" }); closeBtn.onclick = () => close(); closeBtn.onmouseenter = () => closeBtn.style.opacity = "1"; closeBtn.onmouseleave = () => closeBtn.style.opacity = "0.85"; popup.append(closeBtn); keys.forEach(key => { const combo = combined[key]; const wrap = el("div", { style: "background:#262626;border-radius:8px;padding:10px;margin-bottom:10px;" }); const nameEl = el("div", { textContent: combo.label, style: "font-weight:700;font-size:14px;margin-bottom:4px;" }); const srcEl = el("div", { textContent: "from: " + combo.sources.join(", "), style: "color:#bbb;font-size:12px;margin-bottom:8px;" }); const tagsEl = el("div", { textContent: combo.tags.join(" "), style: "color:#ddd;margin-bottom:8px;word-break:break-word;" }); const btnRow = el("div", { style: "display:flex;gap:8px;" }); const mkbtn = (t, bg, fn) => el("button", { textContent: t, onclick: fn, style: `flex:1;padding:8px;border:none;border-radius:6px;background:${bg};color:white;font-weight:700;cursor:pointer;` }); const insertBtn = mkbtn("Insert", "#2196F3", () => { const s = combo.tags.join(" ") + " "; const ok = insertTextAtEnd(s); if (!ok) return alert("Insert failed (composer not found)."); confirmButton(insertBtn, "Insert", "Inserted!"); }); const copyBtn = mkbtn("Copy", "#4CAF50", async () => { await navigator.clipboard.writeText(combo.tags.join(" ")).catch(() => alert("Copy error")); confirmButton(copyBtn, "Copy", "Copied!"); }); const editBtn = mkbtn("Edit", "#f39c12", () => openEditCombinedModal(key)); const delBtn = mkbtn("Delete", "#c0392b", () => { if (!confirm(`Delete "${combo.label}"?`)) return; const all = loadCombined(); delete all[key]; saveCombined(all); wrap.remove(); }); btnRow.append(insertBtn, copyBtn, editBtn, delBtn); wrap.append(nameEl, srcEl, tagsEl, btnRow); popup.append(wrap); }); function close() { const b = document.getElementById("combined-backdrop"); if (b) b.remove(); } }; // ------------------------------ // Auto-panel creation when composer appears // ------------------------------ let panelVisible = false; const observer = new MutationObserver(() => { const c = document.querySelector("textarea, div[contenteditable='true']"); if (c && !panelVisible) { createPanel(); panelVisible = true; } else if (!c && panelVisible) { const p = document.getElementById(PANEL_ID); if (p) p.remove(); panelVisible = false; } }); observer.observe(document.body, { childList: true, subtree: true }); if (document.querySelector("textarea, div[contenteditable='true']")) { createPanel(); panelVisible = true; } })();