mirror of
https://github.com/wekan/wekan.git
synced 2025-12-30 06:08:48 +01:00
122 lines
4.2 KiB
JavaScript
122 lines
4.2 KiB
JavaScript
Meteor.startup(() => {
|
|
const greyscaleIcons = [
|
|
'🔼', '❌', '🏷️', '📅', '📥', '🚀', '👤', '👥', '✍️', '📋', '✏️', '🌐', '📎', '📝', '📋', '📜', '🏠', '🔒', '🔕', '🃏',
|
|
'⏰', '🛒', '🔢', '✅', '❌', '👁️', '👍', '👎', '📋', '🕐', '🎨',
|
|
'📤', '⬆️', '⬇️', '➡️', '📦',
|
|
'⬅️', '↕️', '🔽', '🔍', '▼', '🏊',
|
|
'🔔', '⚙️', '🖼️', '🔑', '🚪', '◀️', '⌨️', '👥', '🏷️', '✅', '🚫', '☑️', '💬',
|
|
// Mobile/Desktop toggle + calendar
|
|
'📱', '🖥️', '🗓️'
|
|
];
|
|
|
|
const EXCLUDE_SELECTOR = '.header-user-bar-avatar, .avatar-initials, script, style';
|
|
let observer = null;
|
|
let enabled = false;
|
|
|
|
function isExcluded(el) {
|
|
if (!el) return true;
|
|
if (el.nodeType === Node.ELEMENT_NODE && (el.matches('script') || el.matches('style'))) return true;
|
|
if (el.closest && el.closest(EXCLUDE_SELECTOR)) return true;
|
|
return false;
|
|
}
|
|
|
|
function wrapTextNodeOnce(parent, textNode) {
|
|
if (!parent || !textNode) return;
|
|
if (isExcluded(parent)) return;
|
|
if (parent.closest && parent.closest('.unicode-icon')) return;
|
|
const raw = textNode.nodeValue;
|
|
if (!raw) return;
|
|
const txt = raw.trim();
|
|
// small guard against long text processing
|
|
if (txt.length > 3) return;
|
|
if (!greyscaleIcons.includes(txt)) return;
|
|
const span = document.createElement('span');
|
|
span.className = 'unicode-icon';
|
|
span.textContent = txt;
|
|
parent.replaceChild(span, textNode);
|
|
}
|
|
|
|
function wrapSubtree(root) {
|
|
try {
|
|
if (!root) return;
|
|
// Walk only within this subtree for text nodes
|
|
const walker = document.createTreeWalker(
|
|
root.nodeType === Node.ELEMENT_NODE ? root : root.parentNode || document.body,
|
|
NodeFilter.SHOW_TEXT,
|
|
{
|
|
acceptNode: (node) => {
|
|
if (!node || !node.nodeValue) return NodeFilter.FILTER_REJECT;
|
|
const parent = node.parentNode;
|
|
if (!parent || isExcluded(parent)) return NodeFilter.FILTER_REJECT;
|
|
if (parent.closest && parent.closest('.unicode-icon')) return NodeFilter.FILTER_REJECT;
|
|
const txt = node.nodeValue.trim();
|
|
if (!txt || txt.length > 3) return NodeFilter.FILTER_REJECT;
|
|
return greyscaleIcons.includes(txt) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
},
|
|
},
|
|
false,
|
|
);
|
|
const toWrap = [];
|
|
while (walker.nextNode()) {
|
|
toWrap.push(walker.currentNode);
|
|
}
|
|
for (const textNode of toWrap) {
|
|
wrapTextNodeOnce(textNode.parentNode, textNode);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
function processInitial() {
|
|
// Process only frequently used UI containers to avoid full-page walks
|
|
const roots = [document.body].filter(Boolean);
|
|
roots.forEach(wrapSubtree);
|
|
}
|
|
|
|
function startObserver() {
|
|
if (observer) return;
|
|
observer = new MutationObserver((mutations) => {
|
|
// Batch process only added nodes, ignore attribute/character changes
|
|
for (const m of mutations) {
|
|
if (m.type !== 'childList') continue;
|
|
m.addedNodes && m.addedNodes.forEach((n) => {
|
|
// Process only within the newly added subtree
|
|
wrapSubtree(n);
|
|
});
|
|
}
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
function stopObserver() {
|
|
if (observer) {
|
|
try { observer.disconnect(); } catch (_) {}
|
|
}
|
|
observer = null;
|
|
}
|
|
|
|
function enableGrey() {
|
|
if (enabled) return;
|
|
enabled = true;
|
|
try { document.body.classList.add('grey-icons-enabled'); } catch (_) {}
|
|
Meteor.defer(processInitial);
|
|
startObserver();
|
|
}
|
|
|
|
function disableGrey() {
|
|
if (!enabled) return;
|
|
enabled = false;
|
|
stopObserver();
|
|
try { document.body.classList.remove('grey-icons-enabled'); } catch (_) {}
|
|
// unwrap existing
|
|
document.querySelectorAll('span.unicode-icon').forEach((span) => {
|
|
const txt = document.createTextNode(span.textContent || '');
|
|
if (span.parentNode) span.parentNode.replaceChild(txt, span);
|
|
});
|
|
}
|
|
|
|
Tracker.autorun(() => {
|
|
const user = Meteor.user();
|
|
const on = !!(user && user.profile && user.profile.GreyIcons);
|
|
if (on) enableGrey(); else disableGrey();
|
|
});
|
|
});
|