Skip to content

Commit

Permalink
feat: line for current time (#85)
Browse files Browse the repository at this point in the history
* feat: add option and slot for adding a line, displaying the current time

* feat: add line for current time
  • Loading branch information
tomosterlund authored Nov 5, 2022
1 parent a04db2a commit 3e00aab
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 54 deletions.
14 changes: 10 additions & 4 deletions development/QalendarView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
@event-was-dragged="handleEventWasDragged"
@interval-was-clicked="handleIntervalWasClicked"
>
<template #customCurrentTime>
<div :style="{ height: '3px', backgroundColor: 'cornflowerblue', position: 'relative' }">
<div :style="{ position: 'absolute', left: '-7px', top: '-6px', height: '15px', width: '15px', backgroundColor: 'cornflowerblue', borderRadius: '50%' }"></div>
</div>
</template>
<!-- <template v-slot:event="eventProps" #event>-->
<!-- <div :style="{ backgroundColor: 'cornflowerblue', color: '#fff', width: '100%', height: '100%', overflow: 'hidden' }">-->
<!-- {{ eventProps.eventData.title }}-->
Expand Down Expand Up @@ -84,7 +89,7 @@ export default defineComponent({
config: {
week: {
startsOn: 'monday',
nDays: 5,
// nDays: 5,
scrollToHour: 8,
},
locale: 'de-DE',
Expand All @@ -102,12 +107,13 @@ export default defineComponent({
},
},
defaultMode: 'week',
showCurrentTime: true,
// disableModes: ['month'],
isSilent: true,
dayIntervals: {
// height: 50,
// length: 15,
// displayClickableInterval: true,
height: 100,
length: 15,
displayClickableInterval: true,
// intervalStyles: {
// color: '#fff',
// backgroundColor: 'rgba(10, 10, 10, 0.9)',
Expand Down
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ data() {
defaultMode: 'day',
// The silent flag can be added, to disable the development warnings. This will also bring a slight performance boost
isSilent: true,
showCurrentTime: true, // Display a line indicating the current time
}
}
}
Expand Down Expand Up @@ -377,6 +378,17 @@ data() {

When a clickable interval is clicked, the calendar emits the event `interval-was-clicked`, containing date-time strings for the start and end of the clicked interval. This can be useful, for letting the user add an event, based on where in a day the user clicks.

### Custom current-time line
As shown above under basic configuration, there is a `showCurrentTime` option for displaying a red line, marking what time of the day it is. If you, however, want to customize the looks of this line, use the `customCurrentTime` slot as shown below, **instead** of `showCurrentTime`. Qalendar takes care of the positioning, you just need to style the line as you wish.

```vue
<template #customCurrentTime>
<div :style="{ height: '3px', backgroundColor: 'cornflowerblue', position: 'relative' }">
<div :style="{ position: 'absolute', left: '-7px', top: '-6px', height: '15px', width: '15px', backgroundColor: 'cornflowerblue', borderRadius: '50%' }"></div>
</div>
</template>
```

### Disabling features

Some features of the calendar can be disabled/hidden through configuration.
Expand Down
4 changes: 4 additions & 0 deletions src/Qalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
:close-event-dialog="p.closeEventDialog"
></slot>
</template>

<template #customCurrentTime>
<slot name="customCurrentTime"></slot>
</template>
</Week>

<Month
Expand Down
75 changes: 74 additions & 1 deletion src/components/week/Week.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

<div class="calendar-week__wrapper">
<section class="calendar-week">
<div v-if="hasCustomCurrentTimeSlot" class="custom-current-time" :style="{ top: `${currentTimePercentage}%` }">
<slot name="customCurrentTime"></slot>
</div>

<div v-else-if="config && config.showCurrentTime" class="current-time-line" :style="{ top: `${currentTimePercentage}%` }">
<div class="current-time-line__circle"></div>
</div>

<DayTimeline
:key="period.start.getTime() + period.end.getTime() + mode"
:time="time"
Expand Down Expand Up @@ -61,7 +69,7 @@
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { defineComponent, PropType, Comment, Text, Slot, VNode } from 'vue';
import {
configInterface,
dayIntervalsType,
Expand Down Expand Up @@ -145,9 +153,34 @@ export default defineComponent({
} as dayIntervalsType | any,
weekHeight: '1584px', // Correlates to the initial values of dayIntervals.length and dayIntervals.height
scrollbar: null as any,
currentTimePercentage: 0,
};
},
computed: {
/**
* Solution from https://github.com/vuejs/core/issues/4733#issuecomment-1024816095
* */
hasCustomCurrentTimeSlot() {
const hasSlotContent = (slot: Slot|undefined) => {
if (!slot) return false;
return slot().some((vnode: VNode) => {
if (vnode.type === Comment) return false;
if (Array.isArray(vnode.children) && !vnode.children.length) return false;
return (
vnode.type !== Text
|| (typeof vnode.children === 'string' && vnode.children.trim() !== '')
);
});
}
return hasSlotContent(this.$slots.customCurrentTime);
},
},
watch: {
period: {
deep: true,
Expand All @@ -170,6 +203,7 @@ export default defineComponent({
this.setInitialEvents(this.modeProp);
this.scrollOnMount();
this.initScrollbar();
if (this.config?.showCurrentTime || this.hasCustomCurrentTimeSlot) this.setCurrentTime();
},
methods: {
Expand Down Expand Up @@ -378,6 +412,15 @@ export default defineComponent({
this.weekHeight =
this.dayIntervals.height * intervalMultiplier * 24 + 'px';
},
setCurrentTime() {
const setTime = () => {
const nowString = this.time.getDateTimeStringFromDate(new Date())
this.currentTimePercentage = this.time.getPercentageOfDayFromDateTimeString(nowString, this.time.DAY_START, this.time.DAY_END)
}
setTime()
setInterval(() => setTime(), 60000);
},
},
});
</script>
Expand All @@ -400,5 +443,35 @@ export default defineComponent({
height: v-bind(weekHeight);
overflow: hidden;
}
.current-time-line {
position: absolute;
left: 0;
width: 100%;
height: 2px;
z-index: 10000;
background-color: red;
&__circle {
position: relative;
&::before {
content: '';
position: absolute;
transform: translate(-45%, -45%);
width: 10px;
height: 10px;
border-radius: 50%;
background-color: red;
}
}
}
.custom-current-time {
position: absolute;
left: 0;
width: 100%;
z-index: 1;
}
}
</style>
61 changes: 13 additions & 48 deletions src/helpers/EventPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,16 @@ import {eventInterface} from '../typings/interfaces/event.interface';
import Time from './Time';
import {fullDayEventsWeek} from '../typings/interfaces/full-day-events-week.type';
import {dayInterface} from '../typings/interfaces/day.interface';
// IMPORTANT: this instance of Time, should not be used for anything sensitive to "locale" or "firstDayOfWeekIs"
const TimeHelper = new Time()

export default class EventPosition {
protected turnMinutesIntoPercentageOfHour(minutes: number): string {
const oneMinutePercentage = 100 / 60;

const minutePoints = oneMinutePercentage * minutes;

if (minutePoints < 10) return "0" + minutePoints;

return minutePoints.toString();
}

/**
* Every hour between 'dayStart' and 'dayEnd' is 100, in this function referred to as 100 points
* If an event starts 30 minutes after 'dayStart', it should have 50 pointsIntoDay
* If a day consists of 4 hours (400 points), we then have to count
* (50 / 400) * 100 = 12.5 => event starts after 12.5 percent of the day
*
* Result is supposed to be a number between 0 and 100, and is used for setting the CSS- top- and height-attributes for events
* */
getPercentageOfDayFromDateTimeString(
dateTimeString: string,
dayStart: number,
dayEnd: number
) {
const pointsInDay = dayEnd - dayStart;
const hour = dateTimeString.substring(11, 13);
const minutes = dateTimeString.substring(14, 16);
const minutesPoints = this.turnMinutesIntoPercentageOfHour(+minutes);
const eventPoints = +(hour + minutesPoints);
const eventPointsIntoDay = eventPoints - dayStart;

return (eventPointsIntoDay / pointsInDay) * 100;
}

export default class EventPosition extends Time {
/**
* Yields a full calendar week, with all full-day events positioned in it
* */
positionFullDayEventsInWeek(weekStart: Date, weekEnd: Date, events: eventInterface[]) {
// 1. add timeJS.start and timeJS.end to all objects
const eventsWithJSDates = events.map((scheduleEvent: eventInterface) => {
const { year: startYear, month: startMonth, date: startDate } = TimeHelper.getAllVariablesFromDateTimeString(scheduleEvent.time.start)
const { year: endYear, month: endMonth, date: endDate } = TimeHelper.getAllVariablesFromDateTimeString(scheduleEvent.time.end)
const { year: startYear, month: startMonth, date: startDate } = this.getAllVariablesFromDateTimeString(scheduleEvent.time.start)
const { year: endYear, month: endMonth, date: endDate } = this.getAllVariablesFromDateTimeString(scheduleEvent.time.end)
scheduleEvent.timeJS = {
start: new Date(startYear, startMonth, startDate),
end: new Date(endYear, endMonth, endDate),
Expand All @@ -66,18 +31,18 @@ export default class EventPosition {

// 2. create a week array, where each day is represented as an object with different levels, level1, level2, level3, level4 etc.
// An event starts on a certain level, the first day when it occurs, and then blocks that level for the rest of its duration
const allDatesOfWeek = TimeHelper.getDatesBetweenTwoDates(weekStart, weekEnd)
const allDatesOfWeek = this.getDatesBetweenTwoDates(weekStart, weekEnd)
const week: fullDayEventsWeek = allDatesOfWeek.map(d => ({ date: d }))

for (const scheduleEvent of eventsWithJSDates) {
for (const [dayIndex, day] of week.entries()) {
const thisDayDateString = TimeHelper.getDateStringFromDate(day.date)
const thisDayDateString = this.getDateStringFromDate(day.date)

if (
// @ts-ignore
TimeHelper.getDateStringFromDate(scheduleEvent.timeJS.start) <= thisDayDateString
this.getDateStringFromDate(scheduleEvent.timeJS.start) <= thisDayDateString
// @ts-ignore
&& TimeHelper.getDateStringFromDate(scheduleEvent.timeJS.end) >= thisDayDateString
&& this.getDateStringFromDate(scheduleEvent.timeJS.end) >= thisDayDateString
) {
// 2A. Get the first free level of the day
let levelToStartOn = 1
Expand All @@ -87,7 +52,7 @@ export default class EventPosition {

// 2B. set the event on this day
// @ts-ignore
let eventNDays = (Math.ceil((scheduleEvent.timeJS.end.getTime() - day.date.getTime()) / TimeHelper.MS_PER_DAY) + 1) // Get difference in days, plus the first day itself
let eventNDays = (Math.ceil((scheduleEvent.timeJS.end.getTime() - day.date.getTime()) / this.MS_PER_DAY) + 1) // Get difference in days, plus the first day itself
const remainingDaysOfWeek = (week.length - dayIndex)
if (eventNDays > remainingDaysOfWeek) eventNDays = remainingDaysOfWeek

Expand Down Expand Up @@ -140,17 +105,17 @@ export default class EventPosition {

return 0
})

for (const fullDayEvent of fullDayEvents) {
const { year: startYear, month: startMonth, date: startDate } = TimeHelper.getAllVariablesFromDateTimeString(fullDayEvent.time.start)
const { year: endYear, month: endMonth, date: endDate } = TimeHelper.getAllVariablesFromDateTimeString(fullDayEvent.time.end)
const allDatesOfEvent = TimeHelper.getDatesBetweenTwoDates(
const { year: startYear, month: startMonth, date: startDate } = this.getAllVariablesFromDateTimeString(fullDayEvent.time.start)
const { year: endYear, month: endMonth, date: endDate } = this.getAllVariablesFromDateTimeString(fullDayEvent.time.end)
const allDatesOfEvent = this.getDatesBetweenTwoDates(
new Date(startYear, startMonth, startDate),
new Date(endYear, endMonth, endDate),
)

for (const date of allDatesOfEvent) {
const dateString = TimeHelper.getDateStringFromDate(date)
const dateString = this.getDateStringFromDate(date)
const dateInMap = monthMap.get(dateString)

if (dateInMap) monthMap.set(dateString, {
Expand Down
33 changes: 33 additions & 0 deletions src/helpers/Time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,37 @@ export default class Time {
999
)
}

protected turnMinutesIntoPercentageOfHour(minutes: number): string {
const oneMinutePercentage = 100 / 60;

const minutePoints = oneMinutePercentage * minutes;

if (minutePoints < 10) return "0" + minutePoints;

return minutePoints.toString();
}

/**
* Every hour between 'dayStart' and 'dayEnd' is 100, in this function referred to as 100 points
* If an event starts 30 minutes after 'dayStart', it should have 50 pointsIntoDay
* If a day consists of 4 hours (400 points), we then have to count
* (50 / 400) * 100 = 12.5 => event starts after 12.5 percent of the day
*
* Result is supposed to be a number between 0 and 100, and is used for setting the CSS- top- and height-attributes for events
* */
getPercentageOfDayFromDateTimeString(
dateTimeString: string,
dayStart: number,
dayEnd: number
) {
const pointsInDay = dayEnd - dayStart;
const hour = dateTimeString.substring(11, 13);
const minutes = dateTimeString.substring(14, 16);
const minutesPoints = this.turnMinutesIntoPercentageOfHour(+minutes);
const eventPoints = +(hour + minutesPoints);
const eventPointsIntoDay = eventPoints - dayStart;

return (eventPointsIntoDay / pointsInDay) * 100;
}
}
1 change: 1 addition & 0 deletions src/typings/config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,5 @@ export interface configInterface {
isDisabled?: boolean;
isCustom?: boolean;
}
showCurrentTime?: boolean;
}
24 changes: 23 additions & 1 deletion tests/unit/components/week/Week.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,26 @@ describe('Week.vue', () => {
const days = wrapper.findAll('.calendar-week__day')
expect(days).toHaveLength(5)
})
})

test('Not showing the currentTimeLine', () => {
expect(() => wrapper.get('.current-time-line')).toThrow()
})

test('Showing the currentTimeLine', async () => {
wrapper = week({
props: {
config: { showCurrentTime: true },
time: new Time('sunday', 'en-US'),
period: {
selectedDate: new Date(),
start: new Date(),
end: new Date()
},
nDays: 5,
}
})

await wrapper.vm.setDays()
expect(wrapper.get('.current-time-line'))
})
})

0 comments on commit 3e00aab

Please sign in to comment.