Advertisement
B1KMusic

declarative style keyboard input

Jul 24th, 2018
2,196
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // jsfiddle link: https://jsfiddle.net/B1KMusic/ofwa3pq2/embedded/result,js,html,css
  2. // Stackoverflow link: https://stackoverflow.com/a/12444641/1175714
  3.  
  4. // The Keyboard Object:
  5.  
  6. const Keyboard = Object.freeze({
  7.     final: Object.freeze({
  8.         bind_proto: Object.freeze({
  9.             key: null,
  10.             ctrlKey: false,
  11.             altKey: false,
  12.             desc: null,
  13.             callback: null,
  14.         })
  15.     }),
  16.  
  17.     private: Object.seal({
  18.         el: null,
  19.         bindings: null,
  20.         ev_keydown_ptr: null
  21.     }),
  22.  
  23.     public: Object.seal({
  24.         /* (nothing here yet) */
  25.     }),
  26.    
  27.     _mkbind: function(bind){
  28.         let self = this;
  29.  
  30.         return Object.seal({...self.final.bind_proto, ...bind});
  31.     },
  32.    
  33.     _binding_filter: function(search){
  34.         return bind => (
  35.             bind.altKey  === search.altKey &&
  36.             bind.ctrlKey === search.ctrlKey &&
  37.             bind.key     === search.key
  38.         );
  39.     },
  40.    
  41.     _binding_lookup: function(bind){
  42.         let self = this;
  43.         let result = self.private.bindings.find(self._binding_filter(bind));
  44.        
  45.         if(typeof result === "undefined")
  46.             return null;
  47.            
  48.         return result;
  49.     },
  50.    
  51.     _ev_keydown: function(){
  52.         let self = this;
  53.  
  54.         return function(ev){
  55.             let result = self._binding_lookup(ev);
  56.  
  57.             if(result === null)
  58.                 return;
  59.  
  60.             ev.preventDefault();
  61.             result.callback(ev);
  62.         }
  63.     },
  64.    
  65.     _get_label: function(binding){
  66.         let ret = binding.key;
  67.        
  68.         if("ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(binding.key) !== -1)
  69.             ret = "shift-" + ret;
  70.        
  71.         if(binding.ctrlKey)
  72.             ret = "ctrl-" + ret;
  73.        
  74.         if(binding.altKey)
  75.             ret = "alt-" + ret;
  76.        
  77.         return ret;
  78.     },
  79.    
  80.     _pad_left: function(text, width){
  81.         while(text.length < width)
  82.             text = " " + text;
  83.        
  84.         return text;
  85.     },
  86.    
  87.     attach: function(el){
  88.         let self = this;
  89.        
  90.         self.private.ev_keydown_ptr = self._ev_keydown();
  91.         self.private.el = el;
  92.         self.private.el.tabIndex = 0;
  93.         self.private.el.addEventListener("keydown", self.private.ev_keydown_ptr);
  94.         self.private.bindings = [];
  95.     },
  96.    
  97.     detach: function(){
  98.         let self = this;
  99.        
  100.         if(self.private.el === null)
  101.             return;
  102.        
  103.         self.private.el.removeEventListener("keydown", self.private.ev_keydown_ptr);
  104.     },
  105.    
  106.     add_binding: function(bind){
  107.         let self = this;
  108.         let bind_proper = self._mkbind(bind);
  109.         let result = self._binding_lookup(bind_proper);
  110.        
  111.         if(result !== null)
  112.             return false;
  113.        
  114.         self.private.bindings.push(bind_proper);
  115.         return true;
  116.     },
  117.    
  118.     remove_binding: function(bind){
  119.         let self = this;
  120.         let bind_proper = self._mkbind(bind);
  121.         let result = self._binding_lookup(bind_proper);
  122.         let index = self.private.bindings.indexOf(result);
  123.        
  124.         if(result === null || index === -1)
  125.             return false;
  126.  
  127.         self.private.bindings.splice(index, 1);
  128.         return true;
  129.     },
  130.    
  131.     list_bindings: function(){
  132.         let self = this;
  133.         let out = "";
  134.         let labels = self.private.bindings.map(self._get_label);
  135.         let longest = labels.map(l => l.length).reduce((a,b) => a>b?a:b, 0);
  136.        
  137.         labels.map(label => self._pad_left(label, longest)).forEach(function(label, i){
  138.             out += `${label}  ${self.private.bindings[i].desc}\n`;
  139.         });
  140.        
  141.         return out;
  142.     }
  143. });
  144.  
  145. // Our custom client code: (assumes two HTML elements: one with class=input and one with class=output)
  146.  
  147. let inputbox = document.querySelector(".input");
  148. let outputbox = document.querySelector(".output");
  149.  
  150. function log(msg){
  151.     outputbox.innerHTML = msg;
  152. }
  153.  
  154. Keyboard.attach(inputbox);
  155.  
  156. // Here's where the magic is...
  157.  
  158. Keyboard.add_binding({
  159.     key: "d",
  160.     desc: "Notify 'd'",
  161.     callback: function(ev){
  162.         log("You pressed d");
  163.     }
  164. });
  165. Keyboard.add_binding({
  166.     key: "D",
  167.     desc: "Notify 'D'",
  168.     callback: function(ev){
  169.         log("You pressed D (shift-d)");
  170.     }
  171. });
  172. Keyboard.add_binding({
  173.     key: "d",
  174.     ctrlKey: true,
  175.     desc: "Notify '^d'",
  176.     callback: function(ev){
  177.         log("You pressed ^d (ctrl-d)");
  178.     }
  179. });
  180. Keyboard.add_binding({
  181.     key: "D",
  182.     ctrlKey: true,
  183.     desc: "Notify '^D'",
  184.     callback: function(ev){
  185.         log("You pressed ^D (ctrl-shift-d)");
  186.     }
  187. });
  188. Keyboard.add_binding({
  189.     key: "?",
  190.     desc: "Print this help.",
  191.     callback: function(ev){
  192.         log(Keyboard.list_bindings().replace(/\n/g, "<br>"));        
  193.     }
  194. });
  195.  
  196. // At this point, you can click the .input element, hit '?' on your keyboard, and watch the magic unfold.
  197.  
  198. // The difference between this and the Input "helper class" as seen on the SO answer, is that the helper class makes quite a few assumptions and pretty much prevents the browser from doing anything so long as you're focused on the attached element. On top of this, the pressed keys are kept track of in an event handler that you have no control over, which means that it only works in a game loop.
  199.  
  200. // This version is heavily functional in design and embraces the event loop cooked into JS. There is one single event handler which is generated upon attachment, which, instead of shotgun-preventDefault()'ing everything, performs a strict lookup based on what you pressed, matching one and only one binding (if you bind multiple callbacks to the same key signature, only the first-registered one will be executed; the others will be ignored), then executes its callback and prevents default. Also, since the input handling is now data-based, a list of keybindings can be dynamically generated. Further, since the default is only prevented when a callback is found, that means you can focus on an input-taking element, press Ctrl+D (assuming it's bound), and have the browser NOT open the bookmark dialogue while doing whatever Ctrl+D was set to do, and at the same time, you can press Ctrl+L (assuming it's NOT bound), and have the browser focus the Address bar, since that's its default action.
  201.  
  202. // That's not to say that this approach is inherently better (even though it is). If you need to check for input in a game loop independent of event handlers, and you need to be able to press multiple keys at once (including combinations like f+j), then you should probably use something like the helper class, because otherwise, with this approach, you might end up doing something silly, like writing callbacks that set global flags that are used in the gameloop. If you are developing a responsive website/webapp and want to easily and dynamically bind input, then you should use something like this. Note that you could "combine" both approaches and implement multi-key with dynamic binding. It's not that complicated.
  203.  
  204. // Also note that this shouldn't be attached to multiple elements at one time, because it's designed for only one. This is a tech demo, not a library. That's why I say "use something like". `Keyboard`, in this case, is a giant state machine. Essentially a singleton class. It ALSO wouldn't be too terribly complicated to make a "prototype object" to generate "keyboards", or to just define everything in a function the way it's done in the "helper class", such that multiple unique instances can be used at once.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement