manolis-ui 1.1.1 → 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 +167 -21
- package/dist/runtime/components/data-input/datetimePicker.vue +162 -139
- package/dist/runtime/components/navigation/categoryNavigation.vue +30 -0
- package/dist/runtime/components/navigation/navigation-bar.vue +12 -5
- package/dist/runtime/composables/useLocalStorage.d.ts +1 -1
- package/package.json +2 -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,39 +1,185 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref, defineProps, defineAsyncComponent, nextTick } from 'vue';
|
|
2
3
|
import { Search } from 'lucide-vue-next';
|
|
3
|
-
import { ref } from 'vue';
|
|
4
4
|
|
|
5
|
+
interface Tab {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
type: "date" | "time" | "datetime";
|
|
9
|
+
range: boolean;
|
|
10
|
+
props?: Object;
|
|
11
|
+
value?: any;
|
|
12
|
+
}
|
|
5
13
|
|
|
6
14
|
interface Props {
|
|
7
|
-
|
|
15
|
+
searchOptions: Array<{ category: string; tabs: Tab[] }>;
|
|
16
|
+
currentCategory: string;
|
|
8
17
|
}
|
|
18
|
+
|
|
9
19
|
const props = defineProps<Props>();
|
|
10
|
-
const
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: 'search'): void;
|
|
22
|
+
(e: 'update:search-data', payload: { tab: string, data: any }): void;
|
|
23
|
+
}>();
|
|
24
|
+
const activeTab = ref<Tab | null>(null);
|
|
25
|
+
|
|
26
|
+
// Dynamic component loader
|
|
27
|
+
const componentMap = {
|
|
28
|
+
datetime: defineAsyncComponent(() => import('./datetimePicker.vue')),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Refs for tabs and popup positioning
|
|
32
|
+
const tabRefs = ref<Record<string, HTMLElement | null>>({});
|
|
33
|
+
const popupStyle = ref({ left: '0px', top: '0px' });
|
|
34
|
+
const searchContainer = ref<HTMLElement | null>(null);
|
|
35
|
+
|
|
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) {
|
|
62
|
+
if (searchContainer.value && !searchContainer.value.contains(event.target as Node)) {
|
|
63
|
+
await nextTick();
|
|
64
|
+
activeTab.value = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
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
|
+
|
|
81
|
+
async function openTab(tab: Tab) {
|
|
82
|
+
activeTab.value = tab;
|
|
83
|
+
await nextTick();
|
|
11
84
|
|
|
12
|
-
const
|
|
85
|
+
const tabElement = tabRefs.value[tab.name];
|
|
86
|
+
if (tabElement) {
|
|
87
|
+
const rect = tabElement.getBoundingClientRect();
|
|
88
|
+
const parentRect = searchContainer.value?.getBoundingClientRect() || { left: 0, top: 0 };
|
|
13
89
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.error('Tab element not found for:', tab.name);
|
|
106
|
+
}
|
|
17
107
|
}
|
|
18
108
|
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
onMounted(() => {
|
|
112
|
+
document.addEventListener('click', handleOutsideClick);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
onBeforeUnmount(() => {
|
|
116
|
+
document.removeEventListener('click', handleOutsideClick);
|
|
117
|
+
});
|
|
19
118
|
</script>
|
|
20
119
|
|
|
21
120
|
<template>
|
|
22
|
-
<div
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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">
|
|
126
|
+
<div class="tabs tabs-boxed bg-base-100 flex gap-4 w-full">
|
|
127
|
+
<button v-for="tab in props.searchOptions.find(opt => opt.category === props.currentCategory)?.tabs || []"
|
|
128
|
+
:key="tab.name" @click="openTab(tab)" :class="{ 'tab-active': activeTab?.name === tab.name }"
|
|
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-['']"
|
|
130
|
+
:ref="(el) => tabRefs[tab.name] = el">
|
|
131
|
+
<p class="text-sm">{{ tab.name }}</p>
|
|
132
|
+
<p class="text-xs opacity-35 truncate ... overflow-hidden md:block hidden">{{ tab.description }}</p>
|
|
32
133
|
</button>
|
|
33
|
-
|
|
34
|
-
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="tab-content absolute w-fit max-w-full flex mt-4 transition-all" v-if="activeTab" :style="popupStyle">
|
|
137
|
+
<component :is="componentMap[activeTab.type]" v-bind="activeTab.props ? activeTab.props : null"
|
|
138
|
+
@updated="componentValueUpdated" />
|
|
139
|
+
</div>
|
|
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="">
|
|
147
|
+
<Search :size="24" color="white" />
|
|
148
|
+
</button>
|
|
35
149
|
</div>
|
|
36
|
-
<!-- search button -->
|
|
37
|
-
<button title="search" type="submit" class="btn btn-primary btn-square ml-1"><Search :size="24" color="white" /></button>
|
|
38
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
|
+
|
|
39
181
|
</template>
|
|
182
|
+
|
|
183
|
+
<style scoped>
|
|
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}}
|
|
185
|
+
</style>
|
|
@@ -1,14 +1,14 @@
|
|
|
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
|
-
<slot>
|
|
4
|
+
<slot v-if="popup">
|
|
5
5
|
<input
|
|
6
6
|
type="text"
|
|
7
7
|
class="input input-bordered w-full cursor-pointer"
|
|
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" class="
|
|
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>
|
|
@@ -109,8 +95,8 @@
|
|
|
109
95
|
</div>
|
|
110
96
|
|
|
111
97
|
<div class="mt-4 flex gap-2">
|
|
112
|
-
<button class="btn btn-secondary w-fit" @click="clearSelection">Clear</button>
|
|
113
|
-
<button class="btn btn-primary btn-wide" @click="closeAndEmit">Close</button>
|
|
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>
|
|
114
100
|
</div>
|
|
115
101
|
</div>
|
|
116
102
|
</div>
|
|
@@ -134,14 +120,33 @@ const props = defineProps({
|
|
|
134
120
|
type: String,
|
|
135
121
|
default: 'Select date and time',
|
|
136
122
|
},
|
|
123
|
+
popup: {
|
|
124
|
+
type: Boolean,
|
|
125
|
+
default: false,
|
|
126
|
+
},
|
|
137
127
|
id: {
|
|
138
128
|
type: String,
|
|
139
|
-
default: 'datetimepicker',
|
|
129
|
+
default: 'datetimepicker',
|
|
130
|
+
},
|
|
131
|
+
initialDate: {
|
|
132
|
+
type: Object,
|
|
133
|
+
default: () => null
|
|
140
134
|
}
|
|
141
135
|
})
|
|
142
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
|
+
|
|
143
148
|
// Emits
|
|
144
|
-
const emit = defineEmits(['
|
|
149
|
+
const emit = defineEmits(['updated'])
|
|
145
150
|
|
|
146
151
|
// State
|
|
147
152
|
const showPopup = ref(false)
|
|
@@ -149,9 +154,8 @@ const selectedDate = ref(props.range ? { start: null, end: null } : null)
|
|
|
149
154
|
const selectedTime = ref(props.range ? { start: null, end: null } : null)
|
|
150
155
|
const currentMonth = ref(new Date().toLocaleString('default', { month: 'long' }))
|
|
151
156
|
const currentYear = ref(new Date().getFullYear())
|
|
152
|
-
const selectedHour = ref(props.range ? { start:
|
|
153
|
-
const selectedMinute = ref(props.range ? { start:
|
|
154
|
-
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);
|
|
155
159
|
|
|
156
160
|
const pickerContainer = ref(null)
|
|
157
161
|
|
|
@@ -163,9 +167,7 @@ const popupId = computed(() => `${props.id}-popup`);
|
|
|
163
167
|
const showDate = computed(() => props.mode === 'date' || props.mode === 'datetime')
|
|
164
168
|
const showTime = computed(() => props.mode === 'time' || props.mode === 'datetime')
|
|
165
169
|
const currentMonthYear = computed(() => new Date(currentYear.value, monthIndex.value))
|
|
166
|
-
const monthIndex = computed(() =>
|
|
167
|
-
return months.indexOf(currentMonth.value)
|
|
168
|
-
})
|
|
170
|
+
const monthIndex = computed(() => months.indexOf(currentMonth.value))
|
|
169
171
|
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
170
172
|
const firstDay = computed(() => new Date(currentYear.value, monthIndex.value).getDay())
|
|
171
173
|
const daysInMonth = computed(() => {
|
|
@@ -173,9 +175,9 @@ const daysInMonth = computed(() => {
|
|
|
173
175
|
return Array.from({ length: days }, (_, i) => new Date(currentYear.value, monthIndex.value, i + 1));
|
|
174
176
|
});
|
|
175
177
|
const emptyDays = computed(() => Array.from({ length: firstDay.value }, (_, i) => i))
|
|
176
|
-
const hours = Array.from({ length:
|
|
177
|
-
const minutes = Array.from({ length: 60 }, (_, i) => i
|
|
178
|
-
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'))
|
|
179
181
|
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
|
180
182
|
const years = computed(() => {
|
|
181
183
|
const currentYear = new Date().getFullYear()
|
|
@@ -186,52 +188,45 @@ const formattedDate = computed(() => {
|
|
|
186
188
|
if (!showDate.value || !selectedDate.value) return '';
|
|
187
189
|
|
|
188
190
|
if (props.range && selectedDate.value.start && selectedDate.value.end) {
|
|
189
|
-
const startDate = selectedDate.value.start.toLocaleDateString('en-
|
|
190
|
-
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');
|
|
191
193
|
return `${startDate} - ${endDate}`;
|
|
192
|
-
}
|
|
194
|
+
}
|
|
193
195
|
|
|
194
196
|
if (!props.range && selectedDate.value) {
|
|
195
|
-
return selectedDate.value.toLocaleDateString('en-
|
|
197
|
+
return selectedDate.value.toLocaleDateString('en-US');
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
return '';
|
|
199
201
|
});
|
|
200
202
|
|
|
203
|
+
const ensureDate = (d) => d instanceof Date ? d : new Date(d);
|
|
204
|
+
const isValidDate = (d) => d instanceof Date && !isNaN(d);
|
|
205
|
+
|
|
201
206
|
const formattedTime = computed(() => {
|
|
202
207
|
if (!showTime.value) return '';
|
|
203
208
|
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
: `${selectedHour.value.start}:${selectedMinute.value.start} ${selectedPeriod.value.start}`;
|
|
208
|
-
const formattedEndTime = isMobile.value
|
|
209
|
-
? selectedTime.value.end
|
|
210
|
-
: `<span class="math-inline">\{selectedHour\.value\.end\}\:</span>{selectedMinute.value.end} ${selectedPeriod.value.end}`;
|
|
211
|
-
return `${formattedStartTime} - ${formattedEndTime}`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
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;
|
|
215
212
|
return isMobile.value
|
|
216
|
-
? selectedTime.value
|
|
217
|
-
:
|
|
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')}`;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
return
|
|
221
|
+
return formatTime();
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
const formattedValue = computed(() => {
|
|
224
225
|
const date = formattedDate.value;
|
|
225
226
|
const time = formattedTime.value;
|
|
226
|
-
|
|
227
|
-
if (date && time) return `${date} ${time}`;
|
|
228
|
-
if (date) return date;
|
|
229
|
-
if (time) return time;
|
|
230
|
-
|
|
231
|
-
return '';
|
|
227
|
+
return `${date} ${time}`.trim();
|
|
232
228
|
});
|
|
233
229
|
|
|
234
|
-
|
|
235
230
|
// Methods
|
|
236
231
|
const togglePopup = () => (showPopup.value = !showPopup.value)
|
|
237
232
|
|
|
@@ -243,10 +238,10 @@ const closeAndEmit = () => {
|
|
|
243
238
|
const closePopup = () => (showPopup.value = false)
|
|
244
239
|
|
|
245
240
|
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
|
-
}
|
|
241
|
+
selectedDate.value = props.range ? { start: null, end: null } : null;
|
|
242
|
+
selectedTime.value = props.range ? { start: null, end: null } : null;
|
|
243
|
+
emitDateTimeChanged();
|
|
244
|
+
};
|
|
250
245
|
|
|
251
246
|
const selectDate = (date) => {
|
|
252
247
|
if (props.range) {
|
|
@@ -260,42 +255,46 @@ const selectDate = (date) => {
|
|
|
260
255
|
} else {
|
|
261
256
|
selectedDate.value = date
|
|
262
257
|
}
|
|
263
|
-
emitDateTimeChanged()
|
|
258
|
+
emitDateTimeChanged()
|
|
264
259
|
}
|
|
265
260
|
|
|
266
261
|
const isDateSelected = (date) => {
|
|
262
|
+
date = ensureDate(date);
|
|
267
263
|
if (props.range) {
|
|
268
|
-
return (selectedDate.value.start &&
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
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
|
+
};
|
|
273
269
|
|
|
274
270
|
const isToday = (date) => {
|
|
275
|
-
|
|
271
|
+
date = ensureDate(date);
|
|
272
|
+
const today = new Date();
|
|
276
273
|
return date.getDate() === today.getDate() &&
|
|
277
274
|
date.getMonth() === today.getMonth() &&
|
|
278
|
-
date.getFullYear() === today.getFullYear()
|
|
279
|
-
}
|
|
275
|
+
date.getFullYear() === today.getFullYear();
|
|
276
|
+
};
|
|
280
277
|
|
|
281
278
|
const isRangeStart = (date) => {
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
date = ensureDate(date);
|
|
280
|
+
return props.range && isValidDate(selectedDate.value.start) && date.getTime() === selectedDate.value.start.getTime();
|
|
281
|
+
};
|
|
284
282
|
|
|
285
283
|
const isRangeEnd = (date) => {
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
date = ensureDate(date);
|
|
285
|
+
return props.range && isValidDate(selectedDate.value.end) && date.getTime() === selectedDate.value.end.getTime();
|
|
286
|
+
};
|
|
288
287
|
|
|
289
288
|
const isInDateRange = (date) => {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
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;
|
|
295
295
|
}
|
|
296
|
-
return false
|
|
297
|
-
}
|
|
298
|
-
|
|
296
|
+
return false;
|
|
297
|
+
};
|
|
299
298
|
|
|
300
299
|
// Month navigation
|
|
301
300
|
const previousMonth = () => {
|
|
@@ -305,6 +304,7 @@ const previousMonth = () => {
|
|
|
305
304
|
currentYear.value--
|
|
306
305
|
}
|
|
307
306
|
currentMonth.value = months[newMonth]
|
|
307
|
+
emitDateTimeChanged()
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
const nextMonth = () => {
|
|
@@ -314,6 +314,7 @@ const nextMonth = () => {
|
|
|
314
314
|
currentYear.value++
|
|
315
315
|
}
|
|
316
316
|
currentMonth.value = months[newMonth]
|
|
317
|
+
emitDateTimeChanged()
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
// Close on outside click
|
|
@@ -324,49 +325,71 @@ const handleClickOutside = (event) => {
|
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
const emitDateTimeChanged = () => {
|
|
327
|
-
let
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const start = selectedDate.value.start?.toLocaleDateString('en-GB') || '';
|
|
334
|
-
const end = selectedDate.value.end?.toLocaleDateString('en-GB') || '';
|
|
335
|
-
dateStr = `${start} - ${end}`;
|
|
336
|
-
} else {
|
|
337
|
-
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();
|
|
338
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
|
+
);
|
|
339
357
|
}
|
|
340
358
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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) };
|
|
349
372
|
}
|
|
350
373
|
}
|
|
351
374
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
} else
|
|
359
|
-
|
|
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();
|
|
360
384
|
}
|
|
385
|
+
});
|
|
361
386
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
onMounted(() => document.addEventListener('click', handleClickOutside))
|
|
387
|
+
if(props.popup) {
|
|
388
|
+
onMounted(() => document.addEventListener('click', handleClickOutside))
|
|
389
|
+
}
|
|
367
390
|
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
|
368
391
|
</script>
|
|
369
392
|
|
|
370
393
|
<style scoped>
|
|
371
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}
|
|
372
|
-
</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
|
+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const useLocalStorage: <T>(key: string, initialValue: T) =>
|
|
1
|
+
export declare const useLocalStorage: <T>(key: string, initialValue: T) => any;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "manolis-ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "My new Nuxt module",
|
|
5
5
|
"repository": "manolis-trading/manolis-ui",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@nuxt/test-utils": "^3.14.4",
|
|
45
45
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
|
46
46
|
"@types/node": "latest",
|
|
47
|
+
"@types/vue": "^1.0.31",
|
|
47
48
|
"autoprefixer": "^10.4.20",
|
|
48
49
|
"changelogen": "^0.5.7",
|
|
49
50
|
"eslint": "^9.15.0",
|