Skip to content

Commit 06481f2

Browse files
committed
feat(search): Add keyboard support.
1 parent 4906d0e commit 06481f2

File tree

3 files changed

+306
-1
lines changed

3 files changed

+306
-1
lines changed

assets/key.txt.js

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
;(function(global){
2+
var k,
3+
_handlers = {},
4+
_mods = { 16: false, 18: false, 17: false, 91: false },
5+
_scope = 'all',
6+
// modifier keys
7+
_MODIFIERS = {
8+
'⇧': 16, shift: 16,
9+
'⌥': 18, alt: 18, option: 18,
10+
'⌃': 17, ctrl: 17, control: 17,
11+
'⌘': 91, command: 91
12+
},
13+
// special keys
14+
_MAP = {
15+
backspace: 8, tab: 9, clear: 12,
16+
enter: 13, 'return': 13,
17+
esc: 27, escape: 27, space: 32,
18+
left: 37, up: 38,
19+
right: 39, down: 40,
20+
del: 46, 'delete': 46,
21+
home: 36, end: 35,
22+
pageup: 33, pagedown: 34,
23+
',': 188, '.': 190, '/': 191,
24+
'`': 192, '-': 189, '=': 187,
25+
';': 186, '\'': 222,
26+
'[': 219, ']': 221, '\\': 220
27+
},
28+
code = function(x){
29+
return _MAP[x] || x.toUpperCase().charCodeAt(0);
30+
},
31+
_downKeys = [];
32+
33+
for(k=1;k<20;k++) _MAP['f'+k] = 111+k;
34+
35+
// IE doesn't support Array#indexOf, so have a simple replacement
36+
function index(array, item){
37+
var i = array.length;
38+
while(i--) if(array[i]===item) return i;
39+
return -1;
40+
}
41+
42+
// for comparing mods before unassignment
43+
function compareArray(a1, a2) {
44+
if (a1.length != a2.length) return false;
45+
for (var i = 0; i < a1.length; i++) {
46+
if (a1[i] !== a2[i]) return false;
47+
}
48+
return true;
49+
}
50+
51+
var modifierMap = {
52+
16:'shiftKey',
53+
18:'altKey',
54+
17:'ctrlKey',
55+
91:'metaKey'
56+
};
57+
function updateModifierKey(event) {
58+
for(k in _mods) _mods[k] = event[modifierMap[k]];
59+
};
60+
61+
// handle keydown event
62+
function dispatch(event) {
63+
var key, handler, k, i, modifiersMatch, scope;
64+
key = event.keyCode;
65+
66+
if (index(_downKeys, key) == -1) {
67+
_downKeys.push(key);
68+
}
69+
70+
// if a modifier key, set the key.<modifierkeyname> property to true and return
71+
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
72+
if(key in _mods) {
73+
_mods[key] = true;
74+
// 'assignKey' from inside this closure is exported to window.key
75+
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true;
76+
return;
77+
}
78+
updateModifierKey(event);
79+
80+
// see if we need to ignore the keypress (filter() can can be overridden)
81+
// by default ignore key presses if a select, textarea, or input is focused
82+
if(!assignKey.filter.call(this, event)) return;
83+
84+
// abort if no potentially matching shortcuts found
85+
if (!(key in _handlers)) return;
86+
87+
scope = getScope();
88+
89+
// for each potential shortcut
90+
for (i = 0; i < _handlers[key].length; i++) {
91+
handler = _handlers[key][i];
92+
93+
// see if it's in the current scope
94+
if(handler.scope == scope || handler.scope == 'all'){
95+
// check if modifiers match if any
96+
modifiersMatch = handler.mods.length > 0;
97+
for(k in _mods)
98+
if((!_mods[k] && index(handler.mods, +k) > -1) ||
99+
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false;
100+
// call the handler and stop the event if neccessary
101+
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){
102+
if(handler.method(event, handler)===false){
103+
if(event.preventDefault) event.preventDefault();
104+
else event.returnValue = false;
105+
if(event.stopPropagation) event.stopPropagation();
106+
if(event.cancelBubble) event.cancelBubble = true;
107+
}
108+
}
109+
}
110+
}
111+
};
112+
113+
// unset modifier keys on keyup
114+
function clearModifier(event){
115+
var key = event.keyCode, k,
116+
i = index(_downKeys, key);
117+
118+
// remove key from _downKeys
119+
if (i >= 0) {
120+
_downKeys.splice(i, 1);
121+
}
122+
123+
if(key == 93 || key == 224) key = 91;
124+
if(key in _mods) {
125+
_mods[key] = false;
126+
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false;
127+
}
128+
};
129+
130+
function resetModifiers() {
131+
for(k in _mods) _mods[k] = false;
132+
for(k in _MODIFIERS) assignKey[k] = false;
133+
};
134+
135+
// parse and assign shortcut
136+
function assignKey(key, scope, method){
137+
var keys, mods;
138+
keys = getKeys(key);
139+
if (method === undefined) {
140+
method = scope;
141+
scope = 'all';
142+
}
143+
144+
// for each shortcut
145+
for (var i = 0; i < keys.length; i++) {
146+
// set modifier keys if any
147+
mods = [];
148+
key = keys[i].split('+');
149+
if (key.length > 1){
150+
mods = getMods(key);
151+
key = [key[key.length-1]];
152+
}
153+
// convert to keycode and...
154+
key = key[0]
155+
key = code(key);
156+
// ...store handler
157+
if (!(key in _handlers)) _handlers[key] = [];
158+
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods });
159+
}
160+
};
161+
162+
// unbind all handlers for given key in current scope
163+
function unbindKey(key, scope) {
164+
var multipleKeys, keys,
165+
mods = [],
166+
i, j, obj;
167+
168+
multipleKeys = getKeys(key);
169+
170+
for (j = 0; j < multipleKeys.length; j++) {
171+
keys = multipleKeys[j].split('+');
172+
173+
if (keys.length > 1) {
174+
mods = getMods(keys);
175+
}
176+
177+
key = keys[keys.length - 1];
178+
key = code(key);
179+
180+
if (scope === undefined) {
181+
scope = getScope();
182+
}
183+
if (!_handlers[key]) {
184+
return;
185+
}
186+
for (i = 0; i < _handlers[key].length; i++) {
187+
obj = _handlers[key][i];
188+
// only clear handlers if correct scope and mods match
189+
if (obj.scope === scope && compareArray(obj.mods, mods)) {
190+
_handlers[key][i] = {};
191+
}
192+
}
193+
}
194+
};
195+
196+
// Returns true if the key with code 'keyCode' is currently down
197+
// Converts strings into key codes.
198+
function isPressed(keyCode) {
199+
if (typeof(keyCode)=='string') {
200+
keyCode = code(keyCode);
201+
}
202+
return index(_downKeys, keyCode) != -1;
203+
}
204+
205+
function getPressedKeyCodes() {
206+
return _downKeys.slice(0);
207+
}
208+
209+
function filter(event) {
210+
if ((event.target || event.srcElement).classList?.contains('search-bar')) {
211+
return true;
212+
}
213+
var tagName = (event.target || event.srcElement).tagName;
214+
// ignore keypressed in any elements that support keyboard data input
215+
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
216+
}
217+
218+
// initialize key.<modifier> to false
219+
for(k in _MODIFIERS) assignKey[k] = false;
220+
221+
// set current scope (default 'all')
222+
function setScope(scope){ _scope = scope || 'all' };
223+
function getScope(){ return _scope || 'all' };
224+
225+
// delete all handlers for a given scope
226+
function deleteScope(scope){
227+
var key, handlers, i;
228+
229+
for (key in _handlers) {
230+
handlers = _handlers[key];
231+
for (i = 0; i < handlers.length; ) {
232+
if (handlers[i].scope === scope) handlers.splice(i, 1);
233+
else i++;
234+
}
235+
}
236+
};
237+
238+
// abstract key logic for assign and unassign
239+
function getKeys(key) {
240+
var keys;
241+
key = key.replace(/\s/g, '');
242+
keys = key.split(',');
243+
if ((keys[keys.length - 1]) == '') {
244+
keys[keys.length - 2] += ',';
245+
}
246+
return keys;
247+
}
248+
249+
// abstract mods logic for assign and unassign
250+
function getMods(key) {
251+
var mods = key.slice(0, key.length - 1);
252+
for (var mi = 0; mi < mods.length; mi++)
253+
mods[mi] = _MODIFIERS[mods[mi]];
254+
return mods;
255+
}
256+
257+
// cross-browser events
258+
function addEvent(object, event, method) {
259+
if (object.addEventListener)
260+
object.addEventListener(event, method, false);
261+
else if(object.attachEvent)
262+
object.attachEvent('on'+event, function(){ method(window.event) });
263+
};
264+
265+
// set the handlers globally on document
266+
addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
267+
addEvent(document, 'keyup', clearModifier);
268+
269+
// reset modifiers to false whenever the window is (re)focused.
270+
addEvent(window, 'focus', resetModifiers);
271+
272+
// store previously defined key
273+
var previousKey = global.key;
274+
275+
// restore previously defined key and return reference to our key object
276+
function noConflict() {
277+
var k = global.key;
278+
global.key = previousKey;
279+
return k;
280+
}
281+
282+
// set window.key and window.key.set/get/deleteScope, and the default filter
283+
global.key = assignKey;
284+
global.key.setScope = setScope;
285+
global.key.getScope = getScope;
286+
global.key.deleteScope = deleteScope;
287+
global.key.filter = filter;
288+
global.key.isPressed = isPressed;
289+
global.key.getPressedKeyCodes = getPressedKeyCodes;
290+
global.key.noConflict = noConflict;
291+
global.key.unbind = unbindKey;
292+
293+
if(typeof module !== 'undefined') module.exports = assignKey;
294+
295+
})(this);

