• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomosterlund / qalendar / 13467125422

21 Feb 2025 11:41PM UTC coverage: 97.288%. Remained the same
13467125422

Pull #269

github

web-flow
Merge 8ec4f6867 into 1e81b86d4
Pull Request #269: chore(deps): update dependency @types/node to v22

914 of 979 branches covered (93.36%)

Branch coverage included in aggregate %.

6439 of 6579 relevant lines covered (97.87%)

87.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.61
/src/components/partials/EventFlyout.vue
1
<template>
1✔
2
  <div
1✔
3
    class="event-flyout"
1✔
4
    :class="{ 'is-visible': isVisible, 'is-not-editable': !isEditable }"
1✔
5
    :style="eventFlyoutInlineStyles"
1✔
6
  >
1✔
7
    <div
1✔
8
      v-if="!config.eventDialog || !config.eventDialog.isCustom"
1✔
9
      class="event-flyout__relative-wrapper"
1✔
10
    >
1✔
11
      <div class="event-flyout__menu">
1✔
12
        <span
1✔
13
          v-if="isEditable"
1✔
14
          class="event-flyout__menu-editable"
1✔
15
        >
1✔
16
          <font-awesome-icon
1✔
17
            class="event-flyout__menu-item is-edit-icon"
1✔
18
            :icon="icons.edit"
1✔
19
            @click="editEvent"
1✔
20
          />
1✔
21

1✔
22
          <font-awesome-icon
1✔
23
            class="event-flyout__menu-item is-trash-icon"
1✔
24
            :icon="icons.trash"
1✔
25
            @click="deleteEvent"
1✔
26
          />
1✔
27
        </span>
1✔
28

1✔
29
        <span class="event-flyout__menu-close">
1✔
30
          <font-awesome-icon
1✔
31
            class="event-flyout__menu-item is-times-icon"
1✔
32
            :icon="icons.times"
1✔
33
            @click="closeFlyout"
1✔
34
          />
1✔
35
        </span>
1✔
36
      </div>
1✔
37

1✔
38
      <div
1✔
39
        v-if="calendarEvent"
1✔
40
        class="event-flyout__info-wrapper"
1✔
41
      >
1✔
42
        <div
1✔
43
          v-if="calendarEvent.title"
1✔
44
          class="event-flyout__row is-title"
1✔
45
        >
1✔
46
          <div
1✔
47
            class="event-flyout__color-icon"
1✔
48
            :style="{ backgroundColor: eventBackgroundColor }"
1✔
49
          />
1✔
50
          {{ calendarEvent.title }}
1✔
51
        </div>
1✔
52

1✔
53
        <div
1✔
54
          v-if="calendarEvent.time"
1✔
55
          class="event-flyout__row is-time"
1✔
56
        >
1✔
57
          {{ getEventTime }}
1✔
58
        </div>
1✔
59

1✔
60
        <div
1✔
61
          v-if="calendarEvent.location"
1✔
62
          class="event-flyout__row is-location"
1✔
63
        >
1✔
64
          <font-awesome-icon :icon="icons.location" />
1✔
65
          {{ calendarEvent.location }}
1✔
66
        </div>
1✔
67

1✔
68
        <div
1✔
69
          v-if="calendarEvent.with"
1✔
70
          class="event-flyout__row is-with"
1✔
71
        >
1✔
72
          <font-awesome-icon :icon="icons.user" />
1✔
73
          {{ calendarEvent.with }}
1✔
74
        </div>
1✔
75

1✔
76
        <div
1✔
77
          v-if="calendarEvent.topic"
1✔
78
          class="event-flyout__row is-topic"
1✔
79
        >
1✔
80
          <font-awesome-icon
1✔
81
            :icon="icons.topic"
1✔
82
            class="calendar-week__event-icon"
1✔
83
          />
1✔
84
          {{ calendarEvent.topic }}
1✔
85
        </div>
1✔
86

1✔
87
        <div
1✔
88
          v-if="calendarEvent.description"
