Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Image resizer #94

Merged
merged 96 commits into from
Aug 19, 2019
Merged
Changes from 1 commit
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
9e6d7ce
Internal: Initial resizer API mocks.
mlewand Apr 29, 2019
d16d34b
Internal: Some basic logic to show up the resizers.
mlewand May 21, 2019
2a51e26
Added WidgetResizer plugin class.
mlewand Jun 26, 2019
df924cf
Internal: dirty version that just shows an overlay when resize handle…
mlewand Jun 27, 2019
d37af34
Internal: mocked very basic implementation for the ResizeContext.upda…
mlewand Jun 27, 2019
bdb110d
Internal: more precise resizing.
mlewand Jun 27, 2019
d7d17ef
Internal: mocked aligning the resize shadow with resize host width.
mlewand Jun 27, 2019
866de28
Internal: workaround to use render event to catch dom element that is…
mlewand Jun 28, 2019
c156ec4
Internal: next implementation of the resizer. This was required to ac…
mlewand Jun 28, 2019
8874fad
Internal: redraw the resizer's overlay once commit change has been re…
mlewand Jul 1, 2019
6d9a991
Internal: added img[height] entry to schema and a basic downcast conv…
mlewand Jul 1, 2019
da6528b
Internal: introduced img[height] upcast converter.
mlewand Jul 1, 2019
da3836c
Internal: enhanced CSS colors for the resized elements.
mlewand Jul 1, 2019
c9aa59a
Internal: added a support for resizing height using bottom handlers.
mlewand Jul 1, 2019
a989d0f
Internal: added extra render, otherwise handler would be mispositione…
mlewand Jul 1, 2019
b636ee6
Internal: now widget resizer accepts additional options. This will al…
mlewand Jul 1, 2019
d3b96bd
Internal: adjusted resizer CSS so that it doesn't block pointer event…
mlewand Jul 1, 2019
9751add
Internal: keep the resized object scale when using handlers.
mlewand Jul 5, 2019
97e1c8f
Internal: simplified resizing logic.
mlewand Jul 5, 2019
f68aeb1
Internal: made the resizing shadow follow the mouse cursor.
mlewand Jul 8, 2019
75cf6e4
Internal: removed unused context properties.
mlewand Jul 8, 2019
620d14c
Internal: introduced resized dimension information, also adjusted sty…
mlewand Jul 9, 2019
9e63118
Internal: adjusted resizing styles.
mlewand Jul 9, 2019
12ef207
Internal: make the sizer UI appear next to the dragged handler.
mlewand Jul 9, 2019
62f7d7f
Internal: fixed sizer alignment for bottom left corner.
mlewand Jul 9, 2019
45a1b8e
Internal: [WIP] delegating resizer strategy into a dedicated type.
mlewand Jul 10, 2019
2184992
Internal: refined resizing relative to the center point.
mlewand Jul 10, 2019
837288f
Internal: improved resizer visuals.
mlewand Jul 17, 2019
c9f3486
Internal: allow for providing a custom pixel ratio function.
mlewand Jul 17, 2019
769a0c4
Internal: added top-bound resizer.
mlewand Jul 18, 2019
43a1974
Internal: top-bound resizer now supports resizing focus host as well …
mlewand Jul 18, 2019
c515998
Internal: adjusted how aspect ratio is computed if no custom function…
mlewand Jul 19, 2019
728b351
Internal: exposed a observable method that allows you to implement a …
mlewand Jul 19, 2019
b446325
Internal: added a possibility to resize the image itself in resizerce…
mlewand Jul 22, 2019
b820a19
Internal: changed the default resizing strategy into center.
mlewand Jul 22, 2019
2430e23
Internal: changed the default resize type.
mlewand Jul 22, 2019
243e33f
Smaller resizer size and changed its position.
Jul 24, 2019
70df28b
Internal: Adjusted resize cursors.
mlewand Jul 25, 2019
26308f7
Internal: Use width inline style rather than height attribute.
mlewand Jul 25, 2019
670554d
Internal: Adjusted mouseover observer to trigger ~30 times per second.
mlewand Jul 25, 2019
a6a2305
Internal: Reduced resize handler outline thickness.
mlewand Jul 25, 2019
37e62e9
Improved resizer & tooltip styling.
Jul 25, 2019
a53b76f
Internal: Listening for events outside of the editable.
mlewand Jul 25, 2019
1520448
Internal: Added a possibility to specify whether resized object is ce…
mlewand Aug 2, 2019
350a249
Internal: Resizer wrapper will be sized based on element offset metrics.
mlewand Aug 5, 2019
8e605ca
Internal: Fixed an issue with resizer border after undoing.
mlewand Aug 6, 2019
92ecfa0
Internal: Fixed an issue where center-aligned image would not be prop…
mlewand Aug 7, 2019
bc4f934
Internal: Fixed a case where resize shadow would remain unaffected by…
mlewand Aug 7, 2019
3ed0812
Make sure resizer is visible only when the editor is focused.
mlewand Aug 8, 2019
fcd1c78
Internal: Further adjusted resizer focus selector.
mlewand Aug 9, 2019
af100d5
Internal: Minor code cleanup.
mlewand Aug 9, 2019
0e2903a
Internal: Simplified resizer's commit method.
mlewand Aug 9, 2019
2acf275
Internal: Removing leftovers after multiple resizing strategies.
mlewand Aug 12, 2019
5afaa59
Internal: Removed unused DOM observers.
mlewand Aug 12, 2019
a95ed36
Merge branch 'master' into t/ckeditor5-image/241
oleq Aug 12, 2019
a6d96c8
Internal: Removed unusued widget feature generic type.
mlewand Aug 12, 2019
29bbf4d
Docs: More resizer API docs.
mlewand Aug 12, 2019
deb43ab
Internal: Removed redundant code.
mlewand Aug 12, 2019
f5126e1
Internal: Fixed center resizing compensation. Also added some missing…
mlewand Aug 12, 2019
45416fe
Internal: Removed unused logic, simplified the code.
mlewand Aug 12, 2019
d06de01
Internal: Cleaned up some CSS.
mlewand Aug 12, 2019
d076a17
Internal: Moved most of methods from strategy to the context itself.
mlewand Aug 12, 2019
09c89d9
Internal: Merged ResizerTopBound class back into ResizeContext.
mlewand Aug 12, 2019
14d4dc6
Internal: Simplified UI building.
mlewand Aug 12, 2019
8001bc3
Internal: Use Rect type instead native DOM getBoundingClientRect.
mlewand Aug 12, 2019
7ca16fc
Internal: Cleanup docs and removed stray isActive variable.
mlewand Aug 12, 2019
0ab8a2f
Docs: Updated the docs.
mlewand Aug 12, 2019
4f4f444
Internal: Moved several properties to a protected scope.
mlewand Aug 12, 2019
6a29d70
Internal: Throttle mousemove/redraw events.
mlewand Aug 12, 2019
e0bb0da
Internal: WidgetResizer.apply will now return a created context. Also…
mlewand Aug 13, 2019
fdff936
Merge branch 'master' into t/ckeditor5-image/241
Reinmar Aug 13, 2019
05261ff
Cleanup, use px unit for width.
Reinmar Aug 13, 2019
c529432
Refactoring.
Reinmar Aug 13, 2019
0f09252
Refactoring.
Reinmar Aug 13, 2019
f360a43
Simplfied observers.
Reinmar Aug 13, 2019
45df167
Refactoring.
Reinmar Aug 13, 2019
11415d9
Intenral: Removed unused class.
mlewand Aug 13, 2019
fdb76ee
Internal: Extracted resizer state class.
mlewand Aug 14, 2019
293dfc7
Internal: Removed unused variables.
mlewand Aug 14, 2019
310661b
Move things around.
Reinmar Aug 14, 2019
c6d778f
Merge branch 'master' into t/ckeditor5-image/241
Reinmar Aug 14, 2019
b0d9081
Moar refactoring.
Reinmar Aug 14, 2019
bc97672
Removed the shadow thing concept.
Reinmar Aug 14, 2019
d5e67fc
Fixed classes naming.
Reinmar Aug 14, 2019
0fe6ebd
Naming.
Reinmar Aug 14, 2019
e4a62d8
Use RTL-proof character.
Reinmar Aug 14, 2019
97919bb
Improved naming.
Reinmar Aug 14, 2019
28971ce
Expose onCommit to the consumer.
Reinmar Aug 14, 2019
5a589f3
Round sizes.
Reinmar Aug 14, 2019
5f4e014
Introduced Resizer#isEnabled.
Reinmar Aug 16, 2019
197b0da
Internal: Added generic support for percent-based resizing.
mlewand Aug 16, 2019
51bbc5f
Added support for percents.
Reinmar Aug 16, 2019
5688291
Docs: Improved API docs.
mlewand Aug 19, 2019
9797970
Docs: Added more docs to the options.onCommit.
mlewand Aug 19, 2019
2e704e7
Docs: Removed aspect ratio resizer option.
mlewand Aug 19, 2019
1a31082
Internal: SizeUI property of the resizer has now become a protected m…
mlewand Aug 19, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Internal: Extracted resizer state class.
mlewand committed Aug 14, 2019
commit fdb76eefa2a42f1e14d918c022da23dcec5e2d25
238 changes: 41 additions & 197 deletions src/resizer.js
Original file line number Diff line number Diff line change
@@ -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,35 +153,13 @@ export default class Resizer {
*/
cancel() {
this._dismissShadow();

this._cleanupContext();
}

destroy() {
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 }`;
}
229 changes: 229 additions & 0 deletions src/resizestate.js
Original file line number Diff line number Diff line change
@@ -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 );