@alfakit/core 0.1.1 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components.css +347 -0
- package/dist/actions.d.ts +9 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +31 -5
- package/dist/actions.js.map +1 -1
- package/dist/advanced-fields.js +2 -2
- package/dist/advanced-fields.js.map +1 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +2 -1
- package/dist/assets.js.map +1 -1
- package/dist/cms-live-preview.d.ts.map +1 -1
- package/dist/cms-live-preview.js +7 -1
- package/dist/cms-live-preview.js.map +1 -1
- package/dist/csp.d.ts +11 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +16 -0
- package/dist/csp.js.map +1 -0
- package/dist/file-size.js +1 -1
- package/dist/file-size.js.map +1 -1
- package/dist/form.d.ts +53 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +96 -4
- package/dist/form.js.map +1 -1
- package/dist/icons.d.ts +6 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +9 -1
- package/dist/icons.js.map +1 -1
- package/dist/image-editor.d.ts +2 -2
- package/dist/image-editor.js +2 -2
- package/dist/image-files.d.ts +8 -0
- package/dist/image-files.d.ts.map +1 -1
- package/dist/image-files.js +11 -1
- package/dist/image-files.js.map +1 -1
- package/dist/index.d.ts +12 -11
- package/dist/index.js +10 -9
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +1 -1
- package/dist/kanban.js.map +1 -1
- package/dist/page-actions.d.ts +10 -2
- package/dist/page-actions.d.ts.map +1 -1
- package/dist/page-actions.js +15 -5
- package/dist/page-actions.js.map +1 -1
- package/dist/schema/advanced-fields.d.ts +5 -0
- package/dist/schema/advanced-fields.d.ts.map +1 -1
- package/dist/schema/cms.d.ts +4 -0
- package/dist/schema/cms.d.ts.map +1 -1
- package/dist/schema/components.d.ts +5 -0
- package/dist/schema/components.d.ts.map +1 -1
- package/dist/schema/components.js.map +1 -1
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/panel.d.ts +31 -0
- package/dist/schema/panel.d.ts.map +1 -1
- package/dist/schema/panel.js.map +1 -1
- package/dist/schema/table-grouping.d.ts +5 -1
- package/dist/schema/table-grouping.d.ts.map +1 -1
- package/dist/standalone.d.ts +10 -3
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +12 -2
- package/dist/standalone.js.map +1 -1
- package/dist/starter-kit-widgets.d.ts +40 -7
- package/dist/starter-kit-widgets.d.ts.map +1 -1
- package/dist/starter-kit-widgets.js +71 -7
- package/dist/starter-kit-widgets.js.map +1 -1
- package/dist/starter-kit.d.ts +24 -1
- package/dist/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit.js +33 -3
- package/dist/starter-kit.js.map +1 -1
- package/dist/utils/debounce.d.ts +2 -0
- package/dist/utils/debounce.d.ts.map +1 -1
- package/dist/utils/debounce.js +1 -0
- package/dist/utils/debounce.js.map +1 -1
- package/dist/utils/path.d.ts +11 -1
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +30 -3
- package/dist/utils/path.js.map +1 -1
- package/package.json +5 -3
- package/themes.css +8 -335
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-live-preview.js","names":[],"sources":["../src/cms-live-preview.ts"],"sourcesContent":["import { callPageMethod } from './page-methods'\nimport type { CmsAutosaveResult } from './schema/cms'\n\n/**\n * The framework-agnostic half of the page editor's live preview (alfasix/alfakit-cms):\n * debounced autosaves of the form state through the page's exposed `cmsAutosave` method,\n * and the preview URL and reload counter the renderers bind an iframe to. The server\n * stores the draft and signs the preview; nothing here renders or validates content.\n */\n\nexport type CmsAutosaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'\n\nexport interface CmsAutosaveSnapshot {\n status: CmsAutosaveStatus\n savedAt: string | null\n previewUrl: string | null\n /** Bumped after every successful save: remount the preview iframe on change. */\n version: number\n /** The saved draft differs from what visitors see (`hasPendingChanges()`). */\n hasChanges: boolean\n}\n\nexport interface CmsAutosaveOptions {\n /** Send the state (the editor page's `cmsAutosave` method). */\n save: (state: Record<string, unknown>) => Promise<CmsAutosaveResult>\n debounce: number\n previewUrl: string | null\n savedAt?: string | null\n hasChanges?: boolean\n onChange?: (snapshot: CmsAutosaveSnapshot) => void\n /** Retry delay after a failed save (milliseconds). */\n retryAfter?: number\n}\n\n/** Preview widths: `null` fills the pane. */\nexport const CMS_PREVIEW_DEVICES = {\n desktop: null,\n tablet: 768,\n phone: 390,\n} as const\n\nexport type CmsPreviewDevice = keyof typeof CMS_PREVIEW_DEVICES\n\nexport class CmsAutosave {\n private timer: ReturnType<typeof setTimeout> | null = null\n private pending: Record<string, unknown> | null = null\n private running = false\n private disposed = false\n private current: CmsAutosaveSnapshot\n\n constructor(private readonly options: CmsAutosaveOptions) {\n this.current = {\n status: options.savedAt ? 'saved' : 'idle',\n savedAt: options.savedAt ?? null,\n previewUrl: options.previewUrl,\n version: 0,\n hasChanges: options.hasChanges ?? false,\n }\n }\n\n get snapshot(): CmsAutosaveSnapshot {\n return this.current\n }\n\n /** The form changed: save its state once it stays unchanged for `debounce` ms. */\n schedule(state: Record<string, unknown>): void {\n if (this.disposed) return\n\n this.pending = state\n this.update({ status: 'pending' })\n\n if (this.timer) clearTimeout(this.timer)\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n\n /** Save the latest state now (when one is waiting). */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n\n if (this.disposed || this.running || this.pending === null) return\n\n const state = this.pending\n this.pending = null\n this.running = true\n this.update({ status: 'saving' })\n\n try {\n const result = await this.options.save(state)\n\n if (this.disposed) return\n\n this.update({\n status: this.pending === null ? 'saved' : 'pending',\n savedAt: result.savedAt,\n previewUrl: result.previewUrl ?? this.current.previewUrl,\n version: this.current.version + 1,\n hasChanges: result.hasChanges,\n })\n } catch {\n if (this.disposed) return\n\n // Keep the newest state and try again later.\n this.pending ??= state\n this.update({ status: 'error' })\n this.timer = setTimeout(() => void this.flush(), this.options.retryAfter ?? 5000)\n } finally {\n this.running = false\n }\n\n if (this.pending !== null && this.timer === null) {\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n }\n\n dispose(): void {\n this.disposed = true\n\n if (this.timer) clearTimeout(this.timer)\n }\n\n private update(patch: Partial<CmsAutosaveSnapshot>): void {\n this.current = { ...this.current, ...patch }\n this.options.onChange?.(this.current)\n }\n}\n\n/**\n * While the side-by-side preview is open, make room for it: the panel's content area\n * (`[data-slot=\"content\"]`) keeps clear of the pane with an inline end margin of the pane's\n * width (it may be centred with a maximum width, so padding would only squeeze the form),\n * restored when it closes. Returns the function that restores it.\n */\nexport function reserveCmsPreviewSpace(from: Element | null, width: string): () => void {\n const content = from?.closest<HTMLElement>('[data-slot=\"content\"]')\n\n if (\n !content ||\n typeof window === 'undefined' ||\n !window.matchMedia?.('(min-width: 1024px)').matches\n ) {\n return () => {}\n }\n\n const previous = { margin: content.style.marginInlineEnd, width: content.style.width }\n content.style.marginInlineEnd = width\n content.style.width = 'auto'\n\n return () => {\n content.style.marginInlineEnd = previous.margin\n content.style.width = previous.width\n }\n}\n\n/**\n * Whether the side-by-side preview should open on its own: a wide enough screen for the\n * form and the pane (`minWidth`, 1536px by default), unless this viewer closed it last\n * time (`remembered === 'closed'`).\n */\nexport function cmsPreviewOpensByDefault(\n remembered: string | null,\n width: number = typeof window === 'undefined' ? 0 : window.innerWidth,\n minWidth = 1536,\n): boolean {\n if (remembered === 'open') return width >= 1024\n if (remembered === 'closed') return false\n\n return width >= minWidth\n}\n\n/** Width of the side-by-side pane: half the screen, at most 56rem (matches `lg:w-[min(50vw,56rem)]`). */\nexport function cmsPreviewPaneWidth(): number {\n if (typeof window === 'undefined') return 0\n\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n\n return Math.min(window.innerWidth / 2, 56 * rem)\n}\n\n/**\n * Whether the preview fits beside the form: at least the `lg` breakpoint, and the column\n * holding the panel's content area (the parent of `[data-slot=\"content\"]` around `from`,\n * i.e. the screen minus the sidebar) keeps `minFormWidth` pixels (640) for the form next to\n * the pane. When it doesn't, renderers open the preview as a full-screen overlay instead, so\n * the form never runs under the pane.\n */\nexport function cmsPreviewFitsBeside(from: Element | null, minFormWidth = 640): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width = column?.clientWidth || window.innerWidth\n\n return width - cmsPreviewPaneWidth() >= minFormWidth\n}\n\n/**\n * Whether the side-by-side preview opens when the editor loads: never below the `lg`\n * breakpoint (phones and tablets keep it closed) or when it wouldn't fit beside the form\n * (`cmsPreviewFitsBeside()`); when this viewer opened or closed it last time, their choice;\n * otherwise when the column holding the panel's content area (the parent of\n * `[data-slot=\"content\"]` around `from`) is at least `minWidth` wide (1440px).\n */\nexport function cmsPreviewOpensOnLoad(from: Element | null, minWidth = 1440): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n if (!cmsPreviewFitsBeside(from)) return false\n\n const remembered = rememberedCmsPreview()\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width =\n remembered === 'open' ? window.innerWidth : (column?.clientWidth ?? window.innerWidth)\n\n return cmsPreviewOpensByDefault(remembered, width, minWidth)\n}\n/** The storage key remembering whether the viewer keeps the preview open. */\nexport const CMS_PREVIEW_STORAGE_KEY = 'alfakit:cms-live-preview'\n\n/** Read / write the viewer's preview choice; storage may be unavailable. */\nexport function rememberedCmsPreview(value?: 'open' | 'closed'): string | null {\n try {\n if (typeof localStorage === 'undefined') return null\n if (value) localStorage.setItem(CMS_PREVIEW_STORAGE_KEY, value)\n\n return localStorage.getItem(CMS_PREVIEW_STORAGE_KEY)\n } catch {\n return null\n }\n}\n\n/** Call the editor page's autosave method with the form state. */\nexport async function callCmsAutosave(\n method: string,\n state: Record<string, unknown>,\n options: { token: string; endpoint: string },\n): Promise<CmsAutosaveResult> {\n const response = await callPageMethod<CmsAutosaveResult>(method, { state }, options)\n\n return response.result\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cms-live-preview.js","names":[],"sources":["../src/cms-live-preview.ts"],"sourcesContent":["import { currentRecordVersion, setRecordVersion } from './conflicts'\nimport { callPageMethod } from './page-methods'\nimport type { CmsAutosaveResult } from './schema/cms'\n\n/**\n * The framework-agnostic half of the page editor's live preview (alfasix/alfakit-cms):\n * debounced autosaves of the form state through the page's exposed `cmsAutosave` method,\n * and the preview URL and reload counter the renderers bind an iframe to. The server\n * stores the draft and signs the preview; nothing here renders or validates content.\n */\n\nexport type CmsAutosaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'\n\nexport interface CmsAutosaveSnapshot {\n status: CmsAutosaveStatus\n savedAt: string | null\n previewUrl: string | null\n /** Bumped after every successful save: remount the preview iframe on change. */\n version: number\n /** The saved draft differs from what visitors see (`hasPendingChanges()`). */\n hasChanges: boolean\n}\n\nexport interface CmsAutosaveOptions {\n /** Send the state (the editor page's `cmsAutosave` method). */\n save: (state: Record<string, unknown>) => Promise<CmsAutosaveResult>\n debounce: number\n previewUrl: string | null\n savedAt?: string | null\n hasChanges?: boolean\n onChange?: (snapshot: CmsAutosaveSnapshot) => void\n /** Retry delay after a failed save (milliseconds). */\n retryAfter?: number\n}\n\n/** Preview widths: `null` fills the pane. */\nexport const CMS_PREVIEW_DEVICES = {\n desktop: null,\n tablet: 768,\n phone: 390,\n} as const\n\nexport type CmsPreviewDevice = keyof typeof CMS_PREVIEW_DEVICES\n\nexport class CmsAutosave {\n private timer: ReturnType<typeof setTimeout> | null = null\n private pending: Record<string, unknown> | null = null\n private running = false\n private disposed = false\n private current: CmsAutosaveSnapshot\n\n constructor(private readonly options: CmsAutosaveOptions) {\n this.current = {\n status: options.savedAt ? 'saved' : 'idle',\n savedAt: options.savedAt ?? null,\n previewUrl: options.previewUrl,\n version: 0,\n hasChanges: options.hasChanges ?? false,\n }\n }\n\n get snapshot(): CmsAutosaveSnapshot {\n return this.current\n }\n\n /** The form changed: save its state once it stays unchanged for `debounce` ms. */\n schedule(state: Record<string, unknown>): void {\n if (this.disposed) return\n\n this.pending = state\n this.update({ status: 'pending' })\n\n if (this.timer) clearTimeout(this.timer)\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n\n /** Save the latest state now (when one is waiting). */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n\n if (this.disposed || this.running || this.pending === null) return\n\n const state = this.pending\n this.pending = null\n this.running = true\n this.update({ status: 'saving' })\n\n try {\n const result = await this.options.save(state)\n\n if (this.disposed) return\n\n this.update({\n status: this.pending === null ? 'saved' : 'pending',\n savedAt: result.savedAt,\n previewUrl: result.previewUrl ?? this.current.previewUrl,\n version: this.current.version + 1,\n hasChanges: result.hasChanges,\n })\n } catch {\n if (this.disposed) return\n\n // Keep the newest state and try again later.\n this.pending ??= state\n this.update({ status: 'error' })\n this.timer = setTimeout(() => void this.flush(), this.options.retryAfter ?? 5000)\n } finally {\n this.running = false\n }\n\n if (this.pending !== null && this.timer === null) {\n this.timer = setTimeout(() => void this.flush(), this.options.debounce)\n }\n }\n\n dispose(): void {\n this.disposed = true\n\n if (this.timer) clearTimeout(this.timer)\n }\n\n private update(patch: Partial<CmsAutosaveSnapshot>): void {\n this.current = { ...this.current, ...patch }\n this.options.onChange?.(this.current)\n }\n}\n\n/**\n * While the side-by-side preview is open, make room for it: the panel's content area\n * (`[data-slot=\"content\"]`) keeps clear of the pane with an inline end margin of the pane's\n * width (it may be centred with a maximum width, so padding would only squeeze the form),\n * restored when it closes. Returns the function that restores it.\n */\nexport function reserveCmsPreviewSpace(from: Element | null, width: string): () => void {\n const content = from?.closest<HTMLElement>('[data-slot=\"content\"]')\n\n if (\n !content ||\n typeof window === 'undefined' ||\n !window.matchMedia?.('(min-width: 1024px)').matches\n ) {\n return () => {}\n }\n\n const previous = { margin: content.style.marginInlineEnd, width: content.style.width }\n content.style.marginInlineEnd = width\n content.style.width = 'auto'\n\n return () => {\n content.style.marginInlineEnd = previous.margin\n content.style.width = previous.width\n }\n}\n\n/**\n * Whether the side-by-side preview should open on its own: a wide enough screen for the\n * form and the pane (`minWidth`, 1536px by default), unless this viewer closed it last\n * time (`remembered === 'closed'`).\n */\nexport function cmsPreviewOpensByDefault(\n remembered: string | null,\n width: number = typeof window === 'undefined' ? 0 : window.innerWidth,\n minWidth = 1536,\n): boolean {\n if (remembered === 'open') return width >= 1024\n if (remembered === 'closed') return false\n\n return width >= minWidth\n}\n\n/** Width of the side-by-side pane: half the screen, at most 56rem (matches `lg:w-[min(50vw,56rem)]`). */\nexport function cmsPreviewPaneWidth(): number {\n if (typeof window === 'undefined') return 0\n\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n\n return Math.min(window.innerWidth / 2, 56 * rem)\n}\n\n/**\n * Whether the preview fits beside the form: at least the `lg` breakpoint, and the column\n * holding the panel's content area (the parent of `[data-slot=\"content\"]` around `from`,\n * i.e. the screen minus the sidebar) keeps `minFormWidth` pixels (640) for the form next to\n * the pane. When it doesn't, renderers open the preview as a full-screen overlay instead, so\n * the form never runs under the pane.\n */\nexport function cmsPreviewFitsBeside(from: Element | null, minFormWidth = 640): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width = column?.clientWidth || window.innerWidth\n\n return width - cmsPreviewPaneWidth() >= minFormWidth\n}\n\n/**\n * Whether the side-by-side preview opens when the editor loads: never below the `lg`\n * breakpoint (phones and tablets keep it closed) or when it wouldn't fit beside the form\n * (`cmsPreviewFitsBeside()`); when this viewer opened or closed it last time, their choice;\n * otherwise when the column holding the panel's content area (the parent of\n * `[data-slot=\"content\"]` around `from`) is at least `minWidth` wide (1440px).\n */\nexport function cmsPreviewOpensOnLoad(from: Element | null, minWidth = 1440): boolean {\n if (typeof window === 'undefined' || !window.matchMedia?.('(min-width: 1024px)').matches) {\n return false\n }\n\n if (!cmsPreviewFitsBeside(from)) return false\n\n const remembered = rememberedCmsPreview()\n const column = from?.closest<HTMLElement>('[data-slot=\"content\"]')?.parentElement\n const width =\n remembered === 'open' ? window.innerWidth : (column?.clientWidth ?? window.innerWidth)\n\n return cmsPreviewOpensByDefault(remembered, width, minWidth)\n}\n/** The storage key remembering whether the viewer keeps the preview open. */\nexport const CMS_PREVIEW_STORAGE_KEY = 'alfakit:cms-live-preview'\n\n/** Read / write the viewer's preview choice; storage may be unavailable. */\nexport function rememberedCmsPreview(value?: 'open' | 'closed'): string | null {\n try {\n if (typeof localStorage === 'undefined') return null\n if (value) localStorage.setItem(CMS_PREVIEW_STORAGE_KEY, value)\n\n return localStorage.getItem(CMS_PREVIEW_STORAGE_KEY)\n } catch {\n return null\n }\n}\n\n/** Call the editor page's autosave method with the form state. */\nexport async function callCmsAutosave(\n method: string,\n state: Record<string, unknown>,\n options: { token: string; endpoint: string },\n): Promise<CmsAutosaveResult> {\n const response = await callPageMethod<CmsAutosaveResult>(\n method,\n { state, recordVersion: currentRecordVersion() },\n options,\n )\n\n // The draft changed the record: the next save sends the new version, so it is not\n // refused as a conflict with this tab's own autosave.\n if (response.result.recordVersion) setRecordVersion(response.result.recordVersion)\n\n return response.result\n}\n"],"mappings":";;;;AAoCA,MAAa,sBAAsB;CACjC,SAAS;CACT,QAAQ;CACR,OAAO;AACT;AAIA,IAAa,cAAb,MAAyB;CAOM;CAN7B,QAAsD;CACtD,UAAkD;CAClD,UAAkB;CAClB,WAAmB;CACnB;CAEA,YAAY,SAA8C;EAA7B,KAAA,UAAA;EAC3B,KAAK,UAAU;GACb,QAAQ,QAAQ,UAAU,UAAU;GACpC,SAAS,QAAQ,WAAW;GAC5B,YAAY,QAAQ;GACpB,SAAS;GACT,YAAY,QAAQ,cAAc;EACpC;CACF;CAEA,IAAI,WAAgC;EAClC,OAAO,KAAK;CACd;;CAGA,SAAS,OAAsC;EAC7C,IAAI,KAAK,UAAU;EAEnB,KAAK,UAAU;EACf,KAAK,OAAO,EAAE,QAAQ,UAAU,CAAC;EAEjC,IAAI,KAAK,OAAO,aAAa,KAAK,KAAK;EACvC,KAAK,QAAQ,iBAAiB,KAAK,KAAK,MAAM,GAAG,KAAK,QAAQ,QAAQ;CACxE;;CAGA,MAAM,QAAuB;EAC3B,IAAI,KAAK,OAAO;GACd,aAAa,KAAK,KAAK;GACvB,KAAK,QAAQ;EACf;EAEA,IAAI,KAAK,YAAY,KAAK,WAAW,KAAK,YAAY,MAAM;EAE5D,MAAM,QAAQ,KAAK;EACnB,KAAK,UAAU;EACf,KAAK,UAAU;EACf,KAAK,OAAO,EAAE,QAAQ,SAAS,CAAC;EAEhC,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,KAAK;GAE5C,IAAI,KAAK,UAAU;GAEnB,KAAK,OAAO;IACV,QAAQ,KAAK,YAAY,OAAO,UAAU;IAC1C,SAAS,OAAO;IAChB,YAAY,OAAO,cAAc,KAAK,QAAQ;IAC9C,SAAS,KAAK,QAAQ,UAAU;IAChC,YAAY,OAAO;GACrB,CAAC;EACH,QAAQ;GACN,IAAI,KAAK,UAAU;GAGnB,KAAK,YAAY;GACjB,KAAK,OAAO,EAAE,QAAQ,QAAQ,CAAC;GAC/B,KAAK,QAAQ,iBAAiB,KAAK,KAAK,MAAM,GAAG,KAAK,QAAQ,cAAc,GAAI;EAClF,UAAU;GACR,KAAK,UAAU;EACjB;EAEA,IAAI,KAAK,YAAY,QAAQ,KAAK,UAAU,MAC1C,KAAK,QAAQ,iBAAiB,KAAK,KAAK,MAAM,GAAG,KAAK,QAAQ,QAAQ;CAE1E;CAEA,UAAgB;EACd,KAAK,WAAW;EAEhB,IAAI,KAAK,OAAO,aAAa,KAAK,KAAK;CACzC;CAEA,OAAe,OAA2C;EACxD,KAAK,UAAU;GAAE,GAAG,KAAK;GAAS,GAAG;EAAM;EAC3C,KAAK,QAAQ,WAAW,KAAK,OAAO;CACtC;AACF;;;;;;;AAQA,SAAgB,uBAAuB,MAAsB,OAA2B;CACtF,MAAM,UAAU,MAAM,QAAqB,yBAAuB;CAElE,IACE,CAAC,WACD,OAAO,WAAW,eAClB,CAAC,OAAO,aAAa,qBAAqB,CAAC,CAAC,SAE5C,aAAa,CAAC;CAGhB,MAAM,WAAW;EAAE,QAAQ,QAAQ,MAAM;EAAiB,OAAO,QAAQ,MAAM;CAAM;CACrF,QAAQ,MAAM,kBAAkB;CAChC,QAAQ,MAAM,QAAQ;CAEtB,aAAa;EACX,QAAQ,MAAM,kBAAkB,SAAS;EACzC,QAAQ,MAAM,QAAQ,SAAS;CACjC;AACF;;;;;;AAOA,SAAgB,yBACd,YACA,QAAgB,OAAO,WAAW,cAAc,IAAI,OAAO,YAC3D,WAAW,MACF;CACT,IAAI,eAAe,QAAQ,OAAO,SAAS;CAC3C,IAAI,eAAe,UAAU,OAAO;CAEpC,OAAO,SAAS;AAClB;;AAGA,SAAgB,sBAA8B;CAC5C,IAAI,OAAO,WAAW,aAAa,OAAO;CAE1C,MAAM,MAAM,WAAW,iBAAiB,SAAS,eAAe,CAAC,CAAC,QAAQ,KAAK;CAE/E,OAAO,KAAK,IAAI,OAAO,aAAa,GAAG,KAAK,GAAG;AACjD;;;;;;;;AASA,SAAgB,qBAAqB,MAAsB,eAAe,KAAc;CACtF,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,aAAa,qBAAqB,CAAC,CAAC,SAC/E,OAAO;CAMT,SAHe,MAAM,QAAqB,yBAAuB,CAAC,EAAE,cAAA,EAC9C,eAAe,OAAO,cAE7B,oBAAoB,KAAK;AAC1C;;;;;;;;AASA,SAAgB,sBAAsB,MAAsB,WAAW,MAAe;CACpF,IAAI,OAAO,WAAW,eAAe,CAAC,OAAO,aAAa,qBAAqB,CAAC,CAAC,SAC/E,OAAO;CAGT,IAAI,CAAC,qBAAqB,IAAI,GAAG,OAAO;CAExC,MAAM,aAAa,qBAAqB;CACxC,MAAM,SAAS,MAAM,QAAqB,yBAAuB,CAAC,EAAE;CAIpE,OAAO,yBAAyB,YAF9B,eAAe,SAAS,OAAO,aAAc,QAAQ,eAAe,OAAO,YAE1B,QAAQ;AAC7D;;AAEA,MAAa,0BAA0B;;AAGvC,SAAgB,qBAAqB,OAA0C;CAC7E,IAAI;EACF,IAAI,OAAO,iBAAiB,aAAa,OAAO;EAChD,IAAI,OAAO,aAAa,QAAQ,yBAAyB,KAAK;EAE9D,OAAO,aAAa,QAAQ,uBAAuB;CACrD,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,eAAsB,gBACpB,QACA,OACA,SAC4B;CAC5B,MAAM,WAAW,MAAM,eACrB,QACA;EAAE;EAAO,eAAe,qBAAqB;CAAE,GAC/C,OACF;CAIA,IAAI,SAAS,OAAO,eAAe,iBAAiB,SAAS,OAAO,aAAa;CAEjF,OAAO,SAAS;AAClB"}
|
package/dist/csp.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/csp.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The page's Content Security Policy nonce, read from the server-rendered tags that carry it
|
|
4
|
+
* (the root view's inline theme `<script>`/`<style>`, Vite's tags). Elements AlfaKit and
|
|
5
|
+
* Inertia add at runtime (the progress bar's `<style>`, on-request assets) take it, so a
|
|
6
|
+
* nonce-based policy (docs: advanced/security § Content Security Policy) allows them.
|
|
7
|
+
* `undefined` on the server and on pages without a nonce.
|
|
8
|
+
*/
|
|
9
|
+
export declare function documentNonce(): string | undefined;
|
|
10
|
+
//#endregion
|
|
11
|
+
//# sourceMappingURL=csp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp.d.ts","names":[],"sources":["../src/csp.ts"],"mappings":";;;;;;;;wBAOgB"}
|
package/dist/csp.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/csp.ts
|
|
2
|
+
/**
|
|
3
|
+
* The page's Content Security Policy nonce, read from the server-rendered tags that carry it
|
|
4
|
+
* (the root view's inline theme `<script>`/`<style>`, Vite's tags). Elements AlfaKit and
|
|
5
|
+
* Inertia add at runtime (the progress bar's `<style>`, on-request assets) take it, so a
|
|
6
|
+
* nonce-based policy (docs: advanced/security § Content Security Policy) allows them.
|
|
7
|
+
* `undefined` on the server and on pages without a nonce.
|
|
8
|
+
*/
|
|
9
|
+
function documentNonce() {
|
|
10
|
+
if (typeof document === "undefined") return void 0;
|
|
11
|
+
return document.querySelector("script[nonce], style[nonce]")?.nonce || void 0;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { documentNonce };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=csp.js.map
|
package/dist/csp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp.js","names":[],"sources":["../src/csp.ts"],"sourcesContent":["/**\n * The page's Content Security Policy nonce, read from the server-rendered tags that carry it\n * (the root view's inline theme `<script>`/`<style>`, Vite's tags). Elements AlfaKit and\n * Inertia add at runtime (the progress bar's `<style>`, on-request assets) take it, so a\n * nonce-based policy (docs: advanced/security § Content Security Policy) allows them.\n * `undefined` on the server and on pages without a nonce.\n */\nexport function documentNonce(): string | undefined {\n if (typeof document === 'undefined') return undefined\n\n return document.querySelector<HTMLElement>('script[nonce], style[nonce]')?.nonce || undefined\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,gBAAoC;CAClD,IAAI,OAAO,aAAa,aAAa,OAAO,KAAA;CAE5C,OAAO,SAAS,cAA2B,6BAA6B,CAAC,EAAE,SAAS,KAAA;AACtF"}
|
package/dist/file-size.js
CHANGED
package/dist/file-size.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-size.js","names":[],"sources":["../src/file-size.ts"],"sourcesContent":["import type { LocaleFormatter } from './i18n'\n\nconst SIZE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'] as const\n\n/** A human file size (`1.4 MB`) in the formatter's locale. Kept apart from file-manager.ts so pickers don't pull the whole controller. */\nexport function fileManagerSize(\n bytes: number | null | undefined,\n formatter: Pick<LocaleFormatter, 'number'>,\n): string {\n let value = typeof bytes === 'number' && Number.isFinite(bytes) && bytes > 0 ? bytes : 0\n let unit = 0\n\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024\n unit++\n }\n\n return formatter.number(value, {\n style: 'unit',\n unit: SIZE_UNITS[unit],\n unitDisplay: 'short',\n maximumFractionDigits: unit === 0 ? 0 : 1,\n })\n}\n"],"mappings":";AAEA,MAAM,aAAa;CAAC;CAAQ;CAAY;CAAY;CAAY;AAAU;;AAG1E,SAAgB,gBACd,OACA,WACQ;CACR,IAAI,QAAQ,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ;CACvF,IAAI,OAAO;CAEX,OAAO,SAAS,QAAQ,OAAO,WAAW,SAAS,GAAG;EACpD,SAAS;EACT;CACF;CAEA,OAAO,UAAU,OAAO,OAAO;EAC7B,OAAO;EACP,MAAM,WAAW;
|
|
1
|
+
{"version":3,"file":"file-size.js","names":[],"sources":["../src/file-size.ts"],"sourcesContent":["import type { LocaleFormatter } from './i18n'\n\nconst SIZE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'] as const\n\n/** A human file size (`1.4 MB`) in the formatter's locale. Kept apart from file-manager.ts so pickers don't pull the whole controller. */\nexport function fileManagerSize(\n bytes: number | null | undefined,\n formatter: Pick<LocaleFormatter, 'number'>,\n): string {\n let value = typeof bytes === 'number' && Number.isFinite(bytes) && bytes > 0 ? bytes : 0\n let unit = 0\n\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024\n unit++\n }\n\n return formatter.number(value, {\n style: 'unit',\n unit: SIZE_UNITS[unit],\n // Bytes have no short form in most locales (\"11 byte\"): the long one is pluralised.\n unitDisplay: unit === 0 ? 'long' : 'short',\n maximumFractionDigits: unit === 0 ? 0 : 1,\n })\n}\n"],"mappings":";AAEA,MAAM,aAAa;CAAC;CAAQ;CAAY;CAAY;CAAY;AAAU;;AAG1E,SAAgB,gBACd,OACA,WACQ;CACR,IAAI,QAAQ,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ;CACvF,IAAI,OAAO;CAEX,OAAO,SAAS,QAAQ,OAAO,WAAW,SAAS,GAAG;EACpD,SAAS;EACT;CACF;CAEA,OAAO,UAAU,OAAO,OAAO;EAC7B,OAAO;EACP,MAAM,WAAW;EAEjB,aAAa,SAAS,IAAI,SAAS;EACnC,uBAAuB,SAAS,IAAI,IAAI;CAC1C,CAAC;AACH"}
|
package/dist/form.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NotificationSchema } from "./schema/notifications.js";
|
|
2
2
|
import { FormPayload, SchemaRefreshResponse } from "./schema/form.js";
|
|
3
3
|
import { IconPickerResponse } from "./schema/icon-picker.js";
|
|
4
|
-
import { FieldSchema, SchemaComponent } from "./schema/components.js";
|
|
4
|
+
import { FieldSchema, SchemaComponent, TabSchema } from "./schema/components.js";
|
|
5
5
|
import { ModalTableSelectResponse } from "./schema/advanced-fields.js";
|
|
6
6
|
import { Store } from "./store.js";
|
|
7
7
|
import { TableQuery } from "./table.js";
|
|
@@ -40,7 +40,16 @@ export interface FormStoreOptions {
|
|
|
40
40
|
onNotifications?: (notifications: NotificationSchema[]) => void;
|
|
41
41
|
/** Called after a live refresh was merged (e.g. to reload what depends on the form). */
|
|
42
42
|
onRefreshed?: (path: string | undefined) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Prefix of the fields' DOM ids (`{idPrefix}-{statePath}`), default `alfakit`. Forms shown
|
|
45
|
+
* over another form (action modals) use their own, so ids never repeat on the page.
|
|
46
|
+
*/
|
|
47
|
+
idPrefix?: string;
|
|
43
48
|
}
|
|
49
|
+
/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */
|
|
50
|
+
export declare function formIdPrefix(scope: string): string;
|
|
51
|
+
/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */
|
|
52
|
+
export declare function fieldDomId(prefix: string, statePath: string): string;
|
|
44
53
|
/**
|
|
45
54
|
* Walk a component tree and collect every field (including those inside Repeater items).
|
|
46
55
|
*/
|
|
@@ -59,6 +68,34 @@ export declare function findFieldByPath(components: SchemaComponent[], path: str
|
|
|
59
68
|
* still move between steps while errors are shown.
|
|
60
69
|
*/
|
|
61
70
|
export declare function wizardStepToReveal(steps: SchemaComponent[], previousErrors: Record<string, unknown>, errors: Record<string, unknown>): number | null;
|
|
71
|
+
/**
|
|
72
|
+
* Whether a layout component (a tab, a section) holds a field with a validation error:
|
|
73
|
+
* one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the
|
|
74
|
+
* component is deferred and its fields aren't loaded, one of its `deferredPaths`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function componentHasErrors(component: Pick<SchemaComponent, 'schema' | 'deferred' | 'deferredPaths'>, errors: Record<string, unknown>): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The value a tab is written to the query string as (`persistTabInQueryString()`): its
|
|
79
|
+
* readable `queryValue` (`billing-address`), else its key.
|
|
80
|
+
*/
|
|
81
|
+
export declare function tabQueryValue(tab: Pick<TabSchema, 'key' | 'queryValue'>): string;
|
|
82
|
+
/**
|
|
83
|
+
* The key of the tab a query-string value names: a tab's `queryValue`, or its key (links
|
|
84
|
+
* written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.
|
|
85
|
+
*/
|
|
86
|
+
export declare function tabFromQuery(tabs: Array<Pick<TabSchema, 'key' | 'queryValue'>>, value: string | null | undefined): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* The tab to show after the errors change: when new error keys arrived (a submit or
|
|
89
|
+
* validation response) and the active tab has none, the first tab with an error.
|
|
90
|
+
*/
|
|
91
|
+
export declare function tabToReveal(tabs: Array<Pick<SchemaComponent, 'key' | 'schema' | 'deferred' | 'deferredPaths'>>, activeKey: string, previousErrors: Record<string, unknown>, errors: Record<string, unknown>): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* The validation message a field shows: its own, else the first of its items' (`labels.2`
|
|
94
|
+
* for a tag refused by `nestedRecursiveRules()`, `attachments.0` for one upload). Those
|
|
95
|
+
* errors have no field of their own, so without this the form refused to save with no
|
|
96
|
+
* message anywhere. Deeper paths (`items.0.name`) belong to item fields and are left out.
|
|
97
|
+
*/
|
|
98
|
+
export declare function fieldError(errors: Readonly<Record<string, readonly string[] | undefined>>, path: string): string | undefined;
|
|
62
99
|
/**
|
|
63
100
|
* Framework-agnostic form state: values addressed by `statePath`, validation errors,
|
|
64
101
|
* dirty tracking and server round-trips for `live()` fields.
|
|
@@ -90,6 +127,8 @@ export declare class FormStore {
|
|
|
90
127
|
setToken(token: string): void;
|
|
91
128
|
/** Context component name of the schema (`form`, `action:table.edit`, …). */
|
|
92
129
|
get component(): string;
|
|
130
|
+
/** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */
|
|
131
|
+
get idPrefix(): string;
|
|
93
132
|
get record(): string | number | null;
|
|
94
133
|
get token(): string;
|
|
95
134
|
get optionsUrl(): string | undefined;
|
|
@@ -131,6 +170,19 @@ export declare class FormStore {
|
|
|
131
170
|
* `afterStateUpdated` hooks, and replace components + values with the response.
|
|
132
171
|
*/
|
|
133
172
|
refresh(path?: string): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Resolves once no live refresh is waiting or in flight: debounced refreshes are sent
|
|
175
|
+
* now and awaited. Submits wait for it, so a click that blurs a `live(onBlur: true)` field
|
|
176
|
+
* sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.
|
|
177
|
+
*/
|
|
178
|
+
settle(): Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Run `callback` once the form is settled (see `settle()`): straight away when no live
|
|
181
|
+
* refresh is waiting or in flight, so a submit stays synchronous in the usual case.
|
|
182
|
+
*/
|
|
183
|
+
whenSettled(callback: () => void): void;
|
|
184
|
+
private refreshInFlight;
|
|
185
|
+
private sendRefresh;
|
|
134
186
|
/**
|
|
135
187
|
* Merge a refresh response (full or partial) for the state `sent` with the request.
|
|
136
188
|
* Paths in `edits` changed while the request was in flight and keep their client value;
|
package/dist/form.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.d.ts","names":[],"sources":["../src/form.ts"],"mappings":";;;;;;;;iBAqBiB;EACf,YAAY;EACZ,QAAQ;EACR,QAAQ;;EAER,OAAO;;EAEP;EACA;EACA,SAAS;;iBAGM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,eAAe;EACf,WAAW;;EAEX,mBAAmB,eAAe;;EAElC,eAAe;;;;;
|
|
1
|
+
{"version":3,"file":"form.d.ts","names":[],"sources":["../src/form.ts"],"mappings":";;;;;;;;iBAqBiB;EACf,YAAY;EACZ,QAAQ;EACR,QAAQ;;EAER,OAAO;;EAEP;EACA;EACA,SAAS;;iBAGM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,eAAe;EACf,WAAW;;EAEX,mBAAmB,eAAe;;EAElC,eAAe;;;;;EAKf;;;wBAIc,aAAa;;wBAKb,WAAW,gBAAgB;;;;wBAO3B,cACd,YAAY,mBACZ,OAAM,gBACL;;;;;;;;wBA2Ba,gBACd,YAAY,mBACZ,cACA,SAAQ,0BACP;;;;;;wBAmEa,mBACd,OAAO,mBACP,gBAAgB,yBAChB,QAAQ;;;;;;wBAkBM,mBACd,WAAW,KAAK,2DAChB,QAAQ;;;;;wBAkBM,cAAc,KAAK,KAAK;;;;;wBAQxB,aACd,MAAM,MAAM,KAAK,mCACjB;;;;;wBAcc,YACd,MAAM,MAAM,KAAK,oEACjB,mBACA,gBAAgB,yBAChB,QAAQ;;;;;;;wBA4BM,WACd,QAAQ,SAAS,gDACjB;;;;;qBAiBW;UAyBD;WAxBM,OAAO,MAAM;mBAEZ;UAET;;;;;UAMA;;;;;;UAOA;;UAGA;EAGN,YAAA,SAAS,aACD,SAAS;MAkBf,SAAS;;;;;EAQb,SAAS;;MAKL;;MAKA;MAIA;MAIA;MAIA;MAIA;;MAKA,uBAAuB;;EAK3B,kBAAkB;;;;;;EAclB,gBAAgB;EAahB,UAAU,WAAW,OAAO;EAM5B,SAAS,aAAa,cAAc,WAAW,IAAI;;;;;EAQnD,SAAS,cAAc,gBAAgB;IAAW;;;EAoClD,KAAK;;EASL,KAAK,SAAS,aAAa;IAAW;;EAYtC,UAAU,QAAQ;EAUlB;EAIA,SAAS;EAIT,cAAc;MAIV;EAIJ,UAAU,eAAe;UAMjB;;;;;EAiBR,QAAQ,gBAAgB;;;;;;EAgBlB,UAAU;;;;;EAYhB,YAAY;UAYJ;UAEM;;;;;;;EAoEd,cACE,UAAU,uBACV,MAAM,yBACN,QAAO;;;;;;EAqCH,aAAa,iBAAiB,uBAAuB;;EA8CrD,cACJ,cACA,iBACC,QAAQ;IAAQ;IAAe;;EAe5B,aACJ,cACA,QAAQ,yBACP,QAAQ;;EAgBL,YACJ,cACA;IAAS;IAAiB;IAAqB;KAC/C,SAAS,cACR,QAAQ;;;;;EAqBL,WACJ,cACA,oBACA,OAAO,YACP,SAAS,cACR,QAAQ;EAgBX;;;;;;;;wBAkCc,aACd;EAAQ;EAAgB;sBACxB;EAAS;EAAa;EAAmB;;;iBAY1B;EACf;EACA,KAAK;EACL;EACA;EACA;;;;;;;;wBASc,eAAe,OAAO,cAAc;iBAuBnC;EACf;EACA,OAAO;;;wBAIO,mBACd,kBACA,eAAe;wBAKD,mBAAmB,kBAAkB;wBAIrC,iBAAiB,kBAAkB,cAAc;wBAYjD,kBAAkB,kBAAkB;;;;;YAWxC,aAAa,KAAK,eAAe;wBAiB7B,iBAAiB,GAC/B,OAAO,aAAa,IACpB,cACA,aACC,aAAa;wBAYA,mBAAmB,GAAG,OAAO,aAAa,IAAI,kBAAkB,aAAa;wBAM7E,kBAAkB,GAAG,OAAO,aAAa,IAAI,iBAAiB,aAAa"}
|
package/dist/form.js
CHANGED
|
@@ -9,6 +9,14 @@ import { mergeRefreshResponse } from "./partial.js";
|
|
|
9
9
|
import { isField } from "./schema/components.js";
|
|
10
10
|
import { debounce } from "./utils/debounce.js";
|
|
11
11
|
//#region src/form.ts
|
|
12
|
+
/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */
|
|
13
|
+
function formIdPrefix(scope) {
|
|
14
|
+
return `alfakit-${scope.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
15
|
+
}
|
|
16
|
+
/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */
|
|
17
|
+
function fieldDomId(prefix, statePath) {
|
|
18
|
+
return `${prefix}-${statePath.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* Walk a component tree and collect every field (including those inside Repeater items).
|
|
14
22
|
*/
|
|
@@ -64,6 +72,56 @@ function wizardStepToReveal(steps, previousErrors, errors) {
|
|
|
64
72
|
return index === -1 ? null : index;
|
|
65
73
|
}
|
|
66
74
|
/**
|
|
75
|
+
* Whether a layout component (a tab, a section) holds a field with a validation error:
|
|
76
|
+
* one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the
|
|
77
|
+
* component is deferred and its fields aren't loaded, one of its `deferredPaths`.
|
|
78
|
+
*/
|
|
79
|
+
function componentHasErrors(component, errors) {
|
|
80
|
+
const keys = Object.keys(errors).filter((key) => errors[key]);
|
|
81
|
+
if (keys.length === 0) return false;
|
|
82
|
+
return (component.deferred && !component.schema?.length ? component.deferredPaths ?? [] : collectFields(component.schema ?? []).map((field) => field.statePath)).some((path) => keys.some((key) => key === path || key.startsWith(`${path}.`)));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The value a tab is written to the query string as (`persistTabInQueryString()`): its
|
|
86
|
+
* readable `queryValue` (`billing-address`), else its key.
|
|
87
|
+
*/
|
|
88
|
+
function tabQueryValue(tab) {
|
|
89
|
+
return tab.queryValue ?? tab.key;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The key of the tab a query-string value names: a tab's `queryValue`, or its key (links
|
|
93
|
+
* written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.
|
|
94
|
+
*/
|
|
95
|
+
function tabFromQuery(tabs, value) {
|
|
96
|
+
if (!value) return null;
|
|
97
|
+
return (tabs.find((tab) => tab.queryValue === value) ?? tabs.find((tab) => tab.key === value))?.key ?? null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The tab to show after the errors change: when new error keys arrived (a submit or
|
|
101
|
+
* validation response) and the active tab has none, the first tab with an error.
|
|
102
|
+
*/
|
|
103
|
+
function tabToReveal(tabs, activeKey, previousErrors, errors) {
|
|
104
|
+
if (!Object.keys(errors).some((key) => !(key in previousErrors))) return null;
|
|
105
|
+
const active = tabs.find((tab) => tab.key === activeKey);
|
|
106
|
+
if (active && componentHasErrors(active, errors)) return null;
|
|
107
|
+
return tabs.find((tab) => componentHasErrors(tab, errors))?.key ?? null;
|
|
108
|
+
}
|
|
109
|
+
/** Whether `key` is the error of one item of the list field at `path` (`labels.2`). */
|
|
110
|
+
function isItemErrorKey(key, path) {
|
|
111
|
+
return key.length > path.length + 1 && key.startsWith(`${path}.`) && /^\d+$/.test(key.slice(path.length + 1));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The validation message a field shows: its own, else the first of its items' (`labels.2`
|
|
115
|
+
* for a tag refused by `nestedRecursiveRules()`, `attachments.0` for one upload). Those
|
|
116
|
+
* errors have no field of their own, so without this the form refused to save with no
|
|
117
|
+
* message anywhere. Deeper paths (`items.0.name`) belong to item fields and are left out.
|
|
118
|
+
*/
|
|
119
|
+
function fieldError(errors, path) {
|
|
120
|
+
const own = errors[path]?.[0];
|
|
121
|
+
if (own) return own;
|
|
122
|
+
for (const key of Object.keys(errors)) if (isItemErrorKey(key, path) && errors[key]?.[0]) return errors[key][0];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
67
125
|
* Framework-agnostic form state: values addressed by `statePath`, validation errors,
|
|
68
126
|
* dirty tracking and server round-trips for `live()` fields.
|
|
69
127
|
*/
|
|
@@ -117,6 +175,10 @@ var FormStore = class {
|
|
|
117
175
|
get component() {
|
|
118
176
|
return this.options.component;
|
|
119
177
|
}
|
|
178
|
+
/** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */
|
|
179
|
+
get idPrefix() {
|
|
180
|
+
return this.options.idPrefix ?? "alfakit";
|
|
181
|
+
}
|
|
120
182
|
get record() {
|
|
121
183
|
return this.options.record ?? null;
|
|
122
184
|
}
|
|
@@ -174,6 +236,7 @@ var FormStore = class {
|
|
|
174
236
|
dirty.add(path);
|
|
175
237
|
const errors = { ...state.errors };
|
|
176
238
|
delete errors[path];
|
|
239
|
+
for (const key of Object.keys(errors)) if (isItemErrorKey(key, path)) delete errors[key];
|
|
177
240
|
return {
|
|
178
241
|
...state,
|
|
179
242
|
values: setPath(state.values, path, value),
|
|
@@ -211,7 +274,7 @@ var FormStore = class {
|
|
|
211
274
|
this.store.update({ errors: {} });
|
|
212
275
|
}
|
|
213
276
|
getError(path) {
|
|
214
|
-
return this.state.errors
|
|
277
|
+
return fieldError(this.state.errors, path);
|
|
215
278
|
}
|
|
216
279
|
setProcessing(processing) {
|
|
217
280
|
this.store.update({ processing });
|
|
@@ -236,7 +299,36 @@ var FormStore = class {
|
|
|
236
299
|
* Send the current state to the server, which re-evaluates closures and
|
|
237
300
|
* `afterStateUpdated` hooks, and replace components + values with the response.
|
|
238
301
|
*/
|
|
239
|
-
|
|
302
|
+
refresh(path) {
|
|
303
|
+
const request = this.sendRefresh(path);
|
|
304
|
+
this.refreshInFlight = request;
|
|
305
|
+
request.finally(() => {
|
|
306
|
+
if (this.refreshInFlight === request) this.refreshInFlight = null;
|
|
307
|
+
});
|
|
308
|
+
return request;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Resolves once no live refresh is waiting or in flight: debounced refreshes are sent
|
|
312
|
+
* now and awaited. Submits wait for it, so a click that blurs a `live(onBlur: true)` field
|
|
313
|
+
* sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.
|
|
314
|
+
*/
|
|
315
|
+
async settle() {
|
|
316
|
+
for (const debounced of this.liveDebouncers.values()) debounced.flush();
|
|
317
|
+
while (this.refreshInFlight) await this.refreshInFlight.catch(() => void 0);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Run `callback` once the form is settled (see `settle()`): straight away when no live
|
|
321
|
+
* refresh is waiting or in flight, so a submit stays synchronous in the usual case.
|
|
322
|
+
*/
|
|
323
|
+
whenSettled(callback) {
|
|
324
|
+
if (![...this.liveDebouncers.values()].some((debounced) => debounced.pending) && !this.refreshInFlight) {
|
|
325
|
+
callback();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.settle().then(callback);
|
|
329
|
+
}
|
|
330
|
+
refreshInFlight = null;
|
|
331
|
+
async sendRefresh(path) {
|
|
240
332
|
if (!this.options.refreshUrl) return;
|
|
241
333
|
this.refreshController?.abort();
|
|
242
334
|
const controller = new AbortController();
|
|
@@ -261,7 +353,7 @@ var FormStore = class {
|
|
|
261
353
|
if (this.editsInFlight === edits) this.editsInFlight = null;
|
|
262
354
|
if (!this.applyResponse(response, sent, edits)) {
|
|
263
355
|
this.fingerprints = void 0;
|
|
264
|
-
await this.
|
|
356
|
+
await this.sendRefresh(path);
|
|
265
357
|
return;
|
|
266
358
|
}
|
|
267
359
|
this.options.onRefreshed?.(path);
|
|
@@ -468,6 +560,6 @@ function cloneIndexedState(state, cloned) {
|
|
|
468
560
|
return result;
|
|
469
561
|
}
|
|
470
562
|
//#endregion
|
|
471
|
-
export { FormStore, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, collectFields, findFieldByPath, formIdentity, isLeavingVisit, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, wizardStepToReveal };
|
|
563
|
+
export { FormStore, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, collectFields, componentHasErrors, fieldDomId, fieldError, findFieldByPath, formIdPrefix, formIdentity, isLeavingVisit, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, tabFromQuery, tabQueryValue, tabToReveal, wizardStepToReveal };
|
|
472
564
|
|
|
473
565
|
//# sourceMappingURL=form.js.map
|
package/dist/form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.js","names":[],"sources":["../src/form.ts"],"sourcesContent":["import { modalTableQuery } from './advanced-fields'\nimport { debugRecorder } from './debug'\nimport { reportRequestError } from './error-notifications'\nimport { HttpError, requestJson } from './http'\nimport { mergeRefreshResponse } from './partial'\nimport type { ModalTableSelectResponse } from './schema/advanced-fields'\nimport type { FieldSchema, SchemaComponent } from './schema/components'\nimport { isField } from './schema/components'\nimport type {\n FormPayload,\n SchemaRefreshResponse,\n StepValidationHaltedResponse,\n} from './schema/form'\nimport type { IconPickerResponse } from './schema/icon-picker'\nimport type { NotificationSchema } from './schema/notifications'\nimport { createStore, type Store } from './store'\nimport type { TableQuery } from './table'\nimport { cloneState } from './utils/clone'\nimport { debounce, type Debounced } from './utils/debounce'\nimport { getPath, setPath } from './utils/path'\n\nexport interface FormState {\n components: SchemaComponent[]\n values: Record<string, unknown>\n errors: Record<string, string[]>\n /** State paths that changed since the last fill. */\n dirty: Set<string>\n /** True while a live refresh or submit is in flight. */\n processing: boolean\n refreshing: boolean\n columns: FormPayload['columns']\n}\n\nexport interface FormStoreOptions {\n /** Encrypted page context token. */\n token: string\n /** Context component name (`form`, `action:table.edit`, …). */\n component: string\n /** Record key when the schema belongs to a table action. */\n record?: string | number | null\n /** URL of the schema refresh endpoint. */\n refreshUrl?: string\n /** URL of the select options endpoint. */\n optionsUrl?: string\n /** URL of the step validation endpoint (wizards validate each step on \"Next\"). */\n validateUrl?: string\n /**\n * Extra parameters for every request (refresh, options). The modal form of a field\n * action sends its owning form's state (`ownerState`) so the server can find the field.\n */\n params?: () => Record<string, unknown>\n onError?: (error: unknown) => void\n /** Shows notifications the server sent with a JSON answer (a halted wizard step). */\n onNotifications?: (notifications: NotificationSchema[]) => void\n /** Called after a live refresh was merged (e.g. to reload what depends on the form). */\n onRefreshed?: (path: string | undefined) => void\n}\n\n/**\n * Walk a component tree and collect every field (including those inside Repeater items).\n */\nexport function collectFields(\n components: SchemaComponent[],\n into: FieldSchema[] = [],\n): FieldSchema[] {\n for (const component of components) {\n if (isField(component)) {\n into.push(component)\n\n if (component.type === 'Repeater' || component.type === 'Builder') {\n for (const item of component.items) {\n collectFields(item.schema, into)\n }\n }\n }\n\n if (component.schema) {\n collectFields(component.schema, into)\n }\n }\n\n return into\n}\n\n/**\n * The field at `path`. Items the browser added (or cloned) before the server has seen\n * them have no per-item schema yet: their fields are found in the Repeater's `template`\n * (or the Builder block's schema, preferring the block named by the item's `type` in\n * `values`), whose `__INDEX__` placeholders stand for any item index, so `live()` fields\n * of fresh items refresh like any other.\n */\nexport function findFieldByPath(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown> = {},\n): FieldSchema | undefined {\n return (\n collectFields(components).find((field) => field.statePath === path) ??\n findTemplateField(components, path, values)\n )\n}\n\nfunction findTemplateField(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown>,\n): FieldSchema | undefined {\n for (const field of collectFields(components)) {\n for (const template of itemTemplates(field, path, values)) {\n const found =\n collectFields(template).find((candidate) =>\n templatePathMatches(candidate.statePath, path),\n ) ?? findTemplateField(template, path, values)\n\n if (found) return found\n }\n }\n\n return undefined\n}\n\n/** The blank item schemas of a Repeater or Builder (the item's own block first). */\nfunction itemTemplates(\n field: FieldSchema,\n path: string,\n values: Record<string, unknown>,\n): SchemaComponent[][] {\n if (field.type === 'Repeater') {\n return [(field as unknown as { template?: SchemaComponent[] }).template ?? []]\n }\n\n if (field.type !== 'Builder') return []\n\n const blocks =\n (field as unknown as { blocks?: Array<{ name: string; schema: SchemaComponent[] }> }).blocks ??\n []\n const item = new RegExp(`^(${templatePathPattern(field.statePath)}\\\\.\\\\d+)\\\\.`).exec(path)\n const type = item ? getPath<unknown>(values, `${item[1]}.type`) : undefined\n\n return [...blocks]\n .sort((a, b) => Number(b.name === type) - Number(a.name === type))\n .map((block) => block.schema)\n}\n\nfunction templatePathPattern(statePath: string): string {\n return statePath\n .split('__INDEX__')\n .map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('\\\\d+')\n}\n\nfunction templatePathMatches(statePath: string, path: string): boolean {\n return (\n statePath.includes('__INDEX__') && new RegExp(`^${templatePathPattern(statePath)}$`).test(path)\n )\n}\n\n/**\n * The wizard step to reveal after the errors change: the first step with an error, but\n * only when new error keys arrived (a submit or validation response), so the user can\n * still move between steps while errors are shown.\n */\nexport function wizardStepToReveal(\n steps: SchemaComponent[],\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): number | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const index = steps.findIndex((step) =>\n collectFields(step.schema ?? []).some((field) => errors[field.statePath]),\n )\n\n return index === -1 ? null : index\n}\n\n/**\n * Framework-agnostic form state: values addressed by `statePath`, validation errors,\n * dirty tracking and server round-trips for `live()` fields.\n */\nexport class FormStore {\n public readonly store: Store<FormState>\n\n private readonly liveDebouncers = new Map<string, Debounced<[]>>()\n\n private refreshController: AbortController | null = null\n\n /**\n * The values as the server last sent them, so a refresh can tell `afterStateUpdated`\n * hooks what the field held before the change (`$old`).\n */\n private serverValues: Record<string, unknown>\n\n /**\n * Paths the user changed while a live refresh was in flight. Only these keep their\n * client value when the response is merged; every other path takes the server's value\n * (including values set by `$set` in `afterStateUpdated` hooks).\n */\n private editsInFlight: Set<string> | null = null\n\n /** The fingerprints of the components the store holds (see `PartialFormPayload`). */\n private fingerprints: Record<string, string> | undefined\n\n constructor(\n payload: FormPayload,\n private options: FormStoreOptions,\n ) {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store = createStore<FormState>({\n components: payload.components,\n values: payload.state ?? {},\n errors: {},\n dirty: new Set(),\n processing: false,\n refreshing: false,\n columns: payload.columns,\n })\n\n // Listed by the developer debug panel while it is enabled; a no-op otherwise.\n debugRecorder.registerForm(this)\n }\n\n get state(): FormState {\n return this.store.get()\n }\n\n /**\n * Swap in a fresher context token (every response re-issues it) without touching the\n * form state, so live refreshes keep working after unrelated reloads.\n */\n setToken(token: string): void {\n this.options = { ...this.options, token }\n }\n\n /** Context component name of the schema (`form`, `action:table.edit`, …). */\n get component(): string {\n return this.options.component\n }\n\n get record(): string | number | null {\n return this.options.record ?? null\n }\n\n get token(): string {\n return this.options.token\n }\n\n get optionsUrl(): string | undefined {\n return this.options.optionsUrl\n }\n\n get validateUrl(): string | undefined {\n return this.options.validateUrl\n }\n\n /** The fingerprints of the held components, sent so responses can be partial. */\n get currentFingerprints(): Record<string, string> | undefined {\n return this.fingerprints\n }\n\n /** The parameters that identify this schema to the internal endpoints. */\n requestContext(): Record<string, unknown> {\n return {\n token: this.options.token,\n component: this.options.component,\n record: this.options.record ?? undefined,\n ...this.options.params?.(),\n }\n }\n\n /**\n * `requestContext()` plus the form state for GET requests (options, picker tables,\n * mentions). States travel as JSON: a query string drops nulls and empty arrays, and with\n * them repeater items that are still blank, so the server couldn't find their fields.\n */\n queryContext(): Record<string, unknown> {\n const context = this.requestContext()\n const ownerState = context.ownerState\n\n return {\n ...context,\n ...(ownerState !== null && typeof ownerState === 'object'\n ? { ownerState: JSON.stringify(ownerState) }\n : {}),\n state: JSON.stringify(this.state.values),\n }\n }\n\n subscribe(listener: (state: FormState) => void): () => void {\n return this.store.subscribe(listener)\n }\n\n // ---------------------------------------------------------------- Values\n\n getValue<T = unknown>(path: string, fallback?: T): T {\n return getPath<T>(this.state.values, path, fallback)\n }\n\n /**\n * Set a value. Live fields schedule a server refresh (debounced); `onBlur` fields\n * refresh via `blur()` instead.\n */\n setValue(path: string, value: unknown, options: { silent?: boolean } = {}): void {\n const current = this.getValue(path)\n\n if (Object.is(current, value)) {\n return\n }\n\n this.store.set((state) => {\n const dirty = new Set(state.dirty)\n dirty.add(path)\n\n const errors = { ...state.errors }\n delete errors[path]\n\n return { ...state, values: setPath(state.values, path, value), dirty, errors }\n })\n\n this.editsInFlight?.add(path)\n\n if (options.silent) {\n return\n }\n\n const field = this.findField(path)\n\n if (field?.live && !field.live.onBlur) {\n this.scheduleRefresh(path, field.live.debounce ?? 150)\n }\n }\n\n /** Call when a field loses focus so `live(onBlur: true)` fields refresh. */\n blur(path: string): void {\n const field = this.findField(path)\n\n if (field?.live?.onBlur && this.state.dirty.has(path)) {\n void this.refresh(path)\n }\n }\n\n /** Replace the whole state (e.g. after the server returns a refreshed payload). */\n fill(payload: FormPayload, options: { keepDirty?: boolean } = {}): void {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store.set((state) => ({\n ...state,\n components: payload.components,\n values: payload.state ?? {},\n columns: payload.columns ?? state.columns,\n dirty: options.keepDirty ? state.dirty : new Set(),\n }))\n }\n\n setErrors(errors: Record<string, string[] | string>): void {\n const normalized: Record<string, string[]> = {}\n\n for (const [key, value] of Object.entries(errors)) {\n normalized[key] = Array.isArray(value) ? value : [value]\n }\n\n this.store.update({ errors: normalized })\n }\n\n clearErrors(): void {\n this.store.update({ errors: {} })\n }\n\n getError(path: string): string | undefined {\n return this.state.errors[path]?.[0]\n }\n\n setProcessing(processing: boolean): void {\n this.store.update({ processing })\n }\n\n get isDirty(): boolean {\n return this.state.dirty.size > 0\n }\n\n findField(path: string): FieldSchema | undefined {\n return findFieldByPath(this.state.components, path, this.state.values)\n }\n\n // ---------------------------------------------------------------- Live refresh\n\n private scheduleRefresh(path: string, wait: number): void {\n let debounced = this.liveDebouncers.get(path)\n\n if (!debounced) {\n debounced = debounce(() => {\n void this.refresh(path)\n }, wait)\n this.liveDebouncers.set(path, debounced)\n }\n\n debounced()\n }\n\n /**\n * Send the current state to the server, which re-evaluates closures and\n * `afterStateUpdated` hooks, and replace components + values with the response.\n */\n async refresh(path?: string): Promise<void> {\n if (!this.options.refreshUrl) {\n return\n }\n\n this.refreshController?.abort()\n const controller = new AbortController()\n this.refreshController = controller\n // The request carries the current state, so only edits made from now on are newer\n // than what the server sees.\n const edits = new Set<string>()\n this.editsInFlight = edits\n\n this.store.update({ refreshing: true })\n const sent = this.state.values\n\n try {\n const response = await requestJson<SchemaRefreshResponse>(this.options.refreshUrl, {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: sent,\n path,\n old: path ? (getPath(this.serverValues, path) ?? null) : undefined,\n // With fingerprints the server answers with only what changed.\n fingerprints: this.fingerprints,\n },\n signal: controller.signal,\n })\n\n if (controller.signal.aborted) {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n if (!this.applyResponse(response, sent, edits)) {\n // The patches didn't fit the tree: ask for the whole form instead.\n this.fingerprints = undefined\n await this.refresh(path)\n\n return\n }\n\n this.options.onRefreshed?.(path)\n } catch (error) {\n if ((error as Error).name === 'AbortError') {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n this.store.update({ refreshing: false })\n // Without an `onError`, the panel's error notification for the status is shown.\n ;(this.options.onError ?? reportRequestError)(error)\n }\n }\n\n /**\n * Merge a refresh response (full or partial) for the state `sent` with the request.\n * Paths in `edits` changed while the request was in flight and keep their client value;\n * everything else takes the server's (including values set by `$set`). Returns false\n * when a partial response doesn't fit the tree the store holds.\n */\n applyResponse(\n response: SchemaRefreshResponse,\n sent: Record<string, unknown>,\n edits: Iterable<string> = [],\n ): boolean {\n const merged = mergeRefreshResponse(this.state.components, sent, response)\n\n if (!merged) {\n this.store.update({ refreshing: false })\n\n return false\n }\n\n this.serverValues = merged.values\n this.fingerprints = merged.fingerprints\n\n this.store.set((state) => {\n let values = merged.values\n\n for (const editedPath of edits) {\n values = setPath(values, editedPath, getPath(state.values, editedPath))\n }\n\n return {\n ...state,\n components: merged.components,\n values,\n columns: merged.columns ?? state.columns,\n refreshing: false,\n }\n })\n\n return true\n }\n\n /**\n * Validate the fields of one wizard step (by the Step component's `key`) on the server.\n * Resolves with true when they pass; otherwise the step's errors replace the errors of\n * those fields and false is returned. Without a validation endpoint every step passes.\n */\n async validateStep(stepKey: string, fieldPaths: string[]): Promise<boolean> {\n if (!this.options.validateUrl) {\n return true\n }\n\n try {\n const response = await requestJson<StepValidationHaltedResponse | null>(\n this.options.validateUrl,\n {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: this.state.values,\n step: stepKey,\n },\n },\n )\n\n // A step hook halted: stay on the step, keep the field errors as they are.\n if (response !== null && typeof response === 'object' && response.halted) {\n const notifications = response.notifications ?? []\n\n if (notifications.length > 0) this.options.onNotifications?.(notifications)\n\n return false\n }\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.store.update({\n errors: { ...withoutStepErrors(this.state.errors, fieldPaths), ...error.errors },\n })\n\n return false\n }\n\n ;(this.options.onError ?? reportRequestError)(error)\n\n return false\n }\n\n this.store.update({ errors: withoutStepErrors(this.state.errors, fieldPaths) })\n\n return true\n }\n\n /** Search options for a searchable select at `path`. */\n async searchOptions(\n path: string,\n search: string,\n ): Promise<Array<{ value: string; label: string }>> {\n if (!this.options.optionsUrl) {\n return []\n }\n\n const response = await requestJson<{ options?: Array<{ value: string; label: string }> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, search },\n },\n )\n\n return response.options ?? []\n }\n\n async optionLabels(\n path: string,\n values: Array<string | number>,\n ): Promise<Record<string, string>> {\n if (!this.options.optionsUrl || values.length === 0) {\n return {}\n }\n\n const response = await requestJson<{ labels?: Record<string, string> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, values },\n },\n )\n\n return response.labels ?? {}\n }\n\n /** One page of icons for the IconPicker at `path` (`set`: one of its sets, or all). */\n async searchIcons(\n path: string,\n query: { search?: string; set?: string | null; page?: number },\n signal?: AbortSignal,\n ): Promise<IconPickerResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<IconPickerResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n search: query.search ?? '',\n ...(query.set ? { set: query.set } : {}),\n page: query.page ?? 1,\n },\n signal,\n })\n }\n\n /**\n * The picker table of a ModalTableSelect at `path` (`query` is its table state) and the\n * labels of the records it lists.\n */\n async modalTable(\n path: string,\n identifier: string,\n query: TableQuery,\n signal?: AbortSignal,\n ): Promise<ModalTableSelectResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<ModalTableSelectResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n table: 1,\n ...modalTableQuery(identifier, query),\n },\n signal,\n })\n }\n\n destroy(): void {\n debugRecorder.unregisterForm(this)\n this.refreshController?.abort()\n\n for (const debounced of this.liveDebouncers.values()) {\n debounced.cancel()\n }\n\n this.liveDebouncers.clear()\n }\n}\n\n/** The errors without those of the given fields (and of their nested paths). */\nfunction withoutStepErrors(\n errors: Record<string, string[]>,\n fieldPaths: string[],\n): Record<string, string[]> {\n const kept: Record<string, string[]> = {}\n\n for (const [key, messages] of Object.entries(errors)) {\n if (!fieldPaths.some((path) => key === path || key.startsWith(`${path}.`))) {\n kept[key] = messages\n }\n }\n\n return kept\n}\n\n/**\n * A stable key for a page form: the page class, record and the form's own key/component.\n * The context token is re-encrypted on every response, so it cannot identify a form.\n * Renderers re-initialise a FormStore from server state only when this key changes (or\n * after the form's own successful submit), never on unrelated reloads of the same page.\n */\nexport function formIdentity(\n page: { class?: string; record?: string | number | null } | null | undefined,\n block: { key: string; component: string; operation?: string | null },\n): string {\n return JSON.stringify([\n page?.class ?? null,\n page?.record ?? null,\n block.key,\n block.component,\n block.operation ?? null,\n ])\n}\n\n/** The parts of an Inertia visit the unsaved-changes guard needs. */\nexport interface GuardedVisit {\n method: string\n url: URL | string\n only?: string[]\n except?: string[]\n preserveState?: unknown\n}\n\n/**\n * True when an Inertia visit would navigate away from the current form, i.e. when the\n * unsaved-changes guard should ask. Partial reloads (`only`/`except`, used by table\n * sorting, paging and polling) and state-preserving visits to the same path stay on the\n * page and keep the form, so they never prompt. Non-GET visits are submissions.\n */\nexport function isLeavingVisit(visit: GuardedVisit, currentUrl?: string): boolean {\n if (visit.method.toLowerCase() !== 'get') {\n return false\n }\n\n if ((visit.only?.length ?? 0) > 0 || (visit.except?.length ?? 0) > 0) {\n return false\n }\n\n const base =\n currentUrl ?? (typeof window !== 'undefined' ? window.location.href : 'http://localhost/')\n const here = new URL(base)\n const target = new URL(String(visit.url), here)\n\n return !(\n visit.preserveState === true &&\n target.origin === here.origin &&\n target.pathname === here.pathname\n )\n}\n\n// ---------------------------------------------------------------- Repeater helpers\n\nexport interface RepeaterItemState {\n key: string\n state: Record<string, unknown>\n}\n\n/** Append a blank item using the Repeater's template state. */\nexport function appendRepeaterItem(\n items: unknown[],\n templateState: Record<string, unknown>,\n): unknown[] {\n return [...items, cloneState(templateState)]\n}\n\nexport function removeRepeaterItem(items: unknown[], index: number): unknown[] {\n return items.filter((_, i) => i !== index)\n}\n\nexport function moveRepeaterItem(items: unknown[], from: number, to: number): unknown[] {\n if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) {\n return items\n }\n\n const copy = [...items]\n const [item] = copy.splice(from, 1)\n copy.splice(to, 0, item)\n\n return copy\n}\n\nexport function cloneRepeaterItem(items: unknown[], index: number): unknown[] {\n const copy = [...items]\n copy.splice(index + 1, 0, cloneState(items[index]))\n\n return copy\n}\n\n/**\n * Per-item UI state (collapsed flags, …) keyed by item index, remapped after the items\n * are reordered, removed or cloned with the helpers above so it stays with its item.\n */\nexport type IndexedState<T> = Record<number, T>\n\nfunction remapIndexedState<T>(\n state: IndexedState<T>,\n newIndex: (index: number) => number | null,\n): IndexedState<T> {\n const result: IndexedState<T> = {}\n\n for (const [key, value] of Object.entries(state) as [string, T][]) {\n const index = newIndex(Number(key))\n\n if (index !== null) result[index] = value\n }\n\n return result\n}\n\nexport function moveIndexedState<T>(\n state: IndexedState<T>,\n from: number,\n to: number,\n): IndexedState<T> {\n if (from === to) return state\n\n return remapIndexedState(state, (index) => {\n if (index === from) return to\n if (from < to && index > from && index <= to) return index - 1\n if (from > to && index >= to && index < from) return index + 1\n\n return index\n })\n}\n\nexport function removeIndexedState<T>(state: IndexedState<T>, removed: number): IndexedState<T> {\n return remapIndexedState(state, (index) =>\n index === removed ? null : index > removed ? index - 1 : index,\n )\n}\n\nexport function cloneIndexedState<T>(state: IndexedState<T>, cloned: number): IndexedState<T> {\n const result = remapIndexedState(state, (index) => (index > cloned ? index + 1 : index))\n\n if (cloned in state) result[cloned + 1] = state[cloned] as T\n\n return result\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"form.js","names":[],"sources":["../src/form.ts"],"sourcesContent":["import { modalTableQuery } from './advanced-fields'\nimport { debugRecorder } from './debug'\nimport { reportRequestError } from './error-notifications'\nimport { HttpError, requestJson } from './http'\nimport { mergeRefreshResponse } from './partial'\nimport type { ModalTableSelectResponse } from './schema/advanced-fields'\nimport type { FieldSchema, SchemaComponent, TabSchema } from './schema/components'\nimport { isField } from './schema/components'\nimport type {\n FormPayload,\n SchemaRefreshResponse,\n StepValidationHaltedResponse,\n} from './schema/form'\nimport type { IconPickerResponse } from './schema/icon-picker'\nimport type { NotificationSchema } from './schema/notifications'\nimport { createStore, type Store } from './store'\nimport type { TableQuery } from './table'\nimport { cloneState } from './utils/clone'\nimport { debounce, type Debounced } from './utils/debounce'\nimport { getPath, setPath } from './utils/path'\n\nexport interface FormState {\n components: SchemaComponent[]\n values: Record<string, unknown>\n errors: Record<string, string[]>\n /** State paths that changed since the last fill. */\n dirty: Set<string>\n /** True while a live refresh or submit is in flight. */\n processing: boolean\n refreshing: boolean\n columns: FormPayload['columns']\n}\n\nexport interface FormStoreOptions {\n /** Encrypted page context token. */\n token: string\n /** Context component name (`form`, `action:table.edit`, …). */\n component: string\n /** Record key when the schema belongs to a table action. */\n record?: string | number | null\n /** URL of the schema refresh endpoint. */\n refreshUrl?: string\n /** URL of the select options endpoint. */\n optionsUrl?: string\n /** URL of the step validation endpoint (wizards validate each step on \"Next\"). */\n validateUrl?: string\n /**\n * Extra parameters for every request (refresh, options). The modal form of a field\n * action sends its owning form's state (`ownerState`) so the server can find the field.\n */\n params?: () => Record<string, unknown>\n onError?: (error: unknown) => void\n /** Shows notifications the server sent with a JSON answer (a halted wizard step). */\n onNotifications?: (notifications: NotificationSchema[]) => void\n /** Called after a live refresh was merged (e.g. to reload what depends on the form). */\n onRefreshed?: (path: string | undefined) => void\n /**\n * Prefix of the fields' DOM ids (`{idPrefix}-{statePath}`), default `alfakit`. Forms shown\n * over another form (action modals) use their own, so ids never repeat on the page.\n */\n idPrefix?: string\n}\n\n/** A DOM-safe id prefix for a form, e.g. `action:field.category_id.createOption` -> `alfakit-action-field-category_id-createOption`. */\nexport function formIdPrefix(scope: string): string {\n return `alfakit-${scope.replace(/[^a-zA-Z0-9_-]/g, '-')}`\n}\n\n/** The DOM id of a field: `{prefix}-{statePath}` with unsafe characters replaced. */\nexport function fieldDomId(prefix: string, statePath: string): string {\n return `${prefix}-${statePath.replace(/[^a-zA-Z0-9_-]/g, '-')}`\n}\n\n/**\n * Walk a component tree and collect every field (including those inside Repeater items).\n */\nexport function collectFields(\n components: SchemaComponent[],\n into: FieldSchema[] = [],\n): FieldSchema[] {\n for (const component of components) {\n if (isField(component)) {\n into.push(component)\n\n if (component.type === 'Repeater' || component.type === 'Builder') {\n for (const item of component.items) {\n collectFields(item.schema, into)\n }\n }\n }\n\n if (component.schema) {\n collectFields(component.schema, into)\n }\n }\n\n return into\n}\n\n/**\n * The field at `path`. Items the browser added (or cloned) before the server has seen\n * them have no per-item schema yet: their fields are found in the Repeater's `template`\n * (or the Builder block's schema, preferring the block named by the item's `type` in\n * `values`), whose `__INDEX__` placeholders stand for any item index, so `live()` fields\n * of fresh items refresh like any other.\n */\nexport function findFieldByPath(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown> = {},\n): FieldSchema | undefined {\n return (\n collectFields(components).find((field) => field.statePath === path) ??\n findTemplateField(components, path, values)\n )\n}\n\nfunction findTemplateField(\n components: SchemaComponent[],\n path: string,\n values: Record<string, unknown>,\n): FieldSchema | undefined {\n for (const field of collectFields(components)) {\n for (const template of itemTemplates(field, path, values)) {\n const found =\n collectFields(template).find((candidate) =>\n templatePathMatches(candidate.statePath, path),\n ) ?? findTemplateField(template, path, values)\n\n if (found) return found\n }\n }\n\n return undefined\n}\n\n/** The blank item schemas of a Repeater or Builder (the item's own block first). */\nfunction itemTemplates(\n field: FieldSchema,\n path: string,\n values: Record<string, unknown>,\n): SchemaComponent[][] {\n if (field.type === 'Repeater') {\n return [(field as unknown as { template?: SchemaComponent[] }).template ?? []]\n }\n\n if (field.type !== 'Builder') return []\n\n const blocks =\n (field as unknown as { blocks?: Array<{ name: string; schema: SchemaComponent[] }> }).blocks ??\n []\n const item = new RegExp(`^(${templatePathPattern(field.statePath)}\\\\.\\\\d+)\\\\.`).exec(path)\n const type = item ? getPath<unknown>(values, `${item[1]}.type`) : undefined\n\n return [...blocks]\n .sort((a, b) => Number(b.name === type) - Number(a.name === type))\n .map((block) => block.schema)\n}\n\nfunction templatePathPattern(statePath: string): string {\n return statePath\n .split('__INDEX__')\n .map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('\\\\d+')\n}\n\nfunction templatePathMatches(statePath: string, path: string): boolean {\n return (\n statePath.includes('__INDEX__') && new RegExp(`^${templatePathPattern(statePath)}$`).test(path)\n )\n}\n\n/**\n * The wizard step to reveal after the errors change: the first step with an error, but\n * only when new error keys arrived (a submit or validation response), so the user can\n * still move between steps while errors are shown.\n */\nexport function wizardStepToReveal(\n steps: SchemaComponent[],\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): number | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const index = steps.findIndex((step) =>\n collectFields(step.schema ?? []).some((field) => errors[field.statePath]),\n )\n\n return index === -1 ? null : index\n}\n\n/**\n * Whether a layout component (a tab, a section) holds a field with a validation error:\n * one of its fields, a nested repeater/builder item field (`items.0.sku`), or, while the\n * component is deferred and its fields aren't loaded, one of its `deferredPaths`.\n */\nexport function componentHasErrors(\n component: Pick<SchemaComponent, 'schema' | 'deferred' | 'deferredPaths'>,\n errors: Record<string, unknown>,\n): boolean {\n const keys = Object.keys(errors).filter((key) => errors[key])\n\n if (keys.length === 0) return false\n\n const paths =\n component.deferred && !component.schema?.length\n ? (component.deferredPaths ?? [])\n : collectFields(component.schema ?? []).map((field) => field.statePath)\n\n return paths.some((path) => keys.some((key) => key === path || key.startsWith(`${path}.`)))\n}\n\n/**\n * The value a tab is written to the query string as (`persistTabInQueryString()`): its\n * readable `queryValue` (`billing-address`), else its key.\n */\nexport function tabQueryValue(tab: Pick<TabSchema, 'key' | 'queryValue'>): string {\n return tab.queryValue ?? tab.key\n}\n\n/**\n * The key of the tab a query-string value names: a tab's `queryValue`, or its key (links\n * written before tabs had readable values, `?tab=tabs-0.tab-1`). Null when none matches.\n */\nexport function tabFromQuery(\n tabs: Array<Pick<TabSchema, 'key' | 'queryValue'>>,\n value: string | null | undefined,\n): string | null {\n if (!value) return null\n\n return (\n (tabs.find((tab) => tab.queryValue === value) ?? tabs.find((tab) => tab.key === value))?.key ??\n null\n )\n}\n\n/**\n * The tab to show after the errors change: when new error keys arrived (a submit or\n * validation response) and the active tab has none, the first tab with an error.\n */\nexport function tabToReveal(\n tabs: Array<Pick<SchemaComponent, 'key' | 'schema' | 'deferred' | 'deferredPaths'>>,\n activeKey: string,\n previousErrors: Record<string, unknown>,\n errors: Record<string, unknown>,\n): string | null {\n if (!Object.keys(errors).some((key) => !(key in previousErrors))) {\n return null\n }\n\n const active = tabs.find((tab) => tab.key === activeKey)\n\n if (active && componentHasErrors(active, errors)) return null\n\n return tabs.find((tab) => componentHasErrors(tab, errors))?.key ?? null\n}\n\n/** Whether `key` is the error of one item of the list field at `path` (`labels.2`). */\nfunction isItemErrorKey(key: string, path: string): boolean {\n return (\n key.length > path.length + 1 &&\n key.startsWith(`${path}.`) &&\n /^\\d+$/.test(key.slice(path.length + 1))\n )\n}\n\n/**\n * The validation message a field shows: its own, else the first of its items' (`labels.2`\n * for a tag refused by `nestedRecursiveRules()`, `attachments.0` for one upload). Those\n * errors have no field of their own, so without this the form refused to save with no\n * message anywhere. Deeper paths (`items.0.name`) belong to item fields and are left out.\n */\nexport function fieldError(\n errors: Readonly<Record<string, readonly string[] | undefined>>,\n path: string,\n): string | undefined {\n const own = errors[path]?.[0]\n\n if (own) return own\n\n for (const key of Object.keys(errors)) {\n if (isItemErrorKey(key, path) && errors[key]?.[0]) return errors[key][0]\n }\n\n return undefined\n}\n\n/**\n * Framework-agnostic form state: values addressed by `statePath`, validation errors,\n * dirty tracking and server round-trips for `live()` fields.\n */\nexport class FormStore {\n public readonly store: Store<FormState>\n\n private readonly liveDebouncers = new Map<string, Debounced<[]>>()\n\n private refreshController: AbortController | null = null\n\n /**\n * The values as the server last sent them, so a refresh can tell `afterStateUpdated`\n * hooks what the field held before the change (`$old`).\n */\n private serverValues: Record<string, unknown>\n\n /**\n * Paths the user changed while a live refresh was in flight. Only these keep their\n * client value when the response is merged; every other path takes the server's value\n * (including values set by `$set` in `afterStateUpdated` hooks).\n */\n private editsInFlight: Set<string> | null = null\n\n /** The fingerprints of the components the store holds (see `PartialFormPayload`). */\n private fingerprints: Record<string, string> | undefined\n\n constructor(\n payload: FormPayload,\n private options: FormStoreOptions,\n ) {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store = createStore<FormState>({\n components: payload.components,\n values: payload.state ?? {},\n errors: {},\n dirty: new Set(),\n processing: false,\n refreshing: false,\n columns: payload.columns,\n })\n\n // Listed by the developer debug panel while it is enabled; a no-op otherwise.\n debugRecorder.registerForm(this)\n }\n\n get state(): FormState {\n return this.store.get()\n }\n\n /**\n * Swap in a fresher context token (every response re-issues it) without touching the\n * form state, so live refreshes keep working after unrelated reloads.\n */\n setToken(token: string): void {\n this.options = { ...this.options, token }\n }\n\n /** Context component name of the schema (`form`, `action:table.edit`, …). */\n get component(): string {\n return this.options.component\n }\n\n /** Prefix of the fields' DOM ids (see `FormStoreOptions.idPrefix`). */\n get idPrefix(): string {\n return this.options.idPrefix ?? 'alfakit'\n }\n\n get record(): string | number | null {\n return this.options.record ?? null\n }\n\n get token(): string {\n return this.options.token\n }\n\n get optionsUrl(): string | undefined {\n return this.options.optionsUrl\n }\n\n get validateUrl(): string | undefined {\n return this.options.validateUrl\n }\n\n /** The fingerprints of the held components, sent so responses can be partial. */\n get currentFingerprints(): Record<string, string> | undefined {\n return this.fingerprints\n }\n\n /** The parameters that identify this schema to the internal endpoints. */\n requestContext(): Record<string, unknown> {\n return {\n token: this.options.token,\n component: this.options.component,\n record: this.options.record ?? undefined,\n ...this.options.params?.(),\n }\n }\n\n /**\n * `requestContext()` plus the form state for GET requests (options, picker tables,\n * mentions). States travel as JSON: a query string drops nulls and empty arrays, and with\n * them repeater items that are still blank, so the server couldn't find their fields.\n */\n queryContext(): Record<string, unknown> {\n const context = this.requestContext()\n const ownerState = context.ownerState\n\n return {\n ...context,\n ...(ownerState !== null && typeof ownerState === 'object'\n ? { ownerState: JSON.stringify(ownerState) }\n : {}),\n state: JSON.stringify(this.state.values),\n }\n }\n\n subscribe(listener: (state: FormState) => void): () => void {\n return this.store.subscribe(listener)\n }\n\n // ---------------------------------------------------------------- Values\n\n getValue<T = unknown>(path: string, fallback?: T): T {\n return getPath<T>(this.state.values, path, fallback)\n }\n\n /**\n * Set a value. Live fields schedule a server refresh (debounced); `onBlur` fields\n * refresh via `blur()` instead.\n */\n setValue(path: string, value: unknown, options: { silent?: boolean } = {}): void {\n const current = this.getValue(path)\n\n if (Object.is(current, value)) {\n return\n }\n\n this.store.set((state) => {\n const dirty = new Set(state.dirty)\n dirty.add(path)\n\n const errors = { ...state.errors }\n delete errors[path]\n\n // Item errors of a list field (`labels.2` from `nestedRecursiveRules()`) go with it.\n for (const key of Object.keys(errors)) {\n if (isItemErrorKey(key, path)) delete errors[key]\n }\n\n return { ...state, values: setPath(state.values, path, value), dirty, errors }\n })\n\n this.editsInFlight?.add(path)\n\n if (options.silent) {\n return\n }\n\n const field = this.findField(path)\n\n if (field?.live && !field.live.onBlur) {\n this.scheduleRefresh(path, field.live.debounce ?? 150)\n }\n }\n\n /** Call when a field loses focus so `live(onBlur: true)` fields refresh. */\n blur(path: string): void {\n const field = this.findField(path)\n\n if (field?.live?.onBlur && this.state.dirty.has(path)) {\n void this.refresh(path)\n }\n }\n\n /** Replace the whole state (e.g. after the server returns a refreshed payload). */\n fill(payload: FormPayload, options: { keepDirty?: boolean } = {}): void {\n this.serverValues = payload.state ?? {}\n this.fingerprints = payload.fingerprints\n this.store.set((state) => ({\n ...state,\n components: payload.components,\n values: payload.state ?? {},\n columns: payload.columns ?? state.columns,\n dirty: options.keepDirty ? state.dirty : new Set(),\n }))\n }\n\n setErrors(errors: Record<string, string[] | string>): void {\n const normalized: Record<string, string[]> = {}\n\n for (const [key, value] of Object.entries(errors)) {\n normalized[key] = Array.isArray(value) ? value : [value]\n }\n\n this.store.update({ errors: normalized })\n }\n\n clearErrors(): void {\n this.store.update({ errors: {} })\n }\n\n getError(path: string): string | undefined {\n return fieldError(this.state.errors, path)\n }\n\n setProcessing(processing: boolean): void {\n this.store.update({ processing })\n }\n\n get isDirty(): boolean {\n return this.state.dirty.size > 0\n }\n\n findField(path: string): FieldSchema | undefined {\n return findFieldByPath(this.state.components, path, this.state.values)\n }\n\n // ---------------------------------------------------------------- Live refresh\n\n private scheduleRefresh(path: string, wait: number): void {\n let debounced = this.liveDebouncers.get(path)\n\n if (!debounced) {\n debounced = debounce(() => {\n void this.refresh(path)\n }, wait)\n this.liveDebouncers.set(path, debounced)\n }\n\n debounced()\n }\n\n /**\n * Send the current state to the server, which re-evaluates closures and\n * `afterStateUpdated` hooks, and replace components + values with the response.\n */\n refresh(path?: string): Promise<void> {\n const request = this.sendRefresh(path)\n this.refreshInFlight = request\n\n void request.finally(() => {\n if (this.refreshInFlight === request) this.refreshInFlight = null\n })\n\n return request\n }\n\n /**\n * Resolves once no live refresh is waiting or in flight: debounced refreshes are sent\n * now and awaited. Submits wait for it, so a click that blurs a `live(onBlur: true)` field\n * sends the state the server computed (`$set` in `afterStateUpdated`), not the one before.\n */\n async settle(): Promise<void> {\n for (const debounced of this.liveDebouncers.values()) debounced.flush()\n\n while (this.refreshInFlight) {\n await this.refreshInFlight.catch(() => undefined)\n }\n }\n\n /**\n * Run `callback` once the form is settled (see `settle()`): straight away when no live\n * refresh is waiting or in flight, so a submit stays synchronous in the usual case.\n */\n whenSettled(callback: () => void): void {\n const waiting = [...this.liveDebouncers.values()].some((debounced) => debounced.pending)\n\n if (!waiting && !this.refreshInFlight) {\n callback()\n\n return\n }\n\n void this.settle().then(callback)\n }\n\n private refreshInFlight: Promise<void> | null = null\n\n private async sendRefresh(path?: string): Promise<void> {\n if (!this.options.refreshUrl) {\n return\n }\n\n this.refreshController?.abort()\n const controller = new AbortController()\n this.refreshController = controller\n // The request carries the current state, so only edits made from now on are newer\n // than what the server sees.\n const edits = new Set<string>()\n this.editsInFlight = edits\n\n this.store.update({ refreshing: true })\n const sent = this.state.values\n\n try {\n const response = await requestJson<SchemaRefreshResponse>(this.options.refreshUrl, {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: sent,\n path,\n old: path ? (getPath(this.serverValues, path) ?? null) : undefined,\n // With fingerprints the server answers with only what changed.\n fingerprints: this.fingerprints,\n },\n signal: controller.signal,\n })\n\n if (controller.signal.aborted) {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n if (!this.applyResponse(response, sent, edits)) {\n // The patches didn't fit the tree: ask for the whole form instead.\n this.fingerprints = undefined\n await this.sendRefresh(path)\n\n return\n }\n\n this.options.onRefreshed?.(path)\n } catch (error) {\n if ((error as Error).name === 'AbortError') {\n return\n }\n\n if (this.editsInFlight === edits) {\n this.editsInFlight = null\n }\n\n this.store.update({ refreshing: false })\n // Without an `onError`, the panel's error notification for the status is shown.\n ;(this.options.onError ?? reportRequestError)(error)\n }\n }\n\n /**\n * Merge a refresh response (full or partial) for the state `sent` with the request.\n * Paths in `edits` changed while the request was in flight and keep their client value;\n * everything else takes the server's (including values set by `$set`). Returns false\n * when a partial response doesn't fit the tree the store holds.\n */\n applyResponse(\n response: SchemaRefreshResponse,\n sent: Record<string, unknown>,\n edits: Iterable<string> = [],\n ): boolean {\n const merged = mergeRefreshResponse(this.state.components, sent, response)\n\n if (!merged) {\n this.store.update({ refreshing: false })\n\n return false\n }\n\n this.serverValues = merged.values\n this.fingerprints = merged.fingerprints\n\n this.store.set((state) => {\n let values = merged.values\n\n for (const editedPath of edits) {\n values = setPath(values, editedPath, getPath(state.values, editedPath))\n }\n\n return {\n ...state,\n components: merged.components,\n values,\n columns: merged.columns ?? state.columns,\n refreshing: false,\n }\n })\n\n return true\n }\n\n /**\n * Validate the fields of one wizard step (by the Step component's `key`) on the server.\n * Resolves with true when they pass; otherwise the step's errors replace the errors of\n * those fields and false is returned. Without a validation endpoint every step passes.\n */\n async validateStep(stepKey: string, fieldPaths: string[]): Promise<boolean> {\n if (!this.options.validateUrl) {\n return true\n }\n\n try {\n const response = await requestJson<StepValidationHaltedResponse | null>(\n this.options.validateUrl,\n {\n method: 'POST',\n body: {\n ...this.requestContext(),\n state: this.state.values,\n step: stepKey,\n },\n },\n )\n\n // A step hook halted: stay on the step, keep the field errors as they are.\n if (response !== null && typeof response === 'object' && response.halted) {\n const notifications = response.notifications ?? []\n\n if (notifications.length > 0) this.options.onNotifications?.(notifications)\n\n return false\n }\n } catch (error) {\n if (error instanceof HttpError && error.status === 422) {\n this.store.update({\n errors: { ...withoutStepErrors(this.state.errors, fieldPaths), ...error.errors },\n })\n\n return false\n }\n\n ;(this.options.onError ?? reportRequestError)(error)\n\n return false\n }\n\n this.store.update({ errors: withoutStepErrors(this.state.errors, fieldPaths) })\n\n return true\n }\n\n /** Search options for a searchable select at `path`. */\n async searchOptions(\n path: string,\n search: string,\n ): Promise<Array<{ value: string; label: string }>> {\n if (!this.options.optionsUrl) {\n return []\n }\n\n const response = await requestJson<{ options?: Array<{ value: string; label: string }> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, search },\n },\n )\n\n return response.options ?? []\n }\n\n async optionLabels(\n path: string,\n values: Array<string | number>,\n ): Promise<Record<string, string>> {\n if (!this.options.optionsUrl || values.length === 0) {\n return {}\n }\n\n const response = await requestJson<{ labels?: Record<string, string> }>(\n this.options.optionsUrl,\n {\n query: { ...this.queryContext(), path, values },\n },\n )\n\n return response.labels ?? {}\n }\n\n /** One page of icons for the IconPicker at `path` (`set`: one of its sets, or all). */\n async searchIcons(\n path: string,\n query: { search?: string; set?: string | null; page?: number },\n signal?: AbortSignal,\n ): Promise<IconPickerResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<IconPickerResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n search: query.search ?? '',\n ...(query.set ? { set: query.set } : {}),\n page: query.page ?? 1,\n },\n signal,\n })\n }\n\n /**\n * The picker table of a ModalTableSelect at `path` (`query` is its table state) and the\n * labels of the records it lists.\n */\n async modalTable(\n path: string,\n identifier: string,\n query: TableQuery,\n signal?: AbortSignal,\n ): Promise<ModalTableSelectResponse | null> {\n if (!this.options.optionsUrl) {\n return null\n }\n\n return requestJson<ModalTableSelectResponse>(this.options.optionsUrl, {\n query: {\n ...this.queryContext(),\n path,\n table: 1,\n ...modalTableQuery(identifier, query),\n },\n signal,\n })\n }\n\n destroy(): void {\n debugRecorder.unregisterForm(this)\n this.refreshController?.abort()\n\n for (const debounced of this.liveDebouncers.values()) {\n debounced.cancel()\n }\n\n this.liveDebouncers.clear()\n }\n}\n\n/** The errors without those of the given fields (and of their nested paths). */\nfunction withoutStepErrors(\n errors: Record<string, string[]>,\n fieldPaths: string[],\n): Record<string, string[]> {\n const kept: Record<string, string[]> = {}\n\n for (const [key, messages] of Object.entries(errors)) {\n if (!fieldPaths.some((path) => key === path || key.startsWith(`${path}.`))) {\n kept[key] = messages\n }\n }\n\n return kept\n}\n\n/**\n * A stable key for a page form: the page class, record and the form's own key/component.\n * The context token is re-encrypted on every response, so it cannot identify a form.\n * Renderers re-initialise a FormStore from server state only when this key changes (or\n * after the form's own successful submit), never on unrelated reloads of the same page.\n */\nexport function formIdentity(\n page: { class?: string; record?: string | number | null } | null | undefined,\n block: { key: string; component: string; operation?: string | null },\n): string {\n return JSON.stringify([\n page?.class ?? null,\n page?.record ?? null,\n block.key,\n block.component,\n block.operation ?? null,\n ])\n}\n\n/** The parts of an Inertia visit the unsaved-changes guard needs. */\nexport interface GuardedVisit {\n method: string\n url: URL | string\n only?: string[]\n except?: string[]\n preserveState?: unknown\n}\n\n/**\n * True when an Inertia visit would navigate away from the current form, i.e. when the\n * unsaved-changes guard should ask. Partial reloads (`only`/`except`, used by table\n * sorting, paging and polling) and state-preserving visits to the same path stay on the\n * page and keep the form, so they never prompt. Non-GET visits are submissions.\n */\nexport function isLeavingVisit(visit: GuardedVisit, currentUrl?: string): boolean {\n if (visit.method.toLowerCase() !== 'get') {\n return false\n }\n\n if ((visit.only?.length ?? 0) > 0 || (visit.except?.length ?? 0) > 0) {\n return false\n }\n\n const base =\n currentUrl ?? (typeof window !== 'undefined' ? window.location.href : 'http://localhost/')\n const here = new URL(base)\n const target = new URL(String(visit.url), here)\n\n return !(\n visit.preserveState === true &&\n target.origin === here.origin &&\n target.pathname === here.pathname\n )\n}\n\n// ---------------------------------------------------------------- Repeater helpers\n\nexport interface RepeaterItemState {\n key: string\n state: Record<string, unknown>\n}\n\n/** Append a blank item using the Repeater's template state. */\nexport function appendRepeaterItem(\n items: unknown[],\n templateState: Record<string, unknown>,\n): unknown[] {\n return [...items, cloneState(templateState)]\n}\n\nexport function removeRepeaterItem(items: unknown[], index: number): unknown[] {\n return items.filter((_, i) => i !== index)\n}\n\nexport function moveRepeaterItem(items: unknown[], from: number, to: number): unknown[] {\n if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length) {\n return items\n }\n\n const copy = [...items]\n const [item] = copy.splice(from, 1)\n copy.splice(to, 0, item)\n\n return copy\n}\n\nexport function cloneRepeaterItem(items: unknown[], index: number): unknown[] {\n const copy = [...items]\n copy.splice(index + 1, 0, cloneState(items[index]))\n\n return copy\n}\n\n/**\n * Per-item UI state (collapsed flags, …) keyed by item index, remapped after the items\n * are reordered, removed or cloned with the helpers above so it stays with its item.\n */\nexport type IndexedState<T> = Record<number, T>\n\nfunction remapIndexedState<T>(\n state: IndexedState<T>,\n newIndex: (index: number) => number | null,\n): IndexedState<T> {\n const result: IndexedState<T> = {}\n\n for (const [key, value] of Object.entries(state) as [string, T][]) {\n const index = newIndex(Number(key))\n\n if (index !== null) result[index] = value\n }\n\n return result\n}\n\nexport function moveIndexedState<T>(\n state: IndexedState<T>,\n from: number,\n to: number,\n): IndexedState<T> {\n if (from === to) return state\n\n return remapIndexedState(state, (index) => {\n if (index === from) return to\n if (from < to && index > from && index <= to) return index - 1\n if (from > to && index >= to && index < from) return index + 1\n\n return index\n })\n}\n\nexport function removeIndexedState<T>(state: IndexedState<T>, removed: number): IndexedState<T> {\n return remapIndexedState(state, (index) =>\n index === removed ? null : index > removed ? index - 1 : index,\n )\n}\n\nexport function cloneIndexedState<T>(state: IndexedState<T>, cloned: number): IndexedState<T> {\n const result = remapIndexedState(state, (index) => (index > cloned ? index + 1 : index))\n\n if (cloned in state) result[cloned + 1] = state[cloned] as T\n\n return result\n}\n"],"mappings":"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"}
|
package/dist/icons.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export declare function iconAlias(name: string, fallback?: string): string;
|
|
|
35
35
|
* lazy-load flash) unless the app registered a replacement.
|
|
36
36
|
*/
|
|
37
37
|
export declare function iconAliasOverride(name: string): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The icon of a built-in repeater, builder or key-value item button: the app's replacement
|
|
40
|
+
* for `alfakit::forms.components.{kind}.actions.{action}`, or the renderer's default.
|
|
41
|
+
* Customised item actions arrive resolved from the server; the defaults use this.
|
|
42
|
+
*/
|
|
43
|
+
export declare function itemActionIcon(kind: 'repeater' | 'builder' | 'key-value', action: string, fallback: string): string;
|
|
38
44
|
/** Whether an icon name is an unresolved `alfakit::` alias. */
|
|
39
45
|
export declare function isIconAlias(name: string): boolean;
|
|
40
46
|
//#endregion
|
package/dist/icons.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icons.d.ts","names":[],"sources":["../src/icons.ts"],"mappings":";;;;;YAIY;EACR;EAAc;;EAAmB;EAAa;;EAAkB;EAAa;;;wBAKjE,WAAW,eAAe;;;;;wBAiB1B,eAAe,WAAW;;;;;;;;;wBAY1B,UAAU,cAAc;;;;;;wBAWxB,kBAAkB;;
|
|
1
|
+
{"version":3,"file":"icons.d.ts","names":[],"sources":["../src/icons.ts"],"mappings":";;;;;YAIY;EACR;EAAc;;EAAmB;EAAa;;EAAkB;EAAa;;;wBAKjE,WAAW,eAAe;;;;;wBAiB1B,eAAe,WAAW;;;;;;;;;wBAY1B,UAAU,cAAc;;;;;;wBAWxB,kBAAkB;;;;;;wBASlB,eACd,4CACA,gBACA;;wBAMc,YAAY"}
|
package/dist/icons.js
CHANGED
|
@@ -44,11 +44,19 @@ function iconAlias(name, fallback) {
|
|
|
44
44
|
function iconAliasOverride(name) {
|
|
45
45
|
return name.startsWith("alfakit::") ? aliases[name] : void 0;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The icon of a built-in repeater, builder or key-value item button: the app's replacement
|
|
49
|
+
* for `alfakit::forms.components.{kind}.actions.{action}`, or the renderer's default.
|
|
50
|
+
* Customised item actions arrive resolved from the server; the defaults use this.
|
|
51
|
+
*/
|
|
52
|
+
function itemActionIcon(kind, action, fallback) {
|
|
53
|
+
return iconAlias(`alfakit::forms.components.${kind}.actions.${action}`, fallback);
|
|
54
|
+
}
|
|
47
55
|
/** Whether an icon name is an unresolved `alfakit::` alias. */
|
|
48
56
|
function isIconAlias(name) {
|
|
49
57
|
return name.startsWith("alfakit::");
|
|
50
58
|
}
|
|
51
59
|
//#endregion
|
|
52
|
-
export { iconAlias, iconAliasOverride, iconSource, isIconAlias, setIconAliases };
|
|
60
|
+
export { iconAlias, iconAliasOverride, iconSource, isIconAlias, itemActionIcon, setIconAliases };
|
|
53
61
|
|
|
54
62
|
//# sourceMappingURL=icons.js.map
|
package/dist/icons.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icons.js","names":[],"sources":["../src/icons.ts"],"sourcesContent":["/**\n * Icon values as the server resolves them (`Icons::resolve()`): a Lucide name, an image URL\n * or sanitised `<svg>` markup. Renderers' `<Icon>` components draw all three.\n */\nexport type IconSource =\n { type: 'name'; name: string } | { type: 'url'; url: string } | { type: 'svg'; svg: string }\n\nconst URL_PATTERN = /^(?:https?:\\/\\/|\\/\\/|\\/|\\.\\/|data:image\\/(?:png|gif|jpeg|webp|svg\\+xml)[;,])/i\n\n/** What kind of icon a resolved value is. */\nexport function iconSource(icon: string): IconSource {\n const value = icon.trim()\n\n // Markup only ever comes from the server, which sanitises it (`SvgSanitizer`).\n if (value.startsWith('<')) return { type: 'svg', svg: value }\n\n if (URL_PATTERN.test(value)) return { type: 'url', url: value }\n\n return { type: 'name', name: value }\n}\n\nlet aliases: Record<string, string> = {}\n\n/**\n * The icon aliases the app replaced with `Icons::register()` (shared as\n * `PanelThemeSchema.icons`). `applyPanelTheme()` keeps them current.\n */\nexport function setIconAliases(overrides: Record<string, string> | null | undefined): void {\n aliases = { ...(overrides ?? {}) }\n}\n\n/**\n * Resolve an `alfakit::` alias: the app's replacement, or `fallback` (the renderer's\n * built-in icon). Other names are returned unchanged.\n *\n * ```ts\n * <Icon name={iconAlias('alfakit::panels.sidebar.collapse-button', 'chevron-left')} />\n * ```\n */\nexport function iconAlias(name: string, fallback?: string): string {\n if (!name.startsWith('alfakit::')) return name\n\n return aliases[name] ?? fallback ?? name\n}\n\n/**\n * The app's replacement for an `alfakit::` alias, or `undefined` when it kept the default.\n * Renderer chrome uses this to draw its statically imported glyph synchronously (no\n * lazy-load flash) unless the app registered a replacement.\n */\nexport function iconAliasOverride(name: string): string | undefined {\n return name.startsWith('alfakit::') ? aliases[name] : undefined\n}\n\n/** Whether an icon name is an unresolved `alfakit::` alias. */\nexport function isIconAlias(name: string): boolean {\n return name.startsWith('alfakit::')\n}\n"],"mappings":";AAOA,MAAM,cAAc;;AAGpB,SAAgB,WAAW,MAA0B;CACnD,MAAM,QAAQ,KAAK,KAAK;CAGxB,IAAI,MAAM,WAAW,GAAG,GAAG,OAAO;EAAE,MAAM;EAAO,KAAK;CAAM;CAE5D,IAAI,YAAY,KAAK,KAAK,GAAG,OAAO;EAAE,MAAM;EAAO,KAAK;CAAM;CAE9D,OAAO;EAAE,MAAM;EAAQ,MAAM;CAAM;AACrC;AAEA,IAAI,UAAkC,CAAC;;;;;AAMvC,SAAgB,eAAe,WAA4D;CACzF,UAAU,EAAE,GAAI,aAAa,CAAC,EAAG;AACnC;;;;;;;;;AAUA,SAAgB,UAAU,MAAc,UAA2B;CACjE,IAAI,CAAC,KAAK,WAAW,WAAW,GAAG,OAAO;CAE1C,OAAO,QAAQ,SAAS,YAAY;AACtC;;;;;;AAOA,SAAgB,kBAAkB,MAAkC;CAClE,OAAO,KAAK,WAAW,WAAW,IAAI,QAAQ,QAAQ,KAAA;AACxD;;AAGA,SAAgB,YAAY,MAAuB;CACjD,OAAO,KAAK,WAAW,WAAW;AACpC"}
|
|
1
|
+
{"version":3,"file":"icons.js","names":[],"sources":["../src/icons.ts"],"sourcesContent":["/**\n * Icon values as the server resolves them (`Icons::resolve()`): a Lucide name, an image URL\n * or sanitised `<svg>` markup. Renderers' `<Icon>` components draw all three.\n */\nexport type IconSource =\n { type: 'name'; name: string } | { type: 'url'; url: string } | { type: 'svg'; svg: string }\n\nconst URL_PATTERN = /^(?:https?:\\/\\/|\\/\\/|\\/|\\.\\/|data:image\\/(?:png|gif|jpeg|webp|svg\\+xml)[;,])/i\n\n/** What kind of icon a resolved value is. */\nexport function iconSource(icon: string): IconSource {\n const value = icon.trim()\n\n // Markup only ever comes from the server, which sanitises it (`SvgSanitizer`).\n if (value.startsWith('<')) return { type: 'svg', svg: value }\n\n if (URL_PATTERN.test(value)) return { type: 'url', url: value }\n\n return { type: 'name', name: value }\n}\n\nlet aliases: Record<string, string> = {}\n\n/**\n * The icon aliases the app replaced with `Icons::register()` (shared as\n * `PanelThemeSchema.icons`). `applyPanelTheme()` keeps them current.\n */\nexport function setIconAliases(overrides: Record<string, string> | null | undefined): void {\n aliases = { ...(overrides ?? {}) }\n}\n\n/**\n * Resolve an `alfakit::` alias: the app's replacement, or `fallback` (the renderer's\n * built-in icon). Other names are returned unchanged.\n *\n * ```ts\n * <Icon name={iconAlias('alfakit::panels.sidebar.collapse-button', 'chevron-left')} />\n * ```\n */\nexport function iconAlias(name: string, fallback?: string): string {\n if (!name.startsWith('alfakit::')) return name\n\n return aliases[name] ?? fallback ?? name\n}\n\n/**\n * The app's replacement for an `alfakit::` alias, or `undefined` when it kept the default.\n * Renderer chrome uses this to draw its statically imported glyph synchronously (no\n * lazy-load flash) unless the app registered a replacement.\n */\nexport function iconAliasOverride(name: string): string | undefined {\n return name.startsWith('alfakit::') ? aliases[name] : undefined\n}\n\n/**\n * The icon of a built-in repeater, builder or key-value item button: the app's replacement\n * for `alfakit::forms.components.{kind}.actions.{action}`, or the renderer's default.\n * Customised item actions arrive resolved from the server; the defaults use this.\n */\nexport function itemActionIcon(\n kind: 'repeater' | 'builder' | 'key-value',\n action: string,\n fallback: string,\n): string {\n return iconAlias(`alfakit::forms.components.${kind}.actions.${action}`, fallback)\n}\n\n/** Whether an icon name is an unresolved `alfakit::` alias. */\nexport function isIconAlias(name: string): boolean {\n return name.startsWith('alfakit::')\n}\n"],"mappings":";AAOA,MAAM,cAAc;;AAGpB,SAAgB,WAAW,MAA0B;CACnD,MAAM,QAAQ,KAAK,KAAK;CAGxB,IAAI,MAAM,WAAW,GAAG,GAAG,OAAO;EAAE,MAAM;EAAO,KAAK;CAAM;CAE5D,IAAI,YAAY,KAAK,KAAK,GAAG,OAAO;EAAE,MAAM;EAAO,KAAK;CAAM;CAE9D,OAAO;EAAE,MAAM;EAAQ,MAAM;CAAM;AACrC;AAEA,IAAI,UAAkC,CAAC;;;;;AAMvC,SAAgB,eAAe,WAA4D;CACzF,UAAU,EAAE,GAAI,aAAa,CAAC,EAAG;AACnC;;;;;;;;;AAUA,SAAgB,UAAU,MAAc,UAA2B;CACjE,IAAI,CAAC,KAAK,WAAW,WAAW,GAAG,OAAO;CAE1C,OAAO,QAAQ,SAAS,YAAY;AACtC;;;;;;AAOA,SAAgB,kBAAkB,MAAkC;CAClE,OAAO,KAAK,WAAW,WAAW,IAAI,QAAQ,QAAQ,KAAA;AACxD;;;;;;AAOA,SAAgB,eACd,MACA,QACA,UACQ;CACR,OAAO,UAAU,6BAA6B,KAAK,WAAW,UAAU,QAAQ;AAClF;;AAGA,SAAgB,YAAY,MAAuB;CACjD,OAAO,KAAK,WAAW,WAAW;AACpC"}
|