input.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. /*
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  6. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  7. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  8. import Input from '../src/input';
  9. import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
  10. import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
  11. import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
  12. import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
  13. import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
  14. import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
  15. import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
  16. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  17. import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
  18. import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  19. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  20. describe( 'Input feature', () => {
  21. let editor, model, modelRoot, view, viewRoot, listenter;
  22. testUtils.createSinonSandbox();
  23. before( () => {
  24. listenter = Object.create( EmitterMixin );
  25. return VirtualTestEditor.create( {
  26. plugins: [ Input, Paragraph ]
  27. } )
  28. .then( newEditor => {
  29. // Mock image feature.
  30. newEditor.document.schema.registerItem( 'image', '$inline' );
  31. buildModelConverter().for( newEditor.data.modelToView, newEditor.editing.modelToView )
  32. .fromElement( 'image' )
  33. .toElement( 'img' );
  34. buildViewConverter().for( newEditor.data.viewToModel )
  35. .fromElement( 'img' )
  36. .toElement( 'image' );
  37. editor = newEditor;
  38. model = editor.editing.model;
  39. modelRoot = model.getRoot();
  40. view = editor.editing.view;
  41. viewRoot = view.getRoot();
  42. } );
  43. } );
  44. beforeEach( () => {
  45. editor.setData( '<p>foobar</p>' );
  46. model.enqueueChanges( () => {
  47. model.selection.setRanges( [
  48. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 3, modelRoot.getChild( 0 ), 3 )
  49. ] );
  50. } );
  51. } );
  52. afterEach( () => {
  53. listenter.stopListening();
  54. } );
  55. describe( 'buffer', () => {
  56. it( 'has a buffer configured to default value of config.typing.undoStep', () => {
  57. expect( editor.plugins.get( Input )._buffer ).to.have.property( 'limit', 20 );
  58. } );
  59. it( 'has a buffer configured to config.typing.undoStep', () => {
  60. return VirtualTestEditor.create( {
  61. plugins: [ Input ],
  62. typing: {
  63. undoStep: 5
  64. }
  65. } )
  66. .then( editor => {
  67. expect( editor.plugins.get( Input )._buffer ).to.have.property( 'limit', 5 );
  68. } );
  69. } );
  70. } );
  71. describe( 'mutations handling', () => {
  72. it( 'should handle text mutation', () => {
  73. view.fire( 'mutations', [
  74. {
  75. type: 'text',
  76. oldText: 'foobar',
  77. newText: 'fooxbar',
  78. node: viewRoot.getChild( 0 ).getChild( 0 )
  79. }
  80. ] );
  81. expect( getModelData( model ) ).to.equal( '<paragraph>foox[]bar</paragraph>' );
  82. expect( getViewData( view ) ).to.equal( '<p>foox{}bar</p>' );
  83. } );
  84. it( 'should handle text mutation change', () => {
  85. view.fire( 'mutations', [
  86. {
  87. type: 'text',
  88. oldText: 'foobar',
  89. newText: 'foodar',
  90. node: viewRoot.getChild( 0 ).getChild( 0 )
  91. }
  92. ] );
  93. expect( getModelData( model ) ).to.equal( '<paragraph>food[]ar</paragraph>' );
  94. expect( getViewData( view ) ).to.equal( '<p>food{}ar</p>' );
  95. } );
  96. it( 'should handle text node insertion', () => {
  97. editor.setData( '<p></p>' );
  98. view.fire( 'mutations', [
  99. {
  100. type: 'children',
  101. oldChildren: [],
  102. newChildren: [ new ViewText( 'x' ) ],
  103. node: viewRoot.getChild( 0 )
  104. }
  105. ] );
  106. expect( getModelData( model ) ).to.equal( '<paragraph>x[]</paragraph>' );
  107. expect( getViewData( view ) ).to.equal( '<p>x{}</p>' );
  108. } );
  109. it( 'should do nothing when two nodes were inserted', () => {
  110. editor.setData( '<p></p>' );
  111. view.fire( 'mutations', [
  112. {
  113. type: 'children',
  114. oldChildren: [],
  115. newChildren: [ new ViewText( 'x' ), new ViewElement( 'img' ) ],
  116. node: viewRoot.getChild( 0 )
  117. }
  118. ] );
  119. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  120. expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
  121. } );
  122. it( 'should do nothing when two nodes were inserted and one removed', () => {
  123. view.fire( 'mutations', [
  124. {
  125. type: 'children',
  126. oldChildren: [ new ViewText( 'foobar' ) ],
  127. newChildren: [ new ViewText( 'x' ), new ViewElement( 'img' ) ],
  128. node: viewRoot.getChild( 0 )
  129. }
  130. ] );
  131. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  132. expect( getViewData( view ) ).to.equal( '<p>foo{}bar</p>' );
  133. } );
  134. it( 'should handle multiple children in the node', () => {
  135. editor.setData( '<p>foo<img></img></p>' );
  136. view.fire( 'mutations', [
  137. {
  138. type: 'children',
  139. oldChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ) ],
  140. newChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ), new ViewText( 'x' ) ],
  141. node: viewRoot.getChild( 0 )
  142. }
  143. ] );
  144. expect( getModelData( model ) ).to.equal( '<paragraph>foo<image></image>x[]</paragraph>' );
  145. expect( getViewData( view ) ).to.equal( '<p>foo<img></img>x{}</p>' );
  146. } );
  147. it( 'should do nothing when node was removed', () => {
  148. view.fire( 'mutations', [
  149. {
  150. type: 'children',
  151. oldChildren: [ new ViewText( 'foobar' ) ],
  152. newChildren: [],
  153. node: viewRoot.getChild( 0 )
  154. }
  155. ] );
  156. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  157. expect( getViewData( view ) ).to.equal( '<p>foo{}bar</p>' );
  158. } );
  159. it( 'should do nothing when element was inserted', () => {
  160. editor.setData( '<p></p>' );
  161. view.fire( 'mutations', [
  162. {
  163. type: 'children',
  164. oldChildren: [],
  165. newChildren: [ new ViewElement( 'img' ) ],
  166. node: viewRoot.getChild( 0 )
  167. }
  168. ] );
  169. expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
  170. expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
  171. } );
  172. it( 'should set model selection appropriately to view selection passed in mutations event', () => {
  173. // This test case emulates spellchecker correction.
  174. const viewSelection = new ViewSelection();
  175. viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
  176. view.fire( 'mutations',
  177. [ {
  178. type: 'text',
  179. oldText: 'foobar',
  180. newText: 'foodar',
  181. node: viewRoot.getChild( 0 ).getChild( 0 )
  182. } ],
  183. viewSelection
  184. );
  185. expect( getModelData( model ) ).to.equal( '<paragraph>foodar[]</paragraph>' );
  186. expect( getViewData( view ) ).to.equal( '<p>foodar{}</p>' );
  187. } );
  188. it( 'should use up to one insert and remove operations', () => {
  189. // This test case emulates spellchecker correction.
  190. const viewSelection = new ViewSelection();
  191. viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
  192. testUtils.sinon.spy( Batch.prototype, 'weakInsert' );
  193. testUtils.sinon.spy( Batch.prototype, 'remove' );
  194. view.fire( 'mutations',
  195. [ {
  196. type: 'text',
  197. oldText: 'foobar',
  198. newText: 'fxobxr',
  199. node: viewRoot.getChild( 0 ).getChild( 0 )
  200. } ],
  201. viewSelection
  202. );
  203. expect( Batch.prototype.weakInsert.calledOnce ).to.be.true;
  204. expect( Batch.prototype.remove.calledOnce ).to.be.true;
  205. } );
  206. } );
  207. describe( 'keystroke handling', () => {
  208. it( 'should remove contents', () => {
  209. model.enqueueChanges( () => {
  210. model.selection.setRanges( [
  211. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  212. } );
  213. listenter.listenTo( view, 'keydown', () => {
  214. expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
  215. view.fire( 'mutations', [
  216. {
  217. type: 'text',
  218. oldText: 'foar',
  219. newText: 'foyar',
  220. node: viewRoot.getChild( 0 ).getChild( 0 )
  221. }
  222. ] );
  223. }, { priority: 'lowest' } );
  224. view.fire( 'keydown', { keyCode: getCode( 'y' ) } );
  225. expect( getModelData( model ) ).to.equal( '<paragraph>foy[]ar</paragraph>' );
  226. expect( getViewData( view ) ).to.equal( '<p>foy{}ar</p>' );
  227. } );
  228. it( 'should do nothing on arrow key', () => {
  229. model.enqueueChanges( () => {
  230. model.selection.setRanges( [
  231. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  232. } );
  233. view.fire( 'keydown', { keyCode: getCode( 'arrowright' ) } );
  234. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  235. } );
  236. it( 'should do nothing on ctrl combinations', () => {
  237. model.enqueueChanges( () => {
  238. model.selection.setRanges( [
  239. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  240. } );
  241. view.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
  242. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  243. } );
  244. it( 'should do nothing on non printable keys', () => {
  245. model.enqueueChanges( () => {
  246. model.selection.setRanges( [
  247. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  248. } );
  249. view.fire( 'keydown', { keyCode: 16 } ); // Shift
  250. view.fire( 'keydown', { keyCode: 35 } ); // Home
  251. view.fire( 'keydown', { keyCode: 112 } ); // F1
  252. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  253. } );
  254. // #69
  255. it( 'should do nothing on tab key', () => {
  256. model.enqueueChanges( () => {
  257. model.selection.setRanges( [
  258. ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
  259. } );
  260. view.fire( 'keydown', { keyCode: 9 } ); // Tab
  261. expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
  262. } );
  263. it( 'should do nothing if selection is collapsed', () => {
  264. view.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
  265. expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
  266. } );
  267. } );
  268. describe( 'destroy', () => {
  269. it( 'should destroy change buffer', () => {
  270. const typing = new Input( new VirtualTestEditor() );
  271. typing.init();
  272. const destroy = typing._buffer.destroy = testUtils.sinon.spy();
  273. typing.destroy();
  274. expect( destroy.calledOnce ).to.be.true;
  275. expect( typing._buffer ).to.be.null;
  276. } );
  277. } );
  278. } );