diff --git a/src/widgetresize.js b/src/widgetresize.js index c3d6c0b2..cd1ebae4 100644 --- a/src/widgetresize.js +++ b/src/widgetresize.js @@ -11,6 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Resizer from './widgetresize/resizer'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; import { throttle } from 'lodash-es'; import '../theme/widgetresize.css'; @@ -21,6 +23,7 @@ import '../theme/widgetresize.css'; * Use the {@link module:widget/widgetresize~WidgetResize#attachTo} method to create a resizer for the specified widget. * * @extends module:core/plugin~Plugin + * @mixes module:utils/observablemixin~ObservableMixin */ export default class WidgetResize extends Plugin { /** @@ -31,11 +34,35 @@ export default class WidgetResize extends Plugin { } init() { - this.resizers = []; - this.activeResizer = null; + /** + * The currently visible resizer. + * + * @protected + * @observable + * @member {module:widget/widgetresize/resizer~Resizer|null} #_visibleResizer + */ + this.set( '_visibleResizer', null ); + + /** + * References an active resizer. + * + * Active resizer means a resizer which handle is actively used by the end user. + * + * @protected + * @observable + * @member {module:widget/widgetresize/resizer~Resizer|null} #_activeResizer + */ + this.set( '_activeResizer', null ); + + /** + * A map of resizers created using this plugin instance. + * + * @private + * @type {Map.} + */ + this._resizers = new Map(); const domDocument = global.window.document; - const THROTTLE_THRESHOLD = 16; // 16ms = ~60fps this.editor.model.schema.setAttributeProperties( 'width', { isFormatting: true @@ -50,38 +77,52 @@ export default class WidgetResize extends Plugin { const resizeHandle = domEventData.target; - this.activeResizer = this._getResizerByHandle( resizeHandle ); + this._activeResizer = this._getResizerByHandle( resizeHandle ); - if ( this.activeResizer ) { - this.activeResizer.begin( resizeHandle ); + if ( this._activeResizer ) { + this._activeResizer.begin( resizeHandle ); } } ); - this._observer.listenTo( domDocument, 'mousemove', throttle( ( event, domEventData ) => { - if ( this.activeResizer ) { - this.activeResizer.updateSize( domEventData ); + this._observer.listenTo( domDocument, 'mousemove', ( event, domEventData ) => { + if ( this._activeResizer ) { + this._activeResizer.updateSize( domEventData ); } - }, THROTTLE_THRESHOLD ) ); + } ); this._observer.listenTo( domDocument, 'mouseup', () => { - if ( this.activeResizer ) { - this.activeResizer.commit(); + if ( this._activeResizer ) { + this._activeResizer.commit(); - this.activeResizer = null; + this._activeResizer = null; } } ); - const redrawResizers = throttle( () => { - for ( const context of this.resizers ) { - context.redraw(); + const redrawFocusedResizer = () => { + if ( this._visibleResizer ) { + this._visibleResizer.redraw(); } - }, THROTTLE_THRESHOLD ); + }; + + const redrawFocusedResizerThrottled = throttle( redrawFocusedResizer, 200 ); // 5fps + + // Redraws occurring upon a change of visible resizer must not be throttled, as it is crucial for the initial + // render. Without it the resizer frame would be misaligned with resizing host for a fraction of second. + this.on( 'change:_visibleResizer', redrawFocusedResizer ); // Redrawing on any change of the UI of the editor (including content changes). - this.editor.ui.on( 'update', redrawResizers ); + this.editor.ui.on( 'update', redrawFocusedResizerThrottled ); // Resizers need to be redrawn upon window resize, because new window might shrink resize host. - this._observer.listenTo( global.window, 'resize', redrawResizers ); + this._observer.listenTo( global.window, 'resize', redrawFocusedResizerThrottled ); + + const viewSelection = this.editor.editing.view.document.selection; + + viewSelection.on( 'change', () => { + const selectedElement = viewSelection.getSelectedElement(); + + this._visibleResizer = this._getResizerByViewElement( selectedElement ) || null; + } ); } destroy() { @@ -97,22 +138,40 @@ export default class WidgetResize extends Plugin { resizer.attach(); - this.editor.editing.view.once( 'render', () => resizer.redraw() ); - - this.resizers.push( resizer ); + this._resizers.set( options.viewElement, resizer ); return resizer; } + /** + * Returns a resizer that contains a given resize handle. + * + * @protected + * @param {HTMLElement} domResizeHandle + * @returns {module:widget/widgetresize/resizer~Resizer} + */ _getResizerByHandle( domResizeHandle ) { - for ( const resizer of this.resizers ) { + for ( const resizer of this._resizers.values() ) { if ( resizer.containsHandle( domResizeHandle ) ) { return resizer; } } } + + /** + * Returns a resizer created for a given view element (widget element). + * + * @protected + * @param {module:engine/view/containerelement~ContainerElement} viewElement + * @returns {module:widget/widgetresize/resizer~Resizer} + */ + _getResizerByViewElement( viewElement ) { + return this._resizers.get( viewElement ); + } } +mix( WidgetResize, ObservableMixin ); + /** * Interface describing a resizer. It allows to specify the resizing host, custom logic for calculating aspect ratio, etc. * diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index 5babc97b..91169731 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -119,8 +119,6 @@ export default class Resizer { this._sizeUI.bindToState( this._options, this.state ); this.state.begin( domResizeHandle, this._getHandleHost(), this._getResizeHost() ); - - this.redraw(); } /** @@ -142,17 +140,15 @@ export default class Resizer { newSize.handleHostWidth = Math.round( domHandleHostRect.width ); newSize.handleHostHeight = Math.round( domHandleHostRect.height ); + // Handle max-width limitation. const domResizeHostRect = new Rect( domHandleHost ); newSize.width = Math.round( domResizeHostRect.width ); newSize.height = Math.round( domResizeHostRect.height ); - this.state.update( newSize ); + this.redraw( domHandleHostRect ); - // Refresh values based on the real image. Real image might be limited by max-width, and thus fetching it - // here will reflect this limitation. - this._domResizerWrapper.style.width = newSize.handleHostWidth + 'px'; - this._domResizerWrapper.style.height = newSize.handleHostHeight + 'px'; + this.state.update( newSize ); } /** @@ -187,8 +183,10 @@ export default class Resizer { /** * Redraws the resizer. + * + * @param {module:utils/dom/rect~Rect} [handleHostRect] Handle host rectangle might be given to improve performance. */ - redraw() { + redraw( handleHostRect ) { // TODO review this const domWrapper = this._domResizerWrapper; @@ -196,21 +194,28 @@ export default class Resizer { // Refresh only if resizer exists in the DOM. const widgetWrapper = domWrapper.parentElement; const handleHost = this._getHandleHost(); - const clientRect = new Rect( handleHost ); + const clientRect = handleHostRect || new Rect( handleHost ); domWrapper.style.width = clientRect.width + 'px'; domWrapper.style.height = clientRect.height + 'px'; + const offsets = { + left: handleHost.offsetLeft, + top: handleHost.offsetTop, + height: handleHost.offsetHeight, + width: handleHost.offsetWidth + }; + // In case a resizing host is not a widget wrapper, we need to compensate // for any additional offsets the resize host might have. E.g. wrapper padding // or simply another editable. By doing that the border and resizers are shown // only around the resize host. if ( !widgetWrapper.isSameNode( handleHost ) ) { - domWrapper.style.left = handleHost.offsetLeft + 'px'; - domWrapper.style.top = handleHost.offsetTop + 'px'; + domWrapper.style.left = offsets.left + 'px'; + domWrapper.style.top = offsets.top + 'px'; - domWrapper.style.height = handleHost.offsetHeight + 'px'; - domWrapper.style.width = handleHost.offsetWidth + 'px'; + domWrapper.style.height = offsets.height + 'px'; + domWrapper.style.width = offsets.width + 'px'; } }