popupTitleOverAnchorsOnDMM

DMM・FANZAで商品(など)のリンクに(だいたいの)フルタイトルをポップアップ

As of 2020-09-24. See the latest version.

/* jshint esversion: 6 */
// ==UserScript==
// @name         popupTitleOverAnchorsOnDMM
// @namespace    https://greasyfork.org/ja/users/289387-unagionunagi
// @description  DMM・FANZAで商品(など)のリンクに(だいたいの)フルタイトルをポップアップ
// @author       unagiOnUnagi
// @match        *://*.dmm.co.jp/*
// @match        *://*.dmm.com/*
// @grant        none
// @license      GPL-2.0-or-later
// @version      0.6.5.1
// ==/UserScript==

function getTrimText(node) {
    return node.textContent.trim().replace(/\s+/g, ' ');
}

function getTitle(anchor) {
    // タイトルの取得
    // alt がなかったら a タグの onclick を見てみる
    // onclick もダメだったら a タグ内のタイトル文字列をそのまま
    //  DMM 動画top: altなし、div/div(title, (price)?, summary)
    //        VR top: altなし、span(title, price)
    //        Idol top: altなし、span(title, price)
    //        IdolCh top: altなし、a直下
    //      見放題ch top: altなし、a直下
    //  FANZA 動画top pickup: altなし、span(title, maker, price)
    let title = anchor.getElementsByTagName('img')[0].getAttribute('alt');
    if (title) return title;

    let nameNode = anchor.querySelector('.mainListLinkWork__txt,.responsive-name,.c-product-card__ttl,.bx-title');
    if (nameNode) {
        if ((title = getTrimText(nameNode))) return title;
    }

    let onclick = anchor.getAttribute('onclick');
    if (onclick && onclick.startsWith('_gaq.push') && !onclick.endsWith("'');")) {
        if ((title = onclick.split("'")[13])) return title;
    }

    for (let c of anchor.childNodes) {
        if (c.nodeName == '#text') {
            if ((title = c.nodeValue.trim())) return title;
        }
    }

    for (let s of anchor.getElementsByTagName('span')) {
        let txt = getTrimText(s);
        if (txt) {
            title = txt;
            break;
        }
    }
    if (!title || title == 't') {
        if ((title = getTrimText(anchor))) return title;
    }
    return title;
}

function addTitleAttrAll(nodes, verbose=false) {
    // a タグに子要素の img の alt から取ってきたタイトルを title 属性として追加
    // ランキングのところは別
    let length = nodes.length || nodes.snapshotLength;
    let getItem = nodes.item || nodes.snapshotItem;
    getItem = getItem.bind(nodes);

    for ( let i=0 ; i < length; i++ ) {
        let anchor = getItem(i);
        let title = getTitle(anchor);

        if (title) {
            anchor.title = title;
            if (verbose) {
            }
        }
    }
}

function addTitleAttr2Rankings(target) {
    // ランキング内商品 anchorに title= 追加
    // DMM DVD通販 IdolTop: altがt、タイトルがa直下 (非固定spanのtail)
    // FANZA 動画 ビデオtop ranking[1:]: altなし、span(rank) a直下 title br latest
    let prefix = '.';
    if (!target) {
        prefix = './/div[@id="side-rank-tab"]';
        target = document.body;
    }
    const ranking = document.evaluate(
        prefix + '//li[div[a[not(@title) or @title=""]]]',
        target, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    const rankingLength = ranking.snapshotLength;
    for ( let i=0 ; i < rankingLength; i++ ) {
        let li = ranking.snapshotItem(i);
        let alt = li.getElementsByTagName('img')[0].getAttribute('alt');
        if (!alt || alt == 't') alt = getTrimText(li);
        let textEl = li.querySelector('div > a');
        if (textEl) textEl.title = alt;
    }
}

function createObserver(target, callback) {
    // MutationObserver の作成
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => callback(mutation));
    });
    for (let t of target()) {
        observer.observe(t, { childList: true, subtree: true });
    }
}

function addTitleAttr2Anchors() {

    // とりあえず title のない画像つきリンクに一律処理
    addTitleAttrAll(
        document.evaluate('.//a[.//img and not(@title)]',
                          document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null));

    // カテゴリトップページランキング
    // 最初に一発やっとかないとあとで更新されないページがある
    addTitleAttr2Rankings();

    // ページの動的変更を監視
    // '.clist,#recent_check_list,#actorother_main,.d-boxslidelist,#recommend_parts,div[id^="list-product"],.mainList'),
    createObserver(
        () => document.querySelectorAll('body'),
        mutation => {
            let target = mutation.target
            addTitleAttrAll(document.evaluate(
                './/a[.//img and not(@title)]',
                target, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
                           );
            if (target.id.match(/(ranking|s-tb-sect)/)) {
                addTitleAttr2Rankings(target);
            }
        }
    );

}


(function() {
    // load 後に追加される要素を待ってから

    let waitAppear = [];
    let waitDisappear = [];

    if (document.querySelector('#browserecommend,#browserecommend1')) {
        waitAppear = ['#list-history-min a img[alt]',];
    } else if (document.querySelector('#recommend')) {
        waitAppear = ['#recommend_main li a img[alt]',
                      '.d-modtogglelink-close,.d-modtogglelink-open'];
    } else if (document.querySelector('#recommend_all')) {
        waitAppear = ['#recent_check_list',];
    }

    if (document.getElementsByClassName('loading').length) {
        waitDisappear = ['.loading',];
    }

    if (waitAppear.length || waitDisappear.length) {
        // 要素が追加されない場合もあるので setInterval でタイムアウトするように

        const isExists = targets => document.querySelectorAll(targets).length;

        let waitId = null;
        let c = 1;
        const waiter = () => {
            let isAppeared = waitAppear.length ? waitAppear.every(t => isExists(t)) : true;
            let isDisappeared = waitDisappear.length ? waitDisappear.every(t => !isExists(t)) : true;
            if ((isAppeared && isDisappeared) || c > 15) {
                clearInterval(waitId);
                setTimeout(addTitleAttr2Anchors, 300);
            }
            c++;
        }
        waitId = setInterval(waiter, 200);

    } else {
        addTitleAttr2Anchors();
    }

})();