Skip to content

Instantly share code, notes, and snippets.

@angorb
Created August 12, 2025 21:16
Show Gist options
  • Save angorb/3b928174a00e47e6a9542551b90bd9b4 to your computer and use it in GitHub Desktop.
Save angorb/3b928174a00e47e6a9542551b90bd9b4 to your computer and use it in GitHub Desktop.
[Tampermonkey] Inject Bootstrap and automatically add Bootstrap classes to arbitrary pages with toggle + live updates.
// ==UserScript==
// @name Auto-Bootstrap Styler
// @namespace https://example.com/
// @version 1.3
// @description Inject Bootstrap and automatically add Bootstrap classes to arbitrary pages with toggle + live updates.
// @match *://*/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
/***** Config *****/
const BOOTSTRAP_CSS_HREF = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css';
const BOOTSTRAP_JS_HREF = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js';
const STORAGE_PREFIX = 'tm_auto_bootstrap_'; // + host
const PROCESSED_ATTR = 'data-ab-processed';
const ORIG_CLASS_ATTR = 'data-ab-orig-class';
const AUTO_ENABLED_ATTR = 'data-ab-auto'; // on elements if applied automatically
// Mapping rules: tag selectors -> classes to add (string or array)
// These are conservative; tweak as you like
const MAPPINGS = [
{ sel: 'table', classes: ['table', 'table-striped', 'table-bordered'] },
{ sel: 'button:not([class*="btn"])', classes: ['btn', 'btn-primary'] },
{ sel: 'input[type="submit"]:not([class*="btn"])', classes: ['btn', 'btn-primary'] },
{ sel: 'input[type="button"]:not([class*="btn"])', classes: ['btn', 'btn-secondary'] },
{ sel: 'a.btn', classes: [] }, // leave <a class="btn"> alone
{ sel: 'input[type="text"]:not([class*="form-control"])', classes: ['form-control'] },
{ sel: 'input[type="email"]:not([class*="form-control"])', classes: ['form-control'] },
{ sel: 'input[type="password"]:not([class*="form-control"])', classes: ['form-control'] },
{ sel: 'textarea:not([class*="form-control"])', classes: ['form-control'] },
{ sel: 'select:not([class*="form-select"])', classes: ['form-select'] },
{ sel: 'img:not([class*="img-fluid"])', classes: ['img-fluid'] },
{ sel: 'form:not([class*="p-"])', classes: ['p-3', 'border', 'rounded'] },
{ sel: 'h1,h2,h3,h4,h5,h6', classes: ['fw-bold'] },
{ sel: 'table thead', classes: ['table-dark'] },
{ sel: 'ul:not([class*="list-"])', classes: ['list-unstyled'] },
{ sel: 'ol:not([class*="list-"])', classes: ['list-unstyled'] },
{ sel: 'blockquote', classes: ['blockquote', 'fs-5', 'px-3'] },
{ sel: 'nav', classes: ['navbar', 'navbar-expand-lg', 'navbar-light', 'bg-light'] },
// Add more rules if you want
];
/***** Helpers *****/
const hostKey = (k) => STORAGE_PREFIX + (location.host || 'global') + '_' + k;
function addLink(href) {
// attempt to add link; return the created element
const l = document.createElement('link');
l.rel = 'stylesheet';
l.href = href;
l.crossOrigin = 'anonymous';
document.head.appendChild(l);
return l;
}
function addScript(src) {
const s = document.createElement('script');
s.src = src;
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
return s;
}
function hasBootstrapInjected() {
return !!document.querySelector('link[href*="bootstrap"]');
}
function saveOriginalClass(el) {
if (el.hasAttribute(ORIG_CLASS_ATTR)) return;
el.setAttribute(ORIG_CLASS_ATTR, el.getAttribute('class') || '');
}
function restoreOriginalClass(el) {
if (!el.hasAttribute(ORIG_CLASS_ATTR)) return;
const orig = el.getAttribute(ORIG_CLASS_ATTR);
if (orig === null || orig === '') el.removeAttribute('class');
else el.setAttribute('class', orig);
el.removeAttribute(ORIG_CLASS_ATTR);
el.removeAttribute(PROCESSED_ATTR);
el.removeAttribute(AUTO_ENABLED_ATTR);
}
function applyClassesToElement(el, classes) {
if (!el || !classes || classes.length === 0) return;
// If already processed by this script, don't re-add duplicates
if (!el.hasAttribute(PROCESSED_ATTR)) {
saveOriginalClass(el);
}
const existing = new Set((el.getAttribute('class') || '').split(/\s+/).filter(Boolean));
classes.forEach(c => existing.add(c));
el.setAttribute('class', Array.from(existing).join(' '));
el.setAttribute(PROCESSED_ATTR, '1');
el.setAttribute(AUTO_ENABLED_ATTR, '1');
}
function applyMappings(root = document) {
try {
MAPPINGS.forEach(rule => {
const nodes = root.querySelectorAll(rule.sel);
nodes.forEach(el => {
// skip if already had the mapped classes explicitly (avoid disturbing elements intentionally styled)
// but we still save original before adding
applyClassesToElement(el, rule.classes);
});
});
// Special: wrap bare tables in responsive container if not already
document.querySelectorAll('table').forEach(t => {
if (!t.parentElement || t.parentElement.classList.contains('table-responsive')) {
// if parent is already responsive, skip
} else {
// wrap if not already in a .table-responsive (we'll not wrap if it's inside certain components)
const p = t.parentElement;
// to avoid breaking scripts, only wrap if direct parent is not a semantic container with many children
const wrap = document.createElement('div');
wrap.className = 'table-responsive';
try {
p.insertBefore(wrap, t);
wrap.appendChild(t);
wrap.setAttribute(PROCESSED_ATTR, '1');
wrap.setAttribute(ORIG_CLASS_ATTR, '');
} catch (e) {
// ignore failures (some pages have locked DOM)
}
}
});
} catch (e) {
console.warn('Auto-Bootstrap: mapping failed', e);
}
}
function applyToNodeAndChildren(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
// If node itself matches any mapping
MAPPINGS.forEach(rule => {
if (node.matches && node.matches(rule.sel)) {
applyClassesToElement(node, rule.classes);
}
});
// children
if (node.querySelector) {
applyMappings(node);
}
}
function undoAutoStyling() {
// restore every element we modified
document.querySelectorAll('[' + ORIG_CLASS_ATTR + ']').forEach(el => {
restoreOriginalClass(el);
});
// remove any wrappers we added (.table-responsive)
document.querySelectorAll('.table-responsive[' + PROCESSED_ATTR + ']').forEach(el => {
// if it contains exactly one child table, unwrap it
if (el.children.length === 1 && el.children[0].tagName.toLowerCase() === 'table') {
const table = el.children[0];
el.parentNode.replaceChild(table, el);
} else {
// otherwise just remove our processed marker
el.removeAttribute(PROCESSED_ATTR);
}
});
}
/***** Toolbar UI *****/
function createToolbar() {
const toolbar = document.createElement('div');
toolbar.id = 'auto-bootstrap-toolbar';
toolbar.style.position = 'fixed';
toolbar.style.right = '12px';
toolbar.style.bottom = '12px';
toolbar.style.zIndex = 2147483647; // very high
toolbar.style.fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial';
toolbar.style.fontSize = '13px';
// container
const wrap = document.createElement('div');
wrap.style.display = 'flex';
wrap.style.flexDirection = 'column';
wrap.style.gap = '6px';
wrap.style.alignItems = 'stretch';
wrap.style.background = 'rgba(255,255,255,0.95)';
wrap.style.border = '1px solid rgba(0,0,0,0.12)';
wrap.style.boxShadow = '0 6px 18px rgba(0,0,0,0.12)';
wrap.style.padding = '8px';
wrap.style.borderRadius = '10px';
wrap.style.minWidth = '150px';
const title = document.createElement('div');
title.textContent = 'Auto-Bootstrap';
title.style.fontWeight = '600';
title.style.marginBottom = '4px';
wrap.appendChild(title);
// status text
const status = document.createElement('div');
status.id = 'auto-bootstrap-status';
status.style.fontSize = '12px';
status.style.color = '#333';
status.textContent = 'Initializing...';
wrap.appendChild(status);
// buttons container
const btnRow = document.createElement('div');
btnRow.style.display = 'flex';
btnRow.style.gap = '6px';
function mkButton(label, onClick) {
const b = document.createElement('button');
b.textContent = label;
b.style.cursor = 'pointer';
b.style.padding = '6px 8px';
b.style.borderRadius = '6px';
b.style.border = '1px solid rgba(0,0,0,0.08)';
b.style.background = '#f6f6f6';
b.style.fontSize = '12px';
b.addEventListener('click', onClick);
return b;
}
const toggleBtn = mkButton('Toggle', toggleBootstrap);
const autoBtn = mkButton('Auto: on', toggleAuto);
const resetBtn = mkButton('Reset', () => {
undoAutoStyling();
setStatus('Reset: restored original classes');
});
btnRow.appendChild(toggleBtn);
btnRow.appendChild(autoBtn);
btnRow.appendChild(resetBtn);
wrap.appendChild(btnRow);
// small help text
const help = document.createElement('div');
help.style.fontSize = '11px';
help.style.opacity = '0.9';
help.style.marginTop = '6px';
help.textContent = 'Ctrl+Shift+B toggles, Auto applies to new content.';
wrap.appendChild(help);
toolbar.appendChild(wrap);
document.body.appendChild(toolbar);
return { toolbar, status, toggleBtn, autoBtn };
}
function setStatus(msg) {
const el = document.getElementById('auto-bootstrap-status');
if (el) el.textContent = msg;
console.log('Auto-Bootstrap:', msg);
}
/***** Main logic *****/
let observer = null;
let autoEnabled = true;
let bootstrapInjected = false;
let toolbarEls = null;
function enableBootstrap() {
if (!bootstrapInjected) {
addLink(BOOTSTRAP_CSS_HREF);
// load JS optionally. Some pages might break if we inject arbitrary JS; keep optional.
// addScript(BOOTSTRAP_JS_HREF);
bootstrapInjected = true;
}
applyMappings();
setStatus('Bootstrap applied');
localStorage.setItem(hostKey('enabled'), '1');
}
function disableBootstrap() {
undoAutoStyling();
setStatus('Bootstrap removed');
localStorage.setItem(hostKey('enabled'), '0');
}
function toggleBootstrap() {
const enabled = localStorage.getItem(hostKey('enabled')) !== '0';
if (enabled) {
disableBootstrap();
updateToggleUI(false);
} else {
enableBootstrap();
updateToggleUI(true);
}
}
function updateToggleUI(isOn) {
const btn = toolbarEls && toolbarEls.toggleBtn;
if (btn) btn.textContent = isOn ? 'On' : 'Off';
const statusText = isOn ? 'Bootstrap ON' : 'Bootstrap OFF';
setStatus(statusText);
}
function toggleAuto() {
autoEnabled = !autoEnabled;
if (autoEnabled) {
startObserver();
toolbarEls.autoBtn.textContent = 'Auto: on';
setStatus('Auto updates ON');
localStorage.setItem(hostKey('auto'), '1');
// apply now in case observer missed something
applyMappings();
} else {
stopObserver();
toolbarEls.autoBtn.textContent = 'Auto: off';
setStatus('Auto updates OFF');
localStorage.setItem(hostKey('auto'), '0');
}
}
function startObserver() {
if (observer) return;
observer = new MutationObserver(mutations => {
mutations.forEach(m => {
m.addedNodes && m.addedNodes.forEach(node => applyToNodeAndChildren(node));
});
});
observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
}
function stopObserver() {
if (!observer) return;
observer.disconnect();
observer = null;
}
function init() {
// create toolbar when DOM ready
toolbarEls = createToolbar();
// load settings
const storedEnabled = localStorage.getItem(hostKey('enabled'));
const shouldEnable = storedEnabled === null ? true : storedEnabled === '1';
const storedAuto = localStorage.getItem(hostKey('auto'));
autoEnabled = storedAuto === null ? true : storedAuto === '1';
// keyboard shortcut
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.code === 'KeyB') {
e.preventDefault();
toggleBootstrap();
}
});
// start / stop according to settings
if (shouldEnable) {
enableBootstrap();
updateToggleUI(true);
} else {
updateToggleUI(false);
}
if (autoEnabled) {
startObserver();
toolbarEls.autoBtn.textContent = 'Auto: on';
} else {
stopObserver();
toolbarEls.autoBtn.textContent = 'Auto: off';
}
// small check: if css injection failed later due to CSP, we'll still allow toggling
setTimeout(() => {
if (!hasBootstrapInjected()) {
setStatus('Warning: Bootstrap CSS not found (CSP may block it). Toggle still works for class changes.');
}
}, 1500);
// expose small API for console tinkering
window.__autoBootstrap = {
enable: enableBootstrap,
disable: disableBootstrap,
toggle: toggleBootstrap,
applyMappings,
undo: undoAutoStyling
};
}
// Wait for DOMContentLoaded (script runs at document-end by @run-at but ensure safety)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment