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.

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