Created
August 12, 2025 21:16
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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