1✔
89
          class="event-flyout__row is-description"
1✔
90
        >
1✔
91
          <font-awesome-icon
1✔
92
            :icon="icons.description"
1✔
93
            class="calendar-week__event-icon"
1✔
94
          />
1✔
95
          <!-- eslint-disable vue/no-v-html -->
1✔
96
          <p v-html="calendarEvent.description" />
1✔
97
          <!--eslint-enable-->
1✔
98
        </div>
1✔
99
      </div>
1✔
100
    </div>
1✔
101

1✔
102
    <slot
1✔
103
      v-else
1✔
104
      :event-dialog-data="calendarEvent"
1✔
105
      :close-event-dialog="closeFlyout"
1✔
106
    />
1✔
107
  </div>
1✔
108
</template>
1✔
109

1✔
110
<script lang="ts">
1✔
111
import {defineComponent, type PropType} from 'vue';
1✔
112
import {EVENT_TYPE, type eventInterface} from '../../typings/interfaces/event.interface';
1✔
113
import EventFlyoutPosition, {EVENT_FLYOUT_WIDTH,} from '../../helpers/EventFlyoutPosition';
1✔
114
import {faMapMarkerAlt, faTimes} from '@fortawesome/free-solid-svg-icons';
1✔
115
import {
1✔
116
  faClock,
1✔
117
  faComment,
1✔
118
  faEdit,
1✔
119
  faQuestionCircle,
1✔
120
  faTrashAlt,
1✔
121
  faUser,
1✔
122
} from '@fortawesome/free-regular-svg-icons';
1✔
123
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
1✔
124
import type {configInterface} from '../../typings/config.interface';
1✔
125
import Time from '../../helpers/Time';
1✔
126
import {EVENT_COLORS,} from '../../constants';
1✔
127
import Helpers from "../../helpers/Helpers";
1✔
128

1✔
129
const eventFlyoutPositionHelper = new EventFlyoutPosition();
1✔
130

