UNPKG

@doubletrade/lit-datepicker

Version:

lit-datepicker provides a simple datepicker with range.

391 lines (361 loc) 11.4 kB
/* eslint-disable no-underscore-dangle */ import { LitElement, html, css } from 'lit'; import format from 'date-fns/esm/format'; import parse from 'date-fns/esm/parse'; import '@polymer/paper-material/paper-material'; import '@polymer/iron-flex-layout/iron-flex-layout-classes'; import '@polymer/paper-input/paper-input'; import '@polymer/paper-icon-button/paper-icon-button'; import getDate from 'date-fns/esm/getDate'; import getMonth from 'date-fns/esm/getMonth'; import getYear from 'date-fns/esm/getYear'; import { ironFlexLayoutAlignTheme, ironFlexLayoutTheme } from '../iron-flex-import'; import localize from './localize'; class LitDatepickerDateInput extends localize(LitElement) { constructor() { super(); this.date = null; this.error = null; this.dateFormat = 'dd/MM/yyyy'; this.language = 'en'; this.resources = { en: { FORMAT_ERROR: 'Bad format ( \\d\\d )', MIN_ERROR: 'Value is too small', MAX_ERROR: 'Value is to high', RANGE_ERROR: 'Bad range', }, 'en-en': { FORMAT_ERROR: 'Bad format ( \\d\\d )', MIN_ERROR: 'Value is too small', MAX_ERROR: 'Value is to high', RANGE_ERROR: 'Bad range', }, 'en-US': { FORMAT_ERROR: 'Bad format ( \\d\\d )', MIN_ERROR: 'Value is too small', MAX_ERROR: 'Value is to high', RANGE_ERROR: 'Date "to" should be greater or equal to date "from"', }, 'en-us': { FORMAT_ERROR: 'Bad format ( \\d\\d )', MIN_ERROR: 'Value is too small', MAX_ERROR: 'Value is to high', RANGE_ERROR: 'Bad range', }, fr: { FORMAT_ERROR: 'Mauvais format ( \\d\\d )', MIN_ERROR: 'Valeur trop petite', MAX_ERROR: 'Valeur trop grande', RANGE_ERROR: 'Mauvais intervalle', }, 'fr-fr': { FORMAT_ERROR: 'Mauvais format ( \\d\\d )', MIN_ERROR: 'Valeur trop petite', MAX_ERROR: 'Valeur trop grande', RANGE_ERROR: 'Mauvais intervalle', }, }; } static get properties() { return { date: { type: String }, min: { type: Number }, max: { type: Number }, dateFormat: { type: String }, _day: { type: String }, _month: { type: String }, _year: { type: String }, error: { type: String }, resources: { type: Object }, language: { type: String }, outline: { type: Boolean }, }; } static get styles() { const mainStyle = css` :host { display: block; position: relative; } .date-input .date-separator::after { content:'/'; border-bottom: 1px solid var(--paper-input-container-color, var(--secondary-text-color)); height: 23px; display: flex; align-items: flex-end; margin-bottom: 5px; padding-bottom: 4px; width: 8px; } .date-input input:focus { outline: none; border-bottom: 2px solid var(--paper-input-container-focus-color, var(--primary-color)); } .date-input input { text-align: center; background-color: transparent; border: none; border-bottom: 1px solid var(--paper-input-container-color, var(--secondary-text-color)); padding: 0; padding-bottom: 2px; margin-bottom: 5px; font-family: Roboto; font-size: 14px; height:23px; } .date-input input.day, .date-input input.month { width: 40px; } .date-input input.year { width: 50px; } .date-input { display: flex; justify-content: center; width: 146px; align-items: flex-end; margin-right: 10px; } #error { height: 20px; color: var(--error-color, red); font-size: 10px; text-align: center; } .date-input input.error { border-bottom: 2px solid var(--error-color, red); } .date-input.error .date-separator::after { border-bottom: 2px solid var(--error-color, red); } .date-input.error input { border-bottom: 2px solid var(--error-color, red); } #error.outline { position: absolute; bottom: -25px; background-color: var(--error-background-color, white); padding: 0 8px; } .outline.date-input .date-separator::after , .outline.date-input input { border-bottom: none; } input { color: var(--lit-datepicker-input-color); } `; return [ironFlexLayoutTheme, ironFlexLayoutAlignTheme, mainStyle]; } render() { return html` <div class="date-input ${this.outline ? 'outline' : ''}"> <input class="day" tabindex=1 max="31" min="1" @keydown=${this.keypress.bind(this)} @focus=${this.selectAll.bind(this)} @tap=${this.selectAll.bind(this)} @focusout=${this.focusout.bind(this)} .value=${this._day} /> <div class="date-separator"></div> <input class="month" tabindex=2 max="12" min="1" @keydown=${this.keypress.bind(this)} @focus=${this.selectAll.bind(this)} @tap=${this.selectAll.bind(this)} @focusout=${this.focusout.bind(this)} .value=${this._month} /> <div class="date-separator"></div> <input class="year" tabindex=3 max="2100" min="1970" @keydown=${this.keypress.bind(this)} @focus=${this.selectAll.bind(this)} @tap=${this.selectAll.bind(this)} @focusout=${this.focusout.bind(this)} .value=${this._year} /> </div> ${this.error ? html` <div class="${this.outline ? 'outline' : ''}" id="error">${this.localize(this.error)}</div> ` : null} `; } updated(changedProperties) { if (changedProperties.has('date')) { this._refreshDayMonthYear(this.date); } } _refreshDayMonthYear(date) { this._day = this.getDay(date); this._month = this.getMonth(date); this._year = this.getYear(date); } focusNext(element) { if (element) { const next = element.nextElementSibling; if (next == null) { element.blur(); this.dispatchEvent(new CustomEvent('end-of-input')); return; } if (next.tagName.toLowerCase() === 'input') { next.focus(); next.select(); return; } this.focusNext(next); } } focusPrev(element) { if (element) { const prev = element.previousElementSibling; if (prev == null) { this.dispatchEvent(new CustomEvent('focus-prev')); return; } if (prev.tagName.toLowerCase() === 'input') { prev.focus(); prev.select(); return; } this.focusPrev(prev); } } keypress(event) { event.preventDefault(); const { currentTarget } = event; this.clearError(currentTarget); if (!currentTarget.currentChar) { currentTarget.currentChar = 1; } if (event.key === 'Enter') { currentTarget.blur(); this.dispatchEvent(new CustomEvent('validate')); } if (event.key === 'ArrowRight' || (!event.shiftKey && event.key === 'Tab')) { this.focusNext(currentTarget); } if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) { this.focusPrev(currentTarget); } if (event.key === 'Delete' || event.key === 'Backspace') { currentTarget.value = ''; currentTarget.currentChar = 1; this.dispatchEvent(new CustomEvent('date-changed')); } if (!Number.isNaN(parseInt(event.key, 10))) { const newValue = this.getFieldValue(currentTarget, event.key); currentTarget.value = newValue > currentTarget.max ? currentTarget.max : newValue; currentTarget.currentChar += 1; if (currentTarget.currentChar > currentTarget.max.toString().length) { currentTarget.currentChar = 1; this.focusNext(currentTarget); } this.dispatchEvent(new CustomEvent('date-changed')); } } getFieldValue(currentTarget, key) { if (currentTarget.currentChar === 1) { return `0000${key}`.slice(-parseInt(currentTarget.max.toString().length, 10)); } return `${currentTarget.value.slice(1 - currentTarget.value.length)}${key}`; } selectAll(e) { e.preventDefault(); const { currentTarget } = e; setTimeout(() => currentTarget.select()); } focusout({ currentTarget }) { if (currentTarget.value === '00') { currentTarget.value = '01'; } currentTarget.currentChar = 1; } clear() { this.clearError(this.shadowRoot.querySelector('.day')); this.clearError(this.shadowRoot.querySelector('.month')); this.clearError(this.shadowRoot.querySelector('.year')); this._refreshDayMonthYear('00/00/0000'); setTimeout(() => { this._refreshDayMonthYear(this.date); }); } getDay(date) { if (date) { const day = getDate(parse(date, this.dateFormat, new Date())); return day ? `0${day}`.slice(-2) : ''; } return ''; } getMonth(date) { if (date) { const month = getMonth(parse(date, this.dateFormat, new Date())) + 1; return month ? `0${month}`.slice(-2) : ''; } return ''; } getYear(date) { if (date) { return getYear(parse(date, this.dateFormat, new Date())) || ''; } return ''; } focus() { super.focus(); } select() { const firstInput = this.shadowRoot.querySelector('.day'); setTimeout(() => firstInput.select()); } selectLast() { const lastInput = this.shadowRoot.querySelector('.year'); setTimeout(() => lastInput.select()); } getDate() { const day = this._validateInput(this.shadowRoot.querySelector('.day')); if (day) { const month = this._validateInput(this.shadowRoot.querySelector('.month')); if (month) { const year = this._validateInput(this.shadowRoot.querySelector('.year')); if (year) { const date = this._getClosestValidDate(day, month, year); return parseInt(format(date, 't'), 10); } } } return false; } _validateInput(input) { const { value } = input; let valid = true; if (!(value.length === input.max.length)) { valid = false; this.error = 'FORMAT_ERROR'; } if (valid && !(parseInt(value, 10) >= parseInt(input.min, 10))) { valid = false; this.error = 'MIN_ERROR'; } if (valid && !(parseInt(value, 10) <= parseInt(input.max, 10))) { valid = false; this.error = 'MAX_ERROR'; } if (!valid) { input.classList.add('error'); input.focus(); return false; } return value; } _getClosestValidDate(day, month, year) { const date = parse(`${day}/${month}/${year}`, 'dd/MM/yyyy', new Date()); if (Number.isNaN(date.getTime())) { return this._getClosestValidDate(parseInt(day, 10) - 1, month, year); } return date; } clearError(element) { if (element) { element.classList.remove('error'); } this.shadowRoot.querySelector('.date-input').classList.remove('error'); this.error = null; } setError(error) { this.error = error; this.shadowRoot.querySelector('.date-input').classList.add('error'); } } window.customElements.define('lit-datepicker-date-input', LitDatepickerDateInput);