8
0

widgetresize.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module widget/widgetresize
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Resizer from './widgetresize/resizer';
  10. import { isWidget } from './utils';
  11. import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
  12. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  13. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  14. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  15. import { throttle } from 'lodash-es';
  16. /**
  17. * The widget resize feature plugin.
  18. *
  19. * Use the {@link module:widget/widgetresize~WidgetResize#attachTo} method to create a resizer for the specified widget.
  20. *
  21. * @extends module:core/plugin~Plugin
  22. * @mixes module:utils/observablemixin~ObservableMixin
  23. */
  24. export default class WidgetResize extends Plugin {
  25. /**
  26. * @inheritDoc
  27. */
  28. static get pluginName() {
  29. return 'WidgetResize';
  30. }
  31. init() {
  32. /**
  33. * A map of resizers created using this plugin instance.
  34. *
  35. * @type {Map.<module:engine/view/containerelement~ContainerElement, module:widget/widgetresize/resizer~Resizer>}
  36. */
  37. this.resizers = new Map();
  38. /**
  39. * @protected
  40. * @observable
  41. * @type {module:widget/widgetresize/resizer~Resizer|null} Currently visible resizer.
  42. */
  43. this.set( '_visibleResizer', null );
  44. /**
  45. * References an active resizer.
  46. *
  47. * Active resizer means a resizer which handle is actively used by the end user.
  48. *
  49. * @protected
  50. * @observable
  51. * @type {module:widget/widgetresize/resizer~Resizer|null}
  52. */
  53. this.set( '_activeResizer', null );
  54. const domDocument = global.window.document;
  55. this.editor.model.schema.setAttributeProperties( 'width', {
  56. isFormatting: true
  57. } );
  58. this._observer = Object.create( DomEmitterMixin );
  59. this._observer.listenTo( domDocument, 'mousedown', ( event, domEventData ) => {
  60. if ( !Resizer.isResizeHandle( domEventData.target ) ) {
  61. return;
  62. }
  63. const resizeHandle = domEventData.target;
  64. this._activeResizer = this._getResizerByHandle( resizeHandle );
  65. if ( this._activeResizer ) {
  66. this._activeResizer.begin( resizeHandle );
  67. }
  68. } );
  69. this._observer.listenTo( domDocument, 'mousemove', throttle( ( event, domEventData ) => {
  70. if ( this._activeResizer ) {
  71. this._activeResizer.updateSize( domEventData );
  72. }
  73. }, 16 ) ); // 60 fps
  74. this._observer.listenTo( domDocument, 'mouseup', () => {
  75. if ( this._activeResizer ) {
  76. this._activeResizer.commit();
  77. this._activeResizer = null;
  78. }
  79. } );
  80. this._attachFocusChangeListener();
  81. const redrawFocusedResizer = throttle( () => {
  82. if ( this._visibleResizer ) {
  83. this._visibleResizer.redraw();
  84. }
  85. }, 200 ); // 5 fps
  86. this.on( 'change:_visibleResizer', redrawFocusedResizer );
  87. // Redrawing on any change of the UI of the editor (including content changes).
  88. this.editor.ui.on( 'update', redrawFocusedResizer );
  89. // Resizers need to be redrawn upon window resize, because new window might shrink resize host.
  90. this._observer.listenTo( global.window, 'resize', redrawFocusedResizer );
  91. }
  92. destroy() {
  93. this._observer.stopListening();
  94. }
  95. /**
  96. * @param {module:widget/widgetresize~ResizerOptions} [options] Resizer options.
  97. * @returns {module:widget/widgetresize/resizer~Resizer}
  98. */
  99. attachTo( options ) {
  100. const resizer = new Resizer( options );
  101. resizer.attach();
  102. this.resizers.set( options.viewElement, resizer );
  103. return resizer;
  104. }
  105. /**
  106. * Listens for selection change and sets the visible resizer accordingly.
  107. *
  108. * @private
  109. */
  110. _attachFocusChangeListener() {
  111. this.editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  112. const viewWriter = conversionApi.writer;
  113. let lastMarked = null;
  114. let matchedResizer = null;
  115. for ( const range of viewWriter.document.selection.getRanges() ) {
  116. for ( const value of range ) {
  117. const node = value.item;
  118. // Do not mark nested widgets in selected one. See: ckeditor/ckeditor5-widget#57.
  119. if ( isWidget( node ) && !isChild( node, lastMarked ) && node.hasClass( 'ck-widget_with-resizer' ) ) {
  120. matchedResizer = this.resizers.get( node ) || matchedResizer;
  121. lastMarked = node;
  122. }
  123. }
  124. }
  125. this._visibleResizer = matchedResizer;
  126. }, { priority: 'low' } );
  127. // Checks whether the specified `element` is a child of the `parent` element.
  128. //
  129. // @param {module:engine/view/element~Element} element An element to check.
  130. // @param {module:engine/view/element~Element|null} parent A parent for the element.
  131. // @returns {Boolean}
  132. function isChild( element, parent ) {
  133. if ( !parent ) {
  134. return false;
  135. }
  136. return Array.from( element.getAncestors() ).includes( parent );
  137. }
  138. }
  139. _getResizerByHandle( domResizeHandle ) {
  140. for ( const resizer of this.resizers.values() ) {
  141. if ( resizer.containsHandle( domResizeHandle ) ) {
  142. return resizer;
  143. }
  144. }
  145. }
  146. }
  147. mix( WidgetResize, ObservableMixin );
  148. /**
  149. * Interface describing a resizer. It allows to specify the resizing host, custom logic for calculating aspect ratio, etc.
  150. *
  151. * @interface ResizerOptions
  152. */
  153. /**
  154. * @member {module:engine/model/element~Element} module:widget/widgetresize~ResizerOptions#modelElement
  155. */
  156. /**
  157. * @member {module:engine/view/containerelement~ContainerElement} module:widget/widgetresize~ResizerOptions#viewElement
  158. */
  159. /**
  160. * @member {module:engine/view/downcastwriter~DowncastWriter} module:widget/widgetresize~ResizerOptions#downcastWriter
  161. */
  162. /**
  163. * A callback to be executed once the resizing process is done.
  164. *
  165. * It receives a `Number` (`newValue`) as a parameter.
  166. *
  167. * For example, {@link module:image/imageresize~ImageResize} uses it to execute the image resize command
  168. * which puts the new value into the model.
  169. *
  170. * ```js
  171. * {
  172. * modelElement: data.item,
  173. * viewElement: widget,
  174. * downcastWriter: conversionApi.writer,
  175. *
  176. * onCommit( newValue ) {
  177. * editor.execute( 'imageResize', { width: newValue } );
  178. * }
  179. * };
  180. * ```
  181. *
  182. *
  183. * @member {Function} module:widget/widgetresize~ResizerOptions#onCommit
  184. */
  185. /**
  186. * @member {Function} module:widget/widgetresize~ResizerOptions#getResizeHost
  187. */
  188. /**
  189. * @member {Function} module:widget/widgetresize~ResizerOptions#isCentered
  190. */