@hybridly/vue 0.10.0-beta.3 → 0.10.0-beta.30

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/dist/index.mjs CHANGED
@@ -1,174 +1,14 @@
1
- import { t as __reExport } from "./_chunks/chunk.mjs";
2
- import { computed, createApp, defineComponent, getCurrentInstance, h, isRef, nextTick, onUnmounted, reactive, readonly, ref, shallowRef, toRaw, toValue, triggerRef, unref, watch } from "vue";
3
- import { can, createRouter, definePlugin, makeUrl, registerHook as registerHook$1, route, route as route$1, router, router as router$1 } from "@hybridly/core";
4
- import { clone, debounce, debug, merge, random, setValueAtPath, showViewComponentErrorModal, unsetPropertyAtPath } from "@hybridly/utils";
5
- import nprogress from "nprogress";
1
+ import { n as __reExport, t as __exportAll } from "./_chunks/chunk.mjs";
2
+ import { debug, merge, random, showViewComponentErrorModal, wrap } from "@hybridly/utils";
3
+ import { computed, createApp, defineComponent, getCurrentInstance, h, isRef, nextTick, onMounted, onUnmounted, reactive, readonly, ref, shallowRef, toRaw, toValue, triggerRef, unref, watch } from "vue";
4
+ import { createRouter, definePlugin, makeUrl, parseQueryString, registerHook as registerHook$1, route, route as route$1, router, router as router$1, stringifyQueryString } from "@hybridly/core";
5
+ import { get, set, unset } from "es-toolkit/compat";
6
+ import { debounce } from "es-toolkit/function";
7
+ import { clone, cloneDeep } from "es-toolkit/object";
8
+ import { isEqual } from "es-toolkit/predicate";
6
9
  import { setupDevtoolsPlugin } from "@vue/devtools-api";
7
- import qs from "qs";
8
- import { getByPath, setByPath } from "@clickbar/dot-diver";
9
- import isEqual from "lodash.isequal";
10
-
11
- //#region src/plugins/progress.ts
12
- /**
13
- * A plugin to display a progress bar.
14
- */
15
- function progress(options) {
16
- const resolved = {
17
- delay: 250,
18
- color: "#29d",
19
- includeCSS: true,
20
- spinner: false,
21
- ...options
22
- };
23
- let timeout;
24
- function startProgress() {
25
- nprogress.done();
26
- nprogress.remove();
27
- nprogress.start();
28
- }
29
- function finishProgress() {
30
- if (nprogress.isStarted()) {
31
- nprogress.done(true);
32
- setTimeout(() => nprogress.remove(), 1e3);
33
- }
34
- clearTimeout(timeout);
35
- }
36
- return definePlugin({
37
- name: "hybridly:progress",
38
- initialized() {
39
- nprogress.configure({ showSpinner: resolved.spinner });
40
- if (resolved.includeCSS) injectCSS(resolved.color);
41
- },
42
- start: (context) => {
43
- if (context.pendingNavigation?.options.progress === false) return;
44
- clearTimeout(timeout);
45
- timeout = setTimeout(() => startProgress(), resolved.delay);
46
- },
47
- progress: (progress) => {
48
- if (nprogress.isStarted() && progress.percentage) nprogress.set(Math.max(nprogress.status, progress.percentage / 100 * .9));
49
- },
50
- after: () => finishProgress()
51
- });
52
- }
53
- function injectCSS(color) {
54
- const element = document.createElement("style");
55
- element.textContent = `
56
- #nprogress {
57
- pointer-events: none;
58
- --progress-color: ${color};
59
- }
60
- #nprogress .bar {
61
- background: var(--progress-color);
62
- position: fixed;
63
- z-index: 1031;
64
- top: 0;
65
- left: 0;
66
- width: 100%;
67
- height: 2px;
68
- }
69
- #nprogress .peg {
70
- display: block;
71
- position: absolute;
72
- right: 0px;
73
- width: 100px;
74
- height: 100%;
75
- box-shadow: 0 0 10px var(--progress-color), 0 0 5px var(--progress-color);
76
- opacity: 1.0;
77
- -webkit-transform: rotate(3deg) translate(0px, -4px);
78
- -ms-transform: rotate(3deg) translate(0px, -4px);
79
- transform: rotate(3deg) translate(0px, -4px);
80
- }
81
- #nprogress .spinner {
82
- display: block;
83
- position: fixed;
84
- z-index: 1031;
85
- top: 15px;
86
- right: 15px;
87
- }
88
- #nprogress .spinner-icon {
89
- width: 18px;
90
- height: 18px;
91
- box-sizing: border-box;
92
- border: solid 2px transparent;
93
- border-top-color: var(--progress-color);
94
- border-left-color: var(--progress-color);
95
- border-radius: 50%;
96
- -webkit-animation: nprogress-spinner 400ms linear infinite;
97
- animation: nprogress-spinner 400ms linear infinite;
98
- }
99
- .nprogress-custom-parent {
100
- overflow: hidden;
101
- position: relative;
102
- }
103
- .nprogress-custom-parent #nprogress .spinner,
104
- .nprogress-custom-parent #nprogress .bar {
105
- position: absolute;
106
- }
107
- @-webkit-keyframes nprogress-spinner {
108
- 0% { -webkit-transform: rotate(0deg); }
109
- 100% { -webkit-transform: rotate(360deg); }
110
- }
111
- @keyframes nprogress-spinner {
112
- 0% { transform: rotate(0deg); }
113
- 100% { transform: rotate(360deg); }
114
- }
115
- `;
116
- document.head.appendChild(element);
117
- }
118
-
119
- //#endregion
120
- //#region src/stores/dialog.ts
121
- const DEBUG_KEY = "vue:state:dialog";
122
- const dialogStore = {
123
- state: {
124
- component: shallowRef(),
125
- properties: ref(),
126
- key: ref(),
127
- show: ref()
128
- },
129
- removeComponent() {
130
- if (dialogStore.state.component.value) {
131
- debug.adapter(DEBUG_KEY, "Removing dialog.");
132
- dialogStore.state.component.value = void 0;
133
- }
134
- },
135
- setComponent(component) {
136
- debug.adapter(DEBUG_KEY, "Setting dialog component:", component);
137
- dialogStore.state.component.value = component;
138
- },
139
- setProperties(properties) {
140
- debug.adapter(DEBUG_KEY, "Setting dialog properties:", properties);
141
- dialogStore.state.properties.value = unref(properties);
142
- },
143
- setKey(key) {
144
- debug.adapter(DEBUG_KEY, "Setting dialog key:", {
145
- new: key,
146
- previous: dialogStore.state.key.value
147
- });
148
- dialogStore.state.key.value = unref(key);
149
- },
150
- show() {
151
- if (!dialogStore.state.show.value) {
152
- debug.adapter(DEBUG_KEY, "Showing the dialog.");
153
- dialogStore.state.show.value = true;
154
- }
155
- },
156
- hide() {
157
- if (dialogStore.state.show.value) {
158
- debug.adapter(DEBUG_KEY, "Hiding the dialog.");
159
- dialogStore.state.show.value = false;
160
- }
161
- }
162
- };
163
-
164
- //#endregion
165
- //#region src/stores/mount.ts
166
- /**
167
- * These callbacks are run on the first stick after the view component is mounted.
168
- */
169
- const onMountedCallbacks = [];
170
-
171
- //#endregion
10
+ import nprogress from "nprogress";
11
+ import { getByPath } from "@clickbar/dot-diver";
172
12
  //#region src/stores/state.ts
173
13
  const state = {
174
14
  context: shallowRef(),
@@ -193,204 +33,71 @@ const state = {
193
33
  state.viewKey.value = unref(key);
194
34
  }
195
35
  };
196
-
197
36
  //#endregion
