UNPKG

12.9 kBJavaScriptView Raw
1import window from '../window';
2import * as util from '../util';
3import Collection from '../collection';
4import * as is from '../is';
5import Promise from '../promise';
6
7import addRemove from './add-remove';
8import animation from './animation';
9import events from './events';
10import exportFormat from './export';
11import layout from './layout';
12import notification from './notification';
13import renderer from './renderer';
14import search from './search';
15import style from './style';
16import viewport from './viewport';
17import data from './data';
18
19let Core = function( opts ){
20 let cy = this;
21
22 opts = util.extend( {}, opts );
23
24 let container = opts.container;
25
26 // allow for passing a wrapped jquery object
27 // e.g. cytoscape({ container: $('#cy') })
28 if( container && !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
29 container = container[0];
30 }
31
32 let reg = container ? container._cyreg : null; // e.g. already registered some info (e.g. readies) via jquery
33 reg = reg || {};
34
35 if( reg && reg.cy ){
36 reg.cy.destroy();
37
38 reg = {}; // old instance => replace reg completely
39 }
40
41 let readies = reg.readies = reg.readies || [];
42
43 if( container ){ container._cyreg = reg; } // make sure container assoc'd reg points to this cy
44 reg.cy = cy;
45
46 let head = window !== undefined && container !== undefined && !opts.headless;
47 let options = opts;
48 options.layout = util.extend( { name: head ? 'grid' : 'null' }, options.layout );
49 options.renderer = util.extend( { name: head ? 'canvas' : 'null' }, options.renderer );
50
51 let defVal = function( def, val, altVal ){
52 if( val !== undefined ){
53 return val;
54 } else if( altVal !== undefined ){
55 return altVal;
56 } else {
57 return def;
58 }
59 };
60
61 let _p = this._private = {
62 container: container, // html dom ele container
63 ready: false, // whether ready has been triggered
64 options: options, // cached options
65 elements: new Collection( this ), // elements in the graph
66 listeners: [], // list of listeners
67 aniEles: new Collection( this ), // elements being animated
68 data: {}, // data for the core
69 scratch: {}, // scratch object for core
70 layout: null,
71 renderer: null,
72 destroyed: false, // whether destroy was called
73 notificationsEnabled: true, // whether notifications are sent to the renderer
74 minZoom: 1e-50,
75 maxZoom: 1e50,
76 zoomingEnabled: defVal( true, options.zoomingEnabled ),
77 userZoomingEnabled: defVal( true, options.userZoomingEnabled ),
78 panningEnabled: defVal( true, options.panningEnabled ),
79 userPanningEnabled: defVal( true, options.userPanningEnabled ),
80 boxSelectionEnabled: defVal( true, options.boxSelectionEnabled ),
81 autolock: defVal( false, options.autolock, options.autolockNodes ),
82 autoungrabify: defVal( false, options.autoungrabify, options.autoungrabifyNodes ),
83 autounselectify: defVal( false, options.autounselectify ),
84 styleEnabled: options.styleEnabled === undefined ? head : options.styleEnabled,
85 zoom: is.number( options.zoom ) ? options.zoom : 1,
86 pan: {
87 x: is.plainObject( options.pan ) && is.number( options.pan.x ) ? options.pan.x : 0,
88 y: is.plainObject( options.pan ) && is.number( options.pan.y ) ? options.pan.y : 0
89 },
90 animation: { // object for currently-running animations
91 current: [],
92 queue: []
93 },
94 hasCompoundNodes: false
95 };
96
97 this.createEmitter();
98
99 // set selection type
100 this.selectionType( options.selectionType );
101
102 // init zoom bounds
103 this.zoomRange({ min: options.minZoom, max: options.maxZoom });
104
105 let loadExtData = function( extData, next ){
106 let anyIsPromise = extData.some( is.promise );
107
108 if( anyIsPromise ){
109 return Promise.all( extData ).then( next ); // load all data asynchronously, then exec rest of init
110 } else {
111 next( extData ); // exec synchronously for convenience
112 }
113 };
114
115 // start with the default stylesheet so we have something before loading an external stylesheet
116 if( _p.styleEnabled ){
117 cy.setStyle([]);
118 }
119
120 // create the renderer
121 let rendererOptions = util.assign({}, options, options.renderer); // allow rendering hints in top level options
122 cy.initRenderer( rendererOptions );
123
124 let setElesAndLayout = function( elements, onload, ondone ){
125 cy.notifications( false );
126
127 // remove old elements
128 let oldEles = cy.mutableElements();
129 if( oldEles.length > 0 ){
130 oldEles.remove();
131 }
132
133 if( elements != null ){
134 if( is.plainObject( elements ) || is.array( elements ) ){
135 cy.add( elements );
136 }
137 }
138
139 cy.one( 'layoutready', function( e ){
140 cy.notifications( true );
141 cy.emit( e ); // we missed this event by turning notifications off, so pass it on
142
143 cy.one( 'load', onload );
144 cy.emitAndNotify( 'load' );
145 } ).one( 'layoutstop', function(){
146 cy.one( 'done', ondone );
147 cy.emit( 'done' );
148 } );
149
150 let layoutOpts = util.extend( {}, cy._private.options.layout );
151 layoutOpts.eles = cy.elements();
152
153 cy.layout( layoutOpts ).run();
154 };
155
156 loadExtData([ options.style, options.elements ], function( thens ){
157 let initStyle = thens[0];
158 let initEles = thens[1];
159
160 // init style
161 if( _p.styleEnabled ){
162 cy.style().append( initStyle );
163 }
164
165 // initial load
166 setElesAndLayout( initEles, function(){ // onready
167 cy.startAnimationLoop();
168 _p.ready = true;
169
170 // if a ready callback is specified as an option, the bind it
171 if( is.fn( options.ready ) ){
172 cy.on( 'ready', options.ready );
173 }
174
175 // bind all the ready handlers registered before creating this instance
176 for( let i = 0; i < readies.length; i++ ){
177 let fn = readies[ i ];
178 cy.on( 'ready', fn );
179 }
180 if( reg ){ reg.readies = []; } // clear b/c we've bound them all and don't want to keep it around in case a new core uses the same div etc
181
182 cy.emit( 'ready' );
183 }, options.done );
184
185 } );
186};
187
188let corefn = Core.prototype; // short alias
189
190util.extend( corefn, {
191 instanceString: function(){
192 return 'core';
193 },
194
195 isReady: function(){
196 return this._private.ready;
197 },
198
199 destroyed: function(){
200 return this._private.destroyed;
201 },
202
203 ready: function( fn ){
204 if( this.isReady() ){
205 this.emitter().emit( 'ready', [], fn ); // just calls fn as though triggered via ready event
206 } else {
207 this.on( 'ready', fn );
208 }
209
210 return this;
211 },
212
213 destroy: function(){
214 let cy = this;
215 if( cy.destroyed() ) return;
216
217 cy.stopAnimationLoop();
218
219 cy.destroyRenderer();
220
221 this.emit( 'destroy' );
222
223 cy._private.destroyed = true;
224
225 return cy;
226 },
227
228 hasElementWithId: function( id ){
229 return this._private.elements.hasElementWithId( id );
230 },
231
232 getElementById: function( id ){
233 return this._private.elements.getElementById( id );
234 },
235
236 hasCompoundNodes: function(){
237 return this._private.hasCompoundNodes;
238 },
239
240 headless: function(){
241 return this._private.renderer.isHeadless();
242 },
243
244 styleEnabled: function(){
245 return this._private.styleEnabled;
246 },
247
248 addToPool: function( eles ){
249 this._private.elements.merge( eles );
250
251 return this; // chaining
252 },
253
254 removeFromPool: function( eles ){
255 this._private.elements.unmerge( eles );
256
257 return this;
258 },
259
260 container: function(){
261 return this._private.container || null;
262 },
263
264 mount: function( container ){
265 if( container == null ){ return; }
266
267 let cy = this;
268 let _p = cy._private;
269 let options = _p.options;
270
271 if( !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
272 container = container[0];
273 }
274
275 cy.stopAnimationLoop();
276
277 cy.destroyRenderer();
278
279 _p.container = container;
280 _p.styleEnabled = true;
281
282 cy.invalidateSize();
283
284 cy.initRenderer( util.assign({}, options, options.renderer, {
285 // allow custom renderer name to be re-used, otherwise use canvas
286 name: options.renderer.name === 'null' ? 'canvas' : options.renderer.name
287 }) );
288
289 cy.startAnimationLoop();
290
291 cy.style( options.style );
292
293 cy.emit( 'mount' );
294
295 return cy;
296 },
297
298 unmount: function(){
299 let cy = this;
300
301 cy.stopAnimationLoop();
302
303 cy.destroyRenderer();
304
305 cy.initRenderer( { name: 'null' } );
306
307 cy.emit( 'unmount' );
308
309 return cy;
310 },
311
312 options: function(){
313 return util.copy( this._private.options );
314 },
315
316 json: function( obj ){
317 let cy = this;
318 let _p = cy._private;
319 let eles = cy.mutableElements();
320 let getFreshRef = ele => cy.getElementById(ele.id());
321
322 if( is.plainObject( obj ) ){ // set
323
324 cy.startBatch();
325
326 if( obj.elements ){
327 let idInJson = {};
328
329 let updateEles = function( jsons, gr ){
330 let toAdd = [];
331 let toMod = [];
332
333 for( let i = 0; i < jsons.length; i++ ){
334 let json = jsons[ i ];
335 let id = '' + json.data.id; // id must be string
336 let ele = cy.getElementById( id );
337
338 idInJson[ id ] = true;
339
340 if( ele.length !== 0 ){ // existing element should be updated
341 toMod.push({ ele, json });
342 } else { // otherwise should be added
343 if( gr ){
344 json.group = gr;
345
346 toAdd.push( json );
347 } else {
348 toAdd.push( json );
349 }
350 }
351 }
352
353 cy.add( toAdd );
354
355 for( let i = 0; i < toMod.length; i++ ){
356 let { ele, json } = toMod[i];
357
358 ele.json(json);
359 }
360 };
361
362 if( is.array( obj.elements ) ){ // elements: []
363 updateEles( obj.elements );
364
365 } else { // elements: { nodes: [], edges: [] }
366 let grs = [ 'nodes', 'edges' ];
367 for( let i = 0; i < grs.length; i++ ){
368 let gr = grs[ i ];
369 let elements = obj.elements[ gr ];
370
371 if( is.array( elements ) ){
372 updateEles( elements, gr );
373 }
374 }
375 }
376
377 let parentsToRemove = cy.collection();
378
379 (eles
380 .filter(ele => !idInJson[ ele.id() ])
381 .forEach(ele => {
382 if ( ele.isParent() ) {
383 parentsToRemove.merge(ele);
384 } else {
385 ele.remove();
386 }
387 })
388 );
389
390 // so that children are not removed w/parent
391 parentsToRemove.forEach(ele => ele.children().move({ parent: null }));
392
393 // intermediate parents may be moved by prior line, so make sure we remove by fresh refs
394 parentsToRemove.forEach(ele => getFreshRef(ele).remove());
395 }
396
397 if( obj.style ){
398 cy.style( obj.style );
399 }
400
401 if( obj.zoom != null && obj.zoom !== _p.zoom ){
402 cy.zoom( obj.zoom );
403 }
404
405 if( obj.pan ){
406 if( obj.pan.x !== _p.pan.x || obj.pan.y !== _p.pan.y ){
407 cy.pan( obj.pan );
408 }
409 }
410
411 if( obj.data ){
412 cy.data( obj.data );
413 }
414
415 let fields = [
416 'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled',
417 'panningEnabled', 'userPanningEnabled',
418 'boxSelectionEnabled',
419 'autolock', 'autoungrabify', 'autounselectify'
420 ];
421
422 for( let i = 0; i < fields.length; i++ ){
423 let f = fields[ i ];
424
425 if( obj[ f ] != null ){
426 cy[ f ]( obj[ f ] );
427 }
428 }
429
430 cy.endBatch();
431
432 return this; // chaining
433 } else { // get
434 let flat = !!obj;
435 let json = {};
436
437 if( flat ){
438 json.elements = this.elements().map( ele => ele.json() );
439 } else {
440 json.elements = {};
441
442 eles.forEach( function( ele ){
443 let group = ele.group();
444
445 if( !json.elements[ group ] ){
446 json.elements[ group ] = [];
447 }
448
449 json.elements[ group ].push( ele.json() );
450 } );
451 }
452
453 if( this._private.styleEnabled ){
454 json.style = cy.style().json();
455 }
456
457 json.data = util.copy( cy.data() );
458
459 let options = _p.options;
460
461 json.zoomingEnabled = _p.zoomingEnabled;
462 json.userZoomingEnabled = _p.userZoomingEnabled;
463 json.zoom = _p.zoom;
464 json.minZoom = _p.minZoom;
465 json.maxZoom = _p.maxZoom;
466 json.panningEnabled = _p.panningEnabled;
467 json.userPanningEnabled = _p.userPanningEnabled;
468 json.pan = util.copy( _p.pan );
469 json.boxSelectionEnabled = _p.boxSelectionEnabled;
470 json.renderer = util.copy( options.renderer );
471 json.hideEdgesOnViewport = options.hideEdgesOnViewport;
472 json.textureOnViewport = options.textureOnViewport;
473 json.wheelSensitivity = options.wheelSensitivity;
474 json.motionBlur = options.motionBlur;
475
476 return json;
477 }
478 }
479
480} );
481
482corefn.$id = corefn.getElementById;
483
484[
485 addRemove,
486 animation,
487 events,
488 exportFormat,
489 layout,
490 notification,
491 renderer,
492 search,
493 style,
494 viewport,
495 data
496].forEach( function( props ){
497 util.extend( corefn, props );
498} );
499
500export default Core;