• 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

92.87
/src/components/week/Week.vue
1
<template>
126!
2
  <WeekTimeline
1✔
3
    :days="days"
1✔
4
    :time="time"
1✔
5
    :full-day-events="fullDayEvents"
1✔
6
    :config="config"
1✔
7
    :mode="mode"
1✔
8
    @event-was-clicked="handleClickOnEvent"
1✔
9
    @day-was-clicked="$emit('day-was-clicked', $event)"
1✔
10
  />
1✔
11

1✔
12
  <div class="calendar-week__wrapper">
1✔
13
    <EventFlyout
1✔
14
      v-if="!config.eventDialog || !config.eventDialog.isDisabled"
1✔
15
      :calendar-event-prop="selectedEvent"
1✔
16
      :event-element="selectedEventElement"
1✔
17
      :time="time"
1✔
18
      :config="config"
1✔
19
      @hide="selectedEvent = null"
1✔
20
      @edit-event="$emit('edit-event', $event)"
1✔
21
      @delete-event="$emit('delete-event', $event)"
1✔
22
    >
1✔
23
      <template #default="p">
1✔
24
        <slot
1✔
25
          name="eventDialog"
1✔
26
          :event-dialog-data="p.eventDialogData"
1✔
27
          :close-event-dialog="p.closeEventDialog"
1✔
28
        />
1✔
29
      </template>
1✔
30
    </EventFlyout>
1✔
31

1✔
32
    <section class="calendar-week">
1✔
33
      <div
1✔
34
        v-if="hasCustomCurrentTimeSlot && showCurrentTime"
1✔
35
        class="custom-current-time"
1✔
36
        :style="{ top: `${currentTimePercentage}%` }"
1✔
37
      >
1✔
38
        <slot name="customCurrentTime" />
1✔
39
      </div>
1✔
40

1✔
41
      <div
1✔
42
        v-else-if="config && config.showCurrentTime && showCurrentTime"
1✔
43
        class="current-time-line"
1✔
44
        :style="{ top: `${currentTimePercentage}%` }"
1✔
45
      >
1✔
46
        <div class="current-time-line__circle" />
1✔
47
      </div>
1✔
48

1✔
49
      <DayTimeline
1✔
50
        :key="period.start.getTime() + period.end.getTime() + mode"
1✔
51
        :time="time"
1✔
52
        :day-intervals="dayIntervals"
1✔
53
        :week-height="weekHeight"
1✔
54
      />
1✔
55

1✔
56
      <div class="calendar-week__events">
1✔
57
        <Day
1✔
58
          v-for="(day, dayIndex) in days"
1✔
59
          :key="day.dateTimeString + mode + weekVersion"
1✔
60
          :day="day"
1✔
61
          :time="time"
1✔
62
          :config="config"
1✔
63
          :day-info="{ daysTotalN: days.length, thisDayIndex: dayIndex, dateTimeString: day.dateTimeString }"
1✔
64
          :mode="mode"
1✔
65
          :day-intervals="dayIntervals"
1✔
66
          :week-height="+weekHeight.replace('px', '')"
1✔
67
          @event-was-clicked="handleClickOnEvent"
1✔
68
          @event-was-resized="$emit('event-was-resized', $event)"
1✔
69
          @event-was-dragged="handleEventWasDragged"
1✔
70
          @interval-was-clicked="$emit('interval-was-clicked', $event)"
1✔
71
          @day-was-clicked="$emit('day-was-clicked', $event)"
1✔
72
          @drag-start="destroyScrollbarAndHideOverflow"
1✔
73
          @drag-end="initScrollbar"
1✔
74
          @datetime-was-clicked="$emit('datetime-was-clicked', $event)"
1✔
75
        >
1✔
76
          <template #weekDayEvent="p">
1✔
77
            <slot
1✔
78
              :event-data="p.eventData"
1✔
79
              name="weekDayEvent"
1✔
80
            />
1✔
81
          </template>
1✔
82
        </Day>
1✔
83
      </div>
1✔
84
    </section>
1✔
85
  </div>
1✔
86
</template>
1✔
87

