refactor: ♻️ refactor vector

This commit is contained in:
bubkoo
2021-04-11 13:11:21 +08:00
parent 7dd5dc056d
commit c84f0213ad
319 changed files with 8096 additions and 4112 deletions

View File

@ -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
}

View File

@ -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)
// }
}

View File

@ -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
// }
// }

View File

@ -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> {}

View File

@ -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> {}

View File

@ -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
> {}

View File

@ -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)

View File

@ -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>

View File

@ -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)
}
}

View File

@ -1 +1,2 @@
import './extension'
export * from './animator/animator'
export * from './scheduler/timeline'

View File

@ -11,4 +11,8 @@ export class MorphableBox
this.height = arr[0]
return this
}
toValue() {
return this.toArray()
}
}

View File

@ -8,4 +8,8 @@ export class MorphableColor
this.set(...arr)
return this
}
toValue() {
return this.toArray()
}
}

View File

@ -16,7 +16,7 @@ export class MorphableFallback<T = any> implements Morphable<T[], T> {
return [this.value]
}
valueOf(): T {
toValue(): T {
return this.value
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -9,4 +9,8 @@ export class MorphableNumberArray
this.push(...arr)
return this
}
toValue() {
return this.toArray()
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -9,4 +9,8 @@ export class MorphablePointArray
this.push(...this.parse(arr))
return this
}
toValue() {
return this.toArray()
}
}

View File

@ -39,7 +39,7 @@ export class MorphableTransform
]
}
valueOf(): MorphableTransform.Array {
toValue(): MorphableTransform.Array {
return this.toArray()
}
}

View File

@ -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()
}
}

View File

@ -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,]+/

View File

@ -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)

View File

@ -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>
}

View File

@ -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 {

View 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)
}
}
}

View 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> {}

View File

@ -0,0 +1 @@
import './mixins'

View 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)

View File

@ -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'
}

View File

@ -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

View File

@ -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)
}
}
}
}

View 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
}
}

View File

@ -0,0 +1,4 @@
export class MockedAnimator {
constructor(public id = -1, public done = true) {}
scheduler() {}
}

View 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)
}

View 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>

View File

@ -1,4 +1,4 @@
import { A } from '../../../element/container/a'
import { A } from '../../vector/a/a'
import { SVGContainerGeometryAnimator } from './container-geometry'
@SVGAAnimator.register('A')

View File

@ -1,4 +1,4 @@
import { Circle } from '../../../element/shape/circle'
import { Circle } from '../../vector/circle/circle'
import { SVGAnimator } from '../svg'
@SVGCircleAnimator.register('Circle')

View File

@ -1,4 +1,4 @@
import { ClipPath } from '../../../element/container/clippath'
import { ClipPath } from '../../vector/clippath/clippath'
import { SVGContainerAnimator } from './container'
@SVGClipPathAnimator.register('ClipPath')

View File

@ -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> {}

View File

@ -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

View 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> {}

View File

@ -1,4 +1,4 @@
import { Defs } from '../../../element/container/defs'
import { Defs } from '../../vector/defs/defs'
import { SVGAnimator } from '../svg'
@SVGDefsAnimator.register('Defs')

View File

@ -1,4 +1,4 @@
import { Ellipse } from '../../../element/shape/ellipse'
import { Ellipse } from '../../vector/ellipse/ellipse'
import { SVGAnimator } from '../svg'
@SVGEllipseAnimator.register('Ellipse')

View File

@ -1,4 +1,4 @@
import { ForeignObject } from '../../../element/shape/foreignobject'
import { ForeignObject } from '../../vector/foreignobject/foreignobject'
import { SVGAnimator } from '../svg'
@SVGForeignObjectAnimator.register('ForeignObject')

View File

@ -1,4 +1,4 @@
import { G } from '../../../element/container/g'
import { G } from '../../vector/g/g'
import { SVGContainerGeometryAnimator } from './container-geometry'
@SVGGAnimator.register('G')

View 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> {}

View File

@ -1,4 +1,4 @@
import { Image } from '../../../element/shape/image'
import { Image } from '../../vector/image/image'
import { SVGAnimator } from '../svg'
@SVGImageAnimator.register('Image')

View File

@ -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')

View File

@ -0,0 +1,8 @@
import { LinearGradient } from '../../vector/gradient/linear'
import { SVGGradientAnimator } from './gradient'
@SVGLinearGradientAnimator.register('LinearGradient')
export class SVGLinearGradientAnimator extends SVGGradientAnimator<
SVGLinearGradientElement,
LinearGradient
> {}

View File

