@alfakit/core 0.1.2 → 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 +6 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +6 -0
- 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/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/form.d.ts +20 -0
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +49 -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 +10 -9
- package/dist/index.js +8 -7
- 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 +2 -2
- package/dist/page-actions.d.ts.map +1 -1
- package/dist/page-actions.js +4 -4
- 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 +2 -0
- package/dist/schema/cms.d.ts.map +1 -1
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/panel.d.ts +25 -0
- package/dist/schema/panel.d.ts.map +1 -1
- package/dist/schema/panel.js.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 +1 -1
- package/themes.css +6 -1
package/dist/form.d.ts
CHANGED
|
@@ -89,6 +89,13 @@ export declare function tabFromQuery(tabs: Array<Pick<TabSchema, 'key' | 'queryV
|
|
|
89
89
|
* validation response) and the active tab has none, the first tab with an error.
|
|
90
90
|
*/
|
|
91
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;
|
|
92
99
|
/**
|
|
93
100
|
* Framework-agnostic form state: values addressed by `statePath`, validation errors,
|
|
94
101
|
* dirty tracking and server round-trips for `live()` fields.
|
|
@@ -163,6 +170,19 @@ export declare class FormStore {
|
|
|
163
170
|
* `afterStateUpdated` hooks, and replace components + values with the response.
|
|
164
171
|
*/
|
|
165
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;
|
|
166
186
|
/**
|
|
167
187
|
* Merge a refresh response (full or partial) for the state `sent` with the request.
|
|
168
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;;;;;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;;;;;
|
|
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
|
@@ -106,6 +106,21 @@ function tabToReveal(tabs, activeKey, previousErrors, errors) {
|
|
|
106
106
|
if (active && componentHasErrors(active, errors)) return null;
|
|
107
107
|
return tabs.find((tab) => componentHasErrors(tab, errors))?.key ?? null;
|
|
108
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
|
+
}
|
|
109
124
|
/**
|
|
110
125
|
* Framework-agnostic form state: values addressed by `statePath`, validation errors,
|
|
111
126
|
* dirty tracking and server round-trips for `live()` fields.
|
|
@@ -221,6 +236,7 @@ var FormStore = class {
|
|
|
221
236
|
dirty.add(path);
|
|
222
237
|
const errors = { ...state.errors };
|
|
223
238
|
delete errors[path];
|
|
239
|
+
for (const key of Object.keys(errors)) if (isItemErrorKey(key, path)) delete errors[key];
|
|
224
240
|
return {
|
|
225
241
|
...state,
|
|
226
242
|
values: setPath(state.values, path, value),
|
|
@@ -258,7 +274,7 @@ var FormStore = class {
|
|
|
258
274
|
this.store.update({ errors: {} });
|
|
259
275
|
}
|
|
260
276
|
getError(path) {
|
|
261
|
-
return this.state.errors
|
|
277
|
+
return fieldError(this.state.errors, path);
|
|
262
278
|
}
|
|
263
279
|
setProcessing(processing) {
|
|
264
280
|
this.store.update({ processing });
|
|
@@ -283,7 +299,36 @@ var FormStore = class {
|
|
|
283
299
|
* Send the current state to the server, which re-evaluates closures and
|
|
284
300
|
* `afterStateUpdated` hooks, and replace components + values with the response.
|
|
285
301
|
*/
|
|
286
|
-
|
|
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) {
|
|
287
332
|
if (!this.options.refreshUrl) return;
|
|
288
333
|
this.refreshController?.abort();
|
|
289
334
|
const controller = new AbortController();
|
|
@@ -308,7 +353,7 @@ var FormStore = class {
|
|
|
308
353
|
if (this.editsInFlight === edits) this.editsInFlight = null;
|
|
309
354
|
if (!this.applyResponse(response, sent, edits)) {
|
|
310
355
|
this.fingerprints = void 0;
|
|
311
|
-
await this.
|
|
356
|
+
await this.sendRefresh(path);
|
|
312
357
|
return;
|
|
313
358
|
}
|
|
314
359
|
this.options.onRefreshed?.(path);
|
|
@@ -515,6 +560,6 @@ function cloneIndexedState(state, cloned) {
|
|
|
515
560
|
return result;
|
|
516
561
|
}
|
|
517
562
|
//#endregion
|
|
518
|
-
export { FormStore, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, collectFields, componentHasErrors, fieldDomId, findFieldByPath, formIdPrefix, formIdentity, isLeavingVisit, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, tabFromQuery, tabQueryValue, tabToReveal, wizardStepToReveal };
|
|
563
|
+
export { FormStore, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, collectFields, componentHasErrors, fieldDomId, fieldError, findFieldByPath, formIdPrefix, formIdentity, isLeavingVisit, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, tabFromQuery, tabQueryValue, tabToReveal, wizardStepToReveal };
|
|
519
564
|
|
|
520
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, 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/**\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 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"}
|
package/dist/image-editor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store } from "./store.js";
|
|
2
|
-
import { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, loadEditorImage } from "./image-files.js";
|
|
2
|
+
import { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, imageEditorOptionsKey, loadEditorImage } from "./image-files.js";
|
|
3
3
|
//#region src/image-editor.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The FileUpload image editor (`imageEditor()`): crop, zoom, rotate and flip an image in
|
|
@@ -251,5 +251,5 @@ export declare class ImageEditor {
|
|
|
251
251
|
export(source: CanvasImageSource, file: Pick<File, 'name' | 'type'>): Promise<File>;
|
|
252
252
|
}
|
|
253
253
|
//#endregion
|
|
254
|
-
export { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, loadEditorImage };
|
|
254
|
+
export { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, imageEditorOptionsKey, loadEditorImage };
|
|
255
255
|
//# sourceMappingURL=image-editor.d.ts.map
|
package/dist/image-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createStore } from "./store.js";
|
|
2
2
|
import { parseAspectRatio } from "./utils/image.js";
|
|
3
|
-
import { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, loadEditorImage } from "./image-files.js";
|
|
3
|
+
import { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, imageEditorOptionsKey, loadEditorImage } from "./image-files.js";
|
|
4
4
|
//#region src/image-editor.ts
|
|
5
5
|
/** Zoom factor of one step (buttons and `+` / `-`). */
|
|
6
6
|
const IMAGE_EDITOR_ZOOM_STEP = 1.2;
|
|
@@ -765,6 +765,6 @@ var ImageEditor = class ImageEditor {
|
|
|
765
765
|
}
|
|
766
766
|
};
|
|
767
767
|
//#endregion
|
|
768
|
-
export { IMAGE_EDITOR_ZOOM_STEP, ImageEditor, canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, loadEditorImage };
|
|
768
|
+
export { IMAGE_EDITOR_ZOOM_STEP, ImageEditor, canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, imageEditorOptionsKey, loadEditorImage };
|
|
769
769
|
|
|
770
770
|
//# sourceMappingURL=image-editor.js.map
|
package/dist/image-files.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export declare function editableImageName(file: {
|
|
|
19
19
|
* the file can't be fetched or isn't an editable image.
|
|
20
20
|
*/
|
|
21
21
|
export declare function fetchEditableImage(url: string, name: string, fetcher?: typeof fetch): Promise<File | null>;
|
|
22
|
+
/**
|
|
23
|
+
* The image editor's settings as a value, for the editor dialogs to restart only when they
|
|
24
|
+
* really change. Schemas rendered from a template (a block or repeater item the server
|
|
25
|
+
* hasn't seen yet) are new objects on every render: depending on the object restarted the
|
|
26
|
+
* image's loading on each render, so inside a new CMS block the dialog never left its
|
|
27
|
+
* spinner.
|
|
28
|
+
*/
|
|
29
|
+
export declare function imageEditorOptionsKey(options: object | null | undefined): string;
|
|
22
30
|
/** Decode a file for the editor (EXIF orientation applied by the browser). */
|
|
23
31
|
export declare function loadEditorImage(file: Blob): Promise<ImageBitmap>;
|
|
24
32
|
/** Image files on the clipboard of a paste event (screenshots, copied images). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-files.d.ts","names":[],"sources":["../src/image-files.ts"],"mappings":";;wBAWgB,aAAa,MAAM,KAAK;;wBAKxB,iBAAiB;;;;;;wBAWjB,kBAAkB;EAAQ;EAAc;;;;;;;;wBAclC,mBACpB,aACA,cACA,iBAAgB,QACf,QAAQ;;
|
|
1
|
+
{"version":3,"file":"image-files.d.ts","names":[],"sources":["../src/image-files.ts"],"mappings":";;wBAWgB,aAAa,MAAM,KAAK;;wBAKxB,iBAAiB;;;;;;wBAWjB,kBAAkB;EAAQ;EAAc;;;;;;;;wBAclC,mBACpB,aACA,cACA,iBAAgB,QACf,QAAQ;;;;;;;;wBAwBK,sBAAsB;;wBAKhB,gBAAgB,MAAM,OAAO,QAAQ;;wBAK3C,mBAAmB,MAAM,sBAAsB"}
|
package/dist/image-files.js
CHANGED
|
@@ -49,6 +49,16 @@ async function fetchEditableImage(url, name, fetcher = fetch) {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The image editor's settings as a value, for the editor dialogs to restart only when they
|
|
54
|
+
* really change. Schemas rendered from a template (a block or repeater item the server
|
|
55
|
+
* hasn't seen yet) are new objects on every render: depending on the object restarted the
|
|
56
|
+
* image's loading on each render, so inside a new CMS block the dialog never left its
|
|
57
|
+
* spinner.
|
|
58
|
+
*/
|
|
59
|
+
function imageEditorOptionsKey(options) {
|
|
60
|
+
return JSON.stringify(options ?? null);
|
|
61
|
+
}
|
|
52
62
|
/** Decode a file for the editor (EXIF orientation applied by the browser). */
|
|
53
63
|
async function loadEditorImage(file) {
|
|
54
64
|
return createImageBitmap(file);
|
|
@@ -61,6 +71,6 @@ function filesFromClipboard(data) {
|
|
|
61
71
|
return Array.from(data.items ?? []).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((file) => file !== null);
|
|
62
72
|
}
|
|
63
73
|
//#endregion
|
|
64
|
-
export { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, loadEditorImage };
|
|
74
|
+
export { canEditImage, canEditImageName, editableImageName, fetchEditableImage, filesFromClipboard, imageEditorOptionsKey, loadEditorImage };
|
|
65
75
|
|
|
66
76
|
//# sourceMappingURL=image-files.js.map
|
package/dist/image-files.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-files.js","names":[],"sources":["../src/image-files.ts"],"sourcesContent":["// The image editor's file helpers, apart from the editor itself (`./image-editor`): a\n// FileUpload field uses them on every render, the editor loads with the first image to edit.\n\nconst EDITABLE_TYPES: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n webp: 'image/webp',\n}\n\n/** Raster images the editor can open (SVG and GIF animations are uploaded as they are). */\nexport function canEditImage(file: Pick<File, 'type'>): boolean {\n return ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)\n}\n\n/** Whether a stored file looks like an image the editor can open, from its name. */\nexport function canEditImageName(name: string): boolean {\n const extension = /\\.([a-z0-9]+)(?:[?#].*)?$/i.exec(name)?.[1]?.toLowerCase() ?? ''\n\n return extension in EDITABLE_TYPES\n}\n\n/**\n * The name to open an uploaded file in the editor under: its display name when that\n * looks like an editable image, else its stored path's basename; null when neither does\n * (the \"Edit\" button is hidden).\n */\nexport function editableImageName(file: { name: string; path: string }): string | null {\n if (canEditImageName(file.name)) return file.name\n\n const base = file.path.replace(/^tmp:/, '').split('/').pop() ?? ''\n\n return canEditImageName(base) ? base : null\n}\n\n/**\n * Download an already uploaded image for the editor (the \"Edit\" button on existing\n * files). Fetched from the file's own URL with same-origin credentials, so signed and\n * private URLs served by the app work; a cross-origin URL needs CORS. Resolves null when\n * the file can't be fetched or isn't an editable image.\n */\nexport async function fetchEditableImage(\n url: string,\n name: string,\n fetcher: typeof fetch = fetch,\n): Promise<File | null> {\n try {\n const response = await fetcher(url, { credentials: 'same-origin' })\n\n if (!response.ok) return null\n\n const blob = await response.blob()\n const extension = /\\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase() ?? ''\n const type = canEditImage(blob) ? blob.type : (EDITABLE_TYPES[extension] ?? blob.type)\n const file = new File([blob], name, { type, lastModified: Date.now() })\n\n return canEditImage(file) ? file : null\n } catch {\n return null\n }\n}\n\n/** Decode a file for the editor (EXIF orientation applied by the browser). */\nexport async function loadEditorImage(file: Blob): Promise<ImageBitmap> {\n return createImageBitmap(file)\n}\n\n/** Image files on the clipboard of a paste event (screenshots, copied images). */\nexport function filesFromClipboard(data: DataTransfer | null): File[] {\n if (!data) return []\n\n const files = Array.from(data.files ?? [])\n\n if (files.length > 0) return files\n\n return Array.from(data.items ?? [])\n .filter((item) => item.kind === 'file')\n .map((item) => item.getAsFile())\n .filter((file): file is File => file !== null)\n}\n"],"mappings":";AAGA,MAAM,iBAAyC;CAC7C,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;AACR;;AAGA,SAAgB,aAAa,MAAmC;CAC9D,OAAO;EAAC;EAAc;EAAa;CAAY,CAAC,CAAC,SAAS,KAAK,IAAI;AACrE;;AAGA,SAAgB,iBAAiB,MAAuB;CAGtD,QAFkB,6BAA6B,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,KAAK,OAE7D;AACtB;;;;;;AAOA,SAAgB,kBAAkB,MAAqD;CACrF,IAAI,iBAAiB,KAAK,IAAI,GAAG,OAAO,KAAK;CAE7C,MAAM,OAAO,KAAK,KAAK,QAAQ,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CAEhE,OAAO,iBAAiB,IAAI,IAAI,OAAO;AACzC;;;;;;;AAQA,eAAsB,mBACpB,KACA,MACA,UAAwB,OACF;CACtB,IAAI;EACF,MAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,aAAa,cAAc,CAAC;EAElE,IAAI,CAAC,SAAS,IAAI,OAAO;EAEzB,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,YAAY,kBAAkB,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,KAAK;EACtE,MAAM,OAAO,aAAa,IAAI,IAAI,KAAK,OAAQ,eAAe,cAAc,KAAK;EACjF,MAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,MAAM;GAAE;GAAM,cAAc,KAAK,IAAI;EAAE,CAAC;EAEtE,OAAO,aAAa,IAAI,IAAI,OAAO;CACrC,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,eAAsB,gBAAgB,MAAkC;CACtE,OAAO,kBAAkB,IAAI;AAC/B;;AAGA,SAAgB,mBAAmB,MAAmC;CACpE,IAAI,CAAC,MAAM,OAAO,CAAC;CAEnB,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC;CAEzC,IAAI,MAAM,SAAS,GAAG,OAAO;CAE7B,OAAO,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAChC,QAAQ,SAAS,KAAK,SAAS,MAAM,CAAC,CACtC,KAAK,SAAS,KAAK,UAAU,CAAC,CAAC,CAC/B,QAAQ,SAAuB,SAAS,IAAI;AACjD"}
|
|
1
|
+
{"version":3,"file":"image-files.js","names":[],"sources":["../src/image-files.ts"],"sourcesContent":["// The image editor's file helpers, apart from the editor itself (`./image-editor`): a\n// FileUpload field uses them on every render, the editor loads with the first image to edit.\n\nconst EDITABLE_TYPES: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n webp: 'image/webp',\n}\n\n/** Raster images the editor can open (SVG and GIF animations are uploaded as they are). */\nexport function canEditImage(file: Pick<File, 'type'>): boolean {\n return ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)\n}\n\n/** Whether a stored file looks like an image the editor can open, from its name. */\nexport function canEditImageName(name: string): boolean {\n const extension = /\\.([a-z0-9]+)(?:[?#].*)?$/i.exec(name)?.[1]?.toLowerCase() ?? ''\n\n return extension in EDITABLE_TYPES\n}\n\n/**\n * The name to open an uploaded file in the editor under: its display name when that\n * looks like an editable image, else its stored path's basename; null when neither does\n * (the \"Edit\" button is hidden).\n */\nexport function editableImageName(file: { name: string; path: string }): string | null {\n if (canEditImageName(file.name)) return file.name\n\n const base = file.path.replace(/^tmp:/, '').split('/').pop() ?? ''\n\n return canEditImageName(base) ? base : null\n}\n\n/**\n * Download an already uploaded image for the editor (the \"Edit\" button on existing\n * files). Fetched from the file's own URL with same-origin credentials, so signed and\n * private URLs served by the app work; a cross-origin URL needs CORS. Resolves null when\n * the file can't be fetched or isn't an editable image.\n */\nexport async function fetchEditableImage(\n url: string,\n name: string,\n fetcher: typeof fetch = fetch,\n): Promise<File | null> {\n try {\n const response = await fetcher(url, { credentials: 'same-origin' })\n\n if (!response.ok) return null\n\n const blob = await response.blob()\n const extension = /\\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase() ?? ''\n const type = canEditImage(blob) ? blob.type : (EDITABLE_TYPES[extension] ?? blob.type)\n const file = new File([blob], name, { type, lastModified: Date.now() })\n\n return canEditImage(file) ? file : null\n } catch {\n return null\n }\n}\n\n/**\n * The image editor's settings as a value, for the editor dialogs to restart only when they\n * really change. Schemas rendered from a template (a block or repeater item the server\n * hasn't seen yet) are new objects on every render: depending on the object restarted the\n * image's loading on each render, so inside a new CMS block the dialog never left its\n * spinner.\n */\nexport function imageEditorOptionsKey(options: object | null | undefined): string {\n return JSON.stringify(options ?? null)\n}\n\n/** Decode a file for the editor (EXIF orientation applied by the browser). */\nexport async function loadEditorImage(file: Blob): Promise<ImageBitmap> {\n return createImageBitmap(file)\n}\n\n/** Image files on the clipboard of a paste event (screenshots, copied images). */\nexport function filesFromClipboard(data: DataTransfer | null): File[] {\n if (!data) return []\n\n const files = Array.from(data.files ?? [])\n\n if (files.length > 0) return files\n\n return Array.from(data.items ?? [])\n .filter((item) => item.kind === 'file')\n .map((item) => item.getAsFile())\n .filter((file): file is File => file !== null)\n}\n"],"mappings":";AAGA,MAAM,iBAAyC;CAC7C,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;AACR;;AAGA,SAAgB,aAAa,MAAmC;CAC9D,OAAO;EAAC;EAAc;EAAa;CAAY,CAAC,CAAC,SAAS,KAAK,IAAI;AACrE;;AAGA,SAAgB,iBAAiB,MAAuB;CAGtD,QAFkB,6BAA6B,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,KAAK,OAE7D;AACtB;;;;;;AAOA,SAAgB,kBAAkB,MAAqD;CACrF,IAAI,iBAAiB,KAAK,IAAI,GAAG,OAAO,KAAK;CAE7C,MAAM,OAAO,KAAK,KAAK,QAAQ,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CAEhE,OAAO,iBAAiB,IAAI,IAAI,OAAO;AACzC;;;;;;;AAQA,eAAsB,mBACpB,KACA,MACA,UAAwB,OACF;CACtB,IAAI;EACF,MAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,aAAa,cAAc,CAAC;EAElE,IAAI,CAAC,SAAS,IAAI,OAAO;EAEzB,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,YAAY,kBAAkB,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,KAAK;EACtE,MAAM,OAAO,aAAa,IAAI,IAAI,KAAK,OAAQ,eAAe,cAAc,KAAK;EACjF,MAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,MAAM;GAAE;GAAM,cAAc,KAAK,IAAI;EAAE,CAAC;EAEtE,OAAO,aAAa,IAAI,IAAI,OAAO;CACrC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;AASA,SAAgB,sBAAsB,SAA4C;CAChF,OAAO,KAAK,UAAU,WAAW,IAAI;AACvC;;AAGA,eAAsB,gBAAgB,MAAkC;CACtE,OAAO,kBAAkB,IAAI;AAC/B;;AAGA,SAAgB,mBAAmB,MAAmC;CACpE,IAAI,CAAC,MAAM,OAAO,CAAC;CAEnB,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC;CAEzC,IAAI,MAAM,SAAS,GAAG,OAAO;CAE7B,OAAO,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAChC,QAAQ,SAAS,KAAK,SAAS,MAAM,CAAC,CACtC,KAAK,SAAS,KAAK,UAAU,CAAC,CAAC,CAC/B,QAAQ,SAAuB,SAAS,IAAI;AACjD"}
|