Files
reddit-tweaks-ffe/content/tweaks/color-popover.js
cottongin 00507fe87c refactor: decompose CSS, unify dark mode, extract shared modules
Structural cleanup based on thermo-nuclear code quality review:
- Extract shared color-picker.css and color-popover.css from badges.css (611→85 lines)
- Extract shared lib/presets.js for preset CRUD used by popup and popover
- Unify dark mode to single applyDarkModeClass on <html>, removing 4 scattered functions
- Fix stale closure in startPostObserver, data-driven reload key check
- Remove dead exports, getHelpers wrapper, duplicated colorToHex
- Replace DOM expando properties with state array in popup.js

Net: -318 lines of duplication, no file over 401 lines.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 01:39:25 -04:00

402 lines
12 KiB
JavaScript

(function () {
"use strict";
var popoverEl = null;
var currentSubreddit = null;
var currentAnchor = null;
var editState = { light: null, dark: null };
var selectedMode = "light";
var dirtyModes = { light: false, dark: false };
var hadOverride = { light: false, dark: false };
var picker = null;
var activeProp = null;
var els = {};
function hexValid(val) {
return /^#[0-9a-f]{6}$/i.test(val);
}
function createPopover() {
var el = document.createElement("div");
el.className = "rt-color-popover";
el.innerHTML = [
'<div class="rt-popover-header">',
' <span class="rt-popover-title"></span>',
' <button class="rt-popover-close" type="button">\u00d7</button>',
"</div>",
'<div class="rt-popover-mode">',
' <select class="rt-popover-mode-select">',
' <option value="light">Light</option>',
' <option value="dark">Dark</option>',
" </select>",
"</div>",
'<div class="rt-popover-body">',
' <div class="rt-popover-row" data-prop="bg">',
' <span class="rt-popover-row-label">Background</span>',
' <div class="rt-popover-row-inputs">',
' <div class="rt-color-swatch"></div>',
' <input type="text" class="rt-color-hex" maxlength="7">',
" </div>",
" </div>",
' <div class="rt-popover-row" data-prop="border">',
' <span class="rt-popover-row-label">Border</span>',
' <div class="rt-popover-row-inputs">',
' <div class="rt-color-swatch"></div>',
' <input type="text" class="rt-color-hex" maxlength="7">',
" </div>",
" </div>",
' <div class="rt-popover-row" data-prop="text">',
' <span class="rt-popover-row-label">Text</span>',
' <div class="rt-popover-row-inputs">',
' <div class="rt-color-swatch"></div>',
' <input type="text" class="rt-color-hex" maxlength="7">',
" </div>",
" </div>",
"</div>",
'<div class="rt-popover-preview">',
' <span class="rt-badge rt-badge--subreddit rt-popover-preview-badge" style="padding-right:6px"></span>',
"</div>",
'<div class="rt-popover-footer">',
' <button class="rt-btn-reset" type="button">Reset Default</button>',
' <div class="rt-popover-footer-right">',
' <button class="rt-btn-cancel" type="button">Cancel</button>',
' <button class="rt-btn-save" type="button">Save</button>',
" </div>",
"</div>",
].join("\n");
document.body.appendChild(el);
popoverEl = el;
els.title = el.querySelector(".rt-popover-title");
els.closeBtn = el.querySelector(".rt-popover-close");
els.modeSelect = el.querySelector(".rt-popover-mode-select");
els.preview = el.querySelector(".rt-popover-preview-badge");
els.saveBtn = el.querySelector(".rt-btn-save");
els.cancelBtn = el.querySelector(".rt-btn-cancel");
els.resetBtn = el.querySelector(".rt-btn-reset");
els.rows = {};
var rows = el.querySelectorAll(".rt-popover-row");
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var prop = row.getAttribute("data-prop");
els.rows[prop] = {
swatch: row.querySelector(".rt-color-swatch"),
hex: row.querySelector(".rt-color-hex"),
};
}
picker = window.RedditTweaks.colorPicker.create();
var previewSection = el.querySelector(".rt-popover-preview");
el.insertBefore(picker.el, previewSection);
var presetsDiv = document.createElement("div");
presetsDiv.className = "rt-popover-presets";
var presetList = document.createElement("div");
presetList.className = "rt-preset-list";
var presetAddBtn = document.createElement("button");
presetAddBtn.className = "rt-preset-add";
presetAddBtn.type = "button";
presetAddBtn.textContent = "+";
presetAddBtn.title = "Save current colors as preset";
presetAddBtn.addEventListener("click", saveAsPreset);
presetsDiv.appendChild(presetList);
presetsDiv.appendChild(presetAddBtn);
el.querySelector(".rt-popover-footer").insertAdjacentElement("beforebegin", presetsDiv);
els.presetList = presetList;
els.closeBtn.addEventListener("click", close);
els.cancelBtn.addEventListener("click", close);
els.saveBtn.addEventListener("click", save);
els.resetBtn.addEventListener("click", resetToDefault);
els.modeSelect.addEventListener("change", function () {
markModeDirty(selectedMode);
storeCurrentEdits();
selectedMode = els.modeSelect.value;
closePicker();
loadModeIntoInputs();
updatePreview();
});
var props = ["bg", "border", "text"];
for (var p = 0; p < props.length; p++) {
(function (prop) {
var row = els.rows[prop];
row.swatch.addEventListener("click", function () {
onSwatchClick(prop);
});
row.hex.addEventListener("input", function () {
markModeDirty(selectedMode);
if (hexValid(row.hex.value)) {
row.swatch.style.backgroundColor = row.hex.value;
row.hex.classList.remove("rt-input-error");
if (picker.isOpen() && activeProp === prop) {
picker.setColor(row.hex.value);
}
updatePreview();
} else {
row.hex.classList.add("rt-input-error");
}
});
})(props[p]);
}
document.addEventListener("mousedown", onDocumentMouseDown, true);
document.addEventListener("keydown", onDocumentKeyDown, true);
}
function onSwatchClick(prop) {
if (picker.isOpen() && activeProp === prop) {
closePicker();
return;
}
closePicker();
activeProp = prop;
var row = els.rows[prop];
picker.open(row.hex.value, function (hex) {
markModeDirty(selectedMode);
row.swatch.style.backgroundColor = hex;
row.hex.value = hex;
row.hex.classList.remove("rt-input-error");
updatePreview();
});
}
function closePicker() {
if (picker && picker.isOpen()) picker.close();
activeProp = null;
}
function onDocumentMouseDown(e) {
if (!popoverEl || !popoverEl.classList.contains("rt-popover-visible")) return;
if (popoverEl.contains(e.target)) return;
close();
}
function onDocumentKeyDown(e) {
if (e.key === "Escape" && popoverEl && popoverEl.classList.contains("rt-popover-visible")) {
close();
}
}
function getDefaultsForMode(settings, mode) {
return {
bg: settings[mode + ".subredditBgColor"],
border: settings[mode + ".subredditBorderColor"],
text: settings[mode + ".subredditTextColor"],
};
}
function getColorsForMode(settings, subredditName, mode) {
var entry = settings.subredditColors[subredditName];
if (entry && entry[mode]) {
return { bg: entry[mode].bg, border: entry[mode].border, text: entry[mode].text };
}
return getDefaultsForMode(settings, mode);
}
function markModeDirty(mode) {
dirtyModes[mode] = true;
}
function resetTracking() {
dirtyModes = { light: false, dark: false };
hadOverride = { light: false, dark: false };
}
function storeCurrentEdits() {
editState[selectedMode] = {
bg: els.rows.bg.hex.value,
border: els.rows.border.hex.value,
text: els.rows.text.hex.value,
};
}
function loadModeIntoInputs() {
var colors = editState[selectedMode];
var props = ["bg", "border", "text"];
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var val = colors[prop] || "#ffffff";
els.rows[prop].swatch.style.backgroundColor = val;
els.rows[prop].hex.value = val;
els.rows[prop].hex.classList.remove("rt-input-error");
}
}
function updatePreview() {
var bg = els.rows.bg.hex.value;
var border = els.rows.border.hex.value;
var text = els.rows.text.hex.value;
els.preview.style.background = bg;
els.preview.style.borderColor = border;
els.preview.style.color = text;
}
function renderPresets(presets) {
window.RedditTweaks.presets.render(
els.presetList,
presets,
applyPreset,
function (index) {
window.RedditTweaks.presets.delete(index, renderPresets);
}
);
}
function applyPreset(p) {
markModeDirty(selectedMode);
var props = ["bg", "border", "text"];
for (var i = 0; i < props.length; i++) {
var prop = props[i];
els.rows[prop].swatch.style.backgroundColor = p[prop];
els.rows[prop].hex.value = p[prop];
els.rows[prop].hex.classList.remove("rt-input-error");
}
updatePreview();
if (picker && picker.isOpen() && activeProp) {
picker.setColor(els.rows[activeProp].hex.value);
}
}
function saveAsPreset() {
window.RedditTweaks.presets.save(
{
bg: els.rows.bg.hex.value,
border: els.rows.border.hex.value,
text: els.rows.text.hex.value,
},
renderPresets
);
}
function positionPopover(anchorEl) {
var rect = anchorEl.getBoundingClientRect();
var gap = 6;
var pad = 10;
popoverEl.style.visibility = "hidden";
popoverEl.style.maxHeight = "";
popoverEl.classList.add("rt-popover-visible");
var popRect = popoverEl.getBoundingClientRect();
var spaceBelow = window.innerHeight - rect.bottom - gap - pad;
var spaceAbove = rect.top - gap - pad;
var top;
if (popRect.height <= spaceBelow) {
top = rect.bottom + window.scrollY + gap;
} else if (popRect.height <= spaceAbove) {
top = rect.top + window.scrollY - popRect.height - gap;
} else if (spaceBelow >= spaceAbove) {
top = rect.bottom + window.scrollY + gap;
popoverEl.style.maxHeight = Math.max(spaceBelow, 200) + "px";
} else {
var capped = Math.max(spaceAbove, 200);
popoverEl.style.maxHeight = capped + "px";
top = rect.top + window.scrollY - capped - gap;
}
var left = rect.left + window.scrollX;
var maxLeft = document.documentElement.scrollWidth - popRect.width - pad;
if (left > maxLeft) left = maxLeft;
if (left < pad) left = pad;
popoverEl.style.top = top + "px";
popoverEl.style.left = left + "px";
popoverEl.style.visibility = "";
}
function open(badgeEl, subredditName, settings) {
if (!popoverEl) createPopover();
if (currentSubreddit === subredditName && popoverEl.classList.contains("rt-popover-visible")) {
close();
return;
}
currentSubreddit = subredditName;
currentAnchor = badgeEl;
resetTracking();
var existingEntry = settings.subredditColors[subredditName];
hadOverride.light = !!(existingEntry && existingEntry.light);
hadOverride.dark = !!(existingEntry && existingEntry.dark);
selectedMode = window.RedditTweaks.getColorPrefix(settings);
editState.light = getColorsForMode(settings, subredditName, "light");
editState.dark = getColorsForMode(settings, subredditName, "dark");
els.title.textContent = "r/" + subredditName;
els.modeSelect.value = selectedMode;
els.preview.textContent = "r/" + subredditName;
closePicker();
loadModeIntoInputs();
updatePreview();
renderPresets(settings.colorPresets || []);
positionPopover(badgeEl);
}
function close() {
if (!popoverEl) return;
closePicker();
popoverEl.classList.remove("rt-popover-visible");
currentSubreddit = null;
currentAnchor = null;
editState = { light: null, dark: null };
resetTracking();
}
async function save() {
if (!currentSubreddit) return;
storeCurrentEdits();
var settings = await window.RedditTweaks.loadSettings();
var entry = settings.subredditColors[currentSubreddit] || {};
if (dirtyModes.light || hadOverride.light) {
entry.light = editState.light;
}
if (dirtyModes.dark || hadOverride.dark) {
entry.dark = editState.dark;
}
if (!entry.light && !entry.dark) {
delete settings.subredditColors[currentSubreddit];
} else {
settings.subredditColors[currentSubreddit] = entry;
}
await window.RedditTweaks.saveSettings(settings);
close();
}
async function resetToDefault() {
if (!currentSubreddit) return;
var settings = await window.RedditTweaks.loadSettings();
delete settings.subredditColors[currentSubreddit];
await window.RedditTweaks.saveSettings(settings);
resetTracking();
editState.light = getDefaultsForMode(settings, "light");
editState.dark = getDefaultsForMode(settings, "dark");
closePicker();
loadModeIntoInputs();
updatePreview();
}
window.RedditTweaks = window.RedditTweaks || {};
window.RedditTweaks.colorPopover = {
open: open,
close: close,
};
})();