Este userscript muestra los atajos de Discourse directamente en el foro:
Está desactivado por defecto, haz clic en el icono superior derecho para activarlo/desactivarlo. ![]()
No está pensado para ser bonito. Simplemente actívalo cuando sientas que quieres usar y memorizar los atajos ![]()
No muestra todos los atajos existentes, pero sí la mayoría.
Los atajos relacionados con marcadores no están (¿todavía?) incluidos, hay muchos para lo que es una sección de nicho del sitio.
Hay unos pocos atajos que no entiendo en la lista de atajos ?:
-
Ctrl + l → Filtrar barra lateral
-
Selección de temas:
Shift + d → Descartar temas seleccionados -
a Insertar selección en el compositor abierto
Si alguien puede explicarme qué hacen ![]()
Código completo
// ==UserScript==
// @name Discourse Shortcut Overlay
// @namespace https://meta.discourse.org/
// @version 1.1
// @description Inyecta pistas kbd directamente en los elementos de la interfaz de Discourse para mostrar atajos de teclado en contexto.
// @match https://meta.discourse.org/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function discourseShortcutOverlay() {
"use strict";
if (window.__dso__) {
window.__dso__.stop();
}
// ── CSS ─────────────────────────────────────────────────────────────────────
const css = document.createElement("style");
css.id = "__dso_style";
css.textContent = `
.dso-wrap {
display: inline-flex; align-items: center; gap: 2px;
pointer-events: none; flex-shrink: 0; user-select: none; vertical-align: text-bottom;
}
.dso-inline { margin-left: auto; padding-left: 6px; }
.dso-abs { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 99; }
.dso-abs-br { position: absolute; right: 4px; bottom: 4px; z-index: 99; }
.dso-abs-br.dso-hint-eq { right: unset; left: 4px; }
.dso-abs-tr { position: absolute; right: 4px; top: 4px; z-index: 99; }
.dso-wrap kbd {
display: inline-flex; align-items: center; justify-content: center;
padding: 0 5px; font: 700 11px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
color: #fff; border-radius: 4px; border-color: rgba(255,255,255,.22);
box-shadow: 0 1px 3px rgba(0,0,0,.2); min-height: 20px; white-space: nowrap; letter-spacing: 0;
}
.dso-wrap.nav kbd { background: color-mix(in srgb, var(--success) 70%, var(--secondary)); }
.dso-wrap.action kbd { background: color-mix(in srgb, var(--tertiary) 70%, var(--secondary)); }
.dso-wrap.write kbd { background: color-mix(in srgb, #8848b8 70%, var(--secondary)); }
.dso-wrap.post kbd { background: color-mix(in srgb, var(--danger) 70%, var(--secondary)); }
.dso-wrap .dso-plus { font: 700 9px/1 ui-monospace, monospace; color: rgba(255,255,255,.5); margin: 0 1px; }
.dso-wrap .dso-seq { font: 700 9px/1 ui-monospace, monospace; color: rgba(255,255,255,.45); margin: 0 2px; }
.dso-gnav-label { font: 10px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; color: rgba(255,255,255,.35); }
.dso-gnav-panel { display: flex; flex-wrap: wrap; gap: 4px 14px; padding: 4px 0 6px; vertical-align: unset; }
.dso-gnav-panel .dso-gnav-row { display: inline-flex; align-items: center; gap: 5px; }
.dso-gnav-panel .dso-gnav-keys { display: inline-flex; align-items: center; }
.timeline-container .topic-timeline .timeline-scroller-content { overflow: visible !important; }
.timeline-container .topic-timeline .timeline-date-wrapper { max-width: none !important; }
body.dso-hidden .dso-wrap { display: none !important; }
#dso-toggle-btn {
position: fixed; top: calc(14px + 3.5em); right: 14px; z-index: 99999; cursor: pointer;
background: var(--secondary); border: 1px solid var(--primary-low); border-radius: 6px;
color: var(--primary-medium); padding: 5px 7px; line-height: 1;
box-shadow: 0 2px 6px rgba(0,0,0,.1);
}
#dso-toggle-btn:hover { color: var(--primary); border-color: var(--primary-medium); }
#dso-toggle-btn.--off { color: var(--primary-low); border-color: var(--primary-very-low, var(--primary-low)); }
`;
document.head.appendChild(css);
// ── Registro de pistas ─────────────────────────────────────────────────────────────
const HINTS = [];
const $ = (s) => document.querySelector(s);
function keysToClass(keys) {
return (
"dso-hint-" +
keys
.map((k) =>
k
.replace(/⇧\s*/g, "shift-")
.replace(/↵/g, "enter")
.replace(/↓/g, "down")
.replace(/↑/g, "up")
.replace(/\//g, "slash")
.replace(/\?/g, "question")
.replace(/!/g, "bang")
.replace(/#/g, "hash")
.replace(/\./g, "dot")
.replace(/=/g, "eq")
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
)
.join("-")
);
}
// Pista de objetivo único
function hint(
keys,
theme,
targetFn,
mode = "inline",
beforeSel = null,
seq = false,
title = ""
) {
HINTS.push({
keys,
theme,
targetFn,
mode,
beforeSel,
seq,
title,
hintClass: keysToClass(keys),
customBuild: null,
state: { el: null, wrap: null },
});
}
// Pista de múltiples objetivos – inyecta después de cada elemento que coincida con multiSel
function hintMulti(
keys,
theme,
multiSel,
mode = "after",
seq = false,
title = ""
) {
HINTS.push({
keys,
theme,
multiSel,
mode,
seq,
title,
hintClass: keysToClass(keys),
state: { pairs: [] },
});
}
// Pista de panel personalizado
function hintPanel(theme, mode, hintClass, targetFn, buildFn, opts = {}) {
HINTS.push({
keys: null,
theme,
mode,
beforeSel: opts.beforeSel ?? null,
seq: opts.seq ?? false,
hintClass,
targetFn,
customBuild: buildFn,
state: { el: null, wrap: null },
});
}
// ── Constructores DOM ──────────────────────────────────────────────────────────────
// Micro-ayuda: crear un elemento con propiedades asignadas
function el(tag, props = {}) {
return Object.assign(document.createElement(tag), props);
}
function buildWrap(keys, theme, mode, seq = false, title = "") {
const modeClass =
{ abs: "dso-abs", "abs-br": "dso-abs-br", "abs-tr": "dso-abs-tr" }[
mode
] ?? (mode === "sibling" || mode === "after" ? "" : "dso-inline");
const wrap = el("span", {
className: `dso-wrap ${theme} ${modeClass}`.trimEnd(),
});
if (title) {
wrap.title = title;
}
keys.forEach((k, i) => {
if (i > 0) {
wrap.appendChild(
el("span", {
className: seq ? "dso-seq" : "dso-plus",
textContent: seq ? "→" : "+",
})
);
}
wrap.appendChild(el("kbd", { textContent: k }));
});
return wrap;
}
// Panel genérico: filas de secuencias kbd con etiquetas opcionales.
// usePlus: usa separadores "+" (combinaciones) en lugar de "→" (secuencias).
function buildPanel(theme, entries, wrapStyle = {}, usePlus = false) {
const wrap = el("span", { className: `dso-wrap ${theme}` });
Object.assign(wrap.style, wrapStyle);
for (const { keys, label } of entries) {
const row = el("span");
Object.assign(row.style, {
display: "inline-flex",
alignItems: "center",
gap: "4px",
});
keys.forEach((k, i) => {
if (i > 0) {
row.appendChild(
el("span", {
className: usePlus ? "dso-plus" : "dso-seq",
textContent: usePlus ? "+" : "→",
})
);
}
row.appendChild(el("kbd", { textContent: k }));
});
if (label) {
row.appendChild(
el("span", { className: "dso-gnav-label", textContent: label })
);
}
wrap.appendChild(row);
}
return wrap;
}
// G-nav usa clases CSS dedicadas para su diseño de envoltura
function buildGNavPanel() {
const panel = el("div", { className: "dso-wrap dso-gnav-panel nav" });
for (const { keys, label } of [
{ keys: ["u"], label: "Volver" },
{ keys: ["g", "h"], label: "Inicio" },
{ keys: ["g", "l"], label: "Más recientes" },
{ keys: ["g", "n"], label: "Nuevos" },
{ keys: ["g", "u"], label: "Sin leer" },
{ keys: ["g", "y"], label: "No vistos" },
{ keys: ["g", "c"], label: "Categorías" },
{ keys: ["g", "t"], label: "Top" },
]) {
const kg = el("span", { className: "dso-gnav-keys" });
keys.forEach((k, i) => {
if (i > 0) {
kg.appendChild(
el("span", { className: "dso-seq", textContent: "→" })
);
}
kg.appendChild(el("kbd", { textContent: k }));
});
const row = el("div", { className: "dso-gnav-row" });
row.appendChild(kg);
row.appendChild(
el("span", { className: "dso-gnav-label", textContent: label })
);
panel.appendChild(row);
}
return panel;
}
// ── Lógica de inyección ───────────────────────────────────────────────────────────
function inject(h) {
const target = h.targetFn();
const { state, keys, theme, mode, beforeSel } = h;
if (target === state.el) {
return;
} // sin cambio – ya inyectado
if (state.wrap) {
state.wrap.remove();
}
if (state.el?.dataset.dsoPos !== undefined) {
state.el.style.position = state.el.dataset.dsoPos || "";
delete state.el.dataset.dsoPos;
}
state.el = target;
state.wrap = null;
if (!target) {
return;
}
if (mode === "abs" || mode === "abs-br" || mode === "abs-tr") {
if (getComputedStyle(target).position === "static") {
target.dataset.dsoPos = "";
target.style.position = "relative";
}
}
const wrap = h.customBuild
? h.customBuild()
: buildWrap(keys, theme, mode, h.seq, h.title);
if (h.hintClass) {
wrap.classList.add(h.hintClass);
}
let insertParent, insertRef;
if (mode === "after") {
insertParent = target.parentNode;
insertRef = target.nextSibling;
} else {
const beforeEl = beforeSel ? target.querySelector(beforeSel) : null;
insertParent = beforeEl ? beforeEl.parentNode : target;
insertRef = beforeEl;
}
insertParent.insertBefore(wrap, insertRef);
state.wrap = wrap;
}
function injectMulti(h) {
const { state } = h;
const targets = new Set(document.querySelectorAll(h.multiSel));
state.pairs = state.pairs.filter(({ el: e, wrap }) => {
if (!targets.has(e)) {
wrap.remove();
return false;
}
return true;
});
const covered = new Set(state.pairs.map((p) => p.el));
targets.forEach((target) => {
if (covered.has(target)) {
return;
}
const wrap = buildWrap(h.keys, h.theme, h.mode, h.seq, h.title);
if (h.hintClass) {
wrap.classList.add(h.hintClass);
}
target.parentNode?.insertBefore(wrap, target.nextSibling);
state.pairs.push({ el: target, wrap });
});
}
// ────────────────────────────────────────────────────────────────────────────
// DEFINICIONES DE PISTAS
// ────────────────────────────────────────────────────────────────────────────
// ── Encabezado ───────────────────────────────────────────────────────────────────
// / → búsqueda (oculto cuando la entrada tiene foco)
hint(
["/"],
"action",
() => {
const input = $("#header-search-input");
if (!input || input === document.activeElement) {
return null;
}
return input.closest(".search-input--header")?.parentElement ?? null;
},
"sibling",
".show-advanced-search",
false,
"Buscar"
);
// ↑ / ↓ → navegar resultados de búsqueda
hint(
["↑ / ↓"],
"action",
() =>
$(".search-input--header")
? $(".search-menu-initial-options, .search-menu-assistant") || null
: null,
"abs-tr",
null,
false,
"Navegar resultados"
);
// Ctrl+↵ → abrir búsqueda de página completa (solo cuando la entrada tiene foco)
hint(
["Ctrl", "↵"],
"action",
() => {
const input = $("#header-search-input");
if (!input || input !== document.activeElement) {
return null;
}
return input;
},
"after",
null,
false,
"Búsqueda de página completa"
);
// c → nuevo tema
hint(
["c"],
"write",
() => $("#create-topic"),
"inline",
null,
false,
"Nuevo tema"
);
// ? → modal de atajos de teclado
hint(
["?"],
"action",
() => $(".keyboard-shortcuts-btn, button[aria-label='Atajos de teclado']"),
"inline",
null,
false,
"Atajos"
);
// . → cargar banner de temas nuevos/actualizados
hint(
["."],
"action",
() => $(".show-more.has-topics a"),
"inline",
null,
false,
"Cargar nuevos temas"
);
// ── Lista de temas ───────────────────────────────────────────────────────────────
// Excluye .more-topics__list / .suggested-topics para que las pistas no se filtren a esa área.
function mainListRows() {
return [...document.querySelectorAll(".topic-list-item")].filter(
(r) => !r.closest(".more-topics__list, .suggested-topics, .more-topics")
);
}
hint(
["j ↓"],
"nav",
() => {
const rows = mainListRows();
if (!rows.length) {
return null;
}
const selIdx = rows.findIndex((r) => r.classList.contains("selected"));
if (selIdx === -1) {
return rows[0]?.querySelector(".main-link") || null;
}
if (selIdx >= rows.length - 1) {
return null;
}
return rows[selIdx + 1]?.querySelector(".main-link") || null;
},
"abs",
null,
false,
"Siguiente tema"
);
hint(
["k ↑"],
"nav",
() => {
const rows = mainListRows();
if (!rows.length) {
return null;
}
const selIdx = rows.findIndex((r) => r.classList.contains("selected"));
if (selIdx <= 0) {
return null;
}
return rows[selIdx - 1]?.querySelector(".main-link") || null;
},
"abs",
null,
false,
"Tema anterior"
);
// o / Enter → abrir tema seleccionado
hint(
["o / ↵"],
"nav",
() =>
$(
".topic-list tr.selected a.title, .topic-list-item.selected .main-link a, .topic-list-item.selected .topic-title a"
),
"inline",
null,
false,
"Abrir tema"
);
// ⇧j / ⇧k → siguiente/anterior sección de pestaña de navegación
hint(
["⇧ j"],
"nav",
() => {
const pills = [...document.querySelectorAll(".nav.nav-pills li")];
const i = pills.findIndex((p) => p.classList.contains("active"));
return pills[i + 1] || null;
},
"inline",
null,
false,
"Siguiente sección"
);
hint(
["⇧ k"],
"nav",
() => {
const pills = [...document.querySelectorAll(".nav.nav-pills li")];
const i = pills.findIndex((p) => p.classList.contains("active"));
return i > 0 ? pills[i - 1] : null;
},
"inline",
null,
false,
"Sección anterior"
);
// ── Compositor ──────────────────────────────────────────────────────────────────
hint(
["Ctrl", "↵"],
"write",
() =>
$(".save-or-cancel .create, .reply-area .create, #reply-control .create"),
"inline",
null,
false,
"Enviar"
);
hint(
["Esc"],
"write",
() => $(".save-or-cancel .cancel, .reply-area .cancel"),
"inline",
null,
false,
"Cancelar"
);
// ⇧c → volver al compositor minimizado
hint(
["⇧ c"],
"write",
() => $("#reply-control.draft .draft-text"),
"inline",
null,
false,
"Volver al compositor"
);
// ⇧F11 → pantalla completa (oculto cuando el compositor es un borrador)
hint(
["⇧ F11"],
"write",
() => ($("#reply-control.draft") ? null : $(".toggle-fullscreen")),
"after",
null,
false,
"Pantalla completa"
);
hint(
["Esc"],
"write",
() => $(".toggle-minimize"),
"after",
null,
false,
"Minimizar"
);
// ── Opciones del compositor (+) ventana emergente ────────────────────────────────────────────────
hint(
["Ctrl", "e"],
"write",
() => $('[data-name="format-code"]'),
"abs-br",
null,
false,
"Texto preformateado"
);
hint(
["Ctrl", "⇧ 8"],
"write",
() => $('[data-name="apply-unordered-list"]'),
"abs-br",
null,
false,
"Lista con viñetas"
);
hint(
["Ctrl", "⇧ 7"],
"write",
() => $('[data-name="apply-ordered-list"]'),
"abs-br",
null,
false,
"Lista ordenada"
);
hint(
["Ctrl", "Alt", "0"],
"write",
() => $('[data-name="heading-paragraph"]'),
"abs-br",
null,
false,
"Párrafo"
);
hint(
["Ctrl", "Alt", "1"],
"write",
() => $('[data-name="heading-1"]'),
"abs-br",
null,
false,
"Título 1"
);
hint(
["Ctrl", "Alt", "2"],
"write",
() => $('[data-name="heading-2"]'),
"abs-br",
null,
false,
"Título 2"
);
hint(
["Ctrl", "Alt", "3"],
"write",
() => $('[data-name="heading-3"]'),
"abs-br",
null,
false,
"Título 3"
);
hint(
["Ctrl", "Alt", "4"],
"write",
() => $('[data-name="heading-4"]'),
"abs-br",
null,
false,
"Título 4"
);
hint(
["Ctrl", "l"],
"write",
() => $('.toolbar__button[title="Hipervínculo"]'),
"abs-br",
null,
false,
"Insertar enlace"
);
// ── Acciones de publicación (inyectadas en cada publicación visible) ─────────────────────────────
hintMulti(
["r"],
"post",
".topic-post .post-controls button.reply",
"after",
false,
"Responder a la publicación"
);
hintMulti(
["e"],
"post",
".topic-post .post-controls button.edit",
"after",
false,
"Editar publicación"
);
hintMulti(
["l"],
"post",
".topic-post .discourse-reactions-double-button, .topic-post .post-controls button.toggle-like",
"after",
false,
"Me gusta la publicación"
);
hintMulti(
["b"],
"post",
".topic-post .post-controls .post-action-menu__bookmark",
"after",
false,
"Marcar publicación"
);
hintMulti(
["s"],
"post",
".topic-post a.post-date",
"after",
false,
"Compartir publicación"
);
hintMulti(
["q"],
"post",
".topic-post .post-controls button.quote-post",
"after",
false,
"Citar publicación"
);
hintMulti(
["!"],
"post",
".topic-post .post-controls .post-action-menu__flag, .topic-post .post-controls button.create-flag",
"after",
false,
"Reportar publicación"
);
hintMulti(
["d"],
"post",
".topic-post .post-controls .post-action-menu__delete, .topic-post .post-controls button.delete",
"after",
false,
"Eliminar publicación"
);
// ── Vista de tema – navegación de publicaciones ──────────────────────────────────────────────
hint(
["j ↓"],
"nav",
() => {
if (!$(".timeline-container")) {
return null;
}
const posts = [...document.querySelectorAll(".topic-post")];
const selIdx = posts.findIndex((p) => p.classList.contains("selected"));
if (selIdx === -1) {
return posts[0] || null;
}
return selIdx < posts.length - 1 ? posts[selIdx + 1] : null;
},
"abs",
null,
false,
"Siguiente publicación"
);
hint(
["k ↑"],
"nav",
() => {
if (!$(".timeline-container")) {
return null;
}
const posts = [...document.querySelectorAll(".topic-post")];
const selIdx = posts.findIndex((p) => p.classList.contains("selected"));
return selIdx > 0 ? posts[selIdx - 1] : null;
},
"abs",
null,
false,
"Publicación anterior"
);
// j/k en el desplazador de línea de tiempo – posicionado a su derecha
hintPanel(
"nav",
"abs",
"dso-hint-timeline-jk",
() => $(".timeline-container") && ($(".timeline-scroller-content") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["k ↑"], label: "Publicación anterior" },
{ keys: ["j ↓"], label: "Siguiente publicación" },
],
{
position: "absolute",
left: "calc(100% + 8px)",
top: "50%",
transform: "translateY(-50%)",
flexDirection: "column",
gap: "3px",
zIndex: "99",
}
)
);
// # → saltar al número de publicación
hint(
["#"],
"nav",
() => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
"inline",
null,
false,
"Saltar a publicación"
);
// ⇧l → ir a la primera sin leer – colocada dentro del área #
hint(
["⇧ l"],
"nav",
() => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
"inline",
null,
false,
"Primera sin leer"
);
// ⇧r → responder al tema (botón del pie de página)
hint(
["⇧ r"],
"post",
() =>
$(
".topic-footer-main-buttons button.btn-primary.create, #topic-footer-buttons .btn.reply"
),
"inline",
null,
false,
"Responder al tema"
);
// ── Navegación G ──────────────────────────────────────────────────────────────
// u + g→h … g→t – panel inyectado encima de .nav-pills
hintPanel(
"nav",
"sibling",
"dso-hint-gnav",
() =>
$(".navigation-container, .list-controls .container, .navigation-bar"),
buildGNavPanel,
{ beforeSel: ".nav-pills", seq: true }
);
hint(
["g", "b"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/bookmarks'], .user-menu a[href*='/bookmarks']"
),
"inline",
null,
true,
"Marcadores"
);
hint(
["g", "m"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/messages'], .user-menu a[href*='/messages']"
),
"inline",
null,
true,
"Mensajes"
);
hint(
["g", "d"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/drafts'], .user-menu a[href*='/drafts']"
),
"inline",
null,
true,
"Borradores"
);
hint(
["g", "p"],
"nav",
() => $("#user-menu-button-profile"),
"inline",
null,
true,
"Perfil"
);
// g→k / g→j – panel de tema anterior/siguiente junto al título del tema
hintPanel(
"nav",
"abs",
"dso-hint-topic-nav",
() =>
$(".timeline-container") &&
($(".title-wrapper #topic-title, .title-wrapper .topic-title") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["g", "k"], label: "Tema anterior" },
{ keys: ["g", "j"], label: "Siguiente tema" },
],
{
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
flexDirection: "column",
gap: "4px",
alignItems: "flex-start",
zIndex: "99",
}
),
{ seq: true }
);
// ── Botones del encabezado ────────────────────────────────────────────────────────────
hint(
["="],
"action",
() => $(".header-sidebar-toggle")?.parentElement ?? null,
"abs-br",
null,
false,
"Barra lateral"
);
hint(
["p"],
"action",
() => $("#current-user"),
"abs-br",
null,
false,
"Perfil"
);
// ── Acciones del tema ─────────────────────────────────────────────────────────────
// f → marcar tema
hint(
["f"],
"post",
() =>
$(".timeline-container") &&
($('.topic-footer-main-buttons [data-identifier="bookmark-menu"]') ||
null),
"inline",
null,
false,
"Marcar tema"
);
hint(
["⇧ s"],
"post",
() => $("#topic-footer-buttons button.share-and-invite"),
"inline",
null,
false,
"Compartir tema"
);
hint(
["⇧ p"],
"nav",
() => $(".pinned-button button"),
"inline",
null,
false,
"Fijar/Desfijar"
);
hint(
["⇧ u"],
"nav",
() =>
$(".timeline-container") &&
($("#topic-footer-buttons button.defer-topic") || null),
"inline",
null,
false,
"Marcar como no leído"
);
hint(
["⇧ a"],
"action",
() => $(".toggle-admin-menu"),
"abs-br",
null,
false,
"Acciones de administrador"
);
hint(
["a"],
"action",
() =>
$(".timeline-container") &&
($("#topic-footer-buttons button.archive-topic") || null),
"inline",
null,
false,
"Archivar"
);
// m→w / m→t / m→r / m→m – panel de nivel de notificación
hintPanel(
"nav",
"inline",
"dso-hint-tracking",
() => $(".timeline-container") && ($(".timeline-footer-controls") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["m", "w"], label: "Observar" },
{ keys: ["m", "t"], label: "Seguir" },
{ keys: ["m", "r"], label: "Normal" },
{ keys: ["m", "m"], label: "Silenciar" },
],
{ flexDirection: "column", gap: "3px" }
),
{ seq: true }
);
// ── Selección masiva ───────────────────────────────────────────────────────────────
hint(
["⇧ b"],
"action",
() => $("button.bulk-select"),
"abs-br",
null,
false,
"Selección masiva"
);
hint(
["⇧ d"],
"action",
() => $("#dismiss-topics-top, #dismiss-new-top, .dismiss-read"),
"inline",
null,
false,
"Descartar"
);
hint(
["x"],
"nav",
() =>
$(".topic-list-item.selected td.bulk-select") ||
$(".topic-list-item td.bulk-select"),
"abs-br",
null,
false,
"Seleccionar fila"
);
// ── Cerrar sesión ────────────────────────────────────────────────────────────────────
hint(
["⇧ z", "⇧ z"],
"action",
() => $("li.logout button"),
"inline",
null,
true,
"Cerrar sesión"
);
// ── Chat ──────────────────────────────────────────────────────────────────────
hint(
["-"],
"action",
() => $(".chat-header-icon"),
"abs-br",
null,
false,
"Alternar chat"
);
// Alt+↑/↓, Alt+⇧↑/↓, ⇧Esc, Ctrl+K – panel añadido al cajón del chat
hintPanel(
"action",
"inline",
"dso-hint-chat-composer",
() => $(".chat-drawer-content"),
() =>
buildPanel(
"action",
[
{ keys: ["Ctrl", "k"], label: "Canal rápido" },
{ keys: ["Alt", "↑ / ↓"], label: "Cambiar canal" },
{ keys: ["Alt", "⇧ ↑ / ↓"], label: "Cambiar sin leer" },
{ keys: ["⇧", "Esc"], label: "Marcar todo como leído" },
],
{
flexWrap: "wrap",
gap: "6px 12px",
padding: "4px 6px",
borderTop: "1px solid rgba(255,255,255,.08)",
marginTop: "2px",
},
true
)
);
hint(
["Esc"],
"action",
() => $(".c-navbar__close-drawer-button"),
"after",
null,
false,
"Cerrar chat"
);
// ── Botón de alternancia ─────────────────────────────────────────────────────────────
let visible = localStorage.getItem("dso-visible") === "true";
const toggleBtn = el("button", {
id: "dso-toggle-btn",
title: "Alternar superposición de atajos",
textContent: "⌨",
});
if (!visible) {
toggleBtn.classList.add("--off");
}
document.body.appendChild(toggleBtn);
toggleBtn.addEventListener("click", () => {
window.__dso__.toggle();
});
// ── MutationObserver ──────────────────────────────────────────────────────────
let rafPending = false;
function scheduleRefresh() {
if (!rafPending) {
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
HINTS.forEach((h) => (h.multiSel ? injectMulti(h) : inject(h)));
});
}
}
const observer = new MutationObserver((mutations) => {
const onlyOurs = mutations.every((m) => {
if (m.type === "attributes") {
return false;
}
return [...m.addedNodes, ...m.removedNodes].every(
(n) => n.nodeType !== 1 || n.classList?.contains("dso-wrap")
);
});
if (!onlyOurs) {
scheduleRefresh();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class"],
});
document.addEventListener("focusin", scheduleRefresh, { passive: true });
document.addEventListener("focusout", scheduleRefresh, { passive: true });
document.body.classList.toggle("dso-hidden", !visible);
HINTS.forEach((h) => (h.multiSel ? injectMulti(h) : inject(h)));
// ── API Pública ────────────────────────────────────────────────────────────────
window.__dso__ = {
stop() {
observer.disconnect();
document.removeEventListener("focusin", scheduleRefresh);
document.removeEventListener("focusout", scheduleRefresh);
HINTS.forEach(({ state }) => {
state.wrap?.remove();
state.pairs?.forEach(({ wrap }) => wrap.remove());
if (state.el?.dataset.dsoPos !== undefined) {
state.el.style.position = state.el.dataset.dsoPos || "";
delete state.el.dataset.dsoPos;
}
});
toggleBtn.remove();
document.body.classList.remove("dso-hidden");
css.remove();
delete window.__dso__;
console.log("%c[DSO] eliminado.", "color:#888");
},
toggle() {
visible = !visible;
localStorage.setItem("dso-visible", visible);
toggleBtn.classList.toggle("--off", !visible);
document.body.classList.toggle("dso-hidden", !visible);
},
};
console.log(
"%c[DSO] Discourse Shortcut Overlay activo.",
"color:#6af;font-weight:bold"
);
})();
Reemplaza o añade coincidencias de URL (// @match https://meta.discourse.org/*) si quieres usarlo en otro foro de Discourse.
Para instalar userscripts, usa una extensión de navegador: Greasemonkey, Tampermonkey, ScriptCat, o cualquier software externo que proporcione dicha funcionalidad, por ejemplo AdGuard.





