Html.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /* *
  2. *
  3. * (c) 2010-2019 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. import H from './Globals.js';
  12. import U from './Utilities.js';
  13. var attr = U.attr, defined = U.defined, extend = U.extend, pick = U.pick, pInt = U.pInt;
  14. import './SvgRenderer.js';
  15. var createElement = H.createElement, css = H.css, isFirefox = H.isFirefox, isMS = H.isMS, isWebKit = H.isWebKit, SVGElement = H.SVGElement, SVGRenderer = H.SVGRenderer, win = H.win;
  16. /* eslint-disable valid-jsdoc */
  17. // Extend SvgElement for useHTML option.
  18. extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
  19. /**
  20. * Apply CSS to HTML elements. This is used in text within SVG rendering and
  21. * by the VML renderer
  22. *
  23. * @private
  24. * @function Highcharts.SVGElement#htmlCss
  25. *
  26. * @param {Highcharts.CSSObject} styles
  27. *
  28. * @return {Highcharts.SVGElement}
  29. */
  30. htmlCss: function (styles) {
  31. var wrapper = this, element = wrapper.element,
  32. // When setting or unsetting the width style, we need to update
  33. // transform (#8809)
  34. isSettingWidth = (element.tagName === 'SPAN' &&
  35. styles &&
  36. 'width' in styles), textWidth = pick(isSettingWidth && styles.width, void 0), doTransform;
  37. if (isSettingWidth) {
  38. delete styles.width;
  39. wrapper.textWidth = textWidth;
  40. doTransform = true;
  41. }
  42. if (styles && styles.textOverflow === 'ellipsis') {
  43. styles.whiteSpace = 'nowrap';
  44. styles.overflow = 'hidden';
  45. }
  46. wrapper.styles = extend(wrapper.styles, styles);
  47. css(wrapper.element, styles);
  48. // Now that all styles are applied, to the transform
  49. if (doTransform) {
  50. wrapper.htmlUpdateTransform();
  51. }
  52. return wrapper;
  53. },
  54. /**
  55. * VML and useHTML method for calculating the bounding box based on offsets.
  56. *
  57. * @private
  58. * @function Highcharts.SVGElement#htmlGetBBox
  59. *
  60. * @param {boolean} refresh
  61. * Whether to force a fresh value from the DOM or to use the cached
  62. * value.
  63. *
  64. * @return {Highcharts.BBoxObject}
  65. * A hash containing values for x, y, width and height.
  66. */
  67. htmlGetBBox: function () {
  68. var wrapper = this, element = wrapper.element;
  69. return {
  70. x: element.offsetLeft,
  71. y: element.offsetTop,
  72. width: element.offsetWidth,
  73. height: element.offsetHeight
  74. };
  75. },
  76. /**
  77. * VML override private method to update elements based on internal
  78. * properties based on SVG transform.
  79. *
  80. * @private
  81. * @function Highcharts.SVGElement#htmlUpdateTransform
  82. * @return {void}
  83. */
  84. htmlUpdateTransform: function () {
  85. // aligning non added elements is expensive
  86. if (!this.added) {
  87. this.alignOnAdd = true;
  88. return;
  89. }
  90. var wrapper = this, renderer = wrapper.renderer, elem = wrapper.element, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, x = wrapper.x || 0, y = wrapper.y || 0, align = wrapper.textAlign || 'left', alignCorrection = {
  91. left: 0, center: 0.5, right: 1
  92. }[align], styles = wrapper.styles, whiteSpace = styles && styles.whiteSpace;
  93. /**
  94. * @private
  95. * @return {number}
  96. */
  97. function getTextPxLength() {
  98. // Reset multiline/ellipsis in order to read width (#4928,
  99. // #5417)
  100. css(elem, {
  101. width: '',
  102. whiteSpace: whiteSpace || 'nowrap'
  103. });
  104. return elem.offsetWidth;
  105. }
  106. // apply translate
  107. css(elem, {
  108. marginLeft: translateX,
  109. marginTop: translateY
  110. });
  111. if (!renderer.styledMode && wrapper.shadows) { // used in labels/tooltip
  112. wrapper.shadows.forEach(function (shadow) {
  113. css(shadow, {
  114. marginLeft: translateX + 1,
  115. marginTop: translateY + 1
  116. });
  117. });
  118. }
  119. // apply inversion
  120. if (wrapper.inverted) { // wrapper is a group
  121. [].forEach.call(elem.childNodes, function (child) {
  122. renderer.invertChild(child, elem);
  123. });
  124. }
  125. if (elem.tagName === 'SPAN') {
  126. var rotation = wrapper.rotation, baseline, textWidth = wrapper.textWidth && pInt(wrapper.textWidth), currentTextTransform = [
  127. rotation,
  128. align,
  129. elem.innerHTML,
  130. wrapper.textWidth,
  131. wrapper.textAlign
  132. ].join(',');
  133. // Update textWidth. Use the memoized textPxLength if possible, to
  134. // avoid the getTextPxLength function using elem.offsetWidth.
  135. // Calling offsetWidth affects rendering time as it forces layout
  136. // (#7656).
  137. if (textWidth !== wrapper.oldTextWidth &&
  138. ((textWidth > wrapper.oldTextWidth) ||
  139. (wrapper.textPxLength || getTextPxLength()) > textWidth) && (
  140. // Only set the width if the text is able to word-wrap, or
  141. // text-overflow is ellipsis (#9537)
  142. /[ \-]/.test(elem.textContent || elem.innerText) ||
  143. elem.style.textOverflow === 'ellipsis')) { // #983, #1254
  144. css(elem, {
  145. width: textWidth + 'px',
  146. display: 'block',
  147. whiteSpace: whiteSpace || 'normal' // #3331
  148. });
  149. wrapper.oldTextWidth = textWidth;
  150. wrapper.hasBoxWidthChanged = true; // #8159
  151. }
  152. else {
  153. wrapper.hasBoxWidthChanged = false; // #8159
  154. }
  155. // Do the calculations and DOM access only if properties changed
  156. if (currentTextTransform !== wrapper.cTT) {
  157. baseline = renderer.fontMetrics(elem.style.fontSize, elem).b;
  158. // Renderer specific handling of span rotation, but only if we
  159. // have something to update.
  160. if (defined(rotation) &&
  161. ((rotation !== (wrapper.oldRotation || 0)) ||
  162. (align !== wrapper.oldAlign))) {
  163. wrapper.setSpanRotation(rotation, alignCorrection, baseline);
  164. }
  165. wrapper.getSpanCorrection(
  166. // Avoid elem.offsetWidth if we can, it affects rendering
  167. // time heavily (#7656)
  168. ((!defined(rotation) && wrapper.textPxLength) || // #7920
  169. elem.offsetWidth), baseline, alignCorrection, rotation, align);
  170. }
  171. // apply position with correction
  172. css(elem, {
  173. left: (x + (wrapper.xCorr || 0)) + 'px',
  174. top: (y + (wrapper.yCorr || 0)) + 'px'
  175. });
  176. // record current text transform
  177. wrapper.cTT = currentTextTransform;
  178. wrapper.oldRotation = rotation;
  179. wrapper.oldAlign = align;
  180. }
  181. },
  182. /**
  183. * Set the rotation of an individual HTML span.
  184. *
  185. * @private
  186. * @function Highcharts.SVGElement#setSpanRotation
  187. * @param {number} rotation
  188. * @param {number} alignCorrection
  189. * @param {number} baseline
  190. * @return {void}
  191. */
  192. setSpanRotation: function (rotation, alignCorrection, baseline) {
  193. var rotationStyle = {}, cssTransformKey = this.renderer.getTransformKey();
  194. rotationStyle[cssTransformKey] = rotationStyle.transform =
  195. 'rotate(' + rotation + 'deg)';
  196. rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] =
  197. rotationStyle.transformOrigin =
  198. (alignCorrection * 100) + '% ' + baseline + 'px';
  199. css(this.element, rotationStyle);
  200. },
  201. /**
  202. * Get the correction in X and Y positioning as the element is rotated.
  203. *
  204. * @private
  205. * @function Highcharts.SVGElement#getSpanCorrection
  206. * @param {number} width
  207. * @param {number} baseline
  208. * @param {number} alignCorrection
  209. * @return {void}
  210. */
  211. getSpanCorrection: function (width, baseline, alignCorrection) {
  212. this.xCorr = -width * alignCorrection;
  213. this.yCorr = -baseline;
  214. }
  215. });
  216. // Extend SvgRenderer for useHTML option.
  217. extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
  218. /**
  219. * @private
  220. * @function Highcharts.SVGRenderer#getTransformKey
  221. *
  222. * @return {string}
  223. */
  224. getTransformKey: function () {
  225. return isMS && !/Edge/.test(win.navigator.userAgent) ?
  226. '-ms-transform' :
  227. isWebKit ?
  228. '-webkit-transform' :
  229. isFirefox ?
  230. 'MozTransform' :
  231. win.opera ?
  232. '-o-transform' :
  233. '';
  234. },
  235. /**
  236. * Create HTML text node. This is used by the VML renderer as well as the
  237. * SVG renderer through the useHTML option.
  238. *
  239. * @private
  240. * @function Highcharts.SVGRenderer#html
  241. *
  242. * @param {string} str
  243. * The text of (subset) HTML to draw.
  244. *
  245. * @param {number} x
  246. * The x position of the text's lower left corner.
  247. *
  248. * @param {number} y
  249. * The y position of the text's lower left corner.
  250. *
  251. * @return {Highcharts.HTMLDOMElement}
  252. */
  253. html: function (str, x, y) {
  254. var wrapper = this.createElement('span'), element = wrapper.element, renderer = wrapper.renderer, isSVG = renderer.isSVG, addSetters = function (gWrapper, style) {
  255. // These properties are set as attributes on the SVG group, and
  256. // as identical CSS properties on the div. (#3542)
  257. ['opacity', 'visibility'].forEach(function (prop) {
  258. gWrapper[prop + 'Setter'] = function (value, key, elem) {
  259. var styleObject = gWrapper.div ?
  260. gWrapper.div.style :
  261. style;
  262. SVGElement.prototype[prop + 'Setter']
  263. .call(this, value, key, elem);
  264. if (styleObject) {
  265. styleObject[key] = value;
  266. }
  267. };
  268. });
  269. gWrapper.addedSetters = true;
  270. };
  271. // Text setter
  272. wrapper.textSetter = function (value) {
  273. if (value !== element.innerHTML) {
  274. delete this.bBox;
  275. delete this.oldTextWidth;
  276. }
  277. this.textStr = value;
  278. element.innerHTML = pick(value, '');
  279. wrapper.doTransform = true;
  280. };
  281. // Add setters for the element itself (#4938)
  282. if (isSVG) { // #4938, only for HTML within SVG
  283. addSetters(wrapper, wrapper.element.style);
  284. }
  285. // Various setters which rely on update transform
  286. wrapper.xSetter =
  287. wrapper.ySetter =
  288. wrapper.alignSetter =
  289. wrapper.rotationSetter =
  290. function (value, key) {
  291. if (key === 'align') {
  292. // Do not overwrite the SVGElement.align method. Same as VML.
  293. key = 'textAlign';
  294. }
  295. wrapper[key] = value;
  296. wrapper.doTransform = true;
  297. };
  298. // Runs at the end of .attr()
  299. wrapper.afterSetters = function () {
  300. // Update transform. Do this outside the loop to prevent redundant
  301. // updating for batch setting of attributes.
  302. if (this.doTransform) {
  303. this.htmlUpdateTransform();
  304. this.doTransform = false;
  305. }
  306. };
  307. // Set the default attributes
  308. wrapper
  309. .attr({
  310. text: str,
  311. x: Math.round(x),
  312. y: Math.round(y)
  313. })
  314. .css({
  315. position: 'absolute'
  316. });
  317. if (!renderer.styledMode) {
  318. wrapper.css({
  319. fontFamily: this.style.fontFamily,
  320. fontSize: this.style.fontSize
  321. });
  322. }
  323. // Keep the whiteSpace style outside the wrapper.styles collection
  324. element.style.whiteSpace = 'nowrap';
  325. // Use the HTML specific .css method
  326. wrapper.css = wrapper.htmlCss;
  327. // This is specific for HTML within SVG
  328. if (isSVG) {
  329. wrapper.add = function (svgGroupWrapper) {
  330. var htmlGroup, container = renderer.box.parentNode, parentGroup, parents = [];
  331. this.parentGroup = svgGroupWrapper;
  332. // Create a mock group to hold the HTML elements
  333. if (svgGroupWrapper) {
  334. htmlGroup = svgGroupWrapper.div;
  335. if (!htmlGroup) {
  336. // Read the parent chain into an array and read from top
  337. // down
  338. parentGroup = svgGroupWrapper;
  339. while (parentGroup) {
  340. parents.push(parentGroup);
  341. // Move up to the next parent group
  342. parentGroup = parentGroup.parentGroup;
  343. }
  344. // Ensure dynamically updating position when any parent
  345. // is translated
  346. parents.reverse().forEach(function (parentGroup) {
  347. var htmlGroupStyle, cls = attr(parentGroup.element, 'class');
  348. /**
  349. * Common translate setter for X and Y on the HTML
  350. * group. Reverted the fix for #6957 du to
  351. * positioning problems and offline export (#7254,
  352. * #7280, #7529)
  353. * @private
  354. * @param {*} value
  355. * @param {string} key
  356. * @return {void}
  357. */
  358. function translateSetter(value, key) {
  359. parentGroup[key] = value;
  360. if (key === 'translateX') {
  361. htmlGroupStyle.left = value + 'px';
  362. }
  363. else {
  364. htmlGroupStyle.top = value + 'px';
  365. }
  366. parentGroup.doTransform = true;
  367. }
  368. // Create a HTML div and append it to the parent div
  369. // to emulate the SVG group structure
  370. htmlGroup =
  371. parentGroup.div =
  372. parentGroup.div || createElement('div', cls ? { className: cls } : void 0, {
  373. position: 'absolute',
  374. left: (parentGroup.translateX || 0) + 'px',
  375. top: (parentGroup.translateY || 0) + 'px',
  376. display: parentGroup.display,
  377. opacity: parentGroup.opacity,
  378. pointerEvents: (parentGroup.styles &&
  379. parentGroup.styles.pointerEvents) // #5595
  380. // the top group is appended to container
  381. }, htmlGroup || container);
  382. // Shortcut
  383. htmlGroupStyle = htmlGroup.style;
  384. // Set listeners to update the HTML div's position
  385. // whenever the SVG group position is changed.
  386. extend(parentGroup, {
  387. // (#7287) Pass htmlGroup to use
  388. // the related group
  389. classSetter: (function (htmlGroup) {
  390. return function (value) {
  391. this.element.setAttribute('class', value);
  392. htmlGroup.className = value;
  393. };
  394. }(htmlGroup)),
  395. on: function () {
  396. if (parents[0].div) { // #6418
  397. wrapper.on.apply({ element: parents[0].div }, arguments);
  398. }
  399. return parentGroup;
  400. },
  401. translateXSetter: translateSetter,
  402. translateYSetter: translateSetter
  403. });
  404. if (!parentGroup.addedSetters) {
  405. addSetters(parentGroup);
  406. }
  407. });
  408. }
  409. }
  410. else {
  411. htmlGroup = container;
  412. }
  413. htmlGroup.appendChild(element);
  414. // Shared with VML:
  415. wrapper.added = true;
  416. if (wrapper.alignOnAdd) {
  417. wrapper.htmlUpdateTransform();
  418. }
  419. return wrapper;
  420. };
  421. }
  422. return wrapper;
  423. }
  424. });