/* jshint esversion: 6 */
// ==UserScript==
// @name popupTitleOverAnchorsOnDMM
// @namespace https://greasyfork.org/ja/users/289387-unagionunagi
// @version 0.3
// @description DMM・FANZAで商品(など)のリンクに(だいたいの)フルタイトルをポップアップ
// @author unagiOnUnagi
// @match *://*.dmm.co.jp/*
// @match *://*.dmm.com/*
// @grant none
// @license GPL-2.0-or-later
// ==/UserScript==
function getTrimText(node) {
return node.textContent.trim().replace(/\s+/g, ' ');
}
function addTitleAttrAll(nodes) {
// a タグに子要素の img の alt から取ってきたタイトルを title 属性として追加
// alt がなかったら a タグの onclick を見てみる
// onclick もダメだったら a タグ内のタイトル文字列をそのまま
// DMM 動画top: altなし、div/div(title, (price)?, sammary)
// 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)
// XPathResult はウ○コ
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 = anchor.getElementsByTagName('img')[0].getAttribute('alt');
if (!title) {
let onclick = anchor.getAttribute('onclick');
if (onclick && onclick.startsWith('_gaq.push') && !onclick.endsWith("'');")) {
title = onclick.split("'")[13];
}
}
if (!title) {
let txt = null;
for (let c of anchor.childNodes) {
if (c.nodeName == '#text') {
txt = c.nodeValue.trim();
if (txt) title = txt;
break;
}
}
if (!title) {
for (let s of anchor.getElementsByTagName('span')) {
txt = getTrimText(s);
if (txt) title = txt;
break;
}
}
if (!title) {
txt = getTrimText(anchor);
if (txt) title = txt;
}
}
if (title) {
anchor.setAttribute('title', title);
// console.log(title);
// console.log(anchor.href);
}
}
}
function addTitleAttr2Rankings() {
// ランキング内商品 anchorに title= 追加
// DMM DVD通販 IdolTop: altがt、タイトルがa直下 (非固定spanのtail)
// FANZA 動画 ビデオtop ranking[1:]: altなし、span(rank) a直下 title br latest
let ranking = document.evaluate(
'.//div[@id="side-rank-tab"]//li[a[img[not(@title)]]]',
document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for ( let i=0 ; i < ranking.snapshotLength; i++ ) {
let li = ranking.snapshotItem(i);
let alt = li.getElementsByTagName('img')[0].getAttribute('alt');
if (!alt || alt == 't') {
alt = getTrimText(li);
}
if (alt) {
for (let a of li.getElementsByTagName('a')) {
a.setAttribute('title', alt);
}
}
}
}
const WAIT_INTERVAL = 200;
const MAX_RETRY_LIMIT = 15;
function waitActualLoading(target, func, arg=null) {
let waitId = null;
let c = 1;
const waiter = function() {
let result = target();
if (result.length || c > MAX_RETRY_LIMIT) {
clearInterval(waitId);
func(arg());
}
c++
}
waitId = setInterval(waiter, WAIT_INTERVAL);
}
function addEvtListener2Carousels(csssels=[['div.prev,div.next', 'div.clist a:not([title])'],
['div.d-prev,div.d-next', 'ul a:not([title])']]) {
// <,>ボタン onclick で商品 anchor に title= を追加するイベントリスナーを追加
csssels.forEach(function(cs) {
document.querySelectorAll(cs[0]).forEach(function(button) {
button.addEventListener('click', function() {
const target = () => button.parentElement.querySelectorAll(cs[1]);
waitActualLoading(target, addTitleAttrAll, target);
}, false);
});
});
}
function addEvtListener2UpdateRecommends() {
// resize イベント後にレコメンド内を更新するイベントリスナーを追加
let timer = 0;
window.addEventListener('resize', function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
let csssel = [['div.prev,div.next', 'div.clist a:not([title])'],
['div.d-prev,div.d-next', 'ul a:not([title])']];
csssel.forEach(function(cs) {
document.querySelectorAll(cs[0]).forEach(function(button) {
const target = () => button.parentElement.querySelectorAll(cs[1]);
waitActualLoading(target, addTitleAttrAll, target);
});
});
}, 200);
}, false);
}
function addTitleAttr2Anchors() {
// とりあえず画像つきリンクに一律処理
addTitleAttrAll(
document.evaluate('.//a[.//img]',
document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null));
// カテゴリトップページランキング
addTitleAttr2Rankings();
// ランキングの日間・週間・月間を onclick で商品 anchor に title= を追加する
// イベントリスナーを追加
// DMM側で伝播を抑止しているので同じ a タグに追加
// こっちでも抑止するとページによっては副作用が起こる
// DMM側で伝播を抑止しているのでこちらのイベントもしない模様?
document.querySelectorAll('.s-tb-capt a:not([title])').forEach(function(tab) {
tab.addEventListener('click', function() {
setTimeout(addTitleAttr2Rankings, 300)
}, false);
});
// <,>ボタン onclick で商品 anchor に title= を追加するイベントリスナーを追加
addEvtListener2Carousels();
// 最近チェックした商品を表示するリンクあたりに onclick で商品 anchor に
// title= を追加するイベントリスナーを追加
let recent = document.getElementById('recent_check_list');
if (recent) {
recent.addEventListener('click', function(e) {
e.stopPropagation();
if (recent.getElementsByClassName('d-modtogglelink-open').length) {
waitActualLoading(
() => recent.getElementsByClassName('d-modtogglelink-close'),
addTitleAttrAll,
() => document.querySelectorAll('div#list-recommend01 a:not([title])'));
}
}, false);
}
// この作品に出演している〜リンクあたりに onclick で
let others = document.getElementById('actorother_main');
if (others) {
others.addEventListener('click', function(e) {
e.stopPropagation();
const targets = () => document.querySelectorAll('#list-recommend07>.clist a:not([title])');
waitActualLoading(
targets,
t => {
addTitleAttrAll(t);
addEvtListener2Carousels([['#list-recommend07>.prev,#list-recommend07>.next',
'div.clist a:not([title])']]);
},
targets);
}, false);
}
addEvtListener2UpdateRecommends();
}
(function() {
const hasElementsAnyId = ids => ids.some(i => document.getElementById(i));
let waitAppear = [];
let waitDisappear = [];
if (hasElementsAnyId(['browserecommend', 'browserecommend1'])) {
waitAppear = ['#list-history-min a img[alt]',];
} else if (hasElementsAnyId(['recommend',])) {
waitAppear = ['#recommend_main li a img[alt]',
'#list-recommend07>div.clist li a img[alt]',
'.d-modtogglelink-close,.d-modtogglelink-open'];
}
if (document.getElementsByClassName('loading').length) waitDisappear = ['li.loading',];
if (waitAppear.length || waitDisappear) {
const isExists = targets => document.querySelectorAll(targets).length;
let waitId = null;
let c = 1;
const waiter = function() {
let isAppeared = waitAppear ? waitAppear.every(t => isExists(t)) : true;
let isDisappeared = waitDisappear ? waitDisappear.every(t => !isExists(t)) : true;
if ((isAppeared && isDisappeared) || c > MAX_RETRY_LIMIT) {
clearInterval(waitId);
setTimeout(addTitleAttr2Anchors, 300);
}
c++
}
waitId = setInterval(waiter, WAIT_INTERVAL);
} else {
addTitleAttr2Anchors();
}
})();