Move everything under the login dir
This commit is contained in:
114
src/login/lib/keycloakJsAdapter.ts
Normal file
114
src/login/lib/keycloakJsAdapter.ts
Normal file
@ -0,0 +1,114 @@
|
||||
export declare namespace keycloak_js {
|
||||
export type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||
export class KeycloakPromise<TSuccess, TError> extends Promise<TSuccess> {
|
||||
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
||||
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
||||
}
|
||||
export interface KeycloakAdapter {
|
||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
logout(options?: KeycloakLogoutOptions): KeycloakPromise<void, void>;
|
||||
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
accountManagement(): KeycloakPromise<void, void>;
|
||||
redirectUri(options: { redirectUri: string }, encodeHash: boolean): string;
|
||||
}
|
||||
export interface KeycloakLogoutOptions {
|
||||
redirectUri?: string;
|
||||
}
|
||||
export interface KeycloakLoginOptions {
|
||||
scope?: string;
|
||||
redirectUri?: string;
|
||||
prompt?: "none" | "login";
|
||||
action?: string;
|
||||
maxAge?: number;
|
||||
loginHint?: string;
|
||||
idpHint?: string;
|
||||
locale?: string;
|
||||
cordovaOptions?: { [optionName: string]: string };
|
||||
}
|
||||
|
||||
export type KeycloakInstance = Record<
|
||||
"createLoginUrl" | "createLogoutUrl" | "createRegisterUrl",
|
||||
(options: KeycloakLoginOptions | undefined) => string
|
||||
> & {
|
||||
createAccountUrl(): string;
|
||||
redirectUri?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
|
||||
* The goal here is just to be able to inject search param in url before keycloak redirect.
|
||||
* Our use case for it is to pass over the login screen the states of useGlobalState
|
||||
* namely isDarkModeEnabled, lgn...
|
||||
*/
|
||||
export function createKeycloakAdapter(params: {
|
||||
keycloakInstance: keycloak_js.KeycloakInstance;
|
||||
transformUrlBeforeRedirect: (url: string) => string;
|
||||
getRedirectMethod?: () => "overwrite location.href" | "location.replace";
|
||||
}): keycloak_js.KeycloakAdapter {
|
||||
const { keycloakInstance, transformUrlBeforeRedirect, getRedirectMethod = () => "overwrite location.href" } = params;
|
||||
|
||||
const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> = Object.defineProperties(new Promise(() => {}), {
|
||||
"success": { "value": () => {} },
|
||||
"error": { "value": () => {} }
|
||||
}) as any;
|
||||
|
||||
return {
|
||||
"login": options => {
|
||||
const newHref = transformUrlBeforeRedirect(keycloakInstance.createLoginUrl(options));
|
||||
switch (getRedirectMethod()) {
|
||||
case "location.replace":
|
||||
window.location.replace(newHref);
|
||||
break;
|
||||
case "overwrite location.href":
|
||||
window.location.href = newHref;
|
||||
break;
|
||||
}
|
||||
return neverResolvingPromise;
|
||||
},
|
||||
"register": options => {
|
||||
const newHref = transformUrlBeforeRedirect(keycloakInstance.createRegisterUrl(options));
|
||||
switch (getRedirectMethod()) {
|
||||
case "location.replace":
|
||||
window.location.replace(newHref);
|
||||
break;
|
||||
case "overwrite location.href":
|
||||
window.location.href = newHref;
|
||||
break;
|
||||
}
|
||||
|
||||
return neverResolvingPromise;
|
||||
},
|
||||
"logout": options => {
|
||||
window.location.replace(transformUrlBeforeRedirect(keycloakInstance.createLogoutUrl(options)));
|
||||
return neverResolvingPromise;
|
||||
},
|
||||
"accountManagement": () => {
|
||||
const accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl());
|
||||
|
||||
if (accountUrl === "undefined") {
|
||||
throw new Error("Not supported by the OIDC server");
|
||||
}
|
||||
|
||||
switch (getRedirectMethod()) {
|
||||
case "location.replace":
|
||||
window.location.replace(accountUrl);
|
||||
break;
|
||||
case "overwrite location.href":
|
||||
window.location.href = accountUrl;
|
||||
break;
|
||||
}
|
||||
|
||||
return neverResolvingPromise;
|
||||
},
|
||||
"redirectUri": options => {
|
||||
if (options && options.redirectUri) {
|
||||
return options.redirectUri;
|
||||
} else if (keycloakInstance.redirectUri) {
|
||||
return keycloakInstance.redirectUri;
|
||||
} else {
|
||||
return window.location.href;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
49
src/login/lib/useDownloadTerms.ts
Normal file
49
src/login/lib/useDownloadTerms.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { useEffect } from "react";
|
||||
import { memoize } from "keycloakify/tools/memoize";
|
||||
import { fallbackLanguageTag } from "keycloakify/login/i18n/i18n";
|
||||
import { useConst } from "keycloakify/tools/useConst";
|
||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { Evt } from "evt";
|
||||
import { KcContext } from "../kcContext";
|
||||
|
||||
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
|
||||
|
||||
export type KcContextLike = {
|
||||
pageId: KcContext["pageId"];
|
||||
locale?: {
|
||||
currentLanguageTag: string;
|
||||
};
|
||||
};
|
||||
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
/** Allow to avoid bundling the terms and download it on demand*/
|
||||
export function useDownloadTerms(params: {
|
||||
kcContext: KcContextLike;
|
||||
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
|
||||
}) {
|
||||
const { kcContext } = params;
|
||||
|
||||
const { downloadTermMarkdownMemoized } = (function useClosure() {
|
||||
const { downloadTermMarkdown } = params;
|
||||
|
||||
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
|
||||
|
||||
const downloadTermMarkdownMemoized = useConst(() =>
|
||||
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }))
|
||||
);
|
||||
|
||||
return { downloadTermMarkdownMemoized };
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (kcContext.pageId !== "terms.ftl") {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
|
||||
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
|
||||
);
|
||||
}, []);
|
||||
}
|
483
src/login/lib/useFormValidation.tsx
Normal file
483
src/login/lib/useFormValidation.tsx
Normal file
@ -0,0 +1,483 @@
|
||||
import "keycloakify/tools/Array.prototype.every";
|
||||
import { useMemo, useReducer, Fragment } from "react";
|
||||
import { id } from "tsafe/id";
|
||||
import type { MessageKey } from "keycloakify/login/i18n/i18n";
|
||||
import type { Attribute, Validators } from "keycloakify/login/kcContext/KcContext";
|
||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||
import { emailRegexp } from "keycloakify/tools/emailRegExp";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
/**
|
||||
* NOTE: The attributesWithPassword returned is actually augmented with
|
||||
* artificial password related attributes only if kcContext.passwordRequired === true
|
||||
*/
|
||||
export function useFormValidation(params: {
|
||||
kcContext: {
|
||||
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
||||
profile: {
|
||||
attributes: Attribute[];
|
||||
};
|
||||
passwordRequired?: boolean;
|
||||
realm: { registrationEmailAsUsername: boolean };
|
||||
};
|
||||
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
||||
passwordValidators?: Validators;
|
||||
i18n: I18n;
|
||||
}) {
|
||||
const {
|
||||
kcContext,
|
||||
passwordValidators = {
|
||||
"length": {
|
||||
"ignore.empty.value": true,
|
||||
"min": "4"
|
||||
}
|
||||
},
|
||||
i18n
|
||||
} = params;
|
||||
|
||||
const attributesWithPassword = useMemo(
|
||||
() =>
|
||||
!kcContext.passwordRequired
|
||||
? kcContext.profile.attributes
|
||||
: (() => {
|
||||
const name = kcContext.realm.registrationEmailAsUsername ? "email" : "username";
|
||||
|
||||
return kcContext.profile.attributes.reduce<Attribute[]>(
|
||||
(prev, curr) => [
|
||||
...prev,
|
||||
...(curr.name !== name
|
||||
? [curr]
|
||||
: [
|
||||
curr,
|
||||
id<Attribute>({
|
||||
"name": "password",
|
||||
"displayName": id<`\${${MessageKey}}`>("${password}"),
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"validators": passwordValidators,
|
||||
"annotations": {},
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "new-password"
|
||||
}),
|
||||
id<Attribute>({
|
||||
"name": "password-confirm",
|
||||
"displayName": id<`\${${MessageKey}}`>("${passwordConfirm}"),
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"validators": {
|
||||
"_compareToOther": {
|
||||
"name": "password",
|
||||
"ignore.empty.value": true,
|
||||
"shouldBe": "equal",
|
||||
"error-message": id<`\${${MessageKey}}`>("${invalidPasswordConfirmMessage}")
|
||||
}
|
||||
},
|
||||
"annotations": {},
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "new-password"
|
||||
})
|
||||
])
|
||||
],
|
||||
[]
|
||||
);
|
||||
})(),
|
||||
[kcContext, passwordValidators]
|
||||
);
|
||||
|
||||
const { getErrors } = useGetErrors({
|
||||
"kcContext": {
|
||||
"messagesPerField": kcContext.messagesPerField,
|
||||
"profile": {
|
||||
"attributes": attributesWithPassword
|
||||
}
|
||||
},
|
||||
i18n
|
||||
});
|
||||
|
||||
const initialInternalState = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
attributesWithPassword
|
||||
.map(attribute => ({
|
||||
attribute,
|
||||
"errors": getErrors({
|
||||
"name": attribute.name,
|
||||
"fieldValueByAttributeName": Object.fromEntries(
|
||||
attributesWithPassword.map(({ name, value }) => [name, { "value": value ?? "" }])
|
||||
)
|
||||
})
|
||||
}))
|
||||
.map(({ attribute, errors }) => [
|
||||
attribute.name,
|
||||
{
|
||||
"value": attribute.value ?? "",
|
||||
errors,
|
||||
"doDisplayPotentialErrorMessages": errors.length !== 0
|
||||
}
|
||||
])
|
||||
),
|
||||
[attributesWithPassword]
|
||||
);
|
||||
|
||||
type InternalState = typeof initialInternalState;
|
||||
|
||||
const [formValidationInternalState, formValidationDispatch] = useReducer(
|
||||
(
|
||||
state: InternalState,
|
||||
params:
|
||||
| {
|
||||
action: "update value";
|
||||
name: string;
|
||||
newValue: string;
|
||||
}
|
||||
| {
|
||||
action: "focus lost";
|
||||
name: string;
|
||||
}
|
||||
): InternalState => ({
|
||||
...state,
|
||||
[params.name]: {
|
||||
...state[params.name],
|
||||
...(() => {
|
||||
switch (params.action) {
|
||||
case "focus lost":
|
||||
return { "doDisplayPotentialErrorMessages": true };
|
||||
case "update value":
|
||||
return {
|
||||
"value": params.newValue,
|
||||
"errors": getErrors({
|
||||
"name": params.name,
|
||||
"fieldValueByAttributeName": {
|
||||
...state,
|
||||
[params.name]: { "value": params.newValue }
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
})()
|
||||
}
|
||||
}),
|
||||
initialInternalState
|
||||
);
|
||||
|
||||
const formValidationState = useMemo(
|
||||
() => ({
|
||||
"fieldStateByAttributeName": Object.fromEntries(
|
||||
Object.entries(formValidationInternalState).map(([name, { value, errors, doDisplayPotentialErrorMessages }]) => [
|
||||
name,
|
||||
{ value, "displayableErrors": doDisplayPotentialErrorMessages ? errors : [] }
|
||||
])
|
||||
),
|
||||
"isFormSubmittable": Object.entries(formValidationInternalState).every(
|
||||
([name, { value, errors }]) =>
|
||||
errors.length === 0 && (value !== "" || !attributesWithPassword.find(attribute => attribute.name === name)!.required)
|
||||
)
|
||||
}),
|
||||
[formValidationInternalState, attributesWithPassword]
|
||||
);
|
||||
|
||||
return {
|
||||
formValidationState,
|
||||
formValidationDispatch,
|
||||
attributesWithPassword
|
||||
};
|
||||
}
|
||||
|
||||
/** Expect to be used in a component wrapped within a <I18nProvider> */
|
||||
function useGetErrors(params: {
|
||||
kcContext: {
|
||||
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
||||
profile: {
|
||||
attributes: { name: string; value?: string; validators: Validators }[];
|
||||
};
|
||||
};
|
||||
i18n: I18n;
|
||||
}) {
|
||||
const { kcContext, i18n } = params;
|
||||
|
||||
const {
|
||||
messagesPerField,
|
||||
profile: { attributes }
|
||||
} = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||
|
||||
const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
|
||||
const { name, fieldValueByAttributeName } = params;
|
||||
|
||||
const { value } = fieldValueByAttributeName[name];
|
||||
|
||||
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
|
||||
|
||||
block: {
|
||||
if (defaultValue !== value) {
|
||||
break block;
|
||||
}
|
||||
|
||||
let doesErrorExist: boolean;
|
||||
|
||||
try {
|
||||
doesErrorExist = messagesPerField.existsError(name);
|
||||
} catch {
|
||||
break block;
|
||||
}
|
||||
|
||||
if (!doesErrorExist) {
|
||||
break block;
|
||||
}
|
||||
|
||||
const errorMessageStr = messagesPerField.get(name);
|
||||
|
||||
return [
|
||||
{
|
||||
"validatorName": undefined,
|
||||
errorMessageStr,
|
||||
"errorMessage": <span key={0}>{errorMessageStr}</span>
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const errors: {
|
||||
errorMessage: JSX.Element;
|
||||
errorMessageStr: string;
|
||||
validatorName: keyof Validators | undefined;
|
||||
}[] = [];
|
||||
|
||||
scope: {
|
||||
const validatorName = "length";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (max !== undefined && value.length > parseInt(max)) {
|
||||
const msgArgs = ["error-invalid-length-too-long", max] as const;
|
||||
|
||||
errors.push({
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
validatorName
|
||||
});
|
||||
}
|
||||
|
||||
if (min !== undefined && value.length < parseInt(min)) {
|
||||
const msgArgs = ["error-invalid-length-too-short", min] as const;
|
||||
|
||||
errors.push({
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
validatorName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "_compareToOther";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, name: otherName, shouldBe, "error-message": errorMessageKey } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { value: otherValue } = fieldValueByAttributeName[otherName];
|
||||
|
||||
const isValid = (() => {
|
||||
switch (shouldBe) {
|
||||
case "different":
|
||||
return otherValue !== value;
|
||||
case "equal":
|
||||
return otherValue === value;
|
||||
}
|
||||
})();
|
||||
|
||||
if (isValid) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArg = [
|
||||
errorMessageKey ??
|
||||
id<MessageKey>(
|
||||
(() => {
|
||||
switch (shouldBe) {
|
||||
case "equal":
|
||||
return "shouldBeEqual";
|
||||
case "different":
|
||||
return "shouldBeDifferent";
|
||||
}
|
||||
})()
|
||||
),
|
||||
otherName,
|
||||
name,
|
||||
shouldBe
|
||||
] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArg)}</Fragment>,
|
||||
"errorMessageStr": advancedMsgStr(...msgArg)
|
||||
});
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "pattern";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (new RegExp(pattern).test(value)) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArgs = [errorMessageKey ?? id<MessageKey>("shouldMatchPattern"), pattern] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": advancedMsgStr(...msgArgs)
|
||||
});
|
||||
}
|
||||
|
||||
scope: {
|
||||
if ([...errors].reverse()[0]?.validatorName === "pattern") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const validatorName = "email";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (emailRegexp.test(value)) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs)
|
||||
});
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "integer";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const intValue = parseInt(value);
|
||||
|
||||
if (isNaN(intValue)) {
|
||||
const msgArgs = ["mustBeAnInteger"] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs)
|
||||
});
|
||||
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (max !== undefined && intValue > parseInt(max)) {
|
||||
const msgArgs = ["error-number-out-of-range-too-big", max] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs)
|
||||
});
|
||||
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (min !== undefined && intValue < parseInt(min)) {
|
||||
const msgArgs = ["error-number-out-of-range-too-small", min] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs)
|
||||
});
|
||||
|
||||
break scope;
|
||||
}
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "options";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (validator.options.indexOf(value) >= 0) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArgs = [id<MessageKey>("notAValidOption")] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": advancedMsgStr(...msgArgs)
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Implement missing validators.
|
||||
|
||||
return errors;
|
||||
});
|
||||
|
||||
return { getErrors };
|
||||
}
|
15
src/login/lib/useGetClassName.ts
Normal file
15
src/login/lib/useGetClassName.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||
|
||||
export function useGetClassName<ClassKey extends string>(params: {
|
||||
defaultClasses?: Record<ClassKey, string | undefined>;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
}) {
|
||||
const { defaultClasses, classes } = params;
|
||||
|
||||
const getClassName = useConstCallback((classKey: ClassKey): string => {
|
||||
return clsx(classKey, defaultClasses?.[classKey], classes?.[classKey]);
|
||||
});
|
||||
|
||||
return { getClassName };
|
||||
}
|
74
src/login/lib/usePrepareTemplate.ts
Normal file
74
src/login/lib/usePrepareTemplate.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { useReducer, useEffect } from "react";
|
||||
import { headInsert } from "keycloakify/tools/headInsert";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
|
||||
export function usePrepareTemplate(params: {
|
||||
doFetchDefaultThemeResources: boolean;
|
||||
stylesCommon?: string[];
|
||||
styles?: string[];
|
||||
scripts?: string[];
|
||||
url: {
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
};
|
||||
htmlClassName: string;
|
||||
}) {
|
||||
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, htmlClassName } = params;
|
||||
|
||||
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doFetchDefaultThemeResources) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isUnmounted = false;
|
||||
|
||||
Promise.all(
|
||||
[
|
||||
...(stylesCommon ?? []).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||
...(styles ?? []).map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
||||
]
|
||||
.reverse()
|
||||
.map(href =>
|
||||
headInsert({
|
||||
"type": "css",
|
||||
href,
|
||||
"position": "prepend"
|
||||
})
|
||||
)
|
||||
).then(() => {
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReady();
|
||||
});
|
||||
|
||||
(scripts ?? []).forEach(relativePath =>
|
||||
headInsert({
|
||||
"type": "javascript",
|
||||
"src": pathJoin(url.resourcesPath, relativePath)
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const htmlClassList = document.getElementsByTagName("html")[0].classList;
|
||||
|
||||
const tokens = clsx(htmlClassName).split(" ");
|
||||
|
||||
htmlClassList.add(...tokens);
|
||||
|
||||
return () => {
|
||||
htmlClassList.remove(...tokens);
|
||||
};
|
||||
}, [htmlClassName]);
|
||||
|
||||
return { isReady };
|
||||
}
|
Reference in New Issue
Block a user