busy-web/ember-date-time

View on GitHub
addon/components/ember-date-range-picker.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * @module Components
 *
 */
import $ from 'jquery';
import Component from '@ember/component';
import EmberObject, { set, get, computed } from '@ember/object';
import { next, later } from '@ember/runloop';
import { underscore, classify } from '@ember/string';
import { on } from '@ember/object/evented';
import { isEmpty, isNone } from '@ember/utils';
import { loc } from '@ember/string';
import { assert } from '@ember/debug';
import _state from '@busy-web/ember-date-time/utils/state';
import _time, { i18n } from '@busy-web/ember-date-time/utils/time';
import keyEvent from '@busy-web/ember-date-time/utils/key-event';
import { longFormatDate } from '@busy-web/ember-date-time/utils/format';
import layout from '../templates/components/ember-date-range-picker';
import {
    DAY_FLAG
} from '@busy-web/ember-date-time/utils/constant';

/**
 * `Component/Busyweb/EmberDateTimePicker`
 *
 * @class EmberDateTimePicker
 */
export default Component.extend({
    /**
     * @private
     * @property classNames
     * @type String
     */
    classNames: ['busyweb', 'emberdatetime', 'c-date-range-picker'],
    classNameBindings: ['large', 'right'],
    layout,

    large: false,
    right: false,

    /**
     * timestamp that is passed in as a milliseconds timestamp
     *
     * @private
     * @property timestamp
     * @type Number
     */
    startTime: null,
    endTime: null,

    /**
     * timestamp that is passed in as a seconds timestamp
     *
     * @public
     * @property unix
     * @type number
     */
    startUnix: null,
    endUnix: null,

    /**
     * can be passed in so a date after the maxDate cannot be selected
     *
     * @private
     * @property maxDate
     * @type Number
     * @optional
     */
    maxDate: null,

    /**
     * can be passed in so a date before the minDate cannot be selected
     *
     * @private
     * @property minDate
     * @type Number
     * @optional
     */
    minDate: null,

    /**
     * set to true if the values passed in should not be converted to local time
     *
     * @public
     * @property utc
     * @type boolean
     */
    utc: false,

    format: 'MMM DD, YYYY',

    _start: null,
    _end: null,
    _min: null,
    _max: null,

    hideTime: true,
    hideDate: false,

    defaultAction: '',

    stateManager: null,
    calendar: null,

    isStart: true,

    isListOpen: false,
    isCustom: false,
    allowCustom: true,
    activeDates: null,
    weekStart: 0,
    defaultDateOnChange: true,
    restrictAfterNow: false,

    inputDataName: computed('elementId', function() {
        return `range-picker-${get(this, 'elementId')}`;
    }),

    _startDate: computed('_start', function() {
        return _time(get(this, '_start'));
    }),

    _endDate: computed('_end', function() {
        return _time(get(this, '_end'));
    }),

    disableNext: computed('selected', '_start', '_max', function() {
        if (get(this, 'restrictAfterNow')) {
            const today = _time();
            const start = _time(getStart(this));
            const end = _time(getEnd(this));
            if (today.valueOf() >= start.valueOf() && today.valueOf() <= end.valueOf()) {
                return true;
            }
        }

        const { startTime } = this.getInterval(1);
        return get(this, '_max') < startTime;
    }),

    disablePrev: computed('selected', '_end', '_min', function() {
        const { end } = this.getInterval(-1);
        return get(this, '_min') > end;
    }),

    isInRange(date, start, end) {
        return (date.valueOf() >= start.valueOf() && date.valueOf() <= end.valueOf())
    },

    selectedDateRange: computed('selected', '_start', '_end', 'format', function() {
        const { id } = get(this, 'selected');
        const start = _time(getStart(this));
        const end = _time(getEnd(this));
        let isCurrent = false;
        if (this.isInRange(_time(), start, end)) {
            isCurrent = true;
        }

        if (id === 'monthly') {
            if (isCurrent) {
                return i18n('this_month');
            } else if (this.isInRange(_time().add(1, 'months'), start, end)) {
                return i18n('next_month');
            } else if (this.isInRange(_time().subtract(1, 'months'), start, end)) {
                return i18n('last_month');
            } else if (_time().year() !== start.year()) {
                return classify(start.format('MMMM')) + ' ' + start.year();
            }
            return classify(start.format('MMMM'));
        } else if (id === 'weekly') {
            if (isCurrent) {
                return i18n('this_week');
            } else if (this.isInRange(_time().add(1, 'weeks'), start, end)) {
                return i18n('next_week');
            } else if (this.isInRange(_time().subtract(1, 'weeks'), start, end)) {
                return i18n('last_week');
            } else if (_time().year() !== start.year() && start.year() !== end.year()) {
                return `${start.format('MMM D YYYY')} - ${end.format('MMM D YYYY')}`;
            } else if (_time().year() !== start.year()) {
                return `${start.format('MMM D')} - ${end.format('MMM D YYYY')}`;
            }
            return `${start.format('MMM D')} - ${end.format('MMM D')}`;
        } else if (id === 'daily') {
            if (isCurrent) {
                return i18n('this_day');
            } else if (this.isInRange(_time().add(1, 'days'), start, end)) {
                return i18n('next_day');
            } else if (this.isInRange(_time().subtract(1, 'days'), start, end)) {
                return i18n('last_day');
            } else if (_time().year() === start.year() && _time().week() === start.week()) {
                return start.format('dddd');
            } else if (_time().year() !== start.year()) {
                return `${start.format('MMM D')} - ${end.format('MMM D YYYY')}`;
            }
            return `${start.format('MMM D')}`;
        } else if (start.year() !== end.year()) {
            return `${start.format('ll')} - ${end.format('ll')}`;
        } else {
            return `${start.format('MMM D')} - ${end.format('MMM D')}`;
        }
    }),

    getAttr(key) {
        const attrs = get(this, 'attrs');
        if (!isNone(get(attrs, key))) {
            return get(this, key);
        }
        return null;
    },

    setup: on('init', function() {
        const isUnix = !isNone(get(this, 'startUnix')) || !isNone(get(this, 'endUnix'));
        set(this, '_isUnix', isUnix);

        // get locale converted format str
        let format = get(this, 'format');
        format = longFormatDate(format);

        assert(
            `Moment format "${get(this, 'format')}" is not supported. All format strings must be a combination of "DD" "MM" "YYYY" with one of the following delimeters [ -./, ] and no spaces`,
            /^(DD.MM.YYYY|DD.YYYY.MM|MM.DD.YYYY|MM.YYYY.DD|YYYY.MM.DD|YYYY.DD.MM)$/.test(format)
        );

        set(this, '__dayIndex', format.search(/D(D|o)/));
        set(this, '__monthIndex', format.search(/M(M|o)/));
        set(this, '__format', format);

        if (isNone(get(this, 'activeDates'))) {
            set(this, 'activeDates', []);
        }

        if (!get(this, 'changeFired') && (!isNone(this.getAttr('startTime')) || !isNone(this.getAttr('startUnix')))) {
            setStart(this, setUserStart(this, (this.getAttr('startTime') || this.getAttr('startUnix'))));
        } else if (isNone(getStart(this))) {
            setStart(this, _time().timestamp());
        }

        if (!get(this, 'changeFired') && (!isNone(this.getAttr('endTime')) || !isNone(this.getAttr('endUnix')))) {
            setEnd(this, setUserEnd(this, (this.getAttr('endTime') || this.getAttr('endUnix'))));
        } else if (isNone(getEnd(this))) {
            setEnd(this, _time().timestamp());
        }

        if (get(this, 'changeFired')) {
            set(this, 'changeFired', false);
        } else {
            this.setState();
        }

        if (!isNone(this.getAttr('minDate'))) {
            let min = this.getAttr('minDate');
            if (isUnix) {
                min = _time.unix(min).timestamp();
            }
            min = _time(min).startOf('day').timestamp();

            if (get(this, '_min') !== min) {
                set(this, '_min', min);
            }
        }

        if (!isNone(get(this, 'maxDate'))) {
            let max = get(this, 'maxDate');
            if (isUnix) {
                max = _time.unix(max).timestamp();
            }
            max = _time(max).endOf('day').valueOf();

            if (get(this, '_max') !== max) {
                set(this, '_max', max);
            }
        }

        let actionList = get(this, '__actionList') || [];
        if (isEmpty(actionList)) {
            let tList = [];
            let sortKey = 400;
            (this.getAttr('actionList') || []).forEach(item => {
                if (!item.get && !item.set) {
                    item = EmberObject.create(item);
                }

                let name = get(item, 'name');

                assert("Action list items must contain a `name` property", !isEmpty(name));

                if (isNone(item, 'id')) {
                    set(item, 'id', underscore(name));
                }

                if (isNone(get(item, 'sort'))) {
                    set(item, 'sort', sortKey);
                    sortKey += 1;
                }

                set(item, 'selected', false);
                tList.push(item);
            });

            actionList = tList;

            // action list is the list used in the select menu.
            //
            // id {string} - string id passed around for reference to a list item
            // name {string} - the label to display in the list
            // span {number|function} - the time span in time relational to {type} if function is provided it will be passed the current timestamp
            // type {string} - the units used to calculate the time {span}
            // sort {number} - a weighted number used to sort the list
            // selected {boolean} a true if the item is currently the selected item
            actionList.push(EmberObject.create({id: 'daily', name: loc('Daily'), span: 1, type: 'days', sort: 100, selected: false}));
            actionList.push(EmberObject.create({id: 'weekly', name: loc('Weekly'), span: 1, type: 'weeks', sort: 200, selected: false}));
            actionList.push(EmberObject.create({id: 'monthly', name: loc('Monthly'), span: 1, type: 'months', sort: 300, selected: false}));

            actionList = actionList.sort((a, b) => get(a, 'sort') > get(b, 'sort') ? 1 : -1);
            set(this, '__actionList', actionList);
        }


        if (isNone(get(this, 'selected'))) {
            this.setSelected();
            this.saveState();
        }

        let elementId = get(this, 'elementId');
        $('body').off(`keydown.${elementId}`);
        $('body').on(`keydown.${elementId}`, keyDownEventHandler(this));

        $('body').off(`click.${elementId}`);
        $('body').on(`click.${elementId}`, clickEventHandler(this));
    }),

    destroy: on('willDestroyElement', function() {
        $('body').off(`keydown.${get(this, 'elementId')}`);
        $('body').off(`click.${get(this, 'elementId')}`);
    }),

    getDefaultAction() {
        if (get(this, 'isCustom')) {
            const span = _time.daysApart(getStart(this), getEnd(this)) + 1;
            return EmberObject.create({name: loc('Custom'), span, type: 'days'});
        } else if (!isEmpty(get(this, 'defaultAction'))) {
            return get(this, '__actionList').findBy('id', get(this, 'defaultAction'));
        } else {
            const start = getStart(this);
            const end = getEnd(this);
            const startDate = _time(start);
            const endDate = _time(end);
            let span = Math.abs(startDate.diff(endDate, 'days'));
            let diff = Number.MAX_VALUE;

            let selected;
            get(this, '__actionList').forEach(item => {
                if (typeof item.span !== 'function') {
                    const timeSpan = _time(start).add(item.span, item.type);
                    const itemSpan = Math.abs(startDate.diff(timeSpan, 'days'));
                    const nDiff = Math.abs(itemSpan - span);
                    if (diff > nDiff) {
                        selected = item;
                        diff = nDiff;
                    }
                }
            });
            return selected;
        }
    },


    setActiveState(isStart) {
        if (get(this, 'allowCustom')) {
            set(this, 'isStart', isStart);
        } else {
            set(this, 'isStart', true);
        }
    },

    getInterval(direction=0) {
        let { span, type } = get(this, 'selected');
        let start, end;
        if (!isEmpty(type) && !isNone(span)) {
            start = _time(getStart(this)).valueOf();
            end = _time(getEnd(this)).valueOf();

            let currentSpanInSeconds = _time(end).unix() - _time(start).unix();
            let selectedSpanInSeconds = _time(start).add(span, type).unix() - _time(start).unix();

            if (typeof span === 'function') {
                start = getUserStart(this);
                end = getUserEnd(this);

                // get range defined by span function
                let range = span.call(get(this, 'selected'), start, end, direction);
                start = setUserStart(this, range.start);
                end = setUserEnd(this, range.end);
            } else {
                // if the current time span is greater than selected time span
                // then set the new start to the end of the current period.
                // this will keep the date more current.
                if (currentSpanInSeconds > selectedSpanInSeconds) {
                    start = _time(getEnd(this)).startOf('day').valueOf();
                    if (!isNone(get(this, '_max')) && start > get(this, '_max')) {
                        start = _time(get(this, '_max')).subtract(span, type).startOf('day').valueOf();
                    }
                }

                if (type === 'weeks') {
                    if (_time(start).day() !== get(this, 'weekStart')) {
                        let __start = _time(start).day(get(this, 'weekStart')).valueOf();
                        if (start < __start) {
                            __start = _time(__start).subtract(7, 'days').valueOf();
                        }
                        start = __start;
                    }
                } else if (type === 'months') {
                    // for months use startof month for proper month alignment
                    start = _time(start).startOf('month').valueOf();
                }

                if (direction === -1) {
                    start = _time(start).subtract(span, type).valueOf();
                } else if (direction === 1) {
                    start = _time(start).add(span, type).valueOf();
                } else {
                    start = _time(start).startOf('day').valueOf();
                }

                start = start.valueOf();
                end = _time(start).add(span, type).subtract(1, 'days').endOf('day').valueOf();
            }
        }
        return { start, end };
    },

    changeInterval(direction=0) {
        let intervalWait = get(this, '__intervalWait');
        if (!isNone(intervalWait)) {
            clearTimeout(intervalWait);
            intervalWait = null;
        }

        const { start, end } = this.getInterval(direction);
        if (!isNone(start) && !isNone(end)) {
            setStart(this, start);
            setEnd(this, end);
            intervalWait = setTimeout(() => {
                this.triggerDateChange();
            }, 500);
        }
        set(this, '__intervalWait', intervalWait);
    },

    /**
     * sets the state object for
     * the date-picker component to get date information
     *
     * @public
     * @method setState
     */
    setState() {
        let start = getStart(this);
        let end = getEnd(this);

        let timestamp = start;
        let calendarDate = get(this, 'calendarDate');
        let minDate = get(this, '_min');
        let maxDate = get(this, '_max');
        let format = get(this, 'format');

        if (!get(this, 'isStart')) {
            timestamp = end;
        }

        const startRange = _time(start).startOf('day').valueOf();
        const endRange = _time(end).startOf('day').valueOf();

        if (isNone(get(this, 'stateManager'))) {
            set(this, 'stateManager', _state({
                isOpen: true, // isOpen should always be true
                timestamp, calendarDate,
                minDate, maxDate,
                format,
                range: [startRange, endRange]
            }));
        } else {
            set(this, 'stateManager.timestamp', timestamp);
            set(this, 'stateManager.calendarDate', calendarDate);
            set(this, 'stateManager.minDate', minDate);
            set(this, 'stateManager.maxDate', maxDate);
            set(this, 'stateManager.format', format);
            set(this, 'stateManager.range', [startRange, endRange]);
        }
    },

    /**
     * triggeres a date change event to send off
     * to listeners of `onChange`
     *
     * @public
     * @method triggerDateChange
     */
    triggerDateChange() {
        let start = getUserStart(this);
        let end = getUserEnd(this);

        this.setState();

        set(this, 'changeFired', true);

        let selectedType = get(this, 'isCustom') ? 'custom' : get((get(this, '__actionList').findBy('selected', true) || {id: 'weekly'}), 'id');
        this.sendAction('onChange', start, end, get(this, 'isCustom'), selectedType);
    },

    /**
     * sets the focus to on of the inputs based on the boolean passed in.
     *
     * true sets focus to the start time input
     *
     * @public
     * @method focusActive
     * @params isStart {boolean}
     */
    focusActive(selection=0) {
        let isStart = get(this, 'isStart');
        set(this, '__saveFocus', { isStart, selection });

        let input = (isStart) ? 'start' : 'end';
        let el = this.$(`.state.${input} > input`);
        if (el && el.data) {
            el.data('selection', selection);
            el.data('position', 0);

            next(() => el.focus());
        }
    },

    /**
     * Update the start or end time date where the date will also set the other
     * if it is incalid
     *
     * @public
     * @method updateDates
     * @params type {string} the type of setter day or month
     * @params timestamp {number} milliseconds timestamp
     * @params calendar {number} milliseconds timestamp
     * @params singleSet {boolean} flag to only set start or end time unless a special case exists. This is for keyboards inputs
     */
    updateDates(type, time, calendar, singleSet=false) {
        let isStart = get(this, 'isStart');
        if (!get(this, 'allowCustom')) {
            isStart = true;
        }

        if (type === DAY_FLAG) {
            if (!singleSet && !isStart && time < getStart(this)) {
                isStart = true;
            }

            // set the start or end time
            // based off the current active state
            if (isStart) {
                setStart(this, time);
            } else {
                setEnd(this, time);
            }

            if (!singleSet && isStart) {
                // if active state is the start and its
                // not a singleSet mode then set the end as well
                setEnd(this, time);
            } else if (isStart && time > getEnd(this)) {
                // if active state is start and the time
                // is greater than the end then set the end time
                setEnd(this, time);
            } else if (!isStart && time < getStart(this)) {
                // if active state is end and the time
                // is less than the start then set the start time
                setStart(this, time);
            }

            // always set the calendar for start times and all
            // single set times
            if (isStart || singleSet) {
                set(this, 'calendarDate', time);
            }

            // set the active state
            this.setActiveState(get(this, 'isStart'));

            // update the dates for the calendar
            this.setState();
        } else {
            set(this, 'calendarDate', calendar);
        }
        return isStart;
    },

    /**
     * set the select menu to the selected type by id
     *
     * @public
     * @method setSelected
     * @params id {string} the id of the menu type to set as the selected menu item
     * @return {object} the selected item
     */
    setSelected(id) {
        // reset selected list
        get(this, '__actionList').forEach(item => set(item, 'selected', false));

        let selected;
        if (isNone(id)) {
            selected = this.getDefaultAction();
        } else if (id === 'custom') {
            set(this, 'isCustom', true);
            const span = _time.daysApart(getStart(this), getEnd(this)) + 1;
            selected = EmberObject.create({id: 'custom', name: loc('Custom'), span, type: 'days'});
        } else {
            set(this, 'isCustom', false);
            selected = get(this, '__actionList').findBy('id', id);
            if (get(this, 'defaultDateOnChange')) {
                if (get(this, 'selected.id') !== get(selected, 'id')) {
                    setStart(this, _time().startOf('day').valueOf());
                    setEnd(this, _time().endOf('day').valueOf());
                }
            }
        }

        set(selected, 'selected', true);
        set(this, 'selected', selected);
        return selected;
    },

    /**
     * Save the current state of the select menu and
     * start end dates
     *
     * @public
     * @method saveState
     */
    saveState() {
        let id = get(this, 'selected.id');
        set(this, '__saveState', {
            isCustom: id === 'custom',
            selectedId: id,
            start: getStart(this),
            end: getEnd(this)
        });
    },

    /**
     * restore the state to the previous state of the select menu
     * and start end dates
     *
     * @public
     * @method restoreState
     */
    restoreState() {
        if (!isNone(get(this, '__saveState'))) {
            setStart(this, get(this, '__saveState.start'));
            setEnd(this, get(this, '__saveState.end'));
            this.setSelected(get(this, '__saveState.selectedId'));
        }
    },

    closeMenu() {
        let focus = this.$('.date-range-picker-dropdown > .focus');
        if (focus.length) {
            focus.removeClass('focus');
        }
        set(this, 'isListOpen', false);
    },

    openMenu() {
        set(this, 'isListOpen', true);
        this.focusActive();
    },

    click(event) {
        if (get(this, 'isListOpen')) {
            let el = $(event.target);
            if (el.parents('.date-range-picker-menu').length || el.hasClass('date-range-picker-menu')) {
                let focus = get(this, '__saveFocus');
                this.setActiveState(focus.isStart);
                this.focusActive(focus.selected);
            }
        }
    },

    actions: {
        intervalBack() {
            if (!get(this, 'disablePrev')) {
                this.changeInterval(-1);
            }
        },

        intervalForward() {
            if (!get(this, 'disableNext')) {
                this.changeInterval(1);
            }
        },

        toggleList() {
            if (!get(this, 'isListOpen')) {
                this.openMenu();
            } else {
                this.closeMenu();
            }
        },

        setFocus(val, event) {
            let index = event.target.selectionStart;
            if (val === 'start') {
                this.setActiveState(true);
                set(this, '__saveFocus', { isStart: true, selected: index });
            } else if (val === 'end') {
                this.setActiveState(false);
                set(this, '__saveFocus', { isStart: false, selected: index });
            }
        },

        selectItem(id) {
            if (!get(this, 'selected') !== id) {
                this.saveState();
                this.setSelected(id);
            }
            this.closeMenu();
            this.changeInterval();
        },

        selectCustom() {
            this.saveState();
            this.setSelected('custom');
            later(() => {
                this.focusActive();
            }, 100);
        },

        applyRange() {
            this.saveState();
            if (get(this, 'allowCustom')) {
                this.setSelected('custom');
            }
            this.closeMenu();
            this.setActiveState(true);
            this.changeInterval();
        },

        cancelRange() {
            this.restoreState();
            this.closeMenu();
            this.setActiveState(true);
        },

        dateChange(time) {
            this.updateDates(DAY_FLAG, time, null, true);
        },

        updateTime(state, time) {
            let isStart = this.updateDates(DAY_FLAG, time);
            this.setActiveState(!isStart);

            // resets the focus after the user clicks a day
            this.focusActive(get(this, '__dayIndex'));
        },

        tabAction() {
            this.setActiveState(!get(this, 'isStart'));
            // change focus to next input
            this.focusActive();
        },

        /**
         * update the clicked value for days and months
         * and set the focus back to the input when done
         *
         */
        calendarChange(type, time, calendar) {
            let isStart;
            if (type === DAY_FLAG) {
                isStart = this.updateDates(type, time, calendar);
                this.setActiveState(!isStart);
                this.focusActive(get(this, '__dayIndex'));
            } else {
                this.updateDates(type, time, calendar);
                this.focusActive(get(this, '__monthIndex'));
            }
        }
    }
});

