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.

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