@open-commune/web 0.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.
Files changed (132) hide show
  1. package/README.md +41 -0
  2. package/app.config.ts +10 -0
  3. package/app.vue +13 -0
  4. package/assets/css/main.css +13 -0
  5. package/components/data/display/row/data-avatar.vue +23 -0
  6. package/components/data/display/row/data-badge.vue +34 -0
  7. package/components/data/display/row/data-color.vue +17 -0
  8. package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
  9. package/components/data/display/row/data-label.vue +37 -0
  10. package/components/data/display/row/data-last-update.vue +45 -0
  11. package/components/data/display/row/data-spacer.vue +4 -0
  12. package/components/data/display/row/data-summary.vue +44 -0
  13. package/components/data/display/row/data-text.vue +14 -0
  14. package/components/data/display/row/data-toggle.vue +16 -0
  15. package/components/data/display/row.vue +111 -0
  16. package/components/data/lists/data-list.vue +85 -0
  17. package/components/data/lists/data-lists-table.vue +155 -0
  18. package/components/elements/avatars/avatar-circular.vue +68 -0
  19. package/components/elements/avatars/avatar-with-text.vue +47 -0
  20. package/components/elements/badges/badge.vue +85 -0
  21. package/components/elements/buttons/primary.vue +32 -0
  22. package/components/elements/buttons/secondary.vue +35 -0
  23. package/components/elements/buttons/tertiary.vue +25 -0
  24. package/components/elements/dropdowns/dots.vue +75 -0
  25. package/components/elements/media/image.vue +82 -0
  26. package/components/elements/notifications/default.vue +90 -0
  27. package/components/forms/handlers/handler-collections.vue +189 -0
  28. package/components/forms/handlers/handler-publication-status.vue +193 -0
  29. package/components/forms/handlers/handler-records.vue +138 -0
  30. package/components/forms/handlers/handler-roles.vue +176 -0
  31. package/components/forms/handlers/handler-schedules.vue +218 -0
  32. package/components/forms/handlers/handler-steps.vue +147 -0
  33. package/components/forms/handlers/handler-tokens.vue +79 -0
  34. package/components/forms/inputs/input-color-picker.vue +159 -0
  35. package/components/forms/inputs/input-editor.vue +363 -0
  36. package/components/forms/inputs/input-files-media.vue +150 -0
  37. package/components/forms/inputs/input-files.vue +98 -0
  38. package/components/forms/inputs/input-json.vue +65 -0
  39. package/components/forms/inputs/input-location.vue +179 -0
  40. package/components/forms/inputs/input-text-area.vue +65 -0
  41. package/components/forms/inputs/input-text.vue +122 -0
  42. package/components/forms/inputs/input-toggle.vue +125 -0
  43. package/components/forms/inputs/media-section/media-editor.vue +95 -0
  44. package/components/forms/layouts/form.vue +203 -0
  45. package/components/forms/layouts/layout.vue +47 -0
  46. package/components/forms/layouts/modal.vue +96 -0
  47. package/components/forms/layouts/navigation.vue +48 -0
  48. package/components/forms/layouts/section.vue +118 -0
  49. package/components/forms/selects/select-collection-enum.vue +195 -0
  50. package/components/forms/selects/select-date.vue +245 -0
  51. package/components/forms/selects/select-enum.vue +186 -0
  52. package/components/forms/selects/select-record.vue +274 -0
  53. package/components/layout/headings/headings-collection.vue +29 -0
  54. package/components/layout/headings/headings-record.vue +33 -0
  55. package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
  56. package/components/layout/navigations/primary.vue +132 -0
  57. package/components/layout/notifications/notifications-list.vue +19 -0
  58. package/composables/core/domains/use-definitions.ts +57 -0
  59. package/composables/core/domains/use-entries.ts +62 -0
  60. package/composables/core/domains/use-media.ts +82 -0
  61. package/composables/core/use-core.ts +12 -0
  62. package/composables/use-ability.js +38 -0
  63. package/composables/use-models.js +27 -0
  64. package/composables/use-session.ts +80 -0
  65. package/composables/use-timer.js +62 -0
  66. package/composables/use-trpc.js +17 -0
  67. package/editor/index.ts +44 -0
  68. package/editor/views/CalloutView.vue +22 -0
  69. package/editor/views/DetailsContentView.vue +11 -0
  70. package/editor/views/DetailsSummaryView.vue +11 -0
  71. package/editor/views/DetailsView.vue +72 -0
  72. package/editor/views/EmbedView.vue +53 -0
  73. package/editor/views/FileView.vue +79 -0
  74. package/editor/views/ImageView.vue +56 -0
  75. package/editor/views/UploadView.vue +71 -0
  76. package/layouts/default.vue +21 -0
  77. package/layouts/entrance.vue +10 -0
  78. package/layouts/record.vue +21 -0
  79. package/middleware/00.auth.global.js +16 -0
  80. package/middleware/01.instance.global.js +21 -0
  81. package/middleware/03.verify-permissions.global.js +11 -0
  82. package/middleware/04.prefetch-page.global.js +18 -0
  83. package/middleware/05.headings-page.global.js +25 -0
  84. package/models/_commun.js +243 -0
  85. package/models/_factory.js +34 -0
  86. package/models/collection.js +87 -0
  87. package/models/entrance.js +42 -0
  88. package/models/media.js +82 -0
  89. package/models/organization.js +92 -0
  90. package/models/record.js +165 -0
  91. package/models/role.js +66 -0
  92. package/models/token.js +56 -0
  93. package/models/user.js +97 -0
  94. package/nuxt.config.ts +151 -0
  95. package/package.json +71 -0
  96. package/pages/[collection]/[record]/index.vue +88 -0
  97. package/pages/[collection]/index.vue +55 -0
  98. package/pages/_lab/editor.vue +149 -0
  99. package/pages/auth/callback.vue +31 -0
  100. package/pages/index/index.vue +85 -0
  101. package/pages/index/password/define/[token].vue +79 -0
  102. package/pages/index/password/recover/index.vue +72 -0
  103. package/pages/index/welcome/[token].vue +90 -0
  104. package/pages/index.vue +44 -0
  105. package/pages/management/index.vue +15 -0
  106. package/pages/management/members.vue +176 -0
  107. package/pages/management/organizations.vue +192 -0
  108. package/pages/management/roles.vue +183 -0
  109. package/pages/overview.vue +15 -0
  110. package/pages/settings.vue +197 -0
  111. package/plugins/01.session.client.ts +5 -0
  112. package/plugins/02.vue-query.ts +25 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/logo-black.svg +1 -0
  115. package/public/logo-white.svg +1 -0
  116. package/server/api/trpc/[...trpc].ts +28 -0
  117. package/server/api/v1/content/deployment.get.ts +45 -0
  118. package/server/api/v1/content/records.get.ts +12 -0
  119. package/server/middleware/0.security.ts +19 -0
  120. package/server/middleware/1.cors.ts +31 -0
  121. package/server/middleware/2.session.ts +20 -0
  122. package/server/middleware/3.api-token.ts +20 -0
  123. package/server/plugins/core.ts +8 -0
  124. package/server/routes/_tasks/[name].post.ts +20 -0
  125. package/server/routes/health.get.ts +6 -0
  126. package/server/tasks/deploy.ts +26 -0
  127. package/server/types.d.ts +8 -0
  128. package/server/utils/core.ts +18 -0
  129. package/store/layout/headings.js +21 -0
  130. package/store/layout/modal.js +80 -0
  131. package/store/layout/notifications.js +52 -0
  132. package/store/layout/workspace.js +80 -0
