Guest User

Untitled

a guest
May 9th, 2019
103
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.79 KB | None | 0 0
  1. const CONFIG = {
  2. /**
  3. * The category, name, key, url, search path, color, icon, and quicklaunch properties for your commands.
  4. * Icons must be added to "icons" folder and their values/names must be updated.
  5. * If none of the specified keys are matched, the '*' key is used.
  6. * Commands without a category don't show up in the help menu.
  7. * Update line 11 and 13 if you prefer using Google.
  8. */
  9. commands: [
  10. {
  11. name: 'Duckduckgo',
  12. key: '*',
  13. url: 'https://duckduckgo.com',
  14. search: '/?q={}',
  15. color: '#DE5833',
  16. },
  17. {
  18. category: 'General',
  19. name: 'Tutanota Mail',
  20. key: 'm',
  21. url: 'https://mail.tutanota.com',
  22. color: 'linear-gradient(135deg, #dd5145, #dd5145)',
  23. icon: 'mail',
  24. quickLaunch: true,
  25. },
  26. {
  27. category: 'General',
  28. name: 'Google Mail',
  29. key: 'g',
  30. url: 'https://gmail.com',
  31. search: '/#search/text={}',
  32. color: 'linear-gradient(135deg, #dd5145, #dd5145)',
  33. icon: 'gmail',
  34. quickLaunch: false,
  35. },
  36. {
  37. category: 'Server',
  38. name: 'justmyhomeserver',
  39. key: 'q',
  40. url: 'https://example.org',
  41. color: 'linear-gradient(135deg, #11c4c9 0%, #0060c9 50%, #004dba 100%)',
  42. icon: 'server',
  43. quickLaunch: true,
  44. },
  45. {
  46. category: 'Server',
  47. name: 'Portainer',
  48. key: 'p',
  49. url: 'http://example.org:9000',
  50. color: 'linear-gradient(135deg, #6441a5, #4b367c)',
  51. icon: 'application',
  52. quickLaunch: false,
  53. },
  54. {
  55. category: 'Server',
  56. name: 'Gitlab',
  57. key: 'g',
  58. url: 'https://example.org:10080',
  59. color: 'linear-gradient(135deg, #2b2b2b, #3b3b3b)',
  60. icon: 'github-box',
  61. quickLaunch: false,
  62. },
  63. {
  64. category: 'Media',
  65. name: 'Jellyfin',
  66. key: 'j',
  67. url: 'http://example.org:8096',
  68. color: 'linear-gradient(135deg, #2b2b2b, #3b3b3b)',
  69. icon: 'play-network',
  70. quickLaunch: false,
  71. },
  72. {
  73. category: 'Media',
  74. name: 'Netflix',
  75. key: 'n',
  76. url: 'https://www.netflix.com',
  77. color: 'linear-gradient(135deg, #E50914, #CB020C)',
  78. icon: 'netflix',
  79. quickLaunch: false,
  80. },
  81. {
  82. category: 'Fun',
  83. name: 'YouTube',
  84. key: 'y',
  85. url: 'https://youtube.com',
  86. search: '/results?search_query={}',
  87. color: 'linear-gradient(135deg, #cd201f, #cd4c1f)',
  88. icon: 'youtube',
  89. quickLaunch: false,
  90. },
  91.  
  92. {
  93. category: 'Fun',
  94. name: 'Twitch',
  95. key: 't',
  96. url: 'https://www.twitch.tv',
  97. search: '/directory/game/{}',
  98. color: 'linear-gradient(135deg, #6441a5, #4b367c)',
  99. icon: 'twitch',
  100. quickLaunch: false,
  101. },
  102. {
  103. category: 'Other',
  104. name: 'Reddit',
  105. key: 'r',
  106. url: 'https://reddit.com',
  107. search: '/search?q={}',
  108. color: 'linear-gradient(135deg, #FF8456, #FF4500)',
  109. icon: 'reddit',
  110. quickLaunch: false,
  111. },
  112. {
  113. category: 'Other',
  114. name: 'IMDb',
  115. key: 'i',
  116. url: 'https://imdb.com',
  117. search: '/find?ref_=nv_sr_fn&q={}',
  118. color: 'linear-gradient(135deg, #7A5F00, #E8B708)',
  119. icon: 'filmstrip',
  120. quickLaunch: false,
  121. },
  122. ],
  123.  
  124. /**
  125. * Get suggestions as you type.
  126. */
  127. suggestions: true,
  128. suggestionsLimit: 4,
  129.  
  130. /**
  131. * The order and limit for each suggestion influencer. An "influencer" is
  132. * just a suggestion source. The following influencers are available:
  133. *
  134. * - "Default" suggestions come from CONFIG.defaultSuggestions
  135. * - "DuckDuckGo" suggestions come from the duck duck go search api
  136. * - "History" suggestions come from your previously entered queries
  137. */
  138. influencers: [
  139. { name: 'Default', limit: 4 },
  140. { name: 'History', limit: 1 },
  141. { name: 'DuckDuckGo', limit: 4 },
  142. ],
  143.  
  144. /**
  145. * Default search suggestions for the specified queries.
  146. */
  147. defaultSuggestions: {
  148. g: ['g/issues', 'g/pulls', 'gist.github.com'],
  149. r: ['r/r/unixporn', 'r/r/startpages', 'r/r/webdev', 'r/r/technology'],
  150. },
  151.  
  152. /**
  153. * Instantly redirect when a key is matched. Put a space before any other
  154. * queries to prevent unwanted redirects.
  155. */
  156. instantRedirect: false,
  157.  
  158. /**
  159. * Open triggered queries in a new tab.
  160. */
  161. newTab: true,
  162.  
  163. /**
  164. * Dynamic overlay background colors when command domains are matched.
  165. */
  166. colors: true,
  167. colors: true,
  168.  
  169. /**
  170. * Reverse color theme
  171. */
  172. reversedColors: false,
  173.  
  174. /**
  175. * The delimiter between a command key and your search query. For example,
  176. * to search GitHub for potatoes, you'd type "g:potatoes".
  177. */
  178. searchDelimiter: ':',
  179.  
  180. /**
  181. * The delimiter between a command key and a path. For example, you'd type
  182. * "r/r/unixporn" to go to "https://reddit.com/r/unixporn".
  183. */
  184. pathDelimiter: '/',
  185.  
  186. /**
  187. * The delimiter between the hours and minutes on the clock.
  188. */
  189. clockDelimiter: ' ',
  190.  
  191. /**
  192. * Show a twenty-four-hour clock instead of a twelve-hour clock with AM/PM.
  193. */
  194. twentyFourHourClock: true,
  195. };
  196.  
  197. const $ = {
  198. bodyClassAdd: c => $.el('body').classList.add(c),
  199. bodyClassRemove: c => $.el('body').classList.remove(c),
  200. el: s => document.querySelector(s),
  201. els: s => [].slice.call(document.querySelectorAll(s) || []),
  202. escapeRegex: s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),
  203. flattenAndUnique: arr => [...new Set([].concat.apply([], arr))],
  204. ieq: (a, b) => a.toLowerCase() === b.toLowerCase(),
  205. iin: (a, b) => a.toLowerCase().indexOf(b.toLowerCase()) !== -1,
  206. isDown: e => ['c-n', 'down', 'tab'].includes($.key(e)),
  207. isRemove: e => ['backspace', 'delete'].includes($.key(e)),
  208. isUp: e => ['c-p', 'up', 's-tab'].includes($.key(e)),
  209.  
  210. jsonp: url => {
  211. let script = document.createElement('script');
  212. script.src = url;
  213. $.el('head').appendChild(script);
  214. },
  215.  
  216. key: e => {
  217. const ctrl = e.ctrlKey;
  218. const shift = e.shiftKey;
  219.  
  220. switch (e.which) {
  221. case 8:
  222. return 'backspace';
  223. case 9:
  224. return shift ? 's-tab' : 'tab';
  225. case 13:
  226. return 'enter';
  227. case 16:
  228. return 'shift';
  229. case 17:
  230. return 'ctrl';
  231. case 18:
  232. return 'alt';
  233. case 27:
  234. return 'escape';
  235. case 38:
  236. return 'up';
  237. case 40:
  238. return 'down';
  239. case 46:
  240. return 'delete';
  241. case 78:
  242. return ctrl ? 'c-n' : 'n';
  243. case 80:
  244. return ctrl ? 'c-p' : 'p';
  245. case 189:
  246. return 'dash';
  247. case 91:
  248. case 93:
  249. case 224:
  250. return 'super';
  251. }
  252. },
  253.  
  254. pad: v => ('0' + v.toString()).slice(-2),
  255. };
  256.  
  257. class Clock {
  258. constructor(options) {
  259. this._el = $.el('#clock');
  260. this._delimiter = options.delimiter;
  261. this._twentyFourHourClock = options.twentyFourHourClock;
  262. this._setTime = this._setTime.bind(this);
  263. this._el.addEventListener('click', options.toggleHelp);
  264. this._start();
  265. }
  266.  
  267. _setTime() {
  268. const date = new Date();
  269. let hours = $.pad(date.getHours());
  270. let amPm = '';
  271.  
  272. if (!this._twentyFourHourClock) {
  273. hours = date.getHours();
  274. if (hours > 12) hours -= 12;
  275. else if (hours === 0) hours = 12;
  276.  
  277. amPm =
  278. `&nbsp;<span class="am-pm">` +
  279. `${date.getHours() > 12 ? 'PM' : 'AM'}</span>`;
  280. }
  281.  
  282. const minutes = $.pad(date.getMinutes());
  283. this._el.innerHTML = `${hours}${this._delimiter}${minutes}${amPm}`;
  284. this._el.setAttribute('datetime', date.toTimeString());
  285. }
  286.  
  287. _start() {
  288. this._setTime();
  289. setInterval(this._setTime, 1000);
  290. }
  291. }
  292.  
  293. class Help {
  294. constructor(options) {
  295. this._el = $.el('#help');
  296. this._commands = options.commands;
  297. this._newTab = options.newTab;
  298. this._toggled = false;
  299. this._handleKeydown = this._handleKeydown.bind(this);
  300. this.toggle = this.toggle.bind(this);
  301. this.launch = this.launch.bind(this);
  302. this._inputEl = $.el('#search-input');
  303. this._inputElVal = '';
  304. this._suggester = options.suggester;
  305. this._reverseColors = options.reversedColors;
  306. this._buildAndAppendLists();
  307. this._registerEvents();
  308. this._invertValue;
  309. }
  310.  
  311. toggle(show) {
  312. this._toggled = typeof show !== 'undefined' ? show : !this._toggled;
  313. this._toggled ? $.bodyClassAdd('help') : $.bodyClassRemove('help');
  314. }
  315.  
  316. hide() {
  317. $.bodyClassRemove('form');
  318. this._inputEl.value = '';
  319. this._inputElVal = '';
  320. this._suggester.suggest('');
  321. }
  322.  
  323. launch() {
  324. this.hide();
  325. this.toggle(true);
  326. $.bodyClassAdd('help');
  327. for (let i = 0; i < CONFIG.commands.length; i++) {
  328. if (CONFIG.commands[i].quickLaunch === true) {
  329. window.open(CONFIG.commands[i].url);
  330. }
  331. }
  332. }
  333.  
  334. _buildAndAppendLists() {
  335. const lists = document.createElement('ul');
  336. lists.classList.add('categories');
  337.  
  338. this._getCategories().forEach(category => {
  339. lists.insertAdjacentHTML(
  340. 'beforeend',
  341. `<li class="category">
  342. <h2 class="category-name">${category}</h2>
  343. <ul>${this._buildListCommands(category)}</ul>
  344. </li>`
  345. );
  346. });
  347.  
  348. this._el.appendChild(lists);
  349. }
  350.  
  351. _buildListCommands(currentCategory) {
  352. this._invertValue = this._reverseColors ? 1: 0;
  353.  
  354. return this._commands
  355. .map(({ category, name, key, url, icon }) => {
  356. if (category === currentCategory) {
  357. return `
  358. <li class="command">
  359. <a href="${url}" target="${this._newTab ? '_blank' : '_self'}">
  360. <span class="command-key"><img src='assets/icons/${icon}.png' height = 36px center style="filter: invert(${this._invertValue});"></span>
  361. <span class="command-name">${name}</span>
  362. </a>
  363. </li>
  364. `;
  365. }
  366. })
  367. .join('');
  368. }
  369.  
  370. _getCategories() {
  371. const categories = this._commands
  372. .map(v => v.category)
  373. .filter(category => category);
  374.  
  375. return [...new Set(categories)];
  376. }
  377.  
  378. _handleKeydown(e) {
  379. if ($.key(e) === 'escape') this.toggle(false);
  380. }
  381.  
  382. _registerEvents() {
  383. document.addEventListener('keydown', this._handleKeydown);
  384. }
  385. }
  386.  
  387. class Influencer {
  388. constructor(options) {
  389. this._limit = options.limit;
  390. this._parseQuery = options.parseQuery;
  391. }
  392.  
  393. addItem() {}
  394. getSuggestions() {}
  395.  
  396. _addSearchPrefix(items, query) {
  397. const searchPrefix = this._getSearchPrefix(query);
  398. return items.map(s => (searchPrefix ? searchPrefix + s : s));
  399. }
  400.  
  401. _getSearchPrefix(query) {
  402. const { isSearch, key, split } = this._parseQuery(query);
  403. return isSearch ? `${key}${split} ` : false;
  404. }
  405. }
  406.  
  407. class DefaultInfluencer extends Influencer {
  408. constructor({ defaultSuggestions }) {
  409. super(...arguments);
  410. this._defaultSuggestions = defaultSuggestions;
  411. }
  412.  
  413. getSuggestions(query) {
  414. return new Promise(resolve => {
  415. const suggestions = this._defaultSuggestions[query];
  416. resolve(suggestions ? suggestions.slice(0, this._limit) : []);
  417. });
  418. }
  419. }
  420.  
  421. class DuckDuckGoInfluencer extends Influencer {
  422. constructor({ queryParser }) {
  423. super(...arguments);
  424. }
  425.  
  426. getSuggestions(rawQuery) {
  427. const { query } = this._parseQuery(rawQuery);
  428. if (!query) return Promise.resolve([]);
  429.  
  430. return new Promise(resolve => {
  431. const endpoint = 'https://duckduckgo.com/ac/';
  432. const callback = 'autocompleteCallback';
  433.  
  434. window[callback] = res => {
  435. const suggestions = res
  436. .map(i => i.phrase)
  437. .filter(s => !$.ieq(s, query))
  438. .slice(0, this._limit);
  439.  
  440. resolve(this._addSearchPrefix(suggestions, rawQuery));
  441. };
  442.  
  443. $.jsonp(`${endpoint}?callback=${callback}&q=${query}`);
  444. });
  445. }
  446. }
  447.  
  448. class HistoryInfluencer extends Influencer {
  449. constructor() {
  450. super(...arguments);
  451. this._storeName = 'history';
  452. }
  453.  
  454. addItem(query) {
  455. if (query.length < 2) return;
  456. let exists;
  457.  
  458. const history = this._getHistory().map(([item, count]) => {
  459. const match = $.ieq(item, query);
  460. if (match) exists = true;
  461. return [item, match ? count + 1 : count];
  462. });
  463.  
  464. if (!exists) history.push([query, 1]);
  465.  
  466. const sorted = history
  467. .sort((current, next) => current[1] - next[1])
  468. .reverse();
  469.  
  470. this._setHistory(sorted);
  471. }
  472.  
  473. getSuggestions(query) {
  474. return new Promise(resolve => {
  475. const suggestions = this._getHistory()
  476. .map(([item]) => item)
  477. .filter(item => query && !$.ieq(item, query) && $.iin(item, query))
  478. .slice(0, this._limit);
  479.  
  480. resolve(suggestions);
  481. });
  482. }
  483.  
  484. _fetch() {
  485. return JSON.parse(localStorage.getItem(this._storeName)) || [];
  486. }
  487.  
  488. _getHistory() {
  489. this._history = this._history || this._fetch();
  490. return this._history;
  491. }
  492.  
  493. _save(history) {
  494. localStorage.setItem(this._storeName, JSON.stringify(history));
  495. }
  496.  
  497. _setHistory(history) {
  498. this._history = history;
  499. this._save(history);
  500. }
  501. }
  502.  
  503. class Suggester {
  504. constructor(options) {
  505. this._el = $.el('#search-suggestions');
  506. this._enabled = options.enabled;
  507. this._influencers = options.influencers;
  508. this._limit = options.limit;
  509. this._suggestionEls = [];
  510. this._handleKeydown = this._handleKeydown.bind(this);
  511. this._registerEvents();
  512. }
  513.  
  514. setOnClick(callback) {
  515. this._onClick = callback;
  516. }
  517.  
  518. setOnHighlight(callback) {
  519. this._onHighlight = callback;
  520. }
  521.  
  522. setOnUnhighlight(callback) {
  523. this._onUnhighlight = callback;
  524. }
  525.  
  526. success(query) {
  527. this._clearSuggestions();
  528. this._influencers.forEach(i => i.addItem(query));
  529. }
  530.  
  531. suggest(input) {
  532. if (!this._enabled) return;
  533. input = input.trim();
  534. if (input === '') this._clearSuggestions();
  535.  
  536. Promise.all(this._getInfluencerPromises(input)).then(res => {
  537. const suggestions = $.flattenAndUnique(res);
  538. this._clearSuggestions();
  539.  
  540. if (suggestions.length) {
  541. this._appendSuggestions(suggestions, input);
  542. this._registerSuggestionHighlightEvents();
  543. this._registerSuggestionClickEvents();
  544. $.bodyClassAdd('suggestions');
  545. }
  546. });
  547. }
  548.  
  549. _appendSuggestions(suggestions, input) {
  550. suggestions.some((suggestion, i) => {
  551. const match = new RegExp($.escapeRegex(input), 'ig');
  552. const suggestionHtml = suggestion.replace(match, `<b>${input}</b>`);
  553.  
  554. this._el.insertAdjacentHTML(
  555. 'beforeend',
  556. `<li>
  557. <button
  558. type="button"
  559. class="js-search-suggestion search-suggestion"
  560. data-suggestion="${suggestion}"
  561. tabindex="-1"
  562. >
  563. ${suggestionHtml}
  564. </button>
  565. </li>`
  566. );
  567.  
  568. if (i + 1 >= this._limit) return true;
  569. });
  570.  
  571. this._suggestionEls = $.els('.js-search-suggestion');
  572. }
  573.  
  574. _clearSuggestionClickEvents() {
  575. this._suggestionEls.forEach(el => {
  576. el.removeEventListener('click', this._onClick);
  577. });
  578. }
  579.  
  580. _clearSuggestionHighlightEvents() {
  581. this._suggestionEls.forEach(el => {
  582. el.removeEventListener('mouseover', this._highlight);
  583. el.removeEventListener('mouseout', this._unHighlight);
  584. });
  585. }
  586.  
  587. _clearSuggestions() {
  588. $.bodyClassRemove('suggestions');
  589. this._clearSuggestionHighlightEvents();
  590. this._clearSuggestionClickEvents();
  591. this._suggestionEls = [];
  592. this._el.innerHTML = '';
  593. }
  594.  
  595. _focusNext(e) {
  596. const exists = this._suggestionEls.some((el, i) => {
  597. if (el.classList.contains('highlight')) {
  598. this._highlight(this._suggestionEls[i + 1], e);
  599. return true;
  600. }
  601. });
  602.  
  603. if (!exists) this._highlight(this._suggestionEls[0], e);
  604. }
  605.  
  606. _focusPrevious(e) {
  607. const exists = this._suggestionEls.some((el, i) => {
  608. if (el.classList.contains('highlight') && i) {
  609. this._highlight(this._suggestionEls[i - 1], e);
  610. return true;
  611. }
  612. });
  613.  
  614. if (!exists) this._unHighlight(e);
  615. }
  616.  
  617. _getInfluencerPromises(input) {
  618. return this._influencers.map(influencer =>
  619. influencer.getSuggestions(input)
  620. );
  621. }
  622.  
  623. _handleKeydown(e) {
  624. if ($.isDown(e)) this._focusNext(e);
  625. if ($.isUp(e)) this._focusPrevious(e);
  626. }
  627.  
  628. _highlight(el, e) {
  629. this._unHighlight();
  630. if (!el) return;
  631. this._onHighlight(el.getAttribute('data-suggestion'));
  632. el.classList.add('highlight');
  633. e.preventDefault();
  634. }
  635.  
  636. _registerEvents() {
  637. document.addEventListener('keydown', this._handleKeydown);
  638. }
  639.  
  640. _registerSuggestionClickEvents() {
  641. this._suggestionEls.forEach(el => {
  642. const value = el.getAttribute('data-suggestion');
  643. el.addEventListener('click', this._onClick.bind(null, value));
  644. });
  645. }
  646.  
  647. _registerSuggestionHighlightEvents() {
  648. const noHighlightUntilMouseMove = () => {
  649. window.removeEventListener('mousemove', noHighlightUntilMouseMove);
  650.  
  651. this._suggestionEls.forEach(el => {
  652. el.addEventListener('mouseover', this._highlight.bind(this, el));
  653. el.addEventListener('mouseout', this._unHighlight.bind(this));
  654. });
  655. };
  656.  
  657. window.addEventListener('mousemove', noHighlightUntilMouseMove);
  658. }
  659.  
  660. _unHighlight(e) {
  661. const el = $.el('.highlight');
  662. if (!el) return;
  663. this._onUnhighlight();
  664. el.classList.remove('highlight');
  665. if (e) e.preventDefault();
  666. }
  667. }
  668.  
  669. class QueryParser {
  670. constructor(options) {
  671. this._commands = options.commands;
  672. this._searchDelimiter = options.searchDelimiter;
  673. this._pathDelimiter = options.pathDelimiter;
  674. this._protocolRegex = /^[a-zA-Z]+:\/\//i;
  675. this._urlRegex = /^((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/i;
  676. this.parse = this.parse.bind(this);
  677. }
  678.  
  679. parse(query) {
  680. const res = { query: query, split: null };
  681.  
  682. if (this._urlRegex.test(query)) {
  683. const hasProtocol = this._protocolRegex.test(query);
  684. res.redirect = hasProtocol ? query : 'http://' + query;
  685. } else {
  686. const trimmed = query.trim();
  687. const splitSearch = trimmed.split(this._searchDelimiter);
  688. const splitPath = trimmed.split(this._pathDelimiter);
  689.  
  690. this._commands.some(({ category, key, name, search, url }) => {
  691. if (query === key) {
  692. res.key = key;
  693. res.isKey = true;
  694. res.redirect = url;
  695. return true;
  696. }
  697.  
  698. if (splitSearch[0] === key && search) {
  699. res.key = key;
  700. res.isSearch = true;
  701. res.split = this._searchDelimiter;
  702. res.query = QueryParser._shiftAndTrim(splitSearch, res.split);
  703. res.redirect = QueryParser._prepSearch(url, search, res.query);
  704. return true;
  705. }
  706.  
  707. if (splitPath[0] === key) {
  708. res.key = key;
  709. res.isPath = true;
  710. res.split = this._pathDelimiter;
  711. res.path = QueryParser._shiftAndTrim(splitPath, res.split);
  712. res.redirect = QueryParser._prepPath(url, res.path);
  713. return true;
  714. }
  715.  
  716. if (key === '*') {
  717. res.redirect = QueryParser._prepSearch(url, search, query);
  718. }
  719. });
  720. }
  721.  
  722. res.color = QueryParser._getColorFromUrl(this._commands, res.redirect);
  723. return res;
  724. }
  725.  
  726. static _getColorFromUrl(commands, url) {
  727. const domain = new URL(url).hostname;
  728.  
  729. return (
  730. commands
  731. .filter(c => new URL(c.url).hostname.includes(domain))
  732. .map(c => c.color)[0] || null
  733. );
  734. }
  735.  
  736. static _prepPath(url, path) {
  737. return QueryParser._stripUrlPath(url) + '/' + path;
  738. }
  739.  
  740. static _prepSearch(url, searchPath, query) {
  741. if (!searchPath) return url;
  742. const baseUrl = QueryParser._stripUrlPath(url);
  743. const urlQuery = encodeURIComponent(query);
  744. searchPath = searchPath.replace('{}', urlQuery);
  745. return baseUrl + searchPath;
  746. }
  747.  
  748. static _shiftAndTrim(arr, delimiter) {
  749. arr.shift();
  750. return arr.join(delimiter).trim();
  751. }
  752.  
  753. static _stripUrlPath(url) {
  754. const parser = document.createElement('a');
  755. parser.href = url;
  756. return `${parser.protocol}//${parser.hostname}`;
  757. }
  758. }
  759.  
  760. class Form {
  761. constructor(options) {
  762. this._colors = options.colors;
  763. this._formEl = $.el('#search-form');
  764. this._inputEl = $.el('#search-input');
  765. this._inputElVal = '';
  766. this._instantRedirect = options.instantRedirect;
  767. this._newTab = options.newTab;
  768. this._parseQuery = options.parseQuery;
  769. this._suggester = options.suggester;
  770. this._toggleHelp = options.toggleHelp;
  771. this._quickLaunch = options.quickLaunch;
  772. this._clearPreview = this._clearPreview.bind(this);
  773. this._handleInput = this._handleInput.bind(this);
  774. this._handleKeydown = this._handleKeydown.bind(this);
  775. this._previewValue = this._previewValue.bind(this);
  776. this._submitForm = this._submitForm.bind(this);
  777. this._submitWithValue = this._submitWithValue.bind(this);
  778. this._reverseColors = options.reversedColors;
  779. this.hide = this.hide.bind(this);
  780. this.show = this.show.bind(this);
  781. this._registerEvents();
  782. this._loadQueryParam();
  783. this.reverse();
  784. }
  785.  
  786. hide() {
  787. $.bodyClassRemove('form');
  788. this._inputEl.value = '';
  789. this._inputElVal = '';
  790. this._suggester.suggest('');
  791. this._setBackgroundFromQuery('');
  792. }
  793.  
  794. show() {
  795. $.bodyClassAdd('form');
  796. this._inputEl.focus();
  797. }
  798.  
  799. reverse() {
  800. if (this._reverseColors) {
  801. document.documentElement.style.setProperty('--background', '#F1F1F1');
  802. document.documentElement.style.setProperty('--foreground', '#0e0e0e');
  803. }
  804. }
  805.  
  806. _clearPreview() {
  807. this._previewValue(this._inputElVal);
  808. this._inputEl.focus();
  809. }
  810.  
  811. _handleInput() {
  812. const newQuery = this._inputEl.value;
  813. const isHelp = newQuery === '?';
  814. const isLaunch = newQuery === '!';
  815. const { isKey } = this._parseQuery(newQuery);
  816. this._inputElVal = newQuery;
  817. this._suggester.suggest(newQuery);
  818. this._setBackgroundFromQuery(newQuery);
  819. if (!newQuery || isHelp) this.hide();
  820. if (isHelp) this._toggleHelp();
  821. if (isLaunch) this._quickLaunch();
  822. if (this._instantRedirect && isKey) this._submitWithValue(newQuery);
  823. }
  824.  
  825. _handleKeydown(e) {
  826. if ($.isUp(e) || $.isDown(e) || $.isRemove(e)) return;
  827.  
  828. switch ($.key(e)) {
  829. case 'alt':
  830. case 'ctrl':
  831. case 'enter':
  832. case 'shift':
  833. case 'super':
  834. return;
  835. case 'escape':
  836. this.hide();
  837. return;
  838. }
  839.  
  840. this.show();
  841. }
  842.  
  843. _loadQueryParam() {
  844. const q = new URLSearchParams(window.location.search).get('q');
  845. if (q) this._submitWithValue(q);
  846. }
  847.  
  848. _previewValue(value) {
  849. this._inputEl.value = value;
  850. this._setBackgroundFromQuery(value);
  851. }
  852.  
  853. _redirect(redirect) {
  854. if (this._newTab) window.open(redirect, '_blank');
  855. else window.location.href = redirect;
  856. }
  857.  
  858. _registerEvents() {
  859. document.addEventListener('keydown', this._handleKeydown);
  860. this._inputEl.addEventListener('input', this._handleInput);
  861. this._formEl.addEventListener('submit', this._submitForm, false);
  862.  
  863. if (this._suggester) {
  864. this._suggester.setOnClick(this._submitWithValue);
  865. this._suggester.setOnHighlight(this._previewValue);
  866. this._suggester.setOnUnhighlight(this._clearPreview);
  867. }
  868. }
  869.  
  870. _setBackgroundFromQuery(query) {
  871. if (!this._colors) return;
  872. this._formEl.style.background = this._parseQuery(query).color;
  873. }
  874.  
  875. _submitForm(e) {
  876. if (e) e.preventDefault();
  877. const query = this._inputEl.value;
  878. if (this._suggester) this._suggester.success(query);
  879. this.hide();
  880. this._redirect(this._parseQuery(query).redirect);
  881. }
  882.  
  883. _submitWithValue(value) {
  884. this._inputEl.value = value;
  885. this._submitForm();
  886. }
  887. }
  888.  
  889. const queryParser = new QueryParser({
  890. commands: CONFIG.commands,
  891. pathDelimiter: CONFIG.pathDelimiter,
  892. searchDelimiter: CONFIG.searchDelimiter,
  893. });
  894.  
  895. const influencers = CONFIG.influencers.map(influencerConfig => {
  896. return new {
  897. Default: DefaultInfluencer,
  898. DuckDuckGo: DuckDuckGoInfluencer,
  899. History: HistoryInfluencer,
  900. }[influencerConfig.name]({
  901. defaultSuggestions: CONFIG.defaultSuggestions,
  902. limit: influencerConfig.limit,
  903. parseQuery: queryParser.parse,
  904. });
  905. });
  906.  
  907. const suggester = new Suggester({
  908. enabled: CONFIG.suggestions,
  909. influencers,
  910. limit: CONFIG.suggestionsLimit,
  911. });
  912.  
  913. const help = new Help({
  914. commands: CONFIG.commands,
  915. newTab: CONFIG.newTab,
  916. suggester,
  917. reversedColors: CONFIG.reversedColors,
  918. });
  919.  
  920. const form = new Form({
  921. colors: CONFIG.colors,
  922. instantRedirect: CONFIG.instantRedirect,
  923. newTab: CONFIG.newTab,
  924. parseQuery: queryParser.parse,
  925. suggester,
  926. toggleHelp: help.toggle,
  927. quickLaunch: help.launch,
  928. reversedColors: CONFIG.reversedColors,
  929. });
  930.  
  931. new Clock({
  932. delimiter: CONFIG.clockDelimiter,
  933. toggleHelp: help.toggle,
  934. twentyFourHourClock: CONFIG.twentyFourHourClock,
  935. });
Advertisement
Add Comment
Please, Sign In to add comment