はてなブックマークメタブ展開

  • // ==UserScript==
    // @name         はてなブックマークメタブ展開
    // @title        はてなブックマークメタブ展開
    // @namespace    https://let.hatelabo.jp/Lhankor_Mhy/let/jtaB0fa4gYAA
    // @version      0.13.2
    // @description  コメントメタブと2階ブクマを展開します。説明→https://realtor-readyabooks.hatenablog.com/entry/2022/12/28/153626
    // @author       Lhankor_Mhy
    // @match        https://b.hatena.ne.jp/entry/*
    // @icon         https://b.hatena.ne.jp/favicon.ico
    // @license      CC0
    // @noframes
    // @grant        none
    // @javascript_url
    // ==/UserScript==
    
    
    (async () => {
        const APIURL = 'https://b.hatena.ne.jp/entry/jsonlite/?url='
    
        document.body.insertAdjacentHTML('beforeend', `
    <style>
    [data-metabu] {
    	background-color: rgba(0,0,0,0.05);
    }
    [data-metabu].focus {
    	outline:dotted 1px;
    }
    #metahatebu-popover{
        inset: auto 0 0 auto;
        opacity: 0.5;
        border: none;
        background-color: #000;
        color: #fff;
        position: fixed;
    }
    </style>
    <dialog popover id="metahatebu-popover">メタブあります<button data-metabuprev>前</button><button data-metabunext>次</button></dialog>
        `)
        document.querySelector('.entry-comments').insertAdjacentHTML('beforeend', `
    <div class="entry-comment-contents" id="upStairsSeat"></div>
        `)
    
        // メタブありますポップオーバーのイベント設定
        const metahatebuPopover = document.getElementById('metahatebu-popover');
        metahatebuPopover.addEventListener('click', ((i = -1) => event => {
            const metabues = document.querySelectorAll('[data-metabu]');
            if (event.target.matches('[data-metabuprev]')) i--;
            if (event.target.matches('[data-metabunext]')) i++;
            i = metabues.length ? (i + metabues.length) % metabues.length : 0;
            metabues[i].scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
            metabues.forEach((metabu, j) => metabu.classList.toggle('focus', i == j));
        })())
    
        // ポップオーバーフォールバック
        const metahatebuPopoverOpen = () => {
            if (HTMLElement.showPopover) {
                metahatebuPopover.showPopover()
            } else {
                metahatebuPopover.show()
            }
        }
    
        // ブクマ展開(x-template再利用)
        const addBookmark = function (targetElement, bookmark, eid) {
            document.querySelector(`[href="/entry/${eid}/comment/${bookmark.user}"]`)?.closest('.entry-comment-contents > .entry-comment-contents').remove();
            targetElement.insertAdjacentHTML(
                'beforeend',
                document.getElementById('autoloader-bookmark-item').textContent
                    .replaceAll('<div class="entry-comment-contents ', '<div data-metabu="metabu" class="entry-comment-contents ')
                    .replaceAll('{{user_name}}', bookmark.user)
                    .replaceAll('{{bookmarked_url}}', bookmark.url)
                    .replaceAll('{{user_page_path}}', `/${bookmark.user}/`)
                    .replaceAll('{{ sort }}', `recent`)
                    .replaceAll('{{profile_image_url}}', `https://cdn.profile-image.st-hatena.com/users/${bookmark.user}/profile.png`)
                    .replaceAll('{{ #is_public }}is-hidden{{ /is_public }}', `is-hidden`)
                    .replaceAll('{{{comment_expanded}}}', bookmark.comment)
                    .replaceAll('{{{tags}}}', `<li>${bookmark.tags.join('<li>')}`)
                    .replaceAll('{{created}}', bookmark.timestamp)
                    .replaceAll('{{comment_page_path}}', `/entry/${eid}/comment/${bookmark.user}`)
                    .replaceAll('{{#should_nofollow}}nofollow{{/should_nofollow}}', 'nofollow')
                    .replaceAll('{{#enable_button}} is-enabled{{/enable_button}}', 'is-enabled')
    
            )
            metahatebuPopoverOpen()
        }
    
        // 2階ブクマAPIコール
        const targetURLs = [location.href], upstairsBookmarksList = []
        for (const targetURL of targetURLs) {
            const { bookmarks, entry_url, eid } = (await (await fetch(`${APIURL}${encodeURIComponent(targetURL)}`)).json()) ?? { entry_url: null }
            if (!entry_url) continue
            targetURLs.push(entry_url)
            upstairsBookmarksList.push([bookmarks, eid])
        }
    
        // 2階ブクマをHTMLに展開
        function addUpstairsBookmark(targetElements) {
            if (upstairsBookmarksList.length) metahatebuPopoverOpen()
            upstairsBookmarksList.forEach(upstairsBookmarks => {
                const [bookmarks, eid] = upstairsBookmarks;
                upstairsBookmarks[0] = bookmarks.filter(bookmark => {
                    const targetUserBookmark = targetElements.find(element => element.dataset?.userName === bookmark.user)
                    if (!targetUserBookmark) {
                        const targetElement = document.getElementById('upStairsSeat');
                        addBookmark(targetElement, bookmark, eid);
                        return true // 遅延読み込み後に再処理するかもしれないのでリストに残す
                    }
                    const targetElement = [targetUserBookmark, ...targetUserBookmark.querySelectorAll(`.entry-comment-contents[data-user-name="${bookmark.user}"]`)].pop()
                    addBookmark(targetElement, bookmark, eid)
                    return false // 遅延読み込み後に再処理しないのでリストから削除する
                })
            })
        }
    
        // メタブをHTMLに展開
        function addMetaBookmark(targetElements) {
            targetElements.forEach(async element => {
                const targetURL = element.querySelector('[data-gtm-label="entry-recent-permalink"]')?.href
                if (!targetURL) return undefined
                const { bookmarks, entry_url, eid } = (await (await fetch(`${APIURL}${encodeURIComponent(targetURL)}`)).json()) ?? { entry_url: null }
                if (!entry_url) return undefined
                bookmarks.forEach(
                    bookmark => {
                        addBookmark(element, bookmark, eid)
                    }
                )
            })
        }
    
        // 初回呼び出し
        addUpstairsBookmark(Array.from(document.querySelectorAll(`.js-bookmarks-recent [data-user-name]`)))
        addMetaBookmark(Array.from(document.querySelectorAll(`.js-bookmarks-recent [data-user-name]`)))
    
        // ブクマ遅延読み込み監視
        const targetNode = document.querySelector('.js-bookmarks-recent');
    
        const upstairsBookmarkMutationConfig = { childList: true };
        const upstairsBookmarkMutationCallback = function (mutationsList, observer) {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    addUpstairsBookmark(Array.from(mutation.addedNodes).filter(node => node.dataset?.userName))
                }
            }
        };
        const upstairsBookmarkMutationObserver = new MutationObserver(upstairsBookmarkMutationCallback);
        upstairsBookmarkMutationObserver.observe(targetNode, upstairsBookmarkMutationConfig);
    
        const metaBookmarkMutationConfig = { childList: true, subtree: true };
        const metaBookmarkMutationCallback = function (mutationsList, observer) {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    addMetaBookmark(Array.from(mutation.addedNodes).filter(node => node.dataset?.userName))
                }
            }
        };
        const metaBookmarkMutationObserver = new MutationObserver(metaBookmarkMutationCallback);
        metaBookmarkMutationObserver.observe(targetNode, metaBookmarkMutationConfig);
    
    })()
    
    
  • Permalink
    このページへの個別リンクです。
    RAW
    書かれたコードへの直接のリンクです。
    Packed
    文字列が圧縮された書かれたコードへのリンクです。
    Userscript
    Greasemonkey 等で利用する場合の .user.js へのリンクです。
    Loader
    @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
    Metadata
    コード中にコメントで @xxx と書かれたメタデータの JSON です。

History

  1. 2024/03/28 12:21:28 - 03/28
  2. 2024/02/13 19:20:41 - 02/13
  3. 2024/02/10 11:23:56 - 02/10
  4. 2023/04/14 19:09:20 - 2023-04-14
  5. 2023/04/13 16:02:01 - 2023-04-13
  6. 2023/04/06 16:06:41 - 2023-04-06
  7. 2023/01/04 15:14:24 - 2023-01-04
  8. 2022/12/28 15:21:54 - 2022-12-28
  9. 2022/12/28 14:33:58 - 2022-12-28
  10. 2022/12/28 14:33:40 - 2022-12-28
  11. 2022/12/27 18:35:10 - 2022-12-27
  12. 2022/12/27 18:28:52 - 2022-12-27
  13. 2022/12/27 18:26:23 - 2022-12-27
  14. 2022/12/27 18:23:32 - 2022-12-27