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