1✔
88
<script lang="ts">
1✔
89
import {defineComponent} from 'vue';
1✔
90
import type {PropType} from 'vue';
1✔
91
import {type configInterface, type dayIntervalsType,} from '../../typings/config.interface';
1✔
92
import DayTimeline from './DayTimeline.vue';
1✔
93
import {type periodInterface} from '../../typings/interfaces/period.interface';
1✔
94
import {type dayInterface} from '../../typings/interfaces/day.interface';
1✔
95
import WeekTimeline from './WeekTimeline.vue';
1✔
96
import Day from './Day.vue';
1✔
97
import EventFlyout from '../partials/EventFlyout.vue';
1✔
98
import { type eventInterface } from '../../typings/interfaces/event.interface';
1✔
99
import Time, {WEEK_START_DAY} from '../../helpers/Time';
1✔
100
import EventPosition from '../../helpers/EventPosition';
1✔
101
import {type fullDayEventsWeek} from '../../typings/interfaces/full-day-events-week.type';
1✔
102
import type{modeType} from '../../typings/types';
1✔
103
import PerfectScrollbar from 'perfect-scrollbar';
1✔
104
import Helpers from '../../helpers/Helpers';
1✔
105
import {EventsFilter} from "../../helpers/EventsFilter";
1✔
106
import {WeekHelper} from "../../helpers/Week";
1✔
107

1✔
108
const eventPosition = new EventPosition();
1✔
109

