8
0

restrictededitingmodeediting.js 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  6. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  7. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  8. import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
  9. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  10. import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  11. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  12. import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
  13. import StrikethroughEditing from '@ckeditor/ckeditor5-basic-styles/src/strikethrough/strikethroughediting';
  14. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  15. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  16. import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
  17. import RestrictedEditingModeNavigationCommand from '../src/restrictededitingmodenavigationcommand';
  18. import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
  19. describe( 'RestrictedEditingModeEditing', () => {
  20. let editor, model;
  21. testUtils.createSinonSandbox();
  22. describe( 'plugin', () => {
  23. beforeEach( async () => {
  24. editor = await VirtualTestEditor.create( { plugins: [ RestrictedEditingModeEditing ] } );
  25. } );
  26. afterEach( async () => {
  27. await editor.destroy();
  28. } );
  29. it( 'should be named', () => {
  30. expect( RestrictedEditingModeEditing.pluginName ).to.equal( 'RestrictedEditingModeEditing' );
  31. } );
  32. it( 'should be loaded', () => {
  33. expect( editor.plugins.get( RestrictedEditingModeEditing ) ).to.be.instanceOf( RestrictedEditingModeEditing );
  34. } );
  35. it( 'root should have "ck-restricted-editing_mode_restricted" class', () => {
  36. for ( const root of editor.editing.view.document.roots ) {
  37. expect( root.hasClass( 'ck-restricted-editing_mode_restricted' ) ).to.be.true;
  38. }
  39. } );
  40. it( 'adds a "goToPreviousRestrictedEditingException" command', () => {
  41. expect( editor.commands.get( 'goToPreviousRestrictedEditingException' ) )
  42. .to.be.instanceOf( RestrictedEditingModeNavigationCommand );
  43. } );
  44. it( 'adds a "goToNextRestrictedEditingException" command', () => {
  45. expect(
  46. editor.commands.get( 'goToNextRestrictedEditingException' )
  47. ).to.be.instanceOf( RestrictedEditingModeNavigationCommand );
  48. } );
  49. } );
  50. describe( 'conversion', () => {
  51. beforeEach( async () => {
  52. editor = await VirtualTestEditor.create( { plugins: [ Paragraph, RestrictedEditingModeEditing ] } );
  53. model = editor.model;
  54. } );
  55. afterEach( () => {
  56. return editor.destroy();
  57. } );
  58. describe( 'upcast', () => {
  59. it( 'should convert <span class="restricted-editing-exception"> to marker', () => {
  60. editor.setData( '<p>foo <span class="restricted-editing-exception">bar</span> baz</p>' );
  61. expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.true;
  62. assertMarkerRangePaths( [ 0, 4 ], [ 0, 7 ] );
  63. } );
  64. it( 'should convert multiple <span class="restricted-editing-exception">', () => {
  65. editor.setData(
  66. '<p>foo <span class="restricted-editing-exception">bar</span> baz</p>' +
  67. '<p>ABCDEF<span class="restricted-editing-exception">GHIJK</span>LMNOPQRST</p>'
  68. );
  69. expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.true;
  70. expect( model.markers.has( 'restrictedEditingException:2' ) ).to.be.true;
  71. // Data for the first marker is the same as in previous tests so no need to test it again.
  72. assertMarkerRangePaths( [ 1, 6 ], [ 1, 11 ], 2 );
  73. } );
  74. it( 'should not convert other <span> elements', () => {
  75. editor.setData( '<p>foo <span class="foo bar">bar</span> baz</p>' );
  76. expect( model.markers.has( 'restrictedEditingException:1' ) ).to.be.false;
  77. } );
  78. } );
  79. describe( 'downcast', () => {
  80. it( 'should convert model marker to <span>', () => {
  81. setModelData( model, '<paragraph>foo bar baz</paragraph>' );
  82. const paragraph = model.document.getRoot().getChild( 0 );
  83. model.change( writer => {
  84. writer.addMarker( 'restrictedEditingException:1', {
  85. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  86. usingOperation: true,
  87. affectsData: true
  88. } );
  89. } );
  90. const expectedView = '<p>foo <span class="restricted-editing-exception">bar</span> baz</p>';
  91. expect( editor.getData() ).to.equal( expectedView );
  92. expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
  93. } );
  94. it( 'should convert collapsed model marker to <span>', () => {
  95. setModelData( model, '<paragraph>foo bar baz</paragraph>' );
  96. const paragraph = model.document.getRoot().getChild( 0 );
  97. model.change( writer => {
  98. writer.addMarker( 'restrictedEditingException:1', {
  99. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 4 ) ),
  100. usingOperation: true,
  101. affectsData: true
  102. } );
  103. } );
  104. expect( editor.getData() ).to.equal( '<p>foo <span class="restricted-editing-exception"></span>bar baz</p>' );
  105. expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
  106. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_collapsed"></span>bar baz</p>'
  107. );
  108. } );
  109. it( 'converted <span> should be the outermost attribute element', () => {
  110. editor.conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'b' } );
  111. setModelData( model, '<paragraph><$text bold="true">foo bar baz</$text></paragraph>' );
  112. const paragraph = model.document.getRoot().getChild( 0 );
  113. model.change( writer => {
  114. writer.addMarker( 'restrictedEditingException:1', {
  115. range: writer.createRange( writer.createPositionAt( paragraph, 0 ), writer.createPositionAt( paragraph, 'end' ) ),
  116. usingOperation: true,
  117. affectsData: true
  118. } );
  119. } );
  120. expect( editor.getData() ).to.equal(
  121. '<p><span class="restricted-editing-exception"><b>foo bar baz</b></span></p>'
  122. );
  123. expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
  124. '<p>' +
  125. '<span class="restricted-editing-exception restricted-editing-exception_selected"><b>foo bar baz</b></span>' +
  126. '</p>'
  127. );
  128. } );
  129. } );
  130. } );
  131. describe( 'editing behavior', () => {
  132. beforeEach( async () => {
  133. editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ] } );
  134. model = editor.model;
  135. } );
  136. afterEach( () => {
  137. return editor.destroy();
  138. } );
  139. it( 'should keep markers in the view when editable region is edited', () => {
  140. setModelData( model,
  141. '<paragraph>foo bar baz</paragraph>' +
  142. '<paragraph>xxx y[]yy zzz</paragraph>'
  143. );
  144. const firstParagraph = model.document.getRoot().getChild( 0 );
  145. const secondParagraph = model.document.getRoot().getChild( 1 );
  146. model.change( writer => {
  147. writer.addMarker( 'restrictedEditingException:1', {
  148. range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
  149. usingOperation: true,
  150. affectsData: true
  151. } );
  152. writer.addMarker( 'restrictedEditingException:2', {
  153. range: writer.createRange(
  154. writer.createPositionAt( secondParagraph, 4 ),
  155. writer.createPositionAt( secondParagraph, 7 )
  156. ),
  157. usingOperation: true,
  158. affectsData: true
  159. } );
  160. } );
  161. model.change( writer => {
  162. model.insertContent( writer.createText( 'R', model.document.selection.getAttributes() ) );
  163. } );
  164. expect( editor.getData() ).to.equal(
  165. '<p>foo <span class="restricted-editing-exception">bar</span> baz</p>' +
  166. '<p>xxx <span class="restricted-editing-exception">yRyy</span> zzz</p>' );
  167. expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
  168. '<p>foo <span class="restricted-editing-exception">bar</span> baz</p>' +
  169. '<p>xxx <span class="restricted-editing-exception restricted-editing-exception_selected">yRyy</span> zzz</p>' );
  170. } );
  171. it( 'should block user typing outside exception markers', () => {
  172. setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
  173. editor.execute( 'input', { text: 'X' } );
  174. assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
  175. } );
  176. it( 'should not block user typing inside exception marker', () => {
  177. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  178. const firstParagraph = model.document.getRoot().getChild( 0 );
  179. model.change( writer => {
  180. writer.addMarker( 'restrictedEditingException:1', {
  181. range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
  182. usingOperation: true,
  183. affectsData: true
  184. } );
  185. } );
  186. model.change( writer => {
  187. writer.setSelection( firstParagraph, 5 );
  188. } );
  189. editor.execute( 'input', { text: 'X' } );
  190. assertEqualMarkup( getModelData( model ), '<paragraph>foo bX[]ar baz</paragraph>' );
  191. } );
  192. it( 'should extend maker when typing on the marker boundary (end)', () => {
  193. setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );
  194. const firstParagraph = model.document.getRoot().getChild( 0 );
  195. model.change( writer => {
  196. writer.addMarker( 'restrictedEditingException:1', {
  197. range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
  198. usingOperation: true,
  199. affectsData: true
  200. } );
  201. } );
  202. editor.execute( 'input', { text: 'X' } );
  203. assertEqualMarkup( getModelData( model ), '<paragraph>foo barX[] baz</paragraph>' );
  204. const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
  205. const expectedRange = model.createRange(
  206. model.createPositionAt( firstParagraph, 4 ),
  207. model.createPositionAt( firstParagraph, 8 )
  208. );
  209. expect( markerRange.isEqual( expectedRange ) ).to.be.true;
  210. } );
  211. it( 'should extend marker when typing on the marker boundary (start)', () => {
  212. setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
  213. const firstParagraph = model.document.getRoot().getChild( 0 );
  214. model.change( writer => {
  215. writer.addMarker( 'restrictedEditingException:1', {
  216. range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ), writer.createPositionAt( firstParagraph, 7 ) ),
  217. usingOperation: true,
  218. affectsData: true
  219. } );
  220. } );
  221. editor.execute( 'input', { text: 'X' } );
  222. assertEqualMarkup( getModelData( model ), '<paragraph>foo X[]bar baz</paragraph>' );
  223. const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
  224. const expectedRange = model.createRange(
  225. model.createPositionAt( firstParagraph, 4 ),
  226. model.createPositionAt( firstParagraph, 8 )
  227. );
  228. expect( markerRange.isEqual( expectedRange ) ).to.be.true;
  229. } );
  230. it( 'should extend marker when typing on the marker boundary (collapsed marker)', () => {
  231. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  232. const firstParagraph = model.document.getRoot().getChild( 0 );
  233. model.change( writer => {
  234. writer.addMarker( 'restrictedEditingException:1', {
  235. range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ) ),
  236. usingOperation: true,
  237. affectsData: true
  238. } );
  239. } );
  240. model.change( writer => {
  241. writer.setSelection( writer.createPositionAt( firstParagraph, 4 ) );
  242. } );
  243. editor.execute( 'input', { text: 'X' } );
  244. assertEqualMarkup( getModelData( model ), '<paragraph>foo X[]bar baz</paragraph>' );
  245. const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
  246. const expectedRange = model.createRange(
  247. model.createPositionAt( firstParagraph, 4 ),
  248. model.createPositionAt( firstParagraph, 5 )
  249. );
  250. expect( markerRange.isEqual( expectedRange ) ).to.be.true;
  251. } );
  252. it( 'should retain marker on non-typing change at the marker boundary (start)', () => {
  253. setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );
  254. const firstParagraph = model.document.getRoot().getChild( 0 );
  255. addExceptionMarker( 4, 7, firstParagraph );
  256. model.change( writer => {
  257. editor.execute( 'delete', {
  258. selection: writer.createSelection( writer.createRange(
  259. writer.createPositionAt( firstParagraph, 4 ),
  260. writer.createPositionAt( firstParagraph, 6 )
  261. ) )
  262. } );
  263. editor.execute( 'input', {
  264. text: 'XX',
  265. range: writer.createRange( writer.createPositionAt( firstParagraph, 4 ) )
  266. } );
  267. } );
  268. assertEqualMarkup( getModelData( model ), '<paragraph>foo XX[]r baz</paragraph>' );
  269. const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
  270. const expectedRange = model.createRange(
  271. model.createPositionAt( firstParagraph, 4 ),
  272. model.createPositionAt( firstParagraph, 7 )
  273. );
  274. expect( markerRange.isEqual( expectedRange ) ).to.be.true;
  275. } );
  276. it( 'should retain marker on non-typing change at marker boundary (end)', () => {
  277. setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );
  278. const firstParagraph = model.document.getRoot().getChild( 0 );
  279. addExceptionMarker( 4, 7, firstParagraph );
  280. model.change( writer => {
  281. editor.execute( 'delete', {
  282. selection: writer.createSelection( writer.createRange(
  283. writer.createPositionAt( firstParagraph, 5 ),
  284. writer.createPositionAt( firstParagraph, 7 )
  285. ) )
  286. } );
  287. editor.execute( 'input', {
  288. text: 'XX',
  289. range: writer.createRange( writer.createPositionAt( firstParagraph, 5 ) )
  290. } );
  291. } );
  292. assertEqualMarkup( getModelData( model ), '<paragraph>foo bXX[] baz</paragraph>' );
  293. const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
  294. const expectedRange = model.createRange(
  295. model.createPositionAt( firstParagraph, 4 ),
  296. model.createPositionAt( firstParagraph, 7 )
  297. );
  298. expect( markerRange.isEqual( expectedRange ) ).to.be.true;
  299. } );
  300. it( 'should not move collapsed marker to $graveyard', () => {
  301. setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
  302. const firstParagraph = model.document.getRoot().getChild( 0 );
  303. model.change( writer => {
  304. writer.addMarker( 'restrictedEditingException:1', {
  305. range: writer.createRange(
  306. writer.createPositionAt( firstParagraph, 4 ),
  307. writer.createPositionAt( firstParagraph, 5 )
  308. ),
  309. usingOperation: true,
  310. affectsData: true
  311. } );
  312. } );
  313. editor.execute( 'delete' );
  314. assertEqualMarkup( getModelData( model ), '<paragraph>foo []ar baz</paragraph>' );
  315. const markerRange = editor.model.markers.get( 'restrictedEditingException:1' ).getRange();
  316. const expectedRange = model.createRange(
  317. model.createPositionAt( firstParagraph, 4 ),
  318. model.createPositionAt( firstParagraph, 4 )
  319. );
  320. expect( markerRange.isEqual( expectedRange ) ).to.be.true;
  321. } );
  322. } );
  323. describe( 'enforcing restrictions on deleteContent', () => {
  324. beforeEach( async () => {
  325. editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ] } );
  326. model = editor.model;
  327. } );
  328. afterEach( async () => {
  329. await editor.destroy();
  330. } );
  331. it( 'should not allow to delete content outside restricted area', () => {
  332. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  333. const firstParagraph = model.document.getRoot().getChild( 0 );
  334. addExceptionMarker( 3, 9, firstParagraph );
  335. model.change( writer => {
  336. writer.setSelection( firstParagraph, 2 );
  337. } );
  338. model.deleteContent( model.document.selection );
  339. assertEqualMarkup( getModelData( model ), '<paragraph>fo[]o bar baz</paragraph>' );
  340. } );
  341. it( 'should trim deleted content to a exception marker (focus in marker)', () => {
  342. setModelData( model, '<paragraph>[]foofoo bar baz</paragraph>' );
  343. const firstParagraph = model.document.getRoot().getChild( 0 );
  344. addExceptionMarker( 3, 9, firstParagraph );
  345. model.change( writer => {
  346. const selection = writer.createSelection( writer.createRange(
  347. writer.createPositionAt( firstParagraph, 0 ),
  348. writer.createPositionAt( firstParagraph, 6 )
  349. ) );
  350. model.deleteContent( selection );
  351. } );
  352. assertEqualMarkup( getModelData( model ), '<paragraph>[]foo bar baz</paragraph>' );
  353. } );
  354. it( 'should trim deleted content to a exception marker (anchor in marker)', () => {
  355. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  356. const firstParagraph = model.document.getRoot().getChild( 0 );
  357. addExceptionMarker( 4, 7, firstParagraph );
  358. model.change( writer => {
  359. const selection = writer.createSelection( writer.createRange(
  360. writer.createPositionAt( firstParagraph, 5 ),
  361. writer.createPositionAt( firstParagraph, 8 )
  362. ) );
  363. model.deleteContent( selection );
  364. } );
  365. assertEqualMarkup( getModelData( model ), '<paragraph>[]foo b baz</paragraph>' );
  366. } );
  367. it( 'should trim deleted content to a exception marker and alter the selection argument (delete command integration)', () => {
  368. setModelData( model, '<paragraph>[]foofoo bar baz</paragraph>' );
  369. const firstParagraph = model.document.getRoot().getChild( 0 );
  370. addExceptionMarker( 3, 9, firstParagraph );
  371. model.change( writer => {
  372. writer.setSelection( firstParagraph, 6 );
  373. } );
  374. editor.execute( 'delete', { unit: 'word' } );
  375. assertEqualMarkup( getModelData( model ), '<paragraph>foo[] bar baz</paragraph>' );
  376. } );
  377. it( 'should work with document selection', () => {
  378. setModelData( model, '<paragraph>f[oo bar] baz</paragraph>' );
  379. const firstParagraph = model.document.getRoot().getChild( 0 );
  380. addExceptionMarker( 2, 'end', firstParagraph );
  381. model.change( () => {
  382. model.deleteContent( model.document.selection );
  383. } );
  384. assertEqualMarkup( getModelData( model, { withoutSelection: true } ), '<paragraph>fo baz</paragraph>' );
  385. } );
  386. } );
  387. describe( 'enforcing restrictions on input command', () => {
  388. let firstParagraph;
  389. beforeEach( async () => {
  390. editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ] } );
  391. model = editor.model;
  392. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  393. firstParagraph = model.document.getRoot().getChild( 0 );
  394. } );
  395. afterEach( async () => {
  396. await editor.destroy();
  397. } );
  398. it( 'should prevent changing text before exception marker', () => {
  399. addExceptionMarker( 4, 7, firstParagraph );
  400. model.change( writer => {
  401. writer.setSelection( firstParagraph, 5 );
  402. } );
  403. // Simulate native spell-check action.
  404. editor.execute( 'input', {
  405. text: 'xxxxxxx',
  406. range: model.createRange(
  407. model.createPositionAt( firstParagraph, 0 ),
  408. model.createPositionAt( firstParagraph, 7 )
  409. )
  410. } );
  411. assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
  412. } );
  413. it( 'should prevent changing text before exception marker', () => {
  414. addExceptionMarker( 4, 7, firstParagraph );
  415. model.change( writer => {
  416. writer.setSelection( firstParagraph, 5 );
  417. } );
  418. // Simulate native spell-check action.
  419. editor.execute( 'input', {
  420. text: 'xxxxxxx',
  421. range: model.createRange(
  422. model.createPositionAt( firstParagraph, 4 ),
  423. model.createPositionAt( firstParagraph, 9 )
  424. )
  425. } );
  426. assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
  427. } );
  428. it( 'should prevent changing text before (change crossing different markers)', () => {
  429. addExceptionMarker( 0, 4, firstParagraph );
  430. addExceptionMarker( 7, 9, firstParagraph, 2 );
  431. model.change( writer => {
  432. writer.setSelection( firstParagraph, 2 );
  433. } );
  434. // Simulate native spell-check action.
  435. editor.execute( 'input', {
  436. text: 'xxxxxxx',
  437. range: model.createRange(
  438. model.createPositionAt( firstParagraph, 2 ),
  439. model.createPositionAt( firstParagraph, 8 )
  440. )
  441. } );
  442. assertEqualMarkup( getModelData( model ), '<paragraph>fo[]o bar baz</paragraph>' );
  443. } );
  444. it( 'should allow changing text inside single marker', () => {
  445. addExceptionMarker( 0, 9, firstParagraph );
  446. model.change( writer => {
  447. writer.setSelection( firstParagraph, 2 );
  448. } );
  449. // Simulate native spell-check action.
  450. editor.execute( 'input', {
  451. text: 'xxxxxxx',
  452. range: model.createRange(
  453. model.createPositionAt( firstParagraph, 2 ),
  454. model.createPositionAt( firstParagraph, 8 )
  455. )
  456. } );
  457. assertEqualMarkup( getModelData( model ), '<paragraph>foxxxxxxx[]baz</paragraph>' );
  458. } );
  459. } );
  460. describe( 'clipboard', () => {
  461. let viewDoc;
  462. beforeEach( async () => {
  463. editor = await VirtualTestEditor.create( {
  464. plugins: [ Paragraph, BoldEditing, ItalicEditing, StrikethroughEditing, Typing, Clipboard, RestrictedEditingModeEditing ]
  465. } );
  466. model = editor.model;
  467. viewDoc = editor.editing.view.document;
  468. } );
  469. afterEach( async () => {
  470. await editor.destroy();
  471. } );
  472. describe( 'cut', () => {
  473. it( 'should be blocked outside exception markers', () => {
  474. setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
  475. const spy = sinon.spy();
  476. viewDoc.on( 'clipboardOutput', spy, { priority: 'high' } );
  477. viewDoc.fire( 'clipboardOutput', {
  478. content: {
  479. isEmpty: true
  480. },
  481. method: 'cut'
  482. } );
  483. sinon.assert.notCalled( spy );
  484. assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
  485. } );
  486. it( 'should cut selected content inside exception marker (selection inside marker)', () => {
  487. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  488. const firstParagraph = model.document.getRoot().getChild( 0 );
  489. addExceptionMarker( 4, 7, firstParagraph );
  490. viewDoc.fire( 'clipboardOutput', {
  491. content: {
  492. isEmpty: true
  493. },
  494. method: 'cut'
  495. } );
  496. assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]r baz</paragraph>' );
  497. } );
  498. it( 'should cut selected content inside exception marker (selection touching marker start)', () => {
  499. setModelData( model, '<paragraph>foo [ba]r baz</paragraph>' );
  500. const firstParagraph = model.document.getRoot().getChild( 0 );
  501. addExceptionMarker( 4, 7, firstParagraph );
  502. viewDoc.fire( 'clipboardOutput', {
  503. content: {
  504. isEmpty: true
  505. },
  506. method: 'cut'
  507. } );
  508. assertEqualMarkup( getModelData( model ), '<paragraph>foo []r baz</paragraph>' );
  509. } );
  510. it( 'should cut selected content inside exception marker (selection touching marker end)', () => {
  511. setModelData( model, '<paragraph>foo b[ar] baz</paragraph>' );
  512. const firstParagraph = model.document.getRoot().getChild( 0 );
  513. addExceptionMarker( 4, 7, firstParagraph );
  514. viewDoc.fire( 'clipboardOutput', {
  515. content: {
  516. isEmpty: true
  517. },
  518. method: 'cut'
  519. } );
  520. assertEqualMarkup( getModelData( model ), '<paragraph>foo b[] baz</paragraph>' );
  521. } );
  522. } );
  523. describe( 'copy', () => {
  524. it( 'should not be blocked outside exception markers', () => {
  525. setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
  526. const spy = sinon.spy();
  527. viewDoc.on( 'clipboardOutput', spy, { priority: 'high' } );
  528. viewDoc.fire( 'clipboardOutput', {
  529. content: {
  530. isEmpty: true
  531. },
  532. method: 'copy'
  533. } );
  534. sinon.assert.calledOnce( spy );
  535. assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
  536. } );
  537. it( 'should not be blocked inside exception marker', () => {
  538. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  539. const firstParagraph = model.document.getRoot().getChild( 0 );
  540. const spy = sinon.spy();
  541. viewDoc.on( 'clipboardOutput', spy, { priority: 'high' } );
  542. model.change( writer => {
  543. writer.addMarker( 'restrictedEditingException:1', {
  544. range: writer.createRange(
  545. writer.createPositionAt( firstParagraph, 4 ),
  546. writer.createPositionAt( firstParagraph, 7 )
  547. ),
  548. usingOperation: true,
  549. affectsData: true
  550. } );
  551. } );
  552. model.change( writer => {
  553. writer.setSelection( firstParagraph, 5 );
  554. } );
  555. viewDoc.fire( 'clipboardOutput', {
  556. content: {
  557. isEmpty: true
  558. },
  559. method: 'copy'
  560. } );
  561. sinon.assert.calledOnce( spy );
  562. assertEqualMarkup( getModelData( model ), '<paragraph>foo b[]ar baz</paragraph>' );
  563. } );
  564. } );
  565. describe( 'paste', () => {
  566. beforeEach( () => {
  567. // Required when testing without DOM using VirtualTestEditor - Clipboard feature scrolls after paste event.
  568. sinon.stub( editor.editing.view, 'scrollToTheSelection' );
  569. } );
  570. it( 'should be blocked outside exception markers (collapsed selection)', () => {
  571. setModelData( model, '<paragraph>foo []bar baz</paragraph>' );
  572. const spy = sinon.spy();
  573. viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
  574. viewDoc.fire( 'clipboardInput', {
  575. dataTransfer: {
  576. getData: sinon.spy()
  577. }
  578. } );
  579. sinon.assert.notCalled( spy );
  580. assertEqualMarkup( getModelData( model ), '<paragraph>foo []bar baz</paragraph>' );
  581. } );
  582. it( 'should be blocked outside exception markers (non-collapsed selection)', () => {
  583. setModelData( model, '<paragraph>[foo bar baz]</paragraph>' );
  584. const spy = sinon.spy();
  585. viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
  586. viewDoc.fire( 'clipboardInput', {
  587. dataTransfer: {
  588. getData: sinon.spy()
  589. }
  590. } );
  591. sinon.assert.notCalled( spy );
  592. assertEqualMarkup( getModelData( model ), '<paragraph>[foo bar baz]</paragraph>' );
  593. } );
  594. it( 'should be blocked outside exception markers (non-collapsed selection, starts inside exception marker)', () => {
  595. setModelData( model, '<paragraph>foo b[ar baz]</paragraph>' );
  596. const spy = sinon.spy();
  597. viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
  598. viewDoc.fire( 'clipboardInput', {
  599. dataTransfer: {
  600. getData: sinon.spy()
  601. }
  602. } );
  603. sinon.assert.notCalled( spy );
  604. assertEqualMarkup( getModelData( model ), '<paragraph>foo b[ar baz]</paragraph>' );
  605. } );
  606. it( 'should be blocked outside exception markers (non-collapsed selection, ends inside exception marker)', () => {
  607. setModelData( model, '<paragraph>[foo ba]r baz</paragraph>' );
  608. const spy = sinon.spy();
  609. viewDoc.on( 'clipboardInput', spy, { priority: 'high' } );
  610. viewDoc.fire( 'clipboardInput', {
  611. dataTransfer: {
  612. getData: sinon.spy()
  613. }
  614. } );
  615. sinon.assert.notCalled( spy );
  616. assertEqualMarkup( getModelData( model ), '<paragraph>[foo ba]r baz</paragraph>' );
  617. } );
  618. describe( 'collapsed selection', () => {
  619. it( 'should paste text inside exception marker', () => {
  620. setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
  621. const firstParagraph = model.document.getRoot().getChild( 0 );
  622. addExceptionMarker( 4, 7, firstParagraph );
  623. viewDoc.fire( 'clipboardInput', {
  624. dataTransfer: createDataTransfer( { 'text/html': '<p>XXX</p>', 'text/plain': 'XXX' } )
  625. } );
  626. assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]ar baz</paragraph>' );
  627. assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
  628. } );
  629. it( 'should paste allowed text attributes inside exception marker', () => {
  630. setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
  631. const firstParagraph = model.document.getRoot().getChild( 0 );
  632. addExceptionMarker( 4, 7, firstParagraph );
  633. viewDoc.fire( 'clipboardInput', {
  634. dataTransfer: createDataTransfer( { 'text/html': '<p><b>XXX</b></p>', 'text/plain': 'XXX' } )
  635. } );
  636. assertEqualMarkup( getModelData( model ), '<paragraph>foo b<$text bold="true">XXX[]</$text>ar baz</paragraph>' );
  637. assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
  638. } );
  639. it( 'should not allow to paste disallowed text attributes inside exception marker', () => {
  640. setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
  641. const firstParagraph = model.document.getRoot().getChild( 0 );
  642. addExceptionMarker( 4, 7, firstParagraph );
  643. viewDoc.fire( 'clipboardInput', {
  644. dataTransfer: createDataTransfer( { 'text/html': '<p><s>XXX</s></p>', 'text/plain': 'XXX' } )
  645. } );
  646. assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]ar baz</paragraph>' );
  647. assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
  648. } );
  649. it( 'should filter out disallowed attributes from other text attributes when pasting inside exception marker', () => {
  650. setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );
  651. const firstParagraph = model.document.getRoot().getChild( 0 );
  652. addExceptionMarker( 4, 7, firstParagraph );
  653. viewDoc.fire( 'clipboardInput', {
  654. dataTransfer: createDataTransfer( { 'text/html': '<p><b><s><i>XXX</i></s></b></p>', 'text/plain': 'XXX' } )
  655. } );
  656. assertEqualMarkup(
  657. getModelData( model ),
  658. '<paragraph>foo b<$text bold="true" italic="true">XXX[]</$text>ar baz</paragraph>'
  659. );
  660. assertMarkerRangePaths( [ 0, 4 ], [ 0, 10 ] );
  661. } );
  662. } );
  663. describe( 'non-collapsed selection', () => {
  664. it( 'should paste text inside exception marker', () => {
  665. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  666. const firstParagraph = model.document.getRoot().getChild( 0 );
  667. addExceptionMarker( 4, 7, firstParagraph );
  668. viewDoc.fire( 'clipboardInput', {
  669. dataTransfer: createDataTransfer( { 'text/html': '<p>XXX</p>', 'text/plain': 'XXX' } )
  670. } );
  671. assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]r baz</paragraph>' );
  672. assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
  673. } );
  674. it( 'should paste allowed text attributes inside exception marker', () => {
  675. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  676. const firstParagraph = model.document.getRoot().getChild( 0 );
  677. addExceptionMarker( 4, 7, firstParagraph );
  678. viewDoc.fire( 'clipboardInput', {
  679. dataTransfer: createDataTransfer( { 'text/html': '<p><b>XXX</b></p>', 'text/plain': 'XXX' } )
  680. } );
  681. assertEqualMarkup( getModelData( model ), '<paragraph>foo b<$text bold="true">XXX[]</$text>r baz</paragraph>' );
  682. assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
  683. } );
  684. it( 'should not allow to paste disallowed text attributes inside exception marker', () => {
  685. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  686. const firstParagraph = model.document.getRoot().getChild( 0 );
  687. addExceptionMarker( 4, 7, firstParagraph );
  688. viewDoc.fire( 'clipboardInput', {
  689. dataTransfer: createDataTransfer( { 'text/html': '<p><s>XXX</s></p>', 'text/plain': 'XXX' } )
  690. } );
  691. assertEqualMarkup( getModelData( model ), '<paragraph>foo bXXX[]r baz</paragraph>' );
  692. assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
  693. } );
  694. it( 'should filter out disallowed attributes from other text attributes when pasting inside exception marker', () => {
  695. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  696. const firstParagraph = model.document.getRoot().getChild( 0 );
  697. addExceptionMarker( 4, 7, firstParagraph );
  698. viewDoc.fire( 'clipboardInput', {
  699. dataTransfer: createDataTransfer( { 'text/html': '<p><b><s><i>XXX</i></s></b></p>', 'text/plain': 'XXX' } )
  700. } );
  701. assertEqualMarkup(
  702. getModelData( model ),
  703. '<paragraph>foo b<$text bold="true" italic="true">XXX[]</$text>r baz</paragraph>'
  704. );
  705. assertMarkerRangePaths( [ 0, 4 ], [ 0, 9 ] );
  706. } );
  707. } );
  708. } );
  709. } );
  710. describe( 'exception highlighting', () => {
  711. let view;
  712. beforeEach( async () => {
  713. editor = await VirtualTestEditor.create( {
  714. plugins: [ Paragraph, RestrictedEditingModeEditing, BoldEditing ]
  715. } );
  716. model = editor.model;
  717. view = editor.editing.view;
  718. } );
  719. afterEach( () => {
  720. return editor.destroy();
  721. } );
  722. it( 'should convert the highlight to a proper view classes', () => {
  723. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  724. const paragraph = model.document.getRoot().getChild( 0 );
  725. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  726. model.change( writer => {
  727. writer.addMarker( 'restrictedEditingException:1', {
  728. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  729. usingOperation: true,
  730. affectsData: true
  731. } );
  732. } );
  733. expect( getViewData( view ) ).to.equal(
  734. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_selected">b{a}r</span> baz</p>'
  735. );
  736. } );
  737. it( 'should remove classes when selection is moved away from an exception', () => {
  738. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  739. const paragraph = model.document.getRoot().getChild( 0 );
  740. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  741. model.change( writer => {
  742. writer.addMarker( 'restrictedEditingException:1', {
  743. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  744. usingOperation: true,
  745. affectsData: true
  746. } );
  747. } );
  748. expect( getViewData( view ) ).to.equal(
  749. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_selected">b{a}r</span> baz</p>'
  750. );
  751. model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 0 ) );
  752. expect( getViewData( view ) ).to.equal(
  753. '<p>{}foo <span class="restricted-editing-exception">bar</span> baz</p>'
  754. );
  755. } );
  756. it( 'should work correctly when selection is moved inside an exception', () => {
  757. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  758. const paragraph = model.document.getRoot().getChild( 0 );
  759. // <paragraph>[]foo <$marker>bar</$marker> baz</paragraph>
  760. model.change( writer => {
  761. writer.addMarker( 'restrictedEditingException:1', {
  762. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  763. usingOperation: true,
  764. affectsData: true
  765. } );
  766. } );
  767. expect( getViewData( view ) ).to.equal(
  768. '<p>{}foo <span class="restricted-editing-exception">bar</span> baz</p>'
  769. );
  770. model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 6 ) );
  771. expect( getViewData( view ) ).to.equal(
  772. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_selected">ba{}r</span> baz</p>'
  773. );
  774. } );
  775. describe( 'editing downcast conversion integration', () => {
  776. it( 'works for the #insert event', () => {
  777. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  778. const paragraph = model.document.getRoot().getChild( 0 );
  779. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  780. model.change( writer => {
  781. writer.addMarker( 'restrictedEditingException:1', {
  782. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  783. usingOperation: true,
  784. affectsData: true
  785. } );
  786. } );
  787. model.change( writer => {
  788. writer.insertText( 'FOO', { linkHref: 'url' }, model.document.selection.getFirstPosition() );
  789. } );
  790. expect( getViewData( view ) ).to.equal(
  791. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_selected">bFOO{a}r</span> baz</p>'
  792. );
  793. } );
  794. it( 'works for the #remove event', () => {
  795. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  796. const paragraph = model.document.getRoot().getChild( 0 );
  797. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  798. model.change( writer => {
  799. writer.addMarker( 'restrictedEditingException:1', {
  800. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  801. usingOperation: true,
  802. affectsData: true
  803. } );
  804. } );
  805. model.change( writer => {
  806. writer.remove( writer.createRange(
  807. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 ),
  808. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 6 )
  809. ) );
  810. } );
  811. expect( getViewData( view ) ).to.equal(
  812. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_selected">b{}r</span> baz</p>'
  813. );
  814. } );
  815. it( 'works for the #attribute event', () => {
  816. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  817. const paragraph = model.document.getRoot().getChild( 0 );
  818. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  819. model.change( writer => {
  820. writer.addMarker( 'restrictedEditingException:1', {
  821. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  822. usingOperation: true,
  823. affectsData: true
  824. } );
  825. } );
  826. model.change( writer => {
  827. writer.setAttribute( 'bold', true, writer.createRange(
  828. model.document.selection.getFirstPosition().getShiftedBy( -1 ),
  829. model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
  830. );
  831. } );
  832. expect( getViewData( view ) ).to.equal(
  833. '<p>foo ' +
  834. '<span class="restricted-editing-exception restricted-editing-exception_selected">' +
  835. '<strong>b{a</strong>' +
  836. '}r</span>' +
  837. ' baz</p>'
  838. );
  839. } );
  840. it( 'works for the #selection event', () => {
  841. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  842. const paragraph = model.document.getRoot().getChild( 0 );
  843. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  844. model.change( writer => {
  845. writer.addMarker( 'restrictedEditingException:1', {
  846. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  847. usingOperation: true,
  848. affectsData: true
  849. } );
  850. } );
  851. model.change( writer => {
  852. writer.setSelection( writer.createRange(
  853. model.document.selection.getFirstPosition().getShiftedBy( -1 ),
  854. model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
  855. );
  856. } );
  857. expect( getViewData( view ) ).to.equal(
  858. '<p>foo {<span class="restricted-editing-exception restricted-editing-exception_selected">ba}r</span> baz</p>'
  859. );
  860. } );
  861. it( 'works for the addMarker and removeMarker events', () => {
  862. editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'fooMarker', view: {} } );
  863. setModelData( model, '<paragraph>foo b[a]r baz</paragraph>' );
  864. const paragraph = model.document.getRoot().getChild( 0 );
  865. // <paragraph>foo <$marker>b[a]r</$marker> baz</paragraph>
  866. model.change( writer => {
  867. writer.addMarker( 'restrictedEditingException:1', {
  868. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  869. usingOperation: true,
  870. affectsData: true
  871. } );
  872. } );
  873. model.change( writer => {
  874. const range = writer.createRange(
  875. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
  876. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
  877. );
  878. writer.addMarker( 'fooMarker', { range, usingOperation: true } );
  879. } );
  880. expect( getViewData( view ) ).to.equal(
  881. '<p>' +
  882. '<span>foo </span>' +
  883. '<span class="restricted-editing-exception restricted-editing-exception_selected">' +
  884. '<span>b</span>{a}r' +
  885. '</span>' +
  886. ' baz</p>'
  887. );
  888. model.change( writer => writer.removeMarker( 'fooMarker' ) );
  889. expect( getViewData( view ) ).to.equal(
  890. '<p>foo <span class="restricted-editing-exception restricted-editing-exception_selected">b{a}r</span> baz</p>'
  891. );
  892. } );
  893. } );
  894. } );
  895. describe( 'exception cycling with the keyboard', () => {
  896. let view, domEvtDataStub;
  897. beforeEach( async () => {
  898. editor = await VirtualTestEditor.create( {
  899. plugins: [ Paragraph, RestrictedEditingModeEditing, BoldEditing ]
  900. } );
  901. model = editor.model;
  902. view = editor.editing.view;
  903. domEvtDataStub = {
  904. keyCode: getCode( 'Tab' ),
  905. preventDefault: sinon.spy(),
  906. stopPropagation: sinon.spy()
  907. };
  908. sinon.spy( editor, 'execute' );
  909. } );
  910. afterEach( () => {
  911. return editor.destroy();
  912. } );
  913. it( 'should move to the closest next exception on tab key', () => {
  914. setModelData( model, '<paragraph>[]foo bar baz qux</paragraph>' );
  915. const paragraph = model.document.getRoot().getChild( 0 );
  916. // <paragraph>[]foo <marker>bar</marker> baz qux</paragraph>
  917. model.change( writer => {
  918. writer.addMarker( 'restrictedEditingException:1', {
  919. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  920. usingOperation: true,
  921. affectsData: true
  922. } );
  923. } );
  924. // <paragraph>[]foo <marker>bar</marker> <marker>baz</marker≥ qux</paragraph>
  925. model.change( writer => {
  926. writer.addMarker( 'restrictedEditingException:2', {
  927. range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
  928. usingOperation: true,
  929. affectsData: true
  930. } );
  931. } );
  932. view.document.fire( 'keydown', domEvtDataStub );
  933. sinon.assert.calledOnce( editor.execute );
  934. sinon.assert.calledWithExactly( editor.execute, 'goToNextRestrictedEditingException' );
  935. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  936. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  937. } );
  938. it( 'should not move to the closest next exception on tab key when there is none', () => {
  939. setModelData( model, '<paragraph>foo qux[]</paragraph>' );
  940. const paragraph = model.document.getRoot().getChild( 0 );
  941. // <paragraph><marker>foo</marker> qux[]</paragraph>
  942. model.change( writer => {
  943. writer.addMarker( 'restrictedEditingException:1', {
  944. range: writer.createRange( writer.createPositionAt( paragraph, 0 ), writer.createPositionAt( paragraph, 3 ) ),
  945. usingOperation: true,
  946. affectsData: true
  947. } );
  948. } );
  949. view.document.fire( 'keydown', domEvtDataStub );
  950. sinon.assert.notCalled( editor.execute );
  951. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  952. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  953. } );
  954. it( 'should move to the closest previous exception on shift+tab key', () => {
  955. setModelData( model, '<paragraph>foo bar baz qux[]</paragraph>' );
  956. const paragraph = model.document.getRoot().getChild( 0 );
  957. // <paragraph>foo <marker>bar</marker> baz qux[]</paragraph>
  958. model.change( writer => {
  959. writer.addMarker( 'restrictedEditingException:1', {
  960. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  961. usingOperation: true,
  962. affectsData: true
  963. } );
  964. } );
  965. // <paragraph>foo <marker>bar</marker> <marker>baz</marker≥ qux[]</paragraph>
  966. model.change( writer => {
  967. writer.addMarker( 'restrictedEditingException:2', {
  968. range: writer.createRange( writer.createPositionAt( paragraph, 8 ), writer.createPositionAt( paragraph, 11 ) ),
  969. usingOperation: true,
  970. affectsData: true
  971. } );
  972. } );
  973. domEvtDataStub.keyCode += getCode( 'Shift' );
  974. view.document.fire( 'keydown', domEvtDataStub );
  975. sinon.assert.calledOnce( editor.execute );
  976. sinon.assert.calledWithExactly( editor.execute, 'goToPreviousRestrictedEditingException' );
  977. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  978. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  979. } );
  980. it( 'should not move to the closest previous exception on shift+tab key when there is none', () => {
  981. setModelData( model, '<paragraph>[]foo qux</paragraph>' );
  982. const paragraph = model.document.getRoot().getChild( 0 );
  983. // <paragraph>[]foo <marker>qux</marker></paragraph>
  984. model.change( writer => {
  985. writer.addMarker( 'restrictedEditingException:1', {
  986. range: writer.createRange( writer.createPositionAt( paragraph, 4 ), writer.createPositionAt( paragraph, 7 ) ),
  987. usingOperation: true,
  988. affectsData: true
  989. } );
  990. } );
  991. domEvtDataStub.keyCode += getCode( 'Shift' );
  992. view.document.fire( 'keydown', domEvtDataStub );
  993. sinon.assert.notCalled( editor.execute );
  994. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  995. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  996. } );
  997. } );
  998. // Helper method that creates an exception marker inside given parent.
  999. // Marker range is set to given position offsets (start, end).
  1000. function addExceptionMarker( startOffset, endOffset = startOffset, parent, id = 1 ) {
  1001. model.change( writer => {
  1002. writer.addMarker( `restrictedEditingException:${ id }`, {
  1003. range: writer.createRange(
  1004. writer.createPositionAt( parent, startOffset ),
  1005. writer.createPositionAt( parent, endOffset )
  1006. ),
  1007. usingOperation: true,
  1008. affectsData: true
  1009. } );
  1010. } );
  1011. }
  1012. function createDataTransfer( data ) {
  1013. return {
  1014. getData( type ) {
  1015. return data[ type ];
  1016. }
  1017. };
  1018. }
  1019. function assertMarkerRangePaths( startPath, endPath, markerId = 1 ) {
  1020. const marker = model.markers.get( `restrictedEditingException:${ markerId }` );
  1021. expect( marker.getStart().path ).to.deep.equal( startPath );
  1022. expect( marker.getEnd().path ).to.deep.equal( endPath );
  1023. }
  1024. } );