@hybridly/vue 0.4.3 → 0.4.5

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.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  const vue = require('vue');
4
4
  const core = require('@hybridly/core');
5
5
  const utils = require('@hybridly/utils');
6
- const progressPlugin = require('@hybridly/progress-plugin');
6
+ const nprogress = require('nprogress');
7
7
  const devtoolsApi = require('@vue/devtools-api');
8
8
  const qs = require('qs');
9
9
  const dotDiver = require('@clickbar/dot-diver');
@@ -12,9 +12,128 @@ const hybridly = require('hybridly');
12
12
 
13
13
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
14
14
 
15
+ const nprogress__default = /*#__PURE__*/_interopDefaultCompat(nprogress);
15
16
  const qs__default = /*#__PURE__*/_interopDefaultCompat(qs);
16
17
  const isEqual__default = /*#__PURE__*/_interopDefaultCompat(isEqual);
17
18
 
19
+ function progress(options) {
20
+ const resolved = {
21
+ delay: 250,
22
+ color: "#29d",
23
+ includeCSS: true,
24
+ spinner: false,
25
+ ...options
26
+ };
27
+ let timeout;
28
+ function startProgress() {
29
+ nprogress__default.start();
30
+ }
31
+ function finishProgress() {
32
+ if (nprogress__default.isStarted()) {
33
+ nprogress__default.done(true);
34
+ }
35
+ }
36
+ function cancelProgress() {
37
+ if (nprogress__default.isStarted()) {
38
+ nprogress__default.done(true);
39
+ nprogress__default.remove();
40
+ }
41
+ }
42
+ return core.definePlugin({
43
+ name: "hybridly:progress",
44
+ initialized() {
45
+ nprogress__default.configure({ showSpinner: resolved.spinner });
46
+ if (resolved.includeCSS) {
47
+ injectCSS(resolved.color);
48
+ }
49
+ },
50
+ start: (context) => {
51
+ if (context.pendingNavigation?.options.progress === false) {
52
+ return;
53
+ }
54
+ clearTimeout(timeout);
55
+ timeout = setTimeout(() => {
56
+ finishProgress();
57
+ startProgress();
58
+ }, resolved.delay);
59
+ },
60
+ progress: (progress2) => {
61
+ if (nprogress__default.isStarted() && progress2.percentage) {
62
+ nprogress__default.set(Math.max(nprogress__default.status, progress2.percentage / 100 * 0.9));
63
+ }
64
+ },
65
+ success: () => finishProgress(),
66
+ error: () => cancelProgress(),
67
+ fail: () => cancelProgress(),
68
+ after: () => clearTimeout(timeout)
69
+ });
70
+ }
71
+ function injectCSS(color) {
72
+ const element = document.createElement("style");
73
+ element.textContent = `
74
+ #nprogress {
75
+ pointer-events: none;
76
+ --progress-color: ${color};
77
+ }
78
+ #nprogress .bar {
79
+ background: var(--progress-color);
80
+ position: fixed;
81
+ z-index: 1031;
82
+ top: 0;
83
+ left: 0;
84
+ width: 100%;
85
+ height: 2px;
86
+ }
87
+ #nprogress .peg {
88
+ display: block;
89
+ position: absolute;
90
+ right: 0px;
91
+ width: 100px;
92
+ height: 100%;
93
+ box-shadow: 0 0 10px var(--progress-color), 0 0 5px var(--progress-color);
94
+ opacity: 1.0;
95
+ -webkit-transform: rotate(3deg) translate(0px, -4px);
96
+ -ms-transform: rotate(3deg) translate(0px, -4px);
97
+ transform: rotate(3deg) translate(0px, -4px);
98
+ }
99
+ #nprogress .spinner {
100
+ display: block;
101
+ position: fixed;
102
+ z-index: 1031;
103
+ top: 15px;
104
+ right: 15px;
105
+ }
106
+ #nprogress .spinner-icon {
107
+ width: 18px;
108
+ height: 18px;
109
+ box-sizing: border-box;
110
+ border: solid 2px transparent;
111
+ border-top-color: var(--progress-color);
112
+ border-left-color: var(--progress-color);
113
+ border-radius: 50%;
114
+ -webkit-animation: nprogress-spinner 400ms linear infinite;
115
+ animation: nprogress-spinner 400ms linear infinite;
116
+ }
117
+ .nprogress-custom-parent {
118
+ overflow: hidden;
119
+ position: relative;
120
+ }
121
+ .nprogress-custom-parent #nprogress .spinner,
122
+ .nprogress-custom-parent #nprogress .bar {
123
+ position: absolute;
124
+ }
125
+ @-webkit-keyframes nprogress-spinner {
126
+ 0% { -webkit-transform: rotate(0deg); }
127
+ 100% { -webkit-transform: rotate(360deg); }
128
+ }
129
+ @keyframes nprogress-spinner {
130
+ 0% { transform: rotate(0deg); }
131
+ 100% { transform: rotate(360deg); }
132
+ }
133
+ `;
134
+ document.head.appendChild(element);
135
+ }
136
+
18
137
  const DEBUG_KEY = "vue:state:dialog";