@ -1,4 +1,4 @@
import { Marker } from '../../../element/container/marker'
import { Marker } from '../../vector/marker/marker'
import { SVGViewboxAnimator } from './container-viewbox'
@SVGMarkerAnimator.register('Marker')

View File

@ -1,4 +1,4 @@
import { Mask } from '../../../element/container/mask'
import { Mask } from '../../vector/mask/mask'
import { SVGContainerAnimator } from './container'
@SVGMaskAnimator.register('Mask')

View File

@ -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')

View File

@ -1,4 +1,4 @@
import { Pattern } from '../../../element/container/pattern'
import { Pattern } from '../../vector/pattern/pattern'
import { SVGViewboxAnimator } from './container-viewbox'
@SVGPatternAnimator.register('Pattern')

View File

@ -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

View File

@ -1,4 +1,4 @@
import { Polygon } from '../../../element/shape/polygon'
import { Polygon } from '../../vector/polygon/polygon'
import { SVGPolyAnimator } from './poly'
@SVGPolygonAnimator.register('Polygon')

View File

@ -1,4 +1,4 @@
import { Polyline } from '../../../element/shape/polyline'
import { Polyline } from '../../vector/polyline/polyline'
import { SVGPolyAnimator } from './poly'
@SVGPolylineAnimator.register('Polyline')

View File

@ -0,0 +1,8 @@
import { RadialGradient } from '../../vector/gradient/radial'
import { SVGGradientAnimator } from './gradient'
@SVGRadialGradientAnimator.register('RadialGradient')
export class SVGRadialGradientAnimator extends SVGGradientAnimator<
SVGRadialGradientElement,
RadialGradient
> {}

View File

@ -1,4 +1,4 @@
import { Rect } from '../../../element/shape/rect'
import { Rect } from '../../vector/rect/rect'
import { SVGAnimator } from '../svg'
@SVGRectAnimator.register('Rect')

View File

@ -1,4 +1,4 @@
import { Style } from '../../../element/shape/style'
import { Style } from '../../vector/style/style'
import { SVGAnimator } from '../svg'
@SVGStyleAnimator.register('Style')

View File

@ -1,4 +1,4 @@
import { Svg } from '../../../element/container/svg'
import { Svg } from '../../vector/svg/svg'
import { SVGViewboxAnimator } from './container-viewbox'
@SVGSVGAnimator.register('Svg')

View File

@ -1,4 +1,4 @@
import { Symbol } from '../../../element/container/symbol'
import { Symbol } from '../../vector/symbol/symbol'
import { SVGViewboxAnimator } from './container-viewbox'
@SVGSymbolAnimator.register('Symbol')

View File

@ -1,4 +1,4 @@
import { Text } from '../../../element/shape/text'
import { Text } from '../../vector/text/text'
import { SVGAnimator } from '../svg'
@SVGTextAnimator.register('Text')

View File

@ -1,4 +1,4 @@
import { TSpan } from '../../../element/shape/tspan'
import { TSpan } from '../../vector/tspan/tspan'
import { SVGAnimator } from '../svg'
@SVGTSpanAnimator.register('Tspan')

View File

@ -1,4 +1,4 @@
import { Use } from '../../../element/shape/use'
import { Use } from '../../vector/use/use'
import { SVGAnimator } from '../svg'
@SVGUseAnimator.register('Use')

View 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
}
}

View 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
}
}

View 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)
}
}
}
}
}

View 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)
},
})
}

View File

@ -0,0 +1,4 @@
export * from './hook'
export * from './base'
export * from './attributes'
export { AttributesMap } from './types'

View 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
)
})
}

View 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>

View 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)}`,
)
}
}
}
}

View File

@ -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,

View 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)
}
}
}

View 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()
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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]

View File

@ -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
}

View File

@ -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
}
}

View File

@ -0,0 +1,3 @@
export * from './hook'
export * from './emitter'
export * from './listener'

View File

@ -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'))
}

View File

@ -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,

View File

@ -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

View File

@ -1,4 +1,4 @@
import { EventHandler } from './event-types'
import { EventHandler } from './types'
export namespace Store {
export type EventTarget = Element | Record<string, unknown>

View File

@ -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,

View File

@ -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)
)
}

View File

@ -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)

View File

@ -0,0 +1,2 @@
export * from './dom'
export * from './fragment'

View File

@ -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)

View 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')
})
})
})
})

View File

@ -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)
}
})
}

View File

@ -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
}
}

View File

@ -0,0 +1 @@
export * from './primer'

View File

@ -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

View 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
}

View File

@ -0,0 +1 @@
csstype.ts

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