import Player, { $, isMobile } from '@oplayer/core'
import { Icons } from '../functions/icons'
import { icon, settingShown, tooltip } from '../style'
import type { Setting, UIInterface } from '../types'
import {
activeCls,
nextIcon,
nextLabelText,
panelCls,
settingCls,
settingItemCls,
settingItemLeft,
settingItemRight,
subPanelCls,
yesIcon,
switcherCls,
switcherContainer,
backIcon,
backRow,
sliderCls
} from './Setting.style'
export const arrowSvg = (className = nextIcon) =>
``
// Selector Options
const selectorOption = (name: string, icon: string = '') =>
`
${icon}
${name}
`
const nexter = (name: string, icon: string = '') =>
`
${icon}
${name}
${arrowSvg()}
`
const back = (name: string) =>
`
${arrowSvg(backIcon)}
${name}
`
const switcher = (name: string, icon: string = '') =>
`
${icon}
${name}
`
const normal = (name: string, icon: string = '') =>
`
${icon}
${name}
`
export const slider = ({
name,
icon = '',
max = 1,
min = 0,
value = 0,
step = 1
}: {
name: string
icon?: string
min: number
max: number
value: number
step: number
}) =>
`
${icon}
${name}
`
function createRow({
type,
key,
name,
icon,
default: selected,
index,
max,
min,
step,
hasChildren
}: Omit & {
hasChildren?: boolean
index?: number
switcherLabe?: string
}) {
let $item: HTMLElement = $.create(`div.${settingItemCls}`, {
'data-key': key,
role: type == 'option' ? 'menuitemradio' : 'menuitem',
'aria-haspopup': hasChildren ? 'menu' : false,
'aria-label': type || 'menuitem'
})
const res = {
$row: $item,
$label: undefined as unknown as HTMLElement
}
switch (type) {
case 'switcher':
$item.innerHTML = switcher(name, icon)
$item.setAttribute('aria-checked', selected || false)
break
case 'selector':
$item.innerHTML = nexter(name, icon)
res['$label'] = $item.querySelector('span[role="label"]')!
break
case 'back' as any:
$item.innerHTML = back(name)
break
case 'slider':
$item.innerHTML = slider({ name, max, min, icon, value: selected, step } as any)
break
case 'option':
$item.innerHTML = selectorOption(name, icon)
$item.setAttribute('aria-checked', selected || false)
if (typeof index == 'number') {
$item.setAttribute('data-index', index.toString())
}
break
default:
if (hasChildren) {
$item.innerHTML = nexter(name, icon)
} else {
$item.innerHTML = normal(name, icon)
}
break
}
return res
}
export type Panel = {
$ref: HTMLElement
key: string
select?: Function // 全是选项才有
parent?: Panel
}
function createPanel(
player: Player,
panels: Panel[],
setting: Setting[],
options: {
/**
* 全是选项面板的用上一个面板的key
*/
key?: string
name?: string
target: HTMLElement
parent?: Panel
isSelectorOptionsPanel?: boolean
parenOnChange?: Function
} = {} as any
): Panel | void {
if (!setting || setting.length == 0) return
const { key: parentKey, target, parent, isSelectorOptionsPanel, name } = options
let panel = {} as Panel
let key: string = parentKey! || 'root'
if (panels[0] && key == 'root') {
panel = panels[0]!
key = panels[0]!.key
} else {
//创建新的选项面板
panel.$ref = $.create(`div.${panels[0] && isSelectorOptionsPanel ? subPanelCls : panelCls}`, {
'data-key': key,
role: 'menu'
})
panel.key = key
panels.push(panel)
}
panel.parent = parent
const isRoot = panel.key == 'root'
if (!isRoot) {
// back row
const { $row } = createRow({ name: name!, type: 'back' as any })
$row.addEventListener('click', () => {
panel.$ref.classList.remove(activeCls)
panel.parent?.$ref.classList.add(activeCls)
})
$.render($row, panel.$ref)
}
for (let i = 0; i < setting.length; i++) {
const {
name,
type,
key,
children,
icon,
default: selected,
onChange,
max,
min,
step,
value
} = setting[i]!
const { $row, $label } = createRow(
Object.assign(
{
name,
type: isSelectorOptionsPanel ? 'option' : type,
key: key,
icon,
default: selected,
max,
min,
step,
hasChildren: Boolean(children)
},
!isRoot && isSelectorOptionsPanel && { index: i }
)
)
$.render($row, panel.$ref)
$.render(panel.$ref, target)
if (children) {
const nextIsSelectorOptionsPanel =
type == 'selector' && children.every((it) => !Boolean(it.type) || it.type == 'option')
const optionPanel = createPanel(player, panels, children, {
key: key || name,
target,
parent: panel,
isSelectorOptionsPanel: nextIsSelectorOptionsPanel,
name,
parenOnChange: onChange
})!
$row.addEventListener('click', () => {
panel.$ref.classList.remove(activeCls)
optionPanel.$ref.classList.add(activeCls)
})
if (nextIsSelectorOptionsPanel) {
const defaultSelected = children.find((it) => it.default)
if (defaultSelected) {
$label.innerText = defaultSelected.name
}
optionPanel.select = (i: number, shouldBeCallFn: boolean) => {
if (i == -1) {
optionPanel
.$ref!.querySelector('[aria-checked=true]')
?.setAttribute('aria-checked', 'false')
return
}
const $targets = optionPanel.$ref.querySelectorAll('[aria-checked]')
if ($targets.item(i).getAttribute('aria-checked') != 'true') {
$targets.forEach((it) => it.setAttribute('aria-checked', 'false'))
$targets.item(i).setAttribute('aria-checked', 'true')
const value = children[i]
$label.innerText = value!.name
if (shouldBeCallFn) onChange?.(value, { index: i, player })
}
}
optionPanel.$ref.addEventListener('click', (e) => {
const target = e.target as HTMLDivElement
if (target.hasAttribute('data-index')) {
optionPanel.select!(+target.getAttribute('data-index')!, true)
panel.$ref.classList.add(activeCls)
optionPanel.$ref.classList.remove(activeCls)
}
})
}
} else {
if (type == 'switcher') {
;($row as any).select = function (shouldBeCallFn: boolean) {
const selected = this.getAttribute('aria-checked') == 'true'
this.setAttribute('aria-checked', `${!selected}`)
if (shouldBeCallFn) onChange?.(!selected)
}
$row.addEventListener('click', () => ($row as any).select(true))
} else if (type == 'slider') {
const $input = $row.querySelector('input')!
$input.oninput = function (event: any) {
event.target.setAttribute('value', event.target.value)
}
$input.onchange = function (event: any) {
onChange?.(event.target.value)
}
// TODO: update methond
} else {
if (type == 'option' || (type == undefined && !isSelectorOptionsPanel)) {
$row.addEventListener('click', () => (onChange || options.parenOnChange)?.(value))
}
}
}
}
return panel
}
export default function (it: UIInterface) {
const { player, $root: $el, config } = it
if (config.settings === false) return
const position = config.theme.controller?.setting
const options = config.settings || []
const isTop = config.theme.controller?.header && (position == 'top' || (isMobile && position == 'auto'))
const $dom = $.create(`div.${settingCls(isTop ? 'top' : 'bottom')}`, {
'aria-label': 'Setting'
})
let panels: Panel[] = []
let hasRendered = false
const defaultSettingMap = {
loop: {
name: player.locales.get('Loop'),
type: 'switcher',
key: 'loop',
icon: Icons.get('loop'),
default: player.isLoop,
onChange: (value: boolean) => player.setLoop(value)
}
}
bootstrap(options.map((it) => (typeof it == 'string' ? defaultSettingMap[it] : it)) as Setting[])
function register(payload: Setting | Setting[]) {
const _payload = Array.isArray(payload) ? payload : [payload]
bootstrap(
_payload
.map((p) => {
const repeated = panels.find((panel) => panel.key == p.key)
if (repeated) {
unregister(repeated.key)
return
}
return p
})
.filter(Boolean) as Setting[]
)
}
function unregister(key: string) {
if (!hasRendered) return
panels[0]?.$ref.querySelector(`[data-key=${key}]`)?.remove()
panels = panels.filter((p) => (p.key === key ? (p.$ref.remove(), (p = null as any), false) : true))
}
function updateLabel(key: string, text: string) {
if (!hasRendered) return
const $item = $dom.querySelector(`[data-key="${key}"] span[role="label"]`)
if ($item) $item.innerText = text
}
function select(key: string, value: boolean | number, shouldBeCallFn: Boolean = true) {
if (!hasRendered) return
if (typeof value == 'number') {
for (let i = 0; i < panels.length; i++) {
const panel = panels[i]!
if (panel.key == key) {
panel.select!(value, shouldBeCallFn)
break
}
}
} else {
$dom
.querySelector(`[data-key="${key}"][aria-checked]`)
?.select(shouldBeCallFn)
}
}
function bootstrap(settings: Setting[]) {
if (settings.length < 1) return
if (!hasRendered) {
hasRendered = true
$.render($dom, $el)
renderSettingMenu()
it.keyboard?.register({ c: showSetting })
}
createPanel(player, panels, settings, { target: $dom })
}
function outClickListener(e: Event) {
if (!$dom.contains(e.target)) {
player.$root.classList.remove(settingShown)
panels.forEach(($p) => $p.$ref.classList.remove(activeCls))
document.removeEventListener('click', outClickListener)
}
}
function showSetting() {
player.$root.classList.add(settingShown)
panels[0]!.$ref.classList.add(activeCls)
setTimeout(() => {
document.addEventListener('click', outClickListener)
})
}
player.on('destroy', () => {
document.removeEventListener('click', outClickListener)
})
function renderSettingMenu() {
const settingButton = $.create(
'button',
{
class: `${icon} ${tooltip}`,
'aria-label': player.locales.get('Settings'),
'data-tooltip-pos': position == 'top' ? 'down' : ''
},
`${Icons.get('setting')}`
)
settingButton.addEventListener('click', (e) => {
e.stopPropagation()
showSetting()
})
const index = [config.pictureInPicture && player.isPipEnabled, config.fullscreen].filter(Boolean).length
if (isTop) {
const parent = it.$controllerBar!.lastElementChild!
parent.insertBefore(settingButton, parent.children[parent.children.length]!)
} else {
const parent = it.$controllerBottom!.lastElementChild!
parent.insertBefore(settingButton, parent.children[parent.children.length - index]!)
}
}
it.setting = { register, unregister, updateLabel, select }
}