Google 検索窓を複製

Also shows the search box to the page bottom.

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 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        Google 検索窓を複製
// @name:ja     Google 検索窓を複製
// @description Also shows the search box to the page bottom.
// @description:ja 検索窓をページ下部にも表示します。
// @namespace   http://userscripts.org/users/347021
// @version     3.0.2
// @include     https://www.google.*/*
// @include     https://www.google.*/?*
// @include     https://www.google.*/#*
// @include     https://www.google.*/webhp
// @include     https://www.google.*/webhp?*
// @include     https://www.google.*/webhp#*
// @include     https://www.google.*/search*
// @include     https://www.google.*/search?*
// @include     https://www.google.*/search#*
// @exclude     https://www.google.*/search?*tbm=isch*
// @require     https://greasyfork.org/scripts/17895/code/polyfill.js?version=189394
// @require     https://greasyfork.org/scripts/19616/code/utilities.js?version=230651
// @require     https://greasyfork.org/scripts/17896/code/start-script.js?version=112958
// @license     Mozilla Public License Version 2.0 (MPL 2.0); https://www.mozilla.org/MPL/2.0/
// @compatible  Edge 非推奨 / Deprecated
// @compatible  Firefox
// @compatible  Opera
// @compatible  Chrome
// @grant       dummy
// @run-at      document-start
// @icon        data:image/vnd.microsoft.icon;base64,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
// @author      100の人
// @homepage    https://greasyfork.org/scripts/274
// ==/UserScript==

