refactor: ♻️ refactor vector
This commit is contained in:
@ -1,6 +1,5 @@
|
||||
import { Entity } from '../../types'
|
||||
import { Mixin } from '../../util/mixin'
|
||||
import { When, Options } from '../types'
|
||||
import type { When, Options } from '../types'
|
||||
import { Easing } from '../stepper/easing'
|
||||
import { Morpher } from '../morpher/morpher'
|
||||
import { Stepper } from '../stepper/stepper'
|
||||
@ -14,13 +13,10 @@ import {
|
||||
RetargetMethod,
|
||||
} from './types'
|
||||
import { Util } from './util'
|
||||
import { Registry } from '../registry'
|
||||
import { AnimatorType } from '../extension/types'
|
||||
|
||||
export class Animator<
|
||||
TElement extends Entity = Entity,
|
||||
TTarget extends Entity = Entity,
|
||||
TAnimator = AnimatorType<TElement>
|
||||
TAnimator,
|
||||
TOwner extends Animator.Owner = Animator.Owner
|
||||
> {
|
||||
public readonly id: number
|
||||
public readonly declarative: boolean
|
||||
@ -29,9 +25,9 @@ export class Animator<
|
||||
protected reseted = true
|
||||
protected persisted: number | boolean
|
||||
|
||||
protected target: TTarget
|
||||
protected owner: TOwner
|
||||
protected stepper: Stepper
|
||||
protected t: Timeline | null = null
|
||||
protected timeline: Timeline | null = null
|
||||
|
||||
protected duration: number
|
||||
protected times = 1
|
||||
@ -43,8 +39,8 @@ export class Animator<
|
||||
protected previousStepTime = 0
|
||||
protected previousStepPosition: number
|
||||
|
||||
protected readonly executors: Executors<TAnimator> = []
|
||||
protected readonly history: History<TAnimator> = {}
|
||||
protected readonly executors: Executors<TAnimator> = []
|
||||
protected readonly callbacks: {
|
||||
[Key in Animator.EventNames]: any[]
|
||||
} = {
|
||||
@ -103,25 +99,25 @@ export class Animator<
|
||||
return this
|
||||
}
|
||||
|
||||
element(): TTarget
|
||||
element(target: TTarget): this
|
||||
element(target?: TTarget) {
|
||||
if (target == null) {
|
||||
return this.target
|
||||
master(): TOwner
|
||||
master(owner: TOwner): this
|
||||
master(owner?: TOwner) {
|
||||
if (owner == null) {
|
||||
return this.owner
|
||||
}
|
||||
|
||||
this.target = target
|
||||
this.owner = owner
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
timeline(): Timeline
|
||||
timeline(timeline: Timeline | null): this
|
||||
timeline(timeline?: Timeline | null) {
|
||||
scheduler(): Timeline
|
||||
scheduler(timeline: Timeline | null): this
|
||||
scheduler(timeline?: Timeline | null) {
|
||||
if (typeof timeline === 'undefined') {
|
||||
return this.t
|
||||
return this.timeline
|
||||
}
|
||||
this.t = timeline
|
||||
this.timeline = timeline
|
||||
return this
|
||||
}
|
||||
|
||||
@ -151,19 +147,21 @@ export class Animator<
|
||||
if (typeof timeline === 'number') {
|
||||
when = delay as When // eslint-disable-line
|
||||
delay = timeline // eslint-disable-line
|
||||
timeline = this.timeline() // eslint-disable-line
|
||||
timeline = this.timeline! // eslint-disable-line
|
||||
}
|
||||
|
||||
if (timeline == null) {
|
||||
throw Error('Runner cannot be scheduled without timeline')
|
||||
}
|
||||
|
||||
timeline.schedule(this, delay as number, when)
|
||||
const scheduler = timeline as Timeline
|
||||
|
||||
scheduler.schedule(this, delay as number, when)
|
||||
return this
|
||||
}
|
||||
|
||||
unschedule() {
|
||||
const timeline = this.timeline()
|
||||
const timeline = this.timeline
|
||||
if (timeline) {
|
||||
timeline.unschedule(this)
|
||||
}
|
||||
@ -305,22 +303,23 @@ export class Animator<
|
||||
return this.loops(position)
|
||||
}
|
||||
|
||||
animate(options: Options): Animator<TElement, TTarget>
|
||||
animate(
|
||||
duration?: number,
|
||||
delay?: number,
|
||||
when?: When,
|
||||
): Animator<TElement, TTarget>
|
||||
animate(options: Options): this
|
||||
animate(duration?: number, delay?: number, when?: When): this
|
||||
animate(duration?: Options | number, delay?: number, when?: When) {
|
||||
const options = Util.sanitise(duration, delay, when)
|
||||
const animator = new Animator<TElement, TTarget>(options.duration)
|
||||
const Ctor = this.constructor as new (duration: number) => Animator<
|
||||
TAnimator,
|
||||
TOwner
|
||||
>
|
||||
|
||||
if (this.t) {
|
||||
animator.timeline(this.t)
|
||||
const animator = new Ctor(options.duration)
|
||||
|
||||
if (this.timeline) {
|
||||
animator.scheduler(this.timeline)
|
||||
}
|
||||
|
||||
if (this.target) {
|
||||
animator.element(this.target)
|
||||
if (this.owner) {
|
||||
animator.master(this.owner)
|
||||
}
|
||||
|
||||
return animator.loop(options).schedule(options.delay, options.when)
|
||||
@ -354,7 +353,7 @@ export class Animator<
|
||||
this.previousStepTime = this.currentTime
|
||||
const callback = (cache: any[]) => {
|
||||
for (let i = 0, l = cache.length; i < l; i += 1) {
|
||||
const handler = cache[i] as Animator.Callback<TAnimator>
|
||||
const handler = cache[i] as Animator.EventHandler<TAnimator>
|
||||
const context = cache[i + 1]
|
||||
if (handler) {
|
||||
const result = handler.call(context, this)
|
||||
@ -404,7 +403,7 @@ export class Animator<
|
||||
|
||||
on(
|
||||
event: Animator.EventNames,
|
||||
callback: Animator.Callback<TAnimator>,
|
||||
callback: Animator.EventHandler<TAnimator>,
|
||||
context?: Entity,
|
||||
) {
|
||||
const cache = this.callbacks[event]
|
||||
@ -414,7 +413,7 @@ export class Animator<
|
||||
|
||||
off(
|
||||
event: Animator.EventNames,
|
||||
callback?: Animator.Callback<TAnimator>,
|
||||
callback?: Animator.EventHandler<TAnimator>,
|
||||
context?: Entity,
|
||||
) {
|
||||
const cache = this.callbacks[event]
|
||||
@ -448,9 +447,8 @@ export class Animator<
|
||||
finished: false,
|
||||
})
|
||||
|
||||
const timeline = this.timeline()
|
||||
if (timeline) {
|
||||
timeline.peek()
|
||||
if (this.timeline) {
|
||||
this.timeline.peek()
|
||||
}
|
||||
|
||||
return this
|
||||
@ -495,9 +493,8 @@ export class Animator<
|
||||
// and later
|
||||
// anim.move(...)
|
||||
if (this.declarative) {
|
||||
const timeline = this.timeline()
|
||||
if (timeline) {
|
||||
timeline.play()
|
||||
if (this.timeline) {
|
||||
this.timeline.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -527,9 +524,8 @@ export class Animator<
|
||||
|
||||
executor.finished = false
|
||||
|
||||
const timeline = this.timeline()
|
||||
if (timeline) {
|
||||
timeline.play()
|
||||
if (this.timeline) {
|
||||
this.timeline.play()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -539,21 +535,7 @@ export class Animator<
|
||||
}
|
||||
|
||||
export namespace Animator {
|
||||
export type Owner = Record<string, any>
|
||||
export type EventNames = 'start' | 'step' | 'finished'
|
||||
|
||||
export type Callback<T> = (animator: T) => any
|
||||
}
|
||||
|
||||
export namespace Animator {
|
||||
export function register(name: string) {
|
||||
return <TDefinition extends Registry.Definition>(ctor: TDefinition) => {
|
||||
Registry.register(ctor, name)
|
||||
}
|
||||
}
|
||||
|
||||
export function mixin(...source: any[]) {
|
||||
return (ctor: Registry.Definition) => {
|
||||
Mixin.applyMixins(ctor, ...source)
|
||||
}
|
||||
}
|
||||
export type EventHandler<T> = (animator: T) => any
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
import type { Animator } from './index'
|
||||
import type { Primer } from '../../element/dom/primer'
|
||||
import { Options, When } from '../types'
|
||||
import type { Options, When } from '../types'
|
||||
import { Stepper } from '../stepper/stepper'
|
||||
|
||||
export namespace Util {
|
||||
@ -48,19 +46,19 @@ export namespace Util {
|
||||
}
|
||||
}
|
||||
|
||||
export function create<TType extends typeof Animator>(
|
||||
Type: TType,
|
||||
element: Primer,
|
||||
duration?: Partial<Options> | number,
|
||||
delay?: number,
|
||||
when?: When,
|
||||
) {
|
||||
const o = sanitise(duration, delay, when)
|
||||
const timeline = element.timeline()
|
||||
return new Type(o.duration)
|
||||
.loop(o)
|
||||
.element(element)
|
||||
.timeline(timeline.play())
|
||||
.schedule(o.delay, o.when)
|
||||
}
|
||||
// export function create<TType extends typeof Animator>(
|
||||
// Type: TType,
|
||||
// element: Primer,
|
||||
// duration?: Partial<Options> | number,
|
||||
// delay?: number,
|
||||
// when?: When,
|
||||
// ) {
|
||||
// const o = sanitise(duration, delay, when)
|
||||
// const timeline = element.timeline()
|
||||
// return new Type(o.duration)
|
||||
// .loop(o)
|
||||
// .element(element)
|
||||
// .timeline(timeline.play())
|
||||
// .schedule(o.delay, o.when)
|
||||
// }
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
// import { Point } from '../../struct/point'
|
||||
// import { Morphable } from '../morpher/morphable'
|
||||
// import { MorphableUnitNumber } from '../morpher/morphable-unit-number'
|
||||
// import { Morpher } from '../morpher/morpher'
|
||||
// import type { VectorElement } from '../../element'
|
||||
// import { HTMLAnimator } from './html-animator'
|
||||
// import { Box } from '../../struct/box'
|
||||
// import { MorphableBox } from '../morpher/morphable-box'
|
||||
|
||||
// export class VectorAnimator<
|
||||
// TTarget extends VectorElement = VectorElement
|
||||
// > extends VectorAnimator<TTarget> {
|
||||
|
||||
// update(o) {
|
||||
// if (typeof o !== 'object') {
|
||||
// return this.update({
|
||||
// offset: arguments[0],
|
||||
// color: arguments[1],
|
||||
// opacity: arguments[2],
|
||||
// })
|
||||
// }
|
||||
|
||||
// if (o.opacity != null) this.attr('stop-opacity', o.opacity)
|
||||
// if (o.color != null) this.attr('stop-color', o.color)
|
||||
// if (o.offset != null) this.attr('offset', o.offset)
|
||||
|
||||
// return this
|
||||
// }
|
||||
// }
|
@ -1,7 +0,0 @@
|
||||
import { GeometryContainer } from '../../../element/container/container-geometry'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
export class SVGContainerGeometryAnimator<
|
||||
TSVGElement extends SVGAElement | SVGGElement,
|
||||
TTarget extends GeometryContainer<TSVGElement> = GeometryContainer<TSVGElement>
|
||||
> extends SVGContainerAnimator<TSVGElement, TTarget> {}
|
@ -1,7 +0,0 @@
|
||||
import { Container } from '../../../element/container/container'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGContainerAnimator<
|
||||
TSVGElement extends SVGElement,
|
||||
TTarget extends Container<TSVGElement> = Container<TSVGElement>
|
||||
> extends SVGAnimator<TSVGElement, TTarget> {}
|
@ -1,8 +0,0 @@
|
||||
import { Gradient } from '../../../element/container/gradient'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
@SVGGradientAnimator.register('Gradient')
|
||||
export class SVGGradientAnimator extends SVGContainerAnimator<
|
||||
SVGLinearGradientElement | SVGRadialGradientElement,
|
||||
Gradient
|
||||
> {}
|
@ -1,32 +0,0 @@
|
||||
import { Mixin } from '../../util/mixin'
|
||||
import { Dom } from '../../element/dom/dom'
|
||||
import { Primer } from '../../element/dom/primer'
|
||||
import { When, Options } from '../types'
|
||||
import { Util } from '../animator/util'
|
||||
import { Registry } from '../registry'
|
||||
import { AnimatorType } from './types'
|
||||
|
||||
export class ElementExtension<TNode extends Node> extends Primer<TNode> {
|
||||
animate(options: Options): AnimatorType<TNode>
|
||||
animate(duration?: number, delay?: number, when?: When): AnimatorType<TNode>
|
||||
animate(duration?: Options | number, delay?: number, when?: When) {
|
||||
const o = Util.sanitise(duration, delay, when)
|
||||
const timeline = this.timeline()
|
||||
const Type = Registry.get(this.node)
|
||||
return new Type(o.duration)
|
||||
.loop(o)
|
||||
.element(this)
|
||||
.timeline(timeline.play())
|
||||
.schedule(o.delay, o.when)
|
||||
}
|
||||
|
||||
delay(by: number, when?: When) {
|
||||
return this.animate(0, by, when)
|
||||
}
|
||||
}
|
||||
|
||||
declare module '../../element/dom/dom' {
|
||||
interface Dom<TNode extends Node = Node> extends ElementExtension<TNode> {}
|
||||
}
|
||||
|
||||
Mixin.applyMixins(Dom, ElementExtension)
|
@ -1,77 +0,0 @@
|
||||
import { Animator } from '../animator'
|
||||
import { HTMLAnimator } from './html'
|
||||
import { SVGAnimator } from './svg'
|
||||
import { SVGAAnimator } from './container/a'
|
||||
import { SVGClipPathAnimator } from './container/clippath'
|
||||
import { SVGGAnimator } from './container/g'
|
||||
import { SVGDefsAnimator } from './container/defs'
|
||||
import { SVGGradientAnimator } from './container/gradient'
|
||||
import { SVGMarkerAnimator } from './container/marker'
|
||||
import { SVGMaskAnimator } from './container/mask'
|
||||
import { SVGPatternAnimator } from './container/pattern'
|
||||
import { SVGSVGAnimator } from './container/svg'
|
||||
import { SVGSymbolAnimator } from './container/symbol'
|
||||
import { SVGCircleAnimator } from './shape/circle'
|
||||
import { SVGEllipseAnimator } from './shape/ellipse'
|
||||
import { SVGImageAnimator } from './shape/image'
|
||||
import { SVGLineAnimator } from './shape/line'
|
||||
import { SVGPolyAnimator } from './shape/poly'
|
||||
import { SVGPolygonAnimator } from './shape/polygon'
|
||||
import { SVGPolylineAnimator } from './shape/polyline'
|
||||
import { SVGRectAnimator } from './shape/rect'
|
||||
import { SVGStyleAnimator } from './shape/style'
|
||||
import { SVGTextAnimator } from './shape/text'
|
||||
import { SVGTSpanAnimator } from './shape/tspan'
|
||||
import { SVGUseAnimator } from './shape/use'
|
||||
import { SVGPathAnimator } from './shape/path'
|
||||
import { SVGViewboxAnimator } from './container/container-viewbox'
|
||||
import { SVGContainerAnimator } from './container/container'
|
||||
import { SVGForeignObjectAnimator } from './shape/foreignobject'
|
||||
|
||||
// prettier-ignore
|
||||
export type AnimatorType<T> =
|
||||
T extends SVGAElement ? SVGAAnimator
|
||||
: T extends SVGLineElement ? SVGLineAnimator
|
||||
: T extends SVGPathElement ? SVGPathAnimator
|
||||
: T extends SVGCircleElement ? SVGCircleAnimator
|
||||
: T extends SVGClipPathElement ? SVGClipPathAnimator
|
||||
: T extends SVGEllipseElement ? SVGEllipseAnimator
|
||||
: T extends SVGImageElement ? SVGImageAnimator
|
||||
: T extends SVGRectElement ? SVGRectAnimator
|
||||
: T extends SVGUseElement ? SVGUseAnimator
|
||||
: T extends SVGSVGElement ? SVGSVGAnimator
|
||||
: T extends SVGForeignObjectElement ? SVGForeignObjectAnimator
|
||||
: T extends SVGTSpanElement ? SVGTSpanAnimator
|
||||
: T extends SVGTextElement ? SVGTextAnimator
|
||||
: T extends SVGPolylineElement ? SVGPolylineAnimator
|
||||
: T extends SVGPolygonElement ? SVGPolygonAnimator
|
||||
: T extends SVGGElement ? SVGGAnimator
|
||||
: T extends SVGDefsElement ? SVGDefsAnimator
|
||||
: T extends SVGGradientElement ? SVGGradientAnimator
|
||||
: T extends SVGMarkerElement ? SVGMarkerAnimator
|
||||
: T extends SVGMaskElement ? SVGMaskAnimator
|
||||
: T extends SVGPatternElement ? SVGPatternAnimator
|
||||
: T extends SVGSymbolElement ? SVGSymbolAnimator
|
||||
: T extends SVGStyleElement ? SVGStyleAnimator
|
||||
: T extends SVGElement ? SVGAnimator<T>
|
||||
|
||||
: T extends
|
||||
| SVGAElement
|
||||
| SVGGElement ? SVGContainerAnimator<T>
|
||||
|
||||
: T extends
|
||||
| SVGLinearGradientElement
|
||||
| SVGRadialGradientElement ? SVGGradientAnimator
|
||||
|
||||
: T extends
|
||||
| SVGPolygonElement
|
||||
| SVGPolylineElement ? SVGPolyAnimator<T>
|
||||
|
||||
: T extends
|
||||
| SVGSVGElement
|
||||
| SVGSymbolElement
|
||||
| SVGPatternElement
|
||||
| SVGMarkerElement ? SVGViewboxAnimator<T>
|
||||
|
||||
: T extends Node ? HTMLAnimator<T>
|
||||
: Animator<any, any, any>
|
@ -1,188 +0,0 @@
|
||||
import { Dom } from '../../element'
|
||||
import { Matrix } from '../../struct/matrix'
|
||||
import { Queue } from '../scheduler/queue'
|
||||
import { Timing } from '../scheduler/timing'
|
||||
import { Animator } from '../animator'
|
||||
import { HTMLAnimator } from './html'
|
||||
|
||||
class Mock {
|
||||
constructor(public id = -1, public done = true) {}
|
||||
timeline() {}
|
||||
}
|
||||
|
||||
function createMockAnimator(matrix?: Matrix, id?: number) {
|
||||
const mock = new Mock(id)
|
||||
AnimatorHelper.addTransform(mock, matrix)
|
||||
return mock
|
||||
}
|
||||
|
||||
function mergeAnimators(a1: Animator | Mock, a2: Animator | Mock) {
|
||||
const m1 = AnimatorHelper.getTransform(a1)!
|
||||
const m2 = AnimatorHelper.getTransform(a2)!
|
||||
AnimatorHelper.clearTransform(a2)
|
||||
return createMockAnimator(m2.lmultiply(m1), a2.id)
|
||||
}
|
||||
|
||||
class AnimatorArray {
|
||||
public readonly ids: number[] = []
|
||||
public readonly animators: (Animator | Mock)[] = []
|
||||
|
||||
add(animator: Animator | Mock) {
|
||||
if (this.animators.includes(animator)) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.ids.push(animator.id)
|
||||
this.animators.push(animator)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
const index = this.ids.indexOf(id)
|
||||
this.ids.splice(index, 1)
|
||||
this.animators.splice(index, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
replace(id: number, animator: Animator | Mock) {
|
||||
const index = this.ids.indexOf(id)
|
||||
this.ids.splice(index, 1, id)
|
||||
this.animators.splice(index, 1, animator)
|
||||
return this
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
return this.animators[this.ids.indexOf(id)]
|
||||
}
|
||||
|
||||
length() {
|
||||
return this.ids.length
|
||||
}
|
||||
|
||||
clearBefore(id: number) {
|
||||
const deleteCnt = this.ids.indexOf(id) || 1
|
||||
this.ids.splice(0, deleteCnt, 0)
|
||||
this.animators.splice(0, deleteCnt, new Mock())
|
||||
return this.animators
|
||||
}
|
||||
|
||||
merge() {
|
||||
let last: Animator | Mock | null = null
|
||||
for (let i = 0; i < this.animators.length; i += 1) {
|
||||
const curr = this.animators[i]
|
||||
const currTimeline = curr.timeline()
|
||||
const lastTimeline = last && last.timeline()
|
||||
|
||||
const condition =
|
||||
last != null &&
|
||||
last.done &&
|
||||
curr.done &&
|
||||
// don't merge runner when persisted on timeline
|
||||
(currTimeline == null || !currTimeline.has(curr.id)) &&
|
||||
(lastTimeline == null || !lastTimeline.has(last.id))
|
||||
|
||||
if (condition) {
|
||||
this.remove(curr.id)
|
||||
const merged = mergeAnimators(curr, last!)
|
||||
this.replace(last!.id, merged)
|
||||
last = merged
|
||||
i -= 1
|
||||
} else {
|
||||
last = curr
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ElementHelper {
|
||||
const list: WeakMap<Dom<Node>, AnimatorArray> = new WeakMap()
|
||||
const frame: WeakMap<Dom<Node>, Queue.Item<Timing.Frame>> = new WeakMap()
|
||||
|
||||
export function prepareAnimator(elem: Dom<Node>) {
|
||||
if (!frame.has(elem)) {
|
||||
const mock = createMockAnimator(new Matrix(elem))
|
||||
const arr = new AnimatorArray().add(mock)
|
||||
list.set(elem, arr)
|
||||
}
|
||||
}
|
||||
|
||||
export function addAnimator(elem: Dom<Node>, animator: HTMLAnimator) {
|
||||
const arr = list.get(elem)!
|
||||
|
||||
arr.add(animator)
|
||||
|
||||
let frameId = frame.get(elem) || null
|
||||
|
||||
// Make sure that the animator merge is executed at the very end of
|
||||
// all animation functions. Thats why we use immediate here to execute
|
||||
// the merge right after all frames are run
|
||||
Timing.cancelImmediate(frameId)
|
||||
|
||||
frameId = Timing.immediate(() => {
|
||||
const arr = list.get(elem)!
|
||||
const next = arr.animators
|
||||
.map((animator) => AnimatorHelper.getTransform(animator)!)
|
||||
.reduce((memo, curr) => memo.lmultiplyO(curr), new Matrix())
|
||||
|
||||
elem.transform(next)
|
||||
arr.merge()
|
||||
|
||||
if (arr.length() === 1) {
|
||||
frame.delete(elem)
|
||||
}
|
||||
})
|
||||
|
||||
frame.set(elem, frameId)
|
||||
}
|
||||
|
||||
export function clearAnimatorsBefore(
|
||||
elem: Dom<Node>,
|
||||
animator: HTMLAnimator,
|
||||
) {
|
||||
const cache = list.get(elem)!
|
||||
return cache.clearBefore(animator.id)
|
||||
}
|
||||
|
||||
export function getCurrentTransform(elem: Dom<Node>, animator: HTMLAnimator) {
|
||||
const arr = list.get(elem)!
|
||||
|
||||
return (
|
||||
arr.animators
|
||||
// we need the equal sign here to make sure, that also transformations
|
||||
// on the same runner which execute before the current transformation are
|
||||
// taken into account
|
||||
.filter((item) => item.id <= animator.id)
|
||||
.map((animator) => AnimatorHelper.getTransform(animator)!)
|
||||
.reduce((memo, curr) => memo.lmultiplyO(curr), new Matrix())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AnimatorHelper {
|
||||
const cache: WeakMap<Animator | Mock, Matrix> = new WeakMap()
|
||||
|
||||
export function addTransform(
|
||||
animator: Animator | Mock,
|
||||
transform?: Matrix.Raw,
|
||||
) {
|
||||
let ctm = cache.get(animator)
|
||||
if (ctm == null) {
|
||||
ctm = new Matrix()
|
||||
}
|
||||
if (transform) {
|
||||
ctm.lmultiplyO(transform)
|
||||
}
|
||||
cache.set(animator, ctm)
|
||||
}
|
||||
|
||||
export function clearTransform(animator: Animator | Mock) {
|
||||
cache.set(animator, new Matrix())
|
||||
}
|
||||
|
||||
export function getTransform(animator: Animator | Mock) {
|
||||
return cache.get(animator)
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
import './extension'
|
||||
export * from './animator/animator'
|
||||
export * from './scheduler/timeline'
|
||||
|
@ -11,4 +11,8 @@ export class MorphableBox
|
||||
this.height = arr[0]
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -8,4 +8,8 @@ export class MorphableColor
|
||||
this.set(...arr)
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ export class MorphableFallback<T = any> implements Morphable<T[], T> {
|
||||
return [this.value]
|
||||
}
|
||||
|
||||
valueOf(): T {
|
||||
toValue(): T {
|
||||
return this.value
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableMatrix
|
||||
extends Matrix
|
||||
implements Morphable<Matrix.MatrixArray, Matrix.MatrixLike> {
|
||||
implements Morphable<Matrix.MatrixArray, Matrix.MatrixArray> {
|
||||
fromArray(arr: Matrix.MatrixArray) {
|
||||
this.a = arr[0]
|
||||
this.b = arr[1]
|
||||
@ -13,4 +13,8 @@ export class MorphableMatrix
|
||||
this.f = arr[5]
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// eslint-disable-next-line
|
||||
export class Morphable<TArray extends any[], TValueOf> {
|
||||
export class Morphable<TArray extends any[], TValue> {
|
||||
constructor()
|
||||
constructor(arg: any)
|
||||
constructor(...args: any[])
|
||||
@ -7,8 +7,8 @@ export class Morphable<TArray extends any[], TValueOf> {
|
||||
constructor(...args: any[]) {}
|
||||
}
|
||||
|
||||
export interface Morphable<TArray extends any[], TValueOf> {
|
||||
export interface Morphable<TArray extends any[], TValue> {
|
||||
fromArray(arr: TArray): this
|
||||
toArray(): TArray
|
||||
valueOf(): TValueOf
|
||||
toValue(): TValue
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Easing } from '../stepper/easing'
|
||||
import { Stepper } from '../stepper/stepper'
|
||||
import { Util } from './morphable-util'
|
||||
import { Util } from './util'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class Morpher<TArray extends any[], TInput, TValue> {
|
||||
@ -37,7 +37,7 @@ export class Morpher<TArray extends any[], TInput, TValue> {
|
||||
),
|
||||
)
|
||||
|
||||
return this.instance.fromArray(current as TArray).valueOf()
|
||||
return this.instance.fromArray(current as TArray).toValue()
|
||||
}
|
||||
|
||||
done(): boolean {
|
||||
|
@ -9,4 +9,8 @@ export class MorphableNumberArray
|
||||
this.push(...arr)
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Morphable } from './morphable'
|
||||
import { Util } from './morphable-util'
|
||||
import { Util } from './util'
|
||||
|
||||
export class MorphableObject<
|
||||
T extends Record<string, any> = Record<string, any>
|
||||
@ -40,7 +40,7 @@ export class MorphableObject<
|
||||
return this.values.slice()
|
||||
}
|
||||
|
||||
valueOf(): T {
|
||||
toValue(): T {
|
||||
const obj: Record<string, any> = {}
|
||||
const arr = this.values
|
||||
while (arr.length) {
|
||||
@ -48,7 +48,7 @@ export class MorphableObject<
|
||||
const Type = arr.shift()
|
||||
const len = arr.shift()
|
||||
const values = arr.splice(0, len)
|
||||
obj[key] = new Type().formArray(values).valueOf()
|
||||
obj[key] = new Type().formArray(values).toValue()
|
||||
}
|
||||
|
||||
return obj as T
|
@ -1,4 +1,4 @@
|
||||
import type { Path } from '../../element/shape/path'
|
||||
import type { Path } from '../../vector/path/path'
|
||||
import { PathArray } from '../../struct/path-array'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
@ -10,4 +10,8 @@ export class MorphablePathArray
|
||||
this.push(...this.parse(arr))
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -9,4 +9,8 @@ export class MorphablePointArray
|
||||
this.push(...this.parse(arr))
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ export class MorphableTransform
|
||||
]
|
||||
}
|
||||
|
||||
valueOf(): MorphableTransform.Array {
|
||||
toValue(): MorphableTransform.Array {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableUnitNumber
|
||||
extends UnitNumber
|
||||
implements Morphable<UnitNumber.UnitNumberArray, number> {
|
||||
implements Morphable<UnitNumber.UnitNumberArray, UnitNumber.UnitNumberArray> {
|
||||
fromArray(arr: UnitNumber.UnitNumberArray) {
|
||||
this.unit = arr[1] || ''
|
||||
if (typeof arr[0] === 'string') {
|
||||
@ -18,4 +18,8 @@ export class MorphableUnitNumber
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import { Morphable } from './morphable'
|
||||
import { MorphableBox } from './morphable-box'
|
||||
import { MorphableColor } from './morphable-color'
|
||||
import { MorphableMatrix } from './morphable-matrix'
|
||||
import { MorphableObject } from './morphable-object'
|
||||
import { MorphableFallback } from './morphable-fallback'
|
||||
import { MorphablePathArray } from './morphable-path-array'
|
||||
import { MorphablePointArray } from './morphable-point-array'
|
||||
import { MorphableTransform } from './morphable-transform'
|
||||
import { MorphableUnitNumber } from './morphable-unit-number'
|
||||
import { MorphableNumberArray } from './morphable-number-array'
|
||||
import { MorphableBox } from './box'
|
||||
import { MorphableColor } from './color'
|
||||
import { MorphableMatrix } from './matrix'
|
||||
import { MorphableObject } from './object'
|
||||
import { MorphableFallback } from './fallback'
|
||||
import { MorphablePathArray } from './path-array'
|
||||
import { MorphablePointArray } from './point-array'
|
||||
import { MorphableTransform } from './transform'
|
||||
import { MorphableUnitNumber } from './unit-number'
|
||||
import { MorphableNumberArray } from './number-array'
|
||||
|
||||
export namespace Util {
|
||||
const delimiter = /[\s,]+/
|
@ -1,28 +1,28 @@
|
||||
import { Mixin } from '../../util/mixin'
|
||||
import { Dom } from '../../element/dom/dom'
|
||||
import { Timeline } from './timeline'
|
||||
|
||||
const cache: WeakMap<ElementExtension, Timeline> = new WeakMap()
|
||||
import { Primer } from '../../dom/primer'
|
||||
|
||||
export class ElementExtension {
|
||||
timeline(): Timeline
|
||||
timeline(timeline: Timeline): this
|
||||
timeline(timeline?: Timeline) {
|
||||
protected timeline: Timeline
|
||||
|
||||
scheduler(): Timeline
|
||||
scheduler(timeline: Timeline): this
|
||||
scheduler(timeline?: Timeline) {
|
||||
if (timeline == null) {
|
||||
if (!cache.has(this)) {
|
||||
cache.set(this, new Timeline())
|
||||
if (this.timeline == null) {
|
||||
this.timeline = new Timeline()
|
||||
}
|
||||
return cache.get(this)!
|
||||
return this.timeline
|
||||
}
|
||||
|
||||
cache.set(this, timeline)
|
||||
this.timeline = timeline
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
declare module '../../element/dom/primer' {
|
||||
declare module '../../dom/primer/primer' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Primer<TNode extends Node = Node> extends ElementExtension {}
|
||||
interface Primer<TElement extends Element = Element>
|
||||
extends ElementExtension {}
|
||||
}
|
||||
|
||||
Mixin.applyMixins(Dom, ElementExtension)
|
||||
Primer.mixin(ElementExtension)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { When, Now } from '../types'
|
||||
import type { Animator } from '../animator/animator'
|
||||
import { Queue } from './queue'
|
||||
import { Timing } from './timing'
|
||||
import { When, Now } from '../types'
|
||||
import type { Animator } from '../animator'
|
||||
|
||||
export class Timeline {
|
||||
public readonly step: () => this
|
||||
@ -251,7 +251,7 @@ export class Timeline {
|
||||
}
|
||||
|
||||
animator.unschedule()
|
||||
animator.timeline(this)
|
||||
animator.scheduler(this)
|
||||
|
||||
const persist = animator.persist()
|
||||
const meta = {
|
||||
@ -281,7 +281,7 @@ export class Timeline {
|
||||
this.animators.splice(index, 1)
|
||||
this.animatorIds.splice(index, 1)
|
||||
|
||||
animator.timeline(null)
|
||||
animator.scheduler(null)
|
||||
return this
|
||||
}
|
||||
|
||||
@ -389,5 +389,5 @@ export class Timeline {
|
||||
}
|
||||
|
||||
export namespace Timeline {
|
||||
export type AnyAnimator = Animator<any, any, any>
|
||||
export type AnyAnimator = Animator<any, any>
|
||||
}
|
||||
|
@ -1,19 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
export class Stepper {
|
||||
export abstract class Stepper {
|
||||
done(context: Stepper.Context) {
|
||||
return context.done
|
||||
}
|
||||
|
||||
step<T>(
|
||||
abstract step<T>(
|
||||
from: T,
|
||||
to: T,
|
||||
pos: number,
|
||||
context: Stepper.Context,
|
||||
contexts: Stepper.Context[],
|
||||
): T {
|
||||
return from
|
||||
}
|
||||
): T
|
||||
}
|
||||
|
||||
export namespace Stepper {
|
||||
|
36
packages/x6-vector/src/animation/base.ts
Normal file
36
packages/x6-vector/src/animation/base.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { Dom } from '../dom'
|
||||
import type { AnimatorMap } from './types'
|
||||
import { applyMixins } from '../util'
|
||||
import { Animator } from '../animating/animator/animator'
|
||||
import { Registry } from './registry'
|
||||
|
||||
export class BaseAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>,
|
||||
TAnimator = AnimatorMap<TElement>
|
||||
> extends Animator<TAnimator, TOwner> {
|
||||
element(): TOwner
|
||||
element(elem: TOwner): this
|
||||
element(elem?: TOwner) {
|
||||
if (elem == null) {
|
||||
return super.master()
|
||||
}
|
||||
|
||||
super.master(elem)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BaseAnimator {
|
||||
export function register(name: string) {
|
||||
return <TDefinition extends Registry.Definition>(ctor: TDefinition) => {
|
||||
Registry.register(ctor, name)
|
||||
}
|
||||
}
|
||||
|
||||
export function mixin(...source: any[]) {
|
||||
return (ctor: Registry.Definition) => {
|
||||
applyMixins(ctor, ...source)
|
||||
}
|
||||
}
|
||||
}
|
106
packages/x6-vector/src/animation/dom.ts
Normal file
106
packages/x6-vector/src/animation/dom.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { Morpher } from '../animating/morpher/morpher'
|
||||
import { MorphableObject } from '../animating/morpher/object'
|
||||
import { Dom } from '../dom'
|
||||
import { AttributesMap } from '../dom/attributes'
|
||||
import { CSSProperties } from '../dom/style'
|
||||
import { BaseAnimator } from './base'
|
||||
import { TransformAnimator } from './transform'
|
||||
|
||||
@DomAnimator.register('HTML')
|
||||
@DomAnimator.mixin(TransformAnimator)
|
||||
export class DomAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>
|
||||
> extends BaseAnimator<TElement, TOwner> {
|
||||
attr<T extends AttributesMap<TElement>>(attrs: T): this
|
||||
attr<T extends AttributesMap<TElement>, K extends keyof T>(
|
||||
name: K,
|
||||
value: T[K],
|
||||
): this
|
||||
attr<T extends AttributesMap<TElement>, K extends keyof T>(
|
||||
name: K | T,
|
||||
value?: T[K],
|
||||
) {
|
||||
return this.queueAttrOrCSS('attr', name, value)
|
||||
}
|
||||
|
||||
css<T extends CSSProperties>(style: T): this
|
||||
css<T extends CSSProperties, K extends keyof T>(name: K, value: T[K]): this
|
||||
css<T extends CSSProperties, K extends keyof T>(name: K | T, value?: T[K]) {
|
||||
return this.queueAttrOrCSS('css', name, value)
|
||||
}
|
||||
|
||||
protected queueAttrOrCSS<
|
||||
M extends 'attr' | 'css',
|
||||
T extends M extends 'attr' ? AttributesMap<TElement> : CSSProperties,
|
||||
K extends keyof T
|
||||
>(method: M, name: K | T, value?: T[K]): this {
|
||||
if (typeof name === 'string') {
|
||||
return this.queueAttrOrCSS(method, { [name]: value } as T)
|
||||
}
|
||||
|
||||
let attrs = name as T
|
||||
if (this.retarget(method, attrs)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<any[], T, T>(this.stepper).to(attrs)
|
||||
|
||||
let keys = Object.keys(attrs)
|
||||
|
||||
this.queue<T>(
|
||||
// prepare
|
||||
(animator) => {
|
||||
const origin = animator.element()[method](keys as any)
|
||||
morpher.from(origin as T)
|
||||
},
|
||||
|
||||
// run
|
||||
(animator, pos) => {
|
||||
const val = morpher.at(pos)
|
||||
animator.element()[method](val as any)
|
||||
return morpher.done()
|
||||
},
|
||||
|
||||
// retarget
|
||||
(animator, newToAttrs) => {
|
||||
// Check if any new keys were added
|
||||
const newKeys = Object.keys(newToAttrs)
|
||||
const diff = (a: string[], b: string[]) =>
|
||||
a.filter((x) => !b.includes(x))
|
||||
const differences = diff(newKeys, keys)
|
||||
|
||||
// If their are new keys, initialize them and add them to morpher
|
||||
if (differences.length) {
|
||||
const addedFromAttrs = animator
|
||||
.element()
|
||||
[method](differences as any) as T
|
||||
const oldFromAttrs = new MorphableObject<T>(morpher.from()).toValue()
|
||||
morpher.from({
|
||||
...oldFromAttrs,
|
||||
...addedFromAttrs,
|
||||
})
|
||||
}
|
||||
|
||||
const oldToAttrs = new MorphableObject<T>(morpher.to()).toValue()
|
||||
morpher.to({
|
||||
...oldToAttrs,
|
||||
...newToAttrs,
|
||||
})
|
||||
|
||||
// Save the work we did so we don't need it to do again
|
||||
keys = newKeys
|
||||
attrs = newToAttrs
|
||||
},
|
||||
)
|
||||
|
||||
this.remember(method, morpher)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export interface DomAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>
|
||||
> extends TransformAnimator<TElement, TOwner> {}
|
1
packages/x6-vector/src/animation/index.ts
Normal file
1
packages/x6-vector/src/animation/index.ts
Normal file
@ -0,0 +1 @@
|
||||
import './mixins'
|
56
packages/x6-vector/src/animation/mixins.ts
Normal file
56
packages/x6-vector/src/animation/mixins.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Dom } from '../dom/dom'
|
||||
import { Primer } from '../dom/primer/primer'
|
||||
import { Util } from '../animating/animator/util'
|
||||
import { Timeline } from '../animating/scheduler/timeline'
|
||||
import { When, Options } from '../animating/types'
|
||||
import { Registry } from './registry'
|
||||
import { AnimatorMap } from './types'
|
||||
|
||||
export class AnimateExtension<
|
||||
TElement extends Element
|
||||
> extends Primer<TElement> {
|
||||
animate(options: Options): AnimatorMap<TElement>
|
||||
animate(duration?: number, delay?: number, when?: When): AnimatorMap<TElement>
|
||||
animate(duration?: Options | number, delay?: number, when?: When) {
|
||||
const o = Util.sanitise(duration, delay, when)
|
||||
const timeline = this.scheduler()
|
||||
const Type = Registry.get(this.node)
|
||||
return new Type(o.duration)
|
||||
.loop(o)
|
||||
.element(this as any)
|
||||
.scheduler(timeline.play())
|
||||
.schedule(o.delay, o.when)
|
||||
}
|
||||
|
||||
delay(by: number, when?: When) {
|
||||
return this.animate(0, by, when)
|
||||
}
|
||||
}
|
||||
|
||||
const cache: WeakMap<Primer, Timeline> = new WeakMap()
|
||||
|
||||
export class TimelineExtension<
|
||||
TElement extends Element = Element
|
||||
> extends Primer<TElement> {
|
||||
scheduler(): Timeline
|
||||
scheduler(timeline: Timeline): this
|
||||
scheduler(timeline?: Timeline) {
|
||||
if (timeline == null) {
|
||||
if (!cache.has(this)) {
|
||||
cache.set(this, new Timeline())
|
||||
}
|
||||
return cache.get(this)!
|
||||
}
|
||||
|
||||
cache.set(this, timeline)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
declare module '../dom/dom' {
|
||||
interface Dom<TElement extends Element = Element>
|
||||
extends AnimateExtension<TElement>,
|
||||
TimelineExtension<TElement> {}
|
||||
}
|
||||
|
||||
Dom.mixin(AnimateExtension, TimelineExtension)
|
@ -1,8 +1,7 @@
|
||||
import { Str } from '../util/str'
|
||||
import type { Animator } from './animator'
|
||||
import type { BaseAnimator } from './base'
|
||||
|
||||
export namespace Registry {
|
||||
export type Definition = { new (...args: any[]): Animator }
|
||||
export type Definition = { new (...args: any[]): BaseAnimator }
|
||||
|
||||
const registry: Record<string, Definition> = {}
|
||||
|
||||
@ -16,11 +15,10 @@ export namespace Registry {
|
||||
return registry[node] as TClass
|
||||
}
|
||||
|
||||
let className = Str.ucfirst(node.nodeName)
|
||||
const nodeName = node.nodeName
|
||||
let className = nodeName[0].toUpperCase() + nodeName.substring(1)
|
||||
|
||||
if (node instanceof SVGElement) {
|
||||
if (className === 'LinearGradient' || className === 'RadialGradient') {
|
||||
className = 'Gradient'
|
||||
}
|
||||
if (registry[className] == null) {
|
||||
className = 'SVG'
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import type { VectorElement } from '../../element'
|
||||
import { Morpher } from '../morpher/morpher'
|
||||
import { Morphable } from '../morpher/morphable'
|
||||
import { MorphableUnitNumber } from '../morpher/morphable-unit-number'
|
||||
import { HTMLAnimator } from './html'
|
||||
import { Vector } from '../vector/vector/vector'
|
||||
import { Morpher } from '../animating/morpher/morpher'
|
||||
import { Morphable } from '../animating/morpher/morphable'
|
||||
import { MorphableUnitNumber } from '../animating/morpher/unit-number'
|
||||
import { DomAnimator } from './dom'
|
||||
|
||||
@SVGAnimator.register('SVG')
|
||||
export class SVGAnimator<
|
||||
TSVGElement extends SVGElement = SVGElement,
|
||||
TTarget extends VectorElement<TSVGElement> = VectorElement<TSVGElement>
|
||||
> extends HTMLAnimator<TSVGElement, TTarget> {
|
||||
TOwner extends Vector<TSVGElement> = Vector<TSVGElement>
|
||||
> extends DomAnimator<TSVGElement, TOwner> {
|
||||
x(x: number | string) {
|
||||
return this.queueNumber('x', x)
|
||||
}
|
||||
@ -62,7 +62,7 @@ export class SVGAnimator<
|
||||
let h = MorphableUnitNumber.toNumber(height!)
|
||||
|
||||
if (width == null || height == null) {
|
||||
const box = this.target.bbox()
|
||||
const box = this.element().bbox()
|
||||
|
||||
if (!width) {
|
||||
w = (box.width / box.height) * h
|
@ -1,118 +1,17 @@
|
||||
import type { Dom } from '../../element'
|
||||
import type { Attrs, CSSKeys } from '../../types'
|
||||
import { Dom } from '../../dom'
|
||||
import { Point } from '../../struct/point'
|
||||
import { Matrix } from '../../struct/matrix'
|
||||
import { Util } from '../../element/dom/transform-util'
|
||||
import { Animator } from '../animator'
|
||||
import { Morpher } from '../morpher/morpher'
|
||||
import { MorphableObject } from '../morpher/morphable-object'
|
||||
import { MorphableMatrix } from '../morpher/morphable-matrix'
|
||||
import { MorphableTransform } from '../morpher/morphable-transform'
|
||||
import { AnimatorHelper, ElementHelper } from './util'
|
||||
|
||||
@HTMLAnimator.register('HTML')
|
||||
export class HTMLAnimator<
|
||||
TNode extends Node = Node,
|
||||
TTarget extends Dom<TNode> = Dom<TNode>
|
||||
> extends Animator<TNode, TTarget> {
|
||||
element(): TTarget
|
||||
element(elem: TTarget): this
|
||||
element(elem?: TTarget) {
|
||||
if (elem == null) {
|
||||
return super.element()
|
||||
}
|
||||
|
||||
super.element(elem)
|
||||
ElementHelper.prepareAnimator(elem)
|
||||
return this
|
||||
}
|
||||
|
||||
attr(attrs: Attrs): this
|
||||
attr(name: string, value: string | number): this
|
||||
attr(name: string | Attrs, value?: string | number) {
|
||||
return this.animateStyleOrAttr('attr', name, value)
|
||||
}
|
||||
|
||||
css(style: CSSStyleDeclaration | Record<string, string>): this
|
||||
css(name: CSSKeys, value: string): this
|
||||
css(
|
||||
name: CSSKeys | CSSStyleDeclaration | Record<string, string>,
|
||||
value?: string,
|
||||
) {
|
||||
return this.animateStyleOrAttr('css', name, value)
|
||||
}
|
||||
|
||||
protected animateStyleOrAttr(
|
||||
method: 'attr' | 'css',
|
||||
name: string | Record<string, any>,
|
||||
value?: string | number,
|
||||
): this {
|
||||
if (typeof name === 'string') {
|
||||
return this.animateStyleOrAttr(method, { [name]: value })
|
||||
}
|
||||
|
||||
let attrs = name
|
||||
if (this.retarget(method, attrs)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<
|
||||
any[],
|
||||
Record<string, any>,
|
||||
Record<string, any>
|
||||
>(this.stepper).to(attrs)
|
||||
|
||||
let keys = Object.keys(attrs)
|
||||
|
||||
this.queue<Record<string, any>>(
|
||||
// prepare
|
||||
(animator) => {
|
||||
const origin = animator.element()[method](keys)
|
||||
morpher.from(origin)
|
||||
},
|
||||
|
||||
// run
|
||||
(animator, pos) => {
|
||||
const val = morpher.at(pos)
|
||||
animator.element()[method](val)
|
||||
return morpher.done()
|
||||
},
|
||||
|
||||
// retarget
|
||||
(animator, newToAttrs) => {
|
||||
// Check if any new keys were added
|
||||
const newKeys = Object.keys(newToAttrs)
|
||||
const diff = (a: string[], b: string[]) =>
|
||||
a.filter((x) => !b.includes(x))
|
||||
const differences = diff(newKeys, keys)
|
||||
|
||||
// If their are new keys, initialize them and add them to morpher
|
||||
if (differences.length) {
|
||||
const addedFromAttrs = animator.element()[method](differences)
|
||||
const oldFromAttrs = new MorphableObject(morpher.from()).valueOf()
|
||||
morpher.from({
|
||||
...oldFromAttrs,
|
||||
...addedFromAttrs,
|
||||
})
|
||||
}
|
||||
|
||||
const oldToAttrs = new MorphableObject(morpher.to()).valueOf()
|
||||
morpher.to({
|
||||
...oldToAttrs,
|
||||
...newToAttrs,
|
||||
})
|
||||
|
||||
// Save the work we did so we don't need it to do again
|
||||
keys = newKeys
|
||||
attrs = newToAttrs
|
||||
},
|
||||
)
|
||||
|
||||
this.remember(method, morpher)
|
||||
|
||||
return this
|
||||
}
|
||||
import { BaseAnimator } from '../base'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphableMatrix } from '../../animating/morpher/matrix'
|
||||
import { MorphableTransform } from '../../animating/morpher/transform'
|
||||
import { getTransformOrigin } from '../../dom/transform/util'
|
||||
import * as Util from './util'
|
||||
|
||||
export class TransformAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>
|
||||
> extends BaseAnimator<TElement, TOwner> {
|
||||
transform(
|
||||
transforms: Matrix.MatrixLike | Matrix.TransformOptions,
|
||||
relative?: boolean,
|
||||
@ -131,6 +30,8 @@ export class HTMLAnimator<
|
||||
// - Note F(1) = T
|
||||
// 4. Now you get the delta matrix as a result: D = F * inv(M)
|
||||
|
||||
Util.prepareTransform(this.element())
|
||||
|
||||
const method = 'transform'
|
||||
|
||||
// If we have a declarative function, we should retarget it if possible
|
||||
@ -151,7 +52,7 @@ export class HTMLAnimator<
|
||||
>(this.stepper).type(affine ? MorphableTransform : MorphableMatrix)
|
||||
|
||||
let origin: [number, number]
|
||||
let element: TTarget
|
||||
let element: TOwner
|
||||
let startTransformMatrix: Matrix
|
||||
let currentTransformMatrix: Matrix
|
||||
let currentAngle: number
|
||||
@ -163,29 +64,22 @@ export class HTMLAnimator<
|
||||
element = element || animator.element()
|
||||
origin =
|
||||
origin ||
|
||||
Util.getTransformOrigin(
|
||||
transforms as Matrix.TransformOptions,
|
||||
element,
|
||||
)
|
||||
getTransformOrigin(transforms as Matrix.TransformOptions, element)
|
||||
|
||||
startTransformMatrix = new Matrix(relative ? undefined : element)
|
||||
|
||||
// add the animator to the element so it can merge transformations
|
||||
ElementHelper.addAnimator(element, animator as HTMLAnimator)
|
||||
Util.addAnimator(element, animator)
|
||||
|
||||
// Deactivate all transforms that have run so far if we are absolute
|
||||
if (!relative) {
|
||||
ElementHelper.clearAnimatorsBefore(element, animator).forEach((a) => {
|
||||
if (a instanceof HTMLAnimator) {
|
||||
if (!a.done) {
|
||||
const timeline = a.timeline()
|
||||
if (timeline == null || !timeline.has(a)) {
|
||||
const exections = a.executors
|
||||
for (let i = exections.length - 1; i >= 0; i -= 1) {
|
||||
if (exections[i].isTransform) {
|
||||
exections.splice(i, 1)
|
||||
}
|
||||
}
|
||||
Util.clearAnimatorsBefore(element, animator).forEach((animator) => {
|
||||
if (animator instanceof BaseAnimator) {
|
||||
if (!animator.done) {
|
||||
const timeline = animator.scheduler()
|
||||
if (timeline == null || !timeline.has(animator)) {
|
||||
const a = (animator as any) as TransformAnimator
|
||||
a.clearTransformExecutors()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,10 +90,10 @@ export class HTMLAnimator<
|
||||
// run
|
||||
(animator, pos) => {
|
||||
if (!relative) {
|
||||
AnimatorHelper.clearTransform(animator)
|
||||
Util.clearTransform(animator)
|
||||
}
|
||||
|
||||
const ctm = ElementHelper.getCurrentTransform(element, animator)
|
||||
const ctm = Util.getCurrentTransform(element, animator)
|
||||
const { x, y } = new Point(origin[0], origin[1]).transform(ctm)
|
||||
const target = new Matrix({ ...transforms, origin: [x, y] })
|
||||
const source =
|
||||
@ -241,8 +135,8 @@ export class HTMLAnimator<
|
||||
currentAngle = affineParameters.rotate!
|
||||
currentTransformMatrix = new Matrix(affineParameters)
|
||||
|
||||
AnimatorHelper.addTransform(animator, currentTransformMatrix)
|
||||
ElementHelper.addAnimator(element, animator)
|
||||
Util.addTransform(animator, currentTransformMatrix)
|
||||
Util.addAnimator(element, animator)
|
||||
|
||||
return morpher.done()
|
||||
},
|
||||
@ -254,7 +148,7 @@ export class HTMLAnimator<
|
||||
|
||||
// only get a new origin if it changed since the last call
|
||||
if (prev.toString() !== next.toString()) {
|
||||
origin = Util.getTransformOrigin(newTransforms, element)
|
||||
origin = getTransformOrigin(newTransforms, element)
|
||||
}
|
||||
|
||||
// overwrite the old transformations with the new ones
|
||||
@ -271,4 +165,13 @@ export class HTMLAnimator<
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
protected clearTransformExecutors() {
|
||||
const executors = this.executors
|
||||
for (let i = executors.length - 1; i >= 0; i -= 1) {
|
||||
if (executors[i].isTransform) {
|
||||
executors.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
packages/x6-vector/src/animation/transform/list.ts
Normal file
81
packages/x6-vector/src/animation/transform/list.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { DomAnimator } from '../dom'
|
||||
import { MockedAnimator } from './mock'
|
||||
|
||||
export class AnimatorList {
|
||||
public readonly ids: number[] = []
|
||||
public readonly animators: (DomAnimator | MockedAnimator)[] = []
|
||||
|
||||
add(animator: DomAnimator | MockedAnimator) {
|
||||
if (this.animators.includes(animator)) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.ids.push(animator.id)
|
||||
this.animators.push(animator)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
const index = this.ids.indexOf(id)
|
||||
this.ids.splice(index, 1)
|
||||
this.animators.splice(index, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
replace(id: number, animator: DomAnimator | MockedAnimator) {
|
||||
const index = this.ids.indexOf(id)
|
||||
this.ids.splice(index, 1, id)
|
||||
this.animators.splice(index, 1, animator)
|
||||
return this
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
return this.animators[this.ids.indexOf(id)]
|
||||
}
|
||||
|
||||
length() {
|
||||
return this.ids.length
|
||||
}
|
||||
|
||||
clearBefore(id: number) {
|
||||
const deleteCount = this.ids.indexOf(id) || 1
|
||||
this.ids.splice(0, deleteCount, 0)
|
||||
this.animators.splice(0, deleteCount, new MockedAnimator())
|
||||
return this.animators
|
||||
}
|
||||
|
||||
merge(
|
||||
mergeFn: (
|
||||
a1: DomAnimator | MockedAnimator,
|
||||
a2: DomAnimator | MockedAnimator,
|
||||
) => MockedAnimator,
|
||||
) {
|
||||
let prev: DomAnimator | MockedAnimator | null = null
|
||||
for (let i = 0; i < this.animators.length; i += 1) {
|
||||
const curr = this.animators[i]
|
||||
const currs = curr.scheduler()
|
||||
const prevs = prev && prev.scheduler()
|
||||
|
||||
const needMerge =
|
||||
prev != null &&
|
||||
prev.done &&
|
||||
curr.done &&
|
||||
// don't merge animator when persisted on timeline
|
||||
(currs == null || !currs.has(curr.id)) &&
|
||||
(prevs == null || !prevs.has(prev.id))
|
||||
|
||||
if (needMerge) {
|
||||
this.remove(curr.id)
|
||||
const merged = mergeFn(curr, prev!)
|
||||
this.replace(prev!.id, merged)
|
||||
prev = merged
|
||||
i -= 1
|
||||
} else {
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
4
packages/x6-vector/src/animation/transform/mock.ts
Normal file
4
packages/x6-vector/src/animation/transform/mock.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class MockedAnimator {
|
||||
constructor(public id = -1, public done = true) {}
|
||||
scheduler() {}
|
||||
}
|
101
packages/x6-vector/src/animation/transform/util.ts
Normal file
101
packages/x6-vector/src/animation/transform/util.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Dom } from '../../dom'
|
||||
import { Matrix } from '../../struct/matrix'
|
||||
import { Queue } from '../../animating/scheduler/queue'
|
||||
import { Timing } from '../../animating/scheduler/timing'
|
||||
import { BaseAnimator } from '../base'
|
||||
import { AnimatorList } from './list'
|
||||
import { MockedAnimator } from './mock'
|
||||
|
||||
const animators: WeakMap<Dom, AnimatorList> = new WeakMap()
|
||||
const frames: WeakMap<Dom, Queue.Item<Timing.Frame>> = new WeakMap()
|
||||
|
||||
export function prepareTransform(elem: Dom) {
|
||||
if (!frames.has(elem)) {
|
||||
const mock = createMockAnimator(new Matrix(elem))
|
||||
const list = new AnimatorList().add(mock)
|
||||
animators.set(elem, list)
|
||||
}
|
||||
}
|
||||
|
||||
function createMockAnimator(matrix?: Matrix, id?: number) {
|
||||
const mock = new MockedAnimator(id)
|
||||
addTransform(mock, matrix)
|
||||
return mock
|
||||
}
|
||||
|
||||
export function addAnimator(elem: Dom, animator: BaseAnimator) {
|
||||
animators.get(elem)!.add(animator)
|
||||
|
||||
let frameId = frames.get(elem) || null
|
||||
|
||||
// Make sure that the animator merge is executed at the very end of
|
||||
// all animation functions. Thats why we use immediate here to execute
|
||||
// the merge right after all frames are run
|
||||
Timing.cancelImmediate(frameId)
|
||||
|
||||
frameId = Timing.immediate(() => {
|
||||
const list = animators.get(elem)!
|
||||
const next = list.animators
|
||||
.map((animator) => getTransform(animator)!)
|
||||
.reduce((memo, curr) => memo.lmultiplyO(curr), new Matrix())
|
||||
|
||||
elem.transform(next)
|
||||
list.merge((a1, a2) => {
|
||||
const m1 = getTransform(a1)!
|
||||
const m2 = getTransform(a2)!
|
||||
clearTransform(a2)
|
||||
return createMockAnimator(m2.lmultiply(m1), a2.id)
|
||||
})
|
||||
|
||||
if (list.length() === 1) {
|
||||
frames.delete(elem)
|
||||
}
|
||||
})
|
||||
|
||||
frames.set(elem, frameId)
|
||||
}
|
||||
|
||||
export function clearAnimatorsBefore(elem: Dom, animator: BaseAnimator) {
|
||||
const cache = animators.get(elem)!
|
||||
return cache.clearBefore(animator.id)
|
||||
}
|
||||
|
||||
export function getCurrentTransform(elem: Dom, animator: BaseAnimator) {
|
||||
const arr = animators.get(elem)!
|
||||
|
||||
return (
|
||||
arr.animators
|
||||
// we need the equal sign here to make sure, that also transformations
|
||||
// on the same runner which execute before the current transformation are
|
||||
// taken into account
|
||||
.filter((item) => item.id <= animator.id)
|
||||
.map((animator) => getTransform(animator)!)
|
||||
.reduce((memo, curr) => memo.lmultiplyO(curr), new Matrix())
|
||||
)
|
||||
}
|
||||
|
||||
const transforms: WeakMap<BaseAnimator | MockedAnimator, Matrix> = new WeakMap()
|
||||
|
||||
export function addTransform(
|
||||
animator: BaseAnimator | MockedAnimator,
|
||||
transform?: Matrix.Raw,
|
||||
) {
|
||||
let ctm = transforms.get(animator)
|
||||
if (ctm == null) {
|
||||
ctm = new Matrix()
|
||||
}
|
||||
|
||||
if (transform) {
|
||||
ctm.lmultiplyO(transform)
|
||||
}
|
||||
|
||||
transforms.set(animator, ctm)
|
||||
}
|
||||
|
||||
export function clearTransform(animator: BaseAnimator | MockedAnimator) {
|
||||
transforms.set(animator, new Matrix())
|
||||
}
|
||||
|
||||
export function getTransform(animator: BaseAnimator | MockedAnimator) {
|
||||
return transforms.get(animator)
|
||||
}
|
61
packages/x6-vector/src/animation/types.ts
Normal file
61
packages/x6-vector/src/animation/types.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { DomAnimator } from './dom'
|
||||
import { SVGAnimator } from './svg'
|
||||
import { SVGAAnimator } from './vector/a'
|
||||
import { SVGClipPathAnimator } from './vector/clippath'
|
||||
import { SVGGAnimator } from './vector/g'
|
||||
import { SVGDefsAnimator } from './vector/defs'
|
||||
import { SVGGradientAnimator } from './vector/gradient'
|
||||
import { SVGMarkerAnimator } from './vector/marker'
|
||||
import { SVGMaskAnimator } from './vector/mask'
|
||||
import { SVGPatternAnimator } from './vector/pattern'
|
||||
import { SVGSVGAnimator } from './vector/svg'
|
||||
import { SVGSymbolAnimator } from './vector/symbol'
|
||||
import { SVGCircleAnimator } from './vector/circle'
|
||||
import { SVGEllipseAnimator } from './vector/ellipse'
|
||||
import { SVGImageAnimator } from './vector/image'
|
||||
import { SVGLineAnimator } from './vector/line'
|
||||
import { SVGPolygonAnimator } from './vector/polygon'
|
||||
import { SVGPolylineAnimator } from './vector/polyline'
|
||||
import { SVGRectAnimator } from './vector/rect'
|
||||
import { SVGStyleAnimator } from './vector/style'
|
||||
import { SVGTextAnimator } from './vector/text'
|
||||
import { SVGTSpanAnimator } from './vector/tspan'
|
||||
import { SVGUseAnimator } from './vector/use'
|
||||
import { SVGPathAnimator } from './vector/path'
|
||||
import { SVGForeignObjectAnimator } from './vector/foreignobject'
|
||||
import { SVGLinearGradientAnimator } from './vector/linear-gradient'
|
||||
import { SVGRadialGradientAnimator } from './vector/radial-gradient'
|
||||
|
||||
// prettier-ignore
|
||||
export type AnimatorMap<T> =
|
||||
T extends SVGAElement ? SVGAAnimator
|
||||
: T extends SVGLineElement ? SVGLineAnimator
|
||||
: T extends SVGPathElement ? SVGPathAnimator
|
||||
: T extends SVGCircleElement ? SVGCircleAnimator
|
||||
: T extends SVGClipPathElement ? SVGClipPathAnimator
|
||||
: T extends SVGEllipseElement ? SVGEllipseAnimator
|
||||
: T extends SVGImageElement ? SVGImageAnimator
|
||||
: T extends SVGRectElement ? SVGRectAnimator
|
||||
: T extends SVGUseElement ? SVGUseAnimator
|
||||
: T extends SVGSVGElement ? SVGSVGAnimator
|
||||
: T extends SVGForeignObjectElement ? SVGForeignObjectAnimator
|
||||
: T extends SVGTSpanElement ? SVGTSpanAnimator
|
||||
: T extends SVGTextElement ? SVGTextAnimator
|
||||
: T extends SVGPolylineElement ? SVGPolylineAnimator
|
||||
: T extends SVGPolygonElement ? SVGPolygonAnimator
|
||||
: T extends SVGGElement ? SVGGAnimator
|
||||
: T extends SVGDefsElement ? SVGDefsAnimator
|
||||
: T extends SVGGradientElement ? SVGGradientAnimator<T>
|
||||
: T extends SVGLinearGradientElement ? SVGLinearGradientAnimator
|
||||
: T extends SVGRadialGradientElement ? SVGRadialGradientAnimator
|
||||
: T extends SVGMarkerElement ? SVGMarkerAnimator
|
||||
: T extends SVGMaskElement ? SVGMaskAnimator
|
||||
: T extends SVGPatternElement ? SVGPatternAnimator
|
||||
: T extends SVGPolygonElement ? SVGPolygonAnimator
|
||||
: T extends SVGPolylineElement ? SVGPolylineAnimator
|
||||
: T extends SVGSymbolElement ? SVGSymbolAnimator
|
||||
: T extends SVGStyleElement ? SVGStyleAnimator
|
||||
: T extends SVGElement ? SVGAnimator<T>
|
||||
|
||||
: T extends Element ? DomAnimator<T>
|
||||
: DomAnimator<any>
|
@ -1,4 +1,4 @@
|
||||
import { A } from '../../../element/container/a'
|
||||
import { A } from '../../vector/a/a'
|
||||
import { SVGContainerGeometryAnimator } from './container-geometry'
|
||||
|
||||
@SVGAAnimator.register('A')
|
@ -1,4 +1,4 @@
|
||||
import { Circle } from '../../../element/shape/circle'
|
||||
import { Circle } from '../../vector/circle/circle'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGCircleAnimator.register('Circle')
|
@ -1,4 +1,4 @@
|
||||
import { ClipPath } from '../../../element/container/clippath'
|
||||
import { ClipPath } from '../../vector/clippath/clippath'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
@SVGClipPathAnimator.register('ClipPath')
|
@ -0,0 +1,7 @@
|
||||
import { GeometryContainer } from '../../vector/container/geometry'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
export class SVGContainerGeometryAnimator<
|
||||
TSVGElement extends SVGAElement | SVGGElement,
|
||||
TOwner extends GeometryContainer<TSVGElement> = GeometryContainer<TSVGElement>
|
||||
> extends SVGContainerAnimator<TSVGElement, TOwner> {}
|
@ -1,7 +1,7 @@
|
||||
import { Point } from '../../../struct/point'
|
||||
import { Viewbox } from '../../../element/container/container-viewbox'
|
||||
import { Morpher } from '../../morpher/morpher'
|
||||
import { MorphableBox } from '../../morpher/morphable-box'
|
||||
import { Point } from '../../struct/point'
|
||||
import { Viewbox } from '../../vector/container/viewbox'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphableBox } from '../../animating/morpher/box'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGViewboxAnimator<
|
||||
@ -10,8 +10,8 @@ export class SVGViewboxAnimator<
|
||||
| SVGSymbolElement
|
||||
| SVGPatternElement
|
||||
| SVGMarkerElement,
|
||||
TTarget extends Viewbox<TSVGElement> = Viewbox<TSVGElement>
|
||||
> extends SVGAnimator<TSVGElement, TTarget> {
|
||||
TOwner extends Viewbox<TSVGElement> = Viewbox<TSVGElement>
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {
|
||||
zoom(level: number, point: Point.PointLike) {
|
||||
if (this.retarget('zoom', level, point)) {
|
||||
return this
|
7
packages/x6-vector/src/animation/vector/container.ts
Normal file
7
packages/x6-vector/src/animation/vector/container.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Container } from '../../vector/container/container'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGContainerAnimator<
|
||||
TSVGElement extends SVGElement,
|
||||
TOwner extends Container<TSVGElement> = Container<TSVGElement>
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {}
|
@ -1,4 +1,4 @@
|
||||
import { Defs } from '../../../element/container/defs'
|
||||
import { Defs } from '../../vector/defs/defs'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGDefsAnimator.register('Defs')
|
@ -1,4 +1,4 @@
|
||||
import { Ellipse } from '../../../element/shape/ellipse'
|
||||
import { Ellipse } from '../../vector/ellipse/ellipse'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGEllipseAnimator.register('Ellipse')
|
@ -1,4 +1,4 @@
|
||||
import { ForeignObject } from '../../../element/shape/foreignobject'
|
||||
import { ForeignObject } from '../../vector/foreignobject/foreignobject'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGForeignObjectAnimator.register('ForeignObject')
|
@ -1,4 +1,4 @@
|
||||
import { G } from '../../../element/container/g'
|
||||
import { G } from '../../vector/g/g'
|
||||
import { SVGContainerGeometryAnimator } from './container-geometry'
|
||||
|
||||
@SVGGAnimator.register('G')
|
7
packages/x6-vector/src/animation/vector/gradient.ts
Normal file
7
packages/x6-vector/src/animation/vector/gradient.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Gradient } from '../../vector/gradient/gradient'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
export class SVGGradientAnimator<
|
||||
TSVGElement extends SVGGradientElement,
|
||||
TOwner extends Gradient<TSVGElement> = Gradient<TSVGElement>
|
||||
> extends SVGContainerAnimator<TSVGElement, TOwner> {}
|
@ -1,4 +1,4 @@
|
||||
import { Image } from '../../../element/shape/image'
|
||||
import { Image } from '../../vector/image/image'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGImageAnimator.register('Image')
|
@ -1,7 +1,7 @@
|
||||
import { Line } from '../../../element/shape/line'
|
||||
import { PointArray } from '../../../struct/point-array'
|
||||
import { MorphablePointArray } from '../../morpher/morphable-point-array'
|
||||
import { Morpher } from '../../morpher/morpher'
|
||||
import { Line } from '../../vector/line/line'
|
||||
import { PointArray } from '../../struct/point-array'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphablePointArray } from '../../animating/morpher/point-array'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGLineAnimator.register('Line')
|
@ -0,0 +1,8 @@
|
||||
import { LinearGradient } from '../../vector/gradient/linear'
|
||||
import { SVGGradientAnimator } from './gradient'
|
||||
|
||||
@SVGLinearGradientAnimator.register('LinearGradient')
|
||||
export class SVGLinearGradientAnimator extends SVGGradientAnimator<
|
||||
SVGLinearGradientElement,
|
||||
LinearGradient
|
||||
> {}
|
@ -1,4 +1,4 @@
|
||||
import { Marker } from '../../../element/container/marker'
|
||||
import { Marker } from '../../vector/marker/marker'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGMarkerAnimator.register('Marker')
|
@ -1,4 +1,4 @@
|
||||
import { Mask } from '../../../element/container/mask'
|
||||
import { Mask } from '../../vector/mask/mask'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
@SVGMaskAnimator.register('Mask')
|
@ -1,7 +1,7 @@
|
||||
import { Path } from '../../../element/shape/path'
|
||||
import { PathArray } from '../../../struct/path-array'
|
||||
import { MorphablePathArray } from '../../morpher/morphable-path-array'
|
||||
import { Morpher } from '../../morpher/morpher'
|
||||
import { Path } from '../../vector/path/path'
|
||||
import { PathArray } from '../../struct/path-array'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphablePathArray } from '../../animating/morpher/path-array'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGPathAnimator.register('Path')
|
@ -1,4 +1,4 @@
|
||||
import { Pattern } from '../../../element/container/pattern'
|
||||
import { Pattern } from '../../vector/pattern/pattern'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGPatternAnimator.register('Pattern')
|
@ -1,12 +1,12 @@
|
||||
import { Poly } from '../../../element/shape/poly'
|
||||
import { MorphablePointArray } from '../../morpher/morphable-point-array'
|
||||
import { Morpher } from '../../morpher/morpher'
|
||||
import { Poly } from '../../vector/poly/poly'
|
||||
import { MorphablePointArray } from '../../animating/morpher/point-array'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGPolyAnimator<
|
||||
TSVGElement extends SVGPolygonElement | SVGPolylineElement,
|
||||
TTarget extends Poly<TSVGElement> = Poly<TSVGElement>
|
||||
> extends SVGAnimator<TSVGElement, TTarget> {
|
||||
TOwner extends Poly<TSVGElement> = Poly<TSVGElement>
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {
|
||||
plot(d: string): this
|
||||
plot(points: [number, number][]): this
|
||||
plot(points: string | [number, number][]): this
|
@ -1,4 +1,4 @@
|
||||
import { Polygon } from '../../../element/shape/polygon'
|
||||
import { Polygon } from '../../vector/polygon/polygon'
|
||||
import { SVGPolyAnimator } from './poly'
|
||||
|
||||
@SVGPolygonAnimator.register('Polygon')
|
@ -1,4 +1,4 @@
|
||||
import { Polyline } from '../../../element/shape/polyline'
|
||||
import { Polyline } from '../../vector/polyline/polyline'
|
||||
import { SVGPolyAnimator } from './poly'
|
||||
|
||||
@SVGPolylineAnimator.register('Polyline')
|
@ -0,0 +1,8 @@
|
||||
import { RadialGradient } from '../../vector/gradient/radial'
|
||||
import { SVGGradientAnimator } from './gradient'
|
||||
|
||||
@SVGRadialGradientAnimator.register('RadialGradient')
|
||||
export class SVGRadialGradientAnimator extends SVGGradientAnimator<
|
||||
SVGRadialGradientElement,
|
||||
RadialGradient
|
||||
> {}
|
@ -1,4 +1,4 @@
|
||||
import { Rect } from '../../../element/shape/rect'
|
||||
import { Rect } from '../../vector/rect/rect'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGRectAnimator.register('Rect')
|
@ -1,4 +1,4 @@
|
||||
import { Style } from '../../../element/shape/style'
|
||||
import { Style } from '../../vector/style/style'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGStyleAnimator.register('Style')
|
@ -1,4 +1,4 @@
|
||||
import { Svg } from '../../../element/container/svg'
|
||||
import { Svg } from '../../vector/svg/svg'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGSVGAnimator.register('Svg')
|
@ -1,4 +1,4 @@
|
||||
import { Symbol } from '../../../element/container/symbol'
|
||||
import { Symbol } from '../../vector/symbol/symbol'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGSymbolAnimator.register('Symbol')
|
@ -1,4 +1,4 @@
|
||||
import { Text } from '../../../element/shape/text'
|
||||
import { Text } from '../../vector/text/text'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGTextAnimator.register('Text')
|
@ -1,4 +1,4 @@
|
||||
import { TSpan } from '../../../element/shape/tspan'
|
||||
import { TSpan } from '../../vector/tspan/tspan'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGTSpanAnimator.register('Tspan')
|
@ -1,4 +1,4 @@
|
||||
import { Use } from '../../../element/shape/use'
|
||||
import { Use } from '../../vector/use/use'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGUseAnimator.register('Use')
|
125
packages/x6-vector/src/dom/attributes/attributes.ts
Normal file
125
packages/x6-vector/src/dom/attributes/attributes.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { Base } from '../common/base'
|
||||
import { Util } from './util'
|
||||
import { Core } from './core'
|
||||
import { Special } from './special'
|
||||
import { CSSProperties } from '../style'
|
||||
import { AttributesMap } from './types'
|
||||
import { AttributesBase } from './base'
|
||||
|
||||
export class Attributes<
|
||||
TElement extends Element,
|
||||
Attributes extends AttributesMap<TElement> = AttributesMap<TElement>,
|
||||
Keys extends keyof Attributes = keyof Attributes
|
||||
>
|
||||
extends Base<TElement>
|
||||
implements AttributesBase {
|
||||
attr(): Record<string, string | number | boolean | undefined | null> & {
|
||||
style: CSSProperties
|
||||
}
|
||||
attr<K extends Keys>(
|
||||
names: K[],
|
||||
): Record<string, string | number | boolean | undefined | null> & {
|
||||
style?: CSSProperties
|
||||
}
|
||||
attr(
|
||||
names: string[],
|
||||
): Record<string, string | number | boolean | undefined | null> & {
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
attr(attrs: Attributes): this
|
||||
attr(attrs: { [name: string]: any }): this
|
||||
attr<K extends Keys>(name: K, value: Exclude<Attributes[K], undefined>): this
|
||||
attr<K extends Keys>(name: K, value: null): this
|
||||
attr<K extends Keys>(name: K, value: string | number | boolean): this
|
||||
attr(name: string, value: null): this
|
||||
attr(name: string, value: string | number | boolean | null): this
|
||||
attr(name: 'style', style: CSSProperties): this
|
||||
|
||||
attr(name: 'style'): CSSProperties
|
||||
attr<T extends Attributes[K], K extends Keys>(name: K): T
|
||||
attr<T extends string | number | boolean>(name: string): T
|
||||
|
||||
attr<T extends Attributes[K], K extends Keys>(
|
||||
name: K,
|
||||
value?: Attributes[K] | null,
|
||||
): T | this
|
||||
|
||||
attr(
|
||||
attr?: string | string[] | Record<string, any>,
|
||||
value?: string | number | boolean | null,
|
||||
): Record<string, any> | string | number | this
|
||||
attr(
|
||||
attr?: string | string[] | Record<string, any>,
|
||||
val?: string | number | boolean | null | CSSProperties,
|
||||
) {
|
||||
const node = this.node
|
||||
|
||||
// get all attributes
|
||||
if (attr == null) {
|
||||
const result: Record<string, any> = {}
|
||||
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) {
|
||||
const name = Util.camelCase(item.nodeName)
|
||||
result[name] = Core.getAttribute(node, name, item.nodeValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// get attributes by specified attribute names
|
||||
if (Array.isArray(attr)) {
|
||||
return attr.reduce<Record<string, any>>((memo, name) => {
|
||||
const attrName = Core.getAttributeName(name)
|
||||
const attrValue = node.getAttribute(attrName) || ''
|
||||
// keep the given names
|
||||
memo[name] = Core.getAttribute(node, Util.camelCase(name), attrValue)
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
if (typeof attr === 'object') {
|
||||
Object.keys(attr).forEach((key) => this.attr(key, attr[key]))
|
||||
return this
|
||||
}
|
||||
|
||||
const special = Special.get(attr)
|
||||
|
||||
// remove attribute
|
||||
if (Core.shouldRemoveAttribute(attr, val, special)) {
|
||||
node.removeAttribute(attr)
|
||||
return this
|
||||
}
|
||||
|
||||
// get attribute by name
|
||||
if (val === undefined) {
|
||||
const attrName = Core.getAttributeName(attr)
|
||||
const attrValue = node.getAttribute(attrName) || ''
|
||||
return Core.getAttribute(node, Util.camelCase(attr), attrValue)
|
||||
}
|
||||
|
||||
// set attribute by k-v
|
||||
Core.setAttribute(node, attr, val)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
round<K extends Keys>(precision = 2, names?: K[]) {
|
||||
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
|
||||
}
|
||||
}
|
6
packages/x6-vector/src/dom/attributes/base.ts
Normal file
6
packages/x6-vector/src/dom/attributes/base.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export abstract class AttributesBase {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
attr(k?: any, v?: any) {
|
||||
return this
|
||||
}
|
||||
}
|
235
packages/x6-vector/src/dom/attributes/core.ts
Normal file
235
packages/x6-vector/src/dom/attributes/core.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import { Env } from '../../global/env'
|
||||
import { Special } from './special'
|
||||
import { Util as Style } from '../style/util'
|
||||
import { Hook } from './hook'
|
||||
import { Util } from './util'
|
||||
|
||||
export namespace Core {
|
||||
const ATTRIBUTE_NAME_START_CHAR = `:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD`
|
||||
const ATTRIBUTE_NAME_CHAR = `${ATTRIBUTE_NAME_START_CHAR}\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040`
|
||||
// eslint-disable-next-line
|
||||
const VALID_ATTRIBUTE_NAME_REGEX = new RegExp(
|
||||
`^[${ATTRIBUTE_NAME_START_CHAR}][${ATTRIBUTE_NAME_CHAR}]*$`,
|
||||
)
|
||||
|
||||
const illegalNames: { [key: string]: true } = {}
|
||||
const validedNames: { [key: string]: true } = {}
|
||||
|
||||
export function isAttributeNameSafe(attributeName: string): boolean {
|
||||
if (attributeName in validedNames) {
|
||||
return true
|
||||
}
|
||||
if (attributeName in illegalNames) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {
|
||||
validedNames[attributeName] = true
|
||||
return true
|
||||
}
|
||||
|
||||
illegalNames[attributeName] = true
|
||||
|
||||
if (Env.isDev) {
|
||||
console.error('Invalid attribute name: `%s`', attributeName)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Core {
|
||||
export function shouldRemoveAttribute(
|
||||
name: string,
|
||||
value: any,
|
||||
special: Special | null,
|
||||
): boolean {
|
||||
if (value === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof value === 'function' || typeof value === 'symbol') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
if (special !== null) {
|
||||
if (!special.acceptsBooleans) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
const prefix = name.toLowerCase().slice(0, 5)
|
||||
if (prefix !== 'data-' && prefix !== 'aria-') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (special !== null) {
|
||||
if (special.removeEmptyString && value === '') {
|
||||
if (Env.isDev) {
|
||||
if (name === 'src' || name === 'href' || name === 'xlinkHref') {
|
||||
console.error(
|
||||
'An empty string ("") was passed to the %s attribute. ' +
|
||||
'This may cause the browser to download the whole page again over the network. ' +
|
||||
'To fix this, either do not render the element at all ' +
|
||||
'or pass null to %s instead of an empty string.',
|
||||
name,
|
||||
name,
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
'An empty string ("") was passed to the %s attribute. ' +
|
||||
'To fix this, either do not render the element at all ' +
|
||||
'or pass null to %s instead of an empty string.',
|
||||
name,
|
||||
name,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
switch (special.type) {
|
||||
case Special.Type.BOOLEAN:
|
||||
return !value
|
||||
case Special.Type.OVERLOADED_BOOLEAN:
|
||||
return value === false
|
||||
case Special.Type.NUMERIC:
|
||||
return Number.isNaN(value)
|
||||
case Special.Type.POSITIVE_NUMERIC:
|
||||
return Number.isNaN(value) || value < 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Core {
|
||||
export function getAttribute<TElement extends Element>(
|
||||
node: TElement,
|
||||
name: string,
|
||||
value: string,
|
||||
) {
|
||||
const hook = Hook.get(name)
|
||||
if (hook && hook.get) {
|
||||
const result = hook.get(node)
|
||||
if (typeof result !== 'undefined') {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'style') {
|
||||
return Style.style(node)
|
||||
}
|
||||
|
||||
const special = Special.get(name)
|
||||
if (special) {
|
||||
if (
|
||||
special.type === Special.Type.BOOLEAN ||
|
||||
special.type === Special.Type.BOOLEANISH_STRING
|
||||
) {
|
||||
const cased = value.toLowerCase()
|
||||
return cased === 'true' ? true : cased === 'false' ? false : undefined
|
||||
}
|
||||
|
||||
if (special.type === Special.Type.OVERLOADED_BOOLEAN) {
|
||||
const cased = value.toLowerCase()
|
||||
if (cased === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (cased === 'false') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
special.type === Special.Type.NUMERIC ||
|
||||
special.type === Special.Type.POSITIVE_NUMERIC
|
||||
) {
|
||||
const num = Util.tryConvertToNumber(value)
|
||||
return typeof num === 'number' && Number.isFinite(num) ? num : undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (value === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
return Util.tryConvertToNumber(value)
|
||||
}
|
||||
|
||||
export function getAttributeName(name: string) {
|
||||
const special = Special.get(name)
|
||||
return special ? special.attributeName : name
|
||||
}
|
||||
|
||||
export function setAttribute<TElement extends Element>(
|
||||
node: TElement,
|
||||
name: string,
|
||||
value: any,
|
||||
) {
|
||||
const hook = Hook.get(name)
|
||||
if (hook && hook.set) {
|
||||
// convert and return the new value of attribute
|
||||
const result = hook.set(node, value)
|
||||
if (typeof result !== 'undefined') {
|
||||
value = result // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
const special = Special.get(name)
|
||||
if (special == null) {
|
||||
if (isAttributeNameSafe(name)) {
|
||||
node.setAttribute(name, value)
|
||||
}
|
||||
} else {
|
||||
const { mustUseProperty, propertyName } = special
|
||||
if (mustUseProperty) {
|
||||
const el = node as any
|
||||
if (value === null) {
|
||||
el[propertyName] = special.type === Special.Type.BOOLEAN ? false : ''
|
||||
} else {
|
||||
el[propertyName] = value
|
||||
}
|
||||
}
|
||||
|
||||
const { attributeName } = special
|
||||
if (value === null) {
|
||||
node.removeAttribute(attributeName)
|
||||
} else {
|
||||
let attributeValue: string | undefined
|
||||
|
||||
if (
|
||||
special.type === Special.Type.BOOLEAN ||
|
||||
(special.type === Special.Type.OVERLOADED_BOOLEAN && value === true)
|
||||
) {
|
||||
attributeValue = ''
|
||||
} else {
|
||||
attributeValue = `${value}`
|
||||
if (special.sanitizeURL) {
|
||||
Util.sanitizeURL(attributeName, attributeValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (special.attributeNamespace) {
|
||||
node.setAttributeNS(
|
||||
special.attributeNamespace,
|
||||
attributeName,
|
||||
attributeValue,
|
||||
)
|
||||
} else {
|
||||
node.setAttribute(attributeName, attributeValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
packages/x6-vector/src/dom/attributes/hook.ts
Normal file
24
packages/x6-vector/src/dom/attributes/hook.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Util as Style } from '../style/util'
|
||||
|
||||
export namespace Hook {
|
||||
export interface Definition {
|
||||
get?: <TElement extends Element>(node: TElement) => any
|
||||
set?: <TElement extends Element>(node: TElement, attributeValue: any) => any
|
||||
}
|
||||
|
||||
const hooks: Record<string, Definition> = {}
|
||||
|
||||
export function get(attributeName: string) {
|
||||
return hooks[attributeName]
|
||||
}
|
||||
|
||||
export function register(attributeName: string, hook: Definition) {
|
||||
hooks[attributeName] = hook
|
||||
}
|
||||
|
||||
register('style', {
|
||||
get(node) {
|
||||
return Style.style(node)
|
||||
},
|
||||
})
|
||||
}
|
4
packages/x6-vector/src/dom/attributes/index.ts
Normal file
4
packages/x6-vector/src/dom/attributes/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './hook'
|
||||
export * from './base'
|
||||
export * from './attributes'
|
||||
export { AttributesMap } from './types'
|
542
packages/x6-vector/src/dom/attributes/special.ts
Normal file
542
packages/x6-vector/src/dom/attributes/special.ts
Normal file
@ -0,0 +1,542 @@
|
||||
import { Util } from './util'
|
||||
|
||||
export class Special {
|
||||
public readonly acceptsBooleans: boolean
|
||||
|
||||
constructor(
|
||||
public readonly type: Special.Type,
|
||||
public readonly propertyName: string,
|
||||
public readonly attributeName: string,
|
||||
public readonly attributeNamespace: string | null = null,
|
||||
public readonly mustUseProperty: boolean = false,
|
||||
public readonly sanitizeURL: boolean = false,
|
||||
public readonly removeEmptyString: boolean = false,
|
||||
) {
|
||||
this.acceptsBooleans =
|
||||
type === Special.Type.BOOLEAN ||
|
||||
type === Special.Type.BOOLEANISH_STRING ||
|
||||
type === Special.Type.OVERLOADED_BOOLEAN
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
export enum Type {
|
||||
/**
|
||||
* A simple string attribute.
|
||||
*
|
||||
* Attributes that aren't in the filter are presumed to have this type.
|
||||
*/
|
||||
STRING,
|
||||
/**
|
||||
* A string attribute that accepts booleans. In HTML, these are called
|
||||
* "enumerated" attributes with "true" and "false" as possible values.
|
||||
*
|
||||
* When true, it should be set to a "true" string.
|
||||
*
|
||||
* When false, it should be set to a "false" string.
|
||||
*/
|
||||
BOOLEANISH_STRING,
|
||||
/**
|
||||
* A real boolean attribute.
|
||||
*
|
||||
* When true, it should be present (set either to an empty string or its name).
|
||||
*
|
||||
* When false, it should be omitted.
|
||||
*/
|
||||
BOOLEAN,
|
||||
/**
|
||||
* An attribute that can be used as a flag as well as with a value.
|
||||
*
|
||||
* When true, it should be present (set either to an empty string or its name).
|
||||
*
|
||||
* When false, it should be omitted.
|
||||
*
|
||||
* For any other value, should be present with that value.
|
||||
*/
|
||||
OVERLOADED_BOOLEAN,
|
||||
|
||||
/**
|
||||
* An attribute that must be numeric or parse as a numeric.
|
||||
*
|
||||
* When falsy, it should be removed.
|
||||
*/
|
||||
NUMERIC,
|
||||
|
||||
/**
|
||||
* An attribute that must be positive numeric or parse as a positive numeric.
|
||||
*
|
||||
* When falsy, it should be removed.
|
||||
*/
|
||||
POSITIVE_NUMERIC,
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
export const specials: Record<string, Special> = {}
|
||||
export function get(name: string) {
|
||||
return specials[name] || null
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
const arr = [
|
||||
'vector:data',
|
||||
'aria-activedescendant',
|
||||
'aria-atomic',
|
||||
'aria-autocomplete',
|
||||
'aria-busy',
|
||||
'aria-checked',
|
||||
'aria-colcount',
|
||||
'aria-colindex',
|
||||
'aria-colspan',
|
||||
'aria-controls',
|
||||
'aria-current',
|
||||
'aria-describedby',
|
||||
'aria-details',
|
||||
'aria-disabled',
|
||||
'aria-dropeffect',
|
||||
'aria-errormessage',
|
||||
'aria-expanded',
|
||||
'aria-flowto',
|
||||
'aria-grabbed',
|
||||
'aria-haspopup',
|
||||
'aria-hidden',
|
||||
'aria-invalid',
|
||||
'aria-keyshortcuts',
|
||||
'aria-label',
|
||||
'aria-labelledby',
|
||||
'aria-level',
|
||||
'aria-live',
|
||||
'aria-modal',
|
||||
'aria-multiline',
|
||||
'aria-multiselectable',
|
||||
'aria-orientation',
|
||||
'aria-owns',
|
||||
'aria-placeholder',
|
||||
'aria-posinset',
|
||||
'aria-pressed',
|
||||
'aria-readonly',
|
||||
'aria-relevant',
|
||||
'aria-required',
|
||||
'aria-roledescription',
|
||||
'aria-rowcount',
|
||||
'aria-rowindex',
|
||||
'aria-rowspan',
|
||||
'aria-selected',
|
||||
'aria-setsize',
|
||||
'aria-sort',
|
||||
'aria-valuemax',
|
||||
'aria-valuemin',
|
||||
'aria-valuenow',
|
||||
'aria-valuetext',
|
||||
]
|
||||
arr.forEach((attributeName) => {
|
||||
const name = Util.camelCase(attributeName)
|
||||
specials[name] = new Special(
|
||||
Type.STRING,
|
||||
name,
|
||||
attributeName,
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
const arr = [
|
||||
'accessKey',
|
||||
'contextMenu',
|
||||
'radioGroup',
|
||||
'autoCapitalize',
|
||||
'autoCorrect',
|
||||
'autoSave',
|
||||
'itemProp',
|
||||
'itemType',
|
||||
'itemID',
|
||||
'itemRef',
|
||||
'input-modalities',
|
||||
'inputMode',
|
||||
'referrerPolicy',
|
||||
'formEncType',
|
||||
'formMethod',
|
||||
'formTarget',
|
||||
'dateTime',
|
||||
'autoComplete',
|
||||
'encType',
|
||||
'allowTransparency',
|
||||
'frameBorder',
|
||||
'marginHeight',
|
||||
'marginWidth',
|
||||
'srcDoc',
|
||||
'crossOrigin',
|
||||
'srcSet',
|
||||
'useMap',
|
||||
'enterKeyHint',
|
||||
'maxLength',
|
||||
'minLength',
|
||||
'keyType',
|
||||
'keyParams',
|
||||
'hrefLang',
|
||||
'charSet',
|
||||
'controlsList',
|
||||
'mediaGroup',
|
||||
'classID',
|
||||
'cellPadding',
|
||||
'cellSpacing',
|
||||
'dirName',
|
||||
'srcLang',
|
||||
]
|
||||
arr.forEach((attributeName) => {
|
||||
specials[attributeName] = new Special(
|
||||
Type.STRING,
|
||||
attributeName,
|
||||
attributeName.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// A few string attributes have a different name.
|
||||
const arr = ['accept-charset', 'http-equiv']
|
||||
arr.forEach((attributeName) => {
|
||||
const name = Util.camelCase(attributeName)
|
||||
specials[name] = new Special(
|
||||
Type.STRING,
|
||||
name,
|
||||
attributeName, // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are HTML boolean attributes.
|
||||
const arr = [
|
||||
'allowFullScreen',
|
||||
'async',
|
||||
// Note: there is a special case that prevents it from being written to the DOM
|
||||
// on the client side because the browsers are inconsistent. Instead we call focus().
|
||||
'autoFocus',
|
||||
'autoPlay',
|
||||
'controls',
|
||||
'default',
|
||||
'defer',
|
||||
'disabled',
|
||||
'disablePictureInPicture',
|
||||
'disableRemotePlayback',
|
||||
'formNoValidate',
|
||||
'hidden',
|
||||
'loop',
|
||||
'noModule',
|
||||
'noValidate',
|
||||
'open',
|
||||
'playsInline',
|
||||
'readOnly',
|
||||
'required',
|
||||
'reversed',
|
||||
'scoped',
|
||||
'seamless',
|
||||
'itemScope',
|
||||
]
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.BOOLEAN,
|
||||
name,
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are "enumerated" HTML attributes that accept "true" and "false".
|
||||
// We can pass `true` and `false` even though technically ese aren't
|
||||
// boolean attributes (they are coerced to strings).
|
||||
const arr = ['contentEditable', 'draggable', 'spellCheck', 'value']
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.BOOLEANISH_STRING,
|
||||
name,
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are "enumerated" SVG attributes that accept "true" and "false".
|
||||
// We can pass `true` and `false` even though technically these aren't
|
||||
// boolean attributes (they are coerced to strings).
|
||||
// Since these are SVG attributes, their attribute names are case-sensitive.
|
||||
const arr = [
|
||||
'autoReverse',
|
||||
'externalResourcesRequired',
|
||||
'focusable',
|
||||
'preserveAlpha',
|
||||
]
|
||||
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.BOOLEANISH_STRING,
|
||||
name,
|
||||
name, // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are the few props that we set as DOM properties
|
||||
// rather than attributes. These are all booleans.
|
||||
const arr = [
|
||||
'checked',
|
||||
// Note: `option.selected` is not updated if `select.multiple` is
|
||||
// disabled with `removeAttribute`. We have special logic for handling this.
|
||||
'multiple',
|
||||
'muted',
|
||||
'selected',
|
||||
]
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.BOOLEAN,
|
||||
name,
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
true, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are HTML attributes that are "overloaded booleans": they behave like
|
||||
// booleans, but can also accept a string value.
|
||||
const arr = ['capture', 'download']
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.OVERLOADED_BOOLEAN,
|
||||
name,
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are HTML attributes that must be positive numbers.
|
||||
const arr = ['cols', 'rows', 'size', 'span']
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.POSITIVE_NUMERIC,
|
||||
name,
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These are HTML attributes that must be numbers.
|
||||
const arr = ['tabIndex', 'rowSpan', 'colSpan', 'start']
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.NUMERIC,
|
||||
name,
|
||||
name.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// This is a list of all SVG attributes that need special casing, namespacing,
|
||||
// or boolean value assignment. Regular attributes that just accept strings
|
||||
// and have the same names are omitted, just like in the HTML attribute filter.
|
||||
// Some of these attributes can be hard to find. This list was created by
|
||||
// scraping the MDN documentation.
|
||||
const arr1 = [
|
||||
'accent-height',
|
||||
'alignment-baseline',
|
||||
'arabic-form',
|
||||
'baseline-shift',
|
||||
'cap-height',
|
||||
'clip-path',
|
||||
'clip-rule',
|
||||
'color-interpolation',
|
||||
'color-interpolation-filters',
|
||||
'color-profile',
|
||||
'color-rendering',
|
||||
'dominant-baseline',
|
||||
'enable-background',
|
||||
'fill-opacity',
|
||||
'fill-rule',
|
||||
'flood-color',
|
||||
'flood-opacity',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-size-adjust',
|
||||
'font-stretch',
|
||||
'font-style',
|
||||
'font-variant',
|
||||
'font-weight',
|
||||
'glyph-name',
|
||||
'glyph-orientation-horizontal',
|
||||
'glyph-orientation-vertical',
|
||||
'horiz-adv-x',
|
||||
'horiz-origin-x',
|
||||
'image-rendering',
|
||||
'letter-spacing',
|
||||
'lighting-color',
|
||||
'marker-end',
|
||||
'marker-mid',
|
||||
'marker-start',
|
||||
'overline-position',
|
||||
'overline-thickness',
|
||||
'paint-order',
|
||||
'panose-1',
|
||||
'pointer-events',
|
||||
'rendering-intent',
|
||||
'shape-rendering',
|
||||
'stop-color',
|
||||
'stop-opacity',
|
||||
'strikethrough-position',
|
||||
'strikethrough-thickness',
|
||||
'stroke-dasharray',
|
||||
'stroke-dashoffset',
|
||||
'stroke-linecap',
|
||||
'stroke-linejoin',
|
||||
'stroke-miterlimit',
|
||||
'stroke-opacity',
|
||||
'stroke-width',
|
||||
'text-anchor',
|
||||
'text-decoration',
|
||||
'text-rendering',
|
||||
'underline-position',
|
||||
'underline-thickness',
|
||||
'unicode-bidi',
|
||||
'unicode-range',
|
||||
'units-per-em',
|
||||
'v-alphabetic',
|
||||
'v-hanging',
|
||||
'v-ideographic',
|
||||
'v-mathematical',
|
||||
'vector-effect',
|
||||
'vert-adv-y',
|
||||
'vert-origin-x',
|
||||
'vert-origin-y',
|
||||
'word-spacing',
|
||||
'writing-mode',
|
||||
'xmlns:xlink',
|
||||
'x-height',
|
||||
]
|
||||
|
||||
arr1.forEach((attributeName) => {
|
||||
const name = Util.camelCase(attributeName)
|
||||
specials[name] = new Special(
|
||||
Type.STRING,
|
||||
name,
|
||||
attributeName,
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
|
||||
// String SVG attributes with the xlink namespace.
|
||||
const arr2 = [
|
||||
'xlink:actuate',
|
||||
'xlink:arcrole',
|
||||
'xlink:role',
|
||||
'xlink:show',
|
||||
'xlink:title',
|
||||
'xlink:type',
|
||||
]
|
||||
arr2.forEach((attributeName) => {
|
||||
const name = Util.camelCase(attributeName)
|
||||
specials[attributeName] = specials[name] = new Special(
|
||||
Type.STRING,
|
||||
name,
|
||||
attributeName,
|
||||
'http://www.w3.org/1999/xlink',
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
|
||||
// String SVG attributes with the xml namespace.
|
||||
const arr3 = ['xml:base', 'xml:lang', 'xml:space']
|
||||
arr3.forEach((attributeName) => {
|
||||
const name = Util.camelCase(attributeName)
|
||||
specials[attributeName] = specials[name] = new Special(
|
||||
Type.STRING,
|
||||
name,
|
||||
attributeName,
|
||||
'http://www.w3.org/XML/1998/namespace', // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
false, // sanitizeURL
|
||||
false, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
// These attributes accept URLs. These must not allow javascript: URLS.
|
||||
// These will also need to accept Trusted Types object in the future.
|
||||
const arr = ['xlinkHref', 'xlink:href']
|
||||
arr.forEach((name) => {
|
||||
specials[name] = new Special(
|
||||
Type.STRING,
|
||||
name,
|
||||
'xlink:href',
|
||||
'http://www.w3.org/1999/xlink',
|
||||
false, // mustUseProperty
|
||||
true, // sanitizeURL
|
||||
true, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
const arr = ['src', 'href', 'action', 'formAction']
|
||||
arr.forEach((attributeName) => {
|
||||
specials[attributeName] = new Special(
|
||||
Type.STRING,
|
||||
attributeName,
|
||||
attributeName.toLowerCase(), // attributeName
|
||||
null, // attributeNamespace
|
||||
false, // mustUseProperty
|
||||
true, // sanitizeURL
|
||||
true, // removeEmptyString
|
||||
)
|
||||
})
|
||||
}
|
8
packages/x6-vector/src/dom/attributes/types.ts
Normal file
8
packages/x6-vector/src/dom/attributes/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SVGAttributesMap } from '../../vector/types'
|
||||
import { HTMLAttributesMap } from '../types'
|
||||
|
||||
export type AttributesMap<T> = T extends SVGElement
|
||||
? SVGAttributesMap<T>
|
||||
: T extends HTMLElement
|
||||
? HTMLAttributesMap<T>
|
||||
: HTMLAttributesMap<HTMLDivElement>
|
40
packages/x6-vector/src/dom/attributes/util.ts
Normal file
40
packages/x6-vector/src/dom/attributes/util.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Env } from '../../global/env'
|
||||
|
||||
export namespace Util {
|
||||
export function tryConvertToNumber(value: string) {
|
||||
const numReg = /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i
|
||||
return numReg.test(value) ? +value : value
|
||||
}
|
||||
|
||||
export function camelCase(str: string) {
|
||||
return str.replace(/[-:]([a-z])/g, (input) => input[1].toUpperCase())
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Util {
|
||||
// A javascript: URL can contain leading C0 control or \u0020 SPACE,
|
||||
// and any newline or tab are filtered out as if they're not part of the URL.
|
||||
// https://url.spec.whatwg.org/#url-parsing
|
||||
// Tab or newline are defined as \r\n\t:
|
||||
// https://infra.spec.whatwg.org/#ascii-tab-or-newline
|
||||
// A C0 control is a code point in the range \u0000 NULL to \u001F
|
||||
// INFORMATION SEPARATOR ONE, inclusive:
|
||||
// https://infra.spec.whatwg.org/#c0-control-or-space
|
||||
|
||||
// eslint-disable-next-line
|
||||
const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i
|
||||
|
||||
let didWarn = false
|
||||
|
||||
export function sanitizeURL(attributeName: string, url: string) {
|
||||
if (Env.isDev) {
|
||||
if (!didWarn && isJavaScriptProtocol.test(url)) {
|
||||
didWarn = true
|
||||
console.error(
|
||||
`Attribute "${attributeName}" with javascript url was blocked for security precaution.` +
|
||||
`Check the passed url: ${JSON.stringify(url)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import { DomUtil } from '../util/dom'
|
||||
import { Global } from '../global'
|
||||
import type { Svg } from './container/svg'
|
||||
import type { Dom } from './dom'
|
||||
import { Global } from '../../global'
|
||||
import { createHTMLNode, createSVGNode } from '../../util'
|
||||
import { Registry } from './registry'
|
||||
import { ElementMapping, HTMLTagMapping, SVGTagMapping } from './types'
|
||||
import type { Dom } from '../dom'
|
||||
import type { Svg } from '../../vector'
|
||||
import type { ElementMap } from '../../types'
|
||||
import type { HTMLAttributesTagNameMap } from '../types'
|
||||
import type { SVGAttributesTagNameMap } from '../../vector/types'
|
||||
|
||||
export namespace Adopter {
|
||||
const store: WeakMap<Node, Dom> = new WeakMap()
|
||||
@ -17,15 +19,11 @@ export namespace Adopter {
|
||||
}
|
||||
|
||||
export function adopt(node: null): null
|
||||
export function adopt<T extends Node>(node: T): ElementMapping<T>
|
||||
export function adopt<T extends Node>(node: T): ElementMap<T>
|
||||
export function adopt<T extends Dom>(node: Node | ChildNode): T
|
||||
export function adopt<T extends Dom>(node: Node | ChildNode | null): T | null
|
||||
export function adopt<T extends Node>(
|
||||
node?: T | null,
|
||||
): ElementMapping<T> | null
|
||||
export function adopt<T extends Node>(
|
||||
node?: T | null,
|
||||
): ElementMapping<T> | null {
|
||||
export function adopt<T extends Node>(node?: T | null): ElementMap<T> | null
|
||||
export function adopt<T extends Node>(node?: T | null): ElementMap<T> | null {
|
||||
if (node == null) {
|
||||
return null
|
||||
}
|
||||
@ -33,11 +31,11 @@ export namespace Adopter {
|
||||
// make sure a node isn't already adopted
|
||||
const instance = store.get(node)
|
||||
if (instance != null) {
|
||||
return instance as ElementMapping<T>
|
||||
return instance as ElementMap<T>
|
||||
}
|
||||
|
||||
const Type = Registry.getClass(node)
|
||||
return new Type(node) as ElementMapping<T>
|
||||
return new Type(node) as ElementMap<T>
|
||||
}
|
||||
|
||||
let adopter = adopt
|
||||
@ -48,18 +46,18 @@ export namespace Adopter {
|
||||
export function makeInstance(node: undefined | null): Svg
|
||||
export function makeInstance<T extends Dom>(instance: T): T
|
||||
export function makeInstance<T extends Dom>(target: Target<T>): T
|
||||
export function makeInstance<T extends Node>(node: T): ElementMapping<T>
|
||||
export function makeInstance<T extends keyof SVGTagMapping>(
|
||||
export function makeInstance<T extends Node>(node: T): ElementMap<T>
|
||||
export function makeInstance<T extends keyof SVGAttributesTagNameMap>(
|
||||
tagName: T,
|
||||
): SVGTagMapping[T]
|
||||
export function makeInstance<T extends keyof SVGTagMapping>(
|
||||
): SVGAttributesTagNameMap[T]
|
||||
export function makeInstance<T extends keyof SVGAttributesTagNameMap>(
|
||||
tagName: T,
|
||||
isHTML: false,
|
||||
): SVGTagMapping[T]
|
||||
export function makeInstance<T extends keyof HTMLTagMapping>(
|
||||
): SVGAttributesTagNameMap[T]
|
||||
export function makeInstance<T extends keyof HTMLAttributesTagNameMap>(
|
||||
tagName: T,
|
||||
isHTML: true,
|
||||
): HTMLTagMapping[T]
|
||||
): HTMLAttributesTagNameMap[T]
|
||||
export function makeInstance<T extends Dom>(
|
||||
tagName: string,
|
||||
isHTML: boolean,
|
||||
@ -86,9 +84,7 @@ export namespace Adopter {
|
||||
}
|
||||
|
||||
// Make sure, that HTML elements are created with the correct namespace
|
||||
const wrapper = isHTML
|
||||
? Global.document.createElement('div')
|
||||
: DomUtil.createNode('svg')
|
||||
const wrapper = isHTML ? createHTMLNode('div') : createSVGNode('svg')
|
||||
wrapper.innerHTML = node
|
||||
|
||||
// We can use firstChild here because we know,
|
42
packages/x6-vector/src/dom/common/base.ts
Normal file
42
packages/x6-vector/src/dom/common/base.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { applyMixins } from '../../util/mixin'
|
||||
import {
|
||||
isDocument,
|
||||
isSVGSVGElement,
|
||||
isDocumentFragment,
|
||||
isInDocument,
|
||||
} from '../../util/dom'
|
||||
import { Registry } from './registry'
|
||||
|
||||
export abstract class Base<TElement extends Element = Element> {
|
||||
public node: TElement
|
||||
|
||||
isDocument() {
|
||||
return isDocument(this.node)
|
||||
}
|
||||
|
||||
isSVGSVGElement() {
|
||||
return isSVGSVGElement(this.node)
|
||||
}
|
||||
|
||||
isDocumentFragment() {
|
||||
return isDocumentFragment(this.node)
|
||||
}
|
||||
|
||||
isInDocument() {
|
||||
return isInDocument(this.node)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Base {
|
||||
export function register(name: string, asRoot?: boolean) {
|
||||
return (ctor: Registry.Definition) => {
|
||||
Registry.register(ctor, name, asRoot)
|
||||
}
|
||||
}
|
||||
|
||||
export function mixin(...source: any[]) {
|
||||
return (ctor: Registry.Definition) => {
|
||||
applyMixins(ctor, ...source)
|
||||
}
|
||||
}
|
||||
}
|
24
packages/x6-vector/src/dom/common/id.ts
Normal file
24
packages/x6-vector/src/dom/common/id.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export namespace ID {
|
||||
let seed = 0
|
||||
|
||||
export function generate() {
|
||||
seed += 1
|
||||
return `vector-${seed}`
|
||||
}
|
||||
|
||||
export function overwrite<TElement extends Element>(
|
||||
node: TElement,
|
||||
deep = true,
|
||||
) {
|
||||
if (deep) {
|
||||
const children = node.children
|
||||
for (let i = children.length - 1; i >= 0; i -= 1) {
|
||||
overwrite(children[i], true)
|
||||
}
|
||||
}
|
||||
|
||||
if (node.id) {
|
||||
node.id = generate()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { Str } from '../util/str'
|
||||
import { Base } from './base'
|
||||
import type { Base } from './base'
|
||||
|
||||
export namespace Registry {
|
||||
export type Definition = { new (...args: any[]): Base }
|
||||
@ -35,14 +34,10 @@ export namespace Registry {
|
||||
}
|
||||
|
||||
const nodeName = node.nodeName
|
||||
let className = Str.ucfirst(nodeName)
|
||||
// ucfirst
|
||||
let className = nodeName.charAt(0).toUpperCase() + nodeName.substring(1)
|
||||
|
||||
if (
|
||||
node instanceof SVGElement &&
|
||||
(className === 'LinearGradient' || className === 'RadialGradient')
|
||||
) {
|
||||
className = 'Gradient'
|
||||
} else if (nodeName === '#document-fragment') {
|
||||
if (nodeName === '#document-fragment') {
|
||||
className = 'Fragment'
|
||||
} else if (!Registry.hasClass(className)) {
|
||||
className = node instanceof SVGElement ? 'Vector' : 'Dom'
|
||||
@ -64,7 +59,8 @@ export namespace Registry {
|
||||
for (let i = 0, l = keys.length; i < l; i += 1) {
|
||||
const key = keys[i]
|
||||
if (cache[key] === cls) {
|
||||
return Str.lcfirst(key)
|
||||
// lcfirst
|
||||
return key.charAt(0).toLowerCase() + key.substring(1)
|
||||
}
|
||||
}
|
||||
return null
|
@ -1,43 +1,16 @@
|
||||
import type { Attrs } from '../../types'
|
||||
import { Global } from '../../global'
|
||||
import { Attr } from '../../util/attr'
|
||||
import { DomUtil } from '../../util/dom'
|
||||
import { Adopter } from '../adopter'
|
||||
import { Data } from './data'
|
||||
import { Affix } from './affix'
|
||||
import { Style } from './style'
|
||||
import { Global } from '../global'
|
||||
import { ID } from './common/id'
|
||||
import * as Util from '../util/dom'
|
||||
import { Adopter } from './common/adopter'
|
||||
import { Registry } from './common/registry'
|
||||
import { Primer } from './primer'
|
||||
import { Memory } from './memory'
|
||||
import { Listener } from './listener'
|
||||
import { ClassName } from './classname'
|
||||
import { Transform } from './transform'
|
||||
import { EventEmitter } from './event-emitter'
|
||||
import { ElementMapping, HTMLTagMapping, SVGTagMapping } from '../types'
|
||||
import { Registry } from '../registry'
|
||||
import { Transform } from './transform/transform'
|
||||
import type { AttributesMap } from './attributes'
|
||||
import type { ElementMap } from '../types'
|
||||
|
||||
@Dom.register('Dom')
|
||||
@Dom.mixin(
|
||||
ClassName,
|
||||
Style,
|
||||
Transform,
|
||||
Data,
|
||||
Affix,
|
||||
Memory,
|
||||
EventEmitter,
|
||||
Listener,
|
||||
)
|
||||
export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
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(node, attrs)
|
||||
this.restoreAffix()
|
||||
Adopter.cache(this.node, this)
|
||||
}
|
||||
|
||||
@Dom.mixin(Transform)
|
||||
export class Dom<TElement extends Element = Element> extends Primer<TElement> {
|
||||
/**
|
||||
* Returns the first child of the element.
|
||||
*/
|
||||
@ -63,21 +36,21 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
* Returns an array of elements matching the given selector.
|
||||
*/
|
||||
find<T extends Dom = Dom>(selector: string): T[] {
|
||||
return Dom.find<T>(selector, DomUtil.toElement(this.node))
|
||||
return Dom.find<T>(selector, this.node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element matching the given selector.
|
||||
*/
|
||||
findOne<T extends Dom = Dom>(selector: string): T | null {
|
||||
return Dom.findOne<T>(selector, DomUtil.toElement(this.node))
|
||||
return Dom.findOne<T>(selector, this.node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the element matching the given selector.
|
||||
*/
|
||||
matches(selector: string): boolean {
|
||||
const elem = DomUtil.toElement(this.node)
|
||||
const elem = this.node
|
||||
const node = this.node as any
|
||||
const matcher = elem.matches
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
@ -119,8 +92,10 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
// write dom data to the dom so the clone can pickup the data
|
||||
this.storeAffix(deep)
|
||||
// clone element and assign new id
|
||||
const Ctor = this.constructor as new (node: Node) => ElementMapping<TNode>
|
||||
return new Ctor(DomUtil.assignNewId(this.node.cloneNode(deep)))
|
||||
const Ctor = this.constructor as new (node: Element) => ElementMap<TElement>
|
||||
const cloned = this.node.cloneNode(deep) as Element
|
||||
ID.overwrite(cloned, true)
|
||||
return new Ctor(cloned)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -189,6 +164,15 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
return parent ? parent.indexOf(this) : -1
|
||||
}
|
||||
|
||||
contains(node: Node): boolean
|
||||
contains(element: Dom): boolean
|
||||
contains(element: Dom | Node): boolean {
|
||||
return Util.isAncestorOf(
|
||||
this.node,
|
||||
element instanceof Node ? element : element.node,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element's id, generate new id if no id set.
|
||||
*/
|
||||
@ -198,11 +182,9 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
*/
|
||||
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()
|
||||
if (typeof id === 'undefined' && this.node.id == null) {
|
||||
this.node.id = ID.generate()
|
||||
}
|
||||
|
||||
// dont't set directly with this.node.id to make `null` work correctly
|
||||
@ -332,7 +314,7 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
* Adds the given node to the end fo child list or the optional child position
|
||||
* and returns the added element.
|
||||
*/
|
||||
put<T extends Node>(node: T, index?: number): ElementMapping<T>
|
||||
put<T extends Node>(node: T, index?: number): ElementMap<T>
|
||||
/**
|
||||
* Adds the node matching the selector to end fo child list or the optional
|
||||
* child position and returns the added element.
|
||||
@ -346,7 +328,7 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
}
|
||||
|
||||
putIn<T extends Dom>(parentElement: T, index?: number): T
|
||||
putIn<T extends Node>(parentNode: T, index?: number): ElementMapping<T>
|
||||
putIn<T extends Node>(parentNode: T, index?: number): ElementMap<T>
|
||||
putIn<T extends Dom>(selector: string, index?: number): T
|
||||
putIn<T extends Dom>(parent: Adopter.Target<T>, index?: number): T
|
||||
putIn<T extends Dom>(parent: Adopter.Target<T>, index?: number): T {
|
||||
@ -354,7 +336,7 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
}
|
||||
|
||||
replace<T extends Dom>(element: T, index?: number): T
|
||||
replace<T extends Node>(node: T, index?: number): ElementMapping<T>
|
||||
replace<T extends Node>(node: T, index?: number): ElementMap<T>
|
||||
replace<T extends Dom>(selector: string, index?: number): T
|
||||
replace<T extends Dom>(element: Adopter.Target<T>, index?: number): T
|
||||
replace<T extends Dom>(element: Adopter.Target<T>): T {
|
||||
@ -369,13 +351,16 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
|
||||
element<T extends keyof SVGElementTagNameMap>(
|
||||
nodeName: T,
|
||||
attrs?: Attrs | null,
|
||||
): SVGTagMapping[T]
|
||||
attrs?: AttributesMap<SVGElementTagNameMap[T]> | null,
|
||||
): ElementMap<T>
|
||||
element<T extends keyof HTMLElementTagNameMap>(
|
||||
nodeName: T,
|
||||
attrs?: Attrs | null,
|
||||
): HTMLTagMapping[T]
|
||||
element<T extends Dom>(nodeName: string, attrs?: Attrs | null): T {
|
||||
attrs?: AttributesMap<HTMLElementTagNameMap[T]> | null,
|
||||
): ElementMap<T>
|
||||
element<T extends Dom>(
|
||||
nodeName: string,
|
||||
attrs?: AttributesMap<any> | null,
|
||||
): T {
|
||||
const elem = Adopter.makeInstance<T>(nodeName)
|
||||
if (attrs) {
|
||||
elem.attr(attrs)
|
||||
@ -592,42 +577,30 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the is of the node.
|
||||
*/
|
||||
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)
|
||||
return this.xml(arg1, arg2, Util.namespaces.html)
|
||||
}
|
||||
|
||||
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,
|
||||
): string
|
||||
xml(
|
||||
arg1?: boolean | string | ((dom: Dom) => false | Dom),
|
||||
arg2?: boolean,
|
||||
@ -687,13 +660,13 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
// The default for import is, that the current node is not replaced
|
||||
isOuterXML = isOuterXML == null ? false : isOuterXML
|
||||
|
||||
const wrapper = DomUtil.createNode('wrapper', ns)
|
||||
const wrapper = Util.createNode('wrapper', ns || Util.namespaces.html)
|
||||
const fragment = Global.document.createDocumentFragment()
|
||||
|
||||
wrapper.innerHTML = content
|
||||
|
||||
for (let i = wrapper.children.length; i > 0; i -= 1) {
|
||||
fragment.append(wrapper.firstElementChild!)
|
||||
fragment.appendChild(wrapper.firstElementChild!)
|
||||
}
|
||||
|
||||
if (isOuterXML) {
|
||||
@ -707,15 +680,8 @@ export class Dom<TNode extends Node = Node> extends Primer<TNode> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Dom<TNode extends Node = Node>
|
||||
extends ClassName<TNode>,
|
||||
Style<TNode>,
|
||||
Transform<TNode>,
|
||||
Data<TNode>,
|
||||
Affix<TNode>,
|
||||
Memory<TNode>,
|
||||
EventEmitter<TNode>,
|
||||
Listener<TNode> {}
|
||||
export interface Dom<TElement extends Element = Element>
|
||||
extends Transform<TElement> {}
|
||||
|
||||
export namespace Dom {
|
||||
export const adopt = Adopter.adopt
|
||||
@ -738,9 +704,3 @@ export namespace Dom {
|
||||
return adopt<T>(parent.querySelector(selectors))
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Dom {
|
||||
export const registerAttrHook = Attr.registerHook
|
||||
export const registerEventHook = EventEmitter.registerHook
|
||||
export const registerStyleHook = Style.registerHook
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { isWindow } from '../../util'
|
||||
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'
|
||||
import { Util } from './util'
|
||||
import { Hook } from './hook'
|
||||
import { Store } from './store'
|
||||
import { EventRaw } from './alias'
|
||||
import { EventObject } from './object'
|
||||
import { EventHandler } from './types'
|
||||
|
||||
export namespace Core {
|
||||
let triggered: string | undefined
|
||||
@ -94,7 +94,7 @@ export namespace Core {
|
||||
!hook.setup ||
|
||||
hook.setup(elem, data, namespaces, mainHandler!) === false
|
||||
) {
|
||||
DomUtil.addEventListener(
|
||||
Util.addEventListener(
|
||||
elem as Element,
|
||||
type,
|
||||
(mainHandler as any) as EventListener,
|
||||
@ -185,7 +185,7 @@ export namespace Core {
|
||||
!hook.teardown ||
|
||||
hook.teardown(elem, namespaces, store.handler!) === false
|
||||
) {
|
||||
DomUtil.removeEventListener(
|
||||
Util.removeEventListener(
|
||||
elem as Element,
|
||||
type,
|
||||
(store.handler as any) as EventListener,
|
||||
@ -340,7 +340,7 @@ export namespace Core {
|
||||
// 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)) {
|
||||
if (!onlyHandlers && !hook.noBubble && !isWindow(node)) {
|
||||
bubbleType = hook.delegateType || type
|
||||
|
||||
let curr = node
|
||||
@ -410,7 +410,7 @@ export namespace Core {
|
||||
if (
|
||||
ontype &&
|
||||
typeof node[type as 'click'] === 'function' &&
|
||||
!DomUtil.isWindow(node)
|
||||
!isWindow(node)
|
||||
) {
|
||||
// Don't re-trigger an onFOO event when we call its FOO() method
|
||||
const tmp = node[ontype]
|
@ -1,14 +1,13 @@
|
||||
/* 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'
|
||||
import { Core } from './core'
|
||||
import { Util } from './util'
|
||||
import { EventRaw } from './alias'
|
||||
import { EventObject } from './object'
|
||||
import { TypeEventHandler, TypeEventHandlers } from './types'
|
||||
import { Base } from '../common/base'
|
||||
|
||||
export class EventEmitter<TElement extends Node> extends Primer<TElement> {
|
||||
export class EventEmitter<TElement extends Element> extends Base<TElement> {
|
||||
on<TType extends string>(
|
||||
events: TType,
|
||||
selector: string,
|
||||
@ -257,7 +256,3 @@ export namespace EventEmitter {
|
||||
Core.off(elem as any, events as string, fn, selector)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace EventEmitter {
|
||||
export const registerHook = Hook.add
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Store } from './event-store'
|
||||
import { EventObject } from './event-object'
|
||||
import { EventHandler } from './event-types'
|
||||
import { Store } from './store'
|
||||
import { EventObject } from './object'
|
||||
import { EventHandler } from './types'
|
||||
|
||||
export namespace Hook {
|
||||
const cache: { [type: string]: Hook } = {}
|
||||
@ -9,7 +9,7 @@ export namespace Hook {
|
||||
return cache[type] || {}
|
||||
}
|
||||
|
||||
export function add(type: string, hook: Hook) {
|
||||
export function register(type: string, hook: Hook) {
|
||||
cache[type] = hook
|
||||
}
|
||||
}
|
3
packages/x6-vector/src/dom/events/index.ts
Normal file
3
packages/x6-vector/src/dom/events/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './hook'
|
||||
export * from './emitter'
|
||||
export * from './listener'
|
@ -1,60 +1,65 @@
|
||||
import { EventEmitter } from './event-emitter'
|
||||
import { TypeEventHandler } from './event-types'
|
||||
import { EventEmitter } from './emitter'
|
||||
import { TypeEventHandler } from './types'
|
||||
|
||||
export class Listener<TElement extends Node> extends EventEmitter<TElement> {}
|
||||
export class EventListener<
|
||||
TElement extends Element
|
||||
> extends EventEmitter<TElement> {}
|
||||
|
||||
export interface Listener<TElement extends Node>
|
||||
extends Listener.Methods<TElement> {}
|
||||
export interface EventListener<TElement extends Element>
|
||||
extends EventListener.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
|
||||
export namespace EventListener {
|
||||
// Generate interface
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false) {
|
||||
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!)
|
||||
events.forEach((event) => {
|
||||
EventListener.prototype[event] = function <TData>(
|
||||
this: EventListener<Element>,
|
||||
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
|
||||
}
|
||||
return this
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const methods = events.map(
|
||||
(event) =>
|
||||
`
|
||||
const methods = events.map(
|
||||
(event) =>
|
||||
`
|
||||
${event}(): this
|
||||
${event}(
|
||||
handler:
|
||||
@ -68,11 +73,8 @@ export namespace Listener {
|
||||
| false,
|
||||
): this
|
||||
`,
|
||||
)
|
||||
)
|
||||
|
||||
// generate interface
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(methods.join('\n'))
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Util } from './event-util'
|
||||
import { Store } from './event-store'
|
||||
import { EventRaw } from './event-alias'
|
||||
import { Util } from './util'
|
||||
import { Store } from './store'
|
||||
import { EventRaw } from './alias'
|
||||
|
||||
export class EventObject<
|
||||
TDelegateTarget = any,
|
@ -1,16 +1,16 @@
|
||||
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'
|
||||
import { isAncestorOf } from '../../util'
|
||||
import { Util } from './util'
|
||||
import { Hook } from './hook'
|
||||
import { Core } from './core'
|
||||
import { Store } from './store'
|
||||
|
||||
export namespace Special {
|
||||
// Prevent triggered image.load events from bubbling to window.load
|
||||
Hook.add('load', {
|
||||
Hook.register('load', {
|
||||
noBubble: true,
|
||||
})
|
||||
|
||||
Hook.add('beforeunload', {
|
||||
Hook.register('beforeunload', {
|
||||
postDispatch(elem, event) {
|
||||
// Support: Chrome <=73+
|
||||
// Chrome doesn't alert on `event.preventDefault()`
|
||||
@ -31,7 +31,7 @@ export namespace Special {
|
||||
}
|
||||
Object.keys(events).forEach((type: keyof typeof events) => {
|
||||
const delegateType = events[type]
|
||||
Hook.add(type, {
|
||||
Hook.register(type, {
|
||||
delegateType,
|
||||
bindType: delegateType,
|
||||
handle(target, event, ...args) {
|
||||
@ -44,7 +44,7 @@ export namespace Special {
|
||||
if (
|
||||
!related ||
|
||||
(related !== target &&
|
||||
!DomUtil.contains(target as Element, related as Element))
|
||||
!isAncestorOf(target as Element, related as Element))
|
||||
) {
|
||||
event.type = handleObj.originType
|
||||
ret = handleObj.handler.call(this, event, ...args)
|
||||
@ -182,7 +182,7 @@ export namespace Special {
|
||||
}
|
||||
|
||||
// Utilize native event to ensure correct state for checkable inputs
|
||||
Hook.add('click', {
|
||||
Hook.register('click', {
|
||||
setup(elem) {
|
||||
if (Util.isCheckableInput(elem)) {
|
||||
leverageNative(elem, 'click', Util.returnTrue)
|
||||
@ -207,7 +207,9 @@ export namespace Special {
|
||||
const target = event.target
|
||||
return (
|
||||
(Util.isCheckableInput(elem) && State.get(target, 'click')) ||
|
||||
DomUtil.isNodeName(target, 'a')
|
||||
(target &&
|
||||
(target as Node).nodeName &&
|
||||
(target as Node).nodeName.toLowerCase() === 'a')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -231,7 +233,7 @@ export namespace Special {
|
||||
const delegateType = events[type]
|
||||
|
||||
// Utilize native event if possible so blur/focus sequence is correct
|
||||
Hook.add(type, {
|
||||
Hook.register(type, {
|
||||
delegateType,
|
||||
setup(elem) {
|
||||
// Claim the first handler
|
@ -1,4 +1,4 @@
|
||||
import { EventHandler } from './event-types'
|
||||
import { EventHandler } from './types'
|
||||
|
||||
export namespace Store {
|
||||
export type EventTarget = Element | Record<string, unknown>
|
@ -1,4 +1,4 @@
|
||||
import { EventObject } from './event-object'
|
||||
import { EventObject } from './object'
|
||||
import {
|
||||
EventRaw,
|
||||
UIEventRaw,
|
||||
@ -7,7 +7,7 @@ import {
|
||||
MouseEventRaw,
|
||||
TouchEventRaw,
|
||||
KeyboardEventRaw,
|
||||
} from './event-alias'
|
||||
} from './alias'
|
||||
|
||||
interface EventBase<
|
||||
TDelegateTarget = any,
|
@ -1,6 +1,5 @@
|
||||
import { DomUtil } from '../../util/dom'
|
||||
import { Store } from './event-store'
|
||||
import type { EventObject } from './event-object'
|
||||
import { Store } from './store'
|
||||
import type { EventObject } from './object'
|
||||
|
||||
export namespace Util {
|
||||
export const returnTrue = () => true
|
||||
@ -8,6 +7,26 @@ export namespace Util {
|
||||
export function stopPropagationCallback(e: Event) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
export function addEventListener<TElement extends Element>(
|
||||
elem: TElement,
|
||||
type: string,
|
||||
handler: EventListener,
|
||||
) {
|
||||
if (elem.addEventListener != null) {
|
||||
elem.addEventListener(type, handler as any)
|
||||
}
|
||||
}
|
||||
|
||||
export function removeEventListener<TElement extends Element>(
|
||||
elem: TElement,
|
||||
type: string,
|
||||
handler: EventListener,
|
||||
) {
|
||||
if (elem.removeEventListener != null) {
|
||||
elem.removeEventListener(type, handler as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Util {
|
||||
@ -36,7 +55,7 @@ export namespace Util {
|
||||
const node = elem as HTMLInputElement
|
||||
return (
|
||||
node.click != null &&
|
||||
DomUtil.isNodeName(node, 'input') &&
|
||||
node.nodeName.toLowerCase() === 'input' &&
|
||||
rcheckableInput.test(node.type)
|
||||
)
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { Global } from '../../global'
|
||||
import { DomUtil } from '../../util/dom'
|
||||
import { Dom } from '../dom'
|
||||
import { Global } from '../global'
|
||||
import { namespaces, createNode } from '../util'
|
||||
import { Dom } from './dom'
|
||||
|
||||
@Fragment.register('Fragment')
|
||||
export class Fragment extends Dom<DocumentFragment> {
|
||||
constructor(node = Global.document.createDocumentFragment()) {
|
||||
export class Fragment extends Dom<HTMLDivElement> {
|
||||
constructor(node = Global.document.createDocumentFragment() as any) {
|
||||
super(node)
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ export class Fragment extends Dom<DocumentFragment> {
|
||||
|
||||
// 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))
|
||||
const wrapper = new Dom(
|
||||
createNode<SVGElement>('wrapper', ns || namespaces.html),
|
||||
)
|
||||
wrapper.add(this.node.cloneNode(true))
|
||||
|
||||
return wrapper.xml(false)
|
2
packages/x6-vector/src/dom/index.ts
Normal file
2
packages/x6-vector/src/dom/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dom'
|
||||
export * from './fragment'
|
@ -1,8 +1,7 @@
|
||||
import { DomUtil } from '../../util/dom'
|
||||
import { Adopter } from '../adopter'
|
||||
import { Primer } from './primer'
|
||||
import { Adopter } from '../common/adopter'
|
||||
import { Base } from '../common/base'
|
||||
|
||||
export class Affix<TNode extends Node> extends Primer<TNode> {
|
||||
export class Affix<TElement extends Element> extends Base<TElement> {
|
||||
protected affixes: Record<string, any>
|
||||
|
||||
affix<T extends Record<string, any>>(): T
|
||||
@ -35,35 +34,38 @@ export class Affix<TNode extends Node> extends Primer<TNode> {
|
||||
return this
|
||||
}
|
||||
|
||||
restoreAffix() {
|
||||
return this.affix(Affix.restore(DomUtil.toElement(this.node)))
|
||||
storeAffix(deep = false) {
|
||||
Affix.store(this.node, deep)
|
||||
return this
|
||||
}
|
||||
|
||||
storeAffix(deep = false) {
|
||||
Affix.store(DomUtil.toElement(this.node), deep)
|
||||
return this
|
||||
restoreAffix() {
|
||||
return this.affix(Affix.restore(this.node))
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Affix {
|
||||
const PERSIST_ATTR_NAME = 'vector:data'
|
||||
|
||||
export function store(node: Element, deep: boolean) {
|
||||
export function store<TElement extends Element>(
|
||||
node: TElement,
|
||||
deep: boolean,
|
||||
) {
|
||||
node.removeAttribute(PERSIST_ATTR_NAME)
|
||||
const elem = Adopter.adopt(node)
|
||||
const affixes = elem.affix()
|
||||
const instance = Adopter.adopt(node)
|
||||
const affixes = instance.affix()
|
||||
if (affixes && Object.keys(affixes).length) {
|
||||
node.setAttribute(PERSIST_ATTR_NAME, JSON.stringify(affixes))
|
||||
}
|
||||
|
||||
if (deep) {
|
||||
node.childNodes.forEach((child: Element) => {
|
||||
store(child, deep)
|
||||
})
|
||||
node.childNodes.forEach((child) => store(child as Element, deep))
|
||||
}
|
||||
}
|
||||
|
||||
export function restore(node: Element): Record<string, any> {
|
||||
export function restore<TElement extends Element>(
|
||||
node: TElement,
|
||||
): Record<string, any> {
|
||||
const raw = node.getAttribute(PERSIST_ATTR_NAME)
|
||||
if (raw) {
|
||||
return JSON.parse(raw)
|
193
packages/x6-vector/src/dom/primer/classname.test.ts
Normal file
193
packages/x6-vector/src/dom/primer/classname.test.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { Dom } from '../dom'
|
||||
import { ClassName } from './classname'
|
||||
|
||||
describe('Dom', () => {
|
||||
describe('classname', () => {
|
||||
let elem: Dom<HTMLDivElement>
|
||||
|
||||
beforeEach(() => {
|
||||
elem = new Dom<HTMLDivElement>('div').addClass('foo bar')
|
||||
})
|
||||
|
||||
describe('classes()', () => {
|
||||
it('should return sorted classnames', () => {
|
||||
expect(elem.classes()).toEqual(['bar', 'foo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasClass()', () => {
|
||||
it('should return `false` when given classname is null or empty', () => {
|
||||
expect(elem.hasClass('')).toBeFalse()
|
||||
expect(elem.hasClass(null as any)).toBeFalse()
|
||||
expect(elem.hasClass(undefined as any)).toBeFalse()
|
||||
expect(ClassName.has(null, null)).toBeFalse()
|
||||
expect(ClassName.has(null, undefined)).toBeFalse()
|
||||
})
|
||||
|
||||
it('should return `false` when the node is invalid', () => {
|
||||
const text = document.createTextNode('text')
|
||||
ClassName.add(text as any, 'test')
|
||||
expect(ClassName.has(text as any, 'test')).toBeFalse()
|
||||
})
|
||||
|
||||
it('should return `true` when contains the given classname', () => {
|
||||
expect(elem.hasClass('foo')).toBeTrue()
|
||||
expect(elem.hasClass('bar')).toBeTrue()
|
||||
})
|
||||
|
||||
it('should return `false` when do not contains the given classname', () => {
|
||||
expect(elem.hasClass('fo')).toBeFalse()
|
||||
expect(elem.hasClass('ba')).toBeFalse()
|
||||
})
|
||||
|
||||
it('should return `true` when contains the given classnames', () => {
|
||||
expect(elem.hasClass('foo bar')).toBeTrue()
|
||||
expect(elem.hasClass('bar foo')).toBeTrue()
|
||||
})
|
||||
|
||||
it('should return `false` when do not contains the given classnames', () => {
|
||||
expect(elem.hasClass('foo bar 0')).toBeFalse()
|
||||
expect(elem.hasClass('bar foo 1')).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addClass()', () => {
|
||||
it('should do nothing for invalid class', () => {
|
||||
elem.addClass(null as any)
|
||||
elem.addClass(undefined as any)
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
})
|
||||
|
||||
it('should add single class', () => {
|
||||
elem.addClass('test')
|
||||
expect(elem.hasClass('test')).toBeTrue()
|
||||
})
|
||||
|
||||
it('should add an array of classes', () => {
|
||||
elem.addClass(['test1', 'test2'])
|
||||
expect(elem.hasClass('foo')).toBeTrue()
|
||||
expect(elem.hasClass('bar')).toBeTrue()
|
||||
expect(elem.hasClass('test1')).toBeTrue()
|
||||
expect(elem.hasClass('test2')).toBeTrue()
|
||||
})
|
||||
|
||||
it('should add an multi classes with string', () => {
|
||||
elem.addClass('test1 test2')
|
||||
expect(elem.hasClass('bar')).toBeTrue()
|
||||
expect(elem.hasClass('foo')).toBeTrue()
|
||||
expect(elem.hasClass('test1')).toBeTrue()
|
||||
expect(elem.hasClass('test2')).toBeTrue()
|
||||
})
|
||||
|
||||
it('should not add the same class twice in same element', () => {
|
||||
elem.addClass('foo').addClass('foo')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
elem.addClass('foo foo')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
})
|
||||
|
||||
it('should not add empty string', () => {
|
||||
elem.addClass('test')
|
||||
elem.addClass(' ')
|
||||
expect(elem.attr('class')).toEqual('foo bar test')
|
||||
})
|
||||
|
||||
it('should call hook', () => {
|
||||
elem.removeClass().addClass('test')
|
||||
elem.addClass((cls) => `${cls} foo`)
|
||||
expect(elem.attr('class')).toEqual('test foo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeClass()', () => {
|
||||
it('should do nothing for invalid node', () => {
|
||||
ClassName.remove(null)
|
||||
})
|
||||
|
||||
it('should remove one', () => {
|
||||
elem.removeClass('foo test')
|
||||
expect(elem.attr('class')).toEqual('bar')
|
||||
})
|
||||
|
||||
it('should remove an array of classes', () => {
|
||||
elem.addClass('test').removeClass(['foo', 'test'])
|
||||
expect(elem.attr('class')).toEqual('bar')
|
||||
})
|
||||
|
||||
it('should remove all', () => {
|
||||
elem.removeClass()
|
||||
expect(elem.attr('class')).toEqual('')
|
||||
|
||||
elem.addClass('test foo')
|
||||
elem.removeClass(null as any)
|
||||
expect(elem.attr('class')).toEqual('')
|
||||
})
|
||||
|
||||
it('should call hook', () => {
|
||||
elem.removeClass((cls) => cls.split(' ')[1])
|
||||
expect(elem.attr('class')).toEqual('foo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggleClass()', () => {
|
||||
it('should do nothing for invalid class', () => {
|
||||
elem.toggleClass('test')
|
||||
elem.toggleClass(null as any)
|
||||
elem.toggleClass(undefined as any)
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
})
|
||||
|
||||
it('should toggle class', () => {
|
||||
elem.removeClass()
|
||||
|
||||
elem.toggleClass('foo bar')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass('foo')
|
||||
expect(elem.attr('class')).toEqual('bar')
|
||||
|
||||
elem.toggleClass('foo')
|
||||
expect(elem.attr('class')).toEqual('bar foo')
|
||||
})
|
||||
|
||||
it('should not toggle empty strings', () => {
|
||||
elem.removeClass()
|
||||
|
||||
elem.toggleClass('foo bar')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass(' ')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass(' ')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
})
|
||||
|
||||
it('should work with the specified next state', () => {
|
||||
elem.removeClass()
|
||||
|
||||
elem.toggleClass('foo bar')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass('foo', true)
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass('foo', true)
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass('foo', false)
|
||||
expect(elem.attr('class')).toEqual('bar')
|
||||
})
|
||||
|
||||
it('should call hook', () => {
|
||||
elem.removeClass()
|
||||
|
||||
elem.toggleClass(() => 'foo bar')
|
||||
expect(elem.attr('class')).toEqual('foo bar')
|
||||
|
||||
elem.toggleClass(() => 'foo', false)
|
||||
expect(elem.attr('class')).toEqual('bar')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -1,31 +1,46 @@
|
||||
import { DomUtil } from '../../util/dom'
|
||||
import { Primer } from './primer'
|
||||
import { Base } from '../common/base'
|
||||
|
||||
export class ClassName<TNode extends Node> extends Primer<TNode> {
|
||||
export class ClassName<TElement extends Element> extends Base<TElement> {
|
||||
classes() {
|
||||
const raw = this.attr<string>('class')
|
||||
return raw == null ? [] : raw.trim().split(/\s+/)
|
||||
const raw = ClassName.get(this.node)
|
||||
return ClassName.split(raw)
|
||||
}
|
||||
|
||||
hasClass(name: string) {
|
||||
return ClassName.has(this.node, name)
|
||||
if (name == null || name.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ClassName.split(name).every((name) => ClassName.has(this.node, name))
|
||||
}
|
||||
|
||||
addClass(name: string): this
|
||||
addClass(names: string[]): this
|
||||
addClass(name: string | string[]) {
|
||||
addClass(hook: (old: string) => string): this
|
||||
addClass(name: string | string[] | ((old: string) => string)) {
|
||||
ClassName.add(this.node, Array.isArray(name) ? name.join(' ') : name)
|
||||
return this
|
||||
}
|
||||
|
||||
removeClass(): this
|
||||
removeClass(name: string): this
|
||||
removeClass(names: string[]): this
|
||||
removeClass(name: string | string[]) {
|
||||
removeClass(hook: (old: string) => string): this
|
||||
removeClass(name?: string | string[] | ((old: string) => string)) {
|
||||
ClassName.remove(this.node, Array.isArray(name) ? name.join(' ') : name)
|
||||
return this
|
||||
}
|
||||
|
||||
toggleClass(name: string, stateValue?: boolean) {
|
||||
toggleClass(name: string): this
|
||||
toggleClass(name: string, stateValue: boolean): this
|
||||
toggleClass(
|
||||
hook: (old: string, status?: boolean) => string,
|
||||
stateValue?: boolean,
|
||||
): this
|
||||
toggleClass(
|
||||
name: string | ((old: string, status?: boolean) => string),
|
||||
stateValue?: boolean,
|
||||
) {
|
||||
ClassName.toggle(this.node, name, stateValue)
|
||||
return this
|
||||
}
|
||||
@ -37,15 +52,26 @@ export namespace ClassName {
|
||||
|
||||
const fillSpaces = (string: string) => ` ${string} `
|
||||
|
||||
const get = (elem: Element) =>
|
||||
(elem && elem.getAttribute && elem.getAttribute('class')) || ''
|
||||
export function split(name: string) {
|
||||
return name
|
||||
.split(/\s+/)
|
||||
.map((name) => name.trim())
|
||||
.filter((name) => name.length > 0)
|
||||
.sort()
|
||||
}
|
||||
|
||||
export function has(elem: Node | null, selector: string | null) {
|
||||
if (elem == null || selector == null) {
|
||||
export function get<TElement extends Element>(node: TElement) {
|
||||
return (node && node.getAttribute && node.getAttribute('class')) || ''
|
||||
}
|
||||
|
||||
export function has<TElement extends Element>(
|
||||
node: TElement | null | undefined,
|
||||
selector: string | null | undefined,
|
||||
) {
|
||||
if (node == null || selector == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const node = DomUtil.toElement(elem)
|
||||
const classNames = fillSpaces(get(node))
|
||||
const className = fillSpaces(selector)
|
||||
|
||||
@ -54,15 +80,14 @@ export namespace ClassName {
|
||||
: false
|
||||
}
|
||||
|
||||
export function add(
|
||||
elem: Node | null,
|
||||
selector: ((cls: string) => string) | string | null,
|
||||
export function add<TElement extends Element>(
|
||||
node: TElement | null | undefined,
|
||||
selector: ((cls: string) => string) | string | null | undefined,
|
||||
): void {
|
||||
if (elem == null || selector == null) {
|
||||
if (node == null || selector == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const node = DomUtil.toElement(elem)
|
||||
if (typeof selector === 'function') {
|
||||
add(node, selector(get(node)))
|
||||
return
|
||||
@ -86,21 +111,20 @@ export namespace ClassName {
|
||||
}
|
||||
}
|
||||
|
||||
export function remove(
|
||||
elem: Node | null,
|
||||
selector?: ((cls: string) => string) | string | null,
|
||||
export function remove<TElement extends Element>(
|
||||
node: TElement | null | undefined,
|
||||
selector?: ((cls: string) => string) | string | null | undefined,
|
||||
): void {
|
||||
if (elem == null) {
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const node = DomUtil.toElement(elem)
|
||||
if (typeof selector === 'function') {
|
||||
remove(elem, selector(get(node)))
|
||||
remove(node, selector(get(node)))
|
||||
return
|
||||
}
|
||||
|
||||
if ((!selector || typeof selector === 'string') && elem.nodeType === 1) {
|
||||
if ((!selector || typeof selector === 'string') && node.nodeType === 1) {
|
||||
const classes = (selector || '').match(rnotwhite) || []
|
||||
const oldValue = fillSpaces(get(node)).replace(rclass, ' ')
|
||||
let newValue = classes.reduce((memo, cls) => {
|
||||
@ -120,36 +144,40 @@ export namespace ClassName {
|
||||
}
|
||||
}
|
||||
|
||||
export function toggle(
|
||||
elem: Node | null,
|
||||
selector: ((cls: string, status?: boolean) => string) | string | null,
|
||||
export function toggle<TElement extends Element>(
|
||||
node: TElement | null | undefined,
|
||||
selector:
|
||||
| ((cls: string, status?: boolean) => string)
|
||||
| string
|
||||
| null
|
||||
| undefined,
|
||||
state?: boolean,
|
||||
): void {
|
||||
if (elem == null || selector == null) {
|
||||
if (node == null || selector == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state != null && typeof selector === 'string') {
|
||||
if (state) {
|
||||
add(elem, selector)
|
||||
add(node, selector)
|
||||
} else {
|
||||
remove(elem, selector)
|
||||
remove(node, selector)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof selector === 'function') {
|
||||
toggle(elem, selector(get(DomUtil.toElement(elem)), state), state)
|
||||
toggle(node, selector(get(node), state), state)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof selector === 'string') {
|
||||
const metches = selector.match(rnotwhite) || []
|
||||
metches.forEach((cls) => {
|
||||
if (has(elem, cls)) {
|
||||
remove(elem, cls)
|
||||
if (has(node, cls)) {
|
||||
remove(node, cls)
|
||||
} else {
|
||||
add(elem, cls)
|
||||
add(node, cls)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import { DomUtil } from '../../util/dom'
|
||||
import { Str } from '../../util/str'
|
||||
import { Primer } from './primer'
|
||||
import { camelCase } from '../../util'
|
||||
import { Base } from '../common/base'
|
||||
|
||||
export class Data<TNode extends Node> extends Primer<TNode> {
|
||||
export class Data<TElement extends Element> extends Base<TElement> {
|
||||
data(): Record<string, any>
|
||||
data<T>(key: string): T
|
||||
data(keys: string[]): Record<string, any>
|
||||
@ -14,8 +13,7 @@ export class Data<TNode extends Node> extends Primer<TNode> {
|
||||
) {
|
||||
// Get all data
|
||||
if (key == null) {
|
||||
const elem = DomUtil.toElement(this.node)
|
||||
const attrs = elem.attributes
|
||||
const attrs = this.node.attributes
|
||||
const keys: string[] = []
|
||||
for (let i = 0, l = attrs.length; i < l; i += 1) {
|
||||
const item = attrs.item(i)
|
||||
@ -31,7 +29,7 @@ export class Data<TNode extends Node> extends Primer<TNode> {
|
||||
const data: Record<string, any> = {}
|
||||
key.forEach((k) => {
|
||||
// Return the camelCased key
|
||||
data[Str.camelCase(k)] = this.data(k)
|
||||
data[camelCase(k)] = this.data(k)
|
||||
})
|
||||
return data
|
||||
}
|
||||
@ -43,11 +41,11 @@ export class Data<TNode extends Node> extends Primer<TNode> {
|
||||
}
|
||||
|
||||
const dataKey = Data.parseKey(key)
|
||||
const node = this.node
|
||||
|
||||
// Get by key
|
||||
if (typeof val === 'undefined') {
|
||||
const value = this.attr(dataKey)
|
||||
return Data.parseValue(value)
|
||||
return Data.parseValue(node.getAttribute(dataKey))
|
||||
}
|
||||
|
||||
// Set with key-value
|
||||
@ -58,8 +56,11 @@ export class Data<TNode extends Node> extends Primer<TNode> {
|
||||
: raw === true || typeof val === 'string' || typeof val === 'number'
|
||||
? val
|
||||
: JSON.stringify(val)
|
||||
return this.attr(dataKey, dataValue)
|
||||
|
||||
node.setAttribute(dataKey, dataValue)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
1
packages/x6-vector/src/dom/primer/index.ts
Normal file
1
packages/x6-vector/src/dom/primer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './primer'
|
@ -1,6 +1,6 @@
|
||||
import { Primer } from './primer'
|
||||
import { Base } from '../common/base'
|
||||
|
||||
export class Memory<TNode extends Node> extends Primer<TNode> {
|
||||
export class Memory<TElement extends Element> extends Base<TElement> {
|
||||
private memo: Record<string, any>
|
||||
|
||||
remember(obj: Record<string, any>): this
|
94
packages/x6-vector/src/dom/primer/primer.ts
Normal file
94
packages/x6-vector/src/dom/primer/primer.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { isNode, createHTMLNode } from '../../util'
|
||||
import { Base } from '../common/base'
|
||||
import { Registry } from '../common/registry'
|
||||
import { Affix } from './affix'
|
||||
import { Data } from './data'
|
||||
import { Memory } from './memory'
|
||||
import { ClassName } from './classname'
|
||||
import { Style, Hook as StyleHook } from '../style'
|
||||
import {
|
||||
Attributes,
|
||||
AttributesMap,
|
||||
Hook as AttributesHook,
|
||||
} from '../attributes'
|
||||
import { EventEmitter, EventListener, Hook as EventHook } from '../events'
|
||||
|
||||
@Primer.mixin(
|
||||
Affix,
|
||||
Data,
|
||||
Memory,
|
||||
Attributes,
|
||||
ClassName,
|
||||
Style,
|
||||
EventEmitter,
|
||||
EventListener,
|
||||
)
|
||||
export class Primer<TElement extends Element> extends Base<TElement> {
|
||||
public readonly node: TElement
|
||||
|
||||
public get type() {
|
||||
return this.node.nodeName
|
||||
}
|
||||
|
||||
constructor()
|
||||
constructor(attrs?: AttributesMap<TElement> | null)
|
||||
constructor(node: TElement | null, attrs?: AttributesMap<TElement> | null)
|
||||
constructor(tagName: string | null, attrs?: AttributesMap<TElement> | null)
|
||||
constructor(
|
||||
nodeOrAttrs?: TElement | string | AttributesMap<TElement> | null,
|
||||
attrs?: AttributesMap<TElement> | null,
|
||||
)
|
||||
constructor(
|
||||
nodeOrAttrs?: TElement | string | AttributesMap<TElement> | null,
|
||||
attrs?: AttributesMap<TElement> | null,
|
||||
) {
|
||||
super()
|
||||
|
||||
let attributes: AttributesMap<TElement> | null | undefined
|
||||
if (isNode(nodeOrAttrs)) {
|
||||
this.node = nodeOrAttrs
|
||||
attributes = attrs
|
||||
} else {
|
||||
const ctor = this.constructor as Registry.Definition
|
||||
const tagName =
|
||||
typeof nodeOrAttrs === 'string'
|
||||
? nodeOrAttrs
|
||||
: Registry.getTagName(ctor)
|
||||
if (tagName) {
|
||||
this.node = this.createNode(tagName)
|
||||
attributes =
|
||||
nodeOrAttrs != null && typeof nodeOrAttrs !== 'string'
|
||||
? nodeOrAttrs
|
||||
: attrs
|
||||
} else {
|
||||
throw new Error(
|
||||
`Can not initialize "${ctor.name}" with unknown tag name`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes) {
|
||||
this.attr(attributes)
|
||||
}
|
||||
}
|
||||
|
||||
protected createNode(tagName: string): TElement {
|
||||
return createHTMLNode(tagName) as any
|
||||
}
|
||||
}
|
||||
|
||||
export interface Primer<TElement extends Element>
|
||||
extends Affix<TElement>,
|
||||
Data<TElement>,
|
||||
Memory<TElement>,
|
||||
Style<TElement>,
|
||||
Attributes<TElement>,
|
||||
ClassName<TElement>,
|
||||
EventEmitter<TElement>,
|
||||
EventListener<TElement> {}
|
||||
|
||||
export namespace Primer {
|
||||
export const registerEventHook = EventHook.register
|
||||
export const registerStyleHook = StyleHook.register
|
||||
export const registerAttributeHook = AttributesHook.register
|
||||
}
|
1
packages/x6-vector/src/dom/style/.gitignore
vendored
Normal file
1
packages/x6-vector/src/dom/style/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
csstype.ts
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user