| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- /**
- * @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/widgetresize/resizer
- */
- import View from '@ckeditor/ckeditor5-ui/src/view';
- 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 './resizerstate';
- /**
- * Represents a resizer for a single resizable object.
- *
- * @mixes module:utils/observablemixin~ObservableMixin
- */
- export default class Resizer {
- /**
- * @param {module:widget/widgetresize~ResizerOptions} options Resizer options.
- */
- constructor( options ) {
- /**
- * Stores the state of the resizable host geometry, such as the original width, the currently proposed height, etc.
- *
- * Note that a new state is created for each resize transaction.
- *
- * @readonly
- * @member {module:widget/widgetresize/resizerstate~ResizerState} #state
- */
- /**
- * A view displaying the proposed new element size during the resizing.
- *
- * @protected
- * @readonly
- * @member {module:widget/widgetresize/resizer~SizeView} #_sizeUI
- */
- /**
- * Options passed to the {@link #constructor}.
- *
- * @private
- * @type {module:widget/widgetresize~ResizerOptions}
- */
- 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._domResizerWrapper = null;
- /**
- * @observable
- */
- this.set( 'isEnabled', true );
- this.decorate( 'begin' );
- this.decorate( 'cancel' );
- this.decorate( 'commit' );
- this.decorate( 'updateSize' );
- }
- /**
- * Attaches the resizer to the DOM.
- */
- attach() {
- const that = this;
- const widgetElement = this._options.viewElement;
- const writer = this._options.downcastWriter;
- const viewResizerWrapper = writer.createUIElement( 'div', {
- class: 'ck ck-reset_all ck-widget__resizer'
- }, function( domDocument ) {
- const domElement = this.toDomElement( domDocument );
- that._appendHandles( domElement );
- that._appendSizeUI( domElement );
- that._domResizerWrapper = domElement;
- that.on( 'change:isEnabled', ( evt, propName, newValue ) => {
- domElement.style.display = newValue ? '' : 'none';
- } );
- domElement.style.display = that.isEnabled ? '' : 'none';
- return domElement;
- } );
- // Append the resizer wrapper to the widget's wrapper.
- writer.insert( writer.createPositionAt( widgetElement, 'end' ), viewResizerWrapper );
- writer.addClass( 'ck-widget_with-resizer', widgetElement );
- }
- /**
- * Starts the resizing process.
- *
- * Creates a new {@link #state} for the current process.
- *
- * @fires begin
- * @param {HTMLElement} domResizeHandle Clicked handle.
- */
- begin( domResizeHandle ) {
- this.state = new ResizeState( this._options );
- this._sizeUI.bindToState( this._options, this.state );
- this.state.begin( domResizeHandle, this._getHandleHost(), this._getResizeHost() );
- this.redraw();
- }
- /**
- * Updates the proposed size based on `domEventData`.
- *
- * @fires updateSize
- * @param {Event} domEventData
- */
- updateSize( domEventData ) {
- const domHandleHost = this._getHandleHost();
- const domResizeHost = this._getResizeHost();
- const unit = this._options.unit;
- const newSize = this._proposeNewSize( domEventData );
- 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.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 = newSize.handleHostWidth + 'px';
- 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;
- this._options.onCommit( newValue );
- this._cleanup();
- }
- /**
- * Cancels and rejects the proposed resize dimensions, hiding the UI.
- *
- * @fires cancel
- */
- cancel() {
- this._cleanup();
- }
- /**
- * Destroys the resizer.
- */
- destroy() {
- this.cancel();
- }
- /**
- * Redraws the resizer.
- */
- redraw() {
- // TODO review this
- const domWrapper = this._domResizerWrapper;
- if ( existsInDom( domWrapper ) ) {
- // Refresh only if resizer exists in the DOM.
- const widgetWrapper = domWrapper.parentElement;
- const handleHost = this._getHandleHost();
- const clientRect = new Rect( handleHost );
- 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( handleHost ) ) {
- domWrapper.style.left = handleHost.offsetLeft + 'px';
- domWrapper.style.top = handleHost.offsetTop + 'px';
- domWrapper.style.height = handleHost.offsetHeight + 'px';
- domWrapper.style.width = handleHost.offsetWidth + 'px';
- }
- }
- function existsInDom( element ) {
- return element && element.ownerDocument && element.ownerDocument.contains( element );
- }
- }
- containsHandle( domElement ) {
- return this._domResizerWrapper.contains( domElement );
- }
- static isResizeHandle( domElement ) {
- return domElement.classList.contains( 'ck-widget__resizer__handle' );
- }
- /**
- * Cleans up the context state.
- *
- * @protected
- */
- _cleanup() {
- this._sizeUI.dismiss();
- this._sizeUI.isVisible = false;
- }
- /**
- * Calculates 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.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;
- // 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 | |
- // | /handle host | |
- // +----------------+ | ^
- // | | | - enlarge y
- // +-------------------+ v
- // <-->
- // enlarge x
- const enlargement = {
- 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 + state.originalWidth );
- }
- // 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( state.originalWidth + enlargement.x ),
- height: Math.abs( state.originalHeight + 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 {
- width: Math.round( targetSize.width ),
- height: Math.round( targetSize.height ),
- widthPercents: Math.min( Math.round( state.originalWidthPercents / state.originalWidth * targetSize.width * 100 ) / 100, 100 )
- };
- }
- /**
- * Obtains the resize host.
- *
- * Resize host is an object that receives dimensions which are the result of resizing.
- *
- * @protected
- * @returns {HTMLElement}
- */
- _getResizeHost() {
- const widgetWrapper = this._domResizerWrapper.parentElement;
- return this._options.getResizeHost( widgetWrapper );
- }
- /**
- * Obtains the handle host.
- *
- * Handle host is an object that the handles are aligned to.
- *
- * Handle host will not always be an entire widget itself. Take an image as an example. The image widget
- * contains an image and a caption. Only the image should be surrounded with handles.
- *
- * @protected
- * @returns {HTMLElement}
- */
- _getHandleHost() {
- const widgetWrapper = this._domResizerWrapper.parentElement;
- return this._options.getHandleHost( widgetWrapper );
- }
- /**
- * Renders the resize handles in the DOM.
- *
- * @private
- * @param {HTMLElement} domElement The resizer wrapper.
- */
- _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__handle ${ getResizerClass( currentPosition ) }`
- }
- } ).render() ) );
- }
- }
- /**
- * Sets up the {@link #_sizeUI} property and adds it to the passed `domElement`.
- *
- * @private
- * @param {HTMLElement} domElement
- */
- _appendSizeUI( domElement ) {
- const sizeUI = new SizeView();
- // Make sure icon#element is rendered before passing to appendChild().
- sizeUI.render();
- this._sizeUI = sizeUI;
- domElement.appendChild( sizeUI.element );
- }
- /**
- * Determines the position of a given resize handle.
- *
- * @private
- * @param {HTMLElement} domHandle Handler used to calculate the reference point.
- * @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched.
- */
- _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;
- }
- }
- }
- /**
- * @event begin
- */
- /**
- * @event updateSize
- */
- /**
- * @event commit
- */
- /**
- * @event cancel
- */
- }
- mix( Resizer, ObservableMixin );
- /**
- * A view displaying the proposed new element size during the resizing.
- *
- * @extends {module:ui/view~View}
- */
- class SizeView extends View {
- constructor() {
- super();
- const bind = this.bindTemplate;
- this.setTemplate( {
- tag: 'div',
- attributes: {
- class: [
- 'ck',
- 'ck-size-view',
- bind.to( 'activeHandlePosition', value => value ? `ck-orientation-${ value }` : '' )
- ],
- style: {
- display: bind.if( 'isVisible', 'none', visible => !visible )
- }
- },
- children: [ {
- text: bind.to( 'label' )
- } ]
- } );
- }
- bindToState( options, resizerState ) {
- this.bind( 'isVisible' ).to( resizerState, 'proposedWidth', resizerState, 'proposedHeight', ( width, height ) =>
- width !== null && height !== null );
- this.bind( 'label' ).to(
- resizerState, 'proposedHandleHostWidth',
- resizerState, 'proposedHandleHostHeight',
- resizerState, 'proposedWidthPercents',
- ( width, height, widthPercents ) => {
- if ( options.unit === 'px' ) {
- return `${ width }×${ height }`;
- } else {
- return `${ widthPercents }%`;
- }
- }
- );
- this.bind( 'activeHandlePosition' ).to( resizerState );
- }
- dismiss() {
- this.unbind();
- this.isVisible = false;
- }
- }
- // @private
- // @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__handle-${ resizerPosition }`;
- }
- function extractCoordinates( event ) {
- return {
- x: event.pageX,
- y: event.pageY
- };
- }
|