8
0

widget.js 8.0 KB

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