assets/webpage.util.txt.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ class SuggestionsList {
297297
// 注册点击、鼠标移入事件
298298
this.containerEl.addEventListener("click", this.onSuggestionClick.bind(this));
299299
this.containerEl.addEventListener("mousemove", this.onSuggestionMouseover.bind(this));
300+
key('up', this.moveUp.bind(this));
301+
key('down', this.moveDown.bind(this));
302+
key('enter', this.onEnter.bind(this));
300303
}
301304

302305
moveUp(event) {
@@ -319,6 +322,10 @@ class SuggestionsList {
319322
}
320323
}
321324

325+
onEnter(event) {
326+
this.useSelectedItem(event);
327+
}
328+
322329
setSuggestions(suggestions) {
323330
while (this.containerEl.firstChild) {
324331
this.containerEl.removeChild(this.containerEl.firstChild);
@@ -501,6 +508,7 @@ class SearchView {
501508
this.updateSearch();
502509
});
503510
document.addEventListener("click", this.onDocumentClick.bind(this))
511+
key('esc', this.onDocumentClick.bind(this));
504512
}
505513

506514
addMessage(text) {

src/html-generation/asset-handler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import graphWASM from "assets/graph_wasm.wasm";
77

88
import tinyColorJS from "assets/tinycolor.txt.js";
99
// @ts-ignore
10+
import keyJS from 'assets/key.txt.js';
11+
// @ts-ignore
1012
import webpageUtilJS from 'assets/webpage.util.txt.js';
1113
// @ts-ignore
1214
import webpageJS from "assets/webpage.txt.js";
@@ -60,7 +62,7 @@ export class AssetHandler
6062

6163
await this.loadAppStyles();
6264
this.webpageStyles = webpageStyles;
63-
this.webpageJS = webpageUtilJS + ';' + webpageJS;
65+
this.webpageJS = keyJS + ';' + webpageUtilJS + ';' + webpageJS;
6466
this.graphViewJS = graphViewJS;
6567
this.graphWASMJS = graphWASMJS;
6668
this.renderWorkerJS = renderWorkerJS;

0 commit comments

Comments
 (0)