UNPKG

@busy-web/ember-date-time

Version:

Date time & and range picker for ember addon

505 lines (435 loc) 12.9 kB
/** * @module Components * */ import Component from '@ember/component'; import { A } from '@ember/array'; import { camelize } from '@ember/string'; import { isNone } from '@ember/utils'; import { assert } from '@ember/debug'; import { computed, observer, get, set } from '@ember/object'; import _state from '@busy-web/ember-date-time/utils/state'; import _time from '@busy-web/ember-date-time/utils/time'; import layout from '../../templates/components/private/date-picker'; import { YEAR_FLAG, MONTH_FLAG, WEEKDAY_FLAG, DAY_FLAG } from '@busy-web/ember-date-time/utils/constant'; /** * `Component/DatePicker` * * @class DatePicker * @namespace Components * @extends Component */ export default Component.extend({ classNames: ['busyweb', 'emberdatetime', 'p-date-picker'], layout: layout, dayKey: DAY_FLAG, monthKey: MONTH_FLAG, yearKey: YEAR_FLAG, stateManager: null, /** * timestamp that is passed in when using date picker * * @private * @property timestamp * @type Number */ timestamp: null, /** * timestamp that controls the dates for the calendar * * @private * @property calendarDate * @type Number */ calendarDate: null, /** * day of the month shown on the calendar header - based off timestamp * * @private * @property day * @type String */ day: null, /** * month of year shown on the calendar header - based off timestamp * * @private * @property month * @type String */ month: null, /** * year shown on the calendar header - based off timestamp * * @private * @property year * @type String */ year: null, /** * array of all days in the current month of calendarTimestamp * * @private * @property daysArray * @type Array */ daysArray: null, /** * based on daysArray, Adds blank days to the front and back of array according to starting day of month * * @private * @property completeDaysArray * @type Array */ completeDaysArray: null, /** * based on completeDaysArray, Adds current day and minDate - maxDate properties * * @private * @property completeArray * @type Array */ completeArray: null, /** * based on completeArray, groups days into 6 week arrays * * @private * @property groupedArray * @type Array */ groupedArray: null, /** * becomes string 'active' (binded to classes in template) if monthActive is active * * @private * @property monthActive * @type String */ months_active: false, /** * becomes string 'active' (binded to classes in template) if dayActive is active * * @private * @property monthActive * @type dayActive */ days_active: false, /** * becomes string 'active' (binded to classes in template) if yearActive is active * * @private * @property yearActive * @type String */ years_active: false, isPrev: true, isNext: true, /** * @private * @method init * @constructor */ init(...args) { this._super(...args); this.updateTime(); this.updateActiveSection(); }, updateTime: observer('stateManager.timestamp', 'stateManager.calendarDate', function() { const timestamp = get(this, 'stateManager.timestamp'); let calendarDate = get(this, 'stateManager.calendarDate'); if (!calendarDate) { calendarDate = timestamp; } if (get(this, 'timestamp') !== timestamp) { this.setTimestamp(get(this, 'stateManager.timestamp')); } if (get(this, 'calendarDate') !== calendarDate) { this.setCalendarDate(get(this, 'stateManager.calendarDate') || get(this, 'stateManager.timestamp')); } }), /** * updates to the new active header (day, month, or year) * * @private * @method updateActiveSection */ updateActiveSection: observer('stateManager.section', function() { let section = get(this, 'stateManager.section'); if (!isNone(section)) { if (section === WEEKDAY_FLAG) { section === DAY_FLAG; } section = camelize(section); const statusType = [DAY_FLAG, MONTH_FLAG, YEAR_FLAG]; // ensure the active status applies to the calendar if (statusType.indexOf(section) !== -1) { // reset active status statusType.forEach(name => set(this, `${name}_active`, false)); // set new active status set(this, `${section}_active`, true); } } }), /** * re configures the calendar when calendarDate is changed, sets the monthYear calendar header * * @private * @method monthYear */ monthYear: computed('calendarDate', 'stateManager.range', function() { const calendarObject = _time(get(this, 'calendarDate')); this.buildDaysArrayForMonth(); return calendarObject.format('MMMM YYYY'); }), /** * makes moment objects for each day in month, disables them if they exceed max/min date * * @private * @method buildDaysArrayForMonth */ buildDaysArrayForMonth: function() { const calendarDate = get(this, 'calendarDate'); const minDate = get(this, 'stateManager.minDate'); const maxDate = get(this, 'stateManager.maxDate'); let [ startRange, endRange ] = (get(this, 'stateManager.range') || []); const currentCalendar = _time(calendarDate); const currentTime = _time(get(this, 'timestamp')); const firstDay = _time(calendarDate).startOf('month'); const lastDay = _time(calendarDate).endOf('month').date(); let start = firstDay.day(); let currentDay = firstDay; if (start === 0) { currentDay.subtract(7, 'days'); start = 7; } else { currentDay.subtract(start, 'days'); } let daysInCalendar = 28; if ((start + lastDay) >= 35) { daysInCalendar = 42; } else if ((start + lastDay) > 28) { daysInCalendar = 35; } const daysArray = A(); for (let i=0; i<daysInCalendar; i++) { const dayState = _state({timestamp: currentDay.valueOf(), minDate, maxDate, type: 'date'}); dayState.set('weekNumber', Math.ceil((i+1)/7)); if (dayState.get('date').year() === currentCalendar.year()) { dayState.set('isCurrentYear', true); if (dayState.get('date').month() === currentCalendar.month()) { dayState.set('isCurrentMonth', true); } } if (dayState.get('date').year() === currentTime.year() && dayState.get('date').month() === currentTime.month() && dayState.get('date').date() === currentTime.date()) { dayState.set('isCurrentDay', true); } if (startRange && endRange) { let mili = dayState.get('date').valueOf(); if (startRange <= mili && mili <= endRange) { dayState.set('isRange', true); if (mili === startRange) { dayState.set('isRangeStart', true); } if (mili === endRange) { dayState.set('isRangeEnd', true); } } } daysArray.pushObject(dayState); currentDay.add(1, 'days'); } this.groupByWeeks(daysArray); }, /** * groups days into week objects * * @private * @method groupByWeeks */ groupByWeeks(completeArray) { let grouped = A([]); grouped.pushObject(completeArray.filterBy('weekNumber', 1)); grouped.pushObject(completeArray.filterBy('weekNumber', 2)); grouped.pushObject(completeArray.filterBy('weekNumber', 3)); grouped.pushObject(completeArray.filterBy('weekNumber', 4)); grouped.pushObject(completeArray.filterBy('weekNumber', 5)); grouped.pushObject(completeArray.filterBy('weekNumber', 6)); set(this, 'groupedArray', grouped); }, /** * puts days into week objects * * @private * @method inRange * @param lower {number} first number in week * @param upper {number} last number in week * @return {boolean} true if day is in week, otherwise false */ inRange(lower, upper) { return function (each, index) { return (index >= lower && index < upper); }; }, /** * receives a moment object and sets it to timestamp * * @private * @method setTimestamp * @param timestamp {number} date time in milliseconds */ setTimestamp(timestamp) { set(this, 'timestamp', timestamp); const time = _time(timestamp); set(this, 'year', time.format('YYYY')); set(this, 'month', time.format('MMM')); set(this, 'day', time.format('DD')); set(this, 'dayOfWeek', time.format('ddd')); }, /** * receives a moment object and sets it to calendarTimestamp * * @private * @method setCalendarDate * @param timestamp {number} timestamp in milliseconds */ setCalendarDate(timestamp) { this.validateNextPrev(timestamp); set(this, 'calendarDate', timestamp); }, /** * validates the next and prev arrows can move in their * respective directions * * @method validateNextPrev * @param time {number} timestamp in milliseconds * @return {void} */ validateNextPrev(time) { // validates the timestamp is inbounds for the // given `name: string` (isPrev | isNext) and `time: number` const validateTime = (name, npTime) => { // sets the prop according to `name: string` and `inBounds: boolean` const update = inBounds => set(this, name, inBounds); // call update to set the property update( name === 'isPrev' ? !this.isBeforeMin(npTime) : !this.isAfterMax(npTime) ); }; // validate that isPrev and isNext will be inBounds // if the next or prev arrows are clicked validateTime('isPrev', getPrevDate(time)); validateTime('isNext', getNextDate(time)); }, isBeforeMin(time) { // if the timestamp gets set to a date before the // minDate then allow the back arrow to go back as // far as the current timestamp if (_time.isDateBefore(get(this, 'timestamp'), time)) { return false; } return _time.isDateBefore(time, get(this, 'stateManager.minDate')); }, isAfterMax(time) { // if the timestamp gets set to a date after the // maxDate then allow the forward arrow to go forward as // far as the current timestamp if (_time.isDateAfter(get(this, 'timestamp'), time)) { return false; } return _time.isDateAfter(time, get(this, 'stateManager.maxDate')); }, triggerUpdate(flag) { assert( `flag is required and must be on of [${YEAR_FLAG}, ${MONTH_FLAG}, ${WEEKDAY_FLAG}, ${DAY_FLAG}]`, [YEAR_FLAG, MONTH_FLAG, WEEKDAY_FLAG, DAY_FLAG].indexOf(flag) !== -1 ); set(this, 'calendarActiveSection', flag); this.sendAction('onUpdate', flag, get(this, 'timestamp'), get(this, 'calendarDate')); }, actions: { /** * sets the timestamp to the clicked day * * @param day {object} moment object of the clicked day * @event dayClicked */ dayClicked(dayState) { if (!get(dayState, 'isDisabled')) { const day = get(dayState, 'date'); const time = _time(get(this, 'timestamp')) .year(day.year()) .month(day.month()) .date(day.date()); this.setTimestamp(time.timestamp()); this.setCalendarDate(time.timestamp()); this.triggerUpdate(DAY_FLAG); } }, /** * subtracts 1 month to the calendarDate * * @event subtractMonth */ subtractMonth() { // get next date const calTime = getPrevDate(get(this, 'calendarDate')); if (!this.isBeforeMin(calTime)) { // set calendar date this.setCalendarDate(calTime); // trigger update this.triggerUpdate(MONTH_FLAG); } }, /** * adds 1 month to the calendarDate * * @event addMonth */ addMonth() { // get next date const calTime = getNextDate(get(this, 'calendarDate')); if (!this.isAfterMax(calTime)) { // set calendar date this.setCalendarDate(calTime); // trigger update this.triggerUpdate(MONTH_FLAG); } }, /** * Sets the new header selection * * @event activateHeader */ activateHeader(section) { // trigger update for input to reposition selection this.triggerUpdate(section); } } }); /** * Creates a timestamp for the first day of the next months * * @method getNextDate * @param time {number} timestamp in milliseconds * @return {number} timestamp in milliseconds */ const getNextDate = time => ( _time(time).add(1, 'months').startOf('month').timestamp() ); /** * Creates a timestamp for the last day of the previous months * * @method getPrevDate * @param time {number} timestamp in milliseconds * @return {number} timestamp in milliseconds */ const getPrevDate = time => ( _time(time).subtract(1, 'month').endOf('month').timestamp() );