// ==UserScript==
// @name QuickCSS [Pre-Alpha]
// @namespace http://tampermonkey.net/
// @version 0.0.1.1
// @description Userscript recreation of the fan-favorite QuickCSS. Modify CSS on any website on a per-URL (or domain/wildcard) basis.
// @author Setnour6
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @license GPLv3
// ==/UserScript==
(function() {
'use strict';
/*** Data Model ***/
let config = GM_getValue('quickCssConfig', null);
if (!config || typeof config !== 'object' || !Array.isArray(config.rules)) {
config = { rules: [] };
GM_setValue('quickCssConfig', config);
}
if (!config.shortcuts || typeof config.shortcuts !== 'object') {
config.shortcuts = { enabled: true, showHints: true };
GM_setValue('quickCssConfig', config);
}
/*** Utility: URL Matcher ***/
function matchPattern(url, pattern, scope) {
if (!pattern) return false;
try {
let u = new URL(url);
let host = u.host; // includes port if present
if (scope === 'exact' && url === pattern) return true;
if (scope === 'exactDomain' && host === pattern) return true;
if (scope === 'subdomain' && (host === pattern || host.endsWith('.' + pattern))) return true;
if (scope === 'wildcard') {
// transform pattern with * to regex (applies to whole URL). escape everything but '*' then replace '*' with '.*'
let parts = pattern.split('*').map(escapeRE);
let regex = new RegExp('^' + parts.join('.*') + '$');
return regex.test(url);
}
} catch (e) {
console.warn('QuickCSS: matchPattern error', e);
}
return false;
}
function escapeRE(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); }
/*** Inject matched CSS ***/
function removeInjectedStyles(root) {
try {
const styles = root.querySelectorAll && root.querySelectorAll('style[data-quick-css]');
if (styles && styles.length) {
styles.forEach(s => s.remove());
}
} catch (e) {
// ignore
}
}
function injectCSS(css, root) {
try {
const docForCreate = (root && root.ownerDocument) ? root.ownerDocument : root;
if (!docForCreate || typeof docForCreate.createElement !== 'function') return;
try { removeInjectedStyles(root); } catch (e) {}
const style = docForCreate.createElement('style');
style.setAttribute('data-quick-css', 'true');
style.textContent = css;
if (root instanceof ShadowRoot) {
root.appendChild(style);
} else if (root.head) {
root.head.appendChild(style);
} else {
(docForCreate.documentElement || docForCreate).appendChild(style);
}
} catch (err) {
console.error('QuickCSS: Failed to inject CSS:', err);
}
}
function walkAndInjectShadowRoots(root, cssText) {
try {
const walkerRoot = root.body || root;
const walker = (walkerRoot && walkerRoot.ownerDocument) ? walkerRoot.ownerDocument.createTreeWalker(walkerRoot, NodeFilter.SHOW_ELEMENT, null, false) : document.createTreeWalker(walkerRoot, NodeFilter.SHOW_ELEMENT, null, false);
while (walker.nextNode()) {
const el = walker.currentNode;
if (el && el.shadowRoot) {
injectCSS(cssText, el.shadowRoot);
walkAndInjectShadowRoots(el.shadowRoot, cssText);
}
}
} catch (e) {
// fallback silent
}
}
function injectIntoDocument(doc, cssText) {
try {
injectCSS(cssText, doc);
walkAndInjectShadowRoots(doc, cssText);
} catch (err) {
console.error('QuickCSS: injectIntoDocument error', err);
}
}
function applyCustomCSS() {
const url = location.href;
const cssText = (config.rules || [])
.filter(rule => matchPattern(url, rule.pattern, rule.scope))
.map(rule => `/* rule ${rule.id} (${rule.pattern} [${rule.scope}]) */\n${rule.css}`)
.join('\n');
try { removeInjectedStyles(document); } catch(e){}
if (cssText) {
injectIntoDocument(document, cssText);
}
document.querySelectorAll('iframe').forEach(iframe => handleIframeInjection(iframe, cssText));
}
function handleIframeInjection(iframe, cssText) {
try {
if (!iframe.contentDocument || !iframe.contentWindow) {
iframe.addEventListener('load', function tryInject() {
iframe.removeEventListener('load', tryInject);
try {
injectIntoDocument(iframe.contentDocument, cssText);
} catch (e) {
console.warn('QuickCSS: cannot inject into iframe after load (likely cross-origin).');
}
}, { once: true });
return;
}
injectIntoDocument(iframe.contentDocument, cssText);
} catch (e) {
// console.warn('QuickCSS: Skipping cross-origin iframe.');
}
}
/*** Live DOM watcher for future iframes & shadow roots ***/
const domObserver = new MutationObserver(mutations => {
const url = location.href;
const cssText = (config.rules || [])
.filter(rule => matchPattern(url, rule.pattern, rule.scope))
.map(rule => `/* rule ${rule.id} (${rule.pattern} [${rule.scope}]) */\n${rule.css}`)
.join('\n');
mutations.forEach(m => {
m.addedNodes && m.addedNodes.forEach(node => {
if (!(node instanceof Element)) return;
if (node.tagName === 'IFRAME') {
handleIframeInjection(node, cssText);
}
if (node.shadowRoot) {
try { injectCSS(cssText, node.shadowRoot); walkAndInjectShadowRoots(node.shadowRoot, cssText); } catch(e){}
}
node.querySelectorAll && node.querySelectorAll('iframe').forEach(f => handleIframeInjection(f, cssText));
node.querySelectorAll && node.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) {
try { injectCSS(cssText, el.shadowRoot); walkAndInjectShadowRoots(el.shadowRoot, cssText); } catch(e){}
}
});
});
});
});
try {
domObserver.observe(document, { childList: true, subtree: true });
} catch (e) {
// ignore if cannot observe
}
/*** UI Overlay ***/
function openEditor() {
if (document.getElementById('quick-css-editor')) return; // Prevent multiple overlays
let overlay = document.createElement('div');
overlay.id = 'quick-css-editor';
Object.assign(overlay.style, {
position:'fixed',top:0,left:0,width:'100%',height:'100%',
background:'rgba(0,0,0,0.6)',zIndex:9999999,
display:'flex',alignItems:'center',justifyContent:'center'
});
let modal = document.createElement('div');
Object.assign(modal.style, {
width:'600px',maxHeight:'90%',overflowY:'auto',
background:'#1e1e1e',borderRadius:'8px',padding:'20px',
boxShadow:'0 4px 20px rgba(0,0,0,0.5)',color:'#fff',
fontFamily:'Segoe UI, sans-serif'
});
modal.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center">
<h2 style="margin:0;font-size:1.3em">Quick CSS Editor</h2>
<button id="qce-close" style="background:none;border:none;color:#fff;font-size:1.2em;cursor:pointer">✕</button>
</div>
<div id="qce-list" style="margin-top:10px"></div>
<div style="margin-top:15px;display:flex;gap:10px;flex-wrap:wrap">
<button id="qce-add" style="padding:8px 12px;border:none;border-radius:4px;background:#007acc;cursor:pointer">+ Add Rule</button>
<button id="qce-export" style="padding:8px 12px;border:none;border-radius:4px;background:#6f42c1;cursor:pointer">Export</button>
<button id="qce-import" style="padding:8px 12px;border:none;border-radius:4px;background:#17a2b8;cursor:pointer">Import</button>
</div>
`;
overlay.appendChild(modal);
function updateShortcutHints(modal) {
const hintMap = {
'#qce-close': 'Alt+Shift+Q',
'#qce-save': 'Alt+Shift+S',
'#qce-cancel': 'Alt+Shift+C',
'#qce-add': 'Alt+Shift+N',
'#qce-export': 'Alt+Shift+X',
'#qce-import': 'Alt+Shift+I'
};
const show = !!(config && config.shortcuts && config.shortcuts.showHints);
Object.keys(hintMap).forEach(sel => {
const btn = (modal && modal.querySelector(sel)) || document.querySelector(sel);
if (!btn) return;
const existing = btn.parentNode && btn.parentNode.querySelector('.qce-shortcut-hint[data-for="' + sel + '"]');
if (existing) existing.remove();
if (show) {
const span = document.createElement('span');
span.className = 'qce-shortcut-hint';
span.setAttribute('data-for', sel);
span.textContent = ' (' + hintMap[sel] + ')';
span.style.cssText = 'margin-left:6px;color:#9ecbff;font-size:0.85em;font-weight:500';
try { btn.insertAdjacentElement('afterend', span); } catch (e) { btn.parentNode && btn.parentNode.appendChild(span); }
}
});
}
const settingsContainer = document.createElement('div');
settingsContainer.style.cssText = 'margin-top:12px;padding:8px;border-top:1px solid #333;color:#ddd;font-size:0.95em;display:flex;flex-direction:column;gap:6px;';
settingsContainer.innerHTML = `
<label style="display:flex;align-items:center;gap:8px">
<input type="checkbox" id="qce-shortcuts-enabled-checkbox"> Enable keyboard shortcuts (functionality)
</label>
<label style="display:flex;align-items:center;gap:8px">
<input type="checkbox" id="qce-shortcuts-show-checkbox"> Show keyboard shortcut hints in the UI
</label>
<div style="color:#9aa; font-size:0.85em">Tip: Open editor — <strong>Alt+Shift+E</strong>. When enabled, other shortcuts: Save (<strong>Alt+Shift+S</strong>), Cancel (<strong>Alt+Shift+C/X</strong>), Close (<strong>Alt+Shift+Q</strong>).</div>
`;
modal.appendChild(settingsContainer);
const shortcutsEnabledCheckbox = modal.querySelector('#qce-shortcuts-enabled-checkbox');
const shortcutsShowCheckbox = modal.querySelector('#qce-shortcuts-show-checkbox');
shortcutsEnabledCheckbox.checked = !!(config && config.shortcuts && config.shortcuts.enabled);
shortcutsShowCheckbox.checked = !!(config && config.shortcuts && config.shortcuts.showHints);
shortcutsEnabledCheckbox.addEventListener('change', () => {
config.shortcuts = config.shortcuts || {};
config.shortcuts.enabled = !!shortcutsEnabledCheckbox.checked;
GM_setValue('quickCssConfig', config);
// no further UI changes necessary; handler reads config directly
});
shortcutsShowCheckbox.addEventListener('change', () => {
config.shortcuts = config.shortcuts || {};
config.shortcuts.showHints = !!shortcutsShowCheckbox.checked;
GM_setValue('quickCssConfig', config);
updateShortcutHints(modal);
});
updateShortcutHints(modal);
const _origRenderRules = renderRules;
renderRules = function() {
_origRenderRules();
setTimeout(()=>updateShortcutHints(modal), 0.01);
};
document.body.appendChild(overlay);
document.getElementById('qce-close').onclick = () => overlay.remove();
document.getElementById('qce-add').onclick = () => renderRuleForm();
const listEl = modal.querySelector('#qce-list');
function renderRules() {
listEl.innerHTML = '';
(config.rules || []).forEach(rule => {
let item = document.createElement('div');
Object.assign(item.style, {padding:'8px',borderBottom:'1px solid #333',display:'flex',justifyContent:'space-between',alignItems:'center'});
item.innerHTML = `
<div style="max-width:70%;word-break:break-word">
<strong>${escapeHtml(rule.pattern)}</strong> <span style="color:#aaa">[${escapeHtml(rule.scope)}]</span>
<div style="color:#9a9a9a;font-size:0.9em;margin-top:6px;white-space:pre-wrap;max-height:4.5em;overflow:hidden">${escapeHtml(rule.css)}</div>
</div>
<div style="flex-shrink:0">
<button data-id="${rule.id}" class="qce-edit" style="margin-right:8px">Edit</button>
<button data-id="${rule.id}" class="qce-del">Delete</button>
</div>`;
listEl.appendChild(item);
});
listEl.querySelectorAll('.qce-edit').forEach(btn=>{
btn.onclick = () => renderRuleForm(btn.dataset.id);
});
listEl.querySelectorAll('.qce-del').forEach(btn=>{
btn.onclick = () => {
if(confirm('Delete this rule?')) {
config.rules = config.rules.filter(r=>r.id!==btn.dataset.id);
GM_setValue('quickCssConfig', config);
renderRules();
applyCustomCSS();
}
};
});
document.getElementById('qce-export').onclick = () => {
const json = JSON.stringify(config, null, 2);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(json).then(() => {
alert('QuickCSS: Config copied to clipboard.');
}, () => {
prompt('Copy this JSON manually:', json);
});
} else {
prompt('Copy this JSON:', json);
}
};
document.getElementById('qce-import').onclick = () => {
const input = prompt('Paste your exported QuickCSS JSON:');
if (!input) return;
try {
const parsed = JSON.parse(input);
if (parsed && parsed.rules && Array.isArray(parsed.rules)) {
config = parsed;
GM_setValue('quickCssConfig', config);
alert('QuickCSS: Config imported.');
overlay.remove();
openEditor();
applyCustomCSS();
} else {
throw new Error();
}
} catch {
alert('QuickCSS: Invalid JSON.');
}
};
}
function renderRuleForm(editId) {
let existing = document.getElementById('qce-form');
if (existing) existing.remove();
let rule = editId ? (config.rules.find(r=>r.id===editId) || { id: editId, pattern:'', scope:'exactDomain', css:'' }) : { id: Date.now().toString(), pattern:'', scope:'exactDomain', css:'' };
let form = document.createElement('div');
form.id = 'qce-form';
Object.assign(form.style, {marginTop:'20px',padding:'10px',background:'#2b2b2b',borderRadius:'6px'});
form.innerHTML = `
<label>URL/Domain Pattern:</label><br>
<input id="qce-pattern" value="${escapeHtmlAttr(rule.pattern)}" style="width:100%;padding:6px;margin-top:4px;background:#1e1e1e;border:1px solid #444;color:#fff;"><br><br>
<label>Scope:</label><br>
<select id="qce-scope" style="width:100%;padding:6px;background:#1e1e1e;border:1px solid #444;color:#fff;margin-top:4px;">
<option value="exact" ${rule.scope==='exact'?'selected':''}>Exact URL</option>
<option value="exactDomain" ${rule.scope==='exactDomain'?'selected':''}>Exact Domain</option>
<option value="subdomain" ${rule.scope==='subdomain'?'selected':''}>Include Subdomains</option>
<option value="wildcard" ${rule.scope==='wildcard'?'selected':''}>Wildcard (*)</option>
</select><br><br>
<label>Custom CSS:</label><br>
<textarea id="qce-css" rows="6" style="width:100%;padding:6px;background:#1e1e1e;border:1px solid #444;color:#fff;">${escapeHtmlAttr(rule.css)}</textarea><br><br>
<button id="qce-save" style="padding:8px 12px;border:none;border-radius:4px;background:#28a745;cursor:pointer">Save</button>
<button id="qce-cancel" style="padding:8px 12px;border:none;border-radius:4px;background:#dc3545;cursor:pointer;margin-left:8px">Cancel</button>
`;
listEl.prepend(form);
modal.querySelector('#qce-cancel').onclick = () => form.remove();
modal.querySelector('#qce-save').onclick = () => {
rule.pattern = form.querySelector('#qce-pattern').value.trim();
rule.scope = form.querySelector('#qce-scope').value;
rule.css = form.querySelector('#qce-css').value;
let idx = config.rules.findIndex(r=>r.id===rule.id);
if (idx>=0) config.rules[idx] = rule;
else config.rules.push(rule);
GM_setValue('quickCssConfig', config);
form.remove();
renderRules();
applyCustomCSS();
};
}
renderRules();
}
document.removeEventListener && document.removeEventListener('keydown', window.__qce_key_handler);
window.__qce_key_handler = function (e) {
if (!e.altKey || !e.shiftKey) return;
if (!config || !config.shortcuts || !config.shortcuts.enabled) return; // Respect the enable/disable toggle
const k = (e.key || '').toLowerCase();
// Alt+Shift+E -> toggle editor (global)
if (k === 'e') {
e.preventDefault();
const existing = document.getElementById('quick-css-editor');
if (existing) existing.remove();
else openEditor();
return;
}
// If editor isn't open, other shortcuts do nothing
const overlay = document.getElementById('quick-css-editor');
if (!overlay) return;
// Alt+Shift+S -> Save (click the Save button if present)
if (k === 's') {
e.preventDefault();
const saveBtn = overlay.querySelector('#qce-save');
if (saveBtn) saveBtn.click();
return;
}
// Alt+Shift+C or Alt+Shift+X -> Cancel (click the Cancel button if present)
if (k === 'c' || k === 'x') {
e.preventDefault();
const cancelBtn = overlay.querySelector('#qce-cancel');
if (cancelBtn) cancelBtn.click();
return;
}
// Alt+Shift+Q -> Close overlay
if (k === 'q') {
e.preventDefault();
overlay.remove();
return;
}
// Alt+Shift+N -> Add new rule
if (k === 'n') {
e.preventDefault();
const addBtn = overlay.querySelector('#qce-add');
if (addBtn) addBtn.click();
return;
}
// Alt+Shift+X -> Export
if (k === 'x' && e.altKey && e.shiftKey && overlay) {
// prefer the explicit export button if present
const expBtn = overlay.querySelector('#qce-export');
if (expBtn) { e.preventDefault(); expBtn.click(); }
return;
}
// Alt+Shift+I -> Import
if (k === 'i' && overlay) {
const impBtn = overlay.querySelector('#qce-import');
if (impBtn) { e.preventDefault(); impBtn.click(); }
return;
}
};
document.addEventListener('keydown', window.__qce_key_handler);
/*** Register menu command ***/
try {
GM_registerMenuCommand && GM_registerMenuCommand('Quick CSS Editor…', openEditor);
} catch (e) {
console.warn('QuickCSS: failed to register menu command', e);
}
function updateShortcutHints(modal) {
const hintMap = {
'#qce-close': 'Alt+Shift+Q',
'#qce-save': 'Alt+Shift+S',
'#qce-cancel': 'Alt+Shift+C',
'#qce-add': 'Alt+Shift+N',
'#qce-export': 'Alt+Shift+X',
'#qce-import': 'Alt+Shift+I'
};
const show = !!(config && config.shortcuts && config.shortcuts.showHints);
Object.keys(hintMap).forEach(sel => {
const btn = (modal && modal.querySelector(sel)) || document.querySelector(sel);
if (!btn) return;
const existing = btn.parentNode && btn.parentNode.querySelector('.qce-shortcut-hint[data-for="' + sel + '"]');
if (existing) existing.remove();
if (show) {
const span = document.createElement('span');
span.className = 'qce-shortcut-hint';
span.setAttribute('data-for', sel);
span.textContent = ' (' + hintMap[sel] + ')';
span.style.cssText = 'margin-left:6px;color:#9ecbff;font-size:0.85em;font-weight:500';
try { btn.insertAdjacentElement('afterend', span); } catch (e) { btn.parentNode && btn.parentNode.appendChild(span); } // works for most layouts
}
});
}
/*** Helpers ***/
function escapeHtml(str) {
if (str == null) return '';
return String(str).replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m]); });
}
function escapeHtmlAttr(str) {
return escapeHtml(str).replace(/\n/g, ' '); // preserve newlines in textarea by escaping attributes, but for safety
}
try { applyCustomCSS(); } catch (e) {} // Apply CSS right away when script runs (Hot Reload)
try { window.__quickCss = { config, applyCustomCSS }; } catch (e) {} // Expose for debugging on `window` (Optional?)
})();