(function () {
'use strict';

class GoogleBottomSearchBox
{
	/**
	 * messageイベントで使用する識別子。
	 * @constant {string}
	 */
	static get ID() {return 'google-bottom-search-box-137';}

	constructor()
	{
		startScript(
			() => {
				if (document.querySelector('#csi + script, #csi + a')) {
					// インスタント検索が有効
					this.main();
				} else if (location.pathname === '/search') {
					// インスタント検索が無効
					this.mainWithoutInstant();
				}
			},
			parent => parent.id === 'viewport' || /* インスタンス検索が無効 */ parent === document.body,
			target => target.id === 'main',
			() => document.getElementById('main')
		);
	}

	/**
	 * @param {Event} event
	 */
	handleEvent(event)
	{
		switch (event.type) {
			case 'focus':
				event.target.closest('#sfdiv').classList.add('sbfcn');
				break;
			case 'blur':
				event.target.closest('#sfdiv').classList.remove('sbfcn');
				break;
			case 'mouseup':
				if (event.target.name !== 'q') {
					event.target.closest('form').q.focus();
				}
				break;
			case 'hashchange':
			case 'message':
				if (event.type === 'hashchange'
					|| event.origin === location.origin && typeof event.data === 'object' && event.data !== null
						&& event.data.id === GoogleBottomSearchBox.ID) {
					if (this.getTbm(location) === this.getTbm(new URL(event.oldURL || event.data.oldURL))) {
						document.querySelector('#foot ~ form [name="q"]').value
							= new URLSearchParams(location.hash.replace('#', '')).get('q') || '';
					}
				}
				break;
		}
	}

	/**
	 * @access protected
	 */
	main()
	{
		this.observeFooterInserting(() => {
			if (!document.querySelector('#foot ~ form [name="q"]')) {
				this.cloneForm();
				this.synchronizeSearchWord();
				this.waitSearchControl().then(() => this.setEventListeners());
			}
		});
	}

	/**
	 * URLのtbmパラメータを取得します。
	 * @access protected
	 * @param {(Location|URL)} url
	 * @returns {string}
	 */
	getTbm(url)
	{
		return new URLSearchParams(url.hash.replace('#', '')).get('tbm')
			|| new URLSearchParams(url.search).get('tbm') || '';
	}

	/**
	 * ページにフッタが挿入されるのを監視します。
	 * @access protected
	 * @param {Function} callback - フッタが挿入されるたびに呼び出されるコールバック関数。
	 * @returns {Promise.<void>}
	 */
	observeFooterInserting(callback)
	{
		new MutationObserver(function (mutations, observer) {
			mutations: for (const mutation of mutations) {
				for (const node of mutation.addedNodes) {
					if (node.id === 'cnt' || node.id === 'foot') {
						callback();
						break mutations;
					}
				}
			}
		}).observe(document.getElementById('main'), {childList: true, subtree: true});
	}

	/**
	 * フォームを複製して挿入します。
 	 * @access protected
	 */
	cloneForm()
	{
		document.head.insertAdjacentHTML('beforeend', `<style>
			.hp #foot ~ form {
				/* トップページから完全に切り替わるまではフォームを表示しない */
				display: none;
			}
			#foot ~ form {
				margin-bottom: 1em;
			}
			:not(.mw) > * > #foot ~ form .tsf-p {
				padding-left: 8px;
			}
			#foot ~ form #sfdiv:hover {
				box-shadow: 0 3px 8px 0 rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.08);
			}
		</style>`);

		const bottomForm = document.getElementById('tsf').cloneNode(true);
		bottomForm.querySelector('#logocont').remove();
		document.getElementById('foot').after(bottomForm);
	}

	/**
	 * テキスト入力欄が置換されるのを待機し、実行時点で置換されて居れなければ、複製して置換します。
 	 * @access protected
 	 * @returns {Promise.<void>}
	 */
	waitSearchControl()
	{
		return new Promise(function (resolve) {
			if (document.getElementsByClassName('sbib_a')[0]) {
				resolve();
			} else {
				const ancestors = document.getElementsByClassName('lst-c');
				new MutationObserver((mutations, observer) => {
					observer.disconnect();
					const clone = ancestors[0].cloneNode(true);
					clone.getElementsByTagName('input')[0].removeAttribute('autocomplete');
					ancestors[1].replaceWith(clone);
					resolve();
				}).observe(ancestors[0], {childList: true});
			}
		});
	}

	/**
	 * フォーカス時とクリック時のイベントリスナーを設定します。
 	 * @access protected
	 */
	setEventListeners()
	{
		const form = document.querySelector('#foot ~ form');
		// 検索窓にフォーカスが移った時
		const q = form.q;
		q.addEventListener('focus', this);
		q.addEventListener('blur', this);
		// 検索窓をクリックしたとき
		form.getElementsByClassName('sbib_a')[0].addEventListener('mouseup', this);
	}

	/**
	 * 疑似ページ移動時、複製した検索窓に検索語句を反映します。
 	 * @access protected
	 */
	synchronizeSearchWord()
	{
		if (!this.alreadyObserved) {
			this.alreadyObserved = true;
			GreasemonkeyUtils.executeOnUnsafeContext(function (id) {
				History.prototype.pushState = new Proxy(History.prototype.pushState, {
					apply(target, thisArg, argumentsList)
					{
						const oldURL = location.href;
						const returnValue = Reflect.apply(target, thisArg, argumentsList);
						window.postMessage({id, oldURL}, location.origin);
						return returnValue;
					},
				});
			}, [GoogleBottomSearchBox.ID]);
			window.addEventListener('message', this);
			window.addEventListener('hashchange', this);
		}
	}

	/**
	 * インスタント検索無効時の処理。
 	 * @access protected
	 */
	mainWithoutInstant()
	{
		// body要素挿入時に実行し、Google検索のバージョンを判別する
		let textBoxId, inputNodeId, inputParentNodesClassName, textBoxBorderClass, classOnfocuse, previousSiblingId;

		let isTargetParent, isTarget, functionsForFirefox;
		if (document.body.id) {
			if (document.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 = parent => parent.id === 'foot';
			isTarget = target => target.id === 'xjs';
			functionsForFirefox = {
				isTargetParent: parent => parent.classList.contains('mw'),
				isTarget(target)
				{
					const firstElementChild = target.firstElementChild;
					return firstElementChild && firstElementChild.id === 'foot';
				},
			};
		} else {
			// IE7版 (UAがIE7以下、またはJavaScriptが無効)
			textBoxId = 'tsf';
			previousSiblingId = 'nav';
			isTargetParent = parent => parent.id === 'foot';
			isTarget = target => target.id === 'nav';
			functionsForFirefox = {
				isTargetParent: parent => parent.localName === 'tbody' && parent.parentNode.id === 'mn',
				isTarget(target)
				{
					const cells = target.cells;
					return cells && cells[0] && cells[0].id === 'leftnav';
				},
			};
		}

		startScript(
			function () {
				// スタイルシートの設定
				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>`);

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

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

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

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

				let textBoxBorder, textBoxBorderClassList, inputParentNodes, submitButtonClassList;

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

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

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

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

				// 検索窓にマウスが載ったとき
				const submitButton = bottomForm.getElementsByClassName('gbqfb')[0];
				if (submitButton) {
					submitButtonClassList = submitButton.classList;
					bottomForm.addEventListener('mouseover', function (event) {
						if (textBoxBorder.contains(event.target)) {
							// 検索窓
							textBoxBorderClassList.add('gbqfqw-hvr', 'gsfe_a');
						} else if (submitButton.contains(event.target)) {
							// 検索ボタン
							submitButtonClassList.add('gbqfb-hvr');
						}
					});

					bottomForm.addEventListener('mouseout', function (event) {
						if (!textBoxBorder.contains(event.relatedTarget)) {
							// 検索窓
							textBoxBorderClassList.remove('gbqfqw-hvr', 'gsfe_a');
						}
						if (!submitButton.contains(event.relatedTarget)) {
							// 検索ボタン
							submitButtonClassList.remove('gbqfb-hvr');
						}
					});
				}
			},
			isTargetParent,
			isTarget,
			() => document.getElementById(previousSiblingId),
			functionsForFirefox
		);
	}
}

new GoogleBottomSearchBox();

})();