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.

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