Clone Turning Page Button in nicovideo

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

As of 2014-09-03. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Clone Turning Page Button in nicovideo
// @namespace   http://pc12.2ch.net/test/read.cgi/streaming/1275642556/
// @version     3.3.1
// @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://greatest.deepsurf.us/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-09-04
 */
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 MozSettingsEvent !== 'undefined') {
		isTargetParent = callbacksForFirefox.isTargetParent || isTargetParent;
		isTarget = callbacksForFirefox.isTarget || 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);
	}
}

})();