feat: add library for manipulating and animating SVG

This commit is contained in:
bubkoo
2021-03-22 01:40:50 +08:00
parent 8a45355ba6
commit c07a17785f
120 changed files with 11371 additions and 51 deletions

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@antv/x6": "^1.17.6",
"@antv/x6-vector": "^1.0.0",
"@antv/x6-react-components": "^1.1.1",
"@antv/x6-react-shape": "^1.3.1",
"antd": "^4.4.2",

View File

@ -0,0 +1,26 @@
import React from 'react'
import { Svg } from '@antv/x6-vector'
import '../index.less'
console.log(Svg)
export default class Example extends React.Component {
private container: HTMLDivElement
componentDidMount() {
new Svg().appendTo(this.container)
}
refContainer = (container: HTMLDivElement) => {
this.container = container
}
render() {
return (
<div className="x6-graph-wrap">
<h1>Default Settings</h1>
<div ref={this.refContainer} className="x6-graph" />
</div>
)
}
}

View File

@ -0,0 +1,3 @@
import { version } from '@antv/x6-vector'
console.log(version)

View File

@ -0,0 +1 @@
# x6-vector

View File

@ -0,0 +1,11 @@
module.exports = (config) =>
require('../../configs/karma-config.js')(
config,
{
files: [{ pattern: 'src/**/*.ts' }],
// logLevel: config.LOG_DEBUG,
},
{
include: ['./src/**/*.ts'],
},
)

View File

@ -0,0 +1,127 @@
{
"private": true,
"version": "1.0.0",
"name": "@antv/x6-vector",
"description": "The lightweight library for manipulating and animating SVG.",
"main": "lib/index.js",
"module": "es/index.js",
"unpkg": "dist/x6-vector.js",
"jsdelivr": "dist/x6.js",
"types": "lib/index.d.ts",
"files": [
"dist",
"es",
"lib"
],
"keywords": [
"vector",
"svg",
"x6",
"antv"
],
"scripts": {
"clean:build": "rimraf dist es lib",
"clean:coverage": "rimraf ./test/coverage",
"clean": "run-p clean:build clean:coverage",
"lint": "eslint 'src/**/*.{js,ts}?(x)' --fix",
"build:esm": "tsc --module esnext --target es2015 --outDir ./es",
"build:cjs": "tsc --module commonjs --target es5 --outDir ./lib",
"build:umd": "rollup -c",
"build:version": "node ../../scripts/version.js",
"build:watch": "yarn build:esm --w",
"build:watch:esm": "yarn build:esm --w",
"build:watch:cjs": "yarn build:cjs --w",
"build:dev": "run-p build:cjs build:esm",
"build": "run-p build:version build:dev build:umd",
"prebuild": "run-s lint clean",
"test": "karma start",
"coveralls": "cat ./test/coverage/lcov.info | coveralls",
"pretest": "run-p clean:coverage",
"prepare": "run-s build:version test build",
"precommit": "lint-staged"
},
"lint-staged": {
"src/**/*.ts": [
"eslint --fix"
]
},
"inherits": [
"@antv/x6-package-json/cli.json",
"@antv/x6-package-json/karma.json",
"@antv/x6-package-json/eslint.json",
"@antv/x6-package-json/rollup.json"
],
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-typescript": "^8.1.0",
"@types/jasmine": "^3.6.2",
"@types/node": "^14.14.14",
"@types/sinon": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"coveralls": "^3.1.0",
"eslint": "^7.22.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-unicorn": "^28.0.2",
"fs-extra": "^9.0.1",
"jasmine-core": "^3.6.0",
"karma": "^6.0.0",
"karma-chrome-launcher": "^3.1.0",
"karma-cli": "^2.0.0",
"karma-jasmine": "^4.0.1",
"karma-spec-reporter": "^0.0.32",
"karma-typescript": "^5.2.0",
"karma-typescript-es6-transform": "^5.5.0",
"lint-staged": "^10.5.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"rimraf": "^3.0.2",
"rollup": "^2.35.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-filesize": "^9.1.0",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-progress": "^1.1.2",
"rollup-plugin-terser": "^7.0.2",
"sinon": "^9.0.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.3"
},
"browserslist": [
"last 2 versions",
"Firefox ESR",
"> 1%"
],
"author": {
"name": "bubkoo",
"email": "bubkoo.wy@gmail.com"
},
"contributors": [],
"license": "MIT",
"homepage": "https://github.com/antvis/x6",
"bugs": {
"url": "https://github.com/antvis/x6/issues"
},
"repository": {
"type": "git",
"url": "ssh://git@github.com/antvis/x6.git",
"directory": "packages/x6"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
}
}

View File

@ -0,0 +1,12 @@
import config from '../../configs/rollup-config'
export default config({
output: [
{
name: 'X6Vector',
format: 'umd',
file: 'dist/x6-vector.js',
sourcemap: true,
},
],
})

View File

@ -0,0 +1,86 @@
import { DomUtil } from '../util/dom'
import { Global } from '../global'
import { Registry } from './registry'
import { Base } from './base'
export namespace Adopter {
const cache: WeakMap<Node, Base> = new WeakMap()
export function ref(node: Node, instance?: Base | null) {
if (instance == null) {
cache.delete(node)
} else {
cache.set(node, instance)
}
}
export function adopt<TVector extends Base>(node: Node): TVector
export function adopt<TVector extends Base>(
node?: Node | null,
): TVector | null
export function adopt<TVector extends Base>(
node?: Node | null,
): TVector | null {
if (node == null) {
return null
}
// make sure a node isn't already adopted
const instance = cache.get(node)
if (instance != null && instance instanceof Base) {
return instance as TVector
}
const cls = Registry.getClass(node)
return new cls(node) // eslint-disable-line new-cap
}
let adopter = adopt
export type Target<TVector extends Base = Base> = TVector | Node | string
export function makeInstance<TVector extends Base>(
node?: TVector | Node | string,
isHTML = false,
): TVector {
if (node == null) {
const root = Registry.getRoot()
return new root() // eslint-disable-line new-cap
}
if (node instanceof Base) {
return node
}
if (typeof node === 'object') {
return adopter(node)
}
if (typeof node === 'string' && node.charAt(0) !== '<') {
return adopter(Global.document.querySelector(node)) as TVector
}
// Make sure, that HTML elements are created with the correct namespace
const wrapper = isHTML
? Global.document.createElement('div')
: DomUtil.createNode('svg')
wrapper.innerHTML = node
// We can use firstChild here because we know,
// that the first char is < and thus an element
const result = adopter(wrapper.firstChild)
// make sure, that element doesnt have its wrapper attached
wrapper.firstChild!.remove()
return result as TVector
}
export function mock(instance = adopt) {
adopter = instance
}
export function unmock() {
adopter = adopt
}
}

View File

@ -0,0 +1,20 @@
import { ObjUtil } from '../util/obj'
import { Registry } from './registry'
export abstract class Base {}
export namespace Base {
type Class = { new (...arguments_: any[]): Base }
export function register(name: string, asRoot?: boolean) {
return <TClass extends Class>(ctor: TClass) => {
Registry.register(ctor, name, asRoot)
}
}
export function mixin(...source: any[]) {
return <TClass extends Class>(ctor: TClass) => {
ObjUtil.applyMixins(ctor, ...source)
}
}
}

View File

@ -0,0 +1,59 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { A } from './a'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
link(): A
link(attrs: Attrs): A
link(url: string, attrs?: Attrs | null): A
link(url?: string | Attrs | null, attrs?: Attrs | null) {
return A.create(url, attrs).appendTo(this)
}
}
export class ElementExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
unlink() {
const link = this.linker()
if (!link) {
return this
}
const parent = link.parent()
if (!parent) {
return this.remove()
}
const index = parent.indexOf(link)
parent.add(this, index)
link.remove()
return this
}
linkTo(url: string | ((this: A, a: A) => void)) {
const link = this.linker() || new A()
if (typeof url === 'function') {
url.call(link, link)
} else {
link.to(url)
}
if (!link.parent()) {
this.wrap(link)
}
return this
}
linker() {
const link = this.parent<A>()
if (link && link.node.nodeName.toLowerCase() === 'a') {
return link
}
return null
}
}

View File

@ -0,0 +1,39 @@
import { Attrs } from '../../types'
import { DomUtil } from '../../util/dom'
import { GeometryContainer } from './container-geometry'
@A.register('A')
export class A extends GeometryContainer<SVGAElement> {
target(): string
target(target: '_self' | '_parent' | '_top' | '_blank' | string | null): this
target(target?: string | null) {
return this.attr('target', target)
}
to(): string
to(url: string | null): this
to(url?: string | null) {
return this.attr('href', url, DomUtil.namespaces.xlink)
}
}
export namespace A {
export function create(): A
export function create(attrs: Attrs): A
export function create(url: string, attrs?: Attrs | null): A
export function create(url?: string | Attrs | null, attrs?: Attrs | null): A
export function create(url?: string | Attrs | null, attrs?: Attrs | null) {
const a = new A()
if (url != null) {
if (typeof url === 'string') {
a.to(url)
if (attrs) {
a.attr(attrs)
}
} else if (typeof url === 'object') {
a.attr(url)
}
}
return a
}
}

View File

@ -0,0 +1,56 @@
import { Attrs } from '../../types'
import { Adopter } from '../adopter'
import { Vector } from '../vector'
import { VectorElement } from '../element'
import { Decorator } from '../decorator'
import { Container } from './container'
import { ClipPath } from './clippath'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
@Decorator.checkDefs
clip(attrs?: Attrs | null): ClipPath {
return this.defs()!.clip(attrs)
}
}
export class DefsExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
clip(attrs?: Attrs | null): ClipPath {
return ClipPath.create(attrs).appendTo(this)
}
}
export class ElementExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
clipper() {
return this.reference<ClipPath>('clip-path')
}
clipWith(element: ClipPath | Adopter.Target<VectorElement>) {
// use given clip or create a new one
let clipper: ClipPath | undefined | null
if (element instanceof ClipPath) {
clipper = element
} else {
const parent = this.parent<Container>()
clipper = parent && parent.clip()
if (clipper) {
clipper.add(element)
}
}
if (clipper) {
this.attr('clip-path', `url("#${clipper.id()}")`)
}
return this
}
unclip() {
return this.attr('clip-path', null)
}
}

View File

@ -0,0 +1,25 @@
import { Attrs } from '../../types'
import { VectorElement } from '../element'
import { Container } from './container'
@ClipPath.register('ClipPath')
export class ClipPath extends Container<SVGClipPathElement> {
remove() {
this.targets().forEach((target) => target.unclip())
return super.remove()
}
targets<TVector extends VectorElement>() {
return ClipPath.find<TVector>(`svg [clip-path*="${this.id()}"]`)
}
}
export namespace ClipPath {
export function create(attrs?: Attrs | null) {
const clip = new ClipPath()
if (attrs) {
clip.attr(attrs)
}
return clip
}
}

View File

@ -0,0 +1,97 @@
import { Box } from '../../struct/box'
import { Point } from '../../struct/point'
import { Matrix } from '../../struct/matrix'
import { SVGNumber } from '../../struct/svg-number'
import { VectorElement } from '../element'
import { Util } from '../util'
import { Container } from './container'
export abstract class GeometryContainer<
TSVGGElement extends SVGAElement | SVGGElement
> extends Container<TSVGGElement> {
dmove(dx: number | string = 0, dy: number | string = 0) {
this.eachChild<VectorElement>((child) => {
const bbox = child.bbox()
const m = new Matrix(child)
// Translate childs matrix by amount and
// transform it back into parents space
const matrix = m
.translate(SVGNumber.toNumber(dx), SVGNumber.toNumber(dy))
.transform(m.inverse())
// Calculate new x and y from old box
const p = new Point(bbox.x, bbox.y).transform(matrix)
// Move child
child.move(p.x, p.y)
})
return this
}
move(x: number | string = 0, y: number | string = 0, box = this.bbox()) {
const dx = SVGNumber.toNumber(x) - box.x
const dy = SVGNumber.toNumber(y) - box.y
return this.dmove(dx, dy)
}
dx(dx: number | string) {
return this.dmove(dx, 0)
}
dy(dy: number | string) {
return this.dmove(0, dy)
}
x(): number
x(x: number | string | null, box?: Box): this
x(x?: number | string | null, box = this.bbox()) {
if (x == null) {
return box.x
}
return this.move(x, box.y, box)
}
y(): number
y(y: number | string | null, box?: Box): this
y(y?: number | string | null, box = this.bbox()) {
if (y == null) {
return box.y
}
return this.move(box.x, y, box)
}
size(
width?: number | string | null,
height?: number | string | null,
box = this.bbox(),
) {
const size = Util.proportionalSize(this, width, height, box)
const sx = SVGNumber.toNumber(size.width) / box.width
const sy = SVGNumber.toNumber(size.height) / box.height
this.eachChild<VectorElement>((child) => {
const o = new Point(box).transform(new Matrix(child).inverse())
child.scale(sx, sy, o.x, o.y)
})
return this
}
width(): number
width(width: number | string | null, box?: Box): this
width(width?: number | string | null, box = this.bbox()) {
if (width == null) {
return box.width
}
return this.size(new SVGNumber(width).value, box.height, box)
}
height(): number
height(height: number | string | null, box?: Box): this
height(height?: number | string | null, box = this.bbox()) {
if (height == null) {
return box.height
}
return this.size(box.width, new SVGNumber(height).value, box)
}
}

View File

@ -0,0 +1,79 @@
import { ObjUtil } from '../../util/obj'
// containers
import { ContainerExtension as AExtension } from './a-ext'
import { ContainerExtension as GExtension } from './g-ext'
import { ContainerExtension as SvgExtension } from './svg-ext'
import { ContainerExtension as MaskExtension } from './mask-ext'
import { ContainerExtension as MarkerExtension } from './marker-ext'
import { ContainerExtension as PatternExtension } from './pattern-ext'
import { ContainerExtension as ClipPathExtension } from './clippath-ext'
import { ContainerExtension as GradientExtension } from './gradient-ext'
import { ContainerExtension as SymbolExtension } from './symbol-ext'
// shapes
import { ContainerExtension as CircleExtension } from '../shape/circle-ext'
import { ContainerExtension as EllipseExtension } from '../shape/ellipse-ext'
import { ContainerExtension as ForeignObjectExtension } from '../shape/foreignobject-ext'
import { ContainerExtension as ImageExtension } from '../shape/image-ext'
import { ContainerExtension as LineExtension } from '../shape/line-ext'
import { ContainerExtension as PathExtension } from '../shape/path-ext'
import { ContainerExtension as PolygonExtension } from '../shape/polygon-ext'
import { ContainerExtension as PolylineExtension } from '../shape/polyline-ext'
import { ContainerExtension as RectExtension } from '../shape/rect-ext'
import { ContainerExtension as TextExtension } from '../shape/text-ext'
import { ContainerExtension as UseExtension } from '../shape/use-ext'
import { ContainerExtension as TextPathExtension } from '../shape/textpath-ext'
import { Container } from './container'
declare module './container' {
interface Container<
TSVGElement extends SVGElement = SVGElement
> extends AExtension<TSVGElement>,
GExtension<TSVGElement>,
SvgExtension<TSVGElement>,
MaskExtension<TSVGElement>,
MarkerExtension<TSVGElement>,
SymbolExtension<TSVGElement>,
PatternExtension<TSVGElement>,
ClipPathExtension<TSVGElement>,
GradientExtension<TSVGElement>,
// shapes
UseExtension<TSVGElement>,
RectExtension<TSVGElement>,
LineExtension<TSVGElement>,
TextExtension<TSVGElement>,
PathExtension<TSVGElement>,
ImageExtension<TSVGElement>,
CircleExtension<TSVGElement>,
EllipseExtension<TSVGElement>,
PolygonExtension<TSVGElement>,
PolylineExtension<TSVGElement>,
TextPathExtension<TSVGElement>,
ForeignObjectExtension<TSVGElement> {}
}
ObjUtil.applyMixins(
Container,
AExtension,
GExtension,
SvgExtension,
MaskExtension,
MarkerExtension,
SymbolExtension,
PatternExtension,
ClipPathExtension,
GradientExtension,
// shapes
UseExtension,
RectExtension,
LineExtension,
TextExtension,
PathExtension,
ImageExtension,
CircleExtension,
EllipseExtension,
PolygonExtension,
PolylineExtension,
TextPathExtension,
ForeignObjectExtension,
)

View File

@ -0,0 +1,87 @@
import { Box } from '../../struct/box'
import { Point } from '../../struct/point'
import { VectorElement } from '../element'
export class Viewbox<
TSVGContainerElement extends
| SVGSVGElement
| SVGSymbolElement
| SVGImageElement
| SVGPatternElement
| SVGMarkerElement
> extends VectorElement<TSVGContainerElement> {
viewbox(): Box
viewbox(box: Box.BoxLike): this
viewbox(
x: number | string,
y: number | string,
width: number | string,
height: number | string,
): this
viewbox(
x?: number | string | Box.BoxLike,
y?: number | string,
width?: number | string,
height?: number | string,
) {
if (x == null) {
return new Box(this.attr('viewBox'))
}
return this.attr(
'viewBox',
typeof x === 'object'
? `${x.x} ${x.y} ${x.width} ${x.height}`
: `${x} ${y} ${width} ${height}`,
)
}
zoom(): number
zoom(level: number, origin?: Point.PointLike): this
zoom(level?: number, origin?: Point.PointLike) {
let { width, height } = this.attr(['width', 'height'])
if (
(width == null && height == null) ||
typeof width === 'string' ||
typeof height === 'string'
) {
width = this.node.clientWidth
height = this.node.clientHeight
}
if (width == null || height == null) {
throw new Error(
'Impossible to get absolute width and height. ' +
'Please provide an absolute width and height attribute on the zooming element',
)
}
const v = this.viewbox()
const zoomX = width / v.width
const zoomY = height / v.height
const zoom = Math.min(zoomX, zoomY)
if (level == null) {
return zoom
}
let zoomAmount = zoom / level
// Set the zoomAmount to the highest value which is safe to process and
// recover from.
// The * 100 is a bit of wiggle room for the matrix transformation.
if (zoomAmount === Number.POSITIVE_INFINITY) {
zoomAmount = Number.MAX_SAFE_INTEGER / 100
}
const o = origin || {
x: width / 2 / zoomX + v.x,
y: height / 2 / zoomY + v.y,
}
const box = new Box(v).transform({ scale: zoomAmount, origin: o })
return this.viewbox(box)
}
}

View File

@ -0,0 +1,49 @@
import { Attrs } from '../../types'
import { VectorElement } from '../element'
@Container.register('Container')
export class Container<TSVGElement extends SVGElement = SVGElement>
extends VectorElement<TSVGElement>
implements Container.IContainer {
constructor()
constructor(attrs: Attrs | null)
constructor(node: TSVGElement | null, attrs?: Attrs | null)
constructor(node?: TSVGElement | Attrs | null, attrs?: Attrs | null)
// eslint-disable-next-line no-useless-constructor
constructor(node?: TSVGElement | Attrs | null, attrs?: Attrs | null) {
super(node, attrs)
}
flatten() {
this.eachChild((child) => {
if (child instanceof Container) {
child.flatten().ungroup()
}
})
return this
}
ungroup(parent?: Container, index?: number) {
const p = parent != null ? parent : this.parent<VectorElement>()
if (p) {
let idx = index == null ? p.indexOf(this) : index
// when parent != this, we want append all elements to the end
idx = idx === -1 ? p.children().length : idx
this.children<VectorElement>()
.reverse()
.forEach((child) => child.toParent(p, idx))
this.remove()
}
return this
}
}
export namespace Container {
export interface IContainer {
flatten(): this
ungroup(): this
}
}

View File

@ -0,0 +1,55 @@
import { ObjUtil } from '../../util/obj'
import { DefsExtension as MaskExtension } from './mask-ext'
import { DefsExtension as MarkerExtension } from './marker-ext'
import { DefsExtension as PatternExtension } from './pattern-ext'
import { DefsExtension as ClipPathExtension } from './clippath-ext'
import { DefsExtension as GradientPathExtension } from './gradient-ext'
import { ContainerExtension as CircleExtension } from '../shape/circle-ext'
import { ContainerExtension as EllipseExtension } from '../shape/ellipse-ext'
import { ContainerExtension as ImageExtension } from '../shape/image-ext'
import { ContainerExtension as LineExtension } from '../shape/line-ext'
import { ContainerExtension as PathExtension } from '../shape/path-ext'
import { ContainerExtension as PolygonExtension } from '../shape/polygon-ext'
import { ContainerExtension as PolylineExtension } from '../shape/polyline-ext'
import { ContainerExtension as RectExtension } from '../shape/rect-ext'
import { ContainerExtension as TextExtension } from '../shape/text-ext'
import { Defs } from './defs'
declare module './defs' {
interface Defs
extends ClipPathExtension<SVGDefsElement>,
GradientPathExtension<SVGDefsElement>,
PatternExtension<SVGDefsElement>,
MaskExtension<SVGDefsElement>,
MarkerExtension<SVGDefsElement>,
// shapes
RectExtension<SVGDefsElement>,
LineExtension<SVGDefsElement>,
TextExtension<SVGDefsElement>,
PathExtension<SVGDefsElement>,
ImageExtension<SVGDefsElement>,
CircleExtension<SVGDefsElement>,
EllipseExtension<SVGDefsElement>,
PolygonExtension<SVGDefsElement>,
PolylineExtension<SVGDefsElement> {}
}
ObjUtil.applyMixins(
Defs,
MaskExtension,
MarkerExtension,
PatternExtension,
ClipPathExtension,
GradientPathExtension,
// shapes
RectExtension,
LineExtension,
TextExtension,
PathExtension,
ImageExtension,
CircleExtension,
EllipseExtension,
PolygonExtension,
PolylineExtension,
)

View File

@ -0,0 +1,15 @@
import { VectorElement } from '../element'
import { Container } from './container'
@Defs.register('Defs')
export class Defs
extends VectorElement<SVGDefsElement>
implements Container.IContainer {
flatten() {
return this
}
ungroup() {
return this
}
}

View File

@ -0,0 +1,11 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { G } from './g'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
group(attrs?: Attrs) {
return G.create(attrs).appendTo(this)
}
}

View File

@ -0,0 +1,15 @@
import { Attrs } from '../../types'
import { GeometryContainer } from './container-geometry'
@G.register('G')
export class G extends GeometryContainer<SVGGElement> {}
export namespace G {
export function create(attrs?: Attrs | null) {
const g = new G()
if (attrs) {
g.attr(attrs)
}
return g
}
}

View File

