123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- /**
- * (c) 2010-2019 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- 'use strict';
- import H from './Globals.js';
- import './Utilities.js';
- import './SvgRenderer.js';
- var attr = H.attr,
- createElement = H.createElement,
- css = H.css,
- defined = H.defined,
- extend = H.extend,
- isFirefox = H.isFirefox,
- isMS = H.isMS,
- isWebKit = H.isWebKit,
- pick = H.pick,
- pInt = H.pInt,
- SVGElement = H.SVGElement,
- SVGRenderer = H.SVGRenderer,
- win = H.win;
- // Extend SvgElement for useHTML option.
- extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
- /**
- * Apply CSS to HTML elements. This is used in text within SVG rendering and
- * by the VML renderer
- *
- * @private
- * @function Highcharts.SVGElement#htmlCss
- *
- * @param {Highcharts.CSSObject} styles
- *
- * @return {Highcharts.SVGElement}
- */
- htmlCss: function (styles) {
- var wrapper = this,
- element = wrapper.element,
- // When setting or unsetting the width style, we need to update
- // transform (#8809)
- isSettingWidth = (
- element.tagName === 'SPAN' &&
- styles &&
- 'width' in styles
- ),
- textWidth = pick(
- isSettingWidth && styles.width,
- undefined
- ),
- doTransform;
- if (isSettingWidth) {
- delete styles.width;
- wrapper.textWidth = textWidth;
- doTransform = true;
- }
- if (styles && styles.textOverflow === 'ellipsis') {
- styles.whiteSpace = 'nowrap';
- styles.overflow = 'hidden';
- }
- wrapper.styles = extend(wrapper.styles, styles);
- css(wrapper.element, styles);
- // Now that all styles are applied, to the transform
- if (doTransform) {
- wrapper.htmlUpdateTransform();
- }
- return wrapper;
- },
- /**
- * VML and useHTML method for calculating the bounding box based on offsets.
- *
- * @private
- * @function Highcharts.SVGElement#htmlGetBBox
- *
- * @param {boolean} refresh
- * Whether to force a fresh value from the DOM or to use the cached
- * value.
- *
- * @return {Highcharts.BBoxObject}
- * A hash containing values for x, y, width and height.
- */
- htmlGetBBox: function () {
- var wrapper = this,
- element = wrapper.element;
- return {
- x: element.offsetLeft,
- y: element.offsetTop,
- width: element.offsetWidth,
- height: element.offsetHeight
- };
- },
- /**
- * VML override private method to update elements based on internal
- * properties based on SVG transform.
- *
- * @private
- * @function Highcharts.SVGElement#htmlUpdateTransform
- */
- htmlUpdateTransform: function () {
- // aligning non added elements is expensive
- if (!this.added) {
- this.alignOnAdd = true;
- return;
- }
- 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 = { left: 0, center: 0.5, right: 1 }[align],
- styles = wrapper.styles,
- whiteSpace = styles && styles.whiteSpace;
- function getTextPxLength() {
- // Reset multiline/ellipsis in order to read width (#4928,
- // #5417)
- css(elem, {
- width: '',
- whiteSpace: whiteSpace || 'nowrap'
- });
- return elem.offsetWidth;
- }
- // apply translate
- css(elem, {
- marginLeft: translateX,
- marginTop: translateY
- });
- if (!renderer.styledMode && wrapper.shadows) { // used in labels/tooltip
- wrapper.shadows.forEach(function (shadow) {
- css(shadow, {
- marginLeft: translateX + 1,
- marginTop: translateY + 1
- });
- });
- }
- // apply inversion
- if (wrapper.inverted) { // wrapper is a group
- elem.childNodes.forEach(function (child) {
- renderer.invertChild(child, elem);
- });
- }
- if (elem.tagName === 'SPAN') {
- var rotation = wrapper.rotation,
- baseline,
- textWidth = wrapper.textWidth && pInt(wrapper.textWidth),
- currentTextTransform = [
- rotation,
- align,
- elem.innerHTML,
- wrapper.textWidth,
- wrapper.textAlign
- ].join(',');
- // Update textWidth. Use the memoized textPxLength if possible, to
- // avoid the getTextPxLength function using elem.offsetWidth.
- // Calling offsetWidth affects rendering time as it forces layout
- // (#7656).
- if (
- textWidth !== wrapper.oldTextWidth &&
- (
- (textWidth > wrapper.oldTextWidth) ||
- (wrapper.textPxLength || getTextPxLength()) > textWidth
- ) && (
- // Only set the width if the text is able to word-wrap, or
- // text-overflow is ellipsis (#9537)
- /[ \-]/.test(elem.textContent || elem.innerText) ||
- elem.style.textOverflow === 'ellipsis'
- )
- ) { // #983, #1254
- css(elem, {
- width: textWidth + 'px',
- display: 'block',
- whiteSpace: whiteSpace || 'normal' // #3331
- });
- wrapper.oldTextWidth = textWidth;
- wrapper.hasBoxWidthChanged = true; // #8159
- } else {
- wrapper.hasBoxWidthChanged = false; // #8159
- }
- // Do the calculations and DOM access only if properties changed
- if (currentTextTransform !== wrapper.cTT) {
- baseline = renderer.fontMetrics(elem.style.fontSize, elem).b;
- // Renderer specific handling of span rotation, but only if we
- // have something to update.
- if (
- defined(rotation) &&
- (
- (rotation !== (wrapper.oldRotation || 0)) ||
- (align !== wrapper.oldAlign)
- )
- ) {
- wrapper.setSpanRotation(
- rotation,
- alignCorrection,
- baseline
- );
- }
- wrapper.getSpanCorrection(
- // Avoid elem.offsetWidth if we can, it affects rendering
- // time heavily (#7656)
- (
- (!defined(rotation) && wrapper.textPxLength) || // #7920
- elem.offsetWidth
- ),
- baseline,
- alignCorrection,
- rotation,
- align
- );
- }
- // apply position with correction
- css(elem, {
- left: (x + (wrapper.xCorr || 0)) + 'px',
- top: (y + (wrapper.yCorr || 0)) + 'px'
- });
- // record current text transform
- wrapper.cTT = currentTextTransform;
- wrapper.oldRotation = rotation;
- wrapper.oldAlign = align;
- }
- },
- /**
- * Set the rotation of an individual HTML span.
- *
- * @private
- * @function Highcharts.SVGElement#setSpanRotation
- *
- * @param {number} rotation
- *
- * @param {number} alignCorrection
- *
- * @param {number} baseline
- */
- setSpanRotation: function (rotation, alignCorrection, baseline) {
- var rotationStyle = {},
- cssTransformKey = this.renderer.getTransformKey();
- rotationStyle[cssTransformKey] = rotationStyle.transform =
- 'rotate(' + rotation + 'deg)';
- rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] =
- rotationStyle.transformOrigin =
- (alignCorrection * 100) + '% ' + baseline + 'px';
- css(this.element, rotationStyle);
- },
- /**
- * Get the correction in X and Y positioning as the element is rotated.
- *
- * @private
- * @function Highcharts.SVGElement#getSpanCorrection
- *
- * @param {number} width
- *
- * @param {number} baseline
- *
- * @param {number} alignCorrection
- */
- getSpanCorrection: function (width, baseline, alignCorrection) {
- this.xCorr = -width * alignCorrection;
- this.yCorr = -baseline;
- }
- });
- // Extend SvgRenderer for useHTML option.
- extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
- /**
- * @private
- * @function Highcharts.SVGRenderer#getTransformKey
- *
- * @return {string}
- */
- getTransformKey: function () {
- return isMS && !/Edge/.test(win.navigator.userAgent) ?
- '-ms-transform' :
- isWebKit ?
- '-webkit-transform' :
- isFirefox ?
- 'MozTransform' :
- win.opera ?
- '-o-transform' :
- '';
- },
- /**
- * Create HTML text node. This is used by the VML renderer as well as the
- * SVG renderer through the useHTML option.
- *
- * @private
- * @function Highcharts.SVGRenderer#html
- *
- * @param {string} str
- * The text of (subset) HTML to draw.
- *
- * @param {number} x
- * The x position of the text's lower left corner.
- *
- * @param {number} y
- * The y position of the text's lower left corner.
- *
- * @return {Highcharts.HTMLDOMElement}
- */
- html: function (str, x, y) {
- var wrapper = this.createElement('span'),
- element = wrapper.element,
- renderer = wrapper.renderer,
- isSVG = renderer.isSVG,
- addSetters = function (element, style) {
- // These properties are set as attributes on the SVG group, and
- // as identical CSS properties on the div. (#3542)
- ['opacity', 'visibility'].forEach(function (prop) {
- element[prop + 'Setter'] = function (
- value,
- key,
- elem
- ) {
- SVGElement.prototype[prop + 'Setter']
- .call(this, value, key, elem);
- style[key] = value;
- };
- });
- element.addedSetters = true;
- },
- chart = H.charts[renderer.chartIndex],
- styledMode = chart && chart.styledMode;
- // Text setter
- wrapper.textSetter = function (value) {
- if (value !== element.innerHTML) {
- delete this.bBox;
- }
- this.textStr = value;
- element.innerHTML = pick(value, '');
- wrapper.doTransform = true;
- };
- // Add setters for the element itself (#4938)
- if (isSVG) { // #4938, only for HTML within SVG
- addSetters(wrapper, wrapper.element.style);
- }
- // Various setters which rely on update transform
- wrapper.xSetter =
- wrapper.ySetter =
- wrapper.alignSetter =
- wrapper.rotationSetter =
- function (value, key) {
- if (key === 'align') {
- // Do not overwrite the SVGElement.align method. Same as VML.
- key = 'textAlign';
- }
- wrapper[key] = value;
- wrapper.doTransform = true;
- };
- // Runs at the end of .attr()
- wrapper.afterSetters = function () {
- // Update transform. Do this outside the loop to prevent redundant
- // updating for batch setting of attributes.
- if (this.doTransform) {
- this.htmlUpdateTransform();
- this.doTransform = false;
- }
- };
- // Set the default attributes
- wrapper
- .attr({
- text: str,
- x: Math.round(x),
- y: Math.round(y)
- })
- .css({
- position: 'absolute'
- });
- if (!styledMode) {
- wrapper.css({
- fontFamily: this.style.fontFamily,
- fontSize: this.style.fontSize
- });
- }
- // Keep the whiteSpace style outside the wrapper.styles collection
- element.style.whiteSpace = 'nowrap';
- // Use the HTML specific .css method
- wrapper.css = wrapper.htmlCss;
- // This is specific for HTML within SVG
- if (isSVG) {
- wrapper.add = function (svgGroupWrapper) {
- var htmlGroup,
- container = renderer.box.parentNode,
- parentGroup,
- parents = [];
- this.parentGroup = svgGroupWrapper;
- // Create a mock group to hold the HTML elements
- if (svgGroupWrapper) {
- htmlGroup = svgGroupWrapper.div;
- if (!htmlGroup) {
- // Read the parent chain into an array and read from top
- // down
- parentGroup = svgGroupWrapper;
- while (parentGroup) {
- parents.push(parentGroup);
- // Move up to the next parent group
- parentGroup = parentGroup.parentGroup;
- }
- // Ensure dynamically updating position when any parent
- // is translated
- parents.reverse().forEach(function (parentGroup) {
- var htmlGroupStyle,
- cls = attr(parentGroup.element, 'class');
- // Common translate setter for X and Y on the HTML
- // group. Reverted the fix for #6957 du to
- // positioning problems and offline export (#7254,
- // #7280, #7529)
- function translateSetter(value, key) {
- parentGroup[key] = value;
- if (key === 'translateX') {
- htmlGroupStyle.left = value + 'px';
- } else {
- htmlGroupStyle.top = value + 'px';
- }
- parentGroup.doTransform = true;
- }
- if (cls) {
- cls = { className: cls };
- } // else null
- // Create a HTML div and append it to the parent div
- // to emulate the SVG group structure
- htmlGroup =
- parentGroup.div =
- parentGroup.div || createElement('div', cls, {
- position: 'absolute',
- left: (parentGroup.translateX || 0) + 'px',
- top: (parentGroup.translateY || 0) + 'px',
- display: parentGroup.display,
- opacity: parentGroup.opacity, // #5075
- pointerEvents: (
- parentGroup.styles &&
- parentGroup.styles.pointerEvents
- ) // #5595
- // the top group is appended to container
- }, htmlGroup || container);
- // Shortcut
- htmlGroupStyle = htmlGroup.style;
- // Set listeners to update the HTML div's position
- // whenever the SVG group position is changed.
- extend(parentGroup, {
- // (#7287) Pass htmlGroup to use
- // the related group
- classSetter: (function (htmlGroup) {
- return function (value) {
- this.element.setAttribute(
- 'class',
- value
- );
- htmlGroup.className = value;
- };
- }(htmlGroup)),
- on: function () {
- if (parents[0].div) { // #6418
- wrapper.on.apply(
- { element: parents[0].div },
- arguments
- );
- }
- return parentGroup;
- },
- translateXSetter: translateSetter,
- translateYSetter: translateSetter
- });
- if (!parentGroup.addedSetters) {
- addSetters(parentGroup, htmlGroupStyle);
- }
- });
- }
- } else {
- htmlGroup = container;
- }
- htmlGroup.appendChild(element);
- // Shared with VML:
- wrapper.added = true;
- if (wrapper.alignOnAdd) {
- wrapper.htmlUpdateTransform();
- }
- return wrapper;
- };
- }
- return wrapper;
- }
- });
|