resize.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* global document, window */
  6. // ClassicTestEditor can't be used, as it doesn't handle the focus, which is needed to test resizer visual cues.
  7. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
  8. import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  9. import ImagePlugin from '../../src/image';
  10. import ImageStyle from '../../src/imagestyle';
  11. import UndoPlugin from '@ckeditor/ckeditor5-undo/src/undo';
  12. import TablePlugin from '@ckeditor/ckeditor5-table/src/table';
  13. import {
  14. getData
  15. } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  16. describe( 'Image resizer', () => {
  17. const FIXTURE_WIDTH = 100;
  18. const FIXTURE_HEIGHT = 50;
  19. const MOUSE_BUTTON_MAIN = 0; // Id of left mouse button.
  20. // 60x30 black png image
  21. const imageFixture = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
  22. 'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
  23. let editor, view, viewDocument, editorElement;
  24. beforeEach( () => {
  25. editorElement = document.createElement( 'div' );
  26. editorElement.innerHTML = `<p>foo</p><figure><img src="${ imageFixture }"></figure>`;
  27. document.body.appendChild( editorElement );
  28. return ClassicEditor
  29. .create( editorElement, {
  30. plugins: [ ImagePlugin, ImageStyle, ParagraphPlugin, UndoPlugin, TablePlugin ]
  31. } )
  32. .then( newEditor => {
  33. editor = newEditor;
  34. view = editor.editing.view;
  35. viewDocument = view.document;
  36. } );
  37. } );
  38. afterEach( () => {
  39. editorElement.remove();
  40. return editor.destroy();
  41. } );
  42. describe( 'visual resizers', () => {
  43. it( 'correct amount is added by default', () => {
  44. const resizers = document.querySelectorAll( '.ck-widget__resizer' );
  45. expect( resizers.length ).to.be.equal( 4 );
  46. } );
  47. describe( 'visibility', () => {
  48. it( 'is hidden by default', () => {
  49. const allResizers = document.querySelectorAll( '.ck-widget__resizer' );
  50. for ( const resizer of allResizers ) {
  51. expect( isVisible( resizer ) ).to.be.false;
  52. }
  53. } );
  54. it( 'is shown when image is focused', () => {
  55. const widget = viewDocument.getRoot().getChild( 1 );
  56. const allResizers = document.querySelectorAll( '.ck-widget__resizer' );
  57. const domEventDataMock = {
  58. target: widget,
  59. preventDefault: sinon.spy()
  60. };
  61. focusEditor( editor );
  62. viewDocument.fire( 'mousedown', domEventDataMock );
  63. for ( const resizer of allResizers ) {
  64. expect( isVisible( resizer ) ).to.be.true;
  65. }
  66. } );
  67. } );
  68. } );
  69. describe( 'standard image', () => {
  70. let widget;
  71. beforeEach( async function() {
  72. await editor.setData( `<p>foo</p><figure><img src="${ imageFixture }"></figure>` );
  73. widget = viewDocument.getRoot().getChild( 1 );
  74. const domEventDataMock = {
  75. target: widget,
  76. preventDefault: sinon.spy()
  77. };
  78. this.widget = widget;
  79. viewDocument.fire( 'mousedown', domEventDataMock );
  80. } );
  81. it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
  82. expectedWidth: 80,
  83. pointerOffset: {
  84. x: 10,
  85. y: -10
  86. },
  87. resizerPosition: 'bottom-left'
  88. } ) );
  89. it( 'shrinks correctly with right-bottom handler', generateResizeTest( {
  90. expectedWidth: 80,
  91. pointerOffset: {
  92. x: -10,
  93. y: -10
  94. },
  95. resizerPosition: 'bottom-right'
  96. } ) );
  97. it( 'enlarges correctly with right-bottom handler, x axis only', generateResizeTest( {
  98. expectedWidth: 110,
  99. pointerOffset: {
  100. x: 10,
  101. y: 0
  102. },
  103. resizerPosition: 'bottom-right'
  104. } ) );
  105. it( 'enlarges correctly with right-bottom handler, y axis only', generateResizeTest( {
  106. expectedWidth: 120,
  107. pointerOffset: {
  108. x: 0,
  109. y: 10
  110. },
  111. resizerPosition: 'bottom-right'
  112. } ) );
  113. it( 'enlarges correctly with left-bottom handler, x axis only', generateResizeTest( {
  114. expectedWidth: 110,
  115. pointerOffset: {
  116. x: -10,
  117. y: 0
  118. },
  119. resizerPosition: 'bottom-left'
  120. } ) );
  121. it( 'enlarges correctly with left-bottom handler, y axis only', generateResizeTest( {
  122. expectedWidth: 120,
  123. pointerOffset: {
  124. x: 0,
  125. y: 10
  126. },
  127. resizerPosition: 'bottom-left'
  128. } ) );
  129. // --- top handlers ---
  130. it( 'enlarges correctly with left-top handler', generateResizeTest( {
  131. expectedWidth: 120,
  132. pointerOffset: {
  133. x: -10,
  134. y: -10
  135. },
  136. resizerPosition: 'top-left'
  137. } ) );
  138. it( 'enlarges correctly with left-top handler, y axis only', generateResizeTest( {
  139. expectedWidth: 120,
  140. pointerOffset: {
  141. x: 0,
  142. y: -10
  143. },
  144. resizerPosition: 'top-left'
  145. } ) );
  146. it( 'enlarges correctly with right-top handler', generateResizeTest( {
  147. expectedWidth: 120,
  148. pointerOffset: {
  149. x: 10,
  150. y: -10
  151. },
  152. resizerPosition: 'top-right'
  153. } ) );
  154. it( 'enlarges correctly with right-top handler, y axis only', generateResizeTest( {
  155. expectedWidth: 120,
  156. pointerOffset: {
  157. x: 0,
  158. y: -10
  159. },
  160. resizerPosition: 'top-right'
  161. } ) );
  162. } );
  163. describe( 'side image', () => {
  164. let widget;
  165. beforeEach( async function() {
  166. await editor.setData( `<p>foo</p><figure class="image image-style-side"><img src="${ imageFixture }"></figure>` );
  167. widget = viewDocument.getRoot().getChild( 1 );
  168. const domEventDataMock = {
  169. target: widget,
  170. preventDefault: sinon.spy()
  171. };
  172. this.widget = widget;
  173. viewDocument.fire( 'mousedown', domEventDataMock );
  174. } );
  175. it( 'shrinks correctly with left-bottom handler', generateSideResizeTest( {
  176. isSideImage: true,
  177. expectedWidth: 80,
  178. pointerOffset: {
  179. x: 20,
  180. y: -10
  181. },
  182. resizerPosition: 'bottom-left'
  183. } ) );
  184. it( 'shrinks correctly with right-bottom handler', generateSideResizeTest( {
  185. isSideImage: true,
  186. expectedWidth: 80,
  187. pointerOffset: {
  188. x: -20,
  189. y: -10
  190. },
  191. resizerPosition: 'bottom-right'
  192. } ) );
  193. it( 'shrinks correctly with left-top handler', generateSideResizeTest( {
  194. isSideImage: true,
  195. expectedWidth: 80,
  196. pointerOffset: {
  197. x: 20,
  198. y: 10
  199. },
  200. resizerPosition: 'top-left'
  201. } ) );
  202. it( 'shrinks correctly with right-top handler', generateSideResizeTest( {
  203. isSideImage: true,
  204. expectedWidth: 80,
  205. pointerOffset: {
  206. x: -20,
  207. y: 10
  208. },
  209. resizerPosition: 'top-right'
  210. } ) );
  211. it( 'enlarges correctly with left-bottom handler', generateSideResizeTest( {
  212. isSideImage: true,
  213. expectedWidth: 120,
  214. pointerOffset: {
  215. x: -10,
  216. y: 10
  217. },
  218. resizerPosition: 'bottom-left'
  219. } ) );
  220. it( 'enlarges correctly with right-bottom handler', generateSideResizeTest( {
  221. isSideImage: true,
  222. expectedWidth: 120,
  223. pointerOffset: {
  224. x: 10,
  225. y: 10
  226. },
  227. resizerPosition: 'bottom-right'
  228. } ) );
  229. it( 'enlarges correctly with right-bottom handler, y axis only', generateSideResizeTest( {
  230. isSideImage: true,
  231. expectedWidth: 140,
  232. pointerOffset: {
  233. x: 0,
  234. y: 20
  235. },
  236. resizerPosition: 'bottom-right'
  237. } ) );
  238. it( 'enlarges correctly with right-bottom handler, x axis only', generateSideResizeTest( {
  239. isSideImage: true,
  240. expectedWidth: 140,
  241. pointerOffset: {
  242. x: 40,
  243. y: 0
  244. },
  245. resizerPosition: 'bottom-right'
  246. } ) );
  247. it( 'enlarges correctly with left-top handler', generateSideResizeTest( {
  248. isSideImage: true,
  249. expectedWidth: 120,
  250. pointerOffset: {
  251. x: -20,
  252. y: -10
  253. },
  254. resizerPosition: 'top-left'
  255. } ) );
  256. it( 'enlarges correctly with right-top handler', generateSideResizeTest( {
  257. isSideImage: true,
  258. expectedWidth: 120,
  259. pointerOffset: {
  260. x: 20,
  261. y: 10
  262. },
  263. resizerPosition: 'top-right'
  264. } ) );
  265. function generateSideResizeTest( options ) {
  266. return generateResizeTest( Object.assign( {
  267. isSideImage: true,
  268. modelRegExp: /<paragraph>foo<\/paragraph><image imageStyle="side" src=".+?" width="(\d+)"><\/image>/
  269. }, options ) );
  270. }
  271. } );
  272. describe( 'undo integration', function() {
  273. let widget;
  274. beforeEach( async function() {
  275. await editor.setData( `<p>foo</p><figure><img src="${ imageFixture }"></figure>` );
  276. widget = viewDocument.getRoot().getChild( 1 );
  277. const domEventDataMock = {
  278. target: widget,
  279. preventDefault: sinon.spy()
  280. };
  281. this.widget = widget;
  282. viewDocument.fire( 'mousedown', domEventDataMock );
  283. } );
  284. it( 'has correct border size after undo', function() {
  285. return generateResizeTest( {
  286. expectedWidth: 120,
  287. pointerOffset: {
  288. x: 0,
  289. y: 10
  290. },
  291. resizerPosition: 'bottom-left'
  292. } ).call( this ).then( () => {
  293. editor.commands.get( 'undo' ).execute();
  294. const resizerShadow = document.querySelector( '.ck-widget__resizer-shadow' );
  295. const shadowBoundingRect = resizerShadow.getBoundingClientRect();
  296. expect( shadowBoundingRect.width ).to.be.equal( 100 );
  297. expect( shadowBoundingRect.height ).to.be.equal( 50 );
  298. } );
  299. } );
  300. } );
  301. describe( 'integration', function() {
  302. beforeEach( async function() {
  303. await editor.setData( `<figure class="table">
  304. <table>
  305. <tbody>
  306. <tr>
  307. <td>
  308. <figure class="image"><img src="${ imageFixture }" alt="Sample image">
  309. </figure>
  310. </td>
  311. </tr>
  312. </tbody>
  313. </table>
  314. </figure>` );
  315. this.widget = viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
  316. const domEventDataMock = {
  317. target: this.widget,
  318. preventDefault: sinon.spy()
  319. };
  320. viewDocument.fire( 'mousedown', domEventDataMock );
  321. } );
  322. it( 'works when resizing in a table', function() {
  323. return generateResizeTest( {
  324. getModel: () => editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ),
  325. expectedWidth: 60,
  326. modelRegExp: /.+/,
  327. pointerOffset: {
  328. x: -40,
  329. y: -20
  330. },
  331. resizerPosition: 'bottom-right'
  332. } );
  333. } );
  334. } );
  335. function isVisible( element ) {
  336. // Checks if the DOM element is visible to the end user.
  337. return element.offsetParent !== null;
  338. }
  339. function fireMouseEvent( target, eventType, eventData ) {
  340. // Using initMouseEvent instead of MouseEvent constructor, as MouseEvent constructor doesn't support passing pageX
  341. // and pageY. See https://stackoverflow.com/questions/45843458/setting-click-events-pagex-and-pagey-always-reverts-to-0
  342. // However there's still a problem, that events created with `initMouseEvent` have **floored** pageX, pageY numbers.
  343. const event = document.createEvent( 'MouseEvent' );
  344. event.initMouseEvent( eventType, true, true, window, null, 0, 0, eventData.pageX, eventData.pageY, false, false, false, false,
  345. MOUSE_BUTTON_MAIN, null );
  346. target.dispatchEvent( event );
  347. }
  348. function getElementPosition( element ) {
  349. // Returns top left corner point.
  350. const viewportPosition = element.getBoundingClientRect();
  351. return {
  352. x: viewportPosition.left + window.scrollX,
  353. y: viewportPosition.top + window.scrollY
  354. };
  355. }
  356. function wait( delay ) {
  357. return new Promise( resolve => window.setTimeout( () => resolve(), delay ) );
  358. }
  359. function generateResizeTest( options ) {
  360. // options.resizerPosition - top-left / top-right / bottom-right / bottom-left
  361. // options.pointerOffset - object - pointer offset relative to the dragged corner. Negative values are perfectly fine.
  362. // e.g. { x: 10, y: -5 }
  363. // options.expectedWidth
  364. // [options.isSideImage=false]
  365. // Returns a test case that puts
  366. return function() {
  367. const widget = this.widget;
  368. const domResizeWrapper = view.domConverter.mapViewToDom( widget.getChild( 1 ) );
  369. const domBottomLeftResizer = domResizeWrapper.querySelector( `.ck-widget__resizer-${ options.resizerPosition }` );
  370. const domImage = view.domConverter.mapViewToDom( widget ).querySelector( 'img' );
  371. const imageTopLeftPosition = getElementPosition( domImage );
  372. const resizerPositionParts = options.resizerPosition.split( '-' );
  373. const modelRegExp = options.modelRegExp ? options.modelRegExp :
  374. /<paragraph>foo<\/paragraph><image src=".+?" width="(\d+)"><\/image>/;
  375. focusEditor( editor );
  376. const initialPointerPosition = {
  377. pageX: imageTopLeftPosition.x,
  378. pageY: imageTopLeftPosition.y
  379. };
  380. if ( resizerPositionParts.includes( 'right' ) ) {
  381. initialPointerPosition.pageX += FIXTURE_WIDTH;
  382. }
  383. if ( resizerPositionParts.includes( 'bottom' ) ) {
  384. initialPointerPosition.pageY += FIXTURE_HEIGHT;
  385. }
  386. const finishPointerPosition = Object.assign( {}, initialPointerPosition );
  387. finishPointerPosition.pageX += options.pointerOffset.x || 0;
  388. finishPointerPosition.pageY += options.pointerOffset.y || 0;
  389. fireMouseEvent( domBottomLeftResizer, 'mousedown', initialPointerPosition );
  390. fireMouseEvent( domBottomLeftResizer, 'mousemove', initialPointerPosition );
  391. // We need to wait as mousemove events are throttled.
  392. return wait( 30 )
  393. .then( () => {
  394. fireMouseEvent( domBottomLeftResizer, 'mousemove', finishPointerPosition );
  395. expect( domImage.width ).to.be.closeTo( options.expectedWidth, 1 );
  396. fireMouseEvent( domBottomLeftResizer, 'mouseup', finishPointerPosition );
  397. expect( getData( editor.model, {
  398. withoutSelection: true
  399. } ) ).to.match( modelRegExp );
  400. const modelItem = options.getModel ? options.getModel() : editor.model.document.getRoot().getChild( 1 );
  401. expect( modelItem.getAttribute( 'width' ) ).to.be.closeTo( options.expectedWidth, 1, 'Model check' );
  402. } );
  403. };
  404. }
  405. function focusEditor( editor ) {
  406. editor.editing.view.focus();
  407. editor.ui.focusTracker.isFocused = true;
  408. }
  409. } );