@ -0,0 +1,43 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Decorator } from '../decorator'
import { Gradient } from './gradient'
type GradientMethod = {
gradient(type: Gradient.Type, attrs?: Attrs | null): Gradient
gradient(
type: Gradient.Type,
block: Gradient.Update,
attrs?: Attrs | null,
): Gradient
gradient(
type: Gradient.Type,
update?: Gradient.Update | Attrs | null,
attrs?: Attrs | null,
): Gradient
}
export class ContainerExtension<TSVGElement extends SVGElement>
extends Vector<TSVGElement>
implements GradientMethod {
@Decorator.checkDefs
gradient(
type: Gradient.Type,
update?: Gradient.Update | Attrs | null,
attrs?: Attrs | null,
) {
return this.defs()!.gradient(type, update, attrs)
}
}
export class DefsExtension<TSVGElement extends SVGElement>
extends Vector<TSVGElement>
implements GradientMethod {
gradient(
type: Gradient.Type,
update?: Gradient.Update | Attrs | null,
attrs?: Attrs | null,
) {
return Gradient.create(type, update, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,75 @@
import { SVGNumber } from '../../struct/svg-number'
import { VectorElement } from '../element'
@Stop.register('Stop')
export class Stop extends VectorElement<SVGStopElement> {
update(
offset?: number | string | SVGNumber,
color?: string,
opacity?: number | string | SVGNumber,
): this
update(options: Stop.Options): this
update(
offset?: Stop.Options | number | string | SVGNumber,
color?: string,
opacity?: number | string | SVGNumber,
): this
update(
offset?: Stop.Options | number | string | SVGNumber,
color?: string,
opacity?: number | string | SVGNumber,
) {
const options: {
offset?: number
color?: string
opacity?: number
} = {}
if (
offset == null ||
typeof offset === 'number' ||
typeof offset === 'string' ||
offset instanceof SVGNumber
) {
if (offset != null) {
options.offset = SVGNumber.create(offset).value
}
if (color != null) {
options.color = color
}
if (opacity != null) {
options.opacity = SVGNumber.create(opacity).value
}
} else {
if (offset.offset != null) {
options.offset = SVGNumber.create(offset.offset).value
}
if (offset.color != null) {
options.color = offset.color
}
if (offset.opacity != null) {
options.opacity = SVGNumber.create(offset.opacity).value
}
}
if (options.opacity != null) {
this.attr('stop-opacity', options.opacity)
}
if (options.color != null) {
this.attr('stop-color', options.color)
}
if (options.offset != null) {
this.attr('offset', options.offset)
}
return this
}
}
export namespace Stop {
export interface Options {
offset?: number | string | SVGNumber
color?: string
opacity?: number | string | SVGNumber
}
}

View File

@ -0,0 +1,150 @@
import { Attrs } from '../../types'
import { DomUtil } from '../../util/dom'
import { Box } from '../../struct/box'
import { SVGNumber } from '../../struct/svg-number'
import { VectorElement } from '../element'
import { Container } from './container'
import { Stop } from './gradient-stop'
@Gradient.register('Gradient')
export class Gradient extends Container<
SVGLinearGradientElement | SVGRadialGradientElement
> {
constructor(
type: Gradient.Type | SVGLinearGradientElement | SVGRadialGradientElement,
attrs?: Attrs,
) {
super(
typeof type === 'string'
? DomUtil.createNode<
SVGLinearGradientElement | SVGRadialGradientElement
>(`${type}Gradient`)
: type,
attrs,
)
}
from(x: number | string, y: number | string) {
return this.type === 'radialGradient'
? this.attr({
fx: SVGNumber.create(x).toString(),
fy: SVGNumber.create(y).toString(),
})
: this.attr({
x1: SVGNumber.create(x).toString(),
y1: SVGNumber.create(y).toString(),
})
}
to(x: number | string, y: number | string) {
return this.type === 'radialGradient'
? this.attr({
cx: SVGNumber.create(x).toString(),
cy: SVGNumber.create(x).toString(),
})
: this.attr({
x2: SVGNumber.create(y).toString(),
y2: SVGNumber.create(y).toString(),
})
}
stop(
offset?: number | string | SVGNumber,
color?: string,
opacity?: number | string | SVGNumber,
): Stop
stop(options: Stop.Options): Stop
stop(
offset?: Stop.Options | number | string | SVGNumber,
color?: string,
opacity?: number | string | SVGNumber,
) {
return new Stop().update(offset, color, opacity).appendTo(this)
}
attr(): Attrs
attr(names: string[]): Attrs
attr<T extends number | string = string>(name: string): T
attr(name: string, value: null): this
attr(name: string, value: number | string, ns?: string): this
attr(attrs: Attrs): this
attr<T extends number | string>(
name?: string,
value?: number | string | null,
ns?: string,
): T | this
attr(
attr?: string | string[] | Attrs,
value?: number | string | null,
ns?: string,
) {
return super.attr(
attr === 'transform' ? 'gradientTransform' : attr,
value,
ns,
)
}
bbox() {
return new Box()
}
targets<TVector extends VectorElement>() {
return Gradient.find<TVector>(`svg [fill*="${this.id()}"]`)
}
update(handler?: Gradient.Update | null) {
this.clear()
if (typeof handler === 'function') {
handler.call(this, this)
}
return this
}
url() {
return `url("#${this.id()}")`
}
toString() {
return this.url()
}
}
export namespace Gradient {
export type Type = 'linear' | 'radial'
export type Update = (this: Gradient, gradient: Gradient) => void
export function create(type: Type, attrs?: Attrs | null): Gradient
export function create(
type: Type,
update: Update,
attrs?: Attrs | null,
): Gradient
export function create(
type: Type,
update?: Update | Attrs | null,
attrs?: Attrs | null,
): Gradient
export function create(
type: Type,
update?: Update | Attrs | null,
attrs?: Attrs | null,
) {
const gradient = new Gradient(type)
if (update) {
if (typeof update === 'function') {
gradient.update(update)
if (attrs) {
gradient.attr(attrs)
}
} else {
gradient.attr(update)
}
} else if (attrs) {
gradient.attr(attrs)
}
return gradient
}
}

View File

@ -0,0 +1,110 @@
import { Attrs } from '../../types'
import { Decorator } from '../decorator'
import { Vector } from '../vector'
import { Marker } from './marker'
type MarkerMethod = {
marker(attrs?: Attrs | null): Marker
marker(size: number | string, attrs?: Attrs | null): Marker
marker(
size: number | string,
update: Marker.Update,
attrs?: Attrs | null,
): Marker
marker(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): Marker
marker(
width: number | string,
height: number | string,
update: Marker.Update,
attrs?: Attrs | null,
): Marker
marker(
width?: number | string | Attrs | null,
height?: number | string | Marker.Update | Attrs | null,
update?: Marker.Update | Attrs | null,
attrs?: Attrs | null,
): Marker
}
export class ContainerExtension<TSVGElement extends SVGElement>
extends Vector<TSVGElement>
implements MarkerMethod {
@Decorator.checkDefs
marker(
width?: number | string | Attrs | null,
height?: number | string | Marker.Update | Attrs | null,
update?: Marker.Update | Attrs | null,
attrs?: Attrs | null,
) {
return this.defs()!.marker(width, height, update, attrs)
}
}
export class DefsExtension<TSVGElement extends SVGElement>
extends Vector<TSVGElement>
implements MarkerMethod {
marker(
width?: number | string | Attrs | null,
height?: number | string | Marker.Update | Attrs | null,
update?: Marker.Update | Attrs | null,
attrs?: Attrs | null,
) {
return Marker.create(width, height, update, attrs).appendTo(this)
}
}
export class LineExtension<
TSVGLineElement extends
| SVGLineElement
| SVGPathElement
| SVGPolygonElement
| SVGPolygonElement
> extends Vector<TSVGLineElement> {
marker(type: Marker.Type, marker: Marker): this
marker(type: Marker.Type, size: number | string, attrs?: Attrs | null): this
marker(
type: Marker.Type,
size: number | string,
update: Marker.Update,
attrs?: Attrs | null,
): this
marker(
type: Marker.Type,
width: number | string,
height: number | string,
attrs?: Attrs | null,
): this
marker(
type: Marker.Type,
width: number | string,
height: number | string,
update: Marker.Update,
attrs?: Attrs | null,
): this
@Decorator.checkDefs
marker(
type: Marker.Type,
width: Marker | number | string,
height?: number | string | Marker.Update | Attrs | null,
update?: Marker.Update | Attrs | null,
attrs?: Attrs | null,
) {
let attr = 'marker'
if (type !== 'all') {
attr += `-${type}`
}
const marker =
width instanceof Marker
? width
: this.defs()!.marker(width, height as number, update, attrs)
this.attr(attr, marker.url())
return this
}
}

View File

@ -0,0 +1,152 @@
import { Attrs } from '../../types'
import { SVGNumber } from '../../struct/svg-number'
import { Container } from './container'
import { Viewbox } from './container-viewbox'
@Marker.register('Marker')
@Marker.mixin(Viewbox)
export class Marker extends Container<SVGMarkerElement> {
height(): number
height(h: number | string | null): this
height(h?: number | string | null) {
return this.attr<number>('markerHeight', h)
}
width(): number
width(w?: number | string | null): this
width(w?: number | string | null) {
return this.attr<number>('markerWidth', w)
}
units(): Marker.Units
units(units: Marker.Units | null): this
units(units?: Marker.Units | null) {
return this.attr<Marker.Units>('markerUnits', units)
}
orient(): Marker.Orient
orient(orient: Marker.Orient | null): this
orient(orient?: Marker.Orient | null) {
return this.attr<Marker.Orient>('orient', orient)
}
ref(x: string | number, y: string | number) {
return this.attr('refX', x).attr('refY', y)
}
update(handler: Marker.Update) {
this.clear()
if (typeof handler === 'function') {
handler.call(this, this)
}
return this
}
url() {
return `url(#${this.id()})`
}
toString() {
return this.url()
}
}
export interface Marker extends Viewbox<SVGMarkerElement> {}
export namespace Marker {
export type Type = 'start' | 'end' | 'mid' | 'all'
export type Units = 'userSpaceOnUse' | 'strokeWidth'
export type Orient = 'auto' | 'auto-start-reverse' | number
export type RefX = 'left' | 'center' | 'right' | number
export type RefY = 'top' | 'center' | 'bottom' | number
export type Update = (this: Marker, marker: Marker) => void
}
export namespace Marker {
export function create(attrs?: Attrs | null): Marker
export function create(size: number | string, attrs?: Attrs | null): Marker
export function create(
size: number | string,
update: Update,
attrs?: Attrs | null,
): Marker
export function create(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): Marker
export function create(
width: number | string,
height: number | string,
update: Update,
attrs?: Attrs | null,
): Marker
export function create(
width?: number | string | Attrs | null,
height?: number | string | Update | Attrs | null,
update?: Update | Attrs | null,
attrs?: Attrs | null,
): Marker
export function create(
width?: number | string | Attrs | null,
height?: number | string | Update | Attrs | null,
update?: Update | Attrs | null,
attrs?: Attrs | null,
): Marker {
const marker = new Marker()
marker.attr('orient', 'auto')
if (width != null) {
if (typeof width === 'object') {
marker.size(0, 0).ref(0, 0).viewbox(0, 0, 0, 0).attr(width)
} else {
const w = SVGNumber.toNumber(width)
if (height != null) {
if (typeof height === 'function') {
marker
.update(height)
.size(w, w)
.ref(w / 2, w / 2)
.viewbox(0, 0, w, w)
if (update) {
marker.attr(update as Attrs)
}
} else if (typeof height === 'object') {
marker
.size(w, w)
.ref(w / 2, w / 2)
.viewbox(0, 0, w, w)
.attr(height)
} else {
const h = SVGNumber.toNumber(height)
marker
.size(w, h)
.ref(w / 2, h / 2)
.viewbox(0, 0, w, h)
if (update != null) {
if (typeof update === 'function') {
marker.update(update)
if (attrs) {
marker.attr(attrs)
}
} else {
marker.attr(update)
}
}
}
} else {
marker
.size(w, w)
.ref(w / 2, w / 2)
.viewbox(0, 0, w, w)
}
}
}
return marker
}
}

View File

@ -0,0 +1,55 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { VectorElement } from '../element'
import { Container } from './container'
import { Adopter } from '../adopter'
import { Decorator } from '../decorator'
import { Mask } from './mask'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
@Decorator.checkDefs
mask(attrs?: Attrs | null) {
return this.defs()!.mask(attrs)
}
}
export class DefsExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
mask(attrs?: Attrs | null) {
return Mask.create(attrs).appendTo(this)
}
}
export class ElementExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
masker() {
return this.reference<Mask>('mask')
}
maskWith(element: Mask | Adopter.Target<VectorElement>) {
let masker: Mask | undefined | null
if (element instanceof Mask) {
masker = element
} else {
const parent = this.parent<Container>()
masker = parent && parent.mask()
if (masker) {
masker.add(element)
}
}
if (masker) {
this.attr('mask', `url("#${masker.id()}")`)
}
return this
}
unmask() {
return this.attr('mask', null)
}
}

View File

@ -0,0 +1,25 @@
import { Attrs } from '../../types'
import { Container } from './container'
import { VectorElement } from '../element'
@Mask.register('Mask')
export class Mask extends Container<SVGMaskElement> {
remove() {
this.targets().forEach((target) => target.unmask())
return super.remove()
}
targets<TVector extends VectorElement>() {
return Mask.find<TVector>(`svg [mask*="${this.id()}"]`)
}
}
export namespace Mask {
export function create(attrs?: Attrs | null) {
const mask = new Mask()
if (attrs) {
mask.attr(attrs)
}
return mask
}
}

View File

@ -0,0 +1,58 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Decorator } from '../decorator'
import { Pattern } from './pattern'
type PatternMethod = {
pattern(attrs?: Attrs | null): Pattern
pattern(size: number | string, attrs?: Attrs | null): Pattern
pattern(
size: number | string,
update: Pattern.Update,
attrs?: Attrs | null,
): Pattern
pattern(
width: number | string,
height: number | string,
update: Pattern.Update,
attrs?: Attrs | null,
): Pattern
pattern(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): Pattern
pattern(
width?: number | string | Attrs | null,
height?: number | string | Pattern.Update | Attrs | null,
update?: Pattern.Update | Attrs | null,
attrs?: Attrs | null,
): Pattern
}
export class ContainerExtension<TSVGElement extends SVGElement>
extends Vector<TSVGElement>
implements PatternMethod {
@Decorator.checkDefs
pattern(
width?: number | string | Attrs | null,
height?: number | string | Pattern.Update | Attrs | null,
update?: Pattern.Update | Attrs | null,
attrs?: Attrs | null,
) {
return this.defs()!.pattern(width, height, update, attrs)
}
}
export class DefsExtension<TSVGElement extends SVGElement>
extends Vector<TSVGElement>
implements PatternMethod {
pattern(
width?: number | string | Attrs | null,
height?: number | string | Pattern.Update | Attrs | null,
update?: Pattern.Update | Attrs | null,
attrs?: Attrs | null,
) {
return Pattern.create(width, height, update, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,147 @@
import { Attrs } from '../../types'
import { Box } from '../../struct/box'
import { Container } from './container'
import { Viewbox } from './container-viewbox'
import { VectorElement } from '../element'
@Pattern.mixin(Viewbox)
@Pattern.register('Pattern')
export class Pattern extends Container<SVGPatternElement> {
attr(): Attrs
attr(names: string[]): Attrs
attr<T extends string | number = string>(name: string): T
attr(name: string, value: null): this
attr(name: string, value: string | number, ns?: string): this
attr(attrs: Attrs): this
attr<T extends string | number>(
name: string,
value?: string | number | null,
ns?: string,
): T | this
attr(
attr?: string | string[] | Attrs,
value?: string | number | null,
ns?: string,
) {
return super.attr(
attr === 'transform' ? 'patternTransform' : attr,
value,
ns,
)
}
bbox() {
return new Box()
}
targets<TVector extends VectorElement>() {
return Pattern.find<TVector>(`svg [fill*="${this.id()}"]`)
}
update(handler?: Pattern.Update) {
this.clear()
if (typeof handler === 'function') {
handler.call(this, this)
}
return this
}
url() {
return `url("#${this.id()}")`
}
toString() {
return this.url()
}
}
export interface Pattern extends Viewbox<SVGPatternElement> {}
export namespace Pattern {
export type Update = (this: Pattern, pattern: Pattern) => void
export function create(attrs?: Attrs | null): Pattern
export function create(size: number | string, attrs?: Attrs | null): Pattern
export function create(
size: number | string,
update: Update,
attrs?: Attrs | null,
): Pattern
export function create(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): Pattern
export function create(
width: number | string,
height: number | string,
update: Update,
attrs?: Attrs | null,
): Pattern
export function create(
width?: number | string | Attrs | null,
height?: number | string | Update | Attrs | null,
update?: Update | Attrs | null,
attrs?: Attrs | null,
): Pattern
export function create(
width?: number | string | Attrs | null,
height?: number | string | Update | Attrs | null,
update?: Update | Attrs | null,
attrs?: Attrs | null,
): Pattern {
const pattern = new Pattern()
const base = {
x: 0,
y: 0,
patternUnits: 'userSpaceOnUse',
}
if (width != null) {
if (typeof width === 'object') {
pattern.attr(width)
} else if (height != null) {
if (typeof height === 'function') {
pattern.update(height).attr({
...base,
width,
height: width,
...update,
})
} else if (typeof height === 'object') {
pattern.attr({
...base,
width,
height: width,
...height,
})
} else if (update != null) {
if (typeof update === 'function') {
pattern.update(update).attr({
...base,
width,
height,
...attrs,
})
} else {
pattern.attr({
...base,
width,
height,
...update,
})
}
}
} else {
pattern.attr({
...base,
width,
height: width,
})
}
}
return pattern
}
}

View File

@ -0,0 +1,11 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Svg } from './svg'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
nested(attrs?: Attrs) {
return Svg.create(attrs).appendTo(this)
}
}

View File

@ -0,0 +1,84 @@
import { Attrs } from '../../types'
import { DomUtil } from '../../util/dom'
import { Global } from '../../global'
import { Adopter } from '../adopter'
import { Container } from './container'
import { Viewbox } from './container-viewbox'
import { Defs } from './defs'
@Svg.mixin(Viewbox)
@Svg.register('Svg', true)
export class Svg extends Container<SVGSVGElement> {
constructor()
constructor(attrs: Attrs | null)
constructor(node: SVGSVGElement | null, attrs?: Attrs | null)
constructor(node?: SVGSVGElement | Attrs | null, attrs?: Attrs | null) {
super(DomUtil.ensureNode<SVGSVGElement>('svg', node), attrs)
this.namespace()
}
isRoot() {
const parentNode = this.node.parentNode
return (
!parentNode ||
(!(parentNode instanceof Global.window.SVGElement) &&
parentNode.nodeName !== '#document-fragment')
)
}
root() {
return this.isRoot() ? this : super.root()
}
defs(): Defs {
if (!this.isRoot()) {
const root = this.root()
if (root) {
return root.defs()
}
}
const defs = this.node.querySelector('defs')
if (defs) {
return Adopter.adopt<Defs>(defs)
}
return this.put(new Defs())
}
namespace() {
if (!this.isRoot()) {
const root = this.root()
if (root) {
root.namespace()
return this
}
}
return this.attr({ xmlns: DomUtil.namespaces.svg, version: '1.1' }).attr(
'xmlns:xlink',
DomUtil.namespaces.xlink,
DomUtil.namespaces.xmlns,
)
}
removeNamespace() {
return this.attr({ xmlns: null, version: null }).attr(
'xmlns:xlink',
null,
DomUtil.namespaces.xmlns,
)
}
}
export interface Svg extends Viewbox<SVGSVGElement> {}
export namespace Svg {
export function create(attrs?: Attrs | null) {
const svg = new Svg()
if (attrs) {
svg.attr(attrs)
}
return svg
}
}

View File

@ -0,0 +1,11 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Symbol } from './symbol'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
symbol(attrs?: Attrs) {
return Symbol.create(attrs).appendTo(this)
}
}

View File

@ -0,0 +1,19 @@
import { Attrs } from '../../types'
import { Container } from './container'
import { Viewbox } from './container-viewbox'
@Symbol.mixin(Viewbox)
@Symbol.register('Symbol')
export class Symbol extends Container<SVGSymbolElement> {}
export interface Symbol extends Viewbox<SVGSymbolElement> {}
export namespace Symbol {
export function create(attrs?: Attrs | null) {
const symbol = new Symbol()
if (attrs) {
symbol.attr(attrs)
}
return symbol
}
}

View File

@ -0,0 +1,24 @@
import type { VectorElement } from './element'
export namespace Decorator {
export function checkDefs<TSVGElement extends SVGElement>(
target: any,
methodName: string,
descriptor: PropertyDescriptor,
) {
const raw = descriptor.value
descriptor.value = function (
this: VectorElement<TSVGElement>,
...arguments_: any[]
) {
const defs = this.defs()
if (defs == null) {
throw new Error(
'Can not get or create SVGDefsElement in the current document tree. ' +
'Please ensure that the current element is attached into any SVG context.',
)
}
return raw.call(this, ...arguments_)
}
}
}

View File

@ -0,0 +1,157 @@
import { DomUtil } from '../../util/dom'
import { Primer } from './primer'
export class ClassName<TNode extends Node> extends Primer<TNode> {
classes() {
const raw = this.attr<string>('class')
return raw == null ? [] : raw.trim().split(/\s+/)
}
hasClass(name: string) {
return ClassName.has(this.node, name)
}
addClass(name: string): this
addClass(names: string[]): this
addClass(name: string | string[]) {
ClassName.add(this.node, Array.isArray(name) ? name.join(' ') : name)
return this
}
removeClass(name: string): this
removeClass(names: string[]): this
removeClass(name: string | string[]) {
ClassName.remove(this.node, Array.isArray(name) ? name.join(' ') : name)
return this
}
toggleClass(name: string, stateValue?: boolean) {
ClassName.toggle(this.node, name, stateValue)
return this
}
}
export namespace ClassName {
const rclass = /[\t\n\f\r]/g
const rnotwhite = /\S+/g
const fillSpaces = (string: string) => ` ${string} `
const get = (elem: Element) =>
(elem && elem.getAttribute && elem.getAttribute('class')) || ''
export function has(elem: Node | null, selector: string | null) {
if (elem == null || selector == null) {
return false
}
const node = DomUtil.toElement(elem)
const classNames = fillSpaces(get(node))
const className = fillSpaces(selector)
return node.nodeType === 1
? classNames.replace(rclass, ' ').includes(className)
: false
}
export function add(
elem: Node | null,
selector: ((cls: string) => string) | string | null,
): void {
if (elem == null || selector == null) {
return
}
const node = DomUtil.toElement(elem)
if (typeof selector === 'function') {
add(node, selector(get(node)))
return
}
if (typeof selector === 'string' && node.nodeType === 1) {
const classes = selector.match(rnotwhite) || []
const oldValue = fillSpaces(get(node)).replace(rclass, ' ')
let newValue = classes.reduce((memo, cls) => {
if (!memo.includes(fillSpaces(cls))) {
return `${memo}${cls} `
}
return memo
}, oldValue)
newValue = newValue.trim()
if (oldValue !== newValue) {
node.setAttribute('class', newValue)
}
}
}
export function remove(
elem: Node | null,
selector?: ((cls: string) => string) | string | null,
): void {
if (elem == null) {
return
}
const node = DomUtil.toElement(elem)
if (typeof selector === 'function') {
remove(elem, selector(get(node)))
return
}
if ((!selector || typeof selector === 'string') && elem.nodeType === 1) {
const classes = (selector || '').match(rnotwhite) || []
const oldValue = fillSpaces(get(node)).replace(rclass, ' ')
let newValue = classes.reduce((memo, cls) => {
const className = fillSpaces(cls)
if (memo.includes(className)) {
return memo.replace(className, ' ')
}
return memo
}, oldValue)
newValue = selector ? newValue.trim() : ''
if (oldValue !== newValue) {
node.setAttribute('class', newValue)
}
}
}
export function toggle(
elem: Node | null,
selector: ((cls: string, status?: boolean) => string) | string | null,
state?: boolean,
): void {
if (elem == null || selector == null) {
return
}
if (state != null && typeof selector === 'string') {
if (state) {
add(elem, selector)
} else {
remove(elem, selector)
}
return
}
if (typeof selector === 'function') {
toggle(elem, selector(get(DomUtil.toElement(elem)), state), state)
return
}
if (typeof selector === 'string') {
const metches = selector.match(rnotwhite) || []
metches.forEach((cls) => {
if (has(elem, cls)) {
remove(elem, cls)
} else {
add(elem, cls)
}
})
}
}
}

View File

@ -0,0 +1,103 @@
import { DomUtil } from '../../util/dom'
import { Str } from '../../util/str'
import { Primer } from './primer'
export class Data<TNode extends Node> extends Primer<TNode> {
data(): Record<string, any>
data<T>(key: string): T
data(keys: string[]): Record<string, any>
data<T>(key: string, value: T, raw?: boolean): this
data(
key?: string | string[] | Record<string, any>,
val?: any,
raw?: boolean,
) {
// Get all data
if (key == null) {
const elem = DomUtil.toElement(this.node)
const attrs = elem.attributes
const keys: string[] = []
for (let i = 0, l = attrs.length; i < l; i += 1) {
const item = attrs.item(i)
if (item && item.nodeName.indexOf('data-')) {
keys.push(item.nodeName.slice(5))
}
}
return this.data(keys)
}
// Get specified data with keys
if (Array.isArray(key)) {
const data: Record<string, any> = {}
key.forEach((k) => {
// Return the camelCased key
data[Str.camelCase(k)] = this.data(k)
})
return data
}
// Set with data object
if (typeof key === 'object') {
Object.keys(key).forEach((k) => this.data(k, key[k]))
return this
}
const dataKey = Data.parseKey(key)
// Get by key
if (typeof val === 'undefined') {
const value = this.attr(dataKey)
return Data.parseValue(value)
}
// Set with key-value
{
const dataValue =
val === null
? null
: raw === true || typeof val === 'string' || typeof val === 'number'
? val
: JSON.stringify(val)
return this.attr(dataKey, dataValue)
}
}
}
export namespace Data {
const rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/
export function parseKey(key: string) {
return `data-${key.replace(/[A-Z]/g, '-$&').toLowerCase()}`
}
export function parseValue(val: string | number | null) {
if (val && typeof val === 'string') {
if (val === 'true') {
return true
}
if (val === 'false') {
return false
}
if (val === 'null') {
return null
}
// Only convert to a number if it doesn't change the string
if (val === `${+val}`) {
return +val
}
if (rbrace.test(val)) {
try {
return JSON.parse(val)
} catch {
// pass
}
}
}
return val
}
}

View File

