typing.js 8.5 KB

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