@bagelink/vue 1.5.11 → 1.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.5.11",
4
+ "version": "1.5.15",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import type { CalendarEvent, CalendarProvider } from '../composables/useAddToCalendar'
3
+ import { Dropdown, Icon, ListItem, useDevice } from '@bagelink/vue'
4
+ import { computed } from 'vue'
5
+ import { useAddToCalendar } from '../composables/useAddToCalendar'
6
+
7
+ interface Props {
8
+ title: string
9
+ description?: string
10
+ location?: string
11
+ startDate: Date | string
12
+ endDate?: Date | string
13
+ value?: string
14
+ providers?: CalendarProvider[]
15
+ outline?: boolean
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ value: 'Add to Calendar',
20
+ })
21
+
22
+ const event: CalendarEvent = {
23
+ title: props.title,
24
+ description: props.description,
25
+ location: props.location,
26
+ startDate: props.startDate,
27
+ endDate: props.endDate,
28
+ }
29
+
30
+ const { addToCalendar } = useAddToCalendar(event)
31
+
32
+ const calendarProviders = [
33
+ { id: 'google' as CalendarProvider, name: 'Google Calendar', icon: 'google', color: 'red' },
34
+ { id: 'apple' as CalendarProvider, name: 'Apple Calendar', icon: 'apple', color: 'black' },
35
+ { id: 'outlook' as CalendarProvider, name: 'Outlook', icon: 'microsoft', color: 'blue' },
36
+ { id: 'outlookcom' as CalendarProvider, name: 'Outlook.com', icon: 'microsoft', color: 'blue' },
37
+ { id: 'yahoo' as CalendarProvider, name: 'Yahoo Calendar', icon: 'yahoo', color: 'purple' },
38
+ { id: 'ics' as CalendarProvider, name: 'Other (ICS file)', icon: 'calendar', color: 'gray' },
39
+ ]
40
+
41
+ const includedProviders = computed(() => {
42
+ if (!props.providers || !props.providers.length) return calendarProviders
43
+ return calendarProviders.filter(provider => props.providers?.includes(provider.id))
44
+ })
45
+
46
+ function handleProviderClick(provider: CalendarProvider, callback: () => void) {
47
+ addToCalendar(provider)
48
+ callback()
49
+ }
50
+
51
+ const { isMobile } = useDevice()
52
+ </script>
53
+
54
+ <template>
55
+ <Dropdown :outline="outline" icon="calendar" :disablePlacement="isMobile" :value="value" color="primary">
56
+ <template #default="{ hide }">
57
+ <ListItem
58
+ v-for="provider in includedProviders" :key="provider.id" type="button"
59
+ @click="handleProviderClick(provider.id, hide)"
60
+ >
61
+ <Icon :icon="provider.icon" :color="provider.color" :size="1.2" class="me-05" />
62
+ {{ provider.name }}
63
+ </ListItem>
64
+ </template>
65
+ </Dropdown>
66
+ </template>
@@ -72,7 +72,7 @@ function autoResizeTextarea() {
72
72
 
73
73
  const textarea = input as HTMLTextAreaElement
74
74
  textarea.style.height = 'auto'
75
- textarea.style.height = (textarea.scrollHeight + 1) + 'px'
75
+ textarea.style.height = `${textarea.scrollHeight + 1}px`
76
76
  }
77
77
 
