Source: components/sl-calendar.js

import Ember from 'ember';
import layout from '../templates/components/sl-calendar';

/**
 * @module
 * @augments ember/Component
 */
export default Ember.Component.extend({

    // -------------------------------------------------------------------------
    // Dependencies

    // -------------------------------------------------------------------------
    // Attributes

    /** @type {String[]} */
    classNameBindings: [
        'locked:sl-calendar-locked'
    ],

    /** @type {String[]} */
    classNames: [
        'sl-calendar'
    ],

    /** @type {Object} */
    layout,

    // -------------------------------------------------------------------------
    // Actions

    /** @type {Object} */
    actions: {

        /**
         * Change the currently-viewed decade by incrementing or decrementing
         * the decadeStart year number
         *
         * @function actions:changeDecade
         * @param {Number} decadeMod - A number to adjust the decadeStart by
         *        (positive to increment, negative to decrement)
         * @returns {undefined}
         */
        changeDecade( decadeMod ) {
            if ( this.get( 'locked' ) ) {
                return;
            }

            this.incrementProperty( 'decadeStart', 10 * decadeMod );
        },

        /**
         * Change the currently-viewed month by incrementing or decrementing
         * the currentMonth (and currentYear if needed)
         *
         * @function actions:changeMonth
         * @param {Number} monthMod - A number to adjust the currentMonth by
         *        (positive to increment, negative to decrement). The
         *        currentYear is adjusted as needed.
         * @returns {undefined}
         */
        changeMonth( monthMod ) {
            let month;
            let year;

            if ( this.get( 'locked' ) ) {
                return;
            }

            month = this.get( 'currentMonth' ) + monthMod;
            year = this.get( 'currentYear' );

            while ( month < 1 ) {
                month += 12;
                year -= 1;
            }

            while ( month > 12 ) {
                month -= 12;
                year += 1;
            }

            this.setProperties({
                currentYear: year,
                currentMonth: month
            });
        },

        /**
         * Change the currently-viewed year by increment or decrementing the
         * currentYear
         *
         * @function actions:changeYear
         * @param {Number} yearMod - A number to adjust the currentYear by
         *        (positive to increment, negative to decrement)
         * @returns {undefined}
         */
        changeYear( yearMod ) {
            if ( this.get( 'locked' ) ) {
                return;
            }

            this.incrementProperty( 'currentYear', yearMod );
        },

        /**
         * Action to trigger component's bound action and pass back content
         * values with dates occurring on the clicked date
         *
         * @function actions:sendDateContent
         * @param {Array} dateContent - Collection of content objects with
         *        date values of the clicked date
         * @returns {undefined}
         */
        sendDateContent( dateContent ) {
            if ( dateContent ) {
                this.sendAction( 'action', dateContent );
            }
        },

        /**
         * Set the current month and change view mode to that month
         *
         * @function actions:setMonth
         * @param {Number} month - The number of the month to change view to
         * @returns {undefined}
         */
        setMonth( month ) {
            if ( this.get( 'locked' ) ) {
                return;
            }

            this.setProperties({
                currentMonth: month,
                viewMode: 'days'
            });
        },

        /**
         * Set the view mode of the calendar
         *
         * @function actions:setView
         * @param {String} view - The view mode to switch to; "days", "months",
         *        or "years"
         * @returns {undefined}
         */
        setView( view ) {
            if ( this.get( 'locked' ) ) {
                return;
            }

            this.set( 'viewMode', view );
        },

        /**
         * Set the current year
         *
         * @function actions:setYear
         * @param {Number} year - The year to set to the current value
         * @returns {undefined}
         */
        setYear( year ) {
            if ( this.get( 'locked' ) ) {
                return;
            }

            this.setProperties({
                viewMode: 'months',
                currentYear: year
            });
        }
    },

    // -------------------------------------------------------------------------
    // Events

    // -------------------------------------------------------------------------
    // Properties

    /**
     * Array of date value objects
     *
     * @type {Object[]}
     */
    content: [],

    /**
     * The currently selected/viewed month (1-12)
     *
     * @type {?Number}
     */
    currentMonth: null,

    /**
     * The currently selected/viewed year
     *
     * @type {?Number}
     */
    currentYear: null,

    /**
     * String lookup for the date value on the content objects
     *
     * @type {String}
     */
    dateValuePath: 'date',

    /**
     * The locale string to use for moment date values
     *
     * @type {String}
     */
    locale: 'en',

    /**
     * When true, the view mode is locked and users cannot navigate forward
     * and back
     *
     * @type {Boolean}
     */
    locked: false,

    /**
     * The current view mode for the calendar
     *
     * @type {String}
     */
    viewMode: 'days',

    // -------------------------------------------------------------------------
    // Observers

    /**
     * Initialize default property values
     *
     * @function
     * @returns {undefined}
     */
    initialize: Ember.on(
        'init',
        function() {
            const today = new Date();

            if ( !this.get( 'currentMonth' ) ) {
                this.set( 'currentMonth', today.getMonth() + 1 );
            }

            if ( !this.get( 'currentYear' ) ) {
                this.set( 'currentYear', today.getFullYear() );
            }
        }
    ),

    // -------------------------------------------------------------------------
    // Methods

    /**
     * Object of nested year, month, and day values, representing the dates
     * supplied by the calendar's content values
     *
     * @function
     * @returns {Object}
     */
    contentDates: Ember.computed(
        'content',
        'dateValuePath',
        function() {
            const content = this.get( 'content' );
            const dates = {};
            const dateValuePath = this.get( 'dateValuePath' );

            if ( content ) {
                content.forEach( ( item ) => {
                    const date = new Date( Ember.get( item, dateValuePath ) );
                    const year = date.getFullYear();
                    const month = date.getMonth() + 1;
                    const day = date.getDate();

                    if ( !dates.hasOwnProperty( year ) ) {
                        dates[ year ] = {};
                    }

                    if ( !dates[ year ].hasOwnProperty( month ) ) {
                        dates[ year ][ month ] = {};
                    }

                    if ( !dates[ year ][ month ].hasOwnProperty( day ) ) {
                        dates[ year ][ month ][ day ] = [];
                    }

                    dates[ year ][ month ][ day ].push( item );
                });
            }

            return dates;
        }
    ),

    /**
     * Name of the currently selected/viewed month
     *
     * @function
     * @returns {String}
     */
    currentMonthString: Ember.computed(
        'currentMonth',
        'currentYear',
        'locale',
        function() {
            return window.moment([
                this.get( 'currentYear' ),
                this.get( 'currentMonth' ) - 1
            ]).locale( this.get( 'locale' ) ).format( 'MMMM' );
        }
    ),

    /**
     * The number of days in the current month
     *
     * @function
     * @returns {Number}
     */
    daysInMonth: Ember.computed(
        'currentMonth',
        'currentYear',
        function() {
            return window.moment([
                this.get( 'currentYear' ),
                this.get( 'currentMonth' ) - 1
            ]).daysInMonth();
        }
    ),

    /**
     * The last year in the currently selected/viewed decade
     *
     * @function
     * @returns {Number}
     */
    decadeEnd: Ember.computed(
        'decadeStart',
        function() {
            return this.get( 'decadeStart' ) + 9;
        }
    ),

    /**
     * The first year in the currently selected/viewed decade
     *
     * @function
     * @returns {Number}
     */
    decadeStart: Ember.computed(
        'currentYear',
        function() {
            const currentYear = this.get( 'currentYear' );

            return currentYear - ( currentYear % 10 );
        }
    ),

    /**
     * Get an array of objects representing months in the year view
     *
     * Each item contains the following values:
     * - {Boolean} active - Whether a content item's date occurs on this month
     * - {Number} month - The month number in the year (1-12)
     *
     * @function
     * @returns {Object[]}
     */
    monthsInYearView: Ember.computed(
        'contentDates',
        'currentYear',
        function() {
            const contentDates = this.get( 'contentDates' );
            const currentYear = this.get( 'currentYear' );
            const months = new Ember.A();

            for ( let month = 1; month <= 12; month++ ) {
                months.push({
                    active: (
                        contentDates.hasOwnProperty( currentYear ) &&
                        contentDates[ currentYear ].hasOwnProperty( month )
                    ),

                    month
                });
            }

            return months;
        }
    ),

    /**
     * An array of abbreviated, formatted day names of each week day
     *
     * @function
     * @returns {ember/Array}
     */
    shortWeekDayNames: Ember.computed(
        'locale',
        function() {
            const m = window.moment().locale( this.get( 'locale' ) );

            return new Ember.A([
                m.day( 0 ).format( 'dd' ),
                m.day( 1 ).format( 'dd' ),
                m.day( 2 ).format( 'dd' ),
                m.day( 3 ).format( 'dd' ),
                m.day( 4 ).format( 'dd' ),
                m.day( 5 ).format( 'dd' ),
                m.day( 6 ).format( 'dd' )
            ]);
        }
    ),

    /**
     * Whether the current view is "days"
     *
     * @function
     * @returns {Boolean}
     */
    viewingDays: Ember.computed(
        'viewMode',
        function() {
            return 'days' === this.get( 'viewMode' );
        }
    ),

    /**
     * Whether the current view is "months"
     *
     * @function
     * @returns {Boolean}
     */
    viewingMonths: Ember.computed(
        'viewMode',
        function() {
            return 'months' === this.get( 'viewMode' );
        }
    ),

    /**
     * Whether the current view is "years"
     *
     * @function
     * @returns {Boolean}
     */
    viewingYears: Ember.computed(
        'viewMode',
        function() {
            return 'years' === this.get( 'viewMode' );
        }
    ),

    /**
     * An array of objects representing weeks and days in the month view
     *
     * Each day object contains the following values:
     * - {Boolean} active - Whether a content item occurs on this date
     * - {Array} content - Collection of content items occurring on this date
     * - {Number} day - The day number of the month (1-31)
     * - {Boolean} new - Whether the day occurs in the next month
     * - {Boolean} old - Whether the day occurs in the previous month
     *
     * @function
     * @returns {ember.Array}
     */
    weeksInMonthView: Ember.computed(
        'contentDates',
        'currentMonth',
        'currentYear',
        'daysInMonth',
        function() {
            const contentDates = this.get( 'contentDates' );
            const currentMonth = this.get( 'currentMonth' );
            const currentYear = this.get( 'currentYear' );
            const daysInCurrentMonth = this.get( 'daysInMonth' );
            const firstWeekdayOfCurrentMonth = (
                new Date( currentYear, currentMonth - 1, 1 )
            ).getDay();

            const weeks = new Ember.A();
            let inNextMonth = false;

            let previousMonth;
            let previousMonthYear;
            if ( 1 === currentMonth ) {
                previousMonth = 12;
                previousMonthYear = currentYear - 1;
            } else {
                previousMonth = currentMonth - 1;
                previousMonthYear = currentYear;
            }

            const previousMonthDays = window.moment([
                previousMonthYear,
                previousMonth - 1
            ]).daysInMonth();

            let nextMonth;
            let nextMonthYear;
            if ( 12 === currentMonth ) {
                nextMonth = 1;
                nextMonthYear = currentYear + 1;
            } else {
                nextMonth = currentMonth + 1;
                nextMonthYear = currentYear;
            }

            let inPreviousMonth;
            let day;
            let month;
            let year;
            if ( firstWeekdayOfCurrentMonth > 0 ) {
                inPreviousMonth = true;
                day = previousMonthDays - firstWeekdayOfCurrentMonth + 1;
                month = previousMonth;
                year = previousMonthYear;
            } else {
                inPreviousMonth = false;
                day = 1;
                month = currentMonth;
                year = currentYear;
            }

            for ( let week = 0; week < 6; week++ ) {
                const days = new Ember.A();

                for ( let wday = 0; wday < 7; wday++ ) {
                    const active = !inPreviousMonth && !inNextMonth &&
                        contentDates.hasOwnProperty( year ) &&
                        contentDates[ year ].hasOwnProperty( month ) &&
                        contentDates[ year ][ month ].hasOwnProperty( day );

                    days.push({
                        active,
                        content: active ?
                            contentDates[ year ][ month ][ day ] :
                            null,
                        day: day++,
                        'new': inNextMonth,
                        old: inPreviousMonth
                    });

                    if ( inPreviousMonth ) {
                        if ( day > previousMonthDays ) {
                            inPreviousMonth = false;
                            day = 1;
                            month = currentMonth;
                            year = currentYear;
                        }
                    } else if ( day > daysInCurrentMonth ) {
                        inNextMonth = true;
                        day = 1;
                        month = nextMonth;
                        year = nextMonthYear;
                    }
                }

                weeks.push( days );
            }

            return weeks;
        }
    ),

    /**
     * An array of objects representing years in the decade view
     *
     * Each object contains the following values:
     * - {Boolean} active - Whether a content item occurs on this year
     * - {Boolean} new - Whether this year is in the next decade range
     * - {Boolean} old - Whether this year is in the previous decade range
     * - {Number} year - The year number
     *
     * @function
     * @returns {Object[]}
     */
    yearsInDecadeView: Ember.computed(
        'contentDates',
        'decadeEnd',
        'decadeStart',
        function() {
            const contentDates = this.get( 'contentDates' );
            const decadeStart = this.get( 'decadeStart' );
            const decadeEnd = this.get( 'decadeEnd' );
            const years = new Ember.A();

            for ( let year = decadeStart - 1; year <= decadeEnd + 1; year++ ) {
                years.push({
                    active: contentDates.hasOwnProperty( year ),
                    'new': year > decadeEnd,
                    old: year < decadeStart,
                    year
                });
            }

            return years;
        }
    )

});