apexcharts
Version:
A JavaScript Chart Library
225 lines (187 loc) • 6.33 kB
JavaScript
// @ts-check
import SVGContainer from './SVGContainer'
import { SVGNS } from './math'
import { Environment } from '../utils/Environment.js'
// Install select + resize behavior on SVGElement prototype
/**
* @param {any} ElementClass
*/
function installSelectable(ElementClass) {
/**
* @param {any} opts
*/
ElementClass.prototype.select = function (opts) {
if (opts === false) {
// Remove selection handles
if (this._selectCleanup) {
this._selectCleanup()
this._selectCleanup = null
}
return this
}
const el = this
const { createHandle, updateHandle } = opts
// Create a group for the handles
const handleGroup = document.createElementNS(SVGNS, 'g')
handleGroup.setAttribute('class', 'svg_select_points')
const parent = el.node.parentNode
if (parent) {
parent.appendChild(handleGroup)
}
/** @type {Record<string, any>} */
const handles = {}
// Create handles for all edge positions
const handleNames = ['t', 'b', 'l', 'r', 'lt', 'rt', 'lb', 'rb']
handleNames.forEach((name, index) => {
const subGroup = new SVGContainer(document.createElementNS(SVGNS, 'g'))
handleGroup.appendChild(subGroup.node)
const handle = createHandle(subGroup, [0, 0], index, [], name)
handles[name] = { group: subGroup, handle }
})
// Position handles based on element's current bbox
const updatePositions = () => {
const x = parseFloat(el.attr('x')) || 0
const y = parseFloat(el.attr('y')) || 0
const w = parseFloat(el.attr('width')) || 0
const h = parseFloat(el.attr('height')) || 0
// Sync transform (e.g. translateX/Y) from the element to the handle group
const elTransform = el.node.getAttribute('transform')
if (elTransform) {
handleGroup.setAttribute('transform', elTransform)
} else {
handleGroup.removeAttribute('transform')
}
/** @type {Record<string, any>} */
const positions = {
t: [x + w / 2, y],
b: [x + w / 2, y + h],
l: [x, y + h / 2],
r: [x + w, y + h / 2],
lt: [x, y],
rt: [x + w, y],
lb: [x, y + h],
rb: [x + w, y + h],
}
handleNames.forEach((name) => {
if (handles[name] && positions[name]) {
updateHandle(handles[name].group, positions[name])
}
})
}
updatePositions()
el._selectHandles = handleGroup
el._selectHandlesMap = handles
el._updateSelectPositions = updatePositions
el._selectCleanup = () => {
if (handleGroup.parentNode) {
handleGroup.parentNode.removeChild(handleGroup)
}
el._selectHandles = null
el._selectHandlesMap = null
el._updateSelectPositions = null
}
return el
}
/**
* @param {boolean} enable
*/
ElementClass.prototype.resize = function (enable) {
if (enable === false) {
if (this._resizeCleanup) {
this._resizeCleanup()
this._resizeCleanup = null
}
return this
}
const el = this
const handles = el._selectHandlesMap
if (!handles) return el
/** @type {any[]} */
const cleanupFns = []
// Make left and right handles draggable for resizing
/**
* @param {string} name
*/
const makeHandleDraggable = (name) => {
const handleInfo = handles[name]
if (!handleInfo || !handleInfo.group || !handleInfo.group.node) return
const handleNode = handleInfo.group.node
const onPointerDown = (/** @type {any} */ e) => {
if (e.button && e.button !== 0) return
e.stopPropagation()
const isTouch = e.type === 'touchstart'
const ev = isTouch ? e.touches[0] : e
const startClientX = ev.clientX
const svgRoot = el.node.ownerSVGElement
/** @type {DOMMatrix | null} */
let ctm = null
if (svgRoot) {
ctm = svgRoot.getScreenCTM()
}
const startX = parseFloat(el.attr('x')) || 0
const startW = parseFloat(el.attr('width')) || 0
/**
* @param {any} me
*/
const onMove = (me) => {
const mev = me.type === 'touchmove' ? me.touches[0] : me
let dx = mev.clientX - startClientX
if (ctm) dx = dx / /** @type {any} */ (ctm).a
let newX = startX
let newW = startW
if (name === 'l') {
newX = startX + dx
newW = startW - dx
} else if (name === 'r') {
newW = startW + dx
}
if (newW < 0) {
newW = 0
}
el.attr({ x: newX, width: newW })
if (el._updateSelectPositions) {
el._updateSelectPositions()
}
// Fire resize event during move so connected charts update in real-time
const event = new CustomEvent('resize', {
detail: { el },
})
el.node.dispatchEvent(event)
}
const onUp = () => {
if (Environment.isBrowser()) {
document.removeEventListener('mousemove', onMove)
document.removeEventListener('touchmove', onMove)
document.removeEventListener('mouseup', onUp)
document.removeEventListener('touchend', onUp)
}
// Fire resize event
const event = new CustomEvent('resize', {
detail: { el },
})
el.node.dispatchEvent(event)
}
if (Environment.isBrowser()) {
document.addEventListener('mousemove', onMove)
document.addEventListener('touchmove', onMove)
document.addEventListener('mouseup', onUp)
document.addEventListener('touchend', onUp)
}
}
handleNode.addEventListener('mousedown', onPointerDown)
handleNode.addEventListener('touchstart', onPointerDown)
cleanupFns.push(() => {
handleNode.removeEventListener('mousedown', onPointerDown)
handleNode.removeEventListener('touchstart', onPointerDown)
})
}
// Only left and right handles are functional (others are 0-size)
makeHandleDraggable('l')
makeHandleDraggable('r')
el._resizeCleanup = () => {
cleanupFns.forEach((fn) => fn())
}
return el
}
}
export { installSelectable }