mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 06:31:20 -08:00
refactor(core): CORE-13 type component registries and own control lifetimes
This commit is contained in:
1 parent
9eb7c945ca
commit
697817eceb
68 files changed
+10100
-1925
No files matched your search
@@ -1,120 +0,0 @@
|
||||
import validator from 'option-validator'
|
||||
import { ComponentOption } from '../scheme'
|
||||
import { addClass, append, createElement, hasClass, remove, removeClass, setStyles, tooltip } from './dom'
|
||||
import { errorHandle } from './error'
|
||||
|
||||
export default class Component {
|
||||
constructor(art) {
|
||||
this.id = 0
|
||||
this.art = art
|
||||
this.cache = new Map()
|
||||
this.add = this.add.bind(this)
|
||||
this.remove = this.remove.bind(this)
|
||||
this.update = this.update.bind(this)
|
||||
}
|
||||
|
||||
get show() {
|
||||
return hasClass(this.art.template.$player, `art-${this.name}-show`)
|
||||
}
|
||||
|
||||
set show(value) {
|
||||
const { $player } = this.art.template
|
||||
const className = `art-${this.name}-show`
|
||||
if (value) {
|
||||
addClass($player, className)
|
||||
}
|
||||
else {
|
||||
removeClass($player, className)
|
||||
}
|
||||
this.art.emit(this.name, value)
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.show = !this.show
|
||||
}
|
||||
|
||||
add(getOption) {
|
||||
const option = typeof getOption === 'function' ? getOption(this.art) : getOption
|
||||
option.html = option.html || ''
|
||||
validator(option, ComponentOption)
|
||||
if (!this.$parent || !this.name || option.disable)
|
||||
return
|
||||
const name = option.name || `${this.name}${this.id}`
|
||||
errorHandle(!this.cache.has(name), `Can't add an existing [${name}] to the [${this.name}]`)
|
||||
|
||||
this.id += 1
|
||||
const $ref = createElement('div')
|
||||
addClass($ref, `art-${this.name}`)
|
||||
addClass($ref, `art-${this.name}-${name}`)
|
||||
|
||||
const childs = Array.from(this.$parent.children)
|
||||
$ref.dataset.index = option.index || this.id
|
||||
const nextChild = childs.find(item => Number(item.dataset.index) >= Number($ref.dataset.index))
|
||||
if (nextChild) {
|
||||
nextChild.insertAdjacentElement('beforebegin', $ref)
|
||||
}
|
||||
else {
|
||||
append(this.$parent, $ref)
|
||||
}
|
||||
|
||||
if (option.html) {
|
||||
append($ref, option.html)
|
||||
}
|
||||
|
||||
if (option.style) {
|
||||
setStyles($ref, option.style)
|
||||
}
|
||||
|
||||
if (option.tooltip) {
|
||||
tooltip($ref, option.tooltip)
|
||||
}
|
||||
|
||||
const events = []
|
||||
if (option.click) {
|
||||
const destroyEvent = this.art.events.proxy($ref, 'click', (event) => {
|
||||
event.preventDefault()
|
||||
option.click.call(this.art, this, event)
|
||||
})
|
||||
events.push(destroyEvent)
|
||||
}
|
||||
|
||||
if (option.selector && ['left', 'right'].includes(option.position)) {
|
||||
this.selector(option, $ref, events)
|
||||
}
|
||||
|
||||
this[name] = $ref
|
||||
this.cache.set(name, { $ref, events, option })
|
||||
|
||||
if (option.mounted) {
|
||||
option.mounted.call(this.art, $ref)
|
||||
}
|
||||
|
||||
return $ref
|
||||
}
|
||||
|
||||
remove(name) {
|
||||
errorHandle(this.cache.has(name), `Can't find [${name}] from the [${this.name}]`)
|
||||
const item = this.cache.get(name)
|
||||
|
||||
if (item.option.beforeUnmount) {
|
||||
item.option.beforeUnmount.call(this.art, item.$ref)
|
||||
}
|
||||
|
||||
for (const event of item.events) {
|
||||
this.art.events.remove(event)
|
||||
}
|
||||
|
||||
this.cache.delete(name)
|
||||
delete this[name]
|
||||
remove(item.$ref)
|
||||
}
|
||||
|
||||
update(option) {
|
||||
if (this.cache.has(option.name)) {
|
||||
const item = this.cache.get(option.name)
|
||||
option = Object.assign(item.option, option)
|
||||
this.remove(option.name)
|
||||
}
|
||||
return this.add(option)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { ComponentHost, Entry, EntryInput, EntryOption, EventCleanup } from '../component/types'
|
||||
import validator from 'option-validator'
|
||||
import { renderEntry } from '../component/dom'
|
||||
import { ownEntry, releaseEntry } from '../component/resources'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { ComponentOption } from '../scheme'
|
||||
import { addClass, hasClass, removeClass } from './dom'
|
||||
import { errorHandle } from './error'
|
||||
|
||||
const removing = new WeakSet<object>()
|
||||
|
||||
export default class Component<Host extends ComponentHost = ComponentHost> {
|
||||
declare id: number
|
||||
declare art: Host
|
||||
declare cache: Map<string, Entry<Host>>
|
||||
declare name?: string
|
||||
declare $parent?: HTMLElement
|
||||
selector?(option: EntryOption<Host>, element: HTMLDivElement, events: EventCleanup[]): void
|
||||
|
||||
constructor(art: Host) {
|
||||
this.id = 0
|
||||
this.art = art
|
||||
this.cache = new Map()
|
||||
this.add = this.add.bind(this)
|
||||
this.remove = this.remove.bind(this)
|
||||
this.update = this.update.bind(this)
|
||||
}
|
||||
|
||||
get show(): boolean {
|
||||
return hasClass(this.art.template.$player, `art-${this.name}-show`)
|
||||
}
|
||||
|
||||
set show(value: boolean) {
|
||||
const { $player } = this.art.template
|
||||
const className = `art-${this.name}-show`
|
||||
if (value)
|
||||
addClass($player, className)
|
||||
else
|
||||
removeClass($player, className)
|
||||
this.art.emit(this.name!, value)
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.show = !this.show
|
||||
}
|
||||
|
||||
add(getOption: EntryInput<Host>): HTMLDivElement | undefined {
|
||||
if (isClosing(this.art))
|
||||
return
|
||||
const option = typeof getOption === 'function' ? getOption(this.art) : getOption
|
||||
if (isClosing(this.art))
|
||||
return
|
||||
option.html = option.html || ''
|
||||
validator(option, ComponentOption)
|
||||
if (!this.$parent || !this.name || option.disable)
|
||||
return
|
||||
const name = option.name || `${this.name}${this.id}`
|
||||
const cache = this.cache
|
||||
errorHandle(!cache.has(name), `Can't add an existing [${name}] to the [${this.name}]`)
|
||||
this.id += 1
|
||||
const $ref = document.createElement('div')
|
||||
const scope = ownEntry(this.art, $ref)
|
||||
const events: EventCleanup[] = []
|
||||
const previous = Object.getOwnPropertyDescriptor(this, name)
|
||||
let aliased = false
|
||||
scope.add(() => {
|
||||
for (const event of events)
|
||||
this.art.events.remove(event)
|
||||
})
|
||||
try {
|
||||
if (scope.closed)
|
||||
return
|
||||
renderEntry($ref, this.$parent, this.name, name, this.id, option)
|
||||
if (scope.closed)
|
||||
return
|
||||
if (option.click) {
|
||||
const cleanup = this.art.events.proxy($ref, 'click', (event) => {
|
||||
if (scope.closed)
|
||||
return
|
||||
event.preventDefault()
|
||||
option.click!.call(this.art, this, event)
|
||||
})
|
||||
events.push(cleanup)
|
||||
}
|
||||
if (option.selector && ['left', 'right'].includes(option.position!))
|
||||
this.selector!(option, $ref, events)
|
||||
if (scope.closed)
|
||||
return
|
||||
assignAlias(this, name, $ref)
|
||||
aliased = true
|
||||
cache.set(name, { $ref, events, option })
|
||||
if (option.mounted)
|
||||
option.mounted.call(this.art, $ref)
|
||||
return $ref
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
releaseEntry($ref)
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('ArtPlayer component cleanup failed:', cleanupError)
|
||||
}
|
||||
if (cache.get(name)?.$ref === $ref)
|
||||
cache.delete(name)
|
||||
if (aliased && (this as unknown as Record<string, unknown>)[name] === $ref) {
|
||||
if (previous)
|
||||
Object.defineProperty(this, name, previous)
|
||||
else
|
||||
delete (this as unknown as Record<string, unknown>)[name]
|
||||
}
|
||||
$ref.remove()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
errorHandle(this.cache.has(name), `Can't find [${name}] from the [${this.name}]`)
|
||||
const item = this.cache.get(name)!
|
||||
if (removing.has(item))
|
||||
return
|
||||
removing.add(item)
|
||||
try {
|
||||
if (item.option.beforeUnmount)
|
||||
item.option.beforeUnmount.call(this.art, item.$ref)
|
||||
try {
|
||||
releaseEntry(item.$ref)
|
||||
}
|
||||
finally {
|
||||
if (this.cache.get(name) === item) {
|
||||
this.cache.delete(name)
|
||||
delete (this as unknown as Record<string, unknown>)[name]
|
||||
}
|
||||
item.$ref.remove()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removing.delete(item)
|
||||
}
|
||||
}
|
||||
|
||||
update(option: EntryOption<Host>): ReturnType<this['add']> {
|
||||
if (this.cache.has(option.name!)) {
|
||||
const item = this.cache.get(option.name!)!
|
||||
option = Object.assign(item.option, option)
|
||||
this.remove(option.name!)
|
||||
}
|
||||
// add is polymorphic: Control intentionally returns undefined.
|
||||
return this.add(option) as ReturnType<this['add']>
|
||||
}
|
||||
}
|
||||
|
||||
function assignAlias(owner: object, name: string, value: HTMLElement): void {
|
||||
const aliases = owner as Record<string, unknown>
|
||||
if (name === '__proto__')
|
||||
Object.defineProperty(aliases, name, { value, enumerable: true, configurable: true, writable: true })
|
||||
else
|
||||
aliases[name] = value
|
||||
}
|
||||
Reference in new issue
Block a user