Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(VCalendar): no event rendering without default slot (#11090) #11131

Merged
merged 1 commit into from
Apr 20, 2020

Conversation

ClickerMonkey
Copy link
Contributor

fixes #11090

Description

resolves #11090

Motivation and Context

When using slots, event rendering can malfunction and not work

How Has This Been Tested?

visually

Markup:

<template>
  <v-sheet class="v-fullscreen">
    <v-sheet
        tile
        height="54"
        color="grey lighten-3"
        class="d-flex"
      >
        <v-btn
          icon
          class="ma-2"
          @click="$refs.calendar.prev()"
        >
          <v-icon>mdi-chevron-left</v-icon>
        </v-btn>
        <v-select
          v-model="type"
          :items="types"
          dense
          outlined
          hide-details
          class="ma-2"
          label="type"
        ></v-select>
        <v-select
          v-model="mode"
          :items="modes"
          dense
          outlined
          hide-details
          label="event-overlap-mode"
          class="ma-2"
        ></v-select>
        <v-select
          v-model="weekday"
          :items="weekdays"
          dense
          outlined
          hide-details
          label="weekdays"
          class="ma-2"
        ></v-select>
        <v-spacer></v-spacer>
        <v-btn
          icon
          class="ma-2"
          @click="$refs.calendar.next()"
        >
          <v-icon>mdi-chevron-right</v-icon>
        </v-btn>
      </v-sheet>
      <v-sheet height="600">
        <v-calendar
          ref="calendar"
          v-model="value"
          :weekdays="weekday"
          :type="type"
          :events="events"
          :event-overlap-mode="mode"
          :event-overlap-threshold="60"
          :event-color="getEventColor"
          :event-ripple="false"
          @change="getEvents"          
          @mousedown:event="startDrag"
          @mousedown:time="startTime"
          @mousemove:time="mouseMove"
          @mouseup:time="endDrag"
          @mouseleave.native="cancelDrag"
        >
          <template #event="{ event, timed }">
            <div class="pl-1" 
              v-html="getEventHTML(event, timed)"
            ></div>
            <div v-if="timed" 
              class="v-event-drag-bottom"
              @mousedown.stop="extendBottom(event)"
            ></div>
          </template>
          <template #day-body="{ date, week }">
            <div class="v-current-time"
               :class="{ first: date === week[0].date }"
               :style="{ top: nowY }">
              <div class="v-current-time-time">
                {{ nowTime }}
              </div>
            </div>
          </template>
        </v-calendar>
         {{ lastEvent }} 
      </v-sheet>
  </v-sheet>
</template>

<script>
export default {
  data: () => ({
    type: 'day',
    types: ['month', 'week', 'day', '4day'],
    mode: 'stack',
    modes: ['stack', 'column'],
    weekday: [0, 1, 2, 3, 4, 5, 6],
    weekdays: [
      { text: 'Sun - Sat', value: [0, 1, 2, 3, 4, 5, 6] },
      { text: 'Mon - Sun', value: [1, 2, 3, 4, 5, 6, 0] },
      { text: 'Mon - Fri', value: [1, 2, 3, 4, 5] },
      { text: 'Mon, Wed, Fri', value: [1, 3, 5] },
    ],
    value: '',
    events: [],
    colors: ['#2196F3', '#3F51B5', '#673AB7', '#00BCD4', '#4CAF50', '#FF9800', '#757575'],
    names: ['Meeting', 'Holiday', 'PTO', 'Travel', 'Event', 'Birthday', 'Conference', 'Party'],
    dragEvent: null,
    dragStart: null,
    lastEvent: '',
    createEvent: null,
    createStart: null,
    extendOriginal: null,
    isMounted: false,
  }),
  computed: {
    nowY() {
      const cal = this.$refs.calendar;
      if (!cal && !this.isMounted) {
        return -1;
      }

      return cal.timeToY(cal.times.now) + 'px';
    },
    nowTime() {
      const cal = this.$refs.calendar;
      if (!cal && !this.isMounted) {
        return -1;
      }

      return cal.formatTime(cal.times.now);
    },
  },
  mounted() {
    const cal = this.$refs.calendar;

    window.Vuetify = Vuetify;
    window.app = this;
    window.cal = cal;

    this.isMounted = true;

    // scroll to the current time
    const minutes = cal.times.now.hour * 60 + cal.times.now.minute;
    const firstTime = Math.max(0, minutes - (minutes % 30) - 30);
    cal.scrollToTime(firstTime);

    // every minute update the current time bar
    setInterval(() => cal.updateTimes(), 60 * 1000);
  },
  methods: {
    getEvents ({ start, end }) {
      const events = []

      const min = new Date(`${start.date}T00:00:00`)
      const max = new Date(`${end.date}T23:59:59`)
      const days = (max.getTime() - min.getTime()) / 86400000
      const eventCount = this.rnd(days, days + 20)

      for (let i = 0; i < eventCount; i++) {
        const allDay = this.rnd(0, 3) === 0
        const firstTimestamp = this.rnd(min.getTime(), max.getTime())
        const first = new Date(firstTimestamp - (firstTimestamp % 900000))
        const secondTimestamp = this.rnd(2, allDay ? 288 : 8) * 900000
        const second = new Date(first.getTime() + secondTimestamp)

        events.push({
          name: this.names[this.rnd(0, this.names.length - 1)],
          start: this.formatDate(first, !allDay),
          end: this.formatDate(second, !allDay),
          color: this.colors[this.rnd(0, this.colors.length - 1)],
        })
      }

      this.events = events
    },
    getEventColor (event) {
      const rgb = parseInt(event.color.substring(1), 16);
      const r = (rgb >> 16) & 0xFF;
      const g = (rgb >>  8) & 0xFF;
      const b = (rgb >>  0) & 0xFF;

      return event === this.dragEvent
        ? `rgba(${r}, ${g}, ${b}, 0.5)`
        : event === this.createEvent
          ? `rgba(${r}, ${g}, ${b}, 0.5)`
          : event.color;
    },
    getEventHTML(event, timed) {
      const cal = this.$refs.calendar;
      let name = event.name;
      if (event.start.hasTime) {
        if (timed) {
          const showStart = event.start.hour < 12 && event.end.hour >= 12;
          const start = cal.formatTime(event.start, showStart);
          const end = cal.formatTime(event.end, true);
          const singline = diffMinutes(event.start, event.end) <= this.parsedEventOverlapThreshold
          const separator = singline ? ', ' : '<br>'
          return `<strong>${name}</strong>${separator}${start} - ${end}`
        } else {
          const time = this.formatTime(event.start, true)
          return `<strong>${time}</strong> ${name}`
        }
      }
      return name;
    },
    rnd (a, b) {
      return Math.floor((b - a + 1) * Math.random()) + a
    },
    formatDate (a, withTime) {
      return withTime
        ? `${a.getFullYear()}-${a.getMonth() + 1}-${a.getDate()} ${a.getHours()}:${a.getMinutes()}`
        : `${a.getFullYear()}-${a.getMonth() + 1}-${a.getDate()}`
    },
    eventMove(e) {
      console.log('eventMove', e);
    },
    startDrag(e) {
      window.console.log('startDrag', e);

      if (e.event && e.timed) {
        this.dragEvent = e.event;
        this.dragTime = null;
        this.extendOriginal = null;
      }

      this.lastEvent = 'startDrag';
    },
    startTime(e) {
      const mouse = this.toDate(e);

      window.console.log('startTime', mouse, e);

      if (!isFinite(mouse.getTime())) {
        return;
      }

      if (this.dragEvent && this.dragTime === null) {
        const start = this.toDate(this.dragEvent.start);

        this.dragTime = mouse.getTime() - start.getTime();

      } else {
        this.createStart = this.roundTime(mouse.getTime());
        this.createEvent = {
          name: '(no title)',
          start: this.toTimestamp(new Date(this.createStart)),
          end: this.toTimestamp(new Date(this.createStart)),
          color: this.colors[this.rnd(0, this.colors.length - 1)],
        };
        this.events.push(this.createEvent);
      }

      window.console.log('startTime dragTime', this.dragTime, 'dragEvent', this.dragEvent, 'createStart', this.createStart);
      this.lastEvent = 'startTime';
    },
    extendBottom(event) {
      this.createEvent = event;
      this.createStart = this.toDate(event.start).getTime();
      this.extendOriginal = event.end;
    },
    mouseMoveEvent(e) {
      console.log('mouseMoveEvent', e);
    },
    mouseMove(e) {
      const mouse = this.toDate(e);

      if (!isFinite(mouse.getTime())) {
        return;
      }

      if (this.dragEvent && this.dragTime !== null && isFinite(this.dragTime)) {
        const start = this.toDate(this.dragEvent.start);
        const end = this.toDate(this.dragEvent.end);
        const duration = end.getTime() - start.getTime();

        const newStartTime = mouse.getTime() - this.dragTime;
        const newStart = new Date(this.roundTime(newStartTime));
        const newEnd = new Date(newStart.getTime() + duration);

        this.dragEvent.start = this.toTimestamp(newStart);
        this.dragEvent.end = this.toTimestamp(newEnd);
      }
      else if (this.createEvent && this.createStart !== null && isFinite(this.createStart)) {
        const mouseRounded = this.roundTime(mouse.getTime(), false);
        const min = Math.min(mouseRounded, this.createStart);
        const max = Math.max(mouseRounded, this.createStart);

        this.createEvent.start = this.toTimestamp(new Date(min));
        this.createEvent.end = this.toTimestamp(new Date(max));
      }
    },
    endDrag(e) {
      this.dragTime = null;
      this.dragEvent = null;
      this.createEvent = null;
      this.createStart = null;
      this.extendOriginal = null;

      this.lastEvent = 'endDrag';
    },
    cancelDrag(e) {
      if (this.createEvent) {
        if (this.extendOriginal) {
          this.createEvent.end = this.extendOriginal;
        } else {
          const i = this.events.indexOf(this.createEvent);
          if (i !== -1) {
            this.events.splice(i, 1);
          }
        }
      }

      this.createEvent = null;
      this.createStart = null;
      this.dragTime = null;
      this.dragEvent = null;

      this.lastEvent = 'cancelDrag';
    },
    roundTime(time, down = true) {
      const roundDownTime = 15 * 60 * 1000; // 15 minutes

      return down
        ? time - time % roundDownTime
        : time + (roundDownTime - (time % roundDownTime));
    },
    toDate(tms) {
      return typeof tms === 'string'
        ? new Date(tms) // Safari is strict about the format
        : new Date(tms.year, tms.month - 1, tms.day, tms.hour, tms.minute);
    },
    toTimestamp(date) {
      return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
    },
  },
}
</script>

<style scoped>
  .v-fullscreen {
    position: absolute;
    padding-top: 1px;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
  }

  .v-calendar {
    user-select: none;
    -webkit-user-select: none;
  }
  .v-event-drag-bottom {
    position: absolute; 
    left: 0; 
    right: 0;
    bottom: 4px; 
    height: 4px; 
    cursor: ns-resize;
  }
  .v-event-drag-bottom::after {
    display: none;
    position: absolute; 
    left: 50%; 
    height: 4px; 
    border-top: 1px solid white; 
    border-bottom: 1px solid white; 
    width: 20px; 
    margin-left: -10px;
    opacity: 0.8;
    content: '';
  }
  .v-event-timed:hover .v-event-drag-bottom::after {
    display: block;
  }
  .v-current-time {
    height: 1px;
    background-color: red;
    position: absolute;
    left: -1px;
    right: 0;
    pointer-events: none;
  }
    
  .v-current-time.first::after {
    content: '';
    position: absolute;
    background-color: red;
    width: 7px;
    height: 7px;
    border-radius: 3.5px;
    margin-top: -3px;
    margin-left: -3px;
  }
    
  .v-current-time .v-current-time-time {
    display: none;
  }
    
  .v-current-time.first .v-current-time-time {
    display: block;
    position: absolute;
    left: -60px;
    color: red;
    font-size: 10px;
    padding: 1px;
    background-color: white;
    width: 50px;
    height: 20px;
    text-align: right;
    top: -9px;
  }
</style>

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Improvement/refactoring (non-breaking change that doesn't add any features but makes things better)

Checklist:

  • The PR title is no longer than 64 characters.
  • The PR is submitted to the correct branch (master for bug fixes and documentation updates, dev for new features and backwards compatible changes and next for non-backwards compatible changes).
  • My code follows the code style of this project.
  • I've added relevant changes to the documentation (applies to new features and breaking changes in core library)

@jacekkarczmarczyk jacekkarczmarczyk added C: VCalendar VCalendar T: bug Functionality that does not work as intended/expected labels Apr 16, 2020
@johnleider johnleider merged commit e949427 into master Apr 20, 2020
@johnleider johnleider deleted the fix/calendar-slot-bug branch April 20, 2020 20:42
@lock lock bot locked as resolved and limited conversation to collaborators May 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
C: VCalendar VCalendar T: bug Functionality that does not work as intended/expected
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug Report] v-calendar empty when event slot is used and the default slot is empty
3 participants