UNPKG

apexcharts

Version:

A JavaScript Chart Library

345 lines (301 loc) 7.57 kB
// @ts-check import { BrowserAPIs } from '../ssr/BrowserAPIs.js' export default class SVGElement { /** * @param {any} node */ constructor(node) { this.node = node if (node) { node.instance = this } /** @type {any} */ this._listeners = [] /** @type {any} */ this._filter = null } // ---- Attribute methods ---- /** * @param {any} a * @param {any} [v] */ attr(a, v) { if (typeof a === 'string' && v === undefined) { return this.node.getAttribute(a) } const attrs = typeof a === 'string' ? { [a]: v } : a for (const key in attrs) { let val = attrs[key] if (val === null) { this.node.removeAttribute(key) } else if (val !== undefined) { // Normalize NaN to 0 to match SVG.js behavior if (typeof val === 'number' && isNaN(val)) val = 0 this.node.setAttribute(key, val) } // Skip if val is undefined — don't modify the attribute (matches SVG.js) } // Propagate x to newLine tspans in text elements (matches SVG.js rebuild) if (this.node.nodeName === 'text' && attrs.x != null) { const tspans = this.node.querySelectorAll('tspan[data-newline]') for (let i = 0; i < tspans.length; i++) { tspans[i].setAttribute('x', attrs.x) } } return this } /** * @param {Record<string, string>} styles */ css(styles) { for (const k in styles) { this.node.style[k] = styles[k] } return this } /** * @param {any} v */ fill(v) { if (typeof v === 'object') { return this.attr(v) } return this.attr('fill', v) } /** * @param {any} v */ stroke(v) { if (typeof v === 'object') { if (v.color !== undefined) this.attr('stroke', v.color) if (v.width !== undefined) this.attr('stroke-width', v.width) if (v.dasharray !== undefined) this.attr('stroke-dasharray', v.dasharray) if (v.linecap !== undefined) this.attr('stroke-linecap', v.linecap) if (v.opacity !== undefined) this.attr('stroke-opacity', v.opacity) return this } return this.attr('stroke', v) } /** * @param {number} w * @param {number} h */ size(w, h) { return this.attr({ width: w, height: h }) } /** * @param {number} x * @param {number} y */ move(x, y) { return this.attr({ x, y }) } /** * @param {number} cx * @param {number} cy */ center(cx, cy) { if (this.node.nodeName === 'g') { // Groups don't have cx/cy — use transform to position content center const box = this.bbox() const dx = cx - (box.x + box.width / 2) const dy = cy - (box.y + box.height / 2) return this.attr('transform', `translate(${dx}, ${dy})`) } return this.attr({ cx, cy }) } // ---- Tree operations ---- /** * @param {any} child */ add(child) { this.node.appendChild(child.node || child) return this } /** * @param {any} parent */ addTo(parent) { const p = parent.node || parent p.appendChild(this.node) return this } remove() { if (this.node.parentNode) { this.node.parentNode.removeChild(this.node) } return this } clear() { while (this.node.firstChild) { this.node.removeChild(this.node.firstChild) } return this } // ---- Query ---- /** * @param {string} selector */ find(selector) { return Array.from(this.node.querySelectorAll(selector)).map( (n) => n.instance || new SVGElement(n), ) } /** * @param {string} selector */ findOne(selector) { const n = this.node.querySelector(selector) return n ? n.instance || new SVGElement(n) : null } // ---- Events ---- /** * @param {Event} event * @param {Function} handler */ on(event, handler) { // Strip namespace suffix (e.g. 'dragmove.namespace' → 'dragmove') const eventType = /** @type {string} */ (/** @type {any} */ (event)).split( '.', )[0] this._listeners.push({ event, eventType, handler }) this.node.addEventListener(eventType, handler) return this } /** * @param {Event} event * @param {Function} handler */ off(event, handler) { if (!event && !handler) { // Remove all listeners registered via .on() this._listeners.forEach((/** @type {any} */ l) => { this.node.removeEventListener(l.eventType, l.handler) }) this._listeners = [] } else if (event && !handler) { const eventType = /** @type {string} */ ( /** @type {any} */ (event) ).split('.')[0] this._listeners = this._listeners.filter((/** @type {any} */ l) => { if (l.eventType === eventType) { this.node.removeEventListener(l.eventType, l.handler) return false } return true }) } else { const eventType = /** @type {string} */ ( /** @type {any} */ (event) ).split('.')[0] this._listeners = this._listeners.filter((/** @type {any} */ l) => { if (l.eventType === eventType && l.handler === handler) { this.node.removeEventListener(l.eventType, l.handler) return false } return true }) } return this } // ---- Iteration ---- /** * @param {Function} fn * @param {boolean} deep */ each(fn, deep) { const children = Array.from(this.node.children) children.forEach((child) => { const inst = child.instance || new SVGElement(child) fn.call(inst) if (deep) inst.each(fn, deep) }) return this } // ---- CSS classes ---- /** * @param {string} cls */ removeClass(cls) { if (cls === '*') { this.node.removeAttribute('class') } else { this.node.classList.remove(cls) } return this } // ---- Children ---- children() { return Array.from(this.node.childNodes) .filter((n) => n.nodeType === 1) .map((n) => n.instance || new SVGElement(n)) } // ---- Visibility ---- hide() { this.node.style.display = 'none' return this } show() { this.node.style.display = '' return this } // ---- Measurement ---- bbox() { if (typeof this.node.getBBox === 'function') { try { return this.node.getBBox() } catch (e) { // getBBox throws in jsdom/detached elements } } return { x: 0, y: 0, width: 0, height: 0 } } // ---- Text-specific ---- /** * @param {string} text */ tspan(text) { const tspan = BrowserAPIs.createElementNS( 'http://www.w3.org/2000/svg', 'tspan', ) tspan.textContent = text this.node.appendChild(tspan) return new SVGElement(tspan) } // ---- Path-specific ---- /** * @param {string} d */ plot(d) { if (typeof d === 'string') { this.attr('d', d) } return this } // ---- Animation (overridden by SVGAnimation mixin) ---- animate() { // This will be set up by the animation module throw new Error('Animation module not loaded') } // ---- Filter methods (set up by SVGFilter module) ---- filterWith() { throw new Error('Filter module not loaded') } /** * @param {boolean} all */ unfilter(all) { if (this._filter) { this.node.removeAttribute('filter') if (all && this._filter.node && this._filter.node.parentNode) { this._filter.node.parentNode.removeChild(this._filter.node) } this._filter = null } return this } filterer() { return this._filter } }