import JS from "./js"; import LiveSocket from "./live_socket"; type Transition = string | string[]; // Base options for commands involving transitions and timing type BaseOpts = { /** * The CSS transition classes to set. * Accepts a string of classes or a 3-tuple like: * `["ease-out duration-300", "opacity-0", "opacity-100"]` */ transition?: Transition; /** The transition duration in milliseconds. Defaults 200. */ time?: number; /** Whether to block UI during transition. Defaults `true`. */ blocking?: boolean; }; type ShowOpts = BaseOpts & { /** The CSS display value to set. Defaults "block". */ display?: string; }; type ToggleOpts = { /** The CSS display value to set. Defaults "block". */ display?: string; /** * The CSS transition classes for showing. * Accepts either the string of classes to apply when toggling in, or * a 3-tuple containing the transition class, the class to apply * to start the transition, and the ending transition class, such as: * `["ease-out duration-300", "opacity-0", "opacity-100"]` */ in?: Transition; /** * The CSS transition classes for hiding. * Accepts either string of classes to apply when toggling out, or * a 3-tuple containing the transition class, the class to apply * to start the transition, and the ending transition class, such as: * `["ease-out duration-300", "opacity-100", "opacity-0"]` */ out?: Transition; /** The transition duration in milliseconds. */ time?: number; /** Whether to block UI during transition. Defaults `true`. */ blocking?: boolean; }; // Options specific to the 'transition' command type TransitionCommandOpts = { /** The transition duration in milliseconds. */ time?: number; /** Whether to block UI during transition. Defaults `true`. */ blocking?: boolean; }; type PushOpts = { /** Data to be merged into the event payload. */ value?: any; /** For targeting a LiveComponent by its ID, a component ID (number), or a CSS selector string. */ target?: HTMLElement | number | string; /** Indicates if a page loading state should be shown. */ page_loading?: boolean; [key: string]: any; // Allow other properties like 'cid', 'redirect', etc. }; type NavigationOpts = { /** Whether to replace the current history entry instead of pushing a new one. */ replace?: boolean; }; /** * Represents all possible JS commands that can be generated by the factory. * This is used as a base for LiveSocketJSCommands and HookJSCommands. */ interface AllJSCommands { /** * Executes encoded JavaScript in the context of the element. * This version is for general use via liveSocket.js(). * * @param el - The element in whose context to execute the JavaScript. * @param encodedJS - The encoded JavaScript string to execute. */ exec(el: HTMLElement, encodedJS: string): void; /** * Shows an element. * * @param el - The element to show. * @param {ShowOpts} [opts={}] - Optional settings. * Accepts: `display`, `transition`, `time`, and `blocking`. */ show(el: HTMLElement, opts?: ShowOpts): void; /** * Hides an element. * * @param el - The element to hide. * @param [opts={}] - Optional settings. * Accepts: `transition`, `time`, and `blocking`. */ hide(el: HTMLElement, opts?: BaseOpts): void; /** * Toggles the visibility of an element. * * @param el - The element to toggle. * @param [opts={}] - Optional settings. * Accepts: `display`, `in`, `out`, `time`, and `blocking`. */ toggle(el: HTMLElement, opts?: ToggleOpts): void; /** * Adds CSS classes to an element. * * @param el - The element to add classes to. * @param names - The class name(s) to add. * @param [opts={}] - Optional settings. * Accepts: `transition`, `time`, and `blocking`. */ addClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void; /** * Removes CSS classes from an element. * * @param el - The element to remove classes from. * @param names - The class name(s) to remove. * @param [opts={}] - Optional settings. * Accepts: `transition`, `time`, and `blocking`. */ removeClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void; /** * Toggles CSS classes on an element. * * @param el - The element to toggle classes on. * @param names - The class name(s) to toggle. * @param [opts={}] - Optional settings. * Accepts: `transition`, `time`, and `blocking`. */ toggleClass(el: HTMLElement, names: string | string[], opts?: BaseOpts): void; /** * Applies a CSS transition to an element. * * @param el - The element to apply the transition to. * @param transition - The transition class(es) to apply. * Accepts a string of classes to apply when transitioning or * a 3-tuple containing the transition class, the class to apply * to start the transition, and the ending transition class, such as: * * ["ease-out duration-300", "opacity-100", "opacity-0"] * * @param [opts={}] - Optional settings for timing and blocking behavior. * Accepts: `time` and `blocking`. */ transition( el: HTMLElement, transition: string | string[], opts?: TransitionCommandOpts, ): void; /** * Sets an attribute on an element. * * @param el - The element to set the attribute on. * @param attr - The attribute name to set. * @param val - The value to set for the attribute. */ setAttribute(el: HTMLElement, attr: string, val: string): void; /** * Removes an attribute from an element. * * @param el - The element to remove the attribute from. * @param attr - The attribute name to remove. */ removeAttribute(el: HTMLElement, attr: string): void; /** * Toggles an attribute on an element between two values. * * @param el - The element to toggle the attribute on. * @param attr - The attribute name to toggle. * @param val1 - The first value to toggle between. * @param val2 - The second value to toggle between. */ toggleAttribute( el: HTMLElement, attr: string, val1: string, val2: string, ): void; /** * Pushes an event to the server. * * @param el - An element that belongs to the target LiveView / LiveComponent or a component ID. * To target a LiveComponent by its ID, pass a separate `target` in the options. * @param type - The event name to push. * @param [opts={}] - Optional settings. * Accepts: `value`, `target`, `page_loading`. */ push(el: HTMLElement, type: string, opts?: PushOpts): void; /** * Sends a navigation event to the server and updates the browser's pushState history. * * @param href - The URL to navigate to. * @param [opts={}] - Optional settings. * Accepts: `replace`. */ navigate(href: string, opts?: NavigationOpts): void; /** * Sends a patch event to the server and updates the browser's pushState history. * * @param href - The URL to patch to. * @param [opts={}] - Optional settings. * Accepts: `replace`. */ patch(href: string, opts?: NavigationOpts): void; /** * Mark attributes as ignored, skipping them when patching the DOM. * * @param el - The element to ignore attributes on. * @param attrs - The attribute name or names to ignore. */ ignoreAttributes(el: HTMLElement, attrs: string | string[]): void; } export default ( liveSocket: LiveSocket, eventType: string | null, ): AllJSCommands => { return { exec(el, encodedJS) { liveSocket.execJS(el, encodedJS, eventType); }, show(el, opts = {}) { const owner = liveSocket.owner(el); JS.show( eventType, owner, el, opts.display, JS.transitionClasses(opts.transition), opts.time, opts.blocking, ); }, hide(el, opts = {}) { const owner = liveSocket.owner(el); JS.hide( eventType, owner, el, null, JS.transitionClasses(opts.transition), opts.time, opts.blocking, ); }, toggle(el, opts = {}) { const owner = liveSocket.owner(el); const inTransition = JS.transitionClasses(opts.in); const outTransition = JS.transitionClasses(opts.out); JS.toggle( eventType, owner, el, opts.display, inTransition, outTransition, opts.time, opts.blocking, ); }, addClass(el, names, opts = {}) { const classNames = Array.isArray(names) ? names : names.split(" "); const owner = liveSocket.owner(el); JS.addOrRemoveClasses( el, classNames, [], JS.transitionClasses(opts.transition), opts.time, owner, opts.blocking, ); }, removeClass(el, names, opts = {}) { const classNames = Array.isArray(names) ? names : names.split(" "); const owner = liveSocket.owner(el); JS.addOrRemoveClasses( el, [], classNames, JS.transitionClasses(opts.transition), opts.time, owner, opts.blocking, ); }, toggleClass(el, names, opts = {}) { const classNames = Array.isArray(names) ? names : names.split(" "); const owner = liveSocket.owner(el); JS.toggleClasses( el, classNames, JS.transitionClasses(opts.transition), opts.time, owner, opts.blocking, ); }, transition(el, transition, opts = {}) { const owner = liveSocket.owner(el); JS.addOrRemoveClasses( el, [], [], JS.transitionClasses(transition), opts.time, owner, opts.blocking, ); }, setAttribute(el, attr, val) { JS.setOrRemoveAttrs(el, [[attr, val]], []); }, removeAttribute(el, attr) { JS.setOrRemoveAttrs(el, [], [attr]); }, toggleAttribute(el, attr, val1, val2) { JS.toggleAttr(el, attr, val1, val2); }, push(el, type, opts = {}) { liveSocket.withinOwners(el, (view) => { const data = opts.value || {}; delete opts.value; let e = new CustomEvent("phx:exec", { detail: { sourceElement: el } }); JS.exec(e, eventType, type, view, el, ["push", { data, ...opts }]); }); }, navigate(href, opts = {}) { const customEvent = new CustomEvent("phx:exec"); liveSocket.historyRedirect( customEvent, href, opts.replace ? "replace" : "push", null, null, ); }, patch(href, opts = {}) { const customEvent = new CustomEvent("phx:exec"); liveSocket.pushHistoryPatch( customEvent, href, opts.replace ? "replace" : "push", null, ); }, ignoreAttributes(el, attrs) { JS.ignoreAttrs(el, Array.isArray(attrs) ? attrs : [attrs]); }, }; }; /** * JSCommands for use with `liveSocket.js()`. * Includes the general `exec` command that requires an element. */ export type LiveSocketJSCommands = AllJSCommands; /** * JSCommands for use within a Hook. * The `exec` command is tailored for hooks, not requiring an explicit element. */ export interface HookJSCommands extends Omit { /** * Executes encoded JavaScript in the context of the hook's element. * * @param {string} encodedJS - The encoded JavaScript string to execute. */ exec(encodedJS: string): void; }