78
78
  function updateInputVal() {
@@ -81,17 +81,17 @@ function updateInputVal() {
81
81
  debouncedEmit()
82
82
 
83
83
  if (props.autoheight) {
84
- nextTick(() => autoResizeTextarea())
84
+ nextTick(() => { autoResizeTextarea() })
85
85
  }
86
86
  }
87
87
 
88
88
  watch(
89
89
  () => props.modelValue,
90
- newVal => {
90
+ (newVal) => {
91
91
  if (newVal !== inputVal) {
92
92
  inputVal = newVal
93
93
  if (props.autoheight) {
94
- nextTick(() => autoResizeTextarea())
94
+ nextTick(() => { autoResizeTextarea() })
95
95
  }
96
96
  }
97
97
  },
@@ -119,55 +119,27 @@ onMounted(async () => {
119
119
 
120
120
  <template>
121
121
  <div
122
- class="bagel-input text-input"
123
- :class="{
122
+ class="bagel-input text-input" :class="{
124
123
  dense,
125
124
  small,
126
125
  shrink,
127
126
  code,
128
127
  textInputIconWrap: icon,
129
128
  txtInputIconStart: iconStart,
130
- }"
131
- :title="title"
129
+ }" :title="title"
132
130
  >
133
131
  <label :for="id">
134
132
  {{ label }} <span v-if="required && label">*</span>
135
133
 
136
134
  <input
137
- v-if="!multiline && !autoheight && !code && inputRows < 2"
138
- :id
139
- ref="input"
140
- v-model.trim="inputVal"
141
- :name
142
- :title
143
- :autocomplete
144
- :type
145
- :rows="1"
146
- :placeholder="placeholder || label"
147
- :disabled
148
- :required
149
- :pattern
150
- v-bind="nativeInputAttrs"
151
- @focusout="onFocusout"
152
- @focus="onFocus"
153
- @input="updateInputVal"
154
- />
135
+ v-if="!multiline && !autoheight && !code && inputRows < 2" :id ref="input" v-model.trim="inputVal"
136
+ :name :title :autocomplete :type :rows="1" :placeholder="placeholder || label" :disabled :required
137
+ :pattern v-bind="nativeInputAttrs" @focusout="onFocusout" @focus="onFocus" @input="updateInputVal"
138
+ >
155
139
  <textarea
156
- v-else
157
- :id
158
- ref="input"
159
- v-model="inputVal"
160
- :name
161
- :title
162
- :type
163
- :rows="inputRows"
164
- :placeholder="placeholder || label"
165
- :disabled
166
- :required
167
- :pattern
168
- v-bind="nativeInputAttrs"
169
- @input="updateInputVal"
170
- @focusout="onFocusout"
140
+ v-else :id ref="input" v-model="inputVal" :name :title :type :rows="inputRows"
141
+ :placeholder="placeholder || label" :disabled :required :pattern v-bind="nativeInputAttrs"
142
+ @input="updateInputVal" @focusout="onFocusout"
171
143
  />
172
144
  <p v-if="helptext" class="opacity-7 light">{{ helptext }}</p>
173
145
  <Icon v-if="iconStart" class="iconStart" :icon="iconStart" />
@@ -236,7 +208,8 @@ onMounted(async () => {
236
208
  line-height: 0;
237
209
  }
238
210
 
239
- .textInputIconWrap input {
211
+ .textInputIconWrap input,
212
+ .textInputIconWrap textarea {
240
213
  padding-inline-end: calc(var(--input-height) / 3 + 1.5rem);
241
214
  }
242
215
 
@@ -1,6 +1,7 @@
1
1
  export { default as Accordion } from './Accordion.vue'
2
2
  export { default as AccordionItem } from './AccordionItem.vue'
3
3
  export { default as AddressSearch } from './AddressSearch.vue'
4
+ export { default as AddToCalendar } from './AddToCalendar.vue'
4
5
  export { default as Alert } from './Alert.vue'
5
6
  export * from './analytics'
6
7
  export { default as Avatar } from './Avatar.vue'
@@ -4,6 +4,7 @@ import type { MaybeRefOrGetter, Ref, UnwrapRef } from 'vue'
4
4
  import { getFallbackSchema } from '@bagelink/vue'
5
5
  import { ref, toValue, watch } from 'vue'
6
6
 
7
+ export { useAddToCalendar } from './useAddToCalendar'
7
8
  export { getBagelInstance, setBagelInstance, useBagel } from './useBagel'
8
9
  export { useDevice } from './useDevice'
9
10
  export { useExcel } from './useExcel'
@@ -0,0 +1,237 @@
1
+ /* eslint-disable ts/strict-boolean-expressions */
2
+ import { computed } from 'vue'
3
+
4
+ export interface CalendarEvent {
5
+ title: string
6
+ description?: string
7
+ location?: string
8
+ startDate: Date | string
9
+ endDate?: Date | string
10
+ }
11
+
12
+ export type CalendarProvider = 'google' | 'apple' | 'outlook' | 'outlookcom' | 'yahoo' | 'ics'
13
+
14
+ export function useAddToCalendar(event: CalendarEvent) {
15
+ // Parse dates if they're strings
16
+ const parseDate = (date: Date | string): Date => {
17
+ return typeof date === 'string' ? new Date(date) : date
18
+ }
19
+
20
+ const eventStartDate = parseDate(event.startDate)
21
+
22
+ // Calculate end date if not provided (45 minutes after start)
23
+ const calculateEndDate = (): Date => {
24
+ if (event.endDate !== undefined) {
25
+ return parseDate(event.endDate)
26
+ }
27
+ const endDate = new Date(eventStartDate)
28
+ endDate.setMinutes(endDate.getMinutes() + 45)
29
+ return endDate
30
+ }
31
+
32
+ const eventEndDate = calculateEndDate()
33
+
34
+ // Check if location is a URL
35
+ const isUrl = (text?: string): boolean => {
36
+ if (!text || text.length === 0) {
37
+ return false
38
+ }
39
+ return /^https?:\/\/.+/.test(text.trim())
40
+ }
41
+
42
+ const locationIsUrl = isUrl(event.location)
43
+
44
+ // Format date to YYYYMMDDTHHMMSS format for calendar URLs
45
+ const formatDate = (date: Date): string => {
46
+ const year = date.getFullYear()
47
+ const month = String(date.getMonth() + 1).padStart(2, '0')
48
+ const day = String(date.getDate()).padStart(2, '0')
49
+ const hours = String(date.getHours()).padStart(2, '0')
50
+ const minutes = String(date.getMinutes()).padStart(2, '0')
51
+ const seconds = String(date.getSeconds()).padStart(2, '0')
52
+ return `${year}${month}${day}T${hours}${minutes}${seconds}`
53
+ }
54
+
55
+ // Format date to ISO format for Outlook
56
+ const formatDateISO = (date: Date): string => {
57
+ return `${date.toISOString().replace(/[-:]/g, '').split('.')[0]}Z`
58
+ }
59
+
60
+ // Generate ICS file content
61
+ const generateICS = (): string => {
62
+ const startDate = formatDate(eventStartDate)
63
+ const endDate = formatDate(eventEndDate)
64
+ const timestamp = formatDate(new Date())
65
+
66
+ const icsContent = [
67
+ 'BEGIN:VCALENDAR',
68
+ 'VERSION:2.0',
69
+ 'PRODID:-//Add to Calendar//EN',
70
+ 'CALSCALE:GREGORIAN',
71
+ 'METHOD:PUBLISH',
72
+ 'BEGIN:VEVENT',
73
+ `UID:${timestamp}@addtocalendar`,
74
+ `DTSTAMP:${timestamp}`,
75
+ `DTSTART:${startDate}`,
76
+ `DTEND:${endDate}`,
77
+ `SUMMARY:${event.title}`,
78
+ ]
79
+
80
+ if (event.description && event.description.length > 0) {
81
+ // ICS descriptions need special handling of newlines and character limits
82
+ const description = event.description.replace(/\n/g, '\\n')
83
+ icsContent.push(`DESCRIPTION:${description}`)
84
+ }
85
+
86
+ if (event.location && event.location.length > 0) {
87
+ icsContent.push(`LOCATION:${event.location}`)
88
+ // If location is a URL, also add it as the URL field
89
+ if (locationIsUrl) {
90
+ icsContent.push(`URL:${event.location}`)
91
+ }
92
+ }
93
+
94
+ icsContent.push('STATUS:CONFIRMED', 'SEQUENCE:0', 'END:VEVENT', 'END:VCALENDAR')
95
+
96
+ return icsContent.join('\r\n')
97
+ }
98
+
99
+ // Generate Google Calendar URL
100
+ const googleUrl = computed(() => {
101
+ const params = new URLSearchParams({
102
+ action: 'TEMPLATE',
103
+ text: event.title,
104
+ dates: `${formatDateISO(eventStartDate)}/${formatDateISO(eventEndDate)}`,
105
+ })
106
+
107
+ if (event.description && event.description.length > 0) {
108
+ params.append('details', event.description)
109
+ }
110
+
111
+ if (event.location && event.location.length > 0) {
112
+ params.append('location', event.location)
113
+ }
114
+
115
+ return `https://calendar.google.com/calendar/render?${params.toString()}`
116
+ })
117
+
118
+ // Generate Outlook Web URL
119
+ const outlookWebUrl = computed(() => {
120
+ const params = new URLSearchParams({
121
+ path: '/calendar/action/compose',
122
+ rru: 'addevent',
123
+ subject: event.title,
124
+ startdt: eventStartDate.toISOString(),
125
+ enddt: eventEndDate.toISOString(),
126
+ })
127
+
128
+ if (event.description && event.description.length > 0) {
129
+ params.append('body', event.description)
130
+ }
131
+
132
+ if (event.location && event.location.length > 0) {
133
+ params.append('location', event.location)
134
+ }
135
+
136
+ return `https://outlook.live.com/calendar/0/deeplink/compose?${params.toString()}`
137
+ })
138
+
139
+ // Generate Outlook Office 365 URL
140
+ const outlookUrl = computed(() => {
141
+ const params = new URLSearchParams({
142
+ path: '/calendar/action/compose',
143
+ rru: 'addevent',
144
+ subject: event.title,
145
+ startdt: eventStartDate.toISOString(),
146
+ enddt: eventEndDate.toISOString(),
147
+ })
148
+
149
+ if (event.description && event.description.length > 0) {
150
+ params.append('body', event.description)
151
+ }
152
+
153
+ if (event.location && event.location.length > 0) {
154
+ params.append('location', event.location)
155
+ }
156
+
157
+ return `https://outlook.office.com/calendar/0/deeplink/compose?${params.toString()}`
158
+ })
159
+
160
+ // Generate Yahoo Calendar URL
161
+ const yahooUrl = computed(() => {
162
+ const duration = Math.floor((eventEndDate.getTime() - eventStartDate.getTime()) / 1000 / 60) // Duration in minutes
163
+
164
+ const params = new URLSearchParams({
165
+ v: '60',
166
+ title: event.title,
167
+ st: formatDateISO(eventStartDate),
168
+ dur: duration.toString(),
169
+ })
170
+
171
+ if (event.description && event.description.length > 0) {
172
+ params.append('desc', event.description)
173
+ }
174
+
175
+ if (event.location && event.location.length > 0) {
176
+ params.append('in_loc', event.location)
177
+ }
178
+
179
+ return `https://calendar.yahoo.com/?${params.toString()}`
180
+ })
181
+
182
+ // Get calendar URL or file based on provider
183
+ const getCalendarUrl = (provider: CalendarProvider): string => {
184
+ switch (provider) {
185
+ case 'google':
186
+ return googleUrl.value
187
+ case 'apple':
188
+ case 'ics':
189
+ // For Apple and ICS, we'll trigger a download
190
+ return ''
191
+ case 'outlook':
192
+ return outlookUrl.value
193
+ case 'outlookcom':
194
+ return outlookWebUrl.value
195
+ case 'yahoo':
196
+ return yahooUrl.value
197
+ default:
198
+ return ''
199
+ }
200
+ }
201
+
202
+ // Download ICS file
203
+ const downloadICS = (filename = 'event.ics') => {
204
+ const icsContent = generateICS()
205
+ const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' })
206
+ const link = document.createElement('a')
207
+ link.href = URL.createObjectURL(blob)
208
+ link.download = filename
209
+ document.body.appendChild(link)
210
+ link.click()
211
+ document.body.removeChild(link)
212
+ URL.revokeObjectURL(link.href)
213
+ }
214
+
215
+ // Add to calendar action
216
+ const addToCalendar = (provider: CalendarProvider) => {
217
+ if (provider === 'apple' || provider === 'ics') {
218
+ downloadICS()
219
+ } else {
220
+ const url = getCalendarUrl(provider)
221
+ if (url) {
222
+ window.open(url, '_blank', 'noopener,noreferrer')
223
+ }
224
+ }
225
+ }
226
+
227
+ return {
228
+ googleUrl,
229
+ outlookUrl,
230
+ outlookWebUrl,
231
+ yahooUrl,
232
+ generateICS,
233
+ downloadICS,
234
+ getCalendarUrl,
235
+ addToCalendar,
236
+ }
237
+ }