Skip to content

Instantly share code, notes, and snippets.

@ksassnowski
Created January 15, 2022 16:09
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ksassnowski/8629ff5ee8ced9984d997f93bba772c4 to your computer and use it in GitHub Desktop.
Save ksassnowski/8629ff5ee8ced9984d997f93bba772c4 to your computer and use it in GitHub Desktop.
Datepicker Framework, I guess
import {computed, ref} from "vue";
import {
addMonths, addYears,
eachDayOfInterval,
endOfISOWeek,
getYear,
getDate,
isSameMonth,
startOfISOWeek,
startOfMonth,
subMonths,
subYears, addWeeks, startOfYear, endOfYear, eachMonthOfInterval, format, isSameDay,
} from "date-fns";
interface Day {
date: Date;
number: number;
active: boolean;
currentMonth: boolean;
}
interface Month {
date: Date;
label: string;
active: boolean;
}
enum ViewMode {
Days,
Months,
Years,
}
function getVisibleDays(selectedDate: Date, startOfCurrentMonth: Date): Day[] {
const startDate = startOfISOWeek(startOfCurrentMonth);
const endDate = endOfISOWeek(addWeeks(startDate, 5));
return eachDayOfInterval({
start: startDate,
end: endDate
}).map((day: Date) => ({
date: day,
number: getDate(day),
active: isSameDay(day, selectedDate),
currentMonth: isSameMonth(day, startOfCurrentMonth),
}));
}
function getVisibleMonths(startOfCurrentYear: Date, startOfCurrentMonth: Date): Month[] {
const startDate = startOfCurrentYear;
const endDate = endOfYear(startOfCurrentYear);
return eachMonthOfInterval({
start: startDate,
end: endDate,
}).map(month => ({
date: month,
label: format(month, 'MMMM'),
active: isSameMonth(month, startOfCurrentMonth),
}));
}
export default function (initialDate?: Date|string|number) {
const visible = ref(false);
const selectedDate = ref(initialDate ? new Date(initialDate) : new Date());
const startOfCurrentMonth = ref(startOfMonth(selectedDate.value));
const currentYear = computed(() => getYear(startOfCurrentMonth.value));
const visibleDays = computed(() => getVisibleDays(selectedDate.value, startOfCurrentMonth.value));
const visibleMonths = computed(() => getVisibleMonths(
startOfYear(startOfCurrentMonth.value),
startOfCurrentMonth.value)
);
const viewMode = ref(ViewMode.Days);
const showingDays = computed(() => viewMode.value === ViewMode.Days);
const showingMonths = computed(() => viewMode.value === ViewMode.Months);
return {
selectedDate,
currentYear,
visibleDays,
visibleMonths,
visible,
startOfCurrentMonth,
showingDays,
showingMonths,
open() {
visible.value = true;
},
close() {
visible.value = false;
},
toggle() {
visible.value = !visible.value;
},
select(date: Date, close: boolean = false) {
selectedDate.value = date;
startOfCurrentMonth.value = startOfMonth(date)
if (close) {
visible.value = false;
}
},
showMonths() {
viewMode.value = ViewMode.Months;
},
selectMonth(month: Month) {
startOfCurrentMonth.value = month.date;
viewMode.value = ViewMode.Days;
},
nextMonth() {
startOfCurrentMonth.value = startOfMonth(addMonths(startOfCurrentMonth.value, 1));
},
previousMonth() {
startOfCurrentMonth.value = startOfMonth(subMonths(startOfCurrentMonth.value, 1));
},
nextYear() {
startOfCurrentMonth.value = startOfMonth(addYears(startOfCurrentMonth.value, 1));
},
previousYear() {
startOfCurrentMonth.value = startOfMonth(subYears(startOfCurrentMonth.value, 1));
},
};
};
@ksassnowski
Copy link
Author

ksassnowski commented Jan 15, 2022

And then build a component on top of that. Boom, instant date picker.

Note, the point is not to use this component but that you can build whatever UI you want on top of it.

(This is still incomplete, like the ViewState.Year isn’t even used anywhere, but you get the point)

https://play.tailwindcss.com/P73Vdh29uV

<template>
  <button @click="toggle">
    {{ format(selectedDate, "yyyy-MM-dd") }}
  </button>

  <div
    class="overflow-hidden rounded-lg shadow-lg bg-white ring-1 ring-black ring-opacity-5">
    <div class="flex items-center justify-between bg-cyan-700 text-white font-semibold py-1.5 px-2">
      <button
        v-if="showingDays"
        type="button"
        class="text-white hover:bg-cyan-800 rounded p-1"
        @click="previousMonth"
      >
        <ChevronLeftIcon class="w-6 h-6 text-current" />
      </button>

      <button
        type="button"
        class="px-2 py-1 rounded text-sm font-medium hover:bg-cyan-800"
        @click="showMonths"
      >
        {{ format(startOfCurrentMonth, "MMMM") }}
      </button>

      <button
        v-if="showingDays"
        type="button"
        class="text-white hover:bg-cyan-800 rounded p-1"
        @click="nextMonth"
      >
        <ChevronRightIcon class="w-6 h-6 text-current" />
      </button>
    </div>

    <div
      class="grid grid-rows-6 pt-1 px-2 pb-3 justify-center items-center"
      :class="{
        'grid-cols-7': showingDays,
        'grid-cols-2 gap-2': showingMonths,
      }"
    >
      <template v-if="showingMonths">
        <button
          v-for="month in visibleMonths"
          @click="selectMonth(month)"
          type="button"
          class="rounded-lg p-2 text-sm"
          :class="{
            'bg-cyan-700 text-white': month.active,
            'hover:bg-cyan-50': !month.active,
          }"
        >
          {{ month.label }}
        </button>
      </template>

      <template v-if="showingDays">
        <div
          class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center">
          Mon
        </div>
        <div class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center"> 
            Tue
        </div>
        <div class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center">
          Wed
        </div>
        <div class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center">
          Thu
        </div>
        <div class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center">
          Fri
        </div>
        <div class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center">
          Sat
        </div>
        <div class="text-xs uppercase text-slate-500 font-medium tracking-tight flex items-center justify-center">
          Sun
        </div>

        <div
          v-for="day in visibleDays"
          class="flex justify-center items-center"
        >
          <button
            type="button"
            class="rounded-full w-8 h-8 inline-flex items-center justify-center text-sm"
            :class="{
              'hover:bg-cyan-50': !day.active,
              'text-slate-700': day.currentMonth && !day.active,
              'text-slate-300': !day.currentMonth,
              'bg-cyan-600 text-white': day.active,
            }"
            @click="select(day.date, true)"
          >
            {{ day.number }}
          </button>
        </div>
      </template>
    </div>
  </div>

  <!-- 
      And so on, you get the point. The rest is left as an exercise to the reader. Note that
      I wrote this in like an hour so there are definitely some rough edges to this. But at
      least this way I have complete control over everything and don't have to fight with 
      the opinions of some random package.
  -->
</template>

<script lang="ts">
import { defineComponent, watch } from "vue";
import { format } from "date-fns";

import useDatepicker from "@/scripts/Composables/useDatepicker";

export default defineComponent({
  props: {
    modelValue: {
      type: Date,
      default: () => new Date(),
    },
  },

  setup(props, { emit }) {
    const datepicker = useDatepicker(props.modelValue);

    watch(datepicker.selectedDate, (newDate: Date) =>
      emit("update:modelValue", newDate)
    );

    return {
      format,
      ...datepicker,
    };
  },
});
</script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment