YouTube: Expand All Video Comments(L)

Adds a "Expand all" button to video comments which expands every comment and replies - no more clicking "Read more".

// ==UserScript==
// @name            YouTube: Expand All Video Comments(L)
// @namespace       org.sidneys.userscripts
// @homepage        https://greasyfork.org/users/4839
// @version         4.7.9
// @description     Adds a "Expand all" button to video comments which expands every comment and replies - no more clicking "Read more".
// @author          sidneys
// @icon            https://www.youtube.com/favicon.ico
// @noframes
// @match           http*://www.youtube.com/*
// @run-at          document-end
// @grant           GM_addStyle
// @license         MIT
// ==/UserScript==
//https://gist.githubusercontent.com/sidneys/6756166a781bd76b97eeeda9fb0bc0c1/raw/

/* global Debug, onElementReady */

/**
 * ESLint
 * @global
 */
Debug = false


/**
 * Applicable URL paths
 * @default
 * @constant
 */
const urlPathList = [
    '/watch','/post/'
]


/**
 * Inject Stylesheet
 */
let injectStylesheet = () => {
    console.debug('injectStylesheet')

    GM_addStyle(`
        /* =======================================
           ELEMENTS
           ======================================= */

        /* Button: Expand all Comments
           --------------------------------------- */

        .expand-all-comments-button
        {
            padding: 0;
            align-self: start;
            margin: 0;
                    background: red;
            font-size:20px;

        }


        .expand-all-comments-button #checkboxLabel
        {
            padding-left: 0;
            display: inline-flex;
        }

        .busy .expand-all-comments-button #checkboxContainer,
        .busy .expand-all-comments-button #checkboxLabel
        {
            animation: var(--animation-busy-on);
        }

        /* Button: Expand all Comments
           Spinner
           --------------------------------------- */

        .expand-all-comments-button #checkboxLabel::after
        {
            background-size: 100%;
            background-repeat: no-repeat;
            height: var(--ytd-margin-4x, 26px);
            width: var(--ytd-margin-4x, 26px);
        }

        .expand-all-comments-button #checkboxLabel,
        .expand-all-comments-button #checkboxLabel::after
        {
            transition: filter 1000ms ease-in-out;
        }

        :not(.busy) .expand-all-comments-button #checkboxLabel::after
        {

            filter: opacity(0);
        }

        .busy .expand-all-comments-button #checkboxLabel::after
        {

            filter: opacity(1);
        }


        /* =======================================
           ANIMATIONS
           ======================================= */

        :root
        {
            --animation-busy-on: 'busy-on' 500ms ease-in-out 1000ms 1 normal forwards running;
        }

        @keyframes busy-on {
            from {
                pointer-events: none;
                cursor: default;
            }
            to {
                filter: saturate(0.1);
                color: hsla(0deg, 0%, 100%, 0.5);
            }
        }
    `)
}

/**
 * Set global busy mode
 * @param {Boolean} isBusy - Yes/No
 * @param {String=} selector - Contextual element selector
 */
let setBusy = (isBusy, selector = 'ytd-comments') => {
    // console.debug('setBusy', 'isBusy:', isBusy)

    let element = document.querySelector(selector)

    if (isBusy === true) {
        element.classList.add('busy')
        return
    } else {
        element.classList.remove('busy')
    }
}

/**
 * Get Button element
 * @returns {Boolean} - On/Off
 */
let getButtonElement = () => document.querySelector('.expand-all-comments-button')

/**
 * Get Toggle state
 * @returns {Boolean} - On/Off
 */
let getToggleState = () => Boolean(getButtonElement() && getButtonElement().checked)


/**
 * Expand all comments
 */
let expandAllComments = () => {
    console.debug('expandAllComments')

    // Look for "View X replies" buttons in comment section
    onElementReady('ytd-comment-replies-renderer #more-replies.ytd-comment-replies-renderer', false, (buttonElement) => {
        // Abort if toggle disabled
        if (!getToggleState()) { return }

        /** @listens buttonElement:Event#click */
        // buttonElement.addEventListener('click', () => setBusy(false), { once: true, passive: true })

        // Busy = yes
        // setBusy(true)

        //  Click button
        buttonElement.click()
    })

    // Look for "Read More" buttons in comment section
    onElementReady('ytd-comments tp-yt-paper-button.ytd-expander#more:not([hidden])', false, (buttonElement) => {
        // Abort if toggle disabled
        if (!getToggleState()) { return }

        /** @listens buttonElement:Event#click */
        // buttonElement.addEventListener('click', () => setBusy(false), { once: true, passive: true })

        // Busy = yes
        // setBusy(true)

        //  Click button
        buttonElement.click()
    })
  //ExpandNestedReplies顯示更多回復
  /*
  onElementReady('ytd-comment-replies-renderer ytd-continuation-item-renderer button', false, (buttonElement2) => {

        //  Click button
        buttonElement.click()
        buttonElement.style.display = 'none';
    })
    */
  //"less"隱藏"顯示部分內容"(ytd-comments刪除開頭,才能支援兼容post)
  onElementReady('ytd-comments tp-yt-paper-button.ytd-expander#less:not([hidden])', false, (buttonElement2) => {

        //  Click button
        buttonElement2.style.display = 'none';
    })
}


/**
 * Check if the toggle is enabled, if yes, start expanding
 */
let tryExpandAllComments = () => {
    console.debug('tryExpandAllComments')

    const toggleState = getToggleState()

    console.debug('toggle state:', toggleState)

    // Abort if toggle disabled
    if (!toggleState) { return }

    expandAllComments()
}


/**
 * Render button: 'Expand all Comments'
 * @param {Element} element - Container element
 */
let renderButton = (element) => {
    console.debug('renderButton')

    const buttonElement = document.createElement('tp-yt-paper-checkbox')
    buttonElement.className = 'expand-all-comments-button'
    buttonElement.innerHTML = `
    <div id="icon-label" class="yt-dropdown-menu">
        V
    </div>
    `

    // Add button
    element.appendChild(buttonElement)

    // Handle button toggle
    buttonElement.onchange = tryExpandAllComments

    // Status
    console.debug('rendered button')
}


/**
 * Init
 */
let init = () => {
    console.info('init')

    // Verify URL path
    if (!urlPathList.some(urlPath => window.location.pathname.startsWith(urlPath))) { return }

    // Add Stylesheet
    injectStylesheet()

    // Wait for menu container
    onElementReady('ytd-comments ytd-comments-header-renderer > #title', false, (element) => {
        console.debug('onElementReady', 'ytd-comments ytd-comments-header-renderer > #title')

        // Render button
        renderButton(element)
    })

    // // Wait for variable section container
    // onElementReady('ytd-item-section-renderer#sections.style-scope.ytd-comments > #contents', false, (element) => {
    //     console.debug('onElementReady', 'element:',  '#contents')
    //
    //     /**
    //      * YouTube: Detect "Load More" stuff
    //      * @listens ytd-item-section-rendere:Event#yt-load-next-continuation
    //      */
    //     element.parentElement.addEventListener('yt-load-next-continuation', (event) => {
    //         console.debug('ytd-item-section-renderer#yt-load-next-continuation')
    //
    //         const currentTarget = event.currentTarget
    //         const shownItems = currentTarget && currentTarget.__data && currentTarget.__data.shownItems || []
    //         const shownItemsCount = shownItems.length
    //
    //         // DEBUG
    //         // console.debug('currentTarget.__data:')
    //         // console.dir(currentTarget.__data)
    //
    //         // Probe whether this is still the initial item batch, if yes, skip
    //         if (shownItemsCount === 0) { return }
    //
    //         tryExpandAllComments()
    //     })
    // })
}

/**
 * ESLint
 * @exports
 */
/* exported onElementReady, waitForKeyElements */


/**
 * @private
 *
 * Query for new DOM nodes matching a specified selector.
 *
 * @param {String} selector - CSS Selector
 * @param {function=} callback - Callback
 */
let queryForElements = (selector, callback) => {
    // console.debug('queryForElements', 'selector:', selector)

    // Remember already-found elements via this attribute
    const attributeName = 'was-queried'

    // Search for elements by selector
    let elementList = document.querySelectorAll(selector) || []
    elementList.forEach((element) => {
        if (element.hasAttribute(attributeName)) { return }
        element.setAttribute(attributeName, 'true')
        callback(element)
    })
}

/**
 * @public
 *
 * Wait for Elements with a given CSS selector to enter the DOM.
 * Returns a Promise resolving with new Elements, and triggers a callback for every Element.
 *
 * @param {String} selector - CSS Selector
 * @param {Boolean=} findOnce - Stop querying after first successful pass
 * @param {function=} callback - Callback with Element
 * @returns {Promise<Element>} - Resolves with Element
 */
let onElementReady = (selector, findOnce = false, callback = () => {}) => {
    // console.debug('onElementReady', 'findOnce:', findOnce)

    return new Promise((resolve) => {
        // Initial Query
        queryForElements(selector, (element) => {
            resolve(element)
            callback(element)
        })

        // Continuous Query
        const observer = new MutationObserver(() => {
            // DOM Changes detected
            queryForElements(selector, (element) => {
                resolve(element)
                callback(element)
            })

            if (findOnce) { observer.disconnect() }
        })

        // Observe DOM Changes
        observer.observe(document.documentElement, {
            attributes: false,
            childList: true,
            subtree: true
        })
    })
}

/**
 * @public
 * @deprecated
 *
 * waitForKeyElements Polyfill
 *
 * @param {String} selector - CSS selector of elements to search / monitor ('.comment')
 * @param {function} callback - Callback executed on element detection (called with element as argument)
 * @param {Boolean=} findOnce - Stop lookup after the last currently available element has been found
 * @returns {Promise<Element>} - Element
 */
let waitForKeyElements = (selector, callback, findOnce) => onElementReady(selector, findOnce, callback)


/**
 * YouTube: Detect in-page navigation
 * @listens window:Event#yt-navigate-finish
 */
    init()

    // Look for "Read More" buttons in comment section
onElementReady('tp-yt-paper-button.ytd-expander#more:not([hidden])', false, (buttonElement) => {

        //  Click button
        buttonElement.click()
    })
  //"less"隱藏"顯示部分內容"
onElementReady('tp-yt-paper-button.ytd-expander#less:not([hidden])', false, (buttonElement2) => {

        //  Click button
        buttonElement2.style.display = 'none';
    })