@hybridly/vue 0.0.1-dev.2
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/LICENSE +21 -0
- package/dist/index.cjs +805 -0
- package/dist/index.d.ts +555 -0
- package/dist/index.mjs +782 -0
- package/package.json +62 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { ref, shallowRef, unref, triggerRef, defineComponent, h, isRef, reactive, readonly, computed, toRaw, watch } from 'vue';
|
|
2
|
+
import { registerHook, registerHookOnce, createRouter, makeUrl, router } from '@hybridly/core';
|
|
3
|
+
export { router } from '@hybridly/core';
|
|
4
|
+
import { debug, showPageComponentErrorModal, merge } from '@hybridly/utils';
|
|
5
|
+
import { progress } from '@hybridly/progress-plugin';
|
|
6
|
+
import { setupDevtoolsPlugin } from '@vue/devtools-api';
|
|
7
|
+
import qs, { parse, stringify } from 'qs';
|
|
8
|
+
import isEqual from 'lodash.isequal';
|
|
9
|
+
import clone from 'lodash.clonedeep';
|
|
10
|
+
|
|
11
|
+
const state = {
|
|
12
|
+
context: ref(),
|
|
13
|
+
view: shallowRef(),
|
|
14
|
+
viewLayout: shallowRef(),
|
|
15
|
+
viewKey: ref(),
|
|
16
|
+
dialog: shallowRef(),
|
|
17
|
+
dialogKey: ref(),
|
|
18
|
+
routes: ref(),
|
|
19
|
+
setRoutes(routes) {
|
|
20
|
+
debug.adapter("vue:state:routes", "Setting routes:", routes);
|
|
21
|
+
if (routes) {
|
|
22
|
+
state.routes.value = unref(routes);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
setView(view) {
|
|
26
|
+
debug.adapter("vue:state:view", "Setting view:", view);
|
|
27
|
+
state.view.value = view;
|
|
28
|
+
},
|
|
29
|
+
setViewLayout(layout) {
|
|
30
|
+
debug.adapter("vue:state:view", "Setting layout:", layout);
|
|
31
|
+
state.viewLayout.value = layout;
|
|
32
|
+
},
|
|
33
|
+
setDialog(dialog) {
|
|
34
|
+
debug.adapter("vue:state:dialog", "Setting dialog:", dialog);
|
|
35
|
+
state.dialog.value = dialog;
|
|
36
|
+
},
|
|
37
|
+
setContext(context) {
|
|
38
|
+
debug.adapter("vue:state:context", "Setting context:", context);
|
|
39
|
+
state.context.value = unref(context);
|
|
40
|
+
triggerRef(state.context);
|
|
41
|
+
},
|
|
42
|
+
setViewKey(key) {
|
|
43
|
+
debug.adapter("vue:state:key", "Setting view key:", key);
|
|
44
|
+
state.viewKey.value = unref(key);
|
|
45
|
+
},
|
|
46
|
+
setDialogKey(key) {
|
|
47
|
+
debug.adapter("vue:state:key", "Setting dialog key:", key);
|
|
48
|
+
state.dialogKey.value = unref(key);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const wrapper = defineComponent({
|
|
53
|
+
name: "Hybridly",
|
|
54
|
+
setup(props) {
|
|
55
|
+
if (typeof window !== "undefined") {
|
|
56
|
+
state.setContext(props.context);
|
|
57
|
+
if (!props.context) {
|
|
58
|
+
throw new Error("Hybridly was not properly initialized. The context is missing.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function renderLayout(child) {
|
|
62
|
+
debug.adapter("vue:render:layout", "Rendering layout.");
|
|
63
|
+
if (typeof state.view.value?.layout === "function") {
|
|
64
|
+
return state.view.value.layout(h, child);
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(state.view.value?.layout)) {
|
|
67
|
+
return state.view.value.layout.concat(child).reverse().reduce((child2, layout) => {
|
|
68
|
+
layout.inheritAttrs = !!layout.inheritAttrs;
|
|
69
|
+
return h(layout, { ...state.context.value.view.properties }, () => child2);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
h(state.view.value?.layout, { ...state.context.value.view.properties }, () => child),
|
|
74
|
+
renderDialog()
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
function renderView() {
|
|
78
|
+
debug.adapter("vue:render:view", "Rendering view.");
|
|
79
|
+
state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
|
|
80
|
+
return h(state.view.value, {
|
|
81
|
+
...state.context.value.view.properties,
|
|
82
|
+
key: state.viewKey.value
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function renderDialog() {
|
|
86
|
+
debug.adapter("vue:render:dialog", "Rendering dialog.");
|
|
87
|
+
if (state.dialog.value) {
|
|
88
|
+
return h(state.dialog.value, {
|
|
89
|
+
...state.dialog.value.properties,
|
|
90
|
+
key: state.dialogKey.value
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return () => {
|
|
95
|
+
if (state.view.value) {
|
|
96
|
+
const view = renderView();
|
|
97
|
+
if (state.viewLayout.value) {
|
|
98
|
+
state.view.value.layout = state.viewLayout.value;
|
|
99
|
+
state.viewLayout.value = void 0;
|
|
100
|
+
}
|
|
101
|
+
if (state.view.value.layout) {
|
|
102
|
+
return renderLayout(view);
|
|
103
|
+
}
|
|
104
|
+
return [view, renderDialog()];
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
props: {
|
|
109
|
+
context: {
|
|
110
|
+
type: Object,
|
|
111
|
+
required: true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const hybridlyStateType = "hybridly";
|
|
117
|
+
const hybridlyEventsTimelineLayerId = "Hybridly";
|
|
118
|
+
function setupDevtools(app) {
|
|
119
|
+
setupDevtoolsPlugin({
|
|
120
|
+
id: "hybridly",
|
|
121
|
+
label: "Hybridly",
|
|
122
|
+
packageName: "@hybridly/vue",
|
|
123
|
+
homepage: "https://github.com/hybridly",
|
|
124
|
+
app,
|
|
125
|
+
enableEarlyProxy: true,
|
|
126
|
+
componentStateTypes: [
|
|
127
|
+
hybridlyStateType
|
|
128
|
+
]
|
|
129
|
+
}, (api) => {
|
|
130
|
+
api.on.inspectComponent((payload) => {
|
|
131
|
+
payload.instanceData.state.push({
|
|
132
|
+
type: hybridlyStateType,
|
|
133
|
+
key: "properties",
|
|
134
|
+
value: state.context.value?.view.properties,
|
|
135
|
+
editable: true
|
|
136
|
+
});
|
|
137
|
+
payload.instanceData.state.push({
|
|
138
|
+
type: hybridlyStateType,
|
|
139
|
+
key: "component",
|
|
140
|
+
value: state.context.value?.view.name
|
|
141
|
+
});
|
|
142
|
+
payload.instanceData.state.push({
|
|
143
|
+
type: hybridlyStateType,
|
|
144
|
+
key: "version",
|
|
145
|
+
value: state.context.value?.version
|
|
146
|
+
});
|
|
147
|
+
payload.instanceData.state.push({
|
|
148
|
+
type: hybridlyStateType,
|
|
149
|
+
key: "url",
|
|
150
|
+
value: state.context.value?.url
|
|
151
|
+
});
|
|
152
|
+
payload.instanceData.state.push({
|
|
153
|
+
type: hybridlyStateType,
|
|
154
|
+
key: "router",
|
|
155
|
+
value: state.routes.value
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
api.on.editComponentState((payload) => {
|
|
159
|
+
if (payload.type === hybridlyStateType) {
|
|
160
|
+
payload.set(state.context.value?.view);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
api.addTimelineLayer({
|
|
164
|
+
id: hybridlyEventsTimelineLayerId,
|
|
165
|
+
color: 16501221,
|
|
166
|
+
label: "Hybridly"
|
|
167
|
+
});
|
|
168
|
+
const listen = [
|
|
169
|
+
"start",
|
|
170
|
+
"data",
|
|
171
|
+
"navigate",
|
|
172
|
+
"progress",
|
|
173
|
+
"error",
|
|
174
|
+
"abort",
|
|
175
|
+
"success",
|
|
176
|
+
"invalid",
|
|
177
|
+
"exception",
|
|
178
|
+
"fail",
|
|
179
|
+
"after"
|
|
180
|
+
];
|
|
181
|
+
registerHook("before", (options) => {
|
|
182
|
+
const groupId = (Math.random() + 1).toString(36).substring(7);
|
|
183
|
+
api.addTimelineEvent({
|
|
184
|
+
layerId: hybridlyEventsTimelineLayerId,
|
|
185
|
+
event: {
|
|
186
|
+
groupId,
|
|
187
|
+
title: "before",
|
|
188
|
+
time: api.now(),
|
|
189
|
+
data: options
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
listen.forEach((event) => registerHookOnce(event, (data) => {
|
|
193
|
+
api.addTimelineEvent({
|
|
194
|
+
layerId: hybridlyEventsTimelineLayerId,
|
|
195
|
+
event: {
|
|
196
|
+
groupId,
|
|
197
|
+
title: event,
|
|
198
|
+
time: api.now(),
|
|
199
|
+
data
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
if (event === "after") {
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
triggerRef(state.context);
|
|
205
|
+
api.notifyComponentUpdate();
|
|
206
|
+
}, 100);
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const plugin = {
|
|
213
|
+
install(app) {
|
|
214
|
+
if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) {
|
|
215
|
+
setupDevtools(app);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
async function initializeHybridly(options) {
|
|
221
|
+
const { element, payload, resolve } = prepare(options);
|
|
222
|
+
if (!element) {
|
|
223
|
+
throw new Error("Could not find an HTML element to initialize Vue on.");
|
|
224
|
+
}
|
|
225
|
+
if (!payload) {
|
|
226
|
+
throw new Error("No payload. Are you using `@hybridly` or the `payload` option?");
|
|
227
|
+
}
|
|
228
|
+
state.setContext(await createRouter({
|
|
229
|
+
plugins: options.plugins,
|
|
230
|
+
serializer: options.serializer,
|
|
231
|
+
adapter: {
|
|
232
|
+
resolveComponent: resolve,
|
|
233
|
+
swapDialog: async () => {
|
|
234
|
+
},
|
|
235
|
+
swapView: async (options2) => {
|
|
236
|
+
state.setView(options2.component);
|
|
237
|
+
if (!options2.preserveState) {
|
|
238
|
+
state.setViewKey(Date.now());
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
update: (context) => {
|
|
242
|
+
state.setContext(context);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
payload
|
|
246
|
+
}));
|
|
247
|
+
await options.setup({
|
|
248
|
+
element,
|
|
249
|
+
wrapper,
|
|
250
|
+
hybridly: plugin,
|
|
251
|
+
props: { context: state.context.value },
|
|
252
|
+
render: () => h(wrapper, { context: state.context.value })
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function prepare(options) {
|
|
256
|
+
debug.adapter("vue", "Preparing Hybridly with options:", options);
|
|
257
|
+
const isServer = typeof window === "undefined";
|
|
258
|
+
const id = options.id ?? "root";
|
|
259
|
+
const element = document?.getElementById(id) ?? void 0;
|
|
260
|
+
debug.adapter("vue", `Element "${id}" is:`, element);
|
|
261
|
+
const payload = options.payload ?? element?.dataset.payload ? JSON.parse(element.dataset.payload) : void 0;
|
|
262
|
+
if (options.cleanup !== false) {
|
|
263
|
+
delete element.dataset.payload;
|
|
264
|
+
}
|
|
265
|
+
debug.adapter("vue", "Resolved:", { isServer, element, payload });
|
|
266
|
+
const resolve = async (name) => {
|
|
267
|
+
debug.adapter("vue", "Resolving component", name);
|
|
268
|
+
if (options.resolve) {
|
|
269
|
+
const component = await options.resolve?.(name);
|
|
270
|
+
return component.default ?? component;
|
|
271
|
+
}
|
|
272
|
+
if (options.pages) {
|
|
273
|
+
return await resolvePageComponent(name, options.pages, options.layout);
|
|
274
|
+
}
|
|
275
|
+
throw new Error("Either `initializeHybridly#resolve` or `initializeHybridly#pages` should be defined.");
|
|
276
|
+
};
|
|
277
|
+
if (typeof window !== "undefined") {
|
|
278
|
+
const routes = window.hybridly?.routes;
|
|
279
|
+
if (routes) {
|
|
280
|
+
state.setRoutes(window.hybridly?.routes);
|
|
281
|
+
window.addEventListener("hybridly:routes", (event) => {
|
|
282
|
+
state.setRoutes(event.detail);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (options.progress !== false) {
|
|
287
|
+
options.plugins = [
|
|
288
|
+
progress(typeof options.progress === "object" ? options.progress : {}),
|
|
289
|
+
...options.plugins ?? []
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
isServer,
|
|
294
|
+
element,
|
|
295
|
+
payload,
|
|
296
|
+
resolve
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function resolvePageComponent(name, pages, defaultLayout) {
|
|
300
|
+
const path = Object.keys(pages).sort((a, b) => a.length - b.length).find((path2) => path2.endsWith(`${name.replaceAll(".", "/")}.vue`));
|
|
301
|
+
if (!path) {
|
|
302
|
+
showPageComponentErrorModal(name);
|
|
303
|
+
console.warn(`Page component "${name}" could not be found. Available pages:`, Object.keys(pages));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let component = typeof pages[path] === "function" ? await pages[path]() : pages[path];
|
|
307
|
+
component = component.default ?? component;
|
|
308
|
+
component.layout ?? (component.layout = defaultLayout);
|
|
309
|
+
return component;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const RouterLink = defineComponent({
|
|
313
|
+
name: "RouterLink",
|
|
314
|
+
setup(_, { slots, attrs }) {
|
|
315
|
+
return (props) => {
|
|
316
|
+
let data = props.data ?? {};
|
|
317
|
+
const url = makeUrl(props.href ?? "");
|
|
318
|
+
const method = props.method ?? "GET";
|
|
319
|
+
const as = typeof props.as === "object" ? props.as : props.as?.toLowerCase() ?? "a";
|
|
320
|
+
if (method === "GET") {
|
|
321
|
+
debug.adapter("vue", "Moving data object to URL parameters.");
|
|
322
|
+
url.search = qs.stringify(merge(data, qs.parse(url.search, { ignoreQueryPrefix: true })), {
|
|
323
|
+
encodeValuesOnly: true,
|
|
324
|
+
arrayFormat: "indices"
|
|
325
|
+
});
|
|
326
|
+
data = {};
|
|
327
|
+
}
|
|
328
|
+
if (as === "a" && method !== "GET") {
|
|
329
|
+
debug.adapter("vue", `Creating POST/PUT/PATCH/DELETE <a> links is discouraged as it causes "Open Link in New Tab/Window" accessibility issues.
|
|
330
|
+
|
|
331
|
+
Please specify a more appropriate element using the "as" attribute. For example:
|
|
332
|
+
|
|
333
|
+
<Link href="${url}" method="${method}" as="button">...</Link>`);
|
|
334
|
+
}
|
|
335
|
+
return h(props.as, {
|
|
336
|
+
...attrs,
|
|
337
|
+
...as === "a" ? { href: url } : {},
|
|
338
|
+
...props.disabled ? { disabled: props.disabled } : {},
|
|
339
|
+
onClick: (event) => {
|
|
340
|
+
if (props.external) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!shouldIntercept(event)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
if (props.disabled) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
router.visit({
|
|
351
|
+
url,
|
|
352
|
+
data,
|
|
353
|
+
method,
|
|
354
|
+
preserveState: method !== "GET",
|
|
355
|
+
...props.options
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}, slots);
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
props: {
|
|
362
|
+
href: {
|
|
363
|
+
type: String,
|
|
364
|
+
required: true
|
|
365
|
+
},
|
|
366
|
+
as: {
|
|
367
|
+
type: [String, Object],
|
|
368
|
+
default: "a"
|
|
369
|
+
},
|
|
370
|
+
method: {
|
|
371
|
+
type: String,
|
|
372
|
+
default: "GET"
|
|
373
|
+
},
|
|
374
|
+
data: {
|
|
375
|
+
type: Object,
|
|
376
|
+
default: () => ({})
|
|
377
|
+
},
|
|
378
|
+
external: {
|
|
379
|
+
type: Boolean,
|
|
380
|
+
default: false
|
|
381
|
+
},
|
|
382
|
+
disabled: {
|
|
383
|
+
type: Boolean,
|
|
384
|
+
default: false
|
|
385
|
+
},
|
|
386
|
+
options: {
|
|
387
|
+
type: Object,
|
|
388
|
+
default: () => ({})
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
function shouldIntercept(event) {
|
|
393
|
+
const isLink = event.currentTarget.tagName.toLowerCase() === "a";
|
|
394
|
+
return !(event.target && (event?.target).isContentEditable || event.defaultPrevented || isLink && event.which > 1 || isLink && event.altKey || isLink && event.ctrlKey || isLink && event.metaKey || isLink && event.shiftKey);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const HybridlyImports = {
|
|
398
|
+
"hybridly/vue": [
|
|
399
|
+
"useProperty",
|
|
400
|
+
"useProperties",
|
|
401
|
+
"useRouter",
|
|
402
|
+
"useBackForward",
|
|
403
|
+
"useContext",
|
|
404
|
+
"useForm",
|
|
405
|
+
"useHistoryState",
|
|
406
|
+
"usePaginator",
|
|
407
|
+
"useLayout",
|
|
408
|
+
"route"
|
|
409
|
+
],
|
|
410
|
+
"hybridly": [
|
|
411
|
+
"registerHook",
|
|
412
|
+
"registerHookOnce",
|
|
413
|
+
"router",
|
|
414
|
+
"can"
|
|
415
|
+
]
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
function HybridlyResolver(options = {}) {
|
|
419
|
+
options = {
|
|
420
|
+
linkName: "RouterLink",
|
|
421
|
+
...options
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
type: "component",
|
|
425
|
+
resolve: (name) => {
|
|
426
|
+
if (name === options.linkName) {
|
|
427
|
+
return {
|
|
428
|
+
name: "RouterLink",
|
|
429
|
+
as: options.linkName,
|
|
430
|
+
from: "hybridly/vue"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function toReactive(objectRef) {
|
|
438
|
+
if (!isRef(objectRef)) {
|
|
439
|
+
return reactive(objectRef);
|
|
440
|
+
}
|
|
441
|
+
const proxy = new Proxy({}, {
|
|
442
|
+
get(_, p, receiver) {
|
|
443
|
+
return unref(Reflect.get(objectRef.value, p, receiver));
|
|
444
|
+
},
|
|
445
|
+
set(_, p, value) {
|
|
446
|
+
if (isRef(objectRef.value[p]) && !isRef(value)) {
|
|
447
|
+
objectRef.value[p].value = value;
|
|
448
|
+
} else {
|
|
449
|
+
objectRef.value[p] = value;
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
},
|
|
453
|
+
deleteProperty(_, p) {
|
|
454
|
+
return Reflect.deleteProperty(objectRef.value, p);
|
|
455
|
+
},
|
|
456
|
+
has(_, p) {
|
|
457
|
+
return Reflect.has(objectRef.value, p);
|
|
458
|
+
},
|
|
459
|
+
ownKeys() {
|
|
460
|
+
return Object.keys(objectRef.value);
|
|
461
|
+
},
|
|
462
|
+
getOwnPropertyDescriptor() {
|
|
463
|
+
return {
|
|
464
|
+
enumerable: true,
|
|
465
|
+
configurable: true
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
return reactive(proxy);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function useProperties() {
|
|
473
|
+
return readonly(toReactive(computed(() => state.context.value?.view.properties)));
|
|
474
|
+
}
|
|
475
|
+
function useProperty(path, fallback) {
|
|
476
|
+
return computed(() => path.split(".").reduce((o, i) => o[i], state.context.value?.view.properties) ?? fallback);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function useContext() {
|
|
480
|
+
return computed(() => state.context.value);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function useRouter() {
|
|
484
|
+
return router;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function safeClone(obj) {
|
|
488
|
+
return clone(toRaw(obj));
|
|
489
|
+
}
|
|
490
|
+
function useForm(options) {
|
|
491
|
+
const shouldRemember = options?.key !== false;
|
|
492
|
+
const historyKey = options?.key ?? "form:default";
|
|
493
|
+
const historyData = shouldRemember ? router.history.get(historyKey) : void 0;
|
|
494
|
+
const timeoutIds = {
|
|
495
|
+
recentlyFailed: void 0,
|
|
496
|
+
recentlySuccessful: void 0
|
|
497
|
+
};
|
|
498
|
+
const initial = readonly(safeClone(options.fields));
|
|
499
|
+
const loaded = readonly(safeClone(historyData?.fields ?? options.fields));
|
|
500
|
+
const fields = reactive(safeClone(historyData?.fields ?? options.fields));
|
|
501
|
+
const errors = ref(historyData?.errors ?? {});
|
|
502
|
+
const isDirty = ref(false);
|
|
503
|
+
const recentlySuccessful = ref(false);
|
|
504
|
+
const successful = ref(false);
|
|
505
|
+
const recentlyFailed = ref(false);
|
|
506
|
+
const failed = ref(false);
|
|
507
|
+
const processing = ref(false);
|
|
508
|
+
function reset(...keys) {
|
|
509
|
+
if (keys.length === 0) {
|
|
510
|
+
keys = Object.keys(fields);
|
|
511
|
+
}
|
|
512
|
+
keys.forEach((key) => Reflect.set(fields, key, safeClone(Reflect.get(initial, key))));
|
|
513
|
+
clearErrors();
|
|
514
|
+
}
|
|
515
|
+
function submit(optionsOverrides) {
|
|
516
|
+
const url = typeof options.url === "function" ? options.url() : options.url;
|
|
517
|
+
const data = typeof options.transform === "function" ? options.transform?.(fields) : fields;
|
|
518
|
+
return router.visit({
|
|
519
|
+
url: url ?? state.context.value?.url,
|
|
520
|
+
method: options.method ?? "POST",
|
|
521
|
+
...optionsOverrides,
|
|
522
|
+
data: safeClone(data),
|
|
523
|
+
preserveState: optionsOverrides?.preserveState === void 0 && options.method !== "GET" ? true : optionsOverrides?.preserveState,
|
|
524
|
+
hooks: {
|
|
525
|
+
before: (visit) => {
|
|
526
|
+
failed.value = false;
|
|
527
|
+
successful.value = false;
|
|
528
|
+
recentlySuccessful.value = false;
|
|
529
|
+
clearTimeout(timeoutIds.recentlySuccessful);
|
|
530
|
+
clearTimeout(timeoutIds.recentlyFailed);
|
|
531
|
+
clearErrors();
|
|
532
|
+
return options.hooks?.before?.(visit);
|
|
533
|
+
},
|
|
534
|
+
start: (context) => {
|
|
535
|
+
processing.value = true;
|
|
536
|
+
return options.hooks?.start?.(context);
|
|
537
|
+
},
|
|
538
|
+
error: (incoming) => {
|
|
539
|
+
setErrors(incoming);
|
|
540
|
+
failed.value = true;
|
|
541
|
+
recentlyFailed.value = true;
|
|
542
|
+
timeoutIds.recentlyFailed = setTimeout(() => recentlyFailed.value = false, options?.timeout ?? 5e3);
|
|
543
|
+
return options.hooks?.error?.(incoming);
|
|
544
|
+
},
|
|
545
|
+
success: (payload) => {
|
|
546
|
+
if (options?.reset !== false) {
|
|
547
|
+
reset();
|
|
548
|
+
}
|
|
549
|
+
successful.value = true;
|
|
550
|
+
recentlySuccessful.value = true;
|
|
551
|
+
timeoutIds.recentlySuccessful = setTimeout(() => recentlySuccessful.value = false, options?.timeout ?? 5e3);
|
|
552
|
+
return options.hooks?.success?.(payload);
|
|
553
|
+
},
|
|
554
|
+
after: (context) => {
|
|
555
|
+
processing.value = false;
|
|
556
|
+
return options.hooks?.after?.(context);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function clearErrors() {
|
|
562
|
+
errors.value = {};
|
|
563
|
+
}
|
|
564
|
+
function setErrors(incoming) {
|
|
565
|
+
errors.value = incoming;
|
|
566
|
+
}
|
|
567
|
+
function abort() {
|
|
568
|
+
router.abort();
|
|
569
|
+
}
|
|
570
|
+
watch([fields, processing, errors], () => {
|
|
571
|
+
isDirty.value = !isEqual(toRaw(loaded), toRaw(fields));
|
|
572
|
+
if (shouldRemember) {
|
|
573
|
+
router.history.remember(historyKey, {
|
|
574
|
+
fields: toRaw(fields),
|
|
575
|
+
errors: toRaw(errors.value)
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}, { deep: true, immediate: true });
|
|
579
|
+
return reactive({
|
|
580
|
+
reset,
|
|
581
|
+
initial,
|
|
582
|
+
fields,
|
|
583
|
+
loaded,
|
|
584
|
+
submit,
|
|
585
|
+
abort,
|
|
586
|
+
setErrors,
|
|
587
|
+
clearErrors,
|
|
588
|
+
hasErrors: computed(() => Object.values(errors.value).length > 0),
|
|
589
|
+
isDirty: readonly(isDirty),
|
|
590
|
+
errors: readonly(errors),
|
|
591
|
+
processing: readonly(processing),
|
|
592
|
+
successful: readonly(successful),
|
|
593
|
+
failed: readonly(failed),
|
|
594
|
+
recentlySuccessful: readonly(recentlySuccessful),
|
|
595
|
+
recentlyFailed: readonly(recentlyFailed)
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function useHistoryState(key, initial) {
|
|
600
|
+
const value = ref(router.history.get(key) ?? initial);
|
|
601
|
+
watch(value, (value2) => {
|
|
602
|
+
router.history.remember(key, toRaw(value2));
|
|
603
|
+
}, { immediate: true, deep: true });
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function useBackForward() {
|
|
608
|
+
const callbacks = [];
|
|
609
|
+
registerHook("navigate", (options) => {
|
|
610
|
+
if (options.isBackForward) {
|
|
611
|
+
callbacks.forEach((fn) => fn(state.context.value));
|
|
612
|
+
callbacks.splice(0, callbacks.length);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
function onBackForward(fn) {
|
|
616
|
+
callbacks.push(fn);
|
|
617
|
+
}
|
|
618
|
+
function reloadOnBackForward(options) {
|
|
619
|
+
onBackForward(() => router.reload(options));
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
onBackForward,
|
|
623
|
+
reloadOnBackForward
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function usePaginator(paginator) {
|
|
628
|
+
const meta = paginator.meta ?? paginator;
|
|
629
|
+
const links = meta.links ?? paginator.links;
|
|
630
|
+
const items = links.map((link, index) => {
|
|
631
|
+
return {
|
|
632
|
+
url: link.url,
|
|
633
|
+
label: link.label,
|
|
634
|
+
isPage: !isNaN(+link.label),
|
|
635
|
+
isPrevious: index === 0,
|
|
636
|
+
isNext: index === links.length - 1,
|
|
637
|
+
isCurrent: link.active,
|
|
638
|
+
isSeparator: link.label === "...",
|
|
639
|
+
isActive: !!link.url && !link.active
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
const pages = items.filter((item) => item.isPage || item.isSeparator);
|
|
643
|
+
const current = items.find((item) => item.isCurrent);
|
|
644
|
+
const previous = items.find((item) => item.isPrevious);
|
|
645
|
+
const next = items.find((item) => item.isNext);
|
|
646
|
+
const first = { ...items[1], isActive: items[1].url !== current?.url, label: "«" };
|
|
647
|
+
const last = { ...items[items.length - 1], isActive: items[items.length - 1].url !== current?.url, label: "»" };
|
|
648
|
+
const from = meta.from;
|
|
649
|
+
const to = meta.to;
|
|
650
|
+
const total = meta.total;
|
|
651
|
+
return { pages, items, previous, next, first, last, total, from, to };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function useLayout(layout) {
|
|
655
|
+
state.setViewLayout(layout);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
class Route {
|
|
659
|
+
constructor(name, absolute) {
|
|
660
|
+
this.name = name;
|
|
661
|
+
this.absolute = absolute;
|
|
662
|
+
this.definition = Route.getDefinition(name);
|
|
663
|
+
}
|
|
664
|
+
static getDefinition(name) {
|
|
665
|
+
if (!state.routes.value) {
|
|
666
|
+
throw new Error("Routing is not initialized. Have you enabled the Vite plugin?");
|
|
667
|
+
}
|
|
668
|
+
const routes = state.routes.value;
|
|
669
|
+
const route = routes?.routes?.[name];
|
|
670
|
+
if (!route) {
|
|
671
|
+
throw new Error(`Route ${name.toString()} does not exist.`);
|
|
672
|
+
}
|
|
673
|
+
return route;
|
|
674
|
+
}
|
|
675
|
+
get template() {
|
|
676
|
+
const origin = !this.absolute ? "" : this.definition.domain ? `${state.routes.value?.url.match(/^\w+:\/\//)?.[0]}${this.definition.domain}${state.routes.value?.port ? `:${state.routes.value?.port}` : ""}` : state.routes.value?.url;
|
|
677
|
+
return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
|
|
678
|
+
}
|
|
679
|
+
get parameterSegments() {
|
|
680
|
+
return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
|
|
681
|
+
name: segment.replace(/{|\??}/g, ""),
|
|
682
|
+
required: !/\?}$/.test(segment)
|
|
683
|
+
})) ?? [];
|
|
684
|
+
}
|
|
685
|
+
matchesUrl(url) {
|
|
686
|
+
if (!this.definition.methods.includes("GET")) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
|
|
690
|
+
const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
|
|
691
|
+
return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
|
|
692
|
+
}).replace(/^\w+:\/\//, "");
|
|
693
|
+
const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
|
|
694
|
+
const matches = new RegExp(`^${pattern}/?$`).exec(location);
|
|
695
|
+
return matches ? { params: matches.groups, query: parse(query) } : false;
|
|
696
|
+
}
|
|
697
|
+
compile(params) {
|
|
698
|
+
const segments = this.parameterSegments;
|
|
699
|
+
if (!segments.length) {
|
|
700
|
+
return this.template;
|
|
701
|
+
}
|
|
702
|
+
return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
|
|
703
|
+
if (!optional && [null, void 0].includes(params?.[segment])) {
|
|
704
|
+
throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
|
|
705
|
+
}
|
|
706
|
+
if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
|
|
707
|
+
return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
|
|
708
|
+
}
|
|
709
|
+
if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
|
|
710
|
+
throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
|
|
711
|
+
}
|
|
712
|
+
return encodeURIComponent(params[segment] ?? "");
|
|
713
|
+
}).replace(/\/+$/, "");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
class Router extends String {
|
|
718
|
+
constructor(name, parameters, absolute = true) {
|
|
719
|
+
super();
|
|
720
|
+
this.route = new Route(name, absolute);
|
|
721
|
+
this.setParameters(parameters);
|
|
722
|
+
}
|
|
723
|
+
toString() {
|
|
724
|
+
const unhandled = Object.keys(this.parameters).filter((key) => !this.route.parameterSegments.some(({ name }) => name === key)).filter((key) => key !== "_query").reduce((result, current) => ({ ...result, [current]: this.parameters[current] }), {});
|
|
725
|
+
return this.route.compile(this.parameters) + stringify({ ...unhandled, ...this.parameters._query }, {
|
|
726
|
+
addQueryPrefix: true,
|
|
727
|
+
arrayFormat: "indices",
|
|
728
|
+
encodeValuesOnly: true,
|
|
729
|
+
skipNulls: true,
|
|
730
|
+
encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
static has(name) {
|
|
734
|
+
try {
|
|
735
|
+
Route.getDefinition(name);
|
|
736
|
+
return true;
|
|
737
|
+
} catch {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
setParameters(parameters) {
|
|
742
|
+
this.parameters = parameters ?? {};
|
|
743
|
+
this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
|
|
744
|
+
const segments = this.route.parameterSegments.filter(({ name }) => !state.routes.value?.defaults[name]);
|
|
745
|
+
if (Array.isArray(this.parameters)) {
|
|
746
|
+
this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
|
|
747
|
+
} else if (segments.length === 1 && !this.parameters[segments[0].name] && (Reflect.has(this.parameters, Object.values(this.route.definition.bindings)[0]) || Reflect.has(this.parameters, "id"))) {
|
|
748
|
+
this.parameters = { [segments[0].name]: this.parameters };
|
|
749
|
+
}
|
|
750
|
+
this.parameters = {
|
|
751
|
+
...this.getDefaults(),
|
|
752
|
+
...this.substituteBindings()
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
getDefaults() {
|
|
756
|
+
return this.route.parameterSegments.filter(({ name }) => state.routes.value?.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: state.routes.value?.defaults[name] }), {});
|
|
757
|
+
}
|
|
758
|
+
substituteBindings() {
|
|
759
|
+
return Object.entries(this.parameters).reduce((result, [key, value]) => {
|
|
760
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
|
|
761
|
+
return { ...result, [key]: value };
|
|
762
|
+
}
|
|
763
|
+
if (!Reflect.has(value, this.route.definition.bindings[key])) {
|
|
764
|
+
if (Reflect.has(value, "id")) {
|
|
765
|
+
this.route.definition.bindings[key] = "id";
|
|
766
|
+
} else {
|
|
767
|
+
throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return { ...result, [key]: value[this.route.definition.bindings[key]] };
|
|
771
|
+
}, {});
|
|
772
|
+
}
|
|
773
|
+
valueOf() {
|
|
774
|
+
return this.toString();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function route(name, parameters, absolute) {
|
|
779
|
+
return new Router(name, parameters, absolute).toString();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export { HybridlyImports, HybridlyResolver, RouterLink, initializeHybridly, route, useBackForward, useContext, useForm, useHistoryState, useLayout, usePaginator, useProperties, useProperty, useRouter };
|