|
const classesBySelector = { |
|
"list-card": "[data-testid='list-card']", |
|
"trello-card": ["[data-testid='trello-card']", "ui-droppable"], |
|
"list-card-composer-textarea": "[data-testid='list-card-composer-textarea']", |
|
"list-cards": "[data-testid='list-cards']", |
|
"list-wrapper": "[data-testid='list-wrapper']", |
|
"list": "[data-testid='list']", |
|
"list-header": "[data-testid='list-header']", |
|
"list-name-textarea": "[data-testid='list-name-textarea']", |
|
"list-footer": "[data-testid='list-footer']", |
|
"list-name": "[data-testid='list-name']", |
|
"list-collapse-button": "[data-testid='list-header'] button:not([data-testid='list-edit-menu-button']", |
|
"list-edit-menu-button": "[data-testid='list-edit-menu-button']", |
|
"list-cards-top-buffer": "[data-testid='list-cards-top-buffer']", |
|
"list-collapse-popup": () => document.querySelectorAll("[data-testid='CloseIcon']").values() |
|
.map(icon => icon.closest("[data-elevation='1']")) |
|
.filter(e => e) |
|
.toArray(), |
|
}; |
|
|
|
const classes = {}; |
|
const resolvedClassElementLists = {}; |
|
let lastMissing = Infinity; |
|
let stylesheet; |
|
let css; |
|
|
|
function getElementsByResolvedClass(cls) { |
|
return resolvedClassElementLists[cls] ??= |
|
classes[cls] && document.getElementsByClassName(classes[cls]); |
|
} |
|
|
|
function listen(cls, apply) { |
|
const elements = getElementsByResolvedClass(cls); |
|
if (!elements) return; |
|
|
|
for (const element of elements) { |
|
if (element.classList.contains("chiri-listening")) continue; |
|
|
|
element.classList.add("chiri-listening"); |
|
apply(element); |
|
} |
|
} |
|
|
|
function update() { |
|
let missing = []; |
|
let newClasses = []; |
|
for (let [unobfuscatedClassName, selector] of Object.entries(classesBySelector)) { |
|
if (!classes[unobfuscatedClassName]) { |
|
let ignoredClasses = []; |
|
if (Array.isArray(selector)) |
|
ignoredClasses = selector.slice(1), selector = selector[0]; |
|
|
|
let found = false; |
|
const matchingElements = [...typeof selector === "function" ? selector() : document.querySelectorAll(selector)]; |
|
|
|
for (const element of matchingElements) { |
|
const classList = [...element.classList] |
|
.filter(cls => !ignoredClasses.includes(cls)) |
|
.filter(cls => document.getElementsByClassName(cls).length === matchingElements.length); |
|
|
|
const className = classList.length === 1 ? classList[0] : undefined; |
|
if (className) { |
|
classes[unobfuscatedClassName] = className; |
|
newClasses.push([unobfuscatedClassName, className]); |
|
console.log(unobfuscatedClassName, className); |
|
found = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!found) |
|
missing.push(unobfuscatedClassName); |
|
} |
|
} |
|
|
|
if (missing.length !== lastMissing) { |
|
lastMissing = missing.length; |
|
if (missing.length) |
|
console.log("Missing classes:", missing); |
|
else |
|
console.log("No missing classes", classes); |
|
|
|
stylesheet ??= document.querySelector("style[data-source='User JavaScript and CSS']"); |
|
if (!stylesheet) |
|
return console.warn("Couldn't find user stylesheet to update"); |
|
|
|
css ??= stylesheet.textContent; |
|
console.log(newClasses); |
|
for (const [unobfuscatedClassName, className] of newClasses) { |
|
console.log("Replacing", unobfuscatedClassName, "with", className); |
|
css = css.replaceAll(new RegExp(`(?<=\\.)${unobfuscatedClassName}(?![\\w-])`, "g"), className); |
|
} |
|
|
|
// console.log(css); |
|
stylesheet.textContent = css; |
|
} |
|
|
|
listen("list-header", target => |
|
target.addEventListener("contextmenu", onRightClickList)); |
|
|
|
listen("list-collapse-button", button => |
|
button.addEventListener("click", onRightClickList)); |
|
|
|
updateListStates(); |
|
} |
|
|
|
setInterval(update, 100); |
|
|
|
function onRightClickList (event) { |
|
toggleVisible(getListName(event.target)); |
|
event.preventDefault(); |
|
} |
|
|
|
function toggleVisible (listName) { |
|
const id = `chiri-list-hidden.${listName}`; |
|
const hidden = !localStorage.getItem(id); |
|
if (hidden) |
|
localStorage.setItem(id, hidden); |
|
else |
|
localStorage.removeItem(id); |
|
|
|
updateListStates(); |
|
} |
|
|
|
function updateListStates() { |
|
for (const list of getElementsByResolvedClass("list-wrapper") ?? []) { |
|
const listName = getListName(list); |
|
const listFilterCardsCount = getListFilterCardsCount(list); |
|
const shouldHide = listFilterCardsCount === undefined ? !!localStorage.getItem(`chiri-list-hidden.${listName}`) : !listFilterCardsCount; |
|
list.classList.toggle("chiri-list-filtered", listFilterCardsCount === 0); |
|
list.classList.toggle("chiri-list-hidden", shouldHide); |
|
} |
|
} |
|
|
|
function getListName (element) { |
|
const listElement = classes["list-wrapper"] |
|
&& element?.closest?.(`.${classes["list-wrapper"]}`); |
|
|
|
return classes["list-name-textarea"] && |
|
listElement?.querySelector(`.${classes["list-name-textarea"]}`)?.getAttribute("aria-label"); |
|
} |
|
|
|
function getListFilterCardsCount (element) { |
|
const paragraphs = classes["list-wrapper"] |
|
&& element?.closest?.(`.${classes["list-wrapper"]}`)?.querySelectorAll("p"); |
|
|
|
if (!paragraphs) |
|
return undefined; |
|
|
|
for (const p of paragraphs) { |
|
if (!p.textContent.endsWith(" filters")) |
|
continue; |
|
|
|
const count = parseInt(p.textContent); |
|
return count; |
|
} |
|
} |