1✔
131
export default defineComponent({
1✔
132
  name: 'EventFlyout',
1✔
133

1✔
134
  components: {
1✔
135
    FontAwesomeIcon,
1✔
136
  },
1✔
137

1✔
138
  props: {
1✔
139
    calendarEventProp: {
1✔
140
      type: Object as PropType<eventInterface | null>,
1✔
141
      default: () => ({}),
1✔
142
    },
1✔
143
    eventElement: {
1✔
144
      type: Object as PropType<HTMLElement | any>,
1✔
145
      default: null,
1✔
146
    },
1✔
147
    time: {
1✔
148
      type: Object as PropType<Time>,
1✔
149
      required: true,
1✔
150
    },
1✔
151
    config: {
1✔
152
      type: Object as PropType<configInterface>,
1✔
153
      required: true,
1✔
154
    },
1✔
155
  },
1✔
156

1✔
157
  emits: ['hide', 'edit-event', 'delete-event'],
1✔
158

1✔
159
  data() {
1✔
160
    return {
110✔
161
      isVisible: false,
110✔
162
      top: 0 as number | null,
110✔
163
      left: 0 as number | null,
110✔
164
      icons: {
110✔
165
        clock: faClock,
110✔
166
        user: faUser,
110✔
167
        description: faComment,
110✔
168
        trash: faTrashAlt,
110✔
169
        edit: faEdit,
110✔
170
        times: faTimes,
110✔
171
        topic: faQuestionCircle,
110✔
172
        location: faMapMarkerAlt,
110✔
173
      },
110✔
174
      calendarEvent: this.calendarEventProp,
110✔
175
      flyoutWidth: EVENT_FLYOUT_WIDTH + 'px',
110✔
176
      colors: EVENT_COLORS,
110✔
177
    };
110✔
178
  },
110✔
179

1✔
180
  computed: {
1✔
181
    getEventTime() {
1✔
182
      if (!this.calendarEvent || !this.calendarEvent.time) return null;
20!
183

20✔
184
      const eventType = Helpers.getEventType(this.calendarEvent, this.time);
20✔
185

20✔
186
      if ([EVENT_TYPE.MULTI_DAY_TIMED].includes(eventType)) {
20✔
187
        const startLocalizedString = this.getDateFromDateString(
1✔
188
          this.calendarEvent.time.start
1✔
189
        ) + ' ' + this.time.getLocalizedTime(this.calendarEvent.time.start)
1✔
190
        const endLocalizedString = this.getDateFromDateString(
1✔
191
          this.calendarEvent.time.end
1✔
192
        ) + ' ' + this.time.getLocalizedTime(this.calendarEvent.time.end)
1✔
193

1✔
194
        return `${startLocalizedString} - ${endLocalizedString}`;
1✔
195
      }
1✔
196

19✔
197
      if ([EVENT_TYPE.SINGLE_DAY_FULL_DAY, EVENT_TYPE.MULTI_DAY_FULL_DAY].includes(eventType)) {
20✔
198
        const startDate = this.getDateFromDateString(this.calendarEvent.time.start);
2✔
199
        const endDate = this.getDateFromDateString(this.calendarEvent.time.end);
2✔
200

2✔
201
        if (startDate === endDate) return startDate;
2✔
202

1✔
203
        return `${startDate} - ${endDate}`;
1✔
204
      }
1✔
205

17✔
206
      const dateString = this.getDateFromDateString(this.calendarEvent.time.start);
17✔
207
      const timeString = this.time.getLocalizedTimeRange(
17✔
208
        this.calendarEvent.time.start,
17✔
209
        this.calendarEvent.time.end
17✔
210
      );
17✔
211

17✔
212
      return `${dateString} â‹… ${timeString}`;
17✔
213
    },
20✔
214

1✔
215
    eventFlyoutInlineStyles() {
1✔
216
      if (typeof this.top === 'number' && !this.left) {
110✔
217
        return {
110✔
218
          top: this.top + 'px',
110✔
219
          left: '50%',
110✔
220
          position: 'fixed' as const,
110✔
221
          transform: 'translateX(-50%)',
110✔
222
        };
110✔
223
      }
110!
224

×
225
      return {
×
226
        top: this.top + 'px',
×
227
        left: this.left + 'px',
×
228
        position: 'fixed' as const, // casting, since tsc otherwise thinks we're casting 'string' to 'PositionProperty'
×
229
      };
×
230
    },
110✔
231

1✔
232
    isEditable() {
1✔
233
      return this.calendarEventProp?.isEditable || false;
114✔
234
    },
114✔
235

1✔
236
    eventBackgroundColor() {
1✔
237
      // First, if the event has a customColorScheme, and the name of that
15✔
238
      if (
15✔
239
        this.calendarEvent?.colorScheme &&
15✔
240
        this.config.style?.colorSchemes &&
1✔
241
        this.config.style.colorSchemes[this.calendarEvent.colorScheme]
1✔
242
      ) {
15✔
243
        return this.config.style.colorSchemes[this.calendarEvent.colorScheme]
1✔
244
          .backgroundColor;
1✔
245
      }
1✔
246

14✔
247
      return this.colors[this.calendarEvent?.color || 'blue'];
15✔
248
    },
15✔
249
  },
1✔
250

1✔
251
  watch: {
1✔
252
    calendarEventProp: {
1✔
253
      deep: true,
1✔
254
      handler(value) {
1✔
255
        // Set the values with a timeout.
8✔
256
        // Otherwise, the click listener for closing the flyout will believe that the flyout is already open
8✔
257
        // When it is in fact just being opened
8✔
258
        setTimeout(() => {
8✔
259
          this.calendarEvent = value;
3✔
260
          this.isVisible = !!value;
3✔
261
          this.$nextTick(() => this.setFlyoutPosition());
3✔
262
        }, 10);
8✔
263
      },
8✔
264
    },
1✔
265
  },
1✔
266

1✔
267
  mounted() {
1✔
268
    this.listenForClickOutside();
110✔
269
  },
110✔
270

1✔
271
  beforeUnmount() {
1✔
272
    document.removeEventListener('click', this.closeFlyoutOnClickOutside);
36✔
273
  },
36✔
274

1✔
275
  methods: {
1✔
276
    setFlyoutPosition() {
1✔
277
      const calendar = this.eventElement?.closest('.calendar-root');
3!
278
      const flyout = document.querySelector('.event-flyout');
3✔
279

3✔
280
      if (!this.eventElement) return;
3!
281

×
282
      const flyoutPosition = eventFlyoutPositionHelper.calculateFlyoutPosition(
×
283
        this.eventElement?.getBoundingClientRect(),
3✔
284
        {
3✔
285
          height: flyout?.clientHeight || 300,
3!
286
          width: flyout?.clientWidth || 0,
3!
287
        },
3✔
288
        calendar ? calendar.getBoundingClientRect() : null
3!
289
      );
3✔
290

3✔
291
      this.top = typeof flyoutPosition?.top === 'number' ? flyoutPosition.top : null;
3!
292
      this.left = typeof flyoutPosition?.left === 'number' ? flyoutPosition.left : null;
3!
293
    },
3✔
294

1✔
295
    editEvent() {
1✔
296
      this.$emit('edit-event', this.calendarEvent?.id);
1!
297
      this.closeFlyout();
1✔
298
    },
1✔
299

1✔
300
    deleteEvent() {
1✔
301
      this.$emit('delete-event', this.calendarEvent?.id);
1!
302
      this.closeFlyout();
1✔
303
    },
1✔
304

1✔
305
    closeFlyout() {
1✔
306
      this.isVisible = false;
3✔
307

3✔
308
      setTimeout(() => {
3✔
309
        this.$emit('hide');
×
310
      }, 100);
3✔
311
    },
3✔
312

1✔
313
    getDateFromDateString(dateString: string) {
1✔
314
      const { year, month, date } =
23✔
315
        this.time.getAllVariablesFromDateTimeString(dateString);
23✔
316

23✔
317
      return new Date(year, month, date).toLocaleDateString(
23✔
318
        this.time.CALENDAR_LOCALE,
23✔
319
        {
23✔
320
          year: 'numeric',
23✔
321
          month: 'long',
23✔
322
          day: 'numeric',
23✔
323
        }
23✔
324
      );
23✔
325
    },
23✔
326

1✔
327
    listenForClickOutside() {
1✔
328
      document.addEventListener('click', this.closeFlyoutOnClickOutside);
110✔
329
    },
110✔
330

1✔
331
    closeFlyoutOnClickOutside(e: any) {
1✔
332
      const flyout = document.querySelector('.event-flyout');
2✔
333
      if (!flyout || !this.isVisible) return;
2!
334

2✔
335
      const isClickOutside = !flyout.contains(e.target);
2✔
336
      const isClickOnEvent = !!e.target.closest('.is-event');
2✔
337
      const closeOnClickOutside = this.config.eventDialog?.closeOnClickOutside ?? true;
2!
338

2✔
339
      if (this.isVisible && isClickOutside && !isClickOnEvent && closeOnClickOutside) {
2✔
340
        this.closeFlyout();
1✔
341
      }
1✔
342
    },
2✔
343
  },
1✔
344
});
1✔
345
</script>
1✔
346

