incremental-search.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. /* global installed */
  2. 'use strict';
  3. onDOMready().then(() => {
  4. let prevText, focusedLink, focusedEntry;
  5. let prevTime = performance.now();
  6. let focusedName = '';
  7. const input = $create('textarea', {
  8. spellcheck: false,
  9. oninput: incrementalSearch,
  10. });
  11. replaceInlineStyle({
  12. position: 'absolute',
  13. color: 'transparent',
  14. border: '1px solid hsla(180, 100%, 100%, .5)',
  15. top: '-1000px',
  16. overflow: 'hidden',
  17. resize: 'none',
  18. 'background-color': 'hsla(180, 100%, 100%, .2)',
  19. 'pointer-events': 'none',
  20. });
  21. document.body.appendChild(input);
  22. window.addEventListener('keydown', maybeRefocus, true);
  23. function incrementalSearch({which}, immediately) {
  24. if (!immediately) {
  25. debounce(incrementalSearch, 100, {}, true);
  26. return;
  27. }
  28. const direction = which === 38 ? -1 : which === 40 ? 1 : 0;
  29. const text = input.value.toLocaleLowerCase();
  30. if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) {
  31. prevText = text;
  32. return;
  33. }
  34. let textAtPos = 1e6;
  35. let rotated;
  36. const entries = [...installed.children];
  37. const focusedIndex = entries.indexOf(focusedEntry);
  38. if (focusedIndex > 0) {
  39. if (direction > 0) {
  40. rotated = entries.slice(focusedIndex + 1).concat(entries.slice(0, focusedIndex + 1));
  41. } else if (direction < 0) {
  42. rotated = entries.slice(0, focusedIndex).reverse().concat(entries.slice(focusedIndex).reverse());
  43. }
  44. }
  45. let found;
  46. for (const entry of rotated || entries) {
  47. const name = entry.styleNameLowerCase;
  48. const pos = name.indexOf(text);
  49. if (pos === 0) {
  50. found = entry;
  51. break;
  52. } else if (pos > 0 && (pos < textAtPos || direction)) {
  53. found = entry;
  54. textAtPos = pos;
  55. if (direction) {
  56. break;
  57. }
  58. }
  59. }
  60. if (found && found !== focusedEntry) {
  61. focusedEntry = found;
  62. focusedLink = $('.style-name-link', found);
  63. focusedName = found.styleNameLowerCase;
  64. scrollElementIntoView(found, {invalidMarginRatio: .25});
  65. animateElement(found, {className: 'highlight-quick'});
  66. resizeTo(focusedLink);
  67. return true;
  68. }
  69. }
  70. function maybeRefocus(event) {
  71. if (event.altKey || event.ctrlKey || event.metaKey ||
  72. event.target.matches('[type="text"], [type="search"]')) {
  73. return;
  74. }
  75. const {which: k, key} = event;
  76. // focus search field on "/" key
  77. if (key === '/' || !key && k === 191 && !event.shiftKey) {
  78. event.preventDefault();
  79. $('#search').focus();
  80. return;
  81. }
  82. const time = performance.now();
  83. if (
  84. // 0-9
  85. k >= 48 && k <= 57 ||
  86. // a-z
  87. k >= 65 && k <= 90 ||
  88. // numpad keys
  89. k >= 96 && k <= 111 ||
  90. // marks
  91. k >= 186
  92. ) {
  93. input.focus();
  94. if (time - prevTime > 1000) {
  95. input.value = '';
  96. }
  97. prevTime = time;
  98. } else
  99. if (k === 13 && focusedLink) {
  100. focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  101. } else
  102. if ((k === 38 || k === 40) && !event.shiftKey &&
  103. time - prevTime < 5000 && incrementalSearch(event, true)) {
  104. prevTime = time;
  105. } else
  106. if (event.target === input) {
  107. (focusedLink || document.body).focus();
  108. input.value = '';
  109. }
  110. }
  111. function resizeTo(el) {
  112. const bounds = el.getBoundingClientRect();
  113. const base = document.scrollingElement;
  114. replaceInlineStyle({
  115. left: bounds.left - 2 + base.scrollLeft + 'px',
  116. top: bounds.top - 1 + base.scrollTop + 'px',
  117. width: bounds.width + 4 + 'px',
  118. height: bounds.height + 2 + 'px',
  119. });
  120. }
  121. function replaceInlineStyle(css) {
  122. for (const prop in css) {
  123. input.style.setProperty(prop, css[prop], 'important');
  124. }
  125. }
  126. });