@ -0,0 +1,545 @@
import { Attrs, Class } from '../../types'
import { Global } from '../../global'
import { DomUtil } from '../../util/dom'
import { Attr } from '../../util/attr'
import { Svg } from '../container/svg'
import { Adopter } from '../adopter'
import { Data } from './data'
import { Event } from './event'
import { Style } from './style'
import { Primer } from './primer'
import { Memory } from './memory'
import { Listener } from './listener'
import { ClassName } from './classname'
@Dom.register('Dom')
@Dom.mixin(Event, ClassName, Style, Data, Memory, Listener)
export class Dom<TNode extends Node = Node> extends Primer<TNode> {
first<T extends Dom = Dom>(): T | null {
return Dom.adopt<T>(this.node.firstChild)
}
last<T extends Dom = Dom>(): T | null {
return Dom.adopt<T>(this.node.lastChild)
}
get<T extends Dom = Dom>(index: number): T | null {
return Dom.adopt<T>(this.node.childNodes[index])
}
find<T extends Dom = Dom>(selectors: string): T[] {
return Dom.find<T>(selectors, DomUtil.toElement(this.node))
}
findOne<T extends Dom = Dom>(selectors: string): T | null {
return Dom.findOne<T>(selectors, DomUtil.toElement(this.node))
}
matches(selector: string): boolean {
const elem = DomUtil.toElement(this.node)
const node = this.node as any
const matcher = elem.matches
// eslint-disable-next-line no-unused-expressions
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
node.matchesSelector ||
node.msMatchesSelector ||
node.mozMatchesSelector ||
elem.webkitMatchesSelector ||
node.oMatchesSelector ||
null
return matcher ? matcher.call(elem, selector) : false
}
children<T extends Dom>(): T[] {
const elems: T[] = []
this.node.childNodes.forEach((node) => {
elems.push(Dom.adopt<T>(node))
})
return elems
}
clear() {
while (this.node.lastChild) {
this.node.lastChild.remove()
}
return this
}
clone<T extends Dom>(deep = true): T {
// write dom data to the dom so the clone can pickup the data
this.storeAssets()
// clone element and assign new id
const ctor = this.constructor as new (node: Node) => T
// eslint-disable-next-line new-cap
return new ctor(DomUtil.assignNewId(this.node.cloneNode(deep)))
}
eachChild<T extends Dom>(
iterator: (this: T, child: T, index: number, children: T[]) => void,
deep?: boolean,
) {
const children = this.children()
for (let i = 0, l = children.length; i < l; i += 1) {
const child = children[i]
iterator.call(child, child, i, children)
if (deep) {
child.eachChild(iterator, deep)
}
}
return this
}
indexOf(element: Dom): number {
const children = Array.prototype.slice.call(this.node.childNodes) as Node[]
return children.indexOf(element.node)
}
index(): number {
const parent: Dom | null = this.parent()
return parent ? parent.indexOf(this) : -1
}
has(element: Dom): boolean {
return this.indexOf(element) !== -1
}
id(): string
id(id: string | null): this
id(id?: string | null) {
const elem = DomUtil.toElement(this.node)
// generate new id if no id set
if (typeof id === 'undefined' && !elem.id) {
elem.id = DomUtil.createNodeId()
}
// dont't set directly with this.node.id to make `null` work correctly
return typeof id === 'undefined' ? this.attr('id') : this.attr('id', id)
}
parent<T extends Dom = Dom>(selectors?: string | Class): T | null {
if (this.node.parentNode == null) {
return null
}
let parent: T | null = Dom.adopt<T>(this.node.parentNode)
if (selectors == null) {
return parent
}
// loop trough ancestors if type is given
do {
if (
typeof selectors === 'string'
? parent.matches(selectors)
: parent instanceof selectors
) {
return parent
}
} while ((parent = Dom.adopt<T>(parent.node.parentNode)))
return null
}
add<T extends Dom>(element: Adopter.Target<T>, index?: number): this {
const instance = Adopter.makeInstance<T>(element)
// If non-root svg nodes are added we have to remove their namespaces
if (instance.isSVGSVGElement()) {
const svg = Dom.adopt<Svg>(instance.node)
svg.removeNamespace()
}
if (index == null) {
this.node.appendChild(instance.node)
} else if (instance.node !== this.node.childNodes[index]) {
this.node.insertBefore(instance.node, this.node.childNodes[index])
}
return this
}
append<T extends Dom>(element: Adopter.Target<T>): this {
return this.add(element)
}
prepend<T extends Dom>(element: Adopter.Target<T>): this {
return this.add(element, 0)
}
appendTo<T extends Dom>(parent: Adopter.Target<T>): this {
return this.addTo(parent)
}
addTo<T extends Dom>(parent: Adopter.Target<T>, index?: number): this {
return Adopter.makeInstance<T>(parent).put(this, index)
}
put<T extends Dom>(element: Adopter.Target<T>, index?: number): T {
const instance = Adopter.makeInstance<T>(element)
this.add(instance, index)
return instance
}
putIn<T extends Dom>(parent: Adopter.Target<T>, index?: number): T {
return Adopter.makeInstance<T>(parent).add(this, index)
}
element<T extends Dom>(nodeName: string, attrs?: Attrs | null): T {
const elem = Adopter.makeInstance<T>(nodeName)
if (attrs) {
elem.attr(attrs)
}
return this.put(elem)
}
replace<T extends Dom>(element: Adopter.Target<T>): T {
const instance = Adopter.makeInstance<T>(element)
if (this.node.parentNode) {
this.node.parentNode.replaceChild(instance.node, this.node)
}
return instance
}
remove() {
const parent = this.parent()
if (parent) {
parent.removeElement(this)
}
return this
}
removeElement(element: Dom) {
this.node.removeChild(element.node)
return this
}
before<T extends Dom>(element: Adopter.Target<T>) {
const parent = this.parent()
if (parent) {
const index = this.index()
const instance = Adopter.makeInstance(element)
instance.remove()
parent.add(instance, index)
}
return this
}
after<T extends Dom>(element: Adopter.Target<T>) {
const parent = this.parent()
if (parent) {
const index = this.index()
const instance = Adopter.makeInstance(element)
instance.remove()
parent.add(element, index + 1)
}
return this
}
insertBefore<T extends Dom>(element: Adopter.Target<T>) {
Adopter.makeInstance(element).before(this)
return this
}
insertAfter<T extends Dom>(element: Adopter.Target<T>) {
Adopter.makeInstance(element).after(this)
return this
}
siblings<T extends Dom>(): T[]
siblings<T extends Dom>(selfInclued?: boolean): T[]
siblings<T extends Dom>(selectors: string, selfInclued?: boolean): T[]
siblings(selectors?: string | boolean, selfInclued?: boolean) {
const parent = this.parent()
const children = parent ? parent.children() : []
if (selectors == null) {
return children.filter((child) => child !== this)
}
if (typeof selectors === 'boolean') {
return selectors ? children : children.filter((child) => child !== this)
}
return children.filter(
(child) => child.matches(selectors) && (selfInclued || child !== this),
)
}
next<T extends Dom>(selectors?: string): T | null {
const parent = this.parent()
if (parent) {
const index = this.index()
const children = parent.children<T>()
for (let i = index + 1, l = children.length; i < l; i += 1) {
const next = children[i]
if (selectors == null || next.matches(selectors)) {
return next
}
}
}
return null
}
nextAll<T extends Dom>(selectors?: string): T[] {
const result: T[] = []
const parent = this.parent()
if (parent) {
const index = this.index()
const children = parent.children<T>()
for (let i = index + 1, l = children.length; i < l; i += 1) {
const next = children[i]
if (selectors == null || next.matches(selectors)) {
result.push(next)
}
}
}
return result
}
prev<T extends Dom>(selectors?: string): T | null {
const parent = this.parent()
if (parent) {
const index = this.index()
const children = parent.children<T>()
for (let i = index - 1; i >= 0; i -= 1) {
const previous = children[i]
if (selectors == null || previous.matches(selectors)) {
return previous
}
}
}
return null
}
prevAll<T extends Dom>(selectors?: string): T[] {
const result: T[] = []
const parent = this.parent()
if (parent) {
const index = this.index()
const children = parent.children<T>()
for (let i = index - 1; i >= 0; i -= 1) {
const previous = children[i]
if (selectors == null || previous.matches(selectors)) {
result.push(previous)
}
}
}
return result
}
forward() {
const parent = this.parent()
if (parent) {
const index = this.index()
parent.add(this.remove(), index + 1)
}
return this
}
backward() {
const parent = this.parent()
if (parent) {
const index = this.index()
parent.add(this.remove(), index ? index - 1 : 0)
}
return this
}
front() {
const parent = this.parent()
if (parent) {
parent.add(this.remove())
}
return this
}
back() {
const parent = this.parent()
if (parent) {
parent.add(this.remove(), 0)
}
return this
}
wrap<T extends Dom>(node: Adopter.Target<T>): this {
const parent = this.parent()
if (!parent) {
return this.addTo<T>(node)
}
const index = parent.indexOf(this)
return parent.put<T>(node, index).put(this)
}
words(text: string) {
this.node.textContent = text
return this
}
storeAssets() {
this.eachChild(() => this.storeAssets())
return this
}
toString() {
return this.id()
}
isDocument() {
return DomUtil.isDocument(this.node)
}
isSVGSVGElement() {
return DomUtil.isSVGSVGElement(this.node)
}
isDocumentFragment() {
return DomUtil.isDocumentFragment(this.node)
}
html(): string
html(outerHTML: boolean): string
html(process: (dom: Dom) => false | Dom, outerHTML?: boolean): string
html(content: string, outerHTML?: boolean): string
html(arg1?: boolean | string | ((dom: Dom) => false | Dom), arg2?: boolean) {
return this.xml(arg1 as string, arg2, DomUtil.namespaces.html)
}
svg(): string
svg(outerXML: boolean): string
svg(process: (dom: Dom) => false | Dom, outerXML?: boolean): string
svg(content: string, outerXML?: boolean): string
svg(arg1?: boolean | string | ((dom: Dom) => false | Dom), arg2?: boolean) {
return this.xml(arg1 as string, arg2, DomUtil.namespaces.svg)
}
xml(): string
xml(outerXML: boolean): string
xml(process: (dom: Dom) => false | Dom, outerXML?: boolean): string
xml(content: string, outerXML?: boolean, ns?: string): string
xml(
arg1?: boolean | string | ((dom: Dom) => false | Dom),
arg2?: boolean,
arg3?: string,
) {
const content = typeof arg1 === 'boolean' ? null : arg1
let isOuterXML = typeof arg1 === 'boolean' ? arg1 : arg2
const ns = arg3
// getter
// ------
if (content == null || typeof content === 'function') {
// The default for exports is, that the outerNode is included
isOuterXML = isOuterXML == null ? true : isOuterXML
this.storeAssets()
let current: Dom = this // eslint-disable-line @typescript-eslint/no-this-alias
// An export modifier was passed
if (typeof content === 'function') {
current = Dom.adopt<Dom>(current.node.cloneNode(true))
// If the user wants outerHTML we need to process this node, too
if (isOuterXML) {
const result = content(current)
current = result || current
// The user does not want this node? Well, then he gets nothing
if (result === false) {
return ''
}
}
// Deep loop through all children and apply modifier
current.eachChild((child) => {
const result = content(child)
const next = result || child
if (result === false) {
// If modifier returns false, discard node
child.remove()
} else if (result && child !== next) {
// If modifier returns new node, use it
child.replace(next)
}
}, true)
}
const element = current.node as Element
return isOuterXML ? element.outerHTML : element.innerHTML
}
// setter
// ------
{
// The default for import is, that the current node is not replaced
isOuterXML = isOuterXML == null ? false : isOuterXML
const wrapper = DomUtil.createNode('wrapper', ns)
const fragment = Global.document.createDocumentFragment()
wrapper.innerHTML = content
for (let i = wrapper.children.length; i > 0; i -= 1) {
fragment.append(wrapper.firstElementChild!)
}
if (isOuterXML) {
const parent = this.parent()
this.replace(fragment)
return parent
}
return this.add(fragment)
}
}
}
export interface Dom<TNode extends Node = Node>
extends ClassName<TNode>,
Event<TNode>,
Style<TNode>,
Data<TNode>,
Memory<TNode>,
Listener<TNode> {}
export namespace Dom {
export function adopt<T extends Dom>(node: Node): T
export function adopt<T extends Dom>(node?: Node | null): T | null
export function adopt<T extends Dom>(node?: Node | null) {
return Adopter.adopt<T>(node)
}
export function find<T extends Dom = Dom>(
selectors: string,
parent: Element | Document = Global.document,
) {
const elems: T[] = []
parent
.querySelectorAll(selectors)
.forEach((node) => elems.push(adopt<T>(node)))
return elems
}
export function findOne<T extends Dom = Dom>(
selectors: string,
parent: Element | Document = Global.document,
) {
return adopt<T>(parent.querySelector(selectors))
}
}
export namespace Dom {
export const registerAttrHook = Attr.registerHook
export const registerEventHook = Event.registerHook
export const registerStyleHook = Style.registerHook
}

View File

@ -0,0 +1,7 @@
export type EventRaw = Event
export type UIEventRaw = UIEvent
export type MouseEventRaw = MouseEvent
export type DragEventRaw = DragEvent
export type KeyboardEventRaw = KeyboardEvent
export type TouchEventRaw = TouchEvent
export type FocusEventRaw = FocusEvent

View File

@ -0,0 +1,445 @@
import { Global } from '../../global'
import { DomUtil } from '../../util/dom'
import { Util } from './event-util'
import { Hook } from './event-hook'
import { Store } from './event-store'
import { EventRaw } from './event-alias'
import { EventObject } from './event-object'
import { EventHandler } from './event-types'
export namespace Core {
let triggered: string | undefined
export function add(
elem: Store.EventTarget,
types: string,
handler:
| EventHandler<any, any>
| ({
handler: EventHandler<any, any>
selector?: string
} & Partial<Store.HandlerObject>),
data?: any,
selector?: string,
) {
if (!Util.isValidTarget(elem)) {
return
}
// Caller can pass in an object of custom data in lieu of the handler
if (typeof handler !== 'function') {
const temp = handler
handler = temp.handler // eslint-disable-line
selector = temp.selector // eslint-disable-line
}
// Ensure that invalid selectors throw exceptions at attach time
if (!Util.isValidSelector(elem, selector)) {
throw new Error('Delegate event with invalid selector.')
}
const store = Store.ensure(elem)
// Ensure the main handle
let mainHandler = store.handler
if (mainHandler == null) {
mainHandler = store.handler = function (e, ...args: any[]) {
return triggered !== e.type ? dispatch(elem, e, ...args) : undefined
}
}
// Make sure that the handler has a unique ID, used to find/remove it later
const guid = Util.ensureHandlerId(handler)
// Handle multiple events separated by a space
Util.splitType(types).forEach((item) => {
const { originType, namespaces } = Util.normalizeType(item)
// There *must* be a type, no attaching namespace-only handlers
if (!originType) {
return
}
let type = originType
let hook = Hook.get(type)
// If selector defined, determine special event type, otherwise given type
type = (selector ? hook.delegateType : hook.bindType) || type
// Update hook based on newly reset type
hook = Hook.get(type)
// handleObj is passed to all event handlers
const others = typeof handler === 'function' ? undefined : handler
const handleObj: Store.HandlerObject = {
type,
originType,
data,
selector,
guid,
handler: handler as EventHandler<any, any>,
namespace: namespaces.join('.'),
needsContext: Util.needsContext(selector),
...others,
}
// Init the event handler queue if we're the first
const events = store.events
let bag = events[type]
if (!bag) {
bag = events[type] = { handlers: [], delegateCount: 0 }
// Only use addEventListener if the `hook.steup` returns false
if (
!hook.setup ||
hook.setup(elem, data, namespaces, mainHandler!) === false
) {
DomUtil.addEventListener(
elem as Element,
type,
(mainHandler as any) as EventListener,
)
}
}
if (hook.add) {
Util.removeHandlerId(handleObj.handler)
hook.add(elem, handleObj)
Util.setHandlerId(handleObj.handler, guid)
}
// Add to the element's handler list, delegates in front
if (selector) {
bag.handlers.splice(bag.delegateCount, 0, handleObj)
bag.delegateCount += 1
} else {
bag.handlers.push(handleObj)
}
})
}
export function remove(
elem: Store.EventTarget,
types: string,
handler?: EventHandler<any, any>,
selector?: string,
mappedTypes?: boolean,
) {
const store = Store.get(elem)
if (!store) {
return
}
const events = store.events
if (!events) {
return
}
// Once for each type.namespace in types; type may be omitted
Util.splitType(types).forEach((item) => {
const { originType, namespaces } = Util.normalizeType(item)
// Unbind all events (on this namespace, if provided) for the element
if (!originType) {
Object.keys(events).forEach((key) => {
remove(elem, key + item, handler, selector, true)
})
return
}
let type = originType
const hook = Hook.get(type)
type = (selector ? hook.delegateType : hook.bindType) || type
const bag = events[type] || {}
const rns =
namespaces.length > 0
? new RegExp(`(^|\\.)${namespaces.join('\\.(?:.*\\.|)')}(\\.|$)`)
: null
// Remove matching events
const originHandlerCount = bag.handlers.length
for (let i = bag.handlers.length - 1; i >= 0; i -= 1) {
const handleObj = bag.handlers[i]
if (
(mappedTypes || originType === handleObj.originType) &&
(!handler || Util.getHandlerId(handler) === handleObj.guid) &&
(!rns || !handleObj.namespace || rns.test(handleObj.namespace)) &&
(!selector ||
selector === handleObj.selector ||
(selector === '**' && handleObj.selector))
) {
bag.handlers.splice(i, 1)
if (handleObj.selector) {
bag.delegateCount -= 1
}
if (hook.remove) {
hook.remove(elem, handleObj)
}
}
}
if (originHandlerCount && bag.handlers.length === 0) {
if (
!hook.teardown ||
hook.teardown(elem, namespaces, store.handler!) === false
) {
DomUtil.removeEventListener(
elem as Element,
type,
(store.handler as any) as EventListener,
)
}
delete events[type]
}
})
// Remove data and the expando if it's no longer used
if (Object.keys(events).length === 0) {
Store.remove(elem)
}
}
export function dispatch(
elem: Store.EventTarget,
evt: Event | EventObject | string,
...args: any[]
) {
const event = EventObject.create(evt)
event.delegateTarget = elem as Element
const hook = Hook.get(event.type)
if (hook.preDispatch && hook.preDispatch(elem, event) === false) {
return undefined
}
const handlerQueue = Util.getHandlerQueue(elem, event)
// Run delegates first; they may want to stop propagation beneath us
for (
let i = 0, l = handlerQueue.length;
i < l && !event.isPropagationStopped();
i += 1
) {
const matched = handlerQueue[i]
event.currentTarget = matched.elem
for (
let j = 0, k = matched.handlers.length;
j < k && !event.isImmediatePropagationStopped();
j += 1
) {
const handleObj = matched.handlers[j]
// If event is namespaced, then each handler is only invoked if it is
// specially universal or its namespaces are a superset of the event's.
if (
event.rnamespace == null ||
!handleObj.namespace ||
event.rnamespace.test(handleObj.namespace)
) {
event.handleObj = handleObj
event.data = handleObj.data
const hookHandle = Hook.get(handleObj.originType).handle
const result = hookHandle
? hookHandle(matched.elem as Store.EventTarget, event, ...args)
: handleObj.handler.call(matched.elem, event, ...args)
if (result !== undefined) {
event.result = result
if (result === false) {
event.preventDefault()
event.stopPropagation()
}
}
}
}
}
// Call the postDispatch hook for the mapped type
if (hook.postDispatch) {
hook.postDispatch(elem, event)
}
return event.result
}
export function trigger(
event: EventObject.Event | EventObject | EventRaw | string,
data: any,
elem?: Store.EventTarget,
onlyHandlers?: boolean,
) {
let eventObj = event as EventObject
let type = typeof event === 'string' ? event : event.type
let namespaces =
typeof event === 'string' || eventObj.namespace == null
? []
: eventObj.namespace.split('.')
const node = (elem || Global.document) as HTMLElement
// Don't do events on text and comment nodes
if (node.nodeType === 3 || node.nodeType === 8) {
return undefined
}
// focus/blur morphs to focusin/out; ensure we're not firing them right now
const rfocusMorph = /^(?:focusinfocus|focusoutblur)$/
if (rfocusMorph.test(type + triggered)) {
return undefined
}
if (type.indexOf('.') > -1) {
// Namespaced trigger; create a regexp to match event type in handle()
namespaces = type.split('.')
type = namespaces.shift()!
namespaces.sort()
}
const ontype = type.indexOf(':') < 0 && (`on${type}` as 'onclick')
// Caller can pass in a EventObject, Object, or just an event type string
eventObj =
event instanceof EventObject
? event
: new EventObject(type, typeof event === 'object' ? event : null)
// Trigger bitmask: & 1 for native handlers; & 2 for custom (always true)
eventObj.isTrigger = onlyHandlers ? 2 : 3
eventObj.namespace = namespaces.join('.')
eventObj.rnamespace = eventObj.namespace
? new RegExp(`(^|\\.)${namespaces.join('\\.(?:.*\\.|)')}(\\.|$)`)
: null
// Clean up the event in case it is being reused
eventObj.result = undefined
if (!eventObj.target) {
eventObj.target = node
}
const args: [EventObject, ...any[]] = [eventObj]
if (Array.isArray(data)) {
args.push(...data)
} else {
args.push(data)
}
const hook = Hook.get(type)
if (
!onlyHandlers &&
hook.trigger &&
hook.trigger(node, eventObj, data) === false
) {
return undefined
}
let bubbleType
// Determine event propagation path in advance, per W3C events spec.
// Bubble up to document, then to window; watch for a global ownerDocument
const eventPath = [node]
if (!onlyHandlers && !hook.noBubble && !DomUtil.isWindow(node)) {
bubbleType = hook.delegateType || type
let curr = node
let last = node
if (!rfocusMorph.test(bubbleType + type)) {
curr = curr.parentNode as HTMLElement
}
for (; curr != null; curr = curr.parentNode as HTMLElement) {
eventPath.push(curr)
last = curr
}
// Only add window if we got to document (e.g., not plain obj or detached DOM)
const doc = node.ownerDocument || Global.document
if ((last as any) === doc) {
const win =
(last as any).defaultView ||
(last as any).parentWindow ||
Global.window
eventPath.push(win)
}
}
let lastElement = node
// Fire handlers on the event path
for (
let i = 0, l = eventPath.length;
i < l && !eventObj.isPropagationStopped();
i += 1
) {
const curr = eventPath[i]
lastElement = curr
eventObj.type = i > 1 ? (bubbleType as string) : hook.bindType || type
// custom handler
const store = Store.get(curr as Element)
if (store) {
if (store.events[eventObj.type] && store.handler) {
store.handler.call(curr, ...args)
}
}
// Native handler
const handle = ontype ? curr[ontype] : null
if (handle && handle.apply && Util.isValidTarget(curr)) {
eventObj.result = handle.call(curr, ...args)
if (eventObj.result === false) {
eventObj.preventDefault()
}
}
}
eventObj.type = type
// If nobody prevented the default action, do it now
if (!onlyHandlers && !eventObj.isDefaultPrevented()) {
if (
(!hook.default ||
hook.default(eventPath.pop()!, eventObj, data) === false) &&
Util.isValidTarget(node)
) {
// Call a native DOM method on the target with the same name as the event.
// Don't do default actions on window, that's where global variables be (#6170)
if (
ontype &&
typeof node[type as 'click'] === 'function' &&
!DomUtil.isWindow(node)
) {
// Don't re-trigger an onFOO event when we call its FOO() method
const tmp = node[ontype]
if (tmp) {
node[ontype] = null
}
// Prevent re-triggering of the same event, since we already bubbled it above
triggered = type
if (eventObj.isPropagationStopped()) {
lastElement.addEventListener(type, Util.stopPropagationCallback)
}
node[type as 'click']()
if (eventObj.isPropagationStopped()) {
lastElement.removeEventListener(type, Util.stopPropagationCallback)
}
triggered = undefined
if (tmp) {
node[ontype] = tmp
}
}
}
}
return eventObj.result
}
}

View File

