8
0

writer.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Document from '../../src/model/document';
  6. import DocumentFragment from '../../src/model/documentfragment';
  7. import Element from '../../src/model/element';
  8. import Text from '../../src/model/text';
  9. import TextProxy from '../../src/model/textproxy';
  10. import Position from '../../src/model/position';
  11. import Range from '../../src/model/range';
  12. import writer from '../../src/model/writer';
  13. import { getData, stringify } from '../../src/dev-utils/model';
  14. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  15. import log from '@ckeditor/ckeditor5-utils/src/log';
  16. let doc, root;
  17. describe( 'writer', () => {
  18. beforeEach( () => {
  19. doc = new Document();
  20. doc.schema.allow( { name: '$text', inside: '$root' } );
  21. root = doc.createRoot();
  22. // index: 0001112333
  23. // offset: 0123456789
  24. // data: foobarIxyz
  25. // bold: ___BBBB___
  26. root.appendChildren( [
  27. new Text( 'foo' ),
  28. new Text( 'bar', { bold: true } ),
  29. new Element( 'image', { src: 'img.jpg' } ),
  30. new Text( 'xyz' )
  31. ] );
  32. } );
  33. describe( 'insert', () => {
  34. it( 'should insert nodes between nodes', () => {
  35. writer.insert( Position.createAt( root, 3 ), [ 'xxx', new Element( 'p' ) ] );
  36. expectData( 'fooxxx<p></p><$text bold="true">bar</$text><image src="img.jpg"></image>xyz' );
  37. } );
  38. it( 'should split text node if nodes at inserted at offset inside text node', () => {
  39. writer.insert( Position.createAt( root, 5 ), new Element( 'p' ) );
  40. expectData( 'foo<$text bold="true">ba</$text><p></p><$text bold="true">r</$text><image src="img.jpg"></image>xyz' );
  41. } );
  42. it( 'should merge text nodes if possible', () => {
  43. writer.insert( Position.createAt( root, 3 ), new Text( 'xxx', { bold: true } ) );
  44. expectData( 'foo<$text bold="true">xxxbar</$text><image src="img.jpg"></image>xyz' );
  45. } );
  46. it( 'should transfer markers from given node to the root element of target position', () => {
  47. root.appendChildren( [ new Element( 'div' ) ] );
  48. const docFrag = new DocumentFragment( [
  49. new Element( 'paragraph', null, [
  50. new Text( 'foo bar' ),
  51. ] )
  52. ] );
  53. docFrag.markers.set( 'foo', new Range( new Position( docFrag, [ 0, 2 ] ), new Position( docFrag, [ 0, 6 ] ) ) );
  54. writer.insert( new Position( root, [ 10, 0 ] ), docFrag );
  55. expect( stringify( root, doc.markers.get( 'foo' ).getRange() ) ).to.equal(
  56. 'foo<$text bold="true">bar</$text><image src="img.jpg"></image>xyz<div><paragraph>fo[o ba]r</paragraph></div>'
  57. );
  58. } );
  59. it( 'should transfer markers from given node to the root element of target position when root is a documentFragment', () => {
  60. const targetDocFrag = new DocumentFragment( [ new Element( 'div' ) ] );
  61. const docFrag = new DocumentFragment( [
  62. new Element( 'paragraph', null, [
  63. new Text( 'foo bar' ),
  64. ] )
  65. ] );
  66. docFrag.markers.set( 'foo', new Range( new Position( docFrag, [ 0, 2 ] ), new Position( docFrag, [ 0, 6 ] ) ) );
  67. writer.insert( new Position( targetDocFrag, [ 0, 0 ] ), docFrag );
  68. expect( stringify( targetDocFrag, targetDocFrag.markers.get( 'foo' ) ) )
  69. .to.equal( '<div><paragraph>fo[o ba]r</paragraph></div>' );
  70. } );
  71. it( 'should log warning when element with markers is set to the element without markers collection', () => {
  72. const warnSpy = sinon.spy( log, 'warn' );
  73. const target = new Element( 'div' );
  74. const docFrag = new DocumentFragment( [
  75. new Element( 'paragraph', null, [
  76. new Text( 'foo bar' ),
  77. ] )
  78. ] );
  79. const range = new Range( new Position( docFrag, [ 1, 2 ] ), new Position( docFrag, [ 1, 4 ] ) );
  80. docFrag.markers.set( 'foo', range );
  81. writer.insert( new Position( target, [ 0 ] ), docFrag );
  82. expect( warnSpy.calledWithExactly(
  83. 'model-writer-insert-lose-markers: Element containing markers is set to element without MarkersCollection.'
  84. ) ).to.true;
  85. } );
  86. } );
  87. describe( 'remove', () => {
  88. it( 'should remove nodes in given range', () => {
  89. const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
  90. writer.remove( range );
  91. expectData( 'foo<image src="img.jpg"></image>xyz' );
  92. } );
  93. it( 'should split text node if range starts or ends inside text node', () => {
  94. const range = Range.createFromParentsAndOffsets( root, 1, root, 5 );
  95. writer.remove( range );
  96. expectData( 'f<$text bold="true">r</$text><image src="img.jpg"></image>xyz' );
  97. } );
  98. it( 'should merge text nodes if possible', () => {
  99. const range = Range.createFromParentsAndOffsets( root, 3, root, 7 );
  100. writer.remove( range );
  101. expectData( 'fooxyz' );
  102. expect( root.childCount ).to.equal( 1 );
  103. } );
  104. it( 'should throw if given range is not flat', () => {
  105. expect( () => {
  106. writer.remove( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1, 2 ] ) ) );
  107. } ).to.throw( CKEditorError, /model-writer-remove-range-not-flat/ );
  108. } );
  109. } );
  110. describe( 'move', () => {
  111. it( 'should move a range of nodes', () => {
  112. const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
  113. writer.move( range, Position.createAt( root, 0 ) );
  114. expectData( '<$text bold="true">bar</$text>foo<image src="img.jpg"></image>xyz' );
  115. } );
  116. it( 'should use remove and insert methods', () => {
  117. sinon.spy( writer, 'remove' );
  118. sinon.spy( writer, 'insert' );
  119. const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
  120. const position = Position.createAt( root, 0 );
  121. writer.move( range, position );
  122. expect( writer.remove.calledWithExactly( range ) ).to.be.true;
  123. expect( writer.insert.calledWith( position ) ).to.be.true;
  124. } );
  125. it( 'should correctly move if target position is in same element as moved range, but after range', () => {
  126. const range = Range.createFromParentsAndOffsets( root, 3, root, 6 );
  127. writer.move( range, Position.createAt( root, 10 ) );
  128. expectData( 'foo<image src="img.jpg"></image>xyz<$text bold="true">bar</$text>' );
  129. } );
  130. it( 'should throw if given range is not flat', () => {
  131. expect( () => {
  132. writer.move( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1, 2 ] ) ), null );
  133. } ).to.throw( CKEditorError, /model-writer-move-range-not-flat/ );
  134. } );
  135. } );
  136. describe( 'setAttribute', () => {
  137. it( 'should set attribute on given range of nodes', () => {
  138. const range = Range.createFromParentsAndOffsets( root, 6, root, 8 );
  139. writer.setAttribute( range, 'newAttr', true );
  140. expectData( 'foo<$text bold="true">bar</$text><image newAttr="true" src="img.jpg"></image><$text newAttr="true">x</$text>yz' );
  141. } );
  142. it( 'should remove attribute if null was passed as a value', () => {
  143. const range = Range.createFromParentsAndOffsets( root, 6, root, 7 );
  144. writer.setAttribute( range, 'src', null );
  145. expectData( 'foo<$text bold="true">bar</$text><image></image>xyz' );
  146. } );
  147. it( 'should merge nodes if possible', () => {
  148. const range = Range.createFromParentsAndOffsets( root, 0, root, 3 );
  149. writer.setAttribute( range, 'bold', true );
  150. expectData( '<$text bold="true">foobar</$text><image src="img.jpg"></image>xyz' );
  151. } );
  152. } );
  153. describe( 'removeAttribute', () => {
  154. it( 'should use setAttribute', () => {
  155. sinon.spy( writer, 'setAttribute' );
  156. const range = Range.createFromParentsAndOffsets( root, 6, root, 7 );
  157. writer.removeAttribute( range, 'src' );
  158. expect( writer.setAttribute.calledWithExactly( range, 'src', null ) ).to.be.true;
  159. } );
  160. } );
  161. } );
  162. describe( 'normalizeNodes', () => {
  163. it( 'should change single object into an array', () => {
  164. const p = new Element( 'p' );
  165. expect( writer.normalizeNodes( p ) ).to.deep.equal( [ p ] );
  166. } );
  167. it( 'should change strings to text nodes', () => {
  168. const text = writer.normalizeNodes( 'abc' )[ 0 ];
  169. expect( text ).to.be.instanceof( Text );
  170. expect( text.data ).to.equal( 'abc' );
  171. } );
  172. it( 'should change text proxies to text nodes', () => {
  173. const textNode = new Text( 'abc' );
  174. const textProxy = new TextProxy( textNode, 1, 1 );
  175. const text = writer.normalizeNodes( textProxy )[ 0 ];
  176. expect( text ).to.be.instanceof( Text );
  177. expect( text.data ).to.equal( 'b' );
  178. } );
  179. it( 'should not change elements', () => {
  180. const p = new Element( 'p' );
  181. expect( writer.normalizeNodes( p )[ 0 ] ).to.equal( p );
  182. } );
  183. it( 'should omit unrecognized objects', () => {
  184. expect( writer.normalizeNodes( 1 ) ).to.deep.equal( [] );
  185. } );
  186. it( 'should accept arrays', () => {
  187. const text = new Text( 'foo', { bold: true } );
  188. const image = new Element( 'image' );
  189. const nodes = [ 'abc', text, image, 1, 'xyz' ];
  190. const normalized = writer.normalizeNodes( nodes );
  191. expect( normalized[ 0 ] ).to.be.instanceof( Text );
  192. expect( normalized[ 1 ] ).to.equal( text );
  193. expect( normalized[ 2 ] ).to.equal( image );
  194. expect( normalized[ 3 ] ).to.be.instanceof( Text );
  195. } );
  196. it( 'should merge text nodes if mergeTextNodes flag is set to true', () => {
  197. const normalized = writer.normalizeNodes( [ 'foo', 'bar' ], true );
  198. expect( normalized.length ).to.equal( 1 );
  199. expect( normalized[ 0 ].data ).to.equal( 'foobar' );
  200. } );
  201. it( 'should replace document fragment by the list of it\'s children', () => {
  202. const nodes = [
  203. new Text( 'foo', { bold: true } ),
  204. new DocumentFragment( [ new Text( 'bar', { bold: true } ), new Element( 'image' ) ] ),
  205. 'xyz'
  206. ];
  207. const normalized = writer.normalizeNodes( nodes, true );
  208. expect( normalized[ 0 ] ).to.be.instanceof( Text );
  209. expect( normalized[ 0 ].getAttribute( 'bold' ) ).to.be.true;
  210. expect( normalized[ 0 ].data ).to.equal( 'foobar' );
  211. expect( normalized[ 1 ].name ).to.equal( 'image' );
  212. expect( normalized[ 2 ].data ).to.equal( 'xyz' );
  213. } );
  214. } );
  215. function expectData( html ) {
  216. expect( getData( doc, { withoutSelection: true } ) ).to.equal( html );
  217. }