UNPKG

invokers

Version:

A powerful, platform-first JavaScript library for creating modern user interfaces with declarative HTML. Features universal command chaining, conditional execution, and declarative workflow orchestration via `data-and-then` attributes and `<and-then>` ele

466 lines (410 loc) 15.7 kB
/** * @file browser.ts * @summary Browser API Command Pack for the Invokers library. * @description * This module provides commands for interacting with browser APIs including * cookies, storage, URL manipulation, history, and device capabilities. * These commands enable rich web platform integration. * * @example * ```javascript * import { registerBrowserCommands } from 'invokers/commands/browser'; * import { InvokerManager } from 'invokers'; * * const invokerManager = InvokerManager.getInstance(); * registerBrowserCommands(invokerManager); * ``` */ import type { InvokerManager } from '../core'; import type { CommandCallback, CommandContext } from '../index'; import { createInvokerError, ErrorSeverity } from '../index'; /** * Browser API commands for web platform integration. * Includes cookie management and other browser-specific functionality. */ const browserCommands: Record<string, CommandCallback> = { // --- URL Commands --- /** * `--url:params-get`: Gets a URL parameter value and sets it on the target element. * @example `<button command="--url:params-get:theme" commandfor="#theme-display">Show Theme</button>` */ "--url:params-get": ({ targetElement, params }: CommandContext) => { const key = params[0]; if (!key) { throw createInvokerError('URL params-get command requires a parameter name', ErrorSeverity.ERROR, { command: '--url:params-get', element: targetElement }); } const urlParams = new URLSearchParams(window.location.search); const value = urlParams.get(key); targetElement.textContent = value || ''; }, /** * `--url:params-set`: Sets a URL parameter with name and value. * @example `<button command="--url:params-set:theme:dark" commandfor="#content">Set Theme</button>` */ "--url:params-set": ({ invoker, params, getTargets }: CommandContext) => { let key = params[0]; let value = params[1] || invoker.dataset.value || ''; // Handle dynamic parameter names (e.g., from data attributes) if (!key && invoker.dataset.urlParamName) { key = invoker.dataset.urlParamName; } if (!key) { throw createInvokerError('URL params-set command requires a parameter name', ErrorSeverity.ERROR, { command: '--url:params-set', element: invoker }); } // Interpolation is already handled in the core executeCustomCommand method const url = new URL(window.location.href); if (value) { url.searchParams.set(key, value); } else { // If no value provided, use target element's value or text content const targets = getTargets(); if (targets.length > 0) { const targetEl = targets[0]; const inputValue = (targetEl as HTMLInputElement).value || (targetEl as HTMLTextAreaElement).value || targetEl.textContent; url.searchParams.set(key, inputValue || ''); } } window.history.replaceState(null, '', url.toString()); }, /** * `--url:params-delete`: Removes a URL parameter. * @example `<button command="--url:params-delete:theme">Clear Theme</button>` */ "--url:params-delete": ({ params }: CommandContext) => { const key = params[0]; if (!key) { throw createInvokerError('URL params-delete command requires a parameter name', ErrorSeverity.ERROR, { command: '--url:params-delete' }); } const url = new URL(window.location.href); url.searchParams.delete(key); window.history.replaceState(null, '', url.toString()); }, /** * `--url:params-clear`: Clears all URL parameters. * @example `<button command="--url:params-clear">Clear All Parameters</button>` */ "--url:params-clear": () => { const url = new URL(window.location.href); url.search = ''; window.history.replaceState(null, '', url.toString()); }, /** * `--url:params-all`: Gets all URL parameters as JSON and sets on target element. * @example `<button command="--url:params-all" commandfor="#params-display">Show All Params</button>` */ "--url:params-all": ({ targetElement }: CommandContext) => { const urlParams = new URLSearchParams(window.location.search); const paramsObj: Record<string, string> = {}; urlParams.forEach((value, key) => { paramsObj[key] = value; }); targetElement.textContent = JSON.stringify(paramsObj); }, // --- URL Hash Commands --- /** * `--url:hash-get`: Gets current hash and sets it on target element. * @example `<button command="--url:hash-get" commandfor="#hash-display">Show Hash</button>` */ "--url:hash-get": ({ targetElement }: CommandContext) => { const hash = window.location.hash.replace('#', ''); targetElement.textContent = hash; }, /** * `--url:hash-set`: Sets the URL hash. * @example `<button command="--url:hash-set:section2">Go to Section 2</button>` */ "--url:hash-set": ({ params, getTargets }: CommandContext) => { let hash = params[0]; // Interpolation is already handled in the core executeCustomCommand method if (hash !== undefined && hash !== '') { window.location.hash = hash.startsWith('#') ? hash : `#${hash}`; } else { // Get value from target element const targets = getTargets(); if (targets.length > 0) { const targetEl = targets[0]; const inputValue = (targetEl as HTMLInputElement).value || (targetEl as HTMLTextAreaElement).value || targetEl.textContent; if (inputValue) { window.location.hash = inputValue.startsWith('#') ? inputValue : `#${inputValue}`; } } } }, /** * `--url:hash-clear`: Clears the URL hash. * @example `<button command="--url:hash-clear">Clear Hash</button>` */ "--url:hash-clear": () => { window.location.hash = ''; }, // --- URL Pathname Commands --- /** * `--url:pathname-get`: Gets current pathname and sets it on target element. * @example `<button command="--url:pathname-get" commandfor="#path-display">Show Path</button>` */ "--url:pathname-get": ({ targetElement }: CommandContext) => { targetElement.textContent = window.location.pathname; }, /** * `--url:pathname-set`: Sets the URL pathname. * @example `<button command="--url:pathname-set:/new-page">Go to New Page</button>` */ "--url:pathname-set": ({ params, getTargets }: CommandContext) => { let pathname = params[0]; // Interpolation is already handled in the core executeCustomCommand method if (pathname !== undefined && pathname !== '') { const url = new URL(window.location.href); url.pathname = pathname; window.history.replaceState(null, '', url.toString()); } else { // Get value from target element const targets = getTargets(); if (targets.length > 0) { const targetEl = targets[0]; const inputValue = (targetEl as HTMLInputElement).value || (targetEl as HTMLTextAreaElement).value || targetEl.textContent; if (inputValue) { const url = new URL(window.location.href); url.pathname = inputValue; window.history.replaceState(null, '', url.toString()); } } } }, // --- URL Navigation Commands --- /** * `--url:navigate`: Navigates to a new URL using pushState. * @example `<button command="--url:navigate:/new-page">Go to New Page</button>` */ "--url:navigate": ({ params }: CommandContext) => { const url = params[0]; if (!url) { throw createInvokerError('URL navigate command requires a URL parameter', ErrorSeverity.ERROR, { command: '--url:navigate' }); } window.history.pushState({}, '', url); }, /** * `--url:replace`: Replaces current URL using replaceState. * @example `<button command="--url:replace:/replaced-page">Replace Current Page</button>` */ "--url:replace": ({ params }: CommandContext) => { const url = params[0]; if (!url) { throw createInvokerError('URL replace command requires a URL parameter', ErrorSeverity.ERROR, { command: '--url:replace' }); } window.history.replaceState(null, '', url); }, /** * `--url:reload`: Reloads the current page. * @example `<button command="--url:reload">Reload Page</button>` */ "--url:reload": () => { window.location.reload(); }, // --- History Commands --- /** * `--history:push`: Pushes a new state to history. * @example `<button command="--history:push:/test-page">Push to History</button>` */ "--history:push": ({ params, targetElement }: CommandContext) => { const url = params[0]; if (!url) { throw createInvokerError('History push command requires a URL parameter', ErrorSeverity.ERROR, { command: '--history:push' }); } window.history.pushState(null, '', url); if (targetElement) { targetElement.textContent = `Pushed ${url} to history`; } }, /** * `--history:replace`: Replaces current history state. * @example `<button command="--history:replace:/replaced-page">Replace History</button>` */ "--history:replace": ({ params, targetElement }: CommandContext) => { const url = params[0]; if (!url) { throw createInvokerError('History replace command requires a URL parameter', ErrorSeverity.ERROR, { command: '--history:replace' }); } window.history.replaceState(null, '', url); if (targetElement) { targetElement.textContent = `Replaced current URL with ${url}`; } }, /** * `--history:back`: Goes back in history. * @example `<button command="--history:back">Go Back</button>` */ "--history:back": ({ targetElement }: CommandContext) => { window.history.back(); if (targetElement) { targetElement.textContent = 'Navigated back in history'; } }, /** * `--history:forward`: Goes forward in history. * @example `<button command="--history:forward">Go Forward</button>` */ "--history:forward": ({ targetElement }: CommandContext) => { window.history.forward(); if (targetElement) { targetElement.textContent = 'Navigated forward in history'; } }, /** * `--history:go`: Goes to specific position in history. * @example `<button command="--history:go:-2">Go Back 2 Pages</button>` */ "--history:go": ({ params, targetElement }: CommandContext) => { const delta = parseInt(params[0], 10); if (isNaN(delta)) { throw createInvokerError('History go command requires a numeric delta parameter', ErrorSeverity.ERROR, { command: '--history:go' }); } window.history.go(delta); if (targetElement) { const direction = delta > 0 ? 'forward' : 'back'; targetElement.textContent = `Navigated ${direction} ${Math.abs(delta)} page(s) in history`; } }, /** * `--history:state:get`: Gets current history state. * @example `<button command="--history:state:get" commandfor="#state-display">Show State</button>` */ "--history:state:get": ({ targetElement }: CommandContext) => { const state = window.history.state; targetElement.textContent = state ? JSON.stringify(state) : 'null'; }, /** * `--history:state`: Alias for --history:state:get. * @example `<button command="--history:state" commandfor="#state-display">Show State</button>` */ "--history:state": ({ targetElement }: CommandContext) => { const state = window.history.state; targetElement.textContent = state ? JSON.stringify(state) : 'null'; }, /** * `--history:state:set`: Sets history state. * @example `<button command="--history:state:set:{"test": "data"}">Set State</button>` */ "--history:state:set": ({ params }: CommandContext) => { // Join all params back together in case JSON was split on colons const stateJson = params.join(':'); if (typeof window !== 'undefined' && (window as any).Invoker?.debug) { console.log('History state set params:', params, 'joined:', stateJson); } if (!stateJson) { throw createInvokerError('History state set command requires a state parameter', ErrorSeverity.ERROR, { command: '--history:state:set' }); } try { const state = JSON.parse(stateJson); window.history.replaceState(state, ''); } catch (e) { throw createInvokerError('History state set command requires valid JSON', ErrorSeverity.ERROR, { command: '--history:state:set', recovery: 'Provide a valid JSON string for the state parameter' }); } }, // --- Cookie Commands --- /** * `--cookie:set`: Sets a browser cookie. * @example `<button command="--cookie:set:theme:dark" data-cookie-expires="365">Set Dark Theme</button>` */ "--cookie:set": ({ invoker, params }: CommandContext) => { const key = params[0]; const value = params[1]; if (!key) { throw createInvokerError('Cookie set command requires a key parameter', ErrorSeverity.ERROR, { command: '--cookie:set', element: invoker }); } let cookieString = `${encodeURIComponent(key)}=${encodeURIComponent(value || '')}`; const expires = invoker.dataset.cookieExpires; if (expires) { const days = parseInt(expires, 10); if (!isNaN(days)) { const date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); cookieString += `; expires=${date.toUTCString()}`; } } cookieString += '; path=/'; document.cookie = cookieString; }, /** * `--cookie:get`: Gets a cookie value and sets it on the target element. * @example `<button command="--cookie:get:theme" commandfor="#theme-display">Show Theme</button>` */ "--cookie:get": ({ targetElement, params }: CommandContext) => { const key = params[0]; if (!key) { throw createInvokerError('Cookie get command requires a key parameter', ErrorSeverity.ERROR, { command: '--cookie:get', element: targetElement }); } const cookies = document.cookie.split(';'); for (const cookie of cookies) { const [cookieKey, cookieValue] = cookie.trim().split('='); if (decodeURIComponent(cookieKey) === key) { targetElement.textContent = decodeURIComponent(cookieValue || ''); return; } } targetElement.textContent = ''; // Not found }, /** * `--cookie:remove`: Removes a browser cookie. * @example `<button command="--cookie:remove:theme">Clear Theme</button>` */ "--cookie:remove": ({ params }: CommandContext) => { const key = params[0]; if (!key) { throw createInvokerError('Cookie remove command requires a key parameter', ErrorSeverity.ERROR, { command: '--cookie:remove' }); } document.cookie = `${encodeURIComponent(key)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; } }; /** * Registers all browser API commands with the InvokerManager. * This includes cookie management and other browser-specific functionality. * * @param manager - The InvokerManager instance to register commands with * @example * ```javascript * import { registerBrowserCommands } from 'invokers/commands/browser'; * import invokerManager from 'invokers'; * * registerBrowserCommands(invokerManager); * ``` */ export function registerBrowserCommands(manager: InvokerManager): void { for (const name in browserCommands) { if (browserCommands.hasOwnProperty(name)) { manager.register(name, browserCommands[name]); } } }