// ==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,AAABAAEAMDAAAAEAIADXCwAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAwAAAAMAgGAAAAVwL5hwAAC55JREFUaIHNmdmTXNV9xz/n3K336ekezWi0IJBkjATGTgyIxeV4icGG2MaqpFIJ5eg5D3lJ8cBD8pS/IDwklUpcocopkqoslB0bV1yUKQULEBisgNBImhlJI2mYtaen9773niUPt7vVEkLqESrjX9Wvunvm9jnf7/ntp+GjIpaWlg52Op1/jOP4tLVW209RjDFhHMcnWq3WX8/Ozu66Dt4r8vzzzwfVavUv4jhe6315eKHbqqOsea2EYXh2ZWXle4AcnPYQ/mBjY+NvisXis0IIYYy5IdnftAghEEJgjGlWKpW/mpyc/FsYYjI7O/uH2Wz2z38bwQNYazHGIKXMZbPZZ48ePXoAwAUolUqFUqn0jO/7OWMM1tpPF+0NxBhDJpPZNTU19W3gvAvw3HPP7RVCHOg/cPvFcrW3foKVrEVKSSaT+drhw4dfdAEnnU5PSykn+g/cDhEfwZusK67zgCU5OLEFkr7v7w2CoOQCbhiGWSGEZ4y5ZQskeBII2kA3TKB1IgvSkvYsSgtaHYtRFqU0OlbEYcRY0WN6RwGjR9vbGIPWOhOGYcYFnHa77RpjhLX2E1mgHcHykuLUezVWlx1cP0WQcxgvC7Ipn42aw/Jim8Zmlc3NJqpt2HNnzDf/YBK2sHePgGi1Wp4LCGOM7AfvrRCQAjbrlmNvwq/f/ZBCLmbHThdByPq6y8qHkM4Zat0c9Y06Rb/G9t1NymNTPPhQnqnpHFppRt25j9MYI12AOI5F33226kJCwlrV4c03Le+/O8eBgym++MA20qkYHEW77fJ/b3d454Sh6VYxcYu42eCb3yiyf69HXvoIklMdlUBy2IY4joU7/MetEhACWh14f85y+mwD39/kvoOfxUPjixS+byhmBeNfK7K02eXErCXlBdSbHd76ZZ29O6fJFD0sFr2FfRMCyftBIbPGYLTGGjOyGm1Y3pCsVTSXlmcpTgiEsAhXkMpJXMfHGpd8Bg49LihNxFjA9dOcer/FxYUq2iiMHn3PgWp9hYDWGqU12iYnMapGWrCy6tJqGqrVNRy/jnC7+CkBQqOFxQKRjrlnm8P+fQ4OLtamqTernJmfIYzCLe2pjUFpTdwjcLULKbUlF4qUoN3RNKoQt7KEtQBhNFJ0MDoNViR+bSUSuO/eAusLktWwSieqs7zk0WpL8tktxl3SEw0TMFirMSbRUcUoQa1mqDZitA5YWbWoCGTog3AxTg+YTSwxnbeUior19S5hXaDCCKVaGJOFkUO4X3PsFQLGgNYWbSzGjL6QMYZz5xusVAROyuPM5Tpr7QblsgDjYaTCWnCQSCye0Ahr6bRDWvUVpspFrN56+yJEglMpdcWFlFJordF6dAsIAY1mm43VLH4mT7NiOXmmzh27A3KEGC0AB4TGooiVR3MjzeL5KoVSjb17x3EdH2M0Wy0/SilgKAupWKF7MTCqOkLx2AM+qlmDMEfBzfPBq3DhPRehI4QyyFBDrIjRnD4lOX3yEiuXzvH00w+xe9cYnqfRevQ9jTGoOEb3CAwsYDEopbDWXqcRu75YKfj8PZIvP2J47RfreCnNesfnRz/bJHC2s2e/xQQxWlvOns3wyv8scnnpLb719CSHDu0nk8puKeYAjLUIIbDDMQAaay3aJC4kpESOwsKA62q+8+0src1FXn2zQjMUfNjOUWlk+dLX0+zeZ1hZGefU0RYXL57mj76/iwcenCTlOriuhzbxlsBbY3Ac5+ospDVobQaKNkgpESOQ0Bo8P+JPjuxn371jHH17jpX1NTL+bvTqBLVAsTBvmJmZ4Ynvljn0aAlXp/FdidZh4sQjxHB/IktEJDj7BKIo+kgr0X/tz6I3E4HiS4+M8/Chh2m02oStgLeOtfjgZJYzF89SizYpb9+NT4AjJVqr3gncHLjtpeE+CiFEUnyHs5DReqAfRTfaqKGVRggopARkYoJch/rCBGGrhOdt8L8/b3D3HWVKY1205oapf/CvofTUfye4MngNstANI7+XXrXWI2QJjdIKqyP2f9bBcZsIfHKpKS5edPjBD9ZYuOQmID5mDT10mB+3j72qEmudBHBPR5GrbWKRDlhhiJVPrRJQ3/RoxYKF5XmaHUXapvGcSc7Nwz/8neX7RyT79ym0sWAZZJURN78miLm1dnpgVBmwcH6CmRnLubk11peqdMKYjorp6g7dqE4Yp8gXp/H9cTbrEf/095s882dj3HMw2nIREwJiFV8hMGy6USuxAJAO1e44s2fTvPVqjQtza5SnFPs+L9m+HfyUJEjniZXHO8c6zH6wiqWO7+do1vL8y7+e5y+f3cNYLtwSiSQjXVUHwBo70BGWAMdjqbGdU2fKvPPaHGurx/n6d+/gwH0uqaCJZ7K4ZJBSg/C5809LfPCu4rVXurTbKfy8y4XFBj975SLPHJ4mjkcvaMPuNiCgt9ALSWGpqmnOXCoxc/IC5069wTNHytz9GRdrLI7J4wYGx23iCQ8pAgIb8egjllzR56cvt9mojSPcCU69d5boqTIWsaV53GgNw2lUjxwDFqTHRsOl2awxe+I8e+5aZ//u+zGqSiaXJghcHCFAwKDoC4G0li/c7xC5eX70H02a9YCNzRWa7TqZdGHk+BNCYJRCMTzQ9FuJm2QhgSVSPir2qS63aW9GTP9uCiFqeE6WwBVI7CDNaSxGREghcIwLUcTdd0n23OVTWYrQ2kUbhTZ69AQiQNnhGNA6GQosN40BIQydLixeyrL2oYNwA5r1EibdRsogWcYmdVMgsAKMUIk1jEWi8YTPWDZH1Nlgz54sruclc/GoLiQZFLhBGtXWDBq6G4kVFm1jZmcaLC8qUrkiK0tdNlp5dpQNMRrHWrASR7pgQVgXIyyYCOsaWs0s65fBxMs89uUxsA7a6JFrgTQSM1yJoygCC8aO0Itrw1hO4zmXadcrZNIecXsbv/xpmmq1jHQ9jLYYbdBKo7XCWoPVBouk3Z3ktWM+J068w+9/K88XDu7AGnrj7GjzQNIfmat7Iduzys38UAgwKuSxhwssXapRWe6QcrexOJfi335Y5XMP+dxzb5Fyvos0MdbGCKMwqsjSossbxyJ+ffIkD3xF8Y0npnCEu6UpsI+1L4MYSObkm9cBK0BpwfbSKn98OMO/v7TG/HwFYadwdYHln2c59kaWwoQhV2gTyBjTsawsOrTqGwSpyzz1VJaHH5pEim7P9+UN97wRi0EMWOxVPcaNpN+LT0w0OHJkmjeOr3J2Zp12U2GMJtxsUanGVGwHaRSFdJrS5AafOxDzxQcnKI8ZoriL7l27iFEGgp4krb3F2KF7oeRuPskb1pqRy7oylkC2ePwrRX7vUYdarU2jtUkYSbQQCBSulRTSgm2TPq7vEscR3W4CpJ91ttIKOY5ECgnmSgwIpdpxGEVRLl/wLUkwjywWVCdCCMvYmKBYTA2BcnuH0kZbQdwd/t6tXeM7wsFxHTrdsB1FkXYBu7Cw1GzWG/Ud0ztyruPSjbsjTWHX8MDo64Gy17x+MvH9ACEkG+uVlUqlEkmA48ePLy+vrFzodrrkcrnB1V3iTr8daozB8zzSqRTNRoOTMzMnKpVK0yGpBb4jRGHnrp2/UyqNB7lclk6nPZRzP31NpQLK5TLdboe5ubnFf37hhRdqtdqs07OtNzc/b/KZbH5q+9RnCoWCUywWBwO9EAIp5W9cHcchCAIKhQLj4+M0Gg3OnTtf++8f//ilt3/1q18Ay26PQAO4/MMXX/xJrVnj8cef+Oqde/ZMTE/vxHWd3vDwcT58baxc5yfV/sctBq6Uyfl2Oh3m5uf14uXLy//5Xy+9/Prrr78CrALN/tISKAN7gQM7d+68/8knnzy0Y3py0vVS3u0KwFsTQRh2u3Nz8wsvv/yTt+v15kngDDDPEAFIakIB2AHcAUwDJSDD7fqV+tbEAE1gDVgGLvZeG4C5FphDArgIbANygM/Q9cunIBoIgU1gA6gBHXr3ef8PS5/7nA7Q79AAAAAASUVORK5CYII=
// @author 100の人
// @homepage https://greasyfork.org/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/
// @downloadURL none
// ==/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', '');
// 検索ボックスを取得
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
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);
}
}
})();