diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index 66ed8ff0..e28c13c1 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -1,9 +1,7 @@ var util = require("../../util"); var DataSet = require('../../DataSet'); var DataView = require('../../DataView'); - var Edge = require("./components/Edge").default; -var Label = require("./components/shared/Label").default; /** * Handler for Edges @@ -31,7 +29,7 @@ class EdgesHandler { this.options = {}; this.defaultOptions = { arrows: { - to: {enabled: false, scaleFactor:1, type: 'arrow'}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + to: {enabled: false, scaleFactor:1, type: 'arrow'},// boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} middle: {enabled: false, scaleFactor:1, type: 'arrow'}, from: {enabled: false, scaleFactor:1, type: 'arrow'} }, @@ -193,7 +191,6 @@ class EdgesHandler { * @param {Object} options */ setOptions(options) { - this.edgeOptions = options; if (options !== undefined) { // use the parser from the Edge class to fill in all shorthand notations Edge.parseOptions(this.options, options, true, this.defaultOptions, true); @@ -210,8 +207,6 @@ class EdgesHandler { // update fonts in all edges if (options.font !== undefined) { - // use the parser from the Label class to fill in all shorthand notations - Label.parseOptions(this.options.font, options); for (let edgeId in this.body.edges) { if (this.body.edges.hasOwnProperty(edgeId)) { this.body.edges[edgeId].updateLabelModule(); @@ -383,17 +378,7 @@ class EdgesHandler { * @returns {Edge} */ create(properties) { - // It is not at all clear why all these separate options should be passed: - // - // - this.edgeOptions is set in setOptions() - // the value of which is also added to this.options with parseOptions() - // - this.defaultOptions has been added to this.options with util.extend() in ctor - // - // So, in theory, this.options should be enough. - // The only reason I can think of for this, is that precedence is important. - // TODO: make unit tests for this, to check if edgeOptions and defaultOptions are redundant - // - return new Edge(properties, this.body, this.options, this.defaultOptions, this.edgeOptions) + return new Edge(properties, this.body, this.options, this.defaultOptions) } /** diff --git a/lib/network/modules/Groups.js b/lib/network/modules/Groups.js index 4e326567..bddd13ad 100644 --- a/lib/network/modules/Groups.js +++ b/lib/network/modules/Groups.js @@ -75,14 +75,17 @@ class Groups { } /** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options + * Get group options of a groupname. + * If groupname is not found, a new group may be created. + * + * @param {*} groupname Can be a number, string, Date, etc. + * @param {boolean} [shouldCreate=true] If true, create a new group + * @return {Object} The found or created group */ - get(groupname) { + get(groupname, shouldCreate = true) { let group = this.groups[groupname]; - if (group === undefined) { + + if (group === undefined && shouldCreate) { if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { // create new group let index = this.groupIndex % this.groupsArray.length; diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index 6f820419..c726df33 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -1,9 +1,8 @@ let util = require("../../util"); let DataSet = require('../../DataSet'); let DataView = require('../../DataView'); - var Node = require("./components/Node").default; -var Label = require("./components/shared/Label").default; + /** * Handler for Nodes @@ -30,7 +29,6 @@ class NodesHandler { remove: (event, params) => { this.remove(params.items); } }; - this.options = {}; this.defaultOptions = { borderWidth: 1, borderWidthSelected: 2, @@ -144,7 +142,7 @@ class NodesHandler { throw 'Internal error: mass in defaultOptions of NodesHandler may not be zero or negative'; } - util.extend(this.options, this.defaultOptions); + this.options = util.bridgeObject(this.defaultOptions); this.bindEventListeners(); } @@ -174,7 +172,6 @@ class NodesHandler { * @param {Object} options */ setOptions(options) { - this.nodeOptions = options; if (options !== undefined) { Node.parseOptions(this.options, options); @@ -189,7 +186,6 @@ class NodesHandler { // update the font in all nodes if (options.font !== undefined) { - Label.parseOptions(this.options.font, options); for (let nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { this.body.nodes[nodeId].updateLabelModule(); @@ -359,7 +355,7 @@ class NodesHandler { * @returns {*} */ create(properties, constructorClass = Node) { - return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions, this.nodeOptions) + return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions) } diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index 997aad43..ab5988e5 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -16,9 +16,8 @@ class Edge { * @param {Object} body shared state from Network instance * @param {Object} globalOptions options from the EdgesHandler instance * @param {Object} defaultOptions default options from the EdgeHandler instance. Value and reference are constant - * @param {Object} edgeOptions option values specific for edges. */ - constructor(options, body, globalOptions, defaultOptions, edgeOptions) { + constructor(options, body, globalOptions, defaultOptions) { if (body === undefined) { throw new Error("No body provided"); } @@ -26,11 +25,9 @@ class Edge { // Since globalOptions is constant in values as well as reference, // Following needs to be done only once. - this.options = util.bridgeObject(globalOptions); this.globalOptions = globalOptions; this.defaultOptions = defaultOptions; - this.edgeOptions = edgeOptions; this.body = body; // initialize variables @@ -84,12 +81,11 @@ class Edge { options.value = parseFloat(options.value); } - let pile = [options, this.options, this.edgeOptions, this.defaultOptions]; + let pile = [options, this.options, this.defaultOptions]; this.chooser = ComponentUtil.choosify('edge', pile); // update label Module this.updateLabelModule(options); - this.labelModule.propagateFonts(this.edgeOptions, options, this.defaultOptions); let dataChanged = this.updateEdgeType(); @@ -134,7 +130,9 @@ class Edge { 'to', 'title', 'value', - 'width' + 'width', + 'font', + 'chosen' ]; // only deep extend the items in the field array. These do not have shorthand. @@ -228,11 +226,7 @@ class Edge { parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options } - // handle the font settings - if (newOptions.font !== undefined && newOptions.font !== null) { - Label.parseOptions(parentOptions.font, newOptions); - } - else if (allowDeletion === true && newOptions.font === null) { + if (allowDeletion === true && newOptions.font === null) { parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options } } @@ -321,7 +315,13 @@ class Edge { * @param {Object} options */ updateLabelModule(options) { - let pile = [options, this.edgeOptions, this.defaultOptions]; + let pile = [ + options, + this.options, + this.globalOptions, // Currently set global edge options + this.defaultOptions + ]; + this.labelModule.update(this.options, pile); if (this.labelModule.baseSize !== undefined) { diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index 19c8013b..3484bc7a 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -32,29 +32,22 @@ class Node { * {string} label Text label for the node * {number} x Horizontal position of the node * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "icon" + * {string} shape Node shape * {string} image An image url - * {string} title An title text, can be HTML + * {string} title A title text, can be HTML * {anytype} group A group name or number - * @param {Object} body - * @param {Network.Images} imagelist A list with images. Only needed - * when the node has an image - * @param {Groups} grouplist A list with groups. Needed for - * retrieving group options - * @param {Object} globalOptions An object with default values for - * example for the color - * @param {Object} defaultOptions - * @param {Object} nodeOptions + * + * @param {Object} body Shared state of current network instance + * @param {Network.Images} imagelist A list with images. Only needed when the node has an image + * @param {Groups} grouplist A list with groups. Needed for retrieving group options + * @param {Object} globalOptions Current global node options; these serve as defaults for the node instance + * @param {Object} defaultOptions Global default options for nodes; note that this is also the prototype + * for parameter `globalOptions`. */ - constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions, nodeOptions) { + constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions) { this.options = util.bridgeObject(globalOptions); this.globalOptions = globalOptions; this.defaultOptions = defaultOptions; - this.nodeOptions = nodeOptions; this.body = body; this.edges = []; // all edges connected to this node @@ -136,16 +129,8 @@ class Node { if (options.size !== undefined) {this.baseSize = options.size;} if (options.value !== undefined) {options.value = parseFloat(options.value);} - // copy group options - if (typeof options.group === 'number' || (typeof options.group === 'string' && options.group != '')) { - var groupObj = this.grouplist.get(options.group); - util.deepExtend(this.options, groupObj); - // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. - this.options.color = util.parseColor(this.options.color); - } - // this transforms all shorthands into fully defined options - Node.parseOptions(this.options, options, true, this.globalOptions); + Node.parseOptions(this.options, options, true, this.globalOptions, this.grouplist); let pile = [options, this.options, this.defaultOptions]; this.chooser = ComponentUtil.choosify('node', pile); @@ -153,7 +138,6 @@ class Node { this._load_images(); this.updateLabelModule(options); this.updateShape(currentShape); - this.labelModule.propagateFonts(this.nodeOptions, options, this.defaultOptions); return (options.hidden !== undefined || options.physics !== undefined); } @@ -199,6 +183,43 @@ class Node { } + /** + * Copy group option values into the node options. + * + * The group options override the global node options, so the copy of group options + * must happen *after* the global node options have been set. + * + * This method must also be called also if the global node options have changed and the group options did not. + * + * @param {Object} parentOptions + * @param {Object} newOptions new values for the options, currently only passed in for check + * @param {Object} groupList + */ + static updateGroupOptions(parentOptions, newOptions, groupList) { + if (groupList === undefined) return; // No groups, nothing to do + + var group = parentOptions.group; + + // paranoia: the selected group is already merged into node options, check. + if (newOptions !== undefined && newOptions.group !== undefined && group !== newOptions.group) { + throw new Error("updateGroupOptions: group values in options don't match."); + } + + var hasGroup = (typeof group === 'number' || (typeof group === 'string' && group != '')); + if (!hasGroup) return; // current node has no group, no need to merge + + var groupObj = groupList.get(group); + + // Skip merging of group font options into parent; these are required to be distinct for labels + // TODO: It might not be a good idea either to merge the rest of the options, investigate this. + util.selectiveNotDeepExtend(['font'], parentOptions, groupObj); + + // the color object needs to be completely defined. + // Since groups can partially overwrite the colors, we parse it again, just in case. + parentOptions.color = util.parseColor(parentOptions.color); + } + + /** * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. * Static so it can also be used by the handler. @@ -207,12 +228,13 @@ class Node { * @param {Object} newOptions * @param {boolean} [allowDeletion=false] * @param {Object} [globalOptions={}] + * @param {Object} [groupList] * @static */ - static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) { + static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}, groupList) { + var fields = [ 'color', - 'font', 'fixed', 'shadow' ]; @@ -248,14 +270,12 @@ class Node { } } - // handle the font options - if (newOptions.font !== undefined && newOptions.font !== null) { - Label.parseOptions(parentOptions.font, newOptions); - } - else if (allowDeletion === true && newOptions.font === null) { + if (allowDeletion === true && newOptions.font === null) { parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options } + Node.updateGroupOptions(parentOptions, newOptions, groupList); + // handle the scaling options, specifically the label part if (newOptions.scaling !== undefined) { util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', globalOptions.scaling); @@ -319,7 +339,27 @@ class Node { if (this.options.label === undefined || this.options.label === null) { this.options.label = ''; } - let pile = [options, this.nodeOptions, this.defaultOptions]; + + Node.updateGroupOptions(this.options, options, this.grouplist); + + // + // Note:The prototype chain for this.options is: + // + // this.options -> NodesHandler.options -> NodesHandler.defaultOptions + // (also: this.globalOptions) + // + // Note that the prototypes are mentioned explicitly in the pile list below; + // WE DON'T WANT THE ORDER OF THE PROTOTYPES!!!! At least, not for font handling of labels. + // This is a good indication that the prototype usage of options is deficient. + // + var currentGroup = this.grouplist.get(this.options.group, false); + let pile = [ + options, // new options + this.options, // current node options, see comment above for prototype + currentGroup, // group options, if any + this.globalOptions, // Currently set global node options + this.defaultOptions // Default global node options + ]; this.labelModule.update(this.options, pile); if (this.labelModule.baseSize !== undefined) { diff --git a/lib/network/modules/components/nodes/Cluster.js b/lib/network/modules/components/nodes/Cluster.js index b05ae50a..0f250477 100644 --- a/lib/network/modules/components/nodes/Cluster.js +++ b/lib/network/modules/components/nodes/Cluster.js @@ -14,9 +14,10 @@ class Cluster extends Node { * @param {Array.}imagelist * @param {Array} grouplist * @param {Object} globalOptions + * @param {Object} defaultOptions Global default options for nodes */ - constructor(options, body, imagelist, grouplist, globalOptions) { - super(options, body, imagelist, grouplist, globalOptions); + constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions) { + super(options, body, imagelist, grouplist, globalOptions, defaultOptions); this.isCluster = true; this.containedNodes = {}; diff --git a/lib/network/modules/components/shared/Label.js b/lib/network/modules/components/shared/Label.js index d214058e..7a98af32 100644 --- a/lib/network/modules/components/shared/Label.js +++ b/lib/network/modules/components/shared/Label.js @@ -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.} 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 properties + * @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; diff --git a/lib/network/modules/components/shared/LabelSplitter.js b/lib/network/modules/components/shared/LabelSplitter.js index b0ccba2c..636f54de 100644 --- a/lib/network/modules/components/shared/LabelSplitter.js +++ b/lib/network/modules/components/shared/LabelSplitter.js @@ -71,6 +71,8 @@ class LabelSplitter { return this.lines.finalize(); } + var font = this.parent.fontOptions; + // Normalize the end-of-line's to a single representation - order important text = text.replace(/\r\n/g, '\n'); // Dos EOL's text = text.replace(/\r/g, '\n'); // Mac EOL's @@ -81,10 +83,10 @@ class LabelSplitter { let nlLines = String(text).split('\n'); let lineCount = nlLines.length; - if (this.parent.elementOptions.font.multi) { + if (font.multi) { // Multi-font case: styling tags active for (let i = 0; i < lineCount; i++) { - let blocks = this.splitBlocks(nlLines[i], this.parent.elementOptions.font.multi); + let blocks = this.splitBlocks(nlLines[i], font.multi); // Post: Sequences of tabs and spaces are reduced to single space if (blocks === undefined) continue; @@ -94,7 +96,7 @@ class LabelSplitter { continue; } - if (this.parent.fontOptions.maxWdt > 0) { + if (font.maxWdt > 0) { // widthConstraint.maximum defined //console.log('Running widthConstraint multi, max: ' + this.fontOptions.maxWdt); for (let j = 0; j < blocks.length; j++) { @@ -115,7 +117,7 @@ class LabelSplitter { } } else { // Single-font case - if (this.parent.fontOptions.maxWdt > 0) { + if (font.maxWdt > 0) { // widthConstraint.maximum defined // console.log('Running widthConstraint normal, max: ' + this.fontOptions.maxWdt); for (let i = 0; i < lineCount; i++) { diff --git a/lib/util.js b/lib/util.js index f21de8e8..ab23f6af 100644 --- a/lib/util.js +++ b/lib/util.js @@ -251,8 +251,8 @@ exports.selectiveDeepExtend = function (props, a, b, allowDeletion = false) { /** - * Extend object `a` with properties of object `b`, ignoring properties which are explicitly specified - * to be excluded. + * Extend object `a` with properties of object `b`, ignoring properties which are explicitly + * specified to be excluded. * * The properties of `b` are considered for copying. * Properties which are themselves objects are are also extended. @@ -1210,7 +1210,7 @@ exports.isValidRGBA = function (rgba) { * @returns {*} */ exports.selectiveBridgeObject = function (fields, referenceObject) { - if (typeof referenceObject == "object") { + if (referenceObject !== null && typeof referenceObject === "object") { // !!! typeof null === 'object' var objectTo = Object.create(referenceObject); for (var i = 0; i < fields.length; i++) { if (referenceObject.hasOwnProperty(fields[i])) { @@ -1234,7 +1234,7 @@ exports.selectiveBridgeObject = function (fields, referenceObject) { * @returns {*} */ exports.bridgeObject = function (referenceObject) { - if (typeof referenceObject == "object") { + if (referenceObject !== null && typeof referenceObject === "object") { // !!! typeof null === 'object' var objectTo = Object.create(referenceObject); if (referenceObject instanceof Element) { // Avoid bridging DOM objects diff --git a/test/Label.test.js b/test/Label.test.js index 21565b18..c29b39ba 100644 --- a/test/Label.test.js +++ b/test/Label.test.js @@ -2,14 +2,20 @@ * TODO - add tests for: * ==== * - * - !!! good test case with the tags for max width - * - pathological cases of spaces (and other whitespace!) * - html unclosed or unopened tags * - html tag combinations with no font defined (e.g. bold within mono) + * - Unit tests for bad font shorthands. + * Currently, only "size[px] name color" is valid, always 3 items with this exact spacing. + * All other combinations should either be rejected as error or handled gracefully. */ var assert = require('assert') var Label = require('../lib/network/modules/components/shared/Label').default; var NodesHandler = require('../lib/network/modules/NodesHandler').default; +var util = require('../lib/util'); +var jsdom_global = require('jsdom-global'); +var vis = require('../dist/vis'); +var Network = vis.network; + /************************************************************** * Dummy class definitions for minimal required functionality. @@ -128,12 +134,10 @@ describe('Network Label', function() { ]; - /************************************************************** * Expected Results **************************************************************/ - var normal_expected = [{ // In first item, width/height kept in for reference width: 120, @@ -315,6 +319,20 @@ describe('Network Label', function() { * End Expected Results **************************************************************/ + before(function() { + this.jsdom_global = jsdom_global( + "
", + { skipWindowCheck: true} + ); + this.container = document.getElementById('mynetwork'); + }); + + + after(function() { + this.jsdom_global(); + }); + + it('parses normal text labels', function (done) { var label = new Label({}, getOptions()); @@ -418,7 +436,760 @@ describe('Network Label', function() { }); - it('compresses spaces in multifont', function (done) { +describe('Multi-Fonts', function() { + + class HelperNode { + constructor(network) { + this.nodes = network.body.nodes; + } + + fontOption(index) { + return this.nodes[index].labelModule.fontOptions; + }; + + modBold(index) { + return this.fontOption(index).bold; + }; + } + + +describe('Node Labels', function() { + + function createNodeNetwork(newOptions) { + var dataNodes = [ + {id: 0, label: '0'}, + {id: 1, label: '1'}, + {id: 2, label: '2', group: 'group1'}, + {id: 3, label: '3', + font: { + bold: { color: 'green' }, + } + }, + {id: 4, label: '4', group: 'group1', + font: { + bold: { color: 'green' }, + } + }, + ]; + + // create a network + var container = document.getElementById('mynetwork'); + var data = { + nodes: new vis.DataSet(dataNodes), + edges: [] + }; + + var options = { + nodes: { + font: { + multi: true + } + }, + groups: { + group1: { + font: { color: 'red' }, + }, + group2: { + font: { color: 'white' }, + }, + }, + }; + + if (newOptions !== undefined) { + util.deepExtend(options, newOptions); + } + + var network = new vis.Network(container, data, options); + return [network, data, options]; + } + + + /** + * Check that setting options for multi-font works as expected + * + * - using multi-font 'bold' for test, the rest should work analogously + * - using multi-font option 'color' for test, the rest should work analogously + */ + it('respects the font option precedence', function (done) { + var [network, data, options] = createNodeNetwork(); + var h = new HelperNode(network); + + assert.equal(h.modBold(0).color, '#343434'); // Default value + assert.equal(h.modBold(1).color, '#343434'); // Default value + assert.equal(h.modBold(2).color, 'red'); // Group value overrides default + assert.equal(h.modBold(3).color, 'green'); // Local value overrides default + assert.equal(h.modBold(4).color, 'green'); // Local value overrides group + + done(); + }); + + + it('handles dynamic data and option updates', function (done) { + var [network, data, options] = createNodeNetwork(); + var h = new HelperNode(network); + + // + // Change some node values dynamically + // + data.nodes.update([ + {id: 1, group: 'group2'}, + {id: 4, font: { bold: { color: 'orange'}}}, + ]); + + assert.equal(h.modBold(0).color, '#343434'); // unchanged + assert.equal(h.modBold(1).color, 'white'); // new group value + assert.equal(h.modBold(3).color, 'green'); // unchanged + assert.equal(h.modBold(4).color, 'orange'); // new local value + + + // + // Change group options dynamically + // + network.setOptions({ + groups: { + group1: { + font: { color: 'brown' }, + }, + }, + }); + + assert.equal(h.modBold(0).color, '#343434'); // unchanged + assert.equal(h.modBold(1).color, 'white'); // Unchanged + assert.equal(h.modBold(2).color, 'brown'); // New group values + assert.equal(h.modBold(3).color, 'green'); // unchanged + assert.equal(h.modBold(4).color, 'orange'); // unchanged + + + network.setOptions({ + nodes: { + font: { + multi: true, + bold: { + color: 'black' + } + } + }, + }); + + assert.equal(h.modBold(0).color, 'black'); // nodes default + assert.equal(h.modBold(1).color, 'black'); // more specific bold value overrides group value + assert.equal(h.modBold(2).color, 'black'); // idem + assert.equal(h.modBold(3).color, 'green'); // unchanged + assert.equal(h.modBold(4).color, 'orange'); // unchanged + + + network.setOptions({ + groups: { + group1: { + font: { bold: {color: 'brown'} }, + }, + }, + }); + + assert.equal(h.modBold(0).color, 'black'); // nodes default + assert.equal(h.modBold(1).color, 'black'); // more specific bold value overrides group value + assert.equal(h.modBold(2).color, 'brown'); // bold group value overrides bold node value + assert.equal(h.modBold(3).color, 'green'); // unchanged + assert.equal(h.modBold(4).color, 'orange'); // unchanged + + done(); + }); + + + it('handles normal font values in default options', function (done) { + var newOptions = { + nodes: { + font: { + color: 'purple' // Override the default value + } + }, + }; + var [network, data, options] = createNodeNetwork(newOptions); + var h = new HelperNode(network); + + assert.equal(h.modBold(0).color, 'purple'); // Nodes value + assert.equal(h.modBold(1).color, 'purple'); // Nodes value + assert.equal(h.modBold(2).color, 'red'); // Group value overrides nodes + assert.equal(h.modBold(3).color, 'green'); // Local value overrides all + assert.equal(h.modBold(4).color, 'green'); // Idem + + done(); + }); + + + it('handles multi-font values in default options/groups', function (done) { + var newOptions = { + nodes: { + font: { + color: 'purple' // This set value should be overridden + } + }, + }; + + newOptions.nodes.font.bold = { color: 'yellow'}; + newOptions.groups = { + group1: { + font: { bold: { color: 'red'}} + } + }; + + var [network, data, options] = createNodeNetwork(newOptions); + var h = new HelperNode(network); + assert(options.nodes.font.multi); + + assert.equal(h.modBold(0).color, 'yellow'); // bold value + assert.equal(h.modBold(1).color, 'yellow'); // bold value + assert.equal(h.modBold(2).color, 'red'); // Group value overrides nodes + assert.equal(h.modBold(3).color, 'green'); // Local value overrides all + assert.equal(h.modBold(4).color, 'green'); // Idem + + done(); + }); + +}); // Node Labels + + +describe('Edge Labels', function() { + + function createEdgeNetwork(newOptions) { + var dataNodes = [ + {id: 1, label: '1'}, + {id: 2, label: '2'}, + {id: 3, label: '3'}, + {id: 4, label: '4'}, + ]; + + var dataEdges = [ + {id: 1, from: 1, to: 2, label: '1'}, + {id: 2, from: 1, to: 4, label: '2', + font: { + bold: { color: 'green' }, + } + }, + {id: 3, from: 2, to: 3, label: '3', + font: { + bold: { color: 'green' }, + } + }, + ]; + + // create a network + var container = document.getElementById('mynetwork'); + var data = { + nodes: new vis.DataSet(dataNodes), + edges: new vis.DataSet(dataEdges), + }; + + var options = { + edges: { + font: { + multi: true + } + }, + }; + + if (newOptions !== undefined) { + util.deepExtend(options, newOptions); + } + + var network = new vis.Network(container, data, options); + return [network, data, options]; + } + + + class HelperEdge { + constructor(network) { + this.edges = network.body.edges; + } + + fontOption(index) { + return this.edges[index].labelModule.fontOptions; + }; + + modBold(index) { + return this.fontOption(index).bold; + }; + } + + + /** + * Check that setting options for multi-font works as expected + * + * - using multi-font 'bold' for test, the rest should work analogously + * - using multi-font option 'color' for test, the rest should work analogously + * - edges have no groups + */ + it('respects the font option precedence', function (done) { + var [network, data, options] = createEdgeNetwork(); + var h = new HelperEdge(network); + + assert.equal(h.modBold(1).color, '#343434'); // Default value + assert.equal(h.modBold(2).color, 'green'); // Local value overrides default + assert.equal(h.modBold(3).color, 'green'); // Local value overrides group + + done(); + }); + + + it('handles dynamic data and option updates', function (done) { + var [network, data, options] = createEdgeNetwork(); + var h = new HelperEdge(network); + + data.edges.update([ + {id: 3, font: { bold: { color: 'orange'}}}, + ]); + + assert.equal(h.modBold(1).color, '#343434'); // unchanged + assert.equal(h.modBold(2).color, 'green'); // unchanged + assert.equal(h.modBold(3).color, 'orange'); // new local value + + + network.setOptions({ + edges: { + font: { + multi: true, + bold: { + color: 'black' + } + } + }, + }); + + assert.equal(h.modBold(1).color, 'black'); // more specific bold value overrides group value + assert.equal(h.modBold(2).color, 'green'); // unchanged + assert.equal(h.modBold(3).color, 'orange'); // unchanged + + done(); + }); + + + it('handles font values in default options', function (done) { + var newOptions = { + edges: { + font: { + color: 'purple' // Override the default value + } + }, + }; + var [network, data, options] = createEdgeNetwork(newOptions); + var h = new HelperEdge(network); + + assert.equal(h.modBold(1).color, 'purple'); // Nodes value + assert.equal(h.modBold(2).color, 'green'); // Local value overrides all + assert.equal(h.modBold(3).color, 'green'); // Idem + + done(); + }); + +}); // Edge Labels + + +describe('Shorthand Font Options', function() { + + var testFonts = { + 'default': {color: '#343434', face: 'arial' , size: 14}, + 'monodef': {color: '#343434', face: 'monospace', size: 15}, + 'font1' : {color: '#010101', face: 'Font1' , size: 1}, + 'font2' : {color: '#020202', face: 'Font2' , size: 2}, + 'font3' : {color: '#030303', face: 'Font3' , size: 3}, + 'font4' : {color: '#040404', face: 'Font4' , size: 4}, + 'font5' : {color: '#050505', face: 'Font5' , size: 5}, + 'font6' : {color: '#060606', face: 'Font6' , size: 6}, + 'font7' : {color: '#070707', face: 'Font7' , size: 7}, + }; + + + function checkFont(opt, expectedLabel) { + var expected = testFonts[expectedLabel]; + + util.forEach(expected, (item, key) => { + assert.equal(opt[key], item); + }); + }; + + + function createNetwork() { + var dataNodes = [ + {id: 1, label: '1'}, + {id: 2, label: '2', group: 'group1'}, + {id: 3, label: '3', group: 'group2'}, + {id: 4, label: '4', font: '5px Font5 #050505'}, + ]; + + var dataEdges = []; + + // create a network + var container = document.getElementById('mynetwork'); + var data = { + nodes: new vis.DataSet(dataNodes), + edges: new vis.DataSet(dataEdges), + }; + + var options = { + nodes: { + font: { + multi: true, + bold: '1 Font1 #010101', + ital: '2 Font2 #020202', + } + }, + groups: { + group1: { + font: '3 Font3 #030303' + }, + group2: { + font: { + bold: '4 Font4 #040404' + } + } + } + }; + + var network = new vis.Network(container, data, options); + return [network, data]; + } + + + it('handles shorthand options correctly', function (done) { + var [network, data] = createNetwork(); + var h = new HelperNode(network); + + // NOTE: 'mono' has its own global default font and size, which will + // trump any other font values set. + + var opt = h.fontOption(1); + checkFont(opt, 'default'); + checkFont(opt.bold, 'font1'); + checkFont(opt.ital, 'font2'); + checkFont(opt.mono, 'monodef'); // Mono should have defaults + + // Node 2 should be using group1 options + opt = h.fontOption(2); + checkFont(opt, 'font3'); + checkFont(opt.bold, 'font1'); // bold retains nodes default options + checkFont(opt.ital, 'font2'); // ital retains nodes default options + assert.equal(opt.mono.color, '#030303'); // New color + assert.equal(opt.mono.face, 'monospace'); // own global default font + assert.equal(opt.mono.size, 15); // Own global default size + + // Node 3 should be using group2 options + opt = h.fontOption(3); + checkFont(opt, 'default'); + checkFont(opt.bold, 'font4'); + checkFont(opt.ital, 'font2'); + checkFont(opt.mono, 'monodef'); // Mono should have defaults + + // Node 4 has its own base font definition + opt = h.fontOption(4); + checkFont(opt, 'font5'); + checkFont(opt.bold, 'font1'); + checkFont(opt.ital, 'font2'); + assert.equal(opt.mono.color, '#050505'); // New color + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + done(); + }); + + + function dynamicAdd1(network, data) { + // Add new shorthand at every level + data.nodes.update([ + {id: 1, font: '5 Font5 #050505'}, + {id: 4, font: { bold: '6 Font6 #060606'} }, // kills node instance base font + ]); + + network.setOptions({ + nodes: { + font: { + multi: true, + ital: '4 Font4 #040404', + } + }, + groups: { + group1: { + font: { + bold: '7 Font7 #070707' // Kills node instance base font + } + }, + group2: { + font: '6 Font6 #060606' // Note: 'bold' removed by this + } + } + }); + } + + + function dynamicAdd2(network, data) { + network.setOptions({ + nodes: { + font: '7 Font7 #070707' // Note: this kills the font.multi, bold and ital settings! + } + }); + } + + + it('deals with dynamic data and option updates for shorthand', function (done) { + var [network, data] = createNetwork(); + var h = new HelperNode(network); + dynamicAdd1(network, data); + + var opt = h.fontOption(1); + checkFont(opt, 'font5'); // New base font + checkFont(opt.bold, 'font1'); + checkFont(opt.ital, 'font4'); // New global node default + assert.equal(opt.mono.color, '#050505'); // New color + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + opt = h.fontOption(2); + checkFont(opt, 'default'); + checkFont(opt.bold, 'font7'); + checkFont(opt.ital, 'font4'); // New global node default + checkFont(opt.mono, 'monodef'); // Mono should have defaults again + + opt = h.fontOption(3); + checkFont(opt, 'font6'); // New base font + checkFont(opt.bold, 'font1'); // group bold option removed, using global default node + checkFont(opt.ital, 'font4'); // New global node default + assert.equal(opt.mono.color, '#060606'); // New color + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + opt = h.fontOption(4); + checkFont(opt, 'default'); + checkFont(opt.bold, 'font6'); + checkFont(opt.ital, 'font4'); + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + done(); + }); + + + it('deals with dynamic change of global node default', function (done) { + var [network, data] = createNetwork(); + var h = new HelperNode(network); + dynamicAdd1(network, data); // Accumulate data of dynamic add + dynamicAdd2(network, data); + + var opt = h.fontOption(1); + checkFont(opt, 'font5'); // Node instance value + checkFont(opt.bold, 'font5'); // bold def removed from global default node + checkFont(opt.ital, 'font5'); // idem + assert.equal(opt.mono.color, '#050505'); // New color + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + opt = h.fontOption(2); + checkFont(opt, 'font7'); // global node default applies for all settings + checkFont(opt.bold, 'font7'); + checkFont(opt.ital, 'font7'); + assert.equal(opt.mono.color, '#070707'); + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + opt = h.fontOption(3); + checkFont(opt, 'font6'); // Group base font + checkFont(opt.bold, 'font6'); // idem + checkFont(opt.ital, 'font6'); // idem + assert.equal(opt.mono.color, '#060606'); // idem + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + opt = h.fontOption(4); + checkFont(opt, 'font7'); // global node default + checkFont(opt.bold, 'font6'); // node instance bold + checkFont(opt.ital, 'font7'); // global node default + assert.equal(opt.mono.color, '#070707'); // idem + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + + done(); + }); + + + it('deals with dynamic delete of shorthand options', function (done) { + var [network, data] = createNetwork(); + var h = new HelperNode(network); + dynamicAdd1(network, data); // Accumulate data of previous dynamic steps + dynamicAdd2(network, data); // idem + + data.nodes.update([ + {id: 1, font: null}, + {id: 4, font: { bold: null}}, + ]); + + var opt; + +/* + // Interesting: following flagged as error in options parsing, avoiding it for that reason + network.setOptions({ + nodes: { + font: { + multi: true, + ital: null, + } + }, + }); +*/ + + network.setOptions({ + groups: { + group1: { + font: { + bold: null + } + }, + group2: { + font: null + } + } + }); + + // global defaults for all + for (let n = 1; n <= 4; ++ n) { + opt = h.fontOption(n); + checkFont(opt, 'font7'); + checkFont(opt.bold, 'font7'); + checkFont(opt.ital, 'font7'); + assert.equal(opt.mono.color, '#070707'); + assert.equal(opt.mono.face, 'monospace'); + assert.equal(opt.mono.size, 15); + } + +/* + // Not testing following because it is an error in options parsing + network.setOptions({ + nodes: { + font: null + }, + }); +*/ + + done(); + }); + +}); // Shorthand Font Options + + + it('sets and uses font.multi in group options', function (done) { + + /** + * Helper function for easily accessing font options in a node + */ + var fontOption = (index) => { + var nodes = network.body.nodes; + return nodes[index].labelModule.fontOptions; + }; + + + /** + * Helper function for easily accessing bold options in a node + */ + var modBold = (index) => { + return fontOption(index).bold; + }; + + + var dataNodes = [ + {id: 1, label: '1', group: 'group1'}, + { + // From example 1 in #3408 + id: 6, + label: '\uf286 \uf2cd colored glyph icon', + shape: 'icon', + group: 'colored', + icon : { color: 'blue' }, + font: + { + bold : { color : 'blue' }, + ital : { color : 'green' } + } + }, + ]; + + // create a network + var container = document.getElementById('mynetwork'); + var data = { + nodes: new vis.DataSet(dataNodes), + edges: [] + }; + + var options = { + groups: { + group1: { + font: { + multi: true, + color: 'red' + }, + }, + colored : + { + // From example 1 in 3408 + icon : + { + face : 'FontAwesome', + code : '\uf2b5', + }, + font: + { + face : 'FontAwesome', + multi: true, + bold : { mod : '' }, + ital : { mod : '' } + } + }, + }, + }; + + var network = new vis.Network(container, data, options); + + assert.equal(modBold(1).color, 'red'); // Group value + assert(fontOption(1).multi); // Group value + assert.equal(modBold(6).color, 'blue'); // node instance value + assert(fontOption(6).multi); // Group value + + + network.setOptions({ + groups: { + group1: { + //font: { color: 'brown' }, // Can not just change one field, entire font object is reset + font: { + multi: true, + color: 'brown' + }, + }, + }, + }); + + assert.equal(modBold(1).color, 'brown'); // New value + assert(fontOption(1).multi); // Group value + assert.equal(modBold(6).color, 'blue'); // unchanged + assert(fontOption(6).multi); // unchanged + + + network.setOptions({ + groups: { + group1: { + font: null, // Remove font from group + }, + }, + }); + + // console.log("==============="); + // console.log(fontOption(1)); + + assert.equal(modBold(1).color, '#343434'); // Reverts to default + assert(!fontOption(1).multi); // idem + assert.equal(modBold(6).color, 'blue'); // unchanged + assert(fontOption(6).multi); // unchanged + + done(); + }); + + + it('compresses spaces for Multi-Font', function (done) { var options = getOptions(options); var text = [ @@ -556,6 +1327,8 @@ describe('Network Label', function() { done(); }); +}); // Multi-Fonts + it('parses single huge word on line with preceding whitespace when max width set', function (done) { var options = getOptions(options); @@ -563,21 +1336,19 @@ describe('Network Label', function() { assert.equal(options.font.multi, false); /** + * Split a string at the given location, return either first or last part + * * Allows negative indexing, counting from back (ruby style) */ -/* - TODO: Use when the actual bug is fixed and tests pass. - let splitAt = (text, pos, getFirst) => { - if (pos < 0) { pos = text.length + pos; + if (pos < 0) pos = text.length + pos; if (getFirst) { - return text.substring(0, pos)); + return text.substring(0, pos); } else { - return text.substring(pos)); + return text.substring(pos); } - } -*/ + }; var label = new Label({}, options); var longWord = "asd;lkfja;lfkdj;alkjfd;alskfj"; @@ -594,9 +1365,9 @@ describe('Network Label', function() { }, { blocks: [{text: ""}] }, { - blocks: [{text: "asd;lkfja;lfkdj;alkjfd;als"}] + blocks: [{text: splitAt(longWord, -3, true)}] }, { - blocks: [{text: "kfj"}] + blocks: [{text: splitAt(longWord, -3, false)}] }] }, { lines: [{ @@ -606,9 +1377,9 @@ describe('Network Label', function() { }, { blocks: [{text: ""}] }, { - blocks: [{text: "asd;lkfja;lfkdj;alkjfd;als"}] + blocks: [{text: splitAt(longWord, -3, true)}] }, { - blocks: [{text: "kfj"}] + blocks: [{text: splitAt(longWord, -3, false)}] }] }, { lines: [{ @@ -618,9 +1389,9 @@ describe('Network Label', function() { }, { blocks: [{text: ""}] }, { - blocks: [{text: "asd;lkfja;lfkdj;alkjfd;als"}] + blocks: [{text: splitAt(longWord, -3, true)}] }, { - blocks: [{text: "kfj"}] + blocks: [{text: splitAt(longWord, -3, false)}] }] }]; @@ -633,6 +1404,7 @@ describe('Network Label', function() { options.font.multi = true; var label = new Label({}, options); checkProcessedLabels(label, text, expected); + done(); }); });