UNPKG

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