🏠 Home 

tab scholar & youtube

ajout tab scholar & youtube

  1. // ==UserScript==
  2. // @name tab scholar & youtube
  3. // @namespace https://google.com/
  4. // @version 1.1
  5. // @description ajout tab scholar & youtube
  6. // @homepage https://greasyfork.org/fr/scripts/35115-tab-scholar-youtube
  7. // @homepageURL https://gist.github.com/Okaido53/4c6dd2915a54b29797193ef5a5d4c269
  8. // @supportURL https://productforums.google.com/forum/#!home
  9. // @contributionURL https://www.paypal.com/
  10. // @icon https://icons.duckduckgo.com/ip2/google.com.ico
  11. // @copyright Okaïdo53
  12. // @author Okaïdo53
  13. // @secure Okaïdo53
  14. // @license GPL v3
  15. // @compatible firefox
  16. // @compatible chrome
  17. // @compatible opera
  18. // @compatible Safari
  19. // @match http://*/*
  20. // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js
  21. // @homepage https://github.com/jmlntw/google-search-region
  22. // @supportURL https://github.com/jmlntw/google-search-region/issues
  23. // @include http://www.google.com/search?*q=*
  24. // @include https://www.google.com/search?*q=*
  25. // @include http*://google.*
  26. // @include http*://www.google.*
  27. // @include https://encrypted.google.*
  28. // @include https://www.google.*/search?*
  29. // @include https://www.google.*/webhp?*
  30. // @include https://encrypted.google.com/search?*
  31. // @include https://encrypted.google.com/webhp?*
  32. // @grant none
  33. // @grant unsafeWindow
  34. // @grant GM_getValue
  35. // @grant GM_setValue
  36. // @grant GM_addStyle
  37. // @grant GM_getResourceText
  38. // @grant GM_xmlhttpRequest
  39. // @grant GM_registerMenuCommand
  40. // @noframes
  41. // @run-at document-end
  42. // ==/UserScript==
  43. (function () {
  44. var videosLink = document.querySelector('#hdtb-msb .hdtb-mitem a[href*="&tbm=vid"]'),
  45. intv;
  46. function changeLink() {
  47. if (videosLink.firstChild.data === 'YouTube' && videosLink.href.indexOf('youtube.com') !== -1) {
  48. return window.clearInterval(intv);
  49. }
  50. // change the link's text
  51. videosLink.firstChild.data = 'YouTube';
  52. // change the link's url
  53. videosLink.href = 'https://www.youtube.com/results?search_query=' + location.href.match(/[?&]?q=([^&]*)/)[1];
  54. }
  55. // make sure the page is not in a frame
  56. // and if there is a "Videos" link
  57. if (window.frameElement || window !== window.top || !videosLink) { return; }
  58. // change the link's text
  59. // keep changing it until it actually changes... sometimes it doesn't work right away
  60. intv = window.setInterval(changeLink, 0);
  61. }());
  62. var scholarUrl = 'https://scholar.google.com/scholar?q=';
  63. var scholarEleId = 'hdtb-us-scholar';
  64. var appendEleId = 'hdtb-msb-vis';
  65. var createScholarElement = function() {
  66. var wrapper = document.createElement('div');
  67. wrapper.id = scholarEleId;
  68. wrapper.classList.add('hdtb-mitem');
  69. wrapper.classList.add('hdtb-imb');
  70. var anchor = document.createElement('a');
  71. var anchorClasses = anchor.classList;
  72. anchorClasses.add('q');
  73. anchorClasses.add('qs');
  74. anchor.textContent = 'Scholar';
  75. wrapper.appendChild(anchor);
  76. return wrapper;
  77. };
  78. var getSearchQuery = function(href) {
  79. var results = /[\\?&]q=([^&#]*)/.exec(href);
  80. return (results) ? results[1] : '';
  81. };
  82. var updateScholarHref = function(wrapper, scholorEle) {
  83. var otherHref = wrapper.querySelector('a').getAttribute('href');
  84. var query = getSearchQuery(otherHref);
  85. var anchor = scholorEle.firstChild;
  86. anchor.setAttribute('href', scholarUrl + query);
  87. };
  88. var addScholarLink = function() {
  89. var wrapper = document.getElementById(appendEleId);
  90. if (wrapper) {
  91. var scholarEle = createScholarElement();
  92. updateScholarHref(wrapper, scholarEle);
  93. wrapper.appendChild(scholarEle);
  94. }
  95. };
  96. var watchScholarLink = function() {
  97. // Whenever the query changes without changing the window href, our node
  98. // is removed, so use a MutationObserver to update and put us back.
  99. new MutationObserver(function(mutations) {
  100. var len = mutations.length;
  101. for (var i = 0; i < len; i++) {
  102. // Normally the link bar is removed then added, along
  103. // with search results, so just check additions.
  104. if (mutations[i].addedNodes) {
  105. if (!document.getElementById(scholarEleId)) {
  106. addScholarLink();
  107. }
  108. break;
  109. }
  110. }
  111. }).observe(document.body, {'childList': true, 'subtree': true});
  112. };
  113. addScholarLink();
  114. watchScholarLink();
  115. // =============================================================================
  116. // Add compatibility between the Greasemonkey 4 APIs and existing/legacy APIs.
  117. // =============================================================================
  118. if (typeof GM === 'undefined') {
  119. // eslint-disable-next-line no-global-assign
  120. GM = {
  121. getValue: (...args) => Promise.resolve(GM_getValue.apply(this, args)),
  122. setValue: (...args) => Promise.resolve(GM_setValue.apply(this, args))
  123. }
  124. }
  125. // eslint-disable-next-line camelcase
  126. function GM_addStyle (css) {
  127. const style = document.createElement('style')
  128. style.type = 'text/css'
  129. style.textContent = css
  130. document.head.appendChild(style)
  131. return style
  132. }
  133. // eslint-disable-next-line camelcase
  134. GM.addStyle = GM_addStyle
  135. // =============================================================================
  136. // Helper Functions
  137. // =============================================================================
  138. /**
  139. * @param {string} selector
  140. * @param {Element} [context]
  141. * @return {Element}
  142. */
  143. function $ (selector, context) {
  144. return (context || document).querySelector(selector)
  145. }
  146. /**
  147. * @param {string} selector
  148. * @param {Element} [context]
  149. * @return {NodeListOf<Element>}
  150. */
  151. function $$ (selector, context) {
  152. return (context || document).querySelectorAll(selector)
  153. }
  154. /**
  155. * @param {Element} target
  156. * @param {string} type
  157. * @param {EventListener} callback
  158. * @param {boolean} [useCapture]
  159. */
  160. function $on (target, type, callback, useCapture) {
  161. target.addEventListener(type, callback, !!useCapture)
  162. }
  163. /**
  164. * @param {Element} target
  165. * @param {string} selector
  166. * @param {string} type
  167. * @param {EventListener} callback
  168. */
  169. function $delegate (target, selector, type, callback) {
  170. const useCapture = (type === 'blur') || (type === 'focus')
  171. const dispatchEvent = function dispatchEvent (event) {
  172. if (event.target.matches(selector)) { callback.call(event.target, event) }
  173. }
  174. $on(target, type, dispatchEvent, useCapture)
  175. }
  176. if (window.NodeList && !window.NodeList.prototype.forEach) {
  177. window.NodeList.prototype.forEach = Array.prototype.forEach
  178. }
  179. // =============================================================================
  180. // Template Engine
  181. // =============================================================================
  182. /**
  183. * @param {string} text
  184. * @param {Object} data
  185. * @return {string}
  186. */
  187. function renderTemplate (text, data) {
  188. const matcher = /<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
  189. const escapeChar = function escapeChar (text) {
  190. return text
  191. .replace(/\\/g, '\\\\')
  192. .replace(/'/g, "\\'")
  193. .replace(/\r/g, '\\r')
  194. .replace(/\n/g, '\\n')
  195. .replace(/\u2028/g, '\\u2028')
  196. .replace(/\u2029/g, '\\u2029')
  197. }
  198. const escape = function escape (text) {
  199. return ('' + text)
  200. .replace(/&/g, '&amp;')
  201. .replace(/</g, '&lt;')
  202. .replace(/>/g, '&gt;')
  203. .replace(/"/g, '&quot;')
  204. .replace(/'/g, '&#x27;')
  205. .replace(/`/g, '&#x60;')
  206. }
  207. let index = 0
  208. let source = "__p += '"
  209. text.replace(matcher, (match, escape, interpolate, evaluate, offset) => {
  210. source += escapeChar(text.slice(index, offset))
  211. index = offset + match.length
  212. if (escape) {
  213. source += `' + ((__t = (${escape})) == null ? '' : escape(__t)) + '`
  214. } else if (interpolate) {
  215. source += `' + ((__t = (${interpolate})) == null ? '' : __t) + '`
  216. } else if (evaluate) {
  217. source += `'; ${evaluate} __p += '`
  218. }
  219. return match
  220. })
  221. source += "';"
  222. source = `
  223. let __t, __p = '';
  224. const __j = Array.prototype.join;
  225. const print = function print () { __p += __j.call(arguments, ''); };
  226. with (data || {}) { ${source} }
  227. return __p;
  228. `
  229. try {
  230. // eslint-disable-next-line no-new-func
  231. return new Function('data', 'escape', source).call(this, data, escape)
  232. } catch (err) {
  233. err.source = source
  234. throw err
  235. }
  236. }
  237. // =============================================================================
  238. // User Script Configuration
  239. // =============================================================================
  240. /**
  241. * @typedef {Object} Config
  242. * @property {boolean} setTLD
  243. * @property {boolean} setHl
  244. * @property {boolean} setGl
  245. * @property {boolean} setCr
  246. * @property {boolean} setLr
  247. * @property {boolean} showFlags
  248. * @property {Array<string>} userRegions
  249. */
  250. /**
  251. * @type {Config}
  252. */
  253. const config = Object.seal({
  254. setTLD: true,
  255. setHl: true,
  256. setGl: true,
  257. setCr: false,
  258. setLr: false,
  259. showFlags: true,
  260. userRegions: ['wt-wt', 'jp-ja', 'tw-zh', 'us-en']
  261. })
  262. /**
  263. * @return {Promise<Config>}
  264. */
  265. function loadConfig () {
  266. return GM.getValue('config')
  267. .then(value => {
  268. try { return JSON.parse(value) } catch (err) { return {} }
  269. })
  270. .then(value => {
  271. return Object.assign(config, value)
  272. })
  273. }
  274. /**
  275. * @return {Promise<Config>}
  276. */
  277. function saveConfig () {
  278. return GM.setValue('config', JSON.stringify(config))
  279. }
  280. // =============================================================================
  281. // Search Regions
  282. // =============================================================================
  283. /**
  284. * @typedef {Object} Region
  285. * @property {string} id
  286. * @property {string} name
  287. * @property {string} [tld]
  288. * @property {string} [country]
  289. * @property {string} [lang]
  290. */
  291. /**
  292. * @type {ReadonlyArray<Region>}
  293. */
  294. const regions = Object.freeze([
  295. {id: 'wt-wt', name: 'All Regions', tld: 'com'},
  296. {id: 'ar-es', name: 'Argentina', tld: 'com.ar', country: 'ar', lang: 'es'},
  297. {id: 'au-en', name: 'Australia', tld: 'com.au', country: 'au', lang: 'en'},
  298. {id: 'at-de', name: 'Austria', tld: 'at', country: 'at', lang: 'de'},
  299. {id: 'be-fr', name: 'Belgium (fr)', tld: 'be', country: 'be', lang: 'fr'},
  300. {id: 'be-nl', name: 'Belgium (nl)', tld: 'be', country: 'be', lang: 'nl'},
  301. {id: 'br-pt', name: 'Brazil', tld: 'com.br', country: 'br', lang: 'pt'},
  302. {id: 'bg-bg', name: 'Bulgaria', tld: 'bg', country: 'bg', lang: 'bg'},
  303. {id: 'ca-en', name: 'Canada', tld: 'ca', country: 'ca', lang: 'en'},
  304. {id: 'ca-fr', name: 'Canada (fr)', tld: 'ca', country: 'ca', lang: 'fr'},
  305. {id: 'ct-ca', name: 'Catalonia', tld: 'cat', country: 'ct', lang: 'ca'},
  306. {id: 'cl-es', name: 'Chile', tld: 'cl', country: 'cl', lang: 'es'},
  307. {id: 'cn-zh', name: 'China', tld: 'com.hk', country: 'cn', lang: 'zh-cn'},
  308. {id: 'co-es', name: 'Colombia', tld: 'com.co', country: 'co', lang: 'es'},
  309. {id: 'hr-hr', name: 'Croatia', tld: 'hr', country: 'hr', lang: 'hr'},
  310. {id: 'cz-cs', name: 'Czech Republic', tld: 'cz', country: 'cz', lang: 'cs'},
  311. {id: 'dk-da', name: 'Denmark', tld: 'dk', country: 'dk', lang: 'da'},
  312. {id: 'ee-et', name: 'Estonia', tld: 'ee', country: 'ee', lang: 'et'},
  313. {id: 'fi-fi', name: 'Finland', tld: 'fi', country: 'fi', lang: 'fi'},
  314. {id: 'fr-fr', name: 'France', tld: 'fr', country: 'fr', lang: 'fr'},
  315. {id: 'de-de', name: 'Germany', tld: 'de', country: 'de', lang: 'de'},
  316. {id: 'gr-el', name: 'Greece', tld: 'gr', country: 'gr', lang: 'el'},
  317. {id: 'hk-zh', name: 'Hong Kong', tld: 'com.hk', country: 'hk', lang: 'zh-hk'},
  318. {id: 'hu-hu', name: 'Hungary', tld: 'hu', country: 'hu', lang: 'hu'},
  319. {id: 'in-en', name: 'India', tld: 'co.in', country: 'in', lang: 'en'},
  320. {id: 'id-id', name: 'Indonesia', tld: 'co.id', country: 'id', lang: 'id'},
  321. {id: 'id-en', name: 'Indonesia (en)', tld: 'co.id', country: 'id', lang: 'en'},
  322. {id: 'ie-en', name: 'Ireland', tld: 'ie', country: 'ie', lang: 'en'},
  323. {id: 'il-he', name: 'Israel', tld: 'co.il', country: 'il', lang: 'he'},
  324. {id: 'it-it', name: 'Italy', tld: 'it', country: 'it', lang: 'it'},
  325. {id: 'jp-ja', name: 'Japan', tld: 'co.jp', country: 'jp', lang: 'ja'},
  326. {id: 'kr-ko', name: 'Korea', tld: 'co.kr', country: 'kr', lang: 'ko'},
  327. {id: 'lv-lv', name: 'Latvia', tld: 'lv', country: 'lv', lang: 'lv'},
  328. {id: 'lt-lt', name: 'Lithuania', tld: 'lt', country: 'lt', lang: 'lt'},
  329. {id: 'my-ms', name: 'Malaysia', tld: 'com.my', country: 'my', lang: 'ms'},
  330. {id: 'my-en', name: 'Malaysia (en)', tld: 'com.my', country: 'my', lang: 'en'},
  331. {id: 'mx-es', name: 'Mexico', tld: 'mx', country: 'mx', lang: 'es'},
  332. {id: 'nl-nl', name: 'Netherlands', tld: 'nl', country: 'nl', lang: 'nl'},
  333. {id: 'nz-en', name: 'New Zealand', tld: 'co.nz', country: 'nz', lang: 'en'},
  334. {id: 'no-no', name: 'Norway', tld: 'no', country: 'no', lang: 'no'},
  335. {id: 'pe-es', name: 'Peru', tld: 'com.pe', country: 'pe', lang: 'es'},
  336. {id: 'ph-en', name: 'Philippines', tld: 'com.ph', country: 'ph', lang: 'en'},
  337. {id: 'ph-tl', name: 'Philippines (tl)', tld: 'com.ph', country: 'ph', lang: 'tl'},
  338. {id: 'pl-pl', name: 'Poland', tld: 'pl', country: 'pl', lang: 'pl'},
  339. {id: 'pt-pt', name: 'Portugal', tld: 'pt', country: 'pt', lang: 'pt'},
  340. {id: 'ro-ro', name: 'Romania', tld: 'ro', country: 'ro', lang: 'ro'},
  341. {id: 'ru-ru', name: 'Russia', tld: 'ru', country: 'ru', lang: 'ru'},
  342. {id: 'sa-ar', name: 'Saudi Arabia', tld: 'com.sa', country: 'sa', lang: 'ar'},
  343. {id: 'sg-en', name: 'Singapore', tld: 'com.sg', country: 'sg', lang: 'en'},
  344. {id: 'sk-sk', name: 'Slovakia', tld: 'sk', country: 'sk', lang: 'sk'},
  345. {id: 'sl-sl', name: 'Slovenia', tld: 'si', country: 'sl', lang: 'sl'},
  346. {id: 'za-en', name: 'South Africa', tld: 'co.za', country: 'za', lang: 'en'},
  347. {id: 'es-es', name: 'Spain', tld: 'es', country: 'es', lang: 'es'},
  348. {id: 'es-ca', name: 'Spain (ca)', tld: 'es', country: 'es', lang: 'ca'},
  349. {id: 'se-sv', name: 'Sweden', tld: 'se', country: 'se', lang: 'sv'},
  350. {id: 'ch-de', name: 'Switzerland (de)', tld: 'ch', country: 'ch', lang: 'de'},
  351. {id: 'ch-fr', name: 'Switzerland (fr)', tld: 'ch', country: 'ch', lang: 'fr'},
  352. {id: 'ch-it', name: 'Switzerland (it)', tld: 'ch', country: 'ch', lang: 'it'},
  353. {id: 'tw-zh', name: 'Taiwan', tld: 'com.tw', country: 'tw', lang: 'zh-tw'},
  354. {id: 'th-th', name: 'Thailand', tld: 'co.th', country: 'th', lang: 'th'},
  355. {id: 'tr-tr', name: 'Turkey', tld: 'com.tr', country: 'tr', lang: 'tr'},
  356. {id: 'gb-en', name: 'United Kingdom', tld: 'co.uk', country: 'gb', lang: 'en'},
  357. {id: 'us-en', name: 'United States', tld: 'com', country: 'us', lang: 'en'},
  358. {id: 'us-es', name: 'United States (es)', tld: 'com', country: 'us', lang: 'es'},
  359. {id: 'vn-vi', name: 'Vietnam', tld: 'com.vn', country: 'vn', lang: 'vi'}
  360. ])
  361. /**
  362. * @param {Object} predicate
  363. * @return {Region}
  364. */
  365. function findRegion (predicate) {
  366. return regions.find(region => {
  367. return Object.keys(predicate).every(key => {
  368. return predicate[key] === region[key]
  369. })
  370. })
  371. }
  372. /**
  373. * @param {string} regionID
  374. * @return {Region}
  375. */
  376. function getRegionByID (regionID) {
  377. return findRegion({ id: regionID })
  378. }
  379. const urlRegExp = Object.freeze({
  380. tld: /^www\.google\.([\w.]+)$/i,
  381. cr: /^country(\w+)$/i,
  382. lr: /^lang_([\w-]+)$/i,
  383. lang: /-\w+$/i
  384. })
  385. /**
  386. * @return {Region}
  387. */
  388. function getCurrentRegion () {
  389. const { hostname, searchParams } = new window.URL(window.location.href)
  390. const { setTLD, setHl, setGl, setCr, setLr } = config
  391. const predicate = {}
  392. if (setTLD && urlRegExp.tld.test(hostname)) {
  393. predicate.tld = hostname.replace(urlRegExp.tld, '$1')
  394. }
  395. if (setHl && searchParams.has('hl')) {
  396. predicate.lang = searchParams.get('hl')
  397. }
  398. if (setGl && searchParams.has('gl')) {
  399. predicate.country = searchParams.get('gl')
  400. }
  401. if (setCr && searchParams.has('cr')) {
  402. predicate.country = searchParams.get('cr').replace(urlRegExp.cr, '$1')
  403. }
  404. if (setLr && searchParams.has('lr')) {
  405. predicate.lang = searchParams.get('lr').replace(urlRegExp.lr, '$1')
  406. }
  407. for (let prop in predicate) {
  408. predicate[prop] = predicate[prop].toLowerCase()
  409. }
  410. return findRegion(predicate)
  411. }
  412. /**
  413. * @type {ReadonlyArray<string>}
  414. */
  415. const delParams = Object.freeze([
  416. 'aqs',
  417. 'bav',
  418. 'bih',
  419. 'biw',
  420. 'bvm',
  421. 'client',
  422. 'cp',
  423. 'dcr',
  424. 'dpr',
  425. 'dq',
  426. 'ech',
  427. 'ei',
  428. 'gfe_rd',
  429. 'gs_gbg',
  430. 'gs_l',
  431. 'gs_mss',
  432. 'gs_rn',
  433. 'gws_rd',
  434. 'oq',
  435. 'pbx',
  436. 'pf',
  437. 'pq',
  438. 'prds',
  439. 'psi',
  440. 'sa',
  441. 'safe',
  442. 'sclient',
  443. 'source',
  444. 'stick',
  445. 'ved'
  446. ])
  447. /**
  448. * @param {Region} region
  449. * @return {string}
  450. */
  451. function getSearchURL (region) {
  452. const url = new window.URL(window.location.href)
  453. const { hostname, searchParams } = url
  454. const { setTLD, setHl, setGl, setCr, setLr } = config
  455. const { tld, country, lang } = region
  456. if (setTLD && tld) {
  457. url.hostname = hostname.replace(urlRegExp.tld, `www.google.${tld}`)
  458. } else if (urlRegExp.tld.test(url.hostname)) {
  459. url.hostname = 'www.google.com'
  460. }
  461. if (setHl && lang) {
  462. searchParams.set('hl', lang)
  463. } else {
  464. searchParams.delete('hl')
  465. }
  466. if (setGl && country) {
  467. searchParams.set('gl', country)
  468. } else {
  469. searchParams.delete('gl')
  470. }
  471. if (setCr && country) {
  472. searchParams.set('cr', `country${country.toUpperCase()}`)
  473. } else {
  474. searchParams.delete('cr')
  475. }
  476. if (setLr && lang) {
  477. const lr = `lang_${lang.replace(urlRegExp.lang, m => m.toUpperCase())}`
  478. searchParams.set('lr', lr)
  479. } else {
  480. searchParams.delete('lr')
  481. }
  482. delParams.forEach(param => {
  483. searchParams.delete(param)
  484. })
  485. return url.toString()
  486. }
  487. // =============================================================================
  488. // User Interface
  489. // =============================================================================
  490. /**
  491. * @param {Element} target
  492. */
  493. function createMenu (target) {
  494. const currentRegion = getCurrentRegion()
  495. const data = { config, regions, getRegionByID, getSearchURL, currentRegion }
  496. const template = `
  497. <% const { showFlags, userRegions } = config; %>
  498. <!-- Menu Dropdown Toggle -->
  499. <div class="hdtb-mn-hd gsr-menu-toggle <%- currentRegion ? 'hdtb-sel' : '' %>" role="button">
  500. <div class="mn-hd-txt">
  501. <% if (currentRegion) { %>
  502. <% let { name, country } = currentRegion; %>
  503. <% if (country && showFlags) { %> <span class="flag flag-<%- country %>"></span> <% } %>
  504. <%- name %>
  505. <% } else { %>
  506. Regions
  507. <% } %>
  508. </div>
  509. <span class="mn-dwn-arw"></span>
  510. </div>
  511. <!-- Menu Dropdown -->
  512. <ul class="hdtbU hdtb-mn-c gsr-menu-dropdown">
  513. <!-- User Regions List -->
  514. <% userRegions.map(getRegionByID).forEach(region => { %>
  515. <% if (!region) { return; } %>
  516. <% let { id, name, country } = region; %>
  517. <% let isCurrent = currentRegion && currentRegion.id === id; %>
  518. <% let url = getSearchURL(region); %>
  519. <li class="hdtbItm <%- isCurrent ? 'hdtbSel' : '' %>">
  520. <a class="q qs" href="<%- url %>">
  521. <% if (country && showFlags) { %> <span class="flag flag-<%- country %>"></span> <% } %>
  522. <%- name %>
  523. </a>
  524. </li>
  525. <% }); %>
  526. <!-- Configuration Modal Toggle -->
  527. <li class="hdtbItm">
  528. <div class="cdr_sep"></div>
  529. <a class="q qs gsr-menu-config" data-gsr-onclick="showModal" title="Google Search Region">...</a>
  530. </li>
  531. </ul>
  532. `
  533. const html = renderTemplate(template, data)
  534. target.insertAdjacentHTML('afterend', html)
  535. }
  536. /**
  537. * @param {Element} target
  538. */
  539. function createModal (target) {
  540. const data = { config, regions }
  541. const template = `
  542. <% const { setTLD, setHl, setGl, setCr, setLr, showFlags, userRegions } = config; %>
  543. <!-- Configuration Modal -->
  544. <div class="gsr-modal" data-gsr-onclick="hideModal">
  545. <!-- Modal Dialog -->
  546. <div class="gsr-modal-dialog">
  547. <!-- Modal Header -->
  548. <div class="gsr-modal-header">
  549. <div class="gsr-modal-title">Google Search Region</div>
  550. <div class="gsr-modal-close" role="button" aria-label="Close" data-gsr-onclick="hideModal"></div>
  551. </div>
  552. <!-- Modal Body -->
  553. <div class="gsr-modal-body">
  554. <!-- Menu Configuration -->
  555. <div class="gsr-modal-subtitle">Menu</div>
  556. <!-- config.showFlags -->
  557. <label class="gsr-control">
  558. <input class="gsr-control-input" type="checkbox" data-gsr-config="showFlags" <%- showFlags ? 'checked' : '' %>>
  559. <span class="gsr-control-indicator"></span>
  560. <span class="gsr-control-description">Show country flags</span>
  561. </label>
  562. <!-- URL Configuration -->
  563. <div class="gsr-modal-subtitle">URL</div>
  564. <!-- config.setTLD -->
  565. <label class="gsr-control">
  566. <input class="gsr-control-input" type="checkbox" data-gsr-config="setTLD" <%- setTLD ? 'checked' : '' %>>
  567. <span class="gsr-control-indicator"></span>
  568. <span class="gsr-control-description">Set top level domain</span>
  569. </label>
  570. <!-- config.setHl -->
  571. <label class="gsr-control">
  572. <input class="gsr-control-input" type="checkbox" data-gsr-config="setHl" <%- setHl ? 'checked' : '' %>>
  573. <span class="gsr-control-indicator"></span>
  574. <span class="gsr-control-description">Set host language (hl)</span>
  575. </label>
  576. <!-- config.setGl -->
  577. <label class="gsr-control">
  578. <input class="gsr-control-input" type="checkbox" data-gsr-config="setGl" <%- setGl ? 'checked' : '' %>>
  579. <span class="gsr-control-indicator"></span>
  580. <span class="gsr-control-description">Set region (gl)</span>
  581. </label>
  582. <!-- config.setCr -->
  583. <label class="gsr-control">
  584. <input class="gsr-control-input" type="checkbox" data-gsr-config="setCr" <%- setCr ? 'checked' : '' %>>
  585. <span class="gsr-control-indicator"></span>
  586. <span class="gsr-control-description">Set country filter (cr)</span>
  587. </label>
  588. <!-- config.setLr -->
  589. <label class="gsr-control">
  590. <input class="gsr-control-input" type="checkbox" data-gsr-config="setLr" <%- setLr ? 'checked' : '' %>>
  591. <span class="gsr-control-indicator"></span>
  592. <span class="gsr-control-description">Set language filter (lr)</span>
  593. </label>
  594. <!-- Regions Configuration -->
  595. <div class="gsr-modal-subtitle">Regions</div>
  596. <div class="gsr-columns">
  597. <!-- config.userRegions -->
  598. <% regions.forEach(region => { %>
  599. <% let { id, name, country } = region; %>
  600. <% let isChecked = userRegions.includes(id); %>
  601. <label class="gsr-control" title="<%- name %>">
  602. <input class="gsr-control-input" type="checkbox"
  603. data-gsr-config="userRegions:<%- id %>" <%- isChecked ? 'checked' : '' %>>
  604. <span class="gsr-control-indicator"></span>
  605. <span class="gsr-control-description">
  606. <% if (country) { %> <span class="flag flag-<%- country %>"></span> <% } %>
  607. <%- name %>
  608. </span>
  609. </label>
  610. <% }); %>
  611. </div>
  612. </div>
  613. <!-- Modal Footer -->
  614. <div class="gsr-modal-footer">
  615. <button class="gsr-btn gsr-btn-primary" data-gsr-onclick="save">Save</button>
  616. <button class="gsr-btn gsr-btn-default" data-gsr-onclick="hideModal">Cancel</button>
  617. </div>
  618. </div>
  619. </div>
  620. `
  621. const html = renderTemplate(template, data)
  622. target.insertAdjacentHTML('beforeend', html)
  623. }
  624. /**
  625. * @return {Promise<void>}
  626. */
  627. function delegateEvents () {
  628. const body = document.body
  629. const events = {}
  630. events.showModal = function showModal (event) {
  631. const modal = $('.gsr-modal')
  632. if (modal) { modal.style.display = null } else { createModal(body) }
  633. }
  634. events.hideModal = function hideModal (event) {
  635. const modal = $('.gsr-modal')
  636. if (modal) { modal.style.display = 'none' }
  637. }
  638. events.save = function save (event) {
  639. const modal = $('.gsr-modal')
  640. const controls = $$('[data-gsr-config]', modal)
  641. const pending = {}
  642. controls.forEach(control => {
  643. const attr = control.getAttribute('data-gsr-config').split(':')
  644. const [name, value = control.value] = attr
  645. if (typeof config[name] === 'boolean') {
  646. pending[name] = control.checked
  647. }
  648. if (Array.isArray(config[name])) {
  649. if (!Array.isArray(pending[name])) { pending[name] = [] }
  650. if (control.checked) { pending[name].push(value) }
  651. }
  652. })
  653. Object.assign(config, pending)
  654. saveConfig().then(() => {
  655. window.location.reload()
  656. })
  657. }
  658. $delegate(body, '[data-gsr-onclick]', 'click', event => {
  659. const name = event.target.getAttribute('data-gsr-onclick')
  660. const callback = events[name]
  661. if (callback) { callback.call(event.target, event) }
  662. })
  663. return Promise.resolve()
  664. }
  665. /**
  666. * @return {Promise<HTMLStyleElement>}
  667. */
  668. function addStyles () {
  669. const style = GM_addStyle(`
  670. /*!
  671. * Region Menu Dropdown CSS
  672. */
  673. .hdtb-sel{font-weight:700}
  674. .gsr-menu-dropdown{max-height:80vh;overflow-y:auto}
  675. .gsr-menu-dropdown .hdtbSel a{padding:0!important}
  676. .gsr-menu-config{cursor:pointer}
  677. /*!
  678. * Configuration Modal CSS
  679. */
  680. .gsr-modal{display:flex;align-items:center;justify-content:center;position:fixed;z-index:10000;top:0;left:0;width:100%;height:100%;background-color:rgba(255,255,255,.75)}
  681. .gsr-modal-dialog{display:block;width:800px;max-width:80vw;max-height:80vh;overflow:auto;margin:32px;padding:32px;border:1px solid #c5c5c5;box-shadow:0 4px 16px rgba(0,0,0,.2);background-color:#fff;font-size:13px}
  682. .gsr-modal-header{display:flex;justify-content:space-between}
  683. .gsr-modal-footer{text-align:right}
  684. .gsr-modal-body{margin:16px 0}
  685. .gsr-modal-title{font-size:16px;font-weight:thin}
  686. .gsr-modal-subtitle{margin:16px 0;font-size:13px;font-weight:700}
  687. .gsr-modal-close{display:inline-block;width:10px;height:10px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKBAMAAAB/HNKOAAAAElBMVEX////39/e9vb2zs7PCwsLv7++5ffrDAAAAL0lEQVQI12MIEWBgdGVwVmQQMmEQMhJUVmRgVFYyEmBgEDJWZICQEBGILEQlRBcAq64FtxSToc8AAAAASUVORK5CYII=);background-repeat:no-repeat;cursor:pointer}
  688. .gsr-columns{max-height:300px;overflow-x:auto;-webkit-column-count:5;-moz-column-count:5;column-count:5}
  689. .gsr-control{display:block;margin:4px 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  690. .gsr-control-input{display:none}
  691. .gsr-control-indicator{display:inline-block;margin:0 4px;width:10px;height:10px;border:1px solid #c6c6c6;border-radius:1px;vertical-align:middle}
  692. .gsr-control-indicator::after{content:" ";display:none;position:relative;top:-3px;width:15px;height:15px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAACpF6WWAAAAtklEQVQ4y2P4//8/A7Ux1Q0cxoaCADIbCUgCMTvVXAoE5kA8CYidyXYpGrAH4iVAHIXiCwoMDQTimUBcBsRMlBrKCsTpUANzkC0j11BuIK6EGlgKsoAkQ4FgChD7AzELVI8YEDdDDawDYk6YQaQY6gg1oAqILYC4D8oHGcyLbBAphoJAKtQgGO4EYiHk2CLHUJAXm6AG9gCxNHoSIMdQEJCFGqiALaGSayjMxQwUGzq0S6nhZygA2ojsbh6J67kAAAAASUVORK5CYII=);background-repeat:no-repeat;background-position:-5px -3px}
  693. .gsr-btn,.gsr-control-input:checked~.gsr-control-indicator::after{display:inline-block}
  694. .gsr-control:hover .gsr-control-indicator{border-color:#b2b2b2;box-shadow:inset 0 1px 1px rgba(0,0,0,.1)}
  695. .gsr-btn{min-width:70px;height:27px;padding:0 8px;border:1px solid;border-radius:2px;font-family:inherit;font-size:11px;font-weight:700;outline:0}
  696. .gsr-btn-default{border-color:rgba(0,0,0,.1);background-image:linear-gradient(#f5f5f5,#f1f1f1);color:#444}
  697. .gsr-btn-default:hover{border-color:#c6c6c6;background-image:linear-gradient(#f8f8f8,#f1f1f1);color:#333}
  698. .gsr-btn-default:focus{border-color:#4d90fe}
  699. .gsr-btn-primary{border-color:#3079ed;background-image:linear-gradient(#4d90fe,#4787ed);color:#fff}
  700. .gsr-btn-primary:hover{border-color:#2f5bb7;background-image:linear-gradient(#4d90fe,#357ae8);color:#fff}
  701. .gsr-btn-primary:focus{border-color:transparent;box-shadow:inset 0 0 0 1px #fff}
  702. /*!
  703. * Generated with CSS Flag Sprite Generator <https://www.flag-sprites.com/>
  704. *
  705. * FAMFAMFAM Flag Icons <http://www.famfamfam.com/lab/icons/flags/>
  706. * These flag icons are available for free use for any purpose with no
  707. * requirement for attribution.
  708. */
  709. .flag{box-sizing:border-box;display:inline-block;width:16px;height:11px;background:url(data:image/png;base64,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) no-repeat;image-rendering:-moz-crisp-edges;image-rendering:crisp-edges;image-rendering:pixelated;vertical-align:middle}
  710. .flag.flag-wt{border:1px dotted;background-image:none!important}
  711. .flag.flag-ar{background-position:0 0}
  712. .flag.flag-at{background-position:-16px 0}
  713. .flag.flag-au{background-position:-32px 0}
  714. .flag.flag-be{background-position:-48px 0}
  715. .flag.flag-bg{background-position:-64px 0}
  716. .flag.flag-br{background-position:-80px 0}
  717. .flag.flag-ca{background-position:-96px 0}
  718. .flag.flag-ct{background-position:-112px 0}
  719. .flag.flag-ch{background-position:0 -11px}
  720. .flag.flag-cl{background-position:-16px -11px}
  721. .flag.flag-cn{background-position:-32px -11px}
  722. .flag.flag-co{background-position:-48px -11px}
  723. .flag.flag-cz{background-position:-64px -11px}
  724. .flag.flag-de{background-position:-80px -11px}
  725. .flag.flag-dk{background-position:-96px -11px}
  726. .flag.flag-ee{background-position:-112px -11px}
  727. .flag.flag-es{background-position:0 -22px}
  728. .flag.flag-fi{background-position:-16px -22px}
  729. .flag.flag-fr{background-position:-32px -22px}
  730. .flag.flag-gb{background-position:-48px -22px}
  731. .flag.flag-gr{background-position:-64px -22px}
  732. .flag.flag-hk{background-position:-80px -22px}
  733. .flag.flag-hr{background-position:-96px -22px}
  734. .flag.flag-hu{background-position:-112px -22px}
  735. .flag.flag-id{background-position:0 -33px}
  736. .flag.flag-ie{background-position:-16px -33px}
  737. .flag.flag-il{background-position:-32px -33px}
  738. .flag.flag-in{background-position:-48px -33px}
  739. .flag.flag-it{background-position:-64px -33px}
  740. .flag.flag-jp{background-position:-80px -33px}
  741. .flag.flag-kr{background-position:-96px -33px}
  742. .flag.flag-lt{background-position:-112px -33px}
  743. .flag.flag-lv{background-position:0 -44px}
  744. .flag.flag-mx{background-position:-16px -44px}
  745. .flag.flag-my{background-position:-32px -44px}
  746. .flag.flag-nl{background-position:-48px -44px}
  747. .flag.flag-no{background-position:-64px -44px}
  748. .flag.flag-nz{background-position:-80px -44px}
  749. .flag.flag-pe{background-position:-96px -44px}
  750. .flag.flag-ph{background-position:-112px -44px}
  751. .flag.flag-pl{background-position:0 -55px}
  752. .flag.flag-pt{background-position:-16px -55px}
  753. .flag.flag-ro{background-position:-32px -55px}
  754. .flag.flag-ru{background-position:-48px -55px}
  755. .flag.flag-sa{background-position:-64px -55px}
  756. .flag.flag-se{background-position:-80px -55px}
  757. .flag.flag-sg{background-position:-96px -55px}
  758. .flag.flag-sk{background-position:-112px -55px}
  759. .flag.flag-sl{background-position:0 -66px}
  760. .flag.flag-th{background-position:-16px -66px}
  761. .flag.flag-tr{background-position:-32px -66px}
  762. .flag.flag-tw{background-position:-48px -66px}
  763. .flag.flag-us{background-position:-64px -66px}
  764. .flag.flag-vn{background-position:-80px -66px}
  765. .flag.flag-za{background-position:-96px -66px}
  766. `)
  767. return Promise.resolve(style)
  768. }
  769. // =============================================================================
  770. // Initialization
  771. // =============================================================================
  772. /**
  773. * @return {Promise<Element>}
  774. */
  775. function waitForPageReady () {
  776. return new Promise(resolve => {
  777. const observee = $('#hdtb')
  778. const observer = new window.MutationObserver(() => {
  779. const target = $('#hdtb-mn-gp')
  780. if (target) { resolve(target) }
  781. })
  782. observer.observe(observee, { childList: true, subtree: true })
  783. })
  784. }
  785. Promise.all([
  786. waitForPageReady(),
  787. loadConfig(),
  788. delegateEvents(),
  789. addStyles()
  790. ]).then(values => createMenu(values[0]))