@@ -0,0 +1,195 @@
1
+ <template>
2
+ <div>
3
+ <label
4
+ v-if="label"
5
+ id="listbox-label"
6
+ class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
7
+ >
8
+ {{ label }}
9
+ </label>
10
+ <div
11
+ :class="{ 'mt-1':label }"
12
+ class="relative"
13
+ >
14
+ <button
15
+ type="button"
16
+ aria-haspopup="listbox"
17
+ aria-expanded="true"
18
+ aria-labelledby="listbox-label"
19
+ :class="{ 'ring-gray-50 border-gray-900':display && !readOnly, 'bg-gray-50 text-gray-500 ring-gray-200': readOnly }"
20
+ class="relative w-full rounded-md pl-3 pr-10 py-2 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none text-sm"
21
+ @click="_toggle"
22
+ >
23
+ <span :class="{ 'text-gray-700':!selectionText }" class="block truncate">
24
+ {{ selectionText || placeHolder }}
25
+ </span>
26
+ <span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
27
+ <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
28
+ <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
29
+ </svg>
30
+ </span>
31
+ </button>
32
+ <transition
33
+ leave-active-class="transition ease-in duration-100"
34
+ leave-from-class="opacity-100"
35
+ leave-to-class="opacity-0"
36
+ >
37
+ <div
38
+ v-if="display"
39
+ ref="selectCollectionEnum"
40
+ class="absolute mt-1 w-full rounded-md bg-white shadow-lg z-50"
41
+ >
42
+ <ul tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-item-3" class="max-h-60 rounded-md p-2 text-base ring-1 ring-black/5 overflow-auto focus:outline-none sm:text-sm">
43
+ <li
44
+ v-for="(option, i) in list"
45
+ :id="`listbox-item-${i}`"
46
+ :key="option"
47
+ :class="readOnly ? 'cursor-not-allowed text-gray-300' : 'cursor-pointer text-black'"
48
+ class=" select-none rounded-sm relative py-2 px-4 hover:bg-gray-50"
49
+ role="option"
50
+ @click="!readOnly && _select(option)"
51
+ >
52
+ <span :class="{ 'font-normal text-black': _isSelected(option) }" class="font-light block truncate">
53
+ {{ option }}
54
+ </span>
55
+ <span
56
+ v-show="_isSelected(option)"
57
+ class="absolute text-black inset-y-0 right-0 flex items-center pr-4"
58
+ >
59
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
60
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
61
+ </svg>
62
+ </span>
63
+ </li>
64
+ </ul>
65
+ </div>
66
+ </transition>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup>
72
+ import { onClickOutside } from '@vueuse/core'
73
+
74
+ // Prepare $emitters
75
+ const $emitters = defineEmits(['change'])
76
+
77
+ // Prepare props
78
+ const $props = defineProps({
79
+ attribute: {
80
+ type: String,
81
+ default: ''
82
+ },
83
+ filter: {
84
+ type: Function,
85
+ default: () => () => true
86
+ },
87
+ itemKey: {
88
+ type: String,
89
+ default: 'label'
90
+ },
91
+ itemValue: {
92
+ type: String,
93
+ default: 'id'
94
+ },
95
+ label: {
96
+ type: String,
97
+ default: ''
98
+ },
99
+ enableMultiple: {
100
+ type: Boolean,
101
+ default: false
102
+ },
103
+ object: {
104
+ type: Boolean,
105
+ default: true
106
+ },
107
+ readOnly: {
108
+ type: Boolean,
109
+ default: false
110
+ },
111
+ placeHolder: {
112
+ type: String,
113
+ default: 'Sélectionnez'
114
+ },
115
+ value: {
116
+ type: [String, Array],
117
+ default: ''
118
+ }
119
+ })
120
+
121
+ // Prepare reactive data
122
+ const items = ref([])
123
+ const display = ref(false)
124
+ const selection = ref('')
125
+ const selectCollectionEnum = ref(null)
126
+
127
+ // Prepare computed
128
+ const list = computed(() => {
129
+ const list = items.value.filter($props.filter)
130
+ if ($props.object) {
131
+ return list.map(item => item[$props.itemKey])
132
+ }
133
+ return list
134
+ })
135
+ const selectionText = computed(() => {
136
+ if ($props.object) {
137
+ return items.value.filter(item => _isSelected(item[$props.itemKey])).map(item => item[$props.itemKey]).join(', ')
138
+ }
139
+ return $props.enableMultiple ? selection.value.join(', ') : selection.value
140
+ })
141
+
142
+ // Prepare watchers
143
+ watch(
144
+ () => $props.value,
145
+ (newValue) => {
146
+ if (newValue) {
147
+ selection.value = newValue
148
+ } else {
149
+ selection.value = $props.enableMultiple ? [] : ''
150
+ }
151
+ },
152
+ { immediate: true }
153
+ )
154
+
155
+ // Prepare methods
156
+ const _isSelected = (option) => {
157
+ const val = $props.object
158
+ ? items.value.find(item => item[$props.itemKey] === option)?.[$props.itemValue]
159
+ : option
160
+ return $props.enableMultiple ? selection.value.includes(val) : selection.value === val
161
+ }
162
+ const _select = (option) => {
163
+ let val = option
164
+ if ($props.object) {
165
+ const { [$props.itemValue]: objectVal } = items.value.find(({ [$props.itemKey]: val }) => val === option)
166
+ val = objectVal
167
+ }
168
+ if ($props.enableMultiple) {
169
+ const index = selection.value.indexOf(val)
170
+ selection.value = index >= 0 ? selection.value.filter(check => check !== val) : [...selection.value, val]
171
+ } else {
172
+ selection.vale = val
173
+ _close()
174
+ }
175
+ $emitters('change', selection.value)
176
+ }
177
+ const _toggle = () => {
178
+ display.value = !display.value
179
+ }
180
+ const _close = () => {
181
+ display.value = false
182
+ }
183
+
184
+ // Prepare mounted
185
+ onMounted(() => {
186
+ const { Collection } = useModels()
187
+ const $route = useRoute()
188
+ const collectionRecord = Collection.repo.where('slug', $route.params.collection).first()
189
+ const { options } = collectionRecord.attributes.find(({ name }) => name === $props.attribute)
190
+ items.value = options.items
191
+ })
192
+
193
+ // Prepare directives
194
+ onClickOutside(selectCollectionEnum, _close)
195
+ </script>
@@ -0,0 +1,245 @@
1
+ <template>
2
+ <div>
3
+ <label
4
+ v-if="label"
5
+ id="listbox-label"
6
+ class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
7
+ >
8
+ {{ label }}
9
+ </label>
10
+ <div
11
+ :class="{ 'mt-1':label }"
12
+ class="relative"
13
+ >
14
+ <button
15
+ type="button"
16
+ aria-haspopup="listbox"
17
+ aria-expanded="true"
18
+ aria-labelledby="listbox-label"
19
+ :class="{ 'ring-gray-50 border-gray-900':display, '!bg-gray-100 !text-gray-200': readOnly }"
20
+ :disabled="readOnly"
21
+ class="relative w-full bg-white rounded-md pl-3 pr-10 py-2 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none focus:ring-gray-50 focus:border-black hover:border-black text-sm"
22
+ @click="_toggle"
23
+ >
24
+ <div class="flex justify-between items-center">
25
+ <span :class="{ '!text-gray-400': readOnly }" class="truncate">
26
+ {{ selectionDate }}
27
+ </span>
28
+ <span v-if="timePicker">
29
+ {{ selectionTimeBegin }} <span v-if="timeEnd">→ {{ selectionTimeEnd }}</span>
30
+ </span>
31
+ </div>
32
+ <span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
33
+ <icon
34
+ class="text-gray-400 h-5 w-5"
35
+ :icon="timePicker ? 'carbon:event-schedule' : 'carbon:calendar'"
36
+ />
37
+ </span>
38
+ </button>
39
+ <transition
40
+ leave-active-class="transition ease-in duration-100"
41
+ leave-from-class="opacity-100"
42
+ leave-to-class="opacity-0"
43
+ >
44
+ <div v-if="display" ref="calendar" class="absolute flex flex-col mt-1 p-4 w-full min-w-[20rem] bg-white shadow-lg z-50 rounded-md">
45
+ <!-- time picker -->
46
+ <div v-if="timePicker" class="inline-flex space-x-1 pb-2 border-b border-gray-100">
47
+ <select-enum
48
+ :items="hoursBegin"
49
+ :value="selectionTimeBegin"
50
+ :object="false"
51
+ class="w-full"
52
+ @change="_updateHour('begin', $event)"
53
+ />
54
+ <select-enum
55
+ v-if="timeEnd"
56
+ :items="hoursEnd"
57
+ :value="selectionTimeEnd"
58
+ :object="false"
59
+ class="w-full"
60
+ @change="_updateHour('end', $event)"
61
+ />
62
+ </div>
63
+ <!-- Header -->
64
+ <div class="flex flex-col items-start md:items-center md:flex-row justify-between w-full pt-2 mb-1">
65
+ <span class="text-sm font-bold mb-2 md:mb-0">
66
+ {{ calendarMonth }}
67
+ </span>
68
+ <div class="inline-flex">
69
+ <icon
70
+ class="text-gray-400 hover:text-white hover:bg-primary p-1 cursor-pointer rounded-md h-6 w-6"
71
+ icon="material-symbols:chevron-left"
72
+ @click="_previousMonth"
73
+ />
74
+ <icon
75
+ class="text-gray-400 hover:text-white hover:bg-primary p-1 cursor-pointer rounded-md h-6 w-6"
76
+ icon="material-symbols:chevron-right-rounded"
77
+ @click="_nextMonth"
78
+ />
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Calendar Body -->
83
+ <div class="flex-grow grid grid-cols-7 gap-0.5 rounded-md">
84
+ <div v-for="day in WEEK_DAYS" :key="day" class="h-min text-center text-gray-500 font-semibold mt-3 text-xs self-end">
85
+ {{ day }}
86
+ </div>
87
+ <button
88
+ v-for="({ dt, selected, current }) in calendarDays"
89
+ :key="dt.ordinal"
90
+ :class="{
91
+ 'bg-white hover:bg-primary text-black hover:text-white': current && !selected,
92
+ 'bg-primary text-white': current && selected,
93
+ 'text-gray-400': !current && !selected
94
+ }"
95
+ class="relative w-full rounded-md px-2 py-1.5"
96
+ @click="_select(dt)"
97
+ >
98
+ <span class="text-xs self-end">
99
+ {{ dt.day }}
100
+ </span>
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </transition>
105
+ </div>
106
+ </div>
107
+
108
+ </template>
109
+
110
+ <script setup>
111
+ import { Icon } from '@iconify/vue'
112
+ import { DateTime, Info } from 'luxon'
113
+ import selectEnum from '~/components/forms/selects/select-enum'
114
+
115
+ // Prepare constant
116
+ const TODAY = DateTime.now({ zone: 'Europe/Paris' })
117
+ const WEEK_DAYS = Info.weekdays('narrow', { locale: 'fr' })
118
+ const HOURS = ['00:00', '00:30', '01:00', '01:30', '02:00', '02:30', '03:00', '03:30', '04:00', '04:30', '05:00', '05:30', '06:00', '06:30', '07:00', '07:30', '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30']
119
+
120
+ // Prepare props
121
+ const $props = defineProps({
122
+ label: {
123
+ type: String,
124
+ default: ''
125
+ },
126
+ timePicker: {
127
+ type: Boolean,
128
+ default: false
129
+ },
130
+ timeEnd: {
131
+ type: Boolean,
132
+ default: false
133
+ },
134
+ readOnly: {
135
+ type: Boolean,
136
+ default: false
137
+ },
138
+ value: {
139
+ type: String,
140
+ default: ''
141
+ }
142
+ })
143
+
144
+ // Prepare emitters
145
+ const $emitters = defineEmits(['select'])
146
+
147
+ // Prepare dynamic data
148
+ const display = ref(false)
149
+ const selectionBegin = ref(TODAY.set({ hour: 0, minute: 0, second: 0 }))
150
+ const selectionEnd = ref(TODAY.set({ hour: 23, minute: 59, second: 0 }))
151
+ const calendar = ref(null)
152
+
153
+ // Prepare computed
154
+ const hoursBegin = computed(() => HOURS)
155
+ const hoursEnd = computed(() => HOURS)
156
+ const selectionDate = computed(() => selectionBegin.value.toFormat('ccc d LLL', { locale: 'fr' }))
157
+ const selectionTimeBegin = computed(() => selectionBegin.value.toFormat('HH:mm', { locale: 'fr' }))
158
+ const selectionTimeEnd = computed(() => selectionEnd.value.toFormat('HH:mm', { locale: 'fr' }))
159
+ const calendarMonth = computed(() => selectionBegin.value.toFormat('LLLL yyyy', { locale: 'fr' }))
160
+ const calendarDays = computed(() => {
161
+ // Prepare array of days
162
+ const selectedDT = selectionBegin.value
163
+ const days = []
164
+
165
+ // Determine the final days of the previous month until the last monday
166
+ let previousMonthDT = selectedDT.minus({ months: 1 }).endOf('month')
167
+ while (previousMonthDT.weekday !== 1) {
168
+ days.push({ dt: previousMonthDT })
169
+ previousMonthDT = previousMonthDT.minus({ days: 1 })
170
+ }
171
+ days.push({ dt: previousMonthDT })
172
+
173
+ // Complete array with seleted month days
174
+ days.reverse().push(
175
+ ...Array.from(
176
+ { length: selectedDT.endOf('month').day },
177
+ (_, day) => ({
178
+ dt: selectedDT.set({ day: day + 1 }),
179
+ selected: selectedDT.day.toString() === (day + 1).toString(),
180
+ current: true
181
+ })
182
+ )
183
+ )
184
+
185
+ // Complete array to with the next month beginning day
186
+ let nextMonthDT = selectedDT.plus({ months: 1 }).startOf('month')
187
+ while (nextMonthDT.weekday !== 1) {
188
+ days.push({ dt: nextMonthDT })
189
+ nextMonthDT = nextMonthDT.plus({ days: 1 })
190
+ }
191
+ return days
192
+ })
193
+
194
+ // Prepare watchers
195
+ watch(
196
+ () => $props.value,
197
+ (newValue) => {
198
+ if (newValue) {
199
+ console.log(newValue)
200
+ selectionBegin.value = DateTime.fromISO(newValue)
201
+ }
202
+ },
203
+ { immediate: true }
204
+ )
205
+
206
+ // Prepare methods
207
+ const _emitChange = () => {
208
+ $emitters('change', {
209
+ date: selectionBegin.value.toISO(),
210
+ beginTime: selectionBegin.value.toISO(),
211
+ endTime: selectionEnd.value.toISO()
212
+ })
213
+ }
214
+ const _updateHour = (beginOrEnd, time) => {
215
+ const [hour, minute] = time.split(':')
216
+ if (beginOrEnd === 'begin') {
217
+ selectionBegin.value = selectionBegin.value.set({ hour, minute })
218
+ } else {
219
+ selectionEnd.value = selectionEnd.value.set({ hour, minute })
220
+ }
221
+ _emitChange()
222
+ }
223
+ const _select = (dt) => {
224
+ selectionBegin.value = dt.set({ hour: selectionBegin.value.hour, minute: selectionBegin.value.minute })
225
+ selectionEnd.value = dt.set({ hour: selectionEnd.value.hour, minute: selectionEnd.value.minute })
226
+ _emitChange()
227
+ }
228
+ const _nextMonth = () => {
229
+ selectionBegin.value = selectionBegin.value.plus({ month: 1 })
230
+ selectionEnd.value = selectionEnd.value.plus({ month: 1 })
231
+ }
232
+ const _previousMonth = () => {
233
+ selectionBegin.value = selectionBegin.value.minus({ months: 1 })
234
+ selectionEnd.value = selectionEnd.value.minus({ months: 1 })
235
+ }
236
+ const _toggle = () => {
237
+ display.value = !display.value
238
+ }
239
+ const _close = () => {
240
+ display.value = false
241
+ }
242
+
243
+ // Prepare directives
244
+ onClickOutside(calendar, _close)
245
+ </script>
@@ -0,0 +1,186 @@
1
+ <template>
2
+ <div>
3
+ <label
4
+ v-if="label"
5
+ id="listbox-label"
6
+ class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
7
+ >
8
+ {{ label }}
9
+ </label>
10
+ <div
11
+ :class="{ 'mt-1':label }"
12
+ class="relative"
13
+ >
14
+ <button
15
+ type="button"
16
+ aria-haspopup="listbox"
17
+ aria-expanded="true"
18
+ aria-labelledby="listbox-label"
19
+ :class="{ 'ring-gray-50 border-gray-900':display, '!bg-gray-100 !text-gray-200': readOnly }"
20
+ :disabled="readOnly"
21
+ class="relative w-full bg-white rounded-md pl-3 pr-10 py-2 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none focus:ring-gray-50 focus:border-black hover:border-black text-sm"
22
+ @click="_toggle"
23
+ >
24
+ <span :class="{ 'text-gray-700':!selectionText, '!text-gray-400': readOnly }" class="block truncate">
25
+ {{ selectionText || placeHolder }}
26
+ </span>
27
+ <span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
28
+ <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
29
+ <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
30
+ </svg>
31
+ </span>
32
+ </button>
33
+ <transition
34
+ leave-active-class="transition ease-in duration-100"
35
+ leave-from-class="opacity-100"
36
+ leave-to-class="opacity-0"
37
+ >
38
+ <div
39
+ v-if="display"
40
+ ref="selectEnum"
41
+ class="absolute mt-1 w-full rounded-md bg-white shadow-lg z-50"
42
+ >
43
+ <ul tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-item-3" class="max-h-60 rounded-md p-2 text-base ring-1 ring-black/5 overflow-auto focus:outline-none sm:text-sm">
44
+ <li
45
+ v-for="(option, i) in list"
46
+ :id="`listbox-item-${i}`"
47
+ :key="option"
48
+ class="cursor-default select-none rounded-md relative py-2 pl-4 pr-4 text-gray-900 hover:bg-gray-100"
49
+ role="option"
50
+ @click="_select(option)"
51
+ >
52
+ <span :class="{ 'font-semibold':_isSelected(option) }" class="font-normal block truncate">
53
+ {{ option }}
54
+ </span>
55
+ <span
56
+ v-show="_isSelected(option)"
57
+ class="absolute text-gray-900 inset-y-0 right-0 flex items-center pr-4"
58
+ >
59
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
60
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
61
+ </svg>
62
+ </span>
63
+ </li>
64
+ </ul>
65
+ </div>
66
+ </transition>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup>
72
+ import { onClickOutside } from '@vueuse/core'
73
+
74
+ // Prepare $emitters
75
+ const $emitters = defineEmits(['change'])
76
+
77
+ // Prepare props
78
+ const $props = defineProps({
79
+ filter: {
80
+ type: Function,
81
+ default: () => () => true
82
+ },
83
+ items: {
84
+ type: Array,
85
+ default: () => []
86
+ },
87
+ itemKey: {
88
+ type: String,
89
+ default: 'name'
90
+ },
91
+ itemValue: {
92
+ type: String,
93
+ default: '_id'
94
+ },
95
+ label: {
96
+ type: String,
97
+ default: ''
98
+ },
99
+ enableMultiple: {
100
+ type: Boolean,
101
+ default: false
102
+ },
103
+ object: {
104
+ type: Boolean,
105
+ default: true
106
+ },
107
+ placeHolder: {
108
+ type: String,
109
+ default: 'Sélectionnez'
110
+ },
111
+ readOnly: {
112
+ type: Boolean,
113
+ default: false
114
+ },
115
+ value: {
116
+ type: [String, Array],
117
+ default: ''
118
+ }
119
+ })
120
+
121
+ // Prepare reactive data
122
+ const display = ref(false)
123
+ const selection = ref($props.enableMultiple ? [] : '')
124
+ const selectEnum = ref(null)
125
+
126
+ // Prepare computed
127
+ const list = computed(() => {
128
+ const list = $props.items.filter($props.filter)
129
+ if ($props.object) {
130
+ return list.map(item => item[$props.itemKey])
131
+ }
132
+ return list
133
+ })
134
+ const selectionText = computed(() => {
135
+ const selectedItems = $props.object
136
+ ? $props.items.map(item => item[$props.itemKey]).filter(_isSelected)
137
+ : selection.value
138
+
139
+ return $props.enableMultiple ? selectedItems.join(', ') : $props.object ? selectedItems[0] : selectedItems
140
+ })
141
+
142
+ // Prepare watchers
143
+ watch(
144
+ () => $props.value,
145
+ (newValue) => {
146
+ if (newValue) {
147
+ selection.value = newValue
148
+ } else {
149
+ selection.value = $props.enableMultiple ? [] : ''
150
+ }
151
+ },
152
+ { immediate: true }
153
+ )
154
+
155
+ // Prepare methods
156
+ const _isSelected = (option) => {
157
+ const val = $props.object
158
+ ? $props.items.find(item => item[$props.itemKey] === option)?.[$props.itemValue]
159
+ : option
160
+ return $props.enableMultiple ? selection.value.includes(val) : selection.value === val;
161
+ }
162
+ const _select = (option) => {
163
+ let val = option
164
+ if ($props.object) {
165
+ const { [$props.itemValue]: objectVal } = $props.items.find(({ [$props.itemKey]: val }) => val === option)
166
+ val = objectVal
167
+ }
168
+ if ($props.enableMultiple) {
169
+ const index = selection.value?.indexOf(val)
170
+ selection.value = index >= 0 ? selection.value.filter(check => check !== val) : [...selection.value, val]
171
+ } else {
172
+ selection.value = val
173
+ _close()
174
+ }
175
+ $emitters('change', selection.value)
176
+ }
177
+ const _toggle = () => {
178
+ display.value = !display.value
179
+ }
180
+ const _close = () => {
181
+ display.value = false
182
+ }
183
+
184
+ // Prepare directives
185
+ onClickOutside(selectEnum, _close)
186
+ </script>