8
0

input.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  1. /*
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document */
  6. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  7. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  8. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  9. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  10. import Bold from '@ckeditor/ckeditor5-basic-styles/src/boldengine';
  11. import Italic from '@ckeditor/ckeditor5-basic-styles/src/italicengine';
  12. import LinkEngine from '@ckeditor/ckeditor5-link/src/linkengine';
  13. import Input from '../src/input';
  14. import Writer from '@ckeditor/ckeditor5-engine/src/model/writer';
  15. import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
  16. import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
  17. import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
  18. import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
  19. import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
  20. import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
  21. import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
  22. import MutationObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mutationobserver';
  23. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  24. import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
  25. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  26. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  27. describe( 'Input feature', () => {
  28. let editor, model, modelRoot, view, viewRoot, listenter;
  29. testUtils.createSinonSandbox();
  30. before( () => {
  31. listenter = Object.create( EmitterMixin );
  32. return VirtualTestEditor
  33. .create( {
  34. plugins: [ Input, Paragraph, Bold ]
  35. } )
  36. .then( newEditor => {
  37. // Mock image feature.
  38. newEditor.model.schema.register( 'image', { allowWhere: '$text' } );
  39. buildModelConverter().for( newEditor.data.modelToView, newEditor.editing.modelToView )
  40. .fromElement( 'image' )
  41. .toElement( 'img' );
  42. buildViewConverter().for( newEditor.data.viewToModel )
  43. .fromElement( 'img' )
  44. .toElement( 'image' );
  45. editor = newEditor;
  46. model = editor.model;
  47. modelRoot = model.document.getRoot();
  48. view = editor.editing.view;
  49. viewRoot = view.getRoot();
  50. } );
  51. } );
  52. after( () => {
  53. return editor.destroy();
  54. } );
  55. beforeEach( () => {
  56. editor.setData( '<p>foobar</p>' );
  57. model.change( () => {
  58. model.document.selection.setRanges( [
  59. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 3, modelRoot.getChild( 0 ), 3 )
  60. ] );
  61. } );
  62. } );
  63. afterEach( () => {
  64. listenter.stopListening();
  65. } );
  66. describe( 'mutations handling', () => {
  67. it( 'should handle text mutation', () => {
  68. view.fire( 'mutations', [
  69. {
  70. type: 'text',
  71. oldText: 'foobar',
  72. newText: 'fooxbar',
  73. node: viewRoot.getChild( 0 ).getChild( 0 )
  74. }
  75. ] );
  76. expect( getModelData( model ) ).to.equal( '<paragraph>foox[]bar</paragraph>' );
  77. expect( getViewData( view ) ).to.equal( '<p>foox{}bar</p>' );
  78. } );
  79. it( 'should handle text mutation change', () => {
  80. view.fire( 'mutations', [
  81. {
  82. type: 'text',
  83. oldText: 'foobar',
  84. newText: 'foodar',
  85. node: viewRoot.getChild( 0 ).getChild( 0 )
  86. }
  87. ] );
  88. expect( getModelData( model ) ).to.equal( '<paragraph>food[]ar</paragraph>' );
  89. expect( getViewData( view ) ).to.equal( '<p>food{}ar</p>' );
  90. } );
  91. it( 'should handle text node insertion', () => {
  92. editor.setData( '<p></p>' );
  93. view.fire( 'mutations', [
  94. {
  95. type: 'children',
  96. oldChildren: [],
  97. newChildren: [ new ViewText( 'x' ) ],
  98. node: viewRoot.getChild( 0 )
  99. }
  100. ] );
  101. expect( getModelData( model ) ).to.equal( '<paragraph>x[]</paragraph>' );
  102. expect( getViewData( view ) ).to.equal( '<p>x{}</p>' );
  103. } );
  104. it( 'should apply selection attributes to the inserted text', () => {
  105. setModelData( model, '<paragraph>[]</paragraph>', {
  106. selectionAttributes: {
  107. bold: true,
  108. italic: true
  109. }
  110. } );
  111. view.fire( 'mutations', [
  112. {
  113. type: 'children',
  114. oldChildren: [],
  115. newChildren: [ new ViewText( 'x' ) ],
  116. node: viewRoot.getChild( 0 )
  117. }
  118. ] );
  119. expect( getModelData( model ) ).to.equal(
  120. '<paragraph><$text bold="true" italic="true">x[]</$text></paragraph>'
  121. );
  122. } );
  123. it( 'should handle multiple text mutations', () => {
  124. editor.setData( '<p>foo<strong>bar</strong></p>' );
  125. view.fire( 'mutations', [
  126. {
  127. type: 'text',
  128. oldText: 'foo',
  129. newText: 'foob',
  130. node: viewRoot.getChild( 0 ).getChild( 0 )
  131. },
  132. {
  133. type: 'text',
  134. oldText: 'bar',
  135. newText: 'ar',
  136. node: viewRoot.getChild( 0 ).getChild( 1 ).getChild( 0 )
  137. }
  138. ] );
  139. expect( getModelData( model ) ).to.equal( '<paragraph>foob[]<$text bold="true">ar</$text></paragraph>' );
  140. expect( getViewData( view ) ).to.equal( '<p>foob{}<strong>ar</strong></p>' );
  141. } );
  142. it( 'should handle multiple text node insertion', () => {
  143. editor.setData( '<p></p><p></p>' );
  144. view.fire( 'mutations', [
  145. {
  146. type: 'children',
  147. oldChildren: [],
  148. newChildren: [ new ViewText( 'x' ) ],
  149. node: viewRoot.getChild( 0 )
  150. },
  151. {
  152. type: 'children',
  153. oldChildren: [],
  154. newChildren: [ new ViewText( 'y' ) ],
  155. node: viewRoot.getChild( 1 )
  156. }
  157. ] );
  158. expect( getModelData( model ) ).to.equal( '<paragraph>x</paragraph><paragraph>y[]</paragraph>' );
  159. expect( getViewData( view ) ).to.equal( '<p>x</p><p>y{}</p>' );
  160. } );
  161. it( 'should do nothing when two nodes were inserted', () => {
  162. editor.setData( '<p></p>' );
  163. view.fire( 'mutations', [
  164. {
  165. type: 'children',
  166. oldChildren: [],
  167. newChildren: [ new ViewText( 'x' ), new ViewElement( 'img' ) ],
  168. node: viewRoot.getChild( 0 )
  169. }
  170. ] );
  171. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  172. expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
  173. } );
  174. it( 'should do nothing when two nodes were inserted and one removed', () => {
  175. view.fire( 'mutations', [
  176. {
  177. type: 'children',
  178. oldChildren: [ new ViewText( 'foobar' ) ],
  179. newChildren: [ new ViewText( 'x' ), new ViewElement( 'img' ) ],
  180. node: viewRoot.getChild( 0 )
  181. }
  182. ] );
  183. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  184. expect( getViewData( view ) ).to.equal( '<p>foo{}bar</p>' );
  185. } );
  186. it( 'should handle multiple children in the node', () => {
  187. editor.setData( '<p>foo<img></img></p>' );
  188. view.fire( 'mutations', [
  189. {
  190. type: 'children',
  191. oldChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ) ],
  192. newChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ), new ViewText( 'x' ) ],
  193. node: viewRoot.getChild( 0 )
  194. }
  195. ] );
  196. expect( getModelData( model ) ).to.equal( '<paragraph>foo<image></image>x[]</paragraph>' );
  197. expect( getViewData( view ) ).to.equal( '<p>foo<img></img>x{}</p>' );
  198. } );
  199. it( 'should do nothing when node was removed', () => {
  200. view.fire( 'mutations', [
  201. {
  202. type: 'children',
  203. oldChildren: [ new ViewText( 'foobar' ) ],
  204. newChildren: [],
  205. node: viewRoot.getChild( 0 )
  206. }
  207. ] );
  208. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  209. expect( getViewData( view ) ).to.equal( '<p>foo{}bar</p>' );
  210. } );
  211. it( 'should do nothing when element was inserted', () => {
  212. editor.setData( '<p></p>' );
  213. view.fire( 'mutations', [
  214. {
  215. type: 'children',
  216. oldChildren: [],
  217. newChildren: [ new ViewElement( 'img' ) ],
  218. node: viewRoot.getChild( 0 )
  219. }
  220. ] );
  221. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  222. expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
  223. } );
  224. it( 'should set model selection appropriately to view selection passed in mutations event', () => {
  225. // This test case emulates spellchecker correction.
  226. const viewSelection = new ViewSelection();
  227. viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
  228. view.fire( 'mutations',
  229. [ {
  230. type: 'text',
  231. oldText: 'foobar',
  232. newText: 'foodar',
  233. node: viewRoot.getChild( 0 ).getChild( 0 )
  234. } ],
  235. viewSelection
  236. );
  237. expect( getModelData( model ) ).to.equal( '<paragraph>foodar[]</paragraph>' );
  238. expect( getViewData( view ) ).to.equal( '<p>foodar{}</p>' );
  239. } );
  240. it( 'should use up to one insert and remove operations (spellchecker)', () => {
  241. // This test case emulates spellchecker correction.
  242. const viewSelection = new ViewSelection();
  243. viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
  244. testUtils.sinon.spy( Writer.prototype, 'insert' );
  245. testUtils.sinon.spy( Writer.prototype, 'remove' );
  246. view.fire( 'mutations',
  247. [ {
  248. type: 'text',
  249. oldText: 'foobar',
  250. newText: 'fxobxr',
  251. node: viewRoot.getChild( 0 ).getChild( 0 )
  252. } ],
  253. viewSelection
  254. );
  255. expect( Writer.prototype.insert.calledOnce ).to.be.true;
  256. expect( Writer.prototype.remove.calledOnce ).to.be.true;
  257. } );
  258. it( 'should place selection after when correcting to longer word (spellchecker)', () => {
  259. // This test case emulates spellchecker correction.
  260. editor.setData( '<p>Foo hous a</p>' );
  261. const viewSelection = new ViewSelection();
  262. viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
  263. view.fire( 'mutations',
  264. [ {
  265. type: 'text',
  266. oldText: 'Foo hous a',
  267. newText: 'Foo house a',
  268. node: viewRoot.getChild( 0 ).getChild( 0 )
  269. } ],
  270. viewSelection
  271. );
  272. expect( getModelData( model ) ).to.equal( '<paragraph>Foo house[] a</paragraph>' );
  273. expect( getViewData( view ) ).to.equal( '<p>Foo house{} a</p>' );
  274. } );
  275. it( 'should place selection after when correcting to shorter word (spellchecker)', () => {
  276. // This test case emulates spellchecker correction.
  277. editor.setData( '<p>Bar athat foo</p>' );
  278. const viewSelection = new ViewSelection();
  279. viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
  280. view.fire( 'mutations',
  281. [ {
  282. type: 'text',
  283. oldText: 'Bar athat foo',
  284. newText: 'Bar that foo',
  285. node: viewRoot.getChild( 0 ).getChild( 0 )
  286. } ],
  287. viewSelection
  288. );
  289. expect( getModelData( model ) ).to.equal( '<paragraph>Bar that[] foo</paragraph>' );
  290. expect( getViewData( view ) ).to.equal( '<p>Bar that{} foo</p>' );
  291. } );
  292. it( 'should place selection after when merging two words (spellchecker)', () => {
  293. // This test case emulates spellchecker correction.
  294. editor.setData( '<p>Foo hous e</p>' );
  295. const viewSelection = new ViewSelection();
  296. viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
  297. view.fire( 'mutations',
  298. [ {
  299. type: 'text',
  300. oldText: 'Foo hous e',
  301. newText: 'Foo house',
  302. node: viewRoot.getChild( 0 ).getChild( 0 )
  303. } ],
  304. viewSelection
  305. );
  306. expect( getModelData( model ) ).to.equal( '<paragraph>Foo house[]</paragraph>' );
  307. expect( getViewData( view ) ).to.equal( '<p>Foo house{}</p>' );
  308. } );
  309. it( 'should place non-collapsed selection after changing single character (composition)', () => {
  310. editor.setData( '<p>Foo house</p>' );
  311. const viewSelection = new ViewSelection();
  312. viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
  313. viewSelection.moveFocusTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
  314. view.fire( 'mutations',
  315. [ {
  316. type: 'text',
  317. oldText: 'Foo house',
  318. newText: 'Foo housa',
  319. node: viewRoot.getChild( 0 ).getChild( 0 )
  320. } ],
  321. viewSelection
  322. );
  323. expect( getModelData( model ) ).to.equal( '<paragraph>Foo hous[a]</paragraph>' );
  324. expect( getViewData( view ) ).to.equal( '<p>Foo hous{a}</p>' );
  325. } );
  326. it( 'should replace last &nbsp; with space', () => {
  327. model.change( () => {
  328. model.document.selection.setRanges( [
  329. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
  330. ] );
  331. } );
  332. view.fire( 'mutations', [
  333. {
  334. type: 'text',
  335. oldText: 'foobar',
  336. newText: 'foobar\u00A0',
  337. node: viewRoot.getChild( 0 ).getChild( 0 )
  338. }
  339. ] );
  340. expect( getModelData( model ) ).to.equal( '<paragraph>foobar []</paragraph>' );
  341. expect( getViewData( view ) ).to.equal( '<p>foobar {}</p>' );
  342. } );
  343. it( 'should replace first &nbsp; with space', () => {
  344. model.change( () => {
  345. model.document.selection.setRanges( [
  346. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 0 ), 0 )
  347. ] );
  348. } );
  349. view.fire( 'mutations', [
  350. {
  351. type: 'text',
  352. oldText: 'foobar',
  353. newText: '\u00A0foobar',
  354. node: viewRoot.getChild( 0 ).getChild( 0 )
  355. }
  356. ] );
  357. expect( getModelData( model ) ).to.equal( '<paragraph> []foobar</paragraph>' );
  358. expect( getViewData( view ) ).to.equal( '<p> {}foobar</p>' );
  359. } );
  360. it( 'should replace all &nbsp; with spaces', () => {
  361. model.change( () => {
  362. model.document.selection.setRanges( [
  363. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
  364. ] );
  365. } );
  366. view.fire( 'mutations', [
  367. {
  368. type: 'text',
  369. oldText: 'foobar',
  370. newText: 'foobar\u00A0\u00A0\u00A0baz',
  371. node: viewRoot.getChild( 0 ).getChild( 0 )
  372. }
  373. ] );
  374. expect( getModelData( model ) ).to.equal( '<paragraph>foobar baz[]</paragraph>' );
  375. expect( getViewData( view ) ).to.equal( '<p>foobar baz{}</p>' );
  376. } );
  377. } );
  378. describe( 'keystroke handling', () => {
  379. it( 'should remove contents', () => {
  380. model.change( () => {
  381. model.document.selection.setRanges( [
  382. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  383. } );
  384. listenter.listenTo( view, 'keydown', () => {
  385. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  386. }, { priority: 'lowest' } );
  387. } );
  388. // #97
  389. it( 'should remove contents and merge blocks', () => {
  390. setModelData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
  391. listenter.listenTo( view, 'keydown', () => {
  392. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  393. view.fire( 'mutations', [
  394. {
  395. type: 'text',
  396. oldText: 'foar',
  397. newText: 'foyar',
  398. node: viewRoot.getChild( 0 ).getChild( 0 )
  399. }
  400. ] );
  401. }, { priority: 'lowest' } );
  402. view.fire( 'keydown', { keyCode: getCode( 'y' ) } );
  403. expect( getModelData( model ) ).to.equal( '<paragraph>foy[]ar</paragraph>' );
  404. expect( getViewData( view ) ).to.equal( '<p>foy{}ar</p>' );
  405. } );
  406. it( 'should do nothing on arrow key', () => {
  407. model.change( () => {
  408. model.document.selection.setRanges( [
  409. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  410. } );
  411. view.fire( 'keydown', { keyCode: getCode( 'arrowdown' ) } );
  412. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  413. } );
  414. it( 'should do nothing on ctrl combinations', () => {
  415. model.change( () => {
  416. model.document.selection.setRanges( [
  417. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  418. } );
  419. view.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
  420. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  421. } );
  422. it( 'should do nothing on non printable keys', () => {
  423. model.change( () => {
  424. model.document.selection.setRanges( [
  425. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  426. } );
  427. view.fire( 'keydown', { keyCode: 16 } ); // Shift
  428. view.fire( 'keydown', { keyCode: 35 } ); // Home
  429. view.fire( 'keydown', { keyCode: 112 } ); // F1
  430. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  431. } );
  432. // #69
  433. it( 'should do nothing on tab key', () => {
  434. model.change( () => {
  435. model.document.selection.setRanges( [
  436. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  437. } );
  438. view.fire( 'keydown', { keyCode: 9 } ); // Tab
  439. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  440. } );
  441. // #82
  442. it( 'should do nothing on composition start key', () => {
  443. model.change( () => {
  444. model.document.selection.setRanges( [
  445. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  446. } );
  447. view.fire( 'keydown', { keyCode: 229 } );
  448. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  449. } );
  450. it( 'should do nothing if selection is collapsed', () => {
  451. view.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
  452. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  453. } );
  454. it( 'should lock buffer if selection is not collapsed', () => {
  455. const buffer = editor.commands.get( 'input' )._buffer;
  456. const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
  457. const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
  458. model.change( () => {
  459. model.document.selection.setRanges( [
  460. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  461. } );
  462. view.fire( 'keydown', { keyCode: getCode( 'y' ) } );
  463. expect( lockSpy.calledOnce ).to.be.true;
  464. expect( unlockSpy.calledOnce ).to.be.true;
  465. } );
  466. it( 'should not lock buffer on non printable keys', () => {
  467. const buffer = editor.commands.get( 'input' )._buffer;
  468. const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
  469. const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
  470. view.fire( 'keydown', { keyCode: 16 } ); // Shift
  471. view.fire( 'keydown', { keyCode: 35 } ); // Home
  472. view.fire( 'keydown', { keyCode: 112 } ); // F1
  473. expect( lockSpy.callCount ).to.be.equal( 0 );
  474. expect( unlockSpy.callCount ).to.be.equal( 0 );
  475. } );
  476. it( 'should not lock buffer on collapsed selection', () => {
  477. const buffer = editor.commands.get( 'input' )._buffer;
  478. const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
  479. const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
  480. view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
  481. view.fire( 'keydown', { keyCode: getCode( 'a' ) } );
  482. view.fire( 'keydown', { keyCode: getCode( 'z' ) } );
  483. expect( lockSpy.callCount ).to.be.equal( 0 );
  484. expect( unlockSpy.callCount ).to.be.equal( 0 );
  485. } );
  486. it( 'should not modify document when input command is disabled and selection is collapsed', () => {
  487. setModelData( model, '<paragraph>foo[]bar</paragraph>' );
  488. editor.commands.get( 'input' ).isEnabled = false;
  489. view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
  490. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  491. } );
  492. it( 'should not modify document when input command is disabled and selection is non-collapsed', () => {
  493. setModelData( model, '<paragraph>fo[ob]ar</paragraph>' );
  494. editor.commands.get( 'input' ).isEnabled = false;
  495. view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
  496. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  497. } );
  498. } );
  499. // NOTE: In all these tests we need to simulate the mutations. However, it's really tricky to tell what
  500. // should be in "newChildren" because we don't have yet access to these nodes. We pass new instances,
  501. // but this means that DomConverter which is used somewhere internally may return a different instance
  502. // (which wouldn't happen in practice because it'd cache it). Besides, it's really hard to tell if the
  503. // browser will keep the instances of the old elements when modifying the tree when the user is typing
  504. // or if it will create new instances itself too.
  505. // However, the code handling these mutations doesn't really care what's inside new/old children. It
  506. // just needs the mutations common ancestor to understand how big fragment of the tree has changed.
  507. describe( '#100', () => {
  508. let domElement, domRoot;
  509. beforeEach( () => {
  510. domElement = document.createElement( 'div' );
  511. document.body.appendChild( domElement );
  512. return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, LinkEngine ] } )
  513. .then( newEditor => {
  514. editor = newEditor;
  515. model = editor.model;
  516. modelRoot = model.document.getRoot();
  517. view = editor.editing.view;
  518. viewRoot = view.getRoot();
  519. domRoot = view.getDomRoot();
  520. // Mock image feature.
  521. newEditor.model.schema.register( 'image', { allowWhere: '$text' } );
  522. buildModelConverter().for( newEditor.data.modelToView, newEditor.editing.modelToView )
  523. .fromElement( 'image' )
  524. .toElement( 'img' );
  525. buildViewConverter().for( newEditor.data.viewToModel )
  526. .fromElement( 'img' )
  527. .toElement( 'image' );
  528. // Disable MO completely and in a way it won't be reenabled on some Document#render() call.
  529. const mutationObserver = view.getObserver( MutationObserver );
  530. mutationObserver.disable();
  531. mutationObserver.enable = () => {};
  532. } );
  533. } );
  534. afterEach( () => {
  535. domElement.remove();
  536. return editor.destroy();
  537. } );
  538. // This happens when browser automatically switches parent and child nodes.
  539. it( 'should handle mutations switching inner and outer node when adding new text node after', () => {
  540. setModelData( model,
  541. '<paragraph>' +
  542. '<$text italic="true" linkHref="foo">' +
  543. 'text[]' +
  544. '</$text>' +
  545. '</paragraph>'
  546. );
  547. expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>text{}</i></a></p>' );
  548. const paragraph = viewRoot.getChild( 0 );
  549. const link = paragraph.getChild( 0 );
  550. const italic = link.getChild( 0 );
  551. const text = italic.getChild( 0 );
  552. // Simulate mutations and DOM change.
  553. domRoot.childNodes[ 0 ].innerHTML = '<i><a href="foo">text</a>x</i>';
  554. view.fire( 'mutations', [
  555. // First mutation - remove all children from link element.
  556. {
  557. type: 'children',
  558. node: link,
  559. oldChildren: [ italic ],
  560. newChildren: []
  561. },
  562. // Second mutation - remove link from paragraph and put italic there.
  563. {
  564. type: 'children',
  565. node: paragraph,
  566. oldChildren: [ link ],
  567. newChildren: [ new ViewElement( 'i' ) ]
  568. },
  569. // Third mutation - italic's new children.
  570. {
  571. type: 'children',
  572. node: italic,
  573. oldChildren: [ text ],
  574. newChildren: [ new ViewElement( 'a', null, text.clone() ), new ViewText( 'x' ) ]
  575. }
  576. ] );
  577. expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>textx{}</i></a></p>' );
  578. } );
  579. it( 'should handle mutations switching inner and outer node when adding new text node before', () => {
  580. setModelData( model,
  581. '<paragraph>' +
  582. '<$text italic="true" linkHref="foo">' +
  583. '[]text' +
  584. '</$text>' +
  585. '</paragraph>'
  586. );
  587. expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>{}text</i></a></p>' );
  588. const paragraph = viewRoot.getChild( 0 );
  589. const link = paragraph.getChild( 0 );
  590. const italic = link.getChild( 0 );
  591. const text = italic.getChild( 0 );
  592. // Simulate mutations and DOM change.
  593. domRoot.childNodes[ 0 ].innerHTML = '<i>x<a href="foo">text</a></i>';
  594. view.fire( 'mutations', [
  595. // First mutation - remove all children from link element.
  596. {
  597. type: 'children',
  598. node: link,
  599. oldChildren: [ italic ],
  600. newChildren: []
  601. },
  602. // Second mutation - remove link from paragraph and put italic there.
  603. {
  604. type: 'children',
  605. node: paragraph,
  606. oldChildren: [ link ],
  607. newChildren: [ new ViewElement( 'i' ) ]
  608. },
  609. // Third mutation - italic's new children.
  610. {
  611. type: 'children',
  612. node: italic,
  613. oldChildren: [ text ],
  614. newChildren: [ new ViewText( 'x' ), new ViewElement( 'a', null, 'text' ) ]
  615. }
  616. ] );
  617. expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>x{}text</i></a></p>' );
  618. } );
  619. it( 'should handle mutations switching inner and outer node - with text before', () => {
  620. setModelData( model,
  621. '<paragraph>' +
  622. 'xxx<$text italic="true" linkHref="foo">' +
  623. 'text[]' +
  624. '</$text>' +
  625. '</paragraph>'
  626. );
  627. expect( getViewData( view ) ).to.equal( '<p>xxx<a href="foo"><i>text{}</i></a></p>' );
  628. const paragraph = viewRoot.getChild( 0 );
  629. const textBefore = paragraph.getChild( 0 );
  630. const link = paragraph.getChild( 1 );
  631. const italic = link.getChild( 0 );
  632. const text = italic.getChild( 0 );
  633. // Simulate mutations and DOM change.
  634. domRoot.childNodes[ 0 ].innerHTML = 'xxx<i><a href="foo">text</a>x</i>';
  635. view.fire( 'mutations', [
  636. // First mutation - remove all children from link element.
  637. {
  638. type: 'children',
  639. node: link,
  640. oldChildren: [ italic ],
  641. newChildren: []
  642. },
  643. // Second mutation - remove link from paragraph and put italic there.
  644. {
  645. type: 'children',
  646. node: paragraph,
  647. oldChildren: [ textBefore, link ],
  648. newChildren: [ new ViewText( 'xxx' ), new ViewElement( 'i' ) ]
  649. },
  650. // Third mutation - italic's new children.
  651. {
  652. type: 'children',
  653. node: italic,
  654. oldChildren: [ text ],
  655. newChildren: [ new ViewElement( 'a', null, 'text' ), new ViewText( 'x' ) ]
  656. }
  657. ] );
  658. expect( getViewData( view ) ).to.equal( '<p>xxx<a href="foo"><i>textx{}</i></a></p>' );
  659. } );
  660. // This happens when spell checker is applied on <strong> element and changes it to <b>.
  661. it( 'should handle mutations replacing node', () => {
  662. setModelData( model,
  663. '<paragraph>' +
  664. '<$text bold="true">' +
  665. 'text[]' +
  666. '</$text>' +
  667. '</paragraph>'
  668. );
  669. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  670. const paragraph = viewRoot.getChild( 0 );
  671. const strong = paragraph.getChild( 0 );
  672. // Simulate mutations and DOM change.
  673. domRoot.childNodes[ 0 ].innerHTML = '<b>fixed text</b>';
  674. view.fire( 'mutations', [
  675. // Replace `<strong>` with `<b>`.
  676. {
  677. type: 'children',
  678. node: paragraph,
  679. oldChildren: [ strong ],
  680. newChildren: [ new ViewElement( 'b', null, 'fixed text' ) ]
  681. }
  682. ] );
  683. expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>fixed text</strong></p>' );
  684. } );
  685. // Spell checker splits text inside attributes to two text nodes.
  686. it( 'should handle mutations inside attribute element', () => {
  687. setModelData( model,
  688. '<paragraph>' +
  689. '<$text bold="true">' +
  690. 'this is foo text[]' +
  691. '</$text>' +
  692. '</paragraph>'
  693. );
  694. expect( getViewData( view ) ).to.equal( '<p><strong>this is foo text{}</strong></p>' );
  695. const paragraph = viewRoot.getChild( 0 );
  696. const strong = paragraph.getChild( 0 );
  697. const text = strong.getChild( 0 );
  698. // Simulate mutations and DOM change.
  699. domRoot.childNodes[ 0 ].childNodes[ 0 ].innerHTML = 'this is bar text';
  700. view.fire( 'mutations', [
  701. {
  702. type: 'children',
  703. node: strong,
  704. oldChildren: [ text ],
  705. newChildren: [ new ViewText( 'this is bar' ), new ViewText( ' text' ) ]
  706. }
  707. ] );
  708. expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>this is bar text</strong></p>' );
  709. } );
  710. it( 'should do nothing if elements mutated', () => {
  711. setModelData( model,
  712. '<paragraph>' +
  713. '<$text bold="true">' +
  714. 'text[]' +
  715. '</$text>' +
  716. '</paragraph>'
  717. );
  718. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  719. const paragraph = viewRoot.getChild( 0 );
  720. const strong = paragraph.getChild( 0 );
  721. // Simulate mutations and DOM change.
  722. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong><img />';
  723. view.fire( 'mutations', [
  724. {
  725. type: 'children',
  726. node: paragraph,
  727. oldChildren: [ strong ],
  728. newChildren: [
  729. new ViewElement( 'strong', null, new ViewText( 'text' ) ),
  730. new ViewElement( 'img' )
  731. ]
  732. }
  733. ] );
  734. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  735. } );
  736. it( 'should do nothing if text is not changed', () => {
  737. setModelData( model,
  738. '<paragraph>' +
  739. '<$text bold="true">' +
  740. 'text[]' +
  741. '</$text>' +
  742. '</paragraph>'
  743. );
  744. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  745. const paragraph = viewRoot.getChild( 0 );
  746. const strong = paragraph.getChild( 0 );
  747. // Simulate mutations and DOM change.
  748. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
  749. view.fire( 'mutations', [
  750. {
  751. type: 'children',
  752. node: paragraph,
  753. oldChildren: [ strong ],
  754. newChildren: [ new ViewElement( 'strong', null, new ViewText( 'text' ) ) ]
  755. }
  756. ] );
  757. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  758. } );
  759. it( 'should do nothing on empty mutations', () => {
  760. setModelData( model,
  761. '<paragraph>' +
  762. '<$text bold="true">' +
  763. 'text[]' +
  764. '</$text>' +
  765. '</paragraph>'
  766. );
  767. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  768. // Simulate mutations and DOM change.
  769. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
  770. view.fire( 'mutations', [] );
  771. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  772. } );
  773. it( 'should do nothing if mutations does not have common ancestor', () => {
  774. setModelData( model,
  775. '<paragraph>' +
  776. '<$text bold="true">' +
  777. 'text[]' +
  778. '</$text>' +
  779. '</paragraph>'
  780. );
  781. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  782. const paragraph = viewRoot.getChild( 0 );
  783. const strong = paragraph.getChild( 0 );
  784. // Simulate mutations and DOM change.
  785. domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
  786. view.fire( 'mutations', [
  787. {
  788. type: 'children',
  789. node: paragraph,
  790. oldChildren: [ strong ],
  791. newChildren: [ strong ]
  792. },
  793. {
  794. type: 'children',
  795. node: new ViewContainerElement( 'div' ),
  796. oldChildren: [],
  797. newChildren: [ new ViewText( 'foo' ), new ViewText( 'bar' ) ]
  798. }
  799. ] );
  800. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  801. } );
  802. it( 'should handle view selection if one is returned from mutations', () => {
  803. setModelData( model,
  804. '<paragraph>' +
  805. '<$text bold="true">' +
  806. 'text[]' +
  807. '</$text>' +
  808. '</paragraph>'
  809. );
  810. expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
  811. const paragraph = viewRoot.getChild( 0 );
  812. const strong = paragraph.getChild( 0 );
  813. const viewSelection = new ViewSelection();
  814. viewSelection.setCollapsedAt( paragraph, 0 );
  815. // Simulate mutations and DOM change.
  816. domRoot.childNodes[ 0 ].innerHTML = '<b>textx</b>';
  817. view.fire( 'mutations', [
  818. // Replace `<strong>` with `<b>`.
  819. {
  820. type: 'children',
  821. node: paragraph,
  822. oldChildren: [ strong ],
  823. newChildren: [ new ViewElement( 'b', null, new ViewText( 'textx' ) ) ]
  824. }
  825. ], viewSelection );
  826. expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]textx</$text></paragraph>' );
  827. expect( getViewData( view ) ).to.equal( '<p><strong>{}textx</strong></p>' );
  828. } );
  829. // #117.
  830. it( 'should handle mixed mutations', () => {
  831. setModelData( model, '<paragraph><$text bold="true">Foo bar aple</$text></paragraph>' );
  832. const paragraph = viewRoot.getChild( 0 );
  833. const strong = paragraph.getChild( 0 );
  834. const viewSelection = new ViewSelection();
  835. viewSelection.setCollapsedAt( paragraph, 0 );
  836. // Simulate mutations and DOM change.
  837. domRoot.childNodes[ 0 ].innerHTML = '<strong>Foo bar </strong><b>apple</b>';
  838. view.fire( 'mutations', [
  839. {
  840. type: 'text',
  841. oldText: 'Foo bar aple',
  842. newText: 'Foo bar ',
  843. node: viewRoot.getChild( 0 ).getChild( 0 )
  844. },
  845. {
  846. type: 'children',
  847. oldChildren: [ strong ],
  848. newChildren: [ strong, new ViewElement( 'b', null, new ViewText( 'apple' ) ) ],
  849. node: paragraph
  850. }
  851. ], viewSelection );
  852. expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]Foo bar apple</$text></paragraph>' );
  853. expect( getViewData( view ) ).to.equal( '<p><strong>{}Foo bar apple</strong></p>' );
  854. } );
  855. } );
  856. } );