8
0

resizecontext.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import View from '@ckeditor/ckeditor5-ui/src/view';
  2. import ResizerTopBound from './resizertopbound';
  3. import { getAbsoluteBoundaryPoint } from './utils';
  4. import Template from '@ckeditor/ckeditor5-ui/src/template';
  5. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  6. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  7. /**
  8. * @module widget/resizecontext
  9. */
  10. const WIDTH_ATTRIBUTE_NAME = 'width';
  11. /**
  12. * Stores the internal state of a single resizable object.
  13. *
  14. * @class ResizeContext
  15. */
  16. export default class ResizeContext {
  17. constructor( options ) {
  18. /**
  19. * View to a wrapper containing all the resizer-related views.
  20. *
  21. * @member {module:engine/view/uielement~UIElement}
  22. */
  23. this.resizeWrapperElement = null;
  24. /**
  25. * View of a widget associated with the resizer.
  26. *
  27. * @member {module:engine/view/element~Element}
  28. */
  29. this.widgetWrapperElement = null;
  30. this.resizeStrategy = new ResizerTopBound( this, options );
  31. /**
  32. * Container of entire resize UI.
  33. *
  34. * Note that this property is initialized only after the element bound with resizer is drawn
  35. * so it will be a `null` when uninitialized.
  36. *
  37. * @member {HTMLElement|null}
  38. */
  39. this.domResizeWrapper = null;
  40. /**
  41. * @member {HTMLElement|null}
  42. */
  43. this.domResizeShadow = null;
  44. this.options = options || {};
  45. // @todo: ---- options below seems like a little outside of a scope of a single context ----
  46. // Reference point of resizer where the dragging started. It is used to measure the distance to user cursor
  47. // traveled, thus how much the image should be enlarged.
  48. // This information is only known after DOM was rendered, so it will be updated later.
  49. this.referenceCoordinates = {
  50. y: 0,
  51. x: 0
  52. };
  53. /**
  54. * Size of an image before resize.
  55. *
  56. * This information is only known after DOM was rendered, so it will be updated later.
  57. */
  58. this.originalSize = {
  59. x: 0,
  60. y: 0
  61. };
  62. this._cleanupContext();
  63. }
  64. /**
  65. * Method to be called to attach a resizer to a given widget element.
  66. *
  67. * @param {module:engine/view/element~Element} widgetElement Widget's wrapper.
  68. * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  69. */
  70. attach( widgetElement, writer ) {
  71. const that = this;
  72. this.widgetWrapperElement = widgetElement;
  73. this.resizeWrapperElement = writer.createUIElement( 'div', {
  74. class: 'ck ck-widget__resizer-wrapper'
  75. }, function( domDocument ) {
  76. const domElement = this.toDomElement( domDocument );
  77. that.domResizeShadow = that._appendShadowElement( domDocument, domElement );
  78. that._appendResizers( that.domResizeShadow );
  79. that._appendSizeUi( that.domResizeShadow );
  80. that.domResizeWrapper = domElement;
  81. return domElement;
  82. } );
  83. // Append resizer wrapper to the widget's wrapper.
  84. writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), this.resizeWrapperElement );
  85. writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement );
  86. }
  87. /**
  88. *
  89. * @param {HTMLElement} domResizeHandler Handler used to calculate reference point.
  90. */
  91. begin( domResizeHandler ) {
  92. const resizeHost = this._getResizeHost();
  93. this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' );
  94. /**
  95. * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc or `null`
  96. * if unknown.
  97. *
  98. * @member {String|null}
  99. */
  100. this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler );
  101. this.set( 'orientation', this.referenceHandlerPosition );
  102. const reversedPosition = this._invertPosition( this.referenceHandlerPosition );
  103. this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition );
  104. // @todo: this part might be lazy used only in case if getAspectRatio is not given as it might force repaint.
  105. this.originalSize = {
  106. x: resizeHost.clientWidth,
  107. y: resizeHost.clientHeight
  108. };
  109. this.aspectRatio = this.options.getAspectRatio ?
  110. this.options.getAspectRatio( resizeHost ) : this.originalSize.x / this.originalSize.y;
  111. this.resizeStrategy.begin( domResizeHandler );
  112. }
  113. commit( editor ) {
  114. const modelEntry = this._getModel( editor, this.widgetWrapperElement );
  115. const newWidth = this.domResizeShadow.clientWidth;
  116. this._dismissShadow();
  117. editor.model.change( writer => {
  118. writer.setAttribute( WIDTH_ATTRIBUTE_NAME, newWidth, modelEntry );
  119. } );
  120. this.redraw();
  121. // Again, render will most likely change image size, so resizers needs a redraw.
  122. editor.editing.view.once( 'render', () => this.redraw() );
  123. this.resizeStrategy.commit( editor );
  124. this._cleanupContext();
  125. }
  126. cancel() {
  127. this._dismissShadow();
  128. this.resizeStrategy.cancel();
  129. this._cleanupContext();
  130. }
  131. _cleanupContext() {
  132. this.referenceHandlerPosition = null;
  133. this.set( {
  134. proposedX: null,
  135. proposedY: null,
  136. orientation: null
  137. } );
  138. }
  139. destroy() {
  140. this.cancel();
  141. this.domResizeShadow = null;
  142. this.wrapper = null;
  143. }
  144. updateSize( domEventData ) {
  145. const proposedSize = this.resizeStrategy.updateSize( domEventData );
  146. this.domResizeWrapper.style.width = proposedSize.x + 'px';
  147. this.domResizeWrapper.style.height = proposedSize.y + 'px';
  148. this.set( {
  149. proposedX: proposedSize.x,
  150. proposedY: proposedSize.y
  151. } );
  152. }
  153. redraw() {
  154. if ( this.domResizeWrapper ) {
  155. const widgetWrapper = this.domResizeWrapper.parentElement;
  156. const resizingHost = this._getResizeHost();
  157. if ( !widgetWrapper.isSameNode( resizingHost ) ) {
  158. this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px';
  159. this.domResizeWrapper.style.top = resizingHost.offsetTop + 'px';
  160. this.domResizeWrapper.style.height = resizingHost.offsetHeight + 'px';
  161. this.domResizeWrapper.style.width = resizingHost.offsetWidth + 'px';
  162. }
  163. }
  164. }
  165. _getResizeHost() {
  166. const widgetWrapper = this.domResizeWrapper.parentElement;
  167. return this.options.getResizeHost ?
  168. this.options.getResizeHost( widgetWrapper ) : widgetWrapper;
  169. }
  170. _appendShadowElement( domDocument, domElement ) {
  171. const shadowElement = domDocument.createElement( 'div' );
  172. shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' );
  173. domElement.appendChild( shadowElement );
  174. return shadowElement;
  175. }
  176. _appendResizers( domElement ) {
  177. const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ];
  178. for ( const currentPosition of resizerPositions ) {
  179. domElement.appendChild( ( new Template( {
  180. tag: 'div',
  181. attributes: {
  182. class: `ck-widget__resizer ${ this._getResizerClass( currentPosition ) }`
  183. }
  184. } ).render() ) );
  185. }
  186. }
  187. _appendSizeUi( domElement ) {
  188. const sizeUi = new SizeView();
  189. sizeUi.bind( 'isVisible' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) =>
  190. x !== null && y !== null );
  191. sizeUi.bind( 'label' ).to( this, 'proposedX', this, 'proposedY', ( x, y ) =>
  192. `${ Math.round( x ) }x${ Math.round( y ) }` );
  193. sizeUi.bind( 'orientation' ).to( this );
  194. // Make sure icon#element is rendered before passing to appendChild().
  195. sizeUi.render();
  196. this.sizeElement = sizeUi.element;
  197. domElement.appendChild( this.sizeElement );
  198. }
  199. _dismissShadow() {
  200. this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' );
  201. this.domResizeShadow.removeAttribute( 'style' );
  202. }
  203. /**
  204. *
  205. * @param {module:core/editor/editor~Editor} editor
  206. * @param {module:engine/view/element~Element} widgetWrapperElement
  207. * @returns {module:engine/model/element~Element|undefined}
  208. * @protected
  209. */
  210. _getModel( editor, widgetWrapperElement ) {
  211. return editor.editing.mapper.toModelElement( widgetWrapperElement );
  212. }
  213. _extractCoordinates( event ) {
  214. return {
  215. x: event.pageX,
  216. y: event.pageY
  217. };
  218. }
  219. /**
  220. * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`.
  221. * @returns {String} A prefixed HTML class name for the resizer element
  222. * @private
  223. */
  224. _getResizerClass( resizerPosition ) {
  225. return `ck-widget__resizer-${ resizerPosition }`;
  226. }
  227. /**
  228. * Determines the position of a given resize handler.
  229. *
  230. * @private
  231. * @param {HTMLElement} domResizeHandler Handler used to calculate reference point.
  232. * @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched.
  233. */
  234. _getResizerPosition( domResizeHandler ) {
  235. const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ];
  236. for ( const position of resizerPositions ) {
  237. if ( domResizeHandler.classList.contains( this._getResizerClass( position ) ) ) {
  238. return position;
  239. }
  240. }
  241. }
  242. /**
  243. * @param {String} position Like `"top-left"`.
  244. * @returns {String} Inverted `position`.
  245. * @protected
  246. */
  247. _invertPosition( position ) {
  248. const parts = position.split( '-' );
  249. const replacements = {
  250. top: 'bottom',
  251. bottom: 'top',
  252. left: 'right',
  253. right: 'left'
  254. };
  255. return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`;
  256. }
  257. }
  258. mix( ResizeContext, ObservableMixin );
  259. class SizeView extends View {
  260. constructor() {
  261. super();
  262. const bind = this.bindTemplate;
  263. this.setTemplate( {
  264. tag: 'div',
  265. attributes: {
  266. class: [
  267. 'ck',
  268. 'ck-size-view',
  269. bind.to( 'orientation', value => value ? `ck-orientation-${ value }` : '' )
  270. ],
  271. style: {
  272. display: bind.if( 'isVisible', 'none', visible => !visible )
  273. }
  274. },
  275. children: [ {
  276. text: bind.to( 'label' )
  277. } ]
  278. } );
  279. }
  280. }