web/admin: Dual select state management, custom event dispatching. (#14490)

* web/admin: Fix issues surrounding dual select state management.

* web: Fix nested path.

* web: Use PatternFly variable.
This commit is contained in:
Teffen Ellis
2025-05-15 14:47:47 +02:00
committed by GitHub
parent 7440900dac
commit e40c5ac617
21 changed files with 671 additions and 581 deletions

View File

@ -1,18 +1,28 @@
import { createMixin } from "@goauthentik/elements/types";
import {
ConstructorWithMixin,
LitElementConstructor,
createMixin,
} from "@goauthentik/elements/types";
import { CustomEventDetail, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
export interface EmmiterElementHandler {
dispatchCustomEvent<T>(
eventName: string,
detail?: T extends CustomEvent<infer U> ? U : T,
export interface CustomEventEmitterMixin<EventType extends string = string> {
dispatchCustomEvent<D extends CustomEventDetail>(
eventType: EventType,
detail?: D,
eventInit?: EventInit,
): void;
}
export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperClass }) => {
return class EmmiterElementHandler extends SuperClass {
export function CustomEmitterElement<
EventType extends string = string,
T extends LitElementConstructor = LitElementConstructor,
>(SuperClass: T) {
abstract class CustomEventEmmiter
extends SuperClass
implements CustomEventEmitterMixin<EventType>
{
public dispatchCustomEvent<D extends CustomEventDetail>(
eventName: string,
eventType: string,
detail: D = {} as D,
eventInit: EventInit = {},
) {
@ -26,7 +36,7 @@ export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperC
}
this.dispatchEvent(
new CustomEvent(eventName, {
new CustomEvent(eventType, {
composed: true,
bubbles: true,
...eventInit,
@ -34,34 +44,20 @@ export const CustomEmitterElement = createMixin<EmmiterElementHandler>(({ SuperC
}),
);
}
};
});
}
/**
* Mixin that enables Lit Elements to handle custom events in a more straightforward manner.
*
*/
// This is a neat trick: this static "class" is just a namespace for these unique symbols. Because
// of all the constraints on them, they're legal field names in Typescript objects! Which means that
// we can use them as identifiers for internal references in a Typescript class with absolutely no
// risk that a future user who wants a name like 'addHandler' or 'removeHandler' will override any
// of those, either in this mixin or in any class that this is mixed into, past or present along the
// chain of inheritance.
class HK {
public static readonly listenHandlers: unique symbol = Symbol();
public static readonly addHandler: unique symbol = Symbol();
public static readonly removeHandler: unique symbol = Symbol();
public static readonly getHandler: unique symbol = Symbol();
return CustomEventEmmiter as unknown as ConstructorWithMixin<
T,
CustomEventEmitterMixin<EventType>
>;
}
type EventHandler = (ev: CustomEvent) => void;
type EventMap = WeakMap<EventHandler, EventHandler>;
type CustomEventListener<D = unknown> = (ev: CustomEvent<D>) => void;
type EventMap<D = unknown> = WeakMap<CustomEventListener<D>, CustomEventListener<D>>;
export interface CustomEventTarget {
addCustomListener(eventName: string, handler: EventHandler): void;
removeCustomListener(eventName: string, handler: EventHandler): void;
export interface CustomEventTarget<EventType extends string = string> {
addCustomListener<D = unknown>(eventType: EventType, handler: CustomEventListener<D>): void;
removeCustomListener<D = unknown>(eventType: EventType, handler: CustomEventListener<D>): void;
}
/**
@ -72,11 +68,15 @@ export interface CustomEventTarget {
*/
export const CustomListenerElement = createMixin<CustomEventTarget>(({ SuperClass }) => {
return class ListenerElementHandler extends SuperClass implements CustomEventTarget {
private [HK.listenHandlers] = new Map<string, EventMap>();
#listenHandlers = new Map<string, EventMap>();
private [HK.getHandler](eventName: string, handler: EventHandler) {
const internalMap = this[HK.listenHandlers].get(eventName);
return internalMap ? internalMap.get(handler) : undefined;
#getListener<D = unknown>(
eventType: string,
handler: CustomEventListener<D>,
): CustomEventListener<D> | undefined {
const internalMap = this.#listenHandlers.get(eventType) as EventMap<D> | undefined;
return internalMap?.get(handler);
}
// For every event NAME, we create a WeakMap that pairs the event handler given to us by the
@ -85,50 +85,58 @@ export const CustomListenerElement = createMixin<CustomEventTarget>(({ SuperClas
// meanwhile, this allows us to remove it from the event listeners if it's still around
// using the original handler's identity as the key.
//
private [HK.addHandler](
eventName: string,
handler: EventHandler,
internalHandler: EventHandler,
#addListener<D = unknown>(
eventType: string,
handler: CustomEventListener<D>,
internalHandler: CustomEventListener<D>,
) {
if (!this[HK.listenHandlers].has(eventName)) {
this[HK.listenHandlers].set(eventName, new WeakMap());
let internalMap = this.#listenHandlers.get(eventType) as EventMap<D> | undefined;
if (!internalMap) {
internalMap = new WeakMap();
this.#listenHandlers.set(eventType, internalMap as EventMap);
}
const internalMap = this[HK.listenHandlers].get(eventName);
internalMap.set(handler, internalHandler);
}
#removeListener<D = unknown>(eventType: string, listener: CustomEventListener<D>) {
const internalMap = this.#listenHandlers.get(eventType) as EventMap<D> | undefined;
if (internalMap) {
internalMap.set(handler, internalHandler);
internalMap.delete(listener);
}
}
private [HK.removeHandler](eventName: string, handler: EventHandler) {
const internalMap = this[HK.listenHandlers].get(eventName);
if (internalMap) {
internalMap.delete(handler);
}
}
addCustomListener(eventName: string, handler: EventHandler) {
addCustomListener<D = unknown>(eventType: string, listener: CustomEventListener<D>) {
const internalHandler = (event: Event) => {
if (!isCustomEvent(event)) {
if (!isCustomEvent<D>(event)) {
console.error(
`Received a standard event for custom event ${eventName}; event will not be handled.`,
`Received a standard event for custom event ${eventType}; event will not be handled.`,
);
return;
return null;
}
handler(event);
return listener(event);
};
this[HK.addHandler](eventName, handler, internalHandler);
this.addEventListener(eventName, internalHandler);
this.#addListener(eventType, listener, internalHandler);
this.addEventListener(eventType, internalHandler);
}
removeCustomListener(eventName: string, handler: EventHandler) {
const realHandler = this[HK.getHandler](eventName, handler);
removeCustomListener<D = unknown>(eventType: string, listener: CustomEventListener<D>) {
const realHandler = this.#getListener(eventType, listener);
if (realHandler) {
this.removeEventListener(
eventName,
eventType,
realHandler as EventListenerOrEventListenerObject,
);
}
this[HK.removeHandler](eventName, handler);
this.#removeListener<D>(eventType, listener);
}
};
});