restrictededitingmodeediting-commands.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  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 VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  7. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  8. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  9. import Command from '@ckeditor/ckeditor5-core/src/command';
  10. import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  11. import RestrictedEditingModeEditing from './../src/restrictededitingmodeediting';
  12. import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
  13. describe( 'RestrictedEditingEditing - commands', () => {
  14. let editor;
  15. testUtils.createSinonSandbox();
  16. describe( 'commands toggling', () => {
  17. let model, firstParagraph;
  18. beforeEach( async () => {
  19. editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, RestrictedEditingModeEditing ] } );
  20. model = editor.model;
  21. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  22. firstParagraph = model.document.getRoot().getChild( 0 );
  23. model.change( writer => {
  24. writer.addMarker( 'restricted-editing-exception:1', {
  25. range: writer.createRange(
  26. writer.createPositionAt( firstParagraph, 4 ),
  27. writer.createPositionAt( firstParagraph, 7 ) ),
  28. usingOperation: true,
  29. affectsData: true
  30. } );
  31. } );
  32. } );
  33. afterEach( async () => {
  34. await editor.destroy();
  35. } );
  36. describe( 'always enabled commands', () => {
  37. describe( 'undo', () => {
  38. beforeEach( () => {
  39. editor.commands.add( 'undo', buildFakeCommand( editor ) );
  40. } );
  41. it( 'should be enabled outside exception marker', () => {
  42. model.change( writer => {
  43. writer.setSelection( firstParagraph, 1 );
  44. } );
  45. expect( editor.commands.get( 'undo' ).isEnabled ).to.be.true;
  46. } );
  47. it( 'should be enabled inside exception marker', () => {
  48. model.change( writer => {
  49. writer.setSelection( firstParagraph, 5 );
  50. } );
  51. expect( editor.commands.get( 'undo' ).isEnabled ).to.be.true;
  52. } );
  53. } );
  54. describe( 'redo', () => {
  55. beforeEach( () => {
  56. editor.commands.add( 'redo', buildFakeCommand( editor ) );
  57. } );
  58. it( 'should be enabled outside exception marker', () => {
  59. model.change( writer => {
  60. writer.setSelection( firstParagraph, 1 );
  61. } );
  62. expect( editor.commands.get( 'redo' ).isEnabled ).to.be.true;
  63. } );
  64. it( 'should be enabled inside exception marker', () => {
  65. model.change( writer => {
  66. writer.setSelection( firstParagraph, 5 );
  67. } );
  68. expect( editor.commands.get( 'redo' ).isEnabled ).to.be.true;
  69. } );
  70. } );
  71. } );
  72. describe( 'commands enabled in exception marker', () => {
  73. const commands = [
  74. 'input', 'bold', 'link', 'italic'
  75. ];
  76. for ( const commandName of commands ) {
  77. describe( commandName, () => {
  78. beforeEach( () => {
  79. editor.commands.add( commandName, buildFakeCommand( editor ) );
  80. model.change( writer => {
  81. writer.setSelection( firstParagraph, 'end' );
  82. } );
  83. } );
  84. it( 'should be disabled when caret is outside exception marker', () => {
  85. model.change( writer => {
  86. writer.setSelection( firstParagraph, 1 );
  87. } );
  88. expect( editor.commands.get( commandName ).isEnabled ).to.be.false;
  89. } );
  90. it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
  91. model.change( writer => {
  92. writer.setSelection( firstParagraph, 5 );
  93. } );
  94. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  95. } );
  96. it( 'should be disabled when caret is inside other marker', () => {
  97. model.change( writer => {
  98. writer.addMarker( 'foo-bar:1', {
  99. range: writer.createRange(
  100. writer.createPositionAt( firstParagraph, 0 ),
  101. writer.createPositionAt( firstParagraph, 3 ) ),
  102. usingOperation: true,
  103. affectsData: true
  104. } );
  105. writer.setSelection( firstParagraph, 1 );
  106. } );
  107. expect( editor.commands.get( commandName ).isEnabled ).to.be.false;
  108. } );
  109. it( 'should be enabled when caret is inside exception marker (start boundary)', () => {
  110. model.change( writer => {
  111. writer.setSelection( firstParagraph, 4 );
  112. } );
  113. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  114. } );
  115. it( 'should be enabled when caret is inside exception marker (end boundary)', () => {
  116. model.change( writer => {
  117. writer.setSelection( firstParagraph, 7 );
  118. } );
  119. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  120. } );
  121. it( 'should be disabled for multi-range selection (collapsed ranges)', () => {
  122. model.change( writer => {
  123. writer.setSelection( [
  124. writer.createRange(
  125. writer.createPositionAt( firstParagraph, 5 )
  126. ),
  127. writer.createRange(
  128. writer.createPositionAt( firstParagraph, 9 )
  129. )
  130. ] );
  131. } );
  132. expect( editor.commands.get( commandName ).isEnabled ).to.be.false;
  133. } );
  134. it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
  135. model.change( writer => {
  136. writer.setSelection( writer.createRange(
  137. writer.createPositionAt( firstParagraph, 0 ),
  138. writer.createPositionAt( firstParagraph, 5 )
  139. ) );
  140. } );
  141. expect( editor.commands.get( commandName ).isEnabled ).to.be.false;
  142. } );
  143. it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
  144. model.change( writer => {
  145. writer.setSelection( writer.createRange(
  146. writer.createPositionAt( firstParagraph, 5 ),
  147. writer.createPositionAt( firstParagraph, 6 )
  148. ) );
  149. } );
  150. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  151. } );
  152. it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
  153. model.change( writer => {
  154. writer.setSelection( writer.createRange(
  155. writer.createPositionAt( firstParagraph, 4 ),
  156. writer.createPositionAt( firstParagraph, 6 )
  157. ) );
  158. } );
  159. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  160. } );
  161. it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
  162. model.change( writer => {
  163. writer.setSelection( writer.createRange(
  164. writer.createPositionAt( firstParagraph, 5 ),
  165. writer.createPositionAt( firstParagraph, 7 )
  166. ) );
  167. } );
  168. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  169. } );
  170. it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
  171. model.change( writer => {
  172. writer.setSelection( writer.createRange(
  173. writer.createPositionAt( firstParagraph, 4 ),
  174. writer.createPositionAt( firstParagraph, 7 )
  175. ) );
  176. } );
  177. expect( editor.commands.get( commandName ).isEnabled ).to.be.true;
  178. } );
  179. it( 'should be disabled for non-collapsed selection with more then one range', () => {
  180. model.change( writer => {
  181. writer.setSelection( [
  182. writer.createRange(
  183. writer.createPositionAt( firstParagraph, 5 ),
  184. writer.createPositionAt( firstParagraph, 6 )
  185. ),
  186. writer.createRange(
  187. writer.createPositionAt( firstParagraph, 8 ),
  188. writer.createPositionAt( firstParagraph, 9 )
  189. )
  190. ] );
  191. } );
  192. expect( editor.commands.get( commandName ).isEnabled ).to.be.false;
  193. } );
  194. } );
  195. }
  196. describe( 'delete', () => {
  197. beforeEach( () => {
  198. editor.commands.add( 'delete', buildFakeCommand( editor ) );
  199. model.change( writer => {
  200. writer.setSelection( firstParagraph, 'end' );
  201. } );
  202. } );
  203. it( 'should be disabled when caret is outside exception marker', () => {
  204. model.change( writer => {
  205. writer.setSelection( firstParagraph, 1 );
  206. } );
  207. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
  208. } );
  209. it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
  210. model.change( writer => {
  211. writer.setSelection( firstParagraph, 5 );
  212. } );
  213. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  214. } );
  215. it( 'should be disabled when caret is inside exception marker (start boundary)', () => {
  216. model.change( writer => {
  217. writer.setSelection( firstParagraph, 4 );
  218. } );
  219. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
  220. } );
  221. it( 'should be enabled when caret is inside exception marker (end boundary)', () => {
  222. model.change( writer => {
  223. writer.setSelection( firstParagraph, 7 );
  224. } );
  225. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  226. } );
  227. it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
  228. model.change( writer => {
  229. writer.setSelection( writer.createRange(
  230. writer.createPositionAt( firstParagraph, 0 ),
  231. writer.createPositionAt( firstParagraph, 5 )
  232. ) );
  233. } );
  234. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.false;
  235. } );
  236. it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
  237. model.change( writer => {
  238. writer.setSelection( writer.createRange(
  239. writer.createPositionAt( firstParagraph, 5 ),
  240. writer.createPositionAt( firstParagraph, 6 )
  241. ) );
  242. } );
  243. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  244. } );
  245. it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
  246. model.change( writer => {
  247. writer.setSelection( writer.createRange(
  248. writer.createPositionAt( firstParagraph, 4 ),
  249. writer.createPositionAt( firstParagraph, 6 )
  250. ) );
  251. } );
  252. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  253. } );
  254. it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
  255. model.change( writer => {
  256. writer.setSelection( writer.createRange(
  257. writer.createPositionAt( firstParagraph, 5 ),
  258. writer.createPositionAt( firstParagraph, 7 )
  259. ) );
  260. } );
  261. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  262. } );
  263. it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
  264. model.change( writer => {
  265. writer.setSelection( writer.createRange(
  266. writer.createPositionAt( firstParagraph, 4 ),
  267. writer.createPositionAt( firstParagraph, 7 )
  268. ) );
  269. } );
  270. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  271. } );
  272. } );
  273. describe( 'forwardDelete', () => {
  274. beforeEach( () => {
  275. editor.commands.add( 'forwardDelete', buildFakeCommand( editor ) );
  276. model.change( writer => {
  277. writer.setSelection( firstParagraph, 'end' );
  278. } );
  279. } );
  280. it( 'should be disabled when caret is outside exception marker', () => {
  281. model.change( writer => {
  282. writer.setSelection( firstParagraph, 1 );
  283. } );
  284. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
  285. } );
  286. it( 'should be enabled when caret is inside exception marker (not touching boundaries)', () => {
  287. model.change( writer => {
  288. writer.setSelection( firstParagraph, 5 );
  289. } );
  290. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
  291. } );
  292. it( 'should be enabled when caret is inside exception marker (start boundary)', () => {
  293. model.change( writer => {
  294. writer.setSelection( firstParagraph, 4 );
  295. } );
  296. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
  297. } );
  298. it( 'should be disabled when caret is inside exception marker (end boundary)', () => {
  299. model.change( writer => {
  300. writer.setSelection( firstParagraph, 7 );
  301. } );
  302. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
  303. } );
  304. it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
  305. model.change( writer => {
  306. writer.setSelection( writer.createRange(
  307. writer.createPositionAt( firstParagraph, 0 ),
  308. writer.createPositionAt( firstParagraph, 5 )
  309. ) );
  310. } );
  311. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.false;
  312. } );
  313. it( 'should be enabled for non-collapsed selection that is fully contained inside exception marker', () => {
  314. model.change( writer => {
  315. writer.setSelection( writer.createRange(
  316. writer.createPositionAt( firstParagraph, 5 ),
  317. writer.createPositionAt( firstParagraph, 6 )
  318. ) );
  319. } );
  320. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
  321. } );
  322. it( 'should be enabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
  323. model.change( writer => {
  324. writer.setSelection( writer.createRange(
  325. writer.createPositionAt( firstParagraph, 4 ),
  326. writer.createPositionAt( firstParagraph, 6 )
  327. ) );
  328. } );
  329. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
  330. } );
  331. it( 'should be enabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
  332. model.change( writer => {
  333. writer.setSelection( writer.createRange(
  334. writer.createPositionAt( firstParagraph, 5 ),
  335. writer.createPositionAt( firstParagraph, 7 )
  336. ) );
  337. } );
  338. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
  339. } );
  340. it( 'should be enabled for non-collapsed selection is equal to exception marker', () => {
  341. model.change( writer => {
  342. writer.setSelection( writer.createRange(
  343. writer.createPositionAt( firstParagraph, 4 ),
  344. writer.createPositionAt( firstParagraph, 7 )
  345. ) );
  346. } );
  347. expect( editor.commands.get( 'forwardDelete' ).isEnabled ).to.be.true;
  348. } );
  349. } );
  350. } );
  351. describe( 'non-enabled commands', () => {
  352. beforeEach( () => {
  353. editor.commands.add( 'other', buildFakeCommand( editor ) );
  354. model.change( writer => {
  355. writer.setSelection( firstParagraph, 'end' );
  356. } );
  357. } );
  358. it( 'should be disabled outside exception marker', () => {
  359. model.change( writer => {
  360. writer.setSelection( firstParagraph, 1 );
  361. } );
  362. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  363. } );
  364. it( 'should be disabled inside exception marker', () => {
  365. model.change( writer => {
  366. writer.setSelection( firstParagraph, 1 );
  367. } );
  368. model.change( writer => {
  369. writer.setSelection( firstParagraph, 5 );
  370. } );
  371. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  372. } );
  373. it( 'should be disabled when caret is inside exception marker (not touching boundaries)', () => {
  374. model.change( writer => {
  375. writer.setSelection( firstParagraph, 5 );
  376. } );
  377. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  378. } );
  379. it( 'should be disabled when caret is inside exception marker (start boundary)', () => {
  380. model.change( writer => {
  381. writer.setSelection( firstParagraph, 4 );
  382. } );
  383. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  384. } );
  385. it( 'should be disabled when caret is inside exception marker (end boundary)', () => {
  386. model.change( writer => {
  387. writer.setSelection( firstParagraph, 7 );
  388. } );
  389. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  390. } );
  391. it( 'should be disabled for non-collapsed selection that expands over exception marker', () => {
  392. model.change( writer => {
  393. writer.setSelection( writer.createRange(
  394. writer.createPositionAt( firstParagraph, 0 ),
  395. writer.createPositionAt( firstParagraph, 5 )
  396. ) );
  397. } );
  398. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  399. } );
  400. it( 'should be disabled for non-collapsed selection that is fully contained inside exception marker', () => {
  401. model.change( writer => {
  402. writer.setSelection( writer.createRange(
  403. writer.createPositionAt( firstParagraph, 5 ),
  404. writer.createPositionAt( firstParagraph, 6 )
  405. ) );
  406. } );
  407. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  408. } );
  409. it( 'should be disabled for non-collapsed selection inside exception marker (start position on marker boundary)', () => {
  410. model.change( writer => {
  411. writer.setSelection( writer.createRange(
  412. writer.createPositionAt( firstParagraph, 4 ),
  413. writer.createPositionAt( firstParagraph, 6 )
  414. ) );
  415. } );
  416. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  417. } );
  418. it( 'should be disabled for non-collapsed selection inside exception marker (end position on marker boundary)', () => {
  419. model.change( writer => {
  420. writer.setSelection( writer.createRange(
  421. writer.createPositionAt( firstParagraph, 5 ),
  422. writer.createPositionAt( firstParagraph, 7 )
  423. ) );
  424. } );
  425. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  426. } );
  427. it( 'should be disabled for non-collapsed selection is equal to exception marker', () => {
  428. model.change( writer => {
  429. writer.setSelection( writer.createRange(
  430. writer.createPositionAt( firstParagraph, 4 ),
  431. writer.createPositionAt( firstParagraph, 7 )
  432. ) );
  433. } );
  434. expect( editor.commands.get( 'other' ).isEnabled ).to.be.false;
  435. } );
  436. } );
  437. } );
  438. describe( 'integration', () => {
  439. let model, firstParagraph;
  440. beforeEach( async () => {
  441. editor = await VirtualTestEditor.create( { plugins: [ Paragraph, Typing, UndoEditing, RestrictedEditingModeEditing ] } );
  442. model = editor.model;
  443. setModelData( model, '<paragraph>[]foo bar baz</paragraph>' );
  444. firstParagraph = model.document.getRoot().getChild( 0 );
  445. model.change( writer => {
  446. writer.addMarker( 'restricted-editing-exception:1', {
  447. range: writer.createRange(
  448. writer.createPositionAt( firstParagraph, 4 ),
  449. writer.createPositionAt( firstParagraph, 7 ) ),
  450. usingOperation: true,
  451. affectsData: true
  452. } );
  453. } );
  454. } );
  455. afterEach( async () => {
  456. await editor.destroy();
  457. } );
  458. describe( 'delete + undo', () => {
  459. it( 'should be enabled after data change (no selection change event on undo)', () => {
  460. model.change( writer => {
  461. writer.setSelection( firstParagraph, 7 );
  462. } );
  463. editor.execute( 'delete' );
  464. editor.execute( 'undo' );
  465. expect( editor.commands.get( 'delete' ).isEnabled ).to.be.true;
  466. } );
  467. } );
  468. } );
  469. class FakeCommand extends Command {
  470. refresh() {
  471. this.isEnabled = true;
  472. }
  473. }
  474. function buildFakeCommand( editor ) {
  475. return new FakeCommand( editor );
  476. }
  477. } );