Greasy Fork is available in English.

Google Search Various Ranges

Add more time ranges on Google search.

  1. // ==UserScript==
  2. // @name Google Search Various Ranges
  3. // @name:ja Google Search Various Ranges
  4. // @name:zh-CN Google Search Various Ranges
  5. // @description Add more time ranges on Google search.
  6. // @description:ja Google検索の期間指定の選択肢を増やします。
  7. // @description:zh-CN 增加Google搜索中指定期间的选项。
  8. // @namespace knoa.jp
  9. // @include https://www.google.*/search?*
  10. // @version 3.1.1
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function(){
  15. const SCRIPTID = 'GoogleSearchVariousRanges';
  16. const SCRIPTNAME = 'Google Search Various Ranges';
  17. const DEBUG = false;/*
  18. [update] 3.1.1
  19. Minor fix.
  20.  
  21. [to do]
  22. 画像検索その他でも効くようにしたい
  23. カスタムレンジでレイアウトが崩れる問題(カスタムレンジそもそも不要?)
  24.  
  25. [memo]
  26. 最初と最後は「期間指定なし」と「期間を指定」とみなせる
  27. 中身にaを含むリストアイテムを1つ見つけてまるまるクローンしていく
  28. 既存要素はガン無視してRANGES定義に従って粛々と独自にDOM構築する!
  29. チェックマークもlocation.hrefから判定していくのみ
  30. */
  31. if(window === top && console.time) console.time(SCRIPTID);
  32. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  33. const LANGS = ['en', 'ja', 'fr', 'ru', 'zh', 'es', 'ar'];
  34. const RANGES = {
  35. "qdr:h": {
  36. "qdr:h": ["Past hour", "1 時間以内", "Moins d'une heure", "За час", "过去 1 小时内", "Última hora", "آخر ساعة"],
  37. "qdr:h2": ["Past 2 hours", "2 時間以内", "Moins de 2 heures", "За 2 часа", "过去 2 小时内", "Últimas 2 horas", "آخر ساعتين"],
  38. "qdr:h12": ["Past 12 hours", "12 時間以内", "Moins de 12 heures", "За 12 часов", "过去 12 小时内", "Últimas 12 horas", "آخر ١٢ ساعة"],
  39. },
  40. "qdr:d": {
  41. "qdr:d": ["Past day", "1 日以内", "Moins d'un jour", "За 1 дня", "过去 1 天内", "Último 1 día", "آخر 24 ساعة"],
  42. "qdr:d2": ["Past 2 days", "2 日以内", "Moins de 2 jours", "За 2 дня", "过去 2 天内", "Últimos 2 días", "آخر يومين"],
  43. "qdr:d3": ["Past 3 days", "3 日以内", "Moins de 3 jours", "За 3 дня", "过去 3 天内", "Últimos 3 días", "آخر ٣ أيام"],
  44. },
  45. "qdr:w": {
  46. "qdr:w": ["Past week", "1 週間以内", "Moins d'une semaine", "За неделю", "过去 1 周内", "Última semana", "آخر أسبوع"],
  47. "qdr:w2": ["Past 2 weeks", "2 週間以内", "Moins de 2 semaines", "За 2 недели", "过去 2 周内", "Últimas 2 semanas", "آخر أسبوعين"],
  48. },
  49. "qdr:m": {
  50. "qdr:m": ["Past month", "1 か月以内", "Moins d'un mois", "За месяц", "过去 1 个月内", "Último mes", "آخر شهر"],
  51. "qdr:m2": ["Past 2 months", "2 か月以内", "Moins de 2 mois", "За 2 месяца", "过去 2 个月内", "Últimos 2 meses", "آخر شهرين"],
  52. "qdr:m6": ["Past 6 months", "6 か月以内", "Moins de 6 mois", "За 6 месяца", "过去 6 个月内", "Últimos 6 meses", "آخر ٦ شهور"],
  53. },
  54. "qdr:y": {
  55. "qdr:y": ["Past year", "1 年以内", "Moins d'une an", "За год", "过去 1 年内", "Último año", "آخر سنة"],
  56. "qdr:y2": ["Past 2 years", "2 年以内", "Moins de 2 ans", "За 2 года", "过去 2 年内", "Últimos 2 años", "آخر سنتين"],
  57. "qdr:y5": ["Past 5 years", "5 年以内", "Moins de 5 ans", "За 5 года", "过去 5 年内", "Últimos 5 años", "آخر ٥ سنوات"],
  58. },
  59. };
  60. const PERIODS = [
  61. // You can edit or add below.
  62. //{
  63. // "in '90s": ['1/1/1990', '12/31/1999'],
  64. // "in '00s": ['1/1/2000', '12/31/2009'],
  65. // "in '10s": ['1/1/2010', '12/31/2019'],
  66. //},
  67. //{
  68. // "Before 2000": ['', '12/31/1999'],
  69. // "After 2000" : ['1/1/2000', ''],
  70. //},
  71. ];
  72. const site = {
  73. targets: {
  74. rangeAnchor: () => (location.href.includes('qdr:h')) ? $('a[href*="qdr:d"]') : $('a[href*="qdr:h"]'),
  75. },
  76. hiddenTargets: {/*dropdown parent displaying none*/
  77. dropdown: () => $('#hdtbMenus'),
  78. listParent: () => elements.rangeList.parentNode,
  79. },
  80. get: {
  81. index: () => document.documentElement.lang ? LANGS.indexOf(document.documentElement.lang.split('-')[0]) : 0,
  82. rangeRow: (rangeAnchor) => rangeAnchor.parentNode.parentNode,
  83. rangeList: (rangeRow) => rangeRow.parentNode,
  84. customRange: (rangeList) => rangeList.lastElementChild,
  85. customRangeHref: (href, from, to) => href.replace(/(qdr:)[a-z][0-9]*/, `cdr:1,cd_min:${from},cd_max:${to}`),
  86. rangeAnchors: (rangeList) => rangeList.querySelectorAll('a[href*="qdr:"]'),
  87. },
  88. };
  89. const PADDING = 32 + 32;/*default left+right padding size of each range items*/
  90. let elements = {}, sizes = {};
  91. let core = {
  92. initialize: function(){
  93. elements.html = document.documentElement;
  94. elements.html.classList.add(SCRIPTID);
  95. core.ready();
  96. },
  97. ready: function(){
  98. if(document.hidden) return document.addEventListener('visibilitychange', core.ready, {once: true});
  99. core.getTargets(site.targets, 40, 250).then(() => {
  100. log("I'm ready.");
  101. /* DOM operations */
  102. core.rebuildRanges();
  103. core.addCustomPeriods();
  104. core.calculateWidth();
  105. }).catch(e => {
  106. console.error(`${SCRIPTID}:`, e);
  107. });
  108. },
  109. rebuildRanges: function(){
  110. const index = site.get.index();
  111. const rangeAnchor = elements.rangeAnchor;
  112. const rangeRow = elements.rangeRow = site.get.rangeRow(rangeAnchor);
  113. const rangeList = elements.rangeList = site.get.rangeList(rangeRow);
  114. while(rangeList.children[1] !== rangeList.lastElementChild) rangeList.children[1].remove();/* only the first and the last remain */
  115. rangeList.children[0].dataset.selector = rangeList.children[1].dataset.selector = 'rangeRow';
  116. Object.keys(RANGES).forEach(r => {
  117. const row = rangeRow.cloneNode(true), a = row.querySelector('a');
  118. row.dataset.selector = 'rangeRow';
  119. Object.keys(RANGES[r]).forEach(c => {
  120. const range = rangeAnchor.cloneNode(true);
  121. range.dataset.selector = 'rangeAnchor';
  122. range.href = range.href.replace(/qdr:[hd]/, c);
  123. range.textContent = RANGES[r][c][index];
  124. if(location.href.includes(c + '&')) range.dataset.selected = 'true';
  125. a.parentNode.append(range);
  126. });
  127. a.remove();
  128. rangeList.lastElementChild.before(row);
  129. });
  130. },
  131. addCustomPeriods: function(){
  132. let customRange = site.get.customRange(elements.rangeList);
  133. for(let i = 0; PERIODS[i]; i++){
  134. let line = document.createElement('div');
  135. for(let key in PERIODS[i]){
  136. let a = elements.rangeAnchor.cloneNode(true);
  137. a.href = site.get.customRangeHref(a.href, PERIODS[i][key][0], PERIODS[i][key][1]);
  138. a.textContent = key;
  139. line.appendChild(a);
  140. }
  141. customRange.parentNode.appendChild(line);
  142. }
  143. },
  144. calculateWidth: function(){
  145. /* for calculating width */
  146. core.getTargets(site.hiddenTargets).then(() => {
  147. elements.dropdown.style.visibility = 'hidden';
  148. elements.dropdown.style.display = 'block';
  149. elements.listParent.style.visibility = 'hidden';
  150. elements.listParent.style.display = 'block';
  151. sizes.maxwidth = 0;
  152. /* calculate */
  153. let as = site.get.rangeAnchors(elements.rangeList);
  154. for(let i = 0, a; a = as[i]; i++){
  155. if(sizes.maxwidth < a.offsetWidth) sizes.maxwidth = a.offsetWidth;
  156. }
  157. if(sizes.maxwidth === 0) return setTimeout(core.calculateWidth, 250);
  158. /* restore */
  159. elements.dropdown.style.visibility = '';
  160. elements.dropdown.style.display = '';
  161. elements.listParent.style.visibility = '';
  162. elements.listParent.style.display = 'none';
  163. core.addStyle();
  164. });
  165. },
  166. getTarget: function(selector, retry = 10, interval = 1*SECOND){
  167. const key = selector.name;
  168. const get = function(resolve, reject){
  169. let selected = selector();
  170. if(selected === null || selected.length === 0){
  171. if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
  172. else return reject(new Error(`Not found: ${selector.name}, I give up.`));
  173. }else{
  174. if(selected.nodeType === Node.ELEMENT_NODE) selected.dataset.selector = key;/* element */
  175. else selected.forEach((s) => s.dataset.selector = key);/* elements */
  176. elements[key] = selected;
  177. resolve(selected);
  178. }
  179. };
  180. return new Promise(function(resolve, reject){
  181. get(resolve, reject);
  182. });
  183. },
  184. getTargets: function(selectors, retry = 10, interval = 1*SECOND){
  185. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
  186. },
  187. addStyle: function(name = 'style', d = document){
  188. if(html[name] === undefined) return;
  189. if(d.head){
  190. let style = createElement(html[name]()), id = SCRIPTID + '-' + name, old = d.getElementById(id);
  191. style.id = id;
  192. d.head.appendChild(style);
  193. if(old) old.remove();
  194. }
  195. else{
  196. let observer = observe(d.documentElement, function(){
  197. if(!d.head) return;
  198. observer.disconnect();
  199. core.addStyle(name);
  200. });
  201. }
  202. },
  203. };
  204. const html = {
  205. style: () => `
  206. <style type="text/css">
  207. [data-selector="rangeRow"]:not(:first-child):not(:last-child):hover,
  208. [data-selector="rangeRow"]:not(:first-child):not(:last-child):active{
  209. background-color: transparent;
  210. }
  211. [data-selector="rangeAnchor"]{
  212. display: inline-block !important;
  213. width: ${sizes.maxwidth - PADDING}px !important;
  214. padding-right: 20px !important;
  215. }
  216. [data-selector="rangeAnchor"]:hover,
  217. [data-selector="rangeAnchor"]:active{
  218. background-color: rgba(0,0,0,.1);
  219. }
  220. [data-selector="rangeAnchor"][data-selected="true"]{
  221. background-image: url(//ssl.gstatic.com/ui/v1/menu/checkmark.png);
  222. background-position: left center;
  223. background-repeat: no-repeat;
  224. }
  225. g-menu-item:not(:hover){
  226. background-color: white !important;
  227. }
  228. </style>
  229. `,
  230. };
  231. const $ = function(s, f = undefined){
  232. let target = document.querySelector(s);
  233. if(target === null) return null;
  234. return f ? f(target) : target;
  235. };
  236. const $$ = function(s, f = undefined){
  237. let targets = document.querySelectorAll(s);
  238. return f ? f(targets) : targets;
  239. };
  240. const createElement = function(html = '<div></div>'){
  241. let outer = document.createElement('div');
  242. outer.insertAdjacentHTML('afterbegin', html);
  243. return outer.firstElementChild;
  244. };
  245. const log = function(){
  246. if(typeof DEBUG === 'undefined') return;
  247. let l = log.last = log.now || new Date(), n = log.now = new Date();
  248. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  249. //console.log(error.stack);
  250. console.log(
  251. SCRIPTID + ':',
  252. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  253. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  254. /* :00 */ ':' + line,
  255. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  256. /* caller */ (callers[1] || '') + '()',
  257. ...arguments
  258. );
  259. };
  260. log.formats = [{
  261. name: 'Firefox Scratchpad',
  262. detector: /MARKER@Scratchpad/,
  263. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  264. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  265. }, {
  266. name: 'Firefox Console',
  267. detector: /MARKER@debugger/,
  268. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  269. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  270. }, {
  271. name: 'Firefox Greasemonkey 3',
  272. detector: /\/gm_scripts\//,
  273. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  274. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  275. }, {
  276. name: 'Firefox Greasemonkey 4+',
  277. detector: /MARKER@user-script:/,
  278. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  279. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  280. }, {
  281. name: 'Firefox Tampermonkey',
  282. detector: /MARKER@moz-extension:/,
  283. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 2,
  284. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  285. }, {
  286. name: 'Chrome Console',
  287. detector: /at MARKER \(<anonymous>/,
  288. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  289. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  290. }, {
  291. name: 'Chrome Tampermonkey',
  292. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
  293. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 1,
  294. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  295. }, {
  296. name: 'Chrome Extension',
  297. detector: /at MARKER \(chrome-extension:/,
  298. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  299. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  300. }, {
  301. name: 'Edge Console',
  302. detector: /at MARKER \(eval/,
  303. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  304. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  305. }, {
  306. name: 'Edge Tampermonkey',
  307. detector: /at MARKER \(Function/,
  308. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  309. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  310. }, {
  311. name: 'Safari',
  312. detector: /^MARKER$/m,
  313. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  314. getCallers: (e) => e.stack.split('\n'),
  315. }, {
  316. name: 'Default',
  317. detector: /./,
  318. getLine: (e) => 0,
  319. getCallers: (e) => [],
  320. }];
  321. log.format = log.formats.find(function MARKER(f){
  322. if(!f.detector.test(new Error().stack)) return false;
  323. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  324. return true;
  325. });
  326. core.initialize();
  327. if(window === top) console.timeEnd(SCRIPTNAME);
  328. })();