manolis-ui 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -2
- package/dist/runtime/components/data-input/advancedSearch.vue +113 -54
- package/dist/runtime/components/data-input/datetimePicker.vue +151 -134
- package/dist/runtime/components/navigation/categoryNavigation.vue +30 -0
- package/dist/runtime/components/navigation/navigation-bar.vue +12 -5
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const __filename = __cjs_url__.fileURLToPath(import.meta.url);
|
|
|
13
13
|
const __dirname = __cjs_path__.dirname(__filename);
|
|
14
14
|
const require = __cjs_mod__.createRequire(import.meta.url);
|
|
15
15
|
const name = "manolis-ui";
|
|
16
|
-
const version = "1.1.
|
|
16
|
+
const version = "1.1.3";
|
|
17
17
|
|
|
18
18
|
function installTailwind(moduleOptions, nuxt = useNuxt(), resolve = createResolver(import.meta.url).resolve) {
|
|
19
19
|
const runtimeDir = resolve("./runtime");
|
|
@@ -110,7 +110,6 @@ const module = defineNuxtModule({
|
|
|
110
110
|
"data-input",
|
|
111
111
|
"feedback",
|
|
112
112
|
"layout",
|
|
113
|
-
"misc",
|
|
114
113
|
"navigation"
|
|
115
114
|
];
|
|
116
115
|
for (const dir of componentDirs) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, defineProps,
|
|
2
|
+
import { ref, defineProps, defineAsyncComponent, nextTick } from 'vue';
|
|
3
3
|
import { Search } from 'lucide-vue-next';
|
|
4
4
|
|
|
5
5
|
interface Tab {
|
|
@@ -8,31 +8,21 @@ interface Tab {
|
|
|
8
8
|
type: "date" | "time" | "datetime";
|
|
9
9
|
range: boolean;
|
|
10
10
|
props?: Object;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export interface SearchTab {
|
|
14
|
-
category: string;
|
|
15
|
-
tabs: Array<Tab>;
|
|
11
|
+
value?: any;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
interface Props {
|
|
19
|
-
searchOptions: Array<
|
|
15
|
+
searchOptions: Array<{ category: string; tabs: Tab[] }>;
|
|
16
|
+
currentCategory: string;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
const props =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const props = defineProps<Props>();
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: 'search'): void;
|
|
22
|
+
(e: 'update:search-data', payload: { tab: string, data: any }): void;
|
|
23
|
+
}>();
|
|
27
24
|
const activeTab = ref<Tab | null>(null);
|
|
28
25
|
|
|
29
|
-
const emit = defineEmits(['category-changed']);
|
|
30
|
-
|
|
31
|
-
function setCurrentCategory(category: string) {
|
|
32
|
-
currentCategory.value = category;
|
|
33
|
-
emit('category-changed', category);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
// Dynamic component loader
|
|
37
27
|
const componentMap = {
|
|
38
28
|
datetime: defineAsyncComponent(() => import('./datetimePicker.vue')),
|
|
@@ -41,39 +31,82 @@ const componentMap = {
|
|
|
41
31
|
// Refs for tabs and popup positioning
|
|
42
32
|
const tabRefs = ref<Record<string, HTMLElement | null>>({});
|
|
43
33
|
const popupStyle = ref({ left: '0px', top: '0px' });
|
|
44
|
-
|
|
45
34
|
const searchContainer = ref<HTMLElement | null>(null);
|
|
46
35
|
|
|
47
|
-
function
|
|
36
|
+
// made this function compatible with mobile and with desktop
|
|
37
|
+
function componentValueUpdated(data: any, currenTab: number) {
|
|
38
|
+
|
|
39
|
+
if (activeTab.value?.name) {
|
|
40
|
+
c('update:search-data', {
|
|
41
|
+
tab: activeTab.name,
|
|
42
|
+
data: data
|
|
43
|
+
})
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let _currentTab = props.searchOptions.find((option: { category: any; }) => option.category === props.currentCategory).tabs[currenTab]
|
|
48
|
+
_currentTab.value = data
|
|
49
|
+
emit('update:search-data', {
|
|
50
|
+
tab: _currentTab.name,
|
|
51
|
+
data: _currentTab.value
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function searchClicked() {
|
|
58
|
+
emit('search');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleOutsideClick(event: MouseEvent) {
|
|
48
62
|
if (searchContainer.value && !searchContainer.value.contains(event.target as Node)) {
|
|
63
|
+
await nextTick();
|
|
49
64
|
activeTab.value = null;
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
|
|
68
|
+
|
|
69
|
+
async function openMobileView() {
|
|
70
|
+
if (typeof window !== 'undefined' && window.innerWidth <= 768) {
|
|
71
|
+
const modal = document.getElementById('advancedSearchMobile') as HTMLDialogElement | null;
|
|
72
|
+
if (modal) {
|
|
73
|
+
modal.showModal();
|
|
74
|
+
} else {
|
|
75
|
+
console.warn('Modal element not found.');
|
|
76
|
+
}
|
|
77
|
+
// return; // Early return to avoid running desktop logic
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
async function openTab(tab: Tab) {
|
|
54
82
|
activeTab.value = tab;
|
|
55
|
-
await nextTick();
|
|
83
|
+
await nextTick();
|
|
56
84
|
|
|
57
85
|
const tabElement = tabRefs.value[tab.name];
|
|
58
86
|
if (tabElement) {
|
|
59
87
|
const rect = tabElement.getBoundingClientRect();
|
|
60
88
|
const parentRect = searchContainer.value?.getBoundingClientRect() || { left: 0, top: 0 };
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
|
|
90
|
+
// Calculate the popup position for centering on larger screens
|
|
91
|
+
if (window.innerWidth > 768) { // Desktop or tablet screen
|
|
92
|
+
popupStyle.value = {
|
|
93
|
+
left: `${rect.left + (rect.width / 2) - (parentRect.left)}px`,
|
|
94
|
+
top: `${rect.bottom - parentRect.top}px`,
|
|
95
|
+
transform: `translateX(-50%)`,
|
|
96
|
+
};
|
|
97
|
+
} else { // Mobile screen
|
|
98
|
+
popupStyle.value = {
|
|
99
|
+
left: '50%',
|
|
100
|
+
top: `${rect.bottom - parentRect.top}px`,
|
|
101
|
+
transform: `translateX(-50%)`, // Center the popup horizontally on mobile
|
|
102
|
+
};
|
|
103
|
+
}
|
|
68
104
|
} else {
|
|
69
105
|
console.error('Tab element not found for:', tab.name);
|
|
70
106
|
}
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
|
|
74
|
-
function setDynamicData(data: any) {
|
|
75
|
-
console.log(data);
|
|
76
|
-
}
|
|
77
110
|
|
|
78
111
|
onMounted(() => {
|
|
79
112
|
document.addEventListener('click', handleOutsideClick);
|
|
@@ -85,42 +118,68 @@ onBeforeUnmount(() => {
|
|
|
85
118
|
</script>
|
|
86
119
|
|
|
87
120
|
<template>
|
|
88
|
-
<div>
|
|
89
|
-
<!--
|
|
90
|
-
<div
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
:class="{ 'font-medium': searchOption.category === currentCategory }"
|
|
94
|
-
@click="setCurrentCategory(searchOption.category)">
|
|
95
|
-
{{ searchOption.category }}
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
<!-- Tab Navigation and Content -->
|
|
100
|
-
<div ref="searchContainer"
|
|
101
|
-
class="relative my-8 rounded border-2 border-opacity-25 group/search flex shadow-md transition-all p-2 place-items-center">
|
|
121
|
+
<div ref="searchContainer" class="w-full">
|
|
122
|
+
<!-- desktop/tablet -->
|
|
123
|
+
<div
|
|
124
|
+
class="relative rounded border-2 border-opacity-25 group/search flex shadow-md transition-all p-2 place-items-center w-full cursor-pointer md:cursor-auto"
|
|
125
|
+
@click="openMobileView">
|
|
102
126
|
<div class="tabs tabs-boxed bg-base-100 flex gap-4 w-full">
|
|
103
|
-
<button v-for="tab in props.searchOptions.find(opt => opt.category === currentCategory)?.tabs || []"
|
|
127
|
+
<button v-for="tab in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
|
|
104
128
|
:key="tab.name" @click="openTab(tab)" :class="{ 'tab-active': activeTab?.name === tab.name }"
|
|
105
|
-
class="group/searchitem text-start relative p-1 hover:bg-base-200 rounded"
|
|
129
|
+
class="group/searchitem pointer-events-none md:pointer-events-auto w-auto overflow-x-hidden text-start relative p-1 hover:bg-base-200 rounded after:h-10 after:bg-base-200 after:absolute after:-right-2 after:w-[1px] after:top-0 last-of-type:after:content-none last-of-type:flex-auto first-of-type:flex-auto after:content-none after:md:content-['']"
|
|
106
130
|
:ref="(el) => tabRefs[tab.name] = el">
|
|
107
131
|
<p class="text-sm">{{ tab.name }}</p>
|
|
108
|
-
<p class="text-xs opacity-35">{{ tab.description }}</p>
|
|
132
|
+
<p class="text-xs opacity-35 truncate ... overflow-hidden md:block hidden">{{ tab.description }}</p>
|
|
109
133
|
</button>
|
|
110
134
|
</div>
|
|
111
135
|
|
|
112
|
-
<!-- Dynamic Component Rendering -->
|
|
113
136
|
<div class="tab-content absolute w-fit max-w-full flex mt-4 transition-all" v-if="activeTab" :style="popupStyle">
|
|
114
|
-
<component :is="componentMap[activeTab.type]"
|
|
137
|
+
<component :is="componentMap[activeTab.type]" v-bind="activeTab.props ? activeTab.props : null"
|
|
138
|
+
@updated="componentValueUpdated" />
|
|
115
139
|
</div>
|
|
116
|
-
|
|
117
|
-
<button title="search" type="submit" class="btn btn-primary btn-square ml-1">
|
|
140
|
+
|
|
141
|
+
<button title="search" type="submit" class="btn btn-primary btn-square ml-1" @click="searchClicked">
|
|
142
|
+
<Search :size="24" color="white" />
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="hidden">
|
|
146
|
+
<button title="search" @click="">
|
|
118
147
|
<Search :size="24" color="white" />
|
|
119
148
|
</button>
|
|
120
149
|
</div>
|
|
121
150
|
</div>
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
<dialog id="advancedSearchMobile" class="modal">
|
|
154
|
+
<div class="modal-box">
|
|
155
|
+
<slot name="additionalForMobile">
|
|
156
|
+
<h3 class="text-lg font-bold">{{ currentCategory }}</h3>
|
|
157
|
+
</slot>
|
|
158
|
+
|
|
159
|
+
<div class="collapse bg-base-200 my-4"
|
|
160
|
+
v-for="(tab, index) in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
|
|
161
|
+
:key="tab.name">
|
|
162
|
+
<input type="radio" name="my-accordion-1" :checked="index === 0" />
|
|
163
|
+
<div class="collapse-title text-xl font-medium flex justify-between pr-4 items-center w-full">{{ tab.name }} <p class="text-sm">{{ tab.description }}</p></div>
|
|
164
|
+
<div class="collapse-content flex place-content-center p-0">
|
|
165
|
+
<br>
|
|
166
|
+
|
|
167
|
+
<component :is="componentMap[tab.type]" v-bind="tab.props || {}"
|
|
168
|
+
@updated="(data: any) => componentValueUpdated(data, index)" />
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="modal-action">
|
|
173
|
+
<form method="dialog">
|
|
174
|
+
<!-- if there is a button in form, it will close the modal -->
|
|
175
|
+
<button class="btn btn-primary" @click="searchClicked">Close and Search</button>
|
|
176
|
+
</form>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</dialog>
|
|
180
|
+
|
|
122
181
|
</template>
|
|
123
182
|
|
|
124
183
|
<style scoped>
|
|
125
|
-
.tabs button{cursor:pointer}.tab-content{position:absolute;transform-origin:top center;transition:opacity .3s ease;z-index:10}.tabs-boxed :is(.tab-active,[aria-selected=true]):not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){@apply bg-base-300 text-base-content}
|
|
184
|
+
.tabs button{cursor:pointer}.tab-content{position:absolute;transform-origin:top center;transition:opacity .3s ease;z-index:10}.tabs-boxed :is(.tab-active,[aria-selected=true]):not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){@apply bg-base-300 text-base-content}@media (max-width:768px){.tab-content{bottom:0;left:50%;top:auto;transform:translateX(-50%);width:100%}}@media (min-width:769px){.tab-content{left:unset;transform:unset;width:auto}}
|
|
126
185
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="relative inline-block" ref="pickerContainer">
|
|
2
|
+
<div class="relative inline-block w-full lg:w-[360px]" ref="pickerContainer">
|
|
3
3
|
<div class="flex items-center gap-2 cursor-pointer" @click="togglePopup">
|
|
4
4
|
<slot v-if="popup">
|
|
5
5
|
<input
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
:placeholder="placeholder"
|
|
9
9
|
:value="formattedValue"
|
|
10
10
|
readonly
|
|
11
|
-
:id="inputId"
|
|
11
|
+
:id="inputId"
|
|
12
12
|
/>
|
|
13
13
|
<button class="btn btn-ghost">
|
|
14
14
|
📅
|
|
@@ -16,23 +16,23 @@
|
|
|
16
16
|
</slot>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
|
-
<div v-if="showPopup || !popup" class="z-50
|
|
19
|
+
<div v-if="showPopup || !popup" class="z-50 rounded-md px-4 md:p-4 md:mt-2 w-full lg:bg-base-100" :class="[{ absolute: popup }]" :id="popupId">
|
|
20
20
|
<div v-if="showDate" class="flex items-center justify-between mb-4 place-content-center">
|
|
21
|
-
<button class="btn btn-sm btn-primary btn-outline" @click="previousMonth">
|
|
21
|
+
<button class="btn hidden md:block md:btn-sm btn-primary btn-outline" @click="previousMonth">
|
|
22
22
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
23
23
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
24
24
|
</svg>
|
|
25
25
|
</button>
|
|
26
26
|
|
|
27
|
-
<select class="select border-none w-fit" v-model="currentMonth" @change="emitDateTimeChanged">
|
|
27
|
+
<select class="select border-none w-fit" v-model="currentMonth" @change="emitDateTimeChanged">
|
|
28
28
|
<option v-for="(month, index) in months" :key="index" :value="month">{{ month }}</option>
|
|
29
29
|
</select>
|
|
30
30
|
|
|
31
|
-
<select class="select w-24 border-none" v-model="currentYear" @change="emitDateTimeChanged">
|
|
31
|
+
<select class="select w-24 border-none" v-model="currentYear" @change="emitDateTimeChanged">
|
|
32
32
|
<option v-for="year in years" :key="year" :value="year">{{ year }}</option>
|
|
33
33
|
</select>
|
|
34
34
|
|
|
35
|
-
<button class="btn btn-sm btn-primary btn-outline" @click="nextMonth">
|
|
35
|
+
<button class="btn hidden md:block md:btn-sm btn-primary btn-outline" @click="nextMonth">
|
|
36
36
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
37
37
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
38
38
|
</svg>
|
|
@@ -42,33 +42,27 @@
|
|
|
42
42
|
<div v-if="showDate" class="grid grid-cols-7 gap-2">
|
|
43
43
|
<div v-for="day in daysOfWeek" :key="day" class="text-center">{{ day }}</div>
|
|
44
44
|
<div v-for="emptyDay in emptyDays" :key="emptyDay" class="text-center"></div>
|
|
45
|
-
<div v-for="day in daysInMonth" :key="day" class="text-center cursor-pointer py-1 rounded-full hover:bg-primary/10"
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}"
|
|
53
|
-
@click="selectDate(day)">
|
|
45
|
+
<div v-for="day in daysInMonth" :key="day" class="text-center cursor-pointer py-1 rounded-full hover:bg-primary/10" :class="{
|
|
46
|
+
'bg-primary text-primary-content': isDateSelected(day),
|
|
47
|
+
'today': isToday(day),
|
|
48
|
+
'range-start': isRangeStart(day),
|
|
49
|
+
'range-end': isRangeEnd(day),
|
|
50
|
+
'in-range': isInDateRange(day),
|
|
51
|
+
}" @click="selectDate(day)">
|
|
54
52
|
{{ day.getDate() }}
|
|
55
53
|
</div>
|
|
56
54
|
</div>
|
|
57
55
|
|
|
58
|
-
<div v-if="showTime" class="mt-4">
|
|
59
|
-
<h3 class="text-lg font-bold mb-2">Select Time</h3>
|
|
56
|
+
<div v-if="showTime" class="mt-4 lg:w-80">
|
|
57
|
+
<h3 class="text-lg font-bold mb-2" v-if="!props.range">Select Time</h3>
|
|
60
58
|
<div v-if="!props.range">
|
|
61
59
|
<input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime" @change="emitDateTimeChanged" />
|
|
62
60
|
<div v-else class="flex gap-4">
|
|
63
|
-
<select class="select select-bordered w-full" v-model="selectedHour" @change="emitDateTimeChanged">
|
|
64
|
-
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
|
|
61
|
+
<select class="select select-bordered w-full" v-model.number="selectedHour" @change="emitDateTimeChanged">
|
|
62
|
+
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
|
|
65
63
|
</select>
|
|
66
|
-
<select class="select select-bordered w-full" v-model="selectedMinute" @change="emitDateTimeChanged">
|
|
67
|
-
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
|
|
68
|
-
</select>
|
|
69
|
-
<select class="select select-bordered w-full" v-model="selectedPeriod" @change="emitDateTimeChanged">
|
|
70
|
-
<option value="AM">AM</option>
|
|
71
|
-
<option value="PM">PM</option>
|
|
64
|
+
<select class="select select-bordered w-full" v-model.number="selectedMinute" @change="emitDateTimeChanged">
|
|
65
|
+
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
|
|
72
66
|
</select>
|
|
73
67
|
</div>
|
|
74
68
|
</div>
|
|
@@ -77,15 +71,11 @@
|
|
|
77
71
|
<p class="font-bold">Start Time</p>
|
|
78
72
|
<input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime.start" @change="emitDateTimeChanged" />
|
|
79
73
|
<div v-else class="flex gap-2">
|
|
80
|
-
<select class="select select-bordered w-full" v-model="selectedHour.start" @change="emitDateTimeChanged">
|
|
81
|
-
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
|
|
82
|
-
</select>
|
|
83
|
-
<select class="select select-bordered w-full" v-model="selectedMinute.start" @change="emitDateTimeChanged">
|
|
84
|
-
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
|
|
74
|
+
<select class="select select-bordered w-full" v-model.number="selectedHour.start" @change="emitDateTimeChanged">
|
|
75
|
+
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
|
|
85
76
|
</select>
|
|
86
|
-
<select class="select select-bordered w-full" v-model="
|
|
87
|
-
<option value="
|
|
88
|
-
<option value="PM">PM</option>
|
|
77
|
+
<select class="select select-bordered w-full" v-model.number="selectedMinute.start" @change="emitDateTimeChanged">
|
|
78
|
+
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
|
|
89
79
|
</select>
|
|
90
80
|
</div>
|
|
91
81
|
</div>
|
|
@@ -93,15 +83,11 @@
|
|
|
93
83
|
<p class="font-bold">End Time</p>
|
|
94
84
|
<input v-if="isMobile" type="time" class="input input-bordered w-full" v-model="selectedTime.end" @change="emitDateTimeChanged" />
|
|
95
85
|
<div v-else class="flex gap-2">
|
|
96
|
-
<select class="select select-bordered w-full" v-model="selectedHour.end" @change="emitDateTimeChanged">
|
|
97
|
-
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour }}</option>
|
|
86
|
+
<select class="select select-bordered w-full" v-model.number="selectedHour.end" @change="emitDateTimeChanged">
|
|
87
|
+
<option v-for="hour in hours" :key="hour" :value="hour">{{ hour.toString().padStart(2, '0') }}</option>
|
|
98
88
|
</select>
|
|
99
|
-
<select class="select select-bordered w-full" v-model="selectedMinute.end" @change="emitDateTimeChanged">
|
|
100
|
-
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute }}</option>
|
|
101
|
-
</select>
|
|
102
|
-
<select class="select select-bordered w-full" v-model="selectedPeriod.end" @change="emitDateTimeChanged">
|
|
103
|
-
<option value="AM">AM</option>
|
|
104
|
-
<option value="PM">PM</option>
|
|
89
|
+
<select class="select select-bordered w-full" v-model.number="selectedMinute.end" @change="emitDateTimeChanged">
|
|
90
|
+
<option v-for="minute in minutes" :key="minute" :value="minute">{{ minute.toString().padStart(2, '0') }}</option>
|
|
105
91
|
</select>
|
|
106
92
|
</div>
|
|
107
93
|
</div>
|
|
@@ -140,10 +126,25 @@ const props = defineProps({
|
|
|
140
126
|
},
|
|
141
127
|
id: {
|
|
142
128
|
type: String,
|
|
143
|
-
default: 'datetimepicker',
|
|
129
|
+
default: 'datetimepicker',
|
|
130
|
+
},
|
|
131
|
+
initialDate: {
|
|
132
|
+
type: Object,
|
|
133
|
+
default: () => null
|
|
144
134
|
}
|
|
145
135
|
})
|
|
146
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
|
+
|
|
147
148
|
// Emits
|
|
148
149
|
const emit = defineEmits(['updated'])
|
|
149
150
|
|
|
@@ -153,9 +154,8 @@ const selectedDate = ref(props.range ? { start: null, end: null } : null)
|
|
|
153
154
|
const selectedTime = ref(props.range ? { start: null, end: null } : null)
|
|
154
155
|
const currentMonth = ref(new Date().toLocaleString('default', { month: 'long' }))
|
|
155
156
|
const currentYear = ref(new Date().getFullYear())
|
|
156
|
-
const selectedHour = ref(props.range ? { start:
|
|
157
|
-
const selectedMinute = ref(props.range ? { start:
|
|
158
|
-
const selectedPeriod = ref(props.range ? { start: new Date().getHours() < 12 ? 'AM' : 'PM', end: new Date().getHours() < 12 ? 'AM' : 'PM' } : new Date().getHours() < 12 ? 'AM' : 'PM')
|
|
157
|
+
const selectedHour = ref(props.range ? { start: 0, end: 0 } : 0);
|
|
158
|
+
const selectedMinute = ref(props.range ? { start: 0, end: 0 } : 0);
|
|
159
159
|
|
|
160
160
|
const pickerContainer = ref(null)
|
|
161
161
|
|
|
@@ -167,9 +167,7 @@ const popupId = computed(() => `${props.id}-popup`);
|
|
|
167
167
|
const showDate = computed(() => props.mode === 'date' || props.mode === 'datetime')
|
|
168
168
|
const showTime = computed(() => props.mode === 'time' || props.mode === 'datetime')
|
|
169
169
|
const currentMonthYear = computed(() => new Date(currentYear.value, monthIndex.value))
|
|
170
|
-
const monthIndex = computed(() =>
|
|
171
|
-
return months.indexOf(currentMonth.value)
|
|
172
|
-
})
|
|
170
|
+
const monthIndex = computed(() => months.indexOf(currentMonth.value))
|
|
173
171
|
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
174
172
|
const firstDay = computed(() => new Date(currentYear.value, monthIndex.value).getDay())
|
|
175
173
|
const daysInMonth = computed(() => {
|
|
@@ -177,9 +175,9 @@ const daysInMonth = computed(() => {
|
|
|
177
175
|
return Array.from({ length: days }, (_, i) => new Date(currentYear.value, monthIndex.value, i + 1));
|
|
178
176
|
});
|
|
179
177
|
const emptyDays = computed(() => Array.from({ length: firstDay.value }, (_, i) => i))
|
|
180
|
-
const hours = Array.from({ length:
|
|
181
|
-
const minutes = Array.from({ length: 60 }, (_, i) => i
|
|
182
|
-
const isMobile = computed(() => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
|
|
178
|
+
const hours = Array.from({ length: 24 }, (_, i) => i)
|
|
179
|
+
const minutes = Array.from({ length: 60 }, (_, i) => i)
|
|
180
|
+
const isMobile = computed(() => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(process.client ? navigator.userAgent : 'nope'))
|
|
183
181
|
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
|
184
182
|
const years = computed(() => {
|
|
185
183
|
const currentYear = new Date().getFullYear()
|
|
@@ -190,52 +188,45 @@ const formattedDate = computed(() => {
|
|
|
190
188
|
if (!showDate.value || !selectedDate.value) return '';
|
|
191
189
|
|
|
192
190
|
if (props.range && selectedDate.value.start && selectedDate.value.end) {
|
|
193
|
-
const startDate = selectedDate.value.start.toLocaleDateString('en-
|
|
194
|
-
const endDate = selectedDate.value.end.toLocaleDateString('en-
|
|
191
|
+
const startDate = selectedDate.value.start.toLocaleDateString('en-US');
|
|
192
|
+
const endDate = selectedDate.value.end.toLocaleDateString('en-US');
|
|
195
193
|
return `${startDate} - ${endDate}`;
|
|
196
|
-
}
|
|
194
|
+
}
|
|
197
195
|
|
|
198
196
|
if (!props.range && selectedDate.value) {
|
|
199
|
-
return selectedDate.value.toLocaleDateString('en-
|
|
197
|
+
return selectedDate.value.toLocaleDateString('en-US');
|
|
200
198
|
}
|
|
201
199
|
|
|
202
200
|
return '';
|
|
203
201
|
});
|
|
204
202
|
|
|
203
|
+
const ensureDate = (d) => d instanceof Date ? d : new Date(d);
|
|
204
|
+
const isValidDate = (d) => d instanceof Date && !isNaN(d);
|
|
205
|
+
|
|
205
206
|
const formattedTime = computed(() => {
|
|
206
207
|
if (!showTime.value) return '';
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
: `${selectedHour.value.start}:${selectedMinute.value.start} ${selectedPeriod.value.start}`;
|
|
212
|
-
const formattedEndTime = isMobile.value
|
|
213
|
-
? selectedTime.value.end
|
|
214
|
-
: `<span class="math-inline">\{selectedHour\.value\.end\}\:</span>{selectedMinute.value.end} ${selectedPeriod.value.end}`;
|
|
215
|
-
return `${formattedStartTime} - ${formattedEndTime}`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!props.range && selectedTime.value) {
|
|
209
|
+
const formatTime = (time) => {
|
|
210
|
+
const hour = selectedHour.value?.[time] ?? selectedHour.value;
|
|
211
|
+
const minute = selectedMinute.value?.[time] ?? selectedMinute.value;
|
|
219
212
|
return isMobile.value
|
|
220
|
-
? selectedTime.value
|
|
221
|
-
:
|
|
213
|
+
? selectedTime.value?.[time] ?? selectedTime.value
|
|
214
|
+
: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (props.range) {
|
|
218
|
+
return `${formatTime('start')} - ${formatTime('end')}`;
|
|
222
219
|
}
|
|
223
220
|
|
|
224
|
-
return
|
|
221
|
+
return formatTime();
|
|
225
222
|
});
|
|
226
223
|
|
|
227
224
|
const formattedValue = computed(() => {
|
|
228
225
|
const date = formattedDate.value;
|
|
229
226
|
const time = formattedTime.value;
|
|
230
|
-
|
|
231
|
-
if (date && time) return `${date} ${time}`;
|
|
232
|
-
if (date) return date;
|
|
233
|
-
if (time) return time;
|
|
234
|
-
|
|
235
|
-
return '';
|
|
227
|
+
return `${date} ${time}`.trim();
|
|
236
228
|
});
|
|
237
229
|
|
|
238
|
-
|
|
239
230
|
// Methods
|
|
240
231
|
const togglePopup = () => (showPopup.value = !showPopup.value)
|
|
241
232
|
|
|
@@ -247,10 +238,10 @@ const closeAndEmit = () => {
|
|
|
247
238
|
const closePopup = () => (showPopup.value = false)
|
|
248
239
|
|
|
249
240
|
const clearSelection = () => {
|
|
250
|
-
selectedDate.value = props.range ? { start: null, end: null } : null
|
|
251
|
-
selectedTime.value = props.range ? { start: null, end: null } : null
|
|
252
|
-
emitDateTimeChanged()
|
|
253
|
-
}
|
|
241
|
+
selectedDate.value = props.range ? { start: null, end: null } : null;
|
|
242
|
+
selectedTime.value = props.range ? { start: null, end: null } : null;
|
|
243
|
+
emitDateTimeChanged();
|
|
244
|
+
};
|
|
254
245
|
|
|
255
246
|
const selectDate = (date) => {
|
|
256
247
|
if (props.range) {
|
|
@@ -264,42 +255,46 @@ const selectDate = (date) => {
|
|
|
264
255
|
} else {
|
|
265
256
|
selectedDate.value = date
|
|
266
257
|
}
|
|
267
|
-
emitDateTimeChanged()
|
|
258
|
+
emitDateTimeChanged()
|
|
268
259
|
}
|
|
269
260
|
|
|
270
261
|
const isDateSelected = (date) => {
|
|
262
|
+
date = ensureDate(date);
|
|
271
263
|
if (props.range) {
|
|
272
|
-
return (selectedDate.value.start &&
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
264
|
+
return (selectedDate.value.start && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime()) ||
|
|
265
|
+
(selectedDate.value.end && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime());
|
|
266
|
+
}
|
|
267
|
+
return isValidDate(selectedDate.value) && date.getTime() === selectedDate.value.getTime();
|
|
268
|
+
};
|
|
277
269
|
|
|
278
270
|
const isToday = (date) => {
|
|
279
|
-
|
|
271
|
+
date = ensureDate(date);
|
|
272
|
+
const today = new Date();
|
|
280
273
|
return date.getDate() === today.getDate() &&
|
|
281
274
|
date.getMonth() === today.getMonth() &&
|
|
282
|
-
date.getFullYear() === today.getFullYear()
|
|
283
|
-
}
|
|
275
|
+
date.getFullYear() === today.getFullYear();
|
|
276
|
+
};
|
|
284
277
|
|
|
285
278
|
const isRangeStart = (date) => {
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
date = ensureDate(date);
|
|
280
|
+
return props.range && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime();
|
|
281
|
+
};
|
|
288
282
|
|
|
289
283
|
const isRangeEnd = (date) => {
|
|
290
|
-
|
|
291
|
-
|
|
284
|
+
date = ensureDate(date);
|
|
285
|
+
return props.range && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime();
|
|
286
|
+
};
|
|
292
287
|
|
|
293
288
|
const isInDateRange = (date) => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
289
|
+
date = ensureDate(date);
|
|
290
|
+
if (props.range && isValidDate(selectedDate.value.start) && isValidDate(selectedDate.value.end)) {
|
|
291
|
+
const start = selectedDate.value.start.getTime();
|
|
292
|
+
const end = selectedDate.value.end.getTime();
|
|
293
|
+
const current = date.getTime();
|
|
294
|
+
return current > start && current < end;
|
|
299
295
|
}
|
|
300
|
-
return false
|
|
301
|
-
}
|
|
302
|
-
|
|
296
|
+
return false;
|
|
297
|
+
};
|
|
303
298
|
|
|
304
299
|
// Month navigation
|
|
305
300
|
const previousMonth = () => {
|
|
@@ -309,6 +304,7 @@ const previousMonth = () => {
|
|
|
309
304
|
currentYear.value--
|
|
310
305
|
}
|
|
311
306
|
currentMonth.value = months[newMonth]
|
|
307
|
+
emitDateTimeChanged()
|
|
312
308
|
}
|
|
313
309
|
|
|
314
310
|
const nextMonth = () => {
|
|
@@ -318,6 +314,7 @@ const nextMonth = () => {
|
|
|
318
314
|
currentYear.value++
|
|
319
315
|
}
|
|
320
316
|
currentMonth.value = months[newMonth]
|
|
317
|
+
emitDateTimeChanged()
|
|
321
318
|
}
|
|
322
319
|
|
|
323
320
|
// Close on outside click
|
|
@@ -328,44 +325,64 @@ const handleClickOutside = (event) => {
|
|
|
328
325
|
}
|
|
329
326
|
|
|
330
327
|
const emitDateTimeChanged = () => {
|
|
331
|
-
let
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const start = selectedDate.value.start?.toLocaleDateString('en-GB') || '';
|
|
338
|
-
const end = selectedDate.value.end?.toLocaleDateString('en-GB') || '';
|
|
339
|
-
dateStr = `${start} - ${end}`;
|
|
340
|
-
} else {
|
|
341
|
-
dateStr = selectedDate.value.toLocaleDateString('en-GB');
|
|
328
|
+
let result = {};
|
|
329
|
+
|
|
330
|
+
const formatUTCDate = (date, hour, minute) => {
|
|
331
|
+
if (!(date instanceof Date && !isNaN(date))) {
|
|
332
|
+
// If no date is provided, use today's date
|
|
333
|
+
date = new Date();
|
|
342
334
|
}
|
|
335
|
+
const newDate = new Date(date);
|
|
336
|
+
newDate.setUTCHours(hour, minute, 0, 0);
|
|
337
|
+
return newDate.toISOString();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (props.range) {
|
|
341
|
+
result.from = formatUTCDate(
|
|
342
|
+
selectedDate.value.start,
|
|
343
|
+
selectedHour.value.start,
|
|
344
|
+
selectedMinute.value.start
|
|
345
|
+
);
|
|
346
|
+
result.to = formatUTCDate(
|
|
347
|
+
selectedDate.value.end,
|
|
348
|
+
selectedHour.value.end,
|
|
349
|
+
selectedMinute.value.end
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
result = formatUTCDate(
|
|
353
|
+
selectedDate.value,
|
|
354
|
+
selectedHour.value,
|
|
355
|
+
selectedMinute.value
|
|
356
|
+
);
|
|
343
357
|
}
|
|
344
358
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
359
|
+
emit('updated', result);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
onMounted(() => {
|
|
363
|
+
// Initialize selectedDate from props.initialDate
|
|
364
|
+
if (props.initialDate) {
|
|
365
|
+
if (props.range && props.initialDate.start && props.initialDate.end) {
|
|
366
|
+
selectedDate.value = {
|
|
367
|
+
start: new Date(props.initialDate.start),
|
|
368
|
+
end: new Date(props.initialDate.end)
|
|
369
|
+
};
|
|
370
|
+
} else if (props.initialDate.start){
|
|
371
|
+
selectedDate.value = { start: new Date(props.initialDate.start) };
|
|
353
372
|
}
|
|
354
373
|
}
|
|
355
374
|
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
} else
|
|
363
|
-
|
|
375
|
+
// Initialize selectedHour and selectedMinute if they are null in range mode
|
|
376
|
+
if (props.range) {
|
|
377
|
+
selectedHour.value.start = selectedHour.value.start ?? new Date().getHours();
|
|
378
|
+
selectedHour.value.end = selectedHour.value.end ?? new Date().getHours();
|
|
379
|
+
selectedMinute.value.start = selectedMinute.value.start ?? new Date().getMinutes();
|
|
380
|
+
selectedMinute.value.end = selectedMinute.value.end ?? new Date().getMinutes();
|
|
381
|
+
} else {
|
|
382
|
+
selectedHour.value = selectedHour.value ?? new Date().getHours();
|
|
383
|
+
selectedMinute.value = selectedMinute.value ?? new Date().getMinutes();
|
|
364
384
|
}
|
|
365
|
-
|
|
366
|
-
// Emit the formatted string
|
|
367
|
-
emit('updated', result.trim());
|
|
368
|
-
};
|
|
385
|
+
});
|
|
369
386
|
|
|
370
387
|
if(props.popup) {
|
|
371
388
|
onMounted(() => document.addEventListener('click', handleClickOutside))
|
|
@@ -375,4 +392,4 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|
|
375
392
|
|
|
376
393
|
<style scoped>
|
|
377
394
|
.input{cursor:pointer}.in-range{@apply bg-primary/20}.in-range:hover{@apply bg-primary/10}.range-end,.range-start{@apply bg-primary text-primary-content}
|
|
378
|
-
</style>
|
|
395
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defineProps, defineEmits } from 'vue';
|
|
3
|
+
|
|
4
|
+
interface Category {
|
|
5
|
+
category: string;
|
|
6
|
+
tabs: Tab[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
searchOptions: Array<Category>;
|
|
11
|
+
currentCategory: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = defineProps<Props>();
|
|
15
|
+
const emit = defineEmits<{ (e: 'update:currentCategory', category: string): void }>();
|
|
16
|
+
|
|
17
|
+
function changeCategory(category: string) {
|
|
18
|
+
emit('update:currentCategory', category);
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="categories flex flex-row gap-4 place-content-center">
|
|
24
|
+
<div v-for="category in props.searchOptions" :key="category.category">
|
|
25
|
+
<button @click="changeCategory(category.category)" class="truncate" :class="{ 'font-semibold': props.currentCategory === category.category }">
|
|
26
|
+
{{ category.category }}
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
<script lang="ts" setup></script>
|
|
2
2
|
|
|
3
3
|
<template>
|
|
4
|
-
<nav class="navbar border-b-4 border-primary px-5 py-5 place-items-start">
|
|
5
|
-
<div class="navbar-start flex
|
|
4
|
+
<nav class="navbar border-b-4 border-primary px-5 py-5 place-items-start transition-all">
|
|
5
|
+
<div class="navbar-start hidden md:flex">
|
|
6
6
|
<slot name="start"></slot>
|
|
7
7
|
</div>
|
|
8
|
-
|
|
9
|
-
<div class="navbar-center
|
|
8
|
+
|
|
9
|
+
<div class="navbar-center hidden md:flex justify-center items-center">
|
|
10
10
|
<slot name="center"></slot>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
<div class="navbar-end flex
|
|
13
|
+
<div class="navbar-end md:flex hidden">
|
|
14
14
|
<slot name="end"></slot>
|
|
15
15
|
</div>
|
|
16
|
+
<div class="navbar-bottom md:flex hidden">
|
|
17
|
+
<slot name="bottom"></slot>
|
|
18
|
+
</div>
|
|
16
19
|
</nav>
|
|
17
20
|
</template>
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
<style scoped>
|
|
24
|
+
.navbar{display:grid;gap:0;grid-template-areas:"center center center" "bottom bottom bottom";grid-template-columns:auto auto auto;grid-template-rows:auto auto;width:100%}@media (min-width:768px){.navbar{grid-row-gap:4rem}}.navbar-start{grid-area:start;height:100%}.navbar-center{grid-area:center;height:100%;width:100%}.navbar-end{grid-area:end;height:100%;width:100%}.navbar-bottom{align-items:center;display:flex;grid-area:center;justify-content:center;width:100%}@media (min-width:768px){.navbar-bottom{grid-area:bottom}.navbar{grid-template-areas:"start center end" "bottom bottom bottom";grid-template-columns:.7fr 1.6fr .7fr}}
|
|
25
|
+
</style>
|
|
26
|
+
|