SVGLabel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. /* *
  2. *
  3. * (c) 2010-2020 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. var __extends = (this && this.__extends) || (function () {
  12. var extendStatics = function (d, b) {
  13. extendStatics = Object.setPrototypeOf ||
  14. ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
  15. function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
  16. return extendStatics(d, b);
  17. };
  18. return function (d, b) {
  19. extendStatics(d, b);
  20. function __() { this.constructor = d; }
  21. d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  22. };
  23. })();
  24. import SVGElement from './SVGElement.js';
  25. import U from './Utilities.js';
  26. var defined = U.defined, extend = U.extend, isNumber = U.isNumber, merge = U.merge, removeEvent = U.removeEvent;
  27. /**
  28. * SVG label to render text.
  29. * @private
  30. * @class
  31. * @name Highcharts.SVGLabel
  32. * @augments Highcharts.SVGElement
  33. */
  34. var SVGLabel = /** @class */ (function (_super) {
  35. __extends(SVGLabel, _super);
  36. /* *
  37. *
  38. * Constructors
  39. *
  40. * */
  41. function SVGLabel(renderer, str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
  42. var _this = _super.call(this) || this;
  43. _this.init(renderer, 'g');
  44. _this.textStr = str;
  45. _this.x = x;
  46. _this.y = y;
  47. _this.anchorX = anchorX;
  48. _this.anchorY = anchorY;
  49. _this.baseline = baseline;
  50. _this.className = className;
  51. if (className !== 'button') {
  52. _this.addClass('highcharts-label');
  53. }
  54. if (className) {
  55. _this.addClass('highcharts-' + className);
  56. }
  57. _this.text = renderer.text('', 0, 0, useHTML)
  58. .attr({
  59. zIndex: 1
  60. });
  61. // Validate the shape argument
  62. var hasBGImage;
  63. if (typeof shape === 'string') {
  64. hasBGImage = /^url\((.*?)\)$/.test(shape);
  65. if (_this.renderer.symbols[shape] || hasBGImage) {
  66. _this.symbolKey = shape;
  67. }
  68. }
  69. _this.bBox = SVGLabel.emptyBBox;
  70. _this.padding = 3;
  71. _this.paddingLeft = 0;
  72. _this.baselineOffset = 0;
  73. _this.needsBox = renderer.styledMode || hasBGImage;
  74. _this.deferredAttr = {};
  75. _this.alignFactor = 0;
  76. return _this;
  77. }
  78. /* *
  79. *
  80. * Functions
  81. *
  82. * */
  83. SVGLabel.prototype.alignSetter = function (value) {
  84. var alignFactor = {
  85. left: 0,
  86. center: 0.5,
  87. right: 1
  88. }[value];
  89. if (alignFactor !== this.alignFactor) {
  90. this.alignFactor = alignFactor;
  91. // Bounding box exists, means we're dynamically changing
  92. if (this.bBox && isNumber(this.xSetting)) {
  93. this.attr({ x: this.xSetting }); // #5134
  94. }
  95. }
  96. };
  97. SVGLabel.prototype.anchorXSetter = function (value, key) {
  98. this.anchorX = value;
  99. this.boxAttr(key, Math.round(value) - this.getCrispAdjust() - this.xSetting);
  100. };
  101. SVGLabel.prototype.anchorYSetter = function (value, key) {
  102. this.anchorY = value;
  103. this.boxAttr(key, value - this.ySetting);
  104. };
  105. /*
  106. * Set a box attribute, or defer it if the box is not yet created
  107. */
  108. SVGLabel.prototype.boxAttr = function (key, value) {
  109. if (this.box) {
  110. this.box.attr(key, value);
  111. }
  112. else {
  113. this.deferredAttr[key] = value;
  114. }
  115. };
  116. /*
  117. * Pick up some properties and apply them to the text instead of the
  118. * wrapper.
  119. */
  120. SVGLabel.prototype.css = function (styles) {
  121. if (styles) {
  122. var textStyles = {}, isWidth, isFontStyle;
  123. // Create a copy to avoid altering the original object
  124. // (#537)
  125. styles = merge(styles);
  126. SVGLabel.textProps.forEach(function (prop) {
  127. if (typeof styles[prop] !== 'undefined') {
  128. textStyles[prop] = styles[prop];
  129. delete styles[prop];
  130. }
  131. });
  132. this.text.css(textStyles);
  133. isWidth = 'width' in textStyles;
  134. isFontStyle = 'fontSize' in textStyles ||
  135. 'fontWeight' in textStyles;
  136. // Update existing text, box (#9400, #12163)
  137. if (isWidth || isFontStyle) {
  138. this.updateBoxSize();
  139. // Keep updated (#9400, #12163)
  140. if (isFontStyle) {
  141. this.updateTextPadding();
  142. }
  143. }
  144. }
  145. return SVGElement.prototype.css.call(this, styles);
  146. };
  147. /*
  148. * Destroy and release memory.
  149. */
  150. SVGLabel.prototype.destroy = function () {
  151. // Added by button implementation
  152. removeEvent(this.element, 'mouseenter');
  153. removeEvent(this.element, 'mouseleave');
  154. if (this.text) {
  155. this.text.destroy();
  156. }
  157. if (this.box) {
  158. this.box = this.box.destroy();
  159. }
  160. // Call base implementation to destroy the rest
  161. SVGElement.prototype.destroy.call(this);
  162. return void 0;
  163. };
  164. SVGLabel.prototype.fillSetter = function (value, key) {
  165. if (value) {
  166. this.needsBox = true;
  167. }
  168. // for animation getter (#6776)
  169. this.fill = value;
  170. this.boxAttr(key, value);
  171. };
  172. /*
  173. * Return the bounding box of the box, not the group.
  174. */
  175. SVGLabel.prototype.getBBox = function () {
  176. var bBox = this.bBox;
  177. var padding = this.padding;
  178. return {
  179. width: bBox.width + 2 * padding,
  180. height: bBox.height + 2 * padding,
  181. x: bBox.x - padding,
  182. y: bBox.y - padding
  183. };
  184. };
  185. SVGLabel.prototype.getCrispAdjust = function () {
  186. return this.renderer.styledMode && this.box ?
  187. this.box.strokeWidth() % 2 / 2 :
  188. (this['stroke-width'] ? parseInt(this['stroke-width'], 10) : 0) % 2 / 2;
  189. };
  190. SVGLabel.prototype.heightSetter = function (value) {
  191. this.heightSetting = value;
  192. };
  193. // Event handling. In case of useHTML, we need to make sure that events
  194. // are captured on the span as well, and that mouseenter/mouseleave
  195. // between the SVG group and the HTML span are not treated as real
  196. // enter/leave events. #13310.
  197. SVGLabel.prototype.on = function (eventType, handler) {
  198. var label = this;
  199. var text = label.text;
  200. var span = text && text.element.tagName === 'SPAN' ? text : void 0;
  201. var selectiveHandler;
  202. if (span) {
  203. selectiveHandler = function (e) {
  204. if ((eventType === 'mouseenter' ||
  205. eventType === 'mouseleave') &&
  206. e.relatedTarget instanceof Element &&
  207. (label.element.contains(e.relatedTarget) ||
  208. span.element.contains(e.relatedTarget))) {
  209. return;
  210. }
  211. handler.call(label.element, e);
  212. };
  213. span.on(eventType, selectiveHandler);
  214. }
  215. SVGElement.prototype.on.call(label, eventType, selectiveHandler || handler);
  216. return label;
  217. };
  218. /*
  219. * After the text element is added, get the desired size of the border
  220. * box and add it before the text in the DOM.
  221. */
  222. SVGLabel.prototype.onAdd = function () {
  223. var str = this.textStr;
  224. this.text.add(this);
  225. this.attr({
  226. // Alignment is available now (#3295, 0 not rendered if given
  227. // as a value)
  228. text: (defined(str) ? str : ''),
  229. x: this.x,
  230. y: this.y
  231. });
  232. if (this.box && defined(this.anchorX)) {
  233. this.attr({
  234. anchorX: this.anchorX,
  235. anchorY: this.anchorY
  236. });
  237. }
  238. };
  239. SVGLabel.prototype.paddingSetter = function (value) {
  240. if (defined(value) && value !== this.padding) {
  241. this.padding = value;
  242. this.updateTextPadding();
  243. }
  244. };
  245. SVGLabel.prototype.paddingLeftSetter = function (value) {
  246. if (defined(value) && value !== this.paddingLeft) {
  247. this.paddingLeft = value;
  248. this.updateTextPadding();
  249. }
  250. };
  251. SVGLabel.prototype.rSetter = function (value, key) {
  252. this.boxAttr(key, value);
  253. };
  254. SVGLabel.prototype.shadow = function (b) {
  255. if (b && !this.renderer.styledMode) {
  256. this.updateBoxSize();
  257. if (this.box) {
  258. this.box.shadow(b);
  259. }
  260. }
  261. return this;
  262. };
  263. SVGLabel.prototype.strokeSetter = function (value, key) {
  264. // for animation getter (#6776)
  265. this.stroke = value;
  266. this.boxAttr(key, value);
  267. };
  268. SVGLabel.prototype['stroke-widthSetter'] = function (value, key) {
  269. if (value) {
  270. this.needsBox = true;
  271. }
  272. this['stroke-width'] = value;
  273. this.boxAttr(key, value);
  274. };
  275. SVGLabel.prototype['text-alignSetter'] = function (value) {
  276. this.textAlign = value;
  277. };
  278. SVGLabel.prototype.textSetter = function (text) {
  279. if (typeof text !== 'undefined') {
  280. // Must use .attr to ensure transforms are done (#10009)
  281. this.text.attr({ text: text });
  282. }
  283. this.updateBoxSize();
  284. this.updateTextPadding();
  285. };
  286. /*
  287. * This function runs after the label is added to the DOM (when the bounding
  288. * box is available), and after the text of the label is updated to detect
  289. * the new bounding box and reflect it in the border box.
  290. */
  291. SVGLabel.prototype.updateBoxSize = function () {
  292. var style = this.text.element.style, crispAdjust, attribs = {};
  293. var padding = this.padding;
  294. var paddingLeft = this.paddingLeft;
  295. // #12165 error when width is null (auto)
  296. // #12163 when fontweight: bold, recalculate bBox withot cache
  297. // #3295 && 3514 box failure when string equals 0
  298. var bBox = ((!isNumber(this.widthSetting) || !isNumber(this.heightSetting) || this.textAlign) &&
  299. defined(this.text.textStr)) ?
  300. this.text.getBBox() : SVGLabel.emptyBBox;
  301. this.width = ((this.widthSetting || bBox.width || 0) +
  302. 2 * padding +
  303. paddingLeft);
  304. this.height = (this.heightSetting || bBox.height || 0) + 2 * padding;
  305. // Update the label-scoped y offset. Math.min because of inline
  306. // style (#9400)
  307. this.baselineOffset = padding + Math.min(this.renderer.fontMetrics(style && style.fontSize, this.text).b,
  308. // When the height is 0, there is no bBox, so go with the font
  309. // metrics. Highmaps CSS demos.
  310. bBox.height || Infinity);
  311. if (this.needsBox) {
  312. // Create the border box if it is not already present
  313. if (!this.box) {
  314. // Symbol definition exists (#5324)
  315. var box = this.box = this.symbolKey ?
  316. this.renderer.symbol(this.symbolKey) :
  317. this.renderer.rect();
  318. box.addClass(// Don't use label className for buttons
  319. (this.className === 'button' ? '' : 'highcharts-label-box') +
  320. (this.className ? ' highcharts-' + this.className + '-box' : ''));
  321. box.add(this);
  322. crispAdjust = this.getCrispAdjust();
  323. attribs.x = crispAdjust;
  324. attribs.y = (this.baseline ? -this.baselineOffset : 0) + crispAdjust;
  325. }
  326. // Apply the box attributes
  327. attribs.width = Math.round(this.width);
  328. attribs.height = Math.round(this.height);
  329. this.box.attr(extend(attribs, this.deferredAttr));
  330. this.deferredAttr = {};
  331. }
  332. this.bBox = bBox;
  333. };
  334. /*
  335. * This function runs after setting text or padding, but only if padding
  336. * is changed.
  337. */
  338. SVGLabel.prototype.updateTextPadding = function () {
  339. var text = this.text;
  340. // Determine y based on the baseline
  341. var textY = this.baseline ? 0 : this.baselineOffset;
  342. var textX = this.paddingLeft + this.padding;
  343. // compensate for alignment
  344. if (defined(this.widthSetting) &&
  345. this.bBox &&
  346. (this.textAlign === 'center' || this.textAlign === 'right')) {
  347. textX += { center: 0.5, right: 1 }[this.textAlign] *
  348. (this.widthSetting - this.bBox.width);
  349. }
  350. // update if anything changed
  351. if (textX !== text.x || textY !== text.y) {
  352. text.attr('x', textX);
  353. // #8159 - prevent misplaced data labels in treemap
  354. // (useHTML: true)
  355. if (text.hasBoxWidthChanged) {
  356. this.bBox = text.getBBox(true);
  357. this.updateBoxSize();
  358. }
  359. if (typeof textY !== 'undefined') {
  360. text.attr('y', textY);
  361. }
  362. }
  363. // record current values
  364. text.x = textX;
  365. text.y = textY;
  366. };
  367. SVGLabel.prototype.widthSetter = function (value) {
  368. // width:auto => null
  369. this.widthSetting = isNumber(value) ? value : void 0;
  370. };
  371. SVGLabel.prototype.xSetter = function (value) {
  372. this.x = value; // for animation getter
  373. if (this.alignFactor) {
  374. value -= this.alignFactor * ((this.widthSetting || this.bBox.width) +
  375. 2 * this.padding);
  376. // Force animation even when setting to the same value (#7898)
  377. this['forceAnimate:x'] = true;
  378. }
  379. this.xSetting = Math.round(value);
  380. this.attr('translateX', this.xSetting);
  381. };
  382. SVGLabel.prototype.ySetter = function (value) {
  383. this.ySetting = this.y = Math.round(value);
  384. this.attr('translateY', this.ySetting);
  385. };
  386. /* *
  387. *
  388. * Static Properties
  389. *
  390. * */
  391. SVGLabel.emptyBBox = { width: 0, height: 0, x: 0, y: 0 };
  392. /* *
  393. *
  394. * Properties
  395. *
  396. * */
  397. /**
  398. * For labels, these CSS properties are applied to the `text` node directly.
  399. *
  400. * @private
  401. * @name Highcharts.SVGLabel#textProps
  402. * @type {Array<string>}
  403. */
  404. SVGLabel.textProps = [
  405. 'color', 'cursor', 'direction', 'fontFamily', 'fontSize', 'fontStyle',
  406. 'fontWeight', 'lineHeight', 'textAlign', 'textDecoration',
  407. 'textOutline', 'textOverflow', 'width'
  408. ];
  409. return SVGLabel;
  410. }(SVGElement));
  411. export default SVGLabel;