API Docs for:
Show:

File: js/calendar-jumpnav.js

/**
 A Plugin for Y.Calendar that sets up Calendar to work with Y.Calendar.JumpNavView, which
 is a View class extension to setup a "click" listener on Calendar's "Month Year" header label
 that opens a popup Panel to provide a quick method to jump to a month / year.

 Please see the Calendar.JumpNavView documentation for full details.

 @example
 	var myCal = new Y.Calendar({
		contentBox: "#mycal",
		width: '200px',
		showPrevMonth: true,
		showNextMonth: true
	});

	// Plugin the View to this Calendar ... available years are 1988 to 2021
	cal.plug( Y.Plugin.Calendar.JumpNav, {
		yearStart: 1988,
		yearEnd:   2021
	});

	cal.render();

 @class Y.Plugin.Calendar.JumpNav
 @param config
 @constructor
 **/
function CalJumpNav(config) {
   CalJumpNav.superclass.constructor.apply(this, arguments);
}

/**
 * The namespace property of this plugin that this can be accessed from via a Calendar instance.
 * @property NS
 * @description This plugin can be accessed from a Calendar instance as "Calendar.jumpnav"
 * @type {String}
 */
CalJumpNav.NS = "jumpnav";

/**
 * The name property of this plugin
 * @property NAME
 * @description name for this plugin
 * @type {String}
 */
CalJumpNav.NAME = "calendarJumpnavPlugin";

CalJumpNav.ATTRS = {

    /**
     * @attribute yearStart
     * @type Number
     * @default 1985
     */
    yearStart:{
        value:      1985,
        validator:  Y.Lang.isNumber
    },

    /**
     * @attribute yearEnd
     * @type Number
     * @default Current year
     */
    yearEnd:{
        value:      new Date().getFullYear(),
        validator:  Y.Lang.isNumber
    },

    /**
     * The x,y offset (horiz, vert) that should be used to offset the popup Panel from the original Calendar "header label"
     *  that was clicked.
     * @attribute offsetXY
     * @type Array
     * @default [ 30, 10 ]
     */
    offsetXY:{
        value:      [ 30, 10 ],
        validator:  Y.Lang.isArray
    },

    /**
     Sets the Event "type" that is used in the Calendar "header label" listener to open the popup Panel.
     Sensible values are "click" or "dblclick".
     @attribute openEventType
     @type String
     @default 'click'
     **/
    openEventType:{
        value:      'click',
        validator:  Y.Lang.isString
    },


	/**
	 This flag sets whether the Panel instance should be hidden after the "Go" button is pressed
	 @attribute closeAfterGo
	 @type Boolean
	 @default true
	 **/
    closeAfterGo:{
        value:      true,
        validator:  Y.Lang.isBoolean
    }

}


Y.extend(CalJumpNav, Y.Plugin.Base, {

    _view:  null,

    /**
     *
     * @method initializer
     * @param config
     */
    initializer: function(config) {
        this.afterHostEvent("render", this._afterHostRenderEvent);
        return this;
    },

    /**
     * Destroys the View that was created and detaches its event listeners
     * @method destructor
     * @protected
     */
    destructor : function() {
        if(this._view) {
            this._view.destroy();
        }
        this._view = null;
    },

    /**
     * Connector method that initializes the View and connects it to the Calendar instance
     * @method _afterHostRenderEvent
     * @private
     */
    _afterHostRenderEvent : function() {
        if(!this._view) {
            var viewCfgs = this.getAttrs(['yearStart','yearEnd','template','offsetXY','closeAfterGo','openEventType']);
            viewCfgs.calendar = this.get('host');
            this._view = new Y.Calendar.JumpNavView(viewCfgs);
        }
     }

});

