Keyboard.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {
  2. isFunction
  3. } from 'min-dash';
  4. import {
  5. closest as domClosest,
  6. event as domEvent,
  7. matches as domMatches
  8. } from 'min-dom';
  9. import {
  10. hasModifier,
  11. isCmd,
  12. isKey,
  13. isShift
  14. } from './KeyboardUtil';
  15. var KEYDOWN_EVENT = 'keyboard.keydown',
  16. KEYUP_EVENT = 'keyboard.keyup';
  17. var HANDLE_MODIFIER_ATTRIBUTE = 'input-handle-modified-keys';
  18. var DEFAULT_PRIORITY = 1000;
  19. /**
  20. * A keyboard abstraction that may be activated and
  21. * deactivated by users at will, consuming global key events
  22. * and triggering diagram actions.
  23. *
  24. * For keys pressed down, keyboard fires `keyboard.keydown` event.
  25. * The event context contains one field which is `KeyboardEvent` event.
  26. *
  27. * The implementation fires the following key events that allow
  28. * other components to hook into key handling:
  29. *
  30. * - keyboard.bind
  31. * - keyboard.unbind
  32. * - keyboard.init
  33. * - keyboard.destroy
  34. *
  35. * All events contain one field which is node.
  36. *
  37. * A default binding for the keyboard may be specified via the
  38. * `keyboard.bindTo` configuration option.
  39. *
  40. * @param {Config} config
  41. * @param {EventBus} eventBus
  42. */
  43. export default function Keyboard(config, eventBus) {
  44. var self = this;
  45. this._config = config || {};
  46. this._eventBus = eventBus;
  47. this._keydownHandler = this._keydownHandler.bind(this);
  48. this._keyupHandler = this._keyupHandler.bind(this);
  49. // properly clean dom registrations
  50. eventBus.on('diagram.destroy', function() {
  51. self._fire('destroy');
  52. self.unbind();
  53. });
  54. eventBus.on('diagram.init', function() {
  55. self._fire('init');
  56. });
  57. eventBus.on('attach', function() {
  58. if (config && config.bindTo) {
  59. self.bind(config.bindTo);
  60. }
  61. });
  62. eventBus.on('detach', function() {
  63. self.unbind();
  64. });
  65. }
  66. Keyboard.$inject = [
  67. 'config.keyboard',
  68. 'eventBus'
  69. ];
  70. Keyboard.prototype._keydownHandler = function(event) {
  71. this._keyHandler(event, KEYDOWN_EVENT);
  72. };
  73. Keyboard.prototype._keyupHandler = function(event) {
  74. this._keyHandler(event, KEYUP_EVENT);
  75. };
  76. Keyboard.prototype._keyHandler = function(event, type) {
  77. var eventBusResult;
  78. if (this._isEventIgnored(event)) {
  79. return;
  80. }
  81. var context = {
  82. keyEvent: event
  83. };
  84. eventBusResult = this._eventBus.fire(type || KEYDOWN_EVENT, context);
  85. if (eventBusResult) {
  86. event.preventDefault();
  87. }
  88. };
  89. Keyboard.prototype._isEventIgnored = function(event) {
  90. if (event.defaultPrevented) {
  91. return true;
  92. }
  93. return isInput(event.target) && this._isModifiedKeyIgnored(event);
  94. };
  95. Keyboard.prototype._isModifiedKeyIgnored = function(event) {
  96. if (!isCmd(event)) {
  97. return true;
  98. }
  99. var allowedModifiers = this._getAllowedModifiers(event.target);
  100. return allowedModifiers.indexOf(event.key) === -1;
  101. };
  102. Keyboard.prototype._getAllowedModifiers = function(element) {
  103. var modifierContainer = domClosest(element, '[' + HANDLE_MODIFIER_ATTRIBUTE + ']', true);
  104. if (!modifierContainer || (this._node && !this._node.contains(modifierContainer))) {
  105. return [];
  106. }
  107. return modifierContainer.getAttribute(HANDLE_MODIFIER_ATTRIBUTE).split(',');
  108. };
  109. Keyboard.prototype.bind = function(node) {
  110. // make sure that the keyboard is only bound once to the DOM
  111. this.unbind();
  112. this._node = node;
  113. // bind key events
  114. domEvent.bind(node, 'keydown', this._keydownHandler);
  115. domEvent.bind(node, 'keyup', this._keyupHandler);
  116. this._fire('bind');
  117. };
  118. Keyboard.prototype.getBinding = function() {
  119. return this._node;
  120. };
  121. Keyboard.prototype.unbind = function() {
  122. var node = this._node;
  123. if (node) {
  124. this._fire('unbind');
  125. // unbind key events
  126. domEvent.unbind(node, 'keydown', this._keydownHandler);
  127. domEvent.unbind(node, 'keyup', this._keyupHandler);
  128. }
  129. this._node = null;
  130. };
  131. Keyboard.prototype._fire = function(event) {
  132. this._eventBus.fire('keyboard.' + event, { node: this._node });
  133. };
  134. /**
  135. * Add a listener function that is notified with `KeyboardEvent` whenever
  136. * the keyboard is bound and the user presses a key. If no priority is
  137. * provided, the default value of 1000 is used.
  138. *
  139. * @param {number} [priority]
  140. * @param {Function} listener
  141. * @param {string} type
  142. */
  143. Keyboard.prototype.addListener = function(priority, listener, type) {
  144. if (isFunction(priority)) {
  145. type = listener;
  146. listener = priority;
  147. priority = DEFAULT_PRIORITY;
  148. }
  149. this._eventBus.on(type || KEYDOWN_EVENT, priority, listener);
  150. };
  151. Keyboard.prototype.removeListener = function(listener, type) {
  152. this._eventBus.off(type || KEYDOWN_EVENT, listener);
  153. };
  154. Keyboard.prototype.hasModifier = hasModifier;
  155. Keyboard.prototype.isCmd = isCmd;
  156. Keyboard.prototype.isShift = isShift;
  157. Keyboard.prototype.isKey = isKey;
  158. // helpers ///////
  159. function isInput(target) {
  160. return target && (domMatches(target, 'input, textarea') || target.contentEditable === 'true');
  161. }