manolis-ui 0.0.11 → 0.0.13
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/package.json +6 -3
- package/.github/workflows/deploy_and_publish.yml +0 -113
- package/.storybook/main.ts +0 -34
- package/.storybook/preview.ts +0 -17
- package/.vscode/extensions.json +0 -3
- package/index.html +0 -13
- package/postcss.config.js +0 -2
- package/public/vite.svg +0 -1
- package/src/App.vue +0 -19
- package/src/assets/vue.svg +0 -1
- package/src/components/actions/ButtonComponent.vue +0 -80
- package/src/components/actions/dropdown.vue +0 -46
- package/src/components/actions/modal.vue +0 -52
- package/src/components/actions/swap.vue +0 -15
- package/src/components/actions/theme-controller.vue +0 -52
- package/src/components/data-display/accordion.vue +0 -29
- package/src/components/data-display/avatar.vue +0 -36
- package/src/components/data-display/badge.vue +0 -35
- package/src/components/data-display/card.vue +0 -60
- package/src/components/data-display/carousel.vue +0 -34
- package/src/components/data-input/advancedSearch.vue +0 -227
- package/src/components/data-input/datetimePicker.vue +0 -402
- package/src/components/data-input/input.vue +0 -98
- package/src/components/data-input/rating.vue +0 -60
- package/src/components/feedback/loader.vue +0 -25
- package/src/components/layout/footer.vue +0 -38
- package/src/components/layout/hero.vue +0 -15
- package/src/components/navigation/categoryNavigation.vue +0 -40
- package/src/components/navigation/navigationBar.vue +0 -107
- package/src/components/navigation/tab.vue +0 -62
- package/src/composables/useLocalStorage.ts +0 -24
- package/src/index.ts +0 -30
- package/src/main.ts +0 -5
- package/src/stories/actions/Button.stories.ts +0 -47
- package/src/stories/actions/Dropdown.stories.ts +0 -70
- package/src/stories/actions/Modal.stories.ts +0 -56
- package/src/stories/actions/Swap.stories.ts +0 -56
- package/src/stories/actions/ThemeSwitcher.stories.ts +0 -41
- package/src/stories/data-display/accordion.stories.ts +0 -49
- package/src/stories/data-display/avatar.stories.ts +0 -75
- package/src/stories/data-display/badge.stories.ts +0 -76
- package/src/stories/data-display/card.stories.ts +0 -79
- package/src/stories/data-input/rating.stories.ts +0 -73
- package/src/stories/feedback/Loader.stories.ts +0 -34
- package/src/stories/layout/footer.stories.ts +0 -63
- package/src/style.css +0 -57
- package/src/vite-env.d.ts +0 -1
- package/storybook-static/assets/Button.stories-B5Gg7Ski.js +0 -6
- package/storybook-static/assets/Color-YHDXOIA2-Cy_mA6cn.js +0 -1
- package/storybook-static/assets/DocsRenderer-CFRXHY34-wSGN0bIp.js +0 -610
- package/storybook-static/assets/Dropdown.stories-Bth3_21L.js +0 -32
- package/storybook-static/assets/Loader.stories-BnqtyQP_.js +0 -5
- package/storybook-static/assets/Modal.stories-CxOA4msz.js +0 -46
- package/storybook-static/assets/Swap.stories-Cpc9q_kE.js +0 -54
- package/storybook-static/assets/ThemeSwitcher.stories-BwHcHihM.js +0 -45
- package/storybook-static/assets/accordion.stories-B6yDsDXk.js +0 -7
- package/storybook-static/assets/avatar.stories-BDN93iYh.js +0 -39
- package/storybook-static/assets/badge.stories-CXQpnu0e.js +0 -39
- package/storybook-static/assets/card.stories-1gVWO2fs.js +0 -48
- package/storybook-static/assets/entry-preview-Cfvj9hgI.js +0 -1
- package/storybook-static/assets/entry-preview-docs-BJQT5BWv.js +0 -16
- package/storybook-static/assets/footer.stories-DPXqApht.js +0 -23
- package/storybook-static/assets/iframe-BNdG_Qtn.js +0 -211
- package/storybook-static/assets/index-Bx-go_-4.js +0 -8
- package/storybook-static/assets/index-CiNYFPF0.js +0 -1
- package/storybook-static/assets/index-DrFu-skq.js +0 -6
- package/storybook-static/assets/preview-4lzcCKUM.css +0 -1
- package/storybook-static/assets/preview-B8lJiyuQ.js +0 -34
- package/storybook-static/assets/preview-BBWR9nbA.js +0 -1
- package/storybook-static/assets/preview-BWzBA1C2.js +0 -396
- package/storybook-static/assets/preview-CvbIS5ZJ.js +0 -1
- package/storybook-static/assets/preview-DD_OYowb.js +0 -1
- package/storybook-static/assets/preview-DGUiP6tS.js +0 -7
- package/storybook-static/assets/preview-DHQbi4pV.js +0 -1
- package/storybook-static/assets/preview-DMNI4LCC.js +0 -15
- package/storybook-static/assets/preview-DnqJFqn_.js +0 -2
- package/storybook-static/assets/preview-Dsq_8SDT.js +0 -240
- package/storybook-static/assets/preview-hHK5u5_Q.js +0 -1
- package/storybook-static/assets/rating.stories-BX0Pzp5i.js +0 -27
- package/storybook-static/assets/vue.esm-bundler-C-YazFc_.js +0 -36
- package/storybook-static/favicon.svg +0 -1
- package/storybook-static/iframe.html +0 -666
- package/storybook-static/index.html +0 -181
- package/storybook-static/index.json +0 -1
- package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/project.json +0 -1
- package/storybook-static/sb-addons/chromatic-com-storybook-9/manager-bundle.js +0 -331
- package/storybook-static/sb-addons/chromatic-com-storybook-9/manager-bundle.js.LEGAL.txt +0 -51
- package/storybook-static/sb-addons/essentials-actions-2/manager-bundle.js +0 -3
- package/storybook-static/sb-addons/essentials-backgrounds-4/manager-bundle.js +0 -12
- package/storybook-static/sb-addons/essentials-controls-1/manager-bundle.js +0 -402
- package/storybook-static/sb-addons/essentials-docs-3/manager-bundle.js +0 -242
- package/storybook-static/sb-addons/essentials-measure-7/manager-bundle.js +0 -3
- package/storybook-static/sb-addons/essentials-outline-8/manager-bundle.js +0 -3
- package/storybook-static/sb-addons/essentials-toolbars-6/manager-bundle.js +0 -3
- package/storybook-static/sb-addons/essentials-viewport-5/manager-bundle.js +0 -3
- package/storybook-static/sb-addons/interactions-10/manager-bundle.js +0 -222
- package/storybook-static/sb-addons/links-11/manager-bundle.js +0 -3
- package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +0 -3
- package/storybook-static/sb-common-assets/favicon.svg +0 -1
- package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/sb-manager/globals-module-info.js +0 -1046
- package/storybook-static/sb-manager/globals-runtime.js +0 -41239
- package/storybook-static/sb-manager/globals.js +0 -48
- package/storybook-static/sb-manager/runtime.js +0 -12048
- package/storybook-static/vite.svg +0 -1
- package/tsconfig.app.json +0 -27
- package/tsconfig.build.json +0 -3
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -25
- package/vite.config.ts +0 -43
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// interface Props {
|
|
3
|
-
// snapTo?: "start" | "center" | "end";
|
|
4
|
-
// vertical?: boolean;
|
|
5
|
-
// }
|
|
6
|
-
|
|
7
|
-
// interface Carousel {
|
|
8
|
-
|
|
9
|
-
// }
|
|
10
|
-
|
|
11
|
-
// interface CarouselItem {
|
|
12
|
-
|
|
13
|
-
// }
|
|
14
|
-
|
|
15
|
-
// const props = withDefaults(defineProps<Props>(), {
|
|
16
|
-
// snapTo: "start",
|
|
17
|
-
// vertical: false
|
|
18
|
-
// });
|
|
19
|
-
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<template>
|
|
23
|
-
<div class="carousel">
|
|
24
|
-
<div class="carousel-item">
|
|
25
|
-
<img src="https://img.daisyui.com/images/stock/photo-1559703248-dcaaec9fab78.webp" alt="">
|
|
26
|
-
</div>
|
|
27
|
-
<div class="carousel-item">
|
|
28
|
-
<img src="https://img.daisyui.com/images/stock/photo-1559703248-dcaaec9fab78.webp" alt="">
|
|
29
|
-
</div>
|
|
30
|
-
<div class="carousel-item">
|
|
31
|
-
<img src="https://img.daisyui.com/images/stock/photo-1559703248-dcaaec9fab78.webp" alt="">
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</template>
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, defineProps, defineAsyncComponent, nextTick } from 'vue';
|
|
3
|
-
import { Search } from 'lucide-vue-next';
|
|
4
|
-
import { onMounted } from 'vue';
|
|
5
|
-
import { onBeforeUnmount } from 'vue';
|
|
6
|
-
|
|
7
|
-
interface Tab {
|
|
8
|
-
name: string | any;
|
|
9
|
-
description: string;
|
|
10
|
-
type: "datetime";
|
|
11
|
-
range: boolean;
|
|
12
|
-
props?: Object;
|
|
13
|
-
value?: any;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
searchOptions: Array<{ category: string; tabs: Tab[] }>;
|
|
18
|
-
currentCategory: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const props = defineProps<Props>();
|
|
22
|
-
const emit = defineEmits<{
|
|
23
|
-
(e: 'search'): void;
|
|
24
|
-
(e: 'update:search-data', payload: { tab: string, data: any }): void;
|
|
25
|
-
}>();
|
|
26
|
-
const activeTab = ref<Tab | null>(null);
|
|
27
|
-
|
|
28
|
-
// Dynamic component loader
|
|
29
|
-
const componentMap = {
|
|
30
|
-
datetime: defineAsyncComponent(() => import('./datetimePicker.vue')),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Refs for tabs and popup positioning
|
|
34
|
-
const tabRefs = ref<Record<string, HTMLElement | null>>({});
|
|
35
|
-
const popupStyle = ref({ left: '0px', top: '0px', transform: 'translateX(0%)' });
|
|
36
|
-
const searchContainer = ref<HTMLElement | null>(null);
|
|
37
|
-
|
|
38
|
-
// made this function compatible with mobile and with desktop
|
|
39
|
-
function componentValueUpdated(data: any, currentTab: number) {
|
|
40
|
-
|
|
41
|
-
if (activeTab.value?.name) {
|
|
42
|
-
emit('update:search-data', {
|
|
43
|
-
tab: activeTab.value.name,
|
|
44
|
-
data: data
|
|
45
|
-
})
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if(props.searchOptions) {
|
|
49
|
-
let _currentTab = props.searchOptions?.find((option: { category: any; }) => option?.category === props.currentCategory)?.tabs[currentTab]
|
|
50
|
-
if(_currentTab !== undefined) {
|
|
51
|
-
_currentTab.value = data;
|
|
52
|
-
emit('update:search-data', {
|
|
53
|
-
tab: _currentTab?.name,
|
|
54
|
-
data: _currentTab?.value
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
function searchClicked() {
|
|
63
|
-
emit('search');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function handleOutsideClick(event: MouseEvent) {
|
|
67
|
-
if (searchContainer.value && !searchContainer.value.contains(event.target as Node)) {
|
|
68
|
-
await nextTick();
|
|
69
|
-
activeTab.value = null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async function openMobileView() {
|
|
75
|
-
if (typeof window !== 'undefined' && window.innerWidth <= 768) {
|
|
76
|
-
const modal = document.getElementById('advancedSearchMobile') as HTMLDialogElement | null;
|
|
77
|
-
if (modal) {
|
|
78
|
-
modal.showModal();
|
|
79
|
-
} else {
|
|
80
|
-
console.warn('Modal element not found.');
|
|
81
|
-
}
|
|
82
|
-
// return; // Early return to avoid running desktop logic
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function openTab(tab: Tab) {
|
|
87
|
-
activeTab.value = tab;
|
|
88
|
-
await nextTick();
|
|
89
|
-
|
|
90
|
-
const tabElement = tabRefs.value[tab.name];
|
|
91
|
-
if (tabElement) {
|
|
92
|
-
const rect = tabElement.getBoundingClientRect();
|
|
93
|
-
const parentRect = searchContainer.value?.getBoundingClientRect() || { left: 0, top: 0 };
|
|
94
|
-
|
|
95
|
-
// Calculate the popup position for centering on larger screens
|
|
96
|
-
if (window.innerWidth > 768) { // Desktop or tablet screen
|
|
97
|
-
popupStyle.value = {
|
|
98
|
-
left: `${rect.left + (rect.width / 2) - (parentRect.left)}px`,
|
|
99
|
-
top: `${rect.bottom - parentRect.top}px`,
|
|
100
|
-
transform: `translateX(-50%)`,
|
|
101
|
-
};
|
|
102
|
-
} else { // Mobile screen
|
|
103
|
-
popupStyle.value = {
|
|
104
|
-
left: '50%',
|
|
105
|
-
top: `${rect.bottom - parentRect.top}px`,
|
|
106
|
-
transform: `translateX(-50%)`, // Center the popup horizontally on mobile
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
console.error('Tab element not found for:', tab.name);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onMounted(() => {
|
|
117
|
-
document.addEventListener('click', handleOutsideClick);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
onBeforeUnmount(() => {
|
|
121
|
-
document.removeEventListener('click', handleOutsideClick);
|
|
122
|
-
});
|
|
123
|
-
</script>
|
|
124
|
-
|
|
125
|
-
<template>
|
|
126
|
-
<div ref="searchContainer" class="w-full">
|
|
127
|
-
<!-- desktop/tablet -->
|
|
128
|
-
<div
|
|
129
|
-
class="group/search relative flex place-items-center border-2 shadow-md p-2 border-base-300/25 rounded-sm w-full transition-all cursor-pointer md:cursor-auto"
|
|
130
|
-
@click="openMobileView">
|
|
131
|
-
<div class="flex gap-4 bg-base-100 w-full tabs tabs-boxed">
|
|
132
|
-
<button v-for="tab in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
|
|
133
|
-
:key="tab.name" @click="openTab(tab)" :class="{ 'tab-active': activeTab?.name === tab.name }"
|
|
134
|
-
class="group/searchitem relative after:top-0 after:-right-2 after:absolute first-of-type:flex-auto last-of-type:flex-auto md:after:content-[''] last-of-type:after:content-none after:content-none after:bg-base-200 hover:bg-base-200 p-1 rounded-sm w-auto after:w-[1px] after:h-10 text-start overflow-x-hidden pointer-events-none md:pointer-events-auto"
|
|
135
|
-
:ref="(el: HTMLElement | any) => tabRefs[tab.name] = el">
|
|
136
|
-
<p class="text-sm">{{ tab.name }}</p>
|
|
137
|
-
<p class="md:block hidden opacity-35 text-xs truncate overflow-hidden ...">{{ tab.description }}</p>
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div class="absolute flex shadow-sm mt-4 w-fit max-w-full transition-all tab-content" v-if="activeTab" :style="popupStyle">
|
|
142
|
-
<component :is="componentMap[activeTab.type]" v-bind="activeTab.props ? activeTab.props : null"
|
|
143
|
-
@updated="componentValueUpdated" />
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<button title="search" type="submit" class="ml-1 btn btn-primary btn-square" @click="searchClicked">
|
|
147
|
-
<Search :size="24" color="white" />
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="hidden">
|
|
151
|
-
<button title="search" @click="">
|
|
152
|
-
<Search :size="24" color="white" />
|
|
153
|
-
</button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<dialog id="advancedSearchMobile" class="modal">
|
|
159
|
-
<div class="modal-box">
|
|
160
|
-
<slot name="additionalForMobile">
|
|
161
|
-
<h3 class="font-bold text-lg">{{ currentCategory }}</h3>
|
|
162
|
-
</slot>
|
|
163
|
-
|
|
164
|
-
<div class="bg-base-200 my-4 collapse"
|
|
165
|
-
v-for="(tab, index) in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
|
|
166
|
-
:key="tab.name">
|
|
167
|
-
<input type="radio" name="my-accordion-1" :checked="index === 0" />
|
|
168
|
-
<div class="flex justify-between items-center pr-4 w-full font-medium text-xl collapse-title">{{ tab.name }} <p class="text-sm">{{ tab.description }}</p></div>
|
|
169
|
-
<div class="flex place-content-center p-0 collapse-content">
|
|
170
|
-
<br>
|
|
171
|
-
|
|
172
|
-
<component :is="componentMap[tab.type]" v-bind="tab.props || {}"
|
|
173
|
-
@updated="(data: any) => componentValueUpdated(data, index)" />
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
<div class="modal-action">
|
|
178
|
-
<form method="dialog">
|
|
179
|
-
<!-- if there is a button in form, it will close the modal -->
|
|
180
|
-
<button class="btn btn-primary" @click="searchClicked">Close and Search</button>
|
|
181
|
-
</form>
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
</dialog>
|
|
185
|
-
|
|
186
|
-
</template>
|
|
187
|
-
|
|
188
|
-
<style scoped>
|
|
189
|
-
.tabs button {
|
|
190
|
-
cursor: pointer;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.tab-content {
|
|
194
|
-
position: absolute;
|
|
195
|
-
z-index: 10;
|
|
196
|
-
transform-origin: top center;
|
|
197
|
-
transition: opacity 0.3s ease;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.tabs-boxed :is(.tab-active, [aria-selected=true]):not(.tab-disabled):not([disabled]),
|
|
201
|
-
.tabs-boxed :is(input:checked) {
|
|
202
|
-
background-color: var(--color-base-300);
|
|
203
|
-
color: var(--color-base-content);}
|
|
204
|
-
|
|
205
|
-
/* Mobile specific styles for centering the popup */
|
|
206
|
-
@media (max-width: 768px) {
|
|
207
|
-
.tab-content {
|
|
208
|
-
width: 100%;
|
|
209
|
-
/* Ensure it can take full width */
|
|
210
|
-
left: 50%;
|
|
211
|
-
transform: translateX(-50%);
|
|
212
|
-
top: auto;
|
|
213
|
-
/* Adjust top position as needed */
|
|
214
|
-
bottom: 0;
|
|
215
|
-
/* Position it just below the tabs or adjust as needed */
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
@media (min-width: 769px) {
|
|
220
|
-
.tab-content {
|
|
221
|
-
/* Ensure that popup on larger screens stays near the tab and doesn't stretch too wide */
|
|
222
|
-
width: auto;
|
|
223
|
-
left: unset;
|
|
224
|
-
transform: unset;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
</style>
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="inline-block relative w-full lg:w-[360px]" ref="pickerContainer">
|
|
3
|
-
<div class="flex items-center gap-2 cursor-pointer" @click="togglePopup">
|
|
4
|
-
<slot v-if="popup">
|
|
5
|
-
<input
|
|
6
|
-
type="text"
|
|
7
|
-
class="input-bordered w-full cursor-pointer input"
|
|
8
|
-
:placeholder="placeholder"
|
|
9
|
-
:value="formattedValue"
|
|
10
|
-
readonly
|
|
11
|
-
:id="inputId"
|
|
12
|
-
/>
|
|
13
|
-
<button class="btn btn-ghost">
|
|
14
|
-
📅
|
|
15
|
-
</button>
|
|
16
|
-
</slot>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<div v-if="showPopup || !popup" class="z-50 lg:bg-base-100 md:mt-2 md:p-4 px-4 rounded-md w-full" :class="[{ absolute: popup }]" :id="popupId">
|
|
20
|
-
<div v-if="showDate" class="flex justify-between items-center place-content-center mb-4">
|
|
21
|
-
<button class="hidden md:block btn-outline btn btn-primary md:btn-sm" @click="previousMonth">
|
|
22
|
-
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
23
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
24
|
-
</svg>
|
|
25
|
-
</button>
|
|
26
|
-
|
|
27
|
-
<select class="border-none w-fit select" v-model="currentMonth" @change="emitDateTimeChanged">
|
|
28
|
-
<option v-for="(month, index) in months" :key="index" :value="month">{{ month }}</option>
|
|
29
|
-
</select>
|
|
30
|
-
|
|
31
|
-
<select class="border-none w-24 select" v-model="currentYear" @change="emitDateTimeChanged">
|
|
32
|
-
<option v-for="year in years" :key="year" :value="year">{{ year }}</option>
|
|
33
|
-
</select>
|
|
34
|
-
|
|
35
|
-
<button class="hidden md:block btn-outline btn btn-primary md:btn-sm" @click="nextMonth">
|
|
36
|
-
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
37
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
38
|
-
</svg>
|
|
39
|
-
</button>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div v-if="showDate" class="gap-2 grid grid-cols-7">
|
|
43
|
-
<div v-for="day in daysOfWeek" :key="day" class="text-center">{{ day }}</div>
|
|
44
|
-
<div v-for="emptyDay in emptyDays" :key="emptyDay" class="text-center"></div>
|
|
45
|
-
<div v-for="day in daysInMonth" :key="day" class="hover:bg-primary-10 py-1 rounded-full text-center cursor-pointer" :class="{
|
|
46
|
-
'bg-primary text-primary-content': isDateSelected(day),
|
|
47
|
-
'today': isToday(day),
|
|
48
|
-
'range-start bg-primary text-primary-content': isRangeStart(day),
|
|
49
|
-
'range-end bg-primary text-primary-content': isRangeEnd(day),
|
|
50
|
-
'in-range bg-primary/20 hover:bg-primary/10': isInDateRange(day),
|
|
51
|
-
}" @click="selectDate(day)">
|
|
52
|
-
{{ day.getDate() }}
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<div v-if="showTime" class="mt-4 lg:w-80">
|
|
57
|
-
<h3 class="mb-2 font-bold text-lg" v-if="!props.range">Select Time</h3>
|
|
58
|
-
<div v-if="!props.range">
|
|
59
|
-
<input v-if="isMobile" type="time" class="input-bordered w-full input" v-model="selectedTime" @change="emitDateTimeChanged" />
|
|
60
|
-
<div v-else class="flex gap-4">
|
|
61
|
-
<select class="w-full select-bordered select" v-model.number="selectedHour" @change="emitDateTimeChanged">
|
|
62
|
-
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
|
|
63
|
-
</select>
|
|
64
|
-
<select class="w-full select-bordered select" v-model.number="selectedMinute" @change="emitDateTimeChanged">
|
|
65
|
-
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
|
|
66
|
-
</select>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<div v-else class="flex flex-col gap-4">
|
|
70
|
-
<div>
|
|
71
|
-
<p class="font-bold">Start Time</p>
|
|
72
|
-
<input v-if="isMobile" type="time" class="input-bordered w-full input" v-model="selectedTime.start" @change="emitDateTimeChanged" />
|
|
73
|
-
<div v-else class="flex gap-2">
|
|
74
|
-
<select class="w-full select-bordered select" v-model.number="selectedHour.start" @change="emitDateTimeChanged">
|
|
75
|
-
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
|
|
76
|
-
</select>
|
|
77
|
-
<select class="w-full select-bordered select" v-model.number="selectedMinute.start" @change="emitDateTimeChanged">
|
|
78
|
-
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
|
|
79
|
-
</select>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
<div>
|
|
83
|
-
<p class="font-bold">End Time</p>
|
|
84
|
-
<input v-if="isMobile" type="time" class="input-bordered w-full input" v-model="selectedTime.end" @change="emitDateTimeChanged" />
|
|
85
|
-
<div v-else class="flex gap-2">
|
|
86
|
-
<select class="w-full select-bordered select" v-model.number="selectedHour.end" @change="emitDateTimeChanged">
|
|
87
|
-
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
|
|
88
|
-
</select>
|
|
89
|
-
<select class="w-full select-bordered select" v-model.number="selectedMinute.end" @change="emitDateTimeChanged">
|
|
90
|
-
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
|
|
91
|
-
</select>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div class="flex gap-2 mt-4">
|
|
98
|
-
<button class="btn btn-secondary" :class="popup ? 'w-fit' : 'w-full'" @click="clearSelection">Clear</button>
|
|
99
|
-
<button v-if="popup" class="btn btn-primary btn-wide" @click="closeAndEmit">Close</button>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
</template>
|
|
104
|
-
|
|
105
|
-
<script setup>
|
|
106
|
-
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
107
|
-
|
|
108
|
-
// Props
|
|
109
|
-
const props = defineProps({
|
|
110
|
-
mode: {
|
|
111
|
-
type: String,
|
|
112
|
-
default: 'datetime', // 'datetime', 'date', 'time'
|
|
113
|
-
validator: (value) => ['datetime', 'date', 'time'].includes(value),
|
|
114
|
-
},
|
|
115
|
-
range: {
|
|
116
|
-
type: Boolean,
|
|
117
|
-
default: false,
|
|
118
|
-
},
|
|
119
|
-
placeholder: {
|
|
120
|
-
type: String,
|
|
121
|
-
default: 'Select date and time',
|
|
122
|
-
},
|
|
123
|
-
popup: {
|
|
124
|
-
type: Boolean,
|
|
125
|
-
default: false,
|
|
126
|
-
},
|
|
127
|
-
id: {
|
|
128
|
-
type: String,
|
|
129
|
-
default: 'datetimepicker',
|
|
130
|
-
},
|
|
131
|
-
initialDate: {
|
|
132
|
-
type: Object,
|
|
133
|
-
default: () => null
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Watchers
|
|
138
|
-
watch(() => props.range, (newRange) => {
|
|
139
|
-
if (newRange) {
|
|
140
|
-
selectedDate.value = { start: null, end: null };
|
|
141
|
-
selectedTime.value = { start: null, end: null };
|
|
142
|
-
} else {
|
|
143
|
-
selectedDate.value = null;
|
|
144
|
-
selectedTime.value = null;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Emits
|
|
149
|
-
const emit = defineEmits(['updated'])
|
|
150
|
-
|
|
151
|
-
// State
|
|
152
|
-
const showPopup = ref(false)
|
|
153
|
-
const selectedDate = ref(props.range ? { start: null, end: null } : null)
|
|
154
|
-
const selectedTime = ref(props.range ? { start: null, end: null } : null)
|
|
155
|
-
const currentMonth = ref(new Date().toLocaleString('default', { month: 'long' }))
|
|
156
|
-
const currentYear = ref(new Date().getFullYear())
|
|
157
|
-
const selectedHour = ref(props.range ? { start: 0, end: 0 } : 0);
|
|
158
|
-
const selectedMinute = ref(props.range ? { start: 0, end: 0 } : 0);
|
|
159
|
-
|
|
160
|
-
const pickerContainer = ref(null)
|
|
161
|
-
|
|
162
|
-
// Computed properties for IDs
|
|
163
|
-
const inputId = computed(() => `${props.id}-input`);
|
|
164
|
-
const popupId = computed(() => `${props.id}-popup`);
|
|
165
|
-
|
|
166
|
-
// Computed properties
|
|
167
|
-
const showDate = computed(() => props.mode === 'date' || props.mode === 'datetime')
|
|
168
|
-
const showTime = computed(() => props.mode === 'time' || props.mode === 'datetime')
|
|
169
|
-
const currentMonthYear = computed(() => new Date(currentYear.value, monthIndex.value))
|
|
170
|
-
const monthIndex = computed(() => months.indexOf(currentMonth.value))
|
|
171
|
-
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
172
|
-
const firstDay = computed(() => new Date(currentYear.value, monthIndex.value).getDay())
|
|
173
|
-
const daysInMonth = computed(() => {
|
|
174
|
-
const days = new Date(currentYear.value, monthIndex.value + 1, 0).getDate();
|
|
175
|
-
return Array.from({ length: days }, (_, i) => new Date(currentYear.value, monthIndex.value, i + 1));
|
|
176
|
-
});
|
|
177
|
-
const emptyDays = computed(() => Array.from({ length: firstDay.value }, (_, i) => i))
|
|
178
|
-
const hours = Array.from({ length: 24 }, (_, i) => i)
|
|
179
|
-
const minutes = Array.from({ length: 60 }, (_, i) => i)
|
|
180
|
-
const isMobile = computed(() =>
|
|
181
|
-
typeof navigator !== 'undefined' &&
|
|
182
|
-
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
183
|
-
navigator.userAgent
|
|
184
|
-
)
|
|
185
|
-
);
|
|
186
|
-
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
|
187
|
-
const years = computed(() => {
|
|
188
|
-
const currentYear = new Date().getFullYear()
|
|
189
|
-
return Array.from({ length: 100 }, (_, i) => currentYear - 50 + i)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const formattedDate = computed(() => {
|
|
193
|
-
if (!showDate.value || !selectedDate.value) return '';
|
|
194
|
-
|
|
195
|
-
if (props.range && selectedDate.value.start && selectedDate.value.end) {
|
|
196
|
-
const startDate = selectedDate.value.start.toLocaleDateString('en-US');
|
|
197
|
-
const endDate = selectedDate.value.end.toLocaleDateString('en-US');
|
|
198
|
-
return `${startDate} - ${endDate}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!props.range && selectedDate.value) {
|
|
202
|
-
return selectedDate.value.toLocaleDateString('en-US');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return '';
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const ensureDate = (d) => d instanceof Date ? d : new Date(d);
|
|
209
|
-
const isValidDate = (d) => d instanceof Date && !isNaN(d);
|
|
210
|
-
|
|
211
|
-
const formattedTime = computed(() => {
|
|
212
|
-
if (!showTime.value) return '';
|
|
213
|
-
|
|
214
|
-
const formatTime = (time) => {
|
|
215
|
-
const hour = selectedHour.value?.[time] ?? selectedHour.value;
|
|
216
|
-
const minute = selectedMinute.value?.[time] ?? selectedMinute.value;
|
|
217
|
-
return isMobile.value
|
|
218
|
-
? selectedTime.value?.[time] ?? selectedTime.value
|
|
219
|
-
: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
if (props.range) {
|
|
223
|
-
return `${formatTime('start')} - ${formatTime('end')}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return formatTime();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const formattedValue = computed(() => {
|
|
230
|
-
const date = formattedDate.value;
|
|
231
|
-
const time = formattedTime.value;
|
|
232
|
-
return `${date} ${time}`.trim();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Methods
|
|
236
|
-
const togglePopup = () => (showPopup.value = !showPopup.value)
|
|
237
|
-
|
|
238
|
-
const closeAndEmit = () => {
|
|
239
|
-
emitDateTimeChanged();
|
|
240
|
-
closePopup();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const closePopup = () => (showPopup.value = false)
|
|
244
|
-
|
|
245
|
-
const clearSelection = () => {
|
|
246
|
-
selectedDate.value = props.range ? { start: null, end: null } : null;
|
|
247
|
-
selectedTime.value = props.range ? { start: null, end: null } : null;
|
|
248
|
-
emitDateTimeChanged();
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const selectDate = (date) => {
|
|
252
|
-
if (props.range) {
|
|
253
|
-
if (selectedDate.value.start && selectedDate.value.end) {
|
|
254
|
-
selectedDate.value = { start: date, end: null }
|
|
255
|
-
} else if (!selectedDate.value.start) {
|
|
256
|
-
selectedDate.value.start = date
|
|
257
|
-
} else {
|
|
258
|
-
selectedDate.value.end = date
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
selectedDate.value = date
|
|
262
|
-
}
|
|
263
|
-
emitDateTimeChanged()
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const isDateSelected = (date) => {
|
|
267
|
-
date = ensureDate(date);
|
|
268
|
-
if (props.range) {
|
|
269
|
-
return (selectedDate.value.start && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime()) ||
|
|
270
|
-
(selectedDate.value.end && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime());
|
|
271
|
-
}
|
|
272
|
-
return isValidDate(selectedDate.value) && date.getTime() === selectedDate.value.getTime();
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const isToday = (date) => {
|
|
276
|
-
date = ensureDate(date);
|
|
277
|
-
const today = new Date();
|
|
278
|
-
return date.getDate() === today.getDate() &&
|
|
279
|
-
date.getMonth() === today.getMonth() &&
|
|
280
|
-
date.getFullYear() === today.getFullYear();
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const isRangeStart = (date) => {
|
|
284
|
-
date = ensureDate(date);
|
|
285
|
-
return props.range && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime();
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const isRangeEnd = (date) => {
|
|
289
|
-
date = ensureDate(date);
|
|
290
|
-
return props.range && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime();
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const isInDateRange = (date) => {
|
|
294
|
-
date = ensureDate(date);
|
|
295
|
-
if (props.range && isValidDate(selectedDate.value.start) && isValidDate(selectedDate.value.end)) {
|
|
296
|
-
const start = selectedDate.value.start.getTime();
|
|
297
|
-
const end = selectedDate.value.end.getTime();
|
|
298
|
-
const current = date.getTime();
|
|
299
|
-
return current > start && current < end;
|
|
300
|
-
}
|
|
301
|
-
return false;
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
// Month navigation
|
|
305
|
-
const previousMonth = () => {
|
|
306
|
-
let newMonth = monthIndex.value - 1
|
|
307
|
-
if (newMonth < 0) {
|
|
308
|
-
newMonth = 11
|
|
309
|
-
currentYear.value--
|
|
310
|
-
}
|
|
311
|
-
currentMonth.value = months[newMonth]
|
|
312
|
-
emitDateTimeChanged()
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const nextMonth = () => {
|
|
316
|
-
let newMonth = monthIndex.value + 1
|
|
317
|
-
if (newMonth > 11) {
|
|
318
|
-
newMonth = 0
|
|
319
|
-
currentYear.value++
|
|
320
|
-
}
|
|
321
|
-
currentMonth.value = months[newMonth]
|
|
322
|
-
emitDateTimeChanged()
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Close on outside click
|
|
326
|
-
const handleClickOutside = (event) => {
|
|
327
|
-
if (pickerContainer.value && !pickerContainer.value.contains(event.target) && showPopup.value == true) {
|
|
328
|
-
closeAndEmit()
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const emitDateTimeChanged = () => {
|
|
333
|
-
let result = {};
|
|
334
|
-
|
|
335
|
-
const formatUTCDate = (date, hour, minute) => {
|
|
336
|
-
if (!(date instanceof Date && !isNaN(date))) {
|
|
337
|
-
// If no date is provided, use today's date
|
|
338
|
-
date = new Date();
|
|
339
|
-
}
|
|
340
|
-
const newDate = new Date(date);
|
|
341
|
-
newDate.setUTCHours(hour, minute, 0, 0);
|
|
342
|
-
return newDate.toISOString();
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
if (props.range) {
|
|
346
|
-
result.from = formatUTCDate(
|
|
347
|
-
selectedDate.value.start,
|
|
348
|
-
selectedHour.value.start,
|
|
349
|
-
selectedMinute.value.start
|
|
350
|
-
);
|
|
351
|
-
result.to = formatUTCDate(
|
|
352
|
-
selectedDate.value.end,
|
|
353
|
-
selectedHour.value.end,
|
|
354
|
-
selectedMinute.value.end
|
|
355
|
-
);
|
|
356
|
-
} else {
|
|
357
|
-
result = formatUTCDate(
|
|
358
|
-
selectedDate.value,
|
|
359
|
-
selectedHour.value,
|
|
360
|
-
selectedMinute.value
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
emit('updated', result);
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
onMounted(() => {
|
|
368
|
-
// Initialize selectedDate from props.initialDate
|
|
369
|
-
if (props.initialDate) {
|
|
370
|
-
if (props.range && props.initialDate.start && props.initialDate.end) {
|
|
371
|
-
selectedDate.value = {
|
|
372
|
-
start: new Date(props.initialDate.start),
|
|
373
|
-
end: new Date(props.initialDate.end)
|
|
374
|
-
};
|
|
375
|
-
} else if (props.initialDate.start){
|
|
376
|
-
selectedDate.value = { start: new Date(props.initialDate.start) };
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Initialize selectedHour and selectedMinute if they are null in range mode
|
|
381
|
-
if (props.range) {
|
|
382
|
-
selectedHour.value.start = selectedHour.value.start ?? new Date().getHours();
|
|
383
|
-
selectedHour.value.end = selectedHour.value.end ?? new Date().getHours();
|
|
384
|
-
selectedMinute.value.start = selectedMinute.value.start ?? new Date().getMinutes();
|
|
385
|
-
selectedMinute.value.end = selectedMinute.value.end ?? new Date().getMinutes();
|
|
386
|
-
} else {
|
|
387
|
-
selectedHour.value = selectedHour.value ?? new Date().getHours();
|
|
388
|
-
selectedMinute.value = selectedMinute.value ?? new Date().getMinutes();
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
if(props.popup) {
|
|
393
|
-
onMounted(() => document.addEventListener('click', handleClickOutside))
|
|
394
|
-
}
|
|
395
|
-
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|
396
|
-
</script>
|
|
397
|
-
|
|
398
|
-
<style scoped>
|
|
399
|
-
.input {
|
|
400
|
-
cursor: pointer;
|
|
401
|
-
}
|
|
402
|
-
</style>
|