input.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124
  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 ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  6. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  7. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  8. import List from '@ckeditor/ckeditor5-list/src/list';
  9. import Link from '@ckeditor/ckeditor5-link/src/link';
  10. import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
  11. import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
  12. import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
  13. import Input from '../src/input';
  14. import Writer from '@ckeditor/ckeditor5-engine/src/model/writer';
  15. import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
  16. import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
  17. import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
  18. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  19. import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
  20. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  21. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  22. import env from '@ckeditor/ckeditor5-utils/src/env';
  23. /* global document */
  24. describe( 'Input feature', () => {
  25. let editor, model, modelRoot, view, viewDocument, viewRoot, listenter;
  26. testUtils.createSinonSandbox();
  27. beforeEach( () => {
  28. listenter = Object.create( EmitterMixin );
  29. const domElement = document.createElement( 'div' );
  30. document.body.appendChild( domElement );
  31. return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, List, ShiftEnter, Link ] } )
  32. .then( newEditor => {
  33. // Mock image feature.
  34. newEditor.model.schema.register( 'image', { allowWhere: '$text' } );
  35. newEditor.conversion.elementToElement( {
  36. model: 'image',
  37. view: 'img'
  38. } );
  39. editor = newEditor;
  40. model = editor.model;
  41. modelRoot = model.document.getRoot();
  42. view = editor.editing.view;
  43. viewDocument = view.document;
  44. viewRoot = viewDocument.getRoot();
  45. editor.setData( '<p>foobar</p>' );
  46. model.change( writer => {
  47. writer.setSelection( modelRoot.getChild( 0 ), 3 );
  48. } );
  49. } );
  50. } );
  51. afterEach( () => {
  52. listenter.stopListening();
  53. return editor.destroy();
  54. } );
  55. describe( 'isInput()', () => {
  56. let input;
  57. beforeEach( () => {
  58. input = editor.plugins.get( 'Input' );
  59. } );
  60. it( 'returns true for batch created using "input" command', done => {
  61. model.document.once( 'change:data', ( evt, batch ) => {
  62. expect( input.isInput( batch ) ).to.be.true;
  63. done();
  64. } );
  65. editor.execute( 'input', { text: 'foo' } );
  66. } );
  67. it( 'returns false for batch not created using "input" command', () => {
  68. const batch = model.createBatch();
  69. expect( input.isInput( batch ) ).to.be.false;
  70. } );
  71. } );
  72. describe( 'mutations handling', () => {
  73. it( 'should handle text mutation', () => {
  74. viewDocument.fire( 'mutations', [
  75. {
  76. type: 'text',
  77. oldText: 'foobar',
  78. newText: 'fooxbar',
  79. node: viewRoot.getChild( 0 ).getChild( 0 )
  80. }
  81. ] );
  82. expect( getModelData( model ) ).to.equal( '<paragraph>foox[]bar</paragraph>' );
  83. expect( getViewData( view ) ).to.equal( '<p>foox{}bar</p>' );
  84. } );
  85. it( 'should handle text mutation change', () => {
  86. viewDocument.fire( 'mutations', [
  87. {
  88. type: 'text',
  89. oldText: 'foobar',
  90. newText: 'foodar',
  91. node: viewRoot.getChild( 0 ).getChild( 0 )
  92. }
  93. ] );
  94. expect( getModelData( model ) ).to.equal( '<paragraph>food[]ar</paragraph>' );
  95. expect( getViewData( view ) ).to.equal( '<p>food{}ar</p>' );
  96. } );
  97. it( 'should handle text node insertion', () => {
  98. editor.setData( '<p></p>' );
  99. viewDocument.fire( 'mutations', [
  100. {
  101. type: 'children',
  102. oldChildren: [],
  103. newChildren: [ new ViewText( 'x' ) ],
  104. node: viewRoot.getChild( 0 )
  105. }
  106. ] );
  107. expect( getModelData( model ) ).to.equal( '<paragraph>x[]</paragraph>' );
  108. expect( getViewData( view ) ).to.equal( '<p>x{}</p>' );
  109. } );
  110. it( 'should apply selection attributes to the inserted text', () => {
  111. setModelData( model, '<paragraph>[]</paragraph>', {
  112. selectionAttributes: {
  113. bold: true,
  114. italic: true
  115. }
  116. } );
  117. viewDocument.fire( 'mutations', [
  118. {
  119. type: 'children',
  120. oldChildren: [],
  121. newChildren: [ new ViewText( 'x' ) ],
  122. node: viewRoot.getChild( 0 )
  123. }
  124. ] );
  125. expect( getModelData( model ) ).to.equal(
  126. '<paragraph><$text bold="true" italic="true">x[]</$text></paragraph>'
  127. );
  128. } );
  129. it( 'should handle multiple text mutations', () => {
  130. editor.setData( '<p>foo<strong>bar</strong></p>' );
  131. viewDocument.fire( 'mutations', [
  132. {
  133. type: 'text',
  134. oldText: 'foo',
  135. newText: 'foob',
  136. node: viewRoot.getChild( 0 ).getChild( 0 )
  137. },
  138. {
  139. type: 'text',
  140. oldText: 'bar',
  141. newText: 'ar',
  142. node: viewRoot.getChild( 0 ).getChild( 1 ).getChild( 0 )
  143. }
  144. ] );
  145. expect( getModelData( model ) ).to.equal( '<paragraph>foob[]<$text bold="true">ar</$text></paragraph>' );
  146. expect( getViewData( view ) ).to.equal( '<p>foob{}<strong>ar</strong></p>' );
  147. } );
  148. it( 'should handle multiple text node insertion', () => {
  149. editor.setData( '<p></p><p></p>' );
  150. viewDocument.fire( 'mutations', [
  151. {
  152. type: 'children',
  153. oldChildren: [],
  154. newChildren: [ new ViewText( 'x' ) ],
  155. node: viewRoot.getChild( 0 )
  156. },
  157. {
  158. type: 'children',
  159. oldChildren: [],
  160. newChildren: [ new ViewText( 'y' ) ],
  161. node: viewRoot.getChild( 1 )
  162. }
  163. ] );
  164. expect( getModelData( model ) ).to.equal( '<paragraph>x</paragraph><paragraph>y[]</paragraph>' );
  165. expect( getViewData( view ) ).to.equal( '<p>x</p><p>y{}</p>' );
  166. } );
  167. it( 'should do nothing when two nodes were inserted', () => {
  168. editor.setData( '<p></p>' );
  169. viewDocument.fire( 'mutations', [
  170. {
  171. type: 'children',
  172. oldChildren: [],
  173. newChildren: [ new ViewText( 'x' ), new ViewElement( 'img' ) ],
  174. node: viewRoot.getChild( 0 )
  175. }
  176. ] );
  177. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  178. expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
  179. } );
  180. it( 'should do nothing when two nodes were inserted and one removed', () => {
  181. viewDocument.fire( 'mutations', [
  182. {
  183. type: 'children',
  184. oldChildren: [ new ViewText( 'foobar' ) ],
  185. newChildren: [ new ViewText( 'x' ), new ViewElement( 'img' ) ],
  186. node: viewRoot.getChild( 0 )
  187. }
  188. ] );
  189. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  190. expect( getViewData( view ) ).to.equal( '<p>foo{}bar</p>' );
  191. } );
  192. it( 'should handle multiple children in the node', () => {
  193. editor.setData( '<p>foo<img></img></p>' );
  194. viewDocument.fire( 'mutations', [
  195. {
  196. type: 'children',
  197. oldChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ) ],
  198. newChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ), new ViewText( 'x' ) ],
  199. node: viewRoot.getChild( 0 )
  200. }
  201. ] );
  202. expect( getModelData( model ) ).to.equal( '<paragraph>foo<image></image>x[]</paragraph>' );
  203. expect( getViewData( view ) ).to.equal( '<p>foo<img></img>x{}</p>' );
  204. } );
  205. it( 'should do nothing when node was removed', () => {
  206. viewDocument.fire( 'mutations', [
  207. {
  208. type: 'children',
  209. oldChildren: [ new ViewText( 'foobar' ) ],
  210. newChildren: [],
  211. node: viewRoot.getChild( 0 )
  212. }
  213. ] );
  214. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  215. expect( getViewData( view ) ).to.equal( '<p>foo{}bar</p>' );
  216. } );
  217. it( 'should do nothing when element was inserted', () => {
  218. editor.setData( '<p></p>' );
  219. viewDocument.fire( 'mutations', [
  220. {
  221. type: 'children',
  222. oldChildren: [],
  223. newChildren: [ new ViewElement( 'img' ) ],
  224. node: viewRoot.getChild( 0 )
  225. }
  226. ] );
  227. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  228. expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
  229. } );
  230. it( 'should set model selection appropriately to view selection passed in mutations event', () => {
  231. // This test case emulates spellchecker correction.
  232. const viewSelection = view.createSelection();
  233. viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
  234. viewDocument.fire( 'mutations',
  235. [ {
  236. type: 'text',
  237. oldText: 'foobar',
  238. newText: 'foodar',
  239. node: viewRoot.getChild( 0 ).getChild( 0 )
  240. } ],
  241. viewSelection
  242. );
  243. expect( getModelData( model ) ).to.equal( '<paragraph>foodar[]</paragraph>' );
  244. expect( getViewData( view ) ).to.equal( '<p>foodar{}</p>' );
  245. } );
  246. it( 'should use up to one insert and remove operations (spellchecker)', () => {
  247. // This test case emulates spellchecker correction.
  248. const viewSelection = view.createSelection();
  249. viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
  250. testUtils.sinon.spy( Writer.prototype, 'insert' );
  251. testUtils.sinon.spy( Writer.prototype, 'remove' );
  252. viewDocument.fire( 'mutations',
  253. [ {
  254. type: 'text',
  255. oldText: 'foobar',
  256. newText: 'fxobxr',
  257. node: viewRoot.getChild( 0 ).getChild( 0 )
  258. } ],
  259. viewSelection
  260. );
  261. expect( Writer.prototype.insert.calledOnce ).to.be.true;
  262. expect( Writer.prototype.remove.calledOnce ).to.be.true;
  263. } );
  264. it( 'should place selection after when correcting to longer word (spellchecker)', () => {
  265. // This test case emulates spellchecker correction.
  266. editor.setData( '<p>Foo hous a</p>' );
  267. const viewSelection = view.createSelection();
  268. viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
  269. viewDocument.fire( 'mutations',
  270. [ {
  271. type: 'text',
  272. oldText: 'Foo hous a',
  273. newText: 'Foo house a',
  274. node: viewRoot.getChild( 0 ).getChild( 0 )
  275. } ],
  276. viewSelection
  277. );
  278. expect( getModelData( model ) ).to.equal( '<paragraph>Foo house[] a</paragraph>' );
  279. expect( getViewData( view ) ).to.equal( '<p>Foo house{} a</p>' );
  280. } );
  281. it( 'should place selection after when correcting to shorter word (spellchecker)', () => {
  282. // This test case emulates spellchecker correction.
  283. editor.setData( '<p>Bar athat foo</p>' );
  284. const viewSelection = view.createSelection();
  285. viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
  286. viewDocument.fire( 'mutations',
  287. [ {
  288. type: 'text',
  289. oldText: 'Bar athat foo',
  290. newText: 'Bar that foo',
  291. node: viewRoot.getChild( 0 ).getChild( 0 )
  292. } ],
  293. viewSelection
  294. );
  295. expect( getModelData( model ) ).to.equal( '<paragraph>Bar that[] foo</paragraph>' );
  296. expect( getViewData( view ) ).to.equal( '<p>Bar that{} foo</p>' );
  297. } );
  298. it( 'should place selection after when merging two words (spellchecker)', () => {
  299. // This test case emulates spellchecker correction.
  300. editor.setData( '<p>Foo hous e</p>' );
  301. const viewSelection = view.createSelection();
  302. viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
  303. viewDocument.fire( 'mutations',
  304. [ {
  305. type: 'text',
  306. oldText: 'Foo hous e',
  307. newText: 'Foo house',
  308. node: viewRoot.getChild( 0 ).getChild( 0 )
  309. } ],
  310. viewSelection
  311. );
  312. expect( getModelData( model ) ).to.equal( '<paragraph>Foo house[]</paragraph>' );
  313. expect( getViewData( view ) ).to.equal( '<p>Foo house{}</p>' );
  314. } );
  315. it( 'should place non-collapsed selection after changing single character (composition)', () => {
  316. editor.setData( '<p>Foo house</p>' );
  317. const viewSelection = view.createSelection();
  318. viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
  319. viewSelection.setFocus( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
  320. viewDocument.fire( 'mutations',
  321. [ {
  322. type: 'text',
  323. oldText: 'Foo house',
  324. newText: 'Foo housa',
  325. node: viewRoot.getChild( 0 ).getChild( 0 )
  326. } ],
  327. viewSelection
  328. );
  329. expect( getModelData( model ) ).to.equal( '<paragraph>Foo hous[a]</paragraph>' );
  330. expect( getViewData( view ) ).to.equal( '<p>Foo hous{a}</p>' );
  331. } );
  332. it( 'should replace last &nbsp; with space', () => {
  333. model.change( writer => {
  334. writer.setSelection( modelRoot.getChild( 0 ), 6 );
  335. } );
  336. viewDocument.fire( 'mutations', [
  337. {
  338. type: 'text',
  339. oldText: 'foobar',
  340. newText: 'foobar\u00A0',
  341. node: viewRoot.getChild( 0 ).getChild( 0 )
  342. }
  343. ] );
  344. expect( getModelData( model ) ).to.equal( '<paragraph>foobar []</paragraph>' );
  345. expect( getViewData( view ) ).to.equal( '<p>foobar {}</p>' );
  346. } );
  347. it( 'should replace first &nbsp; with space', () => {
  348. model.change( writer => {
  349. writer.setSelection(
  350. writer.createRange(
  351. writer.createPositionAt( modelRoot.getChild( 0 ), 0 ),
  352. writer.createPositionAt( modelRoot.getChild( 0 ), 0 )
  353. )
  354. );
  355. } );
  356. viewDocument.fire( 'mutations', [
  357. {
  358. type: 'text',
  359. oldText: 'foobar',
  360. newText: '\u00A0foobar',
  361. node: viewRoot.getChild( 0 ).getChild( 0 )
  362. }
  363. ] );
  364. expect( getModelData( model ) ).to.equal( '<paragraph> []foobar</paragraph>' );
  365. expect( getViewData( view ) ).to.equal( '<p> {}foobar</p>' );
  366. } );
  367. it( 'should replace all &nbsp; with spaces', () => {
  368. model.change( writer => {
  369. writer.setSelection( modelRoot.getChild( 0 ), 6 );
  370. } );
  371. viewDocument.fire( 'mutations', [
  372. {
  373. type: 'text',
  374. oldText: 'foobar',
  375. newText: 'foobar\u00A0\u00A0\u00A0baz',
  376. node: viewRoot.getChild( 0 ).getChild( 0 )
  377. }
  378. ] );
  379. expect( getModelData( model ) ).to.equal( '<paragraph>foobar baz[]</paragraph>' );
  380. expect( getViewData( view ) ).to.equal( '<p>foobar baz{}</p>' );
  381. } );
  382. // ckeditor5#718.
  383. it( 'should not crash and prevent all changes if view common ancestor of mutations cannot be mapped to model', () => {
  384. editor.setData( '<p>Foo</p><ul><li>Bar</li><li>Baz</li></ul>' );
  385. const ul = viewRoot.getChild( 1 );
  386. viewDocument.fire( 'mutations', [
  387. {
  388. type: 'text',
  389. oldText: 'Bar',
  390. newText: 'Bx',
  391. node: ul.getChild( 0 )
  392. },
  393. {
  394. type: 'children',
  395. oldChildren: [ ul.getChild( 0 ), ul.getChild( 1 ) ],
  396. newChildren: [ ul.getChild( 0 ) ],
  397. node: ul
  398. }
  399. ] );
  400. expect( getViewData( view ) ).to.equal( '<p>{}Foo</p><ul><li>Bar</li><li>Baz</li></ul>' );
  401. } );
  402. it( 'should handle bogus br correctly', () => {
  403. editor.setData( '<p><strong>Foo</strong></p>' );
  404. editor.model.change( writer => {
  405. writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' );
  406. writer.removeSelectionAttribute( 'bold' );
  407. } );
  408. // We need to change the DOM content manually because typing algorithm actually does not check
  409. // `newChildren` and `oldChildren` list but takes them from DOM and model.
  410. const p = viewRoot.getChild( 0 );
  411. const domP = editor.editing.view.domConverter.mapViewToDom( p );
  412. domP.appendChild( document.createTextNode( ' ' ) );
  413. domP.appendChild( document.createElement( 'br' ) );
  414. viewDocument.fire( 'mutations', [
  415. {
  416. type: 'children',
  417. oldChildren: [ viewRoot.getChild( 0 ).getChild( 0 ) ],
  418. newChildren: [
  419. new ViewElement( 'strong', null, new ViewText( 'Foo' ) ),
  420. new ViewText( ' ' ),
  421. new ViewElement( 'br' )
  422. ],
  423. node: viewRoot.getChild( 0 )
  424. }
  425. ] );
  426. expect( getViewData( view ) ).to.equal( '<p><strong>Foo</strong> {}</p>' );
  427. } );
  428. it( 'should handle children mutation correctly if there are soft breaks in the mutated container', () => {
  429. editor.setData( '<p><strong>Foo</strong><br /><strong>Bar</strong></p>' );
  430. editor.model.change( writer => {
  431. writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' );
  432. writer.removeSelectionAttribute( 'bold' );
  433. } );
  434. // We need to change the DOM content manually because typing algorithm actually does not check
  435. // `newChildren` and `oldChildren` list but takes them from DOM and model.
  436. const p = viewRoot.getChild( 0 );
  437. const domP = editor.editing.view.domConverter.mapViewToDom( p );
  438. domP.appendChild( document.createTextNode( ' ' ) );
  439. domP.appendChild( document.createElement( 'br' ) );
  440. viewDocument.fire( 'mutations', [
  441. {
  442. type: 'children',
  443. oldChildren: [ ...viewRoot.getChild( 0 ).getChildren() ],
  444. newChildren: [
  445. new ViewElement( 'strong', null, new ViewText( 'Foo' ) ),
  446. new ViewElement( 'br' ),
  447. new ViewElement( 'strong', null, new ViewText( 'Bar' ) ),
  448. new ViewText( ' ' ),
  449. new ViewElement( 'br' )
  450. ],
  451. node: viewRoot.getChild( 0 )
  452. }
  453. ] );
  454. expect( getViewData( view ) ).to.equal( '<p><strong>Foo</strong><br></br><strong>Bar</strong> {}</p>' );
  455. } );
  456. // ckeditor5-typing#170.
  457. it( 'should handle mutations correctly if there was an &nbsp; in model already', () => {
  458. editor.setData( '<p><a href="#"><strong>F</strong></a><strong>oo&nbsp;bar</strong></p>' );
  459. model.change( writer => {
  460. writer.setSelection( modelRoot.getChild( 0 ), 1 );
  461. } );
  462. // We need to change the DOM content manually because typing algorithm actually does not check
  463. // `newChildren` and `oldChildren` list but takes them from DOM and model.
  464. const strong = viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 );
  465. const domStrong = editor.editing.view.domConverter.mapViewToDom( strong );
  466. domStrong.appendChild( document.createTextNode( 'x' ) );
  467. // The mutation provided here is a bit different than what browser outputs, but browser outputs three mutations
  468. // which changes the order of elements in the DOM so to keep it simple, only one, key mutation is used in the test.
  469. // Also, the only thing that the typing algorithm takes from the mutations is `node`...
  470. viewDocument.fire( 'mutations', [
  471. {
  472. type: 'children',
  473. oldChildren: Array.from( viewRoot.getChild( 0 ).getChild( 0 ).getChildren() ),
  474. newChildren: [
  475. new ViewElement( 'strong', null, new ViewText( 'Fx' ) ),
  476. ],
  477. node: viewRoot.getChild( 0 ).getChild( 0 )
  478. }
  479. ] );
  480. expect( getModelData( model ) ).to.equal(
  481. '<paragraph>' +
  482. '<$text bold="true" linkHref="#">Fx[]</$text>' +
  483. '<$text bold="true">oo\u00A0bar</$text>' +
  484. '</paragraph>'
  485. );
  486. expect( getViewData( view ) ).to.equal(
  487. '<p>' +
  488. '<a class="ck-link_selected" href="#"><strong>Fx{}</strong></a>' +
  489. '<strong>oo\u00A0bar</strong>' +
  490. '</p>'
  491. );
  492. } );
  493. it( 'should handle correctly if last element is inline', () => {
  494. editor.model.schema.register( 'placeholder', { allowWhere: '$text', isInline: true } );
  495. editor.conversion.elementToElement( {
  496. model: 'placeholder',
  497. view: 'placeholder'
  498. } );
  499. editor.setData( '<p>foo<placeholder></placeholder></p>' );
  500. editor.model.change( writer => {
  501. writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' );
  502. } );
  503. expect( getViewData( view ) ).to.equal( '<p>foo<placeholder></placeholder>[]</p>' );
  504. // We need to change the DOM content manually because typing algorithm actually does not check
  505. // `newChildren` and `oldChildren` list but takes them from DOM and model.
  506. const p = viewRoot.getChild( 0 );
  507. const domP = editor.editing.view.domConverter.mapViewToDom( p );
  508. domP.appendChild( document.createTextNode( '!' ) );
  509. viewDocument.fire( 'mutations', [
  510. {
  511. type: 'children',
  512. oldChildren: [ ...viewRoot.getChild( 0 ).getChildren() ],
  513. newChildren: [
  514. new ViewText( 'Foo' ),
  515. new ViewContainerElement( 'placeholder' ),
  516. new ViewText( 'f' )
  517. ],
  518. node: viewRoot.getChild( 0 )
  519. }
  520. ] );
  521. expect( getViewData( view ) ).to.equal( '<p>foo<placeholder></placeholder>!{}</p>' );
  522. } );
  523. it( 'should handle correctly if some elements are inline', () => {
  524. editor.model.schema.register( 'placeholder', { allowWhere: '$text', isInline: true } );
  525. editor.conversion.elementToElement( {
  526. model: 'placeholder',
  527. view: 'placeholder'
  528. } );
  529. editor.setData( '<p>foo<placeholder></placeholder>bar<placeholder></placeholder>baz</p>' );
  530. editor.model.change( writer => {
  531. writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' );
  532. } );
  533. // We need to change the DOM content manually because typing algorithm actually does not check
  534. // `newChildren` and `oldChildren` list but takes them from DOM and model.
  535. const p = viewRoot.getChild( 0 );
  536. const domP = editor.editing.view.domConverter.mapViewToDom( p );
  537. domP.appendChild( document.createTextNode( '!' ) );
  538. viewDocument.fire( 'mutations', [
  539. {
  540. type: 'children',
  541. oldChildren: [ ...viewRoot.getChild( 0 ).getChildren() ],
  542. newChildren: [
  543. new ViewText( 'foo' ),
  544. new ViewContainerElement( 'placeholder' ),
  545. new ViewText( 'bar' ),
  546. new ViewContainerElement( 'placeholder' ),
  547. new ViewText( 'baz' )
  548. ],
  549. node: viewRoot.getChild( 0 )
  550. }
  551. ] );
  552. expect( getViewData( view ) ).to.equal( '<p>foo<placeholder></placeholder>bar<placeholder></placeholder>baz!{}</p>' );
  553. } );
  554. // https://github.com/ckeditor/ckeditor5-typing/issues/181
  555. it( 'should not crash if the mutation old text is same as new text', () => {
  556. // It shouldn't matter what data is here, I am putting it like it is in the test scenario, but it is really about
  557. // what mutations are generated.
  558. editor.setData( '<p>Foo<strong> </strong>&nbsp;Bar</p>' );
  559. const p = viewRoot.getChild( 0 );
  560. viewDocument.fire( 'mutations', [
  561. {
  562. type: 'text',
  563. oldText: ' ',
  564. newText: ' ',
  565. node: p.getChild( 1 )
  566. },
  567. {
  568. type: 'text',
  569. oldText: 'Foo',
  570. newText: 'Foox',
  571. node: p.getChild( 0 )
  572. }
  573. ] );
  574. expect( getViewData( view ) ).to.equal( '<p>Foox{}<strong> </strong> Bar</p>' );
  575. } );
  576. } );
  577. describe( 'keystroke handling', () => {
  578. it( 'should remove contents', () => {
  579. model.change( writer => {
  580. writer.setSelection(
  581. writer.createRange(
  582. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  583. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  584. )
  585. );
  586. } );
  587. listenter.listenTo( viewDocument, 'keydown', () => {
  588. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  589. }, { priority: 'lowest' } );
  590. viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } );
  591. } );
  592. // #97
  593. it( 'should remove contents and merge blocks', () => {
  594. setModelData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
  595. listenter.listenTo( viewDocument, 'keydown', () => {
  596. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  597. viewDocument.fire( 'mutations', [
  598. {
  599. type: 'text',
  600. oldText: 'foar',
  601. newText: 'foyar',
  602. node: viewRoot.getChild( 0 ).getChild( 0 )
  603. }
  604. ] );
  605. }, { priority: 'lowest' } );
  606. viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } );
  607. expect( getModelData( model ) ).to.equal( '<paragraph>foy[]ar</paragraph>' );
  608. expect( getViewData( view ) ).to.equal( '<p>foy{}ar</p>' );
  609. } );
  610. it( 'should do nothing on arrow key', () => {
  611. model.change( writer => {
  612. writer.setSelection(
  613. writer.createRange(
  614. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  615. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  616. )
  617. );
  618. } );
  619. viewDocument.fire( 'keydown', { keyCode: getCode( 'arrowdown' ) } );
  620. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  621. } );
  622. it( 'should do nothing on ctrl combinations', () => {
  623. model.change( writer => {
  624. writer.setSelection(
  625. writer.createRange(
  626. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  627. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  628. )
  629. );
  630. } );
  631. viewDocument.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
  632. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  633. } );
  634. it( 'should do nothing on non printable keys', () => {
  635. model.change( writer => {
  636. writer.setSelection(
  637. writer.createRange(
  638. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  639. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  640. )
  641. );
  642. } );
  643. viewDocument.fire( 'keydown', { keyCode: 16 } ); // Shift
  644. viewDocument.fire( 'keydown', { keyCode: 19 } ); // Pause
  645. viewDocument.fire( 'keydown', { keyCode: 35 } ); // Home
  646. viewDocument.fire( 'keydown', { keyCode: 112 } ); // F1
  647. viewDocument.fire( 'keydown', { keyCode: 255 } ); // Display brightness
  648. // Media control keys
  649. viewDocument.fire( 'keydown', { keyCode: 173 } ); // Mute
  650. viewDocument.fire( 'keydown', { keyCode: 174 } ); // Volume up
  651. viewDocument.fire( 'keydown', { keyCode: 175 } ); // Volume down
  652. viewDocument.fire( 'keydown', { keyCode: 176 } ); // Next song
  653. viewDocument.fire( 'keydown', { keyCode: 177 } ); // Previous song
  654. viewDocument.fire( 'keydown', { keyCode: 179 } ); // Stop
  655. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  656. } );
  657. // #69
  658. it( 'should do nothing on tab key', () => {
  659. model.change( writer => {
  660. writer.setSelection(
  661. writer.createRange(
  662. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  663. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  664. )
  665. );
  666. } );
  667. viewDocument.fire( 'keydown', { keyCode: 9 } ); // Tab
  668. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  669. } );
  670. it( 'should do nothing if selection is collapsed', () => {
  671. viewDocument.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
  672. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  673. } );
  674. it( 'should lock buffer if selection is not collapsed', () => {
  675. const buffer = editor.commands.get( 'input' )._buffer;
  676. const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
  677. const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
  678. model.change( writer => {
  679. writer.setSelection(
  680. writer.createRange(
  681. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  682. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  683. )
  684. );
  685. } );
  686. viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } );
  687. expect( lockSpy.calledOnce ).to.be.true;
  688. expect( unlockSpy.calledOnce ).to.be.true;
  689. } );
  690. it( 'should not lock buffer on non printable keys', () => {
  691. const buffer = editor.commands.get( 'input' )._buffer;
  692. const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
  693. const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
  694. viewDocument.fire( 'keydown', { keyCode: 16 } ); // Shift
  695. viewDocument.fire( 'keydown', { keyCode: 35 } ); // Home
  696. viewDocument.fire( 'keydown', { keyCode: 112 } ); // F1
  697. expect( lockSpy.callCount ).to.be.equal( 0 );
  698. expect( unlockSpy.callCount ).to.be.equal( 0 );
  699. } );
  700. it( 'should not lock buffer on collapsed selection', () => {
  701. const buffer = editor.commands.get( 'input' )._buffer;
  702. const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
  703. const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
  704. viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } );
  705. viewDocument.fire( 'keydown', { keyCode: getCode( 'a' ) } );
  706. viewDocument.fire( 'keydown', { keyCode: getCode( 'z' ) } );
  707. expect( lockSpy.callCount ).to.be.equal( 0 );
  708. expect( unlockSpy.callCount ).to.be.equal( 0 );
  709. } );
  710. it( 'should not modify document when input command is disabled and selection is collapsed', () => {
  711. setModelData( model, '<paragraph>foo[]bar</paragraph>' );
  712. editor.commands.get( 'input' ).isEnabled = false;
  713. viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } );
  714. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  715. } );
  716. it( 'should not modify document when input command is disabled and selection is non-collapsed', () => {
  717. setModelData( model, '<paragraph>fo[ob]ar</paragraph>' );
  718. editor.commands.get( 'input' ).isEnabled = false;
  719. viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } );
  720. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  721. } );
  722. describe( '#83', () => {
  723. it( 'should remove contents on composition start key if not during composition', () => {
  724. model.change( writer => {
  725. writer.setSelection(
  726. writer.createRange(
  727. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  728. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  729. )
  730. );
  731. } );
  732. viewDocument.fire( 'keydown', { keyCode: 229 } );
  733. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  734. } );
  735. it( 'should not remove contents on composition start key if during composition', () => {
  736. model.change( writer => {
  737. writer.setSelection(
  738. writer.createRange(
  739. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  740. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  741. )
  742. );
  743. } );
  744. viewDocument.fire( 'compositionstart' );
  745. viewDocument.fire( 'keydown', { keyCode: 229 } );
  746. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  747. } );
  748. it( 'should not remove contents on compositionstart event if selection is flat', () => {
  749. editor.setData( '<p><strong>foo</strong> <i>bar</i></p>' );
  750. model.change( writer => {
  751. writer.setSelection(
  752. writer.createRange(
  753. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  754. writer.createPositionAt( modelRoot.getChild( 0 ), 5 )
  755. )
  756. );
  757. } );
  758. viewDocument.fire( 'compositionstart' );
  759. expect( getModelData( model ) ).to.equal(
  760. '<paragraph><$text bold="true">fo[o</$text> <$text italic="true">b]ar</$text></paragraph>' );
  761. } );
  762. it( 'should not remove contents on compositionstart event if no selection', () => {
  763. editor.setData( '<p><strong>foo</strong> <i>bar</i></p>' );
  764. const documentSelection = model.document.selection;
  765. // Create empty selection.
  766. model.document.selection = model.createSelection();
  767. viewDocument.fire( 'compositionstart' );
  768. expect( getModelData( model ) ).to.equal(
  769. '<paragraph><$text bold="true">foo</$text> <$text italic="true">bar</$text></paragraph>' );
  770. // Restore document selection.
  771. model.document.selection = documentSelection;
  772. } );
  773. it( 'should remove contents on compositionstart event if selection is not flat', () => {
  774. editor.setData( '<p><strong>foo</strong></p><p><i>bar</i></p>' );
  775. model.change( writer => {
  776. writer.setSelection(
  777. writer.createRange(
  778. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  779. writer.createPositionAt( modelRoot.getChild( 1 ), 2 )
  780. )
  781. );
  782. } );
  783. viewDocument.fire( 'compositionstart' );
  784. expect( getModelData( model ) ).to.equal(
  785. '<paragraph><$text bold="true">fo[]</$text><$text italic="true">r</$text></paragraph>' );
  786. } );
  787. it( 'should not remove contents on keydown event after compositionend event if selection did not change', () => {
  788. editor.setData( '<p><strong>foo</strong></p><p><i>bar</i></p>' );
  789. model.change( writer => {
  790. writer.setSelection(
  791. writer.createRange(
  792. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  793. writer.createPositionAt( modelRoot.getChild( 1 ), 2 )
  794. )
  795. );
  796. } );
  797. viewDocument.fire( 'compositionend' );
  798. viewDocument.fire( 'keydown', { keyCode: 229 } );
  799. expect( getModelData( model ) ).to.equal(
  800. '<paragraph><$text bold="true">fo[o</$text></paragraph><paragraph><$text italic="true">ba]r</$text></paragraph>' );
  801. } );
  802. it( 'should remove contents on keydown event after compositionend event if selection have changed', () => {
  803. editor.setData( '<p><strong>foo</strong></p><p><i>bar</i></p>' );
  804. model.change( writer => {
  805. writer.setSelection(
  806. writer.createRange(
  807. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  808. writer.createPositionAt( modelRoot.getChild( 1 ), 2 )
  809. )
  810. );
  811. } );
  812. viewDocument.fire( 'compositionend' );
  813. model.change( writer => {
  814. writer.setSelection(
  815. writer.createRange(
  816. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  817. writer.createPositionAt( modelRoot.getChild( 1 ), 1 )
  818. )
  819. );
  820. } );
  821. viewDocument.fire( 'keydown', { keyCode: 229 } );
  822. expect( getModelData( model ) ).to.equal(
  823. '<paragraph><$text bold="true">fo[]</$text><$text italic="true">ar</$text></paragraph>' );
  824. } );
  825. } );
  826. } );
  827. } );
  828. describe( 'Input feature - Android', () => {
  829. let editor, model, modelRoot, view, viewDocument, listenter, oldEnvIsAndroid;
  830. testUtils.createSinonSandbox();
  831. before( () => {
  832. oldEnvIsAndroid = env.isAndroid;
  833. env.isAndroid = true;
  834. } );
  835. beforeEach( () => {
  836. listenter = Object.create( EmitterMixin );
  837. const domElement = document.createElement( 'div' );
  838. document.body.appendChild( domElement );
  839. return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, List, ShiftEnter, Link ] } )
  840. .then( newEditor => {
  841. editor = newEditor;
  842. model = editor.model;
  843. modelRoot = model.document.getRoot();
  844. view = editor.editing.view;
  845. viewDocument = view.document;
  846. editor.setData( '<p>foobar</p>' );
  847. model.change( writer => {
  848. writer.setSelection( modelRoot.getChild( 0 ), 3 );
  849. } );
  850. } );
  851. } );
  852. afterEach( () => {
  853. listenter.stopListening();
  854. return editor.destroy();
  855. } );
  856. after( () => {
  857. env.isAndroid = oldEnvIsAndroid;
  858. } );
  859. describe( 'keystroke handling', () => {
  860. it( 'should remove contents', () => {
  861. model.change( writer => {
  862. writer.setSelection(
  863. writer.createRange(
  864. writer.createPositionAt( modelRoot.getChild( 0 ), 2 ),
  865. writer.createPositionAt( modelRoot.getChild( 0 ), 4 )
  866. )
  867. );
  868. } );
  869. listenter.listenTo( viewDocument, 'beforeinput', () => {
  870. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  871. }, { priority: 'lowest' } );
  872. // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send).
  873. viewDocument.fire( 'beforeinput', { keyCode: 229 } );
  874. } );
  875. it( 'should remove contents and merge blocks', () => {
  876. setModelData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
  877. listenter.listenTo( viewDocument, 'beforeinput', () => {
  878. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  879. }, { priority: 'lowest' } );
  880. // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send).
  881. viewDocument.fire( 'beforeinput', { keyCode: 229 } );
  882. } );
  883. it( 'should do nothing if selection is collapsed', () => {
  884. viewDocument.fire( 'beforeinput', { keyCode: 229 } );
  885. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  886. } );
  887. it( 'should not modify document when input command is disabled and selection is collapsed', () => {
  888. setModelData( model, '<paragraph>foo[]bar</paragraph>' );
  889. editor.commands.get( 'input' ).isEnabled = false;
  890. // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send).
  891. viewDocument.fire( 'beforeinput', { keyCode: 229 } );
  892. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  893. } );
  894. it( 'should not modify document when input command is disabled and selection is non-collapsed', () => {
  895. setModelData( model, '<paragraph>fo[ob]ar</paragraph>' );
  896. editor.commands.get( 'input' ).isEnabled = false;
  897. // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send).
  898. viewDocument.fire( 'beforeinput', { keyCode: 229 } );
  899. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  900. } );
  901. } );
  902. } );