resizecontext.js 7.6 KB

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