From 75e6263ac376b7a0fef52769f4e915ed8e6619c7 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 29 Sep 2017 18:17:14 +0200 Subject: [PATCH] Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup --- lib/network/modules/EdgesHandler.js | 19 +- lib/network/modules/Groups.js | 15 +- lib/network/modules/NodesHandler.js | 10 +- lib/network/modules/components/Edge.js | 26 +- lib/network/modules/components/Node.js | 110 ++- .../modules/components/nodes/Cluster.js | 5 +- .../modules/components/shared/Label.js | 390 +++++---- .../components/shared/LabelSplitter.js | 10 +- lib/util.js | 8 +- test/Label.test.js | 810 +++++++++++++++++- 10 files changed, 1151 insertions(+), 252 deletions(-) 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(); }); });