state_handler.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. // If you're developing a new keymapping and want to get an idea what's going
  33. // on, then enable debugging.
  34. var DEBUG = false;
  35. function StateHandler(keymapping) {
  36. this.keymapping = this.$buildKeymappingRegex(keymapping);
  37. }
  38. StateHandler.prototype = {
  39. /*
  40. * Build the RegExp from the keymapping as RegExp can't stored directly
  41. * in the metadata JSON and as the RegExp used to match the keys/buffer
  42. * need to be adapted.
  43. */
  44. $buildKeymappingRegex: function(keymapping) {
  45. for (var state in keymapping) {
  46. this.$buildBindingsRegex(keymapping[state]);
  47. }
  48. return keymapping;
  49. },
  50. $buildBindingsRegex: function(bindings) {
  51. // Escape a given Regex string.
  52. bindings.forEach(function(binding) {
  53. if (binding.key) {
  54. binding.key = new RegExp('^' + binding.key + '$');
  55. } else if (Array.isArray(binding.regex)) {
  56. if (!('key' in binding))
  57. binding.key = new RegExp('^' + binding.regex[1] + '$');
  58. binding.regex = new RegExp(binding.regex.join('') + '$');
  59. } else if (binding.regex) {
  60. binding.regex = new RegExp(binding.regex + '$');
  61. }
  62. });
  63. },
  64. $composeBuffer: function(data, hashId, key, e) {
  65. // Initialize the data object.
  66. if (data.state == null || data.buffer == null) {
  67. data.state = "start";
  68. data.buffer = "";
  69. }
  70. var keyArray = [];
  71. if (hashId & 1) keyArray.push("ctrl");
  72. if (hashId & 8) keyArray.push("command");
  73. if (hashId & 2) keyArray.push("option");
  74. if (hashId & 4) keyArray.push("shift");
  75. if (key) keyArray.push(key);
  76. var symbolicName = keyArray.join("-");
  77. var bufferToUse = data.buffer + symbolicName;
  78. // Don't add the symbolic name to the key buffer if the alt_ key is
  79. // part of the symbolic name. If it starts with alt_, this means
  80. // that the user hit an alt keycombo and there will be a single,
  81. // new character detected after this event, which then will be
  82. // added to the buffer (e.g. alt_j will result in ∆).
  83. //
  84. // We test for 2 and not for & 2 as we only want to exclude the case where
  85. // the option key is pressed alone.
  86. if (hashId != 2) {
  87. data.buffer = bufferToUse;
  88. }
  89. var bufferObj = {
  90. bufferToUse: bufferToUse,
  91. symbolicName: symbolicName
  92. };
  93. if (e) {
  94. bufferObj.keyIdentifier = e.keyIdentifier;
  95. }
  96. return bufferObj;
  97. },
  98. $find: function(data, buffer, symbolicName, hashId, key, keyIdentifier) {
  99. // Holds the command to execute and the args if a command matched.
  100. var result = {};
  101. // Loop over all the bindings of the keymap until a match is found.
  102. this.keymapping[data.state].some(function(binding) {
  103. var match;
  104. // Check if the key matches.
  105. if (binding.key && !binding.key.test(symbolicName)) {
  106. return false;
  107. }
  108. // Check if the regex matches.
  109. if (binding.regex && !(match = binding.regex.exec(buffer))) {
  110. return false;
  111. }
  112. // Check if the match function matches.
  113. if (binding.match && !binding.match(buffer, hashId, key, symbolicName, keyIdentifier)) {
  114. return false;
  115. }
  116. // Check for disallowed matches.
  117. if (binding.disallowMatches) {
  118. for (var i = 0; i < binding.disallowMatches.length; i++) {
  119. if (!!match[binding.disallowMatches[i]]) {
  120. return false;
  121. }
  122. }
  123. }
  124. // If there is a command to execute, then figure out the
  125. // command and the arguments.
  126. if (binding.exec) {
  127. result.command = binding.exec;
  128. // Build the arguments.
  129. if (binding.params) {
  130. var value;
  131. result.args = {};
  132. binding.params.forEach(function(param) {
  133. if (param.match != null && match != null) {
  134. value = match[param.match] || param.defaultValue;
  135. } else {
  136. value = param.defaultValue;
  137. }
  138. if (param.type === 'number') {
  139. value = parseInt(value);
  140. }
  141. result.args[param.name] = value;
  142. });
  143. }
  144. data.buffer = "";
  145. }
  146. // Handle the 'then' property.
  147. if (binding.then) {
  148. data.state = binding.then;
  149. data.buffer = "";
  150. }
  151. // If no command is set, then execute the "null" fake command.
  152. if (result.command == null) {
  153. result.command = "null";
  154. }
  155. if (DEBUG) {
  156. console.log("KeyboardStateMapper#find", binding);
  157. }
  158. return true;
  159. });
  160. if (result.command) {
  161. return result;
  162. } else {
  163. data.buffer = "";
  164. return false;
  165. }
  166. },
  167. /*
  168. * This function is called by keyBinding.
  169. */
  170. handleKeyboard: function(data, hashId, key, keyCode, e) {
  171. if (hashId == -1)
  172. hashId = 0
  173. // If we pressed any command key but no other key, then ignore the input.
  174. // Otherwise "shift-" is added to the buffer, and later on "shift-g"
  175. // which results in "shift-shift-g" which doesn't make sense.
  176. if (hashId != 0 && (key == "" || key == String.fromCharCode(0))) {
  177. return null;
  178. }
  179. // Compute the current value of the keyboard input buffer.
  180. var r = this.$composeBuffer(data, hashId, key, e);
  181. var buffer = r.bufferToUse;
  182. var symbolicName = r.symbolicName;
  183. var keyId = r.keyIdentifier;
  184. r = this.$find(data, buffer, symbolicName, hashId, key, keyId);
  185. if (DEBUG) {
  186. console.log("KeyboardStateMapper#match", buffer, symbolicName, r);
  187. }
  188. return r;
  189. }
  190. }
  191. /*
  192. * This is a useful matching function and therefore is defined here so that
  193. * users of KeyboardStateMapper can use it.
  194. *
  195. * @return {Boolean} If no command key (Command|Option|Shift|Ctrl) is pressed, it
  196. * returns true. If the only the Shift key is pressed + a character
  197. * true is returned as well. Otherwise, false is returned.
  198. * Summing up, the function returns true whenever the user typed
  199. * a normal character on the keyboard and no shortcut.
  200. */
  201. exports.matchCharacterOnly = function(buffer, hashId, key, symbolicName) {
  202. // If no command keys are pressed, then catch the input.
  203. if (hashId == 0) {
  204. return true;
  205. }
  206. // If only the shift key is pressed and a character key, then
  207. // catch that input as well.
  208. else if ((hashId == 4) && key.length == 1) {
  209. return true;
  210. }
  211. // Otherwise, we let the input got through.
  212. else {
  213. return false;
  214. }
  215. };
  216. exports.StateHandler = StateHandler;
  217. });