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.

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