Greasy Fork

Greasy Fork is available in English.

Google 検索窓を複製

インスタント検索無効時、検索窓をページ下部にも表示 / When Google Instant is disable, also shows the search box to the page bottom.

当前为 2014-07-21 提交的版本,查看 最新版本

// ==UserScript==
// @name        Google 検索窓を複製
// @namespace   http://userscripts.org/users/347021
// @id          google-clone-search-box-347021
// @version     2.2.0
// @description インスタント検索無効時、検索窓をページ下部にも表示 / When Google Instant is disable, also shows the search box to the page bottom.
// @include     https://www.google.tld/search*
// @run-at      document-start
// @grant       dummy
// @icon        data:image/vnd.microsoft.icon;base64,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
// @author      100の人
// @homepage    http://greasyfork.icu/ja/scripts/274-google-%E6%A4%9C%E7%B4%A2%E7%AA%93%E3%82%92%E8%A4%87%E8%A3%BD
// @license     Creative Commons Attribution 4.0 International Public License; http://creativecommons.org/licenses/by/4.0/
// ==/UserScript==

(function() {
'use strict';

polyfill();

// body要素挿入時に実行し、Google検索のバージョンを判別する
var textBoxId, inputNodeId, inputParentNodesClassName, textBoxBorderClass, classOnfocuse, previousSiblingId;
startScript(function() {
	var isTargetParent, isTarget, functionsForFirefox, body = document.body;

	if (body.id) {
		if (location.search.contains('tbm=isch')) {
			// 画像検索ページなら実行しない
			return;
		}
		if (body.getAttribute('marginheight')) {
			// User-AgentがFirefox
			textBoxId = 'tsf';
			inputNodeId = 'lst-ib';
			inputParentNodesClassName = 'lst-d';
			textBoxBorderClass = 'lst-td';
			classOnfocuse = ['lst-d-f'];
		} else {
			// Google Chrome版 (UAがOpera、Google Chrome、IE8以降)
			textBoxId = 'gbqf';
			inputNodeId = 'gbqfq';
			inputParentNodesClassName = 'gbqfqwc';
			textBoxBorderClass = 'gbqfqw';
			classOnfocuse = ['gbqfqwf', 'gsfe_b'];
		}
		previousSiblingId = 'xjs';

		isTargetParent = function (parent) {
			return parent.id === 'foot';
		};
		isTarget = function (target) {
			return target.id === 'xjs';
		};
		functionsForFirefox = {
			isTargetParent: function (parent) {
				return parent.classList.contains('mw');
			},
			isTarget: function (target) {
				var firstElementChild = target.firstElementChild;
				return firstElementChild && firstElementChild.id === 'foot';
			},
		};
	} else {
		// IE7版 (UAがIE7以下、またはJavaScriptが無効)
		textBoxId = 'tsf';
		previousSiblingId = 'nav';
		isTargetParent = function (parent) {
			return parent.id === 'foot';
		};
		isTarget = function (target) {
			return target.id === 'nav';
		};
		functionsForFirefox = {
			isTargetParent: function (parent) {
				return parent.localName === 'tbody' && parent.parentNode.id === 'mn';
			},
			isTarget: function (target) {
				var cells = target.cells;
				return cells && cells[0] && cells[0].id === 'leftnav';
			},
		};
	}
	startScript(main, isTargetParent, isTarget, function() {
		return document.getElementById(previousSiblingId);
	}, functionsForFirefox);
}, function(parent) {
	return parent.localName === 'html';
}, function(target) {
	return target.localName === 'body';
}, function() {
	return document.body;
});

function main() {
	var style, sheet, cssRules, original, previousSibling,
			bottomForm, textBoxBorder, textBoxBorderClassList, inputParentNodes, submitButton, submitButtonClassList;

	// スタイルシートの設定
	document.head.insertAdjacentHTML('beforeend', '<style> \
		#foot form { \
			margin-top: 13px; \
		} \
		\
		#foot > form { \
			margin-bottom: 1em; \
		} \
		\
		/*------------------------------------ \
			Firefox版 \
		*/ \
		#foot .nojsv { \
			display: none; \
		} \
		#foot .tsf-p { \
			width: 631px; \
			padding-left: 8px; \
		} \
		#nav { \
			margin-bottom: initial !important; \
		} \
	</style>');

	// 検索ボックスを取得
	original = document.getElementById(textBoxId);
	if (!original) {
		return;
	}

	// 複製
	bottomForm = original.cloneNode(true);

	// 移動先を取得
	previousSibling = document.getElementById(previousSiblingId);

	// 挿入
	previousSibling.parentNode.insertBefore(bottomForm, previousSibling.nextSibling);

	// ページ描画後のスクリプトによる書き換えを待機
	if (inputParentNodesClassName) {
		inputParentNodes = document.getElementsByClassName(inputParentNodesClassName);
		startScript(function() {
			// 後から挿入された検索窓を複製
			var table = inputParentNodes[0].firstElementChild.cloneNode(true);
			// オートコンプリートを有効に
			table.getElementsByTagName('input')[0].removeAttribute('autocomplete');
			// 下の検索窓を置き換え
			inputParentNodes[1].replaceChild(table, inputParentNodes[1].firstElementChild);
		}, function(parent) {
			return parent.id === 'gs_lc0';
		}, function(target) {
			return target.id === inputNodeId;
		}, function() {
			return document.querySelector('#' + inputNodeId + '[style]');
		});
	}

	// 検索窓にフォーカスが移った時
	if (textBoxBorderClass) {
		textBoxBorder = bottomForm.getElementsByClassName(textBoxBorderClass)[0];
		textBoxBorderClassList = textBoxBorder.classList;
		textBoxBorder.addEventListener('focus', function() {
			DOMTokenList.prototype.add.apply(textBoxBorderClassList, classOnfocuse);
		}, true);

		textBoxBorder.addEventListener('blur', function() {
			DOMTokenList.prototype.remove.apply(textBoxBorderClassList, classOnfocuse);
		}, true);

		// 検索窓をクリックしたとき
		textBoxBorder.addEventListener('click', function(event) {
			if (event.target.localName !== 'input') {
				bottomForm.elements.namedItem('q').focus();
			}
		});
	}

	// 検索窓にマウスが載ったとき
	submitButton = bottomForm.getElementsByClassName('gbqfb')[0];
	if (submitButton) {
		submitButtonClassList = submitButton.classList;
		bottomForm.addEventListener('mouseover', function(event) {
			var target = event.target;

			if (textBoxBorder.contains(target)) {
				// 検索窓
				textBoxBorderClassList.add('gbqfqw-hvr', 'gsfe_a');
			} else if (submitButton.contains(target)) {
				// 検索ボタン
				submitButtonClassList.add('gbqfb-hvr');
			}
		});

		bottomForm.addEventListener('mouseout', function(event) {
			var relatedTarget = event.relatedTarget;

			if (!textBoxBorder.contains(relatedTarget)) {
				// 検索窓
				textBoxBorderClassList.remove('gbqfqw-hvr', 'gsfe_a');
			}
			if (!submitButton.contains(relatedTarget)) {
				// 検索ボタン
				submitButtonClassList.remove('gbqfb-hvr');
			}
		});
	}
}



/**
 * 挿入された節の親節が、目印となる節の親節か否かを返すコールバック関数。
 * @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-21
 */
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);
		}
		document.removeEventListener('DOMContentLoaded', onDOMContentLoaded);
	}

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

