UNPKG

20.7 kBJavaScriptView Raw
1import * as util from '../util';
2import * as is from '../is';
3import Map from '../map';
4import Set from '../set';
5
6import Element from './element';
7import algorithms from './algorithms';
8import animation from './animation';
9import classNames from './class';
10import comparators from './comparators';
11import compounds from './compounds';
12import data from './data';
13import degree from './degree';
14import dimensions from './dimensions';
15import events from './events';
16import filter from './filter';
17import group from './group';
18import iteration from './iteration';
19import layout from './layout';
20import style from './style';
21import switchFunctions from './switch-functions';
22import traversing from './traversing';
23
24// factory for generating edge ids when no id is specified for a new element
25let idFactory = {
26 generate: function( cy, element, tryThisId ){
27 let id = tryThisId != null ? tryThisId : util.uuid();
28
29 while( cy.hasElementWithId( id ) ){
30 id = util.uuid();
31 }
32
33 return id;
34 }
35};
36
37// represents a set of nodes, edges, or both together
38let Collection = function( cy, elements, options ){
39 if( cy === undefined || !is.core( cy ) ){
40 util.error( 'A collection must have a reference to the core' );
41 return;
42 }
43
44 let map = new Map();
45 let createdElements = false;
46
47 if( !elements ){
48 elements = [];
49 } else if( elements.length > 0 && is.plainObject( elements[0] ) && !is.element( elements[0] ) ){
50 createdElements = true;
51
52 // make elements from json and restore all at once later
53 let eles = [];
54 let elesIds = new Set();
55
56 for( let i = 0, l = elements.length; i < l; i++ ){
57 let json = elements[ i ];
58
59 if( json.data == null ){
60 json.data = {};
61 }
62
63 let data = json.data;
64
65 // make sure newly created elements have valid ids
66 if( data.id == null ){
67 data.id = idFactory.generate( cy, json );
68 } else if( cy.hasElementWithId( data.id ) || elesIds.has( data.id ) ){
69 continue; // can't create element if prior id already exists
70 }
71
72 let ele = new Element( cy, json, false );
73 eles.push( ele );
74 elesIds.add( data.id );
75 }
76
77 elements = eles;
78 }
79
80 this.length = 0;
81
82 for( let i = 0, l = elements.length; i < l; i++ ){
83 let element = elements[i][0]; // [0] in case elements is an array of collections, rather than array of elements
84 if( element == null ){ continue; }
85
86 let id = element._private.data.id;
87
88 if( options == null || ( options.unique && !map.has(id) ) ){
89 map.set( id, {
90 index: this.length,
91 ele: element
92 } );
93
94 this[ this.length ] = element;
95 this.length++;
96 }
97 }
98
99 this._private = {
100 cy: cy,
101 map: map
102 };
103
104 // restore the elements if we created them from json
105 if( createdElements ){
106 this.restore();
107 }
108};
109
110// Functions
111////////////////////////////////////////////////////////////////////////////////////////////////////
112
113// keep the prototypes in sync (an element has the same functions as a collection)
114// and use elefn and elesfn as shorthands to the prototypes
115let elesfn = Element.prototype = Collection.prototype;
116
117elesfn.instanceString = function(){
118 return 'collection';
119};
120
121elesfn.spawn = function( cy, eles, opts ){
122 if( !is.core( cy ) ){ // cy is optional
123 opts = eles;
124 eles = cy;
125 cy = this.cy();
126 }
127
128 return new Collection( cy, eles, opts );
129};
130
131elesfn.spawnSelf = function(){
132 return this.spawn( this );
133};
134
135elesfn.cy = function(){
136 return this._private.cy;
137};
138
139elesfn.renderer = function(){
140 return this._private.cy.renderer();
141};
142
143elesfn.element = function(){
144 return this[0];
145};
146
147elesfn.collection = function(){
148 if( is.collection( this ) ){
149 return this;
150 } else { // an element
151 return new Collection( this._private.cy, [ this ] );
152 }
153};
154
155elesfn.unique = function(){
156 return new Collection( this._private.cy, this, { unique: true } );
157};
158
159elesfn.hasElementWithId = function( id ){
160 id = '' + id; // id must be string
161
162 return this._private.map.has( id );
163};
164
165elesfn.getElementById = function( id ){
166 id = '' + id; // id must be string
167
168 let cy = this._private.cy;
169 let entry = this._private.map.get( id );
170
171 return entry ? entry.ele : new Collection( cy ); // get ele or empty collection
172};
173
174elesfn.$id = elesfn.getElementById;
175
176elesfn.poolIndex = function(){
177 let cy = this._private.cy;
178 let eles = cy._private.elements;
179 let id = this[0]._private.data.id;
180
181 return eles._private.map.get( id ).index;
182};
183
184elesfn.indexOf = function( ele ){
185 let id = ele[0]._private.data.id;
186
187 return this._private.map.get( id ).index;
188};
189
190elesfn.indexOfId = function( id ){
191 id = '' + id; // id must be string
192
193 return this._private.map.get( id ).index;
194};
195
196elesfn.json = function( obj ){
197 let ele = this.element();
198 let cy = this.cy();
199
200 if( ele == null && obj ){ return this; } // can't set to no eles
201
202 if( ele == null ){ return undefined; } // can't get from no eles
203
204 let p = ele._private;
205
206 if( is.plainObject( obj ) ){ // set
207
208 cy.startBatch();
209
210 if( obj.data ){
211 ele.data( obj.data );
212
213 let data = p.data;
214
215 if( ele.isEdge() ){ // source and target are immutable via data()
216 let move = false;
217 let spec = {};
218 let src = obj.data.source;
219 let tgt = obj.data.target;
220
221 if( src != null && src != data.source ){
222 spec.source = '' + src; // id must be string
223 move = true;
224 }
225
226 if( tgt != null && tgt != data.target ){
227 spec.target = '' + tgt; // id must be string
228 move = true;
229 }
230
231 if( move ){
232 ele = ele.move(spec);
233 }
234 } else { // parent is immutable via data()
235 let parent = obj.data.parent;
236
237 if( (parent != null || data.parent != null) && parent != data.parent ){
238 if( parent === undefined ){ // can't set undefined imperatively, so use null
239 parent = null;
240 }
241
242 if( parent != null ){
243 parent = '' + parent; // id must be string
244 }
245
246 ele = ele.move({ parent });
247 }
248 }
249 }
250
251 if( obj.position ){
252 ele.position( obj.position );
253 }
254
255 // ignore group -- immutable
256
257 let checkSwitch = function( k, trueFnName, falseFnName ){
258 let obj_k = obj[ k ];
259
260 if( obj_k != null && obj_k !== p[ k ] ){
261 if( obj_k ){
262 ele[ trueFnName ]();
263 } else {
264 ele[ falseFnName ]();
265 }
266 }
267 };
268
269 checkSwitch( 'removed', 'remove', 'restore' );
270
271 checkSwitch( 'selected', 'select', 'unselect' );
272
273 checkSwitch( 'selectable', 'selectify', 'unselectify' );
274
275 checkSwitch( 'locked', 'lock', 'unlock' );
276
277 checkSwitch( 'grabbable', 'grabify', 'ungrabify' );
278
279 checkSwitch( 'pannable', 'panify', 'unpanify' );
280
281 if( obj.classes != null ){
282 ele.classes( obj.classes );
283 }
284
285 cy.endBatch();
286
287 return this;
288
289 } else if( obj === undefined ){ // get
290
291 let json = {
292 data: util.copy( p.data ),
293 position: util.copy( p.position ),
294 group: p.group,
295 removed: p.removed,
296 selected: p.selected,
297 selectable: p.selectable,
298 locked: p.locked,
299 grabbable: p.grabbable,
300 pannable: p.pannable,
301 classes: null
302 };
303
304 json.classes = '';
305
306 let i = 0;
307 p.classes.forEach( cls => json.classes += ( i++ === 0 ? cls : ' ' + cls ) );
308
309 return json;
310 }
311};
312
313elesfn.jsons = function(){
314 let jsons = [];
315
316 for( let i = 0; i < this.length; i++ ){
317 let ele = this[ i ];
318 let json = ele.json();
319
320 jsons.push( json );
321 }
322
323 return jsons;
324};
325
326elesfn.clone = function(){
327 let cy = this.cy();
328 let elesArr = [];
329
330 for( let i = 0; i < this.length; i++ ){
331 let ele = this[ i ];
332 let json = ele.json();
333 let clone = new Element( cy, json, false ); // NB no restore
334
335 elesArr.push( clone );
336 }
337
338 return new Collection( cy, elesArr );
339};
340elesfn.copy = elesfn.clone;
341
342elesfn.restore = function( notifyRenderer = true, addToPool = true ){
343 let self = this;
344 let cy = self.cy();
345 let cy_p = cy._private;
346
347 // create arrays of nodes and edges, since we need to
348 // restore the nodes first
349 let nodes = [];
350 let edges = [];
351 let elements;
352 for( let i = 0, l = self.length; i < l; i++ ){
353 let ele = self[ i ];
354
355 if( addToPool && !ele.removed() ){
356 // don't need to handle this ele
357 continue;
358 }
359
360 // keep nodes first in the array and edges after
361 if( ele.isNode() ){ // put to front of array if node
362 nodes.push( ele );
363 } else { // put to end of array if edge
364 edges.push( ele );
365 }
366 }
367
368 elements = nodes.concat( edges );
369
370 let i;
371 let removeFromElements = function(){
372 elements.splice( i, 1 );
373 i--;
374 };
375
376 // now, restore each element
377 for( i = 0; i < elements.length; i++ ){
378 let ele = elements[ i ];
379
380 let _private = ele._private;
381 let data = _private.data;
382
383 // the traversal cache should start fresh when ele is added
384 ele.clearTraversalCache();
385
386 // set id and validate
387 if( !addToPool && !_private.removed ){
388 // already in graph, so nothing required
389
390 } else if( data.id === undefined ){
391 data.id = idFactory.generate( cy, ele );
392
393 } else if( is.number( data.id ) ){
394 data.id = '' + data.id; // now it's a string
395
396 } else if( is.emptyString( data.id ) || !is.string( data.id ) ){
397 util.error( 'Can not create element with invalid string ID `' + data.id + '`' );
398
399 // can't create element if it has empty string as id or non-string id
400 removeFromElements();
401 continue;
402 } else if( cy.hasElementWithId( data.id ) ){
403 util.error( 'Can not create second element with ID `' + data.id + '`' );
404
405 // can't create element if one already has that id
406 removeFromElements();
407 continue;
408 }
409
410 let id = data.id; // id is finalised, now let's keep a ref
411
412 if( ele.isNode() ){ // extra checks for nodes
413 let pos = _private.position;
414
415 // make sure the nodes have a defined position
416
417 if( pos.x == null ){
418 pos.x = 0;
419 }
420
421 if( pos.y == null ){
422 pos.y = 0;
423 }
424 }
425
426 if( ele.isEdge() ){ // extra checks for edges
427
428 let edge = ele;
429 let fields = [ 'source', 'target' ];
430 let fieldsLength = fields.length;
431 let badSourceOrTarget = false;
432 for( let j = 0; j < fieldsLength; j++ ){
433
434 let field = fields[ j ];
435 let val = data[ field ];
436
437 if( is.number( val ) ){
438 val = data[ field ] = '' + data[ field ]; // now string
439 }
440
441 if( val == null || val === '' ){
442 // can't create if source or target is not defined properly
443 util.error( 'Can not create edge `' + id + '` with unspecified ' + field );
444 badSourceOrTarget = true;
445 } else if( !cy.hasElementWithId( val ) ){
446 // can't create edge if one of its nodes doesn't exist
447 util.error( 'Can not create edge `' + id + '` with nonexistant ' + field + ' `' + val + '`' );
448 badSourceOrTarget = true;
449 }
450 }
451
452 if( badSourceOrTarget ){ removeFromElements(); continue; } // can't create this
453
454 let src = cy.getElementById( data.source );
455 let tgt = cy.getElementById( data.target );
456
457 // only one edge in node if loop
458 if (src.same(tgt)) {
459 src._private.edges.push( edge );
460 } else {
461 src._private.edges.push( edge );
462 tgt._private.edges.push( edge );
463 }
464
465 edge._private.source = src;
466 edge._private.target = tgt;
467 } // if is edge
468
469 // create mock ids / indexes maps for element so it can be used like collections
470 _private.map = new Map();
471 _private.map.set( id, { ele: ele, index: 0 } );
472
473 _private.removed = false;
474
475 if( addToPool ){
476 cy.addToPool( ele );
477 }
478 } // for each element
479
480 // do compound node sanity checks
481 for( let i = 0; i < nodes.length; i++ ){ // each node
482 let node = nodes[ i ];
483 let data = node._private.data;
484
485 if( is.number( data.parent ) ){ // then automake string
486 data.parent = '' + data.parent;
487 }
488
489 let parentId = data.parent;
490
491 let specifiedParent = parentId != null;
492
493 if( specifiedParent ){
494 let parent = cy.getElementById( parentId );
495
496 if( parent.empty() ){
497 // non-existant parent; just remove it
498 data.parent = undefined;
499 } else {
500 let selfAsParent = false;
501 let ancestor = parent;
502 while( !ancestor.empty() ){
503 if( node.same( ancestor ) ){
504 // mark self as parent and remove from data
505 selfAsParent = true;
506 data.parent = undefined; // remove parent reference
507
508 // exit or we loop forever
509 break;
510 }
511
512 ancestor = ancestor.parent();
513 }
514
515 if( !selfAsParent ){
516 // connect with children
517 parent[0]._private.children.push( node );
518 node._private.parent = parent[0];
519
520 // let the core know we have a compound graph
521 cy_p.hasCompoundNodes = true;
522 }
523 } // else
524 } // if specified parent
525 } // for each node
526
527 if( elements.length > 0 ){
528 let restored = new Collection( cy, elements );
529
530 for( let i = 0; i < restored.length; i++ ){
531 let ele = restored[i];
532
533 if( ele.isNode() ){ continue; }
534
535 // adding an edge invalidates the traversal caches for the parallel edges
536 ele.parallelEdges().clearTraversalCache();
537
538 // adding an edge invalidates the traversal cache for the connected nodes
539 ele.source().clearTraversalCache();
540 ele.target().clearTraversalCache();
541 }
542
543 let toUpdateStyle;
544
545 if( cy_p.hasCompoundNodes ){
546 toUpdateStyle = cy.collection().merge( restored ).merge( restored.connectedNodes() ).merge( restored.parent() );
547 } else {
548 toUpdateStyle = restored;
549 }
550
551 toUpdateStyle.dirtyCompoundBoundsCache().dirtyBoundingBoxCache().updateStyle( notifyRenderer );
552
553 if( notifyRenderer ){
554 restored.emitAndNotify( 'add' );
555 } else if( addToPool ){
556 restored.emit( 'add' );
557 }
558 }
559
560 return self; // chainability
561};
562
563elesfn.removed = function(){
564 let ele = this[0];
565 return ele && ele._private.removed;
566};
567
568elesfn.inside = function(){
569 let ele = this[0];
570 return ele && !ele._private.removed;
571};
572
573elesfn.remove = function( notifyRenderer = true, removeFromPool = true ){
574 let self = this;
575 let elesToRemove = [];
576 let elesToRemoveIds = {};
577 let cy = self._private.cy;
578
579 // add connected edges
580 function addConnectedEdges( node ){
581 let edges = node._private.edges;
582 for( let i = 0; i < edges.length; i++ ){
583 add( edges[ i ] );
584 }
585 }
586
587 // add descendant nodes
588 function addChildren( node ){
589 let children = node._private.children;
590
591 for( let i = 0; i < children.length; i++ ){
592 add( children[ i ] );
593 }
594 }
595
596 function add( ele ){
597 let alreadyAdded = elesToRemoveIds[ ele.id() ];
598 if( (removeFromPool && ele.removed()) || alreadyAdded ){
599 return;
600 } else {
601 elesToRemoveIds[ ele.id() ] = true;
602 }
603
604 if( ele.isNode() ){
605 elesToRemove.push( ele ); // nodes are removed last
606
607 addConnectedEdges( ele );
608 addChildren( ele );
609 } else {
610 elesToRemove.unshift( ele ); // edges are removed first
611 }
612 }
613
614 // make the list of elements to remove
615 // (may be removing more than specified due to connected edges etc)
616
617 for( let i = 0, l = self.length; i < l; i++ ){
618 let ele = self[ i ];
619
620 add( ele );
621 }
622
623 function removeEdgeRef( node, edge ){
624 let connectedEdges = node._private.edges;
625
626 util.removeFromArray( connectedEdges, edge );
627
628 // removing an edges invalidates the traversal cache for its nodes
629 node.clearTraversalCache();
630 }
631
632 function removeParallelRef( pllEdge ){
633 // removing an edge invalidates the traversal caches for the parallel edges
634 pllEdge.clearTraversalCache();
635 }
636
637 let alteredParents = [];
638 alteredParents.ids = {};
639
640 function removeChildRef( parent, ele ){
641 ele = ele[0];
642 parent = parent[0];
643
644 let children = parent._private.children;
645 let pid = parent.id();
646
647 util.removeFromArray( children, ele ); // remove parent => child ref
648
649 ele._private.parent = null; // remove child => parent ref
650
651 if( !alteredParents.ids[ pid ] ){
652 alteredParents.ids[ pid ] = true;
653 alteredParents.push( parent );
654 }
655 }
656
657 self.dirtyCompoundBoundsCache();
658
659 if( removeFromPool ){
660 cy.removeFromPool( elesToRemove ); // remove from core pool
661 }
662
663 for( let i = 0; i < elesToRemove.length; i++ ){
664 let ele = elesToRemove[ i ];
665
666 if( ele.isEdge() ){ // remove references to this edge in its connected nodes
667 let src = ele.source()[0];
668 let tgt = ele.target()[0];
669
670 removeEdgeRef( src, ele );
671 removeEdgeRef( tgt, ele );
672
673 let pllEdges = ele.parallelEdges();
674
675 for( let j = 0; j < pllEdges.length; j++ ){
676 let pllEdge = pllEdges[j];
677
678 removeParallelRef(pllEdge);
679
680 if( pllEdge.isBundledBezier() ){
681 pllEdge.dirtyBoundingBoxCache();
682 }
683 }
684
685 } else { // remove reference to parent
686 let parent = ele.parent();
687
688 if( parent.length !== 0 ){
689 removeChildRef( parent, ele );
690 }
691 }
692
693 if( removeFromPool ){
694 // mark as removed
695 ele._private.removed = true;
696 }
697 }
698
699 // check to see if we have a compound graph or not
700 let elesStillInside = cy._private.elements;
701 cy._private.hasCompoundNodes = false;
702 for( let i = 0; i < elesStillInside.length; i++ ){
703 let ele = elesStillInside[ i ];
704
705 if( ele.isParent() ){
706 cy._private.hasCompoundNodes = true;
707 break;
708 }
709 }
710
711 let removedElements = new Collection( this.cy(), elesToRemove );
712
713 if( removedElements.size() > 0 ){
714 // must manually notify since trigger won't do this automatically once removed
715
716 if( notifyRenderer ){
717 removedElements.emitAndNotify('remove');
718 } else if( removeFromPool ){
719 removedElements.emit('remove');
720 }
721 }
722
723 // the parents who were modified by the removal need their style updated
724 for( let i = 0; i < alteredParents.length; i++ ){
725 let ele = alteredParents[ i ];
726
727 if( !removeFromPool || !ele.removed() ){
728 ele.updateStyle();
729 }
730 }
731
732 return removedElements;
733};
734
735elesfn.move = function( struct ){
736 let cy = this._private.cy;
737 let eles = this;
738
739 // just clean up refs, caches, etc. in the same way as when removing and then restoring
740 // (our calls to remove/restore do not remove from the graph or make events)
741 let notifyRenderer = false;
742 let modifyPool = false;
743
744 let toString = id => id == null ? id : '' + id; // id must be string
745
746 if( struct.source !== undefined || struct.target !== undefined ){
747 let srcId = toString(struct.source);
748 let tgtId = toString(struct.target);
749 let srcExists = srcId != null && cy.hasElementWithId( srcId );
750 let tgtExists = tgtId != null && cy.hasElementWithId( tgtId );
751
752 if( srcExists || tgtExists ){
753 cy.batch(() => { // avoid duplicate style updates
754 eles.remove( notifyRenderer, modifyPool ); // clean up refs etc.
755 eles.emitAndNotify('moveout');
756
757 for( let i = 0; i < eles.length; i++ ){
758 let ele = eles[i];
759 let data = ele._private.data;
760
761 if( ele.isEdge() ){
762 if( srcExists ){ data.source = srcId; }
763
764 if( tgtExists ){ data.target = tgtId; }
765 }
766 }
767
768 eles.restore( notifyRenderer, modifyPool ); // make new refs, style, etc.
769 });
770
771 eles.emitAndNotify('move');
772 }
773
774 } else if( struct.parent !== undefined ){ // move node to new parent
775 let parentId = toString(struct.parent);
776 let parentExists = parentId === null || cy.hasElementWithId( parentId );
777
778 if( parentExists ){
779 let pidToAssign = parentId === null ? undefined : parentId;
780
781 cy.batch(() => { // avoid duplicate style updates
782 let updated = eles.remove( notifyRenderer, modifyPool ); // clean up refs etc.
783 updated.emitAndNotify('moveout');
784
785 for( let i = 0; i < eles.length; i++ ){
786 let ele = eles[i];
787 let data = ele._private.data;
788
789 if( ele.isNode() ){
790 data.parent = pidToAssign;
791 }
792 }
793
794 updated.restore( notifyRenderer, modifyPool ); // make new refs, style, etc.
795 });
796
797 eles.emitAndNotify('move');
798 }
799 }
800
801 return this;
802};
803
804[
805 algorithms,
806 animation,
807 classNames,
808 comparators,
809 compounds,
810 data,
811 degree,
812 dimensions,
813 events,
814 filter,
815 group,
816 iteration,
817 layout,
818 style,
819 switchFunctions,
820 traversing
821].forEach( function( props ){
822 util.extend( elesfn, props );
823} );
824
825export default Collection;