mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
166 lines
5.0 KiB
TypeScript
166 lines
5.0 KiB
TypeScript
import { TimeCore, TimeDataAttrs, type TimeType } from '@videojs/core';
|
|
import { applyElementProps, applyStateDataAttrs, logMissingFeature, selectTime } from '@videojs/core/dom';
|
|
import { type Text, translateText } from '@videojs/core/i18n';
|
|
import { remainingSuffixText } from '@videojs/core/i18n/text/time';
|
|
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
|
import { isInteractiveActivation } from '@videojs/utils/dom';
|
|
import { formatTimeAsPhrase } from '@videojs/utils/time';
|
|
|
|
import { i18nContext } from '../../i18n/context';
|
|
import { I18nController } from '../../i18n/controller';
|
|
import { playerContext } from '../../player/context';
|
|
import { PlayerController } from '../../player/player-controller';
|
|
import { MediaElement } from '../media-element';
|
|
|
|
export class TimeElement extends MediaElement {
|
|
static readonly tagName = 'media-time';
|
|
|
|
static override properties = {
|
|
type: { type: String },
|
|
negativeSign: { type: String, attribute: 'negative-sign' },
|
|
label: { type: String },
|
|
toggle: { type: Boolean },
|
|
} satisfies PropertyDeclarationMap<keyof TimeCore.Props>;
|
|
|
|
type: TimeType = TimeCore.defaultProps.type;
|
|
negativeSign = TimeCore.defaultProps.negativeSign;
|
|
label: Text | string = '';
|
|
toggle = TimeCore.defaultProps.toggle;
|
|
|
|
readonly #core = new TimeCore();
|
|
readonly #state = new PlayerController(this, playerContext, selectTime);
|
|
readonly #i18n = new I18nController(this, i18nContext);
|
|
|
|
readonly #signSpan = document.createElement('span');
|
|
readonly #textNode = document.createTextNode('');
|
|
|
|
#disconnect: AbortController | null = null;
|
|
#listening = false;
|
|
#activeType: TimeType = TimeCore.defaultProps.type;
|
|
|
|
override connectedCallback(): void {
|
|
super.connectedCallback();
|
|
|
|
this.#disconnect = new AbortController();
|
|
this.#syncListeners();
|
|
|
|
if (!this.#signSpan.parentNode) {
|
|
this.#signSpan.setAttribute('aria-hidden', 'true');
|
|
this.#signSpan.hidden = true;
|
|
this.appendChild(this.#signSpan);
|
|
this.appendChild(this.#textNode);
|
|
}
|
|
|
|
if (__DEV__ && !this.#state.value) {
|
|
logMissingFeature(this.localName, this.#state.displayName!);
|
|
}
|
|
}
|
|
|
|
override disconnectedCallback(): void {
|
|
super.disconnectedCallback();
|
|
this.#disconnect?.abort();
|
|
this.#disconnect = null;
|
|
this.#listening = false;
|
|
}
|
|
|
|
protected override willUpdate(changed: PropertyValues): void {
|
|
super.willUpdate(changed);
|
|
if (changed.has('type') || changed.has('toggle')) {
|
|
this.#activeType = this.type;
|
|
}
|
|
}
|
|
|
|
protected override update(changed: PropertyValues): void {
|
|
super.update(changed);
|
|
|
|
if (changed.has('toggle')) {
|
|
this.#syncListeners();
|
|
}
|
|
|
|
const media = this.#state.value;
|
|
if (!media) {
|
|
this.#clearAttrs();
|
|
return;
|
|
}
|
|
|
|
this.#core.setProps({
|
|
type: this.toggle ? this.#activeType : this.type,
|
|
negativeSign: this.negativeSign,
|
|
label: this.label,
|
|
toggle: this.toggle,
|
|
});
|
|
this.#core.setMedia(media);
|
|
const state = this.#core.getState();
|
|
|
|
this.#signSpan.hidden = !state.negative;
|
|
this.#signSpan.textContent = state.negative ? this.negativeSign : '';
|
|
this.#textNode.textContent = state.text;
|
|
|
|
const attrs = this.#core.getAttrs(state, this.type);
|
|
applyElementProps(this, {
|
|
...attrs,
|
|
'aria-label': translateText(attrs['aria-label'], this.#i18n.value, this.#getLabelParams(state)),
|
|
});
|
|
applyStateDataAttrs(this, state, TimeDataAttrs);
|
|
}
|
|
|
|
#getLabelParams(state: TimeCore.State): { duration: string } | undefined {
|
|
const params = this.#core.getLabelParams(state);
|
|
if (!params) return undefined;
|
|
|
|
const duration = formatTimeAsPhrase(Math.abs(state.seconds), { locale: this.#i18n.locale });
|
|
|
|
return {
|
|
duration:
|
|
state.type === 'remaining' ? translateText(remainingSuffixText, this.#i18n.value, { duration }) : duration,
|
|
};
|
|
}
|
|
|
|
#handleClick = (event: MouseEvent): void => {
|
|
if (event.defaultPrevented || !this.toggle || !this.#state.value) return;
|
|
this.#toggleType();
|
|
};
|
|
|
|
#handleKeyDown = (event: KeyboardEvent): void => {
|
|
if (event.defaultPrevented || !isInteractiveActivation(event)) return;
|
|
if (!this.toggle || !this.#state.value) return;
|
|
// Prevent space from scrolling page.
|
|
event.preventDefault();
|
|
if (event.repeat) return;
|
|
this.#toggleType();
|
|
};
|
|
|
|
#toggleType(): void {
|
|
if (this.type === 'current') {
|
|
this.#activeType = this.#activeType === 'remaining' ? 'current' : 'remaining';
|
|
} else {
|
|
this.#activeType = this.#activeType === 'duration' ? 'remaining' : 'duration';
|
|
}
|
|
|
|
this.requestUpdate();
|
|
}
|
|
|
|
#syncListeners(): void {
|
|
if (!this.toggle || !this.#disconnect || this.#listening) return;
|
|
|
|
this.#listening = true;
|
|
applyElementProps(
|
|
this,
|
|
{
|
|
onClick: this.#handleClick,
|
|
onKeyDown: this.#handleKeyDown,
|
|
},
|
|
{ signal: this.#disconnect.signal }
|
|
);
|
|
}
|
|
|
|
#clearAttrs(): void {
|
|
applyElementProps(this, {
|
|
'aria-label': undefined,
|
|
role: undefined,
|
|
tabIndex: undefined,
|
|
'data-type': undefined,
|
|
});
|
|
}
|
|
}
|