UNPKG

25.6 kBJavaScriptView Raw
1import * as util from '../util';
2import * as is from '../is';
3import Promise from '../promise';
4
5const styfn = {};
6
7// keys for style blocks, e.g. ttfftt
8const TRUE = 't';
9const FALSE = 'f';
10
11// (potentially expensive calculation)
12// apply the style to the element based on
13// - its bypass
14// - what selectors match it
15styfn.apply = function( eles ){
16 let self = this;
17 let _p = self._private;
18 let cy = _p.cy;
19 let updatedEles = cy.collection();
20
21 if( _p.newStyle ){ // clear style caches
22 _p.contextStyles = {};
23 _p.propDiffs = {};
24
25 self.cleanElements( eles, true );
26 }
27
28 for( let ie = 0; ie < eles.length; ie++ ){
29 let ele = eles[ ie ];
30
31 let cxtMeta = self.getContextMeta( ele );
32
33 if( cxtMeta.empty ){
34 continue;
35 }
36
37 let cxtStyle = self.getContextStyle( cxtMeta );
38 let app = self.applyContextStyle( cxtMeta, cxtStyle, ele );
39
40 if( !_p.newStyle ){
41 self.updateTransitions( ele, app.diffProps );
42 }
43
44 let hintsDiff = self.updateStyleHints( ele );
45
46 if( hintsDiff ){
47 updatedEles.push( ele );
48 }
49
50 } // for elements
51
52 _p.newStyle = false;
53
54 return updatedEles;
55};
56
57styfn.getPropertiesDiff = function( oldCxtKey, newCxtKey ){
58 let self = this;
59 let cache = self._private.propDiffs = self._private.propDiffs || {};
60 let dualCxtKey = oldCxtKey + '-' + newCxtKey;
61 let cachedVal = cache[ dualCxtKey ];
62
63 if( cachedVal ){
64 return cachedVal;
65 }
66
67 let diffProps = [];
68 let addedProp = {};
69
70 for( let i = 0; i < self.length; i++ ){
71 let cxt = self[ i ];
72 let oldHasCxt = oldCxtKey[ i ] === TRUE;
73 let newHasCxt = newCxtKey[ i ] === TRUE;
74 let cxtHasDiffed = oldHasCxt !== newHasCxt;
75 let cxtHasMappedProps = cxt.mappedProperties.length > 0;
76
77 if( cxtHasDiffed || ( newHasCxt && cxtHasMappedProps )){
78 let props;
79
80 if( cxtHasDiffed && cxtHasMappedProps ){
81 props = cxt.properties; // suffices b/c mappedProperties is a subset of properties
82 } else if( cxtHasDiffed ){
83 props = cxt.properties; // need to check them all
84 } else if( cxtHasMappedProps ){
85 props = cxt.mappedProperties; // only need to check mapped
86 }
87
88 for( let j = 0; j < props.length; j++ ){
89 let prop = props[ j ];
90 let name = prop.name;
91
92 // if a later context overrides this property, then the fact that this context has switched/diffed doesn't matter
93 // (semi expensive check since it makes this function O(n^2) on context length, but worth it since overall result
94 // is cached)
95 let laterCxtOverrides = false;
96 for( let k = i + 1; k < self.length; k++ ){
97 let laterCxt = self[ k ];
98 let hasLaterCxt = newCxtKey[ k ] === TRUE;
99
100 if( !hasLaterCxt ){ continue; } // can't override unless the context is active
101
102 laterCxtOverrides = laterCxt.properties[ prop.name ] != null;
103
104 if( laterCxtOverrides ){ break; } // exit early as long as one later context overrides
105 }
106
107 if( !addedProp[ name ] && !laterCxtOverrides ){
108 addedProp[ name ] = true;
109 diffProps.push( name );
110 }
111 } // for props
112 } // if
113
114 } // for contexts
115
116 cache[ dualCxtKey ] = diffProps;
117 return diffProps;
118};
119
120styfn.getContextMeta = function( ele ){
121 let self = this;
122 let cxtKey = '';
123 let diffProps;
124 let prevKey = ele._private.styleCxtKey || '';
125
126 if( self._private.newStyle ){
127 prevKey = ''; // since we need to apply all style if a fresh stylesheet
128 }
129
130 // get the cxt key
131 for( let i = 0; i < self.length; i++ ){
132 let context = self[ i ];
133 let contextSelectorMatches = context.selector && context.selector.matches( ele ); // NB: context.selector may be null for 'core'
134
135 if( contextSelectorMatches ){
136 cxtKey += TRUE;
137 } else {
138 cxtKey += FALSE;
139 }
140 } // for context
141
142 diffProps = self.getPropertiesDiff( prevKey, cxtKey );
143
144 ele._private.styleCxtKey = cxtKey;
145
146 return {
147 key: cxtKey,
148 diffPropNames: diffProps,
149 empty: diffProps.length === 0
150 };
151};
152
153// gets a computed ele style object based on matched contexts
154styfn.getContextStyle = function( cxtMeta ){
155 let cxtKey = cxtMeta.key;
156 let self = this;
157 let cxtStyles = this._private.contextStyles = this._private.contextStyles || {};
158
159 // if already computed style, returned cached copy
160 if( cxtStyles[ cxtKey ] ){ return cxtStyles[ cxtKey ]; }
161
162 let style = {
163 _private: {
164 key: cxtKey
165 }
166 };
167
168 for( let i = 0; i < self.length; i++ ){
169 let cxt = self[ i ];
170 let hasCxt = cxtKey[ i ] === TRUE;
171
172 if( !hasCxt ){ continue; }
173
174 for( let j = 0; j < cxt.properties.length; j++ ){
175 let prop = cxt.properties[ j ];
176
177 style[ prop.name ] = prop;
178 }
179 }
180
181 cxtStyles[ cxtKey ] = style;
182 return style;
183};
184
185styfn.applyContextStyle = function( cxtMeta, cxtStyle, ele ){
186 let self = this;
187 let diffProps = cxtMeta.diffPropNames;
188 let retDiffProps = {};
189 let types = self.types;
190
191 for( let i = 0; i < diffProps.length; i++ ){
192 let diffPropName = diffProps[ i ];
193 let cxtProp = cxtStyle[ diffPropName ];
194 let eleProp = ele.pstyle( diffPropName );
195
196 if( !cxtProp ){ // no context prop means delete
197 if( !eleProp ){
198 continue; // no existing prop means nothing needs to be removed
199 // nb affects initial application on mapped values like control-point-distances
200 } else if( eleProp.bypass ){
201 cxtProp = { name: diffPropName, deleteBypassed: true };
202 } else {
203 cxtProp = { name: diffPropName, delete: true };
204 }
205 }
206
207 // save cycles when the context prop doesn't need to be applied
208 if( eleProp === cxtProp ){ continue; }
209
210 // save cycles when a mapped context prop doesn't need to be applied
211 if(
212 cxtProp.mapped === types.fn // context prop is function mapper
213 && eleProp != null // some props can be null even by default (e.g. a prop that overrides another one)
214 && eleProp.mapping != null // ele prop is a concrete value from from a mapper
215 && eleProp.mapping.value === cxtProp.value // the current prop on the ele is a flat prop value for the function mapper
216 ){ // NB don't write to cxtProp, as it's shared among eles (stored in stylesheet)
217 let mapping = eleProp.mapping; // can write to mapping, as it's a per-ele copy
218 let fnValue = mapping.fnValue = cxtProp.value( ele ); // temporarily cache the value in case of a miss
219
220 if( fnValue === mapping.prevFnValue ){ continue; }
221 }
222
223 let retDiffProp = retDiffProps[ diffPropName ] = {
224 prev: eleProp
225 };
226
227 self.applyParsedProperty( ele, cxtProp );
228
229 retDiffProp.next = ele.pstyle( diffPropName );
230
231 if( retDiffProp.next && retDiffProp.next.bypass ){
232 retDiffProp.next = retDiffProp.next.bypassed;
233 }
234 }
235
236 return {
237 diffProps: retDiffProps
238 };
239};
240
241styfn.updateStyleHints = function(ele){
242 let _p = ele._private;
243 let self = this;
244 let propNames = self.propertyGroupNames;
245 let propGrKeys = self.propertyGroupKeys;
246 let propHash = ( ele, propNames, seedKey ) => self.getPropertiesHash( ele, propNames, seedKey );
247 let oldStyleKey = _p.styleKey;
248
249 if( ele.removed() ){ return false; }
250
251 let isNode = _p.group === 'nodes';
252
253 // get the style key hashes per prop group
254 // but lazily -- only use non-default prop values to reduce the number of hashes
255 //
256
257 let overriddenStyles = ele._private.style;
258
259 propNames = Object.keys( overriddenStyles );
260
261 for( let i = 0; i < propGrKeys.length; i++ ){
262 let grKey = propGrKeys[i];
263
264 _p.styleKeys[ grKey ] = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
265 }
266
267 let updateGrKey1 = (val, grKey) => _p.styleKeys[ grKey ][0] = util.hashInt( val, _p.styleKeys[ grKey ][0] );
268 let updateGrKey2 = (val, grKey) => _p.styleKeys[ grKey ][1] = util.hashIntAlt( val, _p.styleKeys[ grKey ][1] );
269
270 let updateGrKey = (val, grKey) => {
271 updateGrKey1(val, grKey);
272 updateGrKey2(val, grKey);
273 };
274
275 let updateGrKeyWStr = (strVal, grKey) => {
276 for( let j = 0; j < strVal.length; j++ ){
277 let ch = strVal.charCodeAt(j);
278
279 updateGrKey1(ch, grKey);
280 updateGrKey2(ch, grKey);
281 }
282 };
283
284 // - hashing works on 32 bit ints b/c we use bitwise ops
285 // - small numbers get cut off (e.g. 0.123 is seen as 0 by the hashing function)
286 // - raise up small numbers so more significant digits are seen by hashing
287 // - make small numbers larger than a normal value to avoid collisions
288 // - works in practice and it's relatively cheap
289 let N = 2000000000;
290 let cleanNum = val => (-128 < val && val < 128) && Math.floor(val) !== val ? N - ((val * 1024) | 0) : val;
291
292 for( let i = 0; i < propNames.length; i++ ){
293 let name = propNames[i];
294 let parsedProp = overriddenStyles[ name ];
295
296 if( parsedProp == null ){ continue; }
297
298 let propInfo = this.properties[name];
299 let type = propInfo.type;
300 let grKey = propInfo.groupKey;
301 let normalizedNumberVal;
302
303 if( propInfo.hashOverride != null ){
304 normalizedNumberVal = propInfo.hashOverride(ele, parsedProp);
305 } else if( parsedProp.pfValue != null ){
306 normalizedNumberVal = parsedProp.pfValue;
307 }
308
309 // might not be a number if it allows enums
310 let numberVal = propInfo.enums == null ? parsedProp.value : null;
311 let haveNormNum = normalizedNumberVal != null;
312 let haveUnitedNum = numberVal != null;
313 let haveNum = haveNormNum || haveUnitedNum;
314 let units = parsedProp.units;
315
316 // numbers are cheaper to hash than strings
317 // 1 hash op vs n hash ops (for length n string)
318 if( type.number && haveNum && !type.multiple ){
319 let v = haveNormNum ? normalizedNumberVal : numberVal;
320
321 updateGrKey(cleanNum(v), grKey);
322
323 if( !haveNormNum && units != null ){
324 updateGrKeyWStr(units, grKey);
325 }
326 } else {
327 updateGrKeyWStr(parsedProp.strValue, grKey);
328 }
329 }
330
331 // overall style key
332 //
333
334 let hash = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
335
336 for( let i = 0; i < propGrKeys.length; i++ ){
337 let grKey = propGrKeys[i];
338 let grHash = _p.styleKeys[ grKey ];
339
340 hash[0] = util.hashInt( grHash[0], hash[0] );
341 hash[1] = util.hashIntAlt( grHash[1], hash[1] );
342 }
343
344 _p.styleKey = util.combineHashes(hash[0], hash[1]);
345
346 // label dims
347 //
348
349 let sk = _p.styleKeys;
350
351 _p.labelDimsKey = util.combineHashesArray(sk.labelDimensions);
352
353 let labelKeys = propHash( ele, ['label'], sk.labelDimensions );
354
355 _p.labelKey = util.combineHashesArray(labelKeys);
356 _p.labelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, labelKeys));
357
358 if( !isNode ){
359 let sourceLabelKeys = propHash( ele, ['source-label'], sk.labelDimensions );
360 _p.sourceLabelKey = util.combineHashesArray(sourceLabelKeys);
361 _p.sourceLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, sourceLabelKeys));
362
363 let targetLabelKeys = propHash( ele, ['target-label'], sk.labelDimensions );
364 _p.targetLabelKey = util.combineHashesArray(targetLabelKeys);
365 _p.targetLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, targetLabelKeys));
366 }
367
368 // node
369 //
370
371 if( isNode ){
372 let { nodeBody, nodeBorder, backgroundImage, compound, pie } = _p.styleKeys;
373
374 let nodeKeys = [ nodeBody, nodeBorder, backgroundImage, compound, pie ].filter(k => k != null).reduce(util.hashArrays, [
375 util.DEFAULT_HASH_SEED,
376 util.DEFAULT_HASH_SEED_ALT
377 ]);
378 _p.nodeKey = util.combineHashesArray(nodeKeys);
379
380 _p.hasPie = pie != null && pie[0] !== util.DEFAULT_HASH_SEED && pie[1] !== util.DEFAULT_HASH_SEED_ALT;
381 }
382
383 return oldStyleKey !== _p.styleKey;
384};
385
386styfn.clearStyleHints = function(ele){
387 let _p = ele._private;
388
389 _p.styleKeys = {};
390 _p.styleKey = null;
391 _p.labelKey = null;
392 _p.labelStyleKey = null;
393 _p.sourceLabelKey = null;
394 _p.sourceLabelStyleKey = null;
395 _p.targetLabelKey = null;
396 _p.targetLabelStyleKey = null;
397 _p.nodeKey = null;
398 _p.hasPie = null;
399};
400
401// apply a property to the style (for internal use)
402// returns whether application was successful
403//
404// now, this function flattens the property, and here's how:
405//
406// for parsedProp:{ bypass: true, deleteBypass: true }
407// no property is generated, instead the bypass property in the
408// element's style is replaced by what's pointed to by the `bypassed`
409// field in the bypass property (i.e. restoring the property the
410// bypass was overriding)
411//
412// for parsedProp:{ mapped: truthy }
413// the generated flattenedProp:{ mapping: prop }
414//
415// for parsedProp:{ bypass: true }
416// the generated flattenedProp:{ bypassed: parsedProp }
417styfn.applyParsedProperty = function( ele, parsedProp ){
418 let self = this;
419 let prop = parsedProp;
420 let style = ele._private.style;
421 let flatProp;
422 let types = self.types;
423 let type = self.properties[ prop.name ].type;
424 let propIsBypass = prop.bypass;
425 let origProp = style[ prop.name ];
426 let origPropIsBypass = origProp && origProp.bypass;
427 let _p = ele._private;
428 let flatPropMapping = 'mapping';
429
430 let getVal = p => {
431 if( p == null ){
432 return null;
433 } else if( p.pfValue != null ){
434 return p.pfValue;
435 } else {
436 return p.value;
437 }
438 };
439
440 let checkTriggers = () => {
441 let fromVal = getVal(origProp);
442 let toVal = getVal(prop);
443
444 self.checkTriggers( ele, prop.name, fromVal, toVal );
445 };
446
447 // edge sanity checks to prevent the client from making serious mistakes
448 if(
449 parsedProp.name === 'curve-style'
450 && ele.isEdge()
451 && (
452 ( // loops must be bundled beziers
453 parsedProp.value !== 'bezier'
454 && ele.isLoop()
455 ) || ( // edges connected to compound nodes can not be haystacks
456 parsedProp.value === 'haystack'
457 && ( ele.source().isParent() || ele.target().isParent() )
458 )
459 )
460 ){
461 prop = parsedProp = this.parse( parsedProp.name, 'bezier', propIsBypass );
462 }
463
464 if( prop.delete ){ // delete the property and use the default value on falsey value
465 style[ prop.name ] = undefined;
466
467 checkTriggers();
468
469 return true;
470 }
471
472 if( prop.deleteBypassed ){ // delete the property that the
473 if( !origProp ){
474 checkTriggers();
475
476 return true; // can't delete if no prop
477
478 } else if( origProp.bypass ){ // delete bypassed
479 origProp.bypassed = undefined;
480
481 checkTriggers();
482
483 return true;
484
485 } else {
486 return false; // we're unsuccessful deleting the bypassed
487 }
488 }
489
490 // check if we need to delete the current bypass
491 if( prop.deleteBypass ){ // then this property is just here to indicate we need to delete
492 if( !origProp ){
493 checkTriggers();
494
495 return true; // property is already not defined
496
497 } else if( origProp.bypass ){ // then replace the bypass property with the original
498 // because the bypassed property was already applied (and therefore parsed), we can just replace it (no reapplying necessary)
499 style[ prop.name ] = origProp.bypassed;
500
501 checkTriggers();
502
503 return true;
504
505 } else {
506 return false; // we're unsuccessful deleting the bypass
507 }
508 }
509
510 let printMappingErr = function(){
511 util.warn( 'Do not assign mappings to elements without corresponding data (i.e. ele `' + ele.id() + '` has no mapping for property `' + prop.name + '` with data field `' + prop.field + '`); try a `[' + prop.field + ']` selector to limit scope to elements with `' + prop.field + '` defined' );
512 };
513
514 // put the property in the style objects
515 switch( prop.mapped ){ // flatten the property if mapped
516 case types.mapData: {
517 // flatten the field (e.g. data.foo.bar)
518 let fields = prop.field.split( '.' );
519 let fieldVal = _p.data;
520
521 for( let i = 0; i < fields.length && fieldVal; i++ ){
522 let field = fields[ i ];
523 fieldVal = fieldVal[ field ];
524 }
525
526 if( fieldVal == null ){
527 printMappingErr();
528 return false;
529 }
530
531 let percent;
532 if( !is.number( fieldVal ) ){ // then don't apply and fall back on the existing style
533 util.warn('Do not use continuous mappers without specifying numeric data (i.e. `' + prop.field + ': ' + fieldVal + '` for `' + ele.id() + '` is non-numeric)');
534 return false;
535 } else {
536 let fieldWidth = prop.fieldMax - prop.fieldMin;
537
538 if( fieldWidth === 0 ){ // safety check -- not strictly necessary as no props of zero range should be passed here
539 percent = 0;
540 } else {
541 percent = (fieldVal - prop.fieldMin) / fieldWidth;
542 }
543 }
544
545 // make sure to bound percent value
546 if( percent < 0 ){
547 percent = 0;
548 } else if( percent > 1 ){
549 percent = 1;
550 }
551
552 if( type.color ){
553 let r1 = prop.valueMin[0];
554 let r2 = prop.valueMax[0];
555 let g1 = prop.valueMin[1];
556 let g2 = prop.valueMax[1];
557 let b1 = prop.valueMin[2];
558 let b2 = prop.valueMax[2];
559 let a1 = prop.valueMin[3] == null ? 1 : prop.valueMin[3];
560 let a2 = prop.valueMax[3] == null ? 1 : prop.valueMax[3];
561
562 let clr = [
563 Math.round( r1 + (r2 - r1) * percent ),
564 Math.round( g1 + (g2 - g1) * percent ),
565 Math.round( b1 + (b2 - b1) * percent ),
566 Math.round( a1 + (a2 - a1) * percent )
567 ];
568
569 flatProp = { // colours are simple, so just create the flat property instead of expensive string parsing
570 bypass: prop.bypass, // we're a bypass if the mapping property is a bypass
571 name: prop.name,
572 value: clr,
573 strValue: 'rgb(' + clr[0] + ', ' + clr[1] + ', ' + clr[2] + ')'
574 };
575
576 } else if( type.number ){
577 let calcValue = prop.valueMin + (prop.valueMax - prop.valueMin) * percent;
578 flatProp = this.parse( prop.name, calcValue, prop.bypass, flatPropMapping );
579
580 } else {
581 return false; // can only map to colours and numbers
582 }
583
584 if( !flatProp ){ // if we can't flatten the property, then don't apply the property and fall back on the existing style
585 printMappingErr();
586 return false;
587 }
588
589 flatProp.mapping = prop; // keep a reference to the mapping
590 prop = flatProp; // the flattened (mapped) property is the one we want
591
592 break;
593 }
594
595 // direct mapping
596 case types.data: {
597 // flatten the field (e.g. data.foo.bar)
598 let fields = prop.field.split( '.' );
599 let fieldVal = _p.data;
600
601 for( let i = 0; i < fields.length && fieldVal; i++ ){
602 let field = fields[ i ];
603 fieldVal = fieldVal[ field ];
604 }
605
606 if( fieldVal != null ){
607 flatProp = this.parse( prop.name, fieldVal, prop.bypass, flatPropMapping );
608 }
609
610 if( !flatProp ){ // if we can't flatten the property, then don't apply and fall back on the existing style
611 printMappingErr();
612 return false;
613 }
614
615 flatProp.mapping = prop; // keep a reference to the mapping
616 prop = flatProp; // the flattened (mapped) property is the one we want
617
618 break;
619 }
620
621 case types.fn: {
622 let fn = prop.value;
623 let fnRetVal = prop.fnValue != null ? prop.fnValue : fn( ele ); // check for cached value before calling function
624
625 prop.prevFnValue = fnRetVal;
626
627 if( fnRetVal == null ){
628 util.warn('Custom function mappers may not return null (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is null)');
629 return false;
630 }
631
632 flatProp = this.parse( prop.name, fnRetVal, prop.bypass, flatPropMapping );
633
634 if( !flatProp ){
635 util.warn('Custom function mappers may not return invalid values for the property type (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is invalid)');
636 return false;
637 }
638
639 flatProp.mapping = util.copy( prop ); // keep a reference to the mapping
640 prop = flatProp; // the flattened (mapped) property is the one we want
641
642 break;
643 }
644
645 case undefined:
646 break; // just set the property
647
648 default:
649 return false; // not a valid mapping
650 }
651
652 // if the property is a bypass property, then link the resultant property to the original one
653 if( propIsBypass ){
654 if( origPropIsBypass ){ // then this bypass overrides the existing one
655 prop.bypassed = origProp.bypassed; // steal bypassed prop from old bypass
656 } else { // then link the orig prop to the new bypass
657 prop.bypassed = origProp;
658 }
659
660 style[ prop.name ] = prop; // and set
661
662 } else { // prop is not bypass
663 if( origPropIsBypass ){ // then keep the orig prop (since it's a bypass) and link to the new prop
664 origProp.bypassed = prop;
665 } else { // then just replace the old prop with the new one
666 style[ prop.name ] = prop;
667 }
668 }
669
670 checkTriggers();
671
672 return true;
673};
674
675styfn.cleanElements = function( eles, keepBypasses ){
676 for( let i = 0; i < eles.length; i++ ){
677 let ele = eles[i];
678
679 this.clearStyleHints(ele);
680
681 ele.dirtyCompoundBoundsCache();
682 ele.dirtyBoundingBoxCache();
683
684 if( !keepBypasses ){
685 ele._private.style = {};
686 } else {
687 let style = ele._private.style;
688 let propNames = Object.keys(style);
689
690 for( let j = 0; j < propNames.length; j++ ){
691 let propName = propNames[j];
692 let eleProp = style[ propName ];
693
694 if( eleProp != null ){
695 if( eleProp.bypass ){
696 eleProp.bypassed = null;
697 } else {
698 style[ propName ] = null;
699 }
700 }
701 }
702 }
703 }
704};
705
706// updates the visual style for all elements (useful for manual style modification after init)
707styfn.update = function(){
708 let cy = this._private.cy;
709 let eles = cy.mutableElements();
710
711 eles.updateStyle();
712};
713
714// diffProps : { name => { prev, next } }
715styfn.updateTransitions = function( ele, diffProps ){
716 let self = this;
717 let _p = ele._private;
718 let props = ele.pstyle( 'transition-property' ).value;
719 let duration = ele.pstyle( 'transition-duration' ).pfValue;
720 let delay = ele.pstyle( 'transition-delay' ).pfValue;
721
722 if( props.length > 0 && duration > 0 ){
723
724 let style = {};
725
726 // build up the style to animate towards
727 let anyPrev = false;
728 for( let i = 0; i < props.length; i++ ){
729 let prop = props[ i ];
730 let styProp = ele.pstyle( prop );
731 let diffProp = diffProps[ prop ];
732
733 if( !diffProp ){ continue; }
734
735 let prevProp = diffProp.prev;
736 let fromProp = prevProp;
737 let toProp = diffProp.next != null ? diffProp.next : styProp;
738 let diff = false;
739 let initVal;
740 let initDt = 0.000001; // delta time % value for initVal (allows animating out of init zero opacity)
741
742 if( !fromProp ){ continue; }
743
744 // consider px values
745 if( is.number( fromProp.pfValue ) && is.number( toProp.pfValue ) ){
746 diff = toProp.pfValue - fromProp.pfValue; // nonzero is truthy
747 initVal = fromProp.pfValue + initDt * diff;
748
749 // consider numerical values
750 } else if( is.number( fromProp.value ) && is.number( toProp.value ) ){
751 diff = toProp.value - fromProp.value; // nonzero is truthy
752 initVal = fromProp.value + initDt * diff;
753
754 // consider colour values
755 } else if( is.array( fromProp.value ) && is.array( toProp.value ) ){
756 diff = fromProp.value[0] !== toProp.value[0]
757 || fromProp.value[1] !== toProp.value[1]
758 || fromProp.value[2] !== toProp.value[2]
759 ;
760
761 initVal = fromProp.strValue;
762 }
763
764 // the previous value is good for an animation only if it's different
765 if( diff ){
766 style[ prop ] = toProp.strValue; // to val
767 this.applyBypass( ele, prop, initVal ); // from val
768 anyPrev = true;
769 }
770
771 } // end if props allow ani
772
773 // can't transition if there's nothing previous to transition from
774 if( !anyPrev ){ return; }
775
776 _p.transitioning = true;
777
778 ( new Promise(function( resolve ){
779 if( delay > 0 ){
780 ele.delayAnimation( delay ).play().promise().then( resolve );
781 } else {
782 resolve();
783 }
784 }) ).then(function(){
785 return ele.animation( {
786 style: style,
787 duration: duration,
788 easing: ele.pstyle( 'transition-timing-function' ).value,
789 queue: false
790 } ).play().promise();
791 }).then(function(){
792 // if( !isBypass ){
793 self.removeBypasses( ele, props );
794 ele.emitAndNotify('style');
795 // }
796
797 _p.transitioning = false;
798 });
799
800 } else if( _p.transitioning ){
801 this.removeBypasses( ele, props );
802 ele.emitAndNotify('style');
803
804 _p.transitioning = false;
805 }
806};
807
808styfn.checkTrigger = function( ele, name, fromValue, toValue, getTrigger, onTrigger ){
809 let prop = this.properties[ name ];
810 let triggerCheck = getTrigger( prop );
811
812 if( triggerCheck != null && triggerCheck( fromValue, toValue ) ){
813 onTrigger(prop);
814 }
815};
816
817styfn.checkZOrderTrigger = function( ele, name, fromValue, toValue ){
818 this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersZOrder, () => {
819 this._private.cy.notify('zorder', ele);
820 });
821};
822
823styfn.checkBoundsTrigger = function( ele, name, fromValue, toValue ){
824 this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBounds, prop => {
825 ele.dirtyCompoundBoundsCache();
826 ele.dirtyBoundingBoxCache();
827
828 // if the prop change makes the bb of pll bezier edges invalid,
829 // then dirty the pll edge bb cache as well
830 if( // only for beziers -- so performance of other edges isn't affected
831 ( name === 'curve-style' && (fromValue === 'bezier' || toValue === 'bezier') )
832 && prop.triggersBoundsOfParallelBeziers
833 ){
834 ele.parallelEdges().forEach(pllEdge => {
835 if( pllEdge.isBundledBezier() ){
836 pllEdge.dirtyBoundingBoxCache();
837 }
838 });
839 }
840 } );
841};
842
843styfn.checkTriggers = function( ele, name, fromValue, toValue ){
844 ele.dirtyStyleCache();
845
846 this.checkZOrderTrigger( ele, name, fromValue, toValue );
847 this.checkBoundsTrigger( ele, name, fromValue, toValue );
848};
849
850export default styfn;