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.

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