deletecontent.js 27 KB

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