import { session } from '@hotwired/turbo'; import { z } from 'zod'; const Gon = z .object({ autosave: z .object({ debounce_delay: z.number().default(0), status_visible_duration: z.number().default(0) }) .default({}), autocomplete: z .object({ api_geo_url: z.string().optional(), api_adresse_url: z.string().optional(), api_education_url: z.string().optional() }) .default({}), matomo: z .object({ cookieDomain: z.string().optional(), domain: z.string().optional(), enabled: z.boolean().default(false), host: z.string().optional(), key: z.string().or(z.number()).nullish() }) .default({}), sentry: z .object({ key: z.string().nullish(), enabled: z.boolean().default(false), environment: z.string().optional(), user: z.object({ id: z.string() }).default({ id: '' }), browser: z.object({ modern: z.boolean() }).default({ modern: false }) }) .default({}), crisp: z .object({ key: z.string().nullish(), enabled: z.boolean().default(false), administrateur: z .object({ email: z.string(), DS_SIGN_IN_COUNT: z.number(), DS_NB_DEMARCHES_BROUILLONS: z.number(), DS_NB_DEMARCHES_ACTIVES: z.number(), DS_NB_DEMARCHES_ARCHIVES: z.number(), DS_ID: z.number() }) .default({ email: '', DS_SIGN_IN_COUNT: 0, DS_NB_DEMARCHES_BROUILLONS: 0, DS_NB_DEMARCHES_ACTIVES: 0, DS_NB_DEMARCHES_ARCHIVES: 0, DS_ID: 0 }) }) .default({}) }) .default({}); declare const window: Window & typeof globalThis & { gon: unknown }; export function getConfig() { return Gon.parse(window.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 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( 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; 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 => { 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(): Promise { const response = await request(init, 'application/json'); if (response.status == 204) { return null; } return response.json(); }, async turbo(): Promise { 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 isButtonElement( element: Element ): element is HTMLButtonElement { return ( element.tagName == 'BUTTON' || (element.tagName == 'INPUT' && (element as HTMLInputElement).type == 'submit') ); } export function isSelectElement( element: HTMLElement ): element is HTMLSelectElement { return element.tagName == 'SELECT'; } export function isCheckboxOrRadioInputElement( element: HTMLElement & { type?: string } ): element is HTMLInputElement { return ( element.tagName == 'INPUT' && (element.type == 'checkbox' || element.type == 'radio') ); } export function isTextInputElement( element: HTMLElement & { type?: string } ): element is HTMLInputElement { return ( ['INPUT', 'TEXTAREA'].includes(element.tagName) && typeof element.type == 'string' && !['checkbox', 'radio', 'file'].includes(element.type) ); } export function fire(obj: EventTarget, name: string, data?: T) { const event = new CustomEvent(name, { bubbles: true, cancelable: true, detail: data }); obj.dispatchEvent(event); return !event.defaultPrevented; } function csrfToken() { const meta = document.querySelector('meta[name=csrf-token]'); return meta?.content; } function delegateEvent( 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); }