routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+1050
File diff suppressed because it is too large
Load Diff
+234
@@ -0,0 +1,234 @@
|
||||
import { Fragment as _Fragment, createVNode as _createVNode, mergeProps as _mergeProps, createElementVNode as _createElementVNode } from "vue";
|
||||
// Components
|
||||
import { VBtn } from "../VBtn/index.js";
|
||||
import { VDefaultsProvider } from "../VDefaultsProvider/index.js";
|
||||
import { makeVSnackbarProps, VSnackbar } from "../VSnackbar/VSnackbar.js"; // Composables
|
||||
import { useSnackbarQueue } from "./queue.js";
|
||||
import { useDelay } from "../../composables/delay.js";
|
||||
import { useDocumentVisibility } from "../../composables/documentVisibility.js";
|
||||
import { useLocale } from "../../composables/locale.js"; // Utilities
|
||||
import { computed, mergeProps, ref, shallowRef, toRef, triggerRef, watch } from 'vue';
|
||||
import { genericComponent, omit, propsFactory, useRender } from "../../util/index.js"; // Types
|
||||
export const makeVSnackbarQueueProps = propsFactory({
|
||||
// TODO: Port this to Snackbar on dev
|
||||
closable: [Boolean, String],
|
||||
closeText: {
|
||||
type: String,
|
||||
default: '$vuetify.dismiss'
|
||||
},
|
||||
collapsed: Boolean,
|
||||
displayStrategy: {
|
||||
type: String,
|
||||
default: 'hold'
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
totalVisible: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
gap: {
|
||||
type: [Number, String],
|
||||
default: 8
|
||||
},
|
||||
...omit(makeVSnackbarProps(), ['modelValue', 'collapsed', 'queueIndex', 'queueGap'])
|
||||
}, 'VSnackbarQueue');
|
||||
export const VSnackbarQueue = genericComponent()({
|
||||
name: 'VSnackbarQueue',
|
||||
inheritAttrs: false,
|
||||
props: makeVSnackbarQueueProps(),
|
||||
emits: {
|
||||
'update:modelValue': val => true
|
||||
},
|
||||
setup(props, {
|
||||
attrs,
|
||||
emit,
|
||||
slots
|
||||
}) {
|
||||
const {
|
||||
t
|
||||
} = useLocale();
|
||||
const documentVisibility = useDocumentVisibility();
|
||||
const queue = useSnackbarQueue(props);
|
||||
const isHovered = shallowRef(false);
|
||||
const {
|
||||
runOpenDelay,
|
||||
runCloseDelay
|
||||
} = useDelay({
|
||||
openDelay: 0,
|
||||
closeDelay: 500
|
||||
}, val => {
|
||||
isHovered.value = val;
|
||||
updateDynamicProps();
|
||||
});
|
||||
let _lastId = 0;
|
||||
const visibleItems = ref([]);
|
||||
const limit = toRef(() => Number(props.totalVisible));
|
||||
watch(() => props.modelValue.length, showNext);
|
||||
function removeItem(id) {
|
||||
visibleItems.value = visibleItems.value.filter(x => x.id !== id);
|
||||
if (visibleItems.value.length === 0) {
|
||||
isHovered.value = false;
|
||||
}
|
||||
showNext();
|
||||
}
|
||||
function showNext() {
|
||||
if (!props.modelValue.length) return;
|
||||
const activeCount = visibleItems.value.filter(x => x.active).length;
|
||||
if (activeCount >= limit.value) {
|
||||
if (props.displayStrategy !== 'overflow') return;
|
||||
|
||||
// Dismiss oldest active items to make room
|
||||
visibleItems.value.filter(x => x.active).slice(limit.value - 1).forEach(item => {
|
||||
item.active = false;
|
||||
item.onDismiss?.('overflow');
|
||||
});
|
||||
}
|
||||
const [next, ...rest] = props.modelValue;
|
||||
emit('update:modelValue', rest);
|
||||
const item = typeof next === 'string' ? {
|
||||
text: next
|
||||
} : next;
|
||||
const {
|
||||
promise,
|
||||
success,
|
||||
error,
|
||||
onDismiss,
|
||||
...itemProps
|
||||
} = item;
|
||||
const newItem = {
|
||||
id: _lastId++,
|
||||
item: {
|
||||
...(promise ? {
|
||||
timeout: -1,
|
||||
loading: true
|
||||
} : {}),
|
||||
...itemProps
|
||||
},
|
||||
active: true,
|
||||
onDismiss
|
||||
};
|
||||
visibleItems.value.unshift(newItem);
|
||||
updateDynamicProps();
|
||||
promise?.then(data => {
|
||||
if (!newItem.active) return;
|
||||
newItem.item = success?.(data) ?? {
|
||||
...newItem.item,
|
||||
timeout: 1
|
||||
};
|
||||
updateDynamicProps();
|
||||
triggerRef(visibleItems);
|
||||
}, data => {
|
||||
if (!newItem.active) return;
|
||||
newItem.item = error?.(data) ?? {
|
||||
...newItem.item,
|
||||
timeout: 1
|
||||
};
|
||||
updateDynamicProps();
|
||||
triggerRef(visibleItems);
|
||||
});
|
||||
}
|
||||
function dismiss(id, reason) {
|
||||
const item = visibleItems.value.find(x => x.id === id);
|
||||
if (!item) return;
|
||||
item.active = false;
|
||||
item.onDismiss?.(reason);
|
||||
updateDynamicProps();
|
||||
}
|
||||
function clear() {
|
||||
emit('update:modelValue', []);
|
||||
visibleItems.value.toReversed().forEach((item, i) => setTimeout(() => {
|
||||
item.active = false;
|
||||
item.onDismiss?.('cleared');
|
||||
}, 100 * i));
|
||||
}
|
||||
const btnProps = computed(() => ({
|
||||
color: typeof props.closable === 'string' ? props.closable : undefined,
|
||||
text: t(props.closeText)
|
||||
}));
|
||||
function updateDynamicProps() {
|
||||
let activeIndex = 0;
|
||||
visibleItems.value.forEach(({
|
||||
item,
|
||||
active
|
||||
}) => {
|
||||
item.queueIndex = activeIndex;
|
||||
if (active) activeIndex++;
|
||||
});
|
||||
if (!props.collapsed || isHovered.value) {
|
||||
visibleItems.value.forEach(({
|
||||
item
|
||||
}) => item.collapsed = undefined);
|
||||
return;
|
||||
}
|
||||
for (const {
|
||||
item
|
||||
} of visibleItems.value) {
|
||||
item.collapsed = item.queueIndex > 0 ? {
|
||||
width: queue.lastItemSize.value.width,
|
||||
height: queue.lastItemSize.value.height
|
||||
} : undefined;
|
||||
}
|
||||
}
|
||||
watch(queue.lastItemSize, updateDynamicProps);
|
||||
watch(() => props.collapsed, updateDynamicProps);
|
||||
useRender(() => {
|
||||
const hasActions = !!(props.closable || slots.actions);
|
||||
const snackbarProps = omit(VSnackbar.filterProps(props), ['modelValue', 'collapsed']);
|
||||
const pauseAll = documentVisibility.value === 'hidden' || props.collapsed && isHovered.value;
|
||||
return _createElementVNode(_Fragment, null, [visibleItems.value.map(({
|
||||
id,
|
||||
item,
|
||||
active
|
||||
}) => slots.item ? _createVNode(VDefaultsProvider, {
|
||||
"defaults": {
|
||||
VSnackbar: item
|
||||
}
|
||||
}, {
|
||||
default: () => [slots.item({
|
||||
item
|
||||
})]
|
||||
}) : _createVNode(VSnackbar, _mergeProps({
|
||||
"key": id
|
||||
}, attrs, snackbarProps, item, pauseAll ? {
|
||||
timeout: -1
|
||||
} : {}, {
|
||||
"queueGap": Number(props.gap),
|
||||
"contentProps": mergeProps(snackbarProps.contentProps, {
|
||||
onMouseenter: runOpenDelay,
|
||||
onMouseleave: () => runCloseDelay()
|
||||
}),
|
||||
"modelValue": active,
|
||||
"onUpdate:modelValue": () => dismiss(id, 'auto'),
|
||||
"onAfterLeave": () => removeItem(id)
|
||||
}), {
|
||||
header: slots.header ? () => slots.header?.({
|
||||
item
|
||||
}) : undefined,
|
||||
text: slots.text ? () => slots.text?.({
|
||||
item
|
||||
}) : undefined,
|
||||
actions: hasActions ? () => _createElementVNode(_Fragment, null, [!slots.actions ? _createVNode(VBtn, _mergeProps(btnProps.value, {
|
||||
"onClick": () => dismiss(id, 'dismissed')
|
||||
}), null) : _createVNode(VDefaultsProvider, {
|
||||
"defaults": {
|
||||
VBtn: btnProps.value
|
||||
}
|
||||
}, {
|
||||
default: () => [slots.actions({
|
||||
item,
|
||||
props: {
|
||||
onClick: () => dismiss(id, 'dismissed')
|
||||
}
|
||||
})]
|
||||
})]) : undefined
|
||||
}))]);
|
||||
});
|
||||
return {
|
||||
clear
|
||||
};
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=VSnackbarQueue.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
export { VSnackbarQueue } from './VSnackbarQueue.js';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { VSnackbarQueue } from "./VSnackbarQueue.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","names":["VSnackbarQueue"],"sources":["../../../src/components/VSnackbarQueue/index.ts"],"sourcesContent":["export { VSnackbarQueue } from './VSnackbarQueue'\n"],"mappings":"SAASA,cAAc","ignoreList":[]}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import type { InjectionKey, Ref } from 'vue';
|
||||
export interface SnackbarQueueItemState {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
export interface SnackbarQueueProvide {
|
||||
register: (id: string) => void;
|
||||
unregister: (id: string) => void;
|
||||
setSize: (id: string, height: number, width: number) => void;
|
||||
getOffset: (id: string) => number | null;
|
||||
items: Ref<Map<string, SnackbarQueueItemState>>;
|
||||
gap: Ref<number>;
|
||||
lastItemSize: Ref<{
|
||||
height: number;
|
||||
width: number;
|
||||
}>;
|
||||
}
|
||||
export declare const VSnackbarQueueSymbol: InjectionKey<SnackbarQueueProvide>;
|
||||
export declare function useSnackbarQueue(props: {
|
||||
gap: string | number;
|
||||
}): SnackbarQueueProvide;
|
||||
export declare function useSnackbarItem(isActive: Ref<boolean>, contentEl: () => HTMLElement | undefined): {
|
||||
id: string;
|
||||
offset: import("vue").ComputedRef<number | null>;
|
||||
} | null;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Composables
|
||||
import { useResizeObserver } from "../../composables/resizeObserver.js"; // Utilities
|
||||
import { computed, inject, onBeforeUnmount, provide, ref, toRef, useId, watch } from 'vue';
|
||||
|
||||
// Types
|
||||
|
||||
export const VSnackbarQueueSymbol = Symbol.for('vuetify:v-snackbar-queue');
|
||||
export function useSnackbarQueue(props) {
|
||||
const items = ref(new Map());
|
||||
const gap = toRef(() => Number(props.gap));
|
||||
function register(id) {
|
||||
items.value.set(id, {
|
||||
height: 0,
|
||||
width: 0
|
||||
});
|
||||
}
|
||||
function unregister(id) {
|
||||
items.value.delete(id);
|
||||
}
|
||||
function setSize(id, height, width) {
|
||||
const item = items.value.get(id);
|
||||
if (!item || item.height === height && item.width === width) return;
|
||||
item.height = height;
|
||||
item.width = width;
|
||||
}
|
||||
const lastItemSize = computed(() => {
|
||||
for (const {
|
||||
width,
|
||||
height
|
||||
} of [...items.value.values()].toReversed()) {
|
||||
if (!width || !height) continue;
|
||||
return {
|
||||
width,
|
||||
height
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
});
|
||||
function getOffset(id) {
|
||||
if (!items.value.has(id)) return null;
|
||||
let offset = 0;
|
||||
for (const [itemId, state] of [...items.value.entries()].toReversed()) {
|
||||
if (itemId === id) break;
|
||||
offset += state.height + gap.value;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
const state = {
|
||||
register,
|
||||
unregister,
|
||||
setSize,
|
||||
getOffset,
|
||||
items,
|
||||
gap,
|
||||
lastItemSize
|
||||
};
|
||||
provide(VSnackbarQueueSymbol, state);
|
||||
return state;
|
||||
}
|
||||
export function useSnackbarItem(isActive, contentEl) {
|
||||
const queue = inject(VSnackbarQueueSymbol, null);
|
||||
if (!queue) return null;
|
||||
const id = useId();
|
||||
queue.register(id);
|
||||
onBeforeUnmount(() => queue.unregister(id));
|
||||
watch(isActive, val => !val && queue.unregister(id), {
|
||||
flush: 'sync'
|
||||
});
|
||||
const {
|
||||
resizeRef,
|
||||
contentRect
|
||||
} = useResizeObserver();
|
||||
watch(contentEl, el => {
|
||||
resizeRef.value = el ?? null;
|
||||
});
|
||||
watch(contentRect, rect => {
|
||||
if (rect?.width) queue.setSize(id, rect.height, rect.width);
|
||||
});
|
||||
const offset = computed(() => queue.getOffset(id));
|
||||
return {
|
||||
id,
|
||||
offset
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=queue.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user