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.

1225 lines
36 KiB

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