您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add some magic to CMTT projects!
// ==UserScript== // @name CMTT-MAGIC // @name:ru CMTT-MAGIC // @version 1.0.1 // @description Add some magic to CMTT projects! // @description:ru Добавить немного магии проектам CMTT! // @namespace https://kartoshka.com // @author Kartoshka // @license GPLv2+ // @match https://vc.ru/* // @match https://tjournal.ru/* // @match https://dtf.ru/* // @run-at document-end // @grant GM_setValue // @grant GM.setValue // @grant GM_getValue // @grant GM.getValue // ==/UserScript== const COMMENTS_CONTAINER_SELECTOR = '.comments'; const COMMENTS_TABS_CONTAINER_SELECTOR = '.comments .comments__navigation .ui-tabs__content'; const COMMENTS_TITLE_SELECTOR = '.comments .comments__title'; const TAB_MODE_BUTTON_CLASS = 'magic-comments-mode-button'; const TAB_ACTION_BUTTON_CLASS = 'magic-comments-action-button'; const TAB_BUTTON_TEXT_CLASS = 'magic-comments-tab-button-text'; const OP_EXPAND_TOPLEVEL = 'expand-toplevel'; const OP_EXPAND_ALL = 'expand-all'; const OP_COLLAPSE_ALL = 'collapse-all'; const OP_COLLAPSE_ALL_SCROLL = 'collapse-all-scroll'; const OP_LOADMORE_ALL_NEAR_VIEWPORT = 'loadmore-all-near-viewport'; const OP_LOADMORE_FIRST_BELOW_VIEWPORT = 'loadmore-first-below-viewport'; const OP_LOAD_ALL_IF_PARTIAL = 'load-all-if-partial'; const ACTION_EXPAND = 'expand'; const ACTION_COLLAPSE = 'collapse'; const MODE_EXPAND_ON_SCROLL = 'expand-on-scroll'; const MODE_EXPAND_EARLY = 'expand-early'; const MODE_COLLAPSE_ALL = 'collapse-all'; const ACTIONS = [ { value: ACTION_EXPAND, title: 'Развернуть', operations: [ OP_EXPAND_ALL, { op: OP_LOAD_ALL_IF_PARTIAL, args: [{ afterLoadOperations: OP_EXPAND_ALL }], }, ], newCurrentMode: MODE_EXPAND_EARLY, }, { value: ACTION_COLLAPSE, title: 'Свернуть', operations: OP_COLLAPSE_ALL, newCurrentMode: MODE_COLLAPSE_ALL, }, ]; const MODES = [ { value: null, title: 'Не разворачиваются', initialOperations: null, initialNextAction: ACTION_EXPAND, }, { value: MODE_EXPAND_ON_SCROLL, title: 'Разворачиваются', initialOperations: [ OP_EXPAND_ALL, { op: OP_LOAD_ALL_IF_PARTIAL, args: [{ afterLoadOperations: OP_EXPAND_ALL }], }, ], initialNextAction: ACTION_COLLAPSE, }, { value: MODE_EXPAND_EARLY, title: 'Разворачиваются сразу', initialOperations: [ OP_EXPAND_ALL, { op: OP_LOAD_ALL_IF_PARTIAL, args: [{ afterLoadOperations: OP_EXPAND_ALL }], }, ], initialNextAction: ACTION_COLLAPSE, }, { value: MODE_COLLAPSE_ALL, title: 'Сворачиваются все', initialOperations: OP_COLLAPSE_ALL_SCROLL, initialNextAction: ACTION_EXPAND, }, ]; const DEFAULT_OPTIONS = { mode: null, }; let windowLoaded = false; let magicHappened = false; let options; let currentMode; let nextAction; let collapseProtected; let restoreCommentScrollPosition; let currentUrlPath; let currentPageTitle; let scrollAggregateTimer; let throttledLoadMoreTimer; let newPageDetectorTimer; let waitForCommentsInitializedTimer; let loadAllIfPartialCompleteTimer; loadOptions().then(magic); window.addEventListener('load', () => { windowLoaded = true; remagic(); }); window.addEventListener('popstate', startNewPageDetector); window.addEventListener('click', startNewPageDetector); /** * */ function magic() { if (!options) { return; } if (!windowLoaded) { return; } if (!getTabsContainer()) { return; } if (magicHappened) { return; } magicHappened = true; clean(); init(); } /** * */ function remagic() { magicHappened = false; magic(); } /** * */ function init() { if (!options) { return; } currentMode = options.mode; nextAction = getModeParams(currentMode).initialNextAction; currentPageTitle = document.title; currentUrlPath = window.location.pathname; processOptionsBasedOnUrl(); renderButtons(); applyCurrentMode(); runOperations(getModeParams(currentMode).initialOperations); addLoadAllIfPartialLinksClickHandler(); } /** * */ function clean() { stopAllExpandRoutines(); cleanCollapseProtected(); restoreCommentScrollPosition = null; newPageDetectorTimer = stopTimer(newPageDetectorTimer); waitForCommentsInitializedTimer = stopTimer(waitForCommentsInitializedTimer); loadAllIfPartialCompleteTimer = stopTimer(loadAllIfPartialCompleteTimer); } /** * */ function startNewPageDetector() { stopTimer(newPageDetectorTimer); newPageDetectorTimer = setTimeout(newPageDetectorTick.bind(null, 0), 0); } /** * */ function newPageDetectorTick(iteration = 0) { newPageDetectorTimer = null; if (iteration > 20) { return; } const rescheduleWaitForCommentsInitialized = () => { stopTimer(waitForCommentsInitializedTimer); waitForCommentsInitializedTimer = setTimeout(waitForCommentsInitializedTick.bind(null, 0), 250); }; if (currentUrlPath !== window.location.pathname) { currentUrlPath = window.location.pathname; rescheduleWaitForCommentsInitialized(); return; } if (currentPageTitle !== document.title) { currentPageTitle = document.title; rescheduleWaitForCommentsInitialized(); return; } newPageDetectorTimer = setTimeout(newPageDetectorTick.bind(null, iteration + 1), 250); } /** * */ function waitForCommentsInitializedTick(iteration = 0) { waitForCommentsInitializedTimer = null; if (iteration > 40) { return; } const reschedule = (timeout = 250) => { waitForCommentsInitializedTimer = setTimeout( waitForCommentsInitializedTick.bind(null, iteration + 1), timeout, ); }; const tabs = getTabsContainer(); if (!tabs) { reschedule(); return; } if (optionsModeButtonExists(tabs)) { reschedule(); return; } (async () => { await sleep(1); remagic(); await sleep(250); if (!waitForCommentsInitializedTimer) { // Lets keep trying even after remagic reschedule(); } })(); } /** * */ function processOptionsBasedOnUrl() { const query = new URLSearchParams(window.location.search); const id = query.get('comment'); if (id) { addCollapseProtected(id); restoreCommentScrollPosition = id; } } /** * */ function renderButtons() { const tabs = getTabsContainer(); if (!tabs) { return; } renderOptionsModeButton(tabs); renderActionButton(tabs); } /** * */ function renderOptionsModeButton(container = getTabsContainer()) { if (!options) { return; } // This button always shows mode stored in options (not the currentMode) const modeParams = getModeParams(options.mode, 0); const nextModeParams = getModeParamsByIndex(findModeIndex(options.mode) + 1); const button = ensureTabButton(TAB_MODE_BUTTON_CLASS, container); setTabButtonText(button, modeParams.title); setTabButtonOnClick(button, setOptionsMode.bind(null, nextModeParams.value)); setTabButtonTextStyle( button, 'padding:3px 7px;border:1px dotted #888;border-radius:4px;opacity:.8;user-select:none', ); } /** * */ function optionsModeButtonExists(container = getTabsContainer()) { return !!container.querySelector(`.${TAB_MODE_BUTTON_CLASS}`); } /** * */ function renderActionButton(container = getTabsContainer()) { const actionParams = getActionParams(nextAction); const button = ensureTabButton(TAB_ACTION_BUTTON_CLASS, container); setTabButtonText(button, actionParams?.title); setTabButtonOnClick(button, runAction.bind(null, nextAction)); setTabButtonTextStyle(button, 'opacity:.7;user-select:none'); } /** * */ function setOptionsMode(newMode) { if (!options) { return; } if (options.mode === newMode) { return; } options.mode = newMode; saveOptions().then(remagic); } /** * */ function runAction(action) { const actionParams = getActionParams(action, 0); const nextActionParams = getActionParamsByIndex(findActionIndex(action) + 1); currentMode = actionParams.newCurrentMode; nextAction = nextActionParams.value; renderActionButton(); applyCurrentMode(); runOperations(actionParams.operations); } /** * */ function applyCurrentMode() { switch (currentMode) { case MODE_EXPAND_ON_SCROLL: applyExpandOnScrollMode(); break; case MODE_EXPAND_EARLY: applyExpandEarlyMode(); break; case MODE_COLLAPSE_ALL: applyCollapseAllMode(); break; } } /** * */ function applyExpandOnScrollMode() { runOperations(OP_LOADMORE_ALL_NEAR_VIEWPORT); document.addEventListener('scroll', scrollHandler); } /** * */ function applyExpandEarlyMode() { runOperations(OP_LOADMORE_ALL_NEAR_VIEWPORT); document.addEventListener('scroll', scrollHandler); scheduleThrottledLoadMore(); } /** * */ function applyCollapseAllMode() { stopAllExpandRoutines(); } /** * */ function stopAllExpandRoutines() { document.removeEventListener('scroll', scrollHandler); scrollAggregateTimer = stopTimer(scrollAggregateTimer); throttledLoadMoreTimer = stopTimer(throttledLoadMoreTimer); } /** * */ function scrollHandler() { if (!scrollAggregateTimer) { scrollAggregateTimer = setTimeout(() => { scrollAggregateTimer = null; runOperation(OP_LOADMORE_ALL_NEAR_VIEWPORT); }, 300); } } /** * */ function scheduleThrottledLoadMore() { throttledLoadMoreTimer = setTimeout(throttledLoadMoreTick, 3000); } /** * */ function throttledLoadMoreTick() { if (runOperation(OP_LOADMORE_FIRST_BELOW_VIEWPORT)) { scheduleThrottledLoadMore(); } } /** * */ function runOperations(operations) { if (!operations) { return undefined; } return Array.isArray(operations) ? operations.map((item) => (!!item ? runOperation(item) : undefined)) : runOperation(operations); } /** * */ function runOperation(operation) { let operationArgs = []; if (operation?.op) { if (operation.hasOwnProperty('args')) { operationArgs = Array.isArray(operation.args) ? operation.args : [operation.args]; } operation = operation.op; } else if (typeof operation !== 'string') { throw new Error('Operation must be a string'); } switch (operation) { case OP_EXPAND_TOPLEVEL: return expandCollapsedUpToLevel(1); case OP_EXPAND_ALL: return expandAllCollapsed(); case OP_COLLAPSE_ALL: return collapseAllExceptProtected(); case OP_COLLAPSE_ALL_SCROLL: return collapseAllExceptProtectedScroll(); case OP_LOADMORE_ALL_NEAR_VIEWPORT: return loadMoreAllNearViewport(); case OP_LOADMORE_FIRST_BELOW_VIEWPORT: return loadMoreFirstBelowViewport(); case OP_LOAD_ALL_IF_PARTIAL: return loadAllIfPartial(...operationArgs); default: throw new Error(`Unknown operation: ${operation}`); } } /** * */ function expandCollapsedUpToLevel(level) { let counter = 0; for (let i = 1; i <= level; i++) { counter += clickOnNodesPreservingScroll(`[data-level="${i}"] .comment__expand-branch--visible`); } return counter; } /** * */ function expandAllCollapsed() { return clickOnNodesPreservingScroll('.comment__expand-branch--visible'); } /** * */ function collapseAllExceptProtected() { doCollapseComments('', true, false); } /** * */ function collapseAllExceptProtectedScroll() { doCollapseComments('', true, true); } /** * */ function doCollapseComments(filterSelector = '', exceptProtected = true, updateScroll = true) { const container = getCommentsContainer(); let filter = filterSelector; if (exceptProtected) { filter += generateCollapseProtectedSelector(); } const collapseSelector = `.comment${filter} .comment__branch--no-border`; if (!updateScroll) { return clickOnNodesPreservingScroll(collapseSelector); } (async () => { const windowHeight = window.innerHeight || document.documentElement.clientHeight; await waitForSameValueWithinTime(() => window.scrollY, 1, 1); let commentIdScrollTo; let commentScrollTo; let positionFromBottomScrollTo; if (restoreCommentScrollPosition) { await waitForSameValueWithinTime(() => window.scrollY, 25, 5); commentIdScrollTo = restoreCommentScrollPosition; restoreCommentScrollPosition = null; const el = getCommentElementByCommentId(commentIdScrollTo); if (el) { const top = getElementViewportRelativeTop(el); if (top >= 500 || top <= -500) { commentIdScrollTo = null; } } else { commentIdScrollTo = null; } } if (commentIdScrollTo) { commentScrollTo = getCommentElementByCommentId(commentIdScrollTo, container); } else { commentScrollTo = findFirstCommentElementBeginsWithinViewport(); if (typeof commentScrollTo === 'number') { if (commentScrollTo < 0) { // Comments are below the viewport, collapse should not affect scroll position commentScrollTo = null; } else { // Comments are above the viewport, it's better to scroll to position from bottom instead positionFromBottomScrollTo = document.documentElement.scrollHeight - window.scrollY; commentScrollTo = null; } } } if (!commentScrollTo && !positionFromBottomScrollTo) { return clickOnNodesPreservingScroll(collapseSelector); } let oldTop = null; if (commentScrollTo) { oldTop = getElementViewportRelativeTop(commentScrollTo); } const scrollToNewPos = () => { let newScrollY = null; if (positionFromBottomScrollTo) { newScrollY = document.documentElement.scrollHeight - positionFromBottomScrollTo; } else if (oldTop !== null && commentScrollTo) { if (commentScrollTo?.classList?.contains('comment--collapsed')) { // If commentScrollTo is collapsed now then scroll to its first visible ancestor try { for (const ancestorId of getCommentAncestors( getCommentIdByCommentElement(commentScrollTo), )) { const ancestorEl = getCommentElementByCommentId(ancestorId); if (!ancestorEl?.classList || ancestorEl?.classList?.contains('comment--collapsed')) { continue; } newScrollY = window.scrollY + getElementViewportRelativeTop(ancestorEl) - windowHeight / 2 + 100; break; } } catch (e) { return; } } else { newScrollY = window.scrollY + getElementViewportRelativeTop(commentScrollTo) - oldTop; } } if (typeof newScrollY !== 'number') { return null; } newScrollY = Math.floor(newScrollY); window.scrollTo(0, newScrollY); return newScrollY; }; const result = clickOnNodes(collapseSelector); let scrolledTo = scrollToNewPos(); await sleep(1); if (scrolledTo !== window.scrollY) { scrollToNewPos(); } return result; })(); } /** * */ function generateCollapseProtectedSelector(container = getCommentsContainer()) { if (!(collapseProtected instanceof Set) || !collapseProtected.size) { return ''; } const allDescendents = new Set(); const processedRoots = new Set(); for (const protectedId of [...collapseProtected]) { const rootId = getCommentRootId(protectedId); if (processedRoots.has(rootId)) { continue; } processedRoots.add(rootId); for (const descendentId of getCommentDescendants(rootId)) { allDescendents.add(descendentId); } } const branchClosings = new Set(); for (const descendentId of [...allDescendents]) { if ( container.querySelector(`.comment[data-id="${descendentId}"] .comment__branch--no-border`) ) { branchClosings.add(descendentId); } } if (branchClosings.size > 1000) { return ''; } return [...branchClosings].map((item) => `:not([data-id="${item}"])`).join(''); } /** * */ function loadMoreAllNearViewport() { const inViewport = []; const nearViewport = []; iterateLoadMoreNodes((node) => { const top = getElementViewportRelativeTop(node); const bottom = getElementViewportRelativeBottom(node); if (top > 0) { if (bottom > 0) { inViewport.push(node); } else if (bottom > -200) { nearViewport.push(node); } else if (bottom > -500 && !nearViewport.length) { nearViewport.push(node); } } }); for (const node of new Set([...inViewport, ...nearViewport])) { doClickLoadMoreNode(node); } } /** * */ function loadMoreFirstBelowViewport() { return !!iterateLoadMoreNodes((node) => { const top = getElementViewportRelativeTop(node); const bottom = getElementViewportRelativeBottom(node); if (top > 0 && bottom < 0) { doClickLoadMoreNode(node); return true; } }); } /** * */ function iterateLoadMoreNodes(fn) { const selectors = ['.comment__load-more:not(.comment__load-more--waiting)', '.comments__more']; const container = getCommentsContainer(); for (const selector of selectors) { for (const node of container.querySelectorAll( `${selector}:not(.magic-comments-load-waiting)`, )) { if (fn(node) === true) { return true; } } } } /** * */ function doClickLoadMoreNode(node) { if (node?.classList) { node.classList.add('magic-comments-load-waiting'); } node.click(); } /** * */ function addLoadAllIfPartialLinksClickHandler() { for (const link of document.querySelectorAll( `${COMMENTS_CONTAINER_SELECTOR} .comments__link_to_all a`, )) { link.addEventListener('click', loadAllIfPartialLinksClickHandler); } } /** * */ function loadAllIfPartialLinksClickHandler() { let operations; switch (currentMode) { case MODE_EXPAND_ON_SCROLL: case MODE_EXPAND_EARLY: operations = OP_EXPAND_ALL; break; case MODE_COLLAPSE_ALL: operations = OP_COLLAPSE_ALL_SCROLL; break; default: operations = []; } startLoadAllIfPartialCompleteTimer({ afterLoadOperations: operations, }); } /** * */ function loadAllIfPartial(...args) { const link = document.querySelector(`${COMMENTS_CONTAINER_SELECTOR} .comments__link_to_all a`); if (!link) { return; } link.click(); startLoadAllIfPartialCompleteTimer(...args); } /** * */ function startLoadAllIfPartialCompleteTimer(...args) { stopTimer(loadAllIfPartialCompleteTimer); loadAllIfPartialCompleteTimer = setTimeout( loadAllIfPartialCompleteTick.bind(null, 0, ...args), 250, ); } /** * */ function loadAllIfPartialCompleteTick(iteration = 0, ...args) { loadAllIfPartialCompleteTimer = null; if (iteration > 50) { return; } const tabs = getTabsContainer(); if (tabs) { if (!tabs.querySelector('.comments__link_to_all')) { setTimeout(loadAllIfPartialCompleteCallback.bind(null, ...args), 1000); } return; } loadAllIfPartialCompleteTimer = setTimeout( loadAllIfPartialCompleteTick.bind(null, iteration + 1, ...args), 250, ); } /** * */ function loadAllIfPartialCompleteCallback(params) { if (params?.afterLoadOperations) { runOperations(params.afterLoadOperations); } } /** * */ function getCommentElementByCommentId(id, container = getCommentsContainer()) { return container.querySelector(`.comment[data-id="${id}"]`); } /** * */ function getCommentIdByCommentElement(element) { if (typeof element?.getAttribute !== 'function') { return undefined; } return element.getAttribute('data-id') | undefined; } /** * */ function getNthCommentElement(n, filter = null, comments = null) { return filterCommentElements(filter, comments)?.[n - 1]; } /** * */ function getLastCommentElement(filter = null, comments = null) { comments = filterCommentElements(filter, comments); return comments?.[comments.length - 1]; } /** * */ function getCommentElementsCount(filter = null, comments = null) { return filterCommentElements(filter, comments).length; } /** * */ function binarySearchCommentElement(fn, filter = null, comments = null) { comments = filterCommentElements(filter, comments); if (!comments.length) { return undefined; } let l = 0; let r = comments.length - 1; while (l <= r) { const i = Math.floor((l + r) / 2); const comment = comments?.[i]; if (!comment) { return undefined; } const result = fn(comment, i, comments); if (result === -1) { r = i - 1; } else if (result === 1) { l = i + 1; } else { return comment; } } return undefined; } /** * */ function getAllCommentElements() { return filterCommentElements(null); } /** * */ function filterCommentElements(filter, comments) { if (!comments) { comments = document.getElementsByClassName('comment'); if (!comments) { throw new Error('Function getElementsByClassName() returned unexpected result'); } } // Convert to array if it's an iterable (e.g. Set) comments = [...comments]; if (typeof filter !== 'function') { return comments; } if (comments.length) { comments = comments.filter(filter); } return comments; } /** * */ function visibleCommentsOnlyFilter(comment) { if (!comment?.classList) { return false; } return !comment.classList.contains('comment--collapsed'); } /** * */ function findFirstCommentElementBeginsWithinViewport() { const comments = filterCommentElements(visibleCommentsOnlyFilter); if (!comments.length) { return undefined; } const windowHeight = window.innerHeight || document.documentElement.clientHeight; let top = getElementViewportRelativeTop(comments[0]); if (top >= 0) { if (top < windowHeight) { return comments[0]; } // There is no comment within the viewport (all of them are below it) return top; } top = getElementViewportRelativeBottom(comments[comments.length - 1]); if (top < 0) { // All comments are above the viewport return top; } return binarySearchCommentElement( (comment, index, comments) => { let top = getElementViewportRelativeTop(comment); if (top < 0) { if ( comments?.[index + 1] && getElementViewportRelativeTop(comments?.[index + 1]) >= 0 && getElementViewportRelativeTop(comments?.[index + 1]) < windowHeight ) { return -1; } else { // A case when comment is started above viewport and took the entire window height return 0; } } else if (top > windowHeight) { return 1; } else if ( comments?.[index - 1] && getElementViewportRelativeTop(comments?.[index - 1]) > 0 ) { return -1; } return 0; }, null, comments, ); } /** * */ function getCommentsCountByAttr() { const title = document.querySelector(COMMENTS_TITLE_SELECTOR); if (!title) { return undefined; } const count = parseInt(title.getAttribute('data-count')); return typeof count === 'number' ? count : undefined; } /** * */ function getCommentParentId(id) { id = parseInt(id); if (!id) { throw new Error('Invalid comment id'); } const comment = document.querySelector( `${COMMENTS_CONTAINER_SELECTOR} .comment[data-id="${id}"]`, ); if (!comment) { throw new Error(`Comment "${id}" not found`); } return parseInt(comment.getAttribute('data-reply_to')) || null; } /** * */ function getSiblingCommentId(id) { id = parseInt(id); if (!id) { throw new Error('Invalid comment id'); } const comment = document.querySelector( `${COMMENTS_CONTAINER_SELECTOR} .comment[data-id="${id}"] + .comment`, ); if (!comment) { return null; } return parseInt(comment.getAttribute('data-id')) || null; } /** * */ function getCommentAncestors(id) { const ancestors = []; let loopDetector = 0; let parentId = id; while (parentId && ++loopDetector < 1000) { parentId = getCommentParentId(parentId); if (parentId) { ancestors.push(parentId); } } return ancestors; } /** * */ function isCommentAncestor(id, ancestorId) { ancestorId = parseInt(ancestorId); let loopDetector = 0; let parentId = id; while (parentId && ++loopDetector < 1000) { parentId = getCommentParentId(parentId); if (parentId && parentId === ancestorId) { return true; } } return false; } /** * */ function getCommentRootId(id) { id = parseInt(id); if (!id) { throw new Error('Invalid comment id'); } const ancestors = getCommentAncestors(id); return ancestors.length ? ancestors[ancestors.length - 1] : id; } /** * */ function getCommentDescendants(id) { const descendants = []; let loopDetector = 0; let siblingId = id; while (siblingId && ++loopDetector < 10000) { siblingId = getSiblingCommentId(siblingId); if (siblingId) { if (!isCommentAncestor(siblingId, id)) { break; } descendants.push(siblingId); } } return descendants; } /** * */ function ensureTabButton(className, container = getTabsContainer()) { let button = container.querySelector(`.${className}`); if (!button) { button = createTabButton(); button.classList.add(className); container.appendChild(button); } return button; } /** * */ function createTabButton() { const button = document.createElement('div'); button.classList.add('ui-tab'); const buttonLabel = document.createElement('span'); buttonLabel.classList.add('ui-tab__label'); const buttonMagicText = document.createElement('span'); buttonMagicText.classList.add(TAB_BUTTON_TEXT_CLASS); buttonLabel.appendChild(buttonMagicText); button.appendChild(buttonLabel); return button; } /** * */ function setTabButtonText(button, text) { setTabButtonTextProperty(button, 'textContent', text); } /** * */ function setTabButtonTextStyle(button, style) { setTabButtonTextProperty(button, 'style', style); } /** * */ function setTabButtonTextProperty(button, property, value) { if (!button) { return; } const textEl = button.querySelector(`.${TAB_BUTTON_TEXT_CLASS}`); if (!textEl) { return; } textEl[property] = value; } /** * */ function addTabButtonClass(button, className) { if (button?.classList) { button.classList.add(className); } } /** * */ function removeTabButtonClass(button, className) { if (button?.classList) { button.classList.remove(className); } } /** * */ function setTabButtonOnClick(button, callback) { if (button) { button.onclick = callback; } } /** * */ function clickOnNodes(selector, filter = null, container = getCommentsContainer()) { if (!container) { return 0; } if (filter && typeof filter !== 'function') { throw new Error('Node filter must be a function'); } let counter = 0; for (const node of container.querySelectorAll(selector)) { if (filter && !filter(node)) { continue; } node.click(); ++counter; } return counter; } /** * */ function clickOnNodesPreservingScroll(selector, filter = null, container = getCommentsContainer()) { const pos = window.scrollY; const result = clickOnNodes(selector, filter, container); window.scrollTo(0, pos); return result; } /** * */ function getCommentsContainer() { return document.querySelector(COMMENTS_CONTAINER_SELECTOR); } /** * */ function getTabsContainer() { return document.querySelector(COMMENTS_TABS_CONTAINER_SELECTOR); } /** * */ function getElementViewportRelativeTop(element) { if (!element?.getBoundingClientRect) { return undefined; } return element.getBoundingClientRect().top; } /** * */ function getElementViewportRelativeBottom(element) { if (!element?.getBoundingClientRect) { return undefined; } const windowHeight = window.innerHeight || document.documentElement.clientHeight; const rect = element.getBoundingClientRect(); return windowHeight - (rect.top + rect.height); } /** * */ function addCollapseProtected(id) { if (!(collapseProtected instanceof Set)) { collapseProtected = new Set(); } id = parseInt(id); if (id) { collapseProtected.add(id); } } /** * */ function removeCollapseProtected(id) { if (!(collapseProtected instanceof Set)) { return; } id = parseInt(id); if (id) { collapseProtected.remove(id); } } /** * */ function cleanCollapseProtected() { collapseProtected = null; } /** * */ async function waitForSameValueWithinTime(getter, delay, times, allowedValues) { if (typeof allowedValues !== 'undefined') { if (!Array.isArray(allowedValues)) { allowedValues = [allowedValues]; } } const checkValueIsAllowed = (value) => { if (!Array.isArray(allowedValues)) { return; } if (allowedValues.indexOf(value) === -1) { throw new Error(`Value "${value}" is not allowed`); } }; let i = 0; let val = getter(); checkValueIsAllowed(val); while (++i <= times) { await sleep(delay); const newval = getter(); checkValueIsAllowed(newval); if (newval !== val) { val = newval; i = 0; } } return val; } /** * */ function stopObserver(observer) { if (observer) { observer.disconnect(); } return null; } /** * */ function stopTimer(timer) { if (timer) { clearTimeout(timer); } return null; } /** * */ async function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } /** * */ function findModeIndex(mode) { return findParamsArrayEntryIndex(MODES, mode); } /** * */ function getModeParams(mode, defaultIndex = 0) { return getParamsArrayEntry(MODES, mode, defaultIndex); } /** * */ function getModeParamsByIndex(index, defaultIndex = 0) { return getParamsArrayEntryByIndex(MODES, index, defaultIndex); } /** * */ function findActionIndex(action) { return findParamsArrayEntryIndex(ACTIONS, action); } /** * */ function getActionParams(action, defaultIndex = 0) { return getParamsArrayEntry(ACTIONS, action, defaultIndex); } /** * */ function getActionParamsByIndex(index, defaultIndex = 0) { return getParamsArrayEntryByIndex(ACTIONS, index, defaultIndex); } /** * */ function findParamsArrayEntryIndex(arr, value) { return arr.findIndex((item) => item.value === value); } /** * */ function getParamsArrayEntry(arr, value, defaultIndex = 0) { return arr.find((item) => item.value === value) || arr[defaultIndex]; } /** * */ function getParamsArrayEntryByIndex(arr, index, defaultIndex = 0) { return arr?.[index] || arr[defaultIndex]; } /** * */ async function loadOptions() { options = await loadCurrentHostParam('options', DEFAULT_OPTIONS); } /** * */ async function saveOptions() { if (!options) { return false; } await saveCurrentHostParam('options', options); return true; } /** * */ function formatStorageParamName(name) { return name.replace(/[^a-z0-9]/i, '_'); } /** * */ async function loadParamFromStorage(param, defaultValue) { if (typeof GM?.getValue === 'function') { return await GM.getValue(formatStorageParamName(param), defaultValue); } else if (typeof GM_getValue === 'function') { return GM_getValue(formatStorageParamName(param), defaultValue); } else { throw new Error('Script requires GM.getValue() or GM_getValue() to load parameters'); } } /** * */ async function saveParamToStorage(param, value) { if (typeof value !== 'string') { throw new Error('Storage can contain only string values'); } if (typeof GM?.setValue === 'function') { await GM.setValue(formatStorageParamName(param), value); } else if (typeof GM_setValue === 'function') { GM_setValue(formatStorageParamName(param), value); } else { throw new Error('Script requires GM.setValue() or GM_setValue() to save parameters'); } } /** * */ async function loadParamAsJsonFromStorage(param, defaultValue) { const value = await loadParamFromStorage(param, null); return typeof value === 'string' ? JSON.parse(value) : defaultValue; } /** * */ async function saveParamAsJsonToStorage(param, value) { return saveParamToStorage(param, JSON.stringify(value)); } /** * */ async function loadCurrentHostParam(name, defaultValue) { return loadParamAsJsonFromStorage(`${window.location.host}${name}`, defaultValue); } /** * */ async function saveCurrentHostParam(name, value) { return saveParamAsJsonToStorage(`${window.location.host}${name}`, value); }