@signal24/vue-foundation 3.8.1 → 4.1.0

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.
Files changed (127) hide show
  1. package/.eslintrc.cjs +35 -0
  2. package/.prettierrc.json +4 -2
  3. package/dist/src/components/ajax-select.vue.d.ts +21 -0
  4. package/dist/src/components/alert-helpers.d.ts +8 -0
  5. package/dist/src/components/alert-modal.vue.d.ts +27 -0
  6. package/dist/src/components/ez-smart-select.vue.d.ts +27 -0
  7. package/dist/src/components/index.d.ts +8 -0
  8. package/dist/src/components/modal-container.d.ts +33 -0
  9. package/dist/src/components/modal.vue.d.ts +34 -0
  10. package/dist/src/components/smart-select.vue.d.ts +121 -0
  11. package/dist/src/config.d.ts +8 -0
  12. package/dist/src/directives/autofocus.d.ts +2 -0
  13. package/dist/src/directives/confirm-button.d.ts +2 -0
  14. package/dist/src/directives/date-input.d.ts +2 -0
  15. package/dist/src/directives/datetime.d.ts +2 -0
  16. package/dist/src/directives/disabled.d.ts +2 -0
  17. package/dist/src/directives/duration.d.ts +2 -0
  18. package/dist/src/directives/index.d.ts +24 -0
  19. package/dist/src/directives/infinite-scroll.d.ts +3 -0
  20. package/dist/src/directives/readonly.d.ts +2 -0
  21. package/dist/src/directives/tooltip.d.ts +41 -0
  22. package/dist/src/filters/index.d.ts +39 -0
  23. package/dist/src/helpers/array.d.ts +3 -0
  24. package/dist/src/helpers/context-menu.d.ts +13 -0
  25. package/dist/src/helpers/delay.d.ts +2 -0
  26. package/dist/src/helpers/error.d.ts +7 -0
  27. package/dist/src/helpers/index.d.ts +9 -0
  28. package/dist/src/helpers/mask.d.ts +15 -0
  29. package/dist/src/helpers/number.d.ts +1 -0
  30. package/dist/src/helpers/object.d.ts +2 -0
  31. package/dist/src/helpers/openapi.d.ts +34 -0
  32. package/dist/src/helpers/string.d.ts +5 -0
  33. package/dist/src/hooks/index.d.ts +2 -0
  34. package/dist/src/hooks/infinite-scroll.d.ts +30 -0
  35. package/dist/src/hooks/resize-watcher.d.ts +1 -0
  36. package/dist/src/index.d.ts +8 -0
  37. package/dist/src/types.d.ts +14 -0
  38. package/dist/src/vite-plugins/index.d.ts +1 -0
  39. package/dist/src/vite-plugins/index.js +2 -0
  40. package/dist/src/vite-plugins/vite-openapi-plugin.cli.d.ts +2 -0
  41. package/dist/src/vite-plugins/vite-openapi-plugin.cli.js +10 -0
  42. package/dist/src/vite-plugins/vite-openapi-plugin.d.ts +5 -0
  43. package/dist/src/vite-plugins/vite-openapi-plugin.js +58 -0
  44. package/dist/vue-foundation.css +1 -1
  45. package/dist/vue-foundation.es.js +880 -1880
  46. package/package.json +47 -16
  47. package/src/components/ajax-select.vue +44 -23
  48. package/src/components/alert-helpers.ts +45 -0
  49. package/src/components/alert-modal.vue +68 -0
  50. package/src/components/ez-smart-select.vue +51 -0
  51. package/src/components/index.ts +10 -0
  52. package/src/components/modal-container.ts +131 -0
  53. package/src/components/modal.vue +44 -129
  54. package/src/components/smart-select.vue +196 -243
  55. package/src/config.ts +15 -0
  56. package/src/directives/autofocus.ts +20 -0
  57. package/src/directives/confirm-button.ts +50 -0
  58. package/src/directives/date-input.ts +19 -0
  59. package/src/directives/datetime.ts +48 -0
  60. package/src/directives/disabled.ts +30 -0
  61. package/src/directives/duration.ts +79 -0
  62. package/src/directives/index.ts +37 -0
  63. package/src/directives/infinite-scroll.ts +9 -0
  64. package/src/directives/readonly.ts +15 -0
  65. package/src/directives/tooltip.ts +190 -0
  66. package/src/filters/index.ts +79 -0
  67. package/src/helpers/array.ts +7 -0
  68. package/src/helpers/context-menu.ts +108 -0
  69. package/src/helpers/delay.ts +2 -0
  70. package/src/helpers/error.ts +41 -0
  71. package/src/helpers/index.ts +9 -0
  72. package/src/helpers/mask.ts +105 -0
  73. package/src/helpers/number.ts +3 -0
  74. package/src/helpers/object.ts +19 -0
  75. package/src/helpers/openapi.ts +82 -0
  76. package/src/helpers/string.ts +27 -0
  77. package/src/hooks/index.ts +2 -0
  78. package/src/hooks/infinite-scroll.ts +107 -0
  79. package/src/hooks/resize-watcher.ts +8 -0
  80. package/src/index.ts +14 -0
  81. package/src/types.ts +14 -0
  82. package/src/vite-plugins/index.ts +2 -0
  83. package/src/vite-plugins/vite-openapi-plugin.cli.ts +15 -0
  84. package/src/vite-plugins/vite-openapi-plugin.ts +71 -0
  85. package/tsconfig.app.json +22 -0
  86. package/tsconfig.json +14 -0
  87. package/tsconfig.node.json +9 -0
  88. package/tsconfig.vite-plugins.json +10 -0
  89. package/tsconfig.vitest.json +9 -0
  90. package/vite.config.js +37 -35
  91. package/vitest.config.js +17 -0
  92. package/.eslintrc.js +0 -16
  93. package/CHANGES.md +0 -2
  94. package/dist/vue-foundation.cjs.js +0 -5
  95. package/dist/vue-foundation.umd.js +0 -6
  96. package/postcss.config.cjs +0 -5
  97. package/src/app.js +0 -25
  98. package/src/components/alert.vue +0 -130
  99. package/src/components/index.js +0 -12
  100. package/src/config.js +0 -11
  101. package/src/directives/autofocus.js +0 -17
  102. package/src/directives/confirm-button.js +0 -40
  103. package/src/directives/date-input.js +0 -18
  104. package/src/directives/datetime.js +0 -46
  105. package/src/directives/disabled.js +0 -28
  106. package/src/directives/duration.js +0 -72
  107. package/src/directives/index.js +0 -10
  108. package/src/directives/infinite-scroll.js +0 -17
  109. package/src/directives/readonly.js +0 -17
  110. package/src/directives/tooltip.js +0 -178
  111. package/src/directives/user-text.js +0 -11
  112. package/src/filters/index.js +0 -82
  113. package/src/helpers/array.js +0 -99
  114. package/src/helpers/context-menu.js +0 -66
  115. package/src/helpers/delay.js +0 -3
  116. package/src/helpers/error.js +0 -36
  117. package/src/helpers/http.js +0 -44
  118. package/src/helpers/index.js +0 -9
  119. package/src/helpers/mask.js +0 -90
  120. package/src/helpers/number.js +0 -6
  121. package/src/helpers/string.js +0 -36
  122. package/src/helpers/vue.js +0 -5
  123. package/src/index.js +0 -33
  124. package/src/plugins/index.js +0 -10
  125. package/src/plugins/infinite-scroll/hook.js +0 -30
  126. package/src/plugins/infinite-scroll.js +0 -100
  127. package/src/plugins/resize-watcher.js +0 -28