@ -0,0 +1,163 @@
import { Store } from './event-store'
import { EventObject } from './event-object'
import { EventHandler } from './event-types'
export namespace Hook {
const cache: { [type: string]: Hook } = {}
export function get(type: string) {
return cache[type] || {}
}
export function add(type: string, hook: Hook) {
cache[type] = hook
}
}
export interface Hook {
/**
* Indicates whether this event type should be bubbled when the `.trigger()`
* method is called; by default it is `false`, meaning that a triggered event
* will bubble to the element's parents up to the document (if attached to a
* document) and then to the window. Note that defining `noBubble` on an event
* will effectively prevent that event from being used for delegated events
* with `.trigger()`.
*/
noBubble?: boolean
/**
* When defined, these string properties specify that a special event should
* be handled like another event type until the event is delivered.
*
* The `bindType` is used if the event is attached directly, and the
* `delegateType` is used for delegated events. These types are generally DOM
* event types, and should not be a special event themselves.
*/
bindType?: string
/**
* When defined, these string properties specify that a special event should
* be handled like another event type until the event is delivered.
*
* The `bindType` is used if the event is attached directly, and the
* `delegateType` is used for delegated events. These types are generally DOM
* event types, and should not be a special event themselves.
*/
delegateType?: string
/**
* The setup hook is called the first time an event of a particular type is
* attached to an element; this provides the hook an opportunity to do
* processing that will apply to all events of this type on the element.
*
* The `elem` is the reference to the element where the event is being
* attached and `eventHandle` is the event handler function. In most cases
* the `namespaces` argument should not be used, since it only represents the
* namespaces of the first event being attached; subsequent events may not
* have this same namespaces.
*
* This hook can perform whatever processing it desires, including attaching
* its own event handlers to the element or to other elements and recording
* setup information on the element using the `.data()` method. If the
* setup hook wants me to add a browser event (via `addEventListener` or
* `attachEvent`, depending on browser) it should return `false`. In all
* other cases, me will not add the browser event, but will continue all its
* other bookkeeping for the event. This would be appropriate, for example,
* if the event was never fired by the browser but invoked by `.trigger()`.
* To attach the me event handler in the setup hook, use the `eventHandle`
* argument.
*
*/
setup?: (
elem: Store.EventTarget,
data: any,
namespaces: string[],
eventHandle: EventHandler<Store.EventTarget, any>,
) => any | false
/**
* The teardown hook is called when the final event of a particular type is
* removed from an element. The `elem` is the reference to the element where
* the event is being cleaned up. This hook should return `false` if it wants
* me to remove the event from the browser's event system (via
* `removeEventListener` or `detachEvent`). In most cases, the setup and
* teardown hooks should return the same value.
*
* If the setup hook attached event handlers or added data to an element
* through a mechanism such as `.data()`, the teardown hook should reverse
* the process and remove them. me will generally remove the data and events
* when an element is totally removed from the document, but failing to remove
* data or events on teardown will cause a memory leak if the element stays in
* the document.
*
*/
teardown?: (
elem: Store.EventTarget,
namespaces: string[],
eventHandle: EventHandler<Store.EventTarget, any>,
) => any | false
/**
* Each time an event handler is added to an element through an API such as
* `.on()`, me calls this hook. The `elem` is the element to which the event
* handler is being added, and the `handleObj` argument is as described in the
* section above. The return value of this hook is ignored.
*/
add?: (elem: Store.EventTarget, handleObj: Store.HandlerObject) => void
/**
* When an event handler is removed from an element using an API such as
* `.off()`, this hook is called. The `elem` is the element where the handler
* is being removed, and the `handleObj` argument is as described in the
* section above. The return value of this hook is ignored.
*
*/
remove?: (elem: Store.EventTarget, handleObj: Store.HandlerObject) => void
/**
* The handle hook is called when the event has occurred and me would
* normally call the user's event handler specified by `.on()` or another
* event binding method. If the hook exists, me calls it instead of that
* event handler, passing it the event and any data passed from `.trigger()`
* if it was not a native event. The `elem` argument is the DOM element being
* handled, and `event.handleObj` property has the detailed event information.
*
*/
handle?: (elem: Store.EventTarget, event: EventObject, ...args: any[]) => void
/**
* Called when the `.trigger()` method is used to trigger an event for the
* special type from code, as opposed to events that originate from within
* the browser. The `elem` argument will be the element being triggered, and
* the `event` argument will be a `EventObject` object constructed from the
* caller's input. At minimum, the event type, data, namespace, and target
* properties are set on the event. The data argument represents additional
* data passed by `.trigger()` if present.
*
*/
trigger?: (
elem: Store.EventTarget,
event: EventObject,
data: any,
) => any | false
/**
* When the `.trigger()` method finishes running all the event handlers for
* an event, it also looks for and runs any method on the target object by
* the same name unless of the handlers called `event.preventDefault()`. So,
* `.trigger("submit")` will execute the `submit()` method on the element if
* one exists. When a `default` hook is specified, the hook is called just
* prior to checking for and executing the element's default method. If this
* hook returns the value `false` the element's default method will be called;
* otherwise it is not.
*/
default?: (
elem: Store.EventTarget,
event: EventObject,
data: any,
) => any | false
preDispatch?: (elem: Store.EventTarget, event: EventObject) => void | false
postDispatch?: (elem: Store.EventTarget, event: EventObject) => void
}

View File

@ -0,0 +1,256 @@
import { Util } from './event-util'
import { Store } from './event-store'
import { EventRaw } from './event-alias'
export class EventObject<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any,
TEvent extends Event = Event
> implements EventObject.Event {
isDefaultPrevented: () => boolean
isPropagationStopped: () => boolean = Util.returnFalse
isImmediatePropagationStopped: () => boolean = Util.returnFalse
type: string
originalEvent: TEvent
target: TTarget | null
currentTarget: TCurrentTarget | null
delegateTarget: TDelegateTarget | null
relatedTarget?: EventTarget | null
data: TData
result: any
isTrigger: number
timeStamp: number
handleObj: Store.HandlerObject
namespace?: string
rnamespace?: RegExp | null
isSimulated = false
constructor(e: TEvent | string, props?: Record<string, any> | null) {
if (typeof e === 'string') {
this.type = e
} else if (e.type) {
this.originalEvent = e
this.type = e.type
// Events bubbling up the document may have been marked as prevented
// by a handler lower down the tree; reflect the correct value.
this.isDefaultPrevented = e.defaultPrevented
? Util.returnTrue
: Util.returnFalse
// Create target properties
this.target = (e.target as any) as TTarget
this.currentTarget = (e.currentTarget as any) as TCurrentTarget
this.relatedTarget = ((e as any) as MouseEvent).relatedTarget
this.timeStamp = e.timeStamp
}
// Put explicitly provided properties onto the event object
if (props) {
Object.assign(this, props)
}
// Create a timestamp if incoming event doesn't have one
if (!this.timeStamp) {
this.timeStamp = Date.now()
}
}
preventDefault() {
const e = this.originalEvent
this.isDefaultPrevented = Util.returnTrue
if (e && !this.isSimulated) {
e.preventDefault()
}
}
stopPropagation() {
const e = this.originalEvent
this.isPropagationStopped = Util.returnTrue
if (e && !this.isSimulated) {
e.stopPropagation()
}
}
stopImmediatePropagation() {
const e = this.originalEvent
this.isImmediatePropagationStopped = Util.returnTrue
if (e && !this.isSimulated) {
e.stopImmediatePropagation()
}
this.stopPropagation()
}
}
export interface EventObject extends EventObject.Event {}
export namespace EventObject {
export function create(originalEvent: EventRaw | EventObject | string) {
return originalEvent instanceof EventObject
? originalEvent
: new EventObject(originalEvent)
}
}
export namespace EventObject {
export function addProp(name: string, hook?: any | ((e: EventRaw) => any)) {
Object.defineProperty(EventObject.prototype, name, {
enumerable: true,
configurable: true,
get:
typeof hook === 'function'
? // eslint-disable-next-line
function (this: EventObject) {
if (this.originalEvent) {
return (hook as any)(this.originalEvent)
}
}
: // eslint-disable-next-line
function (this: EventObject) {
if (this.originalEvent) {
return this.originalEvent[name as 'type']
}
},
set(value) {
Object.defineProperty(this, name, {
enumerable: true,
configurable: true,
writable: true,
value,
})
},
})
}
}
export namespace EventObject {
// Common event props including KeyEvent and MouseEvent specific props.
const commonProps = {
bubbles: true,
cancelable: true,
eventPhase: true,
detail: true,
view: true,
button: true,
buttons: true,
clientX: true,
clientY: true,
offsetX: true,
offsetY: true,
pageX: true,
pageY: true,
screenX: true,
screenY: true,
toElement: true,
pointerId: true,
pointerType: true,
char: true,
code: true,
charCode: true,
key: true,
keyCode: true,
touches: true,
changedTouches: true,
targetTouches: true,
which: true,
altKey: true,
ctrlKey: true,
metaKey: true,
shiftKey: true,
}
Object.keys(commonProps).forEach((name: keyof typeof commonProps) =>
EventObject.addProp(name, commonProps[name]),
)
}
export namespace EventObject {
export interface Event {
// Event
bubbles: boolean | undefined
cancelable: boolean | undefined
eventPhase: number | undefined
// UIEvent
detail: number | undefined
view: Window | undefined
// MouseEvent
button: number | undefined
buttons: number | undefined
clientX: number | undefined
clientY: number | undefined
offsetX: number | undefined
offsetY: number | undefined
pageX: number | undefined
pageY: number | undefined
screenX: number | undefined
screenY: number | undefined
/** @deprecated */
toElement: Element | undefined
// PointerEvent
pointerId: number | undefined
pointerType: string | undefined
// KeyboardEvent
/** @deprecated */
char: string | undefined
/** @deprecated */
charCode: number | undefined
key: string | undefined
/** @deprecated */
keyCode: number | undefined
// TouchEvent
touches: TouchList | undefined
targetTouches: TouchList | undefined
changedTouches: TouchList | undefined
// MouseEvent, KeyboardEvent
which: number | undefined
// MouseEvent, KeyboardEvent, TouchEvent
altKey: boolean | undefined
ctrlKey: boolean | undefined
metaKey: boolean | undefined
shiftKey: boolean | undefined
type: string
timeStamp: number
isDefaultPrevented(): boolean
isImmediatePropagationStopped(): boolean
isPropagationStopped(): boolean
preventDefault(): void
stopImmediatePropagation(): void
stopPropagation(): void
}
}

View File

@ -0,0 +1,260 @@
import { Util } from './event-util'
import { Hook } from './event-hook'
import { Core } from './event-core'
import { Store } from './event-store'
import { DomUtil } from '../../util/dom'
export namespace Special {
// Prevent triggered image.load events from bubbling to window.load
Hook.add('load', {
noBubble: true,
})
Hook.add('beforeunload', {
postDispatch(elem, event) {
// Support: Chrome <=73+
// Chrome doesn't alert on `event.preventDefault()`
// as the standard mandates.
if (event.result !== undefined && event.originalEvent) {
event.originalEvent.returnValue = event.result
}
},
})
}
export namespace Special {
const events = {
mouseenter: 'mouseover',
mouseleave: 'mouseout',
pointerenter: 'pointerover',
pointerleave: 'pointerout',
}
Object.keys(events).forEach((type: keyof typeof events) => {
const delegateType = events[type]
Hook.add(type, {
delegateType,
bindType: delegateType,
handle(target, event, ...args) {
let ret
const related = event.relatedTarget
const handleObj = event.handleObj
// For mouseenter/leave call the handler if related is outside the target.
// NB: No relatedTarget if the mouse left/entered the browser window
if (
!related ||
(related !== target &&
!DomUtil.contains(target as Element, related as Element))
) {
event.type = handleObj.originType
ret = handleObj.handler.call(this, event, ...args)
event.type = delegateType
}
return ret
},
})
})
}
export namespace Special {
namespace State {
type Item = boolean | any[] | { value: any }
const cache: WeakMap<
Store.EventTarget,
Record<string, Item>
> = new WeakMap()
export function has(elem: Store.EventTarget, type: string) {
return cache.has(elem) && cache.get(elem)![type] != null
}
export function get(elem: Store.EventTarget, type: string) {
const item = cache.get(elem)
if (item) {
return item[type]
}
return null
}
export function set(elem: Store.EventTarget, type: string, val: Item) {
if (!cache.has(elem)) {
cache.set(elem, {})
}
const bag = cache.get(elem)!
bag[type] = val
}
}
// eslint-disable-next-line no-inner-declarations
function leverageNative(
elem: Store.EventTarget,
type: string,
isSync?: (taget: Store.EventTarget, t: string) => boolean,
) {
if (!isSync) {
if (!State.has(elem, type)) {
Core.add(elem, type, Util.returnTrue)
}
return
}
// Register the controller as a special universal handler for all event namespaces
State.set(elem, type, false)
Core.add(elem, type, {
namespace: false,
handler(event, ...args) {
const node = this as HTMLElement
const nativeHandle = node[type as 'click']
let saved = State.get(this, type)!
// eslint-disable-next-line no-bitwise
if (event.isTrigger & 1 && nativeHandle) {
// Interrupt processing of the outer synthetic .trigger()ed event
// Saved data should be false in such cases, but might be a leftover
// capture object from an async native handler
if (!Array.isArray(saved)) {
// Store arguments for use when handling the inner native event
// There will always be at least one argument (an event object),
// so this array will not be confused with a leftover capture object.
saved = [event, ...args]
State.set(node, type, saved)
// Trigger the native event and capture its result
// Support: IE <=9 - 11+
// focus() and blur() are asynchronous
const notAsync = isSync(this, type)
nativeHandle()
let result = State.get(this, type)
if (saved !== result || notAsync) {
State.set(this, type, false)
} else {
result = { value: undefined }
}
if (saved !== result) {
// Cancel the outer synthetic event
event.stopImmediatePropagation()
event.preventDefault()
// Support: Chrome 86+
// In Chrome, if an element having a focusout handler is blurred
// by clicking outside of it, it invokes the handler synchronously.
// If that handler calls `.remove()` on the element, the data is
// cleared, leaving `result` undefined. We need to guard against
// this.
return result && (result as any).value
}
// If this is an inner synthetic event for an event with a bubbling
// surrogate (focus or blur), assume that the surrogate already
// propagated from triggering the native event and prevent that
// from happening again here. This technically gets the ordering
// wrong w.r.t. to `.trigger()` (in which the bubbling surrogate
// propagates *after* the non-bubbling base), but that seems less
// bad than duplication.
} else if (Hook.get(type).delegateType) {
event.stopPropagation()
}
// If this is a native event triggered above, everything is now in order
// Fire an inner synthetic event with the original arguments
} else if (saved && Array.isArray(saved)) {
// ...and capture the result
State.set(this, type, {
value: Core.trigger(
// Support: IE <=9 - 11+
// Extend with the prototype to reset the above stopImmediatePropagation()
jQuery.extend(saved[0], jQuery.Event.prototype),
saved.slice(1),
this,
),
})
// Abort handling of the native event
event.stopImmediatePropagation()
}
return undefined
},
})
}
// Utilize native event to ensure correct state for checkable inputs
Hook.add('click', {
setup(elem) {
if (Util.isCheckableInput(elem)) {
leverageNative(elem, 'click', Util.returnTrue)
}
// Return false to allow normal processing in the caller
return false
},
trigger(elem) {
// Force setup before triggering a click
if (Util.isCheckableInput(elem)) {
leverageNative(elem, 'click')
}
// Return non-false to allow normal event-path propagation
return true
},
// For cross-browser consistency, suppress native .click() on links
// Also prevent it if we're currently inside a leveraged native-event stack
default(elem, event) {
const target = event.target
return (
(Util.isCheckableInput(elem) && State.get(target, 'click')) ||
DomUtil.isNodeName(target, 'a')
)
},
})
// focus/blur
// ----------
// Support: IE <=9 - 11+
// focus() and blur() are asynchronous, except when they are no-op.
// So expect focus to be synchronous when the element is already active,
// and blur to be synchronous when the element is not already active.
// (focus and blur are always synchronous in other supported browsers,
// this just defines when we can count on it).
// eslint-disable-next-line no-inner-declarations
function expectSync(elem: Element, type: string) {
return (elem === document.activeElement) === (type === 'focus')
}
const events = { focus: 'focusin', blur: 'focusout' }
Object.keys(events).forEach((type: keyof typeof events) => {
const delegateType = events[type]
// Utilize native event if possible so blur/focus sequence is correct
Hook.add(type, {
delegateType,
setup(elem) {
// Claim the first handler
// cache.set( elem, "focus", ... )
// cache.set( elem, "blur", ... )
leverageNative(elem, type, expectSync)
// Return false to allow normal processing in the caller
return false
},
trigger(elem) {
// Force setup before trigger
leverageNative(elem, type)
// Return non-false to allow normal event-path propagation
return true
},
// Suppress native focus or blur as it's already being fired
// in leverageNative.
default() {
return true
},
})
})
}

View File

@ -0,0 +1,43 @@
import { EventHandler } from './event-types'
export namespace Store {
export type EventTarget = Element | Record<string, unknown>
export interface HandlerObject {
type: string
originType: string
data?: any
handler: EventHandler<any, any>
guid: number
selector?: string
namespace: string | false
needsContext: boolean
}
export interface Data {
handler?: EventHandler<any, any>
events: {
[type: string]: {
handlers: HandlerObject[]
delegateCount: number
}
}
}
const cache: WeakMap<EventTarget, Data> = new WeakMap()
export function ensure(target: EventTarget) {
if (!cache.has(target)) {
cache.set(target, { events: Object.create(null) })
}
return cache.get(target)!
}
export function get(target: EventTarget) {
return cache.get(target)
}
export function remove(target: EventTarget) {
return cache.delete(target)
}
}

View File

@ -0,0 +1,735 @@
import { EventObject } from './event-object'
import {
EventRaw,
UIEventRaw,
DragEventRaw,
FocusEventRaw,
MouseEventRaw,
TouchEventRaw,
KeyboardEventRaw,
} from './event-alias'
interface EventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any,
TEvent extends EventRaw = any
> extends EventObject<TDelegateTarget, TData, TCurrentTarget, TTarget, TEvent> {
relatedTarget?: undefined
bubbles: boolean
cancelable: boolean
eventPhase: number
detail: undefined
view: undefined
button: undefined
buttons: undefined
clientX: undefined
clientY: undefined
offsetX: undefined
offsetY: undefined
pageX: undefined
pageY: undefined
screenX: undefined
screenY: undefined
/** @deprecated */
toElement: undefined
pointerId: undefined
pointerType: undefined
/** @deprecated */
char: undefined
/** @deprecated */
charCode: undefined
key: undefined
/** @deprecated */
keyCode: undefined
changedTouches: undefined
targetTouches: undefined
touches: undefined
which: undefined
altKey: undefined
ctrlKey: undefined
metaKey: undefined
shiftKey: undefined
}
interface ChangeEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends EventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'change'
}
interface ResizeEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends EventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'resize'
}
interface ScrollEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends EventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'scroll'
}
interface SelectEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends EventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'select'
}
interface SubmitEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends EventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'submit'
}
interface UIEventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any,
TEvent extends UIEventRaw = any
> extends EventObject<TDelegateTarget, TData, TCurrentTarget, TTarget, TEvent> {
bubbles: boolean
cancelable: boolean
eventPhase: number
detail: number
view: Window
}
interface MouseEventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends UIEventBase<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget,
MouseEventRaw
> {
relatedTarget?: EventTarget | null
button: number
buttons: number
clientX: number
clientY: number
offsetX: number
offsetY: number
pageX: number
pageY: number
screenX: number
screenY: number
/** @deprecated */
toElement: Element
pointerId: undefined
pointerType: undefined
/** @deprecated */
char: undefined
/** @deprecated */
charCode: undefined
key: undefined
/** @deprecated */
keyCode: undefined
changedTouches: undefined
targetTouches: undefined
touches: undefined
which: number
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
}
interface ClickEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
relatedTarget?: null
type: 'click'
}
interface ContextMenuEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
relatedTarget?: null
type: 'contextmenu'
}
interface DoubleClickEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
relatedTarget?: null
type: 'dblclick'
}
interface MouseDownEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
relatedTarget?: null
type: 'mousedown'
}
interface MouseEnterEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'mouseenter'
}
interface MouseLeaveEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'mouseleave'
}
interface MouseMoveEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
relatedTarget?: null
type: 'mousemove'
}
interface MouseOutEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'mouseout'
}
interface MouseOverEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'mouseover'
}
interface MouseUpEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends MouseEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
relatedTarget?: null
type: 'mouseup'
}
interface DragEventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends UIEventBase<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget,
DragEventRaw
> {}
interface DragEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'drag'
}
interface DragEndEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'dragend'
}
interface DragEnterEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'dragenter'
}
interface DragExitEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'dragexit'
}
interface DragLeaveEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'dragleave'
}
interface DragOverEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'dragover'
}
interface DragStartEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'dragstart'
}
interface DropEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends DragEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'drop'
}
interface KeyboardEventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends UIEventBase<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget,
KeyboardEventRaw
> {
relatedTarget?: undefined
button: undefined
buttons: undefined
clientX: undefined
clientY: undefined
offsetX: undefined
offsetY: undefined
pageX: undefined
pageY: undefined
screenX: undefined
screenY: undefined
/** @deprecated */
toElement: undefined
pointerId: undefined
pointerType: undefined
/** @deprecated */
char: string | undefined
/** @deprecated */
charCode: number
code: string
key: string
/** @deprecated */
keyCode: number
changedTouches: undefined
targetTouches: undefined
touches: undefined
which: number
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
}
interface KeyDownEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends KeyboardEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'keydown'
}
interface KeyPressEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends KeyboardEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'keypress'
}
interface KeyUpEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends KeyboardEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'keyup'
}
interface TouchEventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends UIEventBase<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget,
TouchEventRaw
> {
relatedTarget?: undefined
button: undefined
buttons: undefined
clientX: undefined
clientY: undefined
offsetX: undefined
offsetY: undefined
pageY: undefined
screenX: undefined
screenY: undefined
/** @deprecated */
toElement: undefined
pointerId: undefined
pointerType: undefined
/** @deprecated */
char: undefined
/** @deprecated */
charCode: undefined
key: undefined
/** @deprecated */
keyCode: undefined
changedTouches: TouchList
targetTouches: TouchList
touches: TouchList
which: undefined
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
}
interface TouchCancelEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends TouchEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'touchcancel'
}
interface TouchEndEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends TouchEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'touchend'
}
interface TouchMoveEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends TouchEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'touchmove'
}
interface TouchStartEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends TouchEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'touchstart'
}
interface FocusEventBase<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends UIEventBase<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget,
FocusEventRaw
> {
relatedTarget?: EventTarget | null
button: undefined
buttons: undefined
clientX: undefined
clientY: undefined
offsetX: undefined
offsetY: undefined
pageX: undefined
pageY: undefined
screenX: undefined
screenY: undefined
/** @deprecated */
toElement: undefined
pointerId: undefined
pointerType: undefined
/** @deprecated */
char: undefined
/** @deprecated */
charCode: undefined
key: undefined
/** @deprecated */
keyCode: undefined
changedTouches: undefined
targetTouches: undefined
touches: undefined
which: undefined
altKey: undefined
ctrlKey: undefined
metaKey: undefined
shiftKey: undefined
}
interface BlurEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends FocusEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'blur'
}
interface FocusEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends FocusEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'focus'
}
interface FocusInEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends FocusEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'focusin'
}
interface FocusOutEvent<
TDelegateTarget = any,
TData = any,
TCurrentTarget = any,
TTarget = any
> extends FocusEventBase<TDelegateTarget, TData, TCurrentTarget, TTarget> {
type: 'focusout'
}
interface TypeToTriggeredEventMap<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget
> {
// Event
change: ChangeEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
resize: ResizeEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
scroll: ScrollEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
select: SelectEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
submit: SubmitEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
// UIEvent
// MouseEvent
click: ClickEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
contextmenu: ContextMenuEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dblclick: DoubleClickEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mousedown: MouseDownEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mouseenter: MouseEnterEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mouseleave: MouseLeaveEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mousemove: MouseMoveEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mouseout: MouseOutEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mouseover: MouseOverEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
mouseup: MouseUpEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
// DragEvent
drag: DragEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dragend: DragEndEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dragenter: DragEnterEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dragexit: DragExitEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dragleave: DragLeaveEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dragover: DragOverEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
dragstart: DragStartEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
drop: DropEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
// KeyboardEvent
keydown: KeyDownEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
keypress: KeyPressEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
keyup: KeyUpEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
// TouchEvent
touchcancel: TouchCancelEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
touchend: TouchEndEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
touchmove: TouchMoveEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
touchstart: TouchStartEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
// FocusEvent
blur: BlurEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
focus: FocusEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
focusin: FocusInEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
focusout: FocusOutEvent<TDelegateTarget, TData, TCurrentTarget, TTarget>
[type: string]: EventObject<TDelegateTarget, TData, TCurrentTarget, TTarget>
}
export type EventHandlerBase<TContext, T> = (
this: TContext,
e: T,
...args: any[]
) => any
export type EventHandler<TCurrentTarget, TData = undefined> = EventHandlerBase<
TCurrentTarget,
EventObject<TCurrentTarget, TData>
>
export type TypeEventHandler<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget,
TType extends keyof TypeToTriggeredEventMap<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget
>
> = EventHandlerBase<
TCurrentTarget,
TypeToTriggeredEventMap<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget
>[TType]
>
export interface TypeEventHandlers<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget
> extends TypeEventHandlersBase<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget
> {
// No idea why it's necessary to include `object` in the union but otherwise TypeScript complains that
// derived types of Event are not assignable to Event.
[type: string]:
| TypeEventHandler<TDelegateTarget, TData, TCurrentTarget, TTarget, string>
| false
| undefined
| Record<string, unknown>
}
type TypeEventHandlersBase<TDelegateTarget, TData, TCurrentTarget, TTarget> = {
[TType in keyof TypeToTriggeredEventMap<
TDelegateTarget,
TData,
TCurrentTarget,
TTarget
>]?:
| TypeEventHandler<TDelegateTarget, TData, TCurrentTarget, TTarget, TType>
| false
| Record<string, unknown>
}