/**
 This class defines a View class extension for Calendar that configures to load on a "click" on the Calendar's "Month Year"
 header label to display a popup panel that allows for selecting the month / year without requiring to page thru by month.
 The view creates a Panel instance from a standard template (see the property [template](#property_template) for the default)
 and handles populating the SELECT dropdown controls for "month" and "year".

 Attributes are provided that include [yearStart](#attr_yearStart) and [yearEnd](#attr_yearEnd) for defining the range to
 be used for the "year" dropdown elements, for example.

 #####Usage
 The simplest application includes creating a Calendar instance and then creating the View and attaching the calendar to
 the view with the [calendar](#attr_calendar) attribute.

	var cal = new Y.Calendar({
		contentBox: "#mycal",
		width:'240px',
		showPrevMonth: true,
		showNextMonth: true
	}).render();

	// This creates a View instance and connects it to the "cal" Calendar instance.
	var calJNav = new Y.Calendar.JumpNavView({
		calendar:  cal,
		yearStart: 1988,
		yearEnd:   2021
	});

 An additional module is provided, the Y.Plugin.Calendar.JumpNav plugin that attaches the Calendar to the view via a plugin method.

 @class Y.Calendar.JumpNavView
 @extends Y.View
 @version 3.5.0
 **/
Y.Calendar.JumpNavView = Y.Base.create('caljumpnav', Y.View,[],{

    /**
     * Defines event objects as part of View's event handling, specifically defines actions to
     * be taken on "change" events of the month and year SELECT controls.
     * @property events
     * @type Object
     * @static
     */
    events: {
        'select.yui3-calendar-navpanel-month' : { change : '_selectMonth' },
        'select.yui3-calendar-navpanel-year' :  { change: '_selectYear' }
    },

    /**
     Default setting for the `template` attribute that defines the Panel HTML contents, including
     the SELECT options for month and year.

     @example
	// Where classPanel is replaced by 'yui3-calendar-jumpnav-panel',
	// and classMonth by 'yui3-calendar-jumpnav-month'
	// and classYear by 'yui3-calendar-jumpnav-year'
	<div class="{classPanel}">
		<div class="yui3-widget-bd">
		<table>
			<tr><td align="right">Jump to Month:</td><td><select class="{classMonth}"></select></td></tr>
			<tr><td align="right">and Year:</td><td><select class="{classYear}"></select></td></tr>
		</table>
		</div>
	</div>


     @property template
     @type String HTML Setting for Panel's contents
     @default See example below
     @static
     **/
    template:   '<div class="{classPanel}"><div class="yui3-widget-bd">'+
                '<table><tr><td align="right">Jump to Month:</td>' +
                '<td><select class="{classMonth}"></select></td></tr>' +
                '<tr><td align="right">and Year:</td>' +
                '<td><select class="{classYear}"></select></td></tr>'+
                '</table></div></div>',

    /**
     * Placeholder for the Y.Panel instance used in this view
     * @property _panel
     * @type Y.Panel
     * @default null
     * @private
     */
    _panel:     null,

    /**
     * Holder for an array of the Listeners created by this view so we can detach them when finished
     * @property _subscr
     * @type Array
     * @default []
     * @private
     */
    _subscr:    [],

    /**
     * Stores the classname to search the Calendar instance for to hook onto the "header-label" element
     * @property _classCalHead
     * @type String
     * @default 'yui3-calendar-header-label'
     * @private
     */
    _classCalHead:  'yui3-calendar-header-label',

    /**
     * Stores the classname used internally for the Panel srcNode used in this view
     * @property _classPanel
     * @type String
     * @default 'yui3-calendar-jumpnav-panel'
     * @private
     */
    _classPanel:    'yui3-calendar-jumpnav-panel',


    /**
     * Placeholder for the Node instance for this view, set to Panel contentBox
     * @property _viewNode
     * @type Node
     * @default null
     * @private
     */
    _viewNode:      null,

    /**
     * Stores the classname used internally for the Panel's "month" SELECT control
     * @property _classMonth
     * @type String
     * @default 'yui3-calendar-jumpnav-month'
     * @private
     */
    _classMonth:    'yui3-calendar-jumpnav-month',

    /**
     * Stores the classname used internally for the Panel's "month" SELECT control
     * @property _classYear
     * @type String
     * @default 'yui3-calendar-jumpnav-year'
     * @private
     */
    _classYear:     'yui3-calendar-jumpnav-year',


//===========================    LIFECYCLE METHODS   ====================================

    /**
     * Initializer that creates the `container`, the Panel instance and listeners for this view
     * @method initializer
     * @return this
     * @chainable
     * @protected
     */
    initializer: function(){

        //
        //  Read the template and create the container
        //
        var tmpl = this.get('template') || this.template;

        tmpl = Y.Lang.sub(tmpl,{
            classPanel: this._classPanel,
            classMonth: this._classMonth,
            classYear:  this._classYear
        });

        //
        //
        //
        var viewnode = Y.Node.create(tmpl);
        this.set('container',viewnode);
        this._createPanelView(viewnode);

        // Generate the SELECT OPTIONS for the months control ...
        this._regenMonths(new Date());

        //
        //  Define listeners on the Calendar "header label" and "dateChange" events to update the UI
        //
        if(this.get('calendar')) {
            var cal     = this.get('calendar'),
                calHead = cal.get('contentBox').one('.'+this._classCalHead);

            this._subscr.push( calHead.on( this.get('openEventType'),this.render,this) );
            this._subscr.push( cal.on('dateChange',this._onCalendarDateChange,this)  );
        }

        return this;
    },

    /**
     * Renders the Panel and resets the SELECT controls "selected" default to the current Calendar data setting
     * @method render
     * @chainable
     * @return this
     * @protected
     */
    render: function(){
        var pcont   = this._panel,
            cal     = this.get('calendar'),
            cdate   = cal.get('date'),
            pmonth  = this._viewNode.one('.'+this._classMonth).get('selectedIndex') || null,
            pyear   = this._viewNode.one('.'+this._classYear).get('selectedIndex') || null;
        //
        // Check if the months and years "SELECT" controls need regenerating ...
        //
        if( pmonth !== cdate.getMonth() ) {
            this._setSelectedMonth(cdate);
        }

        if(!this._yearInSelect(cdate.getFullYear()))
            this._regenYears(cdate);
        this._setSelectedYear(cdate);

        // show the panel
        pcont.show();

    //
    //  Re-position the container
    //
        var calcbox = cal.get('contentBox'),
            xy      = calcbox.getXY();

        xy[0] += +(+calcbox.getComputedStyle('width').replace(/px/,'')) + this.get('offsetXY')[0];
        xy[1] += this.get('offsetXY')[1];
        pcont.set('xy',xy);

        return this;

    },

    /**
     * Clears up the created listeners and destroys the Panel
     * @method destructor
     * @protected
     */
    destructor: function(){
		Y.Array.each( this._subscr, function(item){
            item.detach();
        });
        this._subscr = null;

        if(this._panel) {
   			this._panel.destroy();
   			this._panel = null;
   		}

   		this._viewNode = null;
   	},


//===========================   PRIVATE  METHODS   ====================================

	/**
	 * @method _createPanelView
	 * @param {Node} vnode The Node that was created from `template` that will be used as the container for the Y.Panel creation.
	 * @private
	 */
    _createPanelView: function(vnode){
        var oSelf = this;

        //
        //  Create the Panel for the CalNavigator, initially hidden
        //
        var npanel = new Y.Panel({
            srcNode : vnode,
            model:    false,
            render  : true,
            visible:  false,
            plugins:  [Y.Plugin.Drag],
            buttons: [
                {
                    value  : 'Go',
                    section: Y.WidgetStdMod.FOOTER,
                    action : function (e) {
                        e.preventDefault();
                        oSelf._onGoButton();
                        this.hide();
                    }
                },
                {
                    value  : 'Cancel',
                    section: Y.WidgetStdMod.FOOTER,
                    action : function (e) {
                        e.preventDefault();
                        this.hide();
                    }
                }
            ]
        });

        this._panel = npanel;
        this.set('container',npanel);

        this._viewNode = npanel.get('contentBox');

    },

    /**
     * Sets the currently "selected" OPTION for the month control to the current month
     * @method _setSelectedMonth
     * @param {Date} curDate
     * @private
     */
    _setSelectedMonth: function(curDate){
        var monthNode = this._viewNode.one('.'+this._classMonth);
        monthNode.set('selectedIndex', curDate.getMonth() );
    },

    /**
     * Sets the currently "selected" OPTION for the year control to the current year.
     * <br/>This method searches the OPTION nodes for "value" set to the year, to get around
     * CSS selector issues in some browers.
     * @method _setSelectedYear
     * @param {Date} curDate
     * @private
     */
    _setSelectedYear: function(curDate){
        var yearNodes = this._viewNode.one('.'+this._classYear).all('option');
        yearNodes.some(function(yn){
            if(yn.get('value') == curDate.getFullYear() ) {
                yn.set('selected',true);
                return true;
            }
        });
    },

    /**
     * Method that regenerates the "month" SELECT control, adding the months and defining the "selected" as the curDate parameter
     * @method _regenMonths
     * @param {Date} curDate Current date to set as "selected"
     * @private
     */
    _regenMonths: function(curDate){
        curDate = curDate || this.get('calendar').get('date');
        var monthNode = this._viewNode.one('.'+this._classMonth),
            msel      = monthNode.getDOMNode(),
            curMonth  = (curDate && curDate.getMonth) ? curDate.getMonth() : null,
            months    = ['January','February','March','April','May','June','July','August','September','October','November','December'];

        if(!msel) return;

        //
        //  Update the SELECT, OPTIONS ... easier and better cross-browser to do via DOMNodes
        //
        msel.options.length = 0;
        Y.Array.each(months,function(m,mindx){
            msel.options[msel.options.length] = new Option(m,mindx);
            // set "selected" for default
            if(curMonth === mindx) msel.options[msel.options.length-1].selected = true;
        });

    },

    /**
     * Method that regenerates the "year" SELECT control, adding the months defined by attributes `yearStart` to `yearEnd`
     * and defining the "selected" as the curDate parameter
     * @method _regenYears
     * @param {Date} curDate Current date to set as "selected"
     * @private
     */
    _regenYears: function(curDate){
        curDate = curDate || this.get('calendar').get('date');
        var yearNode = this._viewNode.one('.'+this._classYear),
            ysel     = yearNode.getDOMNode(),
            curYear  = ( curDate && curDate.getFullYear ) ? curDate.getFullYear() : null,
            yearStart= this.get('yearStart'),
            yearEnd  = this.get('yearEnd');

        if(!ysel) return;

        //
        //  Update the SELECT, OPTIONS ... easier and better cross-browser to do via DOMNodes
        //
        ysel.options.length = 0;
        for(var y=yearStart; y<= yearEnd; y++) {
            ysel.options[ysel.options.length] = new Option(y,y);
            // set "selected" for default
            if(curYear && curYear === y ) ysel.options[ysel.options.length-1].selected = true;
        }
    },

    /**
     * Helper method to check if the specified year is an option in the current SELECT control OPTIONS.
     * @method _yearInSelect
     * @param {Number} year Year to be checked if it is in the current SELECT control
     * @return {Boolean} true if year is in SELECt, false if not
     * @private
     */
    _yearInSelect: function(year){
        var yearNode = this._viewNode.one('.'+this._classYear),
            yindex   = null;

        yearNode.get('options').some(function(yo,yindx){
            if( +(yo.get('value')) === year) {
                yindex = yindx;
                return true;
            }
        });
        return (yindex!==null) ? true : false;
    },


//===========================  PRIVATE METHODS : Listeners  =============================

    /**
     * Listener on the Calendar's "dateChange" event to update the JumpNavigator if it's visible
     * @method _onCalendarDateChange
     * @param {EventTarget} e
     * @private
     */
    _onCalendarDateChange: function(e) {
        var newDate = e.newVal;
        if(this._panel && this._panel.get('visible')){
            this._setSelectedMonth(newDate);
            if(!this._yearInSelect(newDate.getFullYear()))
                this._regenYears(newDate);
            this._setSelectedYear(newDate);
        }
    },

    /**
     * Listener on the Panel's "Go" button, resets the Calendar to the Month/Year and first day,
     * and optionally closes the Panel if `closeAfterGo` is true.
     * @method _onGoButton
     * @private
     */
    _onGoButton: function(){
        var monthIndex = this.get('container').one('.'+this._classMonth).get('value'),
            yearValue  = this.get('container').one('.'+this._classYear).get('value');
        this.get('calendar').set('date', new Date(yearValue,monthIndex,1) );

        if(this.get('closeAfterGo'))
            if(this._panel)
                this._panel.hide();
            else
                this._viewNode.hide();
    },

    /**
     * "change" Listener on the SELECT control for the JumpNavigator's month control
     * @method _selectMonth
     * @param e
     * @private
     */
    _selectMonth: function(e){
        var tar   = e.target,    // SELECT node
            opts  = tar.get('options'),
            sndx  = +tar.get('selectedIndex'),
            sopt  = opts.item(sndx),
            month = +sopt.get('value');

        this.set('month', month);
        this.fire('monthSelected',{evt:e, value:sopt.get('value'), text:sopt.get('text')});

    },
    /**
     * @event monthSelected
     * @param {Object} return
     * @param {EventTarget} return.evt Eventtarget for the SELECT "change" event
     * @param {Number} return.value Value part of the selected OPTION, which is the selected month
     * @param {String} return.text Text from selected OPTION, which is the month name
     */

    /**
     * "change" Listener on the SELECT control for the JumpNavigator's year control
     * @method _selectYear
     * @param e
     * @private
     */
    _selectYear:  function(e){
        var tar   = e.target,    // SELECT node
            opts  = tar.get('options'),
            sndx  = +tar.get('selectedIndex'),
            sopt  = opts.item(sndx),
            year  = +sopt.get('value');

        this.set('year', year);
        this.fire('yearSelected',{evt:e, value:year, text:sopt.get('text')});
    }

    /**
     * @event yearSelected
     * @param {Object} return
     * @param {EventTarget} return.evt Eventtarget for the SELECT "change" event
     * @param {Number} return.value Value part of the selected OPTION, which is the Selected year
     * @param {String} return.text Text from selected OPTION, which is the selected year
     */

},{
    ATTRS:{

        /**
         * Specifies the Calendar instance that this view will be attached to for header label clicks and
         * for updates to the `date` attribute.

         * @attribute calendar
         * @type Y.Calendar
         * @default null
         */
        calendar:{
            value:      null,
            validator:  function(v) { return v instanceof Y.Calendar; }
        },

        /**
         Defines the HTML content that is used to setup the Y.Panel instance that is created by this View.
         See the property [template](#property_template) for the default setting.

         @attribute template
         @type String
         @default this.template
         **/
        template:{
            valueFn:    function(){ return this.template; },
            validator:  Y.Lang.isString
        },

        /**
         Sets the beginning year that will be used to setup the "year" SELECT dropdown control, defaults to
         a favorite year of the author's.
         @attribute yearStart
         @type Number
         @default 1985
         **/
        yearStart:{
            value:      1992,
            validator:  Y.Lang.isNumber
        },

        /**
         Sets the last year that should be setup within the "year" SELECT dropdown control, defaults the
         the current year.
         @attribute yearEnd
         @type Number
         @default Current year
         **/
        yearEnd:{
            value:      new Date().getFullYear(),
            validator:  Y.Lang.isNumber
        },

	    /**
	     The x,y offset (horiz, vert) that should be used to offset the popup Panel from the original Calendar "header label"
	      that was clicked.
	     @attribute offsetXY
	     @type Array
	     @default [ 30, 10 ]
	     **/
        offsetXY:{
            value:      [ 30, 10 ],
            validator:  Y.Lang.isArray
        },


	    /**
	     Sets the Event "type" that is used in the Calendar "header label" listener to open the popup Panel.
         Sensible values are "click" or "dblclick".
	     @attribute openEventType
	     @type String
	     @default 'click'
	     **/
        openEventType:{
            value:      'click',
            validator:  Y.Lang.isString
        },

		/**
		 This flag sets whether the Panel instance should be hidden after the "Go" button is pressed
		 @attribute closeAfterGo
		 @type Boolean
		 @default true
		 **/
        closeAfterGo:{
            value:      true,
            validator:  Y.Lang.isBoolean
        }

    }
});

Y.namespace("Plugin.Calendar").JumpNav = CalJumpNav;