8
0

widgetresize.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document, Event */
  6. import WidgetResize from '../src/widgetresize';
  7. // ClassicTestEditor can't be used, as it doesn't handle the focus, which is needed to test resizer visual cues.
  8. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
  9. import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
  10. import { toWidget } from '../src/utils';
  11. import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  12. import { mouseMock, focusEditor, getHandleCenterPoint, getWidgetDomParts } from './widgetresize/_utils/utils';
  13. describe( 'WidgetResize', () => {
  14. let editor, editorElement, widget, mouseListenerSpies;
  15. const commitStub = sinon.stub();
  16. before( () => {
  17. mouseListenerSpies = {
  18. down: sinon.spy( WidgetResize.prototype, '_mouseDownListener' ),
  19. move: sinon.spy( WidgetResize.prototype, '_mouseMoveListener' ),
  20. up: sinon.spy( WidgetResize.prototype, '_mouseUpListener' )
  21. };
  22. } );
  23. after( () => {
  24. for ( const stub of Object.values( mouseListenerSpies ) ) {
  25. stub.restore();
  26. }
  27. } );
  28. beforeEach( async () => {
  29. editorElement = createEditorElement();
  30. editor = await createEditor( editorElement );
  31. setModelData( editor.model, '[<widget></widget>]' );
  32. focusEditor( editor );
  33. widget = editor.editing.view.document.getRoot().getChild( 0 );
  34. for ( const stub of Object.values( mouseListenerSpies ) ) {
  35. stub.resetHistory();
  36. }
  37. commitStub.resetHistory();
  38. // It's crucial to have a precisely defined editor size for this test suite.
  39. editor.editing.view.change( writer => {
  40. const viewEditableRoot = editor.editing.view.document.getRoot();
  41. writer.setAttribute( 'style', 'width: 400px; padding: 0px; overflow: hidden', viewEditableRoot );
  42. } );
  43. } );
  44. afterEach( () => {
  45. editorElement.remove();
  46. if ( editor ) {
  47. return editor.destroy();
  48. }
  49. } );
  50. describe( 'plugin', () => {
  51. it( 'is loaded', () => {
  52. expect( editor.plugins.get( WidgetResize ) ).to.be.instanceOf( WidgetResize );
  53. } );
  54. } );
  55. describe( 'mouse listeners', () => {
  56. beforeEach( () => {
  57. createResizer();
  58. } );
  59. it( 'don\'t break when called with unexpected element', () => {
  60. const unrelatedElement = document.createElement( 'div' );
  61. editor.plugins.get( WidgetResize )._mouseDownListener( {}, {
  62. target: unrelatedElement
  63. } );
  64. } );
  65. it( 'passes new width to the options.onCommit()', () => {
  66. const usedResizer = 'top-right';
  67. const domParts = getWidgetDomParts( editor, widget, usedResizer );
  68. const initialPointerPosition = getHandleCenterPoint( domParts.widget, usedResizer );
  69. const finalPointerPosition = initialPointerPosition.clone().moveBy( 20, 0 );
  70. mouseMock.dragTo( editor, domParts.resizeHandle, finalPointerPosition );
  71. expect( commitStub.callCount ).to.be.equal( 1 );
  72. sinon.assert.calledWithExactly( commitStub, '120px' );
  73. } );
  74. } );
  75. it( 'are detached when plugin is destroyed', async () => {
  76. await editor.destroy();
  77. const plugin = editor.plugins.get( WidgetResize );
  78. editor = null;
  79. const event = new Event( 'mousedown', { bubbles: true } );
  80. document.body.dispatchEvent( event );
  81. // Ensure nothing got called.
  82. expect( plugin._mouseDownListener.callCount ).to.be.equal( 0 );
  83. } );
  84. it( 'nothing bad happens if activeResizer got unset', () => {
  85. createResizer( {
  86. isCentered: () => true
  87. } );
  88. const usedResizer = 'top-right';
  89. const domParts = getWidgetDomParts( editor, widget, usedResizer );
  90. const initialPointerPosition = getHandleCenterPoint( domParts.widget, usedResizer );
  91. editor.plugins.get( WidgetResize )._getResizerByHandle = sinon.stub().returns( null );
  92. mouseMock.dragTo( editor, domParts.resizeHandle, initialPointerPosition );
  93. // No exception should be thrown.
  94. } );
  95. describe( 'visibility', () => {
  96. beforeEach( () => {
  97. createResizer();
  98. } );
  99. it( 'it\'s hidden by default', () => {
  100. const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
  101. // Since widget is already focused, move the selection out of it.
  102. editor.model.change( writer => {
  103. const modelWidget = editor.editing.mapper.toModelElement( widget );
  104. writer.setSelection( modelWidget, 'before' );
  105. } );
  106. for ( const resizer of allResizers ) {
  107. expect( isVisible( resizer ) ).to.be.false;
  108. }
  109. } );
  110. it( 'it\'s visible once widget is focused', () => {
  111. // Widget is focused by default.
  112. const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
  113. for ( const resizer of allResizers ) {
  114. expect( isVisible( resizer ) ).to.be.true;
  115. }
  116. } );
  117. function isVisible( element ) {
  118. // Checks if the DOM element is visible to the end user.
  119. return element.offsetParent !== null && !element.classList.contains( 'ck-hidden' );
  120. }
  121. } );
  122. describe( 'integration (pixels)', () => {
  123. describe( 'aligned widget', () => {
  124. beforeEach( () => {
  125. createResizer();
  126. } );
  127. it( 'properly sets the state for subsequent resizes', () => {
  128. const usedResizer = 'top-right';
  129. const domParts = getWidgetDomParts( editor, widget, usedResizer );
  130. const initialPointerPosition = getHandleCenterPoint( domParts.widget, usedResizer );
  131. const intermediatePointerPosition = initialPointerPosition.clone().moveBy( 50, 0 );
  132. mouseMock.dragTo( editor, domParts.resizeHandle, intermediatePointerPosition );
  133. sinon.assert.calledWithExactly( commitStub.firstCall, '150px' );
  134. const finalPointerPosition = intermediatePointerPosition.clone().moveBy( 50, 0 );
  135. mouseMock.dragTo( editor, domParts.resizeHandle, finalPointerPosition );
  136. sinon.assert.calledWithExactly( commitStub.secondCall, '200px' );
  137. expect( commitStub.callCount ).to.be.equal( 2 );
  138. } );
  139. it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
  140. usedHandle: 'bottom-left',
  141. movePointerBy: { x: 20, y: -10 },
  142. expectedWidth: '80px'
  143. } ) );
  144. it( 'shrinks correctly with right-bottom handler', generateResizeTest( {
  145. usedHandle: 'bottom-right',
  146. movePointerBy: { x: -20, y: -10 },
  147. expectedWidth: '80px'
  148. } ) );
  149. it( 'shrinks correctly with left-top handler', generateResizeTest( {
  150. usedHandle: 'top-left',
  151. movePointerBy: { x: 20, y: 10 },
  152. expectedWidth: '80px'
  153. } ) );
  154. it( 'shrinks correctly with right-top handler', generateResizeTest( {
  155. usedHandle: 'top-right',
  156. movePointerBy: { x: -20, y: 10 },
  157. expectedWidth: '80px'
  158. } ) );
  159. it( 'enlarges correctly with left-bottom handler', generateResizeTest( {
  160. usedHandle: 'bottom-left',
  161. movePointerBy: { x: -10, y: 10 },
  162. expectedWidth: '120px'
  163. } ) );
  164. it( 'enlarges correctly with right-bottom handler', generateResizeTest( {
  165. usedHandle: 'bottom-right',
  166. movePointerBy: { x: 10, y: 10 },
  167. expectedWidth: '120px'
  168. } ) );
  169. it( 'enlarges correctly with right-bottom handler, y axis only', generateResizeTest( {
  170. usedHandle: 'bottom-right',
  171. movePointerBy: { x: 0, y: 20 },
  172. expectedWidth: '140px'
  173. } ) );
  174. it( 'enlarges correctly with right-bottom handler, x axis only', generateResizeTest( {
  175. usedHandle: 'bottom-right',
  176. movePointerBy: { x: 40, y: 0 },
  177. expectedWidth: '140px'
  178. } ) );
  179. it( 'enlarges correctly with left-top handler', generateResizeTest( {
  180. usedHandle: 'top-left',
  181. movePointerBy: { x: -20, y: -10 },
  182. expectedWidth: '120px'
  183. } ) );
  184. it( 'enlarges correctly with right-top handler', generateResizeTest( {
  185. usedHandle: 'top-right',
  186. movePointerBy: { x: 20, y: 10 },
  187. expectedWidth: '120px'
  188. } ) );
  189. } );
  190. describe( 'centered widget', () => {
  191. beforeEach( () => {
  192. createResizer( {
  193. isCentered: () => true
  194. } );
  195. } );
  196. it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
  197. usedHandle: 'bottom-left',
  198. movePointerBy: { x: 10, y: -10 },
  199. expectedWidth: '80px'
  200. } ) );
  201. it( 'shrinks correctly with right-bottom handler', generateResizeTest( {
  202. usedHandle: 'bottom-right',
  203. movePointerBy: { x: -10, y: -10 },
  204. expectedWidth: '80px'
  205. } ) );
  206. it( 'enlarges correctly with right-bottom handler, x axis only', generateResizeTest( {
  207. usedHandle: 'bottom-right',
  208. movePointerBy: { x: 10, y: 0 },
  209. expectedWidth: '120px'
  210. } ) );
  211. it( 'enlarges correctly with right-bottom handler, y axis only', generateResizeTest( {
  212. usedHandle: 'bottom-right',
  213. movePointerBy: { x: 0, y: 10 },
  214. expectedWidth: '120px'
  215. } ) );
  216. it( 'enlarges correctly with left-bottom handler, x axis only', generateResizeTest( {
  217. usedHandle: 'bottom-left',
  218. movePointerBy: { x: -10, y: 0 },
  219. expectedWidth: '120px'
  220. } ) );
  221. it( 'enlarges correctly with left-bottom handler, y axis only', generateResizeTest( {
  222. usedHandle: 'bottom-left',
  223. movePointerBy: { x: 0, y: 10 },
  224. expectedWidth: '120px'
  225. } ) );
  226. // --- top handlers ---
  227. it( 'enlarges correctly with left-top handler', generateResizeTest( {
  228. usedHandle: 'top-left',
  229. movePointerBy: { x: -10, y: -10 },
  230. expectedWidth: '120px'
  231. } ) );
  232. it( 'enlarges correctly with left-top handler, y axis only', generateResizeTest( {
  233. usedHandle: 'top-left',
  234. movePointerBy: { x: 0, y: -10 },
  235. expectedWidth: '120px'
  236. } ) );
  237. it( 'enlarges correctly with right-top handler', generateResizeTest( {
  238. usedHandle: 'top-right',
  239. movePointerBy: { x: 10, y: -10 },
  240. expectedWidth: '120px'
  241. } ) );
  242. it( 'enlarges correctly with right-top handler, y axis only', generateResizeTest( {
  243. usedHandle: 'top-right',
  244. movePointerBy: { x: 0, y: -10 },
  245. expectedWidth: '120px'
  246. } ) );
  247. } );
  248. } );
  249. describe( 'integration (percents)', () => {
  250. describe( 'side aligned widget', () => {
  251. beforeEach( () => {
  252. createResizer( {
  253. unit: undefined
  254. } );
  255. } );
  256. it( 'properly sets the state for subsequent resizes', () => {
  257. const usedResizer = 'top-right';
  258. const domParts = getWidgetDomParts( editor, widget, usedResizer );
  259. const initialPointerPosition = getHandleCenterPoint( domParts.widget, usedResizer );
  260. const intermediatePointerPosition = initialPointerPosition.clone().moveBy( 100, 0 );
  261. mouseMock.dragTo( editor, domParts.resizeHandle, intermediatePointerPosition );
  262. sinon.assert.calledWithExactly( commitStub.firstCall, '50%' );
  263. const finalPointerPosition = intermediatePointerPosition.clone().moveBy( 100, 0 );
  264. mouseMock.dragTo( editor, domParts.resizeHandle, finalPointerPosition );
  265. sinon.assert.calledWithExactly( commitStub.secondCall, '75%' );
  266. expect( commitStub.callCount ).to.be.equal( 2 );
  267. } );
  268. it( 'shrinks correctly with bottom-left handler', generateResizeTest( {
  269. usedHandle: 'bottom-left',
  270. movePointerBy: { x: 10, y: -10 },
  271. expectedWidth: '22.5%'
  272. } ) );
  273. } );
  274. describe( 'centered widget', () => {
  275. beforeEach( () => {
  276. createResizer( {
  277. isCentered: () => true,
  278. unit: undefined
  279. } );
  280. } );
  281. it( 'shrinks correctly with bottom-left handler', generateResizeTest( {
  282. usedHandle: 'bottom-left',
  283. movePointerBy: { x: 10, y: -10 },
  284. expectedWidth: '20%'
  285. } ) );
  286. it( 'enlarges correctly with bottom-right handler', generateResizeTest( {
  287. usedHandle: 'bottom-right',
  288. movePointerBy: { x: 0, y: 5 },
  289. expectedWidth: '27.5%'
  290. } ) );
  291. it( 'enlarges correctly an image with unsupported width unit', () => {
  292. editor.editing.view.change( writer => {
  293. writer.setStyle( 'width', '100pt', widget );
  294. } );
  295. generateResizeTest( {
  296. usedHandle: 'bottom-right',
  297. movePointerBy: { x: 0, y: 5 },
  298. expectedWidth: '36.67%'
  299. } )();
  300. } );
  301. } );
  302. } );
  303. /**
  304. * @param {Object} options
  305. * @param {String} options.usedHandle Handle that should be used for resize, e.g. 'bottom-right'.
  306. * @param {Object} options.movePointerBy How much should the pointer move during the drag compared to the initial position.
  307. * @param {String} options.expectedWidth
  308. */
  309. function generateResizeTest( options ) {
  310. return () => {
  311. options = options || {};
  312. const usedHandle = options.usedHandle;
  313. const domParts = getWidgetDomParts( editor, widget, usedHandle );
  314. const initialPointerPosition = getHandleCenterPoint( domParts.widget, usedHandle );
  315. const pointerDifference = options.movePointerBy;
  316. const finalPointerPosition = initialPointerPosition.moveBy( pointerDifference.x, pointerDifference.y );
  317. mouseMock.dragTo( editor, domParts.resizeHandle, finalPointerPosition );
  318. expect( commitStub.callCount, 'call count' ).to.be.eql( 1 );
  319. expect( commitStub.args[ 0 ][ 0 ], 'width' ).to.be.equal( options.expectedWidth );
  320. sinon.assert.calledOnce( commitStub );
  321. };
  322. }
  323. function createEditor( element ) {
  324. return ClassicEditor
  325. .create( element, {
  326. plugins: [
  327. ArticlePluginSet, WidgetResize, simpleWidgetPlugin
  328. ]
  329. } );
  330. function simpleWidgetPlugin( editor ) {
  331. editor.model.schema.register( 'widget', {
  332. inheritAllFrom: '$block',
  333. isObject: true
  334. } );
  335. editor.conversion.for( 'downcast' )
  336. .elementToElement( {
  337. model: 'widget',
  338. view: ( modelItem, viewWriter ) => {
  339. const div = viewWriter.createContainerElement( 'div' );
  340. viewWriter.setStyle( 'height', '50px', div );
  341. viewWriter.setStyle( 'width', '25%', div ); // It evaluates to 100px.
  342. return toWidget( div, viewWriter, {
  343. label: 'element label'
  344. } );
  345. }
  346. } );
  347. }
  348. }
  349. function createEditorElement() {
  350. const element = document.createElement( 'div' );
  351. document.body.appendChild( element );
  352. return element;
  353. }
  354. function createResizer( resizerOptions ) {
  355. const widgetModel = editor.model.document.getRoot().getChild( 0 );
  356. const defaultOptions = {
  357. unit: 'px',
  358. modelElement: widgetModel,
  359. viewElement: widget,
  360. editor,
  361. isCentered: () => false,
  362. getHandleHost( domWidgetElement ) {
  363. return domWidgetElement;
  364. },
  365. getResizeHost( domWidgetElement ) {
  366. return domWidgetElement;
  367. },
  368. onCommit: commitStub
  369. };
  370. return editor.plugins.get( WidgetResize ).attachTo( Object.assign( defaultOptions, resizerOptions ) );
  371. }
  372. } );