diff --git a/docs/network/edges.html b/docs/network/edges.html index 423f9a45..1926db29 100644 --- a/docs/network/edges.html +++ b/docs/network/edges.html @@ -135,7 +135,37 @@ var options = { background: 'none', strokeWidth: 2, // px strokeColor: '#ffffff', - align:'horizontal' + align: 'horizontal', + multi: false, + vadjust: 0, + bold: { + color: '#343434', + size: 14, // px + face: 'arial', + vadjust: 0, + mod: 'bold' + }, + ital: { + color: '#343434', + size: 14, // px + face: 'arial', + vadjust: 0, + mod: 'italic', + }, + boldital: { + color: '#343434', + size: 14, // px + face: 'arial', + vadjust: 0, + mod: 'bold italic' + }, + mono: { + color: '#343434', + size: 15, // px + face: 'courier new', + vadjust: 2, + mod: '' + } }, hidden: false, hoverWidth: 1.5, @@ -178,8 +208,9 @@ var options = { roundness: 0.5 }, title:undefined, + value: undefined, width: 1, - value: undefined + widthConstraint: false } } @@ -390,6 +421,171 @@ network.setOptions(options); the label will align itself according to the edge. + + font.vadjust + String + 0 + A font-specific correction to the vertical positioning of the base font in the label text. (Positive is down.) + + + font.multi + Boolean or String + false + If false, the label is treated as pure text drawn with the base font. If true or 'html' the label may be multifonted, with bold, italic and code markup, interpreted as html. If the value is 'markdown' or 'md' the label may be multifonted, with bold, italic and code markup, interpreted as markdown. + The bold, italic, bold-italic and monospaced fonts may be set up under in the font.bold, font.ital, font.boldital and font.mono properties, respectively. + + + font.bold + Object or String + false + This object defines the details of the bold font in the label. A shorthand is also supported in the form 'size face + color' for example: '14px arial red'. + + + + font.bold.color + String + '#343434' + Color of the bold font in the label text. Defaults to the base font's color. + + + font.bold.size + Number + 14 + Size of the bold font in the label text. Defaults to the base font's size. + + + font.bold.face + String + 'arial' + Font face (or font family) of the bold font in the label text. Defaults to the base font's face. + + + font.bold.mod + String + 'bold' + A string added to the face and size when determining the bold font in the label text. + + + font.bold.vadjust + String + 0 + A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign. + + + font.ital + Object or String + false + This object defines the details of the italic font in the label. A shorthand is also supported in the form 'size face + color' for example: '14px arial red'. + + + + font.ital.color + String + '#343434' + Color of the italic font in the label text. Defaults to the base font's color. + + + font.ital.size + Number + 14 + Size of the italic font in the label text. Defaults to the base font's size. + + + font.ital.face + String + 'arial' + Font face (or font family) of the italic font in the label text. Defaults to the base font's face. + + + font.ital.mod + String + 'italic' + A string added to the face and size when determining the italic font in the label text. + + + font.ital.vadjust + String + 0 + A font-specific correction to the vertical positioning of the italic font in the label text. (Positive is down.) Defaults to the base font's valign. + + + font.boldital + Object or String + false + This object defines the details of the bold italic font in the label. A shorthand is also supported in the form 'size face + color' for example: '14px arial red'. + + + + font.boldital.color + String + '#343434' + Color of the bold italic font in the label text. Defaults to the base font's color. + + + font.boldital.size + Number + 14 + Size of the bold italic font in the label text. Defaults to the base font's size. + + + font.boldital.face + String + 'arial' + Font face (or font family) of the bold italic font in the label text. Defaults to the base font's face. + + + font.boldital.mod + String + 'bold' + A string added to the face and size when determining the bold italic font in the label text. + + + font.boldital.vadjust + String + 0 + A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign. + + + font.mono + Object or String + false + This object defines the details of the monospaced font in the label. A shorthand is also supported in the form 'size face + color' for example: '15px courier red'. + + + + font.mono.color + String + '#343434' + Color of the monospaced font in the label text. Defaults to the base font's color. + + + font.mono.size + Number + 15 + Size of the monospaced font in the label text. Defaults to the base font's size. + + + font.mono.face + String + 'courier new' + Font face (or font family) of the monospaced font in the label text. + + + font.mono.mod + String + '' + A string added to the face and size when determining the monospaced font in the label text. + + + font.mono.vadjust + String + 2 + A font-specific correction to the vertical positioning of the monospaced font in the label text. (Positive is down.) Defaults to the base font's valign. + from Number or String @@ -691,6 +887,18 @@ var options: { 1 The width of the edge. If value is set, this is not used. + + widthConstraint + Number, Boolean or Object + false + If false, no widthConstraint is applied. If a number is specified, the maximum width of the edge's label is set to the value. The edge's label's lines will be broken on spaces to stay below the maximum. + + + widthConstraint.maximum + Number + undefined + If a number is specified, the maximum width of the edge's label is set to the value. The edge's label's lines will be broken on spaces to stay below the maximum. + diff --git a/docs/network/nodes.html b/docs/network/nodes.html index 4dbcef5a..53cc052e 100644 --- a/docs/network/nodes.html +++ b/docs/network/nodes.html @@ -133,9 +133,40 @@ var options = { background: 'none', strokeWidth: 0, // px strokeColor: '#ffffff', - align: 'center' + align: 'center', + multi: false, + vadjust: 0, + bold: { + color: '#343434', + size: 14, // px + face: 'arial', + vadjust: 0, + mod: 'bold' + }, + ital: { + color: '#343434', + size: 14, // px + face: 'arial', + vadjust: 0, + mod: 'italic', + }, + boldital: { + color: '#343434', + size: 14, // px + face: 'arial', + vadjust: 0, + mod: 'bold italic' + }, + mono: { + color: '#343434', + size: 15, // px + face: 'courier new', + vadjust: 2, + mod: '' + } }, group: undefined, + heightConstraint: false, hidden: false, icon: { face: 'FontAwesome', @@ -187,6 +218,7 @@ var options = { size: 25, title: undefined, value: undefined, + widthConstraint: false, x: undefined, y: undefined } @@ -381,14 +413,198 @@ network.setOptions(options); This can be set to 'left' to make the label left-aligned. Otherwise, defaults to 'center'. + + font.vadjust + String + 0 + A font-specific correction to the vertical positioning of the base font in the label text. (Positive is down.) + + + font.multi + Boolean or String + false + If false, the label is treated as pure text drawn with the base font. If true or 'html' the label may be multifonted, with bold, italic and code markup, interpreted as html. If the value is 'markdown' or 'md' the label may be multifonted, with bold, italic and code markup, interpreted as markdown. + The bold, italic, bold-italic and monospaced fonts may be set up under in the font.bold, font.ital, font.boldital and font.mono properties, respectively. + + + font.bold + Object or String + false + This object defines the details of the bold font in the label. A shorthand is also supported in the form 'size face + color' for example: '14px arial red'. + + + + font.bold.color + String + '#343434' + Color of the bold font in the label text. Defaults to the base font's color. + + + font.bold.size + Number + 14 + Size of the bold font in the label text. Defaults to the base font's size. + + + font.bold.face + String + 'arial' + Font face (or font family) of the bold font in the label text. Defaults to the base font's face. + + + font.bold.mod + String + 'bold' + A string added to the face and size when determining the bold font in the label text. + + + font.bold.vadjust + String + 0 + A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign. + + + font.ital + Object or String + false + This object defines the details of the italic font in the label. A shorthand is also supported in the form 'size face + color' for example: '14px arial red'. + + + + font.ital.color + String + '#343434' + Color of the italic font in the label text. Defaults to the base font's color. + + + font.ital.size + Number + 14 + Size of the italic font in the label text. Defaults to the base font's size. + + + font.ital.face + String + 'arial' + Font face (or font family) of the italic font in the label text. Defaults to the base font's face. + + + font.ital.mod + String + 'italic' + A string added to the face and size when determining the italic font in the label text. + + + font.ital.vadjust + String + 0 + A font-specific correction to the vertical positioning of the italic font in the label text. (Positive is down.) Defaults to the base font's valign. + + + font.boldital + Object or String + false + This object defines the details of the bold italic font in the label. A shorthand is also supported in the form 'size face + color' for example: '14px arial red'. + + + + font.boldital.color + String + '#343434' + Color of the bold italic font in the label text. Defaults to the base font's color. + + + font.boldital.size + Number + 14 + Size of the bold italic font in the label text. Defaults to the base font's size. + + + font.boldital.face + String + 'arial' + Font face (or font family) of the bold italic font in the label text. Defaults to the base font's face. + + + font.boldital.mod + String + 'bold' + A string added to the face and size when determining the bold italic font in the label text. + + + font.boldital.vadjust + String + 0 + A font-specific correction to the vertical positioning of the bold italic font in the label text. (Positive is down.) Defaults to the base font's valign. + + + font.mono + Object or String + false + This object defines the details of the monospaced font in the label. A shorthand is also supported in the form 'size face + color' for example: '15px courier red'. + + + + font.mono.color + String + '#343434' + Color of the monospaced font in the label text. Defaults to the base font's color. + + + font.mono.size + Number + 15 + Size of the monospaced font in the label text. Defaults to the base font's size. + + + font.mono.face + String + 'courier new' + Font face (or font family) of the monospaced font in the label text. + + + font.mono.mod + String + '' + A string added to the face and size when determining the monospaced font in the label text. + + + font.mono.vadjust + String + 2 + A font-specific correction to the vertical positioning of the monospaced font in the label text. (Positive is down.) Defaults to the base font's valign. + group String undefined When not undefined, the node will belong to the defined group. Styling information of - that group will apply to this node. Node specific styling overrides group styling. + that group will apply to this node. Node specific styling overrides group styling. + + heightConstraint + Number, Boolean or Object + false + If false, no heightConstraint is applied. If a number is specified, the value is used as the minimum height of the node. The node's height will be be set to the minimum if less than the value. + + + heightConstraint.minimum + Number + undefined + If a number is specified, the value is used as the minimum height of the node. The node's height will be be set to the minimum if less than the value. + + + heightConstraint.valign + String + 'middle' + Valid values are 'top', 'middle', and 'bottom'. + When specified, if the height of the label text is less than the minimum (including any top or bottom margins), it will be offset vertically to the designated position. + hidden Boolean @@ -731,6 +947,24 @@ mySize = minSize + diff * scale; When a value is set, the nodes will be scaled using the options in the scaling object defined above. + + widthConstraint + Number, Boolean or Object + false + If false, no widthConstraint is applied. If a number is specified, the minimum and maximum widths of the node are set to the value. The node's label's lines will be broken on spaces to stay below the maximum and the node's width will be set to the minimum if less than the value. + + + widthConstraint.minimum + Number + undefined + If a number is specified, the minimum width of the node is set to the value. The node's width will be set to the minimum if less than the value. + + + widthConstraint.maximum + Number + undefined + If a number is specified, the maximum width of the node is set to the value. The node's label's lines will be broken on spaces to stay below the maximum. + x Number diff --git a/examples/network/labels/labelMultifont.html b/examples/network/labels/labelMultifont.html new file mode 100644 index 00000000..70013818 --- /dev/null +++ b/examples/network/labels/labelMultifont.html @@ -0,0 +1,115 @@ + + + + Network | Multifont Labels + + + + + + + + + + +

Node and edge labels may be marked up to be drawn with multiple fonts.

+ +
+ +

The value of the font.multi property may be set to 'html', 'markdown' or a boolean.

+ + + + + + + +
Embedded Font Markup
font modfont.multi setting
'html' or true'markdown' or 'md'false
bold<b> ... </b> * ... n/a
italic<i> ... </i> _ ... n/a
mono-spaced<code> ... </code> ` ... n/a
+ +

+The html and markdown rendering is limited: bolds may be embedded in italics, italics may be embedded in bolds, and mono-spaced may be embedded in bold or italic, but will not be altered by those font mods, nor will embedded bolds or italics be handled. +The only entities that will be observed in html are &lt; and &amp; and in markdown a backslash will escape the following character (including a backslash) from special processing. +Any font mod that is started in a label line will be implicitly terminated at the end of that line. +While this interpretation may not exactly match official rendering standards, it is a consistent compromise for drawing multifont strings in the non-multifont html canvas element underlying vis. +

+ +

This implies that four additional sets of font properties will be recognized in label processing.

+

font.bold designates the font used for rendering bold font mods. +
font.ital designates the font used for rendering italic font mods. +
font.boldital designates the font used for rendering bold-and-italic font mods. +
font.mono designates the font used for rendering monospaced font mods.

+

Any font mod without a matching font will be rendered using the normal font (or default) value.

+ +

The font.multi and extended font settings may be set in the network's nodes or edges properties, or on individual nodes and edges. +Node and edge label fonts are separate.

+ + + + + diff --git a/examples/network/nodeStyles/widthHeight.html b/examples/network/nodeStyles/widthHeight.html new file mode 100644 index 00000000..565e900b --- /dev/null +++ b/examples/network/nodeStyles/widthHeight.html @@ -0,0 +1,118 @@ + + + + Network | Label Width and Height Settings + + + + + + + + + + +

Nodes may be set to have fixed, minimum and maximum widths and minimum heights. +Nodes with minimum heights may also have a vertical alignment set.

+ +

Edges may be set to have maximum widths.

+ +
+ +

The widthConstraint: value option means a fixed width, the minimum and maximum width of the element are set to the value (respecting left and right margins). Lines exceeding the maximum width will be broken at space boundaries to fit.

+

The widthConstraint: { minimum: value } option sets the minimum width of the element to the value.

+

The widthConstraint: { maximum: value } option sets the maximum width of the element to the value (respecting left and right margins). Lines exceeding the maximum width will be broken at space boundaries to fit.

+

Minimum width line sizing is applied after maximum width line breaking, so counterintuitively, the minimum being greater than the maximum has a meaningful interpretation.

+ +
+ +

The heightConstraint: value option sets the minimum height of the element to the value (respecting top and bottom margins).

+

The heightConstraint: { minimum: value } option also sets the minimum height of the element to the value (respecting top and bottom margins).

+

The heightConstraint: { valign: value } option (with value 'top', 'middle', or 'bottom', sets the alignment of the text in the element's label to the elements top, middle or bottom (respecting top and bottom margins) when it's height is less than the minimum. The middle value is the default.

+ +
+ +

Node width and height constraints may both be applied together, of course.

+

The constraint options may be applied to elements individually, or at the whole-set level. +Whole-set node and edge constraints are exclusive.

+ + + + + diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index e57e86e5..320b7742 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -43,7 +43,24 @@ class EdgesHandler { background: 'none', strokeWidth: 2, // px strokeColor: '#ffffff', - align:'horizontal' + align:'horizontal', + multi: false, + vadjust: 0, + bold: { + mod: 'bold' + }, + boldital: { + mod: 'bold italic' + }, + ital: { + mod: 'italic' + }, + mono: { + mod: '', + size: 15, // px + face: 'courier new', + vadjust: 2 + } }, hidden: false, hoverWidth: 1.5, @@ -155,6 +172,7 @@ class EdgesHandler { } 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); @@ -340,7 +358,7 @@ class EdgesHandler { } create(properties) { - return new Edge(properties, this.body, this.options) + return new Edge(properties, this.body, this.options, this.defaultOptions, this.edgeOptions) } diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index acd5a307..2241d5b3 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -49,7 +49,24 @@ class NodesHandler { background: 'none', strokeWidth: 0, // px strokeColor: '#ffffff', - align: 'center' + align: 'center', + vadjust: 0, + multi: false, + bold: { + mod: 'bold' + }, + boldital: { + mod: 'bold italic' + }, + ital: { + mod: 'italic' + }, + mono: { + mod: '', + size: 15, // px + face: 'courier new', + vadjust: 2 + } }, group: undefined, hidden: false, @@ -135,6 +152,7 @@ class NodesHandler { } setOptions(options) { + this.nodeOptions = options; if (options !== undefined) { Node.parseOptions(this.options, options); @@ -301,7 +319,7 @@ class NodesHandler { * @param constructorClass */ create(properties, constructorClass = Node) { - return new constructorClass(properties, this.body, this.images, this.groups, this.options) + return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions, this.nodeOptions) } @@ -454,4 +472,4 @@ class NodesHandler { } } -export default NodesHandler; \ No newline at end of file +export default NodesHandler; diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index 8e383233..dc75ab1f 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -22,12 +22,14 @@ import StraightEdge from './edges/StraightEdge' * example for the color */ class Edge { - constructor(options, body, globalOptions) { + constructor(options, body, globalOptions, defaultOptions, edgeOptions) { if (body === undefined) { throw "No body provided"; } this.options = util.bridgeObject(globalOptions); this.globalOptions = globalOptions; + this.defaultOptions = defaultOptions; + this.edgeOptions = edgeOptions; this.body = body; // initialize variables @@ -50,7 +52,6 @@ class Edge { this.connected = false; this.labelModule = new Label(this.body, this.options, true /* It's an edge label */); - this.setOptions(options); } @@ -76,7 +77,8 @@ class Edge { // update label Module - this.updateLabelModule(); + this.updateLabelModule(options); + this.labelModule.propagateFonts(this.edgeOptions, options, this.defaultOptions); let dataChanged = this.updateEdgeType(); @@ -199,11 +201,12 @@ class Edge { /** * update the options in the label module */ - updateLabelModule() { + updateLabelModule(options) { this.labelModule.setOptions(this.options, true); if (this.labelModule.baseSize !== undefined) { this.baseFontSize = this.labelModule.baseSize; } + this.labelModule.constrain(this.edgeOptions, options, this.defaultOptions); } /** diff --git a/lib/network/modules/components/Node.js b/lib/network/modules/components/Node.js index d03aace0..e9c4976c 100644 --- a/lib/network/modules/components/Node.js +++ b/lib/network/modules/components/Node.js @@ -46,9 +46,11 @@ import {printStyle} from "../../../shared/Validator"; * */ class Node { - constructor(options, body, imagelist, grouplist, globalOptions) { + constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions, nodeOptions) { 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 @@ -117,7 +119,7 @@ class Node { // clear x and y positions if (options.x !== undefined) { if (options.x === null) {this.x = undefined; this.predefinedPosition = false;} - else {this.x = parseInt(options.x); this.predefinedPosition = true;} + else {this.x = parseInt(options.x); this.predefinedPosition = true;} } if (options.y !== undefined) { if (options.y === null) {this.y = undefined; this.predefinedPosition = false;} @@ -147,8 +149,9 @@ class Node { } } - this.updateLabelModule(); + this.updateLabelModule(options); this.updateShape(currentShape); + this.labelModule.propagateFonts(this.nodeOptions, options, this.defaultOptions); if (options.hidden !== undefined || options.physics !== undefined) { return true; @@ -216,7 +219,7 @@ class Node { } } - updateLabelModule() { + updateLabelModule(options) { if (this.options.label === undefined || this.options.label === null) { this.options.label = ''; } @@ -224,6 +227,7 @@ class Node { if (this.labelModule.baseSize !== undefined) { this.baseFontSize = this.labelModule.baseSize; } + this.labelModule.constrain(this.nodeOptions, options, this.defaultOptions); } updateShape(currentShape) { diff --git a/lib/network/modules/components/nodes/shapes/Box.js b/lib/network/modules/components/nodes/shapes/Box.js index a8b6e56d..1d484be9 100644 --- a/lib/network/modules/components/nodes/shapes/Box.js +++ b/lib/network/modules/components/nodes/shapes/Box.js @@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase' class Box extends NodeBase { constructor (options, body, labelModule) { super(options,body,labelModule); - this._setMargins() + this._setMargins(labelModule); } resize(ctx, selected) { @@ -81,4 +81,4 @@ class Box extends NodeBase { } } -export default Box; \ No newline at end of file +export default Box; diff --git a/lib/network/modules/components/nodes/shapes/Circle.js b/lib/network/modules/components/nodes/shapes/Circle.js index 2c42778b..7b4aa436 100644 --- a/lib/network/modules/components/nodes/shapes/Circle.js +++ b/lib/network/modules/components/nodes/shapes/Circle.js @@ -5,7 +5,7 @@ import CircleImageBase from '../util/CircleImageBase' class Circle extends CircleImageBase { constructor(options, body, labelModule) { super(options, body, labelModule) - this._setMargins(); + this._setMargins(labelModule); } resize(ctx, selected) { diff --git a/lib/network/modules/components/nodes/shapes/Database.js b/lib/network/modules/components/nodes/shapes/Database.js index f17e5457..3422cfdd 100644 --- a/lib/network/modules/components/nodes/shapes/Database.js +++ b/lib/network/modules/components/nodes/shapes/Database.js @@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase' class Database extends NodeBase { constructor (options, body, labelModule) { super(options, body, labelModule); - this._setMargins(); + this._setMargins(labelModule); } resize(ctx, selected) { @@ -74,4 +74,4 @@ class Database extends NodeBase { } } -export default Database; \ No newline at end of file +export default Database; diff --git a/lib/network/modules/components/nodes/shapes/Icon.js b/lib/network/modules/components/nodes/shapes/Icon.js index a5aae214..6562abd8 100644 --- a/lib/network/modules/components/nodes/shapes/Icon.js +++ b/lib/network/modules/components/nodes/shapes/Icon.js @@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase' class Icon extends NodeBase { constructor(options, body, labelModule) { super(options, body, labelModule); - this._setMargins(); + this._setMargins(labelModule); } resize(ctx) { @@ -80,4 +80,4 @@ class Icon extends NodeBase { } } -export default Icon; \ No newline at end of file +export default Icon; diff --git a/lib/network/modules/components/nodes/shapes/Text.js b/lib/network/modules/components/nodes/shapes/Text.js index d2941d4a..7d32c70b 100644 --- a/lib/network/modules/components/nodes/shapes/Text.js +++ b/lib/network/modules/components/nodes/shapes/Text.js @@ -5,7 +5,7 @@ import NodeBase from '../util/NodeBase' class Text extends NodeBase { constructor(options, body, labelModule) { super(options, body, labelModule); - this._setMargins(); + this._setMargins(labelModule); } resize(ctx, selected) { @@ -50,4 +50,4 @@ class Text extends NodeBase { } } -export default Text; \ No newline at end of file +export default Text; diff --git a/lib/network/modules/components/nodes/util/NodeBase.js b/lib/network/modules/components/nodes/util/NodeBase.js index 54956012..9d60fe4f 100644 --- a/lib/network/modules/components/nodes/util/NodeBase.js +++ b/lib/network/modules/components/nodes/util/NodeBase.js @@ -16,7 +16,7 @@ class NodeBase { this.options = options; } - _setMargins() { + _setMargins(labelModule) { this.margin = {}; if (this.options.margin) { if (typeof this.options.margin == 'object') { @@ -31,6 +31,7 @@ class NodeBase { this.margin.left = this.options.margin; } } + labelModule.adjustSizes(this.margin) } _distanceToBorder(ctx,angle) { diff --git a/lib/network/modules/components/shared/Label.js b/lib/network/modules/components/shared/Label.js index 9e00d431..05338dc8 100644 --- a/lib/network/modules/components/shared/Label.js +++ b/lib/network/modules/components/shared/Label.js @@ -1,7 +1,7 @@ let util = require('../../../../util'); class Label { - constructor(body,options,edgelabel = false) { + constructor(body, options, edgelabel = false) { this.body = body; this.pointToSelf = false; @@ -13,7 +13,7 @@ class Label { } setOptions(options, allowDeletion = false) { - this.nodeOptions = options; + this.elementOptions = options; // We want to keep the font options seperated from the node options. // The node options have to mirror the globals when they are not overruled. @@ -39,14 +39,195 @@ class Label { static parseOptions(parentOptions, newOptions, allowDeletion = false) { if (typeof newOptions.font === 'string') { let newOptionsArray = newOptions.font.split(" "); - parentOptions.size = newOptionsArray[0].replace("px",''); - parentOptions.face = newOptionsArray[1]; - parentOptions.color = newOptionsArray[2]; + parentOptions.size = newOptionsArray[0].replace("px",''); + parentOptions.face = newOptionsArray[1]; + parentOptions.color = newOptionsArray[2]; + parentOptions.vadjust = 0; } else if (typeof newOptions.font === 'object') { util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); } - parentOptions.size = Number(parentOptions.size); + parentOptions.size = Number(parentOptions.size); + parentOptions.vadjust = Number(parentOptions.vadjust); + } + + // set the width and height constraints based on 'nearest' value + constrain(elementOptions, options, defaultOptions) { + this.fontOptions.constrainWidth = false; + this.fontOptions.maxWdt = -1; + this.fontOptions.minWdt = -1; + + let pile = [options, elementOptions, defaultOptions]; + + let widthConstraint = util.topMost(pile, 'widthConstraint'); + if (typeof widthConstraint === 'number') { + this.fontOptions.maxWdt = Number(widthConstraint); + this.fontOptions.minWdt = Number(widthConstraint); + } else if (typeof widthConstraint === 'object') { + let widthConstraintMaximum = util.topMost(pile, ['widthConstraint', 'maximum']); + if (typeof widthConstraintMaximum === 'number') { + this.fontOptions.maxWdt = Number(widthConstraintMaximum); + } + let widthConstraintMinimum = util.topMost(pile, ['widthConstraint', 'minimum']) + if (typeof widthConstraintMinimum === 'number') { + this.fontOptions.minWdt = Number(widthConstraintMinimum); + } + } + + this.fontOptions.constrainHeight = false; + this.fontOptions.minHgt = -1; + this.fontOptions.valign = 'middle'; + + let heightConstraint = util.topMost(pile, 'heightConstraint'); + if (typeof heightConstraint === 'number') { + this.fontOptions.minHgt = Number(heightConstraint); + } else if (typeof heightConstraint === 'object') { + let heightConstraintMinimum = util.topMost(pile, ['heightConstraint', 'minimum']); + if (typeof heightConstraintMinimum === 'number') { + this.fontOptions.minHgt = Number(heightConstraintMinimum); + } + let heightConstraintValign = util.topMost(pile, ['heightConstraint', 'valign']); + if (typeof heightConstraintValign === 'string') { + if ((heightConstraintValign === 'top')||(heightConstraintValign === 'bottom')) { + this.fontOptions.valign = heightConstraintValign; + } + } + } + } + + // When margins are set in an element, adjust sizes is called to remove them + // from the width/height constraints. This must be done prior to label sizing. + adjustSizes(margins) { + let widthBias = (margins) ? (margins.right + margins.left) : 0; + if (this.fontOptions.constrainWidth) { + this.fontOptions.maxWdt -= widthBias; + this.fontOptions.minWdt -= widthBias; + } + let heightBias = (margins) ? (margins.top + margins.bottom) : 0; + if (this.fontOptions.constrainHeight) { + this.fontOptions.minHgt -= heightBias; + } + } + + propagateFonts(options, groupOptions, defaultOptions) { + if (this.fontOptions.multi) { + let mods = [ 'bold', 'ital', 'boldital', 'mono' ]; + for (const mod of mods) { + let optionsFontMod; + if (options.font) { + optionsFontMod = options.font[mod]; + } + if (typeof optionsFontMod === 'string') { + let modOptionsArray = optionsFontMod.split(" "); + this.fontOptions[mod].size = modOptionsArray[0].replace("px",""); + this.fontOptions[mod].face = modOptionsArray[1]; + this.fontOptions[mod].color = modOptionsArray[2]; + this.fontOptions[mod].vadjust = this.fontOptions.vadjust; + this.fontOptions[mod].mod = defaultOptions.font[mod].mod; + } else { + // 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: We want to capture any direct settings and overrides, but + // fall back to the base font if there aren't any. We make a + // special exception for mono, since we probably don't want to + // sync to a the base font face. + // + // if the mod face is in the node's options, use it + // else if the mod face is in the global options, use it + // else if the face is in the global options, use it + // else use the base font's face. + if (optionsFontMod && optionsFontMod.hasOwnProperty('face')) { + this.fontOptions[mod].face = optionsFontMod.face; + } else if (groupOptions.font && groupOptions.font[mod] && + groupOptions.font[mod].hasOwnProperty('face')) { + this.fontOptions[mod].face = groupOptions.font[mod].face; + } else if (mod === 'mono') { + this.fontOptions[mod].face = defaultOptions.font[mod].face; + } else if (groupOptions.font && + groupOptions.font.hasOwnProperty('face')) { + this.fontOptions[mod].face = groupOptions.font.face; + } else { + this.fontOptions[mod].face = this.fontOptions.face; + } + + // color: this is handled just like the face. + if (optionsFontMod && optionsFontMod.hasOwnProperty('color')) { + this.fontOptions[mod].color = optionsFontMod.color; + } else if (groupOptions.font && groupOptions.font[mod] && + groupOptions.font[mod].hasOwnProperty('color')) { + this.fontOptions[mod].color = groupOptions.font[mod].color; + } else if (groupOptions.font && + groupOptions.font.hasOwnProperty('color')) { + this.fontOptions[mod].color = groupOptions.font.color; + } else { + this.fontOptions[mod].color = this.fontOptions.color; + } + + // mod: this is handled just like the face, except we never grab the + // base font's mod. We know they're in the defaultOptions, and unless + // we've been steered away from them, we use the default. + if (optionsFontMod && optionsFontMod.hasOwnProperty('mod')) { + this.fontOptions[mod].mod = optionsFontMod.mod; + } else if (groupOptions.font && groupOptions.font[mod] && + groupOptions.font[mod].hasOwnProperty('mod')) { + this.fontOptions[mod].mod = groupOptions.font[mod].mod; + } else if (groupOptions.font && + groupOptions.font.hasOwnProperty('mod')) { + this.fontOptions[mod].mod = groupOptions.font.mod; + } else { + this.fontOptions[mod].mod = defaultOptions.font[mod].mod; + } + + // size: 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. + // + // if the mod size is in the node's options, use it + // else if the mod size is in the global options, use it + // else if the mod face is the same as the default and the base face + // is the same as the default, scale the mod size using the same + // ratio + // else if the size is in the global options, use it + // else use the base font's size. + if (optionsFontMod && optionsFontMod.hasOwnProperty('size')) { + this.fontOptions[mod].size = optionsFontMod.size; + } else if (groupOptions.font && groupOptions.font[mod] && + groupOptions.font[mod].hasOwnProperty('size')) { + this.fontOptions[mod].size = groupOptions.font[mod].size; + } else if ((this.fontOptions[mod].face === defaultOptions.font[mod].face) && + (this.fontOptions.face === defaultOptions.font.face)) { + let ratio = this.fontOptions.size / Number(defaultOptions.font.size); + this.fontOptions[mod].size = defaultOptions.font[mod].size * ratio; + } else if (groupOptions.font && + groupOptions.font.hasOwnProperty('size')) { + this.fontOptions[mod].size = groupOptions.font.size; + } else { + this.fontOptions[mod].size = this.fontOptions.size; + } + + // vadjust: this is handled just like the size. + if (optionsFontMod && optionsFontMod.hasOwnProperty('vadjust')) { + this.fontOptions[mod].vadjust = optionsFontMod.vadjust; + } else if (groupOptions.font && + groupOptions.font[mod] && groupOptions.font[mod].hasOwnProperty('vadjust')) { + this.fontOptions[mod].vadjust = groupOptions.font[mod].vadjust; + } else if ((this.fontOptions[mod].face === defaultOptions.font[mod].face) && + (this.fontOptions.face === defaultOptions.font.face)) { + let ratio = this.fontOptions.size / Number(defaultOptions.font.size); + this.fontOptions[mod].vadjust = defaultOptions.font[mod].vadjust * Math.round(ratio); + } else if (groupOptions.font && + groupOptions.font.hasOwnProperty('vadjust')) { + this.fontOptions[mod].vadjust = groupOptions.font.vadjust; + } else { + this.fontOptions[mod].vadjust = this.fontOptions.vadjust; + } + } + this.fontOptions[mod].size = Number(this.fontOptions[mod].size); + this.fontOptions[mod].vadjust = Number(this.fontOptions[mod].vadjust); + } + } } @@ -60,12 +241,12 @@ class Label { */ draw(ctx, x, y, selected, baseline = 'middle') { // if no label, return - if (this.nodeOptions.label === undefined) + if (this.elementOptions.label === undefined) return; // check if we have to render the label let viewFontSize = this.fontOptions.size * this.body.view.scale; - if (this.nodeOptions.label && viewFontSize < this.nodeOptions.scaling.label.drawThreshold - 1) + if (this.elementOptions.label && viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1) return; // update the size cache if required @@ -121,39 +302,49 @@ class Label { let fontSize = this.fontOptions.size; let viewFontSize = fontSize * this.body.view.scale; // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) - if (viewFontSize >= this.nodeOptions.scaling.label.maxVisible) { - fontSize = Number(this.nodeOptions.scaling.label.maxVisible) / this.body.view.scale; + if (viewFontSize >= this.elementOptions.scaling.label.maxVisible) { + fontSize = Number(this.elementOptions.scaling.label.maxVisible) / this.body.view.scale; } let yLine = this.size.yLine; - let [fontColor, strokeColor] = this._getColor(viewFontSize); [x, yLine] = this._setAlignment(ctx, x, yLine, baseline); - // configure context for drawing the text - ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + fontSize + "px " + this.fontOptions.face; - ctx.fillStyle = fontColor; - // When the textAlign property is 'left', make label left-justified - if ((!this.isEdgeLabel) && this.fontOptions.align === 'left') { - ctx.textAlign = this.fontOptions.align; - x = x - 0.5 * this.size.width; // Shift label 1/2-distance to the left - } else { - ctx.textAlign = 'center'; - } - - // set the strokeWidth - if (this.fontOptions.strokeWidth > 0) { - ctx.lineWidth = this.fontOptions.strokeWidth; - ctx.strokeStyle = strokeColor; - ctx.lineJoin = 'round'; + ctx.textAlign = 'left' + x = x - this.size.width / 2; // Shift label 1/2-distance to the left + if ((this.fontOptions.valign) && (this.size.height > this.size.labelHeight)) { + if (this.fontOptions.valign === 'top') { + yLine -= (this.size.height - this.size.labelHeight) / 2; + } + if (this.fontOptions.valign === 'bottom') { + yLine += (this.size.height - this.size.labelHeight) / 2; + } } // draw the text for (let i = 0; i < this.lineCount; i++) { - if (this.fontOptions.strokeWidth > 0) { - ctx.strokeText(this.lines[i], x, yLine); + if (this.lines[i] && this.lines[i].blocks) { + let width = 0; + if (this.isEdgeLabel || this.fontOptions.align === 'center') { + width += (this.size.width - this.lines[i].width) / 2 + } else if (this.fontOptions.align === 'right') { + width += (this.size.width - this.lines[i].width) + } + for (let j = 0; j < this.lines[i].blocks.length; j++) { + let block = this.lines[i].blocks[j]; + ctx.font = block.font; + let [fontColor, strokeColor] = this._getColor(block.color, viewFontSize); + if (this.fontOptions.strokeWidth > 0) { + ctx.lineWidth = this.fontOptions.strokeWidth; + ctx.strokeStyle = strokeColor; + ctx.lineJoin = 'round'; + ctx.strokeText(block.text, x + width, yLine + block.vadjust); + } + ctx.fillStyle = fontColor; + ctx.fillText(block.text, x + width, yLine + block.vadjust); + width += block.width; + } + yLine += this.lines[i].height; } - ctx.fillText(this.lines[i], x, yLine); - yLine += fontSize; } } @@ -180,7 +371,6 @@ class Label { else { ctx.textBaseline = baseline; } - return [x,yLine]; } @@ -192,11 +382,11 @@ class Label { * @returns {*[]} * @private */ - _getColor(viewFontSize) { - let fontColor = this.fontOptions.color || '#000000'; + _getColor(color, viewFontSize) { + let fontColor = color || '#000000'; let strokeColor = this.fontOptions.strokeColor || '#ffffff'; - if (viewFontSize <= this.nodeOptions.scaling.label.drawThreshold) { - let opacity = Math.max(0, Math.min(1, 1 - (this.nodeOptions.scaling.label.drawThreshold - viewFontSize))); + if (viewFontSize <= this.elementOptions.scaling.label.drawThreshold) { + let opacity = Math.max(0, Math.min(1, 1 - (this.elementOptions.scaling.label.drawThreshold - viewFontSize))); fontColor = util.overrideOpacity(fontColor, opacity); strokeColor = util.overrideOpacity(strokeColor, opacity); } @@ -211,12 +401,12 @@ class Label { * @returns {{width: number, height: number}} */ getTextSize(ctx, selected = false) { - let size = { - width: this._processLabel(ctx,selected), - height: this.fontOptions.size * this.lineCount, + this._processLabel(ctx, selected); + return { + width: this.size.width, + height: this.size.height, lineCount: this.lineCount }; - return size; } @@ -230,9 +420,8 @@ class Label { */ calculateLabelSize(ctx, selected, x = 0, y = 0, baseline = 'middle') { if (this.labelDirty === true) { - this.size.width = this._processLabel(ctx,selected); + this._processLabel(ctx, selected); } - this.size.height = this.fontOptions.size * this.lineCount; this.size.left = x - this.size.width * 0.5; this.size.top = y - this.size.height * 0.5; this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; @@ -241,37 +430,445 @@ class Label { this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers this.size.yLine += 4; // distance from node } - this.labelDirty = false; } + /** + * normalize the markup system + */ + decodeMarkupSystem(markupSystem) { + let system = 'none'; + if (markupSystem === 'markdown' || markupSystem === 'md') { + system = 'markdown'; + } else if (markupSystem === true || markupSystem === 'html') { + system = 'html' + } + return system; + } /** - * This calculates the width as well as explodes the label string and calculates the amount of lines. + * Explodes a piece of text into single-font blocks using a given markup + * @param text + * @param markupSystem + * @returns [{ text, mod }] + */ + splitBlocks(text, markupSystem) { + let system = this.decodeMarkupSystem(markupSystem); + if (system === 'none') { + return [{ + text: text, + mod: 'normal' + }] + } else if (system === 'markdown') { + return this.splitMarkdownBlocks(text); + } else if (system === 'html') { + return this.splitHtmlBlocks(text); + } + } + + splitMarkdownBlocks(text) { + let blocks = []; + let s = { + bold: false, + ital: false, + mono: false, + beginable: true, + spacing: false, + position: 0, + buffer: "", + modStack: [] + }; + s.mod = function() { + return (this.modStack.length === 0) ? 'normal' : this.modStack[0]; + } + s.modName = function() { + if (this.modStack.length === 0) + return 'normal'; + else if (this.modStack[0] === 'mono') + return 'mono'; + else { + if (s.bold && s.ital) { + return 'boldital'; + } else if (s.bold) { + return 'bold'; + } else if (s.ital) { + return 'ital'; + } + } + } + s.emitBlock = function(override = false) { + if (this.spacing) { + this.add(" "); + this.spacing = false; + } + if (this.buffer.length > 0) { + blocks.push({ text: this.buffer, mod: this.modName() }); + this.buffer = ""; + } + } + s.add = function(text) { + if (text === " ") { + s.spacing = true; + } + if (s.spacing) { + this.buffer += " "; + this.spacing = false; + } + if (text != " ") { + this.buffer += text; + } + } + while (s.position < text.length) { + let char = text.charAt(s.position); + if (/[ \t]/.test(char)) { + if (!s.mono) { + s.spacing = true; + } else { + s.add(char); + } + s.beginable = true + } else if (/\\/.test(char)) { + if (s.position < text.length+1) { + s.position++; + char = text.charAt(s.position); + if (/ \t/.test(char)) { + s.spacing = true; + } else { + s.add(char); + s.beginable = false; + } + } + } else if (!s.mono && !s.bold && (s.beginable || s.spacing) && /\*/.test(char)) { + s.emitBlock(); + s.bold = true; + s.modStack.unshift("bold"); + } else if (!s.mono && !s.ital && (s.beginable || s.spacing) && /\_/.test(char)) { + s.emitBlock(); + s.ital = true; + s.modStack.unshift("ital"); + } else if (!s.mono && (s.beginable || s.spacing) && /`/.test(char)) { + s.emitBlock(); + s.mono = true; + s.modStack.unshift("mono"); + } else if (!s.mono && (s.mod() === "bold") && /\*/.test(char)) { + if ((s.position === text.length-1) || /[.,_` \t\n]/.test(text.charAt(s.position+1))) { + s.emitBlock(); + s.bold = false; + s.modStack.shift(); + } else { + s.add(char); + } + } else if (!s.mono && (s.mod() === "ital") && /\_/.test(char)) { + if ((s.position === text.length-1) || /[.,*` \t\n]/.test(text.charAt(s.position+1))) { + s.emitBlock(); + s.ital = false; + s.modStack.shift(); + } else { + s.add(char); + } + } else if (s.mono && (s.mod() === "mono") && /`/.test(char)) { + if ((s.position === text.length-1) || (/[.,*_ \t\n]/.test(text.charAt(s.position+1)))) { + s.emitBlock(); + s.mono = false; + s.modStack.shift(); + } else { + s.add(char); + } + } else { + s.add(char); + s.beginable = false; + } + s.position++ + } + s.emitBlock(); + return blocks; + } + + splitHtmlBlocks(text) { + let blocks = []; + let s = { + bold: false, + ital: false, + mono: false, + spacing: false, + position: 0, + buffer: "", + modStack: [] + }; + s.mod = function() { + return (this.modStack.length === 0) ? 'normal' : this.modStack[0]; + } + s.modName = function() { + if (this.modStack.length === 0) + return 'normal'; + else if (this.modStack[0] === 'mono') + return 'mono'; + else { + if (s.bold && s.ital) { + return 'boldital'; + } else if (s.bold) { + return 'bold'; + } else if (s.ital) { + return 'ital'; + } + } + } + s.emitBlock = function(override = false) { + if (this.spacing) { + this.add(" "); + this.spacing = false; + } + if (this.buffer.length > 0) { + blocks.push({ text: this.buffer, mod: this.modName() }); + this.buffer = ""; + } + } + s.add = function(text) { + if (text === " ") { + s.spacing = true; + } + if (s.spacing) { + this.buffer += " "; + this.spacing = false; + } + if (text != " ") { + this.buffer += text; + } + } + while (s.position < text.length) { + let char = text.charAt(s.position); + if (/[ \t]/.test(char)) { + if (!s.mono) { + s.spacing = true; + } else { + s.add(char); + } + } else if (//.test(text.substr(s.position,3))) { + s.emitBlock(); + s.bold = true; + s.modStack.unshift("bold"); + s.position += 2; + } else if (!s.mono && !s.ital && //.test(text.substr(s.position,3))) { + s.emitBlock(); + s.ital = true; + s.modStack.unshift("ital"); + s.position += 2; + } else if (!s.mono && //.test(text.substr(s.position,6))) { + s.emitBlock(); + s.mono = true; + s.modStack.unshift("mono"); + s.position += 5; + } else if (!s.mono && (s.mod() === 'bold') && /<\/b>/.test(text.substr(s.position,4))) { + s.emitBlock(); + s.bold = false; + s.modStack.shift(); + s.position += 3; + } else if (!s.mono && (s.mod() === 'ital') && /<\/i>/.test(text.substr(s.position,4))) { + s.emitBlock(); + s.ital = false; + s.modStack.shift(); + s.position += 3; + } else if ((s.mod() === 'mono') && /<\/code>/.test(text.substr(s.position,7))) { + s.emitBlock(); + s.mono = false; + s.modStack.shift(); + s.position += 6; + } else { + s.add(char); + } + } else if (/&/.test(char)) { + if (/</.test(text.substr(s.position,4))) { + s.add("<"); + s.position += 3; + } else if (/&/.test(text.substr(s.position,5))) { + s.add("&"); + s.position += 4; + } else { + s.add("&"); + } + } else { + s.add(char); + } + s.position++ + } + s.emitBlock(); + return blocks; + } + + setFont(ctx, selected, mod) { + let height + let vadjust + let color + if (mod === 'normal') { + ctx.font = (selected && this.elementOptions.labelHighlightBold ? 'bold ' : '') + + this.fontOptions.size + "px " + this.fontOptions.face; + color = this.fontOptions.color; + height = this.fontOptions.size; + vadjust = this.fontOptions.vadjust; + } else { + ctx.font = this.fontOptions[mod].mod + " " + + this.fontOptions[mod].size + "px " + this.fontOptions[mod].face; + color = this.fontOptions[mod].color; + height = this.fontOptions[mod].size; + vadjust = this.fontOptions[mod].vadjust || 0; + } + return { + font: ctx.font.replace(/"/g, ""), + color: color, + height: height, + vadjust: vadjust + } + } + + /** + * This explodes the label string into lines and sets the width, height and number of lines. * @param ctx * @param selected - * @returns {number} * @private */ - _processLabel(ctx,selected) { + _processLabel(ctx, selected) { let width = 0; - let lines = ['']; - let lineCount = 0; - if (this.nodeOptions.label !== undefined) { - lines = String(this.nodeOptions.label).split('\n'); - lineCount = lines.length; - ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face; - width = ctx.measureText(lines[0]).width; - for (let i = 1; i < lineCount; i++) { - let lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; + let height = 0; + let nlLines = []; + let lines = []; + let k = 0; + lines.add = function(l, text, font, color, width, height, vadjust) { + if (this.length == l) { + this[l] = { width: 0, height: 0, blocks: [] }; + } + this[l].blocks.push({ text, font, color, width, height, vadjust }); + } + lines.accumulate = function(l, width, height) { + this[l].width += width; + this[l].height = height > this[l].height ? height : this[l].height; + } + lines.addAndAccumulate = function(l, text, font, color, width, height, vadjust) { + this.add(l, text, font, color, width, height, vadjust); + this.accumulate(l, width, height); + } + if (this.elementOptions.label !== undefined) { + let nlLines = String(this.elementOptions.label).split('\n'); + let lineCount = nlLines.length; + if (this.elementOptions.font.multi) { + for (let i = 0; i < lineCount; i++) { + let blocks = this.splitBlocks(nlLines[i], this.elementOptions.font.multi); + let lineWidth = 0; + let lineHeight = 0; + if (blocks) { + if (blocks.length == 0) { + this.setFont(ctx, selected, "normal"); + lines.addAndAccumulate(k, "", ctx.font, "#000000", 0, this.fontOptions.size, this.fontOptions.vadjust); + height += lines[k].height; + k++; + continue; + } + for (let j = 0; j < blocks.length; j++) { + if (this.fontOptions.maxWdt > 0) { + let metrics = this.setFont(ctx, selected, blocks[j].mod); + let words = blocks[j].text.split(" "); + let atStart = true + let text = ""; + let measure; + let lastMeasure; + let w = 0; + while (w < words.length) { + let pre = atStart ? "" : " "; + lastMeasure = measure; + measure = ctx.measureText(text + pre + words[w]); + if (lineWidth + measure.width > this.fontOptions.maxWdt) { + lineHeight = (metrics.height > lineHeight) ? metrics.height : lineHeight; + lines.add(k, text, ctx.font, metrics.color, lastMeasure.width, metrics.height, metrics.vadjust); + lines.accumulate(k, lastMeasure.width, lineHeight); + text = ""; + atStart = true; + lineWidth = 0; + width = lines[k].width > width ? lines[k].width : width; + height += lines[k].height; + k++; + } else { + text = text + pre + words[w]; + if (w === words.length-1) { + lineHeight = (metrics.height > lineHeight) ? metrics.height : lineHeight; + lineWidth += measure.width; + lines.add(k, text, ctx.font, metrics.color, measure.width, metrics.height, metrics.vadjust); + lines.accumulate(k, measure.width, lineHeight); + if (j === blocks.length-1) { + width = lines[k].width > width ? lines[k].width : width; + height += lines[k].height; + k++; + } + } + w++; + atStart = false; + } + } + } else { + let metrics = this.setFont(ctx, selected, blocks[j].mod) + let measure = ctx.measureText(blocks[j].text); + lines.addAndAccumulate(k, blocks[j].text, ctx.font, metrics.color, measure.width, metrics.height, metrics.vadjust); + width = lines[k].width > width ? lines[k].width : width; + if (blocks.length-1 === j) { + height += lines[k].height; + k++; + } + } + } + } + } + } else { + for (let i = 0; i < lineCount; i++) { + ctx.font = (selected && this.elementOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face; + if (this.fontOptions.maxWdt > 0) { + let words = nlLines[i].split(" "); + let text = ""; + let measure; + let lastMeasure; + let w = 0; + while (w < words.length) { + let pre = (text === "") ? "" : " "; + lastMeasure = measure; + measure = ctx.measureText(text + pre + words[w]); + if (measure.width > this.fontOptions.maxWdt) { + lines.addAndAccumulate(k, text, ctx.font, this.fontOptions.color, lastMeasure.width, this.fontOptions.size, this.fontOptions.vadjust) + width = lines[k].width > width ? lines[k].width : width; + height += lines[k].height; + text = ""; + k++; + } else { + text = text + pre + words[w]; + if (w === words.length-1) { + lines.addAndAccumulate(k, text, ctx.font, this.fontOptions.color, measure.width, this.fontOptions.size, this.fontOptions.vadjust) + width = lines[k].width > width ? lines[k].width : width; + height += lines[k].height; + k++; + } + w++; + } + } + } else { + let text = nlLines[i]; + let measure = ctx.measureText(text); + lines.addAndAccumulate(k, text, ctx.font, this.fontOptions.color, measure.width, this.fontOptions.size, this.fontOptions.vadjust); + width = lines[k].width > width ? lines[k].width : width; + height += lines[k].height; + k++; + } + } } } + if ((this.fontOptions.minWdt > 0) && (width < this.fontOptions.minWdt)) { + width = this.fontOptions.minWdt; + } + this.size.labelHeight = height; + if ((this.fontOptions.minHgt > 0) && (height < this.fontOptions.minHgt)) { + height = this.fontOptions.minHgt; + } this.lines = lines; - this.lineCount = lineCount; - - return width; + this.lineCount = lines.length; + this.size.width = width; + this.size.height = height; } } -export default Label; \ No newline at end of file +export default Label; diff --git a/lib/network/options.js b/lib/network/options.js index 008685fe..535a9373 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -47,6 +47,40 @@ let allOptions = { strokeWidth: { number }, // px strokeColor: { string }, align: { string: ['horizontal', 'top', 'middle', 'bottom'] }, + vadjust: { number }, + multi: { boolean, string }, + bold: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, + boldital: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, + ital: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, + mono: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, __type__: { object, string } }, hidden: { boolean }, @@ -88,6 +122,10 @@ let allOptions = { }, title: { string, 'undefined': 'undefined' }, width: { number }, + widthConstraint: { + maximum: { number }, + __type__: { object, boolean, number } + }, value: { number, 'undefined': 'undefined' }, __type__: { object } }, @@ -181,9 +219,48 @@ let allOptions = { background: { string }, strokeWidth: { number }, // px strokeColor: { string }, + vadjust: { number }, + multi: { boolean, string }, + bold: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, + boldital: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, + ital: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, + mono: { + color: { string }, + size: { number }, // px + face: { string }, + mod: { string }, + vadjust: { number }, + __type__: { object, string } + }, __type__: { object, string } }, group: { string, number, 'undefined': 'undefined' }, + heightConstraint: { + minimum: { number }, + valign: { string }, + __type__: { object, boolean, number } + }, hidden: { boolean }, icon: { face: { string }, @@ -240,6 +317,11 @@ let allOptions = { size: { number }, title: { string, 'undefined': 'undefined' }, value: { number, 'undefined': 'undefined' }, + widthConstraint: { + minimum: { number }, + maximum: { number }, + __type__: { object, boolean, number } + }, x: { number }, y: { number }, __type__: { object } diff --git a/lib/util.js b/lib/util.js index 9ced5862..22deac1d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1481,3 +1481,24 @@ exports.getScrollBarWidth = function () { return (w1 - w2); }; + +exports.topMost = function (pile, accessors) { + let candidate; + if (!Array.isArray(accessors)) { + accessors = [accessors]; + } + for (const member of pile) { + candidate = member[accessors[0]]; + for (let i = 1; i < accessors.length; i++){ + if (candidate) { + candidate = candidate[accessors[i]] + } else { + continue; + } + } + if (candidate) { + break; + } + } + return candidate; +}