Browse Source

improve datepicker.vue

tripeur 4 years ago
parent
commit
8770854ea1
5 changed files with 280 additions and 135 deletions
  1. 1 0
      src/App.vue
  2. 3 3
      src/components/EditeurCreneau.vue
  3. 274 130
      src/components/date-picker.vue
  4. 1 1
      src/utils/toast.css
  5. 1 1
      src/views/Home.vue

+ 1 - 0
src/App.vue

@@ -214,6 +214,7 @@ export default defineComponent({
   padding: 16px 32px;
   margin-top: 100px;
   height: min-content;
+  border-radius: 2px;
 }
 .spinner {
   color: var(--color-accent-400);

+ 3 - 3
src/components/EditeurCreneau.vue

@@ -34,7 +34,7 @@
         label="Date"
         id="creneauDate"
         type="text"
-        placeholder="yyyy-mm-dd"
+        placeholder="yyyy/mm/dd"
         v-model.lazy="jour"
       />
       <styled-input
@@ -215,7 +215,7 @@ export default defineComponent({
       );
     },
     validStart(): boolean {
-      return /\d{4}-\d{1,2}-\d{1,2}/.test(this.jour) && dayjs(this.concatStart).isValid();
+      return /\d{4}\/\d{1,2}\/\d{1,2}/.test(this.jour) && dayjs(this.concatStart).isValid();
     },
     startDate(): Date {
       return new Date(this.concatStart);
@@ -288,7 +288,7 @@ export default defineComponent({
     updateForm: function () {
       if (this.creneau) {
         const startDate = dayjs(this.creneau.start);
-        this.jour = startDate.format("YYYY-MM-DD");
+        this.jour = startDate.format("YYYY/MM/DD");
         this.heure = startDate.format("HH:mm");
         this.duree = Math.round(
           (this.creneau.end.getTime() - this.creneau.start.getTime()) / 1000 / 60

+ 274 - 130
src/components/date-picker.vue

@@ -1,35 +1,40 @@
 <template>
-  <div class="ds-formcontrol ds-date-input-control">
-    <div class="ds-formcontrol-label">{{ title }}</div>
-    <input
-      class="ds-date-input browser-default"
-      type="text"
-      placeholder="yyyy/mm/dd"
-      v-model="textValue"
-      @click="openPicker"
-      @focus="openPicker"
-    />
-    <div class="ds-datepicker-container" :class="[{ show: isPickerOpen }, position]">
-      <div class="ds-datepicker">
-        <div class="ds-datepicker-prev" @click="prevMonth" tabindex="-1"></div>
-        <div class="ds-datepicker-current">{{ pickerTitle }}</div>
-        <div class="ds-datepicker-next" @click="nextMonth" tabindex="-1"></div>
-        <div class="ds-datepicker-weekday">
+  <div class="formcontrol date-input-control">
+    <div class="formcontrol-label">{{ label }}</div>
+    <div class="input-wrapper" @click="openPicker">
+      <input
+        class="date-input"
+        type="text"
+        placeholder="yyyy/mm/dd"
+        v-model="textValue"
+        @focus="openPicker"
+      />
+    </div>
+    <div class="datepicker-container" :class="[{ show: isPickerOpen }, position]">
+      <div class="datepicker" :class="zoom">
+        <div class="datepicker-header">
+          <div class="datepicker-arrow" @click="prev" tabindex="-1">chevron_left</div>
+          <div class="datepicker-current" @click="goUp">{{ pickerTitle }}</div>
+          <div class="datepicker-arrow" @click="next" tabindex="-1">chevron_right</div>
+        </div>
+        <div class="datepicker-weekday" v-if="zoom == 'month'">
           <div v-for="(d, i) in weekDays" :key="i">{{ d }}</div>
         </div>
-        <div
-          v-for="(d, i) in currentDays"
-          @click="selectDay(i)"
-          tabindex="-1"
-          :key="i"
-          :class="{
-            'ds-disable': d.isBefore(min) || d.isAfter(max),
-            'ds-out': !d.isSame(currentMonth, 'month'),
-            'ds-selected': d.isSame(valueObject, 'date'),
-            'ds-today': d.isSame(today, 'date'),
-          }"
-        >
-          {{ d.format("D") }}
+        <div class="datepicker-items" :class="zoom">
+          <div
+            v-for="(d, i) in pickerContent"
+            @click="selectItem(i)"
+            tabindex="-1"
+            :key="i"
+            :class="{
+              disable: d.isBefore(min) || d.isAfter(max),
+              out: isOut(d),
+              selected: d.isSame(valueObject, sameKey),
+              today: d.isSame(today, sameKey),
+            }"
+          >
+            {{ d.format(pickerFormat) }}
+          </div>
         </div>
       </div>
     </div>
@@ -38,19 +43,92 @@
 
 <script lang="ts">
 import { defineComponent } from "vue";
-import dayjs, { Dayjs } from "dayjs";
+import dayjs, { Dayjs, OpUnitType } from "dayjs";
 // eslint-disable-next-line
 const dateValidator = (d: any) => dayjs(d).isValid();
+const decadeSpan = 10;
+type ZoomLevel = "day" | "month" | "year" | "decade";
+type ContentParameter = { getFirstDate: (d: Dayjs) => Dayjs; delta: OpUnitType; maxItem: number };
+type ZoomParameter = {
+  pickerTitle: (d: Dayjs) => string;
+  content: ContentParameter;
+  pickerFormat: string;
+  outOfCurrentZoom: (activeDate: Dayjs, other: Dayjs) => boolean;
+  sameKey: OpUnitType;
+  nextKey: OpUnitType;
+};
+type PickerParameter = {
+  [k in ZoomLevel]: ZoomParameter;
+};
+const datepickerParameter: PickerParameter = {
+  day: {
+    pickerTitle: (d) => d.format("ddd D MMMM YYYY"),
+    content: {
+      getFirstDate: (d) => d.startOf("d").startOf("h"),
+      delta: "hour",
+      maxItem: 24,
+    },
+    pickerFormat: "HH:mm",
+    outOfCurrentZoom: () => false,
+    sameKey: "hour",
+    nextKey: "day",
+  },
+  month: {
+    pickerTitle: (d) => d.format("MMMM YYYY"),
+    content: {
+      getFirstDate: (d) => d.startOf("month").startOf("week"),
+      delta: "day",
+      maxItem: 6 * 7,
+    },
+    pickerFormat: "D",
+    outOfCurrentZoom: (d, other) => !d.isSame(other, "month"),
+    sameKey: "day",
+    nextKey: "month",
+  },
+  year: {
+    pickerTitle: (d) => d.format("YYYY"),
+    content: {
+      getFirstDate: (d) => d.startOf("year"),
+      delta: "month",
+      maxItem: 12,
+    },
+    pickerFormat: "MMM",
+    outOfCurrentZoom: (d, other) => !d.isSame(other, "year"),
+    sameKey: "month",
+    nextKey: "year",
+  },
+  decade: {
+    pickerTitle: (d) => {
+      let tmp = d.startOf("y");
+      tmp = tmp.year(Math.floor(tmp.year() / decadeSpan) * decadeSpan);
+      return tmp.format("YYYY") + "-" + tmp.add(decadeSpan - 1, "y").format("YYYY");
+    },
+    content: {
+      getFirstDate: (d) =>
+        d
+          .startOf("y")
+          .year(Math.floor(d.year() / decadeSpan) * decadeSpan)
+          .subtract(1, "y"),
+      delta: "year",
+      maxItem: 12,
+    },
+    pickerFormat: "YYYY",
+    outOfCurrentZoom: (d, other) =>
+      Math.floor(d.year() / decadeSpan) !== Math.floor(other.year() / decadeSpan),
+    sameKey: "year",
+    nextKey: "year",
+  },
+};
 export default defineComponent({
   name: "DatePicker",
-  emit: ["input"],
+  emits: ["update:modelValue"],
   props: {
-    title: {
+    label: {
       type: String,
       required: false,
       default: () => "Select date",
     },
-    value: {
+    modelValue: {
       type: [Number, Object, String, Date],
       required: false,
       default: () => "",
@@ -87,28 +165,46 @@ export default defineComponent({
     return {
       today: now,
       currentMonth: now.startOf("month"),
+      zoom: "month" as "day" | "month" | "year" | "decade",
       isPickerOpen: false,
       valueObject: null as Dayjs | null,
       textValue: "",
-      weekDays: "SMTWTFS".split(""),
       textRegex: /^(?<year>\d{4})\/(?<month>(?:0[1-9])|(?:1[0-2]))\/(?<day>(?:0[1-9])|(?:[12][0-9])|(?:3[01]))$/,
     };
   },
   computed: {
-    pickerTitle: function (): string {
-      return this.currentMonth.format("MMMM YYYY");
+    currentZoomParameter(): ZoomParameter {
+      return datepickerParameter[this.zoom];
+    },
+    pickerTitle(): string {
+      var title: string = this.currentZoomParameter.pickerTitle(this.currentMonth);
+      return title[0].toUpperCase() + title.slice(1);
     },
-    currentDays: function (): Array<Dayjs> {
-      const firstDay = this.currentMonth.startOf("month").startOf("week");
-      const output = [];
-      for (let i = 0; i < 7 * 6; i++) {
-        output.push(firstDay.add(i, "day"));
+    pickerContent(): Array<Dayjs> {
+      const output: Array<Dayjs> = [];
+      let firstDay = this.currentZoomParameter.content.getFirstDate(this.currentMonth);
+      let delta: OpUnitType = this.currentZoomParameter.content.delta;
+      let maxItem: number = this.currentZoomParameter.content.maxItem;
+      for (let i = 0; i < maxItem; i++) {
+        output.push(firstDay.add(i, delta));
       }
       return output;
     },
+    pickerFormat(): string {
+      return this.currentZoomParameter.pickerFormat;
+    },
+    sameKey(): OpUnitType {
+      return this.currentZoomParameter.sameKey;
+    },
+    weekDays: function (): Array<string> {
+      const start = dayjs().startOf("week");
+      return Array(7)
+        .fill(0)
+        .map((_, i) => start.add(i, "d").format("dd"));
+    },
   },
   watch: {
-    value: function (val) {
+    modelValue: function (val) {
       if (["object", "string", "number"].includes(typeof val)) {
         this.valueObject = dayjs(val);
       }
@@ -137,23 +233,47 @@ export default defineComponent({
     valueObject: function (v) {
       if (v) {
         this.textValue = v.format("YYYY/MM/DD");
-        this.$emit("input", this.textValue);
+        this.$emit("update:modelValue", this.textValue);
+        this.zoom = "day";
       } else {
         this.textValue = "";
       }
     },
   },
   methods: {
-    prevMonth: function (): void {
-      this.currentMonth = this.currentMonth.add(-1, "month");
+    prev(): void {
+      this.changeCurrentDate(-1);
+    },
+    next(): void {
+      this.changeCurrentDate(1);
     },
-    nextMonth: function (): void {
-      this.currentMonth = this.currentMonth.add(1, "month");
+    changeCurrentDate(direction: number): void {
+      const amount = direction * (this.zoom == "decade" ? decadeSpan : 1);
+      this.currentMonth = this.currentMonth.add(amount, this.currentZoomParameter.nextKey);
     },
-    selectDay: function (i: number): void {
-      const d = this.currentDays[i];
-      if (d.isAfter(this.min) && d.isBefore(this.max)) {
-        this.valueObject = dayjs(d);
+    isOut(d: Dayjs): boolean {
+      return this.currentZoomParameter.outOfCurrentZoom(this.currentMonth, d);
+    },
+    selectItem: function (i: number): void {
+      const d = this.pickerContent[i];
+      if (this.zoom == "day") {
+        if (d.isAfter(this.min) && d.isBefore(this.max)) {
+          this.valueObject = d;
+        }
+      } else {
+        this.currentMonth = d;
+        this.zoom = this.zoom == "decade" ? "year" : this.zoom == "year" ? "month" : "day";
+      }
+    },
+    goUp() {
+      if (this.zoom === "year") {
+        this.zoom = "decade";
+      }
+      if (this.zoom === "month") {
+        this.zoom = "year";
+      }
+      if (this.zoom === "day") {
+        this.zoom = "month";
       }
     },
     openPicker: function () {
@@ -166,7 +286,7 @@ export default defineComponent({
     loseFocusListener: function (e: Event) {
       if (
         !this.$el.contains(e.target) ||
-        (e.target as HTMLElement).classList.contains("ds-date-input-title")
+        (e.target as HTMLElement).classList.contains("date-input-title")
       ) {
         window.removeEventListener("focusin", this.loseFocusListener);
         window.removeEventListener("click", this.loseFocusListener);
@@ -176,166 +296,190 @@ export default defineComponent({
           this.valueObject = d;
           this.textValue = d.format("YYYY/MM/DD");
         } else {
-          this.$emit("input", "");
+          this.$emit("update:modelValue", "");
         }
       }
     },
   },
   mounted: function () {
-    if (this.value) {
-      if (["object", "string", "number"].includes(typeof this.value)) {
-        this.valueObject = dayjs(this.value);
+    if (this.modelValue) {
+      if (["object", "string", "number"].includes(typeof this.modelValue)) {
+        this.valueObject = dayjs(this.modelValue);
       }
     }
   },
 });
 </script>
 <style>
-.ds-date-input-control {
+.date-input-control {
   width: 8rem;
   margin-right: 4px;
   position: relative;
 }
-.ds-date-input-title {
+.date-input-title {
   color: #505d74;
   font-weight: bold;
   padding-bottom: 4px;
 }
+.input-wrapper {
+  position: relative;
+}
+.input-wrapper:after {
+  font-family: "Material Icons";
+  font-weight: normal;
+  font-style: normal;
+  font-size: 20px;
+  content: "calendar_today";
+  position: absolute;
+  right: 8px;
+  top: 3px;
+  color: var(--color-accent-400);
+  cursor: pointer;
+}
 
-.ds-date-input[type="text"] {
+.date-input[type="text"] {
   all: initial;
-  color: #282e3a;
+  color: var(--color-neutral-100);
   width: 100%;
   border: none;
-  height: 2.5rem;
+  height: 2rem;
   margin: 0;
   outline: 0;
-  padding: 0.5rem;
+  padding: 0.2rem 0.5rem;
   font-size: 0.875rem;
   -webkit-appearance: none;
   -moz-appearance: none;
   appearance: none;
+  box-shadow: 0 -1px 0 0 var(--color-accent-700) inset;
   box-sizing: border-box;
   -webkit-transition: 0.1s ease-in-out;
   transition: 0.1s ease-in-out;
   font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif;
   line-height: 1.25rem;
-  border-radius: 3px 3px 0 0;
-  background-color: #f7f9fa;
+  background-color: var(--color-accent-950);
   -webkit-transition-property: box-shadow, border;
   transition-property: box-shadow, border;
-  border: none;
-  border-bottom: solid 2px grey;
-  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzE4Mzc4MiI+PHBhdGggZD0iTTIwIDNoLTFWMWgtMnYySDdWMUg1djJINGMtMS4xIDAtMiAuOS0yIDJ2MTZjMCAxLjEuOSAyIDIgMmgxNmMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE4SDRWOGgxNnYxM3oiLz48L3N2Zz4=");
-  background-repeat: no-repeat;
-  background-position: right 8px top 45%;
-  background-size: 20px;
+  position: relative;
 }
-
-.ds-date-input[type="text"]:focus {
-  border-bottom: solid 2px #255fcc;
+.date-input[type="text"]:focus {
+  border-bottom: solid 2px var(--color-accent-400);
 }
-.ds-date-input[type="text"]:focus + .ds-datepicker-container,
-.ds-date-input[type="text"]:hover + .ds-datepicker-container {
+.date-input[type="text"]:focus + .datepicker-container,
+.date-input[type="text"]:hover + .datepicker-container {
   display: block;
 }
-.ds-datepicker-container {
+.datepicker-container {
   position: absolute;
   width: 315px;
-  height: 360px;
+  height: 350px;
   display: none;
   z-index: 10;
 }
-.ds-datepicker-container.show {
+.datepicker-container.show {
   display: block;
 }
-.ds-datepicker-container.same-size {
+.datepicker-container.same-size {
   width: 100%;
   height: auto;
   padding-top: 114%;
 }
-.ds-datepicker-container.right {
+.datepicker-container.right {
   right: 0px;
 }
-.ds-datepicker {
+.datepicker {
   position: absolute;
   top: 0;
   left: 0;
   bottom: 0;
   right: 0;
   background: white;
-  display: grid;
-  grid-template-columns: repeat(7, 1fr);
-  grid-template-rows: repeat(8, 1fr);
-  column-gap: 4px;
-  row-gap: 4px;
   box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.14);
   padding: 8px;
   z-index: 10;
+  display: grid;
+  grid-template: 1fr 7fr/ 1fr;
+  row-gap: 4px;
 }
-.ds-datepicker > div {
-  text-align: center;
-  color: #505d74;
+.datepicker.month {
+  grid-template-rows: 1fr 1fr 6fr;
+}
+.datepicker-header,
+.datepicker-weekday,
+.datepicker-items {
+  color: var(--color-neutral-400);
+  display: grid;
+  column-gap: 4px;
+  row-gap: 4px;
+  grid-template-rows: 1fr;
+  grid-template-columns: repeat(7, 1fr);
+}
+.datepicker-items.day {
+  grid-template-columns: repeat(4, 1fr);
+  grid-template-rows: repeat(6, 1fr);
+}
+.datepicker-items.month {
+  grid-template-rows: repeat(6, 1fr);
+}
+.datepicker-items.decade,
+.datepicker-items.year {
+  grid-template-columns: repeat(4, 1fr);
+  grid-template-rows: repeat(3, 1fr);
+}
+.datepicker-header > div,
+.datepicker-weekday > div,
+.datepicker-items > div {
+  border: solid 2px transparent;
   border-radius: 3px;
   display: flex;
   justify-content: center;
   align-items: center;
+  text-align: center;
 }
-.ds-datepicker-prev:before,
-.ds-datepicker-next:before {
-  content: "";
-  width: 100%;
-  height: 100%;
-  background-repeat: no-repeat;
-  background-position: center;
-  background-size: 35px;
-}
-.ds-datepicker-prev:before {
-  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+PHBhdGggZD0iTTE0LjcxIDYuNzFhLjk5Ni45OTYgMCAwMC0xLjQxIDBMOC43MSAxMS4zYS45OTYuOTk2IDAgMDAwIDEuNDFsNC41OSA0LjU5YS45OTYuOTk2IDAgMTAxLjQxLTEuNDFMMTAuODMgMTJsMy44OC0zLjg4Yy4zOS0uMzkuMzgtMS4wMyAwLTEuNDF6IiBmaWxsPSIjMTgzNzgyIi8+PC9zdmc+");
+.datepicker-arrow {
+  font-family: "Material Icons";
+  font-weight: normal;
+  font-style: normal;
+  font-size: 35px;
+  text-align: center;
+  color: var(--color-accent-400);
 }
-.ds-datepicker-next:before {
-  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+PHBhdGggZD0iTTkuMjkgNi43MWEuOTk2Ljk5NiAwIDAwMCAxLjQxTDEzLjE3IDEybC0zLjg4IDMuODhhLjk5Ni45OTYgMCAxMDEuNDEgMS40MWw0LjU5LTQuNTlhLjk5Ni45OTYgMCAwMDAtMS40MUwxMC43IDYuN2MtLjM4LS4zOC0xLjAyLS4zOC0xLjQxLjAxeiIgZmlsbD0iIzE4Mzc4MiIvPjwvc3ZnPg==");
+
+.datepicker-current {
+  grid-column: 2/7;
+  font-weight: bold;
 }
-.ds-datepicker > div:not(.ds-datepicker-weekday):not(.ds-disable) {
+.datepicker-header > div,
+.datepicker-items > div {
   cursor: pointer;
 }
-.ds-datepicker > div:not(.ds-disable):not(.ds-datepicker-weekday):hover {
+.datepicker-header > div:hover,
+.datepicker-items > div:not(.disable):hover {
   background: #f0f4f6;
-  color: #505d74;
+  color: var(--color-neutral-100);
 }
-.ds-datepicker > div:not(.ds-disable):not(.ds-datepicker-weekday):focus {
-  border: solid 2px #255fcc;
-  border-radius: 0px;
+.datepicker-header > div:focus,
+.datepicker-items > div:not(.disable):focus {
+  border-color: var(--color-accent-500);
+  border-radius: 2px;
 }
-.ds-datepicker > div.ds-selected:not(.ds-disable):not(.ds-datepicker-weekday) {
-  background: #063b9e;
+.datepicker-items > div.selected:not(.disable) {
+  background: var(--color-accent-500);
   color: #fff;
   font-weight: bold;
 }
-.ds-datepicker-current {
-  grid-column: 2 / 7;
-  font-weight: bold;
-}
-div.ds-datepicker-weekday {
-  grid-column: 1 / 8;
-  display: grid;
-  grid-template-columns: repeat(7, 1fr);
-  cursor: initial;
-}
-.ds-datepicker-weekday > div {
-  text-align: center;
-  color: #c1c7d3;
+.datepicker-weekday > div {
+  color: var(--color-neutral-600);
 }
-.ds-datepicker > .ds-today {
-  border: solid 2px #063b9e;
+.datepicker-items .today {
+  border: solid 2px var(--color-accent-500);
   font-weight: bold;
-  color: #063b9e;
+  color: var(--color-accent-500);
 }
-.ds-datepicker .ds-disable,
-.ds-datepicker .ds-out {
-  color: #c1c7d3;
+.datepicker-items .disable,
+.datepicker-items .out {
+  color: var(--color-neutral-800);
 }
-.ds-datepicker > .ds-disable {
+.datepicker-items .disable {
   cursor: not-allowed;
 }
 </style>

+ 1 - 1
src/utils/toast.css

@@ -79,7 +79,7 @@
 }
 
 .toast.success{
-  color: #282e3a;
+  color: white;
   background-color: #08875b;
 }
 .toast.success::after{

+ 1 - 1
src/views/Home.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="centered-box">
     <div>
-      <h3>Bienvenue sur le gestionnaire des bénévoles</h3>
+      <h3>Bienvenue sur le gestionnaire du planning bénévoles</h3>
       <styled-input label="Identifiant" :modelValue="evenement.uuid" disabled />
       <styled-input
         label="Nom de l'événement"