8
0

input.js 31 KB

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