mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(packages)!: replace DelegateMixin & ProxyMixin with MediaHost base classes (#1292)
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Convert a NamedNodeMap to a plain object.
|
||||
*/
|
||||
export function namedNodeMapToObject(namedNodeMap: NamedNodeMap) {
|
||||
const obj: Record<string, string> = {};
|
||||
for (const attr of namedNodeMap) {
|
||||
@@ -5,3 +8,16 @@ export function namedNodeMapToObject(namedNodeMap: NamedNodeMap) {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to serialize attributes into a string.
|
||||
*/
|
||||
export function serializeAttributes(attrs: Record<string, string>) {
|
||||
let html = '';
|
||||
for (const key in attrs) {
|
||||
const value = attrs[key];
|
||||
if (value === '') html += ` ${key}`;
|
||||
else html += ` ${key}="${value}"`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { animationFrame } from './animation-frame';
|
||||
export { namedNodeMapToObject } from './attributes';
|
||||
export { namedNodeMapToObject, serializeAttributes } from './attributes';
|
||||
export { isRTL } from './direction';
|
||||
export { type OnEventOptions, onEvent, resolveEventTarget } from './event';
|
||||
export { idleCallback } from './idle-callback';
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
/** Find the `<track>` element that owns the given `TextTrack`. */
|
||||
export function findTrackElement(media: HTMLMediaElement, track: TextTrack): HTMLTrackElement | null {
|
||||
for (const el of media.querySelectorAll?.('track') ?? []) {
|
||||
export function findTrackElement(media: EventTarget, track: unknown): HTMLTrackElement | null {
|
||||
if (!(media instanceof HTMLElement)) return null;
|
||||
for (const el of media.querySelectorAll('track')) {
|
||||
if (el.track === track) return el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getTextTrackList(media: HTMLMediaElement, filterPred: (textTrack: TextTrack) => boolean): TextTrack[] {
|
||||
if (!media?.textTracks) return [];
|
||||
return (Array.from(media.textTracks) as TextTrack[]).filter(filterPred).sort(sortByTextTrackKind);
|
||||
export function getTextTrackList<Track extends { kind: string; mode: string }>(
|
||||
media: { textTracks?: Iterable<Track> },
|
||||
filterPred: (textTrack: Track) => boolean
|
||||
): Track[] {
|
||||
if (!media.textTracks) return [];
|
||||
return Array.from(media.textTracks).filter(filterPred).sort(sortByKind);
|
||||
}
|
||||
|
||||
function sortByTextTrackKind(a: TextTrack, b: TextTrack): number {
|
||||
function sortByKind(a: { kind: string }, b: { kind: string }): number {
|
||||
return a.kind > b.kind ? 1 : a.kind < b.kind ? -1 : 0;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { defaults } from './defaults';
|
||||
export { omit } from './omit';
|
||||
export { pick } from './pick';
|
||||
export { shallowEqual } from './shallow-equal';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Creates a new object without the specified keys.
|
||||
*
|
||||
* @example
|
||||
* const obj = { a: 1, b: 2, c: 3 };
|
||||
* omit(obj, ['b']); // { a: 1, c: 3 }
|
||||
*/
|
||||
export function omit<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: readonly K[]): Omit<T, K> {
|
||||
const result = {} as Record<string, unknown>;
|
||||
|
||||
for (const key in obj) {
|
||||
if (!keys.includes(key as unknown as K)) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result as Omit<T, K>;
|
||||
}
|
||||
@@ -9,7 +9,9 @@ export function pick<T extends Record<string, unknown>, K extends keyof T>(obj:
|
||||
const result = {} as Pick<T, K>;
|
||||
|
||||
for (const key of keys) {
|
||||
result[key] = obj[key];
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { omit } from '../omit';
|
||||
|
||||
describe('omit', () => {
|
||||
it('removes specified keys from object', () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
expect(omit(obj, ['b'])).toEqual({ a: 1, c: 3 });
|
||||
});
|
||||
|
||||
it('removes multiple keys', () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
expect(omit(obj, ['a', 'c'])).toEqual({ b: 2 });
|
||||
});
|
||||
|
||||
it('returns copy of object for empty keys array', () => {
|
||||
const obj = { a: 1, b: 2 };
|
||||
const result = omit(obj, []);
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 2 });
|
||||
expect(result).not.toBe(obj);
|
||||
});
|
||||
|
||||
it('ignores non-existent keys', () => {
|
||||
const obj = { a: 1, b: 2 };
|
||||
expect(omit(obj, ['nonexistent' as keyof typeof obj])).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('returns empty object when all keys are removed', () => {
|
||||
const obj = { a: 1, b: 2 };
|
||||
expect(omit(obj, ['a', 'b'])).toEqual({});
|
||||
});
|
||||
|
||||
it('handles nested objects (shallow copy)', () => {
|
||||
const nested = { a: { x: 1 }, b: { y: 2 }, c: 3 };
|
||||
const result = omit(nested, ['c']);
|
||||
|
||||
expect(result).toEqual({ a: { x: 1 }, b: { y: 2 } });
|
||||
expect((result as any).a).toBe(nested.a);
|
||||
});
|
||||
|
||||
it('preserves value types', () => {
|
||||
const obj = {
|
||||
str: 'hello',
|
||||
num: 42,
|
||||
bool: true,
|
||||
arr: [1, 2, 3],
|
||||
nil: null,
|
||||
undef: undefined,
|
||||
};
|
||||
|
||||
const result = omit(obj, ['str']);
|
||||
|
||||
expect(result).toEqual({
|
||||
num: 42,
|
||||
bool: true,
|
||||
arr: [1, 2, 3],
|
||||
nil: null,
|
||||
undef: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with readonly keys array', () => {
|
||||
const obj = { a: 1, b: 2, c: 3 };
|
||||
const keys = ['a', 'c'] as const;
|
||||
|
||||
expect(omit(obj, keys)).toEqual({ b: 2 });
|
||||
});
|
||||
|
||||
it('filters attrs that are MediaHost props from element attributes', () => {
|
||||
const elementAttrs = {
|
||||
src: 'video.mp4',
|
||||
autoplay: '',
|
||||
class: 'player',
|
||||
'current-time': '10',
|
||||
'playback-rate': '1.5',
|
||||
muted: '',
|
||||
};
|
||||
const mediaPropAttrs = ['current-time', 'playback-rate', 'muted'] as const;
|
||||
|
||||
const result = omit(elementAttrs, mediaPropAttrs);
|
||||
|
||||
expect(result).toEqual({ src: 'video.mp4', autoplay: '', class: 'player' });
|
||||
expect(result).not.toHaveProperty('current-time');
|
||||
expect(result).not.toHaveProperty('playback-rate');
|
||||
expect(result).not.toHaveProperty('muted');
|
||||
});
|
||||
});
|
||||
@@ -57,4 +57,29 @@ describe('pick', () => {
|
||||
|
||||
expect(pick(obj, keys)).toEqual({ a: 1, c: 3 });
|
||||
});
|
||||
|
||||
it('does not include non-existent keys as undefined properties', () => {
|
||||
const obj = { a: 1 };
|
||||
const result = pick(obj, ['a', 'b' as keyof typeof obj]);
|
||||
|
||||
expect(Object.keys(result)).toEqual(['a']);
|
||||
expect(result).not.toHaveProperty('b');
|
||||
});
|
||||
|
||||
it('filters element attributes to only allowed media attributes', () => {
|
||||
const elementAttrs: Record<string, string> = {
|
||||
src: 'video.mp4',
|
||||
autoplay: '',
|
||||
class: 'player',
|
||||
style: 'width: 100%',
|
||||
};
|
||||
const allowedAttrs = ['src', 'autoplay', 'controls', 'muted', 'loop'];
|
||||
|
||||
const result = pick(elementAttrs, allowedAttrs);
|
||||
|
||||
expect(result).toEqual({ src: 'video.mp4', autoplay: '' });
|
||||
expect(result).not.toHaveProperty('class');
|
||||
expect(result).not.toHaveProperty('style');
|
||||
expect(result).not.toHaveProperty('controls');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,9 @@ export type AnyConstructor<T, Arguments extends unknown[] = any[]> =
|
||||
|
||||
export type Mixin<Base, Result> = <T extends Constructor<Base>>(Base: T) => T & Constructor<Result>;
|
||||
|
||||
export type MixinReturn<Base extends AnyConstructor<any>, Props> = Constructor<InstanceType<Base> & Props> &
|
||||
Omit<Base, 'prototype'>;
|
||||
|
||||
export type Falsy<T> = T | false | null | undefined;
|
||||
|
||||
export type EnsureFunction<T> = T extends (...args: any[]) => any ? T : never;
|
||||
@@ -19,3 +22,20 @@ export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
|
||||
export type NonNullableObject<T extends object> = {
|
||||
[P in keyof T]-?: Exclude<T[P], null | undefined>;
|
||||
};
|
||||
|
||||
// Detects readonly vs writable properties via conditional type identity check.
|
||||
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
||||
|
||||
type WritableKeys<T> = {
|
||||
[K in keyof T]-?: IfEquals<{ [Q in K]: T[K] }, { -readonly [Q in K]: T[K] }, K, never>;
|
||||
}[keyof T];
|
||||
|
||||
type SettableKeys<T> = {
|
||||
[K in WritableKeys<T>]: T[K] extends (...args: any[]) => any ? never : K;
|
||||
}[WritableKeys<T>];
|
||||
|
||||
type ExcludeInternal<K> = K extends `_${string}` ? never : K;
|
||||
|
||||
export type InferClassProps<D extends abstract new (...args: any[]) => any> = Partial<
|
||||
Pick<InstanceType<D>, ExcludeInternal<SettableKeys<InstanceType<D>>>>
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user