1✔
110
export default defineComponent({
1✔
111
  name: 'Week',
1✔
112

1✔
113
  components: {
1✔
114
    Day,
1✔
115
    WeekTimeline,
1✔
116
    DayTimeline,
1✔
117
    EventFlyout,
1✔
118
  },
1✔
119

1✔
120
  props: {
1✔
121
    config: {
1✔
122
      type: Object as PropType<configInterface>,
1✔
123
      required: true,
1✔
124
    },
1✔
125
    eventsProp: {
1✔
126
      type: Array as PropType<eventInterface[]>,
1✔
127
      default: () => [],
1✔
128
    },
1✔
129
    period: {
1✔
130
      type: Object as PropType<periodInterface>,
1✔
131
      required: true,
1✔
132
    },
1✔
133
    modeProp: {
1✔
134
      type: String as PropType<modeType>,
1✔
135
      default: 'week',
1✔
136
    },
1✔
137
    time: {
1✔
138
      type: Object as PropType<Time | any>,
1✔
139
      required: true,
1✔
140
    },
1✔
141
  },
1✔
142

1✔
143
  emits: [
1✔
144
    'event-was-clicked',
1✔
145
    'event-was-resized',
1✔
146
    'event-was-dragged',
1✔
147
    'edit-event',
1✔
148
    'delete-event',
1✔
149
    'interval-was-clicked',
1✔
150
    'day-was-clicked',
1✔
151
    'datetime-was-clicked',
1✔
152
  ],
1✔
153

1✔
154
  data() {
1✔
155
    return {
66✔
156
      days: [] as dayInterface[],
66✔
157
      mode: this.modeProp as modeType,
66✔
158
      selectedEvent: null as eventInterface | null,
66✔
159
      selectedEventElement: null as any | null,
66✔
160
      events: this.eventsProp,
66✔
161
      fullDayEvents: [] as fullDayEventsWeek,
66✔
162
      weekVersion: 0, // is simply a dummy value, for re-rendering child components on event-was-dragged
66✔
163
      dayIntervals: {
66✔
164
        length: 60,
66✔
165
        height: 66,
66✔
166
      } as dayIntervalsType | any,
66✔
167
      weekHeight: '1584px', // Correlates to the initial values of dayIntervals.length and dayIntervals.height
66✔
168
      scrollbar: null as any,
66✔
169
      currentTimePercentage: 0,
66✔
170
      // When dayBoundaries are set, and the current time is outside the dayBoundaries, this property is set to false,
66✔
171
      // in order to hide the current time line
66✔
172
      showCurrentTime: !!this.config?.showCurrentTime,
66✔
173
    };
66✔
174
  },
66✔
175

1✔
176
  computed: {
1✔
177
    hasCustomCurrentTimeSlot() {
1✔
178
      return Helpers.hasSlotContent(this.$slots.customCurrentTime)
66✔
179
    },
66✔
180

1✔
181
    nDays() {
1✔
182
      return this.config?.week?.nDays || 7;
66✔
183
    }
66✔
184
  },
1✔
185

1✔
186
  watch: {
1✔
187
    period: {
1✔
188
      deep: true,
1✔
189
      handler() {
1✔
190
        this.setInitialEvents(this.mode);
×
191
      },
×
192
    },
1✔
193
    modeProp: {
1✔
194
      deep: true,
1✔
195
      handler(value) {
1✔
196
        this.mode = value;
28✔
197
        this.setInitialEvents(value);
28✔
198
      },
28✔
199
    },
1✔
200
  },
1✔
201

1✔
202
  mounted() {
1✔
203
    this.setDayIntervals();
66✔
204
    this.separateFullDayEventsFromOtherEvents();
66✔
205
    this.setInitialEvents(this.modeProp);
66✔
206
    this.scrollOnMount();
66✔
207
    this.initScrollbar();
66✔
208
    if (this.config?.showCurrentTime || this.hasCustomCurrentTimeSlot) this.setCurrentTime();
66✔
209
  },
66✔
210

1✔
211
  methods: {
1✔
212
    initScrollbar(elapsedMs = 0) {
1✔
213
      const el = document.querySelector('.calendar-week__wrapper');
3,180✔
214
      if (elapsedMs > 3000) return;
3,180✔
215
      if (!el) this.initScrollbar(elapsedMs + 50);
3,172✔
216
      else {
18✔
217
        this.scrollbar = new PerfectScrollbar(el);
18✔
218
        this.scrollbar.update();
18✔
219
      }
18✔
220
    },
3,180✔
221

1✔
222
    destroyScrollbarAndHideOverflow() {
1✔
223
      const wrapper = document.querySelector('.calendar-week__wrapper');
×
224

×
225
      if (!(wrapper instanceof HTMLElement)) return;
×
226

×
227
      wrapper.style.overflowY = 'hidden';
×
228
      this.scrollbar.destroy();
×
229
    },
×
230

1✔
231
    separateFullDayEventsFromOtherEvents() {
1✔
232
      const {
66✔
233
        singleDayTimedEvents,
66✔
234
        fullDayAndMultipleDayEvents,
66✔
235
      } = WeekHelper.eventSeparator(this.events, this.time)
66✔
236

66✔
237
      this.events = singleDayTimedEvents;
66✔
238
      this.positionFullDayEvents(fullDayAndMultipleDayEvents);
66✔
239
    },
66✔
240

1✔
241
    positionFullDayEvents(fullDayAndMultipleDayEvents: eventInterface[]) {
1✔
242
      const weekEndDate =
66✔
243
        this.nDays === 5
66✔
244
          ? new Date(
3✔
245
            this.period.end.getFullYear(),
3✔
246
            this.period.end.getMonth(),
3✔
247
            this.period.end.getDate() - 2
3✔
248
          )
3✔
249
          : this.period.end;
63✔
250

66✔
251
      this.fullDayEvents = fullDayAndMultipleDayEvents.length
66✔
252
        ? eventPosition.positionFullDayEventsInWeek(
4✔
253
          this.period.start,
4✔
254
          weekEndDate,
4✔
255
          fullDayAndMultipleDayEvents
4✔
256
        )
4✔
257
        : [];
62✔
258
    },
66✔
259

1✔
260
    setDays() {
1✔
261
      const days = this.time
58✔
262
        .getCalendarWeekDateObjects(this.period.start)
58✔
263
        .map((day: Date) => {
58✔
264
          const dayName = this.time.getLocalizedNameOfWeekday(day, 'long');
406✔
265
          const dateTimeString = this.time.getDateTimeStringFromDate(
406✔
266
            day,
406✔
267
            'start'
406✔
268
          );
406✔
269
          const events = new EventsFilter(this.events).getEventsForDay(this.time, dateTimeString);
406✔
270

406✔
271
          return { dayName, dateTimeString, events };
406✔
272
        });
58✔
273

58✔
274
      if (this.nDays === 5 && this.time.FIRST_DAY_OF_WEEK === WEEK_START_DAY.MONDAY) {
58✔
275
        // Delete Saturday & Sunday
2✔
276
        days.splice(5, 2);
2✔
277
        this.fullDayEvents.splice(5, 2);
2✔
278
      } else if (this.nDays === 5 && this.time.FIRST_DAY_OF_WEEK === WEEK_START_DAY.SUNDAY) {
58✔
279
        // First delete Saturday, then Sunday
4✔
280
        days.splice(6, 1);
4✔
281
        this.fullDayEvents.splice(6, 1);
4✔
282
        days.splice(0, 1);
4✔
283
        this.fullDayEvents.splice(0, 1);
4✔
284
      }
4✔
285

58✔
286
      this.days = days;
58✔
287
    },
58✔
288

1✔
289
    mergeFullDayEventsIntoDays() {
1✔
290
      for (const [dayIndex] of this.days.entries()) {
97✔
291
        this.days[dayIndex].fullDayEvents = this.fullDayEvents[dayIndex];
431✔
292
      }
431✔
293
    },
97✔
294

1✔
295
    setDay() {
1✔
296
      const dayDateTimeString = this.time.getDateTimeStringFromDate(
40✔
297
        this.period.selectedDate
40✔
298
      );
40✔
299
      // 1. Set the timed events
40✔
300
      this.days = [
40✔
301
        {
40✔
302
          dayName: new Date(this.period.selectedDate).toLocaleDateString(
40✔
303
            this.time.CALENDAR_LOCALE,
40✔
304
            { weekday: 'long' }
40✔
305
          ),
40✔
306
          dateTimeString: this.time.getDateTimeStringFromDate(
40✔
307
            this.period.selectedDate,
40✔
308
            'start'
40✔
309
          ),
40✔
310
          events: new EventsFilter(this.events).getEventsForDay(this.time, dayDateTimeString),
40✔
311
        },
40✔
312
      ];
40✔
313

40✔
314
      if (!this.fullDayEvents.length) return;
40✔
315

5✔
316
      // 2. Set full day events
5✔
317
      for (const day of this.fullDayEvents) {
5✔
318
        const dayDateString = this.time.getDateTimeStringFromDate(day.date);
5✔
319
        if (dayDateString.substring(0, 11) === dayDateTimeString.substring(0, 11)) {
5✔
320
          this.fullDayEvents = [day];
5✔
321
          return;
5✔
322
        }
5✔
323
      }
5!
324
    },
40✔
325

1✔
326
    setInitialEvents(mode: modeType) {
1✔
327
      if (mode === 'day') this.setDay();
97✔
328
      if (mode === 'week') this.setDays();
97✔
329

97✔
330
      this.mergeFullDayEventsIntoDays();
97✔
331
    },
97✔
332

1✔
333
    handleClickOnEvent(event: {
1✔
334
      eventElement: HTMLDivElement;
2✔
335
      clickedEvent: eventInterface;
2✔
336
    }) {
2✔
337
      this.$emit('event-was-clicked', event);
2✔
338

2✔
339
      this.selectedEventElement = event.eventElement;
2✔
340
      this.selectedEvent = event.clickedEvent;
2✔
341
    },
2✔
342

1✔
343
    handleEventWasDragged(event: eventInterface) {
1✔
344
      this.initScrollbar();
2✔
345
      const cleanedUpEvent = event;
2✔
346
      // Reset all properties of the event, that need be calculated anew
2✔
347
      delete cleanedUpEvent.totalConcurrentEvents;
2✔
348
      delete cleanedUpEvent.nOfPreviousConcurrentEvents;
2✔
349

2✔
350
      const filteredEvents = this.events.filter((e) => e.id !== event.id);
2✔
351
      this.events = [
2✔
352
        cleanedUpEvent,
2✔
353
        ...filteredEvents.map((e) => {
2✔
354
          // Reset all properties of each event, that need be calculated anew
3✔
355
          delete e.nOfPreviousConcurrentEvents;
3✔
356
          delete e.totalConcurrentEvents;
3✔
357

3✔
358
          return e;
3✔
359
        }),
2✔
360
      ];
2✔
361
      this.setInitialEvents(this.mode);
2✔
362
      this.weekVersion = this.weekVersion + 1;
2✔
363
      this.$emit('event-was-dragged', event);
2✔
364
    },
2✔
365

1✔
366
    scrollOnMount() {
1✔
367
      if (typeof this.config.week?.scrollToHour !== 'number') return;
66!
368

×
369
      const weekWrapper = document.querySelector('.calendar-week__wrapper');
×
370

×
371
      if (!weekWrapper) return;
×
372

×
373
      this.$nextTick(() => {
×
374
        const weekHeight = +this.weekHeight.split('p')[0];
×
375
        const oneHourInPixel = weekHeight / this.time.HOURS_PER_DAY;
×
376
        const hourToScrollTo =  WeekHelper.getNHoursIntoDayFromHour(this.config.week!.scrollToHour!, this.time);
×
377
        const desiredNumberOfPixelsToScroll = oneHourInPixel * hourToScrollTo;
×
378
        weekWrapper.scroll(0, desiredNumberOfPixelsToScroll - 10); // -10 to display the hour in DayTimeline
×
379
      })
×
380
    },
66✔
381

1✔
382
    setDayIntervals() {
1✔
383
      if (this.config.dayIntervals) {
66!
384
        for (const [key, value] of Object.entries(this.config.dayIntervals)) {
×
385
          this.dayIntervals[key] = value;
×
386
        }
×
387
      }
×
388

66✔
389
      this.setWeekHeightBasedOnIntervals();
66✔
390
    },
66✔
391

1✔
392
    setWeekHeightBasedOnIntervals() {
1✔
393
      // 1. Catch faulty configurations
66✔
394
      if (![15, 30, 60].includes(this.dayIntervals.length)) {
66!
395
        this.dayIntervals.length = 60;
×
396
        this.dayIntervals.height = 66;
×
397
        console.warn(
×
398
          'The dayIntervals configuration is faulty. It has been reset to default values.'
×
399
        );
×
400
      }
×
401

66✔
402
      // 2. Set a multiplier, for getting length of an hour based on the interval length
66✔
403
      let intervalMultiplier = 1;
66✔
404
      if (this.dayIntervals.length === 15) intervalMultiplier = 4;
66!
405
      if (this.dayIntervals.length === 30) intervalMultiplier = 2;
66!
406

66✔
407
      // 3. Set height of the week based on the number and length of intervals
66✔
408
      this.weekHeight =
66✔
409
        this.dayIntervals.height * intervalMultiplier * this.time.HOURS_PER_DAY + 'px';
66✔
410
    },
66✔
411

1✔
412
    setCurrentTime() {
1✔
413
      const setTime = () => {
1✔
414
        const nowString = this.time.getDateTimeStringFromDate(new Date())
1✔
415
        const currentTimePercentage = this.time.getPercentageOfDayFromDateTimeString(nowString, this.time.DAY_START, this.time.DAY_END)
1✔
416

1✔
417
        if (currentTimePercentage < 0 || currentTimePercentage > 100) return this.showCurrentTime = false;
1!
418

1✔
419
        this.showCurrentTime = true;
1✔
420
        this.currentTimePercentage = currentTimePercentage
1✔
421

1✔
422
      }
1✔
423
      setTime()
1✔
424
      setInterval(() => setTime(), 60000);
1✔
425
    },
1✔
426
  },
1✔
427
});
1✔
428
</script>
1✔
429

