{"version":3,"file":"index.cjs","sources":["../src/forms.ts","../src/index.ts","../src/keys.ts"],"sourcesContent":["import { getCurrentUrl, Location } from 'swup';\n\nexport type FormMethod = 'GET' | 'POST';\n\nexport type FormInfo = {\n\torigin: string;\n\taction: string;\n\thref: string;\n\turl: string;\n\thash: string;\n\tmethod: FormMethod;\n\ttarget: string | null;\n\tdata: FormData;\n\tbody: URLSearchParams | FormData | undefined;\n\tencoding: string;\n};\n\n/**\n * Get information about where and how a form will submit\n */\nexport function getFormInfo(\n\tform: HTMLFormElement,\n\tsubmitter?: HTMLElement | null,\n\t{ stripEmptyParams = false }: { stripEmptyParams?: boolean } = {}\n): FormInfo {\n\tconst method = getFormAttr('method', form, submitter);\n\tconst encoding = getFormAttr('enctype', form, submitter);\n\tconst multipart = encoding === 'multipart/form-data';\n\tconst target = getFormAttr('target', form, submitter, '');\n\tconst { origin, href: action } = Location.fromUrl(getFormAttr('action', form, submitter));\n\n\t// Create form data object\n\tconst data = new FormData(form, submitter);\n\n\t// Create body object for post requests\n\tlet body: FormData | URLSearchParams | undefined;\n\tif (method === 'POST') {\n\t\tif (multipart) {\n\t\t\tbody = data;\n\t\t} else {\n\t\t\tbody = new URLSearchParams(data as unknown as Record<string, string>);\n\t\t}\n\t}\n\n\t// Build relative + absolute request url from action and data\n\tlet href: string;\n\tif (method === 'GET') {\n\t\tconst query = stripEmptyParams ? stripEmptyFormParams(data) : data;\n\t\thref = appendQueryParams(action, query);\n\t} else {\n\t\thref = action;\n\t}\n\tconst { url, hash } = Location.fromUrl(href);\n\n\treturn { origin, action, href, url, hash, method, target, data, body, encoding };\n}\n\n/**\n* Get a form attribute either from the form, or the submitter element if present\n*/\nexport function getFormAttr(attr: 'method', form: HTMLFormElement, submitter?: HTMLElement | null): FormMethod;\nexport function getFormAttr(attr: 'action', form: HTMLFormElement, submitter?: HTMLElement | null): string;\nexport function getFormAttr(attr: 'enctype', form: HTMLFormElement, submitter?: HTMLElement | null): string;\nexport function getFormAttr(attr: string, form: HTMLFormElement, submitter: HTMLElement | null, defaultValue: string): string;\nexport function getFormAttr(attr: string, form: HTMLFormElement, submitter?: HTMLElement | null, defaultValue?: string): string | null;\nexport function getFormAttr(\n\tattr: string,\n\tform: HTMLFormElement,\n\tsubmitter?: HTMLElement | null,\n\tdefaultValue?: string\n): string | null {\n\tconst value = submitter?.getAttribute(`form${attr}`) ?? form.getAttribute(attr) ?? defaultValue;\n\treturn sanitizeFormAttr(attr, value);\n}\n\n/**\n * Sanitize a form attribute to allow easier comparison\n */\nfunction sanitizeFormAttr(attr: string, value: string | undefined): null | string | FormMethod {\n\tswitch (attr) {\n\t\tcase 'action':\n\t\t\treturn value ?? getCurrentUrl();\n\t\tcase 'method':\n\t\t\treturn (value || 'get').toUpperCase() as FormMethod;\n\t\tcase 'enctype':\n\t\t\treturn (value || 'application/x-www-form-urlencoded').toLowerCase();\n\t\tdefault:\n\t\t\treturn value ?? null;\n\t}\n}\n\n/**\n * Strip empty params from a FormData object, returning a new FormData object\n * @see https://stackoverflow.com/a/64029534/586823\n */\nexport function stripEmptyFormParams(data: FormData): FormData {\n\tconst stripped = new FormData();\n\tfor (const [name, value] of Array.from(data.entries())) {\n\t\tif (value !== '') {\n\t\t\tstripped.append(name, value);\n\t\t}\n\t}\n\treturn stripped;\n}\n\n/**\n * Appends query parameters to a URL\n */\nexport function appendQueryParams(url: string, params: FormData): string {\n\tconst [uri, hash] = [...url.split(/(?=#)/), ''];\n\tconst path = uri.split('?')[0];\n\tconst query = new URLSearchParams(params as unknown as Record<string, string>).toString();\n\treturn query ? `${path}?${query}${hash}` : path + hash;\n}\n\n/**\n * Force a form to open in a new tab by setting the target attribute to _blank\n * @returns A function to restore the original target attribute\n */\nexport function forceFormToOpenInNewTab(form: HTMLFormElement): () => void {\n\tconst originalTarget = form.getAttribute('target') || '';\n\tform.setAttribute('target', '_blank');\n\n\treturn () => {\n\t\tif (originalTarget) {\n\t\t\tform.setAttribute('target', originalTarget);\n\t\t} else {\n\t\t\tform.removeAttribute('target');\n\t\t}\n\t};\n}\n","import Plugin from '@swup/plugin';\nimport type { DelegateEvent, DelegateEventUnsubscribe, Handler } from 'swup';\nimport { forceFormToOpenInNewTab, getFormInfo } from './forms.js';\nimport { trackKeys } from './keys.js';\n\ndeclare module 'swup' {\n\texport interface HookDefinitions {\n\t\t'form:submit': { el: HTMLFormElement; event: DelegatedSubmitEvent };\n\t\t'form:submit:newtab': { el: HTMLFormElement; event: DelegatedSubmitEvent };\n\t}\n}\n\ntype DelegatedSubmitEvent = DelegateEvent<SubmitEvent, HTMLFormElement>;\n\ntype Options = {\n\tformSelector: string;\n\tinlineFormSelector: string;\n\tstripEmptyParams: boolean;\n};\n\nexport default class SwupFormsPlugin extends Plugin {\n\tname = 'SwupFormsPlugin';\n\n\trequires = { swup: '>=4' };\n\n\tdefaults: Options = {\n\t\tformSelector: 'form[data-swup-form]',\n\t\tinlineFormSelector: 'form[data-swup-inline-form]',\n\t\tstripEmptyParams: false\n\t};\n\n\toptions: Options;\n\n\t// Track pressed keys to detect form submissions to a new tab\n\tspecialKeys: ReturnType<typeof trackKeys>;\n\n\tformSubmitDelegate?: DelegateEventUnsubscribe;\n\n\tconstructor(options: Partial<Options> = {}) {\n\t\tsuper();\n\t\tthis.options = { ...this.defaults, ...options };\n\t\tthis.specialKeys = trackKeys(['Meta', 'Control', 'Shift']);\n\t\tthis.beforeFormSubmit = this.beforeFormSubmit.bind(this);\n\t}\n\n\tmount() {\n\t\tthis.swup.hooks.create('form:submit');\n\t\tthis.swup.hooks.create('form:submit:newtab');\n\n\t\tthis.specialKeys.watch();\n\n\t\t// Register the submit handler. Using `capture:true` to be\n\t\t// able to set the form's target attribute on the fly.\n\t\tthis.formSubmitDelegate = this.swup.delegateEvent(\n\t\t\tthis.options.formSelector,\n\t\t\t'submit',\n\t\t\tthis.beforeFormSubmit,\n\t\t\t{\n\t\t\t\tcapture: true\n\t\t\t}\n\t\t);\n\n\t\tthis.on('visit:start', this.prepareInlineForms, { priority: 1 });\n\t}\n\n\tunmount() {\n\t\tthis.formSubmitDelegate?.destroy();\n\t\tthis.specialKeys.unwatch();\n\t}\n\n\t/**\n\t * Handles form 'submit' events during the capture phase\n\t */\n\tbeforeFormSubmit(event: DelegatedSubmitEvent): void {\n\t\tconst swup = this.swup;\n\t\tconst { delegateTarget: form, submitter } = event;\n\n\t\tconst { href, url, hash, target } = getFormInfo(form, submitter);\n\t\tconst opensInNewTabFromKeyPress = this.specialKeys.pressed;\n\t\tconst opensInNewTabFromTargetAttr = target === '_blank';\n\t\tconst opensInNewTab = opensInNewTabFromKeyPress || opensInNewTabFromTargetAttr;\n\n\t\t// Create temporary visit object for form:submit:* hooks\n\t\t// @ts-expect-error: createVisit is currently private, need to make this semi-public somehow\n\t\tconst visit = swup.createVisit({ to: url, hash, el: form, event });\n\n\t\t/**\n\t\t * Allow ignoring this form submission via callback\n\t\t * No use in checking if it will open in a new tab anyway\n\t\t */\n\t\tif (!opensInNewTab && swup.shouldIgnoreVisit(href, { el: form, event })) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Open the form in a new tab because of its target attribute\n\t\t */\n\t\tif (opensInNewTabFromTargetAttr) {\n\t\t\tswup.hooks.callSync('form:submit:newtab', visit, { el: form, event });\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Open the form in a new tab if either Command (Mac), Control (Windows) or Shift is pressed.\n\t\t * Normalizes behavior across browsers.\n\t\t */\n\t\tif (opensInNewTabFromKeyPress) {\n\t\t\tswup.hooks.callSync('form:submit:newtab', visit, { el: form, event });\n\t\t\tconst restorePreviousTarget = forceFormToOpenInNewTab(form);\n\t\t\tform.addEventListener('submit', () => setTimeout(restorePreviousTarget), { once: true });\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Trigger the form:submit hook.\n\t\t */\n\t\tswup.hooks.callSync('form:submit', visit, { el: form, event }, () => {\n\t\t\tthis.submitForm(event);\n\t\t});\n\t}\n\n\t/**\n\t * Submits a form through swup\n\t */\n\tsubmitForm(event: DelegatedSubmitEvent): void {\n\t\tconst { delegateTarget: form, submitter } = event;\n\t\tconst { stripEmptyParams } = this.options;\n\t\tconst { href, method, body } = getFormInfo(form, submitter, { stripEmptyParams });\n\t\tconst cache = { read: false, write: true };\n\n\t\tif (!['GET', 'POST'].includes(method)) {\n\t\t\tconsole.warn(`Unsupported form method: ${method}`);\n\t\t\treturn;\n\t\t}\n\n\t\tevent.preventDefault();\n\n\t\tthis.swup.navigate(href, { method, body, cache }, { el: form, event });\n\t}\n\n\t/**\n\t * Handles visits triggered by forms matching [data-swup-inline-form]\n\t */\n\tprepareInlineForms: Handler<'visit:start'> = (visit) => {\n\t\tconst { el } = visit.trigger;\n\t\tif (!el?.matches(this.options.inlineFormSelector)) return;\n\n\t\tif (!el.id) {\n\t\t\tconsole.error(`[@swup/forms-plugin] inline forms must have an id attribute:`, el);\n\t\t\treturn;\n\t\t}\n\n\t\t// Modify visit to only replace and animate the form's container\n\t\tconst selector = `#${el.id}`;\n\t\tvisit.containers = [selector];\n\t\tvisit.animation.scope = 'containers';\n\t\tvisit.animation.selector = selector;\n\t\tvisit.scroll.target = selector;\n\n\t\t// Modify visit to focus the form after the transition\n\t\t// @ts-expect-error: can't know if A11yPlugin is installed\n\t\tconst a11y = visit.a11y as { focus?: boolean | string };\n\t\tif (typeof a11y === 'object') {\n\t\t\ta11y.focus = selector;\n\t\t}\n\t};\n}\n","/**\n * Track currently pressed keys\n */\n\nexport function trackKeys(keys: string[]) {\n\tlet pressed: { [key: string]: boolean } = {};\n\n\tfunction watch() {\n\t\tdocument.addEventListener('keydown', handleKeyDown);\n\t\tdocument.addEventListener('keyup', handleKeyUp);\n\t\twindow.addEventListener('blur', handleWindowBlur);\n\t}\n\n\tfunction unwatch() {\n\t\tdocument.removeEventListener('keydown', handleKeyDown);\n\t\tdocument.removeEventListener('keyup', handleKeyUp);\n\t\tdocument.removeEventListener('blur', handleWindowBlur);\n\t\treset();\n\t}\n\n\tfunction reset() {\n\t\tpressed = {};\n\t}\n\n\tfunction handleKeyDown({ key }: KeyboardEvent) {\n\t\tif (keys.includes(key)) {\n\t\t\tpressed[key] = true;\n\t\t}\n\t}\n\n\tfunction handleKeyUp({ key }: KeyboardEvent) {\n\t\tif (keys.includes(key)) {\n\t\t\tpressed[key] = false;\n\t\t}\n\t}\n\n\tfunction handleWindowBlur() {\n\t\treset();\n\t}\n\n\treturn {\n\t\twatch,\n\t\tunwatch,\n\t\tget pressed(): boolean {\n\t\t\treturn Object.values(pressed).filter(Boolean).length > 0;\n\t\t}\n\t};\n}\n"],"names":["getFormInfo","form","submitter","stripEmptyParams","method","getFormAttr","encoding","multipart","target","origin","href","action","Location","fromUrl","data","FormData","body","URLSearchParams","query","stripped","name","value","Array","from","entries","append","stripEmptyFormParams","url","params","uri","hash","split","path","toString","appendQueryParams","attr","defaultValue","getCurrentUrl","toUpperCase","toLowerCase","sanitizeFormAttr","getAttribute","Plugin","constructor","options","super","this","requires","swup","defaults","formSelector","inlineFormSelector","specialKeys","formSubmitDelegate","prepareInlineForms","visit","el","trigger","matches","id","console","error","selector","containers","animation","scope","scroll","a11y","focus","keys","pressed","reset","handleKeyDown","key","includes","handleKeyUp","handleWindowBlur","watch","document","addEventListener","window","unwatch","removeEventListener","Object","values","filter","Boolean","length","trackKeys","beforeFormSubmit","bind","mount","hooks","create","delegateEvent","capture","on","priority","unmount","destroy","event","delegateTarget","opensInNewTabFromKeyPress","opensInNewTabFromTargetAttr","opensInNewTab","createVisit","to","shouldIgnoreVisit","callSync","restorePreviousTarget","originalTarget","setAttribute","removeAttribute","forceFormToOpenInNewTab","setTimeout","once","submitForm","preventDefault","navigate","cache","read","write","warn"],"mappings":"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"}