1✔
347
<style scoped lang="scss">
1✔
348
@use '../../styles/mixins' as mixins;
1✔
349

1✔
350
.event-flyout {
1✔
351
  position: fixed;
1✔
352
  z-index: 50;
1✔
353
  background-color: #fff;
1✔
354
  max-height: 100%;
1✔
355
  width: v-bind(flyoutWidth);
1✔
356
  max-width: 98%;
1✔
357
  border: var(--qalendar-border-gray-thin);
1✔
358
  border-radius: 8px;
1✔
359
  box-shadow: 0 12px 24px rgb(0 0 0 / 9%), 0 6px 12px rgb(0 0 0 / 18%);
1✔
360
  overflow: hidden;
1✔
361
  transition: all 0.2s ease;
1✔
362
  transition-property: opacity, transform;
1✔
363
  transform: translateY(-40px);
1✔
364
  opacity: 0;
1✔
365
  pointer-events: none;
1✔
366

1✔
367
  @include mixins.dark-mode {
1✔
368
    background-color: var(--qalendar-dark-mode-elevated-surface);
1✔
369
    border-color: transparent;
1✔
370
  }
1✔
371

1✔
372
  &.is-visible {
1✔
373
    opacity: 1;
1✔
374
    transform: translateY(0);
1✔
375
    pointer-events: initial;
1✔
376
  }
1✔
377

1✔
378
  &__relative-wrapper {
1✔
379
    position: relative;
1✔
380
  }
1✔
381

1✔
382
  &__menu {
1✔
383
    display: flex;
1✔
384
    justify-content: space-between;
1✔
385
    align-items: center;
1✔
386

1✔
387
    .event-flyout__menu-editable,
1✔
388
    .event-flyout__menu-close {
1✔
389
      padding: var(--qalendar-spacing) var(--qalendar-spacing) 0
1✔
390
        var(--qalendar-spacing);
1✔
391
      display: flex;
1✔
392
      grid-gap: 20px;
1✔
393
    }
1✔
394

1✔
395
    .event-flyout__menu-close {
1✔
396
      .is-not-editable & {
1✔
397
        position: absolute;
1✔
398
        top: 0;
1✔
399
        right: 0;
1✔
400
      }
1✔
401
    }
1✔
402
  }
1✔
403

1✔
404
  &__menu-item {
1✔
405
    font-size: var(--qalendar-font-l);
1✔
406
    color: gray;
1✔
407

1✔
408
    @include mixins.dark-mode {
1✔
409
      color: var(--qalendar-dark-mode-text-hint);
1✔
410
    }
1✔
411

1✔
412
    &:hover {
1✔
413
      color: var(--qalendar-theme-color);
1✔
414
      cursor: pointer;
1✔
415
    }
1✔
416
  }
1✔
417

1✔
418
  .is-trash-icon {
1✔
419
    &:hover {
1✔
420
      color: red;
1✔
421
    }
1✔
422
  }
1✔
423

1✔
424
  &__info-wrapper {
1✔
425
    padding: var(--qalendar-spacing);
1✔
426
  }
1✔
427

1✔
428
  &__row {
1✔
429
    display: flex;
1✔
430
    grid-gap: var(--qalendar-spacing);
1✔
431
    margin-bottom: 0.25em;
1✔
432
    font-weight: 400;
1✔
433

1✔
434
    p {
1✔
435
      margin: 0;
1✔
436
      padding: 0;
1✔
437
    }
1✔
438

1✔
439
    svg {
1✔
440
      margin-top: 0.1rem;
1✔
441
      color: #5f6368;
1✔
442
      width: 14px;
1✔
443

1✔
444
      @include mixins.dark-mode {
1✔
445
        color: var(--qalendar-dark-mode-text-hint);
1✔
446
      }
1✔
447
    }
1✔
448
  }
1✔
449

1✔
450
  &__color-icon {
1✔
451
    --icon-height: 16px;
1✔
452

1✔
453
    border-radius: 50%;
1✔
454
    height: var(--icon-height);
1✔
455
    width: var(--icon-height);
1✔
456
  }
1✔
457

1✔
458
  .is-title {
1✔
459
    font-size: var(--qalendar-font-l);
1✔
460
    align-items: center;
1✔
461

1✔
462
    .is-not-editable & {
1✔
463
      max-width: 90%;
1✔
464
    }
1✔
465
  }
1✔
466

1✔
467
  .is-time {
1✔
468
    font-size: var(--qalendar-font-s);
1✔
469
    margin-bottom: 0.75em;
1✔
470
  }
1✔
471
}
1✔
472
</style>
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc