utils.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
  2. import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
  3. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  4. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  5. import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
  6. import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
  7. import { getData, parse } from '../../../../src/dev-utils/model';
  8. import transform from '../../../../src/model/operation/transform';
  9. import Position from '../../../../src/model/position';
  10. import Range from '../../../../src/model/range';
  11. import OperationFactory from '../../../../src/model/operation/operationfactory';
  12. const clients = new Set();
  13. const bufferedOperations = new Set();
  14. export class Client {
  15. constructor( name ) {
  16. this.editor = null;
  17. this.document = null;
  18. this.syncedVersion = 0;
  19. this.orderNumber = null;
  20. this.name = name;
  21. }
  22. init() {
  23. return ModelTestEditor.create( {
  24. // Typing is needed for delete command.
  25. // UndoEditing is needed for undo command.
  26. // Block plugins are needed for proper data serializing.
  27. plugins: [ Typing, Paragraph, ListEditing, UndoEditing, BlockQuoteEditing ]
  28. } ).then( editor => {
  29. this.editor = editor;
  30. this.document = editor.model.document;
  31. return this;
  32. } );
  33. }
  34. setData( initModelString ) {
  35. const model = this.editor.model;
  36. const modelRoot = model.document.getRoot();
  37. // Parse data string to model.
  38. const parsedResult = parse( initModelString, model.schema, { context: [ modelRoot.name ] } );
  39. // Retrieve DocumentFragment and Selection from parsed model.
  40. const modelDocumentFragment = parsedResult.model;
  41. const selection = parsedResult.selection;
  42. model.change( writer => {
  43. // Replace existing model in document by new one.
  44. writer.remove( Range.createIn( modelRoot ) );
  45. writer.insert( modelDocumentFragment, modelRoot );
  46. } );
  47. const ranges = [];
  48. for ( const range of selection.getRanges() ) {
  49. const start = new Position( modelRoot, range.start.path );
  50. const end = new Position( modelRoot, range.end.path );
  51. ranges.push( new Range( start, end ) );
  52. }
  53. model.document.selection._setTo( ranges );
  54. this.syncedVersion = this.document.version;
  55. }
  56. setSelection( start, end ) {
  57. if ( !end ) {
  58. end = start.slice();
  59. }
  60. const startPos = this._getPosition( start );
  61. const endPos = this._getPosition( end );
  62. this.editor.model.document.selection._setTo( new Range( startPos, endPos ) );
  63. }
  64. insert( itemString, path ) {
  65. const item = parse( itemString, this.editor.model.schema );
  66. const position = this._getPosition( path, 'start' );
  67. this._processAction( 'insert', item, position );
  68. }
  69. type( text, attributes, path ) {
  70. const position = this._getPosition( path, 'start' );
  71. this._processAction( 'insertText', text, attributes, position );
  72. }
  73. remove( start, end ) {
  74. const startPos = this._getPosition( start, 'start' );
  75. const endPos = this._getPosition( end, 'end' );
  76. this._processAction( 'remove', new Range( startPos, endPos ) );
  77. }
  78. delete() {
  79. this._processExecute( 'delete' );
  80. }
  81. move( target, start, end ) {
  82. const targetPos = this._getPosition( target );
  83. const startPos = this._getPosition( start, 'start' );
  84. const endPos = this._getPosition( end, 'end' );
  85. this._processAction( 'move', new Range( startPos, endPos ), targetPos );
  86. }
  87. rename( newName, path ) {
  88. const pos = this._getPosition( path, 'beforeParent' );
  89. const element = pos.nodeAfter;
  90. this._processAction( 'rename', element, newName );
  91. }
  92. setAttribute( key, value, start, end ) {
  93. const startPos = this._getPosition( start, 'start' );
  94. const endPos = this._getPosition( end, 'end' );
  95. this._processAction( 'setAttribute', key, value, new Range( startPos, endPos ) );
  96. }
  97. removeAttribute( key, start, end ) {
  98. const startPos = this._getPosition( start, 'start' );
  99. const endPos = this._getPosition( end, 'end' );
  100. this._processAction( 'removeAttribute', key, new Range( startPos, endPos ) );
  101. }
  102. setMarker( name, start, end ) {
  103. let actionName;
  104. const startPos = this._getPosition( start, 'start' );
  105. const endPos = this._getPosition( end, 'end' );
  106. if ( this.editor.model.markers.has( name ) ) {
  107. actionName = 'updateMarker';
  108. } else {
  109. actionName = 'addMarker';
  110. }
  111. const range = new Range( startPos, endPos );
  112. this._processAction( actionName, name, { range, usingOperation: true } );
  113. }
  114. removeMarker( name ) {
  115. this._processAction( 'removeMarker', name );
  116. }
  117. wrap( elementName, start, end ) {
  118. const startPos = this._getPosition( start, 'start' );
  119. const endPos = this._getPosition( end, 'end' );
  120. this._processAction( 'wrap', new Range( startPos, endPos ), elementName );
  121. }
  122. unwrap( path ) {
  123. const pos = this._getPosition( path, 'beforeParent' );
  124. const element = pos.nodeAfter;
  125. this._processAction( 'unwrap', element );
  126. }
  127. merge( path ) {
  128. const pos = this._getPosition( path, 'start' );
  129. this._processAction( 'merge', pos );
  130. }
  131. split( path ) {
  132. const pos = this._getPosition( path, 'start' );
  133. this._processAction( 'split', pos );
  134. }
  135. undo() {
  136. this._processExecute( 'undo' );
  137. }
  138. _processExecute( commandName, commandArgs ) {
  139. const oldVersion = this.document.version;
  140. this.editor.execute( commandName, commandArgs );
  141. const operations = this.document.history.getOperations( oldVersion );
  142. bufferOperations( Array.from( operations ), this );
  143. }
  144. _getPosition( path, type ) {
  145. if ( !path ) {
  146. return this._getPositionFromSelection( type );
  147. }
  148. return new Position( this.document.getRoot(), path );
  149. }
  150. _getPositionFromSelection( type ) {
  151. const selRange = this.editor.model.document.selection.getFirstRange();
  152. switch ( type ) {
  153. default:
  154. case 'start':
  155. return Position.createFromPosition( selRange.start );
  156. case 'end':
  157. return Position.createFromPosition( selRange.end );
  158. case 'beforeParent':
  159. return Position.createBefore( selRange.start.parent );
  160. }
  161. }
  162. getModelString() {
  163. return getData( this.editor.model, { withoutSelection: true, convertMarkers: true } );
  164. }
  165. destroy() {
  166. clients.delete( this );
  167. return this.editor.destroy();
  168. }
  169. _processAction( name, ...args ) {
  170. const oldVersion = this.document.version;
  171. this.editor.model.change( writer => {
  172. writer[ name ]( ...args );
  173. } );
  174. const operations = Array.from( this.document.history.getOperations( oldVersion ) );
  175. bufferOperations( operations, this );
  176. }
  177. static get( clientName ) {
  178. const client = new Client( clientName );
  179. client.orderNumber = clients.size;
  180. clients.add( client );
  181. return client.init();
  182. }
  183. }
  184. function bufferOperations( operations, client ) {
  185. bufferedOperations.add( { operations: operations.map( operation => JSON.stringify( operation ) ), client } );
  186. }
  187. export function syncClients() {
  188. const clientsOperations = {};
  189. // For each client, flatten all buffered operations into one set.
  190. for ( const item of bufferedOperations ) {
  191. const name = item.client.name;
  192. if ( !clientsOperations[ name ] ) {
  193. clientsOperations[ name ] = [];
  194. }
  195. clientsOperations[ name ].push( ...item.operations );
  196. }
  197. for ( const localClient of clients ) {
  198. for ( const remoteClient of clients ) {
  199. if ( remoteClient == localClient ) {
  200. continue;
  201. }
  202. const remoteOperationsJson = clientsOperations[ remoteClient.name ];
  203. if ( !remoteOperationsJson ) {
  204. continue;
  205. }
  206. const remoteOperations = remoteOperationsJson.map( op => OperationFactory.fromJSON( JSON.parse( op ), localClient.document ) );
  207. const localOperations = Array.from( localClient.document.history.getOperations( localClient.syncedVersion ) );
  208. let remoteOperationsTransformed = null;
  209. const options = {
  210. useContext: false,
  211. padWithNoOps: true
  212. };
  213. if ( localClient.orderNumber < remoteClient.orderNumber ) {
  214. remoteOperationsTransformed = transform.transformSets( localOperations, remoteOperations, options ).operationsB;
  215. } else {
  216. remoteOperationsTransformed = transform.transformSets( remoteOperations, localOperations, options ).operationsA;
  217. }
  218. localClient.editor.model.enqueueChange( 'transparent', writer => {
  219. for ( const operation of remoteOperationsTransformed ) {
  220. writer.batch.addOperation( operation );
  221. localClient.editor.model.applyOperation( operation );
  222. }
  223. } );
  224. }
  225. localClient.syncedVersion = localClient.document.version;
  226. }
  227. bufferedOperations.clear();
  228. }
  229. export function expectClients( expectedModelString ) {
  230. for ( const client of clients ) {
  231. expect( client.getModelString(), client.name + ' content' ).to.equal( expectedModelString );
  232. }
  233. let syncedVersion = null;
  234. for ( const client of clients ) {
  235. if ( syncedVersion === null ) {
  236. syncedVersion = client.syncedVersion;
  237. continue;
  238. }
  239. expect( client.syncedVersion, client.name + ' version' ).to.equal( syncedVersion );
  240. }
  241. }