View File

@ -0,0 +1,165 @@
import { DomUtil } from '../../util/dom'
import { Store } from './event-store'
import type { EventObject } from './event-object'
export namespace Util {
export const returnTrue = () => true
export const returnFalse = () => false
export function stopPropagationCallback(e: Event) {
e.stopPropagation()
}
}
export namespace Util {
const rnothtmlwhite = /[^\x20\t\r\n\f]+/g
const rtypenamespace = /^([^.]*)(?:\.(.+)|)/
const rcheckableInput = /^(?:checkbox|radio)$/i
const whitespace = '[\\x20\\t\\r\\n\\f]'
const rneedsContext = new RegExp(
`^${whitespace}*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(${whitespace}*((?:-\\d)?\\d*)${whitespace}*\\)|)(?=[^-]|$)`,
'i',
)
export function splitType(types: string) {
return (types || '').match(rnothtmlwhite) || ['']
}
export function normalizeType(type: string) {
const parts = rtypenamespace.exec(type) || []
return {
originType: parts[1],
namespaces: parts[2] ? parts[2].split('.').sort() : [],
}
}
export function isCheckableInput(elem: Store.EventTarget) {
const node = elem as HTMLInputElement
return (
node.click != null &&
DomUtil.isNodeName(node, 'input') &&
rcheckableInput.test(node.type)
)
}
export function isValidTarget(target: Element | Record<string, any>) {
// Accepts only:
// - Node
// - Node.ELEMENT_NODE
// - Node.DOCUMENT_NODE
// - Object
// - Any
return target.nodeType === 1 || target.nodeType === 9 || !+target.nodeType
}
export function isValidSelector(elem: Store.EventTarget, selector?: string) {
if (selector) {
const doce = document.documentElement
const matches = doce.matches || (doce as any).msMatchesSelector
return matches.call(elem, selector) as boolean
}
return true
}
export function needsContext(selector?: string) {
return selector != null && rneedsContext.test(selector)
}
}
export namespace Util {
type Handler = (...args: any[]) => void
let seed = 0
const cache: WeakMap<Handler, number> = new WeakMap()
export function ensureHandlerId(handler: Handler) {
if (!cache.has(handler)) {
cache.set(handler, seed)
seed += 1
}
return cache.get(handler)!
}
export function getHandlerId(handler: Handler) {
return cache.get(handler)
}
export function removeHandlerId(handler: Handler) {
return cache.delete(handler)
}
export function setHandlerId(handler: Handler, id: number) {
return cache.set(handler, id)
}
}
export namespace Util {
export function getHandlerQueue(elem: Store.EventTarget, event: EventObject) {
const queue = []
const store = Store.get(elem)
const bag = store && store.events && store.events[event.type]
const handlers = (bag && bag.handlers) || []
const delegateCount = bag ? bag.delegateCount : 0
if (
delegateCount > 0 &&
// Support: Firefox <=42 - 66+
// Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861)
// https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click
// Support: IE 11+
// ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343)
!(
event.type === 'click' &&
typeof event.button === 'number' &&
event.button >= 1
)
) {
for (
let curr = event.target as Node;
curr !== elem;
curr = curr.parentNode || (elem as Node)
) {
// Don't check non-elements
// Don't process clicks on disabled elements
if (
curr.nodeType === 1 &&
!(event.type === 'click' && (curr as any).disabled === true)
) {
const matchedHandlers: Store.HandlerObject[] = []
const matchedSelectors: { [selector: string]: boolean } = {}
for (let i = 0; i < delegateCount; i += 1) {
const handleObj = handlers[i]
const selector = handleObj.selector!
if (selector != null && matchedSelectors[selector] == null) {
const node = elem as Element
const nodes: Element[] = []
node.querySelectorAll(selector).forEach((child) => {
nodes.push(child)
})
matchedSelectors[selector] = nodes.includes(curr as Element)
}
if (matchedSelectors[selector]) {
matchedHandlers.push(handleObj)
}
}
if (matchedHandlers.length) {
queue.push({ elem: curr, handlers: matchedHandlers })
}
}
}
}
// Add the remaining (directly-bound) handlers
if (delegateCount < handlers.length) {
queue.push({ elem, handlers: handlers.slice(delegateCount) })
}
return queue
}
}

View File

@ -0,0 +1,263 @@
/* eslint-disable no-param-reassign */
import { Primer } from './primer'
import { Core } from './event-core'
import { Util } from './event-util'
import { Hook } from './event-hook'
import { EventRaw } from './event-alias'
import { EventObject } from './event-object'
import { TypeEventHandler, TypeEventHandlers } from './event-types'
export class Event<TElement extends Node> extends Primer<TElement> {
on<TType extends string>(
events: TType,
selector: string,
handler: TypeEventHandler<TElement, undefined, any, any, TType> | false,
): this
on<TType extends string, TData>(
events: TType,
selector: string | null | undefined,
data: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, TType>
| false,
): this
on<TType extends string, TData>(
events: TType,
data: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, TType>
| false,
): this
on<TType extends string>(
events: TType,
handler:
| TypeEventHandler<TElement, undefined, TElement, TElement, TType>
| false,
): this
on<TData>(
events: TypeEventHandlers<TElement, TData, any, any>,
selector: string | null | undefined,
data: TData,
): this
on(
events: TypeEventHandlers<TElement, undefined, any, any>,
selector: string,
): this
on<TData>(
events: TypeEventHandlers<TElement, TData, TElement, TElement>,
data: TData,
): this
on(events: TypeEventHandlers<TElement, undefined, TElement, TElement>): this
on(events: any, selector?: any, data?: any, handler?: any) {
Event.on(this.node as any, events, selector, data, handler)
return this
}
once<TType extends string>(
events: TType,
selector: string,
handler: TypeEventHandler<TElement, undefined, any, any, TType> | false,
): this
once<TType extends string, TData>(
events: TType,
selector: string | null | undefined,
data: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, TType>
| false,
): this
once<TType extends string, TData>(
events: TType,
data: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, TType>
| false,
): this
once<TType extends string>(
events: TType,
handler:
| TypeEventHandler<TElement, undefined, TElement, TElement, TType>
| false,
): this
once<TData>(
events: TypeEventHandlers<TElement, TData, any, any>,
selector: string | null | undefined,
data: TData,
): this
once(
events: TypeEventHandlers<TElement, undefined, any, any>,
selector: string,
): this
once<TData>(
events: TypeEventHandlers<TElement, TData, TElement, TElement>,
data: TData,
): this
once(events: TypeEventHandlers<TElement, undefined, TElement, TElement>): this
once(events: any, selector?: any, data?: any, handler?: any) {
Event.on(this.node as any, events, selector, data, handler, true)
return this
}
off<TType extends string>(
events: TType,
selector: string,
handler: TypeEventHandler<TElement, any, any, any, TType> | false,
): this
off<TType extends string>(
events: TType,
handler: TypeEventHandler<TElement, any, any, any, TType> | false,
): this
off<TType extends string>(
events: TType,
selector_handler?:
| string
| TypeEventHandler<TElement, any, any, any, TType>
| false,
): this
off(
events: TypeEventHandlers<TElement, any, any, any>,
selector?: string,
): this
off(event?: EventObject<TElement>): this
off<TType extends string>(
events?:
| TType
| TypeEventHandlers<TElement, any, any, any>
| EventObject<TElement>,
selector?:
| string
| TypeEventHandler<TElement, any, any, any, TType>
| false,
handler?: TypeEventHandler<TElement, any, any, any, TType> | false,
) {
Event.off(this.node, events, selector, handler)
return this
}
trigger(
event: string | EventObject | EventRaw | EventObject.Event,
data?: any[] | Record<string, any> | string | number | boolean,
onlyHandlers?: boolean,
) {
Core.trigger(event, data, this.node as any, onlyHandlers)
return this
}
}
export namespace Event {
type EventHandler = false | ((...args: any[]) => any)
export function on(
elem: Element,
types: any,
selector?: string | EventHandler | null,
data?: any | EventHandler | null,
fn?: EventHandler | null,
once?: boolean,
) {
// Types can be a map of types/handlers
if (typeof types === 'object') {
// ( types-Object, selector, data )
if (typeof selector !== 'string') {
// ( types-Object, data )
data = data || selector
selector = undefined
}
Object.keys(types).forEach((type) =>
on(elem, type, selector, data, types[type], once),
)
return
}
if (data == null && fn == null) {
// ( types, fn )
fn = selector as EventHandler
data = selector = undefined
} else if (fn == null) {
if (typeof selector === 'string') {
// ( types, selector, fn )
fn = data
data = undefined
} else {
// ( types, data, fn )
fn = data
data = selector
selector = undefined
}
}
if (fn === false) {
fn = Util.returnFalse
} else if (!fn) {
return
}
if (once) {
const originHandler = fn
fn = function (event, ...args: any[]) {
// Can use an empty set, since event contains the info
Event.off(event)
return originHandler.call(this, event, ...args)
}
// Use same guid so caller can remove using origFn
Util.setHandlerId(fn, Util.ensureHandlerId(originHandler))
}
Core.add(elem, types as string, fn, data, selector as string)
}
export function off<TType extends string, TElement>(
elem: TElement,
events?:
| TType
| TypeEventHandlers<TElement, any, any, any>
| EventObject<TElement>,
selector?:
| string
| TypeEventHandler<TElement, any, any, any, TType>
| false,
fn?: TypeEventHandler<TElement, any, any, any, TType> | false,
) {
const evt = events as EventObject
if (evt && evt.preventDefault != null && evt.handleObj != null) {
const obj = evt.handleObj
off(
evt.delegateTarget,
obj.namespace ? `${obj.originType}.${obj.namespace}` : obj.originType,
obj.selector,
obj.handler,
)
return
}
if (typeof events === 'object') {
// ( types-object [, selector] )
const types = events as TypeEventHandlers<TElement, any, any, any>
Object.keys(types).forEach((type) =>
off(elem, type, selector, types[type] as any),
)
return
}
if (selector === false || typeof selector === 'function') {
// ( types [, fn] )
fn = selector
selector = undefined
}
if (fn === false) {
fn = Util.returnFalse
}
Core.remove(elem as any, events as string, fn, selector)
}
}
export namespace Event {
export const registerHook = Hook.add
}

View File

@ -0,0 +1 @@
export { Dom } from './dom'

View File

@ -0,0 +1,432 @@
import { Event } from './event'
import { TypeEventHandler } from './event-types'
export class Listener<TElement extends Node> extends Event<TElement> {}
export interface Listener<TElement extends Node>
extends Listener.Methods<TElement> {}
export namespace Listener {
const events = [
'blur',
'focus',
'focusin',
'focusout',
'resize',
'scroll',
'click',
'dblclick',
'mousedown',
'mouseup',
'mousemove',
'mouseover',
'mouseout',
'mouseenter',
'mouseleave',
'change',
'select',
'submit',
'keydown',
'keypress',
'keyup',
'contextmenu',
'touchstart',
'touchmove',
'touchleave',
'touchend',
'touchcancel',
] as const
events.forEach((event) => {
Listener.prototype[event] = function <TData>(
this: Listener<Node>,
eventData?: TData | TypeEventHandler<any, any, any, any, any> | false,
handler?: TypeEventHandler<any, any, any, any, any> | false,
) {
if (eventData == null) {
this.trigger(event)
} else {
this.on(event, null, eventData, handler!)
}
return this
}
})
const methods = events.map(
(event) =>
`
${event}(): this
${event}(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, '${event}'>
| false,
): this
${event}<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, '${event}'>
| false,
): this
`,
)
// generate interface
// eslint-disable-next-line no-constant-condition
if (false) {
// eslint-disable-next-line no-console
console.log(methods.join('\n'))
}
export interface Methods<TElement extends Node> {
blur(): this
blur(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'blur'>
| false,
): this
blur<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'blur'>
| false,
): this
focus(): this
focus(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'focus'>
| false,
): this
focus<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'focus'>
| false,
): this
focusin(): this
focusin(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'focusin'>
| false,
): this
focusin<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'focusin'>
| false,
): this
focusout(): this
focusout(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'focusout'>
| false,
): this
focusout<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'focusout'>
| false,
): this
resize(): this
resize(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'resize'>
| false,
): this
resize<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'resize'>
| false,
): this
scroll(): this
scroll(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'scroll'>
| false,
): this
scroll<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'scroll'>
| false,
): this
click(): this
click(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'click'>
| false,
): this
click<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'click'>
| false,
): this
dblclick(): this
dblclick(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'dblclick'>
| false,
): this
dblclick<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'dblclick'>
| false,
): this
mousedown(): this
mousedown(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mousedown'>
| false,
): this
mousedown<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mousedown'>
| false,
): this
mouseup(): this
mouseup(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mouseup'>
| false,
): this
mouseup<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mouseup'>
| false,
): this
mousemove(): this
mousemove(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mousemove'>
| false,
): this
mousemove<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mousemove'>
| false,
): this
mouseover(): this
mouseover(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mouseover'>
| false,
): this
mouseover<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mouseover'>
| false,
): this
mouseout(): this
mouseout(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mouseout'>
| false,
): this
mouseout<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mouseout'>
| false,
): this
mouseenter(): this
mouseenter(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mouseenter'>
| false,
): this
mouseenter<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mouseenter'>
| false,
): this
mouseleave(): this
mouseleave(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'mouseleave'>
| false,
): this
mouseleave<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'mouseleave'>
| false,
): this
change(): this
change(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'change'>
| false,
): this
change<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'change'>
| false,
): this
select(): this
select(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'select'>
| false,
): this
select<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'select'>
| false,
): this
submit(): this
submit(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'submit'>
| false,
): this
submit<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'submit'>
| false,
): this
keydown(): this
keydown(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'keydown'>
| false,
): this
keydown<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'keydown'>
| false,
): this
keypress(): this
keypress(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'keypress'>
| false,
): this
keypress<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'keypress'>
| false,
): this
keyup(): this
keyup(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'keyup'>
| false,
): this
keyup<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'keyup'>
| false,
): this
contextmenu(): this
contextmenu(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'contextmenu'>
| false,
): this
contextmenu<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'contextmenu'>
| false,
): this
touchstart(): this
touchstart(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'touchstart'>
| false,
): this
touchstart<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'touchstart'>
| false,
): this
touchmove(): this
touchmove(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'touchmove'>
| false,
): this
touchmove<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'touchmove'>
| false,
): this
touchleave(): this
touchleave(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'touchleave'>
| false,
): this
touchleave<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'touchleave'>
| false,
): this
touchend(): this
touchend(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'touchend'>
| false,
): this
touchend<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'touchend'>
| false,
): this
touchcancel(): this
touchcancel(
handler:
| TypeEventHandler<TElement, null, TElement, TElement, 'touchcancel'>
| false,
): this
touchcancel<TData>(
eventData: TData,
handler:
| TypeEventHandler<TElement, TData, TElement, TElement, 'touchcancel'>
| false,
): this
}
}

View File

@ -0,0 +1,38 @@
import { Primer } from './primer'
export class Memory<TNode extends Node> extends Primer<TNode> {
private memo: Record<string, any>
remember(obj: Record<string, any>): this
remember<T>(key: string): T
remember<T>(key: string, v: T): this
remember<T>(k: Record<string, any> | string, v?: T) {
if (typeof k === 'object') {
Object.keys(k).forEach((key) => this.remember(key, k[key]))
return this
}
if (arguments.length === 1) {
return this.memory()[k]
}
this.memory()[k] = v
return this
}
forget(...keys: string[]) {
if (keys.length === 0) {
this.memo = {}
} else {
keys.forEach((key) => delete this.memory()[key])
}
return this
}
memory() {
if (this.memo == null) {
this.memo = {}
}
return this.memo
}
}

View File

@ -0,0 +1,152 @@
import { Attrs, Class } from '../../types'
import { Color } from '../../struct/color'
import { SVGArray } from '../../struct/svg-array'
import { DomUtil } from '../../util/dom'
import { Attr } from '../../util/attr'
import { Registry } from '../registry'
import { Base } from '../base'
export abstract class Primer<TNode extends Node> extends Base {
public readonly node: TNode
public get type() {
return this.node.nodeName
}
constructor()
constructor(node: Node)
constructor(attrs: Attrs | null)
constructor(node?: TNode | string | null, attrs?: Attrs | null)
constructor(node?: TNode | string | Attrs | null, attrs?: Attrs | null)
constructor(node?: TNode | string | Attrs | null, attrs?: Attrs | null) {
super()
if (DomUtil.isNode(node)) {
this.node = node
if (attrs) {
this.attr(attrs)
}
} else {
const ctor = this.constructor as Class
const name = typeof node === 'string' ? node : Registry.getTagName(ctor)
if (name) {
this.node = DomUtil.createNode<any>(name)
const ats = node != null && typeof node !== 'string' ? node : attrs
if (ats) {
this.attr(ats)
}
} else {
throw new Error(
`Can not initialize "${ctor.name}" with unknown node name`,
)
}
}
}
// #region Attributes
attr(): Attrs
attr(names: string[]): Attrs
attr<T extends string | number = string>(name: string): T
attr(name: string, value: null): this
attr(name: string, value: string | number, ns?: string): this
attr(attrs: Attrs): this
attr<T extends string | number>(
name?: string,
value?: string | number | null,
ns?: string,
): T | this
attr(
attr?: string | string[] | Attrs,
value?: string | number | null,
ns?: string,
): Attrs | string | number | this
attr(
attr?: string | string[] | Attrs,
val?: string | number | null,
ns?: string,
) {
const node = DomUtil.toElement(this.node)
// get all attributes
if (attr == null) {
const result: Attrs = {}
const attrs = node.attributes
if (attrs) {
for (let index = 0, l = attrs.length; index < l; index += 1) {
const item = attrs.item(index)
if (item && item.nodeValue) {
result[item.nodeName] = Attr.parseValue(item.nodeValue)
}
}
}
return result
}
// get attributes by specified attribute names
if (Array.isArray(attr)) {
return attr.reduce<Attrs>((memo, name) => {
memo[name] = this.attr(name)
return memo
}, {})
}
if (typeof attr === 'object') {
Object.keys(attr).forEach((key) => this.attr(key, attr[key]))
return this
}
if (val === null) {
node.removeAttribute(attr)
return this
}
if (val == null) {
const raw = node.getAttribute(attr)
return raw == null
? Attr.defaults[attr as keyof typeof Attr.defaults]
: Attr.parseValue(raw)
}
const value = this.applyAttrHooks(attr, val)
typeof ns === 'string'
? node.setAttributeNS(ns, attr, value.toString())
: node.setAttribute(attr, value.toString())
return this
}
protected applyAttrHooks(attr: string, val: string | number) {
const value = Attr.applyHooks(attr, val, this as any)
if (typeof value === 'number') {
return value
}
if (Color.isColor(value)) {
return new Color(value)
}
if (Array.isArray(value)) {
return new SVGArray(value)
}
return value
}
round(precision = 2, names?: string[]) {
const factor = 10 ** precision
const attrs = names ? this.attr(names) : this.attr()
Object.keys(attrs).forEach((key) => {
const value = attrs[key]
if (typeof value === 'number') {
attrs[key] = Math.round(value * factor) / factor
}
})
this.attr(attrs)
return this
}
// #endregion
}

View File

@ -0,0 +1,24 @@
export interface Hook {
get<TElement extends Element>(
elem: TElement,
computed?: boolean,
extra?: boolean | string,
): string | number
set<TElement extends Element>(
elem: TElement,
value: string | number,
extra?: boolean | string,
): string | undefined
}
export namespace Hook {
const hooks: Record<string, Hook> = {}
export function get(name: string) {
return hooks[name]
}
export function add(name: string, hook: Hook) {
hooks[name] = hook
}
}

View File