function getUserStart(target) {
    let time = getStart(target);
    if (get(target, 'utc')) {
        time = _time.utcFromLocal(time).timestamp();
    }

    if (get(target, '_isUnix')) {
        time = _time(time).unix();
    }
    return time;
}

function getUserEnd(target) {
    let time = getEnd(target);
    if (get(target, 'utc')) {
        time = _time.utcFromLocal(time).timestamp();
    }

    if (get(target, '_isUnix')) {
        time = _time(time).unix();
    }
    return time;
}

function setUserStart(target, time) {
    if (get(target, '_isUnix')) {
        time = _time.unix(time).timestamp();
    }

    if (get(target, 'utc')) {
        time = _time.utcToLocal(time).timestamp();
    }
    return time;
}

function setUserEnd(target, time) {
    if (get(target, '_isUnix')) {
        time = _time.unix(time).timestamp();
    }

    if (get(target, 'utc')) {
        time = _time.utcToLocal(time).timestamp();
    }
    return time;
}

function getStart(target) {
    return get(target, '_start');
}

function getEnd(target) {
    return get(target, '_end');
}

function setStart(target, time) {
    time = _time(time).startOf('day').valueOf();
    if (getStart(target) !== time) {
        set(target, '_start', time);
    }
}

function setEnd(target, time) {
    time = _time(time).endOf('day').valueOf();
    if (getEnd(target) !== time) {
        set(target, '_end', time);
    }
}

