8
0

restrictededitingmodeediting.js 42 KB

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