Ce userscript affiche les raccourcis Discourse directement sur le forum :
Il est désactivé par défaut, cliquez sur l’icône en haut à droite pour l’activer. ![]()
Il n’est pas censé être beau. Activez-le simplement lorsque vous avez envie d’utiliser et de mémoriser les raccourcis ![]()
Il n’affiche pas tous les raccourcis existants, mais la plupart.
Les raccourcis liés aux favoris ne sont pas (encore ?) inclus, car ils sont nombreux pour une section de niche du site.
Il y a quelques raccourcis que je ne comprends pas dans la liste des raccourcis ? :
-
Ctrl + l → Filtrer la barre latérale
-
Sélection de sujet :
Shift + d → Ignorer les sujets sélectionnés -
a Insérer la sélection dans le rédacteur ouvert
Si quelqu’un peut m’expliquer ce qu’ils font ![]()
Code complet
// ==UserScript==
// @name Discourse Shortcut Overlay
// @namespace https://meta.discourse.org/
// @version 1.1
// @description Injecte des indices kbd directement dans les éléments d’interface Discourse pour afficher les raccourcis clavier en contexte.
// @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);
// ── Registre des indices ─────────────────────────────────────────────────────────────
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("-")
);
}
// Indice cible unique
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 },
});
}
// Indice multi-cibles – injecte après chaque élément correspondant à multiSel
function hintMulti(
keys,
theme,
multiSel,
mode = "after",
seq = false,
title = ""
) {
HINTS.push({
keys,
theme,
multiSel,
mode,
seq,
title,
hintClass: keysToClass(keys),
state: { pairs: [] },
});
}
// Indice panneau personnalisé
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 },
});
}
// ── Constructeurs DOM ─────────────────────────────────────────────────────────────
// Micro-aide : créer un élément avec des propriétés assignées
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;
}
// Panneau générique : lignes de séquences kbd avec étiquettes optionnelles.
// usePlus : utiliser des séparateurs « + » (combos) au lieu de « → » (séquences).
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 utilise des classes CSS dédiées pour sa mise en page enveloppante
function buildGNavPanel() {
const panel = el("div", { className: "dso-wrap dso-gnav-panel nav" });
for (const { keys, label } of [
{ keys: ["u"], label: "Retour" },
{ keys: ["g", "h"], label: "Accueil" },
{ keys: ["g", "l"], label: "Récent" },
{ keys: ["g", "n"], label: "Nouveau" },
{ keys: ["g", "u"], label: "Non lu" },
{ keys: ["g", "y"], label: "Non vu" },
{ keys: ["g", "c"], label: "Catégories" },
{ 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;
}
// ── Logique d’injection ───────────────────────────────────────────────────────────
function inject(h) {
const target = h.targetFn();
const { state, keys, theme, mode, beforeSel } = h;
if (target === state.el) {
return;
} // pas de changement – déjà injecté
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 });
});
}
// ────────────────────────────────────────────────────────────────────────────
// DÉFINITIONS DES INDICES
// ────────────────────────────────────────────────────────────────────────────
// ── En-tête ──────────────────────────────────────────────────────────────────
// / → recherche (masqué lorsque l’entrée est focalisée)
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,
"Recherche"
);
// ↑ / ↓ → naviguer dans les résultats de recherche
hint(
["↑ / ↓"],
"action",
() =>
$(".search-input--header")
? $(".search-menu-initial-options, .search-menu-assistant") || null
: null,
"abs-tr",
null,
false,
"Naviguer dans les résultats"
);
// Ctrl+↵ → ouvrir la recherche page complète (uniquement lorsque l’entrée est focalisée)
hint(
["Ctrl", "↵"],
"action",
() => {
const input = $("#header-search-input");
if (!input || input !== document.activeElement) {
return null;
}
return input;
},
"after",
null,
false,
"Recherche page complète"
);
// c → nouveau sujet
hint(
["c"],
"write",
() => $("#create-topic"),
"inline",
null,
false,
"Nouveau sujet"
);
// ? → modal raccourcis clavier
hint(
["?"],
"action",
() => $(".keyboard-shortcuts-btn, button[aria-label='Keyboard shortcuts']"),
"inline",
null,
false,
"Raccourcis"
);
// . → charger la bannière des nouveaux sujets mis à jour
hint(
["."],
"action",
() => $(".show-more.has-topics a"),
"inline",
null,
false,
"Charger les nouveaux sujets"
);
// ── Liste des sujets ───────────────────────────────────────────────────────────────
// Exclut .more-topics__list / .suggested-topics pour que les indices ne débordent pas dans cette zone.
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,
"Sujet suivant"
);
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,
"Sujet précédent"
);
// o / Entrée → ouvrir le sujet sélectionné
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,
"Ouvrir le sujet"
);
// ⇧j / ⇧k → section suivante/précédente de l’onglet de navigation
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,
"Section suivante"
);
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,
"Section précédente"
);
// ── Rédacteur ──────────────────────────────────────────────────────────────────
hint(
["Ctrl", "↵"],
"write",
() =>
$(".save-or-cancel .create, .reply-area .create, #reply-control .create"),
"inline",
null,
false,
"Soumettre"
);
hint(
["Esc"],
"write",
() => $(".save-or-cancel .cancel, .reply-area .cancel"),
"inline",
null,
false,
"Annuler"
);
// ⇧c → revenir au rédacteur minimisé
hint(
["⇧ c"],
"write",
() => $("#reply-control.draft .draft-text"),
"inline",
null,
false,
"Revenir au rédacteur"
);
// ⇧F11 → plein écran (masqué lorsque le rédacteur est un brouillon)
hint(
["⇧ F11"],
"write",
() => ($("#reply-control.draft") ? null : $(".toggle-fullscreen")),
"after",
null,
false,
"Plein écran"
);
hint(
["Esc"],
"write",
() => $(".toggle-minimize"),
"after",
null,
false,
"Minimiser"
);
// ── Options du rédacteur (+) popup ────────────────────────────────────────────────
hint(
["Ctrl", "e"],
"write",
() => $('[data-name="format-code"]'),
"abs-br",
null,
false,
"Texte préformaté"
);
hint(
["Ctrl", "⇧ 8"],
"write",
() => $('[data-name="apply-unordered-list"]'),
"abs-br",
null,
false,
"Liste à puces"
);
hint(
["Ctrl", "⇧ 7"],
"write",
() => $('[data-name="apply-ordered-list"]'),
"abs-br",
null,
false,
"Liste numérotée"
);
hint(
["Ctrl", "Alt", "0"],
"write",
() => $('[data-name="heading-paragraph"]'),
"abs-br",
null,
false,
"Paragraphe"
);
hint(
["Ctrl", "Alt", "1"],
"write",
() => $('[data-name="heading-1"]'),
"abs-br",
null,
false,
"Titre 1"
);
hint(
["Ctrl", "Alt", "2"],
"write",
() => $('[data-name="heading-2"]'),
"abs-br",
null,
false,
"Titre 2"
);
hint(
["Ctrl", "Alt", "3"],
"write",
() => $('[data-name="heading-3"]'),
"abs-br",
null,
false,
"Titre 3"
);
hint(
["Ctrl", "Alt", "4"],
"write",
() => $('[data-name="heading-4"]'),
"abs-br",
null,
false,
"Titre 4"
);
hint(
["Ctrl", "l"],
"write",
() => $('.toolbar__button[title="Hyperlink"]'),
"abs-br",
null,
false,
"Insérer un lien"
);
// ── Actions sur les messages (injectés sur chaque message visible) ─────────────────────────────
hintMulti(
["r"],
"post",
".topic-post .post-controls button.reply",
"after",
false,
"Répondre au message"
);
hintMulti(
["e"],
"post",
".topic-post .post-controls button.edit",
"after",
false,
"Modifier le message"
);
hintMulti(
["l"],
"post",
".topic-post .discourse-reactions-double-button, .topic-post .post-controls button.toggle-like",
"after",
false,
"J’aime le message"
);
hintMulti(
["b"],
"post",
".topic-post .post-controls .post-action-menu__bookmark",
"after",
false,
"Favoriser le message"
);
hintMulti(
["s"],
"post",
".topic-post a.post-date",
"after",
false,
"Partager le message"
);
hintMulti(
["q"],
"post",
".topic-post .post-controls button.quote-post",
"after",
false,
"Citer le message"
);
hintMulti(
["!"],
"post",
".topic-post .post-controls .post-action-menu__flag, .topic-post .post-controls button.create-flag",
"after",
false,
"Signaler le message"
);
hintMulti(
["d"],
"post",
".topic-post .post-controls .post-action-menu__delete, .topic-post .post-controls button.delete",
"after",
false,
"Supprimer le message"
);
// ── Vue du sujet – navigation dans les messages ──────────────────────────────────────
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,
"Message suivant"
);
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,
"Message précédent"
);
// j/k sur le défilement de la chronologie – positionné à sa droite
hintPanel(
"nav",
"abs",
"dso-hint-timeline-jk",
() => $(".timeline-container") && ($(".timeline-scroller-content") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["k ↑"], label: "Message précédent" },
{ keys: ["j ↓"], label: "Message suivant" },
],
{
position: "absolute",
left: "calc(100% + 8px)",
top: "50%",
transform: "translateY(-50%)",
flexDirection: "column",
gap: "3px",
zIndex: "99",
}
)
);
// # → aller au numéro de message
hint(
["#"],
"nav",
() => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
"inline",
null,
false,
"Aller au message"
);
// ⇧l → aller au premier non lu – placé dans la zone #
hint(
["⇧ l"],
"nav",
() => $(".timeline-container") && ($(".timeline-date-wrapper") || null),
"inline",
null,
false,
"Premier non lu"
);
// ⇧r → répondre au sujet (bouton pied de page)
hint(
["⇧ r"],
"post",
() =>
$(
".topic-footer-main-buttons button.btn-primary.create, #topic-footer-buttons .btn.reply"
),
"inline",
null,
false,
"Répondre au sujet"
);
// ── Navigation G ──────────────────────────────────────────────────────────────
// u + g→h … g→t – panneau injecté au-dessus 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,
"Favoris"
);
hint(
["g", "m"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/messages'], .user-menu a[href*='/messages']"
),
"inline",
null,
true,
"Messages"
);
hint(
["g", "d"],
"nav",
() =>
$(
".sidebar-section-link-wrapper a[href*='/drafts'], .user-menu a[href*='/drafts']"
),
"inline",
null,
true,
"Brouillons"
);
hint(
["g", "p"],
"nav",
() => $("#user-menu-button-profile"),
"inline",
null,
true,
"Profil"
);
// g→k / g→j – panneau sujet précédent/suivant à côté du titre du sujet
hintPanel(
"nav",
"abs",
"dso-hint-topic-nav",
() =>
$(".timeline-container") &&
($(".title-wrapper #topic-title, .title-wrapper .topic-title") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["g", "k"], label: "Sujet précédent" },
{ keys: ["g", "j"], label: "Sujet suivant" },
],
{
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
flexDirection: "column",
gap: "4px",
alignItems: "flex-start",
zIndex: "99",
}
),
{ seq: true }
);
// ── Boutons de l’en-tête ────────────────────────────────────────────────────────────
hint(
["="],
"action",
() => $(".header-sidebar-toggle")?.parentElement ?? null,
"abs-br",
null,
false,
"Barre latérale"
);
hint(
["p"],
"action",
() => $("#current-user"),
"abs-br",
null,
false,
"Profil"
);
// ── Actions sur le sujet ─────────────────────────────────────────────────────────────
// f → favoriser le sujet
hint(
["f"],
"post",
() =>
$(".timeline-container") &&
($('.topic-footer-main-buttons [data-identifier="bookmark-menu"]') ||
null),
"inline",
null,
false,
"Favoriser le sujet"
);
hint(
["⇧ s"],
"post",
() => $("#topic-footer-buttons button.share-and-invite"),
"inline",
null,
false,
"Partager le sujet"
);
hint(
["⇧ p"],
"nav",
() => $(".pinned-button button"),
"inline",
null,
false,
"Épingler/Dépingler"
);
hint(
["⇧ u"],
"nav",
() =>
$(".timeline-container") &&
($("#topic-footer-buttons button.defer-topic") || null),
"inline",
null,
false,
"Marquer comme non lu"
);
hint(
["⇧ a"],
"action",
() => $(".toggle-admin-menu"),
"abs-br",
null,
false,
"Actions admin"
);
hint(
["a"],
"action",
() =>
$(".timeline-container") &&
($("#topic-footer-buttons button.archive-topic") || null),
"inline",
null,
false,
"Archiver"
);
// m→w / m→t / m→r / m→m – panneau niveau de notification
hintPanel(
"nav",
"inline",
"dso-hint-tracking",
() => $(".timeline-container") && ($(".timeline-footer-controls") || null),
() =>
buildPanel(
"nav",
[
{ keys: ["m", "w"], label: "Surveiller" },
{ keys: ["m", "t"], label: "Suivre" },
{ keys: ["m", "r"], label: "Normal" },
{ keys: ["m", "m"], label: "Muet" },
],
{ flexDirection: "column", gap: "3px" }
),
{ seq: true }
);
// ── Sélection groupée ───────────────────────────────────────────────────────────────
hint(
["⇧ b"],
"action",
() => $("button.bulk-select"),
"abs-br",
null,
false,
"Sélection groupée"
);
hint(
["⇧ d"],
"action",
() => $("#dismiss-topics-top, #dismiss-new-top, .dismiss-read"),
"inline",
null,
false,
"Ignorer"
);
hint(
["x"],
"nav",
() =>
$(".topic-list-item.selected td.bulk-select") ||
$(".topic-list-item td.bulk-select"),
"abs-br",
null,
false,
"Sélectionner la ligne"
);
// ── Déconnexion ────────────────────────────────────────────────────────────────────
hint(
["⇧ z", "⇧ z"],
"action",
() => $("li.logout button"),
"inline",
null,
true,
"Se déconnecter"
);
// ── Chat ──────────────────────────────────────────────────────────────────────
hint(
["-"],
"action",
() => $(".chat-header-icon"),
"abs-br",
null,
false,
"Basculer le chat"
);
// Alt+↑/↓, Alt+⇧↑/↓, ⇧Esc, Ctrl+K – panneau ajouté au tiroir de chat
hintPanel(
"action",
"inline",
"dso-hint-chat-composer",
() => $(".chat-drawer-content"),
() =>
buildPanel(
"action",
[
{ keys: ["Ctrl", "k"], label: "Canal rapide" },
{ keys: ["Alt", "↑ / ↓"], label: "Changer de canal" },
{ keys: ["Alt", "⇧ ↑ / ↓"], label: "Changer non lu" },
{ keys: ["⇧", "Esc"], label: "Tout marquer comme lu" },
],
{
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,
"Fermer le chat"
);
// ── Bouton de bascule ─────────────────────────────────────────────────────────────
let visible = localStorage.getItem("dso-visible") === "true";
const toggleBtn = el("button", {
id: "dso-toggle-btn",
title: "Basculer la superposition de raccourcis",
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 publique ────────────────────────────────────────────────────────────────
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] retiré.", "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 actif.",
"color:#6af;font-weight:bold"
);
})();
Remplacez ou ajoutez des correspondances d’URL (// @match https://meta.discourse.org/*) si vous souhaitez l’utiliser sur un autre forum Discourse.
Pour installer les userscripts, utilisez une extension de navigateur : Greasemonkey, Tampermonkey, ScriptCat, ou tout autre logiciel externe offrant cette fonctionnalité, par exemple AdGuard.





