web: Fix TypeScript compilation issues for mixins, events. (#13766)
This commit is contained in:
@ -19,7 +19,7 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
|
||||
@customElement("ak-about-modal")
|
||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
|
||||
static get styles() {
|
||||
return super.styles.concat(
|
||||
return ModalButton.styles.concat(
|
||||
PFAbout,
|
||||
css`
|
||||
.pf-c-about-modal-box__hero {
|
||||
|
@ -71,7 +71,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFCard, applicationListStyle);
|
||||
return TablePage.styles.concat(PFCard, applicationListStyle);
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
|
@ -127,7 +127,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
me?: SessionUser;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFDescriptionList, PFAlert, PFBanner);
|
||||
return Table.styles.concat(PFDescriptionList, PFAlert, PFBanner);
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||
|
@ -118,7 +118,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
||||
me?: SessionUser;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [...super.styles, PFDescriptionList, PFCard, PFAlert, recoveryButtonStyles];
|
||||
return [...TablePage.styles, PFDescriptionList, PFCard, PFAlert, recoveryButtonStyles];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -1,18 +1,44 @@
|
||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { Constructor } from "@goauthentik/elements/types.js";
|
||||
import { createMixin } from "@goauthentik/elements/types";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import type { Config } from "@goauthentik/api";
|
||||
|
||||
export function WithAuthentikConfig<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
) {
|
||||
abstract class WithAkConfigProvider extends superclass {
|
||||
@consume({ context: authentikConfigContext, subscribe })
|
||||
public authentikConfig!: Config;
|
||||
}
|
||||
return WithAkConfigProvider;
|
||||
/**
|
||||
* A consumer that provides the application configuration to the element.
|
||||
*/
|
||||
export interface AKConfigMixin {
|
||||
/**
|
||||
* The current configuration of the application.
|
||||
*/
|
||||
readonly authentikConfig: Readonly<Config>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin that provides the application configuration to the element.
|
||||
*
|
||||
* @category Mixin
|
||||
*/
|
||||
export const WithAuthentikConfig = createMixin<AKConfigMixin>(
|
||||
({
|
||||
/**
|
||||
* The superclass constructor to extend.
|
||||
*/
|
||||
SuperClass,
|
||||
/**
|
||||
* Whether or not to subscribe to the context.
|
||||
*/
|
||||
subscribe = true,
|
||||
}) => {
|
||||
abstract class AKConfigProvider extends SuperClass implements AKConfigMixin {
|
||||
@consume({
|
||||
context: authentikConfigContext,
|
||||
subscribe,
|
||||
})
|
||||
public readonly authentikConfig!: Readonly<Config>;
|
||||
}
|
||||
|
||||
return AKConfigProvider;
|
||||
},
|
||||
);
|
||||
|
@ -1,20 +1,48 @@
|
||||
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||
import { createMixin } from "@goauthentik/elements/types";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import type { CurrentBrand } from "@goauthentik/api";
|
||||
|
||||
export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
) {
|
||||
abstract class WithBrandProvider extends superclass {
|
||||
@consume({ context: authentikBrandContext, subscribe })
|
||||
@state()
|
||||
public brand!: CurrentBrand;
|
||||
}
|
||||
return WithBrandProvider;
|
||||
/**
|
||||
* A mixin that provides the current brand to the element.
|
||||
*/
|
||||
export interface StyleBrandMixin {
|
||||
/**
|
||||
* The current style branding configuration.
|
||||
*/
|
||||
brand: CurrentBrand;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin that provides the current brand to the element.
|
||||
*
|
||||
* @category Mixin
|
||||
*
|
||||
* @see {@link https://lit.dev/docs/composition/mixins/#mixins-in-typescript | Lit Mixins}
|
||||
*/
|
||||
export const WithBrandConfig = createMixin<StyleBrandMixin>(
|
||||
({
|
||||
/**
|
||||
* The superclass constructor to extend.
|
||||
*/
|
||||
SuperClass,
|
||||
/**
|
||||
* Whether or not to subscribe to the context.
|
||||
*/
|
||||
subscribe = true,
|
||||
}) => {
|
||||
abstract class StyleBrandProvider extends SuperClass implements StyleBrandMixin {
|
||||
@consume({
|
||||
context: authentikBrandContext,
|
||||
subscribe,
|
||||
})
|
||||
@state()
|
||||
public brand!: CurrentBrand;
|
||||
}
|
||||
|
||||
return StyleBrandProvider;
|
||||
},
|
||||
);
|
||||
|
@ -1,67 +1,85 @@
|
||||
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||
import { createMixin } from "@goauthentik/elements/types";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import { CapabilitiesEnum } from "@goauthentik/api";
|
||||
import { Config } from "@goauthentik/api";
|
||||
|
||||
// Using a unique, lexically scoped, and locally static symbol as the field name for the context
|
||||
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
|
||||
// guarantees in JavaScript.
|
||||
|
||||
class WCC {
|
||||
public static readonly capabilitiesConfig: unique symbol = Symbol();
|
||||
/**
|
||||
* A consumer that provides the capability methods to the element.
|
||||
*
|
||||
*/
|
||||
export interface CapabilitiesMixin {
|
||||
/**
|
||||
* Predicate to determine if the current user has a given capability.
|
||||
*/
|
||||
can(
|
||||
/**
|
||||
* The capability enum to check.
|
||||
*/
|
||||
capability: CapabilitiesEnum,
|
||||
): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* withCapabilitiesContext mixes in a single method to any LitElement, `can()`, which takes a
|
||||
* CapabilitiesEnum and returns true or false.
|
||||
* Lexically-scoped symbol for the capabilities configuration.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const kCapabilitiesConfig: unique symbol = Symbol("capabilitiesConfig");
|
||||
|
||||
/**
|
||||
* A mixin that provides the capability methods to the element.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* After importing, simply mixin this function:
|
||||
*
|
||||
* ```
|
||||
* ```ts
|
||||
* export class AkMyNiftyNewFeature extends withCapabilitiesContext(AKElement) {
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* And then if you need to check on a capability:
|
||||
*
|
||||
* ```
|
||||
* ```ts
|
||||
* if (this.can(CapabilitiesEnum.IsEnterprise) { ... }
|
||||
* ```
|
||||
*
|
||||
* This code re-exports CapabilitiesEnum, so you won't have to import it on a separate line if you
|
||||
* don't need anything else from the API.
|
||||
*
|
||||
* Passing `true` as the second mixin argument will cause the inheriting class to subscribe to the
|
||||
* configuration context. Should the context be explicitly reset, all active web components that are
|
||||
* currently active and subscribed to the context will automatically have a `requestUpdate()`
|
||||
* triggered with the new configuration.
|
||||
* Passing `true` as the second mixin argument
|
||||
*
|
||||
* @category Mixin
|
||||
*
|
||||
*/
|
||||
export const WithCapabilitiesConfig = createMixin<CapabilitiesMixin>(
|
||||
({ SuperClass, subscribe = true }) => {
|
||||
abstract class CapabilitiesProvider extends SuperClass implements CapabilitiesMixin {
|
||||
@consume({
|
||||
context: authentikConfigContext,
|
||||
subscribe,
|
||||
})
|
||||
private readonly [kCapabilitiesConfig]: Config | undefined;
|
||||
|
||||
export function WithCapabilitiesConfig<T extends AbstractConstructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
) {
|
||||
abstract class CapabilitiesContext extends superclass {
|
||||
@consume({ context: authentikConfigContext, subscribe })
|
||||
private [WCC.capabilitiesConfig]!: Config;
|
||||
public can(capability: CapabilitiesEnum) {
|
||||
const config = this[kCapabilitiesConfig];
|
||||
|
||||
can(c: CapabilitiesEnum) {
|
||||
if (!this[WCC.capabilitiesConfig]) {
|
||||
throw new Error(
|
||||
"ConfigContext: Attempted to access site configuration before initialization.",
|
||||
);
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
"ConfigContext: Attempted to access site configuration before initialization.",
|
||||
);
|
||||
}
|
||||
|
||||
return config.capabilities.includes(capability);
|
||||
}
|
||||
return this[WCC.capabilitiesConfig].capabilities.includes(c);
|
||||
}
|
||||
}
|
||||
|
||||
return CapabilitiesContext;
|
||||
}
|
||||
return CapabilitiesProvider;
|
||||
},
|
||||
);
|
||||
|
||||
// Re-export `CapabilitiesEnum`, so you won't have to import it on a separate line if you
|
||||
// don't need anything else from the API.
|
||||
|
||||
export { CapabilitiesEnum };
|
||||
|
@ -1,23 +1,40 @@
|
||||
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import { Constructor } from "@goauthentik/elements/types.js";
|
||||
import { createMixin } from "@goauthentik/elements/types";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import { type LicenseSummary, LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
export function WithLicenseSummary<T extends Constructor<LitElement>>(
|
||||
superclass: T,
|
||||
subscribe = true,
|
||||
) {
|
||||
abstract class WithEnterpriseProvider extends superclass {
|
||||
@consume({ context: authentikEnterpriseContext, subscribe })
|
||||
public licenseSummary!: LicenseSummary;
|
||||
/**
|
||||
* A consumer that provides license information to the element.
|
||||
*/
|
||||
export interface LicenseMixin {
|
||||
/**
|
||||
* Summary of the current license.
|
||||
*/
|
||||
readonly licenseSummary: LicenseSummary;
|
||||
|
||||
/**
|
||||
* Whether or not the current license is an enterprise license.
|
||||
*/
|
||||
readonly hasEnterpriseLicense: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin that provides the license information to the element.
|
||||
*/
|
||||
export const WithLicenseSummary = createMixin<LicenseMixin>(({ SuperClass, subscribe = true }) => {
|
||||
abstract class LicenseProvider extends SuperClass implements LicenseMixin {
|
||||
@consume({
|
||||
context: authentikEnterpriseContext,
|
||||
subscribe,
|
||||
})
|
||||
public readonly licenseSummary!: LicenseSummary;
|
||||
|
||||
get hasEnterpriseLicense() {
|
||||
return this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed;
|
||||
}
|
||||
}
|
||||
|
||||
return WithEnterpriseProvider;
|
||||
}
|
||||
return LicenseProvider;
|
||||
});
|
||||
|
@ -1,18 +1,35 @@
|
||||
import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts";
|
||||
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import { Constructor } from "@lit/reactive-element/decorators/base";
|
||||
import type { LitElement } from "lit";
|
||||
|
||||
import type { Version } from "@goauthentik/api";
|
||||
|
||||
export function WithVersion<T extends AbstractConstructor<LitElement>>(
|
||||
superclass: T,
|
||||
/**
|
||||
* A consumer that provides version information to the element.
|
||||
*/
|
||||
export declare class VersionConsumer {
|
||||
/**
|
||||
* The current version of the application.
|
||||
*/
|
||||
public readonly version: Version;
|
||||
}
|
||||
|
||||
export function WithVersion<T extends Constructor<LitElement>>(
|
||||
/**
|
||||
* The superclass constructor to extend.
|
||||
*/
|
||||
SuperClass: T,
|
||||
/**
|
||||
* Whether or not to subscribe to the context.
|
||||
*/
|
||||
subscribe = true,
|
||||
) {
|
||||
abstract class WithBrandProvider extends superclass {
|
||||
class VersionProvider extends SuperClass {
|
||||
@consume({ context: authentikVersionContext, subscribe })
|
||||
public version!: Version;
|
||||
}
|
||||
return WithBrandProvider;
|
||||
|
||||
return VersionProvider as Constructor<VersionConsumer> & T;
|
||||
}
|
||||
|
@ -30,9 +30,9 @@ export type DataProvision = {
|
||||
|
||||
export type DataProvider = (page: number, search?: string) => Promise<DataProvision>;
|
||||
|
||||
export interface SearchbarEvent extends CustomEvent {
|
||||
detail: {
|
||||
source: string;
|
||||
value: string;
|
||||
};
|
||||
export interface SearchbarEventDetail {
|
||||
source: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type SearchbarEvent = CustomEvent<SearchbarEventDetail>;
|
||||
|
@ -1,41 +1,107 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, nothing } from "lit";
|
||||
import { ReactiveControllerHost } from "lit";
|
||||
import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit";
|
||||
import "lit";
|
||||
|
||||
export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type Constructor<T = object> = new (...args: any[]) => T;
|
||||
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;
|
||||
export type LitElementConstructor = new (...args: never[]) => LitElement;
|
||||
|
||||
// authentik Search/List types
|
||||
//
|
||||
// authentik's list types (ak-dual-select, ak-list-select, ak-search-select) all take a tuple of two
|
||||
// or three items, or a collection of groups of such tuples. In order to push dynamic checking
|
||||
// around, we also allow the inclusion of a fourth component, which is just a scratchpad the
|
||||
// developer can use for their own reasons.
|
||||
/**
|
||||
* A constructor that has been extended with a mixin.
|
||||
*/
|
||||
export type ConstructorWithMixin<SuperClass, Mixin> =
|
||||
// Is the superclass abstract?
|
||||
SuperClass extends abstract new (...args: never[]) => unknown
|
||||
? // Lift the abstractness to of the mixin.
|
||||
new (...args: ConstructorParameters<SuperClass>) => InstanceType<SuperClass> & Mixin
|
||||
: // Is the superclass **not** abstract?
|
||||
SuperClass extends new (...args: never[]) => unknown
|
||||
? // So shall be the mixin.
|
||||
new (...args: ConstructorParameters<SuperClass>) => InstanceType<SuperClass> & Mixin
|
||||
: never;
|
||||
|
||||
// The displayed element for our list can be a TemplateResult. If it is, we *strongly* recommend
|
||||
// that you include the `sortBy` string as well, which is used for sorting but is also used for our
|
||||
// autocomplete element (ak-search-select) both for tracking the user's input and for what we
|
||||
// display in the autocomplete input box.
|
||||
/**
|
||||
* The init object passed to the `createMixin` callback.
|
||||
*/
|
||||
export interface CreateMixinInit<T extends LitElementConstructor = LitElementConstructor> {
|
||||
/**
|
||||
* The superclass constructor to extend.
|
||||
*/
|
||||
SuperClass: T;
|
||||
/**
|
||||
* Whether or not to subscribe to the context.
|
||||
*
|
||||
* Should the context be explicitly reset, all active web components that are
|
||||
* currently active and subscribed to the context will automatically have a `requestUpdate()`
|
||||
* triggered with the new configuration.
|
||||
*/
|
||||
subscribe?: boolean;
|
||||
}
|
||||
|
||||
// - key: string
|
||||
// - label (string). This is the field that will be sorted and used for filtering and searching.
|
||||
// - desc (optional) A string or TemplateResult used to describe the option.
|
||||
// - localMapping: The object the key represents; used by some specific apps. API layers may use
|
||||
// this as a way to find the referenced object, rather than the string and keeping a local map.
|
||||
//
|
||||
// Note that this is a *tuple*, not a record or map!
|
||||
/**
|
||||
* Create a mixin for a LitElement.
|
||||
*
|
||||
* @param mixinCallback The callback that will be called to create the mixin.
|
||||
* @template Mixin The mixin class to union with the superclass.
|
||||
*/
|
||||
export function createMixin<Mixin>(mixinCallback: (init: CreateMixinInit) => unknown) {
|
||||
return <T extends LitElementConstructor | AbstractLitElementConstructor>(
|
||||
/**
|
||||
* The superclass constructor to extend.
|
||||
*/ SuperClass: T,
|
||||
/**
|
||||
* Whether or not to subscribe to the context.
|
||||
*
|
||||
* Should the context be explicitly reset, all active web components that are
|
||||
* currently active and subscribed to the context will automatically have a `requestUpdate()`
|
||||
* triggered with the new configuration.
|
||||
*/
|
||||
subscribe?: boolean,
|
||||
) => {
|
||||
const MixinClass = mixinCallback({
|
||||
SuperClass: SuperClass as LitElementConstructor,
|
||||
subscribe,
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
return MixinClass as ConstructorWithMixin<T, Mixin>;
|
||||
};
|
||||
}
|
||||
|
||||
//#region Search/List types
|
||||
|
||||
/**
|
||||
* authentik's list types (ak-dual-select, ak-list-select, ak-search-select) all take a tuple of two
|
||||
* or three items, or a collection of groups of such tuples. In order to push dynamic checking
|
||||
* around, we also allow the inclusion of a fourth component, which is just a scratchpad the
|
||||
* developer can use for their own reasons.
|
||||
*
|
||||
* The displayed element for our list can be a TemplateResult.
|
||||
*
|
||||
* If it is, we *strongly* recommend that you include the `sortBy` string as well, which is used for sorting but is also used for our autocomplete element (ak-search-select),
|
||||
* both for tracking the user's input and for what we display in the autocomplete input box.
|
||||
*
|
||||
* Note that this is a *tuple*, not a record or map!
|
||||
*/
|
||||
export type SelectOption<T = never> = [
|
||||
/**
|
||||
* The key that will be used for sorting and filtering.
|
||||
*/
|
||||
key: string,
|
||||
/**
|
||||
* The field that will be sorted and used for filtering and searching.
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* A string or TemplateResult used to describe the option.
|
||||
*/
|
||||
desc?: string | TemplateResult,
|
||||
/**
|
||||
* The object the key represents; used by some specific apps. API layers may use
|
||||
* this as a way to find the referenced object, rather than the string and keeping a local map.
|
||||
*/
|
||||
localMapping?: T,
|
||||
];
|
||||
|
||||
@ -44,8 +110,8 @@ export type SelectOption<T = never> = [
|
||||
* `grouped: false` flag. Note that it *is* possible to pass to any of the rendering components an
|
||||
* array of SelectTuples; they will be automatically mapped to a SelectFlat object.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
/* PRIVATE */
|
||||
export type SelectFlat<T = never> = {
|
||||
grouped: false;
|
||||
options: SelectOption<T>[];
|
||||
@ -74,5 +140,14 @@ export type SelectGrouped<T = never> = {
|
||||
export type GroupedOptions<T = never> = SelectGrouped<T> | SelectFlat<T>;
|
||||
export type SelectOptions<T = never> = SelectOption<T>[] | GroupedOptions<T>;
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* A convenience type representing the result of a slotted template, i.e.
|
||||
*
|
||||
* - A string, which will be rendered as text.
|
||||
* - A TemplateResult, which will be rendered as HTML.
|
||||
* - `nothing`, which will not be rendered.
|
||||
*/
|
||||
export type SlottedTemplateResult = string | TemplateResult | typeof nothing;
|
||||
export type Spread = { [key: string]: unknown };
|
||||
|
@ -5,9 +5,15 @@ export const customEvent = (name: string, details = {}) =>
|
||||
detail: details,
|
||||
});
|
||||
|
||||
// "Unknown" seems to violate some obscure Typescript rule and doesn't work here, although it
|
||||
// should.
|
||||
//
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isCustomEvent = (v: any): v is CustomEvent =>
|
||||
v instanceof CustomEvent && "detail" in v;
|
||||
export type SerializablePrimitive = number | string;
|
||||
export type SerializableArray = SerializablePrimitive[];
|
||||
export type CustomEventDetail = SerializablePrimitive | SerializableArray | object;
|
||||
|
||||
/**
|
||||
* Type guard to determine if an event has a `detail` property.
|
||||
*/
|
||||
export function isCustomEvent<D = CustomEventDetail>(
|
||||
eventLike: Event,
|
||||
): eventLike is CustomEvent<D> {
|
||||
return eventLike instanceof CustomEvent && "detail" in eventLike;
|
||||
}
|
||||
|
@ -1,38 +1,41 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { createMixin } from "@goauthentik/elements/types";
|
||||
import { CustomEventDetail, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Constructor<T = object> = new (...args: any[]) => T;
|
||||
export interface EmmiterElementHandler {
|
||||
dispatchCustomEvent<T>(
|
||||
eventName: string,
|
||||
detail?: T extends CustomEvent<infer U> ? U : T,
|
||||
eventInit?: EventInit,
|
||||
): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isCustomEvent = (v: any): v is CustomEvent =>
|
||||
v instanceof CustomEvent && "detail" in v;
|
||||
|
||||
export function CustomEmitterElement<T extends Constructor<LitElement>>(superclass: T) {
|
||||
return class EmmiterElementHandler extends superclass {
|
||||
dispatchCustomEvent<F extends CustomEvent>(
|
||||
export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperClass }) => {
|
||||
return class EmmiterElementHandler extends SuperClass {
|
||||
public dispatchCustomEvent<D extends CustomEventDetail>(
|
||||
eventName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
detail: any = {},
|
||||
options = {},
|
||||
detail: D = {} as D,
|
||||
eventInit: EventInit = {},
|
||||
) {
|
||||
const fullDetail =
|
||||
typeof detail === "object" && !Array.isArray(detail)
|
||||
? {
|
||||
...detail,
|
||||
}
|
||||
: detail;
|
||||
let normalizedDetail: CustomEventDetail;
|
||||
|
||||
if (detail && typeof detail === "object" && !Array.isArray(detail)) {
|
||||
// TODO: Is this destructuring still necessary to shallow copy the object?
|
||||
normalizedDetail = { ...detail };
|
||||
} else {
|
||||
normalizedDetail = detail;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(eventName, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
...options,
|
||||
detail: fullDetail,
|
||||
}) as F,
|
||||
...eventInit,
|
||||
detail: normalizedDetail,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mixin that enables Lit Elements to handle custom events in a more straightforward manner.
|
||||
@ -56,8 +59,19 @@ class HK {
|
||||
type EventHandler = (ev: CustomEvent) => void;
|
||||
type EventMap = WeakMap<EventHandler, EventHandler>;
|
||||
|
||||
export function CustomListenerElement<T extends Constructor<LitElement>>(superclass: T) {
|
||||
return class ListenerElementHandler extends superclass {
|
||||
export interface CustomEventTarget {
|
||||
addCustomListener(eventName: string, handler: EventHandler): void;
|
||||
removeCustomListener(eventName: string, handler: EventHandler): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin that enables Lit Elements to handle custom events in a more straightforward manner.
|
||||
*
|
||||
* @todo Can we lean on the native `EventTarget` class for this?
|
||||
* @category Mixin
|
||||
*/
|
||||
export const CustomListenerElement = createMixin<CustomEventTarget>(({ SuperClass }) => {
|
||||
return class ListenerElementHandler extends SuperClass implements CustomEventTarget {
|
||||
private [HK.listenHandlers] = new Map<string, EventMap>();
|
||||
|
||||
private [HK.getHandler](eventName: string, handler: EventHandler) {
|
||||
@ -93,14 +107,14 @@ export function CustomListenerElement<T extends Constructor<LitElement>>(supercl
|
||||
}
|
||||
|
||||
addCustomListener(eventName: string, handler: EventHandler) {
|
||||
const internalHandler = (ev: Event) => {
|
||||
if (!isCustomEvent(ev)) {
|
||||
const internalHandler = (event: Event) => {
|
||||
if (!isCustomEvent(event)) {
|
||||
console.error(
|
||||
`Received a standard event for custom event ${eventName}; event will not be handled.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
handler(ev);
|
||||
handler(event);
|
||||
};
|
||||
this[HK.addHandler](eventName, handler, internalHandler);
|
||||
this.addEventListener(eventName, internalHandler);
|
||||
@ -117,4 +131,4 @@ export function CustomListenerElement<T extends Constructor<LitElement>>(supercl
|
||||
this[HK.removeHandler](eventName, handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user