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.

1176 lines
36 KiB

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