Skip to content

juancamejoalarcon/offerdatepicker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Offerdatepicker

A cool angular 2+ component built with ❤︎ by Juan Camejo Alarcón

Travis

This is a datepicker calendar that lets you select a range of dates and within that range you are also able to select days when you don't want this range to be applied. For instance, if I were to create an offer, that is going to be available during a period of time, but I don't want this offer to be available on Saturdays, or the day 24th of December, this datepicker lets you select both, so within a range you will also have 'blackouts' (didn't know how to call it).

Quick tutorial

  1. Select the range
  2. Press the 'blackouts' button
  3. Select your blackouts
  4. If you want to unselect a blackout just press it again
  5. If you want to unselect all the blackouts press the 'clear' button

Demo

Mess with it in stackblitz: DEMO

Installation

I built this datepicker on top of the calendar already created by Bootstrap with ng-bootstrap. So the first step would be to install. ng-bootstrap

Usage

As it is done with others ng-bootstrap components create the offerdatepicker using the following code:

component.ts

import {Component, ViewChild, ElementRef, Renderer2, OnInit} from '@angular/core';
import {NgbDateStruct, NgbCalendar} from '@ng-bootstrap/ng-bootstrap';

const equals = (one: NgbDateStruct, two: NgbDateStruct) =>
  one && two && two.year === one.year && two.month === one.month && two.day === one.day;

const before = (one: NgbDateStruct, two: NgbDateStruct) =>
  !one || !two ? false : one.year === two.year ? one.month === two.month ? one.day === two.day
    ? false : one.day < two.day : one.month < two.month : one.year < two.year;

const after = (one: NgbDateStruct, two: NgbDateStruct) =>
  !one || !two ? false : one.year === two.year ? one.month === two.month ? one.day === two.day
    ? false : one.day > two.day : one.month > two.month : one.year > two.year;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  hoveredDate: NgbDateStruct;
  fromDate: NgbDateStruct;
  toDate: NgbDateStruct;

  blackOutDate: NgbDateStruct;
  blackOutsList: Array<NgbDateStruct> = [];
  blackOutModeActive: boolean = false;

  @ViewChild('blackoutsDatePicker') blackoutsDatePicker: ElementRef;

  constructor(calendar: NgbCalendar, private renderer: Renderer2) {
    this.fromDate = calendar.getToday();
    this.toDate = calendar.getNext(calendar.getToday(), 'd', 10);
  }

  ngOnInit() {
  }

  onDateSelection(date: NgbDateStruct) {
    if (!this.blackOutModeActive) {
      if (!this.fromDate && !this.toDate) {
        this.fromDate = date;
      } else if (this.fromDate && !this.toDate && after(date, this.fromDate)) {
        this.toDate = date;
      } else {
        this.toDate = null;
        this.fromDate = date;
      }
    } else {
      if (!this.blackOutsList.length) {
        this.blackOutDate = date;
        this.blackOutsList.push(date);
      } else {
        if (!this.isInBlackOutsList(date)) {
          this.blackOutDate = date;
          this.blackOutsList.push(date);
        } else {
          const index = this.blackOutsList.indexOf(this.isInBlackOutsList(date));
          if (index !== -1) this.blackOutsList.splice(index, 1);
        }
      }
    }
  }

  blackoutMode() {
    if (!this.blackOutModeActive) {
      this.blackOutModeActive = true;
      const days = this.blackoutsDatePicker.nativeElement.querySelectorAll('.ngb-dp-day');
      for (const day of days) {
        if (!day.childNodes[2].classList.contains('range')) {
          day.style.pointerEvents = 'none';
        }
      }
      // (Ugly programming alert)
      // This function makes sure that if you click an arrow you cannot 
      // click days out of the selected range
      for (const arrow of this.blackoutsDatePicker.nativeElement.querySelectorAll('.ngb-dp-arrow')) {
        const arrowListener = this.renderer.listen(arrow, 'click', () => {
          if (this.blackOutModeActive) {
            const days = this.blackoutsDatePicker.nativeElement.querySelectorAll('.ngb-dp-day');
            for (const day of days) {
              if (!day.childNodes[2].classList.contains('range')) {
              day.style.pointerEvents = 'none';
            }
          }
        }
      });
    }
    } else {
      this.blackOutModeActive = false;
      const days = this.blackoutsDatePicker.nativeElement.querySelectorAll('.ngb-dp-day');
      for (const day of days) {
        day.style.pointerEvents = 'auto';
      }
    }
  }

  isInBlackOutsList(date: NgbDateStruct) {
    for (const blackout of this.blackOutsList) {
      if (equals(date, blackout)) {
        return true && blackout;
      }
    }
  }

  clearBlackoutsList() {
    this.blackOutsList = [];
  }

  isHovered = date => this.fromDate && !this.toDate && this.hoveredDate && after(date, this.fromDate) && before(date, this.hoveredDate);
  isInside = date => after(date, this.fromDate) && before(date, this.toDate);
  isFrom = date => equals(date, this.fromDate);
  isTo = date => equals(date, this.toDate);
  // This function checks if a date is within a range
  isInRange = date => this.isFrom(date) || this.isTo(date) || this.isInside(date) || this.isHovered(date);
}

component.html

<p>Example of the range selection</p>

<div #blackoutsDatePicker>

  <ngb-datepicker #dp (select)="onDateSelection($event)" [displayMonths]="1" [dayTemplate]="t">
  </ngb-datepicker>

  <ng-template #t let-date="date" let-focused="focused">
    <span class="custom-day"
        [class.focused]="focused"
        [class.range]="isFrom(date) || isTo(date) || isInside(date) || isHovered(date)"
        [class.faded]="isHovered(date) || isInside(date)"
        [class.blackOutMode]="blackOutModeActive && !isInRange(date)"
        [class.blackOutHover]="blackOutModeActive && isInRange(date)"
        [class.blackout]="isInBlackOutsList(date) && isInRange(date)"
        (mouseenter)="hoveredDate = date"
        (mouseleave)="hoveredDate = null">
      {{ date.day }}
    </span>
  </ng-template>

</div>
<button style="font-size:70%" type="button" class="btn btn-primary btn-sm my-1"(click)="blackoutMode()">
  Blackouts
</button>
<button style="font-size:70%" type="button" class="btn btn-secondary btn-sm my-1 ml-2" 
(click)="clearBlackoutsList()" [class.d-none]="!blackOutModeActive">
  Clear
</button>

<hr>

<pre>From: {{ fromDate | json }} </pre>
<pre>To: {{ toDate | json }} </pre>
<pre>Blackouts: {{ blackOutsList | json }} </pre>

component.css

.custom-day {
    text-align: center;
    padding: 0.185rem 0.25rem;
    display: inline-block;
    height: 2rem;
    width: 2rem;
  }
  .custom-day.focused {
    background-color: #e6e6e6;
  }
  .custom-day.range, .custom-day:hover {
    background-color: rgb(2, 117, 216);
    color: white;
  }
  .custom-day.faded {
    background-color: rgba(2, 117, 216, 0.5);
  }
  .blackOutMode {
    background-color: white !important;
    color: white;
  }
  .blackOutMode:hover {
    color: #f8f9fa;
  }
  .blackOutHover:hover {
    background-color: black !important;
  }
  .blackout {
    background-color: #B6B6B6 !important;
  }