mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(core): add play button component (#383)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import { isFunction, isUndefined } from '@videojs/utils/predicate';
|
||||
|
||||
import type { ElementProps } from '../../core/element';
|
||||
|
||||
/**
|
||||
* Apply props to a DOM element.
|
||||
*
|
||||
* Handles both attributes and event listeners:
|
||||
* - Event props (onClick, onKeyDown, etc.) are attached as listeners
|
||||
* - Boolean props: `true` sets empty attribute, `false` removes
|
||||
* - `undefined` removes the attribute
|
||||
* - Other props are set as string attributes
|
||||
*/
|
||||
export function applyElementProps(element: HTMLElement, props: ElementProps | object, signal?: AbortSignal): void {
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (isFunction(value) && key.startsWith('on')) {
|
||||
const event = key.slice(2).toLowerCase();
|
||||
listen(element, event, value as EventListener, signal ? { signal } : undefined);
|
||||
} else if (isUndefined(value) || value === false) {
|
||||
element.removeAttribute(key);
|
||||
} else if (value === true) {
|
||||
element.setAttribute(key, '');
|
||||
} else {
|
||||
element.setAttribute(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { applyElementProps } from './element-props';
|
||||
export { logMissingFeature } from './log';
|
||||
export { applyStateDataAttrs, getStateDataAttrs } from './state-data-attrs';
|
||||
@@ -0,0 +1,9 @@
|
||||
const warned = new Set<string>();
|
||||
|
||||
export function logMissingFeature(displayName: string, featureName: string): void {
|
||||
const key = `${displayName}:${featureName}`;
|
||||
if (warned.has(key)) return;
|
||||
|
||||
warned.add(key);
|
||||
console.warn(`${displayName} requires ${featureName} feature`);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Convert state object to data attributes.
|
||||
*
|
||||
* - `true` → `data-keyname=""`
|
||||
* - truthy string/number → `data-keyname="value"`
|
||||
* - falsy → no attribute
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const state = { paused: true, ended: false, volume: 0.5 };
|
||||
* getStateDataAttrs(state);
|
||||
* // { 'data-paused': '', 'data-volume': '0.5' }
|
||||
* ```
|
||||
*/
|
||||
export function getStateDataAttrs<State extends object>(state: State): Record<string, string> {
|
||||
const attrs: Record<string, string> = {};
|
||||
|
||||
for (const key in state) {
|
||||
const value = state[key];
|
||||
|
||||
if (value === true) {
|
||||
attrs[`data-${key.toLowerCase()}`] = '';
|
||||
} else if (value) {
|
||||
attrs[`data-${key.toLowerCase()}`] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply state as data attributes to an element.
|
||||
*
|
||||
* - `true` → sets `data-keyname=""`
|
||||
* - truthy string/number → sets `data-keyname="value"`
|
||||
* - falsy → removes the attribute
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const state = { paused: true, ended: false };
|
||||
* applyStateDataAttrs(element, state);
|
||||
* // element has data-paused="", data-ended is removed
|
||||
* ```
|
||||
*/
|
||||
export function applyStateDataAttrs<State extends object>(element: HTMLElement, state: State): void {
|
||||
for (const key in state) {
|
||||
const value = state[key];
|
||||
const attrName = `data-${key.toLowerCase()}`;
|
||||
|
||||
if (value === true) {
|
||||
element.setAttribute(attrName, '');
|
||||
} else if (value) {
|
||||
element.setAttribute(attrName, String(value));
|
||||
} else {
|
||||
element.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { applyStateDataAttrs, getStateDataAttrs } from '../state-data-attrs';
|
||||
|
||||
describe('getStateDataAttrs', () => {
|
||||
it('converts true to empty string attribute', () => {
|
||||
const state = { paused: true };
|
||||
expect(getStateDataAttrs(state)).toEqual({ 'data-paused': '' });
|
||||
});
|
||||
|
||||
it('ignores false values', () => {
|
||||
const state = { paused: false };
|
||||
expect(getStateDataAttrs(state)).toEqual({});
|
||||
});
|
||||
|
||||
it('ignores undefined values', () => {
|
||||
const state = { paused: undefined };
|
||||
expect(getStateDataAttrs(state)).toEqual({});
|
||||
});
|
||||
|
||||
it('ignores null values', () => {
|
||||
const state = { paused: null };
|
||||
expect(getStateDataAttrs(state)).toEqual({});
|
||||
});
|
||||
|
||||
it('converts string values to attribute', () => {
|
||||
const state = { status: 'loading' };
|
||||
expect(getStateDataAttrs(state)).toEqual({ 'data-status': 'loading' });
|
||||
});
|
||||
|
||||
it('converts number values to string attribute', () => {
|
||||
const state = { volume: 0.5 };
|
||||
expect(getStateDataAttrs(state)).toEqual({ 'data-volume': '0.5' });
|
||||
});
|
||||
|
||||
it('ignores zero values', () => {
|
||||
const state = { volume: 0 };
|
||||
expect(getStateDataAttrs(state)).toEqual({});
|
||||
});
|
||||
|
||||
it('converts keys to lowercase', () => {
|
||||
const state = { isPaused: true, currentTime: 10 };
|
||||
expect(getStateDataAttrs(state)).toEqual({
|
||||
'data-ispaused': '',
|
||||
'data-currenttime': '10',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple state properties', () => {
|
||||
const state = { paused: true, ended: false, waiting: true, volume: 0.8 };
|
||||
expect(getStateDataAttrs(state)).toEqual({
|
||||
'data-paused': '',
|
||||
'data-waiting': '',
|
||||
'data-volume': '0.8',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty object for empty state', () => {
|
||||
const state = {};
|
||||
expect(getStateDataAttrs(state)).toEqual({});
|
||||
});
|
||||
|
||||
it('ignores empty string values', () => {
|
||||
const state = { label: '' };
|
||||
expect(getStateDataAttrs(state)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyStateDataAttrs', () => {
|
||||
it('applies data attributes to element', () => {
|
||||
const element = document.createElement('div');
|
||||
const state = { paused: true, volume: 0.5 };
|
||||
|
||||
applyStateDataAttrs(element, state);
|
||||
|
||||
expect(element.getAttribute('data-paused')).toBe('');
|
||||
expect(element.getAttribute('data-volume')).toBe('0.5');
|
||||
});
|
||||
|
||||
it('does not apply falsy values', () => {
|
||||
const element = document.createElement('div');
|
||||
const state = { paused: false, ended: null, waiting: undefined };
|
||||
|
||||
applyStateDataAttrs(element, state);
|
||||
|
||||
expect(element.hasAttribute('data-paused')).toBe(false);
|
||||
expect(element.hasAttribute('data-ended')).toBe(false);
|
||||
expect(element.hasAttribute('data-waiting')).toBe(false);
|
||||
});
|
||||
|
||||
it('applies multiple attributes', () => {
|
||||
const element = document.createElement('div');
|
||||
const state = { paused: true, ended: true, started: true };
|
||||
|
||||
applyStateDataAttrs(element, state);
|
||||
|
||||
expect(element.getAttribute('data-paused')).toBe('');
|
||||
expect(element.getAttribute('data-ended')).toBe('');
|
||||
expect(element.getAttribute('data-started')).toBe('');
|
||||
});
|
||||
|
||||
it('removes attributes when state becomes false', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
applyStateDataAttrs(element, { paused: true });
|
||||
expect(element.hasAttribute('data-paused')).toBe(true);
|
||||
|
||||
applyStateDataAttrs(element, { paused: false });
|
||||
expect(element.hasAttribute('data-paused')).toBe(false);
|
||||
});
|
||||
|
||||
it('removes attributes when state becomes null or undefined', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
applyStateDataAttrs(element, { status: 'loading' });
|
||||
expect(element.getAttribute('data-status')).toBe('loading');
|
||||
|
||||
applyStateDataAttrs(element, { status: null });
|
||||
expect(element.hasAttribute('data-status')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles state transitions correctly', () => {
|
||||
const element = document.createElement('div');
|
||||
|
||||
// Initial state
|
||||
applyStateDataAttrs(element, { paused: true, ended: false, volume: 0.5 });
|
||||
expect(element.hasAttribute('data-paused')).toBe(true);
|
||||
expect(element.hasAttribute('data-ended')).toBe(false);
|
||||
expect(element.getAttribute('data-volume')).toBe('0.5');
|
||||
|
||||
// Transition: paused false, ended true, volume 0
|
||||
applyStateDataAttrs(element, { paused: false, ended: true, volume: 0 });
|
||||
expect(element.hasAttribute('data-paused')).toBe(false);
|
||||
expect(element.hasAttribute('data-ended')).toBe(true);
|
||||
expect(element.hasAttribute('data-volume')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user