@ -0,0 +1,402 @@
import { Hook } from './style-hook'
import { DomUtil } from '../../util/dom'
import { Str } from '../../util/str'
export namespace Util {
export type CSSKey = keyof Pick<CSSStyleDeclaration, 'left'>
export type CSSKeys = Exclude<keyof CSSStyleDeclaration, number>
}
export namespace Util {
export function getComputedStyleByName<TElement extends Element>(
elem: TElement,
name: string,
styles:
| Record<string, string>
| CSSStyleDeclaration = DomUtil.getComputedStyle(elem),
) {
let ret = styles.getPropertyValue
? (styles as CSSStyleDeclaration).getPropertyValue(name)
: (styles as Record<string, string>)[name]
if (ret === '' && !DomUtil.isAttached(elem)) {
ret = style(elem, name)
}
return ret !== undefined ? `${ret}` : ret
}
// Convert dashed to camelCase, handle vendor prefixes.
// Used by the css & effects modules.
// Support: IE <=9 - 11+
// Microsoft forgot to hump their vendor prefix
export function cssCamelCase(str: string) {
return Str.camelCase(str.replace(/^-ms-/, 'ms-')) as CSSKey
}
export const normalizeValue = (val?: string | null) =>
val == null || /^(\s+)?$/.test(val) ? '' : val
export function isCustomName(name: string) {
return /^--/.test(name)
}
const cssPrefixes = ['Webkit', 'Moz', 'ms']
const emptyStyle = document.createElement('div').style
const vendorProps: Record<string, string> = {}
// Return a vendor-prefixed property or undefined
export function vendorName(name: string) {
const capName = name[0].toUpperCase() + name.slice(1)
for (let i = 0, l = cssPrefixes.length; i < l; i += 1) {
const fixed = cssPrefixes[i] + capName
if (fixed in emptyStyle) {
return fixed
}
}
return undefined
}
// Return a potentially-mapped vendor prefixed property
export function normalizeName(name: string) {
const final = vendorProps[name]
if (final) {
return final
}
if (name in emptyStyle) {
return name
}
const fixed = vendorName(name) || name
vendorProps[name] = fixed
return fixed
}
export function isAutoPx(name: string) {
const ralphaStart = /^[a-z]/
const rautoPx = /^(?:Border(?:Top|Right|Bottom|Left)?(?:Width|)|(?:Margin|Padding)?(?:Top|Right|Bottom|Left)?|(?:Min|Max)?(?:Width|Height))$/
// The first test is used to ensure that:
// 1. The name starts with a lowercase letter (as we uppercase it for the second regex).
// 2. The name is not empty.
return (
ralphaStart.test(name) &&
rautoPx.test(name[0].toUpperCase() + name.slice(1))
)
}
const rnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/
const rcssNum = new RegExp(`^(?:([+-])=|)(${rnum.source})([a-z%]*)$`, 'i')
export function parseCSSNum(raw: string) {
return rcssNum.exec(raw)
}
export function adjustCSS<TElement extends Element>(
elem: TElement,
name: string,
valueParts: string[],
): number
export function adjustCSS<TElement extends Element>(
elem: TElement,
name: string,
valueParts: string[] | null,
tween?: any,
) {
const currentValue = tween ? () => tween.cur() : () => css(elem, name, '')
let initial = currentValue()
let unit = (valueParts && valueParts[3]) || (isAutoPx(name) ? 'px' : '')
// Starting value computation is required for potential unit mismatches
const initialInUnit =
elem.nodeType &&
(!isAutoPx(name) || (unit !== 'px' && +initial)) &&
parseCSSNum(css(elem, name))
let tempValue = 0
if (initialInUnit && initialInUnit[3] !== unit) {
// Support: Firefox <=54 - 66+
// Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144)
initial /= 2
// Trust units reported by css()
unit = unit || initialInUnit[3]
// Iteratively approximate from a nonzero starting point
tempValue = +initial || 1
for (let i = 0, maxIterations = 20; i < maxIterations; i += 1) {
// Evaluate and update our best guess (doubling guesses that zero out).
// Finish if the scale equals or crosses 1 (making the old*new product non-positive).
style(elem, name, tempValue + unit)
const scale = currentValue()
if ((1 - scale) * (1 - (scale / initial || 0.5)) <= 0) {
break
}
tempValue /= scale
}
tempValue *= 2
style(elem, name, tempValue + unit)
// Make sure we update the tween properties later on
valueParts = valueParts || [] // eslint-disable-line no-param-reassign
}
let adjusted: number | undefined
if (valueParts) {
tempValue = tempValue || +initial || 0
// Apply relative offset (+=/-=) if specified
adjusted = valueParts[1]
? tempValue + (valueParts[1] === '-' ? -1 : 1) * +valueParts[2]
: +valueParts[2]
if (tween) {
tween.unit = unit
tween.start = tempValue
tween.end = adjusted
}
}
return adjusted
}
}
export namespace Util {
const cssNormalTransform = {
letterSpacing: '0',
fontWeight: '400',
}
export function css<TElement extends Element>(
elem: TElement,
name: string,
): string
export function css<TElement extends Element>(
elem: TElement,
name: string,
extra: false,
styles?: Record<string, string> | CSSStyleDeclaration,
): string
export function css<TElement extends Element>(
elem: TElement,
name: string,
extra: true | '',
styles?: Record<string, string> | CSSStyleDeclaration,
): number
export function css<TElement extends Element>(
elem: TElement,
name: string,
extra?: boolean | string,
styles?: Record<string, string> | CSSStyleDeclaration,
): string | number
export function css<TElement extends Element>(
elem: TElement,
name: string,
extra?: boolean | string,
styles?: Record<string, string> | CSSStyleDeclaration,
) {
const origName = cssCamelCase(name)
const isCustom = isCustomName(name)
// Make sure that we're working with the right name. We don't
// want to modify the value if it is a CSS custom property
// since they are user-defined.
if (!isCustom) {
name = normalizeName(origName) // eslint-disable-line
}
let val: string | number | undefined
const hook = Hook.get(name) || Hook.get(origName)
// If a hook was provided get the computed value from there
if (hook && hook.get) {
val = hook.get(elem, true, extra)
}
// Otherwise, if a way to get the computed value exists, use that
if (val === undefined) {
val = getComputedStyleByName(elem, name, styles)
}
// Convert "normal" to computed value
if (val === 'normal' && name in cssNormalTransform) {
val = cssNormalTransform[name as keyof typeof cssNormalTransform]
}
// Make numeric if forced or a qualifier was provided and val looks numeric
if (extra === '' || extra) {
const num = +val
return extra === true || Number.isFinite(num) ? num || 0 : val!
}
return val!
}
export function style<TElement extends Element>(
elem: TElement,
name: string,
): string
export function style<TElement extends Element>(
elem: TElement,
name: string,
value: string | number,
extra?: boolean,
): void
export function style<TElement extends Element>(
elem: TElement,
name: string,
value?: string | number,
extra?: boolean,
) {
// Don't set styles on text and comment nodes
if (!elem || elem.nodeType === 3 || elem.nodeType === 8) {
return
}
const raw = ((elem as any) as HTMLElement).style
if (!raw) {
return
}
// Make sure that we're working with the right name
const origName = cssCamelCase(name)
const isCustom = isCustomName(name)
// Make sure that we're working with the right name. We don't
// want to query the value if it is a CSS custom property
// since they are user-defined.
if (!isCustom) {
name = normalizeName(origName) // eslint-disable-line
}
// Gets hook for the prefixed version, then unprefixed version
const hook = Hook.get(name) || Hook.get(origName)
// Check if we're setting a value
if (value !== undefined) {
let parts: string[] | null = null
let val: string | number | undefined = value
if (typeof val === 'string') {
parts = parseCSSNum(val)
// Convert "+=" or "-=" to relative numbers
if (parts && parts[1]) {
val = adjustCSS(elem, name, parts)
}
}
// Make sure that null and NaN values aren't set
if (val == null || Number.isNaN(val)) {
return
}
// If the value is a number, add `px` for certain CSS properties
if (typeof val === 'number') {
if (parts && parts[3]) {
val += parseFloat(parts[3])
}
val = `${val}${isAutoPx(origName) ? 'px' : ''}`
}
// Support: IE <=9 - 11+
// background-* props of a cloned element affect the source element
if (DomUtil.isIE() && val === '' && name.startsWith('background')) {
raw[name as CSSKey] = 'inherit'
}
// If a hook was provided, use that value, otherwise just set the specified value
let setting = true
if (hook && hook.set) {
val = hook.set(elem, val, extra)
setting = val !== undefined
}
if (setting) {
if (isCustom) {
raw.setProperty(name, val as string)
} else {
raw[name as CSSKey] = val as string
}
}
} else {
// If a hook was provided get the non-computed value from there
if (hook && hook.get) {
const ret = hook.get(elem, false, extra)
if (ret !== undefined) {
return ret
}
}
// Otherwise just get the value from the style object
return raw[name as CSSKey]
}
return undefined
}
}
export namespace Util {
const cache: WeakMap<Element, string> = new WeakMap()
export function isHiddenWithinTree<TElement extends Element>(elem: TElement) {
const style = ((elem as any) as HTMLElement).style
return (
style.display === 'none' ||
(style.display === '' && css(elem, 'display') === 'none')
)
}
const defaultDisplayMap: Record<string, string> = {}
export function getDefaultDisplay<TElement extends Element>(elem: TElement) {
const doc = elem.ownerDocument
const nodeName = elem.nodeName
let display = defaultDisplayMap[nodeName]
if (display) {
return display
}
const temp = doc.body.appendChild(doc.createElement(nodeName))
display = css(temp, 'display')
temp.parentNode!.removeChild(temp)
if (display === 'none') {
display = 'block'
}
defaultDisplayMap[nodeName] = display
return display
}
export function showHide<TElement extends Element>(
elem: TElement,
show: boolean,
) {
const style = ((elem as any) as HTMLElement).style
if (!style) {
return
}
const display = style.display
if (show) {
// Since we force visibility upon cascade-hidden elements, an immediate (and slow)
// check is required in this first loop unless we have a nonempty display value (either
// inline or about-to-be-restored)
if (display === 'none') {
const value = cache.get(elem)
if (!value) {
style.display = ''
}
}
if (style.display === '' && isHiddenWithinTree(elem)) {
style.display = getDefaultDisplay(elem)
}
} else if (display !== 'none') {
style.display = 'none'
// Remember what we're overwriting
cache.set(elem, display)
}
}
}

View File

@ -0,0 +1,112 @@
import { DomUtil } from '../../util/dom'
import { Util } from './style-util'
import { Hook } from './style-hook'
import { Primer } from './primer'
export class Style<N extends Node> extends Primer<N> {
css(computed?: boolean): Record<string, string>
css(
style: (Util.CSSKeys | string)[],
computed?: boolean,
): Record<string, string>
css(style: Util.CSSKeys | string, computed?: boolean): string
css(style: Record<string, string> | CSSStyleDeclaration): this
css(style: Util.CSSKeys | string, value: string): this
css(
style?:
| boolean
| string
| Util.CSSKeys
| (string | Util.CSSKeys)[]
| Record<string, string>
| CSSStyleDeclaration,
value?: string | null | boolean,
) {
const elem = DomUtil.toElement<HTMLElement>(this.node)
// get full style as object
if (style == null || typeof style === 'boolean') {
const ret: Record<string, string> = {}
if (style) {
const computedStyle = DomUtil.getComputedStyle(elem)
Array.from(computedStyle).forEach((key) => {
ret[key] = Util.css(elem, key, false, computedStyle)
})
} else {
elem.style.cssText
.split(/\s*;\s*/)
.filter((str) => str.length > 0)
.forEach((str) => {
const parts = str.split(/\s*:\s*/)
ret[Util.cssCamelCase(parts[0])] = Util.style(elem, parts[0])
})
}
return ret
}
// get style properties as array
if (Array.isArray(style)) {
const ret: Record<string, string> = {}
const names = style.map((name) => Util.cssCamelCase(name))
if (value) {
const computedStyle = DomUtil.getComputedStyle(elem)
names.forEach((name) => {
ret[name] = Util.css(elem, name, false, computedStyle)
})
} else {
names.forEach((name) => {
ret[name] = Util.style(elem, name)
})
}
return ret
}
// set styles in object
if (typeof style === 'object') {
Object.keys(style).forEach((name) =>
this.css(name, style[name as Util.CSSKey]),
)
return this
}
// get style for property
if (typeof value == null || typeof value === 'boolean') {
return value ? Util.css(elem, style) : Util.style(elem, style)
}
// set style for property
if (typeof value === 'string') {
Util.style(elem, style, value)
}
return this
}
visible() {
return !Util.isHiddenWithinTree(DomUtil.toElement(this.node))
}
show() {
Util.showHide(DomUtil.toElement(this.node), true)
return this
}
hide() {
Util.showHide(DomUtil.toElement(this.node), false)
return this
}
toggle(state?: boolean) {
if (typeof state === 'boolean') {
return state ? this.show() : this.hide()
}
return Util.isHiddenWithinTree(DomUtil.toElement(this.node))
? this.show()
: this.hide()
}
}
export namespace Style {
export const registerHook = Hook.add
}

View File

@ -0,0 +1,19 @@
import { ObjUtil } from '../util/obj'
import { ElementExtension as StyleExtension } from './shape/style-ext'
import { ElementExtension as MaskExtension } from './container/mask-ext'
import { ElementExtension as ClipPathExtension } from './container/clippath-ext'
import { VectorElement } from './element'
declare module './element' {
interface VectorElement<TSVGElement extends SVGElement = SVGElement>
extends ClipPathExtension<TSVGElement>,
StyleExtension<TSVGElement>,
MaskExtension<TSVGElement> {}
}
ObjUtil.applyMixins(
VectorElement,
MaskExtension,
StyleExtension,
ClipPathExtension,
)

View File

