8
0

resizecontext.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview';
  2. import dragHandlerIcon from '../theme/icons/drag-handler.svg';
  3. /**
  4. * @module widget/resizecontext
  5. */
  6. const HEIGHT_ATTRIBUTE_NAME = 'height';
  7. /**
  8. * Returns coordinates of top-left corner of a element, relative to the document's top-left corner.
  9. *
  10. * @param {HTMLElement} element
  11. * @param {String} resizerPosition Position of the resize handler, e.g. `"top-left"`, `"bottom-right"`.
  12. * @returns {Object} return
  13. * @returns {Number} return.x
  14. * @returns {Number} return.y
  15. */
  16. function getAbsoluteBoundaryPoint( element, resizerPosition ) {
  17. const nativeRectangle = element.getBoundingClientRect();
  18. const positionParts = resizerPosition.split( '-' );
  19. const ret = {
  20. x: positionParts[ 1 ] == 'right' ? nativeRectangle.right : nativeRectangle.left,
  21. y: positionParts[ 0 ] == 'bottom' ? nativeRectangle.bottom : nativeRectangle.top
  22. };
  23. ret.x += element.ownerDocument.defaultView.scrollX;
  24. ret.y += element.ownerDocument.defaultView.scrollY;
  25. return ret;
  26. }
  27. function getAspectRatio( element ) {
  28. const nativeRectangle = element.getBoundingClientRect();
  29. return nativeRectangle.width / nativeRectangle.height;
  30. }
  31. /**
  32. * Stores the internal state of a single resizable object.
  33. *
  34. * @class ResizeContext
  35. */
  36. export default class ResizeContext {
  37. constructor( options ) {
  38. // HTMLElement??? - @todo seems to be not needed.
  39. // this.resizeHost = null;
  40. // view/UiElement
  41. this.resizeWrapperElement = null;
  42. // view/Element
  43. this.widgetWrapperElement = null;
  44. /**
  45. * Container of entire resize UI.
  46. *
  47. * Note that this property is initialized only after the element bound with resizer is drawn
  48. * so it will be a `null` when uninitialized.
  49. *
  50. * @member {HTMLElement|null}
  51. */
  52. this.domResizeWrapper = null;
  53. /**
  54. * @member {HTMLElement|null}
  55. */
  56. this.domResizeShadow = null;
  57. this.options = options || {};
  58. // @todo: ---- options below seems like a little outside of a scope of a single context ----
  59. // Size before resizing.
  60. this.initialSize = {
  61. x: 0,
  62. y: 0
  63. };
  64. // Position of a clicked resize handler in x and y axes.
  65. this.direction = {
  66. y: 'top',
  67. x: 'left'
  68. };
  69. // Reference point of resizer where the dragging started. It is used to measure the distance to user cursor
  70. // traveled, thus how much the image should be enlarged.
  71. // This information is only known after DOM was rendered, so it will be updated later.
  72. this.referenceCoordinates = {
  73. y: 0,
  74. x: 0
  75. };
  76. }
  77. /**
  78. *
  79. * @param {module:engine/view/element~Element} widgetElement Widget's wrapper.
  80. * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  81. */
  82. attach( widgetElement, writer ) {
  83. const that = this;
  84. this.widgetWrapperElement = widgetElement;
  85. this.resizeWrapperElement = writer.createUIElement( 'div', {
  86. class: 'ck ck-widget__resizer-wrapper'
  87. }, function( domDocument ) {
  88. const domElement = this.toDomElement( domDocument );
  89. that.domResizeShadow = that._appendShadowElement( domDocument, domElement );
  90. that._appendResizers( domElement );
  91. that.domResizeWrapper = domElement;
  92. return domElement;
  93. } );
  94. // Append resizer wrapper to the widget's wrapper.
  95. writer.insert( writer.createPositionAt( widgetElement, widgetElement.childCount ), this.resizeWrapperElement );
  96. writer.addClass( [ 'ck-widget_with-resizer' ], widgetElement );
  97. }
  98. /**
  99. *
  100. * @param {HTMLElement} domResizeHandler Handler used to calculate reference point.
  101. */
  102. begin( domResizeHandler ) {
  103. const resizeHost = this._getResizeHost();
  104. this.domResizeShadow.classList.add( 'ck-widget__resizer-shadow-active' );
  105. /**
  106. * Position of the handler that has initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc of `null`
  107. * if unknown.
  108. *
  109. * @member {String|null}
  110. */
  111. this.referenceHandlerPosition = this._getResizerPosition( domResizeHandler );
  112. const reversedPosition = this._invertPosition( this.referenceHandlerPosition );
  113. this.referenceCoordinates = getAbsoluteBoundaryPoint( resizeHost, reversedPosition );
  114. if ( resizeHost ) {
  115. this.aspectRatio = getAspectRatio( resizeHost, this.referenceHandlerPosition );
  116. }
  117. }
  118. commit( editor ) {
  119. const modelEntry = this._getModel( editor, this.widgetWrapperElement );
  120. const newHeight = this.domResizeShadow.clientHeight;
  121. this._dismissShadow();
  122. editor.model.change( writer => {
  123. writer.setAttribute( HEIGHT_ATTRIBUTE_NAME, newHeight, modelEntry );
  124. } );
  125. this.redraw();
  126. // Again, render will most likely change image size, so resizers needs a redraw.
  127. editor.editing.view.once( 'render', () => this.redraw() );
  128. this.referenceHandlerPosition = null;
  129. }
  130. cancel() {
  131. this._dismissShadow();
  132. this.referenceHandlerPosition = null;
  133. }
  134. destroy() {
  135. this.cancel();
  136. this.domResizeShadow = null;
  137. this.wrapper = null;
  138. this.referenceHandlerPosition = null;
  139. }
  140. updateSize( domEventData ) {
  141. const currentCoordinates = this._extractCoordinates( domEventData );
  142. const proposedSize = {
  143. x: Math.abs( currentCoordinates.x - this.referenceCoordinates.x ),
  144. y: Math.abs( currentCoordinates.y - this.referenceCoordinates.y )
  145. };
  146. // Dominant determination must take the ratio into account.
  147. proposedSize.dominant = proposedSize.x / this.aspectRatio > proposedSize.y ? 'x' : 'y';
  148. proposedSize.max = proposedSize[ proposedSize.dominant ];
  149. const drawnSize = {
  150. x: proposedSize.x,
  151. y: proposedSize.y
  152. };
  153. if ( proposedSize.dominant == 'x' ) {
  154. drawnSize.y = drawnSize.x / this.aspectRatio;
  155. } else {
  156. drawnSize.x = drawnSize.y * this.aspectRatio;
  157. }
  158. // Reset shadow bounding.
  159. this.domResizeShadow.style.top = 0;
  160. this.domResizeShadow.style.left = 0;
  161. this.domResizeShadow.style.bottom = 0;
  162. this.domResizeShadow.style.right = 0;
  163. this.domResizeShadow.style[ this.referenceHandlerPosition.split( '-' )[ 0 ] ] = 'auto';
  164. this.domResizeShadow.style[ this.referenceHandlerPosition.split( '-' )[ 1 ] ] = 'auto';
  165. // Apply the actual shadow dimensions.
  166. this.domResizeShadow.style.width = `${ drawnSize.x }px`;
  167. this.domResizeShadow.style.height = `${ drawnSize.y }px`;
  168. }
  169. redraw() {
  170. if ( this.domResizeWrapper ) {
  171. const widgetWrapper = this.domResizeWrapper.parentElement;
  172. const resizingHost = this._getResizeHost();
  173. if ( !widgetWrapper.isSameNode( resizingHost ) ) {
  174. this.domResizeWrapper.style.left = resizingHost.offsetLeft + 'px';
  175. this.domResizeWrapper.style.right = resizingHost.offsetLeft + 'px';
  176. }
  177. }
  178. }
  179. _getResizeHost() {
  180. const widgetWrapper = this.domResizeWrapper.parentElement;
  181. return this.options.getResizeHost ?
  182. this.options.getResizeHost( widgetWrapper ) : widgetWrapper;
  183. }
  184. _appendShadowElement( domDocument, domElement ) {
  185. const shadowElement = domDocument.createElement( 'div' );
  186. shadowElement.setAttribute( 'class', 'ck ck-widget__resizer-shadow' );
  187. domElement.appendChild( shadowElement );
  188. return shadowElement;
  189. }
  190. _appendResizers( domElement ) {
  191. const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ];
  192. for ( const currentPosition of resizerPositions ) {
  193. // Use the IconView from the UI library.
  194. const icon = new IconView();
  195. icon.set( 'content', dragHandlerIcon );
  196. icon.extendTemplate( {
  197. attributes: {
  198. 'class': `ck-widget__resizer ${ this._getResizerClass( currentPosition ) }`
  199. }
  200. } );
  201. // Make sure icon#element is rendered before passing to appendChild().
  202. icon.render();
  203. domElement.appendChild( icon.element );
  204. }
  205. }
  206. _dismissShadow() {
  207. this.domResizeShadow.classList.remove( 'ck-widget__resizer-shadow-active' );
  208. this.domResizeShadow.removeAttribute( 'style' );
  209. }
  210. /**
  211. *
  212. * @param {module:@ckeditor/ckeditor5-core/src/editor/editor~Editor} editor
  213. * @param {module:@ckeditor/ckeditor5-engine/src/view/element~Element} widgetWrapperElement
  214. * @returns {module:@ckeditor/ckeditor5-engine/src/model/element~Element|undefined}
  215. */
  216. _getModel( editor, widgetWrapperElement ) {
  217. return editor.editing.mapper.toModelElement( widgetWrapperElement );
  218. }
  219. _extractCoordinates( event ) {
  220. return {
  221. x: event.domEvent.pageX,
  222. y: event.domEvent.pageY
  223. };
  224. }
  225. /**
  226. * @private
  227. * @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`.
  228. * @returns {String} A prefixed HTML class name for the resizer element
  229. */
  230. _getResizerClass( resizerPosition ) {
  231. return `ck-widget__resizer-${ resizerPosition }`;
  232. }
  233. /**
  234. * Determines the position of a given resize handler.
  235. *
  236. * @private
  237. * @param {HTMLElement} domResizeHandler Handler used to calculate reference point.
  238. * @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched.
  239. */
  240. _getResizerPosition( domResizeHandler ) {
  241. const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ];
  242. for ( const position of resizerPositions ) {
  243. if ( domResizeHandler.classList.contains( this._getResizerClass( position ) ) ) {
  244. return position;
  245. }
  246. }
  247. }
  248. /**
  249. * @param {String} position Like `"top-left"`.
  250. * @returns {String} Inverted `position`.
  251. */
  252. _invertPosition( position ) {
  253. const parts = position.split( '-' );
  254. const replacements = {
  255. top: 'bottom',
  256. bottom: 'top',
  257. left: 'right',
  258. right: 'left'
  259. };
  260. return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`;
  261. }
  262. }