@ -2,11 +2,44 @@ let util = require('../../../../util');
let ComponentUtil = require ( './ComponentUtil' ) . default ;
let LabelSplitter = require ( './LabelSplitter' ) . default ;
/ * *
* @ typedef { 'bold' | 'ital' | 'boldital' | 'mono' | 'normal' } MultiFontStyle
*
* The allowed specifiers of multi - fonts .
* /
/ * *
* @ typedef { { color : string , size : number , face : string , mod : string , vadjust : number } } MultiFontOptions
*
* The full set of options of a given multi - font .
* /
/ * *
* @ typedef { Array . < object > } Pile
*
* Sequence of option objects , the order is significant .
* The sequence is used to determine the value of a given option .
*
* Usage principles :
*
* - All search is done in the sequence of the pile .
* - As soon as a value is found , the searching stops .
* - prototypes are totally ignored . The idea is to add option objects used as prototypes
* to the pile , in the correct order .
* /
/ * *
* List of special styles for multi - fonts
* @ private
* /
const multiFontStyle = [ 'bold' , 'ital' , 'boldital' , 'mono' ] ;
/ * *
* A Label to be used for Nodes or Edges .
* /
class Label {
/ * *
* @ param { Object } body
* @ param { Object } options
@ -16,7 +49,7 @@ class Label {
this . body = body ;
this . pointToSelf = false ;
this . baseSize = undefined ;
this . fontOptions = { } ;
this . fontOptions = { } ; // instance variable containing the *instance-local* font options
this . setOptions ( options ) ;
this . size = { top : 0 , left : 0 , width : 0 , height : 0 , yLine : 0 } ; // could be cached
this . isEdgeLabel = edgelabel ;
@ -24,29 +57,26 @@ class Label {
/ * *
*
* @ param { Object } options
* @ param { boolean } [ allowDeletion = false ]
* @ param { Object } options the options of the parent Node - instance
* /
setOptions ( options , allowDeletion = false ) {
this . elementOptions = options ;
setOptions ( options ) {
this . elementOptions = options ; // Reference to the options of the parent Node-instance
// We want to keep the font options separated from the node options.
// The node options have to mirror the globals when they are not overruled.
this . fontOptions = util . deepExtend ( { } , options . font , true ) ;
this . initFontOptions ( options . font ) ;
if ( options . label !== undefined ) {
this . labelDirty = true ;
}
if ( options . font !== undefined ) {
Label . parseOptions ( this . fontOptions , options , allowDeletion ) ;
if ( options . font !== undefined && options . font !== null ) { // font options can be deleted at various levels
if ( typeof options . font === 'string' ) {
this . baseSize = this . fontOptions . size ;
}
else if ( typeof options . font === 'object' ) {
if ( options . font . size !== undefined ) {
this . baseSize = options . font . size ;
let size = options . font . size ;
if ( size !== undefined ) {
this . baseSize = size ;
}
}
}
@ -54,21 +84,33 @@ class Label {
/ * *
* Init the font Options structure .
*
* @ param { Object } parentOptions
* @ param { Object } newOptions
* @ param { boolean } [ allowDeletion = false ]
* @ static
* Member fontOptions serves as an accumulator for the current font options .
* As such , it needs to be completely separated from the node options .
*
* @ param { Object } newFontOptions the new font options to process
* @ private
* /
static parseOptions ( parentOptions , newOptions , allowDeletion = false ) {
if ( Label . parseFontString ( parentOptions , newOptions . font ) ) {
parentOptions . vadjust = 0 ;
}
else if ( typeof newOptions . font === 'object' ) {
util . fillIfDefined ( parentOptions , newOptions . font , allowDeletion ) ;
initFontOptions ( newFontOptions ) {
// Prepare the multi-font option objects.
// These will be filled in propagateFonts(), if required
util . forEach ( multiFontStyle , ( style ) => {
this . fontOptions [ style ] = { } ;
} ) ;
// Handle shorthand option, if present
if ( Label . parseFontString ( this . fontOptions , newFontOptions ) ) {
this . fontOptions . vadjust = 0 ;
return ;
}
parentOptions . size = Number ( parentOptions . size ) ;
parentOptions . vadjust = Number ( parentOptions . vadjust ) ;
// Copy over the non-multifont options, if specified
util . forEach ( newFontOptions , ( prop , n ) => {
if ( prop !== undefined && prop !== null && typeof prop !== 'object' ) {
this . fontOptions [ n ] = prop ;
}
} ) ;
}
@ -153,6 +195,7 @@ class Label {
update ( options , pile ) {
this . setOptions ( options , true ) ;
this . constrain ( pile ) ;
this . propagateFonts ( pile ) ;
this . fontOptions . chooser = ComponentUtil . choosify ( 'label' , pile ) ;
}
@ -176,142 +219,192 @@ class Label {
}
/////////////////////////////////////////////////////////
// Methods for handling options piles
// Eventually, these will be moved to a separate class
/////////////////////////////////////////////////////////
/ * *
* Collapse the font options for the multi - font to single objects , from
* the chain of option objects passed .
* Add the font members of the passed list of option objects to the pile .
*
* If an option for a specific multi - font is not present , the parent
* option is checked for the given option .
* @ param { Pile } dstPile pile of option objects add to
* @ param { Pile } srcPile pile of option objects to take font options from
* @ private
* /
addFontOptionsToPile ( dstPile , srcPile ) {
for ( let i = 0 ; i < srcPile . length ; ++ i ) {
this . addFontToPile ( dstPile , srcPile [ i ] ) ;
}
}
/ * *
* Add given font option object to the list of objects ( the 'pile' ) to consider for determining
* multi - font option values .
*
* NOTE : naming of 'groupOptions' is a misnomer ; the actual value passed
* is the new values to set from setOptions ( ) .
* @ param { Pile } pile pile of option objects to use
* @ param { object } options instance to add to pile
* @ private
* /
addFontToPile ( pile , options ) {
if ( options === undefined ) return ;
if ( options . font === undefined || options . font === null ) return ;
let item = options . font ;
pile . push ( item ) ;
}
/ * *
* Collect all own - property values from the font pile that aren ' t multi - font option objectss .
*
* @ param { Object } options
* @ param { Object } groupOptions
* @ param { Object } defaultOptions
* @ param { Pile } pile pile of option objects to use
* @ returns { object } object with all current own basic font propertie s
* @ private
* /
propagateFonts ( options , groupOptions , defaultOptions ) {
if ( ! this . fontOptions . multi ) return ;
/ * *
* Resolve the font options path .
* If valid , return a reference to the object in question .
* Otherwise , just return null .
*
* @ param { Object } options base object to determine path from
* @ param { 'bold' | 'ital' | 'boldital' | 'mono' | 'normal' } [ mod = undefined ] if present , sub path for the mod - font
* @ returns { Object | null }
* /
var pathP = function ( options , mod ) {
if ( ! options || ! options . font ) return null ;
var opt = options . font ;
if ( mod ) {
if ( ! opt [ mod ] ) return null ;
opt = opt [ mod ] ;
getBasicOptions ( pile ) {
let ret = { } ;
// Scans the whole pile to get all options present
for ( let n = 0 ; n < pile . length ; ++ n ) {
let fontOptions = pile [ n ] ;
// Convert shorthand if necessary
let tmpShorthand = { } ;
if ( Label . parseFontString ( tmpShorthand , fontOptions ) ) {
fontOptions = tmpShorthand ;
}
return opt ;
} ;
util . forEach ( fontOptions , ( opt , name ) => {
if ( opt === undefined ) return ; // multi-font option need not be present
if ( ret . hasOwnProperty ( name ) ) return ; // Keep first value we encounter
if ( multiFontStyle . indexOf ( name ) !== - 1 ) {
// Skip multi-font properties but we do need the structure
ret [ name ] = { } ;
} else {
ret [ name ] = opt ;
}
} ) ;
}
return ret ;
}
/ * *
* Return the value for given option for the given multi - font .
*
* All available option objects are trawled in the set order to construct the option values .
*
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
* # # Traversal of pile for multi - fonts
*
* The determination of multi - font option values is a special case , because any values not
* present in the multi - font options should by definition be taken from the main font options ,
* i . e . from the current 'parent' object of the multi - font option .
*
* # # # Search order for multi - fonts
*
* 'bold' used as example :
*
* - search in option group 'bold' in local properties
* - search in main font option group in local properties
*
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
*
* @ param { Pile } pile pile of option objects to use
* @ param { MultiFontStyle } multiName sub path for the multi - font
* @ param { string } option the option to search for , for the given multi - font
* @ returns { string | number } the value for the given option
* @ private
* /
getFontOption ( pile , multiName , option ) {
let multiFont ;
// Search multi font in local properties
for ( let n = 0 ; n < pile . length ; ++ n ) {
let fontOptions = pile [ n ] ;
if ( fontOptions . hasOwnProperty ( multiName ) ) {
multiFont = fontOptions [ multiName ] ;
if ( multiFont === undefined || multiFont === null ) continue ;
// Convert shorthand if necessary
// TODO: inefficient to do this conversion every time; find a better way.
let tmpShorthand = { } ;
if ( Label . parseFontString ( tmpShorthand , multiFont ) ) {
multiFont = tmpShorthand ;
}
/ * *
* Get property value from options . font [ mod ] [ property ] if present .
* If mod not passed , use property value from options . font [ property ] .
*
* @ param { Label . options } options
* @ param { 'bold' | 'ital' | 'boldital' | 'mono' | 'normal' } mod
* @ param { string } property
* @ return { * | null } value if found , null otherwise .
* /
var getP = function ( options , mod , property ) {
let opt = pathP ( options , mod ) ;
if ( opt && opt . hasOwnProperty ( property ) ) {
return opt [ property ] ;
if ( multiFont . hasOwnProperty ( option ) ) {
return multiFont [ option ] ;
}
}
}
return null ;
} ;
// Option is not mentioned in the multi font options; take it from the parent font options.
// These have already been converted with getBasicOptions(), so use the converted values.
if ( this . fontOptions . hasOwnProperty ( option ) ) {
return this . fontOptions [ option ] ;
}
// A value **must** be found; you should never get here.
throw new Error ( "Did not find value for multi-font for property: '" + option + "'" ) ;
}
let mods = [ 'bold' , 'ital' , 'boldital' , 'mono' ] ;
for ( const mod of mods ) {
let modOptions = this . fontOptions [ mod ] ;
let modDefaults = defaultOptions . font [ mod ] ;
/ * *
* Return all options values for the given multi - font .
*
* All available option objects are trawled in the set order to construct the option values .
*
* @ param { Pile } pile pile of option objects to use
* @ param { MultiFontStyle } multiName sub path for the mod - font
* @ returns { MultiFontOptions }
* @ private
* /
getFontOptions ( pile , multiName ) {
let result = { } ;
let optionNames = [ 'color' , 'size' , 'face' , 'mod' , 'vadjust' ] ; // List of allowed options per multi-font
if ( Label . parseFontString ( modOptions , pathP ( options , mod ) ) ) {
modOptions . vadjust = this . fontOptions . vadjust ;
modOptions . mod = modDefaults . mod ;
} else {
for ( let i = 0 ; i < optionNames . length ; ++ i ) {
let mod = optionNames [ i ] ;
result [ mod ] = this . getFontOption ( pile , multiName , mod ) ;
}
// We need to be crafty about loading the modded fonts. We want as
// much 'natural' versatility as we can get, so a simple global
// change propagates in an expected way, even if not stictly logical.
// 'face' has a special exception for mono, since we probably
// don't want to sync to the base font face.
modOptions . face =
getP ( options , mod , 'face' ) ||
getP ( groupOptions , mod , 'face' ) ||
( mod === 'mono' ? modDefaults . face : null ) ||
getP ( groupOptions , null , 'face' ) ||
this . fontOptions . face
;
// 'color' follows the standard flow
modOptions . color =
getP ( options , mod , 'color' ) ||
getP ( groupOptions , mod , 'color' ) ||
getP ( groupOptions , null , 'color' ) ||
this . fontOptions . color
;
// 'mode' follows the standard flow
modOptions . mod =
getP ( options , mod , 'mod' ) ||
getP ( groupOptions , mod , 'mod' ) ||
getP ( groupOptions , null , 'mod' ) ||
modDefaults . mod
;
// It's important that we size up defaults similarly if we're
// using default faces unless overriden. We want to preserve the
// ratios closely - but if faces have changed, all bets are off.
let ratio ;
// NOTE: Following condition always fails, because modDefaults
// has no explicit font property. This is deliberate, see
// var's 'NodesHandler.defaultOptions.font[mod]'.
// However, I want to keep the original logic while refactoring;
// it appears to be working fine even if ratio is never set.
// TODO: examine if this is a bug, fix if necessary.
//
if ( ( modOptions . face === modDefaults . face ) &&
( this . fontOptions . face === defaultOptions . font . face ) ) {
ratio = this . fontOptions . size / Number ( defaultOptions . font . size ) ;
}
return result ;
}
/////////////////////////////////////////////////////////
// End methods for handling options piles
/////////////////////////////////////////////////////////
modOptions . size =
getP ( options , mod , 'size' ) ||
getP ( groupOptions , mod , 'size' ) ||
( ratio ? modDefaults . size * ratio : null ) || // Scale the mod size using the same ratio
getP ( groupOptions , null , 'size' ) ||
this . fontOptions . size
;
modOptions . vadjust =
getP ( options , mod , 'vadjust' ) ||
getP ( groupOptions , mod , 'vadjust' ) ||
( ratio ? modDefaults . vadjust * Math . round ( ratio ) : null ) || // Scale it using the same ratio
this . fontOptions . vadjust
;
/ * *
* Collapse the font options for the multi - font to single objects , from
* the chain of option objects passed ( the 'pile' ) .
*
* @ param { Pile } pile sequence of option objects to consider .
* First item in list assumed to be the newly set options .
* /
propagateFonts ( pile ) {
let fontPile = [ ] ; // sequence of font objects to consider, order important
}
// Note that this.elementOptions is not used here.
this . addFontOptionsToPile ( fontPile , pile ) ;
this . fontOptions = this . getBasicOptions ( fontPile ) ;
// We set multifont values even if multi === false, for consistency (things break otherwise)
for ( let i = 0 ; i < multiFontStyle . length ; ++ i ) {
let mod = multiFontStyle [ i ] ;
let modOptions = this . fontOptions [ mod ] ;
let tmpMultiFontOptions = this . getFontOptions ( fontPile , mod ) ;
// Copy over found values
util . forEach ( tmpMultiFontOptions , ( option , n ) => {
modOptions [ n ] = option ;
} ) ;
modOptions . size = Number ( modOptions . size ) ;
modOptions . vadjust = Number ( modOptions . vadjust ) ;
@ -546,13 +639,13 @@ class Label {
* @ returns { { color , size , face , mod , vadjust , strokeWidth : * , strokeColor : ( * | string | allOptions . edges . font . strokeColor | { string } | allOptions . nodes . font . strokeColor | Array ) } }
* /
getFormattingValues ( ctx , selected , hover , mod ) {
var getValue = function ( fontOptions , mod , option ) {
let getValue = function ( fontOptions , mod , option ) {
if ( mod === "normal" ) {
if ( option === 'mod' ) return "" ;
return fontOptions [ option ] ;
}
if ( fontOptions [ mod ] [ option ] ) {
if ( fontOptions [ mod ] [ option ] !== undefined ) { // Grumbl leaving out test on undefined equals false for ""
return fontOptions [ mod ] [ option ] ;
} else {
// Take from parent font option
@ -578,7 +671,14 @@ class Label {
}
}
}
ctx . font = ( values . mod + " " + values . size + "px " + values . face ) . replace ( /"/g , "" ) ;
let fontString = "" ;
if ( values . mod !== undefined && values . mod !== "" ) { // safeguard for undefined - this happened
fontString += values . mod + " " ;
}
fontString += values . size + "px " + values . face ;
ctx . font = fontString . replace ( /"/g , "" ) ;
values . font = ctx . font ;
values . height = values . size ;
return values ;