feat: ✨ add library for manipulating and animating SVG
This commit is contained in:
@ -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",
|
||||
|
26
examples/x6-example-features/src/pages/vector/svg.tsx
Normal file
26
examples/x6-example-features/src/pages/vector/svg.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
3
examples/x6-example-features/src/pages/vector/version.ts
Normal file
3
examples/x6-example-features/src/pages/vector/version.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { version } from '@antv/x6-vector'
|
||||
|
||||
console.log(version)
|
1
packages/x6-vector/README.md
Normal file
1
packages/x6-vector/README.md
Normal file
@ -0,0 +1 @@
|
||||
# x6-vector
|
11
packages/x6-vector/karma.conf.js
Normal file
11
packages/x6-vector/karma.conf.js
Normal 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'],
|
||||
},
|
||||
)
|
127
packages/x6-vector/package.json
Normal file
127
packages/x6-vector/package.json
Normal 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"
|
||||
}
|
||||
}
|
12
packages/x6-vector/rollup.config.js
Normal file
12
packages/x6-vector/rollup.config.js
Normal 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,
|
||||
},
|
||||
],
|
||||
})
|
86
packages/x6-vector/src/element/adopter.ts
Normal file
86
packages/x6-vector/src/element/adopter.ts
Normal 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
|
||||
}
|
||||
}
|
20
packages/x6-vector/src/element/base.ts
Normal file
20
packages/x6-vector/src/element/base.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
59
packages/x6-vector/src/element/container/a-ext.ts
Normal file
59
packages/x6-vector/src/element/container/a-ext.ts
Normal 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
|
||||
}
|
||||
}
|
39
packages/x6-vector/src/element/container/a.ts
Normal file
39
packages/x6-vector/src/element/container/a.ts
Normal 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
|
||||
}
|
||||
}
|
56
packages/x6-vector/src/element/container/clippath-ext.ts
Normal file
56
packages/x6-vector/src/element/container/clippath-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
25
packages/x6-vector/src/element/container/clippath.ts
Normal file
25
packages/x6-vector/src/element/container/clippath.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
79
packages/x6-vector/src/element/container/container-mixins.ts
Normal file
79
packages/x6-vector/src/element/container/container-mixins.ts
Normal 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,
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
49
packages/x6-vector/src/element/container/container.ts
Normal file
49
packages/x6-vector/src/element/container/container.ts
Normal 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
|
||||
}
|
||||
}
|
55
packages/x6-vector/src/element/container/defs-mixins.ts
Normal file
55
packages/x6-vector/src/element/container/defs-mixins.ts
Normal 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,
|
||||
)
|
15
packages/x6-vector/src/element/container/defs.ts
Normal file
15
packages/x6-vector/src/element/container/defs.ts
Normal 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
|
||||
}
|
||||
}
|
11
packages/x6-vector/src/element/container/g-ext.ts
Normal file
11
packages/x6-vector/src/element/container/g-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
15
packages/x6-vector/src/element/container/g.ts
Normal file
15
packages/x6-vector/src/element/container/g.ts
Normal 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
|
||||
}
|
||||
}
|
43
packages/x6-vector/src/element/container/gradient-ext.ts
Normal file
43
packages/x6-vector/src/element/container/gradient-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
75
packages/x6-vector/src/element/container/gradient-stop.ts
Normal file
75
packages/x6-vector/src/element/container/gradient-stop.ts
Normal 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
|
||||
}
|
||||
}
|
150
packages/x6-vector/src/element/container/gradient.ts
Normal file
150
packages/x6-vector/src/element/container/gradient.ts
Normal 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
|
||||
}
|
||||
}
|
110
packages/x6-vector/src/element/container/marker-ext.ts
Normal file
110
packages/x6-vector/src/element/container/marker-ext.ts
Normal 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
|
||||
}
|
||||
}
|
152
packages/x6-vector/src/element/container/marker.ts
Normal file
152
packages/x6-vector/src/element/container/marker.ts
Normal 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
|
||||
}
|
||||
}
|
55
packages/x6-vector/src/element/container/mask-ext.ts
Normal file
55
packages/x6-vector/src/element/container/mask-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
25
packages/x6-vector/src/element/container/mask.ts
Normal file
25
packages/x6-vector/src/element/container/mask.ts
Normal 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
|
||||
}
|
||||
}
|
58
packages/x6-vector/src/element/container/pattern-ext.ts
Normal file
58
packages/x6-vector/src/element/container/pattern-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
147
packages/x6-vector/src/element/container/pattern.ts
Normal file
147
packages/x6-vector/src/element/container/pattern.ts
Normal 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
|
||||
}
|
||||
}
|
11
packages/x6-vector/src/element/container/svg-ext.ts
Normal file
11
packages/x6-vector/src/element/container/svg-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
84
packages/x6-vector/src/element/container/svg.ts
Normal file
84
packages/x6-vector/src/element/container/svg.ts
Normal 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
|
||||
}
|
||||
}
|
11
packages/x6-vector/src/element/container/symbol-ext.ts
Normal file
11
packages/x6-vector/src/element/container/symbol-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
19
packages/x6-vector/src/element/container/symbol.ts
Normal file
19
packages/x6-vector/src/element/container/symbol.ts
Normal 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
|
||||
}
|
||||
}
|
24
packages/x6-vector/src/element/decorator.ts
Normal file
24
packages/x6-vector/src/element/decorator.ts
Normal 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_)
|
||||
}
|
||||
}
|
||||
}
|
157
packages/x6-vector/src/element/dom/classname.ts
Normal file
157
packages/x6-vector/src/element/dom/classname.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
103
packages/x6-vector/src/element/dom/data.ts
Normal file
103
packages/x6-vector/src/element/dom/data.ts
Normal 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
|
||||
}
|
||||
}
|
545
packages/x6-vector/src/element/dom/dom.ts
Normal file
545
packages/x6-vector/src/element/dom/dom.ts
Normal 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
|
||||
}
|
7
packages/x6-vector/src/element/dom/event-alias.ts
Normal file
7
packages/x6-vector/src/element/dom/event-alias.ts
Normal 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
|
445
packages/x6-vector/src/element/dom/event-core.ts
Normal file
445
packages/x6-vector/src/element/dom/event-core.ts
Normal 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
|
||||
}
|
||||
}
|
163
packages/x6-vector/src/element/dom/event-hook.ts
Normal file
163
packages/x6-vector/src/element/dom/event-hook.ts
Normal 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
|
||||
}
|
256
packages/x6-vector/src/element/dom/event-object.ts
Normal file
256
packages/x6-vector/src/element/dom/event-object.ts
Normal 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
|
||||
}
|
||||
}
|
260
packages/x6-vector/src/element/dom/event-special.ts
Normal file
260
packages/x6-vector/src/element/dom/event-special.ts
Normal 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
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
43
packages/x6-vector/src/element/dom/event-store.ts
Normal file
43
packages/x6-vector/src/element/dom/event-store.ts
Normal 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)
|
||||
}
|
||||
}
|
735
packages/x6-vector/src/element/dom/event-types.ts
Normal file
735
packages/x6-vector/src/element/dom/event-types.ts
Normal 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>
|
||||
}
|
165
packages/x6-vector/src/element/dom/event-util.ts
Normal file
165
packages/x6-vector/src/element/dom/event-util.ts
Normal 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
|
||||
}
|
||||
}
|
263
packages/x6-vector/src/element/dom/event.ts
Normal file
263
packages/x6-vector/src/element/dom/event.ts
Normal 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
|
||||
}
|
1
packages/x6-vector/src/element/dom/index.ts
Normal file
1
packages/x6-vector/src/element/dom/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Dom } from './dom'
|
432
packages/x6-vector/src/element/dom/listener.ts
Normal file
432
packages/x6-vector/src/element/dom/listener.ts
Normal 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
|
||||
}
|
||||
}
|
38
packages/x6-vector/src/element/dom/memory.ts
Normal file
38
packages/x6-vector/src/element/dom/memory.ts
Normal 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
|
||||
}
|
||||
}
|
152
packages/x6-vector/src/element/dom/primer.ts
Normal file
152
packages/x6-vector/src/element/dom/primer.ts
Normal 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
|
||||
}
|
24
packages/x6-vector/src/element/dom/style-hook.ts
Normal file
24
packages/x6-vector/src/element/dom/style-hook.ts
Normal 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
|
||||
}
|
||||
}
|
402
packages/x6-vector/src/element/dom/style-util.ts
Normal file
402
packages/x6-vector/src/element/dom/style-util.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
112
packages/x6-vector/src/element/dom/style.ts
Normal file
112
packages/x6-vector/src/element/dom/style.ts
Normal 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
|
||||
}
|
19
packages/x6-vector/src/element/element-mixins.ts
Normal file
19
packages/x6-vector/src/element/element-mixins.ts
Normal 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,
|
||||
)
|
541
packages/x6-vector/src/element/element.ts
Normal file
541
packages/x6-vector/src/element/element.ts
Normal 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]
|
||||
}
|
||||
}
|
38
packages/x6-vector/src/element/index.ts
Normal file
38
packages/x6-vector/src/element/index.ts
Normal 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'
|
60
packages/x6-vector/src/element/registry.ts
Normal file
60
packages/x6-vector/src/element/registry.ts
Normal 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
|
||||
}
|
||||
}
|
13
packages/x6-vector/src/element/shape/circle-ext.ts
Normal file
13
packages/x6-vector/src/element/shape/circle-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
99
packages/x6-vector/src/element/shape/circle.ts
Normal file
99
packages/x6-vector/src/element/shape/circle.ts
Normal 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
|
||||
}
|
||||
}
|
22
packages/x6-vector/src/element/shape/ellipse-ext.ts
Normal file
22
packages/x6-vector/src/element/shape/ellipse-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
118
packages/x6-vector/src/element/shape/ellipse.ts
Normal file
118
packages/x6-vector/src/element/shape/ellipse.ts
Normal 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
|
||||
}
|
||||
}
|
22
packages/x6-vector/src/element/shape/foreignobject-ext.ts
Normal file
22
packages/x6-vector/src/element/shape/foreignobject-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
49
packages/x6-vector/src/element/shape/foreignobject.ts
Normal file
49
packages/x6-vector/src/element/shape/foreignobject.ts
Normal 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
|
||||
}
|
||||
}
|
33
packages/x6-vector/src/element/shape/fragment.ts
Normal file
33
packages/x6-vector/src/element/shape/fragment.ts
Normal 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)
|
||||
}
|
||||
}
|
18
packages/x6-vector/src/element/shape/image-ext.ts
Normal file
18
packages/x6-vector/src/element/shape/image-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
108
packages/x6-vector/src/element/shape/image.ts
Normal file
108
packages/x6-vector/src/element/shape/image.ts
Normal 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
|
||||
})
|
||||
}
|
26
packages/x6-vector/src/element/shape/line-ext.ts
Normal file
26
packages/x6-vector/src/element/shape/line-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
9
packages/x6-vector/src/element/shape/line-mixins.ts
Normal file
9
packages/x6-vector/src/element/shape/line-mixins.ts
Normal 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)
|
129
packages/x6-vector/src/element/shape/line.ts
Normal file
129
packages/x6-vector/src/element/shape/line.ts
Normal 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
|
||||
}
|
||||
}
|
17
packages/x6-vector/src/element/shape/path-ext.ts
Normal file
17
packages/x6-vector/src/element/shape/path-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
12
packages/x6-vector/src/element/shape/path-mixins.ts
Normal file
12
packages/x6-vector/src/element/shape/path-mixins.ts
Normal 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)
|
347
packages/x6-vector/src/element/shape/path-util.ts
Normal file
347
packages/x6-vector/src/element/shape/path-util.ts
Normal 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
|
||||
}, '')
|
||||
}
|
117
packages/x6-vector/src/element/shape/path.ts
Normal file
117
packages/x6-vector/src/element/shape/path.ts
Normal 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
|
||||
}
|
10
packages/x6-vector/src/element/shape/poly-mixins.ts
Normal file
10
packages/x6-vector/src/element/shape/poly-mixins.ts
Normal 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)
|
73
packages/x6-vector/src/element/shape/poly.ts
Normal file
73
packages/x6-vector/src/element/shape/poly.ts
Normal 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())
|
||||
}
|
||||
}
|
17
packages/x6-vector/src/element/shape/polygon-ext.ts
Normal file
17
packages/x6-vector/src/element/shape/polygon-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
36
packages/x6-vector/src/element/shape/polygon.ts
Normal file
36
packages/x6-vector/src/element/shape/polygon.ts
Normal 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
|
||||
}
|
||||
}
|
17
packages/x6-vector/src/element/shape/polyline-ext.ts
Normal file
17
packages/x6-vector/src/element/shape/polyline-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
36
packages/x6-vector/src/element/shape/polyline.ts
Normal file
36
packages/x6-vector/src/element/shape/polyline.ts
Normal 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
|
||||
}
|
||||
}
|
22
packages/x6-vector/src/element/shape/rect-ext.ts
Normal file
22
packages/x6-vector/src/element/shape/rect-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
62
packages/x6-vector/src/element/shape/rect.ts
Normal file
62
packages/x6-vector/src/element/shape/rect.ts
Normal 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
|
||||
}
|
||||
}
|
15
packages/x6-vector/src/element/shape/shape.ts
Normal file
15
packages/x6-vector/src/element/shape/shape.ts
Normal 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)
|
||||
}
|
||||
}
|
21
packages/x6-vector/src/element/shape/style-ext.ts
Normal file
21
packages/x6-vector/src/element/shape/style-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
54
packages/x6-vector/src/element/shape/style.ts
Normal file
54
packages/x6-vector/src/element/shape/style.ts
Normal 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
|
||||
}
|
||||
}
|
105
packages/x6-vector/src/element/shape/text-base.ts
Normal file
105
packages/x6-vector/src/element/shape/text-base.ts
Normal 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)
|
||||
}
|
||||
}
|
30
packages/x6-vector/src/element/shape/text-ext.ts
Normal file
30
packages/x6-vector/src/element/shape/text-ext.ts
Normal 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
|
||||
}
|
||||
}
|
12
packages/x6-vector/src/element/shape/text-mixins.ts
Normal file
12
packages/x6-vector/src/element/shape/text-mixins.ts
Normal 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)
|
177
packages/x6-vector/src/element/shape/text.ts
Normal file
177
packages/x6-vector/src/element/shape/text.ts
Normal 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
|
||||
}
|
||||
}
|
81
packages/x6-vector/src/element/shape/textpath-ext.ts
Normal file
81
packages/x6-vector/src/element/shape/textpath-ext.ts
Normal 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()}"]`)
|
||||
}
|
||||
}
|
29
packages/x6-vector/src/element/shape/textpath.ts
Normal file
29
packages/x6-vector/src/element/shape/textpath.ts
Normal 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')
|
||||
}
|
||||
}
|
17
packages/x6-vector/src/element/shape/tspan-ext.ts
Normal file
17
packages/x6-vector/src/element/shape/tspan-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
79
packages/x6-vector/src/element/shape/tspan.ts
Normal file
79
packages/x6-vector/src/element/shape/tspan.ts
Normal 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
|
||||
}
|
||||
}
|
18
packages/x6-vector/src/element/shape/use-ext.ts
Normal file
18
packages/x6-vector/src/element/shape/use-ext.ts
Normal 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)
|
||||
}
|
||||
}
|
58
packages/x6-vector/src/element/shape/use.ts
Normal file
58
packages/x6-vector/src/element/shape/use.ts
Normal 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
|
||||
}
|
||||
}
|
31
packages/x6-vector/src/element/util.ts
Normal file
31
packages/x6-vector/src/element/util.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
96
packages/x6-vector/src/element/vector.ts
Normal file
96
packages/x6-vector/src/element/vector.ts
Normal 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()
|
||||
}
|
||||
}
|
2
packages/x6-vector/src/global/index.ts
Normal file
2
packages/x6-vector/src/global/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './window'
|
||||
export * from './version'
|
9
packages/x6-vector/src/global/version.test.ts
Normal file
9
packages/x6-vector/src/global/version.test.ts
Normal 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)
|
||||
})
|
||||
})
|
7
packages/x6-vector/src/global/version.ts
Normal file
7
packages/x6-vector/src/global/version.ts
Normal 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
Reference in New Issue
Block a user