1✔
430
<style scoped lang="scss">
1✔
431
.calendar-week__wrapper {
1✔
432
  position: relative;
1✔
433
  padding-left: var(--qalendar-week-padding-left);
1✔
434
  overflow-y: auto;
1✔
435
}
1✔
436

1✔
437
.calendar-week {
1✔
438
  position: relative;
1✔
439
  width: 100%;
1✔
440
  flex: 1 1 auto;
1✔
441

1✔
442
  &__events {
1✔
443
    display: flex;
1✔
444
    width: 100%;
1✔
445
    height: v-bind(weekHeight);
1✔
446
    overflow: hidden;
1✔
447
  }
1✔
448

1✔
449
  .current-time-line {
1✔
450
    position: absolute;
1✔
451
    left: 0;
1✔
452
    width: 100%;
1✔
453
    height: 2px;
1✔
454
    z-index: 1;
1✔
455
    background-color: red;
1✔
456

1✔
457
    &__circle {
1✔
458
      position: relative;
1✔
459

1✔
460
      &::before {
1✔
461
        content: '';
1✔
462
        position: absolute;
1✔
463
        transform: translate(-45%, -45%);
1✔
464
        width: 10px;
1✔
465
        height: 10px;
1✔
466
        border-radius: 50%;
1✔
467
        background-color: red;
1✔
468
      }
1✔
469
    }
1✔
470
  }
1✔
471

1✔
472
  .custom-current-time {
1✔
473
    position: absolute;
1✔
474
    left: 0;
1✔
475
    width: 100%;
1✔
476
    z-index: 1;
1✔
477
  }
1✔
478
}
1✔
479
</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