vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1228 lines
36 KiB

  1. let util = require('../../../../util');
  2. let ComponentUtil = require('./ComponentUtil').default;
  3. /**
  4. * Callback to determine text dimensions, using the parent label settings.
  5. * @callback MeasureText
  6. * @param {text} text
  7. * @returns {number}
  8. */
  9. /**
  10. * Internal helper class used for splitting a label text into lines.
  11. *
  12. * This has been moved away from the label processing code for better undestanding upon reading.
  13. *
  14. * @private
  15. */
  16. class LabelAccumulator {
  17. /**
  18. * @param {MeasureText} measureText
  19. */
  20. constructor(measureText) {
  21. this.measureText = measureText;
  22. this.current = 0;
  23. this.width = 0;
  24. this.height = 0;
  25. this.lines = [];
  26. }
  27. /**
  28. * Append given text to the given line.
  29. *
  30. * @param {number} l index of line to add to
  31. * @param {string} text string to append to line
  32. * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
  33. * @private
  34. */
  35. _add(l, text, mod = 'normal') {
  36. if (text === undefined || text === "") return;
  37. if (this.lines[l] === undefined) {
  38. this.lines[l] = {
  39. width : 0,
  40. height: 0,
  41. blocks: []
  42. };
  43. }
  44. // Determine width and get the font properties
  45. let result = this.measureText(text, mod);
  46. let block = Object.assign({}, result.values);
  47. block.text = text;
  48. block.width = result.width;
  49. block.mod = mod;
  50. this.lines[l].blocks.push(block);
  51. // Update the line width. We need this for
  52. // determining if a string goes over max width
  53. this.lines[l].width += result.width;
  54. }
  55. /**
  56. * Returns the width in pixels of the current line.
  57. *
  58. * @returns {number}
  59. */
  60. curWidth() {
  61. let line = this.lines[this.current];
  62. if (line === undefined) return 0;
  63. return line.width;
  64. }
  65. /**
  66. * Add text in block to current line
  67. *
  68. * @param {string} text
  69. * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
  70. */
  71. append(text, mod = 'normal') {
  72. this._add(this.current, text, mod);
  73. }
  74. /**
  75. * Add text in block to current line and start a new line
  76. *
  77. * @param {string} text
  78. * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
  79. */
  80. newLine(text, mod = 'normal') {
  81. this._add(this.current, text, mod);
  82. this.current++;
  83. }
  84. /**
  85. * Set the sizes for all lines and the whole thing.
  86. *
  87. * @returns {{width: (number|*), height: (number|*), lines: Array}}
  88. */
  89. finalize() {
  90. // console.log(JSON.stringify(this.lines, null, 2));
  91. // Determine the heights of the lines
  92. // Note that width has already been set
  93. for (let k = 0; k < this.lines.length; k++) {
  94. let line = this.lines[k];
  95. let height = 0;
  96. for (let l = 0; l < line.blocks.length; l++) {
  97. let block = line.blocks[l];
  98. height += block.height;
  99. }
  100. line.height = height;
  101. }
  102. // Determine the full label size
  103. let width = 0;
  104. let height = 0;
  105. for (let k = 0; k < this.lines.length; k++) {
  106. let line = this.lines[k];
  107. if (line.width > width) {
  108. width = line.width;
  109. }
  110. height += line.height;
  111. }
  112. this.width = width;
  113. this.height = height;
  114. // Return a simple hash object for further processing.
  115. return {
  116. width : this.width,
  117. height: this.height,
  118. lines : this.lines
  119. }
  120. }
  121. }
  122. /**
  123. * A Label to be used for Nodes or Edges.
  124. */
  125. class Label {
  126. /**
  127. * @param {Object} body
  128. * @param {Object} options
  129. * @param {boolean} [edgelabel=false]
  130. */
  131. constructor(body, options, edgelabel = false) {
  132. this.body = body;
  133. this.pointToSelf = false;
  134. this.baseSize = undefined;
  135. this.fontOptions = {};
  136. this.setOptions(options);
  137. this.size = {top: 0, left: 0, width: 0, height: 0, yLine: 0}; // could be cached
  138. this.isEdgeLabel = edgelabel;
  139. }
  140. /**
  141. *
  142. * @param {Object} options
  143. * @param {boolean} [allowDeletion=false]
  144. */
  145. setOptions(options, allowDeletion = false) {
  146. this.elementOptions = options;
  147. // We want to keep the font options separated from the node options.
  148. // The node options have to mirror the globals when they are not overruled.
  149. this.fontOptions = util.deepExtend({},options.font, true);
  150. if (options.label !== undefined) {
  151. this.labelDirty = true;
  152. }
  153. if (options.font !== undefined) {
  154. Label.parseOptions(this.fontOptions, options, allowDeletion);
  155. if (typeof options.font === 'string') {
  156. this.baseSize = this.fontOptions.size;
  157. }
  158. else if (typeof options.font === 'object') {
  159. if (options.font.size !== undefined) {
  160. this.baseSize = options.font.size;
  161. }
  162. }
  163. }
  164. }
  165. /**
  166. *
  167. * @param {Object} parentOptions
  168. * @param {Object} newOptions
  169. * @param {boolean} [allowDeletion=false]
  170. * @static
  171. */
  172. static parseOptions(parentOptions, newOptions, allowDeletion = false) {
  173. if (Label.parseFontString(parentOptions, newOptions.font)) {
  174. parentOptions.vadjust = 0;
  175. }
  176. else if (typeof newOptions.font === 'object') {
  177. util.fillIfDefined(parentOptions, newOptions.font, allowDeletion);
  178. }
  179. parentOptions.size = Number(parentOptions.size);
  180. parentOptions.vadjust = Number(parentOptions.vadjust);
  181. }
  182. /**
  183. * If in-variable is a string, parse it as a font specifier.
  184. *
  185. * Note that following is not done here and have to be done after the call:
  186. * - No number conversion (size)
  187. * - Not all font options are set (vadjust, mod)
  188. *
  189. * @param {Object} outOptions out-parameter, object in which to store the parse results (if any)
  190. * @param {Object} inOptions font options to parse
  191. * @return {boolean} true if font parsed as string, false otherwise
  192. * @static
  193. */
  194. static parseFontString(outOptions, inOptions) {
  195. if (!inOptions || typeof inOptions !== 'string') return false;
  196. let newOptionsArray = inOptions.split(" ");
  197. outOptions.size = newOptionsArray[0].replace("px",'');
  198. outOptions.face = newOptionsArray[1];
  199. outOptions.color = newOptionsArray[2];
  200. return true;
  201. }
  202. /**
  203. * Set the width and height constraints based on 'nearest' value
  204. * @param {Array} pile array of option objects to consider
  205. * @private
  206. */
  207. constrain(pile) {
  208. this.fontOptions.constrainWidth = false;
  209. this.fontOptions.maxWdt = -1;
  210. this.fontOptions.minWdt = -1;
  211. let widthConstraint = util.topMost(pile, 'widthConstraint');
  212. if (typeof widthConstraint === 'number') {
  213. this.fontOptions.maxWdt = Number(widthConstraint);
  214. this.fontOptions.minWdt = Number(widthConstraint);
  215. } else if (typeof widthConstraint === 'object') {
  216. let widthConstraintMaximum = util.topMost(pile, ['widthConstraint', 'maximum']);
  217. if (typeof widthConstraintMaximum === 'number') {
  218. this.fontOptions.maxWdt = Number(widthConstraintMaximum);
  219. }
  220. let widthConstraintMinimum = util.topMost(pile, ['widthConstraint', 'minimum'])
  221. if (typeof widthConstraintMinimum === 'number') {
  222. this.fontOptions.minWdt = Number(widthConstraintMinimum);
  223. }
  224. }
  225. this.fontOptions.constrainHeight = false;
  226. this.fontOptions.minHgt = -1;
  227. this.fontOptions.valign = 'middle';
  228. let heightConstraint = util.topMost(pile, 'heightConstraint');
  229. if (typeof heightConstraint === 'number') {
  230. this.fontOptions.minHgt = Number(heightConstraint);
  231. } else if (typeof heightConstraint === 'object') {
  232. let heightConstraintMinimum = util.topMost(pile, ['heightConstraint', 'minimum']);
  233. if (typeof heightConstraintMinimum === 'number') {
  234. this.fontOptions.minHgt = Number(heightConstraintMinimum);
  235. }
  236. let heightConstraintValign = util.topMost(pile, ['heightConstraint', 'valign']);
  237. if (typeof heightConstraintValign === 'string') {
  238. if ((heightConstraintValign === 'top')||(heightConstraintValign === 'bottom')) {
  239. this.fontOptions.valign = heightConstraintValign;
  240. }
  241. }
  242. }
  243. }
  244. /**
  245. * Set options and update internal state
  246. *
  247. * @param {Object} options options to set
  248. * @param {Array} pile array of option objects to consider for option 'chosen'
  249. */
  250. update(options, pile) {
  251. this.setOptions(options, true);
  252. this.constrain(pile);
  253. this.fontOptions.chooser = ComponentUtil.choosify('label', pile);
  254. }
  255. /**
  256. * When margins are set in an element, adjust sizes is called to remove them
  257. * from the width/height constraints. This must be done prior to label sizing.
  258. *
  259. * @param {{top: number, right: number, bottom: number, left: number}} margins
  260. */
  261. adjustSizes(margins) {
  262. let widthBias = (margins) ? (margins.right + margins.left) : 0;
  263. if (this.fontOptions.constrainWidth) {
  264. this.fontOptions.maxWdt -= widthBias;
  265. this.fontOptions.minWdt -= widthBias;
  266. }
  267. let heightBias = (margins) ? (margins.top + margins.bottom) : 0;
  268. if (this.fontOptions.constrainHeight) {
  269. this.fontOptions.minHgt -= heightBias;
  270. }
  271. }
  272. /**
  273. * Collapse the font options for the multi-font to single objects, from
  274. * the chain of option objects passed.
  275. *
  276. * If an option for a specific multi-font is not present, the parent
  277. * option is checked for the given option.
  278. *
  279. * NOTE: naming of 'groupOptions' is a misnomer; the actual value passed
  280. * is the new values to set from setOptions().
  281. *
  282. * @param {Object} options
  283. * @param {Object} groupOptions
  284. * @param {Object} defaultOptions
  285. */
  286. propagateFonts(options, groupOptions, defaultOptions) {
  287. if (!this.fontOptions.multi) return;
  288. /**
  289. * Resolve the font options path.
  290. * If valid, return a reference to the object in question.
  291. * Otherwise, just return null.
  292. *
  293. * @param {Object} options base object to determine path from
  294. * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod=undefined] if present, sub path for the mod-font
  295. * @returns {Object|null}
  296. */
  297. var pathP = function(options, mod) {
  298. if (!options || !options.font) return null;
  299. var opt = options.font;
  300. if (mod) {
  301. if (!opt[mod]) return null;
  302. opt = opt[mod];
  303. }
  304. return opt;
  305. };
  306. /**
  307. * Get property value from options.font[mod][property] if present.
  308. * If mod not passed, use property value from options.font[property].
  309. *
  310. * @param {Label.options} options
  311. * @param {'bold'|'ital'|'boldital'|'mono'|'normal'} mod
  312. * @param {string} property
  313. * @return {*|null} value if found, null otherwise.
  314. */
  315. var getP = function(options, mod, property) {
  316. let opt = pathP(options, mod);
  317. if (opt && opt.hasOwnProperty(property)) {
  318. return opt[property];
  319. }
  320. return null;
  321. };
  322. let mods = [ 'bold', 'ital', 'boldital', 'mono' ];
  323. for (const mod of mods) {
  324. let modOptions = this.fontOptions[mod];
  325. let modDefaults = defaultOptions.font[mod];
  326. if (Label.parseFontString(modOptions, pathP(options, mod))) {
  327. modOptions.vadjust = this.fontOptions.vadjust;
  328. modOptions.mod = modDefaults.mod;
  329. } else {
  330. // We need to be crafty about loading the modded fonts. We want as
  331. // much 'natural' versatility as we can get, so a simple global
  332. // change propagates in an expected way, even if not stictly logical.
  333. // 'face' has a special exception for mono, since we probably
  334. // don't want to sync to the base font face.
  335. modOptions.face =
  336. getP(options , mod, 'face') ||
  337. getP(groupOptions, mod, 'face') ||
  338. (mod === 'mono'? modDefaults.face:null ) ||
  339. getP(groupOptions, null, 'face') ||
  340. this.fontOptions.face
  341. ;
  342. // 'color' follows the standard flow
  343. modOptions.color =
  344. getP(options , mod, 'color') ||
  345. getP(groupOptions, mod, 'color') ||
  346. getP(groupOptions, null, 'color') ||
  347. this.fontOptions.color
  348. ;
  349. // 'mode' follows the standard flow
  350. modOptions.mod =
  351. getP(options , mod, 'mod') ||
  352. getP(groupOptions, mod, 'mod') ||
  353. getP(groupOptions, null, 'mod') ||
  354. modDefaults.mod
  355. ;
  356. // It's important that we size up defaults similarly if we're
  357. // using default faces unless overriden. We want to preserve the
  358. // ratios closely - but if faces have changed, all bets are off.
  359. let ratio;
  360. // NOTE: Following condition always fails, because modDefaults
  361. // has no explicit font property. This is deliberate, see
  362. // var's 'NodesHandler.defaultOptions.font[mod]'.
  363. // However, I want to keep the original logic while refactoring;
  364. // it appears to be working fine even if ratio is never set.
  365. // TODO: examine if this is a bug, fix if necessary.
  366. //
  367. if ((modOptions.face === modDefaults.face) &&
  368. (this.fontOptions.face === defaultOptions.font.face)) {
  369. ratio = this.fontOptions.size / Number(defaultOptions.font.size);
  370. }
  371. modOptions.size =
  372. getP(options , mod, 'size') ||
  373. getP(groupOptions, mod, 'size') ||
  374. (ratio? modDefaults.size * ratio: null) || // Scale the mod size using the same ratio
  375. getP(groupOptions, null, 'size') ||
  376. this.fontOptions.size
  377. ;
  378. modOptions.vadjust =
  379. getP(options , mod, 'vadjust') ||
  380. getP(groupOptions, mod, 'vadjust') ||
  381. (ratio? modDefaults.vadjust * Math.round(ratio): null) || // Scale it using the same ratio
  382. this.fontOptions.vadjust
  383. ;
  384. }
  385. modOptions.size = Number(modOptions.size);
  386. modOptions.vadjust = Number(modOptions.vadjust);
  387. }
  388. }
  389. /**
  390. * Main function. This is called from anything that wants to draw a label.
  391. * @param {CanvasRenderingContext2D} ctx
  392. * @param {number} x
  393. * @param {number} y
  394. * @param {boolean} selected
  395. * @param {boolean} hover
  396. * @param {string} [baseline='middle']
  397. */
  398. draw(ctx, x, y, selected, hover, baseline = 'middle') {
  399. // if no label, return
  400. if (this.elementOptions.label === undefined)
  401. return;
  402. // check if we have to render the label
  403. let viewFontSize = this.fontOptions.size * this.body.view.scale;
  404. if (this.elementOptions.label && viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1)
  405. return;
  406. // update the size cache if required
  407. this.calculateLabelSize(ctx, selected, hover, x, y, baseline);
  408. // create the fontfill background
  409. this._drawBackground(ctx);
  410. // draw text
  411. this._drawText(ctx, selected, hover, x, y, baseline);
  412. }
  413. /**
  414. * Draws the label background
  415. * @param {CanvasRenderingContext2D} ctx
  416. * @private
  417. */
  418. _drawBackground(ctx) {
  419. if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") {
  420. ctx.fillStyle = this.fontOptions.background;
  421. let lineMargin = 2;
  422. if (this.isEdgeLabel) {
  423. switch (this.fontOptions.align) {
  424. case 'middle':
  425. ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height);
  426. break;
  427. case 'top':
  428. ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height);
  429. break;
  430. case 'bottom':
  431. ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height);
  432. break;
  433. default:
  434. ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height);
  435. break;
  436. }
  437. } else {
  438. ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height);
  439. }
  440. }
  441. }
  442. /**
  443. *
  444. * @param {CanvasRenderingContext2D} ctx
  445. * @param {boolean} selected
  446. * @param {boolean} hover
  447. * @param {number} x
  448. * @param {number} y
  449. * @param {string} [baseline='middle']
  450. * @private
  451. */
  452. _drawText(ctx, selected, hover, x, y, baseline = 'middle') {
  453. let fontSize = this.fontOptions.size;
  454. let viewFontSize = fontSize * this.body.view.scale;
  455. // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
  456. if (viewFontSize >= this.elementOptions.scaling.label.maxVisible) {
  457. // TODO: Does this actually do anything?
  458. fontSize = Number(this.elementOptions.scaling.label.maxVisible) / this.body.view.scale;
  459. }
  460. let yLine = this.size.yLine;
  461. [x, yLine] = this._setAlignment(ctx, x, yLine, baseline);
  462. ctx.textAlign = 'left';
  463. x = x - this.size.width / 2; // Shift label 1/2-distance to the left
  464. if ((this.fontOptions.valign) && (this.size.height > this.size.labelHeight)) {
  465. if (this.fontOptions.valign === 'top') {
  466. yLine -= (this.size.height - this.size.labelHeight) / 2;
  467. }
  468. if (this.fontOptions.valign === 'bottom') {
  469. yLine += (this.size.height - this.size.labelHeight) / 2;
  470. }
  471. }
  472. // draw the text
  473. for (let i = 0; i < this.lineCount; i++) {
  474. if (this.lines[i] && this.lines[i].blocks) {
  475. let width = 0;
  476. if (this.isEdgeLabel || this.fontOptions.align === 'center') {
  477. width += (this.size.width - this.lines[i].width) / 2
  478. } else if (this.fontOptions.align === 'right') {
  479. width += (this.size.width - this.lines[i].width)
  480. }
  481. for (let j = 0; j < this.lines[i].blocks.length; j++) {
  482. let block = this.lines[i].blocks[j];
  483. ctx.font = block.font;
  484. let [fontColor, strokeColor] = this._getColor(block.color, viewFontSize, block.strokeColor);
  485. if (block.strokeWidth > 0) {
  486. ctx.lineWidth = block.strokeWidth;
  487. ctx.strokeStyle = strokeColor;
  488. ctx.lineJoin = 'round';
  489. }
  490. ctx.fillStyle = fontColor;
  491. if (block.strokeWidth > 0) {
  492. ctx.strokeText(block.text, x + width, yLine + block.vadjust);
  493. }
  494. ctx.fillText(block.text, x + width, yLine + block.vadjust);
  495. width += block.width;
  496. }
  497. yLine += this.lines[i].height;
  498. }
  499. }
  500. }
  501. /**
  502. *
  503. * @param {CanvasRenderingContext2D} ctx
  504. * @param {number} x
  505. * @param {number} yLine
  506. * @param {string} baseline
  507. * @returns {Array.<number>}
  508. * @private
  509. */
  510. _setAlignment(ctx, x, yLine, baseline) {
  511. // check for label alignment (for edges)
  512. // TODO: make alignment for nodes
  513. if (this.isEdgeLabel && this.fontOptions.align !== 'horizontal' && this.pointToSelf === false) {
  514. x = 0;
  515. yLine = 0;
  516. let lineMargin = 2;
  517. if (this.fontOptions.align === 'top') {
  518. ctx.textBaseline = 'alphabetic';
  519. yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
  520. }
  521. else if (this.fontOptions.align === 'bottom') {
  522. ctx.textBaseline = 'hanging';
  523. yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
  524. }
  525. else {
  526. ctx.textBaseline = 'middle';
  527. }
  528. }
  529. else {
  530. ctx.textBaseline = baseline;
  531. }
  532. return [x,yLine];
  533. }
  534. /**
  535. * fade in when relative scale is between threshold and threshold - 1.
  536. * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here.
  537. *
  538. * @param {string} color The font color to use
  539. * @param {number} viewFontSize
  540. * @param {string} initialStrokeColor
  541. * @returns {Array.<string>} An array containing the font color and stroke color
  542. * @private
  543. */
  544. _getColor(color, viewFontSize, initialStrokeColor) {
  545. let fontColor = color || '#000000';
  546. let strokeColor = initialStrokeColor || '#ffffff';
  547. if (viewFontSize <= this.elementOptions.scaling.label.drawThreshold) {
  548. let opacity = Math.max(0, Math.min(1, 1 - (this.elementOptions.scaling.label.drawThreshold - viewFontSize)));
  549. fontColor = util.overrideOpacity(fontColor, opacity);
  550. strokeColor = util.overrideOpacity(strokeColor, opacity);
  551. }
  552. return [fontColor, strokeColor];
  553. }
  554. /**
  555. *
  556. * @param {CanvasRenderingContext2D} ctx
  557. * @param {boolean} selected
  558. * @param {boolean} hover
  559. * @returns {{width: number, height: number}}
  560. */
  561. getTextSize(ctx, selected = false, hover = false) {
  562. this._processLabel(ctx, selected, hover);
  563. return {
  564. width: this.size.width,
  565. height: this.size.height,
  566. lineCount: this.lineCount
  567. };
  568. }
  569. /**
  570. *
  571. * @param {CanvasRenderingContext2D} ctx
  572. * @param {boolean} selected
  573. * @param {boolean} hover
  574. * @param {number} [x=0]
  575. * @param {number} [y=0]
  576. * @param {'middle'|'hanging'} [baseline='middle']
  577. */
  578. calculateLabelSize(ctx, selected, hover, x = 0, y = 0, baseline = 'middle') {
  579. if (this.labelDirty === true) {
  580. this._processLabel(ctx, selected, hover);
  581. }
  582. this.size.left = x - this.size.width * 0.5;
  583. this.size.top = y - this.size.height * 0.5;
  584. this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size;
  585. if (baseline === "hanging") {
  586. this.size.top += 0.5 * this.fontOptions.size;
  587. this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
  588. this.size.yLine += 4; // distance from node
  589. }
  590. this.labelDirty = false;
  591. }
  592. /**
  593. * normalize the markup system
  594. *
  595. * @param {boolean|'md'|'markdown'|'html'} markupSystem
  596. * @returns {string}
  597. */
  598. decodeMarkupSystem(markupSystem) {
  599. let system = 'none';
  600. if (markupSystem === 'markdown' || markupSystem === 'md') {
  601. system = 'markdown';
  602. } else if (markupSystem === true || markupSystem === 'html') {
  603. system = 'html'
  604. }
  605. return system;
  606. }
  607. /**
  608. * Explodes a piece of text into single-font blocks using a given markup
  609. * @param {string} text
  610. * @param {boolean|'md'|'markdown'|'html'} markupSystem
  611. * @returns {Array.<{text: string, mod: string}>}
  612. */
  613. splitBlocks(text, markupSystem) {
  614. let system = this.decodeMarkupSystem(markupSystem);
  615. if (system === 'none') {
  616. return [{
  617. text: text,
  618. mod: 'normal'
  619. }]
  620. } else if (system === 'markdown') {
  621. return this.splitMarkdownBlocks(text);
  622. } else if (system === 'html') {
  623. return this.splitHtmlBlocks(text);
  624. }
  625. }
  626. /**
  627. *
  628. * @param {string} text
  629. * @returns {Array}
  630. */
  631. splitMarkdownBlocks(text) {
  632. let blocks = [];
  633. let s = {
  634. bold: false,
  635. ital: false,
  636. mono: false,
  637. beginable: true,
  638. spacing: false,
  639. position: 0,
  640. buffer: "",
  641. modStack: []
  642. };
  643. s.mod = function() {
  644. return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
  645. };
  646. s.modName = function() {
  647. if (this.modStack.length === 0)
  648. return 'normal';
  649. else if (this.modStack[0] === 'mono')
  650. return 'mono';
  651. else {
  652. if (s.bold && s.ital) {
  653. return 'boldital';
  654. } else if (s.bold) {
  655. return 'bold';
  656. } else if (s.ital) {
  657. return 'ital';
  658. }
  659. }
  660. };
  661. s.emitBlock = function(override=false) { // eslint-disable-line no-unused-vars
  662. if (this.spacing) {
  663. this.add(" ");
  664. this.spacing = false;
  665. }
  666. if (this.buffer.length > 0) {
  667. blocks.push({ text: this.buffer, mod: this.modName() });
  668. this.buffer = "";
  669. }
  670. };
  671. s.add = function(text) {
  672. if (text === " ") {
  673. s.spacing = true;
  674. }
  675. if (s.spacing) {
  676. this.buffer += " ";
  677. this.spacing = false;
  678. }
  679. if (text != " ") {
  680. this.buffer += text;
  681. }
  682. };
  683. while (s.position < text.length) {
  684. let ch = text.charAt(s.position);
  685. if (/[ \t]/.test(ch)) {
  686. if (!s.mono) {
  687. s.spacing = true;
  688. } else {
  689. s.add(ch);
  690. }
  691. s.beginable = true
  692. } else if (/\\/.test(ch)) {
  693. if (s.position < text.length+1) {
  694. s.position++;
  695. ch = text.charAt(s.position);
  696. if (/ \t/.test(ch)) {
  697. s.spacing = true;
  698. } else {
  699. s.add(ch);
  700. s.beginable = false;
  701. }
  702. }
  703. } else if (!s.mono && !s.bold && (s.beginable || s.spacing) && /\*/.test(ch)) {
  704. s.emitBlock();
  705. s.bold = true;
  706. s.modStack.unshift("bold");
  707. } else if (!s.mono && !s.ital && (s.beginable || s.spacing) && /\_/.test(ch)) {
  708. s.emitBlock();
  709. s.ital = true;
  710. s.modStack.unshift("ital");
  711. } else if (!s.mono && (s.beginable || s.spacing) && /`/.test(ch)) {
  712. s.emitBlock();
  713. s.mono = true;
  714. s.modStack.unshift("mono");
  715. } else if (!s.mono && (s.mod() === "bold") && /\*/.test(ch)) {
  716. if ((s.position === text.length-1) || /[.,_` \t\n]/.test(text.charAt(s.position+1))) {
  717. s.emitBlock();
  718. s.bold = false;
  719. s.modStack.shift();
  720. } else {
  721. s.add(ch);
  722. }
  723. } else if (!s.mono && (s.mod() === "ital") && /\_/.test(ch)) {
  724. if ((s.position === text.length-1) || /[.,*` \t\n]/.test(text.charAt(s.position+1))) {
  725. s.emitBlock();
  726. s.ital = false;
  727. s.modStack.shift();
  728. } else {
  729. s.add(ch);
  730. }
  731. } else if (s.mono && (s.mod() === "mono") && /`/.test(ch)) {
  732. if ((s.position === text.length-1) || (/[.,*_ \t\n]/.test(text.charAt(s.position+1)))) {
  733. s.emitBlock();
  734. s.mono = false;
  735. s.modStack.shift();
  736. } else {
  737. s.add(ch);
  738. }
  739. } else {
  740. s.add(ch);
  741. s.beginable = false;
  742. }
  743. s.position++
  744. }
  745. s.emitBlock();
  746. return blocks;
  747. }
  748. /**
  749. *
  750. * @param {string} text
  751. * @returns {Array}
  752. */
  753. splitHtmlBlocks(text) {
  754. let blocks = [];
  755. let s = {
  756. bold: false,
  757. ital: false,
  758. mono: false,
  759. spacing: false,
  760. position: 0,
  761. buffer: "",
  762. modStack: []
  763. };
  764. s.mod = function() {
  765. return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
  766. };
  767. s.modName = function() {
  768. if (this.modStack.length === 0)
  769. return 'normal';
  770. else if (this.modStack[0] === 'mono')
  771. return 'mono';
  772. else {
  773. if (s.bold && s.ital) {
  774. return 'boldital';
  775. } else if (s.bold) {
  776. return 'bold';
  777. } else if (s.ital) {
  778. return 'ital';
  779. }
  780. }
  781. };
  782. s.emitBlock = function(override=false) { // eslint-disable-line no-unused-vars
  783. if (this.spacing) {
  784. this.add(" ");
  785. this.spacing = false;
  786. }
  787. if (this.buffer.length > 0) {
  788. blocks.push({ text: this.buffer, mod: this.modName() });
  789. this.buffer = "";
  790. }
  791. };
  792. s.add = function(text) {
  793. if (text === " ") {
  794. s.spacing = true;
  795. }
  796. if (s.spacing) {
  797. this.buffer += " ";
  798. this.spacing = false;
  799. }
  800. if (text != " ") {
  801. this.buffer += text;
  802. }
  803. };
  804. while (s.position < text.length) {
  805. let ch = text.charAt(s.position);
  806. if (/[ \t]/.test(ch)) {
  807. if (!s.mono) {
  808. s.spacing = true;
  809. } else {
  810. s.add(ch);
  811. }
  812. } else if (/</.test(ch)) {
  813. if (!s.mono && !s.bold && /<b>/.test(text.substr(s.position,3))) {
  814. s.emitBlock();
  815. s.bold = true;
  816. s.modStack.unshift("bold");
  817. s.position += 2;
  818. } else if (!s.mono && !s.ital && /<i>/.test(text.substr(s.position,3))) {
  819. s.emitBlock();
  820. s.ital = true;
  821. s.modStack.unshift("ital");
  822. s.position += 2;
  823. } else if (!s.mono && /<code>/.test(text.substr(s.position,6))) {
  824. s.emitBlock();
  825. s.mono = true;
  826. s.modStack.unshift("mono");
  827. s.position += 5;
  828. } else if (!s.mono && (s.mod() === 'bold') && /<\/b>/.test(text.substr(s.position,4))) {
  829. s.emitBlock();
  830. s.bold = false;
  831. s.modStack.shift();
  832. s.position += 3;
  833. } else if (!s.mono && (s.mod() === 'ital') && /<\/i>/.test(text.substr(s.position,4))) {
  834. s.emitBlock();
  835. s.ital = false;
  836. s.modStack.shift();
  837. s.position += 3;
  838. } else if ((s.mod() === 'mono') && /<\/code>/.test(text.substr(s.position,7))) {
  839. s.emitBlock();
  840. s.mono = false;
  841. s.modStack.shift();
  842. s.position += 6;
  843. } else {
  844. s.add(ch);
  845. }
  846. } else if (/&/.test(ch)) {
  847. if (/&lt;/.test(text.substr(s.position,4))) {
  848. s.add("<");
  849. s.position += 3;
  850. } else if (/&amp;/.test(text.substr(s.position,5))) {
  851. s.add("&");
  852. s.position += 4;
  853. } else {
  854. s.add("&");
  855. }
  856. } else {
  857. s.add(ch);
  858. }
  859. s.position++
  860. }
  861. s.emitBlock();
  862. return blocks;
  863. }
  864. /**
  865. *
  866. * @param {CanvasRenderingContext2D} ctx
  867. * @param {boolean} selected
  868. * @param {boolean} hover
  869. * @param {string} mod
  870. * @returns {{color, size, face, mod, vadjust, strokeWidth: *, strokeColor: (*|string|allOptions.edges.font.strokeColor|{string}|allOptions.nodes.font.strokeColor|Array)}}
  871. */
  872. getFormattingValues(ctx, selected, hover, mod) {
  873. var getValue = function(fontOptions, mod, option) {
  874. if (mod === "normal") {
  875. if (option === 'mod' ) return "";
  876. return fontOptions[option];
  877. }
  878. if (fontOptions[mod][option]) {
  879. return fontOptions[mod][option];
  880. } else {
  881. // Take from parent font option
  882. return fontOptions[option];
  883. }
  884. };
  885. let values = {
  886. color : getValue(this.fontOptions, mod, 'color' ),
  887. size : getValue(this.fontOptions, mod, 'size' ),
  888. face : getValue(this.fontOptions, mod, 'face' ),
  889. mod : getValue(this.fontOptions, mod, 'mod' ),
  890. vadjust: getValue(this.fontOptions, mod, 'vadjust'),
  891. strokeWidth: this.fontOptions.strokeWidth,
  892. strokeColor: this.fontOptions.strokeColor
  893. };
  894. if (selected || hover) {
  895. if (mod === "normal" && (this.fontOptions.chooser === true) && (this.elementOptions.labelHighlightBold)) {
  896. values.mod = 'bold';
  897. } else {
  898. if (typeof this.fontOptions.chooser === 'function') {
  899. this.fontOptions.chooser(values, this.elementOptions.id, selected, hover);
  900. }
  901. }
  902. }
  903. ctx.font = (values.mod + " " + values.size + "px " + values.face).replace(/"/g, "");
  904. values.font = ctx.font;
  905. values.height = values.size;
  906. return values;
  907. }
  908. /**
  909. *
  910. * @param {boolean} selected
  911. * @param {boolean} hover
  912. * @returns {boolean}
  913. */
  914. differentState(selected, hover) {
  915. return ((selected !== this.fontOptions.selectedState) && (hover !== this.fontOptions.hoverState));
  916. }
  917. /**
  918. * This explodes the passed text into lines and determines the width, height and number of lines.
  919. *
  920. * @param {CanvasRenderingContext2D} ctx
  921. * @param {boolean} selected
  922. * @param {boolean} hover
  923. * @param {string} text the text to explode
  924. * @returns {{width, height, lines}|*}
  925. * @private
  926. */
  927. _processLabelText(ctx, selected, hover, text) {
  928. let self = this;
  929. /**
  930. * Callback to determine text width; passed to LabelAccumulator instance
  931. *
  932. * @param {String} text string to determine width of
  933. * @param {String} mod font type to use for this text
  934. * @return {Object} { width, values} width in pixels and font attributes
  935. */
  936. let textWidth = function(text, mod) {
  937. if (text === undefined) return 0;
  938. // TODO: This can be done more efficiently with caching
  939. let values = self.getFormattingValues(ctx, selected, hover, mod);
  940. let width = 0;
  941. if (text !== '') {
  942. // NOTE: The following may actually be *incorrect* for the mod fonts!
  943. // This returns the size with a regular font, bold etc. may
  944. // have different sizes.
  945. let measure = ctx.measureText(text);
  946. width = measure.width;
  947. }
  948. return {width, values: values};
  949. };
  950. let lines = new LabelAccumulator(textWidth);
  951. if (text === undefined || text === "") {
  952. return lines.finalize();
  953. }
  954. let overMaxWidth = function(text) {
  955. let width = ctx.measureText(text).width;
  956. return (lines.curWidth() + width > self.fontOptions.maxWdt);
  957. };
  958. /**
  959. * Determine the longest part of the sentence which still fits in the
  960. * current max width.
  961. *
  962. * @param {Array} words Array of strings signifying a text lines
  963. * @return {number} index of first item in string making string go over max
  964. */
  965. let getLongestFit = function(words) {
  966. let text = '';
  967. let w = 0;
  968. while (w < words.length) {
  969. let pre = (text === '') ? '' : ' ';
  970. let newText = text + pre + words[w];
  971. if (overMaxWidth(newText)) break;
  972. text = newText;
  973. w++;
  974. }
  975. return w;
  976. };
  977. /**
  978. * Determine the longest part of the string which still fits in the
  979. * current max width.
  980. *
  981. * @param {Array} words Array of strings signifying a text lines
  982. * @return {number} index of first item in string making string go over max
  983. */
  984. let getLongestFitWord = function(words) {
  985. let w = 0;
  986. while (w < words.length) {
  987. if (overMaxWidth(words.slice(0,w))) break;
  988. w++;
  989. }
  990. return w;
  991. };
  992. let splitStringIntoLines = function(str, mod = 'normal', appendLast = false) {
  993. let words = str.split(" ");
  994. while (words.length > 0) {
  995. let w = getLongestFit(words);
  996. if (w === 0) {
  997. // Special case: the first word may already
  998. // be larger than the max width.
  999. let word = words[0];
  1000. // Break the word to the largest part that fits the line
  1001. let x = getLongestFitWord(word);
  1002. lines.newLine(word.slice(0, x), mod);
  1003. // Adjust the word, so that the rest will be done next iteration
  1004. words[0] = word.slice(x);
  1005. } else {
  1006. let text = words.slice(0, w).join(" ");
  1007. if (w == words.length && appendLast) {
  1008. lines.append(text, mod);
  1009. } else {
  1010. lines.newLine(text, mod);
  1011. }
  1012. words = words.slice(w);
  1013. }
  1014. }
  1015. };
  1016. let nlLines = String(text).split('\n');
  1017. let lineCount = nlLines.length;
  1018. if (this.elementOptions.font.multi) {
  1019. // Multi-font case: styling tags active
  1020. for (let i = 0; i < lineCount; i++) {
  1021. let blocks = this.splitBlocks(nlLines[i], this.elementOptions.font.multi);
  1022. if (blocks === undefined) continue;
  1023. if (blocks.length === 0) {
  1024. lines.newLine("");
  1025. continue;
  1026. }
  1027. if (this.fontOptions.maxWdt > 0) {
  1028. // widthConstraint.maximum defined
  1029. //console.log('Running widthConstraint multi, max: ' + this.fontOptions.maxWdt);
  1030. for (let j = 0; j < blocks.length; j++) {
  1031. let mod = blocks[j].mod;
  1032. let text = blocks[j].text;
  1033. splitStringIntoLines(text, mod, true);
  1034. }
  1035. } else {
  1036. // widthConstraint.maximum NOT defined
  1037. for (let j = 0; j < blocks.length; j++) {
  1038. let mod = blocks[j].mod;
  1039. let text = blocks[j].text;
  1040. lines.append(text, mod);
  1041. }
  1042. }
  1043. lines.newLine();
  1044. }
  1045. } else {
  1046. // Single-font case
  1047. if (this.fontOptions.maxWdt > 0) {
  1048. // widthConstraint.maximum defined
  1049. // console.log('Running widthConstraint normal, max: ' + this.fontOptions.maxWdt);
  1050. for (let i = 0; i < lineCount; i++) {
  1051. splitStringIntoLines(nlLines[i]);
  1052. }
  1053. } else {
  1054. // widthConstraint.maximum NOT defined
  1055. for (let i = 0; i < lineCount; i++) {
  1056. lines.newLine(nlLines[i]);
  1057. }
  1058. }
  1059. }
  1060. return lines.finalize();
  1061. }
  1062. /**
  1063. * This explodes the label string into lines and sets the width, height and number of lines.
  1064. * @param {CanvasRenderingContext2D} ctx
  1065. * @param {boolean} selected
  1066. * @param {boolean} hover
  1067. * @private
  1068. */
  1069. _processLabel(ctx, selected, hover) {
  1070. let state = this._processLabelText(ctx, selected, hover, this.elementOptions.label);
  1071. if ((this.fontOptions.minWdt > 0) && (state.width < this.fontOptions.minWdt)) {
  1072. state.width = this.fontOptions.minWdt;
  1073. }
  1074. this.size.labelHeight =state.height;
  1075. if ((this.fontOptions.minHgt > 0) && (state.height < this.fontOptions.minHgt)) {
  1076. state.height = this.fontOptions.minHgt;
  1077. }
  1078. this.lines = state.lines;
  1079. this.lineCount = state.lines.length;
  1080. this.size.width = state.width;
  1081. this.size.height = state.height;
  1082. this.selectedState = selected;
  1083. this.hoverState = hover;
  1084. }
  1085. }
  1086. export default Label;