|
import * as util from '../util'; |
|
import * as is from '../is'; |
|
import Map from '../map'; |
|
import Set from '../set'; |
|
|
|
import Element from './element'; |
|
import algorithms from './algorithms'; |
|
import animation from './animation'; |
|
import classNames from './class'; |
|
import comparators from './comparators'; |
|
import compounds from './compounds'; |
|
import data from './data'; |
|
import degree from './degree'; |
|
import dimensions from './dimensions'; |
|
import events from './events'; |
|
import filter from './filter'; |
|
import group from './group'; |
|
import iteration from './iteration'; |
|
import layout from './layout'; |
|
import style from './style'; |
|
import switchFunctions from './switch-functions'; |
|
import traversing from './traversing'; |
|
|
|
|
|
let Collection = function( cy, elements, unique = false, removed = false ){ |
|
if( cy === undefined ){ |
|
util.error( 'A collection must have a reference to the core' ); |
|
return; |
|
} |
|
|
|
let map = new Map(); |
|
let createdElements = false; |
|
|
|
if( !elements ){ |
|
elements = []; |
|
} else if( elements.length > 0 && is.plainObject( elements[0] ) && !is.element( elements[0] ) ){ |
|
createdElements = true; |
|
|
|
|
|
let eles = []; |
|
let elesIds = new Set(); |
|
|
|
for( let i = 0, l = elements.length; i < l; i++ ){ |
|
let json = elements[ i ]; |
|
|
|
if( json.data == null ){ |
|
json.data = {}; |
|
} |
|
|
|
let data = json.data; |
|
|
|
|
|
if( data.id == null ){ |
|
data.id = util.uuid(); |
|
} else if( cy.hasElementWithId( data.id ) || elesIds.has( data.id ) ){ |
|
continue; |
|
} |
|
|
|
let ele = new Element( cy, json, false ); |
|
eles.push( ele ); |
|
elesIds.add( data.id ); |
|
} |
|
|
|
elements = eles; |
|
} |
|
|
|
this.length = 0; |
|
|
|
for( let i = 0, l = elements.length; i < l; i++ ){ |
|
let element = elements[i][0]; |
|
if( element == null ){ continue; } |
|
|
|
let id = element._private.data.id; |
|
|
|
if( !unique || !map.has(id) ){ |
|
if( unique ){ |
|
map.set( id, { |
|
index: this.length, |
|
ele: element |
|
} ); |
|
} |
|
|
|
this[ this.length ] = element; |
|
this.length++; |
|
} |
|
} |
|
|
|
this._private = { |
|
eles: this, |
|
cy: cy, |
|
get map(){ |
|
if( this.lazyMap == null ){ |
|
this.rebuildMap(); |
|
} |
|
|
|
return this.lazyMap; |
|
}, |
|
set map(m){ |
|
this.lazyMap = m; |
|
}, |
|
rebuildMap(){ |
|
const m = this.lazyMap = new Map(); |
|
const eles = this.eles; |
|
|
|
for( let i = 0; i < eles.length; i++ ){ |
|
const ele = eles[i]; |
|
|
|
m.set(ele.id(), { index: i, ele }); |
|
} |
|
} |
|
}; |
|
|
|
if( unique ){ |
|
this._private.map = map; |
|
} |
|
|
|
|
|
if( createdElements && !removed ){ |
|
this.restore(); |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
let elesfn = Element.prototype = Collection.prototype = Object.create(Array.prototype); |
|
|
|
elesfn.instanceString = function(){ |
|
return 'collection'; |
|
}; |
|
|
|
elesfn.spawn = function( eles, unique ){ |
|
return new Collection( this.cy(), eles, unique ); |
|
}; |
|
|
|
elesfn.spawnSelf = function(){ |
|
return this.spawn( this ); |
|
}; |
|
|
|
elesfn.cy = function(){ |
|
return this._private.cy; |
|
}; |
|
|
|
elesfn.renderer = function(){ |
|
return this._private.cy.renderer(); |
|
}; |
|
|
|
elesfn.element = function(){ |
|
return this[0]; |
|
}; |
|
|
|
elesfn.collection = function(){ |
|
if( is.collection( this ) ){ |
|
return this; |
|
} else { |
|
return new Collection( this._private.cy, [ this ] ); |
|
} |
|
}; |
|
|
|
elesfn.unique = function(){ |
|
return new Collection( this._private.cy, this, true ); |
|
}; |
|
|
|
elesfn.hasElementWithId = function( id ){ |
|
id = '' + id; |
|
|
|
return this._private.map.has( id ); |
|
}; |
|
|
|
elesfn.getElementById = function( id ){ |
|
id = '' + id; |
|
|
|
let cy = this._private.cy; |
|
let entry = this._private.map.get( id ); |
|
|
|
return entry ? entry.ele : new Collection( cy ); |
|
}; |
|
|
|
elesfn.$id = elesfn.getElementById; |
|
|
|
elesfn.poolIndex = function(){ |
|
let cy = this._private.cy; |
|
let eles = cy._private.elements; |
|
let id = this[0]._private.data.id; |
|
|
|
return eles._private.map.get( id ).index; |
|
}; |
|
|
|
elesfn.indexOf = function( ele ){ |
|
let id = ele[0]._private.data.id; |
|
|
|
return this._private.map.get( id ).index; |
|
}; |
|
|
|
elesfn.indexOfId = function( id ){ |
|
id = '' + id; |
|
|
|
return this._private.map.get( id ).index; |
|
}; |
|
|
|
elesfn.json = function( obj ){ |
|
let ele = this.element(); |
|
let cy = this.cy(); |
|
|
|
if( ele == null && obj ){ return this; } |
|
|
|
if( ele == null ){ return undefined; } |
|
|
|
let p = ele._private; |
|
|
|
if( is.plainObject( obj ) ){ |
|
|
|
cy.startBatch(); |
|
|
|
if( obj.data ){ |
|
ele.data( obj.data ); |
|
|
|
let data = p.data; |
|
|
|
if( ele.isEdge() ){ |
|
let move = false; |
|
let spec = {}; |
|
let src = obj.data.source; |
|
let tgt = obj.data.target; |
|
|
|
if( src != null && src != data.source ){ |
|
spec.source = '' + src; |
|
move = true; |
|
} |
|
|
|
if( tgt != null && tgt != data.target ){ |
|
spec.target = '' + tgt; |
|
move = true; |
|
} |
|
|
|
if( move ){ |
|
ele = ele.move(spec); |
|
} |
|
} else { |
|
let newParentValSpecd = 'parent' in obj.data; |
|
let parent = obj.data.parent; |
|
|
|
if( newParentValSpecd && (parent != null || data.parent != null) && parent != data.parent ){ |
|
if( parent === undefined ){ |
|
parent = null; |
|
} |
|
|
|
if( parent != null ){ |
|
parent = '' + parent; |
|
} |
|
|
|
ele = ele.move({ parent }); |
|
} |
|
} |
|
} |
|
|
|
if( obj.position ){ |
|
ele.position( obj.position ); |
|
} |
|
|
|
|
|
|
|
let checkSwitch = function( k, trueFnName, falseFnName ){ |
|
let obj_k = obj[ k ]; |
|
|
|
if( obj_k != null && obj_k !== p[ k ] ){ |
|
if( obj_k ){ |
|
ele[ trueFnName ](); |
|
} else { |
|
ele[ falseFnName ](); |
|
} |
|
} |
|
}; |
|
|
|
checkSwitch( 'removed', 'remove', 'restore' ); |
|
|
|
checkSwitch( 'selected', 'select', 'unselect' ); |
|
|
|
checkSwitch( 'selectable', 'selectify', 'unselectify' ); |
|
|
|
checkSwitch( 'locked', 'lock', 'unlock' ); |
|
|
|
checkSwitch( 'grabbable', 'grabify', 'ungrabify' ); |
|
|
|
checkSwitch( 'pannable', 'panify', 'unpanify' ); |
|
|
|
if( obj.classes != null ){ |
|
ele.classes( obj.classes ); |
|
} |
|
|
|
cy.endBatch(); |
|
|
|
return this; |
|
|
|
} else if( obj === undefined ){ |
|
|
|
let json = { |
|
data: util.copy( p.data ), |
|
position: util.copy( p.position ), |
|
group: p.group, |
|
removed: p.removed, |
|
selected: p.selected, |
|
selectable: p.selectable, |
|
locked: p.locked, |
|
grabbable: p.grabbable, |
|
pannable: p.pannable, |
|
classes: null |
|
}; |
|
|
|
json.classes = ''; |
|
|
|
let i = 0; |
|
p.classes.forEach( cls => json.classes += ( i++ === 0 ? cls : ' ' + cls ) ); |
|
|
|
return json; |
|
} |
|
}; |
|
|
|
elesfn.jsons = function(){ |
|
let jsons = []; |
|
|
|
for( let i = 0; i < this.length; i++ ){ |
|
let ele = this[ i ]; |
|
let json = ele.json(); |
|
|
|
jsons.push( json ); |
|
} |
|
|
|
return jsons; |
|
}; |
|
|
|
elesfn.clone = function(){ |
|
let cy = this.cy(); |
|
let elesArr = []; |
|
|
|
for( let i = 0; i < this.length; i++ ){ |
|
let ele = this[ i ]; |
|
let json = ele.json(); |
|
let clone = new Element( cy, json, false ); |
|
|
|
elesArr.push( clone ); |
|
} |
|
|
|
return new Collection( cy, elesArr ); |
|
}; |
|
elesfn.copy = elesfn.clone; |
|
|
|
elesfn.restore = function( notifyRenderer = true, addToPool = true ){ |
|
let self = this; |
|
let cy = self.cy(); |
|
let cy_p = cy._private; |
|
|
|
|
|
|
|
let nodes = []; |
|
let edges = []; |
|
let elements; |
|
for( let i = 0, l = self.length; i < l; i++ ){ |
|
let ele = self[ i ]; |
|
|
|
if( addToPool && !ele.removed() ){ |
|
|
|
continue; |
|
} |
|
|
|
|
|
if( ele.isNode() ){ |
|
nodes.push( ele ); |
|
} else { |
|
edges.push( ele ); |
|
} |
|
} |
|
|
|
elements = nodes.concat( edges ); |
|
|
|
let i; |
|
let removeFromElements = function(){ |
|
elements.splice( i, 1 ); |
|
i--; |
|
}; |
|
|
|
|
|
for( i = 0; i < elements.length; i++ ){ |
|
let ele = elements[ i ]; |
|
|
|
let _private = ele._private; |
|
let data = _private.data; |
|
|
|
|
|
ele.clearTraversalCache(); |
|
|
|
|
|
if( !addToPool && !_private.removed ){ |
|
|
|
|
|
} else if( data.id === undefined ){ |
|
data.id = util.uuid(); |
|
|
|
} else if( is.number( data.id ) ){ |
|
data.id = '' + data.id; |
|
|
|
} else if( is.emptyString( data.id ) || !is.string( data.id ) ){ |
|
util.error( 'Can not create element with invalid string ID `' + data.id + '`' ); |
|
|
|
|
|
removeFromElements(); |
|
continue; |
|
} else if( cy.hasElementWithId( data.id ) ){ |
|
util.error( 'Can not create second element with ID `' + data.id + '`' ); |
|
|
|
|
|
removeFromElements(); |
|
continue; |
|
} |
|
|
|
let id = data.id; |
|
|
|
if( ele.isNode() ){ |
|
let pos = _private.position; |
|
|
|
|
|
|
|
if( pos.x == null ){ |
|
pos.x = 0; |
|
} |
|
|
|
if( pos.y == null ){ |
|
pos.y = 0; |
|
} |
|
} |
|
|
|
if( ele.isEdge() ){ |
|
|
|
let edge = ele; |
|
let fields = [ 'source', 'target' ]; |
|
let fieldsLength = fields.length; |
|
let badSourceOrTarget = false; |
|
for( let j = 0; j < fieldsLength; j++ ){ |
|
|
|
let field = fields[ j ]; |
|
let val = data[ field ]; |
|
|
|
if( is.number( val ) ){ |
|
val = data[ field ] = '' + data[ field ]; |
|
} |
|
|
|
if( val == null || val === '' ){ |
|
|
|
util.error( 'Can not create edge `' + id + '` with unspecified ' + field ); |
|
badSourceOrTarget = true; |
|
} else if( !cy.hasElementWithId( val ) ){ |
|
|
|
util.error( 'Can not create edge `' + id + '` with nonexistant ' + field + ' `' + val + '`' ); |
|
badSourceOrTarget = true; |
|
} |
|
} |
|
|
|
if( badSourceOrTarget ){ removeFromElements(); continue; } |
|
|
|
let src = cy.getElementById( data.source ); |
|
let tgt = cy.getElementById( data.target ); |
|
|
|
|
|
if (src.same(tgt)) { |
|
src._private.edges.push( edge ); |
|
} else { |
|
src._private.edges.push( edge ); |
|
tgt._private.edges.push( edge ); |
|
} |
|
|
|
edge._private.source = src; |
|
edge._private.target = tgt; |
|
} |
|
|
|
|
|
_private.map = new Map(); |
|
_private.map.set( id, { ele: ele, index: 0 } ); |
|
|
|
_private.removed = false; |
|
|
|
if( addToPool ){ |
|
cy.addToPool( ele ); |
|
} |
|
} |
|
|
|
|
|
for( let i = 0; i < nodes.length; i++ ){ |
|
let node = nodes[ i ]; |
|
let data = node._private.data; |
|
|
|
if( is.number( data.parent ) ){ |
|
data.parent = '' + data.parent; |
|
} |
|
|
|
let parentId = data.parent; |
|
|
|
let specifiedParent = parentId != null; |
|
|
|
if( specifiedParent || node._private.parent ){ |
|
|
|
let parent = node._private.parent ? cy.collection().merge(node._private.parent) : cy.getElementById( parentId ); |
|
|
|
if( parent.empty() ){ |
|
|
|
data.parent = undefined; |
|
} else if( parent[0].removed() ) { |
|
util.warn('Node added with missing parent, reference to parent removed'); |
|
data.parent = undefined; |
|
node._private.parent = null; |
|
} else { |
|
let selfAsParent = false; |
|
let ancestor = parent; |
|
while( !ancestor.empty() ){ |
|
if( node.same( ancestor ) ){ |
|
|
|
selfAsParent = true; |
|
data.parent = undefined; |
|
|
|
|
|
break; |
|
} |
|
|
|
ancestor = ancestor.parent(); |
|
} |
|
|
|
if( !selfAsParent ){ |
|
|
|
parent[0]._private.children.push( node ); |
|
node._private.parent = parent[0]; |
|
|
|
|
|
cy_p.hasCompoundNodes = true; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if( elements.length > 0 ){ |
|
let restored = elements.length === self.length ? self : new Collection( cy, elements ); |
|
|
|
for( let i = 0; i < restored.length; i++ ){ |
|
let ele = restored[i]; |
|
|
|
if( ele.isNode() ){ continue; } |
|
|
|
|
|
ele.parallelEdges().clearTraversalCache(); |
|
|
|
|
|
ele.source().clearTraversalCache(); |
|
ele.target().clearTraversalCache(); |
|
} |
|
|
|
let toUpdateStyle; |
|
|
|
if( cy_p.hasCompoundNodes ){ |
|
toUpdateStyle = cy.collection().merge( restored ).merge( restored.connectedNodes() ).merge( restored.parent() ); |
|
} else { |
|
toUpdateStyle = restored; |
|
} |
|
|
|
toUpdateStyle.dirtyCompoundBoundsCache().dirtyBoundingBoxCache().updateStyle( notifyRenderer ); |
|
|
|
if( notifyRenderer ){ |
|
restored.emitAndNotify( 'add' ); |
|
} else if( addToPool ){ |
|
restored.emit( 'add' ); |
|
} |
|
} |
|
|
|
return self; |
|
}; |
|
|
|
elesfn.removed = function(){ |
|
let ele = this[0]; |
|
return ele && ele._private.removed; |
|
}; |
|
|
|
elesfn.inside = function(){ |
|
let ele = this[0]; |
|
return ele && !ele._private.removed; |
|
}; |
|
|
|
elesfn.remove = function( notifyRenderer = true, removeFromPool = true ){ |
|
let self = this; |
|
let elesToRemove = []; |
|
let elesToRemoveIds = {}; |
|
let cy = self._private.cy; |
|
|
|
|
|
function addConnectedEdges( node ){ |
|
let edges = node._private.edges; |
|
for( let i = 0; i < edges.length; i++ ){ |
|
add( edges[ i ] ); |
|
} |
|
} |
|
|
|
|
|
function addChildren( node ){ |
|
let children = node._private.children; |
|
|
|
for( let i = 0; i < children.length; i++ ){ |
|
add( children[ i ] ); |
|
} |
|
} |
|
|
|
function add( ele ){ |
|
let alreadyAdded = elesToRemoveIds[ ele.id() ]; |
|
if( (removeFromPool && ele.removed()) || alreadyAdded ){ |
|
return; |
|
} else { |
|
elesToRemoveIds[ ele.id() ] = true; |
|
} |
|
|
|
if( ele.isNode() ){ |
|
elesToRemove.push( ele ); |
|
|
|
addConnectedEdges( ele ); |
|
addChildren( ele ); |
|
} else { |
|
elesToRemove.unshift( ele ); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
for( let i = 0, l = self.length; i < l; i++ ){ |
|
let ele = self[ i ]; |
|
|
|
add( ele ); |
|
} |
|
|
|
function removeEdgeRef( node, edge ){ |
|
let connectedEdges = node._private.edges; |
|
|
|
util.removeFromArray( connectedEdges, edge ); |
|
|
|
|
|
node.clearTraversalCache(); |
|
} |
|
|
|
function removeParallelRef( pllEdge ){ |
|
|
|
pllEdge.clearTraversalCache(); |
|
} |
|
|
|
let alteredParents = []; |
|
alteredParents.ids = {}; |
|
|
|
function removeChildRef( parent, ele ){ |
|
ele = ele[0]; |
|
parent = parent[0]; |
|
|
|
let children = parent._private.children; |
|
let pid = parent.id(); |
|
|
|
util.removeFromArray( children, ele ); |
|
|
|
ele._private.parent = null; |
|
|
|
if( !alteredParents.ids[ pid ] ){ |
|
alteredParents.ids[ pid ] = true; |
|
alteredParents.push( parent ); |
|
} |
|
} |
|
|
|
self.dirtyCompoundBoundsCache(); |
|
|
|
if( removeFromPool ){ |
|
cy.removeFromPool( elesToRemove ); |
|
} |
|
|
|
for( let i = 0; i < elesToRemove.length; i++ ){ |
|
let ele = elesToRemove[ i ]; |
|
|
|
if( ele.isEdge() ){ |
|
let src = ele.source()[0]; |
|
let tgt = ele.target()[0]; |
|
|
|
removeEdgeRef( src, ele ); |
|
removeEdgeRef( tgt, ele ); |
|
|
|
let pllEdges = ele.parallelEdges(); |
|
|
|
for( let j = 0; j < pllEdges.length; j++ ){ |
|
let pllEdge = pllEdges[j]; |
|
|
|
removeParallelRef(pllEdge); |
|
|
|
if( pllEdge.isBundledBezier() ){ |
|
pllEdge.dirtyBoundingBoxCache(); |
|
} |
|
} |
|
|
|
} else { |
|
let parent = ele.parent(); |
|
|
|
if( parent.length !== 0 ){ |
|
removeChildRef( parent, ele ); |
|
} |
|
} |
|
|
|
if( removeFromPool ){ |
|
|
|
ele._private.removed = true; |
|
} |
|
} |
|
|
|
|
|
let elesStillInside = cy._private.elements; |
|
cy._private.hasCompoundNodes = false; |
|
for( let i = 0; i < elesStillInside.length; i++ ){ |
|
let ele = elesStillInside[ i ]; |
|
|
|
if( ele.isParent() ){ |
|
cy._private.hasCompoundNodes = true; |
|
break; |
|
} |
|
} |
|
|
|
let removedElements = new Collection( this.cy(), elesToRemove ); |
|
|
|
if( removedElements.size() > 0 ){ |
|
|
|
|
|
if( notifyRenderer ){ |
|
removedElements.emitAndNotify('remove'); |
|
} else if( removeFromPool ){ |
|
removedElements.emit('remove'); |
|
} |
|
} |
|
|
|
|
|
for( let i = 0; i < alteredParents.length; i++ ){ |
|
let ele = alteredParents[ i ]; |
|
|
|
if( !removeFromPool || !ele.removed() ){ |
|
ele.updateStyle(); |
|
} |
|
} |
|
|
|
return removedElements; |
|
}; |
|
|
|
elesfn.move = function( struct ){ |
|
let cy = this._private.cy; |
|
let eles = this; |
|
|
|
|
|
|
|
let notifyRenderer = false; |
|
let modifyPool = false; |
|
|
|
let toString = id => id == null ? id : '' + id; |
|
|
|
if( struct.source !== undefined || struct.target !== undefined ){ |
|
let srcId = toString(struct.source); |
|
let tgtId = toString(struct.target); |
|
let srcExists = srcId != null && cy.hasElementWithId( srcId ); |
|
let tgtExists = tgtId != null && cy.hasElementWithId( tgtId ); |
|
|
|
if( srcExists || tgtExists ){ |
|
cy.batch(() => { |
|
eles.remove( notifyRenderer, modifyPool ); |
|
eles.emitAndNotify('moveout'); |
|
|
|
for( let i = 0; i < eles.length; i++ ){ |
|
let ele = eles[i]; |
|
let data = ele._private.data; |
|
|
|
if( ele.isEdge() ){ |
|
if( srcExists ){ data.source = srcId; } |
|
|
|
if( tgtExists ){ data.target = tgtId; } |
|
} |
|
} |
|
|
|
eles.restore( notifyRenderer, modifyPool ); |
|
}); |
|
|
|
eles.emitAndNotify('move'); |
|
} |
|
|
|
} else if( struct.parent !== undefined ){ |
|
let parentId = toString(struct.parent); |
|
let parentExists = parentId === null || cy.hasElementWithId( parentId ); |
|
|
|
if( parentExists ){ |
|
let pidToAssign = parentId === null ? undefined : parentId; |
|
|
|
cy.batch(() => { |
|
let updated = eles.remove( notifyRenderer, modifyPool ); |
|
updated.emitAndNotify('moveout'); |
|
|
|
for( let i = 0; i < eles.length; i++ ){ |
|
let ele = eles[i]; |
|
let data = ele._private.data; |
|
|
|
if( ele.isNode() ){ |
|
data.parent = pidToAssign; |
|
} |
|
} |
|
|
|
updated.restore( notifyRenderer, modifyPool ); |
|
}); |
|
|
|
eles.emitAndNotify('move'); |
|
} |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
[ |
|
algorithms, |
|
animation, |
|
classNames, |
|
comparators, |
|
compounds, |
|
data, |
|
degree, |
|
dimensions, |
|
events, |
|
filter, |
|
group, |
|
iteration, |
|
layout, |
|
style, |
|
switchFunctions, |
|
traversing |
|
].forEach( function( props ){ |
|
util.extend( elesfn, props ); |
|
} ); |
|
|
|
export default Collection; |
|
|