input.js 35 KB

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