hash_handler.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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. var keyUtil = require("../lib/keys");
  33. var useragent = require("../lib/useragent");
  34. var KEY_MODS = keyUtil.KEY_MODS;
  35. function HashHandler(config, platform) {
  36. this.platform = platform || (useragent.isMac ? "mac" : "win");
  37. this.commands = {};
  38. this.commandKeyBinding = {};
  39. this.addCommands(config);
  40. this.$singleCommand = true;
  41. }
  42. function MultiHashHandler(config, platform) {
  43. HashHandler.call(this, config, platform);
  44. this.$singleCommand = false;
  45. }
  46. MultiHashHandler.prototype = HashHandler.prototype;
  47. (function() {
  48. this.addCommand = function(command) {
  49. if (this.commands[command.name])
  50. this.removeCommand(command);
  51. this.commands[command.name] = command;
  52. if (command.bindKey)
  53. this._buildKeyHash(command);
  54. };
  55. this.removeCommand = function(command, keepCommand) {
  56. var name = command && (typeof command === 'string' ? command : command.name);
  57. command = this.commands[name];
  58. if (!keepCommand)
  59. delete this.commands[name];
  60. // exhaustive search is brute force but since removeCommand is
  61. // not a performance critical operation this should be OK
  62. var ckb = this.commandKeyBinding;
  63. for (var keyId in ckb) {
  64. var cmdGroup = ckb[keyId];
  65. if (cmdGroup == command) {
  66. delete ckb[keyId];
  67. } else if (Array.isArray(cmdGroup)) {
  68. var i = cmdGroup.indexOf(command);
  69. if (i != -1) {
  70. cmdGroup.splice(i, 1);
  71. if (cmdGroup.length == 1)
  72. ckb[keyId] = cmdGroup[0];
  73. }
  74. }
  75. }
  76. };
  77. this.bindKey = function(key, command, position) {
  78. if (typeof key == "object" && key) {
  79. if (position == undefined)
  80. position = key.position;
  81. key = key[this.platform];
  82. }
  83. if (!key)
  84. return;
  85. if (typeof command == "function")
  86. return this.addCommand({exec: command, bindKey: key, name: command.name || key});
  87. key.split("|").forEach(function(keyPart) {
  88. var chain = "";
  89. if (keyPart.indexOf(" ") != -1) {
  90. var parts = keyPart.split(/\s+/);
  91. keyPart = parts.pop();
  92. parts.forEach(function(keyPart) {
  93. var binding = this.parseKeys(keyPart);
  94. var id = KEY_MODS[binding.hashId] + binding.key;
  95. chain += (chain ? " " : "") + id;
  96. this._addCommandToBinding(chain, "chainKeys");
  97. }, this);
  98. chain += " ";
  99. }
  100. var binding = this.parseKeys(keyPart);
  101. var id = KEY_MODS[binding.hashId] + binding.key;
  102. this._addCommandToBinding(chain + id, command, position);
  103. }, this);
  104. };
  105. function getPosition(command) {
  106. return typeof command == "object" && command.bindKey
  107. && command.bindKey.position || 0;
  108. }
  109. this._addCommandToBinding = function(keyId, command, position) {
  110. var ckb = this.commandKeyBinding, i;
  111. if (!command) {
  112. delete ckb[keyId];
  113. } else if (!ckb[keyId] || this.$singleCommand) {
  114. ckb[keyId] = command;
  115. } else {
  116. if (!Array.isArray(ckb[keyId])) {
  117. ckb[keyId] = [ckb[keyId]];
  118. } else if ((i = ckb[keyId].indexOf(command)) != -1) {
  119. ckb[keyId].splice(i, 1);
  120. }
  121. if (typeof position != "number") {
  122. if (position || command.isDefault)
  123. position = -100;
  124. else
  125. position = getPosition(command);
  126. }
  127. var commands = ckb[keyId];
  128. for (i = 0; i < commands.length; i++) {
  129. var other = commands[i];
  130. var otherPos = getPosition(other);
  131. if (otherPos > position)
  132. break;
  133. }
  134. commands.splice(i, 0, command);
  135. }
  136. };
  137. this.addCommands = function(commands) {
  138. commands && Object.keys(commands).forEach(function(name) {
  139. var command = commands[name];
  140. if (!command)
  141. return;
  142. if (typeof command === "string")
  143. return this.bindKey(command, name);
  144. if (typeof command === "function")
  145. command = { exec: command };
  146. if (typeof command !== "object")
  147. return;
  148. if (!command.name)
  149. command.name = name;
  150. this.addCommand(command);
  151. }, this);
  152. };
  153. this.removeCommands = function(commands) {
  154. Object.keys(commands).forEach(function(name) {
  155. this.removeCommand(commands[name]);
  156. }, this);
  157. };
  158. this.bindKeys = function(keyList) {
  159. Object.keys(keyList).forEach(function(key) {
  160. this.bindKey(key, keyList[key]);
  161. }, this);
  162. };
  163. this._buildKeyHash = function(command) {
  164. this.bindKey(command.bindKey, command);
  165. };
  166. // accepts keys in the form ctrl+Enter or ctrl-Enter
  167. // keys without modifiers or shift only
  168. this.parseKeys = function(keys) {
  169. var parts = keys.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(x){return x});
  170. var key = parts.pop();
  171. var keyCode = keyUtil[key];
  172. if (keyUtil.FUNCTION_KEYS[keyCode])
  173. key = keyUtil.FUNCTION_KEYS[keyCode].toLowerCase();
  174. else if (!parts.length)
  175. return {key: key, hashId: -1};
  176. else if (parts.length == 1 && parts[0] == "shift")
  177. return {key: key.toUpperCase(), hashId: -1};
  178. var hashId = 0;
  179. for (var i = parts.length; i--;) {
  180. var modifier = keyUtil.KEY_MODS[parts[i]];
  181. if (modifier == null) {
  182. if (typeof console != "undefined")
  183. console.error("invalid modifier " + parts[i] + " in " + keys);
  184. return false;
  185. }
  186. hashId |= modifier;
  187. }
  188. return {key: key, hashId: hashId};
  189. };
  190. this.findKeyCommand = function findKeyCommand(hashId, keyString) {
  191. var key = KEY_MODS[hashId] + keyString;
  192. return this.commandKeyBinding[key];
  193. };
  194. this.handleKeyboard = function(data, hashId, keyString, keyCode) {
  195. if (keyCode < 0) return;
  196. var key = KEY_MODS[hashId] + keyString;
  197. var command = this.commandKeyBinding[key];
  198. if (data.$keyChain) {
  199. data.$keyChain += " " + key;
  200. command = this.commandKeyBinding[data.$keyChain] || command;
  201. }
  202. if (command) {
  203. if (command == "chainKeys" || command[command.length - 1] == "chainKeys") {
  204. data.$keyChain = data.$keyChain || key;
  205. return {command: "null"};
  206. }
  207. }
  208. if (data.$keyChain) {
  209. if ((!hashId || hashId == 4) && keyString.length == 1)
  210. data.$keyChain = data.$keyChain.slice(0, -key.length - 1); // wait for input
  211. else if (hashId == -1 || keyCode > 0)
  212. data.$keyChain = ""; // reset keyChain
  213. }
  214. return {command: command};
  215. };
  216. this.getStatusText = function(editor, data) {
  217. return data.$keyChain || "";
  218. };
  219. }).call(HashHandler.prototype);
  220. exports.HashHandler = HashHandler;
  221. exports.MultiHashHandler = MultiHashHandler;
  222. });