19
138
  const dialogStore = {
20
139
  state: {
@@ -117,19 +236,25 @@ const wrapper = vue.defineComponent({
117
236
  renderDialog()
118
237
  ];
119
238
  }
120
- function renderView() {
121
- utils.debug.adapter("vue:render:view", "Rendering view.");
122
- state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
123
- const actual = state.view.value?.mounted;
124
- state.view.value.mounted = () => {
239
+ function hijackOnMounted(component, type) {
240
+ if (!component) {
241
+ return;
242
+ }
243
+ const actual = component?.mounted;
244
+ component.mounted = () => {
125
245
  actual?.();
126
246
  vue.nextTick(() => {
127
- utils.debug.adapter("vue:render:view", "Calling mounted callbacks.");
247
+ utils.debug.adapter(`vue:render:${type}`, "Calling mounted callbacks.");
128
248
  while (onMountedCallbacks.length) {
129
249
  onMountedCallbacks.shift()?.();
130
250
  }
131
251
  });
132
252
  };
253
+ }
254
+ function renderView() {
255
+ utils.debug.adapter("vue:render:view", "Rendering view.");
256
+ state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
257
+ hijackOnMounted(state.view.value, "view");
133
258
  return vue.h(state.view.value, {
134
259
  ...state.properties.value,
135
260
  key: state.viewKey.value
@@ -138,6 +263,7 @@ const wrapper = vue.defineComponent({
138
263
  function renderDialog() {
139
264
  if (dialogStore.state.component.value && dialogStore.state.properties.value) {
140
265
  utils.debug.adapter("vue:render:dialog", "Rendering dialog.");
266
+ hijackOnMounted(dialogStore.state.component.value, "dialog");
141
267
  return vue.h(dialogStore.state.component.value, {
142
268
  ...dialogStore.state.properties.value,
143
269
  key: dialogStore.state.key.value
@@ -191,6 +317,11 @@ function setupDevtools(app) {
191
317
  key: "component",
192
318
  value: state.context.value?.view.component
193
319
  });
320
+ payload.instanceData.state.push({
321
+ type: hybridlyStateType,
322
+ key: "deferred",
323
+ value: state.context.value?.view.deferred
324
+ });
194
325
  payload.instanceData.state.push({
195
326
  type: hybridlyStateType,
196
327
  key: "dialog",
@@ -278,6 +409,33 @@ const devtools = {
278
409
  }
279
410
  };
280
411
 
412
+ function viewTransition() {
413
+ if (!document.startViewTransition) {
414
+ return { name: "view-transition" };
415
+ }
416
+ let domUpdated;
417
+ return {
418
+ name: "view-transition",
419
+ navigating: async ({ type, hasDialog }) => {
420
+ if (type === "initial" || hasDialog) {
421
+ return;
422
+ }
423
+ return new Promise((confirmTransitionStarted) => document.startViewTransition(() => {
424
+ confirmTransitionStarted(true);
425
+ return new Promise((resolve) => domUpdated = resolve);
426
+ }));
427
+ },
428
+ mounted: () => {
429
+ domUpdated?.();
430
+ domUpdated = void 0;
431
+ },
432
+ navigated: () => {
433
+ domUpdated?.();
434
+ domUpdated = void 0;
435
+ }
436
+ };
437
+ }
438
+
281
439
  async function initializeHybridly(options = {}) {
282
440
  const resolved = options;
283
441
  const { element, payload, resolve } = prepare(resolved);
@@ -302,12 +460,16 @@ async function initializeHybridly(options = {}) {
302
460
  state.setContext(context);
303
461
  },
304
462
  onViewSwap: async (options2) => {
305
- state.setView(options2.component);
463
+ if (options2.component) {
464
+ onMountedCallbacks.push(() => options2.onMounted?.({ isDialog: false }));
465
+ state.setView(options2.component);
466
+ }
306
467
  state.setProperties(options2.properties);
307
468
  if (!options2.preserveState && !options2.dialog) {
308
469
  state.setViewKey(utils.random());
309
470
  }
310
471
  if (options2.dialog) {
472
+ onMountedCallbacks.push(() => options2.onMounted?.({ isDialog: true }));
311
473
  dialogStore.setComponent(await resolve(options2.dialog.component));
312
474
  dialogStore.setProperties(options2.dialog.properties);
313
475
  dialogStore.setKey(options2.dialog.key);
@@ -363,11 +525,12 @@ function prepare(options) {
363
525
  }
364
526
  return await resolveViewComponent(name, options);
365
527
  };
528
+ options.plugins ?? (options.plugins = []);
366
529
  if (options.progress !== false) {
367
- options.plugins = [
368
- progressPlugin.progress(typeof options.progress === "object" ? options.progress : {}),
369
- ...options.plugins ?? []
370
- ];
530
+ options.plugins.push(progress(typeof options.progress === "object" ? options.progress : {}));
531
+ }
532
+ if (options.viewTransition !== false) {
533
+ options.plugins.push(viewTransition());
371
534
  }
372
535
  return {
373
536
  isServer,
@@ -737,7 +900,7 @@ function useHistoryState(key, initial) {
737
900
  function useBackForward() {
738
901
  const callbacks = [];
739
902
  core.registerHook("navigated", (options) => {
740
- if (options.isBackForward) {
903
+ if (options.type === "back-forward") {
741
904
  callbacks.forEach((fn) => fn(state.context.value));
742
905
  callbacks.splice(0, callbacks.length);
743
906
  }
package/dist/index.d.cts CHANGED
@@ -4,11 +4,38 @@ import * as _hybridly_core from '@hybridly/core';
4
4
  import { RouterContextOptions, Plugin as Plugin$1, RouterContext, Method as Method$1, HybridRequestOptions as HybridRequestOptions$1, UrlResolvable as UrlResolvable$1, registerHook as registerHook$1 } from '@hybridly/core';
5
5
  export { can, route, router } from '@hybridly/core';
6
6
  import { Axios, AxiosResponse, AxiosProgressEvent } from 'axios';
7
- import { ProgressOptions } from '@hybridly/progress-plugin';
8
7
  import * as _vue_shared from '@vue/shared';
9
8
  import { RequestData } from '@hybridly/utils';
10
9
  import { SearchableObject, Path, PathValue } from '@clickbar/dot-diver';
11
10
 
11
+ interface ProgressOptions {
12
+ /**
13
+ * The delay after which the progress bar will
14
+ * appear during navigation, in milliseconds.
15
+ *
16
+ * @default 250
17
+ */
18
+ delay: number;
19
+ /**
20
+ * The color of the progress bar.
21
+ *
22
+ * Defaults to #29d.
23
+ */
24
+ color: string;
25
+ /**
26
+ * Whether to include the default NProgress styles.
27
+ *
28
+ * Defaults to true.
29
+ */
30
+ includeCSS: boolean;
31
+ /**
32
+ * Whether the NProgress spinner will be shown.
33
+ *
34
+ * Defaults to false.
35
+ */
36
+ spinner: boolean;
37
+ }
38
+
12
39
  /**
13
40
  * Initializes Hybridly's router and context.
14
41
  */
@@ -34,6 +61,11 @@ interface InitializeOptions {
34
61
  plugins?: Plugin$1[];
35
62
  /** Custom Axios instance. */
36
63
  axios?: Axios;
64
+ /**
65
+ * Enables the View Transition API, if supported.
66
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition
67
+ */
68
+ viewTransition?: boolean;
37
69
  }
38
70
  interface SetupArguments {
39
71
  /** DOM element to mount Vue on. */
@@ -153,7 +185,7 @@ declare const RouterLink: vue.DefineComponent<{
153
185
  }, {}>;
154
186
 
155
187
  /** Accesses all current properties. */
156
- declare function useProperties<T extends object, Global extends GlobalHybridlyProperties>(): vue.DeepReadonly<vue.UnwrapNestedRefs<T & Global>>;
188
+ declare function useProperties<T extends object, Global extends GlobalHybridlyProperties = GlobalHybridlyProperties>(): vue.DeepReadonly<vue.UnwrapNestedRefs<T & Global>>;
157
189
  /** Accesses a property with a dot notation. */
158
190
  declare function useProperty<Override = never, T extends SearchableObject = GlobalHybridlyProperties, P extends Path<T> & string = Path<T> & string, ReturnType = [Override] extends [never] ? PathValue<T, P> : Override>(path: [Override] extends [never] ? P : string): ComputedRef<ReturnType>;
159
191
  /**
@@ -233,15 +265,21 @@ interface Hooks extends RequestHooks {
233
265
  /**
234
266
  * Called when a component navigation is being made.
235
267
  */
236
- navigating: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
268
+ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
237
269
  /**
238
270
  * Called when a component has been navigated to.
239
271
  */
240
- navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
272
+ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
241
273
  /**
242
274
  * Called when a component has been navigated to and was mounted by the adapter.
243
275
  */
244
- mounted: (context: InternalRouterContext) => MaybePromise<any>;
276
+ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
277
+ }
278
+ interface MountedHookOptions {
279
+ /**
280
+ * Whether the component being mounted is a dialog.
281
+ */
282
+ isDialog: boolean;
245
283
  }
246
284
 
247
285
  interface RoutingConfiguration {
@@ -289,11 +327,22 @@ interface NavigationOptions {
289
327
  * @internal This is an advanced property meant to be used internally.
290
328
  */
291
329
  updateHistoryState?: boolean;
330
+ }
331
+ interface InternalNavigationOptions extends NavigationOptions {
292
332
  /**
293
- * Defines whether this navigation is a back/forward navigation from the popstate event.
294
- * @internal This is an advanced property meant to be used internally.
333
+ * Defines the kind of navigation being performed.
334
+ * - initial: the initial page load's navigation
335
+ * - server: a navigation initiated by a server round-trip
336
+ * - local: a navigation initiated by `router.local`
337
+ * - back-forward: a navigation initiated by the browser's `popstate` event
338
+ * @internal
339
+ */
340
+ type: 'initial' | 'local' | 'back-forward' | 'server';
341
+ /**
342
+ * Defines whether this navigation opens a dialog.
343
+ * @internal
295
344
  */
296
- isBackForward?: boolean;
345
+ hasDialog?: boolean;
297
346
  }
298
347
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
299
348
  interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
@@ -341,11 +390,13 @@ interface PendingNavigation {
341
390
  /** A page or dialog component. */
342
391
  interface View {
343
392
  /** Name of the component to use. */
344
- component: string;
393
+ component?: string;
345
394
  /** Properties to apply to the component. */
346
395
  properties: Properties;
396
+ /** Deferred properties for this view. */
397
+ deferred: string[];
347
398
  }
348
- interface Dialog extends View {
399
+ interface Dialog extends Required<View> {
349
400
  /** URL that is the base background page when navigating to the dialog directly. */
350
401
  baseUrl: string;
351
402
  /** URL to which the dialog should redirect when closed. */
@@ -366,6 +417,8 @@ interface SwapOptions<T> {
366
417
  preserveState?: boolean;
367
418
  /** Current dialog. */
368
419
  dialog?: Dialog;
420
+ /** On mounted callback. */
421
+ onMounted?: (options: MountedHookOptions) => void;
369
422
  }
370
423
  type ViewComponent = any;
371
424
  type ResolveComponent = (name: string) => Promise<ViewComponent>;
package/dist/index.d.mts CHANGED
@@ -4,11 +4,38 @@ import * as _hybridly_core from '@hybridly/core';
4
4
  import { RouterContextOptions, Plugin as Plugin$1, RouterContext, Method as Method$1, HybridRequestOptions as HybridRequestOptions$1, UrlResolvable as UrlResolvable$1, registerHook as registerHook$1 } from '@hybridly/core';
5
5
  export { can, route, router } from '@hybridly/core';
6
6
  import { Axios, AxiosResponse, AxiosProgressEvent } from 'axios';
7
- import { ProgressOptions } from '@hybridly/progress-plugin';
8
7
  import * as _vue_shared from '@vue/shared';
9
8
  import { RequestData } from '@hybridly/utils';
10
9
  import { SearchableObject, Path, PathValue } from '@clickbar/dot-diver';
11
10
 
11
+ interface ProgressOptions {
12
+ /**
13
+ * The delay after which the progress bar will
14
+ * appear during navigation, in milliseconds.
15
+ *
16
+ * @default 250
17
+ */
18
+ delay: number;
19
+ /**
20
+ * The color of the progress bar.
21
+ *
22
+ * Defaults to #29d.
23
+ */
24
+ color: string;
25
+ /**
26
+ * Whether to include the default NProgress styles.
27
+ *
28
+ * Defaults to true.
29
+ */
30
+ includeCSS: boolean;
31
+ /**
32
+ * Whether the NProgress spinner will be shown.
33
+ *
34
+ * Defaults to false.
35
+ */
36
+ spinner: boolean;
37
+ }
38
+
12
39
  /**
13
40
  * Initializes Hybridly's router and context.
14
41
  */
@@ -34,6 +61,11 @@ interface InitializeOptions {
34
61
  plugins?: Plugin$1[];
35
62
  /** Custom Axios instance. */
36
63
  axios?: Axios;
64
+ /**
65
+ * Enables the View Transition API, if supported.
66
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition
67
+ */
68
+ viewTransition?: boolean;
37
69
  }
38
70
  interface SetupArguments {
39
71
  /** DOM element to mount Vue on. */
@@ -153,7 +185,7 @@ declare const RouterLink: vue.DefineComponent<{
153
185
  }, {}>;
154
186
 
155
187
  /** Accesses all current properties. */
156
- declare function useProperties<T extends object, Global extends GlobalHybridlyProperties>(): vue.DeepReadonly<vue.UnwrapNestedRefs<T & Global>>;
188
+ declare function useProperties<T extends object, Global extends GlobalHybridlyProperties = GlobalHybridlyProperties>(): vue.DeepReadonly<vue.UnwrapNestedRefs<T & Global>>;
157
189
  /** Accesses a property with a dot notation. */
158
190
  declare function useProperty<Override = never, T extends SearchableObject = GlobalHybridlyProperties, P extends Path<T> & string = Path<T> & string, ReturnType = [Override] extends [never] ? PathValue<T, P> : Override>(path: [Override] extends [never] ? P : string): ComputedRef<ReturnType>;
159
191
  /**
@@ -233,15 +265,21 @@ interface Hooks extends RequestHooks {
233
265
  /**
234
266
  * Called when a component navigation is being made.
235
267
  */
236
- navigating: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
268
+ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
237
269
  /**
238
270
  * Called when a component has been navigated to.
239
271
  */
240
- navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
272
+ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
241
273
  /**
242
274
  * Called when a component has been navigated to and was mounted by the adapter.
243
275
  */
244
- mounted: (context: InternalRouterContext) => MaybePromise<any>;
276
+ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
277
+ }
278
+ interface MountedHookOptions {
279
+ /**
280
+ * Whether the component being mounted is a dialog.
281
+ */
282
+ isDialog: boolean;
245
283
  }
246
284
 
247
285
  interface RoutingConfiguration {
@@ -289,11 +327,22 @@ interface NavigationOptions {
289
327
  * @internal This is an advanced property meant to be used internally.
290
328
  */
291
329
  updateHistoryState?: boolean;
330
+ }
331
+ interface InternalNavigationOptions extends NavigationOptions {
292
332
  /**
293
- * Defines whether this navigation is a back/forward navigation from the popstate event.
294
- * @internal This is an advanced property meant to be used internally.
333
+ * Defines the kind of navigation being performed.
334
+ * - initial: the initial page load's navigation
335
+ * - server: a navigation initiated by a server round-trip
336
+ * - local: a navigation initiated by `router.local`
337
+ * - back-forward: a navigation initiated by the browser's `popstate` event
338
+ * @internal
339
+ */
340
+ type: 'initial' | 'local' | 'back-forward' | 'server';
341
+ /**
342
+ * Defines whether this navigation opens a dialog.
343
+ * @internal
295
344
  */
296
- isBackForward?: boolean;
345
+ hasDialog?: boolean;
297
346
  }
298
347
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
299
348
  interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
@@ -341,11 +390,13 @@ interface PendingNavigation {
341
390
  /** A page or dialog component. */
342
391
  interface View {
343
392
  /** Name of the component to use. */
344
- component: string;
393
+ component?: string;
345
394
  /** Properties to apply to the component. */
346
395
  properties: Properties;
396
+ /** Deferred properties for this view. */
397
+ deferred: string[];
347
398
  }
348
- interface Dialog extends View {
399
+ interface Dialog extends Required<View> {
349
400
  /** URL that is the base background page when navigating to the dialog directly. */
350
401
  baseUrl: string;
351
402
  /** URL to which the dialog should redirect when closed. */
@@ -366,6 +417,8 @@ interface SwapOptions<T> {
366
417
  preserveState?: boolean;
367
418
  /** Current dialog. */
368
419
  dialog?: Dialog;
420
+ /** On mounted callback. */
421
+ onMounted?: (options: MountedHookOptions) => void;
369
422
  }
370
423
  type ViewComponent = any;
371
424
  type ResolveComponent = (name: string) => Promise<ViewComponent>;
package/dist/index.d.ts CHANGED
@@ -4,11 +4,38 @@ import * as _hybridly_core from '@hybridly/core';
4
4
  import { RouterContextOptions, Plugin as Plugin$1, RouterContext, Method as Method$1, HybridRequestOptions as HybridRequestOptions$1, UrlResolvable as UrlResolvable$1, registerHook as registerHook$1 } from '@hybridly/core';
5
5
  export { can, route, router } from '@hybridly/core';
6
6
  import { Axios, AxiosResponse, AxiosProgressEvent } from 'axios';
7
- import { ProgressOptions } from '@hybridly/progress-plugin';
8
7
  import * as _vue_shared from '@vue/shared';
9
8
  import { RequestData } from '@hybridly/utils';
10
9
  import { SearchableObject, Path, PathValue } from '@clickbar/dot-diver';
11
10
 
11
+ interface ProgressOptions {
12
+ /**
13
+ * The delay after which the progress bar will
14
+ * appear during navigation, in milliseconds.
15
+ *
16
+ * @default 250
17
+ */
18
+ delay: number;
19
+ /**
20
+ * The color of the progress bar.
21
+ *
22
+ * Defaults to #29d.
23
+ */
24
+ color: string;
25
+ /**
26
+ * Whether to include the default NProgress styles.
27
+ *
28
+ * Defaults to true.
29
+ */
30
+ includeCSS: boolean;
31
+ /**
32
+ * Whether the NProgress spinner will be shown.
33
+ *
34
+ * Defaults to false.
35
+ */
36
+ spinner: boolean;
37
+ }
38
+
12
39
  /**
13
40
  * Initializes Hybridly's router and context.
14
41
  */
@@ -34,6 +61,11 @@ interface InitializeOptions {
34
61
  plugins?: Plugin$1[];
35
62
  /** Custom Axios instance. */
36
63
  axios?: Axios;
64
+ /**
65
+ * Enables the View Transition API, if supported.
66
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition
67
+ */
68
+ viewTransition?: boolean;
37
69
  }
38
70
  interface SetupArguments {
39
71
  /** DOM element to mount Vue on. */
@@ -153,7 +185,7 @@ declare const RouterLink: vue.DefineComponent<{
153
185
  }, {}>;
154
186
 
155
187
  /** Accesses all current properties. */
156
- declare function useProperties<T extends object, Global extends GlobalHybridlyProperties>(): vue.DeepReadonly<vue.UnwrapNestedRefs<T & Global>>;
188
+ declare function useProperties<T extends object, Global extends GlobalHybridlyProperties = GlobalHybridlyProperties>(): vue.DeepReadonly<vue.UnwrapNestedRefs<T & Global>>;
157
189
  /** Accesses a property with a dot notation. */
158
190
  declare function useProperty<Override = never, T extends SearchableObject = GlobalHybridlyProperties, P extends Path<T> & string = Path<T> & string, ReturnType = [Override] extends [never] ? PathValue<T, P> : Override>(path: [Override] extends [never] ? P : string): ComputedRef<ReturnType>;
159
191
  /**
@@ -233,15 +265,21 @@ interface Hooks extends RequestHooks {
233
265
  /**
234
266
  * Called when a component navigation is being made.
235
267
  */
236
- navigating: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
268
+ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
237
269
  /**
238
270
  * Called when a component has been navigated to.
239
271
  */
240
- navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
272
+ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
241
273
  /**
242
274
  * Called when a component has been navigated to and was mounted by the adapter.
243
275
  */
244
- mounted: (context: InternalRouterContext) => MaybePromise<any>;
276
+ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
277
+ }
278
+ interface MountedHookOptions {
279
+ /**
280
+ * Whether the component being mounted is a dialog.
281
+ */
282
+ isDialog: boolean;
245
283
  }
246
284
 
247
285
  interface RoutingConfiguration {
@@ -289,11 +327,22 @@ interface NavigationOptions {
289
327
  * @internal This is an advanced property meant to be used internally.
290
328
  */
291
329
  updateHistoryState?: boolean;
330
+ }
331
+ interface InternalNavigationOptions extends NavigationOptions {
292
332
  /**
293
- * Defines whether this navigation is a back/forward navigation from the popstate event.
294
- * @internal This is an advanced property meant to be used internally.
333
+ * Defines the kind of navigation being performed.
334
+ * - initial: the initial page load's navigation
335
+ * - server: a navigation initiated by a server round-trip
336
+ * - local: a navigation initiated by `router.local`
337
+ * - back-forward: a navigation initiated by the browser's `popstate` event
338
+ * @internal
339
+ */
340
+ type: 'initial' | 'local' | 'back-forward' | 'server';
341
+ /**
342
+ * Defines whether this navigation opens a dialog.
343
+ * @internal
295
344
  */
296
- isBackForward?: boolean;
345
+ hasDialog?: boolean;
297
346
  }
298
347
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
299
348
  interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
@@ -341,11 +390,13 @@ interface PendingNavigation {
341
390
  /** A page or dialog component. */
342
391
  interface View {
343
392
  /** Name of the component to use. */
344
- component: string;
393
+ component?: string;
345
394
  /** Properties to apply to the component. */
346
395
  properties: Properties;
396
+ /** Deferred properties for this view. */
397
+ deferred: string[];
347
398
  }
348
- interface Dialog extends View {
399
+ interface Dialog extends Required<View> {
349
400
  /** URL that is the base background page when navigating to the dialog directly. */
350
401
  baseUrl: string;
351
402
  /** URL to which the dialog should redirect when closed. */
@@ -366,6 +417,8 @@ interface SwapOptions<T> {
366
417
  preserveState?: boolean;
367
418
  /** Current dialog. */
368
419
  dialog?: Dialog;
420
+ /** On mounted callback. */
421
+ onMounted?: (options: MountedHookOptions) => void;
369
422
  }
370
423
  type ViewComponent = any;
371
424
  type ResolveComponent = (name: string) => Promise<ViewComponent>;
package/dist/index.mjs CHANGED
@@ -1,14 +1,132 @@
1
1
  import { shallowRef, ref, unref, triggerRef, defineComponent, toRaw, h, nextTick, createApp, isRef, reactive, readonly, computed, toValue, watch, getCurrentInstance, onUnmounted } from 'vue';
2
- import { registerHook as registerHook$1, createRouter, makeUrl, router } from '@hybridly/core';
2
+ import { definePlugin, registerHook as registerHook$1, createRouter, makeUrl, router } from '@hybridly/core';
3
3
  export { can, route, router } from '@hybridly/core';
4
4
  import { debug, random, showPageComponentErrorModal, merge, clone, unsetPropertyAtPath, setValueAtPath } from '@hybridly/utils';
5
- import { progress } from '@hybridly/progress-plugin';
5
+ import nprogress from 'nprogress';
6
6
  import { setupDevtoolsPlugin } from '@vue/devtools-api';
7
7
  import qs from 'qs';
8
8
  import { getByPath, setByPath } from '@clickbar/dot-diver';
9
9
  import isEqual from 'lodash.isequal';
10
10
  import { router as router$1 } from 'hybridly';
11
11
 
12
+ function progress(options) {
13
+ const resolved = {
14
+ delay: 250,
15
+ color: "#29d",
16
+ includeCSS: true,
17
+ spinner: false,
18
+ ...options
19
+ };
20
+ let timeout;
21
+ function startProgress() {
22
+ nprogress.start();
23
+ }
24
+ function finishProgress() {
25
+ if (nprogress.isStarted()) {
26
+ nprogress.done(true);
27
+ }
28
+ }
29
+ function cancelProgress() {
30
+ if (nprogress.isStarted()) {
31
+ nprogress.done(true);
32
+ nprogress.remove();
33
+ }
34
+ }
35
+ return definePlugin({
36
+ name: "hybridly:progress",
37
+ initialized() {
38
+ nprogress.configure({ showSpinner: resolved.spinner });
39
+ if (resolved.includeCSS) {
40
+ injectCSS(resolved.color);
41
+ }
42
+ },
43
+ start: (context) => {
44
+ if (context.pendingNavigation?.options.progress === false) {
45
+ return;
46
+ }
47
+ clearTimeout(timeout);
48
+ timeout = setTimeout(() => {
49
+ finishProgress();
50
+ startProgress();
51
+ }, resolved.delay);
52
+ },
53
+ progress: (progress2) => {
54
+ if (nprogress.isStarted() && progress2.percentage) {
55
+ nprogress.set(Math.max(nprogress.status, progress2.percentage / 100 * 0.9));
56
+ }
57
+ },
58
+ success: () => finishProgress(),
59
+ error: () => cancelProgress(),
60
+ fail: () => cancelProgress(),
61
+ after: () => clearTimeout(timeout)
62
+ });
63
+ }
64
+ function injectCSS(color) {
65
+ const element = document.createElement("style");
66
+ element.textContent = `
67
+ #nprogress {
68
+ pointer-events: none;
69
+ --progress-color: ${color};
70
+ }
71
+ #nprogress .bar {
72
+ background: var(--progress-color);
73
+ position: fixed;
74
+ z-index: 1031;
75
+ top: 0;
76
+ left: 0;
77
+ width: 100%;
78
+ height: 2px;
79
+ }
80
+ #nprogress .peg {
81
+ display: block;
82
+ position: absolute;
83
+ right: 0px;
84
+ width: 100px;
85
+ height: 100%;
86
+ box-shadow: 0 0 10px var(--progress-color), 0 0 5px var(--progress-color);
87
+ opacity: 1.0;
88
+ -webkit-transform: rotate(3deg) translate(0px, -4px);
89
+ -ms-transform: rotate(3deg) translate(0px, -4px);
90
+ transform: rotate(3deg) translate(0px, -4px);
91
+ }
92
+ #nprogress .spinner {
93
+ display: block;
94
+ position: fixed;
95
+ z-index: 1031;
96
+ top: 15px;
97
+ right: 15px;
98
+ }
99
+ #nprogress .spinner-icon {
100
+ width: 18px;
101
+ height: 18px;
102
+ box-sizing: border-box;
103
+ border: solid 2px transparent;
104
+ border-top-color: var(--progress-color);
105
+ border-left-color: var(--progress-color);
106
+ border-radius: 50%;
107
+ -webkit-animation: nprogress-spinner 400ms linear infinite;
108
+ animation: nprogress-spinner 400ms linear infinite;
109
+ }
110
+ .nprogress-custom-parent {
111
+ overflow: hidden;
112
+ position: relative;
113
+ }
114
+ .nprogress-custom-parent #nprogress .spinner,
115
+ .nprogress-custom-parent #nprogress .bar {
116
+ position: absolute;
117
+ }
118
+ @-webkit-keyframes nprogress-spinner {
119
+ 0% { -webkit-transform: rotate(0deg); }
120
+ 100% { -webkit-transform: rotate(360deg); }
121
+ }
122
+ @keyframes nprogress-spinner {
123
+ 0% { transform: rotate(0deg); }
124
+ 100% { transform: rotate(360deg); }
125
+ }
126
+ `;
127
+ document.head.appendChild(element);
128
+ }
129
+
12
130
  const DEBUG_KEY = "vue:state:dialog";
13
131
  const dialogStore = {
14
132
  state: {
@@ -111,19 +229,25 @@ const wrapper = defineComponent({
111
229
  renderDialog()
112
230
  ];
113
231
  }
114
- function renderView() {
115
- debug.adapter("vue:render:view", "Rendering view.");
116
- state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
117
- const actual = state.view.value?.mounted;
118
- state.view.value.mounted = () => {
232
+ function hijackOnMounted(component, type) {
233
+ if (!component) {
234
+ return;
235
+ }
236
+ const actual = component?.mounted;
237
+ component.mounted = () => {
119
238
  actual?.();
120
239
  nextTick(() => {
121
- debug.adapter("vue:render:view", "Calling mounted callbacks.");
240
+ debug.adapter(`vue:render:${type}`, "Calling mounted callbacks.");
122
241
  while (onMountedCallbacks.length) {
123
242
  onMountedCallbacks.shift()?.();
124
243
  }
125
244
  });
126
245
  };
246
+ }
247
+ function renderView() {
248
+ debug.adapter("vue:render:view", "Rendering view.");
249
+ state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
250
+ hijackOnMounted(state.view.value, "view");
127
251
  return h(state.view.value, {
128
252
  ...state.properties.value,
129
253
  key: state.viewKey.value
@@ -132,6 +256,7 @@ const wrapper = defineComponent({
132
256
  function renderDialog() {
133
257
  if (dialogStore.state.component.value && dialogStore.state.properties.value) {
134
258
  debug.adapter("vue:render:dialog", "Rendering dialog.");
259
+ hijackOnMounted(dialogStore.state.component.value, "dialog");
135
260
  return h(dialogStore.state.component.value, {
136
261
  ...dialogStore.state.properties.value,
137
262
  key: dialogStore.state.key.value
@@ -185,6 +310,11 @@ function setupDevtools(app) {
185
310
  key: "component",
186
311
  value: state.context.value?.view.component
187
312
  });
313
+ payload.instanceData.state.push({
314
+ type: hybridlyStateType,
315
+ key: "deferred",
316
+ value: state.context.value?.view.deferred
317
+ });
188
318
  payload.instanceData.state.push({
189
319
  type: hybridlyStateType,
190
320
  key: "dialog",
@@ -272,6 +402,33 @@ const devtools = {
272
402
  }
273
403
  };
274
404
 
405
+ function viewTransition() {
406
+ if (!document.startViewTransition) {
407
+ return { name: "view-transition" };
408
+ }
409
+ let domUpdated;
410
+ return {
411
+ name: "view-transition",
412
+ navigating: async ({ type, hasDialog }) => {
413
+ if (type === "initial" || hasDialog) {
414
+ return;
415
+ }
416
+ return new Promise((confirmTransitionStarted) => document.startViewTransition(() => {
417
+ confirmTransitionStarted(true);
418
+ return new Promise((resolve) => domUpdated = resolve);
419
+ }));
420
+ },
421
+ mounted: () => {
422
+ domUpdated?.();
423
+ domUpdated = void 0;
424
+ },
425
+ navigated: () => {
426
+ domUpdated?.();
427
+ domUpdated = void 0;
428
+ }
429
+ };
430
+ }
431
+
275
432
  async function initializeHybridly(options = {}) {
276
433
  const resolved = options;
277
434
  const { element, payload, resolve } = prepare(resolved);
@@ -296,12 +453,16 @@ async function initializeHybridly(options = {}) {
296
453
  state.setContext(context);
297
454
  },
298
455
  onViewSwap: async (options2) => {
299
- state.setView(options2.component);
456
+ if (options2.component) {
457
+ onMountedCallbacks.push(() => options2.onMounted?.({ isDialog: false }));
458
+ state.setView(options2.component);
459
+ }
300
460
  state.setProperties(options2.properties);
301
461
  if (!options2.preserveState && !options2.dialog) {
302
462
  state.setViewKey(random());
303
463
  }
304
464
  if (options2.dialog) {
465
+ onMountedCallbacks.push(() => options2.onMounted?.({ isDialog: true }));
305
466
  dialogStore.setComponent(await resolve(options2.dialog.component));
306
467
  dialogStore.setProperties(options2.dialog.properties);
307
468
  dialogStore.setKey(options2.dialog.key);
@@ -357,11 +518,12 @@ function prepare(options) {
357
518
  }
358
519
  return await resolveViewComponent(name, options);
359
520
  };
521
+ options.plugins ?? (options.plugins = []);
360
522
  if (options.progress !== false) {
361
- options.plugins = [
362
- progress(typeof options.progress === "object" ? options.progress : {}),
363
- ...options.plugins ?? []
364
- ];
523
+ options.plugins.push(progress(typeof options.progress === "object" ? options.progress : {}));
524
+ }
525
+ if (options.viewTransition !== false) {
526
+ options.plugins.push(viewTransition());
365
527
  }
366
528
  return {
367
529
  isServer,
@@ -731,7 +893,7 @@ function useHistoryState(key, initial) {
731
893
  function useBackForward() {
732
894
  const callbacks = [];
733
895
  registerHook$1("navigated", (options) => {
734
- if (options.isBackForward) {
896
+ if (options.type === "back-forward") {
735
897
  callbacks.forEach((fn) => fn(state.context.value));
736
898
  callbacks.splice(0, callbacks.length);
737
899
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridly/vue",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Vue adapter for Hybridly",
5
5
  "keywords": [
6
6
  "hybridly",
@@ -44,12 +44,11 @@
44
44
  "lodash.isequal": "^4.5.0",
45
45
  "nprogress": "^0.2.0",
46
46
  "qs": "^6.11.2",
47
- "@hybridly/core": "0.4.3",
48
- "@hybridly/progress-plugin": "0.4.3",
49
- "@hybridly/utils": "0.4.3"
47
+ "@hybridly/core": "0.4.5",
48
+ "@hybridly/utils": "0.4.5"
50
49
  },
51
50
  "devDependencies": {
52
- "@types/lodash": "^4.14.197",
51
+ "@types/lodash": "^4.14.198",
53
52
  "@types/lodash.clonedeep": "^4.5.7",
54
53
  "@types/lodash.isequal": "^4.5.6",
55
54
  "@types/nprogress": "^0.2.0",