Html.js 18 KB

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