@xy-planning-network/trees 0.4.0-rc-7 → 0.4.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/README.md +236 -53
- package/dist/trees.es.js +1069 -330
- package/dist/trees.umd.js +6 -6
- package/package.json +6 -4
- package/src/lib-components/forms/BaseInput.vue +83 -0
- package/src/lib-components/forms/Checkbox.vue +46 -0
- package/src/lib-components/forms/DateRangePicker.vue +65 -0
- package/src/lib-components/forms/InputHelp.vue +24 -0
- package/src/lib-components/forms/InputLabel.vue +23 -0
- package/src/lib-components/forms/MultiCheckboxes.vue +55 -0
- package/src/lib-components/forms/Radio.vue +58 -0
- package/src/lib-components/forms/Select.vue +65 -0
- package/src/lib-components/forms/TextArea.vue +50 -0
- package/src/lib-components/forms/Toggle.vue +25 -0
- package/src/lib-components/forms/YesOrNoRadio.vue +70 -0
- package/src/lib-components/layout/DateFilter.vue +54 -0
- package/src/lib-components/layout/SidebarLayout.vue +239 -0
- package/src/lib-components/layout/StackedLayout.vue +172 -0
- package/src/lib-components/lists/Cards.vue +33 -0
- package/src/lib-components/lists/DetailList.vue +114 -0
- package/src/lib-components/lists/DownloadCell.vue +12 -0
- package/src/lib-components/lists/StaticTable.vue +83 -0
- package/src/lib-components/lists/Table.vue +291 -0
- package/src/lib-components/navigation/ActionsDropdown.vue +78 -0
- package/src/lib-components/navigation/Paginator.vue +111 -0
- package/src/lib-components/navigation/Steps.vue +83 -0
- package/src/lib-components/navigation/Tabs.vue +92 -0
- package/src/lib-components/overlays/ContentModal.vue +95 -0
- package/src/lib-components/overlays/Flash.vue +131 -0
- package/src/lib-components/overlays/Modal.vue +133 -0
- package/src/lib-components/overlays/Popover/Popover.vue +229 -0
- package/src/lib-components/overlays/Popover/PopoverContent.vue +8 -0
- package/src/lib-components/overlays/Slideover.vue +87 -0
- package/src/lib-components/overlays/Spinner.vue +149 -0
- package/src/lib-components/overlays/Tooltip.vue +34 -0
- package/types/components.d.ts +6 -2
- package/types/composables/date.d.ts +4 -0
- package/types/composables/nav.d.ts +13 -0
- package/types/composables/overlay.d.ts +4 -0
- package/types/composables/table.d.ts +32 -0
- package/types/composables/user.d.ts +6 -0
- package/types/global.d.ts +5 -2
- package/types/helpers/Debounce.d.ts +1 -0
- package/types/helpers/Throttle.d.ts +1 -0
- package/types/lib-components/forms/Select.vue.d.ts +2 -2
- package/types/lib-components/index.d.ts +9 -9
- package/types/lib-components/layout/DateFilter.vue.d.ts +1 -4
- package/types/lib-components/layout/SidebarLayout.vue.d.ts +1 -1
- package/types/lib-components/layout/StackedLayout.vue.d.ts +2 -2
- package/types/lib-components/lists/StaticTable.vue.d.ts +1 -1
- package/types/lib-components/lists/Table.vue.d.ts +1 -1
- package/types/lib-components/navigation/ActionsDropdown.vue.d.ts +2 -2
- package/types/lib-components/navigation/Paginator.vue.d.ts +1 -6
- package/types/lib-components/overlays/Flash.vue.d.ts +0 -4
- package/types/lib-components/overlays/Popover/Popover.vue.d.ts +23 -0
- package/types/lib-components/overlays/Popover/PopoverContent.vue.d.ts +2 -0
- package/types/lib-components/overlays/Tooltip.vue.d.ts +23 -0
- package/types/index.d.ts +0 -3
- package/types/nav.d.ts +0 -8
- package/types/table.d.ts +0 -36
- package/types/users.d.ts +0 -10
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogOverlay,
|
|
5
|
+
DialogTitle,
|
|
6
|
+
TransitionChild,
|
|
7
|
+
TransitionRoot,
|
|
8
|
+
} from "@headlessui/vue"
|
|
9
|
+
|
|
10
|
+
withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
modelValue: boolean
|
|
13
|
+
title?: string
|
|
14
|
+
}>(),
|
|
15
|
+
{
|
|
16
|
+
title: "",
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: "update:modelValue", val: boolean): void
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const updateModelValue = (value: boolean) => {
|
|
25
|
+
emit("update:modelValue", value)
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
<template>
|
|
29
|
+
<TransitionRoot as="template" :show="modelValue">
|
|
30
|
+
<Dialog
|
|
31
|
+
as="div"
|
|
32
|
+
static
|
|
33
|
+
class="fixed z-10 inset-0 overflow-y-auto"
|
|
34
|
+
@close="updateModelValue(false)"
|
|
35
|
+
:open="modelValue"
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
|
39
|
+
>
|
|
40
|
+
<TransitionChild
|
|
41
|
+
as="template"
|
|
42
|
+
enter="ease-out duration-300"
|
|
43
|
+
enter-from="opacity-0"
|
|
44
|
+
enter-to="opacity-100"
|
|
45
|
+
leave="ease-in duration-200"
|
|
46
|
+
leave-from="opacity-100"
|
|
47
|
+
leave-to="opacity-0"
|
|
48
|
+
>
|
|
49
|
+
<DialogOverlay
|
|
50
|
+
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
|
51
|
+
/>
|
|
52
|
+
</TransitionChild>
|
|
53
|
+
|
|
54
|
+
<!-- This element is to trick the browser into centering the modal contents. -->
|
|
55
|
+
<span
|
|
56
|
+
class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
>​</span
|
|
59
|
+
>
|
|
60
|
+
<TransitionChild
|
|
61
|
+
as="template"
|
|
62
|
+
enter="ease-out duration-300"
|
|
63
|
+
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
64
|
+
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
|
65
|
+
leave="ease-in duration-200"
|
|
66
|
+
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
|
67
|
+
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
|
|
71
|
+
>
|
|
72
|
+
<div>
|
|
73
|
+
<slot name="icon"></slot>
|
|
74
|
+
<div class="mt-3 text-center sm:mt-5">
|
|
75
|
+
<DialogTitle as="h3" v-text="title"></DialogTitle>
|
|
76
|
+
<div class="mt-2">
|
|
77
|
+
<slot></slot>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="mt-5 sm:mt-6">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="inline-flex justify-center w-full xy-btn"
|
|
85
|
+
@click="updateModelValue(false)"
|
|
86
|
+
>
|
|
87
|
+
Go back
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</TransitionChild>
|
|
92
|
+
</div>
|
|
93
|
+
</Dialog>
|
|
94
|
+
</TransitionRoot>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Flash } from "@/composables/overlay"
|
|
3
|
+
import { onMounted, ref } from "vue"
|
|
4
|
+
|
|
5
|
+
// TODO: spk this might benefit from the composition api to avoid race conditions where a flash is requested before the component is mounted.
|
|
6
|
+
|
|
7
|
+
const flashes = ref<Flash[]>([])
|
|
8
|
+
const flashTypeBorderClass = {
|
|
9
|
+
warning: "border-orange-500",
|
|
10
|
+
error: "border-red-500",
|
|
11
|
+
info: "border-blue-500",
|
|
12
|
+
success: "border-green-500",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const getFlashClass = (flash: Flash): string => {
|
|
16
|
+
const availableTypes = Object.keys(flashTypeBorderClass)
|
|
17
|
+
// default the flash type to "info" if no flash type or an unsupported flash.type is found
|
|
18
|
+
const type =
|
|
19
|
+
flash?.type && availableTypes.includes(flash.type)
|
|
20
|
+
? (flash.type as "warning" | "error" | "info" | "success")
|
|
21
|
+
: "info"
|
|
22
|
+
return flashTypeBorderClass[type]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const remove = (flash: Flash): void => {
|
|
26
|
+
let index = 0
|
|
27
|
+
for (const f of flashes.value) {
|
|
28
|
+
if (flash.message === f.message) {
|
|
29
|
+
flashes.value.splice(index, 1)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
index++
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const renderFlash = (flash: Flash): void => {
|
|
36
|
+
flashes.value.push(flash)
|
|
37
|
+
// Super simple flash implementation. This could get "smarter" by adding an
|
|
38
|
+
// id to the flash object, and then searching for the specific flash in the
|
|
39
|
+
// array and splicing, instead of simply doing a pop().
|
|
40
|
+
setTimeout(
|
|
41
|
+
(flashes: Flash[]) => {
|
|
42
|
+
flashes.pop()
|
|
43
|
+
},
|
|
44
|
+
10000,
|
|
45
|
+
flashes.value
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const renderGenericError = (email: string): void => {
|
|
50
|
+
renderFlash({
|
|
51
|
+
type: "error",
|
|
52
|
+
message:
|
|
53
|
+
"Whoops! Something went wrong, please reach out to " +
|
|
54
|
+
`<a class="underline text-xy-blue" href="mailto:${email}">${email}</a>` +
|
|
55
|
+
" if the issue persists.",
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onMounted(() => {
|
|
60
|
+
window.VueBus.on("Flash-show-message", (flash) => {
|
|
61
|
+
renderFlash(flash)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
window.VueBus.on("Flash-show-generic-error", (email) => {
|
|
65
|
+
renderGenericError(email)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (window.Flashes) {
|
|
69
|
+
for (const flash of window.Flashes) {
|
|
70
|
+
if (typeof flash.type === "undefined") {
|
|
71
|
+
const values: string[] = flash.message.split(": ")
|
|
72
|
+
renderFlash({ type: values[0], message: values[1] })
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
renderFlash({ type: flash.type, message: flash.message })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
</script>
|
|
80
|
+
<template>
|
|
81
|
+
<div
|
|
82
|
+
class="fixed inset-0 flex flex-col items-end justify-end px-4 py-6 pointer-events-none sm:p-6 z-40"
|
|
83
|
+
>
|
|
84
|
+
<transition-group
|
|
85
|
+
tag="div"
|
|
86
|
+
class="max-w-sm w-full"
|
|
87
|
+
enter-active-class="ease-out duration-300"
|
|
88
|
+
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
|
|
89
|
+
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
|
|
90
|
+
leave-active-class="ease-in duration-100"
|
|
91
|
+
leave-from-class="opacity-100"
|
|
92
|
+
leave-to-class="opacity-0"
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
v-for="(flash, idx) in flashes"
|
|
96
|
+
:key="flash.message"
|
|
97
|
+
class="bg-white shadow-lg rounded-lg pointer-events-auto border-t-4 transform"
|
|
98
|
+
:class="[{ 'mt-2': idx > 0 }, getFlashClass(flash)]"
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
class="rounded-lg ring-1 ring-black ring-opacity-5 overflow-hidden"
|
|
102
|
+
>
|
|
103
|
+
<div class="p-4">
|
|
104
|
+
<div class="flex items-center">
|
|
105
|
+
<div class="w-0 flex-1 flex justify-between">
|
|
106
|
+
<p
|
|
107
|
+
class="w-0 flex-1 text-sm leading-5 font-medium text-gray-900"
|
|
108
|
+
v-html="flash.message"
|
|
109
|
+
></p>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="ml-4 flex-shrink-0 flex">
|
|
112
|
+
<button
|
|
113
|
+
@click="remove(flash)"
|
|
114
|
+
class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150"
|
|
115
|
+
>
|
|
116
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
117
|
+
<path
|
|
118
|
+
fill-rule="evenodd"
|
|
119
|
+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
120
|
+
clip-rule="evenodd"
|
|
121
|
+
/>
|
|
122
|
+
</svg>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</transition-group>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogOverlay,
|
|
5
|
+
DialogTitle,
|
|
6
|
+
TransitionChild,
|
|
7
|
+
TransitionRoot,
|
|
8
|
+
} from "@headlessui/vue"
|
|
9
|
+
import { XIcon } from "@heroicons/vue/outline"
|
|
10
|
+
|
|
11
|
+
withDefaults(
|
|
12
|
+
defineProps<{
|
|
13
|
+
destructive?: boolean
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
modelValue: boolean
|
|
16
|
+
submitText?: string
|
|
17
|
+
title?: string
|
|
18
|
+
}>(),
|
|
19
|
+
{
|
|
20
|
+
destructive: false,
|
|
21
|
+
disabled: false,
|
|
22
|
+
submitText: "",
|
|
23
|
+
title: "",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{
|
|
28
|
+
(e: "submit"): void
|
|
29
|
+
(e: "update:modelValue", val: boolean): void
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const submit = () => {
|
|
33
|
+
emit("submit")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const updateModelValue = (value: boolean) => {
|
|
37
|
+
emit("update:modelValue", value)
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
<template>
|
|
41
|
+
<TransitionRoot as="template" :show="modelValue">
|
|
42
|
+
<Dialog
|
|
43
|
+
as="div"
|
|
44
|
+
static
|
|
45
|
+
class="fixed z-10 inset-0 overflow-y-auto"
|
|
46
|
+
@close="updateModelValue(false)"
|
|
47
|
+
:open="modelValue"
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
|
51
|
+
>
|
|
52
|
+
<TransitionChild
|
|
53
|
+
as="template"
|
|
54
|
+
enter="ease-out duration-300"
|
|
55
|
+
enter-from="opacity-0"
|
|
56
|
+
enter-to="opacity-100"
|
|
57
|
+
leave="ease-in duration-200"
|
|
58
|
+
leave-from="opacity-100"
|
|
59
|
+
leave-to="opacity-0"
|
|
60
|
+
>
|
|
61
|
+
<DialogOverlay
|
|
62
|
+
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
|
63
|
+
/>
|
|
64
|
+
</TransitionChild>
|
|
65
|
+
|
|
66
|
+
<!-- This element is to trick the browser into centering the modal contents. -->
|
|
67
|
+
<span
|
|
68
|
+
class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
>​</span
|
|
71
|
+
>
|
|
72
|
+
<TransitionChild
|
|
73
|
+
as="template"
|
|
74
|
+
enter="ease-out duration-300"
|
|
75
|
+
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
76
|
+
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
|
77
|
+
leave="ease-in duration-200"
|
|
78
|
+
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
|
79
|
+
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
80
|
+
>
|
|
81
|
+
<div
|
|
82
|
+
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl w-full"
|
|
83
|
+
>
|
|
84
|
+
<div class="block absolute top-0 right-0 pt-4 pr-4">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
class="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
88
|
+
@click="updateModelValue(false)"
|
|
89
|
+
>
|
|
90
|
+
<span class="sr-only">Close</span>
|
|
91
|
+
<XIcon class="h-6 w-6" aria-hidden="true" />
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
95
|
+
<div class="mt-3 sm:mt-0 sm:text-left">
|
|
96
|
+
<DialogTitle
|
|
97
|
+
as="h3"
|
|
98
|
+
class="text-center text-lg leading-6 font-medium text-gray-900"
|
|
99
|
+
v-text="title"
|
|
100
|
+
></DialogTitle>
|
|
101
|
+
<div class="mt-2">
|
|
102
|
+
<slot></slot>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div
|
|
107
|
+
class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
|
|
108
|
+
v-if="submitText"
|
|
109
|
+
>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="xy-btn w-full sm:ml-3 sm:w-auto sm:text-sm"
|
|
113
|
+
@click="submit()"
|
|
114
|
+
v-text="submitText"
|
|
115
|
+
:class="[destructive ? 'xy-btn-red' : 'xy-btn']"
|
|
116
|
+
:disabled="disabled"
|
|
117
|
+
></button>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
class="xy-btn-white mt-3 w-full sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
|
121
|
+
@click="updateModelValue(false)"
|
|
122
|
+
ref="cancelButtonRef"
|
|
123
|
+
>
|
|
124
|
+
Cancel
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
<slot name="buttons"></slot>
|
|
128
|
+
</div>
|
|
129
|
+
</TransitionChild>
|
|
130
|
+
</div>
|
|
131
|
+
</Dialog>
|
|
132
|
+
</TransitionRoot>
|
|
133
|
+
</template>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export type PopoverPosition =
|
|
3
|
+
| "top-left"
|
|
4
|
+
| "top-center"
|
|
5
|
+
| "top-right"
|
|
6
|
+
| "bottom-left"
|
|
7
|
+
| "bottom-center"
|
|
8
|
+
| "bottom-right"
|
|
9
|
+
| "left"
|
|
10
|
+
| "right"
|
|
11
|
+
| "auto"
|
|
12
|
+
| "none"
|
|
13
|
+
</script>
|
|
14
|
+
<script lang="ts" setup>
|
|
15
|
+
import { throttle } from "@/helpers/Throttle"
|
|
16
|
+
import {
|
|
17
|
+
Popover as HeadlessPopover,
|
|
18
|
+
PopoverButton as HeadlessPopoverButton,
|
|
19
|
+
PopoverPanel as HeadlessPopoverPanel,
|
|
20
|
+
} from "@headlessui/vue"
|
|
21
|
+
import { computed, onMounted, onUnmounted, ref } from "vue"
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(
|
|
24
|
+
defineProps<{
|
|
25
|
+
as?: string
|
|
26
|
+
position?: PopoverPosition
|
|
27
|
+
}>(),
|
|
28
|
+
{
|
|
29
|
+
as: "div",
|
|
30
|
+
position: "auto",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const getViewportDimensions = () => {
|
|
35
|
+
return {
|
|
36
|
+
vw: document.documentElement.clientWidth,
|
|
37
|
+
vh: document.documentElement.clientHeight,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const trigger = ref<typeof HeadlessPopoverButton>()
|
|
42
|
+
const wrapper = ref<typeof HeadlessPopoverPanel>()
|
|
43
|
+
const viewport = ref<{ vw: number; vh: number }>(getViewportDimensions())
|
|
44
|
+
|
|
45
|
+
const classes = computed(() => {
|
|
46
|
+
const classes = {
|
|
47
|
+
wrapper: "",
|
|
48
|
+
content: "",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (props.position === "none") {
|
|
52
|
+
return classes
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// defaults classes when positioning
|
|
56
|
+
classes.wrapper = "h-0 flex w-screen"
|
|
57
|
+
classes.content = "absolute"
|
|
58
|
+
|
|
59
|
+
// merge static positioning classes
|
|
60
|
+
if (props.position !== "auto") {
|
|
61
|
+
classes.wrapper += ` ${staticPosition.value.wrapper}`
|
|
62
|
+
classes.content += ` ${staticPosition.value.content}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return classes
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const staticPosition = computed(() => {
|
|
69
|
+
let wrapperClasses = ""
|
|
70
|
+
let contentClasses = ""
|
|
71
|
+
|
|
72
|
+
switch (props.position) {
|
|
73
|
+
case "top-left":
|
|
74
|
+
wrapperClasses = "top-0 right-0 -translate-y-full justify-end"
|
|
75
|
+
contentClasses = "bottom-full"
|
|
76
|
+
break
|
|
77
|
+
case "top-center":
|
|
78
|
+
wrapperClasses =
|
|
79
|
+
"top-0 -translate-y-full -translate-x-full left-1/2 justify-end"
|
|
80
|
+
contentClasses = "bottom-full translate-x-1/2"
|
|
81
|
+
break
|
|
82
|
+
case "top-right":
|
|
83
|
+
wrapperClasses =
|
|
84
|
+
"top-0 -translate-y-full left-0 -translate-x-full justify-end"
|
|
85
|
+
contentClasses = "bottom-full translate-x-full"
|
|
86
|
+
break
|
|
87
|
+
case "bottom-left":
|
|
88
|
+
wrapperClasses = "top-full right-0 justify-end"
|
|
89
|
+
contentClasses = "top-full"
|
|
90
|
+
break
|
|
91
|
+
case "bottom-center":
|
|
92
|
+
wrapperClasses = "top-full -translate-x-full left-1/2 justify-end"
|
|
93
|
+
contentClasses = "top-full translate-x-1/2"
|
|
94
|
+
break
|
|
95
|
+
case "bottom-right":
|
|
96
|
+
wrapperClasses = "top-full left-0 -translate-x-full justify-end"
|
|
97
|
+
contentClasses = "top-full translate-x-full"
|
|
98
|
+
break
|
|
99
|
+
case "left":
|
|
100
|
+
wrapperClasses =
|
|
101
|
+
"top-1/2 left-0 -translate-y-1/2 -translate-x-full justify-end"
|
|
102
|
+
contentClasses = "-translate-y-1/2"
|
|
103
|
+
break
|
|
104
|
+
case "right":
|
|
105
|
+
wrapperClasses = "top-1/2 -translate-y-1/2 right-0 justify-end"
|
|
106
|
+
contentClasses = "translate-x-full -translate-y-1/2"
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
wrapper: wrapperClasses,
|
|
112
|
+
content: contentClasses,
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const autoPosition = computed(() => {
|
|
117
|
+
if (!wrapper?.value?.el || !trigger?.value?.el) {
|
|
118
|
+
return {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { vw, vh } = viewport.value
|
|
122
|
+
|
|
123
|
+
// avoid bumping up against the edge of the browser when possible
|
|
124
|
+
const offset = 10
|
|
125
|
+
|
|
126
|
+
// base the anchor rectangle off of the entire trigger dom element to move around it
|
|
127
|
+
const anchorRect: DOMRect = trigger.value.el.getBoundingClientRect()
|
|
128
|
+
// the content rectangle is best calculated by our first child (content) element inside the wrapper
|
|
129
|
+
const contentRect: DOMRect =
|
|
130
|
+
wrapper.value.el.firstChild.getBoundingClientRect()
|
|
131
|
+
const distToBottom = vh - anchorRect.bottom
|
|
132
|
+
// NOTE: edge case - there may be more space below in the viewport
|
|
133
|
+
// but less document space for display
|
|
134
|
+
// the inverse could also be true - but will be very rare
|
|
135
|
+
// occurring with unreasonably large popover content
|
|
136
|
+
const positionAbove = anchorRect.top > distToBottom
|
|
137
|
+
const distToRight = vw - anchorRect.left
|
|
138
|
+
const flowLeft = anchorRect.left > distToRight
|
|
139
|
+
|
|
140
|
+
// translate the content container on the x axis to the correct position
|
|
141
|
+
// considering the flow the content should take
|
|
142
|
+
let xPos = 0
|
|
143
|
+
if (flowLeft) {
|
|
144
|
+
if (contentRect.width > anchorRect.right) {
|
|
145
|
+
xPos =
|
|
146
|
+
anchorRect.right -
|
|
147
|
+
contentRect.width +
|
|
148
|
+
(contentRect.width - anchorRect.right)
|
|
149
|
+
} else {
|
|
150
|
+
xPos = anchorRect.right - contentRect.width
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (vw > contentRect.width + offset) {
|
|
154
|
+
xPos = xPos + offset
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
if (contentRect.width > distToRight) {
|
|
158
|
+
xPos = anchorRect.left - (contentRect.width - distToRight)
|
|
159
|
+
} else {
|
|
160
|
+
xPos = anchorRect.left
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (vw > contentRect.width + offset) {
|
|
164
|
+
xPos = xPos - offset
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
wrapper: {
|
|
170
|
+
top: positionAbove ? "auto" : `100%`,
|
|
171
|
+
bottom: positionAbove ? "100%" : `auto`,
|
|
172
|
+
transform: `translate(${anchorRect.left * -1}px, 0)`, // pin to left of window
|
|
173
|
+
width: `${vw}px`,
|
|
174
|
+
},
|
|
175
|
+
content: {
|
|
176
|
+
top: positionAbove ? "auto" : `100%`,
|
|
177
|
+
bottom: positionAbove ? "100%" : `auto`,
|
|
178
|
+
transform: `translate(${xPos}px, 0)`,
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (props.position === "auto") {
|
|
184
|
+
const throttledSetPositions = throttle(() => {
|
|
185
|
+
viewport.value = getViewportDimensions()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
onMounted(() => {
|
|
189
|
+
window.addEventListener("resize", throttledSetPositions)
|
|
190
|
+
window.addEventListener("scroll", throttledSetPositions)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
onUnmounted(() => {
|
|
194
|
+
window.removeEventListener("resize", throttledSetPositions)
|
|
195
|
+
window.removeEventListener("scroll", throttledSetPositions)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<template>
|
|
201
|
+
<HeadlessPopover v-slot="{ open, close }" class="relative" :as="as">
|
|
202
|
+
<HeadlessPopoverButton ref="trigger">
|
|
203
|
+
<slot name="button" :open="open" :close="close"></slot>
|
|
204
|
+
</HeadlessPopoverButton>
|
|
205
|
+
|
|
206
|
+
<transition
|
|
207
|
+
enter-active-class="transition-opacity transition-faster ease-out-quad"
|
|
208
|
+
leave-active-class="transition-opacity transition-fastest ease-in-quad"
|
|
209
|
+
enter-from-class="opacity-0"
|
|
210
|
+
enter-to-class="opacity-100"
|
|
211
|
+
leave-from-class="opacity-100"
|
|
212
|
+
leave-to-class="opacity-0"
|
|
213
|
+
>
|
|
214
|
+
<HeadlessPopoverPanel
|
|
215
|
+
ref="wrapper"
|
|
216
|
+
class="absolute z-10"
|
|
217
|
+
:class="classes.wrapper"
|
|
218
|
+
:style="position === 'auto' ? autoPosition.wrapper : {}"
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
:class="classes.content"
|
|
222
|
+
:style="position === 'auto' ? autoPosition.content : {}"
|
|
223
|
+
>
|
|
224
|
+
<slot :open="open" :close="close"></slot>
|
|
225
|
+
</div>
|
|
226
|
+
</HeadlessPopoverPanel>
|
|
227
|
+
</transition>
|
|
228
|
+
</HeadlessPopover>
|
|
229
|
+
</template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogOverlay,
|
|
5
|
+
DialogTitle,
|
|
6
|
+
TransitionChild,
|
|
7
|
+
TransitionRoot,
|
|
8
|
+
} from "@headlessui/vue"
|
|
9
|
+
import { XIcon } from "@heroicons/vue/outline"
|
|
10
|
+
import { ref } from "vue"
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
header: string
|
|
14
|
+
description: string
|
|
15
|
+
modelValue: boolean
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const open = ref(props.modelValue)
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: "close", val: boolean): void
|
|
22
|
+
(e: "update:modelValue", val: boolean): void
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const close = () => {
|
|
26
|
+
open.value = false
|
|
27
|
+
emit("close", open.value)
|
|
28
|
+
emit("update:modelValue", open.value)
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
<template>
|
|
32
|
+
<TransitionRoot as="template" :show="modelValue">
|
|
33
|
+
<Dialog
|
|
34
|
+
as="div"
|
|
35
|
+
static
|
|
36
|
+
class="fixed inset-0 z-40 overflow-hidden bg-black bg-opacity-50"
|
|
37
|
+
@close="close()"
|
|
38
|
+
:open="modelValue"
|
|
39
|
+
>
|
|
40
|
+
<div class="absolute inset-0 overflow-hidden">
|
|
41
|
+
<DialogOverlay class="absolute inset-0" />
|
|
42
|
+
|
|
43
|
+
<div class="fixed inset-y-0 right-0 pl-10 max-w-full flex">
|
|
44
|
+
<TransitionChild
|
|
45
|
+
as="template"
|
|
46
|
+
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
47
|
+
enter-from="translate-x-full"
|
|
48
|
+
enter-to="translate-x-0"
|
|
49
|
+
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
50
|
+
leave-from="translate-x-0"
|
|
51
|
+
leave-to="translate-x-full"
|
|
52
|
+
>
|
|
53
|
+
<div class="w-screen max-w-md">
|
|
54
|
+
<div
|
|
55
|
+
class="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
56
|
+
>
|
|
57
|
+
<div class="py-6 px-4 bg-blue-700 sm:px-6">
|
|
58
|
+
<div class="flex items-center justify-between">
|
|
59
|
+
<DialogTitle as="h3" class="text-white" v-text="header">
|
|
60
|
+
</DialogTitle>
|
|
61
|
+
<div class="ml-3 h-7 flex items-center">
|
|
62
|
+
<button
|
|
63
|
+
class="bg-blue-700 rounded-md text-blue-200 hover:text-white focus:outline-none focus:ring-2 focus:ring-white"
|
|
64
|
+
@click="close()"
|
|
65
|
+
>
|
|
66
|
+
<span class="sr-only">Close panel</span>
|
|
67
|
+
<XIcon class="h-6 w-6" aria-hidden="true" />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="mt-1">
|
|
72
|
+
<p class="text-blue-300" v-text="description"></p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="relative flex-1 py-6 px-4 sm:px-6">
|
|
76
|
+
<slot></slot>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<slot name="footer"></slot>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</TransitionChild>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</Dialog>
|
|
86
|
+
</TransitionRoot>
|
|
87
|
+
</template>
|