incremental-search.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /* global debounce */// toolbox.js
  2. /* global installed */// manage.js
  3. /* global
  4. $
  5. $create
  6. $isTextInput
  7. animateElement
  8. scrollElementIntoView
  9. */// dom.js
  10. 'use strict';
  11. (() => {
  12. let prevText, focusedLink, focusedEntry;
  13. let prevTime = performance.now();
  14. let focusedName = '';
  15. const input = $create('textarea', {
  16. spellcheck: false,
  17. attributes: {tabindex: -1},
  18. oninput: incrementalSearch,
  19. });
  20. replaceInlineStyle({
  21. opacity: '0',
  22. position: 'absolute',
  23. color: 'transparent',
  24. border: '1px solid hsla(180, 100%, 100%, .5)',
  25. margin: '-1px -2px',
  26. overflow: 'hidden',
  27. resize: 'none',
  28. 'background-color': 'hsla(180, 100%, 100%, .2)',
  29. 'box-sizing': 'content-box',
  30. 'pointer-events': 'none',
  31. });
  32. document.body.appendChild(input);
  33. window.on('keydown', maybeRefocus, true);
  34. function incrementalSearch({key}, immediately) {
  35. if (!immediately) {
  36. debounce(incrementalSearch, 100, {}, true);
  37. return;
  38. }
  39. const direction = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0;
  40. const text = input.value.toLocaleLowerCase();
  41. if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) {
  42. prevText = text;
  43. return;
  44. }
  45. let textAtPos = 1e6;
  46. let rotated;
  47. const entries = [...installed.children];
  48. const focusedIndex = entries.indexOf(focusedEntry);
  49. if (focusedIndex > 0) {
  50. if (direction > 0) {
  51. rotated = entries.slice(focusedIndex + 1).concat(entries.slice(0, focusedIndex + 1));
  52. } else if (direction < 0) {
  53. rotated = entries.slice(0, focusedIndex).reverse()
  54. .concat(entries.slice(focusedIndex).reverse());
  55. }
  56. }
  57. let found;
  58. for (const entry of rotated || entries) {
  59. if (entry.classList.contains('hidden')) continue;
  60. const name = entry.styleNameLowerCase;
  61. const pos = name.indexOf(text);
  62. if (pos === 0) {
  63. found = entry;
  64. break;
  65. } else if (pos > 0 && (pos < textAtPos || direction)) {
  66. found = entry;
  67. textAtPos = pos;
  68. if (direction) {
  69. break;
  70. }
  71. }
  72. }
  73. if (found && found !== focusedEntry) {
  74. focusedEntry = found;
  75. focusedLink = $('.style-name-link', found);
  76. focusedName = found.styleNameLowerCase;
  77. scrollElementIntoView(found, {invalidMarginRatio: .25});
  78. animateElement(found, 'highlight-quick');
  79. replaceInlineStyle({
  80. width: focusedLink.offsetWidth + 'px',
  81. height: focusedLink.offsetHeight + 'px',
  82. opacity: '1',
  83. });
  84. focusedLink.prepend(input);
  85. return true;
  86. }
  87. }
  88. function maybeRefocus(event) {
  89. if (event.altKey || event.metaKey || $('#message-box')) {
  90. return;
  91. }
  92. const inTextInput = $isTextInput(event.target);
  93. const {key, code, ctrlKey: ctrl} = event;
  94. // `code` is independent of the current keyboard language
  95. if ((code === 'KeyF' && ctrl && !event.shiftKey) ||
  96. (code === 'Slash' || key === '/') && !ctrl && !inTextInput) {
  97. // focus search field on "/" or Ctrl-F key
  98. event.preventDefault();
  99. $('#search').focus();
  100. return;
  101. }
  102. if (ctrl || inTextInput && event.target !== input) {
  103. return;
  104. }
  105. const time = performance.now();
  106. if (key.length === 1) {
  107. if (time - prevTime > 1000) {
  108. input.value = '';
  109. }
  110. // Space or Shift-Space is for page down/up
  111. if (key === ' ' && !input.value) {
  112. input.blur();
  113. } else {
  114. input.focus();
  115. prevTime = time;
  116. }
  117. } else
  118. if (key === 'Enter' && focusedLink) {
  119. focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  120. } else
  121. if ((key === 'ArrowUp' || key === 'ArrowDown') && !event.shiftKey &&
  122. time - prevTime < 5000 && incrementalSearch(event, true)) {
  123. prevTime = time;
  124. } else
  125. if (event.target === input) {
  126. (focusedLink || document.body).focus();
  127. input.value = '';
  128. }
  129. }
  130. function replaceInlineStyle(css) {
  131. for (const prop in css) {
  132. input.style.setProperty(prop, css[prop], 'important');
  133. }
  134. }
  135. })();