demarches-normaliennes/app/javascript/shared/utils.ts
2024-07-05 16:07:41 +02:00

323 lines
8.3 KiB
TypeScript

import { session } from '@hotwired/turbo';
import * as s from 'superstruct';
function nullish<T, S>(struct: s.Struct<T, S>) {
return s.optional(s.union([s.literal(null), struct]));
}
const Gon = s.defaulted(
s.type({
autosave: s.defaulted(
s.type({
debounce_delay: s.defaulted(s.number(), 0),
status_visible_duration: s.defaulted(s.number(), 0)
}),
{}
),
autocomplete: s.defaulted(
s.partial(
s.type({
api_geo_url: s.string(),
api_adresse_url: s.string(),
api_education_url: s.string()
})
),
{}
),
locale: s.defaulted(s.string(), 'fr'),
matomo: s.defaulted(
s.type({
cookieDomain: s.optional(s.string()),
domain: s.optional(s.string()),
enabled: s.defaulted(s.boolean(), false),
host: s.optional(s.string()),
key: nullish(s.union([s.string(), s.number()]))
}),
{}
),
sentry: s.defaulted(
s.type({
key: nullish(s.string()),
enabled: s.defaulted(s.boolean(), false),
environment: s.optional(s.string()),
user: s.defaulted(s.type({ id: s.string() }), { id: '' }),
browser: s.defaulted(s.type({ modern: s.boolean() }), {
modern: false
}),
release: nullish(s.string())
}),
{}
),
crisp: s.defaulted(
s.type({
key: nullish(s.string()),
enabled: s.defaulted(s.boolean(), false),
administrateur: s.defaulted(
s.type({
email: s.string(),
DS_SIGN_IN_COUNT: s.number(),
DS_NB_DEMARCHES_BROUILLONS: s.number(),
DS_NB_DEMARCHES_ACTIVES: s.number(),
DS_NB_DEMARCHES_ARCHIVES: s.number(),
DS_ID: s.number()
}),
{
email: '',
DS_SIGN_IN_COUNT: 0,
DS_NB_DEMARCHES_BROUILLONS: 0,
DS_NB_DEMARCHES_ACTIVES: 0,
DS_NB_DEMARCHES_ARCHIVES: 0,
DS_ID: 0
}
)
}),
{}
),
defaultQuery: s.optional(s.string()),
defaultVariables: s.optional(s.string())
}),
{}
);
declare const window: Window & typeof globalThis & { gon: unknown };
export function getConfig() {
return s.create(window.gon, Gon);
}
export function show(el: Element | null) {
el?.classList.remove('hidden');
}
export function hide(el: Element | null) {
el?.classList.add('hidden');
}
export function toggle(el: Element | null, force?: boolean) {
if (force == undefined) {
el?.classList.toggle('hidden');
} else if (force) {
el?.classList.remove('hidden');
} else {
el?.classList.add('hidden');
}
}
export function toggleExpandIcon(icon: Element | null) {
icon?.classList.toggle('fr-icon-arrow-down-s-line');
icon?.classList?.toggle('fr-icon-arrow-up-s-line');
}
export function enable(
el: HTMLSelectElement | HTMLInputElement | HTMLButtonElement | null
) {
el && (el.disabled = false);
}
export function disable(
el: HTMLSelectElement | HTMLInputElement | HTMLButtonElement | null
) {
el && (el.disabled = true);
}
export function hasClass(el: HTMLElement | null, cssClass: string) {
return el?.classList.contains(cssClass);
}
export function addClass(el: HTMLElement | null, cssClass: string) {
el?.classList.add(cssClass);
}
export function removeClass(el: HTMLElement | null, cssClass: string) {
el?.classList.remove(cssClass);
}
export function delegate<E extends Event = Event>(
eventNames: string,
selector: string,
callback: (event: E) => void
): () => void {
const subscriptions = eventNames
.split(' ')
.map((eventName) =>
delegateEvent(
document,
selector,
eventName,
callback as (event: Event) => void
)
);
return () => subscriptions.forEach((unsubscribe) => unsubscribe());
}
export class ResponseError extends Error {
readonly response: Response;
readonly jsonBody?: unknown;
readonly textBody?: string;
constructor(response: Response, jsonBody?: unknown, textBody?: string) {
super(String(response.statusText || response.status));
this.response = response;
this.jsonBody = jsonBody;
this.textBody = textBody;
}
}
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
// Perform an HTTP request using `fetch` API,
// and handle the result depending on the MIME type.
//
// Usage:
//
// Execute a GET request, and return the response as parsed JSON
// const parsedJson = await httpRequest(url).json();
//
// Execute a POST request with some JSON payload
// const parsedJson = await httpRequest(url, { method: 'POST', json: '{ "foo": 1 }').json();
//
// Execute a GET request, and apply the Turbo stream in the Response
// await httpRequest(url).turbo();
//
export function httpRequest(
url: string,
{
csrf = true,
timeout = FETCH_TIMEOUT,
json,
controller,
...init
}: RequestInit & {
csrf?: boolean;
json?: unknown;
timeout?: number | false;
controller?: AbortController;
} = {}
) {
const headers = init.headers ? new Headers(init.headers) : new Headers();
if (csrf) {
headers.set('x-csrf-token', csrfToken() ?? '');
headers.set('x-requested-with', 'XMLHttpRequest');
init.credentials = 'same-origin';
}
init.headers = headers;
init.method = init.method?.toUpperCase() ?? 'GET';
if (json) {
headers.set('content-type', 'application/json');
init.body = JSON.stringify(json);
}
let timer: ReturnType<typeof setTimeout>;
if (!init.signal) {
controller = createAbortController(controller);
if (controller) {
init.signal = controller.signal;
if (timeout != false) {
timer = setTimeout(() => controller?.abort(), timeout);
}
}
}
const request = async (
init: RequestInit,
accept?: string
): Promise<Response> => {
if (accept && init.headers instanceof Headers) {
init.headers.set('accept', accept);
}
try {
const response = await fetch(url, init);
if (response.ok) {
return response;
} else if (response.status == 401) {
location.reload(); // reload whole page so Devise will redirect to sign-in
}
const contentType = response.headers.get('content-type');
let jsonBody: unknown;
let textBody: string | undefined;
try {
if (contentType?.match('json')) {
jsonBody = await response.clone().json();
} else {
textBody = await response.clone().text();
}
} catch {
// ignore
}
throw new ResponseError(response, jsonBody, textBody);
} finally {
clearTimeout(timer);
}
};
return {
async json<T>(): Promise<T | null> {
const response = await request(init, 'application/json');
if (response.status == 204) {
return null;
}
return response.json();
},
async turbo(): Promise<void> {
const response = await request(init, 'text/vnd.turbo-stream.html');
if (response.status != 204) {
const stream = await response.text();
session.renderStreamMessage(stream);
}
}
};
}
function createAbortController(controller?: AbortController) {
if (controller) {
return controller;
} else if (window.AbortController) {
return new AbortController();
}
return;
}
export function isNumeric(s: string) {
const n = parseFloat(s);
return !isNaN(n) && isFinite(n);
}
export function fire<T>(obj: EventTarget, name: string, data?: T) {
const event = new CustomEvent(name, {
bubbles: true,
cancelable: true,
detail: data
});
obj.dispatchEvent(event);
return !event.defaultPrevented;
}
export function csrfToken() {
const meta = document.querySelector<HTMLMetaElement>('meta[name=csrf-token]');
return meta?.content;
}
function delegateEvent<E extends Event = Event>(
element: EventTarget,
selector: string,
eventType: string,
handler: (event: E) => void | boolean
): () => void {
const listener = function (event: Event) {
let { target } = event;
while (!!(target instanceof Element) && !target.matches(selector)) {
target = target.parentNode;
}
if (
target instanceof Element &&
handler.call(target, event as E) === false
) {
event.preventDefault();
event.stopPropagation();
}
};
element.addEventListener(eventType, listener);
return () => element.removeEventListener(eventType, listener);
}