feat: enhance Artplayer type definitions and improve component interfaces

- Updated Artplayer class to specify environment as 'development' | 'production'.
- Refined template query method to return specific element types.
- Improved event proxy method to handle multiple event names.
- Enhanced storage get method to support both key retrieval and full record retrieval.
- Modified i18n interface to include language initialization and partial language records.
- Adjusted notice interface to provide a getter for show state and a setter for messages.
- Updated layers, controls, and contextmenu to allow undefined elements.
- Enhanced subtitle interface with initialization method.
- Refined hotkey methods to use KeyboardEvent instead of Event.
- Updated setting methods to return updated settings instead of arrays.
- Improved plugin method to return a promise.
- Added new events for video handling and refined existing event parameters.
- Enhanced option interface for proxy and plugins to support promises.
- Updated player interface to include reset method.
- Refined setting props to allow optional values.
- Enhanced template interface with additional control elements.
- Improved utils methods for querying and manipulating DOM elements.

fix: update type definitions for notice and subtitle properties, and refine event types

fix: remove unnecessary console warnings in event listener error handling

fix: refactor event listener management to use Set for better performance and error handling
This commit is contained in:
Harvey Zhao committed 2025-11-29 21:41:52 +08:00
1 parent f5f3800449
commit 4681a8bc7a
16 files changed
+152 -94

No files matched your search

