Files
ArtPlayer/packages/artplayer-tool-thumbnail/src/input.ts
T

123 lines
3.8 KiB
TypeScript

import type ArtplayerToolThumbnail from './index'
import type { InputOptions } from './types'
import { cancellation, stateFor } from './lifecycle'
import { normalizePolicy } from './policy'
type Registration = readonly [name: string, callback: EventListener]
interface InputRecord {
input: HTMLInputElement
wrapper: HTMLElement | null
position: string
listeners: Registration[]
callbacks: Registration[] | null
}
const inputs = new WeakMap<ArtplayerToolThumbnail, InputRecord>()
function release(record: InputRecord) {
let failure: unknown
for (const [name, callback] of record.listeners.splice(0)) {
try {
record.input.removeEventListener(name, callback)
}
catch (error) {
failure ||= error
}
}
if (record.wrapper) {
try {
if (record.input.parentNode)
record.input.parentNode.removeChild(record.input)
}
catch (error) {
failure ||= error
}
if (record.wrapper.style.position === 'relative')
record.wrapper.style.position = record.position
}
if (failure)
throw failure
}
function connect(record: InputRecord, callbacks: Registration[]) {
try {
for (const [name, callback] of callbacks) {
record.listeners.push([name, callback])
record.input.addEventListener(name, callback)
}
}
catch (error) {
try {
release(record)
}
catch {}
throw error
}
}
export function setupInput(tool: ArtplayerToolThumbnail, patch: InputOptions) {
const state = stateFor(tool)
const epoch = ++state.inputEpoch
const current = () => !state.closed && state.inputEpoch === epoch
const option = Object.assign({}, tool.option, patch)
const target = option.fileInput!
tool.errorHandle(target instanceof Element, 'The \'fileInput\' is not a Element')
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message))
if (!current())
throw cancellation('input setup superseded')
const previous = inputs.get(tool)
if (previous && (target === previous.input || target === previous.wrapper)) {
option.fileInput = previous.input
tool.option = option
return option
}
// A wrapper is replaced by its owned file input before the record is committed.
const record: InputRecord = { input: target as HTMLInputElement, wrapper: null, position: '', listeners: [], callbacks: previous?.callbacks || null }
try {
if (!(target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'file')) {
record.wrapper = target
record.position = target.style.position
record.input = document.createElement('input')
record.input.type = 'file'
Object.assign(record.input.style, { position: 'absolute', width: '100%', height: '100%', left: '0', top: '0', right: '0', bottom: '0', opacity: '0' })
target.style.position = 'relative'
target.appendChild(record.input)
}
if (!current())
throw cancellation('input setup superseded')
if (record.callbacks)
connect(record, record.callbacks)
if (!current())
throw cancellation('input setup superseded')
}
catch (error) {
try {
release(record)
}
catch {}
throw error
}
option.fileInput = record.input
inputs.set(tool, record)
tool.option = option
if (previous)
release(previous)
return option
}
export function connectInput(tool: ArtplayerToolThumbnail, dragover: (event: DragEvent) => void) {
const record = inputs.get(tool)!
// These callbacks are registered only for their matching DOM event names.
record.callbacks = [['change', tool.inputChange], ['dragover', dragover as EventListener], ['drop', tool.ondrop as EventListener]]
connect(record, record.callbacks)
}
export function releaseInput(tool: ArtplayerToolThumbnail) {
const record = inputs.get(tool)
if (!record)
return
inputs.delete(tool)
release(record)
}