198
- //#region src/components/wrapper.ts
199
- const wrapper = defineComponent({
200
- name: "Hybridly",
201
- setup() {
202
- function renderLayout(view) {
203
- debug.adapter("vue:render:layout", "Rendering layout.");
204
- if (typeof state.view.value?.layout === "function") return state.view.value.layout(h, view, renderDialog(), {
205
- ...state.view.value?.properties ?? {},
206
- ...state.properties.value
207
- });
208
- if (Array.isArray(state.view.value?.layout)) return [state.view.value.layout.concat(view).reverse().reduce((child, layout) => {
209
- layout.inheritAttrs = !!layout.inheritAttrs;
210
- return h(layout, {
211
- ...state.view.value?.properties ?? {},
212
- ...state.properties.value
213
- }, () => child);
214
- }), renderDialog()];
215
- return [h(state.view.value?.layout, {
216
- ...state.view.value?.properties ?? {},
217
- ...state.properties.value
218
- }, () => view), renderDialog()];
219
- }
220
- function hijackOnMounted(component, type) {
221
- if (!component) return;
222
- const actual = component?.mounted;
223
- component.mounted = () => {
224
- actual?.();
225
- nextTick(() => {
226
- debug.adapter(`vue:render:${type}`, "Calling mounted callbacks.");
227
- while (onMountedCallbacks.length) onMountedCallbacks.shift()?.();
228
- });
229
- };
230
- }
231
- function renderView() {
232
- debug.adapter("vue:render:view", "Rendering view.");
233
- state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
234
- hijackOnMounted(state.view.value, "view");
235
- return h(state.view.value, {
236
- ...state.properties.value,
237
- key: state.viewKey.value
238
- });
239
- }
240
- function renderDialog() {
241
- if (dialogStore.state.component.value && dialogStore.state.properties.value) {
242
- debug.adapter("vue:render:dialog", "Rendering dialog.");
243
- hijackOnMounted(dialogStore.state.component.value, "dialog");
244
- return h(dialogStore.state.component.value, {
245
- ...dialogStore.state.properties.value,
246
- key: dialogStore.state.key.value
247
- });
37
+ //#region src/components/load-state.ts
38
+ function keysAreBeingReloaded(only, except, keys) {
39
+ only = wrap(only);
40
+ except = wrap(except);
41
+ if (only.length > 0) return keys.some((key) => only.includes(key));
42
+ if (except.length > 0) return !keys.some((key) => except.includes(key));
43
+ return true;
44
+ }
45
+ function useLoadState(getKeys) {
46
+ const hasLoadedOnce = ref(getKeys().every((key) => get(state.properties.value, key) !== void 0));
47
+ const reloading = ref(false);
48
+ const activeReloads = /* @__PURE__ */ new Set();
49
+ let removeStartListener = null;
50
+ let removeFinishListener = null;
51
+ onMounted(() => {
52
+ removeStartListener = registerHook$1("start", (request) => {
53
+ if (hasLoadedOnce.value === false) return;
54
+ if (request.options.preserveState !== true) return;
55
+ const keys = getKeys();
56
+ if (!keysAreBeingReloaded(request.options.only, request.options.except, keys)) return;
57
+ activeReloads.add(request.id);
58
+ reloading.value = true;
59
+ });
60
+ removeFinishListener = registerHook$1("after", (request) => {
61
+ if (activeReloads.has(request.id)) {
62
+ activeReloads.delete(request.id);
63
+ reloading.value = activeReloads.size > 0;
248
64
  }
249
- }
250
- return (...a) => {
251
- if (!state.view.value) return;
252
- debug.adapter("vue:render:wrapper", "Rendering wrapper component.", a.map(toRaw));
253
- const view = renderView();
254
- if (state.view.value.layout) return renderLayout(view);
255
- return [view, renderDialog()];
65
+ });
66
+ });
67
+ onUnmounted(() => {
68
+ removeStartListener?.();
69
+ removeFinishListener?.();
70
+ activeReloads.clear();
71
+ });
72
+ function getSlotProps() {
73
+ const loaded = getKeys().every((key) => get(state.properties.value, key) !== void 0);
74
+ if (loaded) hasLoadedOnce.value = true;
75
+ return {
76
+ reloading: reloading.value,
77
+ loading: !loaded || reloading.value,
78
+ loaded
79
+ };
80
+ }
81
+ return { getSlotProps };
82
+ }
83
+ //#endregion
84
+ //#region src/components/deferred.ts
85
+ const Deferred = defineComponent({
86
+ name: "Deferred",
87
+ props: { data: {
88
+ type: [String, Array],
89
+ required: true
90
+ } },
91
+ slots: Object,
92
+ setup(props, { slots }) {
93
+ const { getSlotProps } = useLoadState(() => wrap(props.data));
94
+ return () => {
95
+ const slotProps = getSlotProps();
96
+ if (!slotProps.loaded && !!slots.fallback) return slots.fallback(slotProps);
97
+ return slots.default?.(slotProps);
256
98
  };
257
99
  }
258
100
  });
259
-
260
- //#endregion
261
- //#region src/devtools.ts
262
- const hybridlyStateType = "hybridly";
263
- const hybridlyEventsTimelineLayerId = "Hybridly";
264
- function setupDevtools(app) {
265
- setupDevtoolsPlugin({
266
- id: "hybridly",
267
- label: "Hybridly",
268
- packageName: "@hybridly/vue",
269
- homepage: "https://github.com/hybridly",
270
- app,
271
- enableEarlyProxy: true,
272
- componentStateTypes: [hybridlyStateType]
273
- }, (api) => {
274
- api.on.inspectComponent((payload) => {
275
- payload.instanceData.state.push({
276
- type: hybridlyStateType,
277
- key: "properties",
278
- value: state.context.value?.view.properties,
279
- editable: true
280
- });
281
- payload.instanceData.state.push({
282
- type: hybridlyStateType,
283
- key: "component",
284
- value: state.context.value?.view.component
285
- });
286
- payload.instanceData.state.push({
287
- type: hybridlyStateType,
288
- key: "deferred",
289
- value: state.context.value?.view.deferred
290
- });
291
- payload.instanceData.state.push({
292
- type: hybridlyStateType,
293
- key: "dialog",
294
- value: state.context.value?.dialog
295
- });
296
- payload.instanceData.state.push({
297
- type: hybridlyStateType,
298
- key: "version",
299
- value: state.context.value?.version
300
- });
301
- payload.instanceData.state.push({
302
- type: hybridlyStateType,
303
- key: "url",
304
- value: state.context.value?.url
305
- });
306
- payload.instanceData.state.push({
307
- type: hybridlyStateType,
308
- key: "routing",
309
- value: state.context.value?.routing
310
- });
311
- });
312
- api.on.editComponentState((payload) => {
313
- if (payload.type === hybridlyStateType) payload.set(state.context.value?.view);
314
- });
315
- api.addTimelineLayer({
316
- id: hybridlyEventsTimelineLayerId,
317
- color: 16501221,
318
- label: "Hybridly"
319
- });
320
- const listen = [
321
- "start",
322
- "ready",
323
- "data",
324
- "navigating",
325
- "navigated",
326
- "progress",
327
- "error",
328
- "abort",
329
- "success",
330
- "invalid",
331
- "exception",
332
- "fail",
333
- "after",
334
- "backForward",
335
- "success"
336
- ];
337
- registerHook$1("before", (options) => {
338
- const groupId = (Math.random() + 1).toString(36).substring(7);
339
- api.addTimelineEvent({
340
- layerId: hybridlyEventsTimelineLayerId,
341
- event: {
342
- groupId,
343
- title: "before",
344
- time: api.now(),
345
- data: options
346
- }
347
- });
348
- listen.forEach((event) => registerHook$1(event, (data) => {
349
- api.addTimelineEvent({
350
- layerId: hybridlyEventsTimelineLayerId,
351
- event: {
352
- groupId,
353
- title: event,
354
- time: api.now(),
355
- data
356
- }
357
- });
358
- if (event === "after") setTimeout(() => {
359
- api.notifyComponentUpdate();
360
- }, 100);
361
- }, { once: true }));
362
- });
363
- });
364
- }
365
- const devtools = { install(app) {
366
- if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) setupDevtools(app);
367
- } };
368
-
369
- //#endregion
370
- //#region src/plugins/view-transition.ts
371
- function viewTransition() {
372
- if (!document.startViewTransition) return { name: "view-transition" };
373
- let domUpdated;
374
- return {
375
- name: "view-transition",
376
- navigating: async ({ type, hasDialog }) => {
377
- if (type === "initial" || hasDialog) return;
378
- return new Promise((confirmTransitionStarted) => document.startViewTransition(() => {
379
- confirmTransitionStarted(true);
380
- return new Promise((resolve) => domUpdated = resolve);
381
- }));
382
- },
383
- mounted: () => {
384
- domUpdated?.();
385
- domUpdated = void 0;
386
- },
387
- navigated: () => {
388
- domUpdated?.();
389
- domUpdated = void 0;
390
- }
391
- };
392
- }
393
-
394
101
  //#endregion
395
102
  //#region src/stores/form.ts
396
103
  const formStore = {
@@ -402,230 +109,1489 @@ const formStore = {
402
109
  return clone(formStore.defaultConfig);
403
110
  }
404
111
  };
405
-
406
112
  //#endregion
407
- //#region src/initialize.ts
408
- /**
409
- * Initializes Hybridly's router and context.
410
- */
411
- async function initializeHybridly(options = {}) {
412
- const resolved = options;
413
- const { element, payload, resolve } = prepare(resolved);
414
- if (!element) throw new Error("Could not find an HTML element to initialize Vue on.");
415
- state.setContext(await createRouter({
416
- axios: resolved.axios,
417
- plugins: resolved.plugins,
418
- serializer: resolved.serializer,
419
- responseErrorModals: resolved.responseErrorModals ?? process.env.NODE_ENV === "development",
420
- routing: resolved.routing,
421
- adapter: {
422
- resolveComponent: resolve,
423
- executeOnMounted: (callback) => {
424
- onMountedCallbacks.push(callback);
425
- },
426
- onDialogClose: async () => {
427
- dialogStore.hide();
428
- },
429
- onContextUpdate: (context) => {
430
- state.setContext(context);
431
- },
432
- onViewSwap: async (options) => {
433
- if (options.component) {
434
- onMountedCallbacks.push(() => options.onMounted?.({ isDialog: false }));
435
- state.setView(options.component);
436
- }
437
- state.setProperties(options.properties);
438
- if (!options.preserveState && !options.dialog) state.setViewKey(random());
439
- if (options.dialog) {
440
- onMountedCallbacks.push(() => options.onMounted?.({ isDialog: true }));
441
- dialogStore.setComponent(await resolve(options.dialog.component));
442
- dialogStore.setProperties(options.dialog.properties);
443
- dialogStore.setKey(options.dialog.key);
444
- dialogStore.show();
445
- } else dialogStore.hide();
446
- }
447
- },
448
- payload
449
- }));
450
- const render = () => h(wrapper);
451
- if (options.setup) return await options.setup({
452
- element,
453
- wrapper,
454
- render,
455
- hybridly: devtools,
456
- props: { context: state.context.value },
457
- payload
458
- });
459
- const app = createApp({ render });
460
- if (resolved.devtools !== false) app.use(devtools);
461
- await options.enhanceVue?.(app, payload);
462
- return app.mount(element);
113
+ //#region src/composables/form.ts
114
+ function deepCloneRaw(obj) {
115
+ return cloneDeep(toRaw(obj));
463
116
  }
464
- function prepare(options) {
465
- debug.adapter("vue", "Preparing Hybridly with options:", options);
466
- const isServer = typeof window === "undefined";
467
- const id = options.id ?? "root";
468
- const element = document?.getElementById(id) ?? void 0;
469
- debug.adapter("vue", `Element "${id}" is:`, element);
470
- const payload = element?.dataset.payload ? JSON.parse(element.dataset.payload) : void 0;
471
- if (!payload) throw new Error("No payload found. Are you using the `@hybridly` directive?");
472
- if (options.cleanup !== false) delete element.dataset.payload;
473
- debug.adapter("vue", "Resolved:", {
474
- isServer,
475
- element,
476
- payload
477
- });
478
- const resolve = async (name) => {
479
- debug.adapter("vue", "Resolving component", name);
480
- if (!options.imported) throw new Error("No component loaded. Did you initialize Hybridly? Does `php artisan hybridly:config` return an error?");
481
- return await resolveViewComponent(name, options);
482
- };
483
- options.plugins ??= [];
484
- if (options.progress !== false) options.plugins.push(progress(typeof options.progress === "object" ? options.progress : {}));
485
- if (options.viewTransition !== false) options.plugins.push(viewTransition());
486
- if (options.defaultFormOptions) formStore.setDefaultConfig(options.defaultFormOptions);
487
- return {
488
- isServer,
489
- element,
490
- payload,
491
- resolve
117
+ function useForm(options) {
118
+ const shouldRemember = !!options.key;
119
+ const historyKey = options.key ?? "form:default";
120
+ const historyData = shouldRemember ? router$1.history.get(historyKey) : void 0;
121
+ const timeoutIds = {
122
+ recentlyFailed: void 0,
123
+ recentlySuccessful: void 0
492
124
  };
493
- }
494
- /**
495
- * Resolves a view by its name.
496
- */
497
- async function resolveViewComponent(name, options) {
498
- const components = options.imported;
499
- const result = options.components.views.find((view) => name === view.identifier);
500
- const path = Object.keys(components).sort((a, b) => a.length - b.length).find((path) => result ? path.endsWith(result?.path) : false);
501
- if (!result || !path) {
502
- console.warn(`View component [${name}] not found. Available components: `, options.components.views.map(({ identifier }) => identifier));
503
- showViewComponentErrorModal(name);
504
- return;
125
+ /** Fields that were initially set up. */
126
+ const defaults = ref(deepCloneRaw(options.fields));
127
+ /** Fields as they were when loaded. */
128
+ const loaded = deepCloneRaw(historyData?.fields ?? options.fields);
129
+ /** Current fields. */
130
+ const fields = reactive(deepCloneRaw(loaded));
131
+ /** Validation errors for each field. */
132
+ const errors = ref(historyData?.errors ?? {});
133
+ /** Whether the form is dirty. */
134
+ const isDirty = ref(false);
135
+ /** Whether the submission was recently successful. */
136
+ const recentlySuccessful = ref(false);
137
+ /** Whether the submission is successful. */
138
+ const successful = ref(false);
139
+ /** Whether the submission was recently failed. */
140
+ const recentlyFailed = ref(false);
141
+ /** Whether the submission is failed. */
142
+ const failed = ref(false);
143
+ /** Whether the submission is being processed. */
144
+ const processing = ref(false);
145
+ /** The current request's progress. */
146
+ const progress = ref();
147
+ /** The current request. */
148
+ const request = shallowRef();
149
+ /** Abort controller for the current request. */
150
+ let abortController;
151
+ /**
152
+ * Sets new default values for the form, so subsequent resets will use these values.
153
+ */
154
+ function setDefault(newDefault) {
155
+ Object.entries(newDefault).forEach(([key, value]) => {
156
+ set(defaults.value, key, deepCloneRaw(value));
157
+ });
505
158
  }
506
- let component = typeof components[path] === "function" ? await components[path]() : components[path];
507
- component = component.default ?? component;
508
- return component;
509
- }
510
-
511
- //#endregion
512
- //#region src/components/link.ts
513
- const RouterLink = defineComponent({
514
- name: "RouterLink",
515
- setup(_, { slots, attrs }) {
516
- return (props) => {
517
- let data = props.data ?? {};
518
- const preloads = props.preload ?? false;
519
- const preserveScroll = props.preserveScroll;
520
- const preserveState = props.preserveState;
521
- const url = makeUrl(props.href ?? "");
522
- const method = props.method?.toUpperCase() ?? "GET";
523
- const as = typeof props.as === "object" ? props.as : props.as?.toLowerCase() ?? "a";
524
- if (method === "GET") {
525
- debug.adapter("vue", "Moving data object to URL parameters.");
526
- url.search = qs.stringify(merge(data, qs.parse(url.search, { ignoreQueryPrefix: true })), {
527
- encodeValuesOnly: true,
528
- arrayFormat: "indices"
529
- });
530
- data = {};
531
- }
532
- if (as === "a" && method !== "GET") debug.adapter("vue", `Creating POST/PUT/PATCH/DELETE <a> links is discouraged as it causes "Open Link in New Tab/Window" accessibility issues.\n\nPlease specify a more appropriate element using the "as" attribute. For example:\n\n<RouterLink href="${url}" method="${method}" as="button">...</RouterLink>`);
533
- function performPreload(type) {
534
- if (!preloads) return;
535
- if (props.external) return;
536
- if (method !== "GET") return;
537
- if (type !== "mount" && props.disabled) return;
538
- if (type === "hover" && preloads === "mount") return;
539
- if (type === "mount" && preloads !== "mount") return;
540
- router$1.preload(url, {
541
- data,
542
- preserveScroll,
543
- preserveState,
544
- ...props.options
545
- });
546
- }
547
- performPreload("mount");
548
- return h(props.as, {
549
- ...attrs,
550
- ...as === "a" ? { href: url } : {},
551
- ...props.disabled ? { disabled: props.disabled } : {},
552
- onMouseenter: () => performPreload("hover"),
553
- onAuxclick: (event) => {
554
- if (props.disabled) event.preventDefault();
555
- },
556
- onClick: (event) => {
557
- if (props.disabled) {
558
- event.preventDefault();
559
- return;
560
- }
561
- if (props.external) return;
562
- if (!shouldIntercept(event)) return;
563
- event.preventDefault();
564
- router$1.navigate({
565
- url,
566
- data,
567
- method,
568
- preserveState: method !== "GET",
569
- ...props.options
570
- });
159
+ function resolveFieldBehaviorKeys(option, fallback) {
160
+ if (option === false) return;
161
+ if (Array.isArray(option)) return option.length > 0 ? option : void 0;
162
+ if (option === true) return Object.keys(fields);
163
+ return fallback ? Object.keys(fields) : void 0;
164
+ }
165
+ function setDefaultFromFields(option) {
166
+ const keys = resolveFieldBehaviorKeys(option, false);
167
+ if (!keys) return;
168
+ keys.forEach((key) => {
169
+ set(defaults.value, key, deepCloneRaw(get(fields, key)));
170
+ });
171
+ }
172
+ function resetFieldsFromBehavior(option, fallback) {
173
+ const keys = resolveFieldBehaviorKeys(option, fallback);
174
+ if (!keys) return;
175
+ resetFields(...keys);
176
+ }
177
+ /**
178
+ * Resets the form's failed and successful flags.
179
+ */
180
+ function resetSubmissionState() {
181
+ successful.value = false;
182
+ failed.value = false;
183
+ recentlyFailed.value = false;
184
+ recentlySuccessful.value = false;
185
+ clearTimeout(timeoutIds.recentlySuccessful);
186
+ clearTimeout(timeoutIds.recentlyFailed);
187
+ progress.value = void 0;
188
+ }
189
+ /**
190
+ * Resets the fields, errors and submission state.
191
+ */
192
+ function reset() {
193
+ resetSubmissionState();
194
+ clearErrors();
195
+ resetFields();
196
+ }
197
+ /**
198
+ * Resets the fields to their default values.
199
+ */
200
+ function resetFields(...keys) {
201
+ if (keys.length === 0) keys = Object.keys(fields);
202
+ keys.forEach((key) => {
203
+ set(fields, key, deepCloneRaw(get(defaults.value, key)));
204
+ });
205
+ }
206
+ /**
207
+ * Clear the form fields.
208
+ */
209
+ function clear(...keys) {
210
+ if (keys.length === 0) keys = Object.keys(fields);
211
+ keys.forEach((key) => {
212
+ delete fields[key];
213
+ });
214
+ }
215
+ /**
216
+ * Submits the form.
217
+ */
218
+ function submit(optionsOverrides) {
219
+ const { fields: _f, key: _k, automaticallySubmit: _as, ...optionsWithoutFields } = options;
220
+ const resolvedOptions = optionsOverrides ? merge(optionsWithoutFields, optionsOverrides, { mergePlainObjects: true }) : optionsWithoutFields;
221
+ const { timeout, resetOnSuccess, resetOnError, setDefaultOnSuccess, transform, ...requestOptions } = merge(formStore.getDefaultConfig(), resolvedOptions, { mergePlainObjects: true });
222
+ const url = typeof requestOptions.url === "function" ? requestOptions.url() : requestOptions.url;
223
+ const data = typeof transform === "function" ? transform(fields) : fields;
224
+ const preserveState = requestOptions.preserveState ?? requestOptions.method !== "GET";
225
+ const hooks = requestOptions.hooks ?? {};
226
+ abortController = requestOptions.abortController ?? new AbortController();
227
+ return router$1.navigate({
228
+ ...requestOptions,
229
+ abortController,
230
+ url: url ?? state.context.value?.url,
231
+ method: requestOptions.method ?? "POST",
232
+ data: deepCloneRaw(data),
233
+ preserveState,
234
+ hooks: {
235
+ before: (_request, context) => {
236
+ request.value = _request;
237
+ resetSubmissionState();
238
+ return hooks.before?.(_request, context);
239
+ },
240
+ start: (request, context) => {
241
+ processing.value = true;
242
+ return hooks.start?.(request, context);
243
+ },
244
+ progress: (incoming, request, context) => {
245
+ progress.value = {
246
+ event: incoming,
247
+ percentage: incoming.percentage
248
+ };
249
+ return hooks.progress?.(incoming, request, context);
250
+ },
251
+ "validation-error": (incoming, request, context) => {
252
+ setErrors(incoming);
253
+ resetFieldsFromBehavior(resetOnError, false);
254
+ failed.value = true;
255
+ recentlyFailed.value = true;
256
+ timeoutIds.recentlyFailed = setTimeout(() => recentlyFailed.value = false, timeout ?? 5e3);
257
+ return hooks["validation-error"]?.(incoming, request, context);
258
+ },
259
+ success: (payload, request, response, context) => {
260
+ clearErrors();
261
+ setDefaultFromFields(setDefaultOnSuccess);
262
+ resetFieldsFromBehavior(resetOnSuccess, true);
263
+ successful.value = true;
264
+ recentlySuccessful.value = true;
265
+ timeoutIds.recentlySuccessful = setTimeout(() => recentlySuccessful.value = false, timeout ?? 5e3);
266
+ return hooks.success?.(payload, request, response, context);
267
+ },
268
+ after: (_request, context) => {
269
+ request.value = void 0;
270
+ progress.value = void 0;
271
+ processing.value = false;
272
+ return hooks.after?.(_request, context);
571
273
  }
572
- }, slots.default ? slots : props.text);
573
- };
574
- },
274
+ }
275
+ });
276
+ }
277
+ if (options.automaticallySubmit) {
278
+ const submitOptions = options.automaticallySubmit === true ? void 0 : (() => {
279
+ const { debounce: _debounce, ...submitOptions } = options.automaticallySubmit;
280
+ return submitOptions;
281
+ })();
282
+ const automaticallySubmitOptions = options.automaticallySubmit === true ? {
283
+ debounce: 100,
284
+ immediate: true
285
+ } : options.automaticallySubmit;
286
+ watch(() => fields, debounce(() => {
287
+ if (!isDirty.value) return;
288
+ submit(submitOptions);
289
+ }, automaticallySubmitOptions.debounce ?? 100), {
290
+ deep: true,
291
+ immediate: automaticallySubmitOptions.immediate ?? true
292
+ });
293
+ }
294
+ /**
295
+ * Clears all errors.
296
+ */
297
+ function clearErrors(...keys) {
298
+ if (keys.length === 0) keys = Object.keys(fields);
299
+ keys.forEach((key) => {
300
+ clearError(key);
301
+ });
302
+ }
303
+ /**
304
+ * Checks if the given keys are dirty in the form.
305
+ */
306
+ function hasDirty(...keys) {
307
+ if (keys.length === 0) return isDirty.value;
308
+ return keys.some((key) => !isEqual(toRaw(get(fields, key)), toRaw(get(defaults.value, key))));
309
+ }
310
+ /**
311
+ * Clears the given field's error.
312
+ */
313
+ function clearError(key) {
314
+ unset(errors.value, key);
315
+ }
316
+ /**
317
+ * Sets current errors.
318
+ */
319
+ function setErrors(incoming) {
320
+ clearErrors();
321
+ Object.entries(incoming).forEach(([path, value]) => {
322
+ set(errors.value, path, value);
323
+ });
324
+ }
325
+ /**
326
+ * Aborts the submission.
327
+ */
328
+ function abort() {
329
+ abortController?.abort();
330
+ }
331
+ watch([
332
+ fields,
333
+ processing,
334
+ errors
335
+ ], () => {
336
+ isDirty.value = !isEqual(toRaw(defaults.value), toRaw(fields));
337
+ if (shouldRemember) router$1.history.remember(historyKey, {
338
+ fields: toRaw(fields),
339
+ errors: toRaw(errors.value)
340
+ });
341
+ }, {
342
+ deep: true,
343
+ immediate: true
344
+ });
345
+ return reactive({
346
+ resetFields,
347
+ reset,
348
+ resetSubmissionState,
349
+ clear,
350
+ fields,
351
+ abort,
352
+ setErrors,
353
+ clearErrors,
354
+ clearError,
355
+ setDefault,
356
+ hasDirty,
357
+ submit,
358
+ hasErrors: computed(() => Object.values(errors.value ?? {}).length > 0),
359
+ defaults,
360
+ loaded,
361
+ progress,
362
+ isDirty,
363
+ errors,
364
+ processing,
365
+ successful,
366
+ failed,
367
+ recentlySuccessful,
368
+ recentlyFailed
369
+ });
370
+ }
371
+ //#endregion
372
+ //#region src/components/form.ts
373
+ function isSubmitterElement(value) {
374
+ if (value instanceof HTMLButtonElement) return true;
375
+ if (value instanceof HTMLInputElement) return value.type === "submit" || value.type === "image";
376
+ return false;
377
+ }
378
+ function normalizeFieldName(name) {
379
+ const appendToArray = name.endsWith("[]");
380
+ return {
381
+ path: (appendToArray ? name.slice(0, -2) : name).replace(/\[([^\]]+)\]/g, ".$1").replace(/^\./, ""),
382
+ appendToArray
383
+ };
384
+ }
385
+ function appendFieldValue(target, path, value, forceArray) {
386
+ const existing = get(target, path);
387
+ if (existing === void 0) {
388
+ set(target, path, forceArray ? [value] : value);
389
+ return;
390
+ }
391
+ if (Array.isArray(existing)) {
392
+ existing.push(value);
393
+ return;
394
+ }
395
+ set(target, path, [existing, value]);
396
+ }
397
+ function isFormControl(control) {
398
+ return control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement || control instanceof HTMLSelectElement;
399
+ }
400
+ function getControlFieldInfo(control) {
401
+ if (!isFormControl(control) || !control.name) return;
402
+ const { path, appendToArray } = normalizeFieldName(control.name);
403
+ if (!path.length) return;
404
+ return {
405
+ control,
406
+ path,
407
+ appendToArray
408
+ };
409
+ }
410
+ function setCurrentValuesAsDefaults(form, keys) {
411
+ for (const element of Array.from(form.elements)) {
412
+ const field = getControlFieldInfo(element);
413
+ if (keys && (!field || !keys.has(field.path))) continue;
414
+ if (element instanceof HTMLInputElement) {
415
+ if (element.type === "checkbox" || element.type === "radio") {
416
+ element.defaultChecked = element.checked;
417
+ continue;
418
+ }
419
+ if (element.type === "file") continue;
420
+ element.defaultValue = element.value;
421
+ continue;
422
+ }
423
+ if (element instanceof HTMLTextAreaElement) {
424
+ element.defaultValue = element.value;
425
+ continue;
426
+ }
427
+ if (element instanceof HTMLSelectElement) for (const option of Array.from(element.options)) option.defaultSelected = option.selected;
428
+ }
429
+ }
430
+ function resetControlToDefault(control) {
431
+ if (control instanceof HTMLInputElement) {
432
+ if (control.type === "checkbox" || control.type === "radio") {
433
+ control.checked = control.defaultChecked;
434
+ return;
435
+ }
436
+ if (control.type === "file") {
437
+ control.value = "";
438
+ return;
439
+ }
440
+ control.value = control.defaultValue;
441
+ return;
442
+ }
443
+ if (control instanceof HTMLTextAreaElement) {
444
+ control.value = control.defaultValue;
445
+ return;
446
+ }
447
+ if (control instanceof HTMLSelectElement) for (const option of Array.from(control.options)) option.selected = option.defaultSelected;
448
+ }
449
+ function applyFieldValueToControl(control, fieldValue) {
450
+ if (control instanceof HTMLInputElement) {
451
+ if (control.type === "file") return;
452
+ if (control.type === "checkbox") {
453
+ if (Array.isArray(fieldValue)) {
454
+ control.checked = fieldValue.map(String).includes(control.value);
455
+ return;
456
+ }
457
+ if (typeof fieldValue === "boolean") {
458
+ control.checked = fieldValue;
459
+ return;
460
+ }
461
+ control.checked = String(fieldValue ?? "") === control.value;
462
+ return;
463
+ }
464
+ if (control.type === "radio") {
465
+ control.checked = String(fieldValue ?? "") === control.value;
466
+ return;
467
+ }
468
+ control.value = fieldValue == null ? "" : String(fieldValue);
469
+ return;
470
+ }
471
+ if (control instanceof HTMLTextAreaElement) {
472
+ control.value = fieldValue == null ? "" : String(fieldValue);
473
+ return;
474
+ }
475
+ if (control instanceof HTMLSelectElement) {
476
+ if (control.multiple && Array.isArray(fieldValue)) {
477
+ const values = new Set(fieldValue.map((value) => String(value)));
478
+ for (const option of Array.from(control.options)) option.selected = values.has(option.value);
479
+ return;
480
+ }
481
+ control.value = fieldValue == null ? "" : String(fieldValue);
482
+ }
483
+ }
484
+ function syncControlsFromFields(form, fields) {
485
+ const pathIndexes = /* @__PURE__ */ new Map();
486
+ for (const control of Array.from(form.elements)) {
487
+ const field = getControlFieldInfo(control);
488
+ if (!field) continue;
489
+ const value = get(fields, field.path);
490
+ if (field.appendToArray && Array.isArray(value)) {
491
+ const currentIndex = pathIndexes.get(field.path) ?? 0;
492
+ applyFieldValueToControl(control, value[currentIndex]);
493
+ pathIndexes.set(field.path, currentIndex + 1);
494
+ continue;
495
+ }
496
+ applyFieldValueToControl(control, value);
497
+ }
498
+ }
499
+ const Form = defineComponent({
500
+ name: "Form",
501
+ inheritAttrs: false,
575
502
  props: {
576
- href: {
503
+ action: {
577
504
  type: String,
578
505
  required: false,
579
506
  default: void 0
580
507
  },
581
- as: {
582
- type: [String, Object],
583
- default: "a"
584
- },
585
508
  method: {
586
509
  type: String,
587
- default: "GET"
588
- },
589
- data: {
590
- type: Object,
591
- default: () => ({})
592
- },
593
- external: {
594
- type: Boolean,
595
- default: false
596
- },
597
- disabled: {
598
- type: Boolean,
599
- default: false
510
+ default: "POST"
600
511
  },
601
512
  options: {
602
513
  type: Object,
603
514
  default: () => ({})
604
515
  },
605
- text: {
516
+ errorBag: {
606
517
  type: String,
607
518
  required: false,
608
519
  default: void 0
609
520
  },
610
- preload: {
611
- type: [Boolean, String],
612
- default: false
613
- },
614
- preserveScroll: {
521
+ showProgress: {
615
522
  type: Boolean,
523
+ required: false,
616
524
  default: void 0
617
525
  },
618
- preserveState: {
526
+ disableWhileProcessing: {
619
527
  type: Boolean,
620
- default: void 0
528
+ default: false
529
+ },
530
+ resetOnSuccess: {
531
+ type: [Boolean, Array],
532
+ default: true
533
+ },
534
+ resetOnError: {
535
+ type: [Boolean, Array],
536
+ default: false
537
+ },
538
+ setDefaultOnSuccess: {
539
+ type: [Boolean, Array],
540
+ default: false
541
+ }
542
+ },
543
+ slots: Object,
544
+ setup(props, { attrs, slots }) {
545
+ const element = ref();
546
+ const form = useForm({
547
+ fields: {},
548
+ url: () => props.action ?? element.value?.action ?? window.location.href,
549
+ method: props.method,
550
+ errorBag: props.errorBag,
551
+ progress: props.showProgress,
552
+ resetOnSuccess: props.resetOnSuccess,
553
+ resetOnError: props.resetOnError,
554
+ setDefaultOnSuccess: props.setDefaultOnSuccess
555
+ });
556
+ function collectFormFields(submitter) {
557
+ if (!element.value) return {};
558
+ const formData = submitter ? new FormData(element.value, submitter) : new FormData(element.value);
559
+ const fields = {};
560
+ for (const [name, value] of formData.entries()) {
561
+ const { path, appendToArray } = normalizeFieldName(name);
562
+ if (!path.length) continue;
563
+ appendFieldValue(fields, path, value, appendToArray);
564
+ }
565
+ return fields;
566
+ }
567
+ function collectDefaultFormFields() {
568
+ if (!element.value) return {};
569
+ const fields = {};
570
+ for (const control of Array.from(element.value.elements)) {
571
+ const field = getControlFieldInfo(control);
572
+ if (!field) continue;
573
+ if (control instanceof HTMLInputElement) {
574
+ if (control.type === "file") continue;
575
+ if (control.type === "checkbox" || control.type === "radio") {
576
+ if (control.defaultChecked) appendFieldValue(fields, field.path, control.value, field.appendToArray);
577
+ continue;
578
+ }
579
+ appendFieldValue(fields, field.path, control.defaultValue, field.appendToArray);
580
+ continue;
581
+ }
582
+ if (control instanceof HTMLTextAreaElement) {
583
+ appendFieldValue(fields, field.path, control.defaultValue, field.appendToArray);
584
+ continue;
585
+ }
586
+ if (control instanceof HTMLSelectElement) {
587
+ if (control.multiple) {
588
+ for (const option of Array.from(control.options)) if (option.defaultSelected) appendFieldValue(fields, field.path, option.value, field.appendToArray);
589
+ continue;
590
+ }
591
+ const defaultOption = Array.from(control.options).find((option) => option.defaultSelected) ?? control.options.item(0);
592
+ if (defaultOption) appendFieldValue(fields, field.path, defaultOption.value, field.appendToArray);
593
+ }
594
+ }
595
+ return fields;
596
+ }
597
+ function syncDefaultsFromNativeControls() {
598
+ if (!element.value) return;
599
+ const defaultFields = collectDefaultFormFields();
600
+ const nextDefaults = {};
601
+ for (const control of Array.from(element.value.elements)) {
602
+ const field = getControlFieldInfo(control);
603
+ if (!field) continue;
604
+ nextDefaults[field.path] = get(defaultFields, field.path);
605
+ }
606
+ form.setDefault(nextDefaults);
607
+ }
608
+ function syncFields(fields) {
609
+ const target = form.fields;
610
+ Object.keys(target).forEach((key) => {
611
+ delete target[key];
612
+ });
613
+ Object.entries(fields).forEach(([key, value]) => {
614
+ target[key] = value;
615
+ });
616
+ }
617
+ function resetFields(...keys) {
618
+ if (!element.value) return;
619
+ if (keys.length === 0) element.value.reset();
620
+ else {
621
+ const keySet = new Set(keys);
622
+ for (const control of Array.from(element.value.elements)) {
623
+ const field = getControlFieldInfo(control);
624
+ if (!field || !keySet.has(field.path)) continue;
625
+ resetControlToDefault(control);
626
+ }
627
+ }
628
+ syncFields(collectFormFields());
629
+ }
630
+ function reset() {
631
+ form.resetSubmissionState();
632
+ form.clearErrors();
633
+ resetFields();
634
+ }
635
+ function submit(options, submitter) {
636
+ syncFields(collectFormFields(submitter));
637
+ syncDefaultsFromNativeControls();
638
+ const { hooks, ...rest } = merge({
639
+ errorBag: props.errorBag,
640
+ progress: props.showProgress,
641
+ resetOnSuccess: props.resetOnSuccess,
642
+ resetOnError: props.resetOnError,
643
+ setDefaultOnSuccess: props.setDefaultOnSuccess
644
+ }, {
645
+ ...props.options,
646
+ ...options
647
+ }, { mergePlainObjects: true });
648
+ const submitOptions = {
649
+ ...rest,
650
+ hooks: {
651
+ ...hooks,
652
+ "validation-error": (incoming, request, context) => {
653
+ nextTick(() => {
654
+ if (!element.value) return;
655
+ syncControlsFromFields(element.value, form.fields);
656
+ });
657
+ return hooks?.["validation-error"]?.(incoming, request, context);
658
+ },
659
+ success: (payload, request, response, context) => {
660
+ if (element.value && rest.setDefaultOnSuccess !== false) {
661
+ const selectedKeys = Array.isArray(rest.setDefaultOnSuccess) ? new Set(rest.setDefaultOnSuccess) : void 0;
662
+ setCurrentValuesAsDefaults(element.value, selectedKeys);
663
+ syncDefaultsFromNativeControls();
664
+ }
665
+ nextTick(() => {
666
+ if (!element.value) return;
667
+ syncControlsFromFields(element.value, form.fields);
668
+ });
669
+ return hooks?.success?.(payload, request, response, context);
670
+ }
671
+ }
672
+ };
673
+ return form.submit(submitOptions);
674
+ }
675
+ function onSubmit(event) {
676
+ event.preventDefault();
677
+ return submit(void 0, isSubmitterElement(event.submitter) ? event.submitter : void 0);
678
+ }
679
+ return () => h("form", {
680
+ ...attrs,
681
+ ref: element,
682
+ action: props.action,
683
+ method: props.method,
684
+ inert: props.disableWhileProcessing && form.processing ? "" : void 0,
685
+ onSubmit
686
+ }, slots.default?.({
687
+ ...form,
688
+ errors: form.errors,
689
+ getError: (key) => get(form.errors, key),
690
+ submit,
691
+ resetFields,
692
+ reset
693
+ }));
694
+ }
695
+ });
696
+ //#endregion
697
+ //#region src/components/link.ts
698
+ const RouterLink = defineComponent({
699
+ name: "RouterLink",
700
+ setup(_, { slots, attrs }) {
701
+ return (props) => {
702
+ let data = props.data ?? {};
703
+ const mode = props.mode;
704
+ const preserveScroll = props.preserveScroll;
705
+ const preserveState = props.preserveState;
706
+ const url = makeUrl(props.href ?? "");
707
+ const method = props.method?.toUpperCase() ?? "GET";
708
+ const as = typeof props.as === "object" ? props.as : props.as?.toLowerCase() ?? "a";
709
+ if (method === "GET") {
710
+ debug.adapter("vue", "Moving data object to URL parameters.");
711
+ url.search = stringifyQueryString(merge(data, parseQueryString(url.search)), { arrayFormat: "indices" });
712
+ data = {};
713
+ }
714
+ if (as === "a" && method !== "GET") debug.adapter("vue", `Creating POST/PUT/PATCH/DELETE <a> links is discouraged as it causes "Open Link in New Tab/Window" accessibility issues.\n\nPlease specify a more appropriate element using the "as" attribute. For example:\n\n<RouterLink href="${url}" method="${method}" as="button">...</RouterLink>`);
715
+ return h(props.as, {
716
+ ...attrs,
717
+ ...as === "a" ? { href: url } : {},
718
+ ...props.disabled ? { disabled: props.disabled } : {},
719
+ onAuxclick: (event) => {
720
+ if (props.disabled) event.preventDefault();
721
+ },
722
+ onClick: (event) => {
723
+ if (props.disabled) {
724
+ event.preventDefault();
725
+ return;
726
+ }
727
+ if (props.external) return;
728
+ if (!shouldIntercept(event)) return;
729
+ event.preventDefault();
730
+ router$1.navigate({
731
+ url,
732
+ data,
733
+ method,
734
+ mode,
735
+ preserveState: preserveState ?? (method !== "GET" ? true : void 0),
736
+ preserveScroll,
737
+ ...props.options
738
+ });
739
+ }
740
+ }, slots.default ? slots : props.text);
741
+ };
742
+ },
743
+ props: {
744
+ href: {
745
+ type: String,
746
+ required: false,
747
+ default: void 0
748
+ },
749
+ as: {
750
+ type: [String, Object],
751
+ default: "a"
752
+ },
753
+ method: {
754
+ type: String,
755
+ default: "GET"
756
+ },
757
+ data: {
758
+ type: Object,
759
+ default: () => ({})
760
+ },
761
+ external: {
762
+ type: Boolean,
763
+ default: false
764
+ },
765
+ disabled: {
766
+ type: Boolean,
767
+ default: false
768
+ },
769
+ options: {
770
+ type: Object,
771
+ default: () => ({})
772
+ },
773
+ text: {
774
+ type: String,
775
+ required: false,
776
+ default: void 0
777
+ },
778
+ preload: {
779
+ type: [Boolean, String],
780
+ default: false
781
+ },
782
+ preserveScroll: {
783
+ type: Boolean,
784
+ default: void 0
785
+ },
786
+ preserveState: {
787
+ type: Boolean,
788
+ default: void 0
789
+ },
790
+ mode: {
791
+ type: String,
792
+ default: void 0
793
+ }
794
+ }
795
+ });
796
+ function shouldIntercept(event) {
797
+ const isLink = event.currentTarget.tagName.toLowerCase() === "a";
798
+ return !(event.target && (event?.target).isContentEditable || event.defaultPrevented || isLink && event.which > 1 || isLink && event.altKey || isLink && event.ctrlKey || isLink && event.metaKey || isLink && event.shiftKey);
799
+ }
800
+ //#endregion
801
+ //#region src/components/when-visible.ts
802
+ const WhenVisible = defineComponent({
803
+ name: "WhenVisible",
804
+ emits: { loading: (_state) => true },
805
+ props: {
806
+ as: {
807
+ type: String,
808
+ default: "div"
809
+ },
810
+ data: {
811
+ type: [String, Array],
812
+ required: true
813
+ },
814
+ buffer: {
815
+ type: Number,
816
+ default: 0
817
+ },
818
+ root: {
819
+ type: [String, Object],
820
+ default: null
821
+ },
822
+ once: {
823
+ type: Boolean,
824
+ default: true
825
+ },
826
+ fallbackOnHidden: {
827
+ type: Boolean,
828
+ default: false
829
+ },
830
+ options: {
831
+ type: Object,
832
+ default: () => ({})
833
+ }
834
+ },
835
+ slots: Object,
836
+ setup(properties, { emit, slots }) {
837
+ const isVisible = ref(false);
838
+ const element = ref(null);
839
+ const hasRan = ref(false);
840
+ let observer;
841
+ const { getSlotProps } = useLoadState(() => wrap(properties.data));
842
+ function registerObserver() {
843
+ observer?.disconnect();
844
+ const root = typeof properties.root === "string" ? document.querySelector(properties.root) : properties.root;
845
+ observer = new IntersectionObserver(([entry]) => isVisible.value = entry.isIntersecting, {
846
+ root,
847
+ rootMargin: `${properties.buffer}px 0px ${properties.buffer}px 0px`
848
+ });
849
+ if (element.value) observer.observe(element.value);
850
+ }
851
+ onMounted(() => {
852
+ registerObserver();
853
+ watch(() => [properties.buffer, properties.root], () => {
854
+ registerObserver();
855
+ });
856
+ });
857
+ onUnmounted(() => {
858
+ observer?.disconnect();
859
+ });
860
+ watch(isVisible, (visible) => {
861
+ if (hasRan.value && properties.once) return;
862
+ if (visible) {
863
+ emit("loading", getSlotProps());
864
+ router$1.reload({
865
+ ...properties.options,
866
+ only: properties.data
867
+ });
868
+ hasRan.value = true;
869
+ }
870
+ });
871
+ return () => {
872
+ const slotProperties = getSlotProps();
873
+ const shouldRender = slotProperties.loaded && (isVisible.value || !properties.fallbackOnHidden);
874
+ return h(properties.as, { ref: element }, [shouldRender ? slots.default?.(slotProperties) : slots.fallback?.(slotProperties)]);
875
+ };
876
+ }
877
+ });
878
+ //#endregion
879
+ //#region src/stores/dialog.ts
880
+ const DEBUG_KEY = "vue:state:dialog";
881
+ const dialogStore = {
882
+ state: {
883
+ component: shallowRef(),
884
+ properties: ref(),
885
+ key: ref(),
886
+ show: ref()
887
+ },
888
+ removeComponent() {
889
+ if (dialogStore.state.component.value) {
890
+ debug.adapter(DEBUG_KEY, "Removing dialog.");
891
+ dialogStore.state.component.value = void 0;
892
+ }
893
+ },
894
+ setComponent(component) {
895
+ debug.adapter(DEBUG_KEY, "Setting dialog component:", component);
896
+ dialogStore.state.component.value = component;
897
+ },
898
+ setProperties(properties) {
899
+ debug.adapter(DEBUG_KEY, "Setting dialog properties:", properties);
900
+ dialogStore.state.properties.value = unref(properties);
901
+ },
902
+ setKey(key) {
903
+ debug.adapter(DEBUG_KEY, "Setting dialog key:", {
904
+ new: key,
905
+ previous: dialogStore.state.key.value
906
+ });
907
+ dialogStore.state.key.value = unref(key);
908
+ },
909
+ show() {
910
+ if (!dialogStore.state.show.value) {
911
+ debug.adapter(DEBUG_KEY, "Showing the dialog.");
912
+ dialogStore.state.show.value = true;
913
+ }
914
+ },
915
+ hide() {
916
+ if (dialogStore.state.show.value) {
917
+ debug.adapter(DEBUG_KEY, "Hiding the dialog.");
918
+ dialogStore.state.show.value = false;
919
+ }
920
+ }
921
+ };
922
+ //#endregion
923
+ //#region src/stores/mount.ts
924
+ /**
925
+ * These callbacks are run on the first stick after the view component is mounted.
926
+ */
927
+ const onMountedCallbacks = [];
928
+ //#endregion
929
+ //#region src/components/wrapper.ts
930
+ const wrapper = defineComponent({
931
+ name: "Hybridly",
932
+ setup() {
933
+ function renderLayout(view) {
934
+ debug.adapter("vue:render:layout", "Rendering layout.");
935
+ if (typeof state.view.value?.layout === "function") return state.view.value.layout(h, view, renderDialog(), {
936
+ ...state.view.value?.properties ?? {},
937
+ ...state.properties.value
938
+ });
939
+ if (Array.isArray(state.view.value?.layout)) return [state.view.value.layout.concat(view).reverse().reduce((child, layout) => {
940
+ layout.inheritAttrs = !!layout.inheritAttrs;
941
+ return h(layout, {
942
+ ...state.view.value?.properties ?? {},
943
+ ...state.properties.value
944
+ }, () => child);
945
+ }), renderDialog()];
946
+ return [h(state.view.value?.layout, {
947
+ ...state.view.value?.properties ?? {},
948
+ ...state.properties.value
949
+ }, () => view), renderDialog()];
950
+ }
951
+ function hijackOnMounted(component, type) {
952
+ if (!component) return;
953
+ const actual = component?.mounted;
954
+ component.mounted = () => {
955
+ actual?.();
956
+ nextTick(() => {
957
+ debug.adapter(`vue:render:${type}`, "Calling mounted callbacks.");
958
+ while (onMountedCallbacks.length) onMountedCallbacks.shift()?.();
959
+ });
960
+ };
961
+ }
962
+ function renderView() {
963
+ debug.adapter("vue:render:view", "Rendering view.");
964
+ state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
965
+ hijackOnMounted(state.view.value, "view");
966
+ return h(state.view.value, {
967
+ ...state.properties.value,
968
+ key: state.viewKey.value
969
+ });
970
+ }
971
+ function renderDialog() {
972
+ if (dialogStore.state.component.value && dialogStore.state.properties.value) {
973
+ debug.adapter("vue:render:dialog", "Rendering dialog.");
974
+ hijackOnMounted(dialogStore.state.component.value, "dialog");
975
+ return h(dialogStore.state.component.value, {
976
+ ...dialogStore.state.properties.value,
977
+ key: dialogStore.state.key.value
978
+ });
979
+ }
980
+ }
981
+ return (...a) => {
982
+ if (!state.view.value) return;
983
+ debug.adapter("vue:render:wrapper", "Rendering wrapper component.", a.map(toRaw));
984
+ const view = renderView();
985
+ if (state.view.value.layout) return renderLayout(view);
986
+ return [view, renderDialog()];
987
+ };
988
+ }
989
+ });
990
+ //#endregion
991
+ //#region src/default-layout.ts
992
+ function resolveDefaultLayout(layout) {
993
+ if (!layout) return;
994
+ if (Array.isArray(layout)) return layout;
995
+ if (typeof layout === "function" && layout.length === 0) return layout();
996
+ return layout;
997
+ }
998
+ function applyDefaultLayout(component, layout) {
999
+ if (!layout) return component;
1000
+ const resolved = component;
1001
+ if (resolved.layout === void 0) resolved.layout = layout;
1002
+ return component;
1003
+ }
1004
+ //#endregion
1005
+ //#region src/devtools.ts
1006
+ const hybridlyStateType = "hybridly";
1007
+ const hybridlyEventsTimelineLayerId = "Hybridly";
1008
+ function setupDevtools(app) {
1009
+ setupDevtoolsPlugin({
1010
+ id: "hybridly",
1011
+ label: "Hybridly",
1012
+ packageName: "@hybridly/vue",
1013
+ homepage: "https://github.com/hybridly",
1014
+ app,
1015
+ enableEarlyProxy: true,
1016
+ componentStateTypes: [hybridlyStateType]
1017
+ }, (api) => {
1018
+ api.on.inspectComponent((payload) => {
1019
+ payload.instanceData.state.push({
1020
+ type: hybridlyStateType,
1021
+ key: "properties",
1022
+ value: state.context.value?.view.properties,
1023
+ editable: true
1024
+ });
1025
+ payload.instanceData.state.push({
1026
+ type: hybridlyStateType,
1027
+ key: "validation",
1028
+ value: state.context.value?.validation
1029
+ });
1030
+ payload.instanceData.state.push({
1031
+ type: hybridlyStateType,
1032
+ key: "component",
1033
+ value: state.context.value?.view.component
1034
+ });
1035
+ payload.instanceData.state.push({
1036
+ type: hybridlyStateType,
1037
+ key: "deferred",
1038
+ value: state.context.value?.view.deferred
1039
+ });
1040
+ payload.instanceData.state.push({
1041
+ type: hybridlyStateType,
1042
+ key: "dialog",
1043
+ value: state.context.value?.dialog
1044
+ });
1045
+ payload.instanceData.state.push({
1046
+ type: hybridlyStateType,
1047
+ key: "version",
1048
+ value: state.context.value?.version
1049
+ });
1050
+ payload.instanceData.state.push({
1051
+ type: hybridlyStateType,
1052
+ key: "url",
1053
+ value: state.context.value?.url
1054
+ });
1055
+ payload.instanceData.state.push({
1056
+ type: hybridlyStateType,
1057
+ key: "routing",
1058
+ value: state.context.value?.routing
1059
+ });
1060
+ });
1061
+ api.on.editComponentState((payload) => {
1062
+ if (payload.type === hybridlyStateType) payload.set(state.context.value?.view);
1063
+ });
1064
+ api.addTimelineLayer({
1065
+ id: hybridlyEventsTimelineLayerId,
1066
+ color: 16501221,
1067
+ label: "Hybridly"
1068
+ });
1069
+ const listen = [
1070
+ "start",
1071
+ "ready",
1072
+ "data",
1073
+ "navigating",
1074
+ "navigated",
1075
+ "progress",
1076
+ "validation-error",
1077
+ "abort",
1078
+ "success",
1079
+ "invalid",
1080
+ "exception",
1081
+ "fail",
1082
+ "after",
1083
+ "backForward",
1084
+ "success"
1085
+ ];
1086
+ registerHook$1("before", (options) => {
1087
+ const groupId = (Math.random() + 1).toString(36).substring(7);
1088
+ api.addTimelineEvent({
1089
+ layerId: hybridlyEventsTimelineLayerId,
1090
+ event: {
1091
+ groupId,
1092
+ title: "before",
1093
+ time: api.now(),
1094
+ data: options
1095
+ }
1096
+ });
1097
+ listen.forEach((event) => registerHook$1(event, (data) => {
1098
+ api.addTimelineEvent({
1099
+ layerId: hybridlyEventsTimelineLayerId,
1100
+ event: {
1101
+ groupId,
1102
+ title: event,
1103
+ time: api.now(),
1104
+ data
1105
+ }
1106
+ });
1107
+ if (event === "after") setTimeout(() => {
1108
+ api.notifyComponentUpdate();
1109
+ }, 100);
1110
+ }, { once: true }));
1111
+ });
1112
+ });
1113
+ }
1114
+ const devtools = { install(app) {
1115
+ if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) setupDevtools(app);
1116
+ } };
1117
+ //#endregion
1118
+ //#region src/plugins/progress.ts
1119
+ /**
1120
+ * A plugin to display a progress bar.
1121
+ */
1122
+ function progress(options) {
1123
+ const resolved = {
1124
+ delay: 250,
1125
+ color: "#29d",
1126
+ includeCSS: true,
1127
+ spinner: false,
1128
+ ...options
1129
+ };
1130
+ let timeout;
1131
+ function startProgress() {
1132
+ nprogress.done();
1133
+ nprogress.remove();
1134
+ nprogress.start();
1135
+ }
1136
+ function finishProgress() {
1137
+ if (nprogress.isStarted()) {
1138
+ nprogress.done(true);
1139
+ setTimeout(() => nprogress.remove(), 1e3);
1140
+ }
1141
+ clearTimeout(timeout);
1142
+ }
1143
+ return definePlugin({
1144
+ name: "hybridly:progress",
1145
+ initialized() {
1146
+ nprogress.configure({ showSpinner: resolved.spinner });
1147
+ if (resolved.includeCSS) injectCSS(resolved.color);
1148
+ },
1149
+ start: (request) => {
1150
+ if (request.options.progress === false) return;
1151
+ clearTimeout(timeout);
1152
+ timeout = setTimeout(() => startProgress(), resolved.delay);
1153
+ },
1154
+ progress: (progress) => {
1155
+ if (nprogress.isStarted() && progress.percentage) nprogress.set(Math.max(nprogress.status, progress.percentage / 100 * .9));
1156
+ },
1157
+ after: () => finishProgress()
1158
+ });
1159
+ }
1160
+ function injectCSS(color) {
1161
+ const element = document.createElement("style");
1162
+ element.textContent = `
1163
+ #nprogress {
1164
+ pointer-events: none;
1165
+ --progress-color: ${color};
1166
+ }
1167
+ #nprogress .bar {
1168
+ background: var(--progress-color);
1169
+ position: fixed;
1170
+ z-index: 1031;
1171
+ top: 0;
1172
+ left: 0;
1173
+ width: 100%;
1174
+ height: 2px;
1175
+ }
1176
+ #nprogress .peg {
1177
+ display: block;
1178
+ position: absolute;
1179
+ right: 0px;
1180
+ width: 100px;
1181
+ height: 100%;
1182
+ box-shadow: 0 0 10px var(--progress-color), 0 0 5px var(--progress-color);
1183
+ opacity: 1.0;
1184
+ -webkit-transform: rotate(3deg) translate(0px, -4px);
1185
+ -ms-transform: rotate(3deg) translate(0px, -4px);
1186
+ transform: rotate(3deg) translate(0px, -4px);
1187
+ }
1188
+ #nprogress .spinner {
1189
+ display: block;
1190
+ position: fixed;
1191
+ z-index: 1031;
1192
+ top: 15px;
1193
+ right: 15px;
1194
+ }
1195
+ #nprogress .spinner-icon {
1196
+ width: 18px;
1197
+ height: 18px;
1198
+ box-sizing: border-box;
1199
+ border: solid 2px transparent;
1200
+ border-top-color: var(--progress-color);
1201
+ border-left-color: var(--progress-color);
1202
+ border-radius: 50%;
1203
+ -webkit-animation: nprogress-spinner 400ms linear infinite;
1204
+ animation: nprogress-spinner 400ms linear infinite;
1205
+ }
1206
+ .nprogress-custom-parent {
1207
+ overflow: hidden;
1208
+ position: relative;
1209
+ }
1210
+ .nprogress-custom-parent #nprogress .spinner,
1211
+ .nprogress-custom-parent #nprogress .bar {
1212
+ position: absolute;
1213
+ }
1214
+ @-webkit-keyframes nprogress-spinner {
1215
+ 0% { -webkit-transform: rotate(0deg); }
1216
+ 100% { -webkit-transform: rotate(360deg); }
1217
+ }
1218
+ @keyframes nprogress-spinner {
1219
+ 0% { transform: rotate(0deg); }
1220
+ 100% { transform: rotate(360deg); }
1221
+ }
1222
+ `;
1223
+ document.head.appendChild(element);
1224
+ }
1225
+ //#endregion
1226
+ //#region src/plugins/view-transition.ts
1227
+ function viewTransition() {
1228
+ if (!document.startViewTransition) return { name: "view-transition" };
1229
+ let domUpdated;
1230
+ return {
1231
+ name: "view-transition",
1232
+ navigating: async ({ type, hasDialog, viewTransition }) => {
1233
+ if (type === "initial" || hasDialog || viewTransition === false) return;
1234
+ return new Promise((confirmTransitionStarted) => document.startViewTransition({
1235
+ update: () => {
1236
+ confirmTransitionStarted(true);
1237
+ return new Promise((resolve) => domUpdated = resolve);
1238
+ },
1239
+ types: getViewTransitionType(viewTransition)
1240
+ }));
1241
+ },
1242
+ mounted: () => {
1243
+ domUpdated?.();
1244
+ domUpdated = void 0;
1245
+ },
1246
+ navigated: () => {
1247
+ domUpdated?.();
1248
+ domUpdated = void 0;
1249
+ }
1250
+ };
1251
+ }
1252
+ function getViewTransitionType(viewTransition) {
1253
+ if (viewTransition === false || viewTransition === void 0 || viewTransition === true) return null;
1254
+ if (Array.isArray(viewTransition)) return viewTransition;
1255
+ return [viewTransition];
1256
+ }
1257
+ //#endregion
1258
+ //#region src/initialize.ts
1259
+ /**
1260
+ * Initializes Hybridly's router and context.
1261
+ */
1262
+ async function initializeHybridly(options = {}) {
1263
+ const resolved = options;
1264
+ const { element, payload, resolve } = prepare(resolved);
1265
+ if (!element) throw new Error("Could not find an HTML element to initialize Vue on.");
1266
+ state.setContext(await createRouter({
1267
+ http: resolved.http,
1268
+ plugins: resolved.plugins,
1269
+ serializer: resolved.serializer,
1270
+ responseErrorModals: resolved.responseErrorModals ?? process.env.NODE_ENV === "development",
1271
+ routing: resolved.routing,
1272
+ adapter: {
1273
+ resolveComponent: resolve,
1274
+ executeOnMounted: (callback) => {
1275
+ onMountedCallbacks.push(callback);
1276
+ },
1277
+ onDialogClose: async () => {
1278
+ dialogStore.hide();
1279
+ },
1280
+ onContextUpdate: (context) => {
1281
+ state.setContext(context);
1282
+ },
1283
+ onPropertiesUpdate: (properties) => {
1284
+ state.setProperties(properties);
1285
+ },
1286
+ onViewSwap: async (options) => {
1287
+ if (options.component) {
1288
+ onMountedCallbacks.push(() => options.onMounted?.({ isDialog: false }));
1289
+ state.setView(options.component);
1290
+ }
1291
+ state.setProperties(options.properties);
1292
+ if (!options.preserveState && !options.dialog) state.setViewKey(random());
1293
+ if (options.dialog) {
1294
+ onMountedCallbacks.push(() => options.onMounted?.({ isDialog: true }));
1295
+ dialogStore.setComponent(await resolve(options.dialog.component));
1296
+ dialogStore.setProperties(options.dialog.properties);
1297
+ dialogStore.setKey(options.dialog.key);
1298
+ dialogStore.show();
1299
+ } else dialogStore.hide();
1300
+ }
1301
+ },
1302
+ payload
1303
+ }));
1304
+ const render = () => h(wrapper);
1305
+ if (options.setup) return await options.setup({
1306
+ element,
1307
+ wrapper,
1308
+ render,
1309
+ hybridly: devtools,
1310
+ props: { context: state.context.value },
1311
+ payload
1312
+ });
1313
+ const app = createApp({ render });
1314
+ if (resolved.devtools !== false) app.use(devtools);
1315
+ await options.enhanceVue?.(app, payload);
1316
+ return app.mount(element);
1317
+ }
1318
+ function prepare(options) {
1319
+ debug.adapter("vue", "Preparing Hybridly with options:", options);
1320
+ const isServer = typeof window === "undefined";
1321
+ const id = options.id ?? "root";
1322
+ const element = document?.getElementById(id) ?? void 0;
1323
+ debug.adapter("vue", `Element "${id}" is:`, element);
1324
+ const payload = element?.dataset.payload ? JSON.parse(element.dataset.payload) : void 0;
1325
+ if (!payload) throw new Error("No payload found. Are you using the `@hybridly` directive?");
1326
+ if (options.cleanup !== false) delete element.dataset.payload;
1327
+ debug.adapter("vue", "Resolved:", {
1328
+ isServer,
1329
+ element,
1330
+ payload
1331
+ });
1332
+ const resolve = async (name) => {
1333
+ debug.adapter("vue", "Resolving component", name);
1334
+ if (!options.imported) throw new Error("No component loaded. Did you initialize Hybridly? Does `php artisan hybridly:config` return an error?");
1335
+ return await resolveViewComponent(name, options);
1336
+ };
1337
+ options.plugins ??= [];
1338
+ if (options.progress !== false) options.plugins.push(progress(typeof options.progress === "object" ? options.progress : {}));
1339
+ if (options.viewTransition !== false) options.plugins.push(viewTransition());
1340
+ if (options.defaultFormOptions) formStore.setDefaultConfig(options.defaultFormOptions);
1341
+ options.layout = resolveDefaultLayout(options.layout);
1342
+ return {
1343
+ isServer,
1344
+ element,
1345
+ payload,
1346
+ resolve
1347
+ };
1348
+ }
1349
+ /**
1350
+ * Resolves a view by its name.
1351
+ */
1352
+ async function resolveViewComponent(name, options) {
1353
+ const components = options.imported;
1354
+ const result = options.components.views.find((view) => name === view.identifier);
1355
+ const path = Object.keys(components).sort((a, b) => a.length - b.length).find((path) => result ? path.endsWith(result?.path) : false);
1356
+ if (!result || !path) {
1357
+ console.warn(`View component [${name}] not found. Available components: `, options.components.views.map(({ identifier }) => identifier));
1358
+ showViewComponentErrorModal(name);
1359
+ return;
1360
+ }
1361
+ let component = typeof components[path] === "function" ? await components[path]() : components[path];
1362
+ component = component.default ?? component;
1363
+ component = applyDefaultLayout(component, options.layout);
1364
+ return component;
1365
+ }
1366
+ //#endregion
1367
+ //#region src/composables/back-forward.ts
1368
+ function useBackForward(options) {
1369
+ const callbacks = [];
1370
+ registerHook$1("navigated", (options) => {
1371
+ if (options.type === "back-forward") {
1372
+ callbacks.forEach((fn) => fn(state.context.value));
1373
+ callbacks.splice(0, callbacks.length);
1374
+ }
1375
+ });
1376
+ /**
1377
+ * Applies the given callback on back-forward navigations.
1378
+ */
1379
+ function onBackForward(fn) {
1380
+ callbacks.push(fn);
1381
+ }
1382
+ /**
1383
+ * Reloads the page on back-forward navigations.
1384
+ */
1385
+ function reloadOnBackForward(options) {
1386
+ onBackForward(() => router$1.reload(options));
1387
+ }
1388
+ if (options?.reload) reloadOnBackForward(options.reload === true ? void 0 : options.reload);
1389
+ return {
1390
+ onBackForward,
1391
+ reloadOnBackForward
1392
+ };
1393
+ }
1394
+ //#endregion
1395
+ //#region src/composables/bulk-select.ts
1396
+ /**
1397
+ * Returns the inclusive range between two records in the given order.
1398
+ *
1399
+ * This is useful for implementing shift-click bulk selection: keep track of the last selected anchor,
1400
+ * then pass the currently visible record identifiers and the clicked target identifier to this helper.
1401
+ * If the target is not in the list, an empty range is returned. If the anchor is missing, only the target is returned.
1402
+ */
1403
+ function getBulkSelectionRange(records, anchor, target) {
1404
+ const targetIndex = records.indexOf(target);
1405
+ if (targetIndex === -1) return [];
1406
+ if (anchor === void 0) return [target];
1407
+ const anchorIndex = records.indexOf(anchor);
1408
+ if (anchorIndex === -1) return [target];
1409
+ return records.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1);
1410
+ }
1411
+ function useBulkSelect() {
1412
+ const selection = ref({
1413
+ all: false,
1414
+ only: /* @__PURE__ */ new Set(),
1415
+ except: /* @__PURE__ */ new Set()
1416
+ });
1417
+ /**
1418
+ * Toggles selection for all records.
1419
+ */
1420
+ function toggleAll(force) {
1421
+ if (!selection.value.all || force === true) selectAll();
1422
+ else deselectAll();
1423
+ }
1424
+ /**
1425
+ * Selects all records.
1426
+ */
1427
+ function selectAll() {
1428
+ selection.value.all = true;
1429
+ selection.value.only.clear();
1430
+ selection.value.except.clear();
1431
+ }
1432
+ /**
1433
+ * Deselects all records.
1434
+ */
1435
+ function deselectAll() {
1436
+ selection.value.all = false;
1437
+ selection.value.only.clear();
1438
+ selection.value.except.clear();
1439
+ }
1440
+ /**
1441
+ * Selects the given records.
1442
+ */
1443
+ function select(...records) {
1444
+ records.forEach((record) => selection.value.except.delete(record));
1445
+ records.forEach((record) => selection.value.only.add(record));
1446
+ }
1447
+ /**
1448
+ * Deselects the given records.
1449
+ */
1450
+ function deselect(...records) {
1451
+ records.forEach((record) => selection.value.except.add(record));
1452
+ records.forEach((record) => selection.value.only.delete(record));
1453
+ }
1454
+ /**
1455
+ * Toggles selection for the given records.
1456
+ */
1457
+ function toggle(record, force) {
1458
+ if (selected(record) || force === false) return deselect(record);
1459
+ if (!selected(record) || force === true) return select(record);
1460
+ }
1461
+ /**
1462
+ * Checks whether the given record is selected.
1463
+ */
1464
+ function selected(record) {
1465
+ if (selection.value.all) return !selection.value.except.has(record);
1466
+ return selection.value.only.has(record);
1467
+ }
1468
+ /**
1469
+ * Checks whether all records are selected.
1470
+ */
1471
+ const allSelected = computed(() => {
1472
+ return selection.value.all && selection.value.except.size === 0;
1473
+ });
1474
+ /**
1475
+ * Checks whether any record is selected.
1476
+ */
1477
+ const anySelected = computed(() => {
1478
+ return selection.value.all || selection.value.only.size > 0;
1479
+ });
1480
+ /**
1481
+ * Binds a checkbox's properties.
1482
+ */
1483
+ function bindCheckbox(key) {
1484
+ return {
1485
+ onChange: (event) => {
1486
+ const target = event.target;
1487
+ if (target.checked) select(target.value);
1488
+ else deselect(target.value);
1489
+ },
1490
+ checked: selected(key),
1491
+ value: key
1492
+ };
1493
+ }
1494
+ return {
1495
+ allSelected,
1496
+ anySelected,
1497
+ toggleAll,
1498
+ selectAll,
1499
+ deselectAll,
1500
+ select,
1501
+ deselect,
1502
+ toggle,
1503
+ selected,
1504
+ selection,
1505
+ bindCheckbox
1506
+ };
1507
+ }
1508
+ //#endregion
1509
+ //#region src/composables/register-hook.ts
1510
+ /**
1511
+ * Registers a global hook.
1512
+ * If called inside a component, unregisters after the component is unmounted.
1513
+ */
1514
+ const registerHook = (hook, fn, options) => {
1515
+ const unregister = registerHook$1(hook, fn, options);
1516
+ if (getCurrentInstance()) onUnmounted(() => unregister());
1517
+ return unregister;
1518
+ };
1519
+ //#endregion
1520
+ //#region src/composables/current-route.ts
1521
+ const isNavigating = ref(false);
1522
+ function useRoute() {
1523
+ const current = ref(router$1.current());
1524
+ function matches(name, parameters) {
1525
+ return router$1.matches(toValue(name), parameters);
1526
+ }
1527
+ registerHook("before", () => isNavigating.value = true);
1528
+ registerHook("after", () => isNavigating.value = false);
1529
+ registerHook("navigated", () => {
1530
+ current.value = router$1.current();
1531
+ });
1532
+ return {
1533
+ isNavigating: readonly(isNavigating),
1534
+ current: readonly(current),
1535
+ matches
1536
+ };
1537
+ }
1538
+ //#endregion
1539
+ //#region src/composables/dialog.ts
1540
+ /**
1541
+ * Exposes utilities related to the dialogs.
1542
+ */
1543
+ function useDialog() {
1544
+ return {
1545
+ close: () => router$1.dialog.close(),
1546
+ closeLocally: () => router$1.dialog.close({ local: true }),
1547
+ unmount: () => dialogStore.removeComponent(),
1548
+ show: computed({
1549
+ get: () => dialogStore.state.show.value,
1550
+ set: (v) => !v ? router$1.dialog.close({ local: true }) : null
1551
+ }),
1552
+ properties: computed(() => state.context.value?.dialog?.properties)
1553
+ };
1554
+ }
1555
+ //#endregion
1556
+ //#region src/composables/history.ts
1557
+ /**
1558
+ * Returns a ref with a value saved in the history state through Hybridly.
1559
+ * The state is linked to a specific browser history entry.
1560
+ */
1561
+ function useHistoryState(key, initial) {
1562
+ const value = ref(router$1.history.get(key) ?? initial);
1563
+ watch(value, (value) => {
1564
+ router$1.history.remember(key, toRaw(value));
1565
+ }, {
1566
+ immediate: true,
1567
+ deep: true
1568
+ });
1569
+ return value;
1570
+ }
1571
+ //#endregion
1572
+ //#region ../hybridly/src/index.ts
1573
+ var src_exports = /* @__PURE__ */ __exportAll({});
1574
+ import * as import__hybridly_core from "@hybridly/core";
1575
+ __reExport(src_exports, import__hybridly_core);
1576
+ //#endregion
1577
+ //#region src/composables/paginator.ts
1578
+ function createPaginator(paginator, defaultOptions) {
1579
+ const isPaginator = (p) => {
1580
+ return "links" in p && "meta" in p && "last_page" in p.meta;
1581
+ };
1582
+ if (isPaginator(paginator)) return {
1583
+ ...paginator,
1584
+ to: (page, options) => {
1585
+ const link = paginator.links.find((l) => l.page === page);
1586
+ if (link?.url) return src_exports.router.get(link.url, {
1587
+ preserveState: true,
1588
+ ...defaultOptions,
1589
+ ...options
1590
+ });
621
1591
  }
622
- }
623
- });
624
- function shouldIntercept(event) {
625
- const isLink = event.currentTarget.tagName.toLowerCase() === "a";
626
- return !(event.target && (event?.target).isContentEditable || event.defaultPrevented || isLink && event.which > 1 || isLink && event.altKey || isLink && event.ctrlKey || isLink && event.metaKey || isLink && event.shiftKey);
1592
+ };
1593
+ return paginator;
627
1594
  }
628
-
629
1595
  //#endregion
630
1596
  //#region src/utils.ts
631
1597
  /**
@@ -662,7 +1628,6 @@ function toReactive(objectRef) {
662
1628
  }
663
1629
  }));
664
1630
  }
665
-
666
1631
  //#endregion
667
1632
  //#region src/composables/property.ts
668
1633
  /** Accesses all current properties. */
@@ -671,7 +1636,7 @@ function useProperties() {
671
1636
  }
672
1637
  /** Accesses a property with a dot notation. */
673
1638
  function useProperty(path) {
674
- return computed(() => getByPath(state.properties.value, path));
1639
+ return computed(() => get(state.properties.value, path));
675
1640
  }
676
1641
  /**
677
1642
  * Sets the property at the given path to the given value.
@@ -679,338 +1644,144 @@ function useProperty(path) {
679
1644
  */
680
1645
  function setProperty(path, value) {
681
1646
  if (!state.properties.value) return;
682
- setByPath(state.properties.value, path, toValue(value));
683
- if (state.context.value?.view.properties) setByPath(state.context.value.view.properties, path, toValue(value));
684
- }
685
-
686
- //#endregion
687
- //#region src/composables/form.ts
688
- function safeClone(obj) {
689
- return clone(toRaw(obj));
690
- }
691
- function useForm(options) {
692
- const shouldRemember = !!options.key;
693
- const historyKey = options.key ?? "form:default";
694
- const historyData = shouldRemember ? router$1.history.get(historyKey) : void 0;
695
- const timeoutIds = {
696
- recentlyFailed: void 0,
697
- recentlySuccessful: void 0
698
- };
699
- /** Fields that were initially set up. */
700
- const initial = safeClone(options.fields);
701
- /** Fields as they were when loaded. */
702
- const loaded = safeClone(historyData?.fields ?? options.fields);
703
- /** Current fields. */
704
- const fields = reactive(safeClone(loaded));
705
- /** Validation errors for each field. */
706
- const errors = ref(historyData?.errors ?? {});
707
- /** Whether the form is dirty. */
708
- const isDirty = ref(false);
709
- /** Whether the submission was recently successful. */
710
- const recentlySuccessful = ref(false);
711
- /** Whether the submission is successful. */
712
- const successful = ref(false);
713
- /** Whether the submission was recently failed. */
714
- const recentlyFailed = ref(false);
715
- /** Whether the submission is failed. */
716
- const failed = ref(false);
717
- /** Whether the submission is being processed. */
718
- const processing = ref(false);
719
- /** The current request's progress. */
720
- const progress = ref();
721
- /**
722
- * Sets new initial values for the form, so subsequent resets will use thse values.
723
- */
724
- function setInitial(newInitial) {
725
- Object.entries(newInitial).forEach(([key, value]) => {
726
- Reflect.set(initial, key, safeClone(value));
727
- });
728
- }
729
- /**
730
- * Resets the form's failed and successful flags.
731
- */
732
- function resetSubmissionState() {
733
- successful.value = false;
734
- failed.value = false;
735
- recentlyFailed.value = false;
736
- recentlySuccessful.value = false;
737
- clearTimeout(timeoutIds.recentlySuccessful);
738
- clearTimeout(timeoutIds.recentlyFailed);
739
- progress.value = void 0;
740
- }
741
- /**
742
- * Resets the fields, errors and submission state.
743
- */
744
- function reset() {
745
- resetSubmissionState();
746
- clearErrors();
747
- resetFields();
748
- }
749
- /**
750
- * Resets the fields to their initial values.
751
- */
752
- function resetFields(...keys) {
753
- if (keys.length === 0) keys = Object.keys(fields);
754
- keys.forEach((key) => {
755
- Reflect.set(fields, key, safeClone(Reflect.get(initial, key)));
756
- });
757
- }
758
- /**
759
- * Clear the form fields.
760
- */
761
- function clear(...keys) {
762
- if (keys.length === 0) keys = Object.keys(fields);
763
- keys.forEach((key) => {
764
- delete fields[key];
765
- });
766
- }
767
- /**
768
- * Submits the form.
769
- */
770
- function submit(optionsOverrides) {
771
- const { fields: _f, key: _k, ...optionsWithoutFields } = options;
772
- const resolvedOptions = optionsOverrides ? merge(optionsWithoutFields, optionsOverrides, { mergePlainObjects: true }) : optionsWithoutFields;
773
- const optionsWithOverrides = merge(formStore.getDefaultConfig(), resolvedOptions, { mergePlainObjects: true });
774
- const url = typeof optionsWithOverrides.url === "function" ? optionsWithOverrides.url() : optionsWithOverrides.url;
775
- const data = typeof optionsWithOverrides.transform === "function" ? optionsWithOverrides.transform(fields) : fields;
776
- const preserveState = optionsWithOverrides.preserveState ?? optionsWithOverrides.method !== "GET";
777
- const hooks = optionsWithOverrides.hooks ?? {};
778
- return router$1.navigate({
779
- ...optionsWithOverrides,
780
- url: url ?? state.context.value?.url,
781
- method: optionsWithOverrides.method ?? "POST",
782
- data: safeClone(data),
783
- preserveState,
784
- hooks: {
785
- before: (navigation, context) => {
786
- resetSubmissionState();
787
- return hooks.before?.(navigation, context);
788
- },
789
- start: (context) => {
790
- processing.value = true;
791
- return hooks.start?.(context);
792
- },
793
- progress: (incoming, context) => {
794
- progress.value = incoming;
795
- return hooks.progress?.(incoming, context);
796
- },
797
- error: (incoming, context) => {
798
- setErrors(incoming);
799
- failed.value = true;
800
- recentlyFailed.value = true;
801
- timeoutIds.recentlyFailed = setTimeout(() => recentlyFailed.value = false, optionsWithOverrides.timeout ?? 5e3);
802
- return hooks.error?.(incoming, context);
803
- },
804
- success: (payload, context) => {
805
- clearErrors();
806
- if (optionsWithOverrides.updateInitials) setInitial(fields);
807
- if (optionsWithOverrides.reset !== false) resetFields();
808
- successful.value = true;
809
- recentlySuccessful.value = true;
810
- timeoutIds.recentlySuccessful = setTimeout(() => recentlySuccessful.value = false, optionsWithOverrides.timeout ?? 5e3);
811
- return hooks.success?.(payload, context);
812
- },
813
- after: (context) => {
814
- progress.value = void 0;
815
- processing.value = false;
816
- return hooks.after?.(context);
817
- }
818
- }
819
- });
820
- }
821
- /**
822
- * Clears all errors.
823
- */
824
- function clearErrors(...keys) {
825
- if (keys.length === 0) keys = Object.keys(fields);
826
- keys.forEach((key) => {
827
- clearError(key);
828
- });
829
- }
830
- /**
831
- * Checks if the given keys are dirty in the form.
832
- */
833
- function hasDirty(...keys) {
834
- if (keys.length === 0) return isDirty.value;
835
- return keys.some((key) => !isEqual(toRaw(getByPath(fields, key)), toRaw(getByPath(initial, key))));
836
- }
837
- /**
838
- * Clears the given field's error.
839
- */
840
- function clearError(key) {
841
- unsetPropertyAtPath(errors.value, key);
842
- }
843
- /**
844
- * Sets current errors.
845
- */
846
- function setErrors(incoming) {
847
- clearErrors();
848
- Object.entries(incoming).forEach(([path, value]) => {
849
- setValueAtPath(errors.value, path, value);
850
- });
851
- }
852
- /**
853
- * Aborts the submission.
854
- */
855
- function abort() {
856
- router$1.abort();
857
- }
858
- watch([
859
- fields,
860
- processing,
861
- errors
862
- ], () => {
863
- isDirty.value = !isEqual(toRaw(initial), toRaw(fields));
864
- if (shouldRemember) router$1.history.remember(historyKey, {
865
- fields: toRaw(fields),
866
- errors: toRaw(errors.value)
867
- });
868
- }, {
869
- deep: true,
870
- immediate: true
871
- });
872
- return reactive({
873
- resetFields,
874
- reset,
875
- resetSubmissionState,
876
- clear,
877
- fields,
878
- abort,
879
- setErrors,
880
- clearErrors,
881
- clearError,
882
- setInitial,
883
- hasDirty,
884
- submitWith: submit,
885
- submitWithOptions: submit,
886
- submit: () => submit(),
887
- hasErrors: computed(() => Object.values(errors.value ?? {}).length > 0),
888
- initial,
889
- loaded,
890
- progress,
891
- isDirty,
892
- errors,
893
- processing,
894
- successful,
895
- failed,
896
- recentlySuccessful,
897
- recentlyFailed
898
- });
1647
+ set(state.properties.value, path, toValue(value));
1648
+ if (state.context.value?.view.properties) set(state.context.value.view.properties, path, toValue(value));
899
1649
  }
900
-
901
1650
  //#endregion
902
- //#region src/composables/history.ts
1651
+ //#region src/composables/query-parameters.ts
903
1652
  /**
904
- * Returns a ref with a value saved in the history state through Hybridly.
905
- * The state is linked to a specific browser history entry.
1653
+ * Access reactive query parameters.
1654
+ *
1655
+ * @see https://hybridly.dev/api/utils/use-query-parameters.html
906
1656
  */
907
- function useHistoryState(key, initial) {
908
- const value = ref(router$1.history.get(key) ?? initial);
909
- watch(value, (value) => {
910
- router$1.history.remember(key, toRaw(value));
911
- }, {
912
- immediate: true,
913
- deep: true
914
- });
915
- return value;
916
- }
917
-
918
- //#endregion
919
- //#region src/composables/back-forward.ts
920
- function useBackForward(options) {
921
- const callbacks = [];
922
- registerHook$1("navigated", (options) => {
923
- if (options.type === "back-forward") {
924
- callbacks.forEach((fn) => fn(state.context.value));
925
- callbacks.splice(0, callbacks.length);
1657
+ function useQueryParameters() {
1658
+ const state = reactive({});
1659
+ function updateState() {
1660
+ const params = parseQueryString(window.location.search);
1661
+ const unusedKeys = new Set(Object.keys(state));
1662
+ for (const [key, value] of Object.entries(params)) {
1663
+ state[key] = value;
1664
+ unusedKeys.delete(key);
926
1665
  }
927
- });
928
- /**
929
- * Applies the given callback on back-forward navigations.
930
- */
931
- function onBackForward(fn) {
932
- callbacks.push(fn);
933
- }
934
- /**
935
- * Reloads the page on back-forward navigations.
936
- */
937
- function reloadOnBackForward(options) {
938
- onBackForward(() => router$1.reload(options));
1666
+ Array.from(unusedKeys).forEach((key) => delete state[key]);
939
1667
  }
940
- if (options?.reload) reloadOnBackForward(options.reload === true ? void 0 : options.reload);
941
- return {
942
- onBackForward,
943
- reloadOnBackForward
944
- };
1668
+ updateState();
1669
+ registerHook$1("navigated", updateState);
1670
+ return state;
945
1671
  }
946
-
947
- //#endregion
948
- //#region ../hybridly/src/index.ts
949
- var src_exports = {};
950
- import * as import__hybridly_core from "@hybridly/core";
951
- __reExport(src_exports, import__hybridly_core);
952
-
953
- //#endregion
954
- //#region src/composables/paginator.ts
955
- function createPaginator(paginator, options) {
956
- const isPaginator = (p) => {
957
- return "links" in p && "meta" in p && "last_page" in p.meta;
958
- };
959
- if (isPaginator(paginator)) return {
960
- ...paginator,
961
- to: (page) => {
962
- const link = paginator.links.find((l) => l.page === page);
963
- if (link?.url) return src_exports.router.get(link.url, options);
964
- }
1672
+ /**
1673
+ * Makes the specified query parameter reactive.
1674
+ *
1675
+ * @see https://hybridly.dev/api/utils/use-query-parameter.html
1676
+ */
1677
+ function useQueryParameter(name, options = {}) {
1678
+ const query = useQueryParameters();
1679
+ const transform = (value) => {
1680
+ if (options.transform === "bool") return value === true || value === "true" || value === "1" || value === "yes";
1681
+ else if (options.transform === "number") return Number(value);
1682
+ else if (options.transform === "string") return String(value);
1683
+ else if (options.transform === "date") return new Date(value);
1684
+ else if (typeof options.transform === "function") return options.transform(value);
1685
+ return value;
965
1686
  };
966
- return paginator;
1687
+ const value = ref();
1688
+ watch(query, () => {
1689
+ value.value = transform(query[name] ?? toValue(options.defaultValue));
1690
+ }, {
1691
+ deep: true,
1692
+ immediate: true
1693
+ });
1694
+ return value;
967
1695
  }
968
-
969
1696
  //#endregion
970
- //#region src/composables/register-hook.ts
1697
+ //#region src/composables/refinements.ts
971
1698
  /**
972
- * Registers a global hook.
973
- * If called inside a component, unregisters after the component is unmounted.
1699
+ * Checks whether the given filter is a text filter, and narrows its type.
974
1700
  */
975
- const registerHook = (hook, fn, options) => {
976
- const unregister = registerHook$1(hook, fn, options);
977
- if (getCurrentInstance()) onUnmounted(() => unregister());
978
- return unregister;
979
- };
980
-
981
- //#endregion
982
- //#region src/composables/dialog.ts
1701
+ function isTextFilter(filter) {
1702
+ return filter.type === "text";
1703
+ }
983
1704
  /**
984
- * Exposes utilities related to the dialogs.
1705
+ * Checks whether the given filter is a date filter, and narrows its type.
985
1706
  */
986
- function useDialog() {
987
- return {
988
- close: () => router$1.dialog.close(),
989
- closeLocally: () => router$1.dialog.close({ local: true }),
990
- unmount: () => dialogStore.removeComponent(),
991
- show: computed({
992
- get: () => dialogStore.state.show.value,
993
- set: (v) => !v ? router$1.dialog.close({ local: true }) : null
994
- }),
995
- properties: computed(() => state.context.value?.dialog?.properties)
996
- };
1707
+ function isDateFilter(filter) {
1708
+ return filter.type === "date";
997
1709
  }
998
-
999
- //#endregion
1000
- //#region src/composables/refinements.ts
1001
- function useRefinements(properties, refinementsKeys, defaultOptions = {}) {
1002
- const refinements = computed(() => properties[refinementsKeys]);
1710
+ /**
1711
+ * Checks whether the given filter is a select filter, and narrows its type.
1712
+ */
1713
+ function isSelectFilter(filter) {
1714
+ return filter.type === "select";
1715
+ }
1716
+ /**
1717
+ * Checks whether the given filter is a ternary filter, and narrows its type.
1718
+ */
1719
+ function isTernaryFilter(filter) {
1720
+ return filter.type === "ternary";
1721
+ }
1722
+ /**
1723
+ * Checks whether the given filter is a boolean filter, and narrows its type.
1724
+ */
1725
+ function isBooleanFilter(filter) {
1726
+ return filter.type === "boolean";
1727
+ }
1728
+ /**
1729
+ * Checks whether the given filter is a trashed filter, and narrows its type.
1730
+ */
1731
+ function isTrashedFilter(filter) {
1732
+ return filter.type === "trashed";
1733
+ }
1734
+ /**
1735
+ * Checks whether the given filter is a callback filter, and narrows its type.
1736
+ */
1737
+ function isCallbackFilter(filter) {
1738
+ return filter.type === "callback" || ![
1739
+ "text",
1740
+ "select",
1741
+ "ternary",
1742
+ "boolean",
1743
+ "trashed"
1744
+ ].includes(filter.type);
1745
+ }
1746
+ function useRefinements(input, defaultOptions = {}) {
1747
+ const refinements = computed(() => toValue(input));
1003
1748
  const sortsKey = computed(() => refinements.value.keys.sorts);
1004
1749
  const filtersKey = computed(() => refinements.value.keys.filters);
1005
1750
  defaultOptions = {
1006
1751
  replace: false,
1752
+ group: "refining",
1753
+ interruptAsyncOnStart: "same-group",
1754
+ reset: "*",
1007
1755
  ...defaultOptions
1008
1756
  };
1757
+ const sorts = computed(() => {
1758
+ return refinements.value.sorts.map((sort) => ({
1759
+ ...sort,
1760
+ toggle: (options) => toggleSort(sort.name, options),
1761
+ isSorting: (direction) => isSorting(sort.name, direction),
1762
+ clear: (options) => clearSorts(options)
1763
+ }));
1764
+ });
1765
+ const filters = computed(() => {
1766
+ return refinements.value.filters.map((filter) => ({
1767
+ ...filter,
1768
+ apply: (value, options) => applyFilter(filter.name, value, options),
1769
+ clear: (options) => clearFilter(filter.name, options),
1770
+ search: (query, options) => searchFilter(filter.name, query, options),
1771
+ isSelectFilter: () => isSelectFilter(filter),
1772
+ isDateFilter: () => isDateFilter(filter),
1773
+ isSearchFilter: () => isTextFilter(filter),
1774
+ isTernaryFilter: () => isTernaryFilter(filter),
1775
+ isBooleanFilter: () => isBooleanFilter(filter),
1776
+ isTrashedFilter: () => isTrashedFilter(filter),
1777
+ isCallbackFilter: () => isCallbackFilter(filter)
1778
+ }));
1779
+ });
1009
1780
  function getSort(name) {
1010
- return refinements.value.sorts.find((sort) => sort.name === name);
1781
+ return sorts.value.find((sort) => sort.name === name);
1011
1782
  }
1012
1783
  function getFilter(name) {
1013
- return refinements.value.filters.find((sort) => sort.name === name);
1784
+ return filters.value.find((sort) => sort.name === name);
1014
1785
  }
1015
1786
  async function reset(options = {}) {
1016
1787
  return await router$1.reload({
@@ -1036,17 +1807,40 @@ function useRefinements(properties, refinementsKeys, defaultOptions = {}) {
1036
1807
  data: { [filtersKey.value]: { [filter]: void 0 } }
1037
1808
  });
1038
1809
  }
1810
+ async function searchFilter(name, query, options = {}) {
1811
+ if (!getFilter(name)) {
1812
+ console.warn(`[Refinement] Filter "${name}" does not exist.`);
1813
+ return;
1814
+ }
1815
+ if (["", null].includes(query?.toString() || null)) query = void 0;
1816
+ return await router$1.reload({
1817
+ ...defaultOptions,
1818
+ ...options,
1819
+ data: { [filtersKey.value]: { [name]: { search: query } } }
1820
+ });
1821
+ }
1039
1822
  async function applyFilter(name, value, options = {}) {
1040
1823
  const filter = getFilter(name);
1041
1824
  if (!filter) {
1042
1825
  console.warn(`[Refinement] Filter "${name}" does not exist.`);
1043
1826
  return;
1044
1827
  }
1045
- if (["", null].includes(value) || value === filter.default) value = void 0;
1828
+ if (["", null].includes(value) || filter.has_default && isEqual(value, filter.default)) value = void 0;
1829
+ const operator = (() => {
1830
+ const operator = options.operator ?? filter.operator;
1831
+ if (operator === filter.default_operator) return;
1832
+ return operator;
1833
+ })();
1046
1834
  return await router$1.reload({
1835
+ progress: true,
1047
1836
  ...defaultOptions,
1048
1837
  ...options,
1049
- data: { [filtersKey.value]: { [name]: value } }
1838
+ data: { [filtersKey.value]: { [name]: {
1839
+ value,
1840
+ search: void 0,
1841
+ operator,
1842
+ options: options.options ?? void 0
1843
+ } } }
1050
1844
  });
1051
1845
  }
1052
1846
  async function clearSorts(options = {}) {
@@ -1094,15 +1888,15 @@ function useRefinements(properties, refinementsKeys, defaultOptions = {}) {
1094
1888
  const _proxy = ref(getFilterValue());
1095
1889
  let filterIsBeingApplied = false;
1096
1890
  let proxyIsBeingUpdated = false;
1097
- const debouncedApplyFilter = debounce(options.debounce ?? 250, async (value) => {
1891
+ const debouncedApplyFilter = debounce(async (value) => {
1098
1892
  await applyFilter(name, transform(value), options);
1099
1893
  nextTick(() => filterIsBeingApplied = false);
1100
- });
1101
- const debounceUpdateProxyValue = debounce(options.syncDebounce ?? 250, () => {
1894
+ }, options.debounce ?? 250);
1895
+ const debounceUpdateProxyValue = debounce(() => {
1102
1896
  const filter = refinements.value.filters.find((f) => f.name === name);
1103
1897
  if (filter) _proxy.value = transform(filter?.value);
1104
1898
  nextTick(() => proxyIsBeingUpdated = false);
1105
- }, { atBegin: true });
1899
+ }, options.syncDebounce ?? 250, { edges: ["leading"] });
1106
1900
  watch(() => refinements.value.filters.find((f) => f.name === name)?.value, () => {
1107
1901
  if (filterIsBeingApplied === true) return;
1108
1902
  proxyIsBeingUpdated = true;
@@ -1117,18 +1911,10 @@ function useRefinements(properties, refinementsKeys, defaultOptions = {}) {
1117
1911
  }
1118
1912
  return {
1119
1913
  bindFilter,
1120
- filters: toReactive(refinements.value.filters.map((filter) => ({
1121
- ...filter,
1122
- apply: (value, options) => applyFilter(filter.name, value, options),
1123
- clear: (options) => clearFilter(filter.name, options)
1124
- }))),
1125
- sorts: toReactive(refinements.value.sorts.map((sort) => ({
1126
- ...sort,
1127
- toggle: (options) => toggleSort(sort.name, options),
1128
- isSorting: (direction) => isSorting(sort.name, direction),
1129
- clear: (options) => clearSorts(options)
1130
- }))),
1914
+ filters,
1915
+ sorts,
1131
1916
  filtersKey,
1917
+ sortsKey,
1132
1918
  getFilter,
1133
1919
  getSort,
1134
1920
  reset,
@@ -1143,176 +1929,26 @@ function useRefinements(properties, refinementsKeys, defaultOptions = {}) {
1143
1929
  applyFilter
1144
1930
  };
1145
1931
  }
1146
-
1147
- //#endregion
1148
- //#region src/composables/current-route.ts
1149
- const isNavigating = ref(false);
1150
- function useRoute() {
1151
- const current = ref(router$1.current());
1152
- function matches(name, parameters) {
1153
- return router$1.matches(toValue(name), parameters);
1154
- }
1155
- registerHook("before", () => isNavigating.value = true);
1156
- registerHook("after", () => isNavigating.value = false);
1157
- registerHook("navigated", () => {
1158
- current.value = router$1.current();
1159
- });
1160
- return {
1161
- isNavigating: readonly(isNavigating),
1162
- current: readonly(current),
1163
- matches
1164
- };
1165
- }
1166
-
1167
- //#endregion
1168
- //#region src/composables/bulk-select.ts
1169
- function useBulkSelect() {
1170
- const selection = ref({
1171
- all: false,
1172
- only: /* @__PURE__ */ new Set(),
1173
- except: /* @__PURE__ */ new Set()
1174
- });
1175
- /**
1176
- * Selects all records.
1177
- */
1178
- function selectAll() {
1179
- selection.value.all = true;
1180
- selection.value.only.clear();
1181
- selection.value.except.clear();
1182
- }
1183
- /**
1184
- * Deselects all records.
1185
- */
1186
- function deselectAll() {
1187
- selection.value.all = false;
1188
- selection.value.only.clear();
1189
- selection.value.except.clear();
1190
- }
1191
- /**
1192
- * Selects the given records.
1193
- */
1194
- function select(...records) {
1195
- records.forEach((record) => selection.value.except.delete(record));
1196
- records.forEach((record) => selection.value.only.add(record));
1197
- }
1198
- /**
1199
- * Deselects the given records.
1200
- */
1201
- function deselect(...records) {
1202
- records.forEach((record) => selection.value.except.add(record));
1203
- records.forEach((record) => selection.value.only.delete(record));
1204
- }
1205
- /**
1206
- * Toggles selection for the given records.
1207
- */
1208
- function toggle(record, force) {
1209
- if (selected(record) || force === false) return deselect(record);
1210
- if (!selected(record) || force === true) return select(record);
1211
- }
1212
- /**
1213
- * Checks whether the given record is selected.
1214
- */
1215
- function selected(record) {
1216
- if (selection.value.all) return !selection.value.except.has(record);
1217
- return selection.value.only.has(record);
1218
- }
1219
- /**
1220
- * Checks whether all records are selected.
1221
- */
1222
- const allSelected = computed(() => {
1223
- return selection.value.all && selection.value.except.size === 0;
1224
- });
1225
- /**
1226
- * Binds a checkbox's properties.
1227
- */
1228
- function bindCheckbox(key) {
1229
- return {
1230
- onChange: (event) => {
1231
- const target = event.target;
1232
- if (target.checked) select(target.value);
1233
- else deselect(target.value);
1234
- },
1235
- checked: selected(key),
1236
- value: key
1237
- };
1238
- }
1239
- return {
1240
- allSelected,
1241
- selectAll,
1242
- deselectAll,
1243
- select,
1244
- deselect,
1245
- toggle,
1246
- selected,
1247
- selection,
1248
- bindCheckbox
1249
- };
1250
- }
1251
-
1252
- //#endregion
1253
- //#region src/composables/query-parameters.ts
1254
- /**
1255
- * Access reactive query parameters.
1256
- *
1257
- * @see https://hybridly.dev/api/utils/use-query-parameters.html
1258
- */
1259
- function useQueryParameters() {
1260
- const state = reactive({});
1261
- function updateState() {
1262
- const params = new URLSearchParams(window.location.search);
1263
- const unusedKeys = new Set(Object.keys(state));
1264
- for (const key of params.keys()) {
1265
- const paramsForKey = params.getAll(key);
1266
- state[key] = paramsForKey.length > 1 ? paramsForKey : params.get(key) || "";
1267
- unusedKeys.delete(key);
1268
- }
1269
- Array.from(unusedKeys).forEach((key) => delete state[key]);
1270
- }
1271
- updateState();
1272
- registerHook$1("navigated", updateState);
1273
- return state;
1274
- }
1275
- /**
1276
- * Makes the specified query parameter reactive.
1277
- *
1278
- * @see https://hybridly.dev/api/utils/use-query-parameter.html
1279
- */
1280
- function useQueryParameter(name, options = {}) {
1281
- const query = useQueryParameters();
1282
- const transform = (value) => {
1283
- if (options.transform === "bool") return value === true || value === "true" || value === "1" || value === "yes";
1284
- else if (options.transform === "number") return Number(value);
1285
- else if (options.transform === "string") return String(value);
1286
- else if (options.transform === "date") return new Date(value);
1287
- else if (typeof options.transform === "function") return options.transform(value);
1288
- return value;
1289
- };
1290
- const value = ref();
1291
- watch(query, () => {
1292
- value.value = transform(query[name] ?? toValue(options.defaultValue));
1293
- }, {
1294
- deep: true,
1295
- immediate: true
1296
- });
1297
- return value;
1298
- }
1299
-
1300
1932
  //#endregion
1301
1933
  //#region src/composables/table.ts
1302
1934
  /**
1303
1935
  * Provides utilities for working with tables.
1304
1936
  */
1305
- function useTable(props, key, defaultOptions = {}) {
1306
- const table = computed(() => props[key]);
1937
+ function useTable(input, defaultOptions = {}) {
1938
+ const table = computed(() => toValue(input));
1307
1939
  const bulk = useBulkSelect();
1308
- const refinements = useRefinements(toReactive(table), "refinements", defaultOptions);
1940
+ const refinements = useRefinements(() => toValue(input).refinements, defaultOptions);
1309
1941
  /**
1310
1942
  * Gets additionnal data to send with the request.
1311
1943
  */
1312
- function getAdditionnalData() {
1944
+ function getAdditionnalData(options) {
1313
1945
  const data = {};
1946
+ options = {
1947
+ ...defaultOptions,
1948
+ ...options
1949
+ };
1314
1950
  if (defaultOptions?.includeQueryParameters !== false) Object.assign(data, structuredClone(toRaw(useQueryParameters())));
1315
- if (defaultOptions?.data) Object.assign(data, defaultOptions.data);
1951
+ if (options?.data) Object.assign(data, options.data);
1316
1952
  return data;
1317
1953
  }
1318
1954
  /**
@@ -1320,34 +1956,73 @@ function useTable(props, key, defaultOptions = {}) {
1320
1956
  */
1321
1957
  function getRecordKey(record) {
1322
1958
  if (typeof record !== "object") return record;
1323
- if (Reflect.has(record, "__hybridId")) return Reflect.get(record, "__hybridId");
1324
- return Reflect.get(record, table.value.keyName).value;
1959
+ if (!table.value.keyName) return;
1960
+ const value = Reflect.get(record, table.value.keyName);
1961
+ if (typeof value === "string" || typeof value === "number") return value;
1325
1962
  }
1326
- function getActionName(action) {
1327
- return typeof action === "string" ? action : action.name;
1963
+ function getPageRecordKeys() {
1964
+ return table.value.records.map((record) => getRecordKey(record)).filter((key) => key !== void 0);
1965
+ }
1966
+ function warnMissingRecordKey(action) {
1967
+ console.warn(`Cannot ${action} because this table record has no key.`);
1968
+ }
1969
+ function getColumnName(column) {
1970
+ return typeof column === "string" ? column : column.name;
1971
+ }
1972
+ function getCell(index, column) {
1973
+ return table.value.cells[index]?.columns[getColumnName(column)] ?? {
1974
+ value: void 0,
1975
+ extra: {}
1976
+ };
1977
+ }
1978
+ function resolveInlineAction(action) {
1979
+ if (typeof action !== "string") return action;
1980
+ return table.value.inlineActions.find(({ name }) => name === action);
1981
+ }
1982
+ function resolveBulkAction(action) {
1983
+ if (typeof action !== "string") return action;
1984
+ return table.value.bulkActions.find(({ name }) => name === action);
1985
+ }
1986
+ function getActionUrl(action, table) {
1987
+ if (action.url) return action.url;
1988
+ return route$1(table.endpoint);
1328
1989
  }
1329
1990
  /**
1330
1991
  * Executes the given inline action by name.
1331
1992
  */
1332
- async function executeInlineAction(action, record) {
1993
+ async function executeInlineAction(action, options) {
1994
+ const resolvedAction = resolveInlineAction(action);
1995
+ const recordKey = getRecordKey(options.record);
1996
+ if (!resolvedAction) {
1997
+ console.warn(`Action [${action}] is not defined`);
1998
+ return;
1999
+ }
2000
+ if (recordKey === void 0) {
2001
+ warnMissingRecordKey("execute an inline action");
2002
+ return;
2003
+ }
1333
2004
  return await router$1.navigate({
1334
2005
  method: "post",
1335
- url: route$1(table.value.endpoint),
2006
+ url: getActionUrl(resolvedAction, table.value),
1336
2007
  preserveState: true,
1337
2008
  data: {
1338
- ...getAdditionnalData(),
2009
+ ...getAdditionnalData(options),
1339
2010
  type: "action:inline",
1340
- action: getActionName(action),
2011
+ action: resolvedAction.name,
1341
2012
  tableId: table.value.id,
1342
- recordId: getRecordKey(record)
2013
+ recordId: recordKey
1343
2014
  }
1344
2015
  });
1345
2016
  }
1346
2017
  /**
1347
2018
  * Executes the given bulk action for the given records.
1348
2019
  */
1349
- async function executeBulkAction(action, options) {
1350
- const actionName = getActionName(action);
2020
+ async function executeBulkAction(action, options = {}) {
2021
+ const resolvedAction = resolveBulkAction(action);
2022
+ if (!resolvedAction) {
2023
+ console.warn(`Action [${action}] is not defined`);
2024
+ return;
2025
+ }
1351
2026
  const filterParameters = refinements.currentFilters().reduce((carry, filter) => {
1352
2027
  return {
1353
2028
  ...carry,
@@ -1356,12 +2031,12 @@ function useTable(props, key, defaultOptions = {}) {
1356
2031
  }, {});
1357
2032
  return await router$1.navigate({
1358
2033
  method: "post",
1359
- url: route$1(table.value.endpoint),
2034
+ url: getActionUrl(resolvedAction, table.value),
1360
2035
  preserveState: true,
1361
2036
  data: {
1362
- ...getAdditionnalData(),
2037
+ ...getAdditionnalData(options),
1363
2038
  type: "action:bulk",
1364
- action: actionName,
2039
+ action: resolvedAction.name,
1365
2040
  tableId: table.value.id,
1366
2041
  all: bulk.selection.value.all,
1367
2042
  only: [...bulk.selection.value.only],
@@ -1369,29 +2044,46 @@ function useTable(props, key, defaultOptions = {}) {
1369
2044
  [refinements.filtersKey.value]: filterParameters
1370
2045
  },
1371
2046
  hooks: { after: () => {
1372
- if (options?.deselect === true || table.value.bulkActions.find(({ name }) => name === actionName)?.deselect !== false) bulk.deselectAll();
2047
+ if (options?.deselect === true || resolvedAction.deselect !== false) bulk.deselectAll();
1373
2048
  } }
1374
2049
  });
1375
2050
  }
1376
2051
  return reactive({
1377
- selectAll: bulk.selectAll,
2052
+ selectAll: () => table.value.keyName ? bulk.selectAll() : warnMissingRecordKey("select all records"),
1378
2053
  deselectAll: bulk.deselectAll,
1379
- selectPage: () => bulk.select(...table.value.records.map((record) => getRecordKey(record))),
1380
- deselectPage: () => bulk.deselect(...table.value.records.map((record) => getRecordKey(record))),
1381
- isPageSelected: computed(() => table.value.records.length > 0 && table.value.records.every((record) => bulk.selected(getRecordKey(record)))),
1382
- isSelected: (record) => bulk.selected(getRecordKey(record)),
2054
+ selectPage: () => bulk.select(...getPageRecordKeys()),
2055
+ deselectPage: () => bulk.deselect(...getPageRecordKeys()),
2056
+ isPageSelected: computed(() => {
2057
+ const keys = getPageRecordKeys();
2058
+ return keys.length > 0 && keys.every((key) => bulk.selected(key));
2059
+ }),
2060
+ isSelected: (record) => {
2061
+ const key = getRecordKey(record);
2062
+ return key === void 0 ? false : bulk.selected(key);
2063
+ },
1383
2064
  allSelected: bulk.allSelected,
2065
+ anySelected: bulk.anySelected,
1384
2066
  selection: bulk.selection,
1385
2067
  bindCheckbox: (key) => bulk.bindCheckbox(key),
1386
- toggle: (record) => bulk.toggle(getRecordKey(record)),
1387
- select: (record) => bulk.select(getRecordKey(record)),
1388
- deselect: (record) => bulk.deselect(getRecordKey(record)),
2068
+ toggle: (record, force) => {
2069
+ const key = getRecordKey(record);
2070
+ return key === void 0 ? warnMissingRecordKey("toggle record selection") : bulk.toggle(key, force);
2071
+ },
2072
+ toggleAll: (force) => table.value.keyName ? bulk.toggleAll(force) : warnMissingRecordKey("toggle all records"),
2073
+ select: (record) => {
2074
+ const key = getRecordKey(record);
2075
+ return key === void 0 ? warnMissingRecordKey("select this record") : bulk.select(key);
2076
+ },
2077
+ deselect: (record) => {
2078
+ const key = getRecordKey(record);
2079
+ return key === void 0 ? warnMissingRecordKey("deselect this record") : bulk.deselect(key);
2080
+ },
1389
2081
  inlineActions: computed(() => table.value.inlineActions.map((action) => ({
1390
- execute: (record) => executeInlineAction(action.name, record),
2082
+ execute: (record) => executeInlineAction(action, { record }),
1391
2083
  ...action
1392
2084
  }))),
1393
2085
  bulkActions: computed(() => table.value.bulkActions.map((action) => ({
1394
- execute: (options) => executeBulkAction(action.name, options),
2086
+ execute: (options) => executeBulkAction(action, options),
1395
2087
  ...action
1396
2088
  }))),
1397
2089
  executeInlineAction,
@@ -1402,31 +2094,45 @@ function useTable(props, key, defaultOptions = {}) {
1402
2094
  isSorting: (direction) => refinements.isSorting(column.name, direction),
1403
2095
  applyFilter: (value, options) => refinements.applyFilter(column.name, value, options),
1404
2096
  clearFilter: (options) => refinements.clearFilter(column.name, options),
1405
- isSortable: !!refinements.sorts.find((sort) => sort.name === column.name),
1406
- isFilterable: !!refinements.filters.find((filters) => filters.name === column.name)
2097
+ isSortable: !!refinements.sorts.value.find((sort) => sort.name === column.name),
2098
+ isFilterable: !!refinements.filters.value.find((filters) => filters.name === column.name)
1407
2099
  }))),
1408
- data: computed(() => table.value.records.map((record) => {
1409
- return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, value.value]).filter(([key]) => key === "__hybridId"));
2100
+ data: computed(() => table.value.records),
2101
+ records: computed(() => table.value.records.map((record, index) => {
2102
+ const recordKey = getRecordKey(record);
2103
+ const localKey = recordKey ?? `row-${index}`;
2104
+ const selected = recordKey === void 0 ? false : bulk.selected(recordKey);
2105
+ return {
2106
+ record,
2107
+ key: localKey,
2108
+ recordKey,
2109
+ hasKey: recordKey !== void 0,
2110
+ execute: (action) => executeInlineAction(action, { record }),
2111
+ actions: table.value.inlineActions.map((action) => ({
2112
+ ...action,
2113
+ execute: () => executeInlineAction(action.name, { record })
2114
+ })),
2115
+ select: () => recordKey === void 0 ? warnMissingRecordKey("select this record") : bulk.select(recordKey),
2116
+ deselect: () => recordKey === void 0 ? warnMissingRecordKey("deselect this record") : bulk.deselect(recordKey),
2117
+ toggle: (force) => recordKey === void 0 ? warnMissingRecordKey("toggle record selection") : bulk.toggle(recordKey, force),
2118
+ selected,
2119
+ value: (column) => getCell(index, column).value,
2120
+ extra: (column, path) => getByPath(getCell(index, column).extra, path)
2121
+ };
1410
2122
  })),
1411
- records: computed(() => table.value.records.map((record) => ({
1412
- record: Object.values(record).map((record) => record.value),
1413
- key: getRecordKey(record),
1414
- execute: (action) => executeInlineAction(getActionName(action), getRecordKey(record)),
1415
- actions: table.value.inlineActions.map((action) => ({
1416
- ...action,
1417
- execute: () => executeInlineAction(action.name, getRecordKey(record))
1418
- })),
1419
- select: () => bulk.select(getRecordKey(record)),
1420
- deselect: () => bulk.deselect(getRecordKey(record)),
1421
- toggle: (force) => bulk.toggle(getRecordKey(record), force),
1422
- selected: bulk.selected(getRecordKey(record)),
1423
- value: (column) => record[typeof column === "string" ? column : column.name].value,
1424
- extra: (column, path) => getByPath(record[typeof column === "string" ? column : column.name].extra, path)
1425
- }))),
1426
- paginator: computed(() => createPaginator(table.value.paginator)),
2123
+ paginator: computed(() => createPaginator(table.value.paginator, defaultOptions)),
1427
2124
  ...refinements
1428
2125
  });
1429
2126
  }
1430
-
1431
2127
  //#endregion
1432
- export { RouterLink, can, createPaginator, initializeHybridly, registerHook, route, router, setProperty, useBackForward, useBulkSelect, useDialog, useForm, useHistoryState, useProperties, useProperty, useQueryParameter, useQueryParameters, useRefinements, useRoute, useTable };
2128
+ //#region src/composables/validation.ts
2129
+ /** Accesses all validation errors grouped by bag name. */
2130
+ function useValidation() {
2131
+ return readonly(toReactive(computed(() => state.context.value?.validation ?? {})));
2132
+ }
2133
+ /** Accesses validation errors for a single bag. */
2134
+ function useValidationBag(bag = "default") {
2135
+ return readonly(toReactive(computed(() => state.context.value?.validation?.[bag] ?? {})));
2136
+ }
2137
+ //#endregion
2138
+ export { Deferred, Form, RouterLink, WhenVisible, createPaginator, getBulkSelectionRange, initializeHybridly, isBooleanFilter, isCallbackFilter, isDateFilter, isSelectFilter, isTernaryFilter, isTextFilter, isTrashedFilter, registerHook, route, router, setProperty, useBackForward, useBulkSelect, useDialog, useForm, useHistoryState, useProperties, useProperty, useQueryParameter, useQueryParameters, useRefinements, useRoute, useTable, useValidation, useValidationBag };