apexcharts
Version:
A JavaScript Chart Library
345 lines (301 loc) • 7.57 kB
JavaScript
// @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
}
}