+20 -8
View File
@@ -9,7 +9,7 @@ import viewInit from './viewInit'
export default class Events {
constructor(art) {
this.destroyEvents = []
this.destroyEvents = new Set()
this.proxy = this.proxy.bind(this)
this.hover = this.hover.bind(this)
@@ -30,7 +30,7 @@ export default class Events {
target.addEventListener(name, callback, option)
const destroy = () => target.removeEventListener(name, callback, option)
this.destroyEvents.push(destroy)
this.destroyEvents.add(destroy)
return destroy
}
@@ -44,16 +44,28 @@ export default class Events {
}
remove(destroyEvent) {
const index = this.destroyEvents.indexOf(destroyEvent)
if (index > -1) {
destroyEvent()
this.destroyEvents.splice(index, 1)
if (this.destroyEvents.has(destroyEvent)) {
try {
destroyEvent()
}
catch (error) {
console.warn('Failed to remove event listener:', error)
}
finally {
this.destroyEvents.delete(destroyEvent)
}
}
}
destroy() {
for (let index = 0; index < this.destroyEvents.length; index++) {
this.destroyEvents[index]()
for (const destroyEvent of this.destroyEvents) {
try {
destroyEvent()
}
catch (error) {
console.warn('Failed to destroy event listener:', error)
}
}
this.destroyEvents.clear()
}
}
+6 -1
View File
@@ -71,7 +71,9 @@ export default class Hotkey {
add(key, event) {
if (this.keys[key]) {
this.keys[key].push(event)
if (!this.keys[key].includes(event)) {
this.keys[key].push(event)
}
}
else {
this.keys[key] = [event]
@@ -85,6 +87,9 @@ export default class Hotkey {
if (index !== -1) {
this.keys[key].splice(index, 1)
}
if (this.keys[key].length === 0) {
delete this.keys[key]
}
}
return this
}
+9
View File
@@ -4,6 +4,15 @@ export default class Notice {
constructor(art) {
this.art = art
this.timer = null
art.on('destroy', () => this.destroy())
}
destroy() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
}
set show(msg) {
+19 -6
View File
@@ -3,21 +3,30 @@ import { def } from '../utils'
export default function switchMix(art) {
function switchUrl(url, currentTime) {
return new Promise((resolve, reject) => {
if (url === art.url)
if (url === art.url) {
resolve()
return
}
const { playing, aspectRatio, playbackRate } = art
art.pause()
art.url = url
art.notice.show = ''
art.once('video:error', reject)
const handlers = {}
art.once('video:loadedmetadata', () => {
handlers.error = (error) => {
art.off('video:canplay', handlers.canplay)
art.off('video:loadedmetadata', handlers.metadata)
reject(error)
}
handlers.metadata = () => {
art.currentTime = currentTime
})
}
art.once('video:canplay', async () => {
handlers.canplay = async () => {
art.off('video:error', handlers.error)
art.playbackRate = playbackRate
art.aspectRatio = aspectRatio
@@ -28,7 +37,11 @@ export default function switchMix(art) {
art.notice.show = ''
resolve()
})
}
art.once('video:error', handlers.error)
art.once('video:loadedmetadata', handlers.metadata)
art.once('video:canplay', handlers.canplay)
})
}
+6 -7
View File
@@ -40,8 +40,7 @@ export default class Component {
if (!this.$parent || !this.name || option.disable)
return
const name = option.name || `${this.name}${this.id}`
const item = this.cache.get(name)
errorHandle(!item, `Can't add an existing [${name}] to the [${this.name}]`)
errorHandle(!this.cache.has(name), `Can't add an existing [${name}] to the [${this.name}]`)
this.id += 1
const $ref = createElement('div')
@@ -94,15 +93,15 @@ export default class Component {
}
remove(name) {
errorHandle(this.cache.has(name), `Can't find [${name}] from the [${this.name}]`)
const item = this.cache.get(name)
errorHandle(item, `Can't find [${name}] from the [${this.name}]`)
if (item.option.beforeUnmount) {
item.option.beforeUnmount.call(this.art, item.$ref)
}
for (let index = 0; index < item.events.length; index++) {
this.art.events.remove(item.events[index])
for (const event of item.events) {
this.art.events.remove(event)
}
this.cache.delete(name)
@@ -111,8 +110,8 @@ export default class Component {
}
update(option) {
const item = this.cache.get(option.name)
if (item) {
if (this.cache.has(option.name)) {
const item = this.cache.get(option.name)
option = Object.assign(item.option, option)
this.remove(option.name)
}
+34 -26
View File
@@ -29,7 +29,7 @@ export default class Artplayer extends Player {
static readonly instances: Artplayer[]
static readonly version: string
static readonly env: string
static readonly env: 'development' | 'production'
static readonly build: string
static readonly config: Config
static readonly utils: Utils
@@ -110,67 +110,75 @@ export default class Artplayer extends Player {
e: { [K in keyof Events]?: { fn: (...args: Events[K]) => unknown, ctx: unknown }[] }
destroy(removeHtml?: boolean): void
reset(): void
readonly template: {
get html(): string
query: (str: string) => HTMLElement
query: <T extends Element = Element>(selector: string) => T | null
} & Template
readonly events: {
proxy: <KW extends keyof WindowEventMap, KH extends keyof HTMLElementEventMap>(
element: HTMLDivElement | Document | Window,
eventName: KW | KH,
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
options?: boolean | AddEventListenerOptions,
) => () => void
proxy: {
(target: EventTarget, eventName: string, handler: (event: Event) => void, options?: boolean | AddEventListenerOptions): () => void
(target: EventTarget, eventName: string[], handler: (event: Event) => void, options?: boolean | AddEventListenerOptions): Array<() => void>
}
hover: (element: HTMLElement, mouseenter?: (event: Event) => any, mouseleave?: (event: Event) => any) => void
remove: (event: Event) => void
remove: (destroyEvent: () => void) => void
destroy: () => void
bindGlobalEvents: (source?: { window?: Window, document?: Document }) => void
}
readonly storage: {
name: string
settings: Record<string, unknown>
get: (key: string) => unknown
get: {
(key: string): unknown
(): Record<string, unknown>
}
set: (key: string, value: unknown) => void
del: (key: string) => boolean
del: (key: string) => void
clear: () => void
}
readonly icons: Icons
readonly i18n: {
readonly languages: I18n
languages: I18n
language: Partial<Record<string, string>>
init: () => void
get: (key: string) => string
update: (language: Partial<I18n>) => void
}
readonly notice: {
timer: number
set show(msg: string)
timer: number | null
get show(): boolean
set show(msg: string | Error | false | '')
destroy: () => void
}
readonly layers: Record<string, HTMLElement> & Component
readonly controls: Record<string, HTMLElement> & Component
readonly contextmenu: Record<string, HTMLElement> & Component
readonly layers: Record<string, HTMLElement | undefined> & Component
readonly controls: Record<string, HTMLElement | undefined> & Component
readonly contextmenu: Record<string, HTMLElement | undefined> & Component
readonly subtitle: {
get url(): string
set url(url: string)
get textTrack(): TextTrack
get textTrack(): TextTrack | undefined
get activeCues(): VTTCue[]
get cues(): VTTCue[]
style: (name: string | Partial<CSSStyleDeclaration>, value?: string) => void
switch: (url: string, option?: Subtitle) => Promise<string>
init: (subtitle: Subtitle) => Promise<string | null | undefined>
} & Component
readonly info: Component
readonly loading: Component
readonly hotkey: {
keys: Record<string, ((event: Event) => any)[]>
add: (key: string, callback: (this: Artplayer, event: Event) => any) => Artplayer['hotkey']
remove: (key: string, callback: (event: Event) => any) => Artplayer['hotkey']
keys: Record<string, ((event: KeyboardEvent) => any)[]>
add: (key: string, callback: (this: Artplayer, event: KeyboardEvent) => any) => Artplayer['hotkey']
remove: (key: string, callback: (event: KeyboardEvent) => any) => Artplayer['hotkey']
}
readonly mask: Component
@@ -178,15 +186,15 @@ export default class Artplayer extends Player {
readonly setting: {
option: SettingOption[]
updateStyle: (width?: number) => void
find: (name: string) => SettingOption
add: (setting: Setting) => SettingOption[]
update: (settings: Setting) => SettingOption[]
remove: (name: string) => SettingOption[]
find: (name: string) => SettingOption | undefined
add: (setting: Setting) => Artplayer['setting']
update: (settings: Setting) => Artplayer['setting']
remove: (name: string) => Artplayer['setting']
} & Component
readonly plugins: {
add: (
plugin: (this: Artplayer, art: Artplayer) => unknown | Promise<unknown>,
) => Promise<Artplayer['plugins']> | Artplayer['plugins']
) => Promise<Artplayer['plugins']>
} & Record<string, unknown>
}
+7 -2
View File
@@ -11,6 +11,11 @@ export interface Selector {
*/
html: string | HTMLElement
/**
* Value of selector item
*/
value?: string | number
/**
* Allow custom properties
*/
@@ -51,7 +56,7 @@ export interface Component {
/**
* Dynamic add a component
*/
add: (option: ComponentOption) => HTMLElement
add: (option: ComponentOption | ((art: Artplayer) => ComponentOption)) => HTMLElement | undefined
/**
* Dynamic remove a component by name
@@ -61,7 +66,7 @@ export interface Component {
/**
* Dynamic update a component
*/
update: (option: ComponentOption) => HTMLElement
update: (option: ComponentOption) => HTMLElement | undefined
}
export interface ComponentOption {
+4 -4
View File
@@ -1,5 +1,5 @@
export interface Config {
properties: [
readonly properties: readonly [
'audioTracks',
'autoplay',
'buffered',
@@ -30,8 +30,8 @@ export interface Config {
'videoTracks',
'volume',
]
methods: ['addTextTrack', 'canPlayType', 'load', 'play', 'pause']
events: [
readonly methods: readonly ['addTextTrack', 'canPlayType', 'load', 'play', 'pause']
readonly events: readonly [
'abort',
'canplay',
'canplaythrough',
@@ -55,7 +55,7 @@ export interface Config {
'volumechange',
'waiting',
]
prototypes: [
readonly prototypes: readonly [
'width',
'height',
'videoWidth',
+8 -3
View File
@@ -20,15 +20,18 @@ export interface Events {
'window:scroll': [event: Event]
'window:orientationchange': [event: Event]
'video:abort': [event: Event]
'video:canplay': [event: Event]
'video:canplaythrough': [event: Event]
'video:complete': [event: Event]
'video:durationchange': [event: Event]
'video:emptied': [event: Event]
'video:encrypted': [event: Event]
'video:ended': [event: Event]
'video:error': [error: Error]
'video:loadeddata': [event: Event]
'video:loadedmetadata': [event: Event]
'video:loadstart': [event: Event]
'video:pause': [event: Event]
'video:play': [event: Event]
'video:playing': [event: Event]
@@ -50,7 +53,7 @@ export interface Events {
'contextmenu': [state: boolean]
'control': [state: boolean]
'setting': [state: boolean]
'hotkey': [event: Event]
'hotkey': [event: KeyboardEvent]
'destroy': []
@@ -71,8 +74,10 @@ export interface Events {
'lock': [state: boolean]
'aspectRatio': [aspectRatio: AspectRatio]
'autoHeight': [height: number]
'autoSize': []
'autoSize': [size: { width: number, height: number }]
'ready': []
'airplay': []
'raf': []
'error': [error: Error, reconnectTime: number]
'flip': [flip: Flip]
@@ -84,7 +89,7 @@ export interface Events {
'pip': [state: boolean]
'play': []
'screenshot': [dataUri: string]
'seek': [currentTime: number]
'seek': [currentTime: number, time: number]
'restart': [url: string]
'muted': [state: boolean]
'setBar': [type: Bar, percentage: number, event?: Event | undefined]
+3 -3
View File
@@ -227,12 +227,12 @@ export interface Option {
/**
* Custom video proxy
*/
proxy?: (this: Artplayer, art: Artplayer) => HTMLCanvasElement | HTMLVideoElement
proxy?: (this: Artplayer, art: Artplayer) => HTMLCanvasElement | HTMLVideoElement | undefined
/**
* Custom plugin list
*/
plugins?: ((this: Artplayer, art: Artplayer) => unknown)[]
plugins?: ((this: Artplayer, art: Artplayer) => unknown | Promise<unknown>)[]
/**
* Custom layer list
@@ -316,7 +316,7 @@ export interface Option {
customType?: Partial<
Record<
CustomType,
(this: Artplayer, video: HTMLVideoElement, url: string, art: Artplayer) => unknown
(this: Artplayer, video: HTMLVideoElement, url: string, art: Artplayer) => unknown | Promise<unknown>
>
>
}
+1 -3
View File
@@ -76,9 +76,6 @@ export declare class Player {
get muted(): boolean
set muted(state: boolean)
get title(): string
set title(title: string)
get theme(): string
set theme(theme: string)
@@ -111,4 +108,5 @@ export declare class Player {
airplay(): void
autoSize(): void
autoHeight(): void
reset(): void
}
+12 -14
View File
@@ -1,23 +1,21 @@
import type Artplayer from './artplayer'
type Props<T> = {
export interface SettingOption extends Omit<Setting, 'html' | 'icon' | 'tooltip'> {
html: string
icon: string
tooltip: string
icon: string | undefined
tooltip: string | undefined
$item: HTMLDivElement
$icon: HTMLDivElement
$icon: HTMLDivElement | undefined
$html: HTMLDivElement
$tooltip: HTMLDivElement
$switch: HTMLDivElement
$range: HTMLInputElement
$parent: Setting
$parents: Setting[]
$option: Setting[]
$events: Array<(...args: unknown[]) => unknown>
$tooltip: HTMLDivElement | undefined
$switch: HTMLDivElement | undefined
$range: HTMLInputElement | undefined
$parent: SettingOption | undefined
$parents: SettingOption[]
$option: SettingOption[]
$events: Array<() => void>
$formatted: boolean
} & Omit<T, 'html' | 'icon' | 'tooltip'>
export type SettingOption = Props<Setting>
}
export interface Setting {
/**
+2 -1
View File
@@ -1,4 +1,5 @@
export interface Template {
readonly html: string
readonly $container: HTMLDivElement
readonly $player: HTMLDivElement
readonly $video: HTMLVideoElement
@@ -10,6 +11,7 @@ export interface Template {
readonly $progress: HTMLDivElement
readonly $controls: HTMLDivElement
readonly $controlsLeft: HTMLDivElement
readonly $controlsCenter: HTMLDivElement
readonly $controlsRight: HTMLDivElement
readonly $layer: HTMLDivElement
readonly $loading: HTMLDivElement
@@ -22,5 +24,4 @@ export interface Template {
readonly $infoPanel: HTMLDivElement
readonly $infoClose: HTMLDivElement
readonly $contextmenu: HTMLDivElement
readonly $mini: HTMLDivElement
}
+13 -12
View File
@@ -1,17 +1,18 @@
export interface Utils {
isBrowser: boolean
userAgent: string
isMobile: boolean
isSafari: boolean
isIOS: boolean
isIOS13: boolean
query: (selector: string, parent?: HTMLElement) => HTMLElement
queryAll: (selector: string, parent?: HTMLElement) => HTMLElement[]
query: <T extends Element = Element>(selector: string, parent?: Document | HTMLElement) => T | null
queryAll: <T extends Element = Element>(selector: string, parent?: Document | HTMLElement) => T[]
addClass: (target: HTMLElement, className: string) => void
removeClass: (target: HTMLElement, className: string) => void
hasClass: (target: HTMLElement, className: string) => boolean
append: (target: HTMLElement, child: HTMLElement) => HTMLElement
remove: (target: HTMLElement) => void
append: (target: HTMLElement, child: HTMLElement | string) => Element | ChildNode
remove: (target: HTMLElement) => HTMLElement
replaceElement: (newChild: HTMLElement, oldChild: HTMLElement) => HTMLElement
siblings: (target: HTMLElement) => HTMLElement[]
inverseClass: (target: HTMLElement, className: string) => void
@@ -19,15 +20,14 @@ export interface Utils {
setStyle: <T extends keyof CSSStyleDeclaration>(
element: HTMLElement,
key: T,
value: CSSStyleDeclaration[T],
value: string | CSSStyleDeclaration[T],
) => HTMLElement
setStyles: (element: HTMLElement, styles: Partial<CSSStyleDeclaration>) => HTMLElement
getStyle: <K extends keyof CSSStyleDeclaration>(
element: HTMLElement,
key: K,
numberType?: boolean,
) => boolean extends true ? number : string
setStyleText: (element: HTMLElement, text: string) => void
getStyle: {
(element: HTMLElement, key: keyof CSSStyleDeclaration, numberType?: true): number
(element: HTMLElement, key: keyof CSSStyleDeclaration, numberType: false): string
}
setStyleText: (id: string, cssText: string) => void
getRect: (el: HTMLElement) => { top: number, left: number, width: number, height: number }
tooltip: (target: HTMLElement, msg: string, pos?: string) => void
isInViewport: (target: HTMLElement, offset?: number) => boolean
@@ -56,6 +56,7 @@ export interface Utils {
escape: (str: string) => string
capitalize: (str: string) => string
getIcon: (key: string, html: string | HTMLElement) => HTMLElement
getIcon: (key?: string, html?: string | HTMLElement) => HTMLElement
getComposedPath: (event: Event) => EventTarget[]
supportsFlex: () => boolean
}