/**
 * ECMAScript仕様のPolyfill。
 */
function polyfill() {
	if (!String.prototype.hasOwnProperty('contains')) {
		/**
		 * Determines whether one string may be found within another string, returning true or false as appropriate.
		 * @param {string} searchString - A string to be searched for within this string.
		 * @param {number} [position=0] - The position in this string at which to begin searching for searchString.
		 * @returns {boolean}
		 * @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.contains 21.1.3.6 String.prototype.contains (searchString, position = 0 )}
		 * @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/contains String.contains - JavaScript | MDN}
		 * @version polyfill-2013-11-05
		 * @name String.prototype.contains
		 */
		Object.defineProperty(String.prototype, 'contains', {
			writable: true,
			enumerable: false,
			configurable: true,
			value: function (searchString) {
				return this.indexOf(searchString, arguments[1]) !== -1;
			},
		});
	}

	// Polyfill for Firefox 24 ESR
	if (!('EPSILON' in Number)) {
		// Bug 814014 – implement the new classList specification which permits adding/removing several classes with one call <https://bugzilla.mozilla.org/show_bug.cgi?id=814014>
		var DOMTokenListPrototype = DOMTokenList.prototype;
		var handler = {
			apply: function (addOrRemove, domTokenList, argumentList) {
				for (var i = 0, l = argumentList.length; i < l; i++) {
					addOrRemove.call(domTokenList, argumentList[i]);
				}
			},
		};
		DOMTokenListPrototype.add = new Proxy(DOMTokenListPrototype.add, handler);
		DOMTokenListPrototype.remove = new Proxy(DOMTokenListPrototype.remove, handler);
	}
}

})();