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.
Files changed (117) hide show
  1. package/package.json +6 -3
  2. package/.github/workflows/deploy_and_publish.yml +0 -113
  3. package/.storybook/main.ts +0 -34
  4. package/.storybook/preview.ts +0 -17
  5. package/.vscode/extensions.json +0 -3
  6. package/index.html +0 -13
  7. package/postcss.config.js +0 -2
  8. package/public/vite.svg +0 -1
  9. package/src/App.vue +0 -19
  10. package/src/assets/vue.svg +0 -1
  11. package/src/components/actions/ButtonComponent.vue +0 -80
  12. package/src/components/actions/dropdown.vue +0 -46
  13. package/src/components/actions/modal.vue +0 -52
  14. package/src/components/actions/swap.vue +0 -15
  15. package/src/components/actions/theme-controller.vue +0 -52
  16. package/src/components/data-display/accordion.vue +0 -29
  17. package/src/components/data-display/avatar.vue +0 -36
  18. package/src/components/data-display/badge.vue +0 -35
  19. package/src/components/data-display/card.vue +0 -60
  20. package/src/components/data-display/carousel.vue +0 -34
  21. package/src/components/data-input/advancedSearch.vue +0 -227
  22. package/src/components/data-input/datetimePicker.vue +0 -402
  23. package/src/components/data-input/input.vue +0 -98
  24. package/src/components/data-input/rating.vue +0 -60
  25. package/src/components/feedback/loader.vue +0 -25
  26. package/src/components/layout/footer.vue +0 -38
  27. package/src/components/layout/hero.vue +0 -15
  28. package/src/components/navigation/categoryNavigation.vue +0 -40
  29. package/src/components/navigation/navigationBar.vue +0 -107
  30. package/src/components/navigation/tab.vue +0 -62
  31. package/src/composables/useLocalStorage.ts +0 -24
  32. package/src/index.ts +0 -30
  33. package/src/main.ts +0 -5
  34. package/src/stories/actions/Button.stories.ts +0 -47
  35. package/src/stories/actions/Dropdown.stories.ts +0 -70
  36. package/src/stories/actions/Modal.stories.ts +0 -56
  37. package/src/stories/actions/Swap.stories.ts +0 -56
  38. package/src/stories/actions/ThemeSwitcher.stories.ts +0 -41
  39. package/src/stories/data-display/accordion.stories.ts +0 -49
  40. package/src/stories/data-display/avatar.stories.ts +0 -75
  41. package/src/stories/data-display/badge.stories.ts +0 -76
  42. package/src/stories/data-display/card.stories.ts +0 -79
  43. package/src/stories/data-input/rating.stories.ts +0 -73
  44. package/src/stories/feedback/Loader.stories.ts +0 -34
  45. package/src/stories/layout/footer.stories.ts +0 -63
  46. package/src/style.css +0 -57
  47. package/src/vite-env.d.ts +0 -1
  48. package/storybook-static/assets/Button.stories-B5Gg7Ski.js +0 -6
  49. package/storybook-static/assets/Color-YHDXOIA2-Cy_mA6cn.js +0 -1
  50. package/storybook-static/assets/DocsRenderer-CFRXHY34-wSGN0bIp.js +0 -610
  51. package/storybook-static/assets/Dropdown.stories-Bth3_21L.js +0 -32
  52. package/storybook-static/assets/Loader.stories-BnqtyQP_.js +0 -5
  53. package/storybook-static/assets/Modal.stories-CxOA4msz.js +0 -46
  54. package/storybook-static/assets/Swap.stories-Cpc9q_kE.js +0 -54
  55. package/storybook-static/assets/ThemeSwitcher.stories-BwHcHihM.js +0 -45
  56. package/storybook-static/assets/accordion.stories-B6yDsDXk.js +0 -7
  57. package/storybook-static/assets/avatar.stories-BDN93iYh.js +0 -39
  58. package/storybook-static/assets/badge.stories-CXQpnu0e.js +0 -39
  59. package/storybook-static/assets/card.stories-1gVWO2fs.js +0 -48
  60. package/storybook-static/assets/entry-preview-Cfvj9hgI.js +0 -1
  61. package/storybook-static/assets/entry-preview-docs-BJQT5BWv.js +0 -16
  62. package/storybook-static/assets/footer.stories-DPXqApht.js +0 -23
  63. package/storybook-static/assets/iframe-BNdG_Qtn.js +0 -211
  64. package/storybook-static/assets/index-Bx-go_-4.js +0 -8
  65. package/storybook-static/assets/index-CiNYFPF0.js +0 -1
  66. package/storybook-static/assets/index-DrFu-skq.js +0 -6
  67. package/storybook-static/assets/preview-4lzcCKUM.css +0 -1
  68. package/storybook-static/assets/preview-B8lJiyuQ.js +0 -34
  69. package/storybook-static/assets/preview-BBWR9nbA.js +0 -1
  70. package/storybook-static/assets/preview-BWzBA1C2.js +0 -396
  71. package/storybook-static/assets/preview-CvbIS5ZJ.js +0 -1
  72. package/storybook-static/assets/preview-DD_OYowb.js +0 -1
  73. package/storybook-static/assets/preview-DGUiP6tS.js +0 -7
  74. package/storybook-static/assets/preview-DHQbi4pV.js +0 -1
  75. package/storybook-static/assets/preview-DMNI4LCC.js +0 -15
  76. package/storybook-static/assets/preview-DnqJFqn_.js +0 -2
  77. package/storybook-static/assets/preview-Dsq_8SDT.js +0 -240
  78. package/storybook-static/assets/preview-hHK5u5_Q.js +0 -1
  79. package/storybook-static/assets/rating.stories-BX0Pzp5i.js +0 -27
  80. package/storybook-static/assets/vue.esm-bundler-C-YazFc_.js +0 -36
  81. package/storybook-static/favicon.svg +0 -1
  82. package/storybook-static/iframe.html +0 -666
  83. package/storybook-static/index.html +0 -181
  84. package/storybook-static/index.json +0 -1
  85. package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
  86. package/storybook-static/nunito-sans-bold.woff2 +0 -0
  87. package/storybook-static/nunito-sans-italic.woff2 +0 -0
  88. package/storybook-static/nunito-sans-regular.woff2 +0 -0
  89. package/storybook-static/project.json +0 -1
  90. package/storybook-static/sb-addons/chromatic-com-storybook-9/manager-bundle.js +0 -331
  91. package/storybook-static/sb-addons/chromatic-com-storybook-9/manager-bundle.js.LEGAL.txt +0 -51
  92. package/storybook-static/sb-addons/essentials-actions-2/manager-bundle.js +0 -3
  93. package/storybook-static/sb-addons/essentials-backgrounds-4/manager-bundle.js +0 -12
  94. package/storybook-static/sb-addons/essentials-controls-1/manager-bundle.js +0 -402
  95. package/storybook-static/sb-addons/essentials-docs-3/manager-bundle.js +0 -242
  96. package/storybook-static/sb-addons/essentials-measure-7/manager-bundle.js +0 -3
  97. package/storybook-static/sb-addons/essentials-outline-8/manager-bundle.js +0 -3
  98. package/storybook-static/sb-addons/essentials-toolbars-6/manager-bundle.js +0 -3
  99. package/storybook-static/sb-addons/essentials-viewport-5/manager-bundle.js +0 -3
  100. package/storybook-static/sb-addons/interactions-10/manager-bundle.js +0 -222
  101. package/storybook-static/sb-addons/links-11/manager-bundle.js +0 -3
  102. package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +0 -3
  103. package/storybook-static/sb-common-assets/favicon.svg +0 -1
  104. package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  105. package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  106. package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  107. package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  108. package/storybook-static/sb-manager/globals-module-info.js +0 -1046
  109. package/storybook-static/sb-manager/globals-runtime.js +0 -41239
  110. package/storybook-static/sb-manager/globals.js +0 -48
  111. package/storybook-static/sb-manager/runtime.js +0 -12048
  112. package/storybook-static/vite.svg +0 -1
  113. package/tsconfig.app.json +0 -27
  114. package/tsconfig.build.json +0 -3
  115. package/tsconfig.json +0 -7
  116. package/tsconfig.node.json +0 -25
  117. 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>