deletecontent.js 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  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. import Model from '../../../src/model/model';
  6. import Position from '../../../src/model/position';
  7. import Range from '../../../src/model/range';
  8. import Selection from '../../../src/model/selection';
  9. import Element from '../../../src/model/element';
  10. import deleteContent from '../../../src/model/utils/deletecontent';
  11. import { setData, getData } from '../../../src/dev-utils/model';
  12. import { stringify } from '../../../src/dev-utils/view';
  13. describe( 'DataController utils', () => {
  14. let model, doc;
  15. describe( 'deleteContent', () => {
  16. it( 'should use parent batch', () => {
  17. model = new Model();
  18. doc = model.document;
  19. doc.createRoot();
  20. model.schema.extend( '$text', { allowIn: '$root' } );
  21. setData( model, 'x[abc]x' );
  22. model.change( writer => {
  23. deleteContent( model, doc.selection );
  24. expect( writer.batch.operations ).to.length( 1 );
  25. } );
  26. } );
  27. it( 'should not do anything if the selection is already in graveyard', () => {
  28. model = new Model();
  29. doc = model.document;
  30. const gy = model.document.graveyard;
  31. gy._appendChild( new Element( 'paragraph' ) );
  32. const baseVersion = model.document.baseVersion;
  33. model.change( writer => {
  34. sinon.spy( writer, 'remove' );
  35. const selection = writer.createSelection( writer.createRangeIn( gy ) );
  36. deleteContent( model, selection );
  37. expect( writer.remove.called ).to.be.false;
  38. } );
  39. expect( model.document.baseVersion ).to.equal( baseVersion );
  40. } );
  41. describe( 'in simple scenarios', () => {
  42. beforeEach( () => {
  43. model = new Model();
  44. doc = model.document;
  45. doc.createRoot();
  46. const schema = model.schema;
  47. schema.register( 'image', { allowWhere: '$text' } );
  48. schema.extend( '$text', { allowIn: '$root' } );
  49. } );
  50. it( 'should be able to delete content at custom selection', () => {
  51. setData( model, 'a[]bcd' );
  52. const range = new Range(
  53. new Position( doc.getRoot(), [ 2 ] ),
  54. new Position( doc.getRoot(), [ 3 ] )
  55. );
  56. const selection = new Selection( [ range ] );
  57. model.change( () => {
  58. deleteContent( model, selection );
  59. expect( getData( model ) ).to.equal( 'a[]bd' );
  60. } );
  61. } );
  62. test(
  63. 'does nothing on collapsed selection',
  64. 'f[]oo',
  65. 'f[]oo'
  66. );
  67. test(
  68. 'deletes single character',
  69. 'f[o]o',
  70. 'f[]o'
  71. );
  72. it( 'deletes single character (backward selection)', () => {
  73. setData( model, 'f[o]o', { lastRangeBackward: true } );
  74. deleteContent( model, doc.selection );
  75. expect( getData( model ) ).to.equal( 'f[]o' );
  76. } );
  77. test(
  78. 'deletes whole text',
  79. '[foo]',
  80. '[]'
  81. );
  82. test(
  83. 'deletes whole text between nodes',
  84. '<image></image>[foo]<image></image>',
  85. '<image></image>[]<image></image>'
  86. );
  87. test(
  88. 'deletes an element',
  89. 'x[<image></image>]y',
  90. 'x[]y'
  91. );
  92. test(
  93. 'deletes a bunch of nodes',
  94. 'w[x<image></image>y]z',
  95. 'w[]z'
  96. );
  97. test(
  98. 'does not break things when option.merge passed',
  99. 'w[x<image></image>y]z',
  100. 'w[]z'
  101. );
  102. } );
  103. describe( 'with text attributes', () => {
  104. beforeEach( () => {
  105. model = new Model();
  106. doc = model.document;
  107. doc.createRoot();
  108. const schema = model.schema;
  109. schema.register( 'image', { allowWhere: '$text' } );
  110. schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  111. schema.extend( '$text', {
  112. allowIn: '$root',
  113. allowAttributes: [ 'bold', 'italic' ]
  114. } );
  115. } );
  116. it( 'deletes characters (first half has attrs)', () => {
  117. setData( model, '<$text bold="true">fo[o</$text>b]ar' );
  118. deleteContent( model, doc.selection );
  119. expect( getData( model ) ).to.equal( '<$text bold="true">fo[]</$text>ar' );
  120. expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
  121. } );
  122. it( 'deletes characters (2nd half has attrs)', () => {
  123. setData( model, 'fo[o<$text bold="true">b]ar</$text>' );
  124. deleteContent( model, doc.selection );
  125. expect( getData( model ) ).to.equal( 'fo[]<$text bold="true">ar</$text>' );
  126. expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
  127. } );
  128. it( 'clears selection attrs when emptied content', () => {
  129. setData( model, '<paragraph>x</paragraph><paragraph>[<$text bold="true">foo</$text>]</paragraph><paragraph>y</paragraph>' );
  130. deleteContent( model, doc.selection );
  131. expect( getData( model ) ).to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
  132. expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
  133. } );
  134. it( 'leaves selection attributes when text contains them', () => {
  135. setData(
  136. model,
  137. '<paragraph>x<$text bold="true">a[foo]b</$text>y</paragraph>',
  138. {
  139. selectionAttributes: {
  140. bold: true
  141. }
  142. }
  143. );
  144. deleteContent( model, doc.selection );
  145. expect( getData( model ) ).to.equal( '<paragraph>x<$text bold="true">a[]b</$text>y</paragraph>' );
  146. expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
  147. } );
  148. } );
  149. // Note: The algorithm does not care what kind of it's merging as it knows nothing useful about these elements.
  150. // In most cases it handles all elements like you'd expect to handle block elements in HTML. However,
  151. // in some scenarios where the tree depth is bigger results may be hard to justify. In fact, such cases
  152. // should not happen unless we're talking about lists or tables, but these features will need to cover
  153. // their scenarios themselves. In all generic scenarios elements are never nested.
  154. //
  155. // You may also be thinking – but I don't want my elements to be merged. It means that there are some special rules,
  156. // like – multiple editing hosts (cE=true/false in use) or block limit elements like <td>.
  157. // Those case should, again, be handled by their specific implementations.
  158. describe( 'in multi-element scenarios', () => {
  159. beforeEach( () => {
  160. model = new Model();
  161. doc = model.document;
  162. doc.createRoot();
  163. const schema = model.schema;
  164. schema.register( 'paragraph', {
  165. inheritAllFrom: '$block',
  166. allowIn: 'pparent',
  167. allowAttributes: 'align'
  168. } );
  169. schema.register( 'heading1', { inheritAllFrom: '$block' } );
  170. schema.register( 'image', { inheritAllFrom: '$text' } );
  171. schema.register( 'pchild', { allowIn: 'paragraph' } );
  172. schema.register( 'pparent', { allowIn: '$root' } );
  173. schema.extend( '$text', { allowIn: [ 'pchild', 'pparent' ] } );
  174. } );
  175. test(
  176. 'do not merge when no need to',
  177. '<paragraph>x</paragraph><paragraph>[foo]</paragraph><paragraph>y</paragraph>',
  178. '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>'
  179. );
  180. test(
  181. 'merges second element into the first one (same name)',
  182. '<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
  183. '<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>y</paragraph>'
  184. );
  185. test(
  186. 'does not merge second element into the first one (same name, !option.merge)',
  187. '<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
  188. '<paragraph>x</paragraph><paragraph>fo[]</paragraph><paragraph>ar</paragraph><paragraph>y</paragraph>',
  189. { leaveUnmerged: true }
  190. );
  191. test(
  192. 'merges second element into the first one (different name)',
  193. '<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
  194. '<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>'
  195. );
  196. // Note: in all these cases we ignore the direction of merge.
  197. // If https://github.com/ckeditor/ckeditor5-engine/issues/470 was fixed we could differently treat
  198. // forward and backward delete.
  199. it( 'merges second element into the first one (different name, backward selection)', () => {
  200. setData(
  201. model,
  202. '<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
  203. { lastRangeBackward: true }
  204. );
  205. deleteContent( model, doc.selection );
  206. expect( getData( model ) ).to.equal( '<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>' );
  207. } );
  208. test(
  209. 'merges second element into the first one (different attrs)',
  210. '<paragraph>x</paragraph><paragraph align="l">fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
  211. '<paragraph>x</paragraph><paragraph align="l">fo[]ar</paragraph><paragraph>y</paragraph>'
  212. );
  213. test(
  214. 'merges second element to an empty first element',
  215. '<paragraph>x</paragraph><heading1>[</heading1><paragraph>fo]o</paragraph><paragraph>y</paragraph>',
  216. '<paragraph>x</paragraph><heading1>[]o</heading1><paragraph>y</paragraph>'
  217. );
  218. test(
  219. 'merges empty element into the first element',
  220. '<heading1>f[oo</heading1><paragraph>bar]</paragraph><paragraph>x</paragraph>',
  221. '<heading1>f[]</heading1><paragraph>x</paragraph>'
  222. );
  223. test(
  224. 'leaves just one element when all selected',
  225. '<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]bar</paragraph>',
  226. '<heading1>[]bar</heading1>'
  227. );
  228. it( 'uses merge operation even if merged element is empty', () => {
  229. let mergeSpy;
  230. setData( model, '<paragraph>ab[cd</paragraph><paragraph>efgh]</paragraph>' );
  231. model.change( writer => {
  232. mergeSpy = sinon.spy( writer, 'merge' );
  233. deleteContent( model, doc.selection );
  234. } );
  235. expect( getData( model ) ).to.equal( '<paragraph>ab[]</paragraph>' );
  236. expect( mergeSpy.called ).to.be.true;
  237. } );
  238. it( 'uses merge operation even if merged element is empty #2', () => {
  239. let mergeSpy;
  240. setData( model, '<paragraph>ab[</paragraph><paragraph>]</paragraph>' );
  241. model.change( writer => {
  242. mergeSpy = sinon.spy( writer, 'merge' );
  243. deleteContent( model, doc.selection );
  244. } );
  245. expect( getData( model ) ).to.equal( '<paragraph>ab[]</paragraph>' );
  246. expect( mergeSpy.called ).to.be.true;
  247. } );
  248. it( 'does not try to move the second block if not needed', () => {
  249. let mergeSpy, moveSpy;
  250. setData( model, '<paragraph>ab[cd</paragraph><paragraph>ef]gh</paragraph>' );
  251. model.change( writer => {
  252. mergeSpy = sinon.spy( writer, 'merge' );
  253. moveSpy = sinon.spy( writer, 'move' );
  254. deleteContent( model, doc.selection );
  255. } );
  256. expect( getData( model ) ).to.equal( '<paragraph>ab[]gh</paragraph>' );
  257. expect( moveSpy.called ).to.be.false;
  258. expect( mergeSpy.called ).to.be.true;
  259. } );
  260. // Note: in all these cases we ignore the direction of merge.
  261. // If https://github.com/ckeditor/ckeditor5-engine/issues/470 was fixed we could differently treat
  262. // forward and backward delete.
  263. describe( 'with nested elements', () => {
  264. test(
  265. 'merges elements when deep nested',
  266. '<paragraph>x<pchild>fo[o</pchild></paragraph><paragraph><pchild>b]ar</pchild>y</paragraph>',
  267. '<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>'
  268. );
  269. it( 'merges elements when deep nested (3rd level)', () => {
  270. const root = doc.getRoot();
  271. // We need to use the raw API due to https://github.com/ckeditor/ckeditor5-engine/issues/905.
  272. // <pparent>x<paragraph>x<pchild>fo[o</pchild></paragraph></pparent>
  273. // <pparent><paragraph><pchild>b]ar</pchild>y</paragraph>y</pparent>
  274. root._appendChild(
  275. new Element( 'pparent', null, [
  276. 'x',
  277. new Element( 'paragraph', null, [
  278. 'x',
  279. new Element( 'pchild', null, 'foo' )
  280. ] )
  281. ] )
  282. );
  283. root._appendChild(
  284. new Element( 'pparent', null, [
  285. new Element( 'paragraph', null, [
  286. new Element( 'pchild', null, 'bar' ),
  287. 'y'
  288. ] ),
  289. 'y'
  290. ] )
  291. );
  292. const range = new Range(
  293. new Position( doc.getRoot(), [ 0, 1, 1, 2 ] ), // fo[o
  294. new Position( doc.getRoot(), [ 1, 0, 0, 1 ] ) // b]ar
  295. );
  296. model.change( writer => {
  297. writer.setSelection( range );
  298. } );
  299. deleteContent( model, doc.selection );
  300. expect( getData( model ) )
  301. .to.equal( '<pparent>x<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>y</pparent>' );
  302. } );
  303. test(
  304. 'merges elements when left end deep nested',
  305. '<paragraph>x<pchild>fo[o</pchild></paragraph><paragraph>b]ary</paragraph><paragraph>x</paragraph>',
  306. '<paragraph>x<pchild>fo[]ary</pchild></paragraph><paragraph>x</paragraph>'
  307. );
  308. test(
  309. 'merges elements when right end deep nested',
  310. '<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph><pchild>b]ar</pchild>x</paragraph>',
  311. '<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>x</paragraph>'
  312. );
  313. it( 'merges elements when left end deep nested (3rd level)', () => {
  314. const root = doc.getRoot();
  315. // We need to use the raw API due to https://github.com/ckeditor/ckeditor5-engine/issues/905.
  316. // <pparent>x<paragraph>foo<pchild>ba[r</pchild></paragraph></pparent><paragraph>b]om</paragraph>
  317. root._appendChild(
  318. new Element( 'pparent', null, [
  319. 'x',
  320. new Element( 'paragraph', null, [
  321. 'foo',
  322. new Element( 'pchild', null, 'bar' )
  323. ] )
  324. ] )
  325. );
  326. root._appendChild(
  327. new Element( 'paragraph', null, 'bom' )
  328. );
  329. const range = new Range(
  330. new Position( doc.getRoot(), [ 0, 1, 3, 2 ] ), // ba[r
  331. new Position( doc.getRoot(), [ 1, 1 ] ) // b]om
  332. );
  333. model.change( writer => {
  334. writer.setSelection( range );
  335. } );
  336. deleteContent( model, doc.selection );
  337. expect( getData( model ) )
  338. .to.equal( '<pparent>x<paragraph>foo<pchild>ba[]om</pchild></paragraph></pparent>' );
  339. } );
  340. test(
  341. 'merges elements when right end deep nested (in an empty container)',
  342. '<paragraph>fo[o</paragraph><paragraph><pchild>bar]</pchild></paragraph>',
  343. '<paragraph>fo[]</paragraph>'
  344. );
  345. test(
  346. 'merges elements when left end deep nested (in an empty container)',
  347. '<paragraph><pchild>[foo</pchild></paragraph><paragraph>b]ar</paragraph><paragraph>x</paragraph>',
  348. '<paragraph><pchild>[]ar</pchild></paragraph><paragraph>x</paragraph>'
  349. );
  350. it( 'merges elements when right end deep nested (3rd level)', () => {
  351. const root = doc.getRoot();
  352. // We need to use the raw API due to https://github.com/ckeditor/ckeditor5-engine/issues/905.
  353. // <paragraph>fo[o</paragraph><pparent><paragraph><pchild>bar]</pchild></paragraph></pparent>
  354. root._appendChild(
  355. new Element( 'paragraph', null, 'foo' )
  356. );
  357. root._appendChild(
  358. new Element( 'pparent', null, [
  359. new Element( 'paragraph', null, [
  360. new Element( 'pchild', null, 'bar' )
  361. ] )
  362. ] )
  363. );
  364. const range = new Range(
  365. new Position( doc.getRoot(), [ 0, 2 ] ), // f[oo
  366. new Position( doc.getRoot(), [ 1, 0, 0, 3 ] ) // bar]
  367. );
  368. model.change( writer => {
  369. writer.setSelection( range );
  370. } );
  371. deleteContent( model, doc.selection );
  372. expect( getData( model ) )
  373. .to.equal( '<paragraph>fo[]</paragraph>' );
  374. } );
  375. } );
  376. describe( 'with object elements', () => {
  377. beforeEach( () => {
  378. const schema = model.schema;
  379. schema.register( 'blockWidget', {
  380. isObject: true
  381. } );
  382. schema.register( 'nestedEditable', {
  383. isLimit: true
  384. } );
  385. schema.extend( 'blockWidget', { allowIn: '$root' } );
  386. schema.extend( 'nestedEditable', { allowIn: 'blockWidget' } );
  387. schema.extend( '$text', { allowIn: 'nestedEditable' } );
  388. } );
  389. test(
  390. 'does not merge an object element (if it is first)',
  391. '<blockWidget><nestedEditable>fo[o</nestedEditable></blockWidget><paragraph>b]ar</paragraph>',
  392. '<blockWidget><nestedEditable>fo[]</nestedEditable></blockWidget><paragraph>ar</paragraph>'
  393. );
  394. test(
  395. 'does not merge an object element (if it is second)',
  396. '<paragraph>ba[r</paragraph><blockWidget><nestedEditable>f]oo</nestedEditable></blockWidget>',
  397. '<paragraph>ba[]</paragraph><blockWidget><nestedEditable>oo</nestedEditable></blockWidget>'
  398. );
  399. } );
  400. describe( 'filtering out', () => {
  401. beforeEach( () => {
  402. const schema = model.schema;
  403. schema.addAttributeCheck( ( ctx, attributeName ) => {
  404. // Disallow 'c' on pchild>pchild>$text.
  405. if ( ctx.endsWith( 'pchild pchild $text' ) && attributeName == 'c' ) {
  406. return false;
  407. }
  408. // Allow 'a' and 'b' on paragraph>$text.
  409. if ( ctx.endsWith( 'paragraph $text' ) && [ 'a', 'b' ].includes( attributeName ) ) {
  410. return true;
  411. }
  412. // Allow 'b' and 'c' in pchild>$text.
  413. if ( ctx.endsWith( 'pchild $text' ) && [ 'b', 'c' ].includes( attributeName ) ) {
  414. return true;
  415. }
  416. } );
  417. schema.extend( 'pchild', { allowIn: 'pchild' } );
  418. } );
  419. test(
  420. 'filters out disallowed attributes after left merge',
  421. '<paragraph>x<pchild>fo[o</pchild></paragraph><paragraph>y]<$text a="1" b="1">z</$text></paragraph>',
  422. '<paragraph>x<pchild>fo[]<$text b="1">z</$text></pchild></paragraph>'
  423. );
  424. test(
  425. 'filters out disallowed attributes from nested nodes after left merge',
  426. '<paragraph>' +
  427. 'x' +
  428. '<pchild>fo[o</pchild>' +
  429. '</paragraph>' +
  430. '<paragraph>' +
  431. 'b]a<$text a="1" b="1">r</$text>' +
  432. '<pchild>b<$text b="1" c="1">i</$text>z</pchild>' +
  433. 'y' +
  434. '</paragraph>',
  435. '<paragraph>' +
  436. 'x' +
  437. '<pchild>' +
  438. 'fo[]a<$text b="1">r</$text>' +
  439. '<pchild>b<$text b="1">i</$text>z</pchild>' +
  440. 'y' +
  441. '</pchild>' +
  442. '</paragraph>'
  443. );
  444. test(
  445. 'filters out disallowed attributes after right merge',
  446. '<paragraph>fo[o</paragraph><paragraph><pchild>x<$text b="1" c="1">y]z</$text></pchild></paragraph>',
  447. '<paragraph>fo[]<$text b="1">z</$text></paragraph>'
  448. );
  449. } );
  450. } );
  451. describe( 'in element selections scenarios', () => {
  452. beforeEach( () => {
  453. model = new Model();
  454. doc = model.document;
  455. // <p> like root.
  456. doc.createRoot( 'paragraph', 'paragraphRoot' );
  457. // <body> like root.
  458. doc.createRoot( '$root', 'bodyRoot' );
  459. // Special root which allows only blockWidgets inside itself.
  460. doc.createRoot( 'restrictedRoot', 'restrictedRoot' );
  461. const schema = model.schema;
  462. schema.register( 'image', { allowWhere: '$text' } );
  463. schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  464. schema.register( 'heading1', { inheritAllFrom: '$block' } );
  465. schema.register( 'blockWidget', { isLimit: true } );
  466. schema.register( 'restrictedRoot', {
  467. isLimit: true
  468. } );
  469. schema.extend( '$block', { allowIn: '$root' } );
  470. schema.extend( 'blockWidget', { allowIn: '$root' } );
  471. schema.extend( 'blockWidget', { allowIn: 'restrictedRoot' } );
  472. } );
  473. // See also "in simple scenarios => deletes an element".
  474. it( 'deletes two inline elements', () => {
  475. model.schema.extend( 'paragraph', {
  476. isLimit: true
  477. } );
  478. setData(
  479. model,
  480. 'x[<image></image><image></image>]z',
  481. { rootName: 'paragraphRoot' }
  482. );
  483. deleteContent( model, doc.selection );
  484. expect( getData( model, { rootName: 'paragraphRoot' } ) )
  485. .to.equal( 'x[]z' );
  486. } );
  487. it( 'moves the (custom) selection to the nearest paragraph', () => {
  488. setData(
  489. model,
  490. '<paragraph>[x]</paragraph><paragraph>yyy</paragraph><paragraph>z</paragraph>',
  491. { rootName: 'bodyRoot' }
  492. );
  493. const root = doc.getRoot( 'bodyRoot' );
  494. // [<paragraph>yyy</paragraph>]
  495. const selection = new Selection( [
  496. new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) )
  497. ] );
  498. deleteContent( model, selection );
  499. expect( getData( model, { rootName: 'bodyRoot' } ) )
  500. .to.equal( '<paragraph>[x]</paragraph><paragraph></paragraph><paragraph>z</paragraph>' );
  501. expect( stringify( root, selection ) )
  502. .to.equal( '<$root><paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph></$root>' );
  503. } );
  504. it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
  505. setData(
  506. model,
  507. '<paragraph>x</paragraph>[<blockWidget></blockWidget>]<paragraph>z</paragraph>',
  508. { rootName: 'bodyRoot' }
  509. );
  510. deleteContent( model, doc.selection );
  511. expect( getData( model, { rootName: 'bodyRoot' } ) )
  512. .to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
  513. } );
  514. it( 'creates paragraph when text is not allowed (heading selected)', () => {
  515. setData(
  516. model,
  517. '<paragraph>x</paragraph><heading1>yyy</heading1><paragraph>z</paragraph>',
  518. { rootName: 'bodyRoot' }
  519. );
  520. // [<heading1>yyy</heading1>]
  521. const range = new Range(
  522. new Position( doc.getRoot( 'bodyRoot' ), [ 1 ] ),
  523. new Position( doc.getRoot( 'bodyRoot' ), [ 2 ] )
  524. );
  525. deleteContent( model, new Selection( range ) );
  526. expect( getData( model, { rootName: 'bodyRoot', withoutSelection: true } ) )
  527. .to.equal( '<paragraph>x</paragraph><paragraph></paragraph><paragraph>z</paragraph>' );
  528. } );
  529. it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
  530. setData(
  531. model,
  532. '<paragraph>x</paragraph><heading1>yyy</heading1><paragraph>yyy</paragraph><paragraph>z</paragraph>',
  533. { rootName: 'bodyRoot' }
  534. );
  535. // [<heading1>yyy</heading1><paragraph>yyy</paragraph>]
  536. const range = new Range(
  537. new Position( doc.getRoot( 'bodyRoot' ), [ 1 ] ),
  538. new Position( doc.getRoot( 'bodyRoot' ), [ 3 ] )
  539. );
  540. deleteContent( model, new Selection( range ) );
  541. expect( getData( model, { rootName: 'bodyRoot', withoutSelection: true } ) )
  542. .to.equal( '<paragraph>x</paragraph><paragraph></paragraph><paragraph>z</paragraph>' );
  543. } );
  544. it( 'creates paragraph when text is not allowed (all content selected)', () => {
  545. setData(
  546. model,
  547. '[<heading1>x</heading1><paragraph>z</paragraph>]',
  548. { rootName: 'bodyRoot' }
  549. );
  550. deleteContent( model, doc.selection );
  551. expect( getData( model, { rootName: 'bodyRoot' } ) )
  552. .to.equal( '<paragraph>[]</paragraph>' );
  553. } );
  554. it( 'does not create a paragraph when it is not allowed', () => {
  555. setData(
  556. model,
  557. '<blockWidget></blockWidget>[<blockWidget></blockWidget>]<blockWidget></blockWidget>',
  558. { rootName: 'restrictedRoot' }
  559. );
  560. deleteContent( model, doc.selection );
  561. expect( getData( model, { rootName: 'restrictedRoot' } ) )
  562. .to.equal( '<blockWidget></blockWidget>[]<blockWidget></blockWidget>' );
  563. } );
  564. it( 'does not create a paragraph when doNotAutoparagraph option is set to true', () => {
  565. setData(
  566. model,
  567. '<paragraph>x</paragraph>[<blockWidget></blockWidget>]<paragraph>z</paragraph>',
  568. { rootName: 'bodyRoot' }
  569. );
  570. deleteContent( model, doc.selection, { doNotAutoparagraph: true } );
  571. expect( getData( model, { rootName: 'bodyRoot' } ) )
  572. .to.equal( '<paragraph>x[]</paragraph><paragraph>z</paragraph>' );
  573. } );
  574. it( 'does not create a paragraph when after deletion there is no valid selection range (empty root)', () => {
  575. setData(
  576. model,
  577. '[<blockWidget></blockWidget>]',
  578. { rootName: 'bodyRoot' }
  579. );
  580. deleteContent( model, doc.selection, { doNotAutoparagraph: true } );
  581. expect( getData( model, { rootName: 'bodyRoot' } ) )
  582. .to.equal( '[]' );
  583. } );
  584. } );
  585. describe( 'integration with inline limit elements', () => {
  586. beforeEach( () => {
  587. model = new Model();
  588. doc = model.document;
  589. doc.createRoot();
  590. const schema = model.schema;
  591. schema.register( 'inlineLimit', {
  592. isLimit: true,
  593. allowIn: '$root'
  594. } );
  595. schema.extend( '$text', {
  596. allowIn: [ 'inlineLimit', '$root', 'x' ]
  597. } );
  598. schema.register( 'x', { allowIn: '$root' } );
  599. } );
  600. test(
  601. 'should delete inside inline limit element',
  602. '<inlineLimit>foo [bar] baz</inlineLimit>',
  603. '<inlineLimit>foo [] baz</inlineLimit>'
  604. );
  605. test(
  606. 'should delete whole inline limit element',
  607. 'x[<inlineLimit>foo bar</inlineLimit>]x',
  608. 'x[]x'
  609. );
  610. test(
  611. 'should delete from two inline limit elements',
  612. '<inlineLimit>foo [bar</inlineLimit><inlineLimit>baz] qux</inlineLimit>',
  613. '<inlineLimit>foo []</inlineLimit><inlineLimit> qux</inlineLimit>'
  614. );
  615. test(
  616. 'merge option should be ignored if both elements are limits',
  617. '<inlineLimit>foo [bar</inlineLimit><inlineLimit>baz] qux</inlineLimit>',
  618. '<inlineLimit>foo []</inlineLimit><inlineLimit> qux</inlineLimit>'
  619. );
  620. test(
  621. 'merge option should be ignored if the first element is a limit',
  622. '<inlineLimit>foo [bar</inlineLimit><x>baz] qux</x>',
  623. '<inlineLimit>foo []</inlineLimit><x> qux</x>'
  624. );
  625. test(
  626. 'merge option should be ignored if the second element is a limit',
  627. '<x>baz [qux</x><inlineLimit>foo] bar</inlineLimit>',
  628. '<x>baz []</x><inlineLimit> bar</inlineLimit>'
  629. );
  630. } );
  631. describe( 'integration with block limit elements', () => {
  632. beforeEach( () => {
  633. model = new Model();
  634. doc = model.document;
  635. doc.createRoot();
  636. const schema = model.schema;
  637. schema.register( 'blockLimit', {
  638. isLimit: true
  639. } );
  640. schema.extend( 'blockLimit', { allowIn: '$root' } );
  641. schema.extend( '$block', { allowIn: 'blockLimit' } );
  642. schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  643. schema.register( 'blockQuote', {
  644. allowWhere: '$block',
  645. allowContentOf: '$root'
  646. } );
  647. } );
  648. test(
  649. 'should delete inside block limit element',
  650. '<blockLimit><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph></blockLimit>',
  651. '<blockLimit><paragraph>fo[]</paragraph><paragraph>ar</paragraph></blockLimit>',
  652. { leaveUnmerged: true }
  653. );
  654. test(
  655. 'should delete inside block limit element (with merge)',
  656. '<blockLimit><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph></blockLimit>',
  657. '<blockLimit><paragraph>fo[]ar</paragraph></blockLimit>'
  658. );
  659. test(
  660. 'should delete whole block limit element',
  661. '<paragraph>x</paragraph>[<blockLimit><paragraph>foo</paragraph></blockLimit>]<paragraph>x</paragraph>',
  662. '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>x</paragraph>'
  663. );
  664. test(
  665. 'should delete from two block limit elements',
  666. '<blockLimit><paragraph>foo [bar</paragraph></blockLimit><blockLimit><paragraph>baz] qux</paragraph></blockLimit>',
  667. '<blockLimit><paragraph>foo []</paragraph></blockLimit><blockLimit><paragraph> qux</paragraph></blockLimit>'
  668. );
  669. test(
  670. 'merge option should be ignored if any of the elements is a limit',
  671. '<blockLimit><paragraph>foo [bar</paragraph></blockLimit><blockLimit><paragraph>baz] qux</paragraph></blockLimit>',
  672. '<blockLimit><paragraph>foo []</paragraph></blockLimit><blockLimit><paragraph> qux</paragraph></blockLimit>'
  673. );
  674. // See: https://github.com/ckeditor/ckeditor5/issues/1265.
  675. it( 'should proper merge two elements which are inside limit element', () => {
  676. setData( model,
  677. '<blockLimit>' +
  678. '<blockQuote>' +
  679. '<paragraph>Foo</paragraph>' +
  680. '</blockQuote>' +
  681. '<paragraph>[]Bar</paragraph>' +
  682. '</blockLimit>'
  683. );
  684. model.modifySelection( doc.selection, { direction: 'backward' } );
  685. deleteContent( model, doc.selection );
  686. expect( getData( model ) ).to.equal(
  687. '<blockLimit>' +
  688. '<blockQuote>' +
  689. '<paragraph>Foo[]Bar</paragraph>' +
  690. '</blockQuote>' +
  691. '</blockLimit>' );
  692. } );
  693. it( 'should proper merge elements which are inside limit element (nested elements)', () => {
  694. setData( model,
  695. '<blockQuote>' +
  696. '<blockLimit>' +
  697. '<blockQuote>' +
  698. '<paragraph>Foo.</paragraph>' +
  699. '<blockQuote>' +
  700. '<paragraph>Foo</paragraph>' +
  701. '</blockQuote>' +
  702. '</blockQuote>' +
  703. '<paragraph>[]Bar</paragraph>' +
  704. '</blockLimit>' +
  705. '</blockQuote>'
  706. );
  707. model.modifySelection( doc.selection, { direction: 'backward' } );
  708. deleteContent( model, doc.selection );
  709. expect( getData( model ) ).to.equal(
  710. '<blockQuote>' +
  711. '<blockLimit>' +
  712. '<blockQuote>' +
  713. '<paragraph>Foo.</paragraph>' +
  714. '<blockQuote>' +
  715. '<paragraph>Foo[]Bar</paragraph>' +
  716. '</blockQuote>' +
  717. '</blockQuote>' +
  718. '</blockLimit>' +
  719. '</blockQuote>'
  720. );
  721. } );
  722. } );
  723. describe( 'should leave a paragraph if the entire content was selected', () => {
  724. beforeEach( () => {
  725. model = new Model();
  726. doc = model.document;
  727. doc.createRoot();
  728. const schema = model.schema;
  729. schema.register( 'div', {
  730. inheritAllFrom: '$block',
  731. isLimit: true
  732. } );
  733. schema.register( 'article', {
  734. inheritAllFrom: '$block',
  735. isLimit: true
  736. } );
  737. schema.register( 'image', {
  738. allowWhere: '$text',
  739. isObject: true
  740. } );
  741. schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  742. schema.register( 'heading1', { inheritAllFrom: '$block' } );
  743. schema.register( 'heading2', { inheritAllFrom: '$block' } );
  744. schema.extend( '$text', { allowIn: '$root' } );
  745. schema.extend( 'image', { allowIn: '$root' } );
  746. schema.extend( 'image', { allowIn: 'heading1' } );
  747. schema.extend( 'heading1', { allowIn: 'div' } );
  748. schema.extend( 'paragraph', { allowIn: 'div' } );
  749. schema.extend( 'heading1', { allowIn: 'article' } );
  750. schema.extend( 'heading2', { allowIn: 'article' } );
  751. } );
  752. test(
  753. 'but not if only one block was selected',
  754. '<heading1>[xx]</heading1>',
  755. '<heading1>[]</heading1>'
  756. );
  757. test(
  758. 'when the entire heading and paragraph were selected',
  759. '<heading1>[xx</heading1><paragraph>yy]</paragraph>',
  760. '<paragraph>[]</paragraph>'
  761. );
  762. test(
  763. 'when the entire content was selected',
  764. '<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]</paragraph>',
  765. '<paragraph>[]</paragraph>'
  766. );
  767. test(
  768. 'inside the limit element when the entire heading and paragraph were inside',
  769. '<div><heading1>[xx</heading1><paragraph>yy]</paragraph></div>',
  770. '<div><paragraph>[]</paragraph></div>'
  771. );
  772. test(
  773. 'but not if schema does not accept paragraph in limit element',
  774. '<article><heading1>[xx</heading1><heading2>yy]</heading2></article>',
  775. '<article><heading1>[]</heading1></article>'
  776. );
  777. test(
  778. 'but not if selection is not containing the whole content',
  779. '<image></image><heading1>[xx</heading1><paragraph>yy]</paragraph>',
  780. '<image></image><heading1>[]</heading1>'
  781. );
  782. test(
  783. 'but not if only single element is selected',
  784. '<heading1>[<image></image>xx]</heading1>',
  785. '<heading1>[]</heading1>'
  786. );
  787. it( 'when root element was not added as Schema limits work fine as well', () => {
  788. doc.createRoot( 'paragraph', 'paragraphRoot' );
  789. setData(
  790. model,
  791. 'x[<image></image><image></image>]z',
  792. { rootName: 'paragraphRoot' }
  793. );
  794. deleteContent( model, doc.selection );
  795. expect( getData( model, { rootName: 'paragraphRoot' } ) )
  796. .to.equal( 'x[]z' );
  797. } );
  798. test(
  799. 'but not if the flag "doNotResetEntireContent" is set to true',
  800. '<heading1>[</heading1><paragraph>]</paragraph>',
  801. '<heading1>[]</heading1>',
  802. {
  803. doNotResetEntireContent: true
  804. }
  805. );
  806. } );
  807. function test( title, input, output, options ) {
  808. it( title, () => {
  809. model.enqueueChange( 'transparent', () => {
  810. setData( model, input );
  811. deleteContent( model, doc.selection, options );
  812. } );
  813. expect( getData( model ) ).to.equal( output );
  814. } );
  815. }
  816. } );
  817. } );