@@ -0,0 +1,108 @@
1
+ interface ContextMenuItem {
2
+ title: string;
3
+ handler: () => void;
4
+ class?: string;
5
+ shouldConfirm?: boolean;
6
+ }
7
+
8
+ interface ContextMenuConfig {
9
+ targetClass?: string;
10
+ class?: string;
11
+ items: (ContextMenuItem | '-')[];
12
+ }
13
+
14
+ export function showContextMenu(e: MouseEvent, config: ContextMenuConfig) {
15
+ const wrapperEl = document.createElement('div');
16
+ wrapperEl.classList.add('vf-overlay');
17
+ wrapperEl.addEventListener('click', closeMenu);
18
+ document.body.appendChild(wrapperEl);
19
+
20
+ const menuEl = document.createElement('div');
21
+ menuEl.classList.add('vf-context-menu');
22
+ menuEl.style.position = 'absolute';
23
+ wrapperEl.appendChild(menuEl);
24
+
25
+ const target = e.currentTarget as HTMLElement;
26
+ target.style.userSelect = 'none';
27
+ target.classList.add('context-menu-active');
28
+
29
+ if (config.targetClass) {
30
+ target.classList.add(config.targetClass);
31
+ }
32
+
33
+ if (config.class) {
34
+ menuEl.classList.add(config.class);
35
+ }
36
+
37
+ config.items.forEach(item => {
38
+ if (item == '-') {
39
+ const separatorEl = document.createElement('div');
40
+ separatorEl.classList.add('separator');
41
+ menuEl.appendChild(separatorEl);
42
+ return;
43
+ }
44
+
45
+ const itemEl = document.createElement('div');
46
+ itemEl.classList.add('item');
47
+ itemEl.style.userSelect = 'none';
48
+ itemEl.innerText = item.title;
49
+ menuEl.appendChild(itemEl);
50
+
51
+ if (item.class) {
52
+ itemEl.classList.add(item.class);
53
+ }
54
+
55
+ if (item.shouldConfirm) {
56
+ itemEl.addEventListener('click', () => item.handler());
57
+ } else {
58
+ itemEl.addEventListener('click', () => confirmAction(itemEl, item.handler));
59
+ }
60
+ });
61
+
62
+ const dx = window.innerWidth - e.clientX;
63
+ const dy = window.innerHeight - e.clientY;
64
+ const menuHeight = menuEl.offsetHeight;
65
+ const menuWidth = menuEl.offsetWidth;
66
+
67
+ const left = dx < menuWidth ? e.clientX - menuWidth - 1 : e.clientX + 1;
68
+ const top = dy < menuHeight ? e.clientY - menuHeight - 1 : e.clientY + 1;
69
+
70
+ menuEl.style.left = left + 'px';
71
+ menuEl.style.top = top + 'px';
72
+
73
+ setTimeout(() => {
74
+ menuEl.style.width = menuEl.offsetWidth + 'px';
75
+ }, 50);
76
+
77
+ function closeMenu() {
78
+ if (config.targetClass) {
79
+ target.classList.remove(config.targetClass);
80
+ }
81
+
82
+ target.classList.remove('context-menu-active');
83
+ target.style.userSelect = '';
84
+
85
+ wrapperEl.remove();
86
+ }
87
+
88
+ function confirmAction(itemEl: HTMLElement, handler: () => void) {
89
+ if (itemEl.classList.contains('pending-confirm')) {
90
+ return handler();
91
+ }
92
+
93
+ const originalContent = itemEl.innerHTML;
94
+ itemEl.classList.add('pending-confirm');
95
+ itemEl.innerText = 'Confirm';
96
+
97
+ const onMouseLeave = () => {
98
+ itemEl.classList.remove('pending-confirm');
99
+ itemEl.innerHTML = originalContent;
100
+ itemEl.removeEventListener('mouseleave', onMouseLeave);
101
+ };
102
+
103
+ itemEl.addEventListener('mouseleave', onMouseLeave);
104
+ e.stopPropagation();
105
+ }
106
+ }
107
+
108
+ // TODO: actually de-select text rather than just using CSS to hide its selection
@@ -0,0 +1,2 @@
1
+ export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
2
+ export const sleepSecs = (secs: number) => sleep(secs * 1000);
@@ -0,0 +1,41 @@
1
+ import { showAlert } from '@/components/alert-helpers';
2
+
3
+ import { VfOptions } from '../config';
4
+
5
+ export class UserError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = 'UserError';
9
+ }
10
+ }
11
+
12
+ export function formatError(err: any): string {
13
+ if (err instanceof UserError) {
14
+ return err.message;
15
+ }
16
+
17
+ const errMessage = toError(err).message;
18
+ return `An application error has occurred:\n\n${errMessage}\n\nPlease refresh the page and try again. If this error persists, ${VfOptions.unhandledErrorSupportText}.`;
19
+ }
20
+
21
+ export function toError(err: any) {
22
+ return err instanceof Error ? err : new Error(String(err));
23
+ }
24
+
25
+ export async function handleErrorAndAlert(errIn: any, alertTitle?: string) {
26
+ const err = toError(errIn);
27
+
28
+ if (!(err instanceof UserError)) {
29
+ VfOptions.errorHandler(err);
30
+ }
31
+
32
+ return alertTitle ? showAlert(alertTitle, err) : showAlert(err);
33
+ }
34
+
35
+ export async function handleError(errIn: any) {
36
+ const err = toError(errIn);
37
+
38
+ if (!(err instanceof UserError)) {
39
+ VfOptions.errorHandler(err);
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ export * from './array';
2
+ export * from './context-menu';
3
+ export * from './delay';
4
+ export * from './error';
5
+ export * from './mask';
6
+ export * from './number';
7
+ export * from './object';
8
+ export * from './openapi';
9
+ export * from './string';
@@ -0,0 +1,105 @@
1
+ import type { AnyComponentPublicInstance } from '@/components';
2
+
3
+ /*///////////////////////////////////////////////
4
+ Component Overlay Masking
5
+ //////////////////////////////////////////////*/
6
+ const MaskState = Symbol('MaskState');
7
+ interface IMaskState {
8
+ [MaskState]?: {
9
+ maskEl: HTMLElement;
10
+ };
11
+ }
12
+ type MaskElement = Element & IMaskState;
13
+
14
+ export function maskComponent(cmp: AnyComponentPublicInstance, message?: string) {
15
+ const el = cmp.$.vnode.el;
16
+ const modalParentlEl = el!.closest('.vf-modal');
17
+ return maskEl(modalParentlEl ?? el, message);
18
+ }
19
+
20
+ export function unmaskComponent(cmp: AnyComponentPublicInstance) {
21
+ const el = cmp.$.vnode.el;
22
+ const modalParentlEl = el!.closest('.vf-modal');
23
+ return unmaskEl(modalParentlEl ?? el);
24
+ }
25
+
26
+ export function maskEl(el: MaskElement, message?: string) {
27
+ if (!el[MaskState]) {
28
+ const maskEl = document.createElement('div');
29
+ maskEl.classList.add('vf-mask');
30
+ el.appendChild(maskEl);
31
+ el[MaskState] = { maskEl };
32
+ }
33
+
34
+ el[MaskState].maskEl.innerText = message ?? '';
35
+
36
+ // todo: add inner HTML to config
37
+
38
+ return () => unmaskEl(el);
39
+ }
40
+
41
+ export function unmaskEl(el: MaskElement) {
42
+ if (!el[MaskState]) return;
43
+ el.removeChild(el[MaskState].maskEl);
44
+ }
45
+
46
+ /*///////////////////////////////////////////////
47
+ Form Masking
48
+ //////////////////////////////////////////////*/
49
+ const FormMaskState = Symbol('FormMaskState');
50
+ interface IFormMaskState {
51
+ [FormMaskState]?: {
52
+ disabledElements: HTMLElement[];
53
+ waitButton: HTMLElement;
54
+ buttonText: string;
55
+ };
56
+ }
57
+ type FormMaskElement = Element & IFormMaskState;
58
+
59
+ export function maskForm(formOrCmp: Element | AnyComponentPublicInstance, buttonSelector?: string | Element, buttonText?: string) {
60
+ const form = formOrCmp instanceof Element ? formOrCmp : getFormFromCmp(formOrCmp);
61
+ form.classList.add('vf-masked');
62
+
63
+ const buttonEl = (
64
+ buttonSelector instanceof Element ? buttonSelector : form.querySelectorAll(buttonSelector ?? 'button:not([disabled])')[0]
65
+ ) as HTMLElement;
66
+ const originalButtonText = buttonEl.tagName === 'INPUT' ? (buttonEl as HTMLInputElement).value : buttonEl.innerText;
67
+ buttonEl.setAttribute('disabled', 'disabled');
68
+ buttonEl.innerText = buttonText ?? 'Please wait...';
69
+
70
+ const inputsQR = form.querySelectorAll('input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])');
71
+ const inputs = [...inputsQR] as HTMLElement[];
72
+ inputs.forEach(el => el.setAttribute('disabled', 'disabled'));
73
+
74
+ (form as FormMaskElement)[FormMaskState] = {
75
+ disabledElements: inputs,
76
+ waitButton: buttonEl,
77
+ buttonText: originalButtonText
78
+ };
79
+
80
+ return () => unmaskForm(form);
81
+ }
82
+
83
+ export function unmaskForm(formOrCmp: Element | AnyComponentPublicInstance) {
84
+ const form = formOrCmp instanceof Element ? formOrCmp : getFormFromCmp(formOrCmp);
85
+
86
+ const state = (form as FormMaskElement)[FormMaskState];
87
+ if (!state) return;
88
+
89
+ form.classList.remove('vf-masked');
90
+
91
+ state.disabledElements.forEach(el => el.removeAttribute('disabled'));
92
+ state.waitButton.innerText = state.buttonText;
93
+ state.waitButton.removeAttribute('disabled');
94
+
95
+ delete (form as FormMaskElement)[FormMaskState];
96
+ }
97
+
98
+ function getFormFromCmp(cmp: AnyComponentPublicInstance) {
99
+ const cmpEl = cmp.$.vnode.el!;
100
+ if (cmpEl.tagName === 'FORM') {
101
+ return cmpEl as HTMLElement;
102
+ } else {
103
+ return cmpEl.querySelector('form') as HTMLElement;
104
+ }
105
+ }
@@ -0,0 +1,3 @@
1
+ export function formatNumber(value: number) {
2
+ return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
3
+ }
@@ -0,0 +1,19 @@
1
+ import { cloneDeep } from 'lodash';
2
+
3
+ export function cloneProp<T>(prop: T | undefined | null, fallback: T): T {
4
+ if (prop !== undefined && prop !== null) {
5
+ return cloneDeep(prop);
6
+ } else {
7
+ return fallback;
8
+ }
9
+ }
10
+
11
+ export function nullifyEmptyInputs<T extends Record<string, unknown>, K extends keyof T>(obj: T, fields: K[]): T {
12
+ const result = { ...obj };
13
+ for (const key of fields) {
14
+ if (result[key] === '') {
15
+ result[key] = null as any;
16
+ }
17
+ }
18
+ return result;
19
+ }
@@ -0,0 +1,82 @@
1
+ import { UserError } from './error';
2
+
3
+ interface IRequestOptions {
4
+ readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
5
+ readonly url: string;
6
+ readonly path?: Record<string, any>;
7
+ readonly cookies?: Record<string, any>;
8
+ readonly headers?: Record<string, any>;
9
+ readonly query?: Record<string, any>;
10
+ readonly formData?: Record<string, any>;
11
+ readonly body?: any;
12
+ readonly mediaType?: string;
13
+ readonly responseHeader?: string;
14
+ readonly errors?: Record<number, string>;
15
+ }
16
+
17
+ interface IBaseHttpRequest {
18
+ request<T>(options: IRequestOptions): ICancelablePromise<T>;
19
+ }
20
+
21
+ interface IApiClient {
22
+ request: IBaseHttpRequest;
23
+ }
24
+
25
+ interface IApiError extends Error {
26
+ status: number;
27
+ statusText: string;
28
+ body: any;
29
+ }
30
+
31
+ declare class ICancelablePromise<T = any> {
32
+ constructor(executor: (resolve: (value: any) => void, reject: (reason: any) => void, onCancel: (cancel: () => void) => void) => void);
33
+ then<TResult1 = any, TResult2 = never>(
34
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
35
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
36
+ ): Promise<TResult1 | TResult2>;
37
+ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
38
+ finally(onfinally?: (() => void) | undefined | null): Promise<T>;
39
+ cancel(): void;
40
+ }
41
+
42
+ interface IWrappedApiClientOptions<P extends ICancelablePromise = ICancelablePromise, Arguments extends unknown[] = any[]> {
43
+ apiClient: IApiClient;
44
+ onRequest?: (options: IRequestOptions) => IRequestOptions;
45
+ onError?: (err: Error, options: IRequestOptions) => Error | null | void;
46
+ CancelablePromise: new (...arguments_: Arguments) => P;
47
+ }
48
+
49
+ function isApiError(err: any): err is IApiError {
50
+ return err instanceof Error && 'status' in err && 'body' in err;
51
+ }
52
+
53
+ export function installApiClientInterceptors({ apiClient, onRequest, onError, CancelablePromise }: IWrappedApiClientOptions) {
54
+ const originalRequest = apiClient.request.request.bind(apiClient.request);
55
+ apiClient.request.request = (options: IRequestOptions) => {
56
+ if (onRequest) {
57
+ options = onRequest(options);
58
+ }
59
+
60
+ return new CancelablePromise((resolve: (value: any) => void, reject: (err: any) => void, onCancel: (handler: () => void) => void) => {
61
+ const promise = originalRequest(options);
62
+ onCancel(promise.cancel);
63
+ promise.then(resolve).catch(err => {
64
+ if (isApiError(err) && typeof err.body === 'object' && 'error' in err.body) {
65
+ if (err.status === 422) {
66
+ return reject(new UserError(err.body.error));
67
+ }
68
+ }
69
+ if (onError) {
70
+ const handlerResult = onError(err, options);
71
+ if (handlerResult === null) {
72
+ return;
73
+ }
74
+ if (handlerResult instanceof Error) {
75
+ return reject(handlerResult);
76
+ }
77
+ }
78
+ reject(err);
79
+ });
80
+ });
81
+ };
82
+ }
@@ -0,0 +1,27 @@
1
+ // placing this here so we don't have to use the ESLint rule everywhere
2
+ // eslint-disable-next-line vue/prefer-import-from-vue
3
+ export { escapeHtml } from '@vue/shared';
4
+
5
+ export function nl2br(value: string) {
6
+ return value.replace(/\n/g, '<br>');
7
+ }
8
+
9
+ export function desnakeCase(value: string) {
10
+ return value.replace(/_/g, ' ');
11
+ }
12
+
13
+ export function formatPhone(value: string) {
14
+ const cleanValue = value.replace(/\D/g, '').replace(/^1/, '');
15
+ if (cleanValue.length != 10) return value;
16
+ return '(' + cleanValue.substring(0, 3) + ') ' + cleanValue.substring(3, 6) + '-' + cleanValue.substring(6);
17
+ }
18
+
19
+ export function formatUSCurrency(value: string | number) {
20
+ return (
21
+ '$' +
22
+ Number(value)
23
+ .toFixed(3)
24
+ .replace(/0$/, '')
25
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
26
+ );
27
+ }
@@ -0,0 +1,2 @@
1
+ export { useInfiniteScroll } from './infinite-scroll';
2
+ export { useResizeWatcher } from './resize-watcher';
@@ -0,0 +1,107 @@
1
+ import { type ComponentInternalInstance, getCurrentInstance, onActivated, onBeforeUnmount, onDeactivated, onMounted } from 'vue';
2
+
3
+ const HookState = Symbol('HookState');
4
+ interface IHookState {
5
+ el?: InfiniteScrollHandler;
6
+ ancestor?: InfiniteScrollHandler;
7
+ window?: InfiniteScrollHandler;
8
+ }
9
+ type InfiniteScrollComponent = ComponentInternalInstance & { [HookState]?: IHookState };
10
+
11
+ export interface IInfiniteScrollOptions {
12
+ elScrolledToBottom?: () => void;
13
+ ancestorScrolledToBottom?: () => void;
14
+ windowScrolledToBottom?: () => void;
15
+ }
16
+
17
+ export function useInfiniteScroll(options: IInfiniteScrollOptions, instance?: ComponentInternalInstance) {
18
+ const resolvedInstance = instance ?? getCurrentInstance()!;
19
+ onMounted(() => installScrollHook(resolvedInstance, options), resolvedInstance);
20
+ onActivated(() => reinstallScrollHook(resolvedInstance), resolvedInstance);
21
+ onDeactivated(() => uninstallScrollHook(resolvedInstance), resolvedInstance);
22
+ onBeforeUnmount(() => uninstallScrollHook(resolvedInstance), resolvedInstance);
23
+ }
24
+
25
+ export function installScrollHook(cmp: InfiniteScrollComponent, options: IInfiniteScrollOptions) {
26
+ const hookState: IHookState = {};
27
+
28
+ if (options.elScrolledToBottom) {
29
+ hookState.el = new InfiniteScrollHandler(cmp.vnode.el as Element, options.elScrolledToBottom);
30
+ hookState.el.install();
31
+ }
32
+
33
+ if (options.ancestorScrolledToBottom) {
34
+ const scrollableAncestorEl = discoverScrollableAncestorEl(cmp.vnode.el as Element);
35
+ if (scrollableAncestorEl) {
36
+ hookState.ancestor = new InfiniteScrollHandler(scrollableAncestorEl, options.ancestorScrolledToBottom);
37
+ hookState.ancestor.install();
38
+ } else {
39
+ console.warn('no scollable ancestor found for component:', cmp);
40
+ }
41
+ }
42
+
43
+ if (options.windowScrolledToBottom) {
44
+ hookState.window = new InfiniteScrollHandler(window as unknown as Element, options.windowScrolledToBottom);
45
+ hookState.window.install();
46
+ }
47
+
48
+ cmp[HookState] = hookState;
49
+ }
50
+
51
+ export function reinstallScrollHook(cmp: InfiniteScrollComponent) {
52
+ const hookState = cmp[HookState];
53
+ hookState?.el?.install();
54
+ hookState?.ancestor?.install();
55
+ hookState?.window?.install();
56
+ }
57
+
58
+ export function uninstallScrollHook(cmp: InfiniteScrollComponent) {
59
+ const hookState = cmp[HookState];
60
+ hookState?.el?.uninstall();
61
+ hookState?.ancestor?.uninstall();
62
+ hookState?.window?.uninstall();
63
+ }
64
+
65
+ const ScrollableOverflowValues = ['auto', 'scroll'];
66
+ function discoverScrollableAncestorEl(el: Element): Element | null {
67
+ const parent = el.parentElement;
68
+ if (!parent) return null;
69
+
70
+ const computedStyle = window.getComputedStyle(parent);
71
+ if (
72
+ ScrollableOverflowValues.includes(computedStyle.overflow) ||
73
+ ScrollableOverflowValues.includes(computedStyle.overflowX) ||
74
+ ScrollableOverflowValues.includes(computedStyle.overflowY)
75
+ ) {
76
+ return parent;
77
+ }
78
+
79
+ return discoverScrollableAncestorEl(parent);
80
+ }
81
+
82
+ // TODO: switch to intersection observer
83
+ export class InfiniteScrollHandler {
84
+ isTripped = false;
85
+
86
+ constructor(private el: Element, private handler: (e: Event) => void) {}
87
+
88
+ install() {
89
+ this.el.addEventListener('scroll', this.onScrollWithContext);
90
+ }
91
+
92
+ uninstall() {
93
+ this.el.removeEventListener('scroll', this.onScrollWithContext);
94
+ }
95
+
96
+ onScrollWithContext = this.onScroll.bind(this);
97
+ onScroll(e: Event) {
98
+ if (Math.ceil(this.el.scrollTop + this.el.clientHeight + 5) >= this.el.scrollHeight) {
99
+ if (!this.isTripped) {
100
+ this.handler(e);
101
+ this.isTripped = true;
102
+ }
103
+ } else if (this.isTripped) {
104
+ this.isTripped = false;
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,8 @@
1
+ import { onActivated, onBeforeUnmount, onDeactivated, onMounted } from 'vue';
2
+
3
+ export function useResizeWatcher(fn: () => void) {
4
+ onMounted(() => window.addEventListener('resize', fn));
5
+ onActivated(() => window.addEventListener('resize', fn));
6
+ onDeactivated(() => window.removeEventListener('resize', fn));
7
+ onBeforeUnmount(() => window.removeEventListener('resize', fn));
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { App } from 'vue';
2
+
3
+ export * from './components';
4
+ export { configureVf } from './config';
5
+ export * from './filters';
6
+ export * from './helpers';
7
+ export * from './hooks';
8
+ export * from './types';
9
+
10
+ import { registerDirectives } from './directives';
11
+
12
+ export function installVf(app: App) {
13
+ registerDirectives(app);
14
+ }
package/src/types.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { OptionalKeysOf, RequiredKeysOf } from 'type-fest';
2
+
3
+ export type Branded<T, A> = A & { __brand: T };
4
+ export type BrandOf<A> = [A] extends [Branded<infer R, unknown>] ? R : never;
5
+ export type Debrand<A> = A extends Branded<BrandOf<A>, infer R> ? R : never;
6
+
7
+ export type PickRequired<T> = T extends object ? Pick<T, RequiredKeysOf<T>> : never;
8
+ export type PickOptional<T> = T extends object ? Pick<T, OptionalKeysOf<T>> : never;
9
+
10
+ export type WithoutNever<T> = { [P in keyof T as T[P] extends never ? never : P]: T[P] };
11
+
12
+ export type UnwrapBrand<T, B> = WithoutNever<{
13
+ [K in keyof T]: BrandOf<T[K]> extends B ? Debrand<T[K]> : never;
14
+ }>;
@@ -0,0 +1,2 @@
1
+ // todo: dunno why this needs .JS, but Vite panics with ERR_MODULE_NOT_FOUND if we don't
2
+ export * from './vite-openapi-plugin.js';
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from 'fs';
4
+
5
+ import { generateOpenapiClient } from './vite-openapi-plugin.js';
6
+
7
+ if (!process.argv[2]) {
8
+ throw new Error('Usage: vf-generate-openapi-client <openapi-yaml-path>');
9
+ }
10
+
11
+ if (!existsSync(process.argv[2])) {
12
+ throw new Error(`OpenAPI YAML file not found: ${process.argv[2]}`);
13
+ }
14
+
15
+ await generateOpenapiClient(process.argv[2]);
@@ -0,0 +1,71 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, watch } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+
5
+ import * as OpenAPI from 'openapi-typescript-codegen';
6
+
7
+ let generatedHash: string | null = null;
8
+
9
+ export function openapiClientGeneratorPlugin(openapiYamlPath: string) {
10
+ const generator = getGenerator(openapiYamlPath);
11
+
12
+ return {
13
+ name: 'openapi-types-generator',
14
+
15
+ closeBundle() {
16
+ generator?.close();
17
+ }
18
+ };
19
+ }
20
+
21
+ function getGenerator(openapiYamlPath: string) {
22
+ if (!existsSync(openapiYamlPath)) {
23
+ console.log(`OpenAPI YAML file not found: ${openapiYamlPath}`);
24
+ return null;
25
+ }
26
+
27
+ const watcher = watch(openapiYamlPath);
28
+ watcher.on('change', () => {
29
+ // give the writes a moment to settle
30
+ setTimeout(() => generateOpenapiClient(openapiYamlPath), 100);
31
+ });
32
+
33
+ generateOpenapiClient(openapiYamlPath);
34
+
35
+ return {
36
+ close() {
37
+ watcher.close();
38
+ }
39
+ };
40
+ }
41
+
42
+ export async function generateOpenapiClient(openapiYamlPath: string) {
43
+ const yaml = readFileSync(openapiYamlPath, 'utf8');
44
+ const hash = createHash('sha256').update(yaml).digest('hex');
45
+
46
+ if (hash === generatedHash) {
47
+ return;
48
+ }
49
+
50
+ generatedHash = hash;
51
+
52
+ try {
53
+ try {
54
+ await rm('./src/openapi-client-generated', { recursive: true });
55
+ } catch (e) {
56
+ // ignore
57
+ }
58
+
59
+ await OpenAPI.generate({
60
+ input: openapiYamlPath,
61
+ output: './src/openapi-client-generated',
62
+ clientName: 'ApiClient',
63
+ useOptions: true,
64
+ useUnionTypes: true
65
+ });
66
+
67
+ console.log(`[${new Date().toISOString()}] Generated client from ${openapiYamlPath} to ./src/openapi-client/`);
68
+ } catch (err) {
69
+ console.error(`[${new Date().toISOString()}] Error generating client from ${openapiYamlPath}:`, err);
70
+ }
71
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.web.json",
3
+ "include": ["src/**/*.ts", "src/**/*.vue"],
4
+ "exclude": ["src/**/__tests__/*", "src/vite-plugins/*"],
5
+ "compilerOptions": {
6
+ "composite": true,
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@/*": ["./src/*"]
10
+ },
11
+ "noImplicitAny": true,
12
+ "outDir": "dist",
13
+ "declaration": true,
14
+ "declarationDir": "dist",
15
+ "target": "ES2020",
16
+ "lib": [
17
+ "ES2020",
18
+ "DOM",
19
+ "DOM.Iterable"
20
+ ]
21
+ }
22
+ }