Html.js 17 KB

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