Clone Turning Page Button in nicovideo

ニコニコ生放送の検索結果と「放送中の番組」ページにおいて、ページ送りボタンを上部にも表示 / Also shows the pagination on the top of Nico Live search result and Lives by Category page.

目前為 2014-07-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Clone Turning Page Button in nicovideo
// @namespace   http://pc12.2ch.net/test/read.cgi/streaming/1275642556/
// @version     3.3.0
// @description ニコニコ生放送の検索結果と「放送中の番組」ページにおいて、ページ送りボタンを上部にも表示 / Also shows the pagination on the top of Nico Live search result and Lives by Category page.
// @match       http://live.nicovideo.jp/search?*
// @match       http://live.nicovideo.jp/recent
// @match       http://live.nicovideo.jp/recent?*
// @match       http://live.nicovideo.jp/recent#*
// @match       http://watch.live.nicovideo.jp/search?*
// @match       http://watch.live.nicovideo.jp/recent
// @match       http://watch.live.nicovideo.jp/recent?*
// @match       http://watch.live.nicovideo.jp/recent#*
// @run-at      document-start
// @grant       dummy
// @icon        data:image/vnd.microsoft.icon;base64,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
// @author      100の人
// @homepage    https://greasyfork.org/ja/scripts/275-clone-turning-page-button-in-nicovideo
// @license     Creative Commons Attribution 4.0 International Public License; http://creativecommons.org/licenses/by/4.0/
// ==/UserScript==