function findAction(target, key) {
    let actions = get(target, '__actionList').map(i => {
        let id = i.id;
        let regx = new RegExp('^' + id.charAt(0).toLowerCase() + '$');
        return { id, regx };
    });
    actions.push({ id: 'custom', regx: /^C$/});
    let res = actions.find(i => i.regx.test(key));
    if (isNone(res)) {
        res = { id: null, regx: null };
    }
    return res;
}

function handleAltKeys(target, keyName, isOpen) {
    if (keyName === '/') {
        target.send('toggleList');
    } else {
        let selectedId = get(target, 'selected.id');
        let { id } = findAction(target, keyName);
        if (!isNone(id)) {
            if (selectedId !== id) {
                if (id === 'custom') {
                    if (!isOpen) {
                        target.send('selectCustom');
                    }
                    target.send('toggleList');
                } else {
                    target.send('selectItem', id);
                }
            }
        }
    }
}

function keyDownEventHandler(target) {
    return function(event) {
        let isOpen = get(target, 'isListOpen');
        let handler = keyEvent({ event });
        if (event.altKey) {
            if (handler.type === 'letter' || handler.type === 'math') {
                handleAltKeys(target, handler.keyName, isOpen);
            } else if (handler.keyName === 'ArrowLeft') {
                if (!isOpen) {
                    target.send('intervalBack');
                }
            } else if (handler.keyName === 'ArrowRight') {
                if (!isOpen) {
                    target.send('intervalForward');
                }
            }
        } else {
            if (handler.keyName === 'Tab') {
                if (isOpen) {
                    target.send('tabAction', event);
                }
            } else if (handler.keyName === 'Escape') {
                if (isOpen) {
                    target.send('toggleList');
                }
            } else if (handler.keyName === 'ArrowDown') {
                if (isOpen) {
                    nextListItem(target);
                }
            } else if (handler.keyName === 'ArrowUp') {
                if (isOpen) {
                    prevListItem(target);
                }
            } else if (handler.keyName === 'Enter') {
                if (isOpen) {
                    let selected = target.$('.date-range-picker-dropdown > .focus');
                    if (selected.length) {
                        selected.click();
                    }
                }
            }
        }
        return true;
    }
}

function nextListItem(target) {
    let element = target.$('.date-range-picker-dropdown');
    let active = element.find('.focus');
    if (!active.length) {
        active = element.find('.active');
    }

    let next = active.next('.item');
    if (!next.length) {
        next = element.children().first();
    }
    next.addClass('focus');
    active.removeClass('focus');
}

function prevListItem(target) {
    let element = target.$('.date-range-picker-dropdown');
    let active = element.find('.focus');
    if (!active.length) {
        active = element.find('.active');
    }

    let next = active.prev('.item');
    if (!next.length) {
        next = element.children('.item').last();
    }
    next.addClass('focus');
    active.removeClass('focus');
}


function clickEventHandler(target) {
    return function(evt) {
        let el = $(evt.target);
        let isOpen = get(target, 'isListOpen');
        if (isOpen) {
            if (el.parents('.c-date-range-picker').length) { // is in date picker
                if (!el.parents('.date-range-picker-dropdown').length && !el.hasClass('select') && !el.hasClass('date-range-picker-dropdown')) {
                    target.send('toggleList');
                }
            } else {
                target.send('toggleList');
            }
        }
    }
}