UNPKG

13.3 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: options.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 multiClickDebounceTime: defVal(250, options.multiClickDebounceTime)
96 };
97
98 this.createEmitter();
99
100 // set selection type
101 this.selectionType( options.selectionType );
102
103 // init zoom bounds
104 this.zoomRange({ min: options.minZoom, max: options.maxZoom });
105
106 let loadExtData = function( extData, next ){
107 let anyIsPromise = extData.some( is.promise );
108
109 if( anyIsPromise ){
110 return Promise.all( extData ).then( next ); // load all data asynchronously, then exec rest of init
111 } else {
112 next( extData ); // exec synchronously for convenience
113 }
114 };
115
116 // start with the default stylesheet so we have something before loading an external stylesheet
117 if( _p.styleEnabled ){
118 cy.setStyle([]);
119 }
120
121 // create the renderer
122 let rendererOptions = util.assign({}, options, options.renderer); // allow rendering hints in top level options
123 cy.initRenderer( rendererOptions );
124
125 let setElesAndLayout = function( elements, onload, ondone ){
126 cy.notifications( false );
127
128 // remove old elements
129 let oldEles = cy.mutableElements();
130 if( oldEles.length > 0 ){
131 oldEles.remove();
132 }
133
134 if( elements != null ){
135 if( is.plainObject( elements ) || is.array( elements ) ){
136 cy.add( elements );
137 }
138 }
139
140 cy.one( 'layoutready', function( e ){
141 cy.notifications( true );
142 cy.emit( e ); // we missed this event by turning notifications off, so pass it on
143
144 cy.one( 'load', onload );
145 cy.emitAndNotify( 'load' );
146 } ).one( 'layoutstop', function(){
147 cy.one( 'done', ondone );
148 cy.emit( 'done' );
149 } );
150
151 let layoutOpts = util.extend( {}, cy._private.options.layout );
152 layoutOpts.eles = cy.elements();
153
154 cy.layout( layoutOpts ).run();
155 };
156
157 loadExtData([ options.style, options.elements ], function( thens ){
158 let initStyle = thens[0];
159 let initEles = thens[1];
160
161 // init style
162 if( _p.styleEnabled ){
163 cy.style().append( initStyle );
164 }
165
166 // initial load
167 setElesAndLayout( initEles, function(){ // onready
168 cy.startAnimationLoop();
169 _p.ready = true;
170
171 // if a ready callback is specified as an option, the bind it
172 if( is.fn( options.ready ) ){
173 cy.on( 'ready', options.ready );
174 }
175
176 // bind all the ready handlers registered before creating this instance
177 for( let i = 0; i < readies.length; i++ ){
178 let fn = readies[ i ];
179 cy.on( 'ready', fn );
180 }
181 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
182
183 cy.emit( 'ready' );
184 }, options.done );
185
186 } );
187};
188
189let corefn = Core.prototype; // short alias
190
191util.extend( corefn, {
192 instanceString: function(){
193 return 'core';
194 },
195
196 isReady: function(){
197 return this._private.ready;
198 },
199
200 destroyed: function(){
201 return this._private.destroyed;
202 },
203
204 ready: function( fn ){
205 if( this.isReady() ){
206 this.emitter().emit( 'ready', [], fn ); // just calls fn as though triggered via ready event
207 } else {
208 this.on( 'ready', fn );
209 }
210
211 return this;
212 },
213
214 destroy: function(){
215 let cy = this;
216 if( cy.destroyed() ) return;
217
218 cy.stopAnimationLoop();
219
220 cy.destroyRenderer();
221
222 this.emit( 'destroy' );
223
224 cy._private.destroyed = true;
225
226 return cy;
227 },
228
229 hasElementWithId: function( id ){
230 return this._private.elements.hasElementWithId( id );
231 },
232
233 getElementById: function( id ){
234 return this._private.elements.getElementById( id );
235 },
236
237 hasCompoundNodes: function(){
238 return this._private.hasCompoundNodes;
239 },
240
241 headless: function(){
242 return this._private.renderer.isHeadless();
243 },
244
245 styleEnabled: function(){
246 return this._private.styleEnabled;
247 },
248
249 addToPool: function( eles ){
250 this._private.elements.merge( eles );
251
252 return this; // chaining
253 },
254
255 removeFromPool: function( eles ){
256 this._private.elements.unmerge( eles );
257
258 return this;
259 },
260
261 container: function(){
262 return this._private.container || null;
263 },
264
265 mount: function( container ){
266 if( container == null ){ return; }
267
268 let cy = this;
269 let _p = cy._private;
270 let options = _p.options;
271
272 if( !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
273 container = container[0];
274 }
275
276 cy.stopAnimationLoop();
277
278 cy.destroyRenderer();
279
280 _p.container = container;
281 _p.styleEnabled = true;
282
283 cy.invalidateSize();
284
285 cy.initRenderer( util.assign({}, options, options.renderer, {
286 // allow custom renderer name to be re-used, otherwise use canvas
287 name: options.renderer.name === 'null' ? 'canvas' : options.renderer.name
288 }) );
289
290 cy.startAnimationLoop();
291
292 cy.style( options.style );
293
294 cy.emit( 'mount' );
295
296 return cy;
297 },
298
299 unmount: function(){
300 let cy = this;
301
302 cy.stopAnimationLoop();
303
304 cy.destroyRenderer();
305
306 cy.initRenderer( { name: 'null' } );
307
308 cy.emit( 'unmount' );
309
310 return cy;
311 },
312
313 options: function(){
314 return util.copy( this._private.options );
315 },
316
317 json: function( obj ){
318 let cy = this;
319 let _p = cy._private;
320 let eles = cy.mutableElements();
321 let getFreshRef = ele => cy.getElementById(ele.id());
322
323 if( is.plainObject( obj ) ){ // set
324
325 cy.startBatch();
326
327 if( obj.elements ){
328 let idInJson = {};
329
330 let updateEles = function( jsons, gr ){
331 let toAdd = [];
332 let toMod = [];
333
334 for( let i = 0; i < jsons.length; i++ ){
335 let json = jsons[ i ];
336
337 if( !json.data.id ){
338 util.warn( 'cy.json() cannot handle elements without an ID attribute' );
339 continue;
340 }
341
342 let id = '' + json.data.id; // id must be string
343 let ele = cy.getElementById( id );
344
345 idInJson[ id ] = true;
346
347 if( ele.length !== 0 ){ // existing element should be updated
348 toMod.push({ ele, json });
349 } else { // otherwise should be added
350 if( gr ){
351 json.group = gr;
352
353 toAdd.push( json );
354 } else {
355 toAdd.push( json );
356 }
357 }
358 }
359
360 cy.add( toAdd );
361
362 for( let i = 0; i < toMod.length; i++ ){
363 let { ele, json } = toMod[i];
364
365 ele.json(json);
366 }
367 };
368
369 if( is.array( obj.elements ) ){ // elements: []
370 updateEles( obj.elements );
371
372 } else { // elements: { nodes: [], edges: [] }
373 let grs = [ 'nodes', 'edges' ];
374 for( let i = 0; i < grs.length; i++ ){
375 let gr = grs[ i ];
376 let elements = obj.elements[ gr ];
377
378 if( is.array( elements ) ){
379 updateEles( elements, gr );
380 }
381 }
382 }
383
384 let parentsToRemove = cy.collection();
385
386 (eles
387 .filter(ele => !idInJson[ ele.id() ])
388 .forEach(ele => {
389 if ( ele.isParent() ) {
390 parentsToRemove.merge(ele);
391 } else {
392 ele.remove();
393 }
394 })
395 );
396
397 // so that children are not removed w/parent
398 parentsToRemove.forEach(ele => ele.children().move({ parent: null }));
399
400 // intermediate parents may be moved by prior line, so make sure we remove by fresh refs
401 parentsToRemove.forEach(ele => getFreshRef(ele).remove());
402 }
403
404 if( obj.style ){
405 cy.style( obj.style );
406 }
407
408 if( obj.zoom != null && obj.zoom !== _p.zoom ){
409 cy.zoom( obj.zoom );
410 }
411
412 if( obj.pan ){
413 if( obj.pan.x !== _p.pan.x || obj.pan.y !== _p.pan.y ){
414 cy.pan( obj.pan );
415 }
416 }
417
418 if( obj.data ){
419 cy.data( obj.data );
420 }
421
422 let fields = [
423 'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled',
424 'panningEnabled', 'userPanningEnabled',
425 'boxSelectionEnabled',
426 'autolock', 'autoungrabify', 'autounselectify',
427 'multiClickDebounceTime'
428 ];
429
430 for( let i = 0; i < fields.length; i++ ){
431 let f = fields[ i ];
432
433 if( obj[ f ] != null ){
434 cy[ f ]( obj[ f ] );
435 }
436 }
437
438 cy.endBatch();
439
440 return this; // chaining
441 } else { // get
442 let flat = !!obj;
443 let json = {};
444
445 if( flat ){
446 json.elements = this.elements().map( ele => ele.json() );
447 } else {
448 json.elements = {};
449
450 eles.forEach( function( ele ){
451 let group = ele.group();
452
453 if( !json.elements[ group ] ){
454 json.elements[ group ] = [];
455 }
456
457 json.elements[ group ].push( ele.json() );
458 } );
459 }
460
461 if( this._private.styleEnabled ){
462 json.style = cy.style().json();
463 }
464
465 json.data = util.copy( cy.data() );
466
467 let options = _p.options;
468
469 json.zoomingEnabled = _p.zoomingEnabled;
470 json.userZoomingEnabled = _p.userZoomingEnabled;
471 json.zoom = _p.zoom;
472 json.minZoom = _p.minZoom;
473 json.maxZoom = _p.maxZoom;
474 json.panningEnabled = _p.panningEnabled;
475 json.userPanningEnabled = _p.userPanningEnabled;
476 json.pan = util.copy( _p.pan );
477 json.boxSelectionEnabled = _p.boxSelectionEnabled;
478 json.renderer = util.copy( options.renderer );
479 json.hideEdgesOnViewport = options.hideEdgesOnViewport;
480 json.textureOnViewport = options.textureOnViewport;
481 json.wheelSensitivity = options.wheelSensitivity;
482 json.motionBlur = options.motionBlur;
483 json.multiClickDebounceTime = options.multiClickDebounceTime;
484
485 return json;
486 }
487 }
488
489} );
490
491corefn.$id = corefn.getElementById;
492
493[
494 addRemove,
495 animation,
496 events,
497 exportFormat,
498 layout,
499 notification,
500 renderer,
501 search,
502 style,
503 viewport,
504 data
505].forEach( function( props ){
506 util.extend( corefn, props );
507} );
508
509export default Core;