widget.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module image/widget/widget
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import WidgetEngine from './widgetengine';
  10. import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
  11. import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
  12. import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
  13. import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
  14. import { isWidget } from './utils';
  15. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  16. /**
  17. * The widget plugin.
  18. * Adds default {@link module:engine/view/document~Document#event:mousedown mousedown} handling on widget elements.
  19. *
  20. * @extends module:core/plugin~Plugin.
  21. */
  22. export default class Widget extends Plugin {
  23. /**
  24. * @inheritDoc
  25. */
  26. static get requires() {
  27. return [ WidgetEngine ];
  28. }
  29. /**
  30. * @inheritDoc
  31. */
  32. init() {
  33. const viewDocument = this.editor.editing.view;
  34. // If mouse down is pressed on widget - create selection over whole widget.
  35. viewDocument.addObserver( MouseObserver );
  36. this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
  37. // Handle custom keydown behaviour.
  38. this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
  39. }
  40. /**
  41. * Handles {@link module:engine/view/document~Document#event:mousedown mousedown} events on widget elements.
  42. *
  43. * @private
  44. * @param {module:utils/eventinfo~EventInfo} eventInfo
  45. * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
  46. */
  47. _onMousedown( eventInfo, domEventData ) {
  48. let widgetElement = domEventData.target;
  49. const editor = this.editor;
  50. const viewDocument = editor.editing.view;
  51. // If target is not a widget element - check if one of the ancestors is.
  52. if ( !isWidget( widgetElement ) ) {
  53. widgetElement = widgetElement.findAncestor( element => isWidget( element ) );
  54. if ( !widgetElement ) {
  55. return;
  56. }
  57. }
  58. domEventData.preventDefault();
  59. // Focus editor if is not focused already.
  60. if ( !viewDocument.isFocused ) {
  61. viewDocument.focus();
  62. }
  63. // Create model selection over widget.
  64. const modelElement = editor.editing.mapper.toModelElement( widgetElement );
  65. editor.document.enqueueChanges( ( ) => {
  66. this._setSelectionOverElement( modelElement );
  67. } );
  68. }
  69. /**
  70. * Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
  71. *
  72. * @private
  73. * @param {module:utils/eventinfo~EventInfo} eventInfo
  74. * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
  75. */
  76. _onKeydown( eventInfo, domEventData ) {
  77. const keyCode = domEventData.keyCode;
  78. const isForward = keyCode == keyCodes.delete || keyCode == keyCodes.arrowdown || keyCode == keyCodes.arrowright;
  79. // Checks if delete/backspace or arrow keys were handled and then prevents default event behaviour and stops
  80. // event propagation.
  81. if ( ( isDeleteKeyCode( keyCode ) && this._handleDelete( isForward ) ) ||
  82. ( isArrowKeyCode( keyCode ) && this._handleArrowKeys( isForward ) ) ) {
  83. domEventData.preventDefault();
  84. eventInfo.stop();
  85. }
  86. }
  87. /**
  88. * Handles delete keys: backspace and delete.
  89. *
  90. * @private
  91. * @param {Boolean} isForward Set to true if delete was performed in forward direction.
  92. * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
  93. */
  94. _handleDelete( isForward ) {
  95. const modelDocument = this.editor.document;
  96. const modelSelection = modelDocument.selection;
  97. // Do nothing on non-collapsed selection.
  98. if ( !modelSelection.isCollapsed ) {
  99. return;
  100. }
  101. const objectElement = this._getObjectElementNextToSelection( isForward );
  102. if ( objectElement ) {
  103. modelDocument.enqueueChanges( () => {
  104. // Remove previous element if empty.
  105. const previousNode = modelSelection.anchor.parent;
  106. if ( previousNode.isEmpty ) {
  107. const batch = modelDocument.batch();
  108. batch.remove( previousNode );
  109. }
  110. this._setSelectionOverElement( objectElement );
  111. } );
  112. return true;
  113. }
  114. }
  115. /**
  116. * Handles arrow keys.
  117. *
  118. * @param {Boolean} isForward Set to true if arrow key should be handled in forward direction.
  119. * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
  120. */
  121. _handleArrowKeys( isForward ) {
  122. const modelDocument = this.editor.document;
  123. const schema = modelDocument.schema;
  124. const modelSelection = modelDocument.selection;
  125. const objectElement = modelSelection.getSelectedElement();
  126. // if object element is selected.
  127. if ( objectElement && schema.objects.has( objectElement.name ) ) {
  128. const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
  129. const newRange = modelDocument.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
  130. if ( newRange ) {
  131. modelDocument.enqueueChanges( () => {
  132. modelSelection.setRanges( [ newRange ] );
  133. } );
  134. }
  135. return true;
  136. }
  137. // If selection is next to object element.
  138. // Return if not collapsed.
  139. if ( !modelSelection.isCollapsed ) {
  140. return;
  141. }
  142. const objectElement2 = this._getObjectElementNextToSelection( isForward );
  143. if ( objectElement2 instanceof ModelElement && modelDocument.schema.objects.has( objectElement2.name ) ) {
  144. modelDocument.enqueueChanges( () => {
  145. this._setSelectionOverElement( objectElement2 );
  146. } );
  147. return true;
  148. }
  149. }
  150. /**
  151. * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
  152. *
  153. * @private
  154. * @param {module:engine/model/element~Element} element
  155. */
  156. _setSelectionOverElement( element ) {
  157. this.editor.document.selection.setRanges( [ ModelRange.createOn( element ) ] );
  158. }
  159. /**
  160. * Checks if {@link module:engine/model/element~Element element} placed next to the current
  161. * {@link module:engine/model/selection~Selection model selection} exists and is marked in
  162. * {@link module:engine/model/schema~Schema schema} as `object`.
  163. *
  164. * @private
  165. * @param {Boolean} forward Direction of checking.
  166. * @returns {module:engine/model/element~Element|null}
  167. */
  168. _getObjectElementNextToSelection( forward ) {
  169. const modelDocument = this.editor.document;
  170. const schema = modelDocument.schema;
  171. const modelSelection = modelDocument.selection;
  172. const dataController = this.editor.data;
  173. // Clone current selection to use it as a probe. We must leave default selection as it is so it can return
  174. // to its current state after undo.
  175. const probe = ModelSelection.createFromSelection( modelSelection );
  176. dataController.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } );
  177. const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
  178. if ( objectElement instanceof ModelElement && schema.objects.has( objectElement.name ) ) {
  179. return objectElement;
  180. }
  181. return null;
  182. }
  183. }
  184. // Returns 'true' if provided key code represents one of the arrow keys.
  185. //
  186. // @param {Number} keyCode
  187. // @returns {Boolean}
  188. function isArrowKeyCode( keyCode ) {
  189. return keyCode == keyCodes.arrowright ||
  190. keyCode == keyCodes.arrowleft ||
  191. keyCode == keyCodes.arrowup ||
  192. keyCode == keyCodes.arrowdown;
  193. }
  194. //Returns 'true' if provided key code represents one of the delete keys: delete or backspace.
  195. //
  196. //@param {Number} keyCode
  197. //@returns {Boolean}
  198. function isDeleteKeyCode( keyCode ) {
  199. return keyCode == keyCodes.delete || keyCode == keyCodes.backspace;
  200. }