imageresize.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  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. /* 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 Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  9. import Image from '../src/image';
  10. import ImageResize from '../src/imageresize';
  11. import ImageResizeCommand from '../src/imageresize/imageresizecommand';
  12. import ImageStyle from '../src/imagestyle';
  13. import ImageToolbar from '../src/imagetoolbar';
  14. import ImageTextAlternative from '../src/imagetextalternative';
  15. import Undo from '@ckeditor/ckeditor5-undo/src/undo';
  16. import Table from '@ckeditor/ckeditor5-table/src/table';
  17. import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
  18. import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  19. describe( 'ImageResize', () => {
  20. // Id of the left mouse button.
  21. const MOUSE_BUTTON_MAIN = 0;
  22. // 60x50 black png image
  23. const IMAGE_SRC_FIXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
  24. 'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
  25. let absoluteContainer, widget, editor, view, viewDocument, editorElement;
  26. before( () => {
  27. // This container is required to position editor element in a reliable element.
  28. // See fireMouseEvent method for more information regarding imprecise mouse position.
  29. absoluteContainer = document.createElement( 'div' );
  30. absoluteContainer.style.top = '50px';
  31. absoluteContainer.style.left = '50px';
  32. absoluteContainer.style.height = '1000px';
  33. absoluteContainer.style.width = '500px';
  34. absoluteContainer.style.position = 'absolute';
  35. document.body.appendChild( absoluteContainer );
  36. } );
  37. after( () => {
  38. absoluteContainer.remove();
  39. } );
  40. afterEach( () => {
  41. if ( editorElement ) {
  42. editorElement.remove();
  43. }
  44. if ( editor ) {
  45. return editor.destroy();
  46. }
  47. } );
  48. describe( 'conversion', () => {
  49. beforeEach( () => createEditor() );
  50. it( 'upcasts 100px width correctly', () => {
  51. editor.setData( `<figure class="image" style="width:100px;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
  52. expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'width' ) ).to.equal( '100px' );
  53. } );
  54. it( 'upcasts 50% width correctly', () => {
  55. editor.setData( `<figure class="image" style="width:50%;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
  56. expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'width' ) ).to.equal( '50%' );
  57. } );
  58. it( 'downcasts 100px width correctly', () => {
  59. setData( editor.model, `<image src="${ IMAGE_SRC_FIXTURE }" width="100px"></image>` );
  60. expect( editor.getData() )
  61. .to.equal( `<figure class="image image_resized" style="width:100px;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
  62. } );
  63. it( 'downcasts 50% width correctly', () => {
  64. setData( editor.model, `<image src="${ IMAGE_SRC_FIXTURE }" width="50%"></image>` );
  65. expect( editor.getData() )
  66. .to.equal( `<figure class="image image_resized" style="width:50%;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
  67. } );
  68. } );
  69. describe( 'schema', () => {
  70. beforeEach( () => createEditor() );
  71. it( 'allows the width attribute', () => {
  72. expect( editor.model.schema.checkAttribute( 'image', 'width' ) ).to.be.true;
  73. } );
  74. it( 'defines width as a formatting attribute', () => {
  75. expect( editor.model.schema.getAttributeProperties( 'width' ) ).to.have.property( 'isFormatting', true );
  76. } );
  77. } );
  78. describe( 'command', () => {
  79. beforeEach( () => createEditor() );
  80. it( 'defines the imageResize command', () => {
  81. expect( editor.commands.get( 'imageResize' ) ).to.be.instanceOf( ImageResizeCommand );
  82. } );
  83. it( 'uses the command on commit', async () => {
  84. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  85. widget = viewDocument.getRoot().getChild( 1 );
  86. const spy = sinon.spy( editor.commands.get( 'imageResize' ), 'execute' );
  87. await generateResizeTest( {
  88. expectedWidth: 80,
  89. pointerOffset: {
  90. x: 10,
  91. y: -10
  92. },
  93. resizerPosition: 'bottom-left'
  94. } )();
  95. // It's either 80px or 81px depending on the device, so we need to make the test a bit more loose.
  96. const realWidth = editor.model.document.getRoot().getChild( 1 ).getAttribute( 'width' );
  97. expect( realWidth ).to.match( /^\d\dpx$/ );
  98. expect( spy.calledOnce ).to.be.true;
  99. expect( spy.args[ 0 ][ 0 ] ).to.deep.equal( { width: realWidth } );
  100. } );
  101. it( 'disables the resizer if the command is disabled', () => {
  102. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  103. const resizer = getSelectedImageResizer( editor );
  104. let isEnabled = false;
  105. editor.commands.get( 'imageResize' ).on( 'set:isEnabled', evt => {
  106. evt.return = isEnabled;
  107. evt.stop();
  108. }, { priority: 'highest' } );
  109. editor.commands.get( 'imageResize' ).refresh();
  110. expect( resizer.isEnabled ).to.be.false;
  111. isEnabled = true;
  112. editor.commands.get( 'imageResize' ).refresh();
  113. expect( resizer.isEnabled ).to.be.true;
  114. } );
  115. it( 'the resizer is disabled from the beginning when the command is disabled when the image is inserted', () => {
  116. setData( editor.model, '<paragraph>foo[]</paragraph>' );
  117. editor.commands.get( 'imageResize' ).on( 'set:isEnabled', evt => {
  118. evt.return = false;
  119. evt.stop();
  120. }, { priority: 'highest' } );
  121. editor.commands.get( 'imageResize' ).refresh();
  122. editor.model.change( writer => {
  123. editor.model.insertContent( writer.createElement( 'image', { src: IMAGE_SRC_FIXTURE } ) );
  124. } );
  125. const resizer = getSelectedImageResizer( editor );
  126. const resizerWrapper = editor.ui.getEditableElement().querySelector( '.ck-widget__resizer' );
  127. expect( resizer.isEnabled ).to.be.false;
  128. expect( resizerWrapper.style.display ).to.equal( 'none' );
  129. } );
  130. } );
  131. describe( 'visual resizers', () => {
  132. beforeEach( async () => {
  133. await createEditor();
  134. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  135. widget = viewDocument.getRoot().getChild( 1 );
  136. } );
  137. it( 'correct number is added by default', () => {
  138. const resizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
  139. expect( resizers.length ).to.be.equal( 4 );
  140. } );
  141. describe( 'visibility', () => {
  142. it( 'is hidden by default', () => {
  143. const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
  144. for ( const resizer of allResizers ) {
  145. expect( isVisible( resizer ) ).to.be.false;
  146. }
  147. } );
  148. it( 'is shown when image is focused', () => {
  149. const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
  150. const domEventDataMock = {
  151. target: widget,
  152. preventDefault: sinon.spy()
  153. };
  154. focusEditor( editor );
  155. viewDocument.fire( 'mousedown', domEventDataMock );
  156. for ( const resizer of allResizers ) {
  157. expect( isVisible( resizer ) ).to.be.true;
  158. }
  159. } );
  160. } );
  161. } );
  162. describe( 'standard image resizing', () => {
  163. beforeEach( async () => {
  164. await createEditor();
  165. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  166. widget = viewDocument.getRoot().getChild( 1 );
  167. } );
  168. it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
  169. expectedWidth: 80,
  170. pointerOffset: {
  171. x: 10,
  172. y: -10
  173. },
  174. resizerPosition: 'bottom-left'
  175. } ) );
  176. it( 'shrinks correctly with right-bottom handler', generateResizeTest( {
  177. expectedWidth: 80,
  178. pointerOffset: {
  179. x: -10,
  180. y: -10
  181. },
  182. resizerPosition: 'bottom-right'
  183. } ) );
  184. it( 'enlarges correctly with right-bottom handler, x axis only', generateResizeTest( {
  185. expectedWidth: 120,
  186. pointerOffset: {
  187. x: 10,
  188. y: 0
  189. },
  190. resizerPosition: 'bottom-right'
  191. } ) );
  192. it( 'enlarges correctly with right-bottom handler, y axis only', generateResizeTest( {
  193. expectedWidth: 120,
  194. pointerOffset: {
  195. x: 0,
  196. y: 10
  197. },
  198. resizerPosition: 'bottom-right'
  199. } ) );
  200. it( 'enlarges correctly with left-bottom handler, x axis only', generateResizeTest( {
  201. expectedWidth: 120,
  202. pointerOffset: {
  203. x: -10,
  204. y: 0
  205. },
  206. resizerPosition: 'bottom-left'
  207. } ) );
  208. it( 'enlarges correctly with left-bottom handler, y axis only', generateResizeTest( {
  209. expectedWidth: 120,
  210. pointerOffset: {
  211. x: 0,
  212. y: 10
  213. },
  214. resizerPosition: 'bottom-left'
  215. } ) );
  216. // --- top handlers ---
  217. it( 'enlarges correctly with left-top handler', generateResizeTest( {
  218. expectedWidth: 120,
  219. pointerOffset: {
  220. x: -10,
  221. y: -10
  222. },
  223. resizerPosition: 'top-left'
  224. } ) );
  225. it( 'enlarges correctly with left-top handler, y axis only', generateResizeTest( {
  226. expectedWidth: 120,
  227. pointerOffset: {
  228. x: 0,
  229. y: -10
  230. },
  231. resizerPosition: 'top-left'
  232. } ) );
  233. it( 'enlarges correctly with right-top handler', generateResizeTest( {
  234. expectedWidth: 120,
  235. pointerOffset: {
  236. x: 10,
  237. y: -10
  238. },
  239. resizerPosition: 'top-right'
  240. } ) );
  241. it( 'enlarges correctly with right-top handler, y axis only', generateResizeTest( {
  242. expectedWidth: 120,
  243. pointerOffset: {
  244. x: 0,
  245. y: -10
  246. },
  247. resizerPosition: 'top-right'
  248. } ) );
  249. } );
  250. describe( 'side image resizing', () => {
  251. beforeEach( async () => {
  252. await createEditor();
  253. setData( editor.model, `<paragraph>foo</paragraph>[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  254. widget = viewDocument.getRoot().getChild( 1 );
  255. } );
  256. it( 'shrinks correctly with left-bottom handler', generateSideResizeTest( {
  257. expectedWidth: 80,
  258. pointerOffset: {
  259. x: 20,
  260. y: -10
  261. },
  262. resizerPosition: 'bottom-left'
  263. } ) );
  264. it( 'shrinks correctly with right-bottom handler', generateSideResizeTest( {
  265. expectedWidth: 80,
  266. pointerOffset: {
  267. x: -20,
  268. y: -10
  269. },
  270. resizerPosition: 'bottom-right'
  271. } ) );
  272. it( 'shrinks correctly with left-top handler', generateSideResizeTest( {
  273. expectedWidth: 80,
  274. pointerOffset: {
  275. x: 20,
  276. y: 10
  277. },
  278. resizerPosition: 'top-left'
  279. } ) );
  280. it( 'shrinks correctly with right-top handler', generateSideResizeTest( {
  281. expectedWidth: 80,
  282. pointerOffset: {
  283. x: -20,
  284. y: 10
  285. },
  286. resizerPosition: 'top-right'
  287. } ) );
  288. it( 'enlarges correctly with left-bottom handler', generateSideResizeTest( {
  289. expectedWidth: 120,
  290. pointerOffset: {
  291. x: -10,
  292. y: 10
  293. },
  294. resizerPosition: 'bottom-left'
  295. } ) );
  296. it( 'enlarges correctly with right-bottom handler', generateSideResizeTest( {
  297. expectedWidth: 120,
  298. pointerOffset: {
  299. x: 10,
  300. y: 10
  301. },
  302. resizerPosition: 'bottom-right'
  303. } ) );
  304. it( 'enlarges correctly with right-bottom handler, y axis only', generateSideResizeTest( {
  305. expectedWidth: 140,
  306. pointerOffset: {
  307. x: 0,
  308. y: 20
  309. },
  310. resizerPosition: 'bottom-right'
  311. } ) );
  312. it( 'enlarges correctly with right-bottom handler, x axis only', generateSideResizeTest( {
  313. expectedWidth: 140,
  314. pointerOffset: {
  315. x: 40,
  316. y: 0
  317. },
  318. resizerPosition: 'bottom-right'
  319. } ) );
  320. it( 'enlarges correctly with left-top handler', generateSideResizeTest( {
  321. expectedWidth: 120,
  322. pointerOffset: {
  323. x: -20,
  324. y: -10
  325. },
  326. resizerPosition: 'top-left'
  327. } ) );
  328. it( 'enlarges correctly with right-top handler', generateSideResizeTest( {
  329. expectedWidth: 120,
  330. pointerOffset: {
  331. x: 20,
  332. y: 10
  333. },
  334. resizerPosition: 'top-right'
  335. } ) );
  336. it( 'doesn\'t flicker at the beginning of the resize', async () => {
  337. // (#5189)
  338. const resizerPosition = 'bottom-left';
  339. const domParts = getWidgetDomParts( widget, resizerPosition );
  340. const initialPointerPosition = getResizerCoordinates( domParts.figure, resizerPosition );
  341. const resizeWrapperView = widget.getChild( 1 );
  342. focusEditor( editor );
  343. fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
  344. await wait( 40 );
  345. fireMouseEvent( domParts.resizeHandle, 'mousemove', initialPointerPosition );
  346. expect( resizeWrapperView.getStyle( 'width' ) ).to.be.equal( '100px' );
  347. fireMouseEvent( domParts.resizeHandle, 'mouseup', initialPointerPosition );
  348. } );
  349. it( 'makes no change when clicking the handle without drag', () => {
  350. const resizerPosition = 'bottom-left';
  351. const expectedWidth = 100;
  352. const domParts = getWidgetDomParts( widget, resizerPosition );
  353. const initialPointerPosition = getResizerCoordinates( domParts.figure, resizerPosition );
  354. focusEditor( editor );
  355. fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
  356. expect( getDomWidth( domParts.figure ), 'DOM width check' ).to.be.closeTo( expectedWidth, 2 );
  357. fireMouseEvent( domParts.resizeHandle, 'mouseup', initialPointerPosition );
  358. const modelItem = editor.model.document.getRoot().getChild( 1 );
  359. expect( modelItem.getAttribute( 'width' ), 'model width attribute' ).to.be.undefined;
  360. } );
  361. function generateSideResizeTest( options ) {
  362. return generateResizeTest( Object.assign( {
  363. modelRegExp: /<paragraph>foo<\/paragraph><image imageStyle="side" src=".+?" width="([\d.]+)px"><\/image>/
  364. }, options ) );
  365. }
  366. } );
  367. describe( 'percent resizing', () => {
  368. beforeEach( () => createEditor( {
  369. plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResize ]
  370. } ) );
  371. describe( 'standard image', () => {
  372. beforeEach( () => {
  373. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  374. widget = viewDocument.getRoot().getChild( 1 );
  375. } );
  376. it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
  377. expectedWidth: 16,
  378. modelRegExp: /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
  379. pointerOffset: {
  380. x: 10,
  381. y: -10
  382. },
  383. resizerPosition: 'bottom-left'
  384. } ) );
  385. it( 'enlarges correctly with right-bottom handler', generateResizeTest( {
  386. expectedWidth: 22,
  387. modelRegExp: /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
  388. pointerOffset: {
  389. x: 0,
  390. y: 5
  391. },
  392. resizerPosition: 'bottom-right'
  393. } ) );
  394. it( 'enlarges correctly an image with unsupported width unit', async () => {
  395. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }" width="50pt"></image>]` );
  396. widget = viewDocument.getRoot().getChild( 1 );
  397. await generateResizeTest( {
  398. expectedWidth: 15,
  399. modelRegExp: /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
  400. pointerOffset: {
  401. x: 0,
  402. y: 5
  403. },
  404. resizerPosition: 'bottom-right'
  405. } )();
  406. } );
  407. } );
  408. describe( 'side image', () => {
  409. beforeEach( () => {
  410. setData( editor.model, `<paragraph>foo</paragraph>[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  411. view = editor.editing.view;
  412. viewDocument = view.document;
  413. widget = viewDocument.getRoot().getChild( 1 );
  414. } );
  415. it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
  416. expectedWidth: 18,
  417. modelRegExp: /<paragraph>foo<\/paragraph><image imageStyle="side" src=".+?" width="([\d]{2}(?:\.[\d]{1,2}))%"><\/image>/,
  418. pointerOffset: {
  419. x: 10,
  420. y: -10
  421. },
  422. resizerPosition: 'bottom-left'
  423. } ) );
  424. } );
  425. } );
  426. describe( 'undo integration', () => {
  427. beforeEach( async () => {
  428. await createEditor();
  429. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  430. widget = viewDocument.getRoot().getChild( 1 );
  431. } );
  432. it( 'has correct border size after undo', async () => {
  433. await generateResizeTest( {
  434. expectedWidth: 120,
  435. pointerOffset: {
  436. x: 0,
  437. y: 10
  438. },
  439. resizerPosition: 'bottom-left'
  440. } )();
  441. editor.commands.get( 'undo' ).execute();
  442. await wait( 160 ); // ui#update event is throttled.
  443. const resizerWrapper = document.querySelector( '.ck-widget__resizer' );
  444. const shadowBoundingRect = resizerWrapper.getBoundingClientRect();
  445. expect( shadowBoundingRect.width ).to.be.equal( 100 );
  446. expect( shadowBoundingRect.height ).to.be.equal( 50 );
  447. } );
  448. } );
  449. describe( 'table integration', () => {
  450. beforeEach( async () => {
  451. await createEditor();
  452. setData( editor.model,
  453. '<table>' +
  454. `<tableRow><tableCell>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]</tableCell></tableRow>` +
  455. '</table>'
  456. );
  457. widget = viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
  458. } );
  459. it( 'works when resizing in a table', generateResizeTest( {
  460. getModel: () => editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ),
  461. expectedWidth: 60,
  462. modelRegExp: /.+/,
  463. pointerOffset: {
  464. x: -40,
  465. y: -20
  466. },
  467. resizerPosition: 'bottom-right'
  468. } ) );
  469. } );
  470. describe( 'srcset integration', () => {
  471. // The image is 96x96 pixels.
  472. const imageBaseUrl = '/assets/sample.png';
  473. const getModel = () => editor.model.document.getRoot().getChild( 0 );
  474. let images = [];
  475. before( () => {
  476. return Promise.all( [
  477. preloadImage( imageBaseUrl ),
  478. preloadImage( imageBaseUrl + '?a' ),
  479. preloadImage( imageBaseUrl + '?b' ),
  480. preloadImage( imageBaseUrl + '?c' )
  481. ] ).then( loadedImages => {
  482. images = loadedImages;
  483. } );
  484. } );
  485. after( () => {
  486. for ( const image of images ) {
  487. image.remove();
  488. }
  489. } );
  490. beforeEach( async () => {
  491. await createEditor();
  492. editor.setData(
  493. `<figure class="image">
  494. <img src="${ imageBaseUrl }"
  495. srcset="${ imageBaseUrl }?a 110w,
  496. ${ imageBaseUrl }?b 440w,
  497. ${ imageBaseUrl }?c 1025w"
  498. sizes="100vw" width="96">
  499. </figure>`
  500. );
  501. widget = viewDocument.getRoot().getChild( 0 );
  502. } );
  503. it( 'works with images containing srcset', generateResizeTest( {
  504. getModel,
  505. expectedWidth: 76,
  506. modelRegExp: /.+/,
  507. pointerOffset: {
  508. x: -20,
  509. y: -20
  510. },
  511. resizerPosition: 'bottom-right'
  512. } ) );
  513. it( 'retains width after removing srcset', async () => {
  514. await generateResizeTest( {
  515. getModel,
  516. expectedWidth: 80,
  517. modelRegExp: /.+/,
  518. pointerOffset: {
  519. x: -16,
  520. y: -16
  521. },
  522. resizerPosition: 'bottom-right'
  523. } )();
  524. editor.model.change( writer => {
  525. writer.removeAttribute( 'srcset', getModel() );
  526. } );
  527. expect( editor.getData() )
  528. .to.match( /<figure class="image image_resized" style="width:[\d.]{2,}px;"><img src="\/assets\/sample.png"><\/figure>/ );
  529. } );
  530. async function preloadImage( imageUrl ) {
  531. const image = document.createElement( 'img' );
  532. image.src = imageUrl;
  533. return new Promise( ( resolve, reject ) => {
  534. image.addEventListener( 'load', () => resolve( image ) );
  535. image.addEventListener( 'error', () => reject( image ) );
  536. document.body.appendChild( image );
  537. } );
  538. }
  539. } );
  540. // TODO move to Resizer tests.
  541. describe( 'Resizer', () => {
  542. beforeEach( () => createEditor() );
  543. it( 'uses rounded (int) values', async () => {
  544. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  545. widget = viewDocument.getRoot().getChild( 1 );
  546. await generateResizeTest( {
  547. expectedWidth: 97,
  548. // Makes it resize the image to 97.2188px, unless there's a rounding.
  549. pointerOffset: {
  550. x: 7.3,
  551. y: -1
  552. },
  553. resizerPosition: 'bottom-left',
  554. checkBeforeMouseUp( domFigure, domResizeWrapper ) {
  555. expect( domFigure.style.width ).to.match( /^\d\dpx$/ );
  556. expect( domResizeWrapper.style.width ).to.match( /^\d\dpx$/ );
  557. }
  558. } )();
  559. expect( editor.model.document.getRoot().getChild( 1 ).getAttribute( 'width' ) ).to.match( /^\d\dpx$/ );
  560. } );
  561. it( 'hides the resize wrapper when its disabled', () => {
  562. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  563. const resizer = getSelectedImageResizer( editor );
  564. const resizerWrapper = editor.ui.getEditableElement().querySelector( '.ck-widget__resizer' );
  565. expect( resizerWrapper.style.display ).to.equal( '' );
  566. resizer.isEnabled = false;
  567. expect( resizerWrapper.style.display ).to.equal( 'none' );
  568. } );
  569. } );
  570. describe( 'widget toolbar integration', () => {
  571. let widgetToolbarRepository;
  572. beforeEach( async () => {
  573. await createEditor( {
  574. plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResize, ImageToolbar, ImageTextAlternative ],
  575. image: {
  576. toolbar: [ 'imageTextAlternative' ],
  577. resizeUnit: 'px'
  578. }
  579. } );
  580. setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
  581. widget = viewDocument.getRoot().getChild( 1 );
  582. widgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' );
  583. } );
  584. it( 'default toolbar visibility', async () => {
  585. expect( widgetToolbarRepository.isEnabled ).to.be.true;
  586. } );
  587. it( 'visibility during the resize', async () => {
  588. await generateResizeTest( {
  589. expectedWidth: 100,
  590. modelRegExp: /.+/,
  591. pointerOffset: {
  592. x: 0,
  593. y: 0
  594. },
  595. resizerPosition: 'bottom-right',
  596. checkBeforeMouseUp: () => {
  597. expect( widgetToolbarRepository.isEnabled ).to.be.false;
  598. }
  599. } )();
  600. } );
  601. it( 'visibility after the resize', async () => {
  602. await generateResizeTest( {
  603. expectedWidth: 100,
  604. modelRegExp: /.+/,
  605. pointerOffset: {
  606. x: 0,
  607. y: 0
  608. },
  609. resizerPosition: 'bottom-right'
  610. } )();
  611. expect( widgetToolbarRepository.isEnabled ).to.be.true;
  612. } );
  613. it( 'visibility after the resize was canceled', async () => {
  614. const resizer = getSelectedImageResizer( editor );
  615. await generateResizeTest( {
  616. expectedWidth: 100,
  617. modelRegExp: /.+/,
  618. pointerOffset: {
  619. x: 0,
  620. y: 0
  621. },
  622. resizerPosition: 'bottom-right',
  623. checkBeforeMouseUp: () => {
  624. resizer.cancel();
  625. expect( widgetToolbarRepository.isEnabled ).to.be.true;
  626. }
  627. } )();
  628. } );
  629. it( 'restores toolbar when clicking the handle without drag', () => {
  630. // (https://github.com/ckeditor/ckeditor5-widget/pull/112#pullrequestreview-337725256).
  631. const resizerPosition = 'bottom-left';
  632. const domParts = getWidgetDomParts( widget, resizerPosition );
  633. const initialPointerPosition = getResizerCoordinates( domParts.figure, resizerPosition );
  634. focusEditor( editor );
  635. fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
  636. fireMouseEvent( domParts.resizeHandle, 'mouseup', initialPointerPosition );
  637. expect( widgetToolbarRepository.isEnabled ).to.be.true;
  638. } );
  639. } );
  640. function isVisible( element ) {
  641. // Checks if the DOM element is visible to the end user.
  642. return element.offsetParent !== null && !element.classList.contains( 'ck-hidden' );
  643. }
  644. function fireMouseEvent( target, eventType, eventData ) {
  645. // Using initMouseEvent instead of MouseEvent constructor, as MouseEvent constructor doesn't support passing pageX
  646. // and pageY. See https://stackoverflow.com/questions/45843458/setting-click-events-pagex-and-pagey-always-reverts-to-0
  647. // However there's still a problem, that events created with `initMouseEvent` have **floored** pageX, pageY numbers.
  648. const event = document.createEvent( 'MouseEvent' );
  649. event.initMouseEvent( eventType, true, true, window, null, 0, 0, eventData.pageX, eventData.pageY, false, false, false, false,
  650. MOUSE_BUTTON_MAIN, null );
  651. target.dispatchEvent( event );
  652. }
  653. function wait( delay ) {
  654. return new Promise( resolve => window.setTimeout( () => resolve(), delay ) );
  655. }
  656. function generateResizeTest( options ) {
  657. // options.resizerPosition - top-left / top-right / bottom-right / bottom-left
  658. // options.pointerOffset - object - pointer offset relative to the dragged corner. Negative values are perfectly fine.
  659. // e.g. { x: 10, y: -5 }
  660. // options.expectedWidth
  661. // Returns a test case that puts
  662. return async function() {
  663. const domParts = getWidgetDomParts( widget, options.resizerPosition );
  664. const domResizeWrapper = view.domConverter.mapViewToDom( widget.getChild( 1 ) );
  665. const modelRegExp = options.modelRegExp ? options.modelRegExp :
  666. /<paragraph>foo<\/paragraph><image src=".+?" width="([\d]+)px"><\/image>/;
  667. focusEditor( editor );
  668. const initialPointerPosition = getResizerCoordinates( domParts.figure, options.resizerPosition );
  669. fireMouseEvent( domParts.resizeHandle, 'mousedown', initialPointerPosition );
  670. fireMouseEvent( domParts.resizeHandle, 'mousemove', initialPointerPosition );
  671. // We need to wait as mousemove events are throttled.
  672. await wait( 40 );
  673. const finishPointerPosition = Object.assign( {}, initialPointerPosition );
  674. finishPointerPosition.pageX += options.pointerOffset.x || 0;
  675. finishPointerPosition.pageY += options.pointerOffset.y || 0;
  676. fireMouseEvent( domParts.resizeHandle, 'mousemove', finishPointerPosition );
  677. expect( parseInt( domParts.figure.style.width ) ).to.be.closeTo( options.expectedWidth, 2, 'DOM width check' );
  678. if ( options.checkBeforeMouseUp ) {
  679. options.checkBeforeMouseUp( domParts.figure, domResizeWrapper );
  680. }
  681. fireMouseEvent( domParts.resizeHandle, 'mouseup', finishPointerPosition );
  682. expect( getData( editor.model, {
  683. withoutSelection: true
  684. } ) ).to.match( modelRegExp );
  685. const modelItem = options.getModel ? options.getModel() : editor.model.document.getRoot().getChild( 1 );
  686. const modelWidth = modelItem.getAttribute( 'width' );
  687. expect( parseFloat( modelWidth, 0 ) )
  688. .to.be.closeTo( options.expectedWidth, 2, 'Model width check' );
  689. };
  690. }
  691. function focusEditor( editor ) {
  692. editor.editing.view.focus();
  693. editor.ui.focusTracker.isFocused = true;
  694. }
  695. function getResizerCoordinates( domFigure, resizerPosition ) {
  696. const domImage = domFigure.querySelector( 'img' );
  697. const imageRect = new Rect( domImage );
  698. const initialPointerPosition = {
  699. pageX: imageRect.left,
  700. pageY: imageRect.top
  701. };
  702. const resizerPositionParts = resizerPosition.split( '-' );
  703. if ( resizerPositionParts.includes( 'right' ) ) {
  704. initialPointerPosition.pageX = imageRect.right;
  705. }
  706. if ( resizerPositionParts.includes( 'bottom' ) ) {
  707. initialPointerPosition.pageY = imageRect.bottom;
  708. }
  709. return initialPointerPosition;
  710. }
  711. function getWidgetDomParts( widget, resizerPosition ) {
  712. const resizeWrapper = view.domConverter.mapViewToDom( widget.getChild( 1 ) );
  713. const resizeHandle = resizeWrapper.querySelector( `.ck-widget__resizer__handle-${ resizerPosition }` );
  714. const figure = view.domConverter.mapViewToDom( widget );
  715. return {
  716. resizeWrapper,
  717. resizeHandle,
  718. figure
  719. };
  720. }
  721. function getDomWidth( domElement ) {
  722. return new Rect( domElement ).width;
  723. }
  724. function getSelectedImageResizer( editor ) {
  725. return editor.plugins.get( 'WidgetResize' )._getResizerByViewElement(
  726. editor.editing.view.document.selection.getSelectedElement()
  727. );
  728. }
  729. function createEditor( config ) {
  730. editorElement = document.createElement( 'div' );
  731. absoluteContainer.appendChild( editorElement );
  732. return ClassicEditor
  733. .create( editorElement, config || {
  734. plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResize ],
  735. image: {
  736. resizeUnit: 'px'
  737. }
  738. } )
  739. .then( newEditor => {
  740. editor = newEditor;
  741. view = editor.editing.view;
  742. viewDocument = view.document;
  743. widget = viewDocument.getRoot().getChild( 1 );
  744. } );
  745. }
  746. } );