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.

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