@ -0,0 +1,541 @@
import type { Svg } from './container/svg'
import type { Text } from './shape/text'
import type { KeyValue } from '../types'
import { DomUtil } from '../util/dom'
import { Util } from './util'
import { Box } from '../struct/box'
import { Point } from '../struct/point'
import { Color } from '../struct/color'
import { Matrix } from '../struct/matrix'
import { SVGNumber } from '../struct/svg-number'
import { Vector } from './vector'
@VectorElement.register('Element')
export class VectorElement<TSVGElement extends SVGElement = SVGElement>
extends Vector<TSVGElement>
implements Matrix.Matrixifiable {
width(): number
width(width: string | number | null): this
width(width?: string | number | null) {
return this.attr<number>('width', width)
}
height(): number
height(height: string | number | null): this
height(height?: string | number | null) {
return this.attr<number>('height', height)
}
size(width: string | number, height: string | number): this
size(width: string | number, height: string | number | null | undefined): this
size(width: string | number | null | undefined, height: string | number): this
size(width?: string | number | null, height?: string | number | null) {
const bbox = Util.proportionalSize(this, width, height)
return this.width(bbox.width).height(bbox.height)
}
x(): number
x(x: string | number | null): this
x(x?: string | number | null) {
return this.attr<number>('x', x)
}
y(): number
y(y: string | number | null): this
y(y?: string | number | null) {
return this.attr<number>('y', y)
}
move(x: string | number = 0, y: string | number = 0) {
return this.x(x).y(y)
}
cx(): number
cx(x: null): number
cx(x: string | number): this
cx(x?: string | number | null) {
return x == null
? this.x() + this.width() / 2
: this.x(SVGNumber.minus(x, this.width() / 2))
}
cy(): number
cy(y: null): number
cy(y: string | number | null): this
cy(y?: string | number | null) {
return y == null
? this.y() + this.height() / 2
: this.y(SVGNumber.minus(y, this.height() / 2))
}
center(x: string | number, y: string | number) {
return this.cx(x).cy(y)
}
dx(x: string | number) {
return this.x(SVGNumber.plus(x, this.x()))
}
dy(y: string | number) {
return this.y(SVGNumber.plus(y, this.y()))
}
dmove(x: string | number = 0, y: string | number = 0) {
return this.dx(x).dy(y)
}
// #region Fill and Stroke
fill(): string
fill(color: string | Color | Color.RGBA | VectorElement | null): this
fill(attrs: {
color?: string
opacity?: number
rule?: 'nonzero' | 'evenodd'
}): this
fill(
value?:
| null
| string
| Color
| Color.RGBA
| VectorElement
| {
color?: string
opacity?: number
rule?: 'nonzero' | 'evenodd'
},
) {
if (typeof value === 'undefined') {
return this.attr('fill')
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
Private.fillOrStroke(this, 'fill', value, ['color', 'opacity', 'rule'])
return this
}
stroke(): string
stroke(color: string | Color | Color.RGBA | VectorElement | null): this
stroke(attrs: {
color?: string
width?: number
opacity?: number
linecap?: 'butt' | 'round' | 'square'
linejoin?: 'arcs' | 'bevel' | 'miter' | 'miter-clip' | 'round'
miterlimit?: number
dasharray?: string
dashoffset?: string
}): this
stroke(
value?:
| null
| string
| Color
| Color.RGBA
| VectorElement
| {
color?: string
width?: number
opacity?: number
linecap?: 'butt' | 'round' | 'square'
linejoin?: 'arcs' | 'bevel' | 'miter' | 'miter-clip' | 'round'
miterlimit?: number
dasharray?: string
dashoffset?: string
},
) {
if (typeof value === 'undefined') {
return this.attr('stroke')
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
Private.fillOrStroke(this, 'fill', value, [
'color',
'width',
'opacity',
'linecap',
'linejoin',
'miterlimit',
'dasharray',
'dashoffset',
])
return this
}
opacity(): number
opacity(value: number | null): this
opacity(value?: number | null) {
return this.attr<number>('opacity', value)
}
// #endregion
// #region Font
font(attrs: KeyValue<string | number>): this
font(key: string, value: string | number): this
font(a: KeyValue<string | number> | string, v?: string | number) {
if (typeof a === 'object') {
Object.keys(a).forEach((key) => this.font(key, a[key]))
return this
}
if (a === 'leading') {
const text = (this as any) as Text // eslint-disable-line
if (text.leading) {
text.leading(v)
}
return this
}
return a === 'anchor'
? this.attr('text-anchor', v)
: a === 'size' ||
a === 'family' ||
a === 'weight' ||
a === 'stretch' ||
a === 'variant' ||
a === 'style'
? this.attr(`font-${a}`, v)
: this.attr(a, v)
}
// #endregion
// #region Transform
transform(): Matrix.Transform
transform(type: keyof Matrix.Transform): number
transform(
options: Matrix.TransformOptions,
relative?: boolean | VectorElement | Matrix.Raw,
): this
transform(
matrix: Matrix.MatrixLike,
relative?: boolean | VectorElement | Matrix.Raw,
): this
transform(
o?: keyof Matrix.Transform | Matrix.MatrixLike | Matrix.TransformOptions,
relative?: boolean | VectorElement | Matrix.Raw,
) {
if (o == null || typeof o === 'string') {
const decomposed = new Matrix(this).decompose()
return o == null ? decomposed : decomposed[o]
}
const m = Matrix.isMatrixLike(o)
? o
: {
...o,
origin: Private.getOrigin(o, this),
}
// The user can pass a boolean, an Element or an Matrix or nothing
const raw = relative === true ? this : relative || undefined
const res = new Matrix(raw).transform(m)
return this.attr('transform', res.toString())
}
untransform() {
return this.attr('transform', null)
}
matrixify(): Matrix {
const raw = this.attr('transform') || ''
return (
raw
.split(/\)\s*,?\s*/)
.slice(0, -1)
.map((str) => {
const kv = str.trim().split('(')
return [kv[0], kv[1].split(/[\s,]+/).map((s) => Number.parseFloat(s))]
})
.reverse()
// merge every transformation into one matrix
.reduce(
(
matrix,
transform: ['matrix' | 'rotate' | 'scale' | 'translate', number[]],
) => {
if (transform[0] === 'matrix') {
return matrix.lmultiply(
Matrix.toMatrixLike(transform[1] as Matrix.MatrixArray),
)
}
return matrix[transform[0]].call(matrix, ...transform[1])
},
new Matrix(),
)
)
}
toParent(parent: VectorElement, index?: number): this {
if (this !== parent) {
const ctm = this.screenCTM()
const pCtm = parent.screenCTM().inverse()
this.addTo(parent, index).untransform().transform(pCtm.multiply(ctm))
}
return this
}
toRoot(index?: number): this {
const root = this.root()
if (root) {
return this.toParent(root, index)
}
return this
}
ctm() {
return new Matrix(DomUtil.toElement<SVGGraphicsElement>(this.node).getCTM())
}
screenCTM() {
/* https://bugzilla.mozilla.org/show_bug.cgi?id=1344537
This is needed because FF does not return the transformation matrix
for the inner coordinate system when getScreenCTM() is called on nested svgs.
However all other Browsers do that */
const svg = (this as any) as Svg
if (typeof svg.isRoot === 'function' && !svg.isRoot()) {
const rect = svg.rect(1, 1)
const m = rect.node.getScreenCTM()
rect.remove()
return new Matrix(m)
}
return new Matrix(
DomUtil.toElement<SVGGraphicsElement>(this.node).getScreenCTM(),
)
}
point(x: number, y: number) {
return new Point(x, y).transform(this.screenCTM().inverse())
}
matrix(): Matrix
matrix(a: number, b: number, c: number, d: number, e: number, f: number): this
matrix(
a?: number,
b?: number,
c?: number,
d?: number,
e?: number,
f?: number,
) {
if (a == null) {
return new Matrix(this)
}
return this.attr(
'transform',
new Matrix(
a,
b as number,
c as number,
d as number,
e as number,
f as number,
).toString(),
)
}
rotate(angle: number, cx?: number, cy?: number) {
return this.transform({ rotate: angle, ox: cx, oy: cy }, true)
}
skew(x: number, y: number, cx?: number, cy?: number) {
return arguments.length === 1 || arguments.length === 3
? this.transform({ skew: x, ox: y, oy: cx }, true)
: this.transform({ skew: [x, y], ox: cx, oy: cy }, true)
}
shear(lam: number, cx?: number, cy?: number) {
return this.transform({ shear: lam, ox: cx, oy: cy }, true)
}
scale(x: number, y: number, cx?: number, cy?: number) {
return arguments.length === 1 || arguments.length === 3
? this.transform({ scale: x, ox: y, oy: cx }, true)
: this.transform({ scale: [x, y], ox: cx, oy: cy }, true)
}
translate(x: number, y: number) {
return this.transform({ translate: [x, y] }, true)
}
relative(x: number, y: number) {
return this.transform({ relative: [x, y] }, true)
}
flip(origin?: number | [number, number] | Point.PointLike): this
flip(
direction: 'both' | 'x' | 'y',
origin?: number | [number, number] | Point.PointLike,
): this
flip(
direction:
| 'both'
| 'x'
| 'y'
| number
| [number, number]
| Point.PointLike = 'both',
origin?: number | [number, number] | Point.PointLike,
) {
if (typeof direction !== 'string') {
origin = direction // eslint-disable-line
direction = 'both' // eslint-disable-line
}
return this.transform({ origin, flip: direction }, true)
}
// #endregion
// #region BBox
bbox() {
const getBBox = (node: SVGGraphicsElement) => node.getBBox()
const retry = (node: SVGGraphicsElement) =>
DomUtil.withSvgContext((svg) => {
try {
const cloned = this.clone<VectorElement>().addTo(svg).show()
const box = DomUtil.toElement<SVGGraphicsElement>(
cloned.node,
).getBBox()
cloned.remove()
return box
} catch (error) {
throw new Error(
`Getting bbox of element "${
node.nodeName
}" is not possible: ${error.toString()}`,
)
}
})
const box = DomUtil.getBox(
DomUtil.toElement<SVGGraphicsElement>(this.node),
getBBox,
retry,
)
return new Box(box)
}
rbox(elem?: VectorElement) {
const getRBox = (node: SVGGraphicsElement) => node.getBoundingClientRect()
const retry = (node: SVGGraphicsElement) => {
// There is no point in trying tricks here because if we insert the
// element into the dom ourselfes it obviously will be at the wrong position
throw new Error(
`Getting rbox of element "${node.nodeName}" is not possible`,
)
}
const box = DomUtil.getBox(
DomUtil.toElement<SVGGraphicsElement>(this.node),
getRBox,
retry,
)
const rbox = new Box(box)
// If an element was passed, return the bbox in the coordinate system of
// that element.
if (elem) {
return rbox.transform(elem.screenCTM().inverseO())
}
// Else we want it in absolute screen coordinates
// Therefore we need to add the scrollOffset
return rbox.addOffset()
}
inside(x: number, y: number) {
const box = this.bbox()
return (
x > box.x && y > box.y && x < box.x + box.width && y < box.y + box.height
)
}
// #endregion
}
namespace Private {
export function fillOrStroke(
elem: VectorElement,
type: 'fill' | 'stroke',
value: string | Color | VectorElement | KeyValue | null,
names: string[],
) {
if (value === null) {
elem.attr(type, null)
} else if (
typeof value === 'string' ||
value instanceof Color ||
Color.isRgbLike(value) ||
value instanceof VectorElement
) {
elem.attr(type, value.toString())
} else {
const prefix = (t: string, a: string) => (a === 'color' ? t : `${t}-${a}`)
for (let i = names.length - 1; i >= 0; i -= 1) {
const k = names[i]
const v = value[k]
if (v != null) {
elem.attr(prefix(type, k), v)
}
}
}
}
/**
* This function adds support for string origins.
* It searches for an origin in o.origin o.ox and o.originX.
* This way, origin: {x: 'center', y: 50} can be passed as well as ox: 'center', oy: 50
* */
export function getOrigin(
o: Matrix.TransformOptions,
element: VectorElement,
): [number, number] {
const { origin } = o
// First check if origin is in ox or originX
let ox = o.ox != null ? o.ox : o.originX != null ? o.originX : 'center'
let oy = o.oy != null ? o.oy : o.originY != null ? o.originY : 'center'
// Then check if origin was used and overwrite in that case
if (origin != null) {
;[ox, oy] = Array.isArray(origin)
? origin
: typeof origin === 'object'
? [origin.x, origin.y]
: [origin, origin]
}
// Make sure to only call bbox when actually needed
if (typeof ox === 'string' || typeof oy === 'string') {
const { height, width, x, y } = element.bbox()
// And only overwrite if string was passed for this specific axis
if (typeof ox === 'string') {
ox = ox.includes('left')
? x
: ox.includes('right')
? x + width
: x + width / 2
}
if (typeof oy === 'string') {
oy = oy.includes('top')
? y
: oy.includes('bottom')
? y + height
: y + height / 2
}
}
return [ox, oy]
}
}

View File

@ -0,0 +1,38 @@
import './element-mixins'
import './container/container-mixins'
import './container/defs-mixins'
import './shape/line-mixins'
import './shape/path-mixins'
import './shape/poly-mixins'
import './shape/text-mixins'
export * from './adopter'
export * from './dom'
export * from './element'
export * from './container/a'
export * from './container/clippath'
export * from './container/defs'
export * from './container/g'
export * from './container/gradient'
export * from './container/marker'
export * from './container/mask'
export * from './container/pattern'
export * from './container/svg'
export * from './container/symbol'
export * from './shape/circle'
export * from './shape/ellipse'
export * from './shape/foreignobject'
export * from './shape/fragment'
export * from './shape/image'
export * from './shape/line'
export * from './shape/path'
export * from './shape/poly'
export * from './shape/polygon'
export * from './shape/polyline'
export * from './shape/rect'
export * from './shape/style'
export * from './shape/text'
export * from './shape/textpath'
export * from './shape/use'

View File

@ -0,0 +1,60 @@
import { Class } from '../types'
import { Str } from '../util/str'
export namespace Registry {
let root: Class
const cache: Record<string, Class> = {}
export function register(ctor: Class, name = ctor.name, asRoot?: boolean) {
cache[name] = ctor
if (asRoot) {
root = ctor
}
return ctor
}
export function getClass<TClass extends Class = Class>(node: Node): TClass
export function getClass<TClass extends Class = Class>(name: string): TClass
export function getClass<TClass extends Class = Class>(node: string | Node) {
if (typeof node === 'string') {
return cache[node] as TClass
}
const nodeName = node.nodeName || 'Dom'
let className = Str.ucfirst(nodeName)
if (nodeName === '#document-fragment') {
className = 'Fragment'
} else if (
className === 'LinearGradient' ||
className === 'RadialGradient'
) {
className = 'Gradient'
} else if (!Registry.hasClass(className)) {
className = 'Dom'
}
return cache[className] as TClass
}
export function hasClass(name: string) {
return cache[name] != null
}
export function getRoot() {
return root
}
export function getTagName(cls: Class) {
const keys = Object.keys(cache)
for (let i = 0, l = keys.length; i < l; i += 1) {
const key = keys[i]
if (cache[key] === cls) {
return Str.lcfirst(key)
}
}
return null
}
}

View File

@ -0,0 +1,13 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Circle } from './circle'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
circle(attrs?: Attrs | null): Circle
circle(size: number | string, attrs?: Attrs | null): Circle
circle(size?: number | string | Attrs | null, attrs?: Attrs | null) {
return Circle.create(size, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,99 @@
import { Attrs } from '../../types'
import { SVGNumber } from '../../struct/svg-number'
import { Shape } from './shape'
@Circle.register('Circle')
export class Circle extends Shape<SVGCircleElement> {
rx(): number
rx(rx: string | number | null): this
rx(rx?: string | number | null) {
return this.attr<number>('r', rx)
}
ry(): number
ry(ry: string | number | null): this
ry(ry?: string | number | null) {
return this.attr<number>('r', ry)
}
radius(): number
radius(r: string | number | null): this
radius(r?: string | number | null) {
return this.attr<number>('r', r)
}
size(size: string | number) {
return this.radius(SVGNumber.divide(size, 2))
}
cx(): number
cx(x: string | number | null): this
cx(x?: string | number | null) {
return this.attr<number>('cx', x)
}
cy(): number
cy(y: string | number | null): this
cy(y?: string | number | null) {
return this.attr<number>('cy', y)
}
x(): number
x(x?: null): number
x(x: string | number): this
x(x?: string | number | null) {
return x == null
? this.cx() - this.rx()
: this.cx(SVGNumber.plus(x, this.rx()))
}
y(): number
y(y: null): number
y(y: string | number): this
y(y?: string | number | null) {
return y == null
? this.cy() - this.ry()
: this.cy(SVGNumber.plus(y, this.ry()))
}
width(): number
width(w: null): number
width(w: string | number): this
width(w?: string | number | null) {
return w == null ? this.rx() * 2 : this.rx(SVGNumber.divide(w, 2))
}
height(): number
height(h: null): number
height(h: string | number): this
height(h?: string | number | null) {
return h == null ? this.ry() * 2 : this.ry(SVGNumber.divide(h, 2))
}
}
export namespace Circle {
export function create(attrs?: Attrs | null): Circle
export function create(size: number | string, attrs?: Attrs | null): Circle
export function create(
size?: number | string | Attrs | null,
attrs?: Attrs | null,
): Circle
export function create(
size?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
const circle = new Circle()
if (size == null) {
circle.size(0)
} else if (size != null && typeof size === 'object') {
circle.size(0).attr(size)
} else {
circle.size(size)
if (attrs) {
circle.attr(attrs)
}
}
return circle
}
}

View File

@ -0,0 +1,22 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Ellipse } from './ellipse'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
ellipse(attrs?: Attrs | null): Ellipse
ellipse(size: number | string, attrs?: Attrs | null): Ellipse
ellipse(
width: number | string,
height: string | number,
attrs?: Attrs | null,
): Ellipse
ellipse(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
return Ellipse.create(width, height, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,118 @@
import { Attrs } from '../../types'
import { SVGNumber } from '../../struct/svg-number'
import { Util } from '../util'
import { Shape } from './shape'
@Ellipse.register('Ellipse')
export class Ellipse extends Shape<SVGEllipseElement> {
rx(): number
rx(rx: string | number | null): this
rx(rx?: string | number | null) {
return this.attr('rx', rx)
}
ry(): number
ry(ry: string | number | null): this
ry(ry?: string | number | null) {
return this.attr('ry', ry)
}
radius(rx: string | number, ry: string | number = rx) {
return this.rx(rx).ry(ry)
}
size(width: string | number, height: string | number): this
size(width: string | number, height: string | number | null | undefined): this
size(width: string | number | null | undefined, height: string | number): this
size(width?: string | number | null, height?: string | number | null) {
const size = Util.proportionalSize(this, width, height)
const rx = SVGNumber.divide(size.width, 2)
const ry = SVGNumber.divide(size.height, 2)
return this.rx(rx).ry(ry)
}
x(): number
x(x?: null): number
x(x: string | number): this
x(x?: string | number | null) {
return x == null
? this.cx() - this.rx()
: this.cx(SVGNumber.plus(x, this.rx()))
}
y(): number
y(x: null): number
y(x: string | number): this
y(y?: string | number | null) {
return y == null
? this.cy() - this.ry()
: this.cy(SVGNumber.plus(y, this.ry()))
}
cx(): number
cx(x: string | number | null): this
cx(x?: string | number | null) {
return this.attr<number>('cx', x)
}
cy(): number
cy(y: string | number | null): this
cy(y?: string | number | null) {
return this.attr<number>('cy', y)
}
width(): number
width(w: null): number
width(w: string | number): this
width(w?: string | number | null) {
return w == null ? this.rx() * 2 : this.rx(SVGNumber.divide(w, 2))
}
height(): number
height(h: null): number
height(h: string | number): this
height(h?: string | number | null) {
return h == null ? this.ry() * 2 : this.ry(SVGNumber.divide(h, 2))
}
}
export namespace Ellipse {
export function create(attrs?: Attrs | null): Ellipse
export function create(size: number | string, attrs?: Attrs | null): Ellipse
export function create(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): Ellipse
export function create(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
): Ellipse
export function create(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
const ellipse = new Ellipse()
if (width == null) {
ellipse.size(0, 0)
} else if (typeof width === 'object') {
ellipse.size(0, 0).attr(width)
} else if (height != null && typeof height === 'object') {
ellipse.size(width, width).attr(height)
} else {
if (typeof height === 'undefined') {
ellipse.size(width, width)
} else {
ellipse.size(width, height)
}
if (attrs) {
ellipse.attr(attrs)
}
}
return ellipse
}
}

View File

@ -0,0 +1,22 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { ForeignObject } from './foreignobject'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
foreignObject(attrs?: Attrs | null): ForeignObject
foreignObject(size: number | string, attrs?: Attrs | null): ForeignObject
foreignObject(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): ForeignObject
foreignObject(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
return ForeignObject.create(width, height, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,49 @@
import { Attrs } from '../../types'
import { VectorElement } from '../element'
@ForeignObject.register('ForeignObject')
export class ForeignObject extends VectorElement<SVGForeignObjectElement> {}
export namespace ForeignObject {
export function create(attrs?: Attrs | null): ForeignObject
export function create(
size: number | string,
attrs?: Attrs | null,
): ForeignObject
export function create(
width: number | string,
height: number | string,
attrs?: Attrs | null,
): ForeignObject
export function create(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
): ForeignObject
export function create(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
const fo = new ForeignObject()
if (width == null) {
fo.size(0, 0)
} else if (typeof width === 'object') {
fo.size(0, 0).attr(width)
} else if (height != null && typeof height === 'object') {
fo.size(width, width).attr(height)
} else {
if (typeof height === 'undefined') {
fo.size(width, width)
} else {
fo.size(width, height)
}
if (attrs) {
fo.attr(attrs)
}
}
return fo
}
}

View File

@ -0,0 +1,33 @@
import { Global } from '../../global'
import { DomUtil } from '../../util/dom'
import { Dom } from '../dom'
@Fragment.register('Fragment')
export class Fragment extends Dom<DocumentFragment> {
constructor(node = Global.document.createDocumentFragment()) {
super(node)
}
xml(): string
xml(outerXML: boolean): string
xml(process: (dom: Dom) => false | Dom, outerXML?: boolean): string
xml(content: string, outerXML?: boolean, ns?: string): string
xml(
arg1?: boolean | string | ((dom: Dom) => false | Dom),
arg2?: boolean,
arg3?: string,
) {
const content = typeof arg1 === 'boolean' ? null : arg1
const ns = arg3
// Put all elements into a wrapper before we can get the innerXML from it
if (content == null || typeof content === 'function') {
const wrapper = new Dom(DomUtil.createNode<SVGElement>('wrapper', ns))
wrapper.add(this.node.cloneNode(true))
return wrapper.xml(false)
}
return super.xml(content, false, ns)
}
}

View File

@ -0,0 +1,18 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Image } from './image'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
image(attrs?: Attrs | null): Image
image(url?: string, attrs?: Attrs | null): Image
image(url?: string, callback?: Image.Callback, attrs?: Attrs | null): Image
image(
url?: string | Attrs | null,
callback?: Image.Callback | Attrs | null,
attrs?: Attrs | null,
) {
return Image.create(url, callback, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,108 @@
import { Attrs } from '../../types'
import { Global } from '../../global'
import { DomUtil } from '../../util/dom'
import { Attr } from '../../util/attr'
import { Pattern } from '../container/pattern'
import { Shape } from './shape'
@Image.register('Image')
export class Image extends Shape<SVGImageElement> {
load(url?: string, callback?: Image.Callback | null) {
if (!url) {
return this
}
const img = new Global.window.Image()
img.addEventListener('load', (e) => {
// ensure image size
if (this.width() === 0 && this.height() === 0) {
this.size(img.width, img.height)
}
const p = this.parent(Pattern)
if (
p instanceof Pattern && // ensure pattern size if not set
p.width() === 0 &&
p.height() === 0
) {
p.size(this.width(), this.height())
}
if (typeof callback === 'function') {
callback.call(this, e)
}
})
img.src = url
return this.attr('href', url, DomUtil.namespaces.xlink)
}
}
export namespace Image {
export function create(attrs?: Attrs | null): Image
export function create(url?: string, attrs?: Attrs | null): Image
export function create(
url?: string,
callback?: Image.Callback,
attrs?: Attrs | null,
): Image
export function create(
url?: string | Attrs | null,
callback?: Image.Callback | Attrs | null,
attrs?: Attrs | null,
): Image
export function create(
url?: string | Attrs | null,
callback?: Image.Callback | Attrs | null,
attrs?: Attrs | null,
) {
const image = new Image().size(0, 0)
if (url) {
if (typeof url === 'string') {
if (typeof callback === 'function' || callback == null) {
image.load(url, callback)
if (attrs) {
image.attr(attrs)
}
} else {
image.load(url).attr(callback)
}
} else {
image.attr(url)
}
}
return image
}
}
export namespace Image {
export type Callback = (this: Image, e: Event) => void
Attr.registerHook<Image>((attr, url: Image | string, elem) => {
// convert image fill and stroke to patterns
if (attr === 'fill' || attr === 'stroke') {
const root = elem.root()
const defs = root && root.defs()
if (defs) {
let image: Image | null = null
if (typeof url === 'string') {
const isImage = /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i
if (isImage.test(url)) {
image = defs.image(url)
}
} else {
image = url
}
if (image != null) {
return defs.pattern(0, 0, (pattern) => pattern.add(image!)).url()
}
}
}
return url
})
}

View File

@ -0,0 +1,26 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Line } from './line'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
line(attrs?: Attrs | null): Line
line(points: [[number, number], [number, number]], attrs?: Attrs | null): Line
line(
x1: number,
y1: number,
x2: number,
y2: number,
attrs?: Attrs | null,
): Line
line(
x1?: [[number, number], [number, number]] | number | Attrs | null,
y1?: number | Attrs | null,
x2?: number,
y2?: number,
attrs?: Attrs | null,
) {
return Line.create(x1, y1, x2, y2, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,9 @@
import { ObjUtil } from '../../util/obj'
import { LineExtension as MarkerLineExtension } from '../container/marker-ext'
import { Line } from './line'
declare module './line' {
interface Line extends MarkerLineExtension<SVGLineElement> {}
}
ObjUtil.applyMixins(Line, MarkerLineExtension)

View File

@ -0,0 +1,129 @@
import { Attrs } from '../../types'
import { PointArray } from '../../struct/point-array'
import { Util } from '../util'
import { Shape } from './shape'
@Line.register('Line')
export class Line extends Shape<SVGLineElement> {
x(): number
x(x: number | string): this
x(x?: number | string) {
return x == null ? this.bbox().x : this.move(x, this.bbox().y)
}
y(): number
y(y: number | string): this
y(y?: number | string) {
return y == null ? this.bbox().y : this.move(this.bbox().x, y)
}
width(): number
width(w: number | string): this
width(w?: number | string) {
return w == null ? this.bbox().width : this.size(w, this.bbox().height)
}
height(): number
height(h: number | string): this
height(h?: number | string) {
return h == null ? this.bbox().height : this.size(this.bbox().width, h)
}
array() {
return new PointArray([
[this.attr<number>('x1'), this.attr<number>('y1')],
[this.attr<number>('x2'), this.attr<number>('y2')],
])
}
move(x: number | string, y: number | string) {
return this.attr(this.array().move(x, y).toLine())
}
plot(): PointArray
plot(points: [[number, number], [number, number]]): this
plot(x1: number, y1: number, x2: number, y2: number): this
plot(
x1?: [[number, number], [number, number]] | number,
y1?: number,
x2?: number,
y2?: number,
): this
plot(
x1?: [[number, number], [number, number]] | number,
y1?: number,
x2?: number,
y2?: number,
) {
if (x1 == null) {
return this.array()
}
const attrs = Array.isArray(x1)
? new PointArray(x1).toLine()
: {
x1,
y1,
x2,
y2,
}
return this.attr(attrs)
}
size(width: string | number, height: string | number): this
size(width: string | number, height: string | number | null | undefined): this
size(width: string | number | null | undefined, height: string | number): this
size(width?: string | number | null, height?: string | number | null) {
const p = Util.proportionalSize(this, width, height)
return this.attr(this.array().size(p.width, p.height).toLine())
}
}
export namespace Line {
export function create(attrs?: Attrs | null): Line
export function create(
points: [[number, number], [number, number]],
attrs?: Attrs | null,
): Line
export function create(
x1: number,
y1: number,
x2: number,
y2: number,
attrs?: Attrs | null,
): Line
export function create(
x1?: [[number, number], [number, number]] | number | Attrs | null,
y1?: number | Attrs | null,
x2?: number,
y2?: number,
attrs?: Attrs | null,
): Line
export function create(
x1?: [[number, number], [number, number]] | number | Attrs | null,
y1?: number | Attrs | null,
x2?: number,
y2?: number,
attrs?: Attrs | null,
) {
const line = new Line()
if (x1 == null) {
line.plot(0, 0, 0, 0)
} else if (Array.isArray(x1)) {
line.plot(x1)
if (y1 != null && typeof y1 === 'object') {
line.attr(y1)
}
} else if (typeof x1 === 'object') {
line.plot(0, 0, 0, 0).attr(x1)
} else {
line.plot(x1, y1 as number, x2 as number, y2 as number)
if (attrs) {
line.attr(attrs)
}
}
return line
}
}

View File

@ -0,0 +1,17 @@
import { Attrs } from '../../types'
import { PathArray } from '../../struct/path-array'
import { Vector } from '../vector'
import { Path } from './path'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
path(attrs?: Attrs | null): Path
path(d: string | Path.Segment[] | PathArray, attrs?: Attrs | null): Path
path(
d?: string | Path.Segment[] | PathArray | Attrs | null,
attrs?: Attrs | null,
) {
return Path.create(d, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,12 @@
import { ObjUtil } from '../../util/obj'
import { PathExtension as TextpathExtension } from './textpath-ext'
import { LineExtension as MarkerLineExtension } from '../container/marker-ext'
import { Path } from './path'
declare module './path' {
interface Path
extends MarkerLineExtension<SVGPathElement>,
TextpathExtension<SVGPathElement> {}
}
ObjUtil.applyMixins(Path, TextpathExtension, MarkerLineExtension)

View File

@ -0,0 +1,347 @@
import { Point } from '../../struct/point'
type CommandUpperCase =
| 'M'
| 'L'
| 'H'
| 'V'
| 'C'
| 'S'
| 'Q'
| 'T'
| 'A'
| 'Z'
type CommandLowerCase =
| 'm'
| 'l'
| 'h'
| 'v'
| 'c'
| 's'
| 'q'
| 't'
| 'a'
| 'z'
type Command = CommandUpperCase | CommandLowerCase
export type Segment = [Command, ...number[]]
interface Options {
segment: Segment
segments: Segment[]
lastToken: string
lastCommand: Command
inNumber: boolean
number: string
inSegment: boolean
hasDecimal: boolean
hasExponent: boolean
absolute: boolean
p0: Point
p: Point
}
type Hnadler = (parameters: number[], p: Point, p0: Point) => Segment
const upperHandler: { [key in CommandUpperCase]: Hnadler } = {
M(c, p, p0) {
p.x = p0.x = c[0]
p.y = p0.y = c[1]
return ['M', p.x, p.y]
},
L(c, p) {
p.x = c[0]
p.y = c[1]
return ['L', c[0], c[1]]
},
H(c, p) {
p.x = c[0]
return ['H', c[0]]
},
V(c, p) {
p.y = c[0]
return ['V', c[0]]
},
C(c, p) {
p.x = c[4]
p.y = c[5]
return ['C', c[0], c[1], c[2], c[3], c[4], c[5]]
},
S(c, p) {
p.x = c[2]
p.y = c[3]
return ['S', c[0], c[1], c[2], c[3]]
},
Q(c, p) {
p.x = c[2]
p.y = c[3]
return ['Q', c[0], c[1], c[2], c[3]]
},
T(c, p) {
p.x = c[0]
p.y = c[1]
return ['T', c[0], c[1]]
},
Z(c, p, p0) {
p.x = p0.x
p.y = p0.y
return ['Z']
},
A(c, p) {
p.x = c[5]
p.y = c[6]
return ['A', c[0], c[1], c[2], c[3], c[4], c[5], c[6]]
},
}
const handlers: { [key in Command]: Hnadler } = { ...upperHandler } as any
Object.keys(upperHandler).forEach((upper: CommandUpperCase) => {
const lower = upper.toLowerCase() as CommandLowerCase
handlers[lower] = (c: number[], p: Point, p0: Point) => {
if (upper === 'H') {
c[0] += p.x
} else if (upper === 'V') {
c[0] += p.y
} else if (upper === 'A') {
c[5] += p.x
c[6] += p.y
} else {
for (let i = 0, l = c.length; i < l; i += 1) {
c[i] += i % 2 ? p.y : p.x
}
}
return handlers[upper](c, p, p0)
}
})
function isCommand(token: string): token is Command {
return /[achlmqstvz]/i.test(token)
}
function isSegmentComplete(parser: Options) {
const count = parser.segment.length
if (count > 0) {
const parameterMap = {
M: 2,
L: 2,
H: 1,
V: 1,
C: 6,
S: 4,
Q: 4,
T: 2,
A: 7,
Z: 0,
}
const cmd = parser.segment[0].toUpperCase() as CommandUpperCase
return count === parameterMap[cmd] + 1
}
return false
}
function startNewSegment(parser: Options, token: string) {
if (parser.inNumber) {
finalizeNumber(parser, false)
}
const isNewCommand = isCommand(token)
if (isNewCommand) {
parser.segment = [token as Command]
} else {
const { lastCommand } = parser
const small = lastCommand.toLowerCase()
const isSmall = lastCommand === small
parser.segment = [small === 'm' ? (isSmall ? 'l' : 'L') : lastCommand]
}
parser.inSegment = true
parser.lastCommand = parser.segment[0]
return isNewCommand
}
function finalizeSegment(parser: Options) {
parser.inSegment = false
if (parser.absolute) {
parser.segment = toAbsolut(parser)
}
parser.segments.push(parser.segment)
}
function finalizeNumber(parser: Options, inNumber: boolean) {
if (!parser.inNumber) {
throw new Error('Parser Error')
}
if (parser.number) {
parser.segment.push(Number.parseFloat(parser.number))
}
parser.number = ''
parser.inNumber = inNumber
parser.hasDecimal = false
parser.hasExponent = false
if (isSegmentComplete(parser)) {
finalizeSegment(parser)
}
}
function toAbsolut(parser: Options) {
const command = parser.segment[0]
const args = parser.segment.slice(1) as number[]
return handlers[command](args, parser.p, parser.p0)
}
function isArcFlag(parser: Options) {
if (parser.segment.length === 0) {
return false
}
const isArc = parser.segment[0].toUpperCase() === 'A'
const { length } = parser.segment
return isArc && (length === 4 || length === 5)
}
function isExponential(parser: Options) {
return parser.lastToken.toUpperCase() === 'E'
}
export function parse(d: string, toAbsolute = true) {
let index = 0
let token = ''
const parser: Options = {
segment: null as any,
segments: [],
inNumber: false,
number: '',
lastToken: '',
lastCommand: null as any,
inSegment: false,
hasDecimal: false,
hasExponent: false,
absolute: toAbsolute,
p0: new Point(),
p: new Point(),
}
while (((parser.lastToken = token), (token = d.charAt(index)))) {
index += 1
if (!parser.inSegment && startNewSegment(parser, token)) {
continue
}
if (token === '.') {
if (parser.hasDecimal || parser.hasExponent) {
finalizeNumber(parser, false)
index -= 1
continue
}
parser.inNumber = true
parser.hasDecimal = true
parser.number += token
continue
}
if (!Number.isNaN(Number.parseInt(token, 10))) {
if (parser.number === '0' || (parser.inNumber && isArcFlag(parser))) {
finalizeNumber(parser, true)
}
parser.inNumber = true
parser.number += token
continue
}
if (token === ' ' || token === ',') {
if (parser.inNumber) {
finalizeNumber(parser, false)
}
continue
}
if (token === '-') {
if (parser.inNumber && !isExponential(parser)) {
finalizeNumber(parser, false)
index -= 1
continue
}
parser.number += token
parser.inNumber = true
continue
}
if (token.toUpperCase() === 'E') {
parser.number += token
parser.hasExponent = true
continue
}
if (isCommand(token)) {
if (parser.inNumber) {
finalizeNumber(parser, false)
} else if (!isSegmentComplete(parser)) {
throw new Error('parser Error')
} else {
finalizeSegment(parser)
}
index -= 1
}
}
if (parser.inNumber) {
finalizeNumber(parser, false)
}
if (parser.inSegment && isSegmentComplete(parser)) {
finalizeSegment(parser)
}
return parser.segments
}
export function toString(segments: Segment[]) {
return segments.reduce<string>((memo, seg) => {
let ret = memo
ret += seg[0]
if (seg[1] != null) {
ret += seg[1]
if (seg[2] != null) {
ret += ' '
ret += seg[2]
if (seg[3] != null) {
ret += ' '
ret += seg[3]
ret += ' '
ret += seg[4]
if (seg[5] != null) {
ret += ' '
ret += seg[5]
ret += ' '
ret += seg[6]
if (seg[7] != null) {
ret += ' '
ret += seg[7]
}
}
}
}
}
return ret
}, '')
}

View File

@ -0,0 +1,117 @@
import { Attrs } from '../../types'
import { Point } from '../../struct/point'
import { PathArray } from '../../struct/path-array'
import { Util } from '../util'
import { Shape } from './shape'
import * as Helper from './path-util'
@Path.register('Path')
export class Path extends Shape<SVGPathElement> {
protected arr: PathArray | null
x(): number
x(x: number | string): this
x(x?: number | string) {
return x == null ? this.bbox().x : this.move(x, this.bbox().y)
}
y(): number
y(y: number | string): this
y(y?: number | string) {
return y == null ? this.bbox().y : this.move(this.bbox().x, y)
}
width(): number
width(w: number | string): this
width(w?: number | string) {
return w == null ? this.bbox().width : this.size(w, this.bbox().height)
}
height(): number
height(h: number | string): this
height(h?: number | string) {
return h == null ? this.bbox().height : this.size(this.bbox().width, h)
}
array() {
if (this.arr == null) {
this.arr = new PathArray(this.attr('d'))
}
return this.arr
}
move(x: number | string, y: number | string) {
return this.attr('d', this.array().move(x, y).toString())
}
plot(): PathArray
plot(d: string | Path.Segment[] | PathArray): this
plot(d?: string | Path.Segment[] | PathArray) {
if (d == null) {
return this.array()
}
this.arr = null
if (typeof d === 'string') {
this.attr('d', d)
} else {
this.arr = new PathArray(d)
this.attr('d', this.arr.toString())
}
return this
}
size(width: string | number, height: string | number): this
size(width: string | number, height: string | number | null | undefined): this
size(width: string | number | null | undefined, height: string | number): this
size(width?: string | number | null, height?: string | number | null) {
const p = Util.proportionalSize(this, width, height)
return this.attr('d', this.array().size(p.width, p.height).toString())
}
length() {
return this.node.getTotalLength()
}
pointAt(length: number) {
return new Point(this.node.getPointAtLength(length))
}
}
export namespace Path {
export function create(attrs?: Attrs | null): Path
export function create(
d: string | Path.Segment[] | PathArray,
attrs?: Attrs | null,
): Path
export function create(
d?: string | Path.Segment[] | PathArray | Attrs | null,
attrs?: Attrs | null,
): Path
export function create(
d?: string | Path.Segment[] | PathArray | Attrs | null,
attrs?: Attrs | null,
) {
const path = new Path()
if (d != null) {
if (typeof d === 'string' || Array.isArray(d)) {
path.plot(d)
if (attrs) {
path.attr(attrs)
}
} else {
path.attr(d)
}
}
return path
}
}
export namespace Path {
export type Segment = Helper.Segment
export const parse = Helper.parse
export const toString = Helper.toString
}

View File

@ -0,0 +1,10 @@
import { Poly } from './poly'
import { LineExtension as MarkerLineExtension } from '../container/marker-ext'
import { ObjUtil } from '../../util/obj'
declare module './poly' {
interface Poly<TSVGPolyElement extends SVGPolygonElement | SVGPolylineElement>
extends MarkerLineExtension<TSVGPolyElement> {}
}
ObjUtil.applyMixins(Poly, MarkerLineExtension)

View File

@ -0,0 +1,73 @@
import { PointArray } from '../../struct/point-array'
import { Util } from '../util'
import { Shape } from './shape'
export class Poly<
TSVGPolyElement extends SVGPolygonElement | SVGPolylineElement
> extends Shape<TSVGPolyElement> {
protected arr: PointArray | null
x(): number
x(x: number | string): this
x(x?: number | string) {
return x == null ? this.bbox().x : this.move(x, this.bbox().y)
}
y(): number
y(y: number | string): this
y(y?: number | string) {
return y == null ? this.bbox().y : this.move(this.bbox().x, y)
}
width(): number
width(w: number | string): this
width(w?: number | string) {
return w == null ? this.bbox().width : this.size(w, this.bbox().height)
}
height(): number
height(h: number | string): this
height(h?: number | string) {
return h == null ? this.bbox().height : this.size(this.bbox().width, h)
}
array() {
if (this.arr == null) {
this.arr = new PointArray(this.attr('points'))
}
return this.arr
}
move(x: number | string, y: number | string) {
return this.attr('points', this.array().move(x, y).toString())
}
plot(): PointArray
plot(d: string): this
plot(points: [number, number][]): this
plot(points: string | [number, number][]): this
plot(d?: string | [number, number][]) {
if (d == null) {
return this.array()
}
this.arr = null
if (typeof d === 'string') {
this.attr('points', d)
} else {
this.arr = new PointArray(d)
this.attr('points', this.arr.toString())
}
return this
}
size(width: string | number, height: string | number): this
size(width: string | number, height: string | number | null | undefined): this
size(width: string | number | null | undefined, height: string | number): this
size(width?: string | number | null, height?: string | number | null) {
const s = Util.proportionalSize(this, width, height)
return this.attr('points', this.array().size(s.width, s.height).toString())
}
}

View File

@ -0,0 +1,17 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Polygon } from './polygon'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
polygon(attrs?: Attrs | null): Polygon
polygon(points: string, attrs?: Attrs | null): Polygon
polygon(points: [number, number][], attrs?: Attrs | null): Polygon
polygon(
points?: string | [number, number][] | Attrs | null,
attrs?: Attrs | null,
) {
return Polygon.create(points, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,36 @@
import { Attrs } from '../../types'
import { Poly } from './poly'
@Polygon.register('Polygon')
export class Polygon extends Poly<SVGPolygonElement> {}
export namespace Polygon {
export function create(attrs?: Attrs | null): Polygon
export function create(points: string, attrs?: Attrs | null): Polygon
export function create(
points: [number, number][],
attrs?: Attrs | null,
): Polygon
export function create(
points?: string | [number, number][] | Attrs | null,
attrs?: Attrs | null,
): Polygon
export function create(
points?: string | [number, number][] | null | Attrs,
attrs?: Attrs,
) {
const poly = new Polygon()
if (points != null) {
if (Array.isArray(points) || typeof points === 'string') {
poly.plot(points)
if (attrs) {
poly.attr(attrs)
}
} else {
poly.attr(points)
}
}
return poly
}
}

View File

@ -0,0 +1,17 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Polyline } from './polyline'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
polyline(attrs?: Attrs | null): Polyline
polyline(points: string, attrs?: Attrs | null): Polyline
polyline(points: [number, number][], attrs?: Attrs | null): Polyline
polyline(
points?: string | [number, number][] | Attrs | null,
attrs?: Attrs | null,
) {
return Polyline.create(points, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,36 @@
import { Attrs } from '../../types'
import { Poly } from './poly'
@Polyline.register('Polyline')
export class Polyline extends Poly<SVGPolylineElement> {}
export namespace Polyline {
export function create(attrs?: Attrs | null): Polyline
export function create(points: string, attrs?: Attrs | null): Polyline
export function create(
points: [number, number][],
attrs?: Attrs | null,
): Polyline
export function create(
points?: string | [number, number][] | Attrs | null,
attrs?: Attrs | null,
): Polyline
export function create(
points?: string | [number, number][] | Attrs | null,
attrs?: Attrs | null,
) {
const poly = new Polyline()
if (points != null) {
if (Array.isArray(points) || typeof points === 'string') {
poly.plot(points)
if (attrs) {
poly.attr(attrs)
}
} else {
poly.attr(points)
}
}
return poly
}
}

View File

@ -0,0 +1,22 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Rect } from './rect'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
rect(attrs?: Attrs | null): Rect
rect(size: number | string, attrs?: Attrs | null): Rect
rect(
width: number | string,
height: string | number,
attrs?: Attrs | null,
): Rect
rect(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
return Rect.create(width, height, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,62 @@
import { Attrs } from '../../types'
import { Shape } from './shape'
@Rect.register('Rect')
export class Rect extends Shape<SVGRectElement> {
rx(): number
rx(rx: string | number | null): this
rx(rx?: string | number | null) {
return this.attr<number>('rx', rx)
}
ry(): number
ry(ry: string | number | null): this
ry(ry?: string | number | null) {
return this.attr<number>('ry', ry)
}
radius(rx: string | number, ry: string | number = rx) {
return this.rx(rx).ry(ry)
}
}
export namespace Rect {
export function create(attrs?: Attrs | null): Rect
export function create(size: number | string, attrs?: Attrs | null): Rect
export function create(
width: number | string,
height: string | number,
attrs?: Attrs | null,
): Rect
export function create(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
): Rect
export function create(
width?: number | string | Attrs | null,
height?: number | string | Attrs | null,
attrs?: Attrs | null,
) {
const rect = new Rect()
if (width == null) {
rect.size(0, 0)
} else if (typeof width === 'object') {
rect.size(0, 0).attr(width)
} else if (height != null && typeof height === 'object') {
rect.size(width, width).attr(height)
} else {
if (typeof height === 'undefined') {
rect.size(width, width)
} else {
rect.size(width, height)
}
if (attrs) {
rect.attr(attrs)
}
}
return rect
}
}

View File

@ -0,0 +1,15 @@
import { Attrs } from '../../types'
import { VectorElement } from '../element'
@Shape.register('Shape')
export class Shape<
TSVGGraphicsElement extends SVGGraphicsElement
> extends VectorElement<TSVGGraphicsElement> {
constructor()
constructor(attrs: Attrs | null)
constructor(node: TSVGGraphicsElement | null, attrs?: Attrs | null)
// eslint-disable-next-line no-useless-constructor
constructor(node?: TSVGGraphicsElement | Attrs | null, attrs?: Attrs | null) {
super(node, attrs)
}
}

View File

@ -0,0 +1,21 @@
import { Vector } from '../vector'
import { Style } from './style'
export class ElementExtension<
TSVGElement extends SVGElement = SVGElement
> extends Vector<TSVGElement> {
style(
selector: string,
style?: Record<string | number, string | number>,
): Style {
return new Style().addRule(selector, style).appendTo(this)
}
fontFace(
name: string,
source: string,
parameters: Record<string | number, string | number>,
): Style {
return new Style().addFont(name, source, parameters).appendTo(this)
}
}

View File

@ -0,0 +1,54 @@
import { VectorElement } from '../element'
@Style.register('Style')
export class Style extends VectorElement<SVGStyleElement> {
addText(content = '') {
this.node.textContent += content
return this
}
addFont(
name: string,
source: string,
parameters: Record<string, string | number> = {},
) {
return this.addRule('@font-face', {
fontFamily: name,
src: source,
...parameters,
})
}
addRule(selector: string, object?: Record<string | number, string | number>) {
return this.addText(Style.cssRule(selector, object))
}
}
export namespace Style {
const unCamelCase = (s: string) => {
return s.replace(/([A-Z])/g, (m, g) => `-${g.toLowerCase()}`)
}
export function cssRule(
selector?: string,
rule?: Record<string | number, string | number>,
) {
if (!selector) {
return ''
}
if (!rule) {
return selector
}
let ret = `${selector} {`
Object.keys(rule).forEach((key) => {
ret += `${unCamelCase(key)}: ${rule[key]};`
})
ret += '}'
return ret
}
}

View File

@ -0,0 +1,105 @@
import { Global } from '../../global'
import { Box } from '../../struct/box'
import { SVGNumber } from '../../struct/svg-number'
import { Shape } from './shape'
export class TextBase<
TSVGTextElement extends SVGTextElement | SVGTSpanElement | SVGTextPathElement
> extends Shape<TSVGTextElement> {
protected building = false
build(building = false) {
this.building = building
return this
}
plain(text: string) {
if (this.building === false) {
this.clear()
}
this.node.append(Global.document.createTextNode(text))
return this
}
length() {
return this.node.getComputedTextLength()
}
x(): number
x(x: number | string, box?: Box): this
x(x?: number | string, box = this.bbox()) {
if (x == null) {
return box.x
}
return this.attr(
'x',
this.attr<number>('x') + SVGNumber.toNumber(x) - box.x,
)
}
y(): number
y(y: number | string, box?: Box): this
y(y?: number | string, box = this.bbox()) {
if (y == null) {
return box.y
}
return this.attr(
'y',
this.attr<number>('y') + SVGNumber.toNumber(y) - box.y,
)
}
move(x: number | string, y: number | string, box = this.bbox()) {
return this.x(x, box).y(y, box)
}
cx(): number
cx(x: number | string, box?: Box): this
cx(x?: number | string, box = this.bbox()) {
if (x == null) {
return box.cx
}
return this.attr(
'x',
this.attr<number>('x') + SVGNumber.toNumber(x) - box.cx,
)
}
cy(): number
cy(y: number | string, box?: Box): this
cy(y?: number | string, box = this.bbox()) {
if (y == null) {
return box.cy
}
return this.attr(
'y',
this.attr<number>('y') + SVGNumber.toNumber(y) - box.cy,
)
}
center(x: number | string, y: number | string, box = this.bbox()) {
return this.cx(x, box).cy(y, box)
}
ax(): number
ax(x: number | string): this
ax(x?: number | string) {
return this.attr<number>('x', x)
}
ay(): number
ay(y: number | string): this
ay(y?: number | string) {
return this.attr<number>('y', y)
}
amove(x: number | string, y: number | string) {
return this.ax(x).ay(y)
}
}

View File

@ -0,0 +1,30 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Text } from './text'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
text(attrs?: Attrs | null): Text
text(text: string, attrs?: Attrs | null): Text
text(text?: string | Attrs | null, attrs?: Attrs | null) {
return Text.create(text, attrs).appendTo(this)
}
plain(attrs?: Attrs | null): Text
plain(text: string, attrs?: Attrs | null): Text
plain(text?: string | Attrs | null, attrs?: Attrs) {
const t = Text.create()
if (text) {
if (typeof text === 'string') {
t.plain(text)
if (attrs) {
t.attr(attrs)
}
} else {
t.attr(text)
}
}
return t
}
}

View File

@ -0,0 +1,12 @@
import { ObjUtil } from '../../util/obj'
import { TextExtension as TextpathExtension } from './textpath-ext'
import { TextExtension as TspanExtension } from './tspan-ext'
import { Text } from './text'
declare module './text' {
interface Text<TSVGTextElement extends SVGTextElement | SVGTextPathElement>
extends TextpathExtension<TSVGTextElement>,
TspanExtension<TSVGTextElement> {}
}
ObjUtil.applyMixins(Text, TspanExtension, TextpathExtension)

View File

@ -0,0 +1,177 @@
import { Attrs } from '../../types'
import { Global } from '../../global'
import { SVGNumber } from '../../struct/svg-number'
import { Adopter } from '../adopter'
import { Tspan } from './tspan'
import { TextBase } from './text-base'
@Text.register('Text')
export class Text<
TSVGTextElement extends SVGTextElement | SVGTextPathElement = SVGTextElement
> extends TextBase<TSVGTextElement> {
public assets: Record<string | number, any> & {
leading: number
}
protected rebuilding = true
attr(): Attrs
attr(names: string[]): Attrs
attr<T extends string | number = string>(name: string): T
attr(name: string, value: null): this
attr(name: string, value: string | number, ns?: string): this
attr(attrs: Attrs): this
attr<T extends string | number>(
name?: string,
value?: string | number | null,
ns?: string,
): T | this
attr(
name?: string | string[] | Attrs,
value?: string | number | null,
ns?: string,
): Attrs | string | number | this
attr(
name?: string | string[] | Attrs,
value?: string | number | null,
ns?: string,
) {
if (name === 'leading') {
return this.leading(value)
}
const ret = super.attr(name, value, ns)
if (name === 'font-size' || name === 'x') {
this.rebuild()
}
return ret
}
restoreAssets() {
super.restoreAssets()
if (this.assets.leading == null) {
this.assets.leading = 1.3
}
return this
}
leading(): number
leading(value: SVGNumber.Raw): this
leading(value?: SVGNumber.Raw) {
if (value == null) {
return this.assets.leading
}
this.assets.leading = SVGNumber.create(value).valueOf()
return this.rebuild()
}
rebuild(rebuilding?: boolean) {
if (typeof rebuilding === 'boolean') {
this.rebuilding = rebuilding
}
// define position of all lines
if (this.rebuilding) {
let blankLineOffset = 0
const leading = this.leading()
this.eachChild<Tspan>((child, index) => {
const fontSize = Global.window
.getComputedStyle(this.node)
.getPropertyValue('font-size')
const dy = leading * Number.parseFloat(fontSize)
if (child.assets.newLined) {
child.attr('x', this.attr('x'))
if (child.text() === '\n') {
blankLineOffset += dy
} else {
child.attr('dy', index ? dy + blankLineOffset : 0)
blankLineOffset = 0
}
}
})
this.trigger('rebuild')
}
return this
}
text(): string
text(text: string | ((this: Tspan, tspan: Tspan) => void)): this
text(text?: string | ((this: Tspan, tspan: Tspan) => void)) {
// getter
if (text === undefined) {
const children = this.node.childNodes
let firstLine = 0
let content = ''
for (let index = 0, l = children.length; index < l; index += 1) {
// skip textPaths - they are no lines
if (children[index].nodeName === 'textPath') {
if (index === 0) {
firstLine = 1
}
continue
}
// add newline if its not the first child and newLined is set to true
if (
index !== firstLine &&
children[index].nodeType !== 3 &&
Adopter.adopt<Tspan>(children[index]).assets.newLined === true
) {
content += '\n'
}
// add content of this node
content += children[index].textContent
}
return content
}
this.clear().build(true)
if (typeof text === 'function') {
text.call(this, this)
} else {
const lines = `${text}`.split('\n')
lines.forEach((line) => this.newLine(line))
}
return this.build(false).rebuild()
}
newLine(text = '') {
return this.tspan(text).newLine()
}
}
export namespace Text {
export function create(attrs?: Attrs | null): Text
export function create(text: string, attrs?: Attrs | null): Text
export function create(
text?: string | Attrs | null,
attrs?: Attrs | null,
): Text
export function create(text?: string | Attrs | null, attrs?: Attrs | null) {
const t = new Text()
if (text) {
if (typeof text === 'string') {
t.text(text)
if (attrs) {
t.attr(attrs)
}
} else {
t.attr(text)
}
}
return t
}
}

View File

@ -0,0 +1,81 @@
import { DomUtil } from '../../util/dom'
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { VectorElement } from '../element'
import { Text } from './text'
import { Path } from './path'
import { TextPath } from './textpath'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
textPath(text: string | Text, path: string | Path, attrs?: Attrs) {
const instance = text instanceof Text ? text : Text.create(text)
const textPath = instance.path(path)
if (attrs) {
textPath.attr(attrs)
}
return textPath
}
}
export class TextExtension<
TTSVGTextElement extends SVGTextElement | SVGTextPathElement
> extends Vector<TTSVGTextElement> {
path(track: string | Path, importNodes = true, attrs?: Attrs | null) {
const textPath = new TextPath()
let path: Path | null = null
if (track instanceof Path) {
path = track
} else {
const defs = this.defs()
if (defs) {
path = defs.path(track)
}
}
if (path) {
textPath.attr('href', `#${path.toString()}`, DomUtil.namespaces.xlink)
// Transplant all nodes from text to textPath
let node
if (importNodes) {
while ((node = this.node.firstChild)) {
textPath.node.append(node)
}
}
}
if (attrs) {
textPath.attr(attrs)
}
return this.put(textPath)
}
textPath() {
return this.findOne<TextPath>('textPath')
}
}
export class PathExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
text(text: string | Text, attrs?: Attrs) {
const instance = text instanceof Text ? text : Text.create(text)
if (!instance.parent()) {
this.after(instance)
}
if (attrs) {
instance.attr(attrs)
}
return instance.path(this as any)
}
targets<TVector extends VectorElement>() {
return Path.find<TVector>(`svg [mask*="${this.id()}"]`)
}
}

View File

@ -0,0 +1,29 @@
import { PathArray } from '../../struct/path-array'
import { Text } from './text'
import { Path } from './path'
Text.register('TextPath')
export class TextPath extends Text<SVGTextPathElement> {
array() {
const track = this.track()
return track ? track.array() : null
}
plot(): PathArray
plot(d: string | Path.Segment[] | PathArray): this
plot(d?: string | Path.Segment[] | PathArray) {
const track = this.track()
if (d == null) {
return track ? track.plot() : null
}
if (track) {
track.plot(d)
}
return this
}
track() {
return this.reference<Path>('href')
}
}

View File

@ -0,0 +1,17 @@
import { Attrs } from '../../types'
import { TextBase } from './text-base'
import { Tspan } from './tspan'
export class TextExtension<
TSVGTextElement extends SVGTextElement | SVGTSpanElement | SVGTextPathElement
> extends TextBase<TSVGTextElement> {
tspan(text = '', attrs?: Attrs | null) {
const tspan = Tspan.create(text, attrs)
if (!this.building) {
this.clear()
}
return tspan.appendTo(this)
}
}

View File

@ -0,0 +1,79 @@
import { Attrs } from '../../types'
import { Global } from '../../global'
import { TextBase } from './text-base'
import { Text } from './text'
@Tspan.register('Tspan')
export class Tspan extends TextBase<SVGTSpanElement> {
public assets: Record<string | number, any> & {
leading?: number
newLined?: boolean
}
dx(): number
dx(dx: number | string): this
dx(dx?: number | string) {
return this.attr<number>('dx', dx)
}
dy(): number
dy(dy: number | string): this
dy(dy?: number | string) {
return this.attr<number>('dy', dy)
}
newLine() {
// mark new line
this.assets.newLined = true
const text = this.parent()
if (text == null || !(text instanceof Text)) {
return this
}
const index = text.indexOf(this)
const fontSize = Global.window
.getComputedStyle(this.node)
.getPropertyValue('font-size')
const dy = text.assets.leading * Number.parseFloat(fontSize)
return this.dy(index ? dy : 0).attr('x', text.x())
}
text(): string
text(text: string | ((this: Tspan, tspan: Tspan) => void)): this
text(text?: string | ((this: Tspan, tspan: Tspan) => void)) {
if (text == null) {
return this.node.textContent + (this.assets.newLined ? '\n' : '')
}
if (typeof text === 'function') {
this.clear().build(true)
text.call(this, this)
this.build(false)
} else {
this.plain(text)
}
return this
}
}
export namespace Tspan {
export function create(): Tspan
export function create(attrs: Attrs | null): Tspan
export function create(text: string, attrs?: Attrs | null): Tspan
export function create(text?: string | Attrs | null, attrs?: Attrs | null) {
const tspan = new Tspan()
if (text != null) {
if (typeof text === 'string') {
tspan.text(text)
if (attrs) {
tspan.attr(attrs)
}
} else {
tspan.attr(text)
}
}
return tspan
}
}

View File

@ -0,0 +1,18 @@
import { Attrs } from '../../types'
import { Vector } from '../vector'
import { Use } from './use'
export class ContainerExtension<
TSVGElement extends SVGElement
> extends Vector<TSVGElement> {
use(attrs?: Attrs | null): Use
use(elementId: string, attrs?: Attrs | null): Use
use(elementId: string, file: string, attrs?: Attrs | null): Use
use(
elementId?: string | Attrs | null,
file?: string | Attrs | null,
attrs?: Attrs | null,
) {
return Use.create(elementId, file, attrs).appendTo(this)
}
}

View File

@ -0,0 +1,58 @@
import { DomUtil } from '../../util/dom'
import { Attrs } from '../../types'
import { Shape } from './shape'
@Use.register('Use')
export class Use extends Shape<SVGUseElement> {
use(elementId: string, file?: string) {
return this.attr(
'href',
`${file || ''}#${elementId}`,
DomUtil.namespaces.xlink,
)
}
}
export namespace Use {
export function create(attrs?: Attrs | null): Use
export function create(elementId: string, attrs?: Attrs | null): Use
export function create(
elementId: string,
file: string,
attrs?: Attrs | null,
): Use
export function create(
elementId?: string | Attrs | null,
file?: string | Attrs | null,
attrs?: Attrs | null,
): Use
export function create(
elementId?: string | Attrs | null,
file?: string | Attrs | null,
attrs?: Attrs | null,
) {
const use = new Use()
if (elementId) {
if (typeof elementId === 'string') {
if (file) {
if (typeof file === 'string') {
use.use(elementId, file)
if (attrs) {
use.attr(attrs)
}
} else {
use.use(elementId).attr(file)
}
} else {
use.use(elementId)
if (attrs) {
use.attr(attrs)
}
}
} else {
use.attr(elementId)
}
}
return use
}
}

View File

@ -0,0 +1,31 @@
import type { Box } from '../struct/box'
import type { VectorElement } from './element'
import { SVGNumber } from '../struct/svg-number'
export namespace Util {
export function proportionalSize(
element: VectorElement,
width?: string | number | null,
height?: string | number | null,
box?: Box,
) {
if (width == null || height == null) {
const bbox = box || element.bbox()
let w = width
let h = height
if (w == null) {
w = (bbox.width / bbox.height) * SVGNumber.toNumber(h!)
} else if (h == null) {
h = (bbox.height / bbox.width) * SVGNumber.toNumber(w)
}
return { width: w, height: h! }
}
return {
width,
height,
}
}
}

View File

@ -0,0 +1,96 @@
import { KeyValue, Attrs } from '../types'
import { Adopter } from './adopter'
import { Registry } from './registry'
import { Svg } from './container/svg'
import { Dom } from './dom'
const PERSIST_ATTR_NAME = 'vector:data'
export class Vector<TSVGElement extends SVGElement> extends Dom<TSVGElement> {
public readonly node: TSVGElement
protected assets: KeyValue
constructor()
constructor(attrs: Attrs | null)
constructor(node?: TSVGElement | string | null, attrs?: Attrs | null)
constructor(node?: TSVGElement | string | Attrs | null, attrs?: Attrs | null)
constructor(
node?: TSVGElement | string | Attrs | null,
attrs?: Attrs | null,
) {
super(node, attrs)
this.restoreAssets()
Adopter.ref(this.node, this)
}
root(): Svg | null {
const parent = this.parent<Svg>(Registry.getRoot())
return parent ? parent.root() : null
}
defs() {
const root = this.root()
return root ? root.defs() : null
}
parents(until: Adopter.Target<Svg> | null = this.root()) {
const stop = until ? Adopter.makeInstance<Dom>(until) : null
const parents: Dom[] = []
let parent = this.parent()
while (parent && !parent.isDocument() && !parent.isDocumentFragment()) {
parents.push(parent)
if (stop && parent.node === stop.node) {
break
}
parent = parent.parent()
}
return parents
}
reference<T extends Dom>(attribute: string) {
const value = this.attr(attribute)
if (!value) {
return null
}
// reference id
const reg = /(#[_a-z][\w-]*)/i
const matches = `${value}`.match(reg)
return matches ? Adopter.makeInstance<T>(matches[1]) : null
}
getAssets<T>(): T
getAssets<T>(key: string, defaultValue: T): T
getAssets(key?: string, defaultValue?: any) {
if (key == null) {
return this.assets
}
const val = this.assets[key]
return val === undefined ? defaultValue : val
}
resetAssets(data: KeyValue) {
this.assets = data
return this
}
restoreAssets() {
const raw = this.node.getAttribute(PERSIST_ATTR_NAME)
if (raw) {
this.resetAssets(JSON.parse(raw) || {})
} else {
this.resetAssets({})
}
return this
}
storeAssets() {
this.node.removeAttribute(PERSIST_ATTR_NAME)
if (this.assets && Object.keys(this.assets).length > 0) {
this.node.setAttribute(PERSIST_ATTR_NAME, JSON.stringify(this.assets))
}
return super.storeAssets()
}
}

View File

@ -0,0 +1,2 @@
export * from './window'
export * from './version'

View File

@ -0,0 +1,9 @@
import { version } from './version'
describe('version', () => {
it('should match the `version` field of package.json', () => {
// eslint-disable-next-line
const expected = require('../../package.json').version
expect(version).toBe(expected)
})
})

View File

@ -0,0 +1,7 @@
/* eslint-disable */
/**
* Auto generated version file, do not modify it!
*/
const version = '1.0.0'
export { version }

Some files were not shown because too many files have changed in this diff Show More