kopia lustrzana https://github.com/shoelace-style/shoelace
171 wiersze
6.4 KiB
TypeScript
171 wiersze
6.4 KiB
TypeScript
import { LitElement } from 'lit';
|
|
import { property } from 'lit/decorators.js';
|
|
|
|
// Match event type name strings that are registered on GlobalEventHandlersEventMap...
|
|
type EventTypeRequiresDetail<T> = T extends keyof GlobalEventHandlersEventMap
|
|
? // ...where the event detail is an object...
|
|
GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, unknown>>
|
|
? // ...that is non-empty...
|
|
GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, never>>
|
|
? never
|
|
: // ...and has at least one non-optional property
|
|
Partial<GlobalEventHandlersEventMap[T]['detail']> extends GlobalEventHandlersEventMap[T]['detail']
|
|
? never
|
|
: T
|
|
: never
|
|
: never;
|
|
|
|
// The inverse of the above (match any type that doesn't match EventTypeRequiresDetail)
|
|
type EventTypeDoesNotRequireDetail<T> = T extends keyof GlobalEventHandlersEventMap
|
|
? GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, unknown>>
|
|
? GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, never>>
|
|
? T
|
|
: Partial<GlobalEventHandlersEventMap[T]['detail']> extends GlobalEventHandlersEventMap[T]['detail']
|
|
? T
|
|
: never
|
|
: T
|
|
: T;
|
|
|
|
// `keyof EventTypesWithRequiredDetail` lists all registered event types that require detail
|
|
type EventTypesWithRequiredDetail = {
|
|
[EventType in keyof GlobalEventHandlersEventMap as EventTypeRequiresDetail<EventType>]: true;
|
|
};
|
|
|
|
// `keyof EventTypesWithoutRequiredDetail` lists all registered event types that do NOT require detail
|
|
type EventTypesWithoutRequiredDetail = {
|
|
[EventType in keyof GlobalEventHandlersEventMap as EventTypeDoesNotRequireDetail<EventType>]: true;
|
|
};
|
|
|
|
// Helper to make a specific property of an object non-optional
|
|
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
|
|
|
// Given an event name string, get a valid type for the options to initialize the event that is more restrictive than
|
|
// just CustomEventInit when appropriate (validate the type of the event detail, and require it to be provided if the
|
|
// event requires it)
|
|
type SlEventInit<T> = T extends keyof GlobalEventHandlersEventMap
|
|
? GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, unknown>>
|
|
? GlobalEventHandlersEventMap[T] extends CustomEvent<Record<PropertyKey, never>>
|
|
? CustomEventInit<GlobalEventHandlersEventMap[T]['detail']>
|
|
: Partial<GlobalEventHandlersEventMap[T]['detail']> extends GlobalEventHandlersEventMap[T]['detail']
|
|
? CustomEventInit<GlobalEventHandlersEventMap[T]['detail']>
|
|
: WithRequired<CustomEventInit<GlobalEventHandlersEventMap[T]['detail']>, 'detail'>
|
|
: CustomEventInit
|
|
: CustomEventInit;
|
|
|
|
// Given an event name string, get the type of the event
|
|
type GetCustomEventType<T> = T extends keyof GlobalEventHandlersEventMap
|
|
? GlobalEventHandlersEventMap[T] extends CustomEvent<unknown>
|
|
? GlobalEventHandlersEventMap[T]
|
|
: CustomEvent<unknown>
|
|
: CustomEvent<unknown>;
|
|
|
|
// `keyof ValidEventTypeMap` is equivalent to `keyof GlobalEventHandlersEventMap` but gives a nicer error message
|
|
type ValidEventTypeMap = EventTypesWithRequiredDetail | EventTypesWithoutRequiredDetail;
|
|
|
|
export default class ShoelaceElement extends LitElement {
|
|
// Make localization attributes reactive
|
|
@property() dir: string;
|
|
@property() lang: string;
|
|
|
|
/** Emits a custom event with more convenient defaults. */
|
|
emit<T extends string & keyof EventTypesWithoutRequiredDetail>(
|
|
name: EventTypeDoesNotRequireDetail<T>,
|
|
options?: SlEventInit<T> | undefined
|
|
): GetCustomEventType<T>;
|
|
emit<T extends string & keyof EventTypesWithRequiredDetail>(
|
|
name: EventTypeRequiresDetail<T>,
|
|
options: SlEventInit<T>
|
|
): GetCustomEventType<T>;
|
|
emit<T extends string & keyof ValidEventTypeMap>(
|
|
name: T,
|
|
options?: SlEventInit<T> | undefined
|
|
): GetCustomEventType<T> {
|
|
const event = new CustomEvent(name, {
|
|
bubbles: true,
|
|
cancelable: false,
|
|
composed: true,
|
|
detail: {},
|
|
...options
|
|
});
|
|
|
|
this.dispatchEvent(event);
|
|
|
|
return event as GetCustomEventType<T>;
|
|
}
|
|
|
|
/* eslint-disable */
|
|
// @ts-expect-error This is auto-injected at build time.
|
|
static version = __SHOELACE_VERSION__;
|
|
/* eslint-enable */
|
|
|
|
static define(name: string, elementConstructor = this, options: ElementDefinitionOptions = {}) {
|
|
const currentlyRegisteredConstructor = customElements.get(name) as
|
|
| CustomElementConstructor
|
|
| typeof ShoelaceElement;
|
|
|
|
if (!currentlyRegisteredConstructor) {
|
|
customElements.define(name, class extends elementConstructor {} as unknown as CustomElementConstructor, options);
|
|
return;
|
|
}
|
|
|
|
let newVersion = ' (unknown version)';
|
|
let existingVersion = newVersion;
|
|
|
|
if ('version' in elementConstructor && elementConstructor.version) {
|
|
newVersion = ' v' + elementConstructor.version;
|
|
}
|
|
|
|
if ('version' in currentlyRegisteredConstructor && currentlyRegisteredConstructor.version) {
|
|
existingVersion = ' v' + currentlyRegisteredConstructor.version;
|
|
}
|
|
|
|
// Need to make sure we're not working with null or empty strings before doing version comparisons.
|
|
if (newVersion && existingVersion && newVersion === existingVersion) {
|
|
// If versions match, we don't need to warn anyone. Carry on.
|
|
return;
|
|
}
|
|
|
|
console.warn(
|
|
`Attempted to register <${name}>${newVersion}, but <${name}>${existingVersion} has already been registered.`
|
|
);
|
|
}
|
|
|
|
static dependencies: Record<string, typeof ShoelaceElement> = {};
|
|
|
|
constructor() {
|
|
super();
|
|
Object.entries((this.constructor as typeof ShoelaceElement).dependencies).forEach(([name, component]) => {
|
|
(this.constructor as typeof ShoelaceElement).define(name, component);
|
|
});
|
|
}
|
|
}
|
|
|
|
export interface ShoelaceFormControl extends ShoelaceElement {
|
|
// Form attributes
|
|
name: string;
|
|
value: unknown;
|
|
disabled?: boolean;
|
|
defaultValue?: unknown;
|
|
defaultChecked?: boolean;
|
|
form?: string;
|
|
|
|
// Constraint validation attributes
|
|
pattern?: string;
|
|
min?: number | string | Date;
|
|
max?: number | string | Date;
|
|
step?: number | 'any';
|
|
required?: boolean;
|
|
minlength?: number;
|
|
maxlength?: number;
|
|
|
|
// Form validation properties
|
|
readonly validity: ValidityState;
|
|
readonly validationMessage: string;
|
|
|
|
// Form validation methods
|
|
checkValidity: () => boolean;
|
|
getForm: () => HTMLFormElement | null;
|
|
reportValidity: () => boolean;
|
|
setCustomValidity: (message: string) => void;
|
|
}
|