|
|
@@ -0,0 +1,121 @@
|
|
|
+Meteor.startup(() => {
|
|
|
+ const greyscaleIcons = [
|
|
|
+ '🔼', '❌', '🏷️', '📅', '📥', '🚀', '👤', '👥', '✍️', '📋', '✏️', '🌐', '📎', '📝', '📋', '📜', '🏠', '🔒', '🔕', '🃏',
|
|
|
+ '⏰', '🛒', '🔢', '✅', '❌', '👁️', '👍', '📋', '🕐', '🎨',
|
|
|
+ '📤', '⬆️', '⬇️', '➡️', '📦',
|
|
|
+ '⬅️', '↕️', '🔽', '🔍', '▼', '🏊',
|
|
|
+ '🔔', '⚙️', '🖼️', '🔑', '🚪', '◀️', '⌨️', '👥', '🏷️', '✅', '🚫'
|
|
|
+ ];
|
|
|
+
|
|
|
+ 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 processNode(root) {
|
|
|
+ try {
|
|
|
+ if (!root) return;
|
|
|
+ if (root.nodeType === Node.TEXT_NODE) {
|
|
|
+ wrapTextNodeOnce(root.parentNode, root);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (root.nodeType !== Node.ELEMENT_NODE) return;
|
|
|
+ if (isExcluded(root)) return;
|
|
|
+ // Fast path: only check direct text children first
|
|
|
+ const children = Array.from(root.childNodes);
|
|
|
+ for (const child of children) {
|
|
|
+ if (child.nodeType === Node.TEXT_NODE) {
|
|
|
+ wrapTextNodeOnce(root, child);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // If element is small, also scan one level deeper to catch common structures
|
|
|
+ if (children.length <= 20) {
|
|
|
+ for (const child of children) {
|
|
|
+ if (child.nodeType === Node.ELEMENT_NODE && !isExcluded(child)) {
|
|
|
+ for (const gchild of Array.from(child.childNodes)) {
|
|
|
+ if (gchild.nodeType === Node.TEXT_NODE) wrapTextNodeOnce(child, gchild);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (_) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ function processInitial() {
|
|
|
+ // Process only frequently used UI containers to avoid full-page walks
|
|
|
+ const roots = [
|
|
|
+ document.body,
|
|
|
+ document.querySelector('#header-user-bar'),
|
|
|
+ ...Array.from(document.querySelectorAll('.pop-over, .pop-over-list, .board-header, .card-details, .sidebar-content')),
|
|
|
+ ].filter(Boolean);
|
|
|
+ roots.forEach(processNode);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) => {
|
|
|
+ // Avoid scanning huge subtrees repeatedly by limiting depth
|
|
|
+ processNode(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;
|
|
|
+ Meteor.defer(processInitial);
|
|
|
+ startObserver();
|
|
|
+ }
|
|
|
+
|
|
|
+ function disableGrey() {
|
|
|
+ if (!enabled) return;
|
|
|
+ enabled = false;
|
|
|
+ stopObserver();
|
|
|
+ // 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();
|
|
|
+ });
|
|
|
+});
|