From 9e6d7cef1b50eb97bade409ca8df34930f3c38a5 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 29 Apr 2019 11:04:30 +0200 Subject: [PATCH 01/93] Internal: Initial resizer API mocks. --- src/utils.js | 10 ++++++++++ src/widgetfeature.js | 16 ++++++++++++++++ src/widgetresizefeature.js | 27 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/widgetfeature.js create mode 100644 src/widgetresizefeature.js diff --git a/src/utils.js b/src/utils.js index 643a7c13..5c4d0f65 100644 --- a/src/utils.js +++ b/src/utils.js @@ -108,6 +108,10 @@ export function toWidget( element, writer, options = {} ) { addSelectionHandler( element, writer ); } + if ( options.features ) { + addWidgetFeatures( element, writer, options.features ); + } + setHighlightHandling( element, writer, @@ -379,3 +383,9 @@ function addSelectionHandler( widgetElement, writer ) { writer.insert( writer.createPositionAt( widgetElement, 0 ), selectionHandler ); writer.addClass( [ 'ck-widget_with-selection-handler' ], widgetElement ); } + +function addWidgetFeatures( element, writer, features ) { + for ( const currentFeature of features ) { + currentFeature.apply( element, writer ); + } +} diff --git a/src/widgetfeature.js b/src/widgetfeature.js new file mode 100644 index 00000000..822a76cc --- /dev/null +++ b/src/widgetfeature.js @@ -0,0 +1,16 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/widget + */ + +/** + * The base class for widget features. This type provides a common API for reusable features of widgets. + */ +export default class WidgetFeature { + apply() { + } +} diff --git a/src/widgetresizefeature.js b/src/widgetresizefeature.js new file mode 100644 index 00000000..e69a149a --- /dev/null +++ b/src/widgetresizefeature.js @@ -0,0 +1,27 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/widget + */ + +import WidgetFeature from './widgetfeature'; + +export const WIDGET_RESIZE_ATTRIBUTE_NAME = 'resizer'; + +/** + * The base class for widget features. This type provides a common API for reusable features of widgets. + */ +export default class WidgetResizeFeature extends WidgetFeature { + apply( widget, writer ) { + super.apply( widget, writer ); + + // writer.setCustomProperty( WIDGET_RESIZE_ATTRIBUTE_NAME, true, widget ); + writer.setAttribute( WIDGET_RESIZE_ATTRIBUTE_NAME, true, widget ); + + // widget.setAttribute( WIDGET_RESIZE_ATTRIBUTE_NAME ); + // aaa + } +} From d16d34bd0ca94d1496ccbbdb1d828df6b6712297 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 21 May 2019 23:05:17 +0200 Subject: [PATCH 02/93] Internal: Some basic logic to show up the resizers. --- src/widgetresizefeature.js | 39 +++++++++++++++++++++------ theme/widget.css | 55 +++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/widgetresizefeature.js b/src/widgetresizefeature.js index e69a149a..cde22309 100644 --- a/src/widgetresizefeature.js +++ b/src/widgetresizefeature.js @@ -8,20 +8,43 @@ */ import WidgetFeature from './widgetfeature'; - -export const WIDGET_RESIZE_ATTRIBUTE_NAME = 'resizer'; +import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; +import dragHandlerIcon from '../theme/icons/drag-handler.svg'; /** * The base class for widget features. This type provides a common API for reusable features of widgets. */ export default class WidgetResizeFeature extends WidgetFeature { - apply( widget, writer ) { - super.apply( widget, writer ); + apply( widgetElement, writer ) { + super.apply( widgetElement, writer ); + + const selectionHandler = writer.createUIElement( 'div', { + class: 'ck ck-widget__resizer-wrapper' + }, function( domDocument ) { + const domElement = this.toDomElement( domDocument ); + const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; + + for ( const currentPosition of resizerPositions ) { + // Use the IconView from the UI library. + const icon = new IconView(); + icon.set( 'content', dragHandlerIcon ); + icon.extendTemplate( { + attributes: { + 'class': `ck-widget__resizer ck-widget__resizer-${ currentPosition }` + } + } ); + + // Make sure icon#element is rendered before passing to appendChild(). + icon.render(); + + domElement.appendChild( icon.element ); + } - // writer.setCustomProperty( WIDGET_RESIZE_ATTRIBUTE_NAME, true, widget ); - writer.setAttribute( WIDGET_RESIZE_ATTRIBUTE_NAME, true, widget ); + return domElement; + } ); - // widget.setAttribute( WIDGET_RESIZE_ATTRIBUTE_NAME ); - // aaa + // Append resizer wrapper to the widget's wrapper. + writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), selectionHandler ); + writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement ); } } diff --git a/theme/widget.css b/theme/widget.css index fa6acf6d..2833d007 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -3,12 +3,65 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +.ck .ck-widget_with-resizer { + /* Make the widget wrapper a relative positioning container for the drag handler. */ + position: relative; + + & .ck-widget__resizer-wrapper { + visibility: hidden; + position: absolute; + + left: 0; + right: 0; + top: 0; + bottom: 0; + } + + & .ck-widget__resizer { + position: absolute; + + &.ck-widget__resizer-top-left { + top: 0; + left: 0; + } + &.ck-widget__resizer-top-right { + top: 0; + right: 0; + } + &.ck-widget__resizer-bottom-right { + bottom: 0; + right: 0; + } + &.ck-widget__resizer-bottom-left { + bottom: 0; + left: 0; + } + + &.ck-widget__resizer-top-left, &.ck-widget__resizer-bottom-right { + &:hover { + cursor: nwse-resize; + } + } + + /* Show the selection handler on mouse hover over the widget. */ + &:hover { + cursor: nesw-resize; + } + } + + &:hover { + & .ck-widget__resizer-wrapper { + visibility: visible; + } + } +} + .ck .ck-widget.ck-widget_with-selection-handler { /* Make the widget wrapper a relative positioning container for the drag handler. */ position: relative; & .ck-widget__selection-handler { - visibility: hidden; + /* visibility: hidden; */ position: absolute; & .ck-icon { From 2a51e26448b861f9565a44e5368052bbc638532a Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 26 Jun 2019 14:32:56 +0200 Subject: [PATCH 03/93] Added WidgetResizer plugin class. Reason why I'm adding a plugin is that in the end resizer feature needs editor instance. --- src/widgetresizer.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/widgetresizer.js diff --git a/src/widgetresizer.js b/src/widgetresizer.js new file mode 100644 index 00000000..52a5e986 --- /dev/null +++ b/src/widgetresizer.js @@ -0,0 +1,32 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/widget + */ + +import WidgetResizeFeature from './widgetresizefeature'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +/** + * The base class for widget features. This type provides a common API for reusable features of widgets. + */ +export default class WidgetResizer extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'WidgetResizer'; + } + + apply( widgetElement, writer ) { + // @todo inline the logic + const ret = new WidgetResizeFeature(); + + ret.apply( widgetElement, writer ); + + return ret; + } +} From df924cf5bfe2d5eb10ae77de1c5924d1099e3489 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 27 Jun 2019 12:41:02 +0200 Subject: [PATCH 04/93] Internal: dirty version that just shows an overlay when resize handler is clicked. --- src/view/mousemoveobserver.js | 54 ++++++++++++++++++++++++ src/view/mouseobserver.js | 40 ++++++++++++++++++ src/widgetresizefeature.js | 4 ++ src/widgetresizer.js | 77 +++++++++++++++++++++++++++++++++++ theme/widget.css | 23 +++++++++++ 5 files changed, 198 insertions(+) create mode 100644 src/view/mousemoveobserver.js create mode 100644 src/view/mouseobserver.js diff --git a/src/view/mousemoveobserver.js b/src/view/mousemoveobserver.js new file mode 100644 index 00000000..a9e84344 --- /dev/null +++ b/src/view/mousemoveobserver.js @@ -0,0 +1,54 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/view/MouseMoveObserver + */ + +import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; +import { throttle } from 'lodash-es'; + +/** + * Mouse move observer. + * + * It throttles the event so that it doesn't fire too often. + * + * @extends module:engine/view/observer/domeventobserver~DomEventObserver + */ +export default class MouseMoveObserver extends DomEventObserver { + constructor( view ) { + super( view ); + + this.domEventType = 'mousemove'; + + this._fireMouseMoveEvent = throttle( domEvent => this.fire( domEvent.type, domEvent ), 60 ); + } + + /** + * @inheritDoc + */ + destroy() { + super.destroy(); + + this._fireMouseMoveEvent.cancel(); + } + + onDomEvent( domEvent ) { + this._fireMouseMoveEvent( domEvent ); + } +} + +/** + * Fired when mouse moves over the editable. + * + * Introduced by {@link module:widget/view/MouseMoveObserver~MouseMoveObserver}. + * + * Note that this event is not available by default. To make it available {@link widget/view/MouseMoveObserver~MouseMoveObserver} + * needs to be added to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * + * @see module:widget/view/MouseMoveObserver~MouseMoveObserver + * @event module:engine/view/document~Document#event:mousemove + * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. + */ diff --git a/src/view/mouseobserver.js b/src/view/mouseobserver.js new file mode 100644 index 00000000..2084faa8 --- /dev/null +++ b/src/view/mouseobserver.js @@ -0,0 +1,40 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/view/mouseobserver + */ + +import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; + +/** + * Mouse events observer. + * + * @extends module:engine/view/observer/domeventobserver~DomEventObserver + */ +export default class MouseObserver extends DomEventObserver { + constructor( view ) { + super( view ); + + this.domEventType = [ 'mousedown', 'mouseup' ]; + } + + onDomEvent( domEvent ) { + this.fire( domEvent.type, domEvent ); + } +} + +/** + * Fired when mouse button is pressed down on one of the editables. + * + * Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}. + * + * Note that this event is not available by default. To make it available {@link module:engine/view/observer/mouseobserver~MouseObserver} + * needs to be added to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. + * + * @see module:engine/view/observer/mouseobserver~MouseObserver + * @event module:engine/view/document~Document#event:mousedown + * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. + */ diff --git a/src/widgetresizefeature.js b/src/widgetresizefeature.js index cde22309..37afb4dc 100644 --- a/src/widgetresizefeature.js +++ b/src/widgetresizefeature.js @@ -24,6 +24,10 @@ export default class WidgetResizeFeature extends WidgetFeature { const domElement = this.toDomElement( domDocument ); const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; + const shadowElement = domDocument.createElement( 'div' ); + shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' ); + domElement.appendChild( shadowElement ); + for ( const currentPosition of resizerPositions ) { // Use the IconView from the UI library. const icon = new IconView(); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 52a5e986..89d8397f 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -9,6 +9,34 @@ import WidgetResizeFeature from './widgetresizefeature'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import MouseObserver from './view/mouseobserver'; +import MouseMoveObserver from './view/mousemoveobserver'; + +import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; + +class ResizeContext { + constructor( handler ) { + const resizeWrapper = getAncestors( handler ).filter( + element => element.classList.contains( 'ck-widget_with-resizer' ) + )[ 0 ]; + + this.widgetWrapper = resizeWrapper; + this.shadowWrapper = resizeWrapper.querySelector( '.ck-widget__resizer-shadow' ); + } + + initialize() { + this.shadowWrapper.classList.add( 'ck-widget__resizer-shadow-active' ); + } + + destroy() { + this.shadowWrapper.classList.remove( 'ck-widget__resizer-shadow-active' ); + + this.shadowWrapper = null; + this.wrapper = null; + } + + updateSize() {} +} /** * The base class for widget features. This type provides a common API for reusable features of widgets. @@ -21,6 +49,55 @@ export default class WidgetResizer extends Plugin { return 'WidgetResizer'; } + init() { + const view = this.editor.editing.view; + const viewDocument = view.document; + + this._observers = { + mouseMove: view.addObserver( MouseMoveObserver ), + mouseDownUp: view.addObserver( MouseObserver ) + }; + + // It should start disabled, only upon clicking drag handler it interests us. + // Currently broken due to https://github.com/ckeditor/ckeditor5-engine/blob/ce6422b/src/view/view.js#L364 + this._observers.mouseMove.disable(); + + let isActive = false; + let resizeContext = null; + + // Mouse move observer is only needed when the mouse button is pressed. + // this.listenTo( viewDocument, 'mousemove', () => console.log( 'move' ) ); + + this.listenTo( viewDocument, 'mousedown', ( event, domEventData ) => { + const target = domEventData.domTarget; + + const resizeHandler = isResizeWrapper( target ) || getAncestors( target ).filter( isResizeWrapper )[ 0 ]; + + if ( resizeHandler ) { + isActive = true; + this._observers.mouseMove.enable(); + + resizeContext = new ResizeContext( resizeHandler ); + resizeContext.initialize(); + } + } ); + + this.listenTo( viewDocument, 'mouseup', () => { + // @todo listen also for mouse up outside of the editable. + if ( isActive ) { + isActive = false; + this._observers.mouseMove.disable(); + + resizeContext.destroy(); + resizeContext = null; + } + } ); + + function isResizeWrapper( element ) { + return element.classList && element.classList.contains( 'ck-widget__resizer' ); + } + } + apply( widgetElement, writer ) { // @todo inline the logic const ret = new WidgetResizeFeature(); diff --git a/theme/widget.css b/theme/widget.css index 2833d007..6d47dea4 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -15,6 +15,23 @@ right: 0; top: 0; bottom: 0; + + & .ck-widget__resizer-shadow { + display: none; + position: absolute; + + left: 0; + right: 0; + top: 0; + bottom: 0; + + outline: 2px dashed red; + + &.ck-widget__resizer-shadow-active { + display: block; + visibility: visible; + } + } } & .ck-widget__resizer { @@ -85,3 +102,9 @@ } } } + +.ck .ck-widget_resizer-shadow { + width: 60px; + height: 60px; + outline: 2px solid red !important; +} \ No newline at end of file From d37af34a693751e418b40c07e626e425e79df7d6 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 27 Jun 2019 12:56:29 +0200 Subject: [PATCH 05/93] Internal: mocked very basic implementation for the ResizeContext.updateSize method. --- src/widgetresizer.js | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 89d8397f..563cc462 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -24,7 +24,9 @@ class ResizeContext { this.shadowWrapper = resizeWrapper.querySelector( '.ck-widget__resizer-shadow' ); } - initialize() { + initialize( domEventData ) { + this.initialClickCoordinates = this._extractCoordinates( domEventData ); + this.shadowWrapper.classList.add( 'ck-widget__resizer-shadow-active' ); } @@ -35,7 +37,26 @@ class ResizeContext { this.wrapper = null; } - updateSize() {} + updateSize( domEventData ) { + const currentCoordinates = this._extractCoordinates( domEventData ); + const yDistance = this.initialClickCoordinates.y - currentCoordinates.y; + // for top, left handler: + // yDistance > 0 - element is enlarged + // yDistance < 0 - element is shrinked + + if ( yDistance > 0 ) { + // console.log( 'enlarging' ); + } else { + // console.log( 'shrinking' ); + } + } + + _extractCoordinates( event ) { + return { + x: event.domEvent.pageX, + y: event.domEvent.pageY + }; + } } /** @@ -67,6 +88,11 @@ export default class WidgetResizer extends Plugin { // Mouse move observer is only needed when the mouse button is pressed. // this.listenTo( viewDocument, 'mousemove', () => console.log( 'move' ) ); + this.listenTo( viewDocument, 'mousemove', ( event, domEventData ) => { + if ( resizeContext ) { + resizeContext.updateSize( domEventData ); + } + } ); this.listenTo( viewDocument, 'mousedown', ( event, domEventData ) => { const target = domEventData.domTarget; @@ -78,11 +104,11 @@ export default class WidgetResizer extends Plugin { this._observers.mouseMove.enable(); resizeContext = new ResizeContext( resizeHandler ); - resizeContext.initialize(); + resizeContext.initialize( domEventData ); } } ); - this.listenTo( viewDocument, 'mouseup', () => { + const finishResizing = () => { // @todo listen also for mouse up outside of the editable. if ( isActive ) { isActive = false; @@ -91,7 +117,10 @@ export default class WidgetResizer extends Plugin { resizeContext.destroy(); resizeContext = null; } - } ); + }; + + // @todo: it should listen on the entire window. + this.listenTo( viewDocument, 'mouseup', finishResizing ); function isResizeWrapper( element ) { return element.classList && element.classList.contains( 'ck-widget__resizer' ); From bdb110de561a6b7783646bf4c25db94d61330e4b Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 27 Jun 2019 14:12:54 +0200 Subject: [PATCH 06/93] Internal: more precise resizing. Note current implementation supports only top-left corner. --- src/widgetresizer.js | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 563cc462..b80d3364 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -14,6 +14,23 @@ import MouseMoveObserver from './view/mousemoveobserver'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; +/** + * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. + * + * @param {HTMLElement} element + * @returns {Object} return + * @returns {Number} return.x + * @returns {Number} return.y + */ +function getAbsolutePosition( element ) { + const nativeRectangle = element.getBoundingClientRect(); + + return { + x: nativeRectangle.left + element.ownerDocument.defaultView.scrollX, + y: nativeRectangle.top + element.ownerDocument.defaultView.scrollY + }; +} + class ResizeContext { constructor( handler ) { const resizeWrapper = getAncestors( handler ).filter( @@ -22,16 +39,22 @@ class ResizeContext { this.widgetWrapper = resizeWrapper; this.shadowWrapper = resizeWrapper.querySelector( '.ck-widget__resizer-shadow' ); - } - initialize( domEventData ) { - this.initialClickCoordinates = this._extractCoordinates( domEventData ); + // Reference edge (corner) that should be used to calculate resize difference. + this.referenceCoordinates = getAbsolutePosition( handler ); + + // Initial height of resizing host / resized element. + // @todo: hardcoded img support + this.initialHeight = resizeWrapper.querySelector( 'img' ).height; + } + initialize() { this.shadowWrapper.classList.add( 'ck-widget__resizer-shadow-active' ); } destroy() { this.shadowWrapper.classList.remove( 'ck-widget__resizer-shadow-active' ); + this.shadowWrapper.removeAttribute( 'style' ); this.shadowWrapper = null; this.wrapper = null; @@ -39,15 +62,18 @@ class ResizeContext { updateSize( domEventData ) { const currentCoordinates = this._extractCoordinates( domEventData ); - const yDistance = this.initialClickCoordinates.y - currentCoordinates.y; - // for top, left handler: + const yDistance = this.referenceCoordinates.y - currentCoordinates.y; + + // For top, left handler: // yDistance > 0 - element is enlarged // yDistance < 0 - element is shrinked if ( yDistance > 0 ) { // console.log( 'enlarging' ); + this.shadowWrapper.style.top = ( yDistance * -1 ) + 'px'; } else { // console.log( 'shrinking' ); + this.shadowWrapper.style.top = ( yDistance * -1 ) + 'px'; } } From d7d17efcf1ee7d63ba5555a848a1593e7c45e2ed Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 27 Jun 2019 15:19:31 +0200 Subject: [PATCH 07/93] Internal: mocked aligning the resize shadow with resize host width. Although it's a very simplified approach, also it's triggered upon first resize activation. --- src/widgetresizer.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index b80d3364..ae8974a9 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -33,19 +33,31 @@ function getAbsolutePosition( element ) { class ResizeContext { constructor( handler ) { - const resizeWrapper = getAncestors( handler ).filter( + const widgetWrapper = getAncestors( handler ).filter( element => element.classList.contains( 'ck-widget_with-resizer' ) )[ 0 ]; - this.widgetWrapper = resizeWrapper; - this.shadowWrapper = resizeWrapper.querySelector( '.ck-widget__resizer-shadow' ); + this.widgetWrapper = widgetWrapper; + this.shadowWrapper = widgetWrapper.querySelector( '.ck-widget__resizer-shadow' ); // Reference edge (corner) that should be used to calculate resize difference. this.referenceCoordinates = getAbsolutePosition( handler ); // Initial height of resizing host / resized element. // @todo: hardcoded img support - this.initialHeight = resizeWrapper.querySelector( 'img' ).height; + const resizingHost = widgetWrapper.querySelector( 'img' ); + this.initialHeight = resizingHost.height; + + const resizeWrapper = widgetWrapper.querySelector( '.ck-widget__resizer-wrapper' ); + + resizeWrapper.style.left = resizingHost.offsetLeft + 'px'; + resizeWrapper.style.right = resizingHost.offsetLeft + 'px'; + + // Position of a clicked resize handler in x and y axes. + this.direction = { + y: 'top', + x: 'left' + }; } initialize() { From 866de28d6d459565676b04ade4deff95d2ff5ce0 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 28 Jun 2019 11:02:10 +0200 Subject: [PATCH 08/93] Internal: workaround to use render event to catch dom element that is placed in editable in order to obtain image offset from widget wrapper. The same offset is applied to the resizer wrapper so that it is aligned with the image. --- src/widgetresizefeature.js | 11 +++++++++++ src/widgetresizer.js | 24 ++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/widgetresizefeature.js b/src/widgetresizefeature.js index 37afb4dc..a538fad4 100644 --- a/src/widgetresizefeature.js +++ b/src/widgetresizefeature.js @@ -18,6 +18,8 @@ export default class WidgetResizeFeature extends WidgetFeature { apply( widgetElement, writer ) { super.apply( widgetElement, writer ); + let domSelectionHandler = null; + const selectionHandler = writer.createUIElement( 'div', { class: 'ck ck-widget__resizer-wrapper' }, function( domDocument ) { @@ -44,11 +46,20 @@ export default class WidgetResizeFeature extends WidgetFeature { domElement.appendChild( icon.element ); } + domSelectionHandler = domElement; + return domElement; } ); // Append resizer wrapper to the widget's wrapper. writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), selectionHandler ); writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement ); + + return () => { + const resizingHost = domSelectionHandler.parentElement.querySelector( 'img' ); + + domSelectionHandler.style.left = resizingHost.offsetLeft + 'px'; + domSelectionHandler.style.right = resizingHost.offsetLeft + 'px'; + }; } } diff --git a/src/widgetresizer.js b/src/widgetresizer.js index ae8974a9..125aa533 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -48,11 +48,6 @@ class ResizeContext { const resizingHost = widgetWrapper.querySelector( 'img' ); this.initialHeight = resizingHost.height; - const resizeWrapper = widgetWrapper.querySelector( '.ck-widget__resizer-wrapper' ); - - resizeWrapper.style.left = resizingHost.offsetLeft + 'px'; - resizeWrapper.style.right = resizingHost.offsetLeft + 'px'; - // Position of a clicked resize handler in x and y axes. this.direction = { y: 'top', @@ -89,6 +84,20 @@ class ResizeContext { } } + // Accepts the proposed resize intent. + commit() { + } + + /** + * + * @param {module:@ckeditor/ckeditor5-core/src/editor/editor~Editor} editor + * @param {module:@ckeditor/ckeditor5-engine/src/view/element~Element} widgetWrapperElement + * @returns {module:@ckeditor/ckeditor5-engine/src/model/element~Element|undefined} + */ + _getModel( editor, widgetWrapperElement ) { + return editor.editing.mapper.toModelElement( widgetWrapperElement ); + } + _extractCoordinates( event ) { return { x: event.domEvent.pageX, @@ -152,6 +161,7 @@ export default class WidgetResizer extends Plugin { isActive = false; this._observers.mouseMove.disable(); + resizeContext.commit(); resizeContext.destroy(); resizeContext = null; } @@ -169,7 +179,9 @@ export default class WidgetResizer extends Plugin { // @todo inline the logic const ret = new WidgetResizeFeature(); - ret.apply( widgetElement, writer ); + const renderRefresh = ret.apply( widgetElement, writer ); + + this.editor.editing.view.once( 'render', renderRefresh ); return ret; } From c156ec4a1218d3e5cf03c6769c54f7dca4c553fe Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 28 Jun 2019 17:39:41 +0200 Subject: [PATCH 09/93] Internal: next implementation of the resizer. This was required to acquire access to model item of resized element (here an image). Most of the logic has been moved to the ResizeContext type. Now the idea is to create a ResizeManager that will include zero or more contexts during the runtime and will manage (only one) active context during the resize. --- src/resizecontext.js | 192 +++++++++++++++++++++++++++++++++++++++++++ src/widgetresizer.js | 151 +++++++++------------------------- 2 files changed, 232 insertions(+), 111 deletions(-) create mode 100644 src/resizecontext.js diff --git a/src/resizecontext.js b/src/resizecontext.js new file mode 100644 index 00000000..417d3c15 --- /dev/null +++ b/src/resizecontext.js @@ -0,0 +1,192 @@ +import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; +import dragHandlerIcon from '../theme/icons/drag-handler.svg'; + +const HEIGHT_ATTRIBUTE_NAME = 'height'; + +/** + * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. + * + * @param {HTMLElement} element + * @returns {Object} return + * @returns {Number} return.x + * @returns {Number} return.y + */ +function getAbsolutePosition( element ) { + const nativeRectangle = element.getBoundingClientRect(); + + return { + x: nativeRectangle.left + element.ownerDocument.defaultView.scrollX, + y: nativeRectangle.top + element.ownerDocument.defaultView.scrollY + }; +} + +export default class ResizeContext { + constructor() { + // HTMLElement??? + this.resizeHost = null; + // view/UiElement + this.resizeWrapperElement = null; + // view/Element + this.widgetWrapperElement = null; + // HTMLElement|null - might be uninitialized + this.domResizeWrapper = null; + this.domResizeShadow = null; + + // @todo: ---- options below seems like a little outside of a scope of a single context ---- + // Size before resizing. + this.initialSize = { + x: 0, + y: 0 + }; + + // Position of a clicked resize handler in x and y axes. + this.direction = { + y: 'top', + x: 'left' + }; + + // Reference point of resizer where the dragging started. It is used to measure the distance to user cursor + // traveled, thus how much the image should be enlarged. + // This information is only known after DOM was rendered, so it will be updated later. + this.referenceCoordinates = { + y: 0, + x: 0 + }; + } + + /** + * + * @param {module:engine/view/element~Element} widgetElement Widget's wrapper. + * @param {module:engine/view/downcastwriter~DowncastWriter} writer + */ + attach( widgetElement, writer ) { + const that = this; + + this.widgetWrapperElement = widgetElement; + + this.resizeWrapperElement = writer.createUIElement( 'div', { + class: 'ck ck-widget__resizer-wrapper' + }, function( domDocument ) { + const domElement = this.toDomElement( domDocument ); + + that.domResizeShadow = that._appendShadowElement( domDocument, domElement ); + that._appendResizers( domElement ); + + that.domResizeWrapper = domElement; + + return domElement; + } ); + + // Append resizer wrapper to the widget's wrapper. + writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), this.resizeWrapperElement ); + writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement ); + } + + /** + * + * @param {HTMLElement} domResizeHandler Handler used to calculate reference point. + */ + begin( domResizeHandler ) { + this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); + + this.referenceCoordinates = getAbsolutePosition( domResizeHandler ); + } + + commit( editor ) { + const modelEntry = this._getModel( editor, this.widgetWrapperElement ); + const newHeight = this.domResizeShadow.clientHeight; + + this._dismissShadow(); + + editor.model.change( writer => { + writer.setAttribute( HEIGHT_ATTRIBUTE_NAME, newHeight, modelEntry ); + } ); + } + + cancel() { + this._dismissShadow(); + } + + destroy() { + this.cancel(); + + this.domResizeShadow = null; + this.wrapper = null; + } + + updateSize( domEventData ) { + const currentCoordinates = this._extractCoordinates( domEventData ); + const yDistance = this.referenceCoordinates.y - currentCoordinates.y; + + // For top, left handler: + // yDistance > 0 - element is enlarged + // yDistance < 0 - element is shrinked + + if ( yDistance > 0 ) { + // console.log( 'enlarging' ); + this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + } else { + // console.log( 'shrinking' ); + this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + } + } + + redraw() { + if ( this.domResizeWrapper ) { + const resizingHost = this.domResizeWrapper.parentElement.querySelector( 'img' ); + + this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; + this.domResizeWrapper.style.right = resizingHost.offsetLeft + 'px'; + } + } + + _appendShadowElement( domDocument, domElement ) { + const shadowElement = domDocument.createElement( 'div' ); + shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' ); + domElement.appendChild( shadowElement ); + + return shadowElement; + } + + _appendResizers( domElement ) { + const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; + + for ( const currentPosition of resizerPositions ) { + // Use the IconView from the UI library. + const icon = new IconView(); + icon.set( 'content', dragHandlerIcon ); + icon.extendTemplate( { + attributes: { + 'class': `ck-widget__resizer ck-widget__resizer-${ currentPosition }` + } + } ); + + // Make sure icon#element is rendered before passing to appendChild(). + icon.render(); + + domElement.appendChild( icon.element ); + } + } + + _dismissShadow() { + this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); + this.domResizeShadow.removeAttribute( 'style' ); + } + + /** + * + * @param {module:@ckeditor/ckeditor5-core/src/editor/editor~Editor} editor + * @param {module:@ckeditor/ckeditor5-engine/src/view/element~Element} widgetWrapperElement + * @returns {module:@ckeditor/ckeditor5-engine/src/model/element~Element|undefined} + */ + _getModel( editor, widgetWrapperElement ) { + return editor.editing.mapper.toModelElement( widgetWrapperElement ); + } + + _extractCoordinates( event ) { + return { + x: event.domEvent.pageX, + y: event.domEvent.pageY + }; + } +} diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 125aa533..405b516e 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -7,104 +7,11 @@ * @module widget/widget */ -import WidgetResizeFeature from './widgetresizefeature'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import MouseObserver from './view/mouseobserver'; import MouseMoveObserver from './view/mousemoveobserver'; - import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; - -/** - * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. - * - * @param {HTMLElement} element - * @returns {Object} return - * @returns {Number} return.x - * @returns {Number} return.y - */ -function getAbsolutePosition( element ) { - const nativeRectangle = element.getBoundingClientRect(); - - return { - x: nativeRectangle.left + element.ownerDocument.defaultView.scrollX, - y: nativeRectangle.top + element.ownerDocument.defaultView.scrollY - }; -} - -class ResizeContext { - constructor( handler ) { - const widgetWrapper = getAncestors( handler ).filter( - element => element.classList.contains( 'ck-widget_with-resizer' ) - )[ 0 ]; - - this.widgetWrapper = widgetWrapper; - this.shadowWrapper = widgetWrapper.querySelector( '.ck-widget__resizer-shadow' ); - - // Reference edge (corner) that should be used to calculate resize difference. - this.referenceCoordinates = getAbsolutePosition( handler ); - - // Initial height of resizing host / resized element. - // @todo: hardcoded img support - const resizingHost = widgetWrapper.querySelector( 'img' ); - this.initialHeight = resizingHost.height; - - // Position of a clicked resize handler in x and y axes. - this.direction = { - y: 'top', - x: 'left' - }; - } - - initialize() { - this.shadowWrapper.classList.add( 'ck-widget__resizer-shadow-active' ); - } - - destroy() { - this.shadowWrapper.classList.remove( 'ck-widget__resizer-shadow-active' ); - this.shadowWrapper.removeAttribute( 'style' ); - - this.shadowWrapper = null; - this.wrapper = null; - } - - updateSize( domEventData ) { - const currentCoordinates = this._extractCoordinates( domEventData ); - const yDistance = this.referenceCoordinates.y - currentCoordinates.y; - - // For top, left handler: - // yDistance > 0 - element is enlarged - // yDistance < 0 - element is shrinked - - if ( yDistance > 0 ) { - // console.log( 'enlarging' ); - this.shadowWrapper.style.top = ( yDistance * -1 ) + 'px'; - } else { - // console.log( 'shrinking' ); - this.shadowWrapper.style.top = ( yDistance * -1 ) + 'px'; - } - } - - // Accepts the proposed resize intent. - commit() { - } - - /** - * - * @param {module:@ckeditor/ckeditor5-core/src/editor/editor~Editor} editor - * @param {module:@ckeditor/ckeditor5-engine/src/view/element~Element} widgetWrapperElement - * @returns {module:@ckeditor/ckeditor5-engine/src/model/element~Element|undefined} - */ - _getModel( editor, widgetWrapperElement ) { - return editor.editing.mapper.toModelElement( widgetWrapperElement ); - } - - _extractCoordinates( event ) { - return { - x: event.domEvent.pageX, - y: event.domEvent.pageY - }; - } -} +import ResizeContext2 from './resizecontext'; /** * The base class for widget features. This type provides a common API for reusable features of widgets. @@ -118,6 +25,9 @@ export default class WidgetResizer extends Plugin { } init() { + this.contexts = []; + this.activeContext = null; + const view = this.editor.editing.view; const viewDocument = view.document; @@ -131,58 +41,77 @@ export default class WidgetResizer extends Plugin { this._observers.mouseMove.disable(); let isActive = false; - let resizeContext = null; // Mouse move observer is only needed when the mouse button is pressed. // this.listenTo( viewDocument, 'mousemove', () => console.log( 'move' ) ); this.listenTo( viewDocument, 'mousemove', ( event, domEventData ) => { - if ( resizeContext ) { - resizeContext.updateSize( domEventData ); + if ( this.activeContext ) { + this.activeContext.updateSize( domEventData ); } } ); this.listenTo( viewDocument, 'mousedown', ( event, domEventData ) => { const target = domEventData.domTarget; - const resizeHandler = isResizeWrapper( target ) || getAncestors( target ).filter( isResizeWrapper )[ 0 ]; + const resizeHandler = isResizeHandler( target ) || getAncestors( target ).filter( isResizeHandler )[ 0 ]; if ( resizeHandler ) { isActive = true; this._observers.mouseMove.enable(); - resizeContext = new ResizeContext( resizeHandler ); - resizeContext.initialize( domEventData ); + this.activeContext = this._getContextByHandler( resizeHandler ); + + if ( this.activeContext ) { + this.activeContext.begin( resizeHandler ); + } } } ); const finishResizing = () => { - // @todo listen also for mouse up outside of the editable. if ( isActive ) { isActive = false; this._observers.mouseMove.disable(); - resizeContext.commit(); - resizeContext.destroy(); - resizeContext = null; + if ( this.activeContext ) { + this.activeContext.commit( this.editor ); + } + + this.activeContext = null; } }; - // @todo: it should listen on the entire window. + // @todo: it should listen on the entire window, as it should also catch events outside of the editable. this.listenTo( viewDocument, 'mouseup', finishResizing ); - function isResizeWrapper( element ) { + function isResizeHandler( element ) { return element.classList && element.classList.contains( 'ck-widget__resizer' ); } } apply( widgetElement, writer ) { - // @todo inline the logic - const ret = new WidgetResizeFeature(); + const context = new ResizeContext2(); + context.attach( widgetElement, writer ); - const renderRefresh = ret.apply( widgetElement, writer ); + this.editor.editing.view.once( 'render', () => context.redraw() ); - this.editor.editing.view.once( 'render', renderRefresh ); + this.contexts.push( context ); + } + + /** + * Returns a resize context associated with given `domResizeWrapper`. + * + * @param {HTMLElement} domResizeWrapper + */ + _getContextByWrapper( domResizeWrapper ) { + for ( const context of this.contexts ) { + if ( domResizeWrapper.isSameNode( context.domResizeWrapper ) ) { + return context; + } + } + } - return ret; + _getContextByHandler( domResizeHandler ) { + return this._getContextByWrapper( getAncestors( domResizeHandler ) + .filter( element => element.classList.contains( 'ck-widget__resizer-wrapper' ) )[ 0 ] ); } } From 8874fade38c3df31f7dbdd1b925576e978ae23b2 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 13:39:15 +0200 Subject: [PATCH 10/93] Internal: redraw the resizer's overlay once commit change has been rendered. --- src/resizecontext.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/resizecontext.js b/src/resizecontext.js index 417d3c15..418eee36 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -101,6 +101,9 @@ export default class ResizeContext { editor.model.change( writer => { writer.setAttribute( HEIGHT_ATTRIBUTE_NAME, newHeight, modelEntry ); } ); + + // Again, render will most likely change image size, so resizers needs a redraw. + editor.editing.view.once( 'render', () => this.redraw() ); } cancel() { From 6d9a991c059e46de09252d8d607241698e8a8f34 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 13:40:07 +0200 Subject: [PATCH 11/93] Internal: added img[height] entry to schema and a basic downcast converter. Upcast converter is still missing, will follow soon. --- src/widgetresizer.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 405b516e..4b0d2bde 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -13,6 +13,8 @@ import MouseMoveObserver from './view/mousemoveobserver'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import ResizeContext2 from './resizecontext'; +const HEIGHT_ATTRIBUTE_NAME = 'height'; + /** * The base class for widget features. This type provides a common API for reusable features of widgets. */ @@ -28,6 +30,9 @@ export default class WidgetResizer extends Plugin { this.contexts = []; this.activeContext = null; + this._registerSchema(); + this._registerConverters(); + const view = this.editor.editing.view; const viewDocument = view.document; @@ -114,4 +119,38 @@ export default class WidgetResizer extends Plugin { return this._getContextByWrapper( getAncestors( domResizeHandler ) .filter( element => element.classList.contains( 'ck-widget__resizer-wrapper' ) )[ 0 ] ); } + + _registerSchema() { + const editor = this.editor; + // Allow bold attribute on text nodes. + editor.model.schema.extend( 'image', { + allowAttributes: HEIGHT_ATTRIBUTE_NAME + } ); + + editor.model.schema.setAttributeProperties( HEIGHT_ATTRIBUTE_NAME, { + isFormatting: true + } ); + } + + _registerConverters() { + const editor = this.editor; + + // Dedicated converter to propagate image's attribute to the img tag. + editor.conversion.for( 'downcast' ).add( dispatcher => + dispatcher.on( 'attribute:height:image', ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const img = conversionApi.mapper.toViewElement( data.item ).getChild( 0 ); + + if ( data.attributeNewValue !== null ) { + viewWriter.setAttribute( HEIGHT_ATTRIBUTE_NAME, data.attributeNewValue, img ); + } else { + viewWriter.removeAttribute( HEIGHT_ATTRIBUTE_NAME, img ); + } + } ) + ); + } } From da6528b17e6dd54db5846896be226ddb6ed2f174 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 13:45:19 +0200 Subject: [PATCH 12/93] Internal: introduced img[height] upcast converter. --- src/widgetresizer.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 4b0d2bde..57aeb4d9 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -152,5 +152,14 @@ export default class WidgetResizer extends Plugin { } } ) ); + + editor.conversion.for( 'upcast' ) + .attributeToAttribute( { + view: { + name: 'img', + key: 'height' + }, + model: 'height' + } ); } } From da3836c7e16fb8b93ba3a83966dde0c68a3e5c07 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 14:07:53 +0200 Subject: [PATCH 13/93] Internal: enhanced CSS colors for the resized elements. --- theme/widget.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theme/widget.css b/theme/widget.css index 6d47dea4..d503c502 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -25,7 +25,8 @@ top: 0; bottom: 0; - outline: 2px dashed red; + outline: 2px dashed var(--ck-color-focus-border); + background-color: rgba(71, 164, 245, 0.25); &.ck-widget__resizer-shadow-active { display: block; From c9aa59a7498447553bf289082757a547ebe4af0e Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 15:27:26 +0200 Subject: [PATCH 14/93] Internal: added a support for resizing height using bottom handlers. --- src/resizecontext.js | 76 +++++++++++++++++++++++++++++++++++--------- theme/widget.css | 4 +++ 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 418eee36..b328f624 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -7,17 +7,23 @@ const HEIGHT_ATTRIBUTE_NAME = 'height'; * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. * * @param {HTMLElement} element + * @param {String} resizerPosition Position of the resize handler, e.g. `"top-left"`, `"bottom-right"`. * @returns {Object} return * @returns {Number} return.x * @returns {Number} return.y */ -function getAbsolutePosition( element ) { +// function getAbsoluteBoundaryPoint( element ) { +function getAbsoluteBoundaryPoint( element, resizerPosition ) { const nativeRectangle = element.getBoundingClientRect(); - - return { + const positionParts = resizerPosition.split( '-' ); + const ret = { x: nativeRectangle.left + element.ownerDocument.defaultView.scrollX, - y: nativeRectangle.top + element.ownerDocument.defaultView.scrollY + y: positionParts[ 0 ] == 'bottom' ? nativeRectangle.bottom : nativeRectangle.top }; + + ret.y += element.ownerDocument.defaultView.scrollY; + + return ret; } export default class ResizeContext { @@ -89,7 +95,9 @@ export default class ResizeContext { begin( domResizeHandler ) { this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); - this.referenceCoordinates = getAbsolutePosition( domResizeHandler ); + this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler ); + + this.referenceCoordinates = getAbsoluteBoundaryPoint( domResizeHandler, this.referenceHandlerPosition ); } commit( editor ) { @@ -104,10 +112,14 @@ export default class ResizeContext { // Again, render will most likely change image size, so resizers needs a redraw. editor.editing.view.once( 'render', () => this.redraw() ); + + this.referenceHandlerPosition = null; } cancel() { this._dismissShadow(); + + this.referenceHandlerPosition = null; } destroy() { @@ -115,22 +127,30 @@ export default class ResizeContext { this.domResizeShadow = null; this.wrapper = null; + this.referenceHandlerPosition = null; } updateSize( domEventData ) { const currentCoordinates = this._extractCoordinates( domEventData ); const yDistance = this.referenceCoordinates.y - currentCoordinates.y; - // For top, left handler: - // yDistance > 0 - element is enlarged - // yDistance < 0 - element is shrinked - - if ( yDistance > 0 ) { - // console.log( 'enlarging' ); - this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + if ( this.referenceHandlerPosition.includes( 'bottom-' ) ) { + if ( yDistance < 0 ) { + // enlarging + this.domResizeShadow.style.bottom = `${ yDistance }px`; + } else { + // shrinking + this.domResizeShadow.style.bottom = `${ yDistance }px`; + } } else { - // console.log( 'shrinking' ); - this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + // default handler: top-left. + if ( yDistance > 0 ) { + // enlarging + this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + } else { + // shrinking + this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + } } } @@ -160,7 +180,7 @@ export default class ResizeContext { icon.set( 'content', dragHandlerIcon ); icon.extendTemplate( { attributes: { - 'class': `ck-widget__resizer ck-widget__resizer-${ currentPosition }` + 'class': `ck-widget__resizer ${ this._getResizerClass( currentPosition ) }` } } ); @@ -192,4 +212,30 @@ export default class ResizeContext { y: event.domEvent.pageY }; } + + /** + * @private + * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. + * @returns {String} A prefixed HTML class name for the resizer element + */ + _getResizerClass( resizerPosition ) { + return `ck-widget__resizer-${ resizerPosition }`; + } + + /** + * Determines the position of a given resize handler. + * + * @private + * @param {HTMLElement} domResizeHandler Handler used to calculate reference point. + * @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched. + */ + _getResizerPosition( domResizeHandler ) { + const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; + + for ( const position of resizerPositions ) { + if ( domResizeHandler.classList.contains( this._getResizerClass( position ) ) ) { + return position; + } + } + } } diff --git a/theme/widget.css b/theme/widget.css index d503c502..6f935ab4 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -25,6 +25,10 @@ top: 0; bottom: 0; + /* @todo: remove this dirty hack. It's purpose is that shadow element should not be + overlapped by elements that follow it in the DOM. */ + z-index: 100; + outline: 2px dashed var(--ck-color-focus-border); background-color: rgba(71, 164, 245, 0.25); From a989d0f59c1b7958884b035e156d4655a379a572 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 15:29:24 +0200 Subject: [PATCH 15/93] Internal: added extra render, otherwise handler would be mispositioned after an initial resize. --- src/resizecontext.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/resizecontext.js b/src/resizecontext.js index b328f624..46015b36 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -110,6 +110,8 @@ export default class ResizeContext { writer.setAttribute( HEIGHT_ATTRIBUTE_NAME, newHeight, modelEntry ); } ); + this.redraw(); + // Again, render will most likely change image size, so resizers needs a redraw. editor.editing.view.once( 'render', () => this.redraw() ); From b636ee6e39626aa4956dd3d3fb9458290148fb38 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 18:50:26 +0200 Subject: [PATCH 16/93] Internal: now widget resizer accepts additional options. This will allow to customize resize host. --- src/resizecontext.js | 42 +++++++++++++++++++++++++++++++++++------- src/widgetresizer.js | 24 +++++++++++++++++++++--- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 46015b36..ce842cb7 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,6 +1,10 @@ import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; import dragHandlerIcon from '../theme/icons/drag-handler.svg'; +/** + * @module widget/resizecontext + */ + const HEIGHT_ATTRIBUTE_NAME = 'height'; /** @@ -26,18 +30,37 @@ function getAbsoluteBoundaryPoint( element, resizerPosition ) { return ret; } +/** + * Stores the internal state of a single resizable object. + * + * @class ResizeContext + */ export default class ResizeContext { - constructor() { - // HTMLElement??? - this.resizeHost = null; + constructor( options ) { + // HTMLElement??? - @todo seems to be not needed. + // this.resizeHost = null; // view/UiElement this.resizeWrapperElement = null; // view/Element this.widgetWrapperElement = null; - // HTMLElement|null - might be uninitialized + + /** + * Container of entire resize UI. + * + * Note that this property is initialized only after the element bound with resizer is drawn + * so it will be a `null` when uninitialized. + * + * @member {HTMLElement|null} + */ this.domResizeWrapper = null; + + /** + * @member {HTMLElement|null} + */ this.domResizeShadow = null; + this.options = options || {}; + // @todo: ---- options below seems like a little outside of a scope of a single context ---- // Size before resizing. this.initialSize = { @@ -158,10 +181,15 @@ export default class ResizeContext { redraw() { if ( this.domResizeWrapper ) { - const resizingHost = this.domResizeWrapper.parentElement.querySelector( 'img' ); + const widgetWrapper = this.domResizeWrapper.parentElement; - this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; - this.domResizeWrapper.style.right = resizingHost.offsetLeft + 'px'; + const resizingHost = this.options.getResizeHost ? + this.options.getResizeHost( widgetWrapper ) : widgetWrapper; + + if ( !widgetWrapper.isSameNode( resizingHost ) ) { + this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; + this.domResizeWrapper.style.right = resizingHost.offsetLeft + 'px'; + } } } diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 57aeb4d9..497ed77a 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -4,7 +4,7 @@ */ /** - * @module widget/widget + * @module widget/widgetresizer */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; @@ -15,6 +15,18 @@ import ResizeContext2 from './resizecontext'; const HEIGHT_ATTRIBUTE_NAME = 'height'; +/** + * Interface describing a resizer. It allows to define available resizer set, specify resizing host etc. + * + * @interface ResizerOptions + */ + +/** + * List of available resizers like `"top-left"`, `"bottom-right"`, etc. + * + * @member {Array.} module:widget/widgetresizer~ResizerOptions#resizers + */ + /** * The base class for widget features. This type provides a common API for reusable features of widgets. */ @@ -93,8 +105,14 @@ export default class WidgetResizer extends Plugin { } } - apply( widgetElement, writer ) { - const context = new ResizeContext2(); + /** + * @param {module:engine/view/containerelement~ContainerElement} widgetElement + * @param {module:engine/view/downcastwriter~DowncastWriter} writer + * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. + * @memberof WidgetResizer + */ + apply( widgetElement, writer, options ) { + const context = new ResizeContext2( options ); context.attach( widgetElement, writer ); this.editor.editing.view.once( 'render', () => context.redraw() ); From d3b96bda17a3d745e207435dc632b8e9b7db0af4 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 1 Jul 2019 21:14:42 +0200 Subject: [PATCH 17/93] Internal: adjusted resizer CSS so that it doesn't block pointer events within the editable. --- theme/widget.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/theme/widget.css b/theme/widget.css index 6f935ab4..d012d314 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -10,6 +10,8 @@ & .ck-widget__resizer-wrapper { visibility: hidden; position: absolute; + /* The wrapper itself should not interfere with pointer device */ + pointer-events: none; left: 0; right: 0; @@ -41,6 +43,8 @@ & .ck-widget__resizer { position: absolute; + /* Resizers are the only UI elements that should interfere with pointer device */ + pointer-events: all; &.ck-widget__resizer-top-left { top: 0; From 9751add7d4b91721a7ce1a764eb86974b193ff84 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 5 Jul 2019 12:57:19 +0200 Subject: [PATCH 18/93] Internal: keep the resized object scale when using handlers. --- src/resizecontext.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index ce842cb7..fdb5c570 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -16,7 +16,6 @@ const HEIGHT_ATTRIBUTE_NAME = 'height'; * @returns {Number} return.x * @returns {Number} return.y */ -// function getAbsoluteBoundaryPoint( element ) { function getAbsoluteBoundaryPoint( element, resizerPosition ) { const nativeRectangle = element.getBoundingClientRect(); const positionParts = resizerPosition.split( '-' ); @@ -30,6 +29,12 @@ function getAbsoluteBoundaryPoint( element, resizerPosition ) { return ret; } +function getAspectRatio( element ) { + const nativeRectangle = element.getBoundingClientRect(); + + return nativeRectangle.width / nativeRectangle.height; +} + /** * Stores the internal state of a single resizable object. * @@ -116,11 +121,17 @@ export default class ResizeContext { * @param {HTMLElement} domResizeHandler Handler used to calculate reference point. */ begin( domResizeHandler ) { + const resizeHost = this._getResizeHost(); + this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler ); this.referenceCoordinates = getAbsoluteBoundaryPoint( domResizeHandler, this.referenceHandlerPosition ); + + if ( resizeHost ) { + this.aspectRatio = getAspectRatio( resizeHost, this.referenceHandlerPosition ); + } } commit( editor ) { @@ -163,18 +174,22 @@ export default class ResizeContext { if ( yDistance < 0 ) { // enlarging this.domResizeShadow.style.bottom = `${ yDistance }px`; + this.domResizeShadow.style.right = `${ yDistance * this.aspectRatio }px`; } else { // shrinking this.domResizeShadow.style.bottom = `${ yDistance }px`; + this.domResizeShadow.style.right = `${ yDistance * this.aspectRatio }px`; } } else { // default handler: top-left. if ( yDistance > 0 ) { // enlarging this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + this.domResizeShadow.style.left = ( yDistance * this.aspectRatio * -1 ) + 'px'; } else { // shrinking this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + this.domResizeShadow.style.left = ( yDistance * this.aspectRatio * -1 ) + 'px'; } } } @@ -183,8 +198,7 @@ export default class ResizeContext { if ( this.domResizeWrapper ) { const widgetWrapper = this.domResizeWrapper.parentElement; - const resizingHost = this.options.getResizeHost ? - this.options.getResizeHost( widgetWrapper ) : widgetWrapper; + const resizingHost = this._getResizeHost(); if ( !widgetWrapper.isSameNode( resizingHost ) ) { this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; @@ -193,6 +207,13 @@ export default class ResizeContext { } } + _getResizeHost() { + const widgetWrapper = this.domResizeWrapper.parentElement; + + return this.options.getResizeHost ? + this.options.getResizeHost( widgetWrapper ) : widgetWrapper; + } + _appendShadowElement( domDocument, domElement ) { const shadowElement = domDocument.createElement( 'div' ); shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' ); From 97e1c8fa77c5efb54ae97135e4fdeaf528d240e7 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 5 Jul 2019 13:21:01 +0200 Subject: [PATCH 19/93] Internal: simplified resizing logic. --- src/resizecontext.js | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index fdb5c570..e9f4ed03 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -171,26 +171,11 @@ export default class ResizeContext { const yDistance = this.referenceCoordinates.y - currentCoordinates.y; if ( this.referenceHandlerPosition.includes( 'bottom-' ) ) { - if ( yDistance < 0 ) { - // enlarging - this.domResizeShadow.style.bottom = `${ yDistance }px`; - this.domResizeShadow.style.right = `${ yDistance * this.aspectRatio }px`; - } else { - // shrinking - this.domResizeShadow.style.bottom = `${ yDistance }px`; - this.domResizeShadow.style.right = `${ yDistance * this.aspectRatio }px`; - } + this.domResizeShadow.style.bottom = `${ yDistance }px`; + this.domResizeShadow.style.right = `${ yDistance * this.aspectRatio }px`; } else { - // default handler: top-left. - if ( yDistance > 0 ) { - // enlarging - this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; - this.domResizeShadow.style.left = ( yDistance * this.aspectRatio * -1 ) + 'px'; - } else { - // shrinking - this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; - this.domResizeShadow.style.left = ( yDistance * this.aspectRatio * -1 ) + 'px'; - } + this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; + this.domResizeShadow.style.left = ( yDistance * this.aspectRatio * -1 ) + 'px'; } } From f68aeb145797f0775dece9f67edfb0ec7c9a0a57 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 8 Jul 2019 17:15:15 +0200 Subject: [PATCH 20/93] Internal: made the resizing shadow follow the mouse cursor. The goal is to always pick the largest resized size allowed by resize cursor position. --- src/resizecontext.js | 65 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index e9f4ed03..fad5ef94 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -20,10 +20,11 @@ function getAbsoluteBoundaryPoint( element, resizerPosition ) { const nativeRectangle = element.getBoundingClientRect(); const positionParts = resizerPosition.split( '-' ); const ret = { - x: nativeRectangle.left + element.ownerDocument.defaultView.scrollX, + x: positionParts[ 1 ] == 'right' ? nativeRectangle.right : nativeRectangle.left, y: positionParts[ 0 ] == 'bottom' ? nativeRectangle.bottom : nativeRectangle.top }; + ret.x += element.ownerDocument.defaultView.scrollX; ret.y += element.ownerDocument.defaultView.scrollY; return ret; @@ -125,9 +126,17 @@ export default class ResizeContext { this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); + /** + * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc of `null` + * if unknown. + * + * @member {String|null} + */ this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler ); - this.referenceCoordinates = getAbsoluteBoundaryPoint( domResizeHandler, this.referenceHandlerPosition ); + const reversedPosition = this._invertPosition( this.referenceHandlerPosition ); + + this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); if ( resizeHost ) { this.aspectRatio = getAspectRatio( resizeHost, this.referenceHandlerPosition ); @@ -168,15 +177,39 @@ export default class ResizeContext { updateSize( domEventData ) { const currentCoordinates = this._extractCoordinates( domEventData ); - const yDistance = this.referenceCoordinates.y - currentCoordinates.y; - if ( this.referenceHandlerPosition.includes( 'bottom-' ) ) { - this.domResizeShadow.style.bottom = `${ yDistance }px`; - this.domResizeShadow.style.right = `${ yDistance * this.aspectRatio }px`; + const proposedSize = { + x: Math.abs( currentCoordinates.x - this.referenceCoordinates.x ), + y: Math.abs( currentCoordinates.y - this.referenceCoordinates.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.x / this.aspectRatio > proposedSize.y ? 'x' : 'y'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + const drawnSize = { + x: proposedSize.x, + y: proposedSize.y + }; + + if ( proposedSize.dominant == 'x' ) { + drawnSize.y = drawnSize.x / this.aspectRatio; } else { - this.domResizeShadow.style.top = ( yDistance * -1 ) + 'px'; - this.domResizeShadow.style.left = ( yDistance * this.aspectRatio * -1 ) + 'px'; + drawnSize.x = drawnSize.y * this.aspectRatio; } + + // Reset shadow bounding. + this.domResizeShadow.style.top = 0; + this.domResizeShadow.style.left = 0; + this.domResizeShadow.style.bottom = 0; + this.domResizeShadow.style.right = 0; + + this.domResizeShadow.style[ this.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto'; + this.domResizeShadow.style[ this.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto'; + + // Apply the actual shadow dimensions. + this.domResizeShadow.style.width = `${ drawnSize.x }px`; + this.domResizeShadow.style.height = `${ drawnSize.y }px`; } redraw() { @@ -274,4 +307,20 @@ export default class ResizeContext { } } } + + /** + * @param {String} position Like `"top-left"`. + * @returns {String} Inverted `position`. + */ + _invertPosition( position ) { + const parts = position.split( '-' ); + const replacements = { + top: 'bottom', + bottom: 'top', + left: 'right', + right: 'left' + }; + + return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; + } } From 75cf6e4ec799d8e9c3e41d336b29d91295f4e97e Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 8 Jul 2019 17:23:36 +0200 Subject: [PATCH 21/93] Internal: removed unused context properties. --- src/resizecontext.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index fad5ef94..bb16aae9 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -68,17 +68,6 @@ export default class ResizeContext { this.options = options || {}; // @todo: ---- options below seems like a little outside of a scope of a single context ---- - // Size before resizing. - this.initialSize = { - x: 0, - y: 0 - }; - - // Position of a clicked resize handler in x and y axes. - this.direction = { - y: 'top', - x: 'left' - }; // Reference point of resizer where the dragging started. It is used to measure the distance to user cursor // traveled, thus how much the image should be enlarged. From 620d14cee7ff2a6bcdb11737693f59f27a13016c Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 9 Jul 2019 11:55:56 +0200 Subject: [PATCH 22/93] Internal: introduced resized dimension information, also adjusted styles. --- src/resizecontext.js | 64 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index bb16aae9..7851ef21 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,6 +1,10 @@ import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; +import View from '@ckeditor/ckeditor5-ui/src/view'; import dragHandlerIcon from '../theme/icons/drag-handler.svg'; +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + /** * @module widget/resizecontext */ @@ -76,6 +80,8 @@ export default class ResizeContext { y: 0, x: 0 }; + + this._cleanupContext(); } /** @@ -94,7 +100,8 @@ export default class ResizeContext { const domElement = this.toDomElement( domDocument ); that.domResizeShadow = that._appendShadowElement( domDocument, domElement ); - that._appendResizers( domElement ); + that._appendResizers( that.domResizeShadow ); + that._appendSizeUi( that.domResizeShadow ); that.domResizeWrapper = domElement; @@ -147,13 +154,22 @@ export default class ResizeContext { // Again, render will most likely change image size, so resizers needs a redraw. editor.editing.view.once( 'render', () => this.redraw() ); - this.referenceHandlerPosition = null; + this._cleanupContext(); } cancel() { this._dismissShadow(); + this._cleanupContext(); + } + + _cleanupContext() { this.referenceHandlerPosition = null; + + this.set( { + proposedX: null, + proposedY: null + } ); } destroy() { @@ -161,7 +177,6 @@ export default class ResizeContext { this.domResizeShadow = null; this.wrapper = null; - this.referenceHandlerPosition = null; } updateSize( domEventData ) { @@ -181,6 +196,11 @@ export default class ResizeContext { y: proposedSize.y }; + this.set( { + proposedX: proposedSize.x, + proposedY: proposedSize.y + } ); + if ( proposedSize.dominant == 'x' ) { drawnSize.y = drawnSize.x / this.aspectRatio; } else { @@ -249,6 +269,21 @@ export default class ResizeContext { } } + _appendSizeUi( domElement ) { + const sizeUi = new SizeView(); + + sizeUi.bind( 'isVisible' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) => + x !== null && y !== null ); + + sizeUi.bind( 'label' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) => + `${ Math.round( x ) } x ${ Math.round( y ) }` ); + + // Make sure icon#element is rendered before passing to appendChild(). + sizeUi.render(); + + domElement.appendChild( sizeUi.element ); + } + _dismissShadow() { this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); this.domResizeShadow.removeAttribute( 'style' ); @@ -313,3 +348,26 @@ export default class ResizeContext { return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; } } + +mix( ResizeContext, ObservableMixin ); + +class SizeView extends View { + constructor() { + super(); + + const bind = this.bindTemplate; + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck ck-size-view' ], + style: { + display: bind.if( 'isVisible', 'none', visible => !visible ) + } + }, + children: [ { + text: bind.to( 'label' ) + } ] + } ); + } +} From 9e63118d9028304c4f6e4e9998989be84126acc7 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 9 Jul 2019 12:43:10 +0200 Subject: [PATCH 23/93] Internal: adjusted resizing styles. --- theme/widget.css | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/theme/widget.css b/theme/widget.css index d012d314..b5f969dc 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -3,6 +3,10 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ + .ck .ck-widget_with-resizer:hover .ck-widget__resizer-shadow { + display: block !important; + } + .ck .ck-widget_with-resizer { /* Make the widget wrapper a relative positioning container for the drag handler. */ position: relative; @@ -31,8 +35,8 @@ overlapped by elements that follow it in the DOM. */ z-index: 100; - outline: 2px dashed var(--ck-color-focus-border); - background-color: rgba(71, 164, 245, 0.25); + outline: 2px solid var(--ck-color-focus-border); + /* background-color: rgba(71, 164, 245, 0.25); */ &.ck-widget__resizer-shadow-active { display: block; @@ -45,22 +49,24 @@ position: absolute; /* Resizers are the only UI elements that should interfere with pointer device */ pointer-events: all; + /* background: red; */ + background: var( --ck-color-focus-border ); &.ck-widget__resizer-top-left { - top: 0; - left: 0; + top: -5px; + left: -5px; } &.ck-widget__resizer-top-right { - top: 0; - right: 0; + top: -5px; + right: -5px; } &.ck-widget__resizer-bottom-right { - bottom: 0; - right: 0; + bottom: -5px; + right: -5px; } &.ck-widget__resizer-bottom-left { - bottom: 0; - left: 0; + bottom: -5px; + left: -5px; } &.ck-widget__resizer-top-left, &.ck-widget__resizer-bottom-right { @@ -116,4 +122,15 @@ width: 60px; height: 60px; outline: 2px solid red !important; +} + +.ck .ck-size-view { + background: lightyellow; + border: 2px solid var(--ck-color-focus-border); + display: inline-block; + padding: 3px; +} + +.ck.ck-content.ck-editor__editable.ck-rounded-corners.ck-editor__editable_inline { + /* padding: 100px !important; */ } \ No newline at end of file From 12ef20706d09565a5b56d1a2b08892725ff9d5f7 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 9 Jul 2019 13:45:05 +0200 Subject: [PATCH 24/93] Internal: make the sizer UI appear next to the dragged handler. --- src/resizecontext.js | 17 ++++++++++++++--- theme/widget.css | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 7851ef21..f64cdbdc 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -130,6 +130,8 @@ export default class ResizeContext { */ this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler ); + this.set( 'orientation', this.referenceHandlerPosition ); + const reversedPosition = this._invertPosition( this.referenceHandlerPosition ); this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); @@ -168,7 +170,8 @@ export default class ResizeContext { this.set( { proposedX: null, - proposedY: null + proposedY: null, + orientation: null } ); } @@ -278,10 +281,14 @@ export default class ResizeContext { sizeUi.bind( 'label' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) => `${ Math.round( x ) } x ${ Math.round( y ) }` ); + sizeUi.bind( 'orientation' ).to( this ); + // Make sure icon#element is rendered before passing to appendChild(). sizeUi.render(); - domElement.appendChild( sizeUi.element ); + this.sizeElement = sizeUi.element; + + domElement.appendChild( this.sizeElement ); } _dismissShadow() { @@ -360,7 +367,11 @@ class SizeView extends View { this.setTemplate( { tag: 'div', attributes: { - class: [ 'ck ck-size-view' ], + class: [ + 'ck', + 'ck-size-view', + bind.to( 'orientation', value => value ? `ck-orientation-${ value }` : '' ) + ], style: { display: bind.if( 'isVisible', 'none', visible => !visible ) } diff --git a/theme/widget.css b/theme/widget.css index b5f969dc..675fb0af 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -129,6 +129,29 @@ border: 2px solid var(--ck-color-focus-border); display: inline-block; padding: 3px; + + --ck-sizer-offset: 20px; + + &.ck-orientation-top-left, &.ck-orientation-top-right, &.ck-orientation-bottom-right, &.ck-orientation-bottom-left { + position: absolute; + } + + &.ck-orientation-top-left { + top: var( --ck-sizer-offset ); + left: var( --ck-sizer-offset ); + } + &.ck-orientation-top-right { + top: var( --ck-sizer-offset ); + right: var( --ck-sizer-offset ); + } + &.ck-orientation-bottom-right { + bottom: var( --ck-sizer-offset ); + right: var( --ck-sizer-offset ); + } + &.ck-orientation-bottom-left { + bottom: var( --ck-sizer-offset ); + right: var( --ck-sizer-offset ); + } } .ck.ck-content.ck-editor__editable.ck-rounded-corners.ck-editor__editable_inline { From 62f7d7fcec2dd60446c24a948e6e4d5f20c238b9 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 9 Jul 2019 14:13:00 +0200 Subject: [PATCH 25/93] Internal: fixed sizer alignment for bottom left corner. --- theme/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/widget.css b/theme/widget.css index 675fb0af..52cb1c7e 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -150,7 +150,7 @@ } &.ck-orientation-bottom-left { bottom: var( --ck-sizer-offset ); - right: var( --ck-sizer-offset ); + left: var( --ck-sizer-offset ); } } From 45a1b8e2498cdff79f52642948bab8b5368124ed Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 10 Jul 2019 13:35:18 +0200 Subject: [PATCH 26/93] Internal: [WIP] delegating resizer strategy into a dedicated type. --- src/resizecontext.js | 68 +++++----------------------- src/resizercentral.js | 100 ++++++++++++++++++++++++++++++++++++++++++ src/resizerside.js | 74 +++++++++++++++++++++++++++++++ src/utils.js | 23 ++++++++++ 4 files changed, 208 insertions(+), 57 deletions(-) create mode 100644 src/resizercentral.js create mode 100644 src/resizerside.js diff --git a/src/resizecontext.js b/src/resizecontext.js index f64cdbdc..e1a15619 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,6 +1,8 @@ import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; import View from '@ckeditor/ckeditor5-ui/src/view'; import dragHandlerIcon from '../theme/icons/drag-handler.svg'; +import ResizerCentral from './resizercentral'; +import { getAbsoluteBoundaryPoint } from './utils'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; @@ -11,29 +13,6 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; const HEIGHT_ATTRIBUTE_NAME = 'height'; -/** - * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. - * - * @param {HTMLElement} element - * @param {String} resizerPosition Position of the resize handler, e.g. `"top-left"`, `"bottom-right"`. - * @returns {Object} return - * @returns {Number} return.x - * @returns {Number} return.y - */ -function getAbsoluteBoundaryPoint( element, resizerPosition ) { - const nativeRectangle = element.getBoundingClientRect(); - const positionParts = resizerPosition.split( '-' ); - const ret = { - x: positionParts[ 1 ] == 'right' ? nativeRectangle.right : nativeRectangle.left, - y: positionParts[ 0 ] == 'bottom' ? nativeRectangle.bottom : nativeRectangle.top - }; - - ret.x += element.ownerDocument.defaultView.scrollX; - ret.y += element.ownerDocument.defaultView.scrollY; - - return ret; -} - function getAspectRatio( element ) { const nativeRectangle = element.getBoundingClientRect(); @@ -54,6 +33,8 @@ export default class ResizeContext { // view/Element this.widgetWrapperElement = null; + this.resizeStrategy = new ResizerCentral( this, options ); + /** * Container of entire resize UI. * @@ -139,6 +120,8 @@ export default class ResizeContext { if ( resizeHost ) { this.aspectRatio = getAspectRatio( resizeHost, this.referenceHandlerPosition ); } + + this.resizeStrategy.begin( domResizeHandler ); } commit( editor ) { @@ -156,12 +139,16 @@ export default class ResizeContext { // Again, render will most likely change image size, so resizers needs a redraw. editor.editing.view.once( 'render', () => this.redraw() ); + this.resizeStrategy.commit( editor ); + this._cleanupContext(); } cancel() { this._dismissShadow(); + this.resizeStrategy.cancel(); + this._cleanupContext(); } @@ -183,45 +170,12 @@ export default class ResizeContext { } updateSize( domEventData ) { - const currentCoordinates = this._extractCoordinates( domEventData ); - - const proposedSize = { - x: Math.abs( currentCoordinates.x - this.referenceCoordinates.x ), - y: Math.abs( currentCoordinates.y - this.referenceCoordinates.y ) - }; - - // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.x / this.aspectRatio > proposedSize.y ? 'x' : 'y'; - proposedSize.max = proposedSize[ proposedSize.dominant ]; - - const drawnSize = { - x: proposedSize.x, - y: proposedSize.y - }; + const proposedSize = this.resizeStrategy.updateSize( domEventData ); this.set( { proposedX: proposedSize.x, proposedY: proposedSize.y } ); - - if ( proposedSize.dominant == 'x' ) { - drawnSize.y = drawnSize.x / this.aspectRatio; - } else { - drawnSize.x = drawnSize.y * this.aspectRatio; - } - - // Reset shadow bounding. - this.domResizeShadow.style.top = 0; - this.domResizeShadow.style.left = 0; - this.domResizeShadow.style.bottom = 0; - this.domResizeShadow.style.right = 0; - - this.domResizeShadow.style[ this.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto'; - this.domResizeShadow.style[ this.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto'; - - // Apply the actual shadow dimensions. - this.domResizeShadow.style.width = `${ drawnSize.x }px`; - this.domResizeShadow.style.height = `${ drawnSize.y }px`; } redraw() { diff --git a/src/resizercentral.js b/src/resizercentral.js new file mode 100644 index 00000000..c2eaf541 --- /dev/null +++ b/src/resizercentral.js @@ -0,0 +1,100 @@ +import { + getAbsoluteBoundaryPoint +} from './utils'; + +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +/** + * Implements a resizer that enlarges/shrinks in all directions. + * + * @class ResizerCentral + */ +export default class ResizerCentral { + constructor( context, options ) { + this.context = context; + this.options = options || {}; + } + + attach() {} + + begin() { + const resizeHost = this.context._getResizeHost(); + this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); + } + + commit() {} + + cancel() {} + + destroy() {} + + updateSize( domEventData ) { + const context = this.context; + const currentCoordinates = context._extractCoordinates( domEventData ); + + const enlargement = { + x: ( currentCoordinates.x - this.closestReferencePoint.x ) * 2, + y: ( currentCoordinates.y - this.closestReferencePoint.y ) * 2 + }; + + // const resizeHost = this.context._getResizeHost(); + + // const initialSize = { + // x: resizeHost.width, + // y: resizeHost.height + // }; + + const proposedSize = { + // x: Math.abs( currentCoordinates.x - context.referenceCoordinates.x ), + // y: Math.abs( currentCoordinates.y - context.referenceCoordinates.y ) + x: Math.abs( this.closestReferencePoint.x - context.referenceCoordinates.x + enlargement.x ), + y: Math.abs( this.closestReferencePoint.y - context.referenceCoordinates.y + enlargement.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.x / context.aspectRatio > proposedSize.y ? 'x' : 'y'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + const drawnSize = { + x: proposedSize.x, + y: proposedSize.y + }; + + if ( proposedSize.dominant == 'x' ) { + drawnSize.y = drawnSize.x / context.aspectRatio; + } else { + drawnSize.x = drawnSize.y * context.aspectRatio; + } + + // console.log( 'initial', initialSize, 'drawn', drawnSize ); + + // Reset shadow bounding. + context.domResizeShadow.style.top = 'auto'; + context.domResizeShadow.style.left = 'auto'; + context.domResizeShadow.style.bottom = 'auto'; + context.domResizeShadow.style.right = 'auto'; + + // context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto'; + // context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto'; + + // const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); + + // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ ( drawnSize.x - initialSize.x ) / 2 * -1 }px`; + // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ ( drawnSize.y - initialSize.y ) / 2 * -1 }px`; + // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ enlargement.x / 2 * -1 }px`; + // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ enlargement.y / 2 * -1 }px`; + + // Apply the actual shadow dimensions. + context.domResizeShadow.style.width = `${ drawnSize.x }px`; + context.domResizeShadow.style.height = `${ drawnSize.y }px`; + + return proposedSize; + } + + redraw() {} + + _getResizeHost() {} +} + +mix( ResizerCentral, ObservableMixin ); diff --git a/src/resizerside.js b/src/resizerside.js new file mode 100644 index 00000000..d0aad32a --- /dev/null +++ b/src/resizerside.js @@ -0,0 +1,74 @@ +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +/** + * @module widget/resizecontext + */ + +/** + * Implements a resizer that enlarges to the dragged side. + * + * @class ResizerSide + */ +export default class ResizerSide { + constructor( context, options ) { + this.context = context; + this.options = options || {}; + } + + attach() {} + + begin() {} + + commit() {} + + cancel() {} + + destroy() {} + + updateSize( domEventData ) { + const context = this.context; + const currentCoordinates = context._extractCoordinates( domEventData ); + + const proposedSize = { + x: Math.abs( currentCoordinates.x - context.referenceCoordinates.x ), + y: Math.abs( currentCoordinates.y - context.referenceCoordinates.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.x / context.aspectRatio > proposedSize.y ? 'x' : 'y'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + const drawnSize = { + x: proposedSize.x, + y: proposedSize.y + }; + + if ( proposedSize.dominant == 'x' ) { + drawnSize.y = drawnSize.x / context.aspectRatio; + } else { + drawnSize.x = drawnSize.y * context.aspectRatio; + } + + // Reset shadow bounding. + context.domResizeShadow.style.top = 0; + context.domResizeShadow.style.left = 0; + context.domResizeShadow.style.bottom = 0; + context.domResizeShadow.style.right = 0; + + context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto'; + context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto'; + + // Apply the actual shadow dimensions. + context.domResizeShadow.style.width = `${ drawnSize.x }px`; + context.domResizeShadow.style.height = `${ drawnSize.y }px`; + + return proposedSize; + } + + redraw() {} + + _getResizeHost() {} +} + +mix( ResizerSide, ObservableMixin ); diff --git a/src/utils.js b/src/utils.js index 5c4d0f65..83b5b6df 100644 --- a/src/utils.js +++ b/src/utils.js @@ -352,6 +352,29 @@ export function viewToModelPositionOutsideModelElement( model, viewElementMatche }; } +/** + * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. + * + * @param {HTMLElement} element + * @param {String} resizerPosition Position of the resize handler, e.g. `"top-left"`, `"bottom-right"`. + * @returns {Object} return + * @returns {Number} return.x + * @returns {Number} return.y + */ +export function getAbsoluteBoundaryPoint( element, resizerPosition ) { + const nativeRectangle = element.getBoundingClientRect(); + const positionParts = resizerPosition.split( '-' ); + const ret = { + x: positionParts[ 1 ] == 'right' ? nativeRectangle.right : nativeRectangle.left, + y: positionParts[ 0 ] == 'bottom' ? nativeRectangle.bottom : nativeRectangle.top + }; + + ret.x += element.ownerDocument.defaultView.scrollX; + ret.y += element.ownerDocument.defaultView.scrollY; + + return ret; +} + // Default filler offset function applied to all widget elements. // // @returns {null} From 218499250f80261dbeba150f0a0cb75e4b5a73fa Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 10 Jul 2019 17:37:25 +0200 Subject: [PATCH 27/93] Internal: refined resizing relative to the center point. --- src/resizecontext.js | 25 ++++++++++++++++--------- src/resizercentral.js | 24 +++++------------------- src/resizerside.js | 2 +- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index e1a15619..931278ea 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -13,12 +13,6 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; const HEIGHT_ATTRIBUTE_NAME = 'height'; -function getAspectRatio( element ) { - const nativeRectangle = element.getBoundingClientRect(); - - return nativeRectangle.width / nativeRectangle.height; -} - /** * Stores the internal state of a single resizable object. * @@ -62,6 +56,16 @@ export default class ResizeContext { x: 0 }; + /** + * Size of an image before resize. + * + * This information is only known after DOM was rendered, so it will be updated later. + */ + this.originalSize = { + x: 0, + y: 0 + }; + this._cleanupContext(); } @@ -117,9 +121,12 @@ export default class ResizeContext { this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); - if ( resizeHost ) { - this.aspectRatio = getAspectRatio( resizeHost, this.referenceHandlerPosition ); - } + this.originalSize = { + x: resizeHost.width, + y: resizeHost.height + }; + + this.aspectRatio = this.originalSize.x / this.originalSize.y; this.resizeStrategy.begin( domResizeHandler ); } diff --git a/src/resizercentral.js b/src/resizercentral.js index c2eaf541..baa6059d 100644 --- a/src/resizercentral.js +++ b/src/resizercentral.js @@ -38,16 +38,9 @@ export default class ResizerCentral { y: ( currentCoordinates.y - this.closestReferencePoint.y ) * 2 }; - // const resizeHost = this.context._getResizeHost(); - - // const initialSize = { - // x: resizeHost.width, - // y: resizeHost.height - // }; + const originalSize = this.context.originalSize; const proposedSize = { - // x: Math.abs( currentCoordinates.x - context.referenceCoordinates.x ), - // y: Math.abs( currentCoordinates.y - context.referenceCoordinates.y ) x: Math.abs( this.closestReferencePoint.x - context.referenceCoordinates.x + enlargement.x ), y: Math.abs( this.closestReferencePoint.y - context.referenceCoordinates.y + enlargement.y ) }; @@ -67,29 +60,22 @@ export default class ResizerCentral { drawnSize.x = drawnSize.y * context.aspectRatio; } - // console.log( 'initial', initialSize, 'drawn', drawnSize ); - // Reset shadow bounding. context.domResizeShadow.style.top = 'auto'; context.domResizeShadow.style.left = 'auto'; context.domResizeShadow.style.bottom = 'auto'; context.domResizeShadow.style.right = 'auto'; - // context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto'; - // context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto'; - - // const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); + const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); - // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ ( drawnSize.x - initialSize.x ) / 2 * -1 }px`; - // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ ( drawnSize.y - initialSize.y ) / 2 * -1 }px`; - // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ enlargement.x / 2 * -1 }px`; - // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ enlargement.y / 2 * -1 }px`; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ ( originalSize.y - drawnSize.y ) / 2 }px`; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ ( originalSize.x - drawnSize.x ) / 2 }px`; // Apply the actual shadow dimensions. context.domResizeShadow.style.width = `${ drawnSize.x }px`; context.domResizeShadow.style.height = `${ drawnSize.y }px`; - return proposedSize; + return drawnSize; // @todo decide what size should actually be returned, drawn or intended. } redraw() {} diff --git a/src/resizerside.js b/src/resizerside.js index d0aad32a..183fc0a9 100644 --- a/src/resizerside.js +++ b/src/resizerside.js @@ -63,7 +63,7 @@ export default class ResizerSide { context.domResizeShadow.style.width = `${ drawnSize.x }px`; context.domResizeShadow.style.height = `${ drawnSize.y }px`; - return proposedSize; + return drawnSize; // @todo decide what size should actually be returned. } redraw() {} From 837288ff77bd758593440dcf44bd73fa85c29f30 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 17 Jul 2019 16:46:41 +0200 Subject: [PATCH 28/93] Internal: improved resizer visuals. --- src/resizecontext.js | 18 +++++------------- src/widgetresizer.js | 2 +- theme/widget.css | 24 +++++++++++++++--------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 931278ea..7e6ca077 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,8 +1,7 @@ -import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; import View from '@ckeditor/ckeditor5-ui/src/view'; -import dragHandlerIcon from '../theme/icons/drag-handler.svg'; import ResizerCentral from './resizercentral'; import { getAbsoluteBoundaryPoint } from './utils'; +import Template from '@ckeditor/ckeditor5-ui/src/template'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; @@ -217,19 +216,12 @@ export default class ResizeContext { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; for ( const currentPosition of resizerPositions ) { - // Use the IconView from the UI library. - const icon = new IconView(); - icon.set( 'content', dragHandlerIcon ); - icon.extendTemplate( { + domElement.appendChild( ( new Template( { + tag: 'div', attributes: { - 'class': `ck-widget__resizer ${ this._getResizerClass( currentPosition ) }` + class: `ck-widget__resizer ${ this._getResizerClass( currentPosition ) }` } - } ); - - // Make sure icon#element is rendered before passing to appendChild(). - icon.render(); - - domElement.appendChild( icon.element ); + } ).render() ) ); } } diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 497ed77a..edff9d66 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -70,7 +70,7 @@ export default class WidgetResizer extends Plugin { this.listenTo( viewDocument, 'mousedown', ( event, domEventData ) => { const target = domEventData.domTarget; - const resizeHandler = isResizeHandler( target ) || getAncestors( target ).filter( isResizeHandler )[ 0 ]; + const resizeHandler = isResizeHandler( target ) ? target : getAncestors( target ).filter( isResizeHandler )[ 0 ]; if ( resizeHandler ) { isActive = true; diff --git a/theme/widget.css b/theme/widget.css index 52cb1c7e..9750e850 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -22,6 +22,10 @@ top: 0; bottom: 0; + --ck-resizer-size: 14px; + --ck-resizer-border-width: 2px; + --ck-resizer-offset: -2px; + & .ck-widget__resizer-shadow { display: none; position: absolute; @@ -49,24 +53,26 @@ position: absolute; /* Resizers are the only UI elements that should interfere with pointer device */ pointer-events: all; - /* background: red; */ + width: var( --ck-resizer-size ); + height: var( --ck-resizer-size ); background: var( --ck-color-focus-border ); + outline: var( --ck-resizer-border-width ) solid white; &.ck-widget__resizer-top-left { - top: -5px; - left: -5px; + top: var( --ck-resizer-offset ); + left: var( --ck-resizer-offset ); } &.ck-widget__resizer-top-right { - top: -5px; - right: -5px; + top: var( --ck-resizer-offset ); + right: var( --ck-resizer-offset ); } &.ck-widget__resizer-bottom-right { - bottom: -5px; - right: -5px; + bottom: var( --ck-resizer-offset ); + right: var( --ck-resizer-offset ); } &.ck-widget__resizer-bottom-left { - bottom: -5px; - left: -5px; + bottom: var( --ck-resizer-offset ); + left: var( --ck-resizer-offset ); } &.ck-widget__resizer-top-left, &.ck-widget__resizer-bottom-right { From c9f3486589978d128eaf7f60376ed4732e9e512a Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 17 Jul 2019 17:02:13 +0200 Subject: [PATCH 29/93] Internal: allow for providing a custom pixel ratio function. --- src/resizecontext.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 7e6ca077..30765330 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -125,7 +125,8 @@ export default class ResizeContext { y: resizeHost.height }; - this.aspectRatio = this.originalSize.x / this.originalSize.y; + this.aspectRatio = this.options.getAspectRatio ? + this.options.getAspectRatio( resizeHost ) : this.originalSize.x / this.originalSize.y; this.resizeStrategy.begin( domResizeHandler ); } From 769a0c46b9ac0e3b3fe936550c127e9c63015034 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 18 Jul 2019 11:51:22 +0200 Subject: [PATCH 30/93] Internal: added top-bound resizer. --- src/resizertopbound.js | 104 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/resizertopbound.js diff --git a/src/resizertopbound.js b/src/resizertopbound.js new file mode 100644 index 00000000..cea65889 --- /dev/null +++ b/src/resizertopbound.js @@ -0,0 +1,104 @@ +import { + getAbsoluteBoundaryPoint +} from './utils'; + +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +/** + * + * This resizer is top bound, and expands symmetrically towards left, right and increasingly fast toward bottom + * (to compensate for top anchoring). + * + * + * + */ + +/** + * Implements a resizer that enlarges/shrinks in all directions. + * + * @class ResizerCentral + */ +export default class ResizerCentral { + constructor( context, options ) { + this.context = context; + this.options = options || {}; + } + + attach() {} + + begin() { + const resizeHost = this.context._getResizeHost(); + this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); + } + + commit() {} + + cancel() {} + + destroy() {} + + updateSize( domEventData ) { + const context = this.context; + const currentCoordinates = context._extractCoordinates( domEventData ); + + const enlargement = { + x: ( currentCoordinates.x - this.closestReferencePoint.x ) * 2, + y: ( currentCoordinates.y - this.closestReferencePoint.y ) + }; + + const resizeHost = this.context._getResizeHost(); + + const originalSize = { + x: resizeHost.width, + y: resizeHost.height + }; + + const proposedSize = { + x: Math.abs( this.closestReferencePoint.x - context.referenceCoordinates.x + enlargement.x ), + y: Math.abs( this.closestReferencePoint.y - context.referenceCoordinates.y + enlargement.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.x / context.aspectRatio > proposedSize.y ? 'x' : 'y'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + const drawnSize = { + x: proposedSize.x, + y: proposedSize.y + }; + + if ( proposedSize.dominant == 'x' ) { + drawnSize.y = drawnSize.x / context.aspectRatio; + } else { + drawnSize.x = drawnSize.y * context.aspectRatio; + } + + // Reset shadow bounding. + context.domResizeShadow.style.top = 'auto'; + context.domResizeShadow.style.left = 'auto'; + context.domResizeShadow.style.bottom = 'auto'; + context.domResizeShadow.style.right = 'auto'; + + const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); + + const diff2 = { + x: parseFloat( originalSize.x ) - drawnSize.x, + y: parseFloat( originalSize.y ) - drawnSize.y + }; + + // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ diff2.y / 2 }px`; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = '0px'; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ diff2.x / 2 }px`; + + // Apply the actual shadow dimensions. + context.domResizeShadow.style.width = `${ drawnSize.x }px`; + context.domResizeShadow.style.height = `${ drawnSize.y }px`; + + return drawnSize; // @todo decide what size should actually be returned. + } + + redraw() {} +} + +mix( ResizerCentral, ObservableMixin ); From 43a19741de3e3b2b6df953b44ac4fe7ef954520c Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 18 Jul 2019 13:58:13 +0200 Subject: [PATCH 31/93] Internal: top-bound resizer now supports resizing focus host as well as the shadow alone. --- src/resizecontext.js | 8 +++++--- src/resizercentral.js | 5 +++++ src/resizertopbound.js | 45 +++++++++++++++++++++++++++--------------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 30765330..c3e0b0d7 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,5 +1,6 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; -import ResizerCentral from './resizercentral'; +// import ResizerCentral from './resizercentral'; +import ResizerTopBound from './resizertopbound'; import { getAbsoluteBoundaryPoint } from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; @@ -26,7 +27,8 @@ export default class ResizeContext { // view/Element this.widgetWrapperElement = null; - this.resizeStrategy = new ResizerCentral( this, options ); + // this.resizeStrategy = new ResizerCentral( this, options ); + this.resizeStrategy = new ResizerTopBound( this, options ); /** * Container of entire resize UI. @@ -233,7 +235,7 @@ export default class ResizeContext { x !== null && y !== null ); sizeUi.bind( 'label' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) => - `${ Math.round( x ) } x ${ Math.round( y ) }` ); + `${ Math.round( x ) }x${ Math.round( y ) }` ); sizeUi.bind( 'orientation' ).to( this ); diff --git a/src/resizercentral.js b/src/resizercentral.js index baa6059d..917a57c4 100644 --- a/src/resizercentral.js +++ b/src/resizercentral.js @@ -75,6 +75,11 @@ export default class ResizerCentral { context.domResizeShadow.style.width = `${ drawnSize.x }px`; context.domResizeShadow.style.height = `${ drawnSize.y }px`; + // const resizingHost = this.context._getResizeHost(); + + // resizingHost.style.width = `${ drawnSize.x }px`; + // resizingHost.style.height = `${ drawnSize.y }px`; + return drawnSize; // @todo decide what size should actually be returned, drawn or intended. } diff --git a/src/resizertopbound.js b/src/resizertopbound.js index cea65889..0e40bf51 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -4,6 +4,7 @@ import { import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; /** * @@ -74,26 +75,38 @@ export default class ResizerCentral { drawnSize.x = drawnSize.y * context.aspectRatio; } - // Reset shadow bounding. - context.domResizeShadow.style.top = 'auto'; - context.domResizeShadow.style.left = 'auto'; - context.domResizeShadow.style.bottom = 'auto'; - context.domResizeShadow.style.right = 'auto'; + const resizeUsingImage = global.window.pocResizeUsingImage !== false; + let shadowBoundValue = '0px'; - const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); + if ( !resizeUsingImage ) { + shadowBoundValue = 'auto'; + } - const diff2 = { - x: parseFloat( originalSize.x ) - drawnSize.x, - y: parseFloat( originalSize.y ) - drawnSize.y - }; + // Reset shadow bounding. + context.domResizeShadow.style.top = shadowBoundValue; + context.domResizeShadow.style.left = shadowBoundValue; + context.domResizeShadow.style.bottom = shadowBoundValue; + context.domResizeShadow.style.right = shadowBoundValue; + + if ( resizeUsingImage ) { + resizeHost.style.width = `${ drawnSize.x }px`; + resizeHost.style.height = `${ drawnSize.y }px`; + } else { + const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); + + const diff2 = { + x: parseFloat( originalSize.x ) - drawnSize.x, + y: parseFloat( originalSize.y ) - drawnSize.y + }; - // context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ diff2.y / 2 }px`; - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = '0px'; - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ diff2.x / 2 }px`; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ diff2.y / 2 }px`; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = '0px'; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ diff2.x / 2 }px`; - // Apply the actual shadow dimensions. - context.domResizeShadow.style.width = `${ drawnSize.x }px`; - context.domResizeShadow.style.height = `${ drawnSize.y }px`; + // Apply the actual shadow dimensions. + context.domResizeShadow.style.width = `${ drawnSize.x }px`; + context.domResizeShadow.style.height = `${ drawnSize.y }px`; + } return drawnSize; // @todo decide what size should actually be returned. } From c5159980f2d5bb6d2c33fd346188d776f28282c4 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 19 Jul 2019 13:08:10 +0200 Subject: [PATCH 32/93] Internal: adjusted how aspect ratio is computed if no custom function is provided. Previous implementation didn't work with tables. --- src/resizecontext.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index c3e0b0d7..6eb7d46a 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -122,9 +122,10 @@ export default class ResizeContext { this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); + // @todo: this part might be lazy used only in case if getAspectRatio is not given as it might force repaint. this.originalSize = { - x: resizeHost.width, - y: resizeHost.height + x: resizeHost.clientWidth, + y: resizeHost.clientHeight }; this.aspectRatio = this.options.getAspectRatio ? From 728b35154f844c9ff5cec19e0d1d29784d6b919b Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 19 Jul 2019 17:47:17 +0200 Subject: [PATCH 33/93] Internal: exposed a observable method that allows you to implement a custom resize strategy. Added to be able to preview different implementations in PoC. --- src/widgetresizer.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index edff9d66..eb1d8d6d 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -12,6 +12,7 @@ import MouseObserver from './view/mouseobserver'; import MouseMoveObserver from './view/mousemoveobserver'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import ResizeContext2 from './resizecontext'; +import ResizerTopBound from './resizertopbound'; const HEIGHT_ATTRIBUTE_NAME = 'height'; @@ -39,6 +40,14 @@ export default class WidgetResizer extends Plugin { } init() { + this.set( 'resizerStrategy', null ); + + this.on( 'change:resizerStrategy', ( event, name, value ) => { + for ( const context of this.contexts ) { + context.resizeStrategy = new ( value || ResizerTopBound )( context, context.options ); + } + } ); + this.contexts = []; this.activeContext = null; From b4463258b4bec6a56dd85b50caef5ae301e1fdb2 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 22 Jul 2019 10:23:10 +0200 Subject: [PATCH 34/93] Internal: added a possibility to resize the image itself in resizercentral too. --- src/resizercentral.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/resizercentral.js b/src/resizercentral.js index 917a57c4..a239095a 100644 --- a/src/resizercentral.js +++ b/src/resizercentral.js @@ -4,6 +4,7 @@ import { import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; /** * Implements a resizer that enlarges/shrinks in all directions. @@ -68,18 +69,22 @@ export default class ResizerCentral { const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ ( originalSize.y - drawnSize.y ) / 2 }px`; - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ ( originalSize.x - drawnSize.x ) / 2 }px`; + const resizeUsingImage = global.window.pocResizeUsingImage !== false; + + if ( !resizeUsingImage ) { + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ ( originalSize.y - drawnSize.y ) / 2 }px`; + context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ ( originalSize.x - drawnSize.x ) / 2 }px`; + } else { + const resizingHost = this.context._getResizeHost(); + + resizingHost.style.width = `${ drawnSize.x }px`; + resizingHost.style.height = `${ drawnSize.y }px`; + } // Apply the actual shadow dimensions. context.domResizeShadow.style.width = `${ drawnSize.x }px`; context.domResizeShadow.style.height = `${ drawnSize.y }px`; - // const resizingHost = this.context._getResizeHost(); - - // resizingHost.style.width = `${ drawnSize.x }px`; - // resizingHost.style.height = `${ drawnSize.y }px`; - return drawnSize; // @todo decide what size should actually be returned, drawn or intended. } From b820a19dbe8aa574c00ea990be90d535a6d7a9bd Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 22 Jul 2019 11:28:37 +0200 Subject: [PATCH 35/93] Internal: changed the default resizing strategy into center. --- src/resizecontext.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 6eb7d46a..3697bc7f 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,6 +1,5 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; -// import ResizerCentral from './resizercentral'; -import ResizerTopBound from './resizertopbound'; +import ResizerCentral from './resizercentral'; import { getAbsoluteBoundaryPoint } from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; @@ -27,8 +26,7 @@ export default class ResizeContext { // view/Element this.widgetWrapperElement = null; - // this.resizeStrategy = new ResizerCentral( this, options ); - this.resizeStrategy = new ResizerTopBound( this, options ); + this.resizeStrategy = new ResizerCentral( this, options ); /** * Container of entire resize UI. From 2430e23ab4451d4bc8295ecbfa2627cda6b57bed Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 22 Jul 2019 15:35:06 +0200 Subject: [PATCH 36/93] Internal: changed the default resize type. --- src/resizecontext.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 3697bc7f..d320875c 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,5 +1,5 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; -import ResizerCentral from './resizercentral'; +import ResizerTopBound from './resizertopbound'; import { getAbsoluteBoundaryPoint } from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; @@ -26,7 +26,7 @@ export default class ResizeContext { // view/Element this.widgetWrapperElement = null; - this.resizeStrategy = new ResizerCentral( this, options ); + this.resizeStrategy = new ResizerTopBound( this, options ); /** * Container of entire resize UI. From 243e33f7783ab9968739d08bc82147a33f2e5ed8 Mon Sep 17 00:00:00 2001 From: dkonopka Date: Wed, 24 Jul 2019 10:28:57 +0200 Subject: [PATCH 37/93] Smaller resizer size and changed its position. --- theme/widget.css | 198 +++++++++++++++++++++++------------------------ 1 file changed, 98 insertions(+), 100 deletions(-) diff --git a/theme/widget.css b/theme/widget.css index 9750e850..02874dfa 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -3,94 +3,97 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ - .ck .ck-widget_with-resizer:hover .ck-widget__resizer-shadow { - display: block !important; - } +:root { + --ck-color-resizer: var(--ck-color-focus-border); + --ck-resizer-size: 10px; + --ck-resizer-border-width: 2px; + + /* Set resizer with 50% offset. */ + --ck-resizer-offset: calc( ( var(--ck-resizer-size) / -2 ) - 1px ); +} .ck .ck-widget_with-resizer { /* Make the widget wrapper a relative positioning container for the drag handler. */ position: relative; +} +.ck .ck-widget_with-resizer.ck-widget_selected { & .ck-widget__resizer-wrapper { - visibility: hidden; - position: absolute; - /* The wrapper itself should not interfere with pointer device */ - pointer-events: none; - - left: 0; - right: 0; - top: 0; - bottom: 0; - - --ck-resizer-size: 14px; - --ck-resizer-border-width: 2px; - --ck-resizer-offset: -2px; - - & .ck-widget__resizer-shadow { - display: none; - position: absolute; - - left: 0; - right: 0; - top: 0; - bottom: 0; - - /* @todo: remove this dirty hack. It's purpose is that shadow element should not be - overlapped by elements that follow it in the DOM. */ - z-index: 100; - - outline: 2px solid var(--ck-color-focus-border); - /* background-color: rgba(71, 164, 245, 0.25); */ - - &.ck-widget__resizer-shadow-active { - display: block; - visibility: visible; - } - } + visibility: visible; } - & .ck-widget__resizer { - position: absolute; - /* Resizers are the only UI elements that should interfere with pointer device */ - pointer-events: all; - width: var( --ck-resizer-size ); - height: var( --ck-resizer-size ); - background: var( --ck-color-focus-border ); - outline: var( --ck-resizer-border-width ) solid white; - - &.ck-widget__resizer-top-left { - top: var( --ck-resizer-offset ); - left: var( --ck-resizer-offset ); - } - &.ck-widget__resizer-top-right { - top: var( --ck-resizer-offset ); - right: var( --ck-resizer-offset ); - } - &.ck-widget__resizer-bottom-right { - bottom: var( --ck-resizer-offset ); - right: var( --ck-resizer-offset ); - } - &.ck-widget__resizer-bottom-left { - bottom: var( --ck-resizer-offset ); - left: var( --ck-resizer-offset ); - } + & .ck-widget__resizer-shadow { + display: block; + } +} - &.ck-widget__resizer-top-left, &.ck-widget__resizer-bottom-right { - &:hover { - cursor: nwse-resize; - } - } +.ck .ck-widget__resizer-wrapper { + visibility: hidden; + position: absolute; - /* Show the selection handler on mouse hover over the widget. */ - &:hover { - cursor: nesw-resize; - } + /* The wrapper itself should not interfere with pointer device */ + pointer-events: none; + + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +.ck .ck-widget__resizer-shadow { + display: none; + position: absolute; + + left: 0; + right: 0; + top: 0; + bottom: 0; + + outline: 2px solid var(--ck-color-resizer); + + /* @todo: remove this dirty hack. It's purpose is that shadow element should not be + overlapped by elements that follow it in the DOM. */ + z-index: var(--ck-z-default); + + &.ck-widget__resizer-shadow-active { + display: block; + visibility: visible; } +} - &:hover { - & .ck-widget__resizer-wrapper { - visibility: visible; - } +.ck .ck-widget__resizer { + position: absolute; + + /* Resizers are the only UI elements that should interfere with pointer device */ + pointer-events: all; + + width: var(--ck-resizer-size); + height: var(--ck-resizer-size); + background: var(--ck-color-focus-border); + outline: var(--ck-resizer-border-width) solid #fff; + + &.ck-widget__resizer-top-left { + top: var( --ck-resizer-offset ); + left: var( --ck-resizer-offset ); + cursor: nw-resize; + } + + &.ck-widget__resizer-top-right { + top: var( --ck-resizer-offset ); + right: var( --ck-resizer-offset ); + cursor: ne-resize; + } + + &.ck-widget__resizer-bottom-right { + bottom: var( --ck-resizer-offset ); + right: var( --ck-resizer-offset ); + cursor: se-resize; + } + + &.ck-widget__resizer-bottom-left { + bottom: var( --ck-resizer-offset ); + left: var( --ck-resizer-offset ); + cursor: sw-resize; } } @@ -98,24 +101,23 @@ /* Make the widget wrapper a relative positioning container for the drag handler. */ position: relative; + /* Show the selection handler on mouse hover over the widget. */ + &:hover { + & .ck-widget__selection-handler { + visibility: visible; + } + } + & .ck-widget__selection-handler { - /* visibility: hidden; */ position: absolute; & .ck-icon { - /* Make sure the icon in not a subject to fon-size/line-height to avoid + /* Make sure the icon in not a subject to font-size/line-height to avoid unnecessary spacing around it. */ display: block; } } - /* Show the selection handler on mouse hover over the widget. */ - &:hover { - & .ck-widget__selection-handler { - visibility: visible; - } - } - /* Show the selection handler when the widget is selected. */ &.ck-widget_selected { & .ck-widget__selection-handler { @@ -124,21 +126,18 @@ } } -.ck .ck-widget_resizer-shadow { - width: 60px; - height: 60px; - outline: 2px solid red !important; -} - .ck .ck-size-view { - background: lightyellow; + background: #fff; border: 2px solid var(--ck-color-focus-border); - display: inline-block; - padding: 3px; + display: block; + padding: var(--ck-spacing-small); - --ck-sizer-offset: 20px; + --ck-sizer-offset: 10px; - &.ck-orientation-top-left, &.ck-orientation-top-right, &.ck-orientation-bottom-right, &.ck-orientation-bottom-left { + &.ck-orientation-top-left, + &.ck-orientation-top-right, + &.ck-orientation-bottom-right, + &.ck-orientation-bottom-left { position: absolute; } @@ -146,20 +145,19 @@ top: var( --ck-sizer-offset ); left: var( --ck-sizer-offset ); } + &.ck-orientation-top-right { top: var( --ck-sizer-offset ); right: var( --ck-sizer-offset ); } + &.ck-orientation-bottom-right { bottom: var( --ck-sizer-offset ); right: var( --ck-sizer-offset ); } + &.ck-orientation-bottom-left { bottom: var( --ck-sizer-offset ); left: var( --ck-sizer-offset ); } } - -.ck.ck-content.ck-editor__editable.ck-rounded-corners.ck-editor__editable_inline { - /* padding: 100px !important; */ -} \ No newline at end of file From 70df28b6263191eb2a0227ce61f35f81ff9962fa Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 25 Jul 2019 12:11:33 +0200 Subject: [PATCH 38/93] Internal: Adjusted resize cursors. --- theme/widget.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/theme/widget.css b/theme/widget.css index 02874dfa..91f1be41 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -75,25 +75,25 @@ &.ck-widget__resizer-top-left { top: var( --ck-resizer-offset ); left: var( --ck-resizer-offset ); - cursor: nw-resize; + cursor: nwse-resize; } &.ck-widget__resizer-top-right { top: var( --ck-resizer-offset ); right: var( --ck-resizer-offset ); - cursor: ne-resize; + cursor: nesw-resize; } &.ck-widget__resizer-bottom-right { bottom: var( --ck-resizer-offset ); right: var( --ck-resizer-offset ); - cursor: se-resize; + cursor: nwse-resize; } &.ck-widget__resizer-bottom-left { bottom: var( --ck-resizer-offset ); left: var( --ck-resizer-offset ); - cursor: sw-resize; + cursor: nesw-resize; } } From 26308f7620b7285804b26a84c1095b79532fff3b Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 25 Jul 2019 15:40:41 +0200 Subject: [PATCH 39/93] Internal: Use width inline style rather than height attribute. --- src/resizecontext.js | 6 +++--- src/widgetresizer.js | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index d320875c..ceb30634 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -10,7 +10,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; * @module widget/resizecontext */ -const HEIGHT_ATTRIBUTE_NAME = 'height'; +const WIDTH_ATTRIBUTE_NAME = 'width'; /** * Stores the internal state of a single resizable object. @@ -134,12 +134,12 @@ export default class ResizeContext { commit( editor ) { const modelEntry = this._getModel( editor, this.widgetWrapperElement ); - const newHeight = this.domResizeShadow.clientHeight; + const newWidth = this.domResizeShadow.clientWidth; this._dismissShadow(); editor.model.change( writer => { - writer.setAttribute( HEIGHT_ATTRIBUTE_NAME, newHeight, modelEntry ); + writer.setAttribute( WIDTH_ATTRIBUTE_NAME, newWidth, modelEntry ); } ); this.redraw(); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index eb1d8d6d..d78afce6 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -14,7 +14,7 @@ import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import ResizeContext2 from './resizecontext'; import ResizerTopBound from './resizertopbound'; -const HEIGHT_ATTRIBUTE_NAME = 'height'; +const WIDTH_ATTRIBUTE_NAME = 'width'; /** * Interface describing a resizer. It allows to define available resizer set, specify resizing host etc. @@ -151,10 +151,10 @@ export default class WidgetResizer extends Plugin { const editor = this.editor; // Allow bold attribute on text nodes. editor.model.schema.extend( 'image', { - allowAttributes: HEIGHT_ATTRIBUTE_NAME + allowAttributes: WIDTH_ATTRIBUTE_NAME } ); - editor.model.schema.setAttributeProperties( HEIGHT_ATTRIBUTE_NAME, { + editor.model.schema.setAttributeProperties( WIDTH_ATTRIBUTE_NAME, { isFormatting: true } ); } @@ -164,7 +164,7 @@ export default class WidgetResizer extends Plugin { // Dedicated converter to propagate image's attribute to the img tag. editor.conversion.for( 'downcast' ).add( dispatcher => - dispatcher.on( 'attribute:height:image', ( evt, data, conversionApi ) => { + dispatcher.on( 'attribute:width:image', ( evt, data, conversionApi ) => { if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; } @@ -173,9 +173,9 @@ export default class WidgetResizer extends Plugin { const img = conversionApi.mapper.toViewElement( data.item ).getChild( 0 ); if ( data.attributeNewValue !== null ) { - viewWriter.setAttribute( HEIGHT_ATTRIBUTE_NAME, data.attributeNewValue, img ); + viewWriter.setStyle( WIDTH_ATTRIBUTE_NAME, data.attributeNewValue + 'px', img ); } else { - viewWriter.removeAttribute( HEIGHT_ATTRIBUTE_NAME, img ); + viewWriter.removeStyle( WIDTH_ATTRIBUTE_NAME, img ); } } ) ); @@ -184,9 +184,14 @@ export default class WidgetResizer extends Plugin { .attributeToAttribute( { view: { name: 'img', - key: 'height' + styles: { + 'width': /[\d.]+(px)?/ + } }, - model: 'height' + model: { + key: WIDTH_ATTRIBUTE_NAME, + value: viewElement => viewElement.getStyle( 'width' ).replace( 'px', '' ) + } } ); } } From 670554dd15d688b80f4c04898df71621360b3c85 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 25 Jul 2019 15:49:47 +0200 Subject: [PATCH 40/93] Internal: Adjusted mouseover observer to trigger ~30 times per second. --- src/view/mousemoveobserver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/mousemoveobserver.js b/src/view/mousemoveobserver.js index a9e84344..2444458f 100644 --- a/src/view/mousemoveobserver.js +++ b/src/view/mousemoveobserver.js @@ -23,7 +23,7 @@ export default class MouseMoveObserver extends DomEventObserver { this.domEventType = 'mousemove'; - this._fireMouseMoveEvent = throttle( domEvent => this.fire( domEvent.type, domEvent ), 60 ); + this._fireMouseMoveEvent = throttle( domEvent => this.fire( domEvent.type, domEvent ), 33.3 ); } /** From a6a2305c3b07186a3843fb2fd4c4a200112240da Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 25 Jul 2019 16:04:18 +0200 Subject: [PATCH 41/93] Internal: Reduced resize handler outline thickness. --- theme/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/widget.css b/theme/widget.css index 91f1be41..b7b0729b 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -49,7 +49,7 @@ top: 0; bottom: 0; - outline: 2px solid var(--ck-color-resizer); + outline: 1px solid var(--ck-color-resizer); /* @todo: remove this dirty hack. It's purpose is that shadow element should not be overlapped by elements that follow it in the DOM. */ From 37e62e971940fb03d9332ed4af843876e6262646 Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Thu, 25 Jul 2019 16:24:38 +0200 Subject: [PATCH 42/93] Improved resizer & tooltip styling. --- theme/widget.css | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/theme/widget.css b/theme/widget.css index b7b0729b..fe0ec732 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -6,10 +6,15 @@ :root { --ck-color-resizer: var(--ck-color-focus-border); --ck-resizer-size: 10px; - --ck-resizer-border-width: 2px; + --ck-resizer-border-width: 1px; + --ck-resizer-border-radius: 2px; /* Set resizer with 50% offset. */ - --ck-resizer-offset: calc( ( var(--ck-resizer-size) / -2 ) - 1px ); + --ck-resizer-offset: calc( ( var(--ck-resizer-size) / -2 ) - 2px); + + --ck-resizer-tooltip-offset: 10px; + --ck-color-resizer-tooltip-background: hsl(0, 0%, 15%); + --ck-color-resizer-tooltip-text: hsl(0, 0%, 95%); } .ck .ck-widget_with-resizer { @@ -70,7 +75,8 @@ width: var(--ck-resizer-size); height: var(--ck-resizer-size); background: var(--ck-color-focus-border); - outline: var(--ck-resizer-border-width) solid #fff; + border: var(--ck-resizer-border-width) solid #fff; + border-radius: var(--ck-resizer-border-radius); &.ck-widget__resizer-top-left { top: var( --ck-resizer-offset ); @@ -127,13 +133,14 @@ } .ck .ck-size-view { - background: #fff; - border: 2px solid var(--ck-color-focus-border); + background: var(--ck-color-resizer-tooltip-background); + color: var(--ck-color-resizer-tooltip-text); + border: 1px solid var(--ck-color-resizer-tooltip-text); + border-radius: var(--ck-resizer-border-radius); + font-size: var(--ck-font-size-tiny); display: block; padding: var(--ck-spacing-small); - --ck-sizer-offset: 10px; - &.ck-orientation-top-left, &.ck-orientation-top-right, &.ck-orientation-bottom-right, @@ -142,22 +149,22 @@ } &.ck-orientation-top-left { - top: var( --ck-sizer-offset ); - left: var( --ck-sizer-offset ); + top: var( --ck-resizer-tooltip-offset ); + left: var( --ck-resizer-tooltip-offset ); } &.ck-orientation-top-right { - top: var( --ck-sizer-offset ); - right: var( --ck-sizer-offset ); + top: var( --ck-resizer-tooltip-offset ); + right: var( --ck-resizer-tooltip-offset ); } &.ck-orientation-bottom-right { - bottom: var( --ck-sizer-offset ); - right: var( --ck-sizer-offset ); + bottom: var( --ck-resizer-tooltip-offset ); + right: var( --ck-resizer-tooltip-offset ); } &.ck-orientation-bottom-left { - bottom: var( --ck-sizer-offset ); - left: var( --ck-sizer-offset ); + bottom: var( --ck-resizer-tooltip-offset ); + left: var( --ck-resizer-tooltip-offset ); } } From a53b76f54a1ea1118c91c240a614ccc4cab418b8 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 25 Jul 2019 18:00:00 +0200 Subject: [PATCH 43/93] Internal: Listening for events outside of the editable. The code is currently not polished, I couldn't hook up properly to DOMDocument using Observers, so I used Emitters and managed listening manually. --- src/resizecontext.js | 4 ++-- src/widgetresizer.js | 33 ++++++++++++++------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index ceb30634..f6217759 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -263,8 +263,8 @@ export default class ResizeContext { _extractCoordinates( event ) { return { - x: event.domEvent.pageX, - y: event.domEvent.pageY + x: event.pageX, + y: event.pageY }; } diff --git a/src/widgetresizer.js b/src/widgetresizer.js index d78afce6..d74e17c6 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -8,11 +8,11 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import MouseObserver from './view/mouseobserver'; -import MouseMoveObserver from './view/mousemoveobserver'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import ResizeContext2 from './resizecontext'; import ResizerTopBound from './resizertopbound'; +import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; const WIDTH_ATTRIBUTE_NAME = 'width'; @@ -54,36 +54,30 @@ export default class WidgetResizer extends Plugin { this._registerSchema(); this._registerConverters(); - const view = this.editor.editing.view; - const viewDocument = view.document; + const mouseObserverHost = global.window.document; this._observers = { - mouseMove: view.addObserver( MouseMoveObserver ), - mouseDownUp: view.addObserver( MouseObserver ) + mouseMove: Object.create( DomEmitterMixin ), + mouseDownUp: Object.create( DomEmitterMixin ), }; - // It should start disabled, only upon clicking drag handler it interests us. - // Currently broken due to https://github.com/ckeditor/ckeditor5-engine/blob/ce6422b/src/view/view.js#L364 - this._observers.mouseMove.disable(); - let isActive = false; - // Mouse move observer is only needed when the mouse button is pressed. - // this.listenTo( viewDocument, 'mousemove', () => console.log( 'move' ) ); - this.listenTo( viewDocument, 'mousemove', ( event, domEventData ) => { + const mouseMoveListener = ( event, domEventData ) => { if ( this.activeContext ) { this.activeContext.updateSize( domEventData ); } - } ); + }; - this.listenTo( viewDocument, 'mousedown', ( event, domEventData ) => { - const target = domEventData.domTarget; + this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mousedown', ( event, domEventData ) => { + const target = domEventData.target; const resizeHandler = isResizeHandler( target ) ? target : getAncestors( target ).filter( isResizeHandler )[ 0 ]; if ( resizeHandler ) { isActive = true; - this._observers.mouseMove.enable(); + // this._observers.mouseMove.enable(); + this._observers.mouseMove.listenTo( mouseObserverHost, 'mousemove', mouseMoveListener ); this.activeContext = this._getContextByHandler( resizeHandler ); @@ -96,7 +90,8 @@ export default class WidgetResizer extends Plugin { const finishResizing = () => { if ( isActive ) { isActive = false; - this._observers.mouseMove.disable(); + // this._observers.mouseMove.disable(); + this._observers.mouseMove.stopListening( mouseObserverHost, 'mousemove', mouseMoveListener ); if ( this.activeContext ) { this.activeContext.commit( this.editor ); @@ -107,7 +102,7 @@ export default class WidgetResizer extends Plugin { }; // @todo: it should listen on the entire window, as it should also catch events outside of the editable. - this.listenTo( viewDocument, 'mouseup', finishResizing ); + this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mouseup', finishResizing ); function isResizeHandler( element ) { return element.classList && element.classList.contains( 'ck-widget__resizer' ); From 152044823e41a3084074e8c6b0e233674f90f647 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 2 Aug 2019 11:02:38 +0200 Subject: [PATCH 44/93] Internal: Added a possibility to specify whether resized object is central or side aligned. Improved aligning in case of side images. --- src/resizecontext.js | 2 +- src/resizertopbound.js | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index f6217759..f57b3e47 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -107,7 +107,7 @@ export default class ResizeContext { this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); /** - * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc of `null` + * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` * if unknown. * * @member {String|null} diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 0e40bf51..f1fa21ae 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -31,6 +31,12 @@ export default class ResizerCentral { begin() { const resizeHost = this.context._getResizeHost(); this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); + + // Size of resize host before current resizing transaction. + this.initialSize = { + width: resizeHost.width, + height: resizeHost.height + }; } commit() {} @@ -42,12 +48,18 @@ export default class ResizerCentral { updateSize( domEventData ) { const context = this.context; const currentCoordinates = context._extractCoordinates( domEventData ); + const isCentered = this.options.isCentered ? this.options.isCentered( this.context ) : true; const enlargement = { - x: ( currentCoordinates.x - this.closestReferencePoint.x ) * 2, - y: ( currentCoordinates.y - this.closestReferencePoint.y ) + // @todo it could be simplified if context.referenceCoordinates was an inverted corner (at least for bottom-left). + x: this.context.referenceCoordinates.x - ( currentCoordinates.x + this.initialSize.width ), + y: ( currentCoordinates.y - this.initialSize.height ) - this.context.referenceCoordinates.y }; + if ( isCentered ) { + enlargement.x *= 2; + } + const resizeHost = this.context._getResizeHost(); const originalSize = { @@ -56,8 +68,8 @@ export default class ResizerCentral { }; const proposedSize = { - x: Math.abs( this.closestReferencePoint.x - context.referenceCoordinates.x + enlargement.x ), - y: Math.abs( this.closestReferencePoint.y - context.referenceCoordinates.y + enlargement.y ) + x: Math.abs( this.initialSize.width + enlargement.x ), + y: Math.abs( this.initialSize.height + enlargement.y ) }; // Dominant determination must take the ratio into account. @@ -99,7 +111,6 @@ export default class ResizerCentral { y: parseFloat( originalSize.y ) - drawnSize.y }; - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ diff2.y / 2 }px`; context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = '0px'; context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ diff2.x / 2 }px`; From 350a2493d2fd383c96957ddf88d1d0f7d9bc572e Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 5 Aug 2019 12:00:34 +0200 Subject: [PATCH 45/93] Internal: Resizer wrapper will be sized based on element offset metrics. This is made especially for image captions, but will handle also cases when resize host has a margin to a container, or the container has a padding etc. --- src/resizecontext.js | 8 +++++++- theme/widget.css | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index f57b3e47..f4b8dc47 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -180,6 +180,9 @@ export default class ResizeContext { updateSize( domEventData ) { const proposedSize = this.resizeStrategy.updateSize( domEventData ); + this.domResizeWrapper.style.width = proposedSize.x + 'px'; + this.domResizeWrapper.style.height = proposedSize.y + 'px'; + this.set( { proposedX: proposedSize.x, proposedY: proposedSize.y @@ -194,7 +197,10 @@ export default class ResizeContext { if ( !widgetWrapper.isSameNode( resizingHost ) ) { this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; - this.domResizeWrapper.style.right = resizingHost.offsetLeft + 'px'; + this.domResizeWrapper.style.top = resizingHost.offsetTop + 'px'; + + this.domResizeWrapper.style.height = resizingHost.offsetHeight + 'px'; + this.domResizeWrapper.style.width = resizingHost.offsetWidth + 'px'; } } } diff --git a/theme/widget.css b/theme/widget.css index fe0ec732..5b7b3489 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -40,9 +40,7 @@ pointer-events: none; left: 0; - right: 0; top: 0; - bottom: 0; } .ck .ck-widget__resizer-shadow { From 8e605cace50ea39e1d08aae4542066573a0317d7 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 6 Aug 2019 14:41:57 +0200 Subject: [PATCH 46/93] Internal: Fixed an issue with resizer border after undoing. --- src/resizertopbound.js | 35 +++++++++++++++++++++++++---------- src/widgetresizer.js | 10 ++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/resizertopbound.js b/src/resizertopbound.js index f1fa21ae..95f9666a 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -29,14 +29,24 @@ export default class ResizerCentral { attach() {} begin() { + this.redraw(); const resizeHost = this.context._getResizeHost(); this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); + } - // Size of resize host before current resizing transaction. - this.initialSize = { - width: resizeHost.width, - height: resizeHost.height - }; + redraw() { + if ( this.context.domResizeWrapper ) { + const clientRect = this.context._getResizeHost().getBoundingClientRect(); + + // Size of resize host before current resizing transaction. + this.initialSize = { + width: clientRect.width, + height: clientRect.height + }; + + this.context.domResizeWrapper.style.width = this.initialSize.width + 'px'; + this.context.domResizeWrapper.style.height = this.initialSize.height + 'px'; + } } commit() {} @@ -56,15 +66,22 @@ export default class ResizerCentral { y: ( currentCoordinates.y - this.initialSize.height ) - this.context.referenceCoordinates.y }; - if ( isCentered ) { + // temp workaround + if ( isCentered && this.context.referenceHandlerPosition.endsWith( '-right' ) ) { + enlargement.x = currentCoordinates.x - ( this.context.referenceCoordinates.x + this.initialSize.width ); + } + + // @todo: oddly enough, this condition **check** is not needed for tables. + if ( isCentered && enlargement.x < 0 ) { enlargement.x *= 2; } const resizeHost = this.context._getResizeHost(); + const clientRect = resizeHost.getBoundingClientRect(); const originalSize = { - x: resizeHost.width, - y: resizeHost.height + x: clientRect.width, + y: clientRect.height }; const proposedSize = { @@ -121,8 +138,6 @@ export default class ResizerCentral { return drawnSize; // @todo decide what size should actually be returned. } - - redraw() {} } mix( ResizerCentral, ObservableMixin ); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index d74e17c6..622fdee6 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -69,6 +69,16 @@ export default class WidgetResizer extends Plugin { } }; + this.editor.editing.view.document.on( 'layoutChanged', () => { + // This works around the issue witn undo. + for ( const context of this.contexts ) { + // This check is needed, as there were cases when widget was not yet initialized but layoutChanged happened. + if ( context.domResizeWrapper && context.domResizeWrapper.parentElement ) { + context.resizeStrategy.redraw(); + } + } + } ); + this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mousedown', ( event, domEventData ) => { const target = domEventData.target; From 92ecfa0bd3b247fc29f0c62cf11043a9aa38afd2 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 7 Aug 2019 09:01:59 +0200 Subject: [PATCH 47/93] Internal: Fixed an issue where center-aligned image would not be properly shrinked. It was mispositioned relative to the cursor. --- src/resizertopbound.js | 25 +++++++++++++++---------- src/widgetresizer.js | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 95f9666a..fdc94006 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -29,23 +29,26 @@ export default class ResizerCentral { attach() {} begin() { - this.redraw(); + const clientRect = this.context._getResizeHost().getBoundingClientRect(); + + // Size of resize host before current resizing transaction. + this.initialSize = { + width: clientRect.width, + height: clientRect.height + }; + + this.redrawShadow(); + const resizeHost = this.context._getResizeHost(); this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); } - redraw() { + redrawShadow() { if ( this.context.domResizeWrapper ) { const clientRect = this.context._getResizeHost().getBoundingClientRect(); - // Size of resize host before current resizing transaction. - this.initialSize = { - width: clientRect.width, - height: clientRect.height - }; - - this.context.domResizeWrapper.style.width = this.initialSize.width + 'px'; - this.context.domResizeWrapper.style.height = this.initialSize.height + 'px'; + this.context.domResizeWrapper.style.width = clientRect.width + 'px'; + this.context.domResizeWrapper.style.height = clientRect.height + 'px'; } } @@ -138,6 +141,8 @@ export default class ResizerCentral { return drawnSize; // @todo decide what size should actually be returned. } + + redraw() {} } mix( ResizerCentral, ObservableMixin ); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 622fdee6..b20f0e2f 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -74,7 +74,7 @@ export default class WidgetResizer extends Plugin { for ( const context of this.contexts ) { // This check is needed, as there were cases when widget was not yet initialized but layoutChanged happened. if ( context.domResizeWrapper && context.domResizeWrapper.parentElement ) { - context.resizeStrategy.redraw(); + context.resizeStrategy.redrawShadow(); } } } ); From bc4f9347ea0af3f58b6bb5d7fed232a06e918b20 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 7 Aug 2019 09:14:59 +0200 Subject: [PATCH 48/93] Internal: Fixed a case where resize shadow would remain unaffected by image's max-width CSS rule. --- src/resizertopbound.js | 9 ++++++++- src/widgetresizer.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/resizertopbound.js b/src/resizertopbound.js index fdc94006..673ffcc8 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -122,7 +122,14 @@ export default class ResizerCentral { if ( resizeUsingImage ) { resizeHost.style.width = `${ drawnSize.x }px`; - resizeHost.style.height = `${ drawnSize.y }px`; + // resizeHost.style.height = `${ drawnSize.y }px`; + + const latestRect = resizeHost.getBoundingClientRect(); + + // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it + // here will reflect this limitation on resizer shadow later on. + drawnSize.x = latestRect.width; + drawnSize.y = latestRect.height; } else { const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index b20f0e2f..ff6c9be3 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -70,7 +70,7 @@ export default class WidgetResizer extends Plugin { }; this.editor.editing.view.document.on( 'layoutChanged', () => { - // This works around the issue witn undo. + // This works around the issue with undo. for ( const context of this.contexts ) { // This check is needed, as there were cases when widget was not yet initialized but layoutChanged happened. if ( context.domResizeWrapper && context.domResizeWrapper.parentElement ) { From 3ed0812446e3e9bc79656eb4b85542215d9be2e7 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Thu, 8 Aug 2019 14:37:22 +0200 Subject: [PATCH 49/93] Make sure resizer is visible only when the editor is focused. --- theme/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/widget.css b/theme/widget.css index 5b7b3489..4fe18e76 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -22,7 +22,7 @@ position: relative; } -.ck .ck-widget_with-resizer.ck-widget_selected { +.ck-focused .ck-widget_with-resizer.ck-widget_selected { & .ck-widget__resizer-wrapper { visibility: visible; } From fcd1c7874a45c4399bbcff6890ba71976a6f879e Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 9 Aug 2019 08:49:50 +0200 Subject: [PATCH 50/93] Internal: Further adjusted resizer focus selector. --- theme/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/widget.css b/theme/widget.css index 4fe18e76..f02f86f3 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -23,7 +23,7 @@ } .ck-focused .ck-widget_with-resizer.ck-widget_selected { - & .ck-widget__resizer-wrapper { + & > .ck-widget__resizer-wrapper { visibility: visible; } From af100d514917aabdc5cd8fcc7c88c87a17ee12e3 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 9 Aug 2019 11:53:04 +0200 Subject: [PATCH 51/93] Internal: Minor code cleanup. --- src/resizecontext.js | 26 +++++++++++++++-------- src/widgetresizer.js | 49 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index f4b8dc47..9c5544bf 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -19,11 +19,18 @@ const WIDTH_ATTRIBUTE_NAME = 'width'; */ export default class ResizeContext { constructor( options ) { - // HTMLElement??? - @todo seems to be not needed. - // this.resizeHost = null; - // view/UiElement + /** + * View to a wrapper containing all the resizer-related views. + * + * @member {module:engine/view/uielement~UIElement} + */ this.resizeWrapperElement = null; - // view/Element + + /** + * View of a widget associated with the resizer. + * + * @member {module:engine/view/element~Element} + */ this.widgetWrapperElement = null; this.resizeStrategy = new ResizerTopBound( this, options ); @@ -69,6 +76,7 @@ export default class ResizeContext { } /** + * Method to be called to attach a resizer to a given widget element. * * @param {module:engine/view/element~Element} widgetElement Widget's wrapper. * @param {module:engine/view/downcastwriter~DowncastWriter} writer @@ -259,9 +267,10 @@ export default class ResizeContext { /** * - * @param {module:@ckeditor/ckeditor5-core/src/editor/editor~Editor} editor - * @param {module:@ckeditor/ckeditor5-engine/src/view/element~Element} widgetWrapperElement - * @returns {module:@ckeditor/ckeditor5-engine/src/model/element~Element|undefined} + * @param {module:core/editor/editor~Editor} editor + * @param {module:engine/view/element~Element} widgetWrapperElement + * @returns {module:engine/model/element~Element|undefined} + * @protected */ _getModel( editor, widgetWrapperElement ) { return editor.editing.mapper.toModelElement( widgetWrapperElement ); @@ -275,9 +284,9 @@ export default class ResizeContext { } /** - * @private * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. * @returns {String} A prefixed HTML class name for the resizer element + * @private */ _getResizerClass( resizerPosition ) { return `ck-widget__resizer-${ resizerPosition }`; @@ -303,6 +312,7 @@ export default class ResizeContext { /** * @param {String} position Like `"top-left"`. * @returns {String} Inverted `position`. + * @protected */ _invertPosition( position ) { const parts = position.split( '-' ); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index ff6c9be3..98c13600 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -29,7 +29,9 @@ const WIDTH_ATTRIBUTE_NAME = 'width'; */ /** - * The base class for widget features. This type provides a common API for reusable features of widgets. + * Widget resize feature plugin. + * + * Use the {@link #apply} method to create resizer for a provided widget. */ export default class WidgetResizer extends Plugin { /** @@ -120,10 +122,51 @@ export default class WidgetResizer extends Plugin { } /** + * Method that applies a resizer to a given `widgetElement`. + * + * ```js + * conversion.for( 'editingDowncast' ).elementToElement( { + * model: 'image', + * view: ( modelElement, viewWriter ) => { + * const widget = toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) ); + * + * editor.plugins.get( 'WidgetResizer' ).apply( widget, viewWriter ); + * + * return widget; + * } + * } ); + * ``` + * + * You can use the `options` parameter to customize the behavior of the resizer: + * + * ```js + * conversion.for( 'editingDowncast' ).elementToElement( { + * model: 'image', + * view: ( modelElement, viewWriter ) => { + * const widget = toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) ); + * + * editor.plugins.get( 'WidgetResizer' ).apply( widget, viewWriter, { + * getResizeHost( wrapper ) { + * return wrapper.querySelector( 'img' ); + * }, + * getAspectRatio( resizeHost ) { + * return resizeHost.naturalWidth / resizeHost.naturalHeight; + * }, + * isCentered( context ) { + * const imageStyle = context._getModel( editor, context.widgetWrapperElement ).getAttribute( 'imageStyle' ); + * + * return !imageStyle || imageStyle == 'full'; + * } + * } ); + * + * return widget; + * } + * } ); + * ``` + * * @param {module:engine/view/containerelement~ContainerElement} widgetElement * @param {module:engine/view/downcastwriter~DowncastWriter} writer * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. - * @memberof WidgetResizer */ apply( widgetElement, writer, options ) { const context = new ResizeContext2( options ); @@ -154,7 +197,7 @@ export default class WidgetResizer extends Plugin { _registerSchema() { const editor = this.editor; - // Allow bold attribute on text nodes. + editor.model.schema.extend( 'image', { allowAttributes: WIDTH_ATTRIBUTE_NAME } ); From 0e2903a8307935028dc9b6ea981153ecac0c74b1 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 9 Aug 2019 15:40:50 +0200 Subject: [PATCH 52/93] Internal: Simplified resizer's commit method. This fixed an issue when an exception was thrown after resizing image contained in a table. --- src/resizecontext.js | 11 ++++------- src/widgetresizer.js | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 9c5544bf..f40adcec 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -146,17 +146,14 @@ export default class ResizeContext { this._dismissShadow(); - editor.model.change( writer => { - writer.setAttribute( WIDTH_ATTRIBUTE_NAME, newWidth, modelEntry ); - } ); - this.redraw(); - // Again, render will most likely change image size, so resizers needs a redraw. - editor.editing.view.once( 'render', () => this.redraw() ); - this.resizeStrategy.commit( editor ); + editor.model.change( writer => { + writer.setAttribute( WIDTH_ATTRIBUTE_NAME, newWidth, modelEntry ); + } ); + this._cleanupContext(); } diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 98c13600..bc5496e7 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -31,7 +31,7 @@ const WIDTH_ATTRIBUTE_NAME = 'width'; /** * Widget resize feature plugin. * - * Use the {@link #apply} method to create resizer for a provided widget. + * Use the {@link module:widget/widgetresizer~WidgetResizer#apply} method to create resizer for a provided widget. */ export default class WidgetResizer extends Plugin { /** From 2acf2758c86e20322deb0a506eef01fe27e1200f Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 09:36:13 +0200 Subject: [PATCH 53/93] Internal: Removing leftovers after multiple resizing strategies. --- src/resizercentral.js | 96 ------------------------------------------ src/resizerside.js | 74 -------------------------------- src/resizertopbound.js | 6 +-- src/utils.js | 10 ----- src/widgetresizer.js | 49 ++------------------- 5 files changed, 6 insertions(+), 229 deletions(-) delete mode 100644 src/resizercentral.js delete mode 100644 src/resizerside.js diff --git a/src/resizercentral.js b/src/resizercentral.js deleted file mode 100644 index a239095a..00000000 --- a/src/resizercentral.js +++ /dev/null @@ -1,96 +0,0 @@ -import { - getAbsoluteBoundaryPoint -} from './utils'; - -import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; -import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import global from '@ckeditor/ckeditor5-utils/src/dom/global'; - -/** - * Implements a resizer that enlarges/shrinks in all directions. - * - * @class ResizerCentral - */ -export default class ResizerCentral { - constructor( context, options ) { - this.context = context; - this.options = options || {}; - } - - attach() {} - - begin() { - const resizeHost = this.context._getResizeHost(); - this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); - } - - commit() {} - - cancel() {} - - destroy() {} - - updateSize( domEventData ) { - const context = this.context; - const currentCoordinates = context._extractCoordinates( domEventData ); - - const enlargement = { - x: ( currentCoordinates.x - this.closestReferencePoint.x ) * 2, - y: ( currentCoordinates.y - this.closestReferencePoint.y ) * 2 - }; - - const originalSize = this.context.originalSize; - - const proposedSize = { - x: Math.abs( this.closestReferencePoint.x - context.referenceCoordinates.x + enlargement.x ), - y: Math.abs( this.closestReferencePoint.y - context.referenceCoordinates.y + enlargement.y ) - }; - - // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.x / context.aspectRatio > proposedSize.y ? 'x' : 'y'; - proposedSize.max = proposedSize[ proposedSize.dominant ]; - - const drawnSize = { - x: proposedSize.x, - y: proposedSize.y - }; - - if ( proposedSize.dominant == 'x' ) { - drawnSize.y = drawnSize.x / context.aspectRatio; - } else { - drawnSize.x = drawnSize.y * context.aspectRatio; - } - - // Reset shadow bounding. - context.domResizeShadow.style.top = 'auto'; - context.domResizeShadow.style.left = 'auto'; - context.domResizeShadow.style.bottom = 'auto'; - context.domResizeShadow.style.right = 'auto'; - - const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); - - const resizeUsingImage = global.window.pocResizeUsingImage !== false; - - if ( !resizeUsingImage ) { - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = `${ ( originalSize.y - drawnSize.y ) / 2 }px`; - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ ( originalSize.x - drawnSize.x ) / 2 }px`; - } else { - const resizingHost = this.context._getResizeHost(); - - resizingHost.style.width = `${ drawnSize.x }px`; - resizingHost.style.height = `${ drawnSize.y }px`; - } - - // Apply the actual shadow dimensions. - context.domResizeShadow.style.width = `${ drawnSize.x }px`; - context.domResizeShadow.style.height = `${ drawnSize.y }px`; - - return drawnSize; // @todo decide what size should actually be returned, drawn or intended. - } - - redraw() {} - - _getResizeHost() {} -} - -mix( ResizerCentral, ObservableMixin ); diff --git a/src/resizerside.js b/src/resizerside.js deleted file mode 100644 index 183fc0a9..00000000 --- a/src/resizerside.js +++ /dev/null @@ -1,74 +0,0 @@ -import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; -import mix from '@ckeditor/ckeditor5-utils/src/mix'; - -/** - * @module widget/resizecontext - */ - -/** - * Implements a resizer that enlarges to the dragged side. - * - * @class ResizerSide - */ -export default class ResizerSide { - constructor( context, options ) { - this.context = context; - this.options = options || {}; - } - - attach() {} - - begin() {} - - commit() {} - - cancel() {} - - destroy() {} - - updateSize( domEventData ) { - const context = this.context; - const currentCoordinates = context._extractCoordinates( domEventData ); - - const proposedSize = { - x: Math.abs( currentCoordinates.x - context.referenceCoordinates.x ), - y: Math.abs( currentCoordinates.y - context.referenceCoordinates.y ) - }; - - // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.x / context.aspectRatio > proposedSize.y ? 'x' : 'y'; - proposedSize.max = proposedSize[ proposedSize.dominant ]; - - const drawnSize = { - x: proposedSize.x, - y: proposedSize.y - }; - - if ( proposedSize.dominant == 'x' ) { - drawnSize.y = drawnSize.x / context.aspectRatio; - } else { - drawnSize.x = drawnSize.y * context.aspectRatio; - } - - // Reset shadow bounding. - context.domResizeShadow.style.top = 0; - context.domResizeShadow.style.left = 0; - context.domResizeShadow.style.bottom = 0; - context.domResizeShadow.style.right = 0; - - context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto'; - context.domResizeShadow.style[ context.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto'; - - // Apply the actual shadow dimensions. - context.domResizeShadow.style.width = `${ drawnSize.x }px`; - context.domResizeShadow.style.height = `${ drawnSize.y }px`; - - return drawnSize; // @todo decide what size should actually be returned. - } - - redraw() {} - - _getResizeHost() {} -} - -mix( ResizerSide, ObservableMixin ); diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 673ffcc8..c812516d 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -18,9 +18,9 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; /** * Implements a resizer that enlarges/shrinks in all directions. * - * @class ResizerCentral + * @class ResizerTopBound */ -export default class ResizerCentral { +export default class ResizerTopBound { constructor( context, options ) { this.context = context; this.options = options || {}; @@ -152,4 +152,4 @@ export default class ResizerCentral { redraw() {} } -mix( ResizerCentral, ObservableMixin ); +mix( ResizerTopBound, ObservableMixin ); diff --git a/src/utils.js b/src/utils.js index 83b5b6df..e1198e93 100644 --- a/src/utils.js +++ b/src/utils.js @@ -108,10 +108,6 @@ export function toWidget( element, writer, options = {} ) { addSelectionHandler( element, writer ); } - if ( options.features ) { - addWidgetFeatures( element, writer, options.features ); - } - setHighlightHandling( element, writer, @@ -406,9 +402,3 @@ function addSelectionHandler( widgetElement, writer ) { writer.insert( writer.createPositionAt( widgetElement, 0 ), selectionHandler ); writer.addClass( [ 'ck-widget_with-selection-handler' ], widgetElement ); } - -function addWidgetFeatures( element, writer, features ) { - for ( const currentFeature of features ) { - currentFeature.apply( element, writer ); - } -} diff --git a/src/widgetresizer.js b/src/widgetresizer.js index bc5496e7..bb3c07f5 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; -import ResizeContext2 from './resizecontext'; +import ResizeContext from './resizecontext'; import ResizerTopBound from './resizertopbound'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -54,7 +54,6 @@ export default class WidgetResizer extends Plugin { this.activeContext = null; this._registerSchema(); - this._registerConverters(); const mouseObserverHost = global.window.document; @@ -169,7 +168,7 @@ export default class WidgetResizer extends Plugin { * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. */ apply( widgetElement, writer, options ) { - const context = new ResizeContext2( options ); + const context = new ResizeContext( options ); context.attach( widgetElement, writer ); this.editor.editing.view.once( 'render', () => context.redraw() ); @@ -196,50 +195,8 @@ export default class WidgetResizer extends Plugin { } _registerSchema() { - const editor = this.editor; - - editor.model.schema.extend( 'image', { - allowAttributes: WIDTH_ATTRIBUTE_NAME - } ); - - editor.model.schema.setAttributeProperties( WIDTH_ATTRIBUTE_NAME, { + this.editor.model.schema.setAttributeProperties( WIDTH_ATTRIBUTE_NAME, { isFormatting: true } ); } - - _registerConverters() { - const editor = this.editor; - - // Dedicated converter to propagate image's attribute to the img tag. - editor.conversion.for( 'downcast' ).add( dispatcher => - dispatcher.on( 'attribute:width:image', ( evt, data, conversionApi ) => { - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const img = conversionApi.mapper.toViewElement( data.item ).getChild( 0 ); - - if ( data.attributeNewValue !== null ) { - viewWriter.setStyle( WIDTH_ATTRIBUTE_NAME, data.attributeNewValue + 'px', img ); - } else { - viewWriter.removeStyle( WIDTH_ATTRIBUTE_NAME, img ); - } - } ) - ); - - editor.conversion.for( 'upcast' ) - .attributeToAttribute( { - view: { - name: 'img', - styles: { - 'width': /[\d.]+(px)?/ - } - }, - model: { - key: WIDTH_ATTRIBUTE_NAME, - value: viewElement => viewElement.getStyle( 'width' ).replace( 'px', '' ) - } - } ); - } } From 5afaa59e472ae4d587c64cee0034159965e4521c Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 09:52:49 +0200 Subject: [PATCH 54/93] Internal: Removed unused DOM observers. --- src/view/mousemoveobserver.js | 54 ----------------------------------- src/view/mouseobserver.js | 40 -------------------------- 2 files changed, 94 deletions(-) delete mode 100644 src/view/mousemoveobserver.js delete mode 100644 src/view/mouseobserver.js diff --git a/src/view/mousemoveobserver.js b/src/view/mousemoveobserver.js deleted file mode 100644 index 2444458f..00000000 --- a/src/view/mousemoveobserver.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module widget/view/MouseMoveObserver - */ - -import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; -import { throttle } from 'lodash-es'; - -/** - * Mouse move observer. - * - * It throttles the event so that it doesn't fire too often. - * - * @extends module:engine/view/observer/domeventobserver~DomEventObserver - */ -export default class MouseMoveObserver extends DomEventObserver { - constructor( view ) { - super( view ); - - this.domEventType = 'mousemove'; - - this._fireMouseMoveEvent = throttle( domEvent => this.fire( domEvent.type, domEvent ), 33.3 ); - } - - /** - * @inheritDoc - */ - destroy() { - super.destroy(); - - this._fireMouseMoveEvent.cancel(); - } - - onDomEvent( domEvent ) { - this._fireMouseMoveEvent( domEvent ); - } -} - -/** - * Fired when mouse moves over the editable. - * - * Introduced by {@link module:widget/view/MouseMoveObserver~MouseMoveObserver}. - * - * Note that this event is not available by default. To make it available {@link widget/view/MouseMoveObserver~MouseMoveObserver} - * needs to be added to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. - * - * @see module:widget/view/MouseMoveObserver~MouseMoveObserver - * @event module:engine/view/document~Document#event:mousemove - * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. - */ diff --git a/src/view/mouseobserver.js b/src/view/mouseobserver.js deleted file mode 100644 index 2084faa8..00000000 --- a/src/view/mouseobserver.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module widget/view/mouseobserver - */ - -import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver'; - -/** - * Mouse events observer. - * - * @extends module:engine/view/observer/domeventobserver~DomEventObserver - */ -export default class MouseObserver extends DomEventObserver { - constructor( view ) { - super( view ); - - this.domEventType = [ 'mousedown', 'mouseup' ]; - } - - onDomEvent( domEvent ) { - this.fire( domEvent.type, domEvent ); - } -} - -/** - * Fired when mouse button is pressed down on one of the editables. - * - * Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}. - * - * Note that this event is not available by default. To make it available {@link module:engine/view/observer/mouseobserver~MouseObserver} - * needs to be added to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method. - * - * @see module:engine/view/observer/mouseobserver~MouseObserver - * @event module:engine/view/document~Document#event:mousedown - * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data. - */ From a6d96c8dcb3631cf080f41e17373f78e013e8cfa Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 11:30:12 +0200 Subject: [PATCH 55/93] Internal: Removed unusued widget feature generic type. --- src/widgetfeature.js | 16 ---------- src/widgetresizefeature.js | 65 -------------------------------------- 2 files changed, 81 deletions(-) delete mode 100644 src/widgetfeature.js delete mode 100644 src/widgetresizefeature.js diff --git a/src/widgetfeature.js b/src/widgetfeature.js deleted file mode 100644 index 822a76cc..00000000 --- a/src/widgetfeature.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module widget/widget - */ - -/** - * The base class for widget features. This type provides a common API for reusable features of widgets. - */ -export default class WidgetFeature { - apply() { - } -} diff --git a/src/widgetresizefeature.js b/src/widgetresizefeature.js deleted file mode 100644 index a538fad4..00000000 --- a/src/widgetresizefeature.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module widget/widget - */ - -import WidgetFeature from './widgetfeature'; -import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; -import dragHandlerIcon from '../theme/icons/drag-handler.svg'; - -/** - * The base class for widget features. This type provides a common API for reusable features of widgets. - */ -export default class WidgetResizeFeature extends WidgetFeature { - apply( widgetElement, writer ) { - super.apply( widgetElement, writer ); - - let domSelectionHandler = null; - - const selectionHandler = writer.createUIElement( 'div', { - class: 'ck ck-widget__resizer-wrapper' - }, function( domDocument ) { - const domElement = this.toDomElement( domDocument ); - const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; - - const shadowElement = domDocument.createElement( 'div' ); - shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' ); - domElement.appendChild( shadowElement ); - - for ( const currentPosition of resizerPositions ) { - // Use the IconView from the UI library. - const icon = new IconView(); - icon.set( 'content', dragHandlerIcon ); - icon.extendTemplate( { - attributes: { - 'class': `ck-widget__resizer ck-widget__resizer-${ currentPosition }` - } - } ); - - // Make sure icon#element is rendered before passing to appendChild(). - icon.render(); - - domElement.appendChild( icon.element ); - } - - domSelectionHandler = domElement; - - return domElement; - } ); - - // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), selectionHandler ); - writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement ); - - return () => { - const resizingHost = domSelectionHandler.parentElement.querySelector( 'img' ); - - domSelectionHandler.style.left = resizingHost.offsetLeft + 'px'; - domSelectionHandler.style.right = resizingHost.offsetLeft + 'px'; - }; - } -} From 29bbf4dd6cd7a53838741fbdb831686aa5500f07 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 13:05:27 +0200 Subject: [PATCH 56/93] Docs: More resizer API docs. --- src/resizecontext.js | 87 +++++++++++++++++++++++++++++++++++++----- src/resizertopbound.js | 10 ++++- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index f40adcec..3f06ff54 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -73,6 +73,35 @@ export default class ResizeContext { }; this._cleanupContext(); + + /** + * Width proposed (but not yet accepted) using the widget resizer. + * + * It goes back to `null` once the resizer is dismissed or accepted. + * + * @readonly + * @observable + * @member {Number|null} #proposedX + */ + + /** + * Height proposed (but not yet accepted) using the widget resizer. + * + * It goes back to `null` once the resizer is dismissed or accepted. + * + * @readonly + * @observable + * @member {Number|null} #proposedY + */ + + /** + * Position of a handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` + * if not active. + * + * @readonly + * @observable + * @member {String|null} #orientation + */ } /** @@ -165,16 +194,6 @@ export default class ResizeContext { this._cleanupContext(); } - _cleanupContext() { - this.referenceHandlerPosition = null; - - this.set( { - proposedX: null, - proposedY: null, - orientation: null - } ); - } - destroy() { this.cancel(); @@ -182,6 +201,13 @@ export default class ResizeContext { this.wrapper = null; } + /** + * Method used to calculate the proposed size as the resize handlers are dragged. + * + * Proposed size can also be observed with {@link #proposedX} and {@link #proposedY} properties. + * + * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. + */ updateSize( domEventData ) { const proposedSize = this.resizeStrategy.updateSize( domEventData ); @@ -210,6 +236,32 @@ export default class ResizeContext { } } + /** + * Cleans up the context state. + * + * @protected + */ + _cleanupContext() { + this.referenceHandlerPosition = null; + + this.set( { + proposedX: null, + proposedY: null, + orientation: null + } ); + } + + /** + * Method used to obtain the resize host. + * + * Resize host is an object that is actually resized. + * + * Resize host will not always be an entire widget itself. Take an image as an example. Image widget + * contains an image and caption. Only the image should be used to resize the widget, while the caption + * will simply follow the image size. + * + * @protected + */ _getResizeHost() { const widgetWrapper = this.domResizeWrapper.parentElement; @@ -217,6 +269,11 @@ export default class ResizeContext { this.options.getResizeHost( widgetWrapper ) : widgetWrapper; } + /** + * @private + * @param {HTMLDocument} domDocument Document where the widget is used. + * @param {HTMLElement} domElement The outer wrapper of resize UI within a given widget. + */ _appendShadowElement( domDocument, domElement ) { const shadowElement = domDocument.createElement( 'div' ); shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' ); @@ -225,6 +282,12 @@ export default class ResizeContext { return shadowElement; } + /** + * Renders the resize handlers in DOM. + * + * @private + * @param {HTMLElement} domElement Resize shadow where the resizers should be appended to. + */ _appendResizers( domElement ) { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; @@ -238,6 +301,10 @@ export default class ResizeContext { } } + /** + * @private + * @param {HTMLElement} domElement + */ _appendSizeUi( domElement ) { const sizeUi = new SizeView(); diff --git a/src/resizertopbound.js b/src/resizertopbound.js index c812516d..971532bb 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -58,6 +58,14 @@ export default class ResizerTopBound { destroy() {} + /** + * Method used to calculate the proposed size as the resize handlers are dragged. + * + * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. + * @returns {Object} return + * @returns {Number} return.x Proposed width. + * @returns {Number} return.y Proposed height. + */ updateSize( domEventData ) { const context = this.context; const currentCoordinates = context._extractCoordinates( domEventData ); @@ -146,7 +154,7 @@ export default class ResizerTopBound { context.domResizeShadow.style.height = `${ drawnSize.y }px`; } - return drawnSize; // @todo decide what size should actually be returned. + return drawnSize; } redraw() {} From deb43ab38473b0ec9ccf0be3371fb26bf2f33101 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 13:35:08 +0200 Subject: [PATCH 57/93] Internal: Removed redundant code. --- src/resizecontext.js | 28 ++++++++++++++-------------- src/resizertopbound.js | 26 ++++++++++---------------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 3f06ff54..f9d56c6a 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -52,6 +52,16 @@ export default class ResizeContext { this.options = options || {}; + /** + * The size of resize host before current resize process. + * + * This information is only known after DOM was rendered, so it will be updated later. + */ + this.originalSize = { + x: 0, + y: 0 + }; + // @todo: ---- options below seems like a little outside of a scope of a single context ---- // Reference point of resizer where the dragging started. It is used to measure the distance to user cursor @@ -62,16 +72,6 @@ export default class ResizeContext { x: 0 }; - /** - * Size of an image before resize. - * - * This information is only known after DOM was rendered, so it will be updated later. - */ - this.originalSize = { - x: 0, - y: 0 - }; - this._cleanupContext(); /** @@ -140,6 +140,7 @@ export default class ResizeContext { */ begin( domResizeHandler ) { const resizeHost = this._getResizeHost(); + const clientRect = resizeHost.getBoundingClientRect(); this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); @@ -157,14 +158,13 @@ export default class ResizeContext { this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); - // @todo: this part might be lazy used only in case if getAspectRatio is not given as it might force repaint. this.originalSize = { - x: resizeHost.clientWidth, - y: resizeHost.clientHeight + width: clientRect.width, + height: clientRect.height }; this.aspectRatio = this.options.getAspectRatio ? - this.options.getAspectRatio( resizeHost ) : this.originalSize.x / this.originalSize.y; + this.options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; this.resizeStrategy.begin( domResizeHandler ); } diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 971532bb..27d5b6a5 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -29,14 +29,6 @@ export default class ResizerTopBound { attach() {} begin() { - const clientRect = this.context._getResizeHost().getBoundingClientRect(); - - // Size of resize host before current resizing transaction. - this.initialSize = { - width: clientRect.width, - height: clientRect.height - }; - this.redrawShadow(); const resizeHost = this.context._getResizeHost(); @@ -70,16 +62,17 @@ export default class ResizerTopBound { const context = this.context; const currentCoordinates = context._extractCoordinates( domEventData ); const isCentered = this.options.isCentered ? this.options.isCentered( this.context ) : true; + const initialSize = this.context.originalSize; const enlargement = { // @todo it could be simplified if context.referenceCoordinates was an inverted corner (at least for bottom-left). - x: this.context.referenceCoordinates.x - ( currentCoordinates.x + this.initialSize.width ), - y: ( currentCoordinates.y - this.initialSize.height ) - this.context.referenceCoordinates.y + x: this.context.referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), + y: ( currentCoordinates.y - initialSize.height ) - this.context.referenceCoordinates.y }; // temp workaround if ( isCentered && this.context.referenceHandlerPosition.endsWith( '-right' ) ) { - enlargement.x = currentCoordinates.x - ( this.context.referenceCoordinates.x + this.initialSize.width ); + enlargement.x = currentCoordinates.x - ( this.context.referenceCoordinates.x + initialSize.width ); } // @todo: oddly enough, this condition **check** is not needed for tables. @@ -90,14 +83,15 @@ export default class ResizerTopBound { const resizeHost = this.context._getResizeHost(); const clientRect = resizeHost.getBoundingClientRect(); - const originalSize = { + // The size of currently visible resize host. + const currentSize = { x: clientRect.width, y: clientRect.height }; const proposedSize = { - x: Math.abs( this.initialSize.width + enlargement.x ), - y: Math.abs( this.initialSize.height + enlargement.y ) + x: Math.abs( initialSize.width + enlargement.x ), + y: Math.abs( initialSize.height + enlargement.y ) }; // Dominant determination must take the ratio into account. @@ -142,8 +136,8 @@ export default class ResizerTopBound { const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); const diff2 = { - x: parseFloat( originalSize.x ) - drawnSize.x, - y: parseFloat( originalSize.y ) - drawnSize.y + x: parseFloat( currentSize.x ) - drawnSize.x, + y: parseFloat( currentSize.y ) - drawnSize.y }; context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = '0px'; From f5126e18a2320634f1e82a4be3a7a6b28be130e8 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 14:15:03 +0200 Subject: [PATCH 58/93] Internal: Fixed center resizing compensation. Also added some missing docs. --- src/resizecontext.js | 14 +++++++++----- src/resizertopbound.js | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index f9d56c6a..e64db518 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -62,16 +62,20 @@ export default class ResizeContext { y: 0 }; - // @todo: ---- options below seems like a little outside of a scope of a single context ---- - - // Reference point of resizer where the dragging started. It is used to measure the distance to user cursor - // traveled, thus how much the image should be enlarged. - // This information is only known after DOM was rendered, so it will be updated later. + /** + * Reference point of resizer where the dragging started. It is used to measure the distance to user cursor + * traveled, thus how much the image should be enlarged. + * This information is only known after DOM was rendered, so it will be updated later. + * + * @protected + */ this.referenceCoordinates = { y: 0, x: 0 }; + // @todo: ---- options below seems like a little outside of a scope of a single context ---- + this._cleanupContext(); /** diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 27d5b6a5..52d26ad6 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -64,19 +64,30 @@ export default class ResizerTopBound { const isCentered = this.options.isCentered ? this.options.isCentered( this.context ) : true; const initialSize = this.context.originalSize; + // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number + // meaning that it has been shrunk. + // + // +----------------+--+ + // | | | + // | img | | + // | | | + // +----------------+ | ^ + // | | | - enlarge y + // +-------------------+ v + // <--> + // enlarge x const enlargement = { - // @todo it could be simplified if context.referenceCoordinates was an inverted corner (at least for bottom-left). x: this.context.referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), y: ( currentCoordinates.y - initialSize.height ) - this.context.referenceCoordinates.y }; - // temp workaround if ( isCentered && this.context.referenceHandlerPosition.endsWith( '-right' ) ) { enlargement.x = currentCoordinates.x - ( this.context.referenceCoordinates.x + initialSize.width ); } - // @todo: oddly enough, this condition **check** is not needed for tables. - if ( isCentered && enlargement.x < 0 ) { + // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from one resized + // corner to your cursor. It needs to be duplicated to compensate for the other side too. + if ( isCentered ) { enlargement.x *= 2; } From 45416fe195571d872574de91b19f67452a3cc5c5 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 14:35:34 +0200 Subject: [PATCH 59/93] Internal: Removed unused logic, simplified the code. --- src/resizecontext.js | 2 -- src/resizertopbound.js | 51 ++++++------------------------------------ 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index e64db518..42211938 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -74,8 +74,6 @@ export default class ResizeContext { x: 0 }; - // @todo: ---- options below seems like a little outside of a scope of a single context ---- - this._cleanupContext(); /** diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 52d26ad6..98797181 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -4,7 +4,6 @@ import { import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import global from '@ckeditor/ckeditor5-utils/src/dom/global'; /** * @@ -92,13 +91,6 @@ export default class ResizerTopBound { } const resizeHost = this.context._getResizeHost(); - const clientRect = resizeHost.getBoundingClientRect(); - - // The size of currently visible resize host. - const currentSize = { - x: clientRect.width, - y: clientRect.height - }; const proposedSize = { x: Math.abs( initialSize.width + enlargement.x ), @@ -120,44 +112,15 @@ export default class ResizerTopBound { drawnSize.x = drawnSize.y * context.aspectRatio; } - const resizeUsingImage = global.window.pocResizeUsingImage !== false; - let shadowBoundValue = '0px'; - - if ( !resizeUsingImage ) { - shadowBoundValue = 'auto'; - } - - // Reset shadow bounding. - context.domResizeShadow.style.top = shadowBoundValue; - context.domResizeShadow.style.left = shadowBoundValue; - context.domResizeShadow.style.bottom = shadowBoundValue; - context.domResizeShadow.style.right = shadowBoundValue; - - if ( resizeUsingImage ) { - resizeHost.style.width = `${ drawnSize.x }px`; - // resizeHost.style.height = `${ drawnSize.y }px`; + resizeHost.style.width = `${ drawnSize.x }px`; + // resizeHost.style.height = `${ drawnSize.y }px`; - const latestRect = resizeHost.getBoundingClientRect(); + // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it + // here will reflect this limitation on resizer shadow later on. + const latestRect = resizeHost.getBoundingClientRect(); - // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it - // here will reflect this limitation on resizer shadow later on. - drawnSize.x = latestRect.width; - drawnSize.y = latestRect.height; - } else { - const invertedPosition = this.context._invertPosition( context.referenceHandlerPosition ); - - const diff2 = { - x: parseFloat( currentSize.x ) - drawnSize.x, - y: parseFloat( currentSize.y ) - drawnSize.y - }; - - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 0 ] ] = '0px'; - context.domResizeShadow.style[ invertedPosition.split( '-' )[ 1 ] ] = `${ diff2.x / 2 }px`; - - // Apply the actual shadow dimensions. - context.domResizeShadow.style.width = `${ drawnSize.x }px`; - context.domResizeShadow.style.height = `${ drawnSize.y }px`; - } + drawnSize.x = latestRect.width; + drawnSize.y = latestRect.height; return drawnSize; } From d06de01267f51291a0c42dd802eae508676eb079 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 14:48:40 +0200 Subject: [PATCH 60/93] Internal: Cleaned up some CSS. --- theme/widget.css | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/theme/widget.css b/theme/widget.css index f02f86f3..9bec7436 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -36,7 +36,7 @@ visibility: hidden; position: absolute; - /* The wrapper itself should not interfere with pointer device */ + /* The wrapper itself should not interfere with pointer device, only the handlers. */ pointer-events: none; left: 0; @@ -53,21 +53,12 @@ bottom: 0; outline: 1px solid var(--ck-color-resizer); - - /* @todo: remove this dirty hack. It's purpose is that shadow element should not be - overlapped by elements that follow it in the DOM. */ - z-index: var(--ck-z-default); - - &.ck-widget__resizer-shadow-active { - display: block; - visibility: visible; - } } .ck .ck-widget__resizer { position: absolute; - /* Resizers are the only UI elements that should interfere with pointer device */ + /* Resizers are the only UI elements that should interfere with pointer device. */ pointer-events: all; width: var(--ck-resizer-size); @@ -123,10 +114,8 @@ } /* Show the selection handler when the widget is selected. */ - &.ck-widget_selected { - & .ck-widget__selection-handler { - visibility: visible; - } + &.ck-widget_selected .ck-widget__selection-handler { + visibility: visible; } } From d076a178c3e4d10bf90fd0ec448a9777b296b6ef Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 14:49:26 +0200 Subject: [PATCH 61/93] Internal: Moved most of methods from strategy to the context itself. --- src/resizecontext.js | 16 ++++++++++------ src/resizertopbound.js | 33 ++------------------------------- src/widgetresizer.js | 2 +- 3 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 42211938..4d019f55 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -168,7 +168,7 @@ export default class ResizeContext { this.aspectRatio = this.options.getAspectRatio ? this.options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; - this.resizeStrategy.begin( domResizeHandler ); + this.redraw(); } commit( editor ) { @@ -179,8 +179,6 @@ export default class ResizeContext { this.redraw(); - this.resizeStrategy.commit( editor ); - editor.model.change( writer => { writer.setAttribute( WIDTH_ATTRIBUTE_NAME, newWidth, modelEntry ); } ); @@ -191,8 +189,6 @@ export default class ResizeContext { cancel() { this._dismissShadow(); - this.resizeStrategy.cancel(); - this._cleanupContext(); } @@ -225,9 +221,16 @@ export default class ResizeContext { redraw() { if ( this.domResizeWrapper ) { const widgetWrapper = this.domResizeWrapper.parentElement; - const resizingHost = this._getResizeHost(); + const clientRect = resizingHost.getBoundingClientRect(); + + this.domResizeWrapper.style.width = clientRect.width + 'px'; + this.domResizeWrapper.style.height = clientRect.height + 'px'; + // 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( resizingHost ) ) { this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; this.domResizeWrapper.style.top = resizingHost.offsetTop + 'px'; @@ -263,6 +266,7 @@ export default class ResizeContext { * will simply follow the image size. * * @protected + * @returns {HTMLElement} */ _getResizeHost() { const widgetWrapper = this.domResizeWrapper.parentElement; diff --git a/src/resizertopbound.js b/src/resizertopbound.js index 98797181..9a37a391 100644 --- a/src/resizertopbound.js +++ b/src/resizertopbound.js @@ -1,6 +1,3 @@ -import { - getAbsoluteBoundaryPoint -} from './utils'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; @@ -25,30 +22,6 @@ export default class ResizerTopBound { this.options = options || {}; } - attach() {} - - begin() { - this.redrawShadow(); - - const resizeHost = this.context._getResizeHost(); - this.closestReferencePoint = getAbsoluteBoundaryPoint( resizeHost, this.context.referenceHandlerPosition ); - } - - redrawShadow() { - if ( this.context.domResizeWrapper ) { - const clientRect = this.context._getResizeHost().getBoundingClientRect(); - - this.context.domResizeWrapper.style.width = clientRect.width + 'px'; - this.context.domResizeWrapper.style.height = clientRect.height + 'px'; - } - } - - commit() {} - - cancel() {} - - destroy() {} - /** * Method used to calculate the proposed size as the resize handlers are dragged. * @@ -84,8 +57,8 @@ export default class ResizerTopBound { enlargement.x = currentCoordinates.x - ( this.context.referenceCoordinates.x + initialSize.width ); } - // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from one resized - // corner to your cursor. It needs to be duplicated to compensate for the other side too. + // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from + // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. if ( isCentered ) { enlargement.x *= 2; } @@ -124,8 +97,6 @@ export default class ResizerTopBound { return drawnSize; } - - redraw() {} } mix( ResizerTopBound, ObservableMixin ); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index bb3c07f5..66a169a7 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -75,7 +75,7 @@ export default class WidgetResizer extends Plugin { for ( const context of this.contexts ) { // This check is needed, as there were cases when widget was not yet initialized but layoutChanged happened. if ( context.domResizeWrapper && context.domResizeWrapper.parentElement ) { - context.resizeStrategy.redrawShadow(); + context.redraw(); } } } ); From 09c89d9bd03b423553adcd1eda4c252ff52a064c Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 14:58:35 +0200 Subject: [PATCH 62/93] Internal: Merged ResizerTopBound class back into ResizeContext. --- src/resizecontext.js | 150 ++++++++++++++++++++++++++++++++--------- src/resizertopbound.js | 102 ---------------------------- src/widgetresizer.js | 9 --- 3 files changed, 118 insertions(+), 143 deletions(-) delete mode 100644 src/resizertopbound.js diff --git a/src/resizecontext.js b/src/resizecontext.js index 4d019f55..0c54fecc 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,5 +1,4 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; -import ResizerTopBound from './resizertopbound'; import { getAbsoluteBoundaryPoint } from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; @@ -33,8 +32,6 @@ export default class ResizeContext { */ this.widgetWrapperElement = null; - this.resizeStrategy = new ResizerTopBound( this, options ); - /** * Container of entire resize UI. * @@ -171,6 +168,11 @@ export default class ResizeContext { this.redraw(); } + /** + * Accepts currently proposed resize and applies it on the resize host. + * + * @param {module:core/editor/editor~Editor} editor + */ commit( editor ) { const modelEntry = this._getModel( editor, this.widgetWrapperElement ); const newWidth = this.domResizeShadow.clientWidth; @@ -186,6 +188,9 @@ export default class ResizeContext { this._cleanupContext(); } + /** + * Cancels and rejects proposed resize dimensions hiding all the UI. + */ cancel() { this._dismissShadow(); @@ -207,7 +212,7 @@ export default class ResizeContext { * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. */ updateSize( domEventData ) { - const proposedSize = this.resizeStrategy.updateSize( domEventData ); + const proposedSize = this._updateImageSize( domEventData ); this.domResizeWrapper.style.width = proposedSize.x + 'px'; this.domResizeWrapper.style.height = proposedSize.y + 'px'; @@ -275,6 +280,42 @@ export default class ResizeContext { this.options.getResizeHost( widgetWrapper ) : widgetWrapper; } + /** + * @protected + */ + _dismissShadow() { + this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); + this.domResizeShadow.removeAttribute( 'style' ); + } + + /** + * + * @param {module:core/editor/editor~Editor} editor + * @param {module:engine/view/element~Element} widgetWrapperElement + * @returns {module:engine/model/element~Element|undefined} + * @protected + */ + _getModel( editor, widgetWrapperElement ) { + return editor.editing.mapper.toModelElement( widgetWrapperElement ); + } + + /** + * @param {String} position Like `"top-left"`. + * @returns {String} Inverted `position`. + * @protected + */ + _invertPosition( position ) { + const parts = position.split( '-' ); + const replacements = { + top: 'bottom', + bottom: 'top', + left: 'right', + right: 'left' + }; + + return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; + } + /** * @private * @param {HTMLDocument} domDocument Document where the widget is used. @@ -330,22 +371,84 @@ export default class ResizeContext { domElement.appendChild( this.sizeElement ); } - _dismissShadow() { - this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); - this.domResizeShadow.removeAttribute( 'style' ); - } - /** + * Method used to calculate the proposed size as the resize handlers are dragged. * - * @param {module:core/editor/editor~Editor} editor - * @param {module:engine/view/element~Element} widgetWrapperElement - * @returns {module:engine/model/element~Element|undefined} - * @protected + * @private + * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. + * @returns {Object} return + * @returns {Number} return.x Proposed width. + * @returns {Number} return.y Proposed height. */ - _getModel( editor, widgetWrapperElement ) { - return editor.editing.mapper.toModelElement( widgetWrapperElement ); + _updateImageSize( domEventData ) { + const currentCoordinates = this._extractCoordinates( domEventData ); + const isCentered = this.options.isCentered ? this.options.isCentered( this ) : true; + const initialSize = this.originalSize; + + // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number + // meaning that it has been shrunk. + // + // +----------------+--+ + // | | | + // | img | | + // | | | + // +----------------+ | ^ + // | | | - enlarge y + // +-------------------+ v + // <--> + // enlarge x + const enlargement = { + x: this.referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), + y: ( currentCoordinates.y - initialSize.height ) - this.referenceCoordinates.y + }; + + if ( isCentered && this.referenceHandlerPosition.endsWith( '-right' ) ) { + enlargement.x = currentCoordinates.x - ( this.referenceCoordinates.x + initialSize.width ); + } + + // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from + // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. + if ( isCentered ) { + enlargement.x *= 2; + } + + const resizeHost = this._getResizeHost(); + + const proposedSize = { + x: Math.abs( initialSize.width + enlargement.x ), + y: Math.abs( initialSize.height + enlargement.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.x / this.aspectRatio > proposedSize.y ? 'x' : 'y'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + const drawnSize = { + x: proposedSize.x, + y: proposedSize.y + }; + + if ( proposedSize.dominant == 'x' ) { + drawnSize.y = drawnSize.x / this.aspectRatio; + } else { + drawnSize.x = drawnSize.y * this.aspectRatio; + } + + resizeHost.style.width = `${ drawnSize.x }px`; + + // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it + // here will reflect this limitation on resizer shadow later on. + const latestRect = resizeHost.getBoundingClientRect(); + + drawnSize.x = latestRect.width; + drawnSize.y = latestRect.height; + + return drawnSize; } + /** + * @private + */ _extractCoordinates( event ) { return { x: event.pageX, @@ -378,23 +481,6 @@ export default class ResizeContext { } } } - - /** - * @param {String} position Like `"top-left"`. - * @returns {String} Inverted `position`. - * @protected - */ - _invertPosition( position ) { - const parts = position.split( '-' ); - const replacements = { - top: 'bottom', - bottom: 'top', - left: 'right', - right: 'left' - }; - - return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; - } } mix( ResizeContext, ObservableMixin ); diff --git a/src/resizertopbound.js b/src/resizertopbound.js deleted file mode 100644 index 9a37a391..00000000 --- a/src/resizertopbound.js +++ /dev/null @@ -1,102 +0,0 @@ - -import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; -import mix from '@ckeditor/ckeditor5-utils/src/mix'; - -/** - * - * This resizer is top bound, and expands symmetrically towards left, right and increasingly fast toward bottom - * (to compensate for top anchoring). - * - * - * - */ - -/** - * Implements a resizer that enlarges/shrinks in all directions. - * - * @class ResizerTopBound - */ -export default class ResizerTopBound { - constructor( context, options ) { - this.context = context; - this.options = options || {}; - } - - /** - * Method used to calculate the proposed size as the resize handlers are dragged. - * - * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. - * @returns {Object} return - * @returns {Number} return.x Proposed width. - * @returns {Number} return.y Proposed height. - */ - updateSize( domEventData ) { - const context = this.context; - const currentCoordinates = context._extractCoordinates( domEventData ); - const isCentered = this.options.isCentered ? this.options.isCentered( this.context ) : true; - const initialSize = this.context.originalSize; - - // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number - // meaning that it has been shrunk. - // - // +----------------+--+ - // | | | - // | img | | - // | | | - // +----------------+ | ^ - // | | | - enlarge y - // +-------------------+ v - // <--> - // enlarge x - const enlargement = { - x: this.context.referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), - y: ( currentCoordinates.y - initialSize.height ) - this.context.referenceCoordinates.y - }; - - if ( isCentered && this.context.referenceHandlerPosition.endsWith( '-right' ) ) { - enlargement.x = currentCoordinates.x - ( this.context.referenceCoordinates.x + initialSize.width ); - } - - // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from - // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. - if ( isCentered ) { - enlargement.x *= 2; - } - - const resizeHost = this.context._getResizeHost(); - - const proposedSize = { - x: Math.abs( initialSize.width + enlargement.x ), - y: Math.abs( initialSize.height + enlargement.y ) - }; - - // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.x / context.aspectRatio > proposedSize.y ? 'x' : 'y'; - proposedSize.max = proposedSize[ proposedSize.dominant ]; - - const drawnSize = { - x: proposedSize.x, - y: proposedSize.y - }; - - if ( proposedSize.dominant == 'x' ) { - drawnSize.y = drawnSize.x / context.aspectRatio; - } else { - drawnSize.x = drawnSize.y * context.aspectRatio; - } - - resizeHost.style.width = `${ drawnSize.x }px`; - // resizeHost.style.height = `${ drawnSize.y }px`; - - // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it - // here will reflect this limitation on resizer shadow later on. - const latestRect = resizeHost.getBoundingClientRect(); - - drawnSize.x = latestRect.width; - drawnSize.y = latestRect.height; - - return drawnSize; - } -} - -mix( ResizerTopBound, ObservableMixin ); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 66a169a7..ccd4a945 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -10,7 +10,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import ResizeContext from './resizecontext'; -import ResizerTopBound from './resizertopbound'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -42,14 +41,6 @@ export default class WidgetResizer extends Plugin { } init() { - this.set( 'resizerStrategy', null ); - - this.on( 'change:resizerStrategy', ( event, name, value ) => { - for ( const context of this.contexts ) { - context.resizeStrategy = new ( value || ResizerTopBound )( context, context.options ); - } - } ); - this.contexts = []; this.activeContext = null; From 14d4dc6ddf6647a890e52f51d1eaa059ad568e3e Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 15:04:09 +0200 Subject: [PATCH 63/93] Internal: Simplified UI building. --- src/resizecontext.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 0c54fecc..02fd2dad 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,5 +1,7 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; -import { getAbsoluteBoundaryPoint } from './utils'; +import { + getAbsoluteBoundaryPoint +} from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; @@ -119,7 +121,7 @@ export default class ResizeContext { }, function( domDocument ) { const domElement = this.toDomElement( domDocument ); - that.domResizeShadow = that._appendShadowElement( domDocument, domElement ); + that.domResizeShadow = that._appendShadowElement( domElement ); that._appendResizers( that.domResizeShadow ); that._appendSizeUi( that.domResizeShadow ); @@ -318,12 +320,16 @@ export default class ResizeContext { /** * @private - * @param {HTMLDocument} domDocument Document where the widget is used. * @param {HTMLElement} domElement The outer wrapper of resize UI within a given widget. */ - _appendShadowElement( domDocument, domElement ) { - const shadowElement = domDocument.createElement( 'div' ); - shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' ); + _appendShadowElement( domElement ) { + const shadowElement = new Template( { + tag: 'div', + attributes: { + class: 'ck ck-widget__resizer-shadow' + } + } ).render(); + domElement.appendChild( shadowElement ); return shadowElement; From 8001bc31e4c9cf014d44aa7608a8568733240161 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 15:13:21 +0200 Subject: [PATCH 64/93] Internal: Use Rect type instead native DOM getBoundingClientRect. --- src/resizecontext.js | 7 ++++--- src/utils.js | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 02fd2dad..e08b44a9 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -3,6 +3,7 @@ import { getAbsoluteBoundaryPoint } from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; +import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; @@ -141,7 +142,7 @@ export default class ResizeContext { */ begin( domResizeHandler ) { const resizeHost = this._getResizeHost(); - const clientRect = resizeHost.getBoundingClientRect(); + const clientRect = new Rect( resizeHost ); this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); @@ -229,7 +230,7 @@ export default class ResizeContext { if ( this.domResizeWrapper ) { const widgetWrapper = this.domResizeWrapper.parentElement; const resizingHost = this._getResizeHost(); - const clientRect = resizingHost.getBoundingClientRect(); + const clientRect = new Rect( resizingHost ); this.domResizeWrapper.style.width = clientRect.width + 'px'; this.domResizeWrapper.style.height = clientRect.height + 'px'; @@ -444,7 +445,7 @@ export default class ResizeContext { // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it // here will reflect this limitation on resizer shadow later on. - const latestRect = resizeHost.getBoundingClientRect(); + const latestRect = new Rect( resizeHost ); drawnSize.x = latestRect.width; drawnSize.y = latestRect.height; diff --git a/src/utils.js b/src/utils.js index e1198e93..a23513ec 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,6 +9,7 @@ import HighlightStack from './highlightstack'; import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; +import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import env from '@ckeditor/ckeditor5-utils/src/env'; import dragHandlerIcon from '../theme/icons/drag-handler.svg'; @@ -358,11 +359,11 @@ export function viewToModelPositionOutsideModelElement( model, viewElementMatche * @returns {Number} return.y */ export function getAbsoluteBoundaryPoint( element, resizerPosition ) { - const nativeRectangle = element.getBoundingClientRect(); + const elementRect = new Rect( element ); const positionParts = resizerPosition.split( '-' ); const ret = { - x: positionParts[ 1 ] == 'right' ? nativeRectangle.right : nativeRectangle.left, - y: positionParts[ 0 ] == 'bottom' ? nativeRectangle.bottom : nativeRectangle.top + x: positionParts[ 1 ] == 'right' ? elementRect.right : elementRect.left, + y: positionParts[ 0 ] == 'bottom' ? elementRect.bottom : elementRect.top }; ret.x += element.ownerDocument.defaultView.scrollX; From 7ca16fcff8da2c093d93c83ab130f0ca61edadba Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 15:29:58 +0200 Subject: [PATCH 65/93] Internal: Cleanup docs and removed stray isActive variable. --- src/widgetresizer.js | 62 +++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index ccd4a945..12d744ff 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -15,18 +15,6 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; const WIDTH_ATTRIBUTE_NAME = 'width'; -/** - * Interface describing a resizer. It allows to define available resizer set, specify resizing host etc. - * - * @interface ResizerOptions - */ - -/** - * List of available resizers like `"top-left"`, `"bottom-right"`, etc. - * - * @member {Array.} module:widget/widgetresizer~ResizerOptions#resizers - */ - /** * Widget resize feature plugin. * @@ -53,8 +41,6 @@ export default class WidgetResizer extends Plugin { mouseDownUp: Object.create( DomEmitterMixin ), }; - let isActive = false; - const mouseMoveListener = ( event, domEventData ) => { if ( this.activeContext ) { this.activeContext.updateSize( domEventData ); @@ -62,7 +48,7 @@ export default class WidgetResizer extends Plugin { }; this.editor.editing.view.document.on( 'layoutChanged', () => { - // This works around the issue with undo. + // Redrawing on layout change fixes issue with browser window resize or undo causing a mispositioned resizer. for ( const context of this.contexts ) { // This check is needed, as there were cases when widget was not yet initialized but layoutChanged happened. if ( context.domResizeWrapper && context.domResizeWrapper.parentElement ) { @@ -77,8 +63,6 @@ export default class WidgetResizer extends Plugin { const resizeHandler = isResizeHandler( target ) ? target : getAncestors( target ).filter( isResizeHandler )[ 0 ]; if ( resizeHandler ) { - isActive = true; - // this._observers.mouseMove.enable(); this._observers.mouseMove.listenTo( mouseObserverHost, 'mousemove', mouseMoveListener ); this.activeContext = this._getContextByHandler( resizeHandler ); @@ -90,9 +74,7 @@ export default class WidgetResizer extends Plugin { } ); const finishResizing = () => { - if ( isActive ) { - isActive = false; - // this._observers.mouseMove.disable(); + if ( this.activeContext ) { this._observers.mouseMove.stopListening( mouseObserverHost, 'mousemove', mouseMoveListener ); if ( this.activeContext ) { @@ -103,7 +85,6 @@ export default class WidgetResizer extends Plugin { } }; - // @todo: it should listen on the entire window, as it should also catch events outside of the editable. this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mouseup', finishResizing ); function isResizeHandler( element ) { @@ -191,3 +172,42 @@ export default class WidgetResizer extends Plugin { } ); } } + +/** + * Interface describing a resizer. It allows to specify resizing host, custom logic for calculating aspect ratio etc. + * + * @interface ResizerOptions + */ + +/** + * Function to explicitly point the resizing host. + * + * By default resizer will use widget wrapper, but it's possible to point any child within widget wrapper. + * + * ```js + * editor.plugins.get( 'WidgetResizer' ).apply( widget, conversionApi.writer, { + * getResizeHost( wrapper ) { + * return wrapper.querySelector( 'img' ); + * } + * } ); + * ``` + * + * @member {Function} module:widget/widgetresizer~ResizerOptions#getResizeHost + */ + +/** + * @member {Function} module:widget/widgetresizer~ResizerOptions#getAspectRatio + */ + +/** + * ```js + * editor.plugins.get( 'WidgetResizer' ).apply( widget, conversionApi.writer, { + * isCentered( context ) { + * const imageStyle = context._getModel( editor, context.widgetWrapperElement ).getAttribute( 'imageStyle' ); + * + * return !imageStyle || imageStyle == 'full'; + * } + * } ); + * ``` + * @member {Function} module:widget/widgetresizer~ResizerOptions#isCentered + */ From 0ab8a2f772ff26f7c42db7740a8eaac905e45607 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 15:32:56 +0200 Subject: [PATCH 66/93] Docs: Updated the docs. --- src/resizecontext.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index e08b44a9..11b15d6d 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -97,7 +97,7 @@ export default class ResizeContext { */ /** - * Position of a handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` + * Direction towards which the widget is being resized, e.g. `"top-left"`, `"bottom-right"` etc or `null` * if not active. * * @readonly From 4f4f444a9d613619bb46def8268396da2f0a6749 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 23:25:09 +0200 Subject: [PATCH 67/93] Internal: Moved several properties to a protected scope. --- src/resizecontext.js | 94 ++++++++++++++++++++++++-------------------- src/widgetresizer.js | 5 +-- 2 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 11b15d6d..acae4090 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -21,13 +21,6 @@ const WIDTH_ATTRIBUTE_NAME = 'width'; */ export default class ResizeContext { constructor( options ) { - /** - * View to a wrapper containing all the resizer-related views. - * - * @member {module:engine/view/uielement~UIElement} - */ - this.resizeWrapperElement = null; - /** * View of a widget associated with the resizer. * @@ -45,13 +38,6 @@ export default class ResizeContext { */ this.domResizeWrapper = null; - /** - * @member {HTMLElement|null} - */ - this.domResizeShadow = null; - - this.options = options || {}; - /** * The size of resize host before current resize process. * @@ -62,6 +48,11 @@ export default class ResizeContext { y: 0 }; + /** + * @member {module:widget/widgetresizer~ResizerOptions} + */ + this._options = options || {}; + /** * Reference point of resizer where the dragging started. It is used to measure the distance to user cursor * traveled, thus how much the image should be enlarged. @@ -69,11 +60,25 @@ export default class ResizeContext { * * @protected */ - this.referenceCoordinates = { + this._referenceCoordinates = { y: 0, x: 0 }; + /** + * View to a wrapper containing all the resizer-related views. + * + * @private + * @member {module:engine/view/uielement~UIElement} + */ + this._resizeWrapperElement = null; + + /** + * @private + * @member {HTMLElement|null} + */ + this._domResizeShadow = null; + this._cleanupContext(); /** @@ -117,14 +122,14 @@ export default class ResizeContext { this.widgetWrapperElement = widgetElement; - this.resizeWrapperElement = writer.createUIElement( 'div', { + this._resizeWrapperElement = writer.createUIElement( 'div', { class: 'ck ck-widget__resizer-wrapper' }, function( domDocument ) { const domElement = this.toDomElement( domDocument ); - that.domResizeShadow = that._appendShadowElement( domElement ); - that._appendResizers( that.domResizeShadow ); - that._appendSizeUi( that.domResizeShadow ); + that._domResizeShadow = that._appendShadowElement( domElement ); + that._appendResizers( that._domResizeShadow ); + that._appendSizeUi( that._domResizeShadow ); that.domResizeWrapper = domElement; @@ -132,7 +137,7 @@ export default class ResizeContext { } ); // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), this.resizeWrapperElement ); + writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), this._resizeWrapperElement ); writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement ); } @@ -144,7 +149,7 @@ export default class ResizeContext { const resizeHost = this._getResizeHost(); const clientRect = new Rect( resizeHost ); - this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); + this._domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); /** * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` @@ -158,15 +163,15 @@ export default class ResizeContext { const reversedPosition = this._invertPosition( this.referenceHandlerPosition ); - this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); + this._referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); this.originalSize = { width: clientRect.width, height: clientRect.height }; - this.aspectRatio = this.options.getAspectRatio ? - this.options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; + this.aspectRatio = this._options.getAspectRatio ? + this._options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; this.redraw(); } @@ -178,7 +183,7 @@ export default class ResizeContext { */ commit( editor ) { const modelEntry = this._getModel( editor, this.widgetWrapperElement ); - const newWidth = this.domResizeShadow.clientWidth; + const newWidth = this._domResizeShadow.clientWidth; this._dismissShadow(); @@ -203,7 +208,7 @@ export default class ResizeContext { destroy() { this.cancel(); - this.domResizeShadow = null; + this._domResizeShadow = null; this.wrapper = null; } @@ -227,24 +232,27 @@ export default class ResizeContext { } redraw() { - if ( this.domResizeWrapper ) { - const widgetWrapper = this.domResizeWrapper.parentElement; + const domWrapper = this.domResizeWrapper; + + if ( domWrapper && domWrapper.parentElement ) { + // Refresh only if resizer exists in the DOM. + const widgetWrapper = domWrapper.parentElement; const resizingHost = this._getResizeHost(); const clientRect = new Rect( resizingHost ); - this.domResizeWrapper.style.width = clientRect.width + 'px'; - this.domResizeWrapper.style.height = clientRect.height + 'px'; + domWrapper.style.width = clientRect.width + 'px'; + domWrapper.style.height = clientRect.height + 'px'; // 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( resizingHost ) ) { - this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px'; - this.domResizeWrapper.style.top = resizingHost.offsetTop + 'px'; + domWrapper.style.left = resizingHost.offsetLeft + 'px'; + domWrapper.style.top = resizingHost.offsetTop + 'px'; - this.domResizeWrapper.style.height = resizingHost.offsetHeight + 'px'; - this.domResizeWrapper.style.width = resizingHost.offsetWidth + 'px'; + domWrapper.style.height = resizingHost.offsetHeight + 'px'; + domWrapper.style.width = resizingHost.offsetWidth + 'px'; } } } @@ -279,16 +287,16 @@ export default class ResizeContext { _getResizeHost() { const widgetWrapper = this.domResizeWrapper.parentElement; - return this.options.getResizeHost ? - this.options.getResizeHost( widgetWrapper ) : widgetWrapper; + return this._options.getResizeHost ? + this._options.getResizeHost( widgetWrapper ) : widgetWrapper; } /** * @protected */ _dismissShadow() { - this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); - this.domResizeShadow.removeAttribute( 'style' ); + this._domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); + this._domResizeShadow.removeAttribute( 'style' ); } /** @@ -304,7 +312,7 @@ export default class ResizeContext { /** * @param {String} position Like `"top-left"`. - * @returns {String} Inverted `position`. + * @returns {String} Inverted `position`, e.g. returns `"bottom-right"` if `"top-left"` was given as `position`. * @protected */ _invertPosition( position ) { @@ -389,7 +397,7 @@ export default class ResizeContext { */ _updateImageSize( domEventData ) { const currentCoordinates = this._extractCoordinates( domEventData ); - const isCentered = this.options.isCentered ? this.options.isCentered( this ) : true; + const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; const initialSize = this.originalSize; // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number @@ -405,12 +413,12 @@ export default class ResizeContext { // <--> // enlarge x const enlargement = { - x: this.referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), - y: ( currentCoordinates.y - initialSize.height ) - this.referenceCoordinates.y + x: this._referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), + y: ( currentCoordinates.y - initialSize.height ) - this._referenceCoordinates.y }; if ( isCentered && this.referenceHandlerPosition.endsWith( '-right' ) ) { - enlargement.x = currentCoordinates.x - ( this.referenceCoordinates.x + initialSize.width ); + enlargement.x = currentCoordinates.x - ( this._referenceCoordinates.x + initialSize.width ); } // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 12d744ff..5c751e42 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -50,10 +50,7 @@ export default class WidgetResizer extends Plugin { this.editor.editing.view.document.on( 'layoutChanged', () => { // Redrawing on layout change fixes issue with browser window resize or undo causing a mispositioned resizer. for ( const context of this.contexts ) { - // This check is needed, as there were cases when widget was not yet initialized but layoutChanged happened. - if ( context.domResizeWrapper && context.domResizeWrapper.parentElement ) { - context.redraw(); - } + context.redraw(); } } ); From 6a29d7098d5c1b7c85b38dc82674324957c8485b Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 12 Aug 2019 23:57:37 +0200 Subject: [PATCH 68/93] Internal: Throttle mousemove/redraw events. --- package.json | 3 ++- src/resizecontext.js | 6 +++++- src/widgetresizer.js | 27 ++++++++++++++++++--------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index ece4f579..15ffed8b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "@ckeditor/ckeditor5-core": "^12.2.1", "@ckeditor/ckeditor5-engine": "^13.2.1", "@ckeditor/ckeditor5-ui": "^13.0.2", - "@ckeditor/ckeditor5-utils": "^13.0.1" + "@ckeditor/ckeditor5-utils": "^13.0.1", + "lodash-es": "^4.17.10" }, "devDependencies": { "@ckeditor/ckeditor5-basic-styles": "^11.1.3", diff --git a/src/resizecontext.js b/src/resizecontext.js index acae4090..2b9f1a53 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -234,7 +234,7 @@ export default class ResizeContext { redraw() { const domWrapper = this.domResizeWrapper; - if ( domWrapper && domWrapper.parentElement ) { + if ( existsInDom( domWrapper ) ) { // Refresh only if resizer exists in the DOM. const widgetWrapper = domWrapper.parentElement; const resizingHost = this._getResizeHost(); @@ -255,6 +255,10 @@ export default class ResizeContext { domWrapper.style.width = resizingHost.offsetWidth + 'px'; } } + + function existsInDom( element ) { + return element && element.ownerDocument && element.ownerDocument.contains( element ); + } } /** diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 5c751e42..b24d7942 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -12,6 +12,7 @@ import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import ResizeContext from './resizecontext'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import { throttle } from 'lodash-es'; const WIDTH_ATTRIBUTE_NAME = 'width'; @@ -36,23 +37,19 @@ export default class WidgetResizer extends Plugin { const mouseObserverHost = global.window.document; + const THROTTLE_THRESHOLD = 16; // 16ms = ~60fps + this._observers = { mouseMove: Object.create( DomEmitterMixin ), mouseDownUp: Object.create( DomEmitterMixin ), + windowResize: Object.create( DomEmitterMixin ) }; - const mouseMoveListener = ( event, domEventData ) => { + const mouseMoveListener = throttle( ( event, domEventData ) => { if ( this.activeContext ) { this.activeContext.updateSize( domEventData ); } - }; - - this.editor.editing.view.document.on( 'layoutChanged', () => { - // Redrawing on layout change fixes issue with browser window resize or undo causing a mispositioned resizer. - for ( const context of this.contexts ) { - context.redraw(); - } - } ); + }, THROTTLE_THRESHOLD ); this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mousedown', ( event, domEventData ) => { const target = domEventData.target; @@ -82,8 +79,20 @@ export default class WidgetResizer extends Plugin { } }; + const resizeContexts = throttle( () => { + for ( const context of this.contexts ) { + context.redraw(); + } + }, THROTTLE_THRESHOLD ); + this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mouseup', finishResizing ); + // Redrawing on layout change fixes issue with browser window resize or undo causing a mispositioned resizer. + this.editor.editing.view.document.on( 'layoutChanged', resizeContexts ); + + // Resizers need to be redrawn upon window resize, because new window might shrink resize host. + this._observers.windowResize.listenTo( global.window, 'resize', resizeContexts ); + function isResizeHandler( element ) { return element.classList && element.classList.contains( 'ck-widget__resizer' ); } From e0bb0da8aefdfe4ec7655ef085b0e986013de575 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 13 Aug 2019 09:14:35 +0200 Subject: [PATCH 69/93] Internal: WidgetResizer.apply will now return a created context. Also context's method are now observable. --- src/resizecontext.js | 5 +++++ src/widgetresizer.js | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/resizecontext.js b/src/resizecontext.js index 2b9f1a53..60ced7d2 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -81,6 +81,11 @@ export default class ResizeContext { this._cleanupContext(); + this.decorate( 'begin' ); + this.decorate( 'cancel' ); + this.decorate( 'commit' ); + this.decorate( 'updateSize' ); + /** * Width proposed (but not yet accepted) using the widget resizer. * diff --git a/src/widgetresizer.js b/src/widgetresizer.js index b24d7942..09f4de92 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -144,6 +144,7 @@ export default class WidgetResizer extends Plugin { * @param {module:engine/view/containerelement~ContainerElement} widgetElement * @param {module:engine/view/downcastwriter~DowncastWriter} writer * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. + * @returns {module:widget/resizecontext~ResizeContext} */ apply( widgetElement, writer, options ) { const context = new ResizeContext( options ); @@ -152,6 +153,8 @@ export default class WidgetResizer extends Plugin { this.editor.editing.view.once( 'render', () => context.redraw() ); this.contexts.push( context ); + + return context; } /** From 05261ffac21be5ea2f2be94f3ce581879bfcbb2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 13 Aug 2019 10:31:18 +0200 Subject: [PATCH 70/93] Cleanup, use px unit for width. --- src/resizecontext.js | 17 ++++++++++------- src/widgetresizer.js | 15 ++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/resizecontext.js b/src/resizecontext.js index 60ced7d2..888361b8 100644 --- a/src/resizecontext.js +++ b/src/resizecontext.js @@ -1,3 +1,12 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/resizecontext + */ + import View from '@ckeditor/ckeditor5-ui/src/view'; import { getAbsoluteBoundaryPoint @@ -8,12 +17,6 @@ import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; -/** - * @module widget/resizecontext - */ - -const WIDTH_ATTRIBUTE_NAME = 'width'; - /** * Stores the internal state of a single resizable object. * @@ -195,7 +198,7 @@ export default class ResizeContext { this.redraw(); editor.model.change( writer => { - writer.setAttribute( WIDTH_ATTRIBUTE_NAME, newWidth, modelEntry ); + writer.setAttribute( 'width', newWidth + 'px', modelEntry ); } ); this._cleanupContext(); diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 09f4de92..bec23eee 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -14,8 +14,6 @@ import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { throttle } from 'lodash-es'; -const WIDTH_ATTRIBUTE_NAME = 'width'; - /** * Widget resize feature plugin. * @@ -33,12 +31,13 @@ export default class WidgetResizer extends Plugin { this.contexts = []; this.activeContext = null; - this._registerSchema(); - const mouseObserverHost = global.window.document; - const THROTTLE_THRESHOLD = 16; // 16ms = ~60fps + this.editor.model.schema.setAttributeProperties( 'width', { + isFormatting: true + } ); + this._observers = { mouseMove: Object.create( DomEmitterMixin ), mouseDownUp: Object.create( DomEmitterMixin ), @@ -174,12 +173,6 @@ export default class WidgetResizer extends Plugin { return this._getContextByWrapper( getAncestors( domResizeHandler ) .filter( element => element.classList.contains( 'ck-widget__resizer-wrapper' ) )[ 0 ] ); } - - _registerSchema() { - this.editor.model.schema.setAttributeProperties( WIDTH_ATTRIBUTE_NAME, { - isFormatting: true - } ); - } } /** From c529432a2971d6c7c93b137c261f9f8eff02420e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 13 Aug 2019 11:17:29 +0200 Subject: [PATCH 71/93] Refactoring. --- src/{resizecontext.js => resizer.js} | 57 ++++--------- src/widgetresizer.js | 120 ++++++++------------------- 2 files changed, 52 insertions(+), 125 deletions(-) rename src/{resizecontext.js => resizer.js} (90%) diff --git a/src/resizecontext.js b/src/resizer.js similarity index 90% rename from src/resizecontext.js rename to src/resizer.js index 888361b8..34db44ae 100644 --- a/src/resizecontext.js +++ b/src/resizer.js @@ -4,7 +4,7 @@ */ /** - * @module widget/resizecontext + * @module widget/resizer */ import View from '@ckeditor/ckeditor5-ui/src/view'; @@ -20,24 +20,20 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; /** * Stores the internal state of a single resizable object. * - * @class ResizeContext + * @class Resizer */ -export default class ResizeContext { +export default class Resizer { + /** + * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. + */ constructor( options ) { - /** - * View of a widget associated with the resizer. - * - * @member {module:engine/view/element~Element} - */ - this.widgetWrapperElement = null; - /** * Container of entire resize UI. * * Note that this property is initialized only after the element bound with resizer is drawn * so it will be a `null` when uninitialized. * - * @member {HTMLElement|null} + * @type {HTMLElement|null} #domResizeWrapper */ this.domResizeWrapper = null; @@ -52,7 +48,7 @@ export default class ResizeContext { }; /** - * @member {module:widget/widgetresizer~ResizerOptions} + * @type {module:widget/widgetresizer~ResizerOptions} */ this._options = options || {}; @@ -72,13 +68,13 @@ export default class ResizeContext { * View to a wrapper containing all the resizer-related views. * * @private - * @member {module:engine/view/uielement~UIElement} + * @type {module:engine/view/uielement~UIElement} */ this._resizeWrapperElement = null; /** * @private - * @member {HTMLElement|null} + * @type {HTMLElement|null} */ this._domResizeShadow = null; @@ -120,15 +116,12 @@ export default class ResizeContext { } /** - * Method to be called to attach a resizer to a given widget element. * - * @param {module:engine/view/element~Element} widgetElement Widget's wrapper. - * @param {module:engine/view/downcastwriter~DowncastWriter} writer */ - attach( widgetElement, writer ) { + attach() { const that = this; - - this.widgetWrapperElement = widgetElement; + const viewElement = this._options.viewElement; + const writer = this._options.downcastWriter; this._resizeWrapperElement = writer.createUIElement( 'div', { class: 'ck ck-widget__resizer-wrapper' @@ -145,8 +138,8 @@ export default class ResizeContext { } ); // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), this._resizeWrapperElement ); - writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement ); + writer.insert( writer.createPositionAt( viewElement, viewElement.childCount ), this._resizeWrapperElement ); + writer.addClass( [ 'ck-widget_with-resizer' ], viewElement ); } /** @@ -190,7 +183,7 @@ export default class ResizeContext { * @param {module:core/editor/editor~Editor} editor */ commit( editor ) { - const modelEntry = this._getModel( editor, this.widgetWrapperElement ); + const modelElement = this._options.modelElement; const newWidth = this._domResizeShadow.clientWidth; this._dismissShadow(); @@ -198,7 +191,7 @@ export default class ResizeContext { this.redraw(); editor.model.change( writer => { - writer.setAttribute( 'width', newWidth + 'px', modelEntry ); + writer.setAttribute( 'width', newWidth + 'px', modelElement ); } ); this._cleanupContext(); @@ -215,9 +208,6 @@ export default class ResizeContext { destroy() { this.cancel(); - - this._domResizeShadow = null; - this.wrapper = null; } /** @@ -312,20 +302,9 @@ export default class ResizeContext { } /** - * - * @param {module:core/editor/editor~Editor} editor - * @param {module:engine/view/element~Element} widgetWrapperElement - * @returns {module:engine/model/element~Element|undefined} * @protected - */ - _getModel( editor, widgetWrapperElement ) { - return editor.editing.mapper.toModelElement( widgetWrapperElement ); - } - - /** * @param {String} position Like `"top-left"`. * @returns {String} Inverted `position`, e.g. returns `"bottom-right"` if `"top-left"` was given as `position`. - * @protected */ _invertPosition( position ) { const parts = position.split( '-' ); @@ -510,7 +489,7 @@ export default class ResizeContext { } } -mix( ResizeContext, ObservableMixin ); +mix( Resizer, ObservableMixin ); class SizeView extends View { constructor() { diff --git a/src/widgetresizer.js b/src/widgetresizer.js index bec23eee..861e198d 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; -import ResizeContext from './resizecontext'; +import Resizer from './resizer'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { throttle } from 'lodash-es'; @@ -28,8 +28,8 @@ export default class WidgetResizer extends Plugin { } init() { - this.contexts = []; - this.activeContext = null; + this.resizers = []; + this.activeResizer = null; const mouseObserverHost = global.window.document; const THROTTLE_THRESHOLD = 16; // 16ms = ~60fps @@ -45,8 +45,8 @@ export default class WidgetResizer extends Plugin { }; const mouseMoveListener = throttle( ( event, domEventData ) => { - if ( this.activeContext ) { - this.activeContext.updateSize( domEventData ); + if ( this.activeResizer ) { + this.activeResizer.updateSize( domEventData ); } }, THROTTLE_THRESHOLD ); @@ -58,28 +58,28 @@ export default class WidgetResizer extends Plugin { if ( resizeHandler ) { this._observers.mouseMove.listenTo( mouseObserverHost, 'mousemove', mouseMoveListener ); - this.activeContext = this._getContextByHandler( resizeHandler ); + this.activeResizer = this._getContextByHandler( resizeHandler ); - if ( this.activeContext ) { - this.activeContext.begin( resizeHandler ); + if ( this.activeResizer ) { + this.activeResizer.begin( resizeHandler ); } } } ); const finishResizing = () => { - if ( this.activeContext ) { + if ( this.activeResizer ) { this._observers.mouseMove.stopListening( mouseObserverHost, 'mousemove', mouseMoveListener ); - if ( this.activeContext ) { - this.activeContext.commit( this.editor ); + if ( this.activeResizer ) { + this.activeResizer.commit( this.editor ); } - this.activeContext = null; + this.activeResizer = null; } }; const resizeContexts = throttle( () => { - for ( const context of this.contexts ) { + for ( const context of this.resizers ) { context.redraw(); } }, THROTTLE_THRESHOLD ); @@ -98,62 +98,19 @@ export default class WidgetResizer extends Plugin { } /** - * Method that applies a resizer to a given `widgetElement`. - * - * ```js - * conversion.for( 'editingDowncast' ).elementToElement( { - * model: 'image', - * view: ( modelElement, viewWriter ) => { - * const widget = toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) ); - * - * editor.plugins.get( 'WidgetResizer' ).apply( widget, viewWriter ); - * - * return widget; - * } - * } ); - * ``` - * - * You can use the `options` parameter to customize the behavior of the resizer: - * - * ```js - * conversion.for( 'editingDowncast' ).elementToElement( { - * model: 'image', - * view: ( modelElement, viewWriter ) => { - * const widget = toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) ); - * - * editor.plugins.get( 'WidgetResizer' ).apply( widget, viewWriter, { - * getResizeHost( wrapper ) { - * return wrapper.querySelector( 'img' ); - * }, - * getAspectRatio( resizeHost ) { - * return resizeHost.naturalWidth / resizeHost.naturalHeight; - * }, - * isCentered( context ) { - * const imageStyle = context._getModel( editor, context.widgetWrapperElement ).getAttribute( 'imageStyle' ); - * - * return !imageStyle || imageStyle == 'full'; - * } - * } ); - * - * return widget; - * } - * } ); - * ``` - * - * @param {module:engine/view/containerelement~ContainerElement} widgetElement - * @param {module:engine/view/downcastwriter~DowncastWriter} writer * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. - * @returns {module:widget/resizecontext~ResizeContext} + * @returns {module:widget/resizer~Resizer} */ - apply( widgetElement, writer, options ) { - const context = new ResizeContext( options ); - context.attach( widgetElement, writer ); + attachTo( options ) { + const resizer = new Resizer( options ); - this.editor.editing.view.once( 'render', () => context.redraw() ); + resizer.attach(); - this.contexts.push( context ); + this.editor.editing.view.once( 'render', () => resizer.redraw() ); - return context; + this.resizers.push( resizer ); + + return resizer; } /** @@ -162,7 +119,7 @@ export default class WidgetResizer extends Plugin { * @param {HTMLElement} domResizeWrapper */ _getContextByWrapper( domResizeWrapper ) { - for ( const context of this.contexts ) { + for ( const context of this.resizers ) { if ( domResizeWrapper.isSameNode( context.domResizeWrapper ) ) { return context; } @@ -182,18 +139,18 @@ export default class WidgetResizer extends Plugin { */ /** - * Function to explicitly point the resizing host. - * - * By default resizer will use widget wrapper, but it's possible to point any child within widget wrapper. - * - * ```js - * editor.plugins.get( 'WidgetResizer' ).apply( widget, conversionApi.writer, { - * getResizeHost( wrapper ) { - * return wrapper.querySelector( 'img' ); - * } - * } ); - * ``` - * + * @member {module:engine/model/element~Element} module:widget/widgetresizer~ResizerOptions#modelElement + */ + +/** + * @member {module:engine/view/containerelement~ContainerElement} module:widget/widgetresizer~ResizerOptions#viewElement + */ + +/** + * @member {module:engine/view/downcastwriter~DowncastWriter} module:widget/widgetresizer~ResizerOptions#downcastWriter + */ + +/** * @member {Function} module:widget/widgetresizer~ResizerOptions#getResizeHost */ @@ -202,14 +159,5 @@ export default class WidgetResizer extends Plugin { */ /** - * ```js - * editor.plugins.get( 'WidgetResizer' ).apply( widget, conversionApi.writer, { - * isCentered( context ) { - * const imageStyle = context._getModel( editor, context.widgetWrapperElement ).getAttribute( 'imageStyle' ); - * - * return !imageStyle || imageStyle == 'full'; - * } - * } ); - * ``` * @member {Function} module:widget/widgetresizer~ResizerOptions#isCentered */ From 0f09252361bfc43b5381336f22c94f86c832b4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 13 Aug 2019 11:35:57 +0200 Subject: [PATCH 72/93] Refactoring. --- src/resizer.js | 8 ++++++++ src/widgetresizer.js | 39 ++++++++++++--------------------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/resizer.js b/src/resizer.js index 34db44ae..370d527b 100644 --- a/src/resizer.js +++ b/src/resizer.js @@ -259,6 +259,14 @@ export default class Resizer { } } + containsHandle( domElement ) { + return this.domResizeWrapper.contains( domElement ); + } + + static isResizeHandle( domElement ) { + return domElement.classList.contains( 'ck-widget__resizer' ); + } + /** * Cleans up the context state. * diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 861e198d..1b536875 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -8,7 +8,6 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import Resizer from './resizer'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -51,18 +50,18 @@ export default class WidgetResizer extends Plugin { }, THROTTLE_THRESHOLD ); this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mousedown', ( event, domEventData ) => { - const target = domEventData.target; + if ( !Resizer.isResizeHandle( domEventData.target ) ) { + return; + } - const resizeHandler = isResizeHandler( target ) ? target : getAncestors( target ).filter( isResizeHandler )[ 0 ]; + const resizeHandle = domEventData.target; - if ( resizeHandler ) { - this._observers.mouseMove.listenTo( mouseObserverHost, 'mousemove', mouseMoveListener ); + this._observers.mouseMove.listenTo( mouseObserverHost, 'mousemove', mouseMoveListener ); - this.activeResizer = this._getContextByHandler( resizeHandler ); + this.activeResizer = this._getResizerByHandle( resizeHandle ); - if ( this.activeResizer ) { - this.activeResizer.begin( resizeHandler ); - } + if ( this.activeResizer ) { + this.activeResizer.begin( resizeHandle ); } } ); @@ -91,10 +90,6 @@ export default class WidgetResizer extends Plugin { // Resizers need to be redrawn upon window resize, because new window might shrink resize host. this._observers.windowResize.listenTo( global.window, 'resize', resizeContexts ); - - function isResizeHandler( element ) { - return element.classList && element.classList.contains( 'ck-widget__resizer' ); - } } /** @@ -113,23 +108,13 @@ export default class WidgetResizer extends Plugin { return resizer; } - /** - * Returns a resize context associated with given `domResizeWrapper`. - * - * @param {HTMLElement} domResizeWrapper - */ - _getContextByWrapper( domResizeWrapper ) { - for ( const context of this.resizers ) { - if ( domResizeWrapper.isSameNode( context.domResizeWrapper ) ) { - return context; + _getResizerByHandle( domResizeHandle ) { + for ( const resizer of this.resizers ) { + if ( resizer.containsHandle( domResizeHandle ) ) { + return resizer; } } } - - _getContextByHandler( domResizeHandler ) { - return this._getContextByWrapper( getAncestors( domResizeHandler ) - .filter( element => element.classList.contains( 'ck-widget__resizer-wrapper' ) )[ 0 ] ); - } } /** From f360a432ff83af31ce6fcb8920cef5f33711e1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 13 Aug 2019 11:48:30 +0200 Subject: [PATCH 73/93] Simplfied observers. --- src/widgetresizer.js | 46 ++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 1b536875..512b7853 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -30,34 +30,22 @@ export default class WidgetResizer extends Plugin { this.resizers = []; this.activeResizer = null; - const mouseObserverHost = global.window.document; + const domDocument = global.window.document; const THROTTLE_THRESHOLD = 16; // 16ms = ~60fps this.editor.model.schema.setAttributeProperties( 'width', { isFormatting: true } ); - this._observers = { - mouseMove: Object.create( DomEmitterMixin ), - mouseDownUp: Object.create( DomEmitterMixin ), - windowResize: Object.create( DomEmitterMixin ) - }; + this._observer = Object.create( DomEmitterMixin ); - const mouseMoveListener = throttle( ( event, domEventData ) => { - if ( this.activeResizer ) { - this.activeResizer.updateSize( domEventData ); - } - }, THROTTLE_THRESHOLD ); - - this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mousedown', ( event, domEventData ) => { + this._observer.listenTo( domDocument, 'mousedown', ( event, domEventData ) => { if ( !Resizer.isResizeHandle( domEventData.target ) ) { return; } const resizeHandle = domEventData.target; - this._observers.mouseMove.listenTo( mouseObserverHost, 'mousemove', mouseMoveListener ); - this.activeResizer = this._getResizerByHandle( resizeHandle ); if ( this.activeResizer ) { @@ -65,31 +53,35 @@ export default class WidgetResizer extends Plugin { } } ); - const finishResizing = () => { + this._observer.listenTo( domDocument, 'mousemove', throttle( ( event, domEventData ) => { if ( this.activeResizer ) { - this._observers.mouseMove.stopListening( mouseObserverHost, 'mousemove', mouseMoveListener ); + this.activeResizer.updateSize( domEventData ); + } + }, THROTTLE_THRESHOLD ) ); - if ( this.activeResizer ) { - this.activeResizer.commit( this.editor ); - } + this._observer.listenTo( domDocument, 'mouseup', () => { + if ( this.activeResizer ) { + this.activeResizer.commit( this.editor ); this.activeResizer = null; } - }; + } ); - const resizeContexts = throttle( () => { + const redrawResizers = throttle( () => { for ( const context of this.resizers ) { context.redraw(); } }, THROTTLE_THRESHOLD ); - this._observers.mouseDownUp.listenTo( mouseObserverHost, 'mouseup', finishResizing ); - - // Redrawing on layout change fixes issue with browser window resize or undo causing a mispositioned resizer. - this.editor.editing.view.document.on( 'layoutChanged', resizeContexts ); + // Redrawing on any change of the UI of the editor (including content changes). + this.editor.ui.on( 'update', redrawResizers ); // Resizers need to be redrawn upon window resize, because new window might shrink resize host. - this._observers.windowResize.listenTo( global.window, 'resize', resizeContexts ); + this._observer.listenTo( global.window, 'resize', redrawResizers ); + } + + destroy() { + this._observer.stopListening(); } /** From 45df16740ae959a52742fe38cceb4eecf43b844f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 13 Aug 2019 13:07:16 +0200 Subject: [PATCH 74/93] Refactoring. --- src/resizer.js | 256 +++++++++++++++++++++++-------------------------- 1 file changed, 121 insertions(+), 135 deletions(-) diff --git a/src/resizer.js b/src/resizer.js index 370d527b..a9cff357 100644 --- a/src/resizer.js +++ b/src/resizer.js @@ -24,45 +24,78 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; */ export default class Resizer { /** - * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. + * @param {module:widget/widgetresizer~ResizerOptions} options Resizer options. */ constructor( options ) { /** - * Container of entire resize UI. + * The size of resize host before current resize process. * - * Note that this property is initialized only after the element bound with resizer is drawn - * so it will be a `null` when uninitialized. + * This information is only known after DOM was rendered, so it will be updated later. + * + * It contains an object with `width` and `height` properties. * - * @type {HTMLElement|null} #domResizeWrapper + * @type {Object} */ - this.domResizeWrapper = null; + this.originalSize = null; /** - * The size of resize host before current resize process. + * Position of the handle that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` + * if unknown. * - * This information is only known after DOM was rendered, so it will be updated later. + * @readonly + * @observable + * @member {String|null} #activeHandlePosition */ - this.originalSize = { - x: 0, - y: 0 - }; + this.set( 'activeHandlePosition', null ); /** + * Width proposed (but not yet accepted) using the widget resizer. + * + * It goes back to `null` once the resizer is dismissed or accepted. + * + * @readonly + * @observable + * @member {Number|null} #proposedWidth + */ + this.set( 'proposedWidth', null ); + + /** + * Height proposed (but not yet accepted) using the widget resizer. + * + * It goes back to `null` once the resizer is dismissed or accepted. + * + * @readonly + * @observable + * @member {Number|null} #proposedHeight + */ + this.set( 'proposedHeight', null ); + + /** + * @private * @type {module:widget/widgetresizer~ResizerOptions} */ - this._options = options || {}; + this._options = options; + + /** + * Container of the entire resize UI. + * + * Note that this property is initialized only after the element bound with the resizer is drawn + * so it will be a `null` when uninitialized. + * + * @private + * @type {HTMLElement|null} + */ + this._domResizeWrapper = null; /** * Reference point of resizer where the dragging started. It is used to measure the distance to user cursor * traveled, thus how much the image should be enlarged. * This information is only known after DOM was rendered, so it will be updated later. * - * @protected + * @private + * @type {Object} */ - this._referenceCoordinates = { - y: 0, - x: 0 - }; + this._referenceCoordinates = null; /** * View to a wrapper containing all the resizer-related views. @@ -78,41 +111,10 @@ export default class Resizer { */ this._domResizeShadow = null; - this._cleanupContext(); - this.decorate( 'begin' ); this.decorate( 'cancel' ); this.decorate( 'commit' ); this.decorate( 'updateSize' ); - - /** - * Width proposed (but not yet accepted) using the widget resizer. - * - * It goes back to `null` once the resizer is dismissed or accepted. - * - * @readonly - * @observable - * @member {Number|null} #proposedX - */ - - /** - * Height proposed (but not yet accepted) using the widget resizer. - * - * It goes back to `null` once the resizer is dismissed or accepted. - * - * @readonly - * @observable - * @member {Number|null} #proposedY - */ - - /** - * Direction towards which the widget is being resized, e.g. `"top-left"`, `"bottom-right"` etc or `null` - * if not active. - * - * @readonly - * @observable - * @member {String|null} #orientation - */ } /** @@ -132,39 +134,27 @@ export default class Resizer { that._appendResizers( that._domResizeShadow ); that._appendSizeUi( that._domResizeShadow ); - that.domResizeWrapper = domElement; + that._domResizeWrapper = domElement; return domElement; } ); // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( viewElement, viewElement.childCount ), this._resizeWrapperElement ); + writer.insert( writer.createPositionAt( viewElement, 'end' ), this._resizeWrapperElement ); writer.addClass( [ 'ck-widget_with-resizer' ], viewElement ); } /** * - * @param {HTMLElement} domResizeHandler Handler used to calculate reference point. + * @param {HTMLElement} domResizeHandle The handle used to calculate the reference point. */ - begin( domResizeHandler ) { + begin( domResizeHandle ) { const resizeHost = this._getResizeHost(); const clientRect = new Rect( resizeHost ); - this._domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); - - /** - * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` - * if unknown. - * - * @member {String|null} - */ - this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler ); - - this.set( 'orientation', this.referenceHandlerPosition ); - - const reversedPosition = this._invertPosition( this.referenceHandlerPosition ); + this.activeHandlePosition = this._getHandlePosition( domResizeHandle ); - this._referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition ); + this._referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, getOppositePosition( this.activeHandlePosition ) ); this.originalSize = { width: clientRect.width, @@ -174,6 +164,8 @@ export default class Resizer { this.aspectRatio = this._options.getAspectRatio ? this._options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; + this._domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); + this.redraw(); } @@ -211,26 +203,28 @@ export default class Resizer { } /** - * Method used to calculate the proposed size as the resize handlers are dragged. + * Method used to calculate the proposed size as the resize handles are dragged. * - * Proposed size can also be observed with {@link #proposedX} and {@link #proposedY} properties. + * Proposed size can also be observed with {@link #proposedWidth} and {@link #proposedHeight} properties. * * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. */ updateSize( domEventData ) { - const proposedSize = this._updateImageSize( domEventData ); + this._updateResizeHostSize( domEventData ); + + // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it + // here will reflect this limitation on resizer shadow later on. + const realSize = this._getRealResizeHostSize(); - this.domResizeWrapper.style.width = proposedSize.x + 'px'; - this.domResizeWrapper.style.height = proposedSize.y + 'px'; + this._domResizeWrapper.style.width = realSize.width + 'px'; + this._domResizeWrapper.style.height = realSize.height + 'px'; - this.set( { - proposedX: proposedSize.x, - proposedY: proposedSize.y - } ); + this.proposedWidth = realSize.width; + this.proposedHeight = realSize.height; } redraw() { - const domWrapper = this.domResizeWrapper; + const domWrapper = this._domResizeWrapper; if ( existsInDom( domWrapper ) ) { // Refresh only if resizer exists in the DOM. @@ -260,7 +254,7 @@ export default class Resizer { } containsHandle( domElement ) { - return this.domResizeWrapper.contains( domElement ); + return this._domResizeWrapper.contains( domElement ); } static isResizeHandle( domElement ) { @@ -273,13 +267,9 @@ export default class Resizer { * @protected */ _cleanupContext() { - this.referenceHandlerPosition = null; - - this.set( { - proposedX: null, - proposedY: null, - orientation: null - } ); + this.activeHandlePosition = null; + this.proposedWidth = null; + this.proposedHeight = null; } /** @@ -295,7 +285,7 @@ export default class Resizer { * @returns {HTMLElement} */ _getResizeHost() { - const widgetWrapper = this.domResizeWrapper.parentElement; + const widgetWrapper = this._domResizeWrapper.parentElement; return this._options.getResizeHost ? this._options.getResizeHost( widgetWrapper ) : widgetWrapper; @@ -309,23 +299,6 @@ export default class Resizer { this._domResizeShadow.removeAttribute( 'style' ); } - /** - * @protected - * @param {String} position Like `"top-left"`. - * @returns {String} Inverted `position`, e.g. returns `"bottom-right"` if `"top-left"` was given as `position`. - */ - _invertPosition( position ) { - const parts = position.split( '-' ); - const replacements = { - top: 'bottom', - bottom: 'top', - left: 'right', - right: 'left' - }; - - return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; - } - /** * @private * @param {HTMLElement} domElement The outer wrapper of resize UI within a given widget. @@ -344,7 +317,7 @@ export default class Resizer { } /** - * Renders the resize handlers in DOM. + * Renders the resize handles in DOM. * * @private * @param {HTMLElement} domElement Resize shadow where the resizers should be appended to. @@ -369,24 +342,22 @@ export default class Resizer { _appendSizeUi( domElement ) { const sizeUi = new SizeView(); - sizeUi.bind( 'isVisible' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) => + sizeUi.bind( 'isVisible' ).to( this, 'proposedWidth', this, 'proposedHeight', ( x, y ) => x !== null && y !== null ); - sizeUi.bind( 'label' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) => + sizeUi.bind( 'label' ).to( this, 'proposedWidth', this, 'proposedHeight', ( x, y ) => `${ Math.round( x ) }x${ Math.round( y ) }` ); - sizeUi.bind( 'orientation' ).to( this ); + sizeUi.bind( 'activeHandlePosition' ).to( this ); // Make sure icon#element is rendered before passing to appendChild(). sizeUi.render(); - this.sizeElement = sizeUi.element; - - domElement.appendChild( this.sizeElement ); + domElement.appendChild( sizeUi.element ); } /** - * Method used to calculate the proposed size as the resize handlers are dragged. + * Method used to calculate the proposed size as the resize handles are dragged. * * @private * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. @@ -394,7 +365,7 @@ export default class Resizer { * @returns {Number} return.x Proposed width. * @returns {Number} return.y Proposed height. */ - _updateImageSize( domEventData ) { + _updateResizeHostSize( domEventData ) { const currentCoordinates = this._extractCoordinates( domEventData ); const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; const initialSize = this.originalSize; @@ -416,7 +387,7 @@ export default class Resizer { y: ( currentCoordinates.y - initialSize.height ) - this._referenceCoordinates.y }; - if ( isCentered && this.referenceHandlerPosition.endsWith( '-right' ) ) { + if ( isCentered && this.activeHandlePosition.endsWith( '-right' ) ) { enlargement.x = currentCoordinates.x - ( this._referenceCoordinates.x + initialSize.width ); } @@ -428,36 +399,35 @@ export default class Resizer { const resizeHost = this._getResizeHost(); + // The size proposed by the user. It does not consider the aspect ratio. const proposedSize = { - x: Math.abs( initialSize.width + enlargement.x ), - y: Math.abs( initialSize.height + enlargement.y ) + width: Math.abs( initialSize.width + enlargement.x ), + height: Math.abs( initialSize.height + enlargement.y ) }; // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.x / this.aspectRatio > proposedSize.y ? 'x' : 'y'; + proposedSize.dominant = proposedSize.width / this.aspectRatio > proposedSize.height ? 'width' : 'height'; proposedSize.max = proposedSize[ proposedSize.dominant ]; - const drawnSize = { - x: proposedSize.x, - y: proposedSize.y + // Proposed size, respecting the aspect ratio. + const targetSize = { + width: proposedSize.width, + height: proposedSize.height }; - if ( proposedSize.dominant == 'x' ) { - drawnSize.y = drawnSize.x / this.aspectRatio; + if ( proposedSize.dominant == 'width' ) { + targetSize.height = targetSize.width / this.aspectRatio; } else { - drawnSize.x = drawnSize.y * this.aspectRatio; + targetSize.width = targetSize.height * this.aspectRatio; } - resizeHost.style.width = `${ drawnSize.x }px`; - - // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it - // here will reflect this limitation on resizer shadow later on. - const latestRect = new Rect( resizeHost ); + resizeHost.style.width = `${ targetSize.width }px`; + } - drawnSize.x = latestRect.width; - drawnSize.y = latestRect.height; + _getRealResizeHostSize() { + const rect = new Rect( this._getResizeHost() ); - return drawnSize; + return { width: rect.width, height: rect.height }; } /** @@ -480,17 +450,17 @@ export default class Resizer { } /** - * Determines the position of a given resize handler. + * Determines the position of a given resize handle. * * @private - * @param {HTMLElement} domResizeHandler Handler used to calculate reference point. + * @param {HTMLElement} domHandle Handler used to calculate reference point. * @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched. */ - _getResizerPosition( domResizeHandler ) { + _getHandlePosition( domHandle ) { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; for ( const position of resizerPositions ) { - if ( domResizeHandler.classList.contains( this._getResizerClass( position ) ) ) { + if ( domHandle.classList.contains( this._getResizerClass( position ) ) ) { return position; } } @@ -511,7 +481,7 @@ class SizeView extends View { class: [ 'ck', 'ck-size-view', - bind.to( 'orientation', value => value ? `ck-orientation-${ value }` : '' ) + bind.to( 'activeHandlePosition', value => value ? `ck-orientation-${ value }` : '' ) ], style: { display: bind.if( 'isVisible', 'none', visible => !visible ) @@ -523,3 +493,19 @@ class SizeView extends View { } ); } } + +/** + * @param {String} position Like `"top-left"`. + * @returns {String} Inverted `position`, e.g. returns `"bottom-right"` if `"top-left"` was given as `position`. + */ +function getOppositePosition( position ) { + const parts = position.split( '-' ); + const replacements = { + top: 'bottom', + bottom: 'top', + left: 'right', + right: 'left' + }; + + return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; +} From 11415d9981a9b034ddf18f20998b4ab71807b9ec Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Tue, 13 Aug 2019 13:42:11 +0200 Subject: [PATCH 75/93] Intenral: Removed unused class. --- src/resizer.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/resizer.js b/src/resizer.js index a9cff357..66b90608 100644 --- a/src/resizer.js +++ b/src/resizer.js @@ -164,8 +164,6 @@ export default class Resizer { this.aspectRatio = this._options.getAspectRatio ? this._options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; - this._domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' ); - this.redraw(); } @@ -295,7 +293,6 @@ export default class Resizer { * @protected */ _dismissShadow() { - this._domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' ); this._domResizeShadow.removeAttribute( 'style' ); } From fdb76eefa2a42f1e14d918c022da23dcec5e2d25 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 14 Aug 2019 09:06:17 +0200 Subject: [PATCH 76/93] Internal: Extracted resizer state class. --- src/resizer.js | 238 ++++++++------------------------------------- src/resizestate.js | 229 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 197 deletions(-) create mode 100644 src/resizestate.js diff --git a/src/resizer.js b/src/resizer.js index 66b90608..2c7322d8 100644 --- a/src/resizer.js +++ b/src/resizer.js @@ -8,14 +8,12 @@ */ import View from '@ckeditor/ckeditor5-ui/src/view'; -import { - getAbsoluteBoundaryPoint -} from './utils'; import Template from '@ckeditor/ckeditor5-ui/src/template'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import ResizeState from './resizestate'; /** * Stores the internal state of a single resizable object. @@ -38,38 +36,6 @@ export default class Resizer { */ this.originalSize = null; - /** - * Position of the handle that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` - * if unknown. - * - * @readonly - * @observable - * @member {String|null} #activeHandlePosition - */ - this.set( 'activeHandlePosition', null ); - - /** - * Width proposed (but not yet accepted) using the widget resizer. - * - * It goes back to `null` once the resizer is dismissed or accepted. - * - * @readonly - * @observable - * @member {Number|null} #proposedWidth - */ - this.set( 'proposedWidth', null ); - - /** - * Height proposed (but not yet accepted) using the widget resizer. - * - * It goes back to `null` once the resizer is dismissed or accepted. - * - * @readonly - * @observable - * @member {Number|null} #proposedHeight - */ - this.set( 'proposedHeight', null ); - /** * @private * @type {module:widget/widgetresizer~ResizerOptions} @@ -144,34 +110,29 @@ export default class Resizer { writer.addClass( [ 'ck-widget_with-resizer' ], viewElement ); } - /** - * - * @param {HTMLElement} domResizeHandle The handle used to calculate the reference point. - */ begin( domResizeHandle ) { - const resizeHost = this._getResizeHost(); - const clientRect = new Rect( resizeHost ); + this.state = new ResizeState( this._options ); + // this._bindSizeUi( this.state ); + this.sizeUi.bindResizer( this.state ); + this.state.begin( domResizeHandle, this._getResizeHost() ); - this.activeHandlePosition = this._getHandlePosition( domResizeHandle ); + this.redraw(); + } - this._referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, getOppositePosition( this.activeHandlePosition ) ); + updateSize( domEventData ) { + const resizeHost = this._getResizeHost(); + const newSize = this.state.proposeNewSize( domEventData ); - this.originalSize = { - width: clientRect.width, - height: clientRect.height - }; + resizeHost.style.width = `${ newSize.width }px`; - this.aspectRatio = this._options.getAspectRatio ? - this._options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; + this.state.fetchSizeFromElement( this._getResizeHost() ); - this.redraw(); + // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it + // here will reflect this limitation on resizer shadow later on. + this._domResizeWrapper.style.width = this.state.proposedWidth + 'px'; + this._domResizeWrapper.style.height = this.state.proposedHeight + 'px'; } - /** - * Accepts currently proposed resize and applies it on the resize host. - * - * @param {module:core/editor/editor~Editor} editor - */ commit( editor ) { const modelElement = this._options.modelElement; const newWidth = this._domResizeShadow.clientWidth; @@ -192,7 +153,6 @@ export default class Resizer { */ cancel() { this._dismissShadow(); - this._cleanupContext(); } @@ -200,27 +160,6 @@ export default class Resizer { this.cancel(); } - /** - * Method used to calculate the proposed size as the resize handles are dragged. - * - * Proposed size can also be observed with {@link #proposedWidth} and {@link #proposedHeight} properties. - * - * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. - */ - updateSize( domEventData ) { - this._updateResizeHostSize( domEventData ); - - // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it - // here will reflect this limitation on resizer shadow later on. - const realSize = this._getRealResizeHostSize(); - - this._domResizeWrapper.style.width = realSize.width + 'px'; - this._domResizeWrapper.style.height = realSize.height + 'px'; - - this.proposedWidth = realSize.width; - this.proposedHeight = realSize.height; - } - redraw() { const domWrapper = this._domResizeWrapper; @@ -265,9 +204,8 @@ export default class Resizer { * @protected */ _cleanupContext() { - this.activeHandlePosition = null; - this.proposedWidth = null; - this.proposedHeight = null; + this.sizeUi.dismiss(); + this.sizeUi.isVisible = false; } /** @@ -326,7 +264,7 @@ export default class Resizer { domElement.appendChild( ( new Template( { tag: 'div', attributes: { - class: `ck-widget__resizer ${ this._getResizerClass( currentPosition ) }` + class: `ck-widget__resizer ${ getResizerClass( currentPosition ) }` } } ).render() ) ); } @@ -339,111 +277,12 @@ export default class Resizer { _appendSizeUi( domElement ) { const sizeUi = new SizeView(); - sizeUi.bind( 'isVisible' ).to( this, 'proposedWidth', this, 'proposedHeight', ( x, y ) => - x !== null && y !== null ); - - sizeUi.bind( 'label' ).to( this, 'proposedWidth', this, 'proposedHeight', ( x, y ) => - `${ Math.round( x ) }x${ Math.round( y ) }` ); - - sizeUi.bind( 'activeHandlePosition' ).to( this ); - // Make sure icon#element is rendered before passing to appendChild(). sizeUi.render(); - domElement.appendChild( sizeUi.element ); - } - - /** - * Method used to calculate the proposed size as the resize handles are dragged. - * - * @private - * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. - * @returns {Object} return - * @returns {Number} return.x Proposed width. - * @returns {Number} return.y Proposed height. - */ - _updateResizeHostSize( domEventData ) { - const currentCoordinates = this._extractCoordinates( domEventData ); - const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; - const initialSize = this.originalSize; - - // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number - // meaning that it has been shrunk. - // - // +----------------+--+ - // | | | - // | img | | - // | | | - // +----------------+ | ^ - // | | | - enlarge y - // +-------------------+ v - // <--> - // enlarge x - const enlargement = { - x: this._referenceCoordinates.x - ( currentCoordinates.x + initialSize.width ), - y: ( currentCoordinates.y - initialSize.height ) - this._referenceCoordinates.y - }; - - if ( isCentered && this.activeHandlePosition.endsWith( '-right' ) ) { - enlargement.x = currentCoordinates.x - ( this._referenceCoordinates.x + initialSize.width ); - } - - // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from - // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. - if ( isCentered ) { - enlargement.x *= 2; - } - - const resizeHost = this._getResizeHost(); - - // The size proposed by the user. It does not consider the aspect ratio. - const proposedSize = { - width: Math.abs( initialSize.width + enlargement.x ), - height: Math.abs( initialSize.height + enlargement.y ) - }; - - // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.width / this.aspectRatio > proposedSize.height ? 'width' : 'height'; - proposedSize.max = proposedSize[ proposedSize.dominant ]; - - // Proposed size, respecting the aspect ratio. - const targetSize = { - width: proposedSize.width, - height: proposedSize.height - }; - - if ( proposedSize.dominant == 'width' ) { - targetSize.height = targetSize.width / this.aspectRatio; - } else { - targetSize.width = targetSize.height * this.aspectRatio; - } - - resizeHost.style.width = `${ targetSize.width }px`; - } - - _getRealResizeHostSize() { - const rect = new Rect( this._getResizeHost() ); + this.sizeUi = sizeUi; - return { width: rect.width, height: rect.height }; - } - - /** - * @private - */ - _extractCoordinates( event ) { - return { - x: event.pageX, - y: event.pageY - }; - } - - /** - * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. - * @returns {String} A prefixed HTML class name for the resizer element - * @private - */ - _getResizerClass( resizerPosition ) { - return `ck-widget__resizer-${ resizerPosition }`; + domElement.appendChild( sizeUi.element ); } /** @@ -457,7 +296,7 @@ export default class Resizer { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; for ( const position of resizerPositions ) { - if ( domHandle.classList.contains( this._getResizerClass( position ) ) ) { + if ( domHandle.classList.contains( getResizerClass( position ) ) ) { return position; } } @@ -489,20 +328,25 @@ class SizeView extends View { } ] } ); } + + bindResizer( observable ) { + this.bind( 'isVisible' ).to( observable, 'proposedWidth', observable, 'proposedHeight', ( x, y ) => + x !== null && y !== null ); + + this.bind( 'label' ).to( observable, 'proposedWidth', observable, 'proposedHeight', ( x, y ) => + `${ Math.round( x ) }x${ Math.round( y ) }` ); + + this.bind( 'activeHandlePosition' ).to( observable ); + } + + dismiss() { + this.unbind(); + this.isVisible = false; + } } -/** - * @param {String} position Like `"top-left"`. - * @returns {String} Inverted `position`, e.g. returns `"bottom-right"` if `"top-left"` was given as `position`. - */ -function getOppositePosition( position ) { - const parts = position.split( '-' ); - const replacements = { - top: 'bottom', - bottom: 'top', - left: 'right', - right: 'left' - }; - - return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; +// @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. +// @returns {String} A prefixed HTML class name for the resizer element +function getResizerClass( resizerPosition ) { + return `ck-widget__resizer-${ resizerPosition }`; } diff --git a/src/resizestate.js b/src/resizestate.js new file mode 100644 index 00000000..5176c6be --- /dev/null +++ b/src/resizestate.js @@ -0,0 +1,229 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module widget/resizer + */ +import { + getAbsoluteBoundaryPoint +} from './utils'; +import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; + +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +/** + * Stores the internal state of a single resizable object. + * + * @class ResizeState + */ +export default class ResizeState { + /** + * @param {module:widget/widgetresizer~ResizerOptions} options Resizer options. + */ + constructor( options ) { + /** + * The size of resize host before current resize process. + * + * This information is only known after DOM was rendered, so it will be updated later. + * + * It contains an object with `width` and `height` properties. + * + * @type {Object} + */ + this.originalSize = null; + + /** + * Position of the handle that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` + * if unknown. + * + * @readonly + * @observable + * @member {String|null} #activeHandlePosition + */ + this.set( 'activeHandlePosition', null ); + + /** + * Width proposed (but not yet accepted) using the widget resizer. + * + * It goes back to `null` once the resizer is dismissed or accepted. + * + * @readonly + * @observable + * @member {Number|null} #proposedWidth + */ + this.set( 'proposedWidth', null ); + + /** + * Height proposed (but not yet accepted) using the widget resizer. + * + * It goes back to `null` once the resizer is dismissed or accepted. + * + * @readonly + * @observable + * @member {Number|null} #proposedHeight + */ + this.set( 'proposedHeight', null ); + + /** + * @private + * @type {module:widget/widgetresizer~ResizerOptions} + */ + this._options = options; + } + + /** + * + * @param {HTMLElement} domResizeHandle The handle used to calculate the reference point. + */ + begin( domResizeHandle, resizeHost ) { + const clientRect = new Rect( resizeHost ); + + this.activeHandlePosition = getHandlePosition( domResizeHandle ); + + this._referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, getOppositePosition( this.activeHandlePosition ) ); + + this.originalSize = { + width: clientRect.width, + height: clientRect.height + }; + + this.aspectRatio = this._options.getAspectRatio ? + this._options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; + } + + /** + * Sets `proposedWidth` / `proposedHeight` properties based on provided element. + * + * @param {HTMLElement} domElement + */ + fetchSizeFromElement( domElement ) { + const rect = new Rect( domElement ); + + this.proposedWidth = rect.width; + this.proposedHeight = rect.height; + } + + /** + * Method used to calculate the proposed size as the resize handles are dragged. + * + * @private + * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. + * @returns {Object} return + * @returns {Number} return.x Proposed width. + * @returns {Number} return.y Proposed height. + */ + proposeNewSize( domEventData ) { + const currentCoordinates = extractCoordinates( domEventData ); + const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; + const originalSize = this.originalSize; + + // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number + // meaning that it has been shrunk. + // + // +----------------+--+ + // | | | + // | img | | + // | | | + // +----------------+ | ^ + // | | | - enlarge y + // +-------------------+ v + // <--> + // enlarge x + const enlargement = { + x: this._referenceCoordinates.x - ( currentCoordinates.x + originalSize.width ), + y: ( currentCoordinates.y - originalSize.height ) - this._referenceCoordinates.y + }; + + if ( isCentered && this.activeHandlePosition.endsWith( '-right' ) ) { + enlargement.x = currentCoordinates.x - ( this._referenceCoordinates.x + originalSize.width ); + } + + // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from + // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. + if ( isCentered ) { + enlargement.x *= 2; + } + + // const resizeHost = this._getResizeHost(); + + // The size proposed by the user. It does not consider the aspect ratio. + const proposedSize = { + width: Math.abs( originalSize.width + enlargement.x ), + height: Math.abs( originalSize.height + enlargement.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.width / this.aspectRatio > proposedSize.height ? 'width' : 'height'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + // Proposed size, respecting the aspect ratio. + const targetSize = { + width: proposedSize.width, + height: proposedSize.height + }; + + if ( proposedSize.dominant == 'width' ) { + targetSize.height = targetSize.width / this.aspectRatio; + } else { + targetSize.width = targetSize.height * this.aspectRatio; + } + + // resizeHost.style.width = `${ targetSize.width }px`; + + return targetSize; + } +} + +/** + * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. + * @returns {String} A prefixed HTML class name for the resizer element + * @private + */ +function getResizerClass( resizerPosition ) { + return `ck-widget__resizer-${ resizerPosition }`; +} + +/** + * Determines the position of a given resize handle. + * + * @private + * @param {HTMLElement} domHandle Handler used to calculate reference point. + * @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched. + */ +function getHandlePosition( domHandle ) { + const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; + + for ( const position of resizerPositions ) { + if ( domHandle.classList.contains( getResizerClass( position ) ) ) { + return position; + } + } +} + +/** + * @param {String} position Like `"top-left"`. + * @returns {String} Inverted `position`, e.g. returns `"bottom-right"` if `"top-left"` was given as `position`. + */ +function getOppositePosition( position ) { + const parts = position.split( '-' ); + const replacements = { + top: 'bottom', + bottom: 'top', + left: 'right', + right: 'left' + }; + + return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; +} + +function extractCoordinates( event ) { + return { + x: event.pageX, + y: event.pageY + }; +} + +mix( ResizeState, ObservableMixin ); From 293dfc76a9f95f171f1d29c23646e5aef726dccf Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Wed, 14 Aug 2019 09:26:57 +0200 Subject: [PATCH 77/93] Internal: Removed unused variables. --- src/resizer.js | 19 ++++--------------- src/resizestate.js | 2 -- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/resizer.js b/src/resizer.js index 2c7322d8..a79f0ac1 100644 --- a/src/resizer.js +++ b/src/resizer.js @@ -25,17 +25,6 @@ export default class Resizer { * @param {module:widget/widgetresizer~ResizerOptions} options Resizer options. */ constructor( options ) { - /** - * The size of resize host before current resize process. - * - * This information is only known after DOM was rendered, so it will be updated later. - * - * It contains an object with `width` and `height` properties. - * - * @type {Object} - */ - this.originalSize = null; - /** * @private * @type {module:widget/widgetresizer~ResizerOptions} @@ -112,8 +101,8 @@ export default class Resizer { begin( domResizeHandle ) { this.state = new ResizeState( this._options ); - // this._bindSizeUi( this.state ); this.sizeUi.bindResizer( this.state ); + this.state.begin( domResizeHandle, this._getResizeHost() ); this.redraw(); @@ -145,7 +134,7 @@ export default class Resizer { writer.setAttribute( 'width', newWidth + 'px', modelElement ); } ); - this._cleanupContext(); + this._cleanup(); } /** @@ -153,7 +142,7 @@ export default class Resizer { */ cancel() { this._dismissShadow(); - this._cleanupContext(); + this._cleanup(); } destroy() { @@ -203,7 +192,7 @@ export default class Resizer { * * @protected */ - _cleanupContext() { + _cleanup() { this.sizeUi.dismiss(); this.sizeUi.isVisible = false; } diff --git a/src/resizestate.js b/src/resizestate.js index 5176c6be..e7de79b7 100644 --- a/src/resizestate.js +++ b/src/resizestate.js @@ -171,8 +171,6 @@ export default class ResizeState { targetSize.width = targetSize.height * this.aspectRatio; } - // resizeHost.style.width = `${ targetSize.width }px`; - return targetSize; } } From 310661bdbf77e6da1562434c6f4aed6e839c2090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 09:41:47 +0200 Subject: [PATCH 78/93] Move things around. --- src/utils.js | 24 ------------ src/widgetresizer.js | 2 +- src/{ => widgetresizer}/resizer.js | 5 ++- .../resizerstate.js} | 37 +++++++++++++++---- 4 files changed, 33 insertions(+), 35 deletions(-) rename src/{ => widgetresizer}/resizer.js (99%) rename src/{resizestate.js => widgetresizer/resizerstate.js} (88%) diff --git a/src/utils.js b/src/utils.js index a23513ec..643a7c13 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,7 +9,6 @@ import HighlightStack from './highlightstack'; import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview'; -import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import env from '@ckeditor/ckeditor5-utils/src/env'; import dragHandlerIcon from '../theme/icons/drag-handler.svg'; @@ -349,29 +348,6 @@ export function viewToModelPositionOutsideModelElement( model, viewElementMatche }; } -/** - * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. - * - * @param {HTMLElement} element - * @param {String} resizerPosition Position of the resize handler, e.g. `"top-left"`, `"bottom-right"`. - * @returns {Object} return - * @returns {Number} return.x - * @returns {Number} return.y - */ -export function getAbsoluteBoundaryPoint( element, resizerPosition ) { - const elementRect = new Rect( element ); - const positionParts = resizerPosition.split( '-' ); - const ret = { - x: positionParts[ 1 ] == 'right' ? elementRect.right : elementRect.left, - y: positionParts[ 0 ] == 'bottom' ? elementRect.bottom : elementRect.top - }; - - ret.x += element.ownerDocument.defaultView.scrollX; - ret.y += element.ownerDocument.defaultView.scrollY; - - return ret; -} - // Default filler offset function applied to all widget elements. // // @returns {null} diff --git a/src/widgetresizer.js b/src/widgetresizer.js index 512b7853..68c3c8b8 100644 --- a/src/widgetresizer.js +++ b/src/widgetresizer.js @@ -8,7 +8,7 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import Resizer from './resizer'; +import Resizer from './widgetresizer/resizer'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { throttle } from 'lodash-es'; diff --git a/src/resizer.js b/src/widgetresizer/resizer.js similarity index 99% rename from src/resizer.js rename to src/widgetresizer/resizer.js index a79f0ac1..f51208d4 100644 --- a/src/resizer.js +++ b/src/widgetresizer/resizer.js @@ -4,7 +4,7 @@ */ /** - * @module widget/resizer + * @module widget/widgetresizer/resizer */ import View from '@ckeditor/ckeditor5-ui/src/view'; @@ -13,7 +13,8 @@ import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import ResizeState from './resizestate'; + +import ResizeState from './resizerstate'; /** * Stores the internal state of a single resizable object. diff --git a/src/resizestate.js b/src/widgetresizer/resizerstate.js similarity index 88% rename from src/resizestate.js rename to src/widgetresizer/resizerstate.js index e7de79b7..af8973f8 100644 --- a/src/resizestate.js +++ b/src/widgetresizer/resizerstate.js @@ -4,11 +4,9 @@ */ /** - * @module widget/resizer + * @module widget/widgetresizer/resizerstate */ -import { - getAbsoluteBoundaryPoint -} from './utils'; + import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; @@ -109,7 +107,6 @@ export default class ResizeState { /** * Method used to calculate the proposed size as the resize handles are dragged. * - * @private * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. * @returns {Object} return * @returns {Number} return.x Proposed width. @@ -175,10 +172,36 @@ export default class ResizeState { } } +mix( ResizeState, ObservableMixin ); + +/** + * Returns coordinates of top-left corner of a element, relative to the document's top-left corner. + * + * @private + * @param {HTMLElement} element + * @param {String} resizerPosition Position of the resize handler, e.g. `"top-left"`, `"bottom-right"`. + * @returns {Object} return + * @returns {Number} return.x + * @returns {Number} return.y + */ +function getAbsoluteBoundaryPoint( element, resizerPosition ) { + const elementRect = new Rect( element ); + const positionParts = resizerPosition.split( '-' ); + const ret = { + x: positionParts[ 1 ] == 'right' ? elementRect.right : elementRect.left, + y: positionParts[ 0 ] == 'bottom' ? elementRect.bottom : elementRect.top + }; + + ret.x += element.ownerDocument.defaultView.scrollX; + ret.y += element.ownerDocument.defaultView.scrollY; + + return ret; +} + /** + * @private * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. * @returns {String} A prefixed HTML class name for the resizer element - * @private */ function getResizerClass( resizerPosition ) { return `ck-widget__resizer-${ resizerPosition }`; @@ -223,5 +246,3 @@ function extractCoordinates( event ) { y: event.pageY }; } - -mix( ResizeState, ObservableMixin ); From b0d9081544b0bd2120b41e528355e4414ba091af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 09:59:08 +0200 Subject: [PATCH 79/93] Moar refactoring. --- src/widgetresizer/resizer.js | 108 +++++++++++++++++++++++++----- src/widgetresizer/resizerstate.js | 95 ++++++-------------------- 2 files changed, 109 insertions(+), 94 deletions(-) diff --git a/src/widgetresizer/resizer.js b/src/widgetresizer/resizer.js index f51208d4..d9cd7e04 100644 --- a/src/widgetresizer/resizer.js +++ b/src/widgetresizer/resizer.js @@ -26,6 +26,11 @@ export default class Resizer { * @param {module:widget/widgetresizer~ResizerOptions} options Resizer options. */ constructor( options ) { + /** + * @readonly + * @member {module:widget/widgetresizer/resizerstate~ResizerState} #state + */ + /** * @private * @type {module:widget/widgetresizer~ResizerOptions} @@ -43,16 +48,6 @@ export default class Resizer { */ this._domResizeWrapper = null; - /** - * Reference point of resizer where the dragging started. It is used to measure the distance to user cursor - * traveled, thus how much the image should be enlarged. - * This information is only known after DOM was rendered, so it will be updated later. - * - * @private - * @type {Object} - */ - this._referenceCoordinates = null; - /** * View to a wrapper containing all the resizer-related views. * @@ -102,7 +97,8 @@ export default class Resizer { begin( domResizeHandle ) { this.state = new ResizeState( this._options ); - this.sizeUi.bindResizer( this.state ); + + this.sizeUi.bindToState( this.state ); this.state.begin( domResizeHandle, this._getResizeHost() ); @@ -111,11 +107,11 @@ export default class Resizer { updateSize( domEventData ) { const resizeHost = this._getResizeHost(); - const newSize = this.state.proposeNewSize( domEventData ); + const newSize = this._proposeNewSize( domEventData ); - resizeHost.style.width = `${ newSize.width }px`; + resizeHost.style.width = newSize.width + 'px'; - this.state.fetchSizeFromElement( this._getResizeHost() ); + this.state.fetchSizeFromElement( resizeHost ); // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it // here will reflect this limitation on resizer shadow later on. @@ -198,6 +194,75 @@ export default class Resizer { this.sizeUi.isVisible = false; } + /** + * Method used to calculate the proposed size as the resize handles are dragged. + * + * @private + * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. + * @returns {Object} return + * @returns {Number} return.x Proposed width. + * @returns {Number} return.y Proposed height. + */ + _proposeNewSize( domEventData ) { + const state = this.state; + const currentCoordinates = extractCoordinates( domEventData ); + const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; + const originalSize = state.originalSize; + + // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number + // meaning that it has been shrunk. + // + // +----------------+--+ + // | | | + // | img | | + // | | | + // +----------------+ | ^ + // | | | - enlarge y + // +-------------------+ v + // <--> + // enlarge x + const enlargement = { + x: state._referenceCoordinates.x - ( currentCoordinates.x + originalSize.width ), + y: ( currentCoordinates.y - originalSize.height ) - state._referenceCoordinates.y + }; + + if ( isCentered && state.activeHandlePosition.endsWith( '-right' ) ) { + enlargement.x = currentCoordinates.x - ( state._referenceCoordinates.x + originalSize.width ); + } + + // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from + // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. + if ( isCentered ) { + enlargement.x *= 2; + } + + // const resizeHost = this._getResizeHost(); + + // The size proposed by the user. It does not consider the aspect ratio. + const proposedSize = { + width: Math.abs( originalSize.width + enlargement.x ), + height: Math.abs( originalSize.height + enlargement.y ) + }; + + // Dominant determination must take the ratio into account. + proposedSize.dominant = proposedSize.width / state.aspectRatio > proposedSize.height ? 'width' : 'height'; + proposedSize.max = proposedSize[ proposedSize.dominant ]; + + // Proposed size, respecting the aspect ratio. + const targetSize = { + width: proposedSize.width, + height: proposedSize.height + }; + + if ( proposedSize.dominant == 'width' ) { + targetSize.height = targetSize.width / state.aspectRatio; + } else { + targetSize.width = targetSize.height * state.aspectRatio; + } + + return targetSize; + } + /** * Method used to obtain the resize host. * @@ -319,14 +384,14 @@ class SizeView extends View { } ); } - bindResizer( observable ) { - this.bind( 'isVisible' ).to( observable, 'proposedWidth', observable, 'proposedHeight', ( x, y ) => + bindToState( resizerState ) { + this.bind( 'isVisible' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( x, y ) => x !== null && y !== null ); - this.bind( 'label' ).to( observable, 'proposedWidth', observable, 'proposedHeight', ( x, y ) => + this.bind( 'label' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( x, y ) => `${ Math.round( x ) }x${ Math.round( y ) }` ); - this.bind( 'activeHandlePosition' ).to( observable ); + this.bind( 'activeHandlePosition' ).to( resizerState ); } dismiss() { @@ -340,3 +405,10 @@ class SizeView extends View { function getResizerClass( resizerPosition ) { return `ck-widget__resizer-${ resizerPosition }`; } + +function extractCoordinates( event ) { + return { + x: event.pageX, + y: event.pageY + }; +} diff --git a/src/widgetresizer/resizerstate.js b/src/widgetresizer/resizerstate.js index af8973f8..36e589ea 100644 --- a/src/widgetresizer/resizerstate.js +++ b/src/widgetresizer/resizerstate.js @@ -29,9 +29,9 @@ export default class ResizeState { * * It contains an object with `width` and `height` properties. * - * @type {Object} + * @readonly + * @member {Object} #originalSize */ - this.originalSize = null; /** * Position of the handle that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null` @@ -65,11 +65,28 @@ export default class ResizeState { */ this.set( 'proposedHeight', null ); + /** + * TODO + * + * @readonly + * @member {Number} #aspectRatio + */ + /** * @private * @type {module:widget/widgetresizer~ResizerOptions} */ this._options = options; + + /** + * Reference point of resizer where the dragging started. It is used to measure the distance to user cursor + * traveled, thus how much the image should be enlarged. + * This information is only known after DOM was rendered, so it will be updated later. + * + * @private + * @type {Object} + */ + this._referenceCoordinates = null; } /** @@ -103,73 +120,6 @@ export default class ResizeState { this.proposedWidth = rect.width; this.proposedHeight = rect.height; } - - /** - * Method used to calculate the proposed size as the resize handles are dragged. - * - * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. - * @returns {Object} return - * @returns {Number} return.x Proposed width. - * @returns {Number} return.y Proposed height. - */ - proposeNewSize( domEventData ) { - const currentCoordinates = extractCoordinates( domEventData ); - const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; - const originalSize = this.originalSize; - - // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number - // meaning that it has been shrunk. - // - // +----------------+--+ - // | | | - // | img | | - // | | | - // +----------------+ | ^ - // | | | - enlarge y - // +-------------------+ v - // <--> - // enlarge x - const enlargement = { - x: this._referenceCoordinates.x - ( currentCoordinates.x + originalSize.width ), - y: ( currentCoordinates.y - originalSize.height ) - this._referenceCoordinates.y - }; - - if ( isCentered && this.activeHandlePosition.endsWith( '-right' ) ) { - enlargement.x = currentCoordinates.x - ( this._referenceCoordinates.x + originalSize.width ); - } - - // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from - // one resized corner to your cursor. It needs to be duplicated to compensate for the other side too. - if ( isCentered ) { - enlargement.x *= 2; - } - - // const resizeHost = this._getResizeHost(); - - // The size proposed by the user. It does not consider the aspect ratio. - const proposedSize = { - width: Math.abs( originalSize.width + enlargement.x ), - height: Math.abs( originalSize.height + enlargement.y ) - }; - - // Dominant determination must take the ratio into account. - proposedSize.dominant = proposedSize.width / this.aspectRatio > proposedSize.height ? 'width' : 'height'; - proposedSize.max = proposedSize[ proposedSize.dominant ]; - - // Proposed size, respecting the aspect ratio. - const targetSize = { - width: proposedSize.width, - height: proposedSize.height - }; - - if ( proposedSize.dominant == 'width' ) { - targetSize.height = targetSize.width / this.aspectRatio; - } else { - targetSize.width = targetSize.height * this.aspectRatio; - } - - return targetSize; - } } mix( ResizeState, ObservableMixin ); @@ -239,10 +189,3 @@ function getOppositePosition( position ) { return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`; } - -function extractCoordinates( event ) { - return { - x: event.pageX, - y: event.pageY - }; -} From bc9767255af0bd6a689676f7eced887422af7b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 10:07:49 +0200 Subject: [PATCH 80/93] Removed the shadow thing concept. --- src/widgetresizer/resizer.js | 66 +++++++++--------------------------- theme/widget.css | 28 ++++----------- 2 files changed, 23 insertions(+), 71 deletions(-) diff --git a/src/widgetresizer/resizer.js b/src/widgetresizer/resizer.js index d9cd7e04..95aa5228 100644 --- a/src/widgetresizer/resizer.js +++ b/src/widgetresizer/resizer.js @@ -46,7 +46,7 @@ export default class Resizer { * @private * @type {HTMLElement|null} */ - this._domResizeWrapper = null; + this._domResizerWrapper = null; /** * View to a wrapper containing all the resizer-related views. @@ -54,13 +54,7 @@ export default class Resizer { * @private * @type {module:engine/view/uielement~UIElement} */ - this._resizeWrapperElement = null; - - /** - * @private - * @type {HTMLElement|null} - */ - this._domResizeShadow = null; + this._viewResizerWrapper = null; this.decorate( 'begin' ); this.decorate( 'cancel' ); @@ -76,22 +70,21 @@ export default class Resizer { const viewElement = this._options.viewElement; const writer = this._options.downcastWriter; - this._resizeWrapperElement = writer.createUIElement( 'div', { + this._viewResizerWrapper = writer.createUIElement( 'div', { class: 'ck ck-widget__resizer-wrapper' }, function( domDocument ) { const domElement = this.toDomElement( domDocument ); - that._domResizeShadow = that._appendShadowElement( domElement ); - that._appendResizers( that._domResizeShadow ); - that._appendSizeUi( that._domResizeShadow ); + that._appendResizers( domElement ); + that._appendSizeUi( domElement ); - that._domResizeWrapper = domElement; + that._domResizerWrapper = domElement; return domElement; } ); // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( viewElement, 'end' ), this._resizeWrapperElement ); + writer.insert( writer.createPositionAt( viewElement, 'end' ), this._viewResizerWrapper ); writer.addClass( [ 'ck-widget_with-resizer' ], viewElement ); } @@ -113,17 +106,15 @@ export default class Resizer { this.state.fetchSizeFromElement( resizeHost ); - // Refresh values based on real image. Real image might be limited by max-width, and thus fetching it - // here will reflect this limitation on resizer shadow later on. - this._domResizeWrapper.style.width = this.state.proposedWidth + 'px'; - this._domResizeWrapper.style.height = this.state.proposedHeight + 'px'; + // 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 = this.state.proposedWidth + 'px'; + this._domResizerWrapper.style.height = this.state.proposedHeight + 'px'; } commit( editor ) { const modelElement = this._options.modelElement; - const newWidth = this._domResizeShadow.clientWidth; - - this._dismissShadow(); + const newWidth = this._domResizerWrapper.clientWidth; this.redraw(); @@ -138,7 +129,6 @@ export default class Resizer { * Cancels and rejects proposed resize dimensions hiding all the UI. */ cancel() { - this._dismissShadow(); this._cleanup(); } @@ -147,7 +137,7 @@ export default class Resizer { } redraw() { - const domWrapper = this._domResizeWrapper; + const domWrapper = this._domResizerWrapper; if ( existsInDom( domWrapper ) ) { // Refresh only if resizer exists in the DOM. @@ -177,7 +167,7 @@ export default class Resizer { } containsHandle( domElement ) { - return this._domResizeWrapper.contains( domElement ); + return this._domResizerWrapper.contains( domElement ); } static isResizeHandle( domElement ) { @@ -276,41 +266,17 @@ export default class Resizer { * @returns {HTMLElement} */ _getResizeHost() { - const widgetWrapper = this._domResizeWrapper.parentElement; + const widgetWrapper = this._domResizerWrapper.parentElement; return this._options.getResizeHost ? this._options.getResizeHost( widgetWrapper ) : widgetWrapper; } - /** - * @protected - */ - _dismissShadow() { - this._domResizeShadow.removeAttribute( 'style' ); - } - - /** - * @private - * @param {HTMLElement} domElement The outer wrapper of resize UI within a given widget. - */ - _appendShadowElement( domElement ) { - const shadowElement = new Template( { - tag: 'div', - attributes: { - class: 'ck ck-widget__resizer-shadow' - } - } ).render(); - - domElement.appendChild( shadowElement ); - - return shadowElement; - } - /** * Renders the resize handles in DOM. * * @private - * @param {HTMLElement} domElement Resize shadow where the resizers should be appended to. + * @param {HTMLElement} domElement The resizer wrapper. */ _appendResizers( domElement ) { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; diff --git a/theme/widget.css b/theme/widget.css index 9bec7436..b6ea46c4 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -22,18 +22,8 @@ position: relative; } -.ck-focused .ck-widget_with-resizer.ck-widget_selected { - & > .ck-widget__resizer-wrapper { - visibility: visible; - } - - & .ck-widget__resizer-shadow { - display: block; - } -} - .ck .ck-widget__resizer-wrapper { - visibility: hidden; + display: none; position: absolute; /* The wrapper itself should not interfere with pointer device, only the handlers. */ @@ -41,20 +31,16 @@ left: 0; top: 0; -} - -.ck .ck-widget__resizer-shadow { - display: none; - position: absolute; - - left: 0; - right: 0; - top: 0; - bottom: 0; outline: 1px solid var(--ck-color-resizer); } +.ck-focused .ck-widget_with-resizer.ck-widget_selected { + & > .ck-widget__resizer-wrapper { + display: block; + } +} + .ck .ck-widget__resizer { position: absolute; From d5e67fcafe400d71850d576f2c720708d4826d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 10:17:22 +0200 Subject: [PATCH 81/93] Fixed classes naming. --- src/widgetresizer/resizer.js | 18 ++++++++--------- src/widgetresizer/resizerstate.js | 6 +++--- theme/widget.css | 32 +++++++++++++++---------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/widgetresizer/resizer.js b/src/widgetresizer/resizer.js index 95aa5228..3e9c42cc 100644 --- a/src/widgetresizer/resizer.js +++ b/src/widgetresizer/resizer.js @@ -67,15 +67,15 @@ export default class Resizer { */ attach() { const that = this; - const viewElement = this._options.viewElement; + const widgetElement = this._options.viewElement; const writer = this._options.downcastWriter; this._viewResizerWrapper = writer.createUIElement( 'div', { - class: 'ck ck-widget__resizer-wrapper' + class: 'ck ck-reset_all ck-widget__resizer' }, function( domDocument ) { const domElement = this.toDomElement( domDocument ); - that._appendResizers( domElement ); + that._appendHandles( domElement ); that._appendSizeUi( domElement ); that._domResizerWrapper = domElement; @@ -84,8 +84,8 @@ export default class Resizer { } ); // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( viewElement, 'end' ), this._viewResizerWrapper ); - writer.addClass( [ 'ck-widget_with-resizer' ], viewElement ); + writer.insert( writer.createPositionAt( widgetElement, 'end' ), this._viewResizerWrapper ); + writer.addClass( 'ck-widget_with-resizer', widgetElement ); } begin( domResizeHandle ) { @@ -171,7 +171,7 @@ export default class Resizer { } static isResizeHandle( domElement ) { - return domElement.classList.contains( 'ck-widget__resizer' ); + return domElement.classList.contains( 'ck-widget__resizer__handle' ); } /** @@ -278,14 +278,14 @@ export default class Resizer { * @private * @param {HTMLElement} domElement The resizer wrapper. */ - _appendResizers( domElement ) { + _appendHandles( domElement ) { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; for ( const currentPosition of resizerPositions ) { domElement.appendChild( ( new Template( { tag: 'div', attributes: { - class: `ck-widget__resizer ${ getResizerClass( currentPosition ) }` + class: `ck-widget__resizer__handle ${ getResizerClass( currentPosition ) }` } } ).render() ) ); } @@ -369,7 +369,7 @@ class SizeView extends View { // @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. // @returns {String} A prefixed HTML class name for the resizer element function getResizerClass( resizerPosition ) { - return `ck-widget__resizer-${ resizerPosition }`; + return `ck-widget__resizer__handle-${ resizerPosition }`; } function extractCoordinates( event ) { diff --git a/src/widgetresizer/resizerstate.js b/src/widgetresizer/resizerstate.js index 36e589ea..4f939dec 100644 --- a/src/widgetresizer/resizerstate.js +++ b/src/widgetresizer/resizerstate.js @@ -153,8 +153,8 @@ function getAbsoluteBoundaryPoint( element, resizerPosition ) { * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`. * @returns {String} A prefixed HTML class name for the resizer element */ -function getResizerClass( resizerPosition ) { - return `ck-widget__resizer-${ resizerPosition }`; +function getResizerHandleClass( resizerPosition ) { + return `ck-widget__resizer__handle-${ resizerPosition }`; } /** @@ -168,7 +168,7 @@ function getHandlePosition( domHandle ) { const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ]; for ( const position of resizerPositions ) { - if ( domHandle.classList.contains( getResizerClass( position ) ) ) { + if ( domHandle.classList.contains( getResizerHandleClass( position ) ) ) { return position; } } diff --git a/theme/widget.css b/theme/widget.css index b6ea46c4..de25c4da 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -18,15 +18,15 @@ } .ck .ck-widget_with-resizer { - /* Make the widget wrapper a relative positioning container for the drag handler. */ + /* Make the widget wrapper a relative positioning container for the drag handle. */ position: relative; } -.ck .ck-widget__resizer-wrapper { +.ck .ck-widget__resizer { display: none; position: absolute; - /* The wrapper itself should not interfere with pointer device, only the handlers. */ + /* The wrapper itself should not interfere with pointer device, only the handles. */ pointer-events: none; left: 0; @@ -36,12 +36,12 @@ } .ck-focused .ck-widget_with-resizer.ck-widget_selected { - & > .ck-widget__resizer-wrapper { + & > .ck-widget__resizer { display: block; } } -.ck .ck-widget__resizer { +.ck .ck-widget__resizer__handle { position: absolute; /* Resizers are the only UI elements that should interfere with pointer device. */ @@ -53,43 +53,43 @@ border: var(--ck-resizer-border-width) solid #fff; border-radius: var(--ck-resizer-border-radius); - &.ck-widget__resizer-top-left { + &.ck-widget__resizer__handle-top-left { top: var( --ck-resizer-offset ); left: var( --ck-resizer-offset ); cursor: nwse-resize; } - &.ck-widget__resizer-top-right { + &.ck-widget__resizer__handle-top-right { top: var( --ck-resizer-offset ); right: var( --ck-resizer-offset ); cursor: nesw-resize; } - &.ck-widget__resizer-bottom-right { + &.ck-widget__resizer__handle-bottom-right { bottom: var( --ck-resizer-offset ); right: var( --ck-resizer-offset ); cursor: nwse-resize; } - &.ck-widget__resizer-bottom-left { + &.ck-widget__resizer__handle-bottom-left { bottom: var( --ck-resizer-offset ); left: var( --ck-resizer-offset ); cursor: nesw-resize; } } -.ck .ck-widget.ck-widget_with-selection-handler { - /* Make the widget wrapper a relative positioning container for the drag handler. */ +.ck .ck-widget.ck-widget_with-selection-handle { + /* Make the widget wrapper a relative positioning container for the drag handle. */ position: relative; - /* Show the selection handler on mouse hover over the widget. */ + /* Show the selection handle on mouse hover over the widget. */ &:hover { - & .ck-widget__selection-handler { + & .ck-widget__selection-handle { visibility: visible; } } - & .ck-widget__selection-handler { + & .ck-widget__selection-handle { position: absolute; & .ck-icon { @@ -99,8 +99,8 @@ } } - /* Show the selection handler when the widget is selected. */ - &.ck-widget_selected .ck-widget__selection-handler { + /* Show the selection handle when the widget is selected. */ + &.ck-widget_selected .ck-widget__selection-handle { visibility: visible; } } From 0fe6ebdb6fa908bf5e97a9cc773e8a2a4efd93e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 10:29:16 +0200 Subject: [PATCH 82/93] Naming. --- src/widgetresizer/resizer.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/widgetresizer/resizer.js b/src/widgetresizer/resizer.js index 3e9c42cc..76d059ae 100644 --- a/src/widgetresizer/resizer.js +++ b/src/widgetresizer/resizer.js @@ -76,7 +76,7 @@ export default class Resizer { const domElement = this.toDomElement( domDocument ); that._appendHandles( domElement ); - that._appendSizeUi( domElement ); + that._appendSizeUI( domElement ); that._domResizerWrapper = domElement; @@ -91,7 +91,7 @@ export default class Resizer { begin( domResizeHandle ) { this.state = new ResizeState( this._options ); - this.sizeUi.bindToState( this.state ); + this.sizeUI.bindToState( this.state ); this.state.begin( domResizeHandle, this._getResizeHost() ); @@ -180,8 +180,8 @@ export default class Resizer { * @protected */ _cleanup() { - this.sizeUi.dismiss(); - this.sizeUi.isVisible = false; + this.sizeUI.dismiss(); + this.sizeUI.isVisible = false; } /** @@ -295,15 +295,15 @@ export default class Resizer { * @private * @param {HTMLElement} domElement */ - _appendSizeUi( domElement ) { - const sizeUi = new SizeView(); + _appendSizeUI( domElement ) { + const sizeUI = new SizeView(); // Make sure icon#element is rendered before passing to appendChild(). - sizeUi.render(); + sizeUI.render(); - this.sizeUi = sizeUi; + this.sizeUI = sizeUI; - domElement.appendChild( sizeUi.element ); + domElement.appendChild( sizeUI.element ); } /** From e4a62d8bb516d17a3d2150c1b08daced14b965aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 10:40:38 +0200 Subject: [PATCH 83/93] Use RTL-proof character. --- src/widgetresizer/resizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgetresizer/resizer.js b/src/widgetresizer/resizer.js index 76d059ae..05d7047b 100644 --- a/src/widgetresizer/resizer.js +++ b/src/widgetresizer/resizer.js @@ -355,7 +355,7 @@ class SizeView extends View { x !== null && y !== null ); this.bind( 'label' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( x, y ) => - `${ Math.round( x ) }x${ Math.round( y ) }` ); + `${ Math.round( x ) }×${ Math.round( y ) }` ); this.bind( 'activeHandlePosition' ).to( resizerState ); } From 97919bbc1e1061b26c2dd0b2500d521952409049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 10:47:34 +0200 Subject: [PATCH 84/93] Improved naming. --- src/{widgetresizer.js => widgetresize.js} | 10 +++++----- src/{widgetresizer => widgetresize}/resizer.js | 2 +- src/{widgetresizer => widgetresize}/resizerstate.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/{widgetresizer.js => widgetresize.js} (92%) rename src/{widgetresizer => widgetresize}/resizer.js (99%) rename src/{widgetresizer => widgetresize}/resizerstate.js (99%) diff --git a/src/widgetresizer.js b/src/widgetresize.js similarity index 92% rename from src/widgetresizer.js rename to src/widgetresize.js index 68c3c8b8..5c82b0bf 100644 --- a/src/widgetresizer.js +++ b/src/widgetresize.js @@ -4,11 +4,11 @@ */ /** - * @module widget/widgetresizer + * @module widget/widgetresize */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import Resizer from './widgetresizer/resizer'; +import Resizer from './widgetresize/resizer'; import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { throttle } from 'lodash-es'; @@ -16,14 +16,14 @@ import { throttle } from 'lodash-es'; /** * Widget resize feature plugin. * - * Use the {@link module:widget/widgetresizer~WidgetResizer#apply} method to create resizer for a provided widget. + * Use the {@link module:widget/widgetresizer~WidgetResize#apply} method to create resizer for a provided widget. */ -export default class WidgetResizer extends Plugin { +export default class WidgetResize extends Plugin { /** * @inheritDoc */ static get pluginName() { - return 'WidgetResizer'; + return 'WidgetResize'; } init() { diff --git a/src/widgetresizer/resizer.js b/src/widgetresize/resizer.js similarity index 99% rename from src/widgetresizer/resizer.js rename to src/widgetresize/resizer.js index 05d7047b..f80bfbc6 100644 --- a/src/widgetresizer/resizer.js +++ b/src/widgetresize/resizer.js @@ -4,7 +4,7 @@ */ /** - * @module widget/widgetresizer/resizer + * @module widget/widgetresize/resizer */ import View from '@ckeditor/ckeditor5-ui/src/view'; diff --git a/src/widgetresizer/resizerstate.js b/src/widgetresize/resizerstate.js similarity index 99% rename from src/widgetresizer/resizerstate.js rename to src/widgetresize/resizerstate.js index 4f939dec..d3c2e6eb 100644 --- a/src/widgetresizer/resizerstate.js +++ b/src/widgetresize/resizerstate.js @@ -4,7 +4,7 @@ */ /** - * @module widget/widgetresizer/resizerstate + * @module widget/widgetresize/resizerstate */ import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; From 28971ce18f3b62c1ab4c2bb52e6d2c20b6c35cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 10:58:06 +0200 Subject: [PATCH 85/93] Expose onCommit to the consumer. --- src/widgetresize.js | 6 +++++- src/widgetresize/resizer.js | 11 ++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/widgetresize.js b/src/widgetresize.js index 5c82b0bf..7368e284 100644 --- a/src/widgetresize.js +++ b/src/widgetresize.js @@ -61,7 +61,7 @@ export default class WidgetResize extends Plugin { this._observer.listenTo( domDocument, 'mouseup', () => { if ( this.activeResizer ) { - this.activeResizer.commit( this.editor ); + this.activeResizer.commit(); this.activeResizer = null; } @@ -127,6 +127,10 @@ export default class WidgetResize extends Plugin { * @member {module:engine/view/downcastwriter~DowncastWriter} module:widget/widgetresizer~ResizerOptions#downcastWriter */ +/** + * @member {Function} module:widget/widgetresizer~ResizerOptions#onCommit + */ + /** * @member {Function} module:widget/widgetresizer~ResizerOptions#getResizeHost */ diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index f80bfbc6..2665e719 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -112,15 +112,8 @@ export default class Resizer { this._domResizerWrapper.style.height = this.state.proposedHeight + 'px'; } - commit( editor ) { - const modelElement = this._options.modelElement; - const newWidth = this._domResizerWrapper.clientWidth; - - this.redraw(); - - editor.model.change( writer => { - writer.setAttribute( 'width', newWidth + 'px', modelElement ); - } ); + commit() { + this._options.onCommit( this.state ); this._cleanup(); } From 5a589f3aa7919cb4abb9e4a6863d72202d55c755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Wed, 14 Aug 2019 17:01:51 +0200 Subject: [PATCH 86/93] Round sizes. --- src/widgetresize/resizer.js | 15 ++++++++++----- src/widgetresize/resizerstate.js | 8 ++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index 2665e719..853bcdc4 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -243,7 +243,12 @@ export default class Resizer { targetSize.width = targetSize.height * state.aspectRatio; } - return targetSize; + return { + // TODO I couldn't find an automated TC for that, but rounding must be done here. + // Do not move it above. + width: Math.round( targetSize.width ), + height: Math.round( targetSize.height ) + }; } /** @@ -344,11 +349,11 @@ class SizeView extends View { } bindToState( resizerState ) { - this.bind( 'isVisible' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( x, y ) => - x !== null && y !== null ); + this.bind( 'isVisible' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( width, height ) => + width !== null && height !== null ); - this.bind( 'label' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( x, y ) => - `${ Math.round( x ) }×${ Math.round( y ) }` ); + this.bind( 'label' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( width, height ) => + `${ width }×${ height }` ); this.bind( 'activeHandlePosition' ).to( resizerState ); } diff --git a/src/widgetresize/resizerstate.js b/src/widgetresize/resizerstate.js index d3c2e6eb..f0a19327 100644 --- a/src/widgetresize/resizerstate.js +++ b/src/widgetresize/resizerstate.js @@ -115,10 +115,14 @@ export default class ResizeState { * @param {HTMLElement} domElement */ fetchSizeFromElement( domElement ) { + // TODO the logic from this method should be moved to resizer.js. + // Search for other places where we do rounding – it's all in resizer.js. + // TODO this needs an automated test – currently it only affects the SizeView + // which we don't test. const rect = new Rect( domElement ); - this.proposedWidth = rect.width; - this.proposedHeight = rect.height; + this.proposedWidth = Math.round( rect.width ); + this.proposedHeight = Math.round( rect.height ); } } From 5f4e014fad93697e188d39943c3d254c23394c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 16 Aug 2019 11:17:38 +0200 Subject: [PATCH 87/93] Introduced Resizer#isEnabled. --- src/widgetresize/resizer.js | 18 +++++++++++------- theme/widget.css | 14 +++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index 853bcdc4..16eff355 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -20,6 +20,7 @@ import ResizeState from './resizerstate'; * Stores the internal state of a single resizable object. * * @class Resizer + * @mixes module:utils/observablemixin~ObservableMixin */ export default class Resizer { /** @@ -49,12 +50,9 @@ export default class Resizer { this._domResizerWrapper = null; /** - * View to a wrapper containing all the resizer-related views. - * - * @private - * @type {module:engine/view/uielement~UIElement} + * @observable */ - this._viewResizerWrapper = null; + this.set( 'isEnabled', true ); this.decorate( 'begin' ); this.decorate( 'cancel' ); @@ -70,7 +68,7 @@ export default class Resizer { const widgetElement = this._options.viewElement; const writer = this._options.downcastWriter; - this._viewResizerWrapper = writer.createUIElement( 'div', { + const viewResizerWrapper = writer.createUIElement( 'div', { class: 'ck ck-reset_all ck-widget__resizer' }, function( domDocument ) { const domElement = this.toDomElement( domDocument ); @@ -80,11 +78,17 @@ export default class Resizer { that._domResizerWrapper = domElement; + that.on( 'change:isEnabled', ( evt, propName, newValue ) => { + domElement.style.display = newValue ? '' : 'none'; + } ); + + domElement.style.display = that.isEnabled ? '' : 'none'; + return domElement; } ); // Append resizer wrapper to the widget's wrapper. - writer.insert( writer.createPositionAt( widgetElement, 'end' ), this._viewResizerWrapper ); + writer.insert( writer.createPositionAt( widgetElement, 'end' ), viewResizerWrapper ); writer.addClass( 'ck-widget_with-resizer', widgetElement ); } diff --git a/theme/widget.css b/theme/widget.css index de25c4da..7efe2c5a 100644 --- a/theme/widget.css +++ b/theme/widget.css @@ -78,18 +78,18 @@ } } -.ck .ck-widget.ck-widget_with-selection-handle { - /* Make the widget wrapper a relative positioning container for the drag handle. */ +.ck .ck-widget.ck-widget_with-selection-handler { + /* Make the widget wrapper a relative positioning container for the drag handler. */ position: relative; - /* Show the selection handle on mouse hover over the widget. */ + /* Show the selection handler on mouse hover over the widget. */ &:hover { - & .ck-widget__selection-handle { + & .ck-widget__selection-handler { visibility: visible; } } - & .ck-widget__selection-handle { + & .ck-widget__selection-handler { position: absolute; & .ck-icon { @@ -99,8 +99,8 @@ } } - /* Show the selection handle when the widget is selected. */ - &.ck-widget_selected .ck-widget__selection-handle { + /* Show the selection handler when the widget is selected. */ + &.ck-widget_selected .ck-widget__selection-handler { visibility: visible; } } From 197b0da50f1c66102c8aef32900f01be7e2f1bd3 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Fri, 16 Aug 2019 12:12:28 +0200 Subject: [PATCH 88/93] Internal: Added generic support for percent-based resizing. --- src/widgetresize/resizer.js | 54 +++++++++++++++++++++++++------- src/widgetresize/resizerstate.js | 37 +++++++++++++++++----- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index 16eff355..924fc6d2 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -97,18 +97,24 @@ export default class Resizer { this.sizeUI.bindToState( this.state ); - this.state.begin( domResizeHandle, this._getResizeHost() ); + this.state.begin( domResizeHandle, this._getHandleHost() ); this.redraw(); } + _newGetResizeHost() { + return this._getResizeHost().parentElement; + } + updateSize( domEventData ) { const resizeHost = this._getResizeHost(); + const newResizeHost = this._newGetResizeHost(); const newSize = this._proposeNewSize( domEventData ); - resizeHost.style.width = newSize.width + 'px'; + resizeHost.style.width = null; + newResizeHost.style.width = newSize.width + 'px'; - this.state.fetchSizeFromElement( resizeHost ); + this.state.fetchSizeFromElement( resizeHost, newResizeHost ); // Refresh values based on the real image. Real image might be limited by max-width, and thus fetching it // here will reflect this limitation. @@ -117,6 +123,8 @@ export default class Resizer { } commit() { + this.state.fetchSizeFromElement( this._getResizeHost(), this._newGetResizeHost() ); + this._options.onCommit( this.state ); this._cleanup(); @@ -202,7 +210,7 @@ export default class Resizer { // +----------------+--+ // | | | // | img | | - // | | | + // | /handle host | | // +----------------+ | ^ // | | | - enlarge y // +-------------------+ v @@ -258,11 +266,7 @@ export default class Resizer { /** * Method used to obtain the resize host. * - * Resize host is an object that is actually resized. - * - * Resize host will not always be an entire widget itself. Take an image as an example. Image widget - * contains an image and caption. Only the image should be used to resize the widget, while the caption - * will simply follow the image size. + * Resize host is an object that receives dimensions that are result of resizing. * * @protected * @returns {HTMLElement} @@ -274,6 +278,24 @@ export default class Resizer { this._options.getResizeHost( widgetWrapper ) : widgetWrapper; } + /** + * Method used to obtain the resize host. + * + * Handle host is an object to which the handles are aligned to. + * + * Handle host will not always be an entire widget itself. Take an image as an example. Image widget + * contains an image and a caption. Only image should be surrounded with handles. + * + * @protected + * @returns {HTMLElement} + */ + _getHandleHost() { + const widgetWrapper = this._domResizerWrapper.parentElement; + + return this._options.getHandleHost ? + this._options.getHandleHost( widgetWrapper ) : widgetWrapper; + } + /** * Renders the resize handles in DOM. * @@ -356,8 +378,18 @@ class SizeView extends View { this.bind( 'isVisible' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( width, height ) => width !== null && height !== null ); - this.bind( 'label' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( width, height ) => - `${ width }×${ height }` ); + this.bind( 'label' ).to( + resizerState, 'proposedWidth', + resizerState, 'proposedHeight', + resizerState, 'proposedWidthPercents', + ( width, height, widthPercents ) => { + if ( widthPercents ) { + return `${ widthPercents }%`; + } else { + return `${ width }×${ height }`; + } + } + ); this.bind( 'activeHandlePosition' ).to( resizerState ); } diff --git a/src/widgetresize/resizerstate.js b/src/widgetresize/resizerstate.js index f0a19327..9144d0d5 100644 --- a/src/widgetresize/resizerstate.js +++ b/src/widgetresize/resizerstate.js @@ -65,6 +65,15 @@ export default class ResizeState { */ this.set( 'proposedHeight', null ); + /** + * TODO + * + * @readonly + * @observable + * @member {Number|null} #proposedWidthPercents + */ + this.set( 'proposedWidthPercents', null ); + /** * TODO * @@ -93,12 +102,12 @@ export default class ResizeState { * * @param {HTMLElement} domResizeHandle The handle used to calculate the reference point. */ - begin( domResizeHandle, resizeHost ) { - const clientRect = new Rect( resizeHost ); + begin( domResizeHandle, handleHost ) { + const clientRect = new Rect( handleHost ); this.activeHandlePosition = getHandlePosition( domResizeHandle ); - this._referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, getOppositePosition( this.activeHandlePosition ) ); + this._referenceCoordinates = getAbsoluteBoundaryPoint( handleHost, getOppositePosition( this.activeHandlePosition ) ); this.originalSize = { width: clientRect.width, @@ -106,23 +115,37 @@ export default class ResizeState { }; this.aspectRatio = this._options.getAspectRatio ? - this._options.getAspectRatio( resizeHost ) : clientRect.width / clientRect.height; + this._options.getAspectRatio( handleHost ) : clientRect.width / clientRect.height; } /** * Sets `proposedWidth` / `proposedHeight` properties based on provided element. * - * @param {HTMLElement} domElement + * @param {HTMLElement} domHandleHost + * @param {HTMLElement} domResizeHost */ - fetchSizeFromElement( domElement ) { + fetchSizeFromElement( domHandleHost, domResizeHost ) { // TODO the logic from this method should be moved to resizer.js. // Search for other places where we do rounding – it's all in resizer.js. // TODO this needs an automated test – currently it only affects the SizeView // which we don't test. - const rect = new Rect( domElement ); + const rect = new Rect( domHandleHost ); this.proposedWidth = Math.round( rect.width ); this.proposedHeight = Math.round( rect.height ); + + if ( [ 'percent', '%' ].includes( this._options.unit ) ) { + this._proposePercents( new Rect( domResizeHost ), domResizeHost ); + } + } + + _proposePercents( rect, domResizeHost ) { + const hostParentElement = domResizeHost.parentElement; + // We need to use getComputedStyle instead of bounding rect, as rect width also include parent's padding, that **can not** + // be occupied by the resized object. Computed width does not include it. + const hostParentInnerWith = parseFloat( hostParentElement.ownerDocument.defaultView.getComputedStyle( hostParentElement ).width ); + + this.proposedWidthPercents = Math.min( Math.round( rect.width / hostParentInnerWith * 100 ), 100 ); } } From 51bbc5f9f498e306026a71c14a08cd0f7cdc5310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 16 Aug 2019 16:05:54 +0200 Subject: [PATCH 89/93] Added support for percents. --- src/widgetresize/resizer.js | 92 ++++++++++++++-------------- src/widgetresize/resizerstate.js | 100 +++++++++++++++---------------- 2 files changed, 98 insertions(+), 94 deletions(-) diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index 924fc6d2..b0a3502b 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -95,37 +95,44 @@ export default class Resizer { begin( domResizeHandle ) { this.state = new ResizeState( this._options ); - this.sizeUI.bindToState( this.state ); + this.sizeUI.bindToState( this._options, this.state ); - this.state.begin( domResizeHandle, this._getHandleHost() ); + this.state.begin( domResizeHandle, this._getHandleHost(), this._getResizeHost() ); this.redraw(); } - _newGetResizeHost() { - return this._getResizeHost().parentElement; - } - updateSize( domEventData ) { - const resizeHost = this._getResizeHost(); - const newResizeHost = this._newGetResizeHost(); + const domHandleHost = this._getHandleHost(); + const domResizeHost = this._getResizeHost(); + const unit = this._options.unit; const newSize = this._proposeNewSize( domEventData ); - resizeHost.style.width = null; - newResizeHost.style.width = newSize.width + 'px'; + domResizeHost.style.width = ( unit === '%' ? newSize.widthPercents : newSize.width ) + this._options.unit; + + const domHandleHostRect = new Rect( domHandleHost ); + + newSize.handleHostWidth = Math.round( domHandleHostRect.width ); + newSize.handleHostHeight = Math.round( domHandleHostRect.height ); + + const domResizeHostRect = new Rect( domHandleHost ); + + newSize.width = Math.round( domResizeHostRect.width ); + newSize.height = Math.round( domResizeHostRect.height ); - this.state.fetchSizeFromElement( resizeHost, newResizeHost ); + this.state.update( newSize ); // 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 = this.state.proposedWidth + 'px'; - this._domResizerWrapper.style.height = this.state.proposedHeight + 'px'; + this._domResizerWrapper.style.width = newSize.handleHostWidth + 'px'; + this._domResizerWrapper.style.height = newSize.handleHostHeight + 'px'; } commit() { - this.state.fetchSizeFromElement( this._getResizeHost(), this._newGetResizeHost() ); + const unit = this._options.unit; + const newValue = ( unit === '%' ? this.state.proposedWidthPercents : this.state.proposedWidth ) + this._options.unit; - this._options.onCommit( this.state ); + this._options.onCommit( newValue ); this._cleanup(); } @@ -141,14 +148,15 @@ export default class Resizer { this.cancel(); } + // TODO review this redraw() { const domWrapper = this._domResizerWrapper; if ( existsInDom( domWrapper ) ) { // Refresh only if resizer exists in the DOM. const widgetWrapper = domWrapper.parentElement; - const resizingHost = this._getResizeHost(); - const clientRect = new Rect( resizingHost ); + const handleHost = this._getHandleHost(); + const clientRect = new Rect( handleHost ); domWrapper.style.width = clientRect.width + 'px'; domWrapper.style.height = clientRect.height + 'px'; @@ -157,12 +165,12 @@ export default class Resizer { // 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( resizingHost ) ) { - domWrapper.style.left = resizingHost.offsetLeft + 'px'; - domWrapper.style.top = resizingHost.offsetTop + 'px'; + if ( !widgetWrapper.isSameNode( handleHost ) ) { + domWrapper.style.left = handleHost.offsetLeft + 'px'; + domWrapper.style.top = handleHost.offsetTop + 'px'; - domWrapper.style.height = resizingHost.offsetHeight + 'px'; - domWrapper.style.width = resizingHost.offsetWidth + 'px'; + domWrapper.style.height = handleHost.offsetHeight + 'px'; + domWrapper.style.width = handleHost.offsetWidth + 'px'; } } @@ -195,14 +203,13 @@ export default class Resizer { * @private * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size. * @returns {Object} return - * @returns {Number} return.x Proposed width. - * @returns {Number} return.y Proposed height. + * @returns {Number} return.width Proposed width. + * @returns {Number} return.height Proposed height. */ _proposeNewSize( domEventData ) { const state = this.state; const currentCoordinates = extractCoordinates( domEventData ); const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true; - const originalSize = state.originalSize; // Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number // meaning that it has been shrunk. @@ -217,12 +224,12 @@ export default class Resizer { // <--> // enlarge x const enlargement = { - x: state._referenceCoordinates.x - ( currentCoordinates.x + originalSize.width ), - y: ( currentCoordinates.y - originalSize.height ) - state._referenceCoordinates.y + x: state._referenceCoordinates.x - ( currentCoordinates.x + state.originalWidth ), + y: ( currentCoordinates.y - state.originalHeight ) - state._referenceCoordinates.y }; if ( isCentered && state.activeHandlePosition.endsWith( '-right' ) ) { - enlargement.x = currentCoordinates.x - ( state._referenceCoordinates.x + originalSize.width ); + enlargement.x = currentCoordinates.x - ( state._referenceCoordinates.x + state.originalWidth ); } // Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from @@ -235,8 +242,8 @@ export default class Resizer { // The size proposed by the user. It does not consider the aspect ratio. const proposedSize = { - width: Math.abs( originalSize.width + enlargement.x ), - height: Math.abs( originalSize.height + enlargement.y ) + width: Math.abs( state.originalWidth + enlargement.x ), + height: Math.abs( state.originalHeight + enlargement.y ) }; // Dominant determination must take the ratio into account. @@ -256,10 +263,9 @@ export default class Resizer { } return { - // TODO I couldn't find an automated TC for that, but rounding must be done here. - // Do not move it above. width: Math.round( targetSize.width ), - height: Math.round( targetSize.height ) + height: Math.round( targetSize.height ), + widthPercents: Math.min( Math.round( state.originalWidthPercents / state.originalWidth * targetSize.width * 100 ) / 100, 100 ) }; } @@ -274,12 +280,11 @@ export default class Resizer { _getResizeHost() { const widgetWrapper = this._domResizerWrapper.parentElement; - return this._options.getResizeHost ? - this._options.getResizeHost( widgetWrapper ) : widgetWrapper; + return this._options.getResizeHost( widgetWrapper ); } /** - * Method used to obtain the resize host. + * Method used to obtain the handle host. * * Handle host is an object to which the handles are aligned to. * @@ -292,8 +297,7 @@ export default class Resizer { _getHandleHost() { const widgetWrapper = this._domResizerWrapper.parentElement; - return this._options.getHandleHost ? - this._options.getHandleHost( widgetWrapper ) : widgetWrapper; + return this._options.getHandleHost( widgetWrapper ); } /** @@ -374,19 +378,19 @@ class SizeView extends View { } ); } - bindToState( resizerState ) { + bindToState( options, resizerState ) { this.bind( 'isVisible' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( width, height ) => width !== null && height !== null ); this.bind( 'label' ).to( - resizerState, 'proposedWidth', - resizerState, 'proposedHeight', + resizerState, 'proposedHandleHostWidth', + resizerState, 'proposedHandleHostHeight', resizerState, 'proposedWidthPercents', ( width, height, widthPercents ) => { - if ( widthPercents ) { - return `${ widthPercents }%`; - } else { + if ( options.unit === 'px' ) { return `${ width }×${ height }`; + } else { + return `${ widthPercents }%`; } } ); diff --git a/src/widgetresize/resizerstate.js b/src/widgetresize/resizerstate.js index 9144d0d5..7e96ec8b 100644 --- a/src/widgetresize/resizerstate.js +++ b/src/widgetresize/resizerstate.js @@ -23,14 +23,24 @@ export default class ResizeState { */ constructor( options ) { /** - * The size of resize host before current resize process. + * TODO * - * This information is only known after DOM was rendered, so it will be updated later. + * @readonly + * @member {Number} #originalWidth + */ + + /** + * TODO * - * It contains an object with `width` and `height` properties. + * @readonly + * @member {Number} #originalHeight + */ + + /** + * TODO * * @readonly - * @member {Object} #originalSize + * @member {Number} #originalWidthPercents */ /** @@ -44,35 +54,34 @@ export default class ResizeState { this.set( 'activeHandlePosition', null ); /** - * Width proposed (but not yet accepted) using the widget resizer. - * - * It goes back to `null` once the resizer is dismissed or accepted. + * TODO * * @readonly * @observable - * @member {Number|null} #proposedWidth + * @member {Number|null} #proposedWidthPercents */ - this.set( 'proposedWidth', null ); + this.set( 'proposedWidthPercents', null ); /** - * Height proposed (but not yet accepted) using the widget resizer. - * - * It goes back to `null` once the resizer is dismissed or accepted. + * TODO * * @readonly * @observable - * @member {Number|null} #proposedHeight + * @member {Number|null} #proposedWidthPixels */ - this.set( 'proposedHeight', null ); + this.set( 'proposedWidth', null ); /** * TODO * * @readonly * @observable - * @member {Number|null} #proposedWidthPercents + * @member {Number|null} #proposedHeightPixels */ - this.set( 'proposedWidthPercents', null ); + this.set( 'proposedHeight', null ); + + this.set( 'proposedHandleHostWidth', null ); + this.set( 'proposedHandleHostHeight', null ); /** * TODO @@ -101,51 +110,42 @@ export default class ResizeState { /** * * @param {HTMLElement} domResizeHandle The handle used to calculate the reference point. + * @param {HTMLElement} domHandleHost + * @param {HTMLElement} domResizeHost */ - begin( domResizeHandle, handleHost ) { - const clientRect = new Rect( handleHost ); + begin( domResizeHandle, domHandleHost, domResizeHost ) { + const clientRect = new Rect( domHandleHost ); this.activeHandlePosition = getHandlePosition( domResizeHandle ); - this._referenceCoordinates = getAbsoluteBoundaryPoint( handleHost, getOppositePosition( this.activeHandlePosition ) ); + this._referenceCoordinates = getAbsoluteBoundaryPoint( domHandleHost, getOppositePosition( this.activeHandlePosition ) ); - this.originalSize = { - width: clientRect.width, - height: clientRect.height - }; + this.originalWidth = clientRect.width; + this.originalHeight = clientRect.height; - this.aspectRatio = this._options.getAspectRatio ? - this._options.getAspectRatio( handleHost ) : clientRect.width / clientRect.height; - } + this.aspectRatio = clientRect.width / clientRect.height; - /** - * Sets `proposedWidth` / `proposedHeight` properties based on provided element. - * - * @param {HTMLElement} domHandleHost - * @param {HTMLElement} domResizeHost - */ - fetchSizeFromElement( domHandleHost, domResizeHost ) { - // TODO the logic from this method should be moved to resizer.js. - // Search for other places where we do rounding – it's all in resizer.js. - // TODO this needs an automated test – currently it only affects the SizeView - // which we don't test. - const rect = new Rect( domHandleHost ); - - this.proposedWidth = Math.round( rect.width ); - this.proposedHeight = Math.round( rect.height ); - - if ( [ 'percent', '%' ].includes( this._options.unit ) ) { - this._proposePercents( new Rect( domResizeHost ), domResizeHost ); + const widthStyle = domResizeHost.style.width; + + if ( widthStyle ) { + if ( widthStyle.match( /^\d+\.?\d*%$/ ) ) { + this.originalWidthPercents = parseFloat( widthStyle ); + } else { + // TODO we need to check it via comparing to the parent's width. + this.originalWidthPercents = 50; + } + } else { + this.originalWidthPercents = 100; } } - _proposePercents( rect, domResizeHost ) { - const hostParentElement = domResizeHost.parentElement; - // We need to use getComputedStyle instead of bounding rect, as rect width also include parent's padding, that **can not** - // be occupied by the resized object. Computed width does not include it. - const hostParentInnerWith = parseFloat( hostParentElement.ownerDocument.defaultView.getComputedStyle( hostParentElement ).width ); + update( newSize ) { + this.proposedWidth = newSize.width; + this.proposedHeight = newSize.height; + this.proposedWidthPercents = newSize.widthPercents; - this.proposedWidthPercents = Math.min( Math.round( rect.width / hostParentInnerWith * 100 ), 100 ); + this.proposedHandleHostWidth = newSize.handleHostWidth; + this.proposedHandleHostHeight = newSize.handleHostHeight; } } From 568829103e671f2b3d73890a3e91002bbce9b115 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 19 Aug 2019 07:32:41 +0200 Subject: [PATCH 90/93] Docs: Improved API docs. --- src/widgetresize.js | 22 ++++----- src/widgetresize/resizer.js | 76 +++++++++++++++++++++++++++++--- src/widgetresize/resizerstate.js | 5 +-- 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/widgetresize.js b/src/widgetresize.js index 7368e284..0ea03cca 100644 --- a/src/widgetresize.js +++ b/src/widgetresize.js @@ -16,7 +16,9 @@ import { throttle } from 'lodash-es'; /** * Widget resize feature plugin. * - * Use the {@link module:widget/widgetresizer~WidgetResize#apply} method to create resizer for a provided widget. + * Use the {@link module:widget/widgetresize~WidgetResize#attachTo} method to create a resizer for the specified widget. + * + * @extends module:core/plugin~Plugin */ export default class WidgetResize extends Plugin { /** @@ -85,8 +87,8 @@ export default class WidgetResize extends Plugin { } /** - * @param {module:widget/widgetresizer~ResizerOptions} [options] Resizer options. - * @returns {module:widget/resizer~Resizer} + * @param {module:widget/widgetresize~ResizerOptions} [options] Resizer options. + * @returns {module:widget/widgetresize/resizer~Resizer} */ attachTo( options ) { const resizer = new Resizer( options ); @@ -116,29 +118,29 @@ export default class WidgetResize extends Plugin { */ /** - * @member {module:engine/model/element~Element} module:widget/widgetresizer~ResizerOptions#modelElement + * @member {module:engine/model/element~Element} module:widget/widgetresize~ResizerOptions#modelElement */ /** - * @member {module:engine/view/containerelement~ContainerElement} module:widget/widgetresizer~ResizerOptions#viewElement + * @member {module:engine/view/containerelement~ContainerElement} module:widget/widgetresize~ResizerOptions#viewElement */ /** - * @member {module:engine/view/downcastwriter~DowncastWriter} module:widget/widgetresizer~ResizerOptions#downcastWriter + * @member {module:engine/view/downcastwriter~DowncastWriter} module:widget/widgetresize~ResizerOptions#downcastWriter */ /** - * @member {Function} module:widget/widgetresizer~ResizerOptions#onCommit + * @member {Function} module:widget/widgetresize~ResizerOptions#onCommit */ /** - * @member {Function} module:widget/widgetresizer~ResizerOptions#getResizeHost + * @member {Function} module:widget/widgetresize~ResizerOptions#getResizeHost */ /** - * @member {Function} module:widget/widgetresizer~ResizerOptions#getAspectRatio + * @member {Function} module:widget/widgetresize~ResizerOptions#getAspectRatio */ /** - * @member {Function} module:widget/widgetresizer~ResizerOptions#isCentered + * @member {Function} module:widget/widgetresize~ResizerOptions#isCentered */ diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index b0a3502b..64dd42f3 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -17,24 +17,29 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ResizeState from './resizerstate'; /** - * Stores the internal state of a single resizable object. + * Represents a resizer for a single resizable object. * - * @class Resizer * @mixes module:utils/observablemixin~ObservableMixin */ export default class Resizer { /** - * @param {module:widget/widgetresizer~ResizerOptions} options Resizer options. + * @param {module:widget/widgetresize~ResizerOptions} options Resizer options. */ constructor( options ) { /** + * Stores the state of resizable host geometry, such as original width, currently proposed height, etc. + * + * Note that a new state is created for each resize transaction. + * * @readonly - * @member {module:widget/widgetresizer/resizerstate~ResizerState} #state + * @member {module:widget/widgetresize/resizerstate~ResizerState} #state */ /** + * Options passed to the {@link #constructor}. + * * @private - * @type {module:widget/widgetresizer~ResizerOptions} + * @type {module:widget/widgetresize~ResizerOptions} */ this._options = options; @@ -49,6 +54,13 @@ export default class Resizer { */ this._domResizerWrapper = null; + /** + * A view displaying new proposed element's size during the resizing. + * + * @readonly + * @member {module:widget/widgetresize/resizer~SizeView} #sizeUI + */ + /** * @observable */ @@ -61,7 +73,7 @@ export default class Resizer { } /** - * + * Attaches the resizer to the DOM. */ attach() { const that = this; @@ -92,6 +104,14 @@ export default class Resizer { writer.addClass( 'ck-widget_with-resizer', widgetElement ); } + /** + * Starts the resizing process. + * + * Creates a new {@link #state} for current process. + * + * @fires begin + * @param {HTMLElement} domResizeHandle Clicked handle. + */ begin( domResizeHandle ) { this.state = new ResizeState( this._options ); @@ -102,6 +122,12 @@ export default class Resizer { this.redraw(); } + /** + * Updates the proposed size based on `domEventData`. + * + * @fires updateSize + * @param {Event} domEventData + */ updateSize( domEventData ) { const domHandleHost = this._getHandleHost(); const domResizeHost = this._getResizeHost(); @@ -128,6 +154,11 @@ export default class Resizer { this._domResizerWrapper.style.height = newSize.handleHostHeight + 'px'; } + /** + * Applies the geometry proposed with the resizer. + * + * @fires commit + */ commit() { const unit = this._options.unit; const newValue = ( unit === '%' ? this.state.proposedWidthPercents : this.state.proposedWidth ) + this._options.unit; @@ -139,17 +170,25 @@ export default class Resizer { /** * Cancels and rejects proposed resize dimensions hiding all the UI. + * + * @fires cancel */ cancel() { this._cleanup(); } + /** + * Destroys the resizer. + */ destroy() { this.cancel(); } - // TODO review this + /** + * Redraws the resizer. + */ redraw() { + // TODO review this const domWrapper = this._domResizerWrapper; if ( existsInDom( domWrapper ) ) { @@ -320,6 +359,8 @@ export default class Resizer { } /** + * Sets up the {@link #sizeUI} property and adds it to the passed `domElement`. + * * @private * @param {HTMLElement} domElement */ @@ -350,10 +391,31 @@ export default class Resizer { } } } + + /** + * @event begin + */ + + /** + * @event updateSize + */ + + /** + * @event commit + */ + + /** + * @event cancel + */ } mix( Resizer, ObservableMixin ); +/** + * A view displaying new proposed element's size during the resizing. + * + * @extends {module:ui/view~View} + */ class SizeView extends View { constructor() { super(); diff --git a/src/widgetresize/resizerstate.js b/src/widgetresize/resizerstate.js index 7e96ec8b..0262a07e 100644 --- a/src/widgetresize/resizerstate.js +++ b/src/widgetresize/resizerstate.js @@ -15,11 +15,10 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; /** * Stores the internal state of a single resizable object. * - * @class ResizeState */ export default class ResizeState { /** - * @param {module:widget/widgetresizer~ResizerOptions} options Resizer options. + * @param {module:widget/widgetresize~ResizerOptions} options Resizer options. */ constructor( options ) { /** @@ -92,7 +91,7 @@ export default class ResizeState { /** * @private - * @type {module:widget/widgetresizer~ResizerOptions} + * @type {module:widget/widgetresize~ResizerOptions} */ this._options = options; From 9797970845376bd078a647e03cc2035836bdd8b2 Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 19 Aug 2019 08:28:09 +0200 Subject: [PATCH 91/93] Docs: Added more docs to the options.onCommit. --- src/widgetresize.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/widgetresize.js b/src/widgetresize.js index 0ea03cca..222aff4c 100644 --- a/src/widgetresize.js +++ b/src/widgetresize.js @@ -130,6 +130,26 @@ export default class WidgetResize extends Plugin { */ /** + * A callback to be executed once resizing process is done. + * + * It receives a `Number` (`newValue`) as a parameter. + * + * For example, {@link module:image/imageresize~ImageResize} uses it to execute image resize command, + * which puts new value into the model. + * + * ```js + * { + * modelElement: data.item, + * viewElement: widget, + * downcastWriter: conversionApi.writer, + * + * onCommit( newValue ) { + * editor.execute( 'imageResize', { width: newValue } ); + * } + * }; + * ``` + * + * * @member {Function} module:widget/widgetresize~ResizerOptions#onCommit */ From 2e704e751fb5217d71e307a841000d8fc2e839ff Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 19 Aug 2019 09:02:49 +0200 Subject: [PATCH 92/93] Docs: Removed aspect ratio resizer option. --- src/widgetresize.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/widgetresize.js b/src/widgetresize.js index 222aff4c..94a1c5bf 100644 --- a/src/widgetresize.js +++ b/src/widgetresize.js @@ -157,10 +157,6 @@ export default class WidgetResize extends Plugin { * @member {Function} module:widget/widgetresize~ResizerOptions#getResizeHost */ -/** - * @member {Function} module:widget/widgetresize~ResizerOptions#getAspectRatio - */ - /** * @member {Function} module:widget/widgetresize~ResizerOptions#isCentered */ From 1a31082d815adf144dc154c1cf564b91bd20294e Mon Sep 17 00:00:00 2001 From: Marek Lewandowski Date: Mon, 19 Aug 2019 09:04:55 +0200 Subject: [PATCH 93/93] Internal: SizeUI property of the resizer has now become a protected member. --- src/widgetresize/resizer.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/widgetresize/resizer.js b/src/widgetresize/resizer.js index 64dd42f3..43b24f0a 100644 --- a/src/widgetresize/resizer.js +++ b/src/widgetresize/resizer.js @@ -35,6 +35,14 @@ export default class Resizer { * @member {module:widget/widgetresize/resizerstate~ResizerState} #state */ + /** + * A view displaying new proposed element's size during the resizing. + * + * @protected + * @readonly + * @member {module:widget/widgetresize/resizer~SizeView} #_sizeUI + */ + /** * Options passed to the {@link #constructor}. * @@ -54,13 +62,6 @@ export default class Resizer { */ this._domResizerWrapper = null; - /** - * A view displaying new proposed element's size during the resizing. - * - * @readonly - * @member {module:widget/widgetresize/resizer~SizeView} #sizeUI - */ - /** * @observable */ @@ -115,7 +116,7 @@ export default class Resizer { begin( domResizeHandle ) { this.state = new ResizeState( this._options ); - this.sizeUI.bindToState( this._options, this.state ); + this._sizeUI.bindToState( this._options, this.state ); this.state.begin( domResizeHandle, this._getHandleHost(), this._getResizeHost() ); @@ -232,8 +233,8 @@ export default class Resizer { * @protected */ _cleanup() { - this.sizeUI.dismiss(); - this.sizeUI.isVisible = false; + this._sizeUI.dismiss(); + this._sizeUI.isVisible = false; } /** @@ -359,7 +360,7 @@ export default class Resizer { } /** - * Sets up the {@link #sizeUI} property and adds it to the passed `domElement`. + * Sets up the {@link #_sizeUI} property and adds it to the passed `domElement`. * * @private * @param {HTMLElement} domElement @@ -370,7 +371,7 @@ export default class Resizer { // Make sure icon#element is rendered before passing to appendChild(). sizeUI.render(); - this.sizeUI = sizeUI; + this._sizeUI = sizeUI; domElement.appendChild( sizeUI.element ); }