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.

953 lines
32 KiB

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