(function () {
'use strict';

switch (location.pathname) {
	case '/search':
		// ニコニコ生放送 検索結果
		startScript(function () {
			var pager = document.getElementsByClassName('pager')[0];
			if (pager) {
				// 41件以上ヒットしていれば
				// スタイルシートの設定
				document.head.insertAdjacentHTML('beforeend', '<style> \
					.result_list { \
						border-top: 1px solid #888888; \
					} \
				</style>');
				// 数字リンクの複製
				var ref = document.getElementsByClassName('result_list')[0];
				ref.parentElement.insertBefore(pager.cloneNode(true), ref);
			}
		},
				function (parent) { return parent.classList.contains('container'); },
				function (target) { return target.id === 'search_right'; },
				function () { return document.getElementById('search_right'); });
		break;

	case '/recent':
		// ニコニコ生放送 放送中の番組
		startScript(function () {
			var parent = document.getElementById('onair_stream_list');

			clonePager();

			var observer = new MutationObserver(function (mutations) {
				if (mutations[0].addedNodes.length > 1) {
					// 数字リンクの複製でなければ
					clonePager();
				}
			});
			observer.observe(parent, {
				childList: true,
			});

			function clonePager() {
				parent.insertBefore(document.getElementById('pagerFrame').cloneNode(true), document.getElementById('userFrame'));
			}
		},
				function (parent) { return parent.id === 'left'; },
				function (target) { return target.id === 'pickupFrame'; },
				function () { return document.getElementById('pickupFrame'); });
		break;
}



/**
 * 挿入された節の親節が、目印となる節の親節か否かを返すコールバック関数。
 * @callback isTargetParent
 * @param {(Document|Element)} parent
 * @returns {boolean}
 */

/**
 * 挿入された節が、目印となる節か否かを返すコールバック関数。
 * @callback isTarget
 * @param {(DocumentType|Element)} target
 * @returns {boolean}
 */

/**
 * 目印となる節が文書に存在するか否かを返すコールバック関数。
 * @callback existsTarget
 * @returns {boolean}
 */

/**
 * 目印となる節が挿入された直後に関数を実行する。
 * @param {Function} main - 実行する関数。
 * @param {isTargetParent} isTargetParent
 * @param {isTarget} isTarget
 * @param {existsTarget} existsTarget
 * @param {Object} [callbacksForFirefox]
 * @param {isTargetParent} [callbacksForFirefox.isTargetParent] - Firefoxにおける{@link isTargetParent}。
 * @param {isTarget} [callbacksForFirefox.isTarget] - Firefoxにおける{@link isTarget}。
 * @param {boolean} [timeoutSinceStopParsingDocument=0] - DOM構築完了後に監視を続けるミリ秒数。
 * @version 2014-07-06
 */
function startScript(main, isTargetParent, isTarget, existsTarget) {
	/**
	 * {@link checkExistingTarget}で{@link startMain}を実行する間隔(ミリ秒)。
	 * @constant {number}
	 */
	var INTERVAL = 10;
	/**
	 * {@link checkExistingTarget}で{@link startMain}を実行する回数。
	 * @constant {number}
	 */
	var LIMIT = 500;

	/**
	 * 実行済みなら真。
	 * @type {boolean}
	 */
	var alreadyCalled = false;

	// 指定した節が既に存在していれば、即実行
	startMain();
	if (alreadyCalled) {
		return;
	}

	// FirefoxのMutationObserverは、HTMLのDOM構築に関して要素をまとめて挿入したと見なすため、isTargetParent、isTargetを変更
	var callbacksForFirefox = arguments[4];
	if (callbacksForFirefox && typeof sidebar !== 'undefined') {
		if (callbacksForFirefox.isTargetParent) {
			isTargetParent = callbacksForFirefox.isTargetParent;
		}
		if (callbacksForFirefox.isTarget) {
			isTarget = callbacksForFirefox.isTarget;
		}
	}

	var observer = new MutationObserver(mutationCallback);
	observer.observe(document, {
		childList: true,
		subtree: true,
	});

	var timeoutSinceStopParsingDocument = arguments[5] || 0;
	if (document.readyState === 'complete') {
		// DOMの構築が完了していれば
		onDOMContentLoaded();
	} else {
		document.addEventListener('DOMContentLoaded', onDOMContentLoaded);
	}

	/**
	 * {@link startMain}を実行し、スクリプトが開始されていなければさらに{@link timeoutSinceStopParsingDocument}ミリ秒待機し、
	 * スクリプトが開始されていなければ{@link stopObserving}を実行する。
	 */
	function onDOMContentLoaded() {
		startMain();
		if (timeoutSinceStopParsingDocument === 0) {
			if (!alreadyCalled) {
				stopObserving();
			}
		} else {
			window.setTimeout(function () {
				if (!alreadyCalled) {
					stopObserving();
				}
			}, timeoutSinceStopParsingDocument);
		}
	}

	/**
	 * 目印となる節が挿入されたら、監視を停止し、{@link checkExistingTarget}を実行する。
	 * @param {MutationRecord[]} mutations - A list of MutationRecord objects.
	 * @param {MutationObserver} observer - The constructed MutationObserver object.
	 */
	function mutationCallback(mutations, observer) {
		var mutation, target, nodeType, addedNodes, addedNode, i, j, l, l2;
		for (i = 0, l = mutations.length; i < l; i++) {
			mutation = mutations[i];
			target = mutation.target;
			nodeType = target.nodeType;
			if ((nodeType === Node.ELEMENT_NODE) && isTargetParent(target)) {
				// 子が追加された節が要素節で、かつその節についてisTargetParentが真を返せば
				addedNodes = Array.prototype.slice.call(mutation.addedNodes);
				for (j = 0, l2 = addedNodes.length; j < l2; j++) {
					addedNode = addedNodes[j];
					nodeType = addedNode.nodeType;
					if ((nodeType === Node.ELEMENT_NODE) && isTarget(addedNode)) {
						// 追加された子が要素節で、かつその節についてisTargetが真を返せば
						observer.disconnect();
						checkExistingTarget(0);
						return;
					}
				}
			}
		}
	}

	/**
	 * {@link startMain}を実行し、スクリプトが開始されていなければ再度実行。
	 * @param {number} count - {@link startMain}を実行した回数。
	 */
	function checkExistingTarget(count) {
		startMain();
		if (!alreadyCalled && count < LIMIT) {
			window.setTimeout(checkExistingTarget, INTERVAL, count + 1);
		}
	}

	/**
	 * 指定した節が存在するか確認し、存在すれば{@link stopObserving}を実行しスクリプトを開始。
	 */
	function startMain() {
		if (!alreadyCalled && existsTarget()) {
			stopObserving();
			main();
		}
	}

	/**
	 * 監視を停止する。
	 */
	function stopObserving() {
		alreadyCalled = true;
		if (observer) {
			observer.disconnect();
		}
		document.removeEventListener('DOMContentLoaded', onDOMContentLoaded);
	}
}

})();