can3p (can3p) wrote in changelog,
can3p
can3p
changelog

[ljcom] r10678: LJSUP-8956: New calendar widget for S2 s...

Committer: dpetrov
LJSUP-8956: New calendar widget for S2 style 'Minimalism'
A   trunk/htdocs/js/jquery/jquery.lj.inlineCalendar.js
A   trunk/htdocs/js/s2.js
Added: trunk/htdocs/js/jquery/jquery.lj.inlineCalendar.js
===================================================================
--- trunk/htdocs/js/jquery/jquery.lj.inlineCalendar.js	                        (rev 0)
+++ trunk/htdocs/js/jquery/jquery.lj.inlineCalendar.js	2011-06-22 07:45:54 UTC (rev 10678)
@@ -0,0 +1,563 @@
+/*!
+ * LiveJournal Inline calendar
+ *
+ * Copyright 2011, dmitry.petrov@sup.com
+ *
+ * http://docs.jquery.com/UI
+ * 
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *
+ * @overview Inline calendar widget.
+ *
+ * Widget can be attached to any existant markup.
+ *
+ * Date wildcards used:
+ *  - %D - day ( 01 - 31 )
+ *  - %M - month ( 01 - 02 )
+ *  - %Y - year ( yyyy, e.g. 2002 )
+ *  - %s - unix timestamp in ms
+ *
+ * Options:
+ *  - dayRef: Format of the url that will be attached to each day in the calendar.
+ *  - allRefs: Wether to attach links to days in the calendar.
+ *  and override currentDate on success.
+ *  - activeFrom: Days before this will be inactive in calendar.
+ *  - actoveUntil: Days after this willbe inactive incalendar.
+ *  - startMonth: Widget will not allow to switch calendar pane to the month before this.
+ *  - endMonth: Widget will not allow to switch calendar pane to the month after this.
+ *  - startAtSunday: Wether to count sunday as the start of the week.
+ *  - events: Object, containing events to show in the calendar. They will be rendered as links. Structure of the object:
+ *    { "yyyy": { "mm1" : [ d1, d2, d3, d4 ], "mm2": [ d5, d6, d7 ] } }
+ *
+ *  Events:
+ *  - daySelected: Event is triggered when user selects a day in the calendar. The second parameter passed to the
+ *  function is a Date object.
+ *  - dateChange
+ *
+ *  Consistent options ( setting these options is guaranteed to work correctly ):
+ *  - currentDate, date - Set/get current date.
+ *  - activeFrom, date - Set/get earliest active date.
+ *  - activeUntil, date - Set/get last active date.
+ *  - title, title - set calendar title.
+ *  - events, obj - override current events object
+ *
+ *  @TODO: move all service functions to the widget object and merge it with the view.
+ *
+ */
+
+(function( $, window ) {
+
+	var defaultOptions = {
+		dayRef: '/%Y/%M/%D',
+		monthRef: '', //the same, but for the months and year. Calendar will render link, if options are set
+		yearRef: '',
+		allRefs: false,
+		currentDate: new Date(),
+		//allow user to select dates in this range
+		activeUntil: null,
+		activeFrom: null,
+		//allow user to switch months between these dates
+		startMonth: new Date( 1900, 0, 1 ),
+		endMonth: new Date( 2050, 0, 1 ),
+		startAtSunday: false,
+		dateFormat: "%Y-%M-%D",
+		defaultTitle: "Calendar",
+
+		events: null, //object with events to show in the calendar
+		displayedMonth: null, //month displayed on the calendar. If not specified at
+								//startup currentDate is used instead.
+		dateChange: null,
+
+		selectors: {
+			table: 'table',
+			title: 'h5',
+			tbody: 'tbody',
+
+			prevMonth: '.cal-nav-month .cal-nav-prev',
+			nextMonth: '.cal-nav-month .cal-nav-next',
+			prevYear:  '.cal-nav-year .cal-nav-prev',
+			nextYear:  '.cal-nav-year .cal-nav-next',
+
+			monthLabel: '.cal-nav-month .cal-month',
+			yearLabel: '.cal-nav-year .cal-year'
+		},
+
+		classNames: {
+			container: '',
+			inactive : 'other',
+			future : 'other',
+			current  : 'current',
+			nextDisabled : 'cal-nav-next-dis',
+			prevDisabled : 'cal-nav-prev-dis',
+			cellHover : 'hover'
+		},
+
+		ml: {
+			monthNamesShort: [ "January", "February", "March", "April", "May", "June", "July",
+							 "August", "September", "October", "November", "December"],
+			monthNamesLong: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+			caption: "Calendar"
+		}
+	};
+
+	function getDateNumber( d, dropDays ) {
+		dropDays = dropDays || false;
+		var day = d.getDate().toString();
+		if( day.length === 1 ) { day = "0" + day; }
+		if( dropDays ) {
+			day = "";
+		}
+
+		var month = d.getMonth().toString();
+		if( month.length === 1 ) { month = "0" + month; }
+
+		return parseInt( d.getFullYear().toString() + month + day, 10);
+	}
+
+	function insideTimeRange( range, iDate ) {
+		return getDateNumber( iDate, true ) >= getDateNumber( range[0], true ) &&
+				getDateNumber( iDate, true ) <= getDateNumber( range[1], true );
+	}
+
+	/**
+	 * Parse date from string according following format.
+	 * We suppose that every token takes place in the string only once.
+	 *
+	 * @param string str Date string.
+	 * @param string format Date format.
+	 *
+	 * @return Date|null Returns new Date object or null on parse failure.
+	 */
+	function parseDate( str, format ) {
+		var testStr = format,
+			positions = [ null ],
+			pos = 0, token,
+			regs = {
+				'%Y' : '(\\d{4})',
+				'%M' : '(\\d{2})',
+				'%D' : '(\\d{2})',
+				'%S' : '(\\d{13})'
+			};
+
+		while( ( pos = testStr.indexOf( '%', pos ) ) !== -1 ) {
+			token = testStr.substr( pos, 2 );
+			if( token in regs ) {
+				testStr = testStr.replace( token, regs[ token ] );
+				positions.push( token );
+			} else {
+				positions.push( null );
+			}
+		}
+
+		var r = new RegExp( testStr ),
+			arr = r.exec( str );
+
+		if( !arr ) {
+			return null;
+		} else {
+			var d = new Date();
+			for( var i = 1; i < arr.length; ++i ) {
+				if( positions[ i ] ) {
+					switch( positions[ i ] ) {
+						case '%D':
+							d.setDate( arr[ i ] );
+							break;
+						case '%M':
+							d.setMonth( parseInt( arr[ i ], 10 ) - 1 );
+							break;
+						case '%Y':
+							d.setFullYear( arr[ i ] );
+							break;
+					}
+				}
+			}
+
+			return d;
+		}
+	}
+
+	function View(nodes, styles, o)
+	{
+		this.initialize = function () {
+			this.tbody = this.catchTableStructure();
+		};
+
+		this.modelChanged = function (monthDate, events, switcherStates)
+		{
+			//we have a 30% speedup when we temporary remove tbody from dom
+			this.tbody.detach();
+			this.fillDates(monthDate, events);
+
+			for (var sws in switcherStates) {
+				nodes[sws][ (!switcherStates[sws]) ? 'addClass' : 'removeClass']( this.disabledStyle(sws) );
+			}
+
+			var monthText = o.monthRef
+					? $( '<a>', { href: Calendar._formatDate( monthDate, o.monthRef ), text: o.ml.monthNamesShort[ monthDate.getMonth() ] } )
+					: o.ml.monthNamesShort[ monthDate.getMonth() ];
+
+			var yearText = o.yearRef
+					? $( '<a>', { href: Calendar._formatDate( monthDate, o.yearRef ), text: monthDate.getFullYear() } )
+					: monthDate.getFullYear();
+
+			nodes.monthLabel.empty().append( monthText );
+			nodes.yearLabel.empty().append( yearText );
+
+			this.tbody.appendTo( nodes.table );
+		};
+
+		this.catchTableStructure = function() {
+			var tbody = nodes.tbody[0];
+			nodes.daysCells = [];
+
+			var row, rowsCount = tbody.rows.length, cell, cellsCount;
+
+			var toAdd = 6 - rowsCount;
+			while( toAdd-- > 0 ) {
+				//add missing rows if server has rendered not enough markup
+				$( '<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>' ).appendTo( nodes.tbody );
+			}
+			rowsCount = 6;
+			nodes.lastRow = jQuery( tbody.rows[ tbody.rows.length - 1 ] );
+
+			for( row = 0; row < rowsCount; ++row ) {
+				for( cell = 0, cellsCount = tbody.rows[ row ].cells.length; cell < cellsCount; ++cell ) {
+					var node = jQuery( tbody.rows[ row ].cells[ cell ] );
+					nodes.daysCells.push( node );
+				}
+			}
+
+			return jQuery( tbody );
+		};
+
+		this.fillDates = function (monthDate, events)
+		{
+			function hasEvents( date ) {
+				var year = date.getFullYear(),
+					month = date.getMonth(),
+					day = date.getDate();
+
+				return( events && events[ year ] && events[ year ][ month ] && events[ year ][ month ][ day ] );
+			}
+
+			var d = new Date( monthDate );
+			d.setDate( 1 );
+
+			var offset;
+			if( o.startAtSunday ) {
+				offset = d.getDay();
+			} else {
+				offset = ( d.getDay() === 0 ? 6 : d.getDay()  - 1 );
+			}
+			d.setDate( 1 - offset );
+
+			for( var i = 0, l = nodes.daysCells.length; i < l; ++i ) {
+				var cell = nodes.daysCells[ i ];
+				this.formDayString( d, cell, hasEvents( d ), this.isActiveDate( d, monthDate ) );
+
+				d.setDate( d.getDate() + 1 );
+			}
+
+			d.setDate( d.getDate() - 1 ); //get the date from the last cell
+			if( d.getDate() < 7 ) {
+				nodes.lastRow.show();
+			} else {
+				nodes.lastRow.hide();
+			}
+		};
+
+		this.isActiveDate = function( date, currentMonth ) {
+			var isActive = true;
+
+			isActive = ( currentMonth.getFullYear() === date.getFullYear() && currentMonth.getMonth() === date.getMonth() );
+
+			if( isActive && ( o.activeFrom || o.activeUntil ) ) {
+				isActive = ( o.activeFrom && getDateNumber( o.activeFrom ) <= getDateNumber( date ) ) ||
+					( o.activeUntil && getDateNumber( o.activeUntil ) >= getDateNumber( date ) );
+			}
+
+			return isActive;
+		};
+
+		this.formDayString = function( d, label, hasEvents, isActive )
+		{
+			d = new Date( d );
+			var oldDay = label.data( 'day' ),
+				oldHasEvents = label.data( 'hasEvents' ),
+				oldIsActive = label.data( 'isActive' );
+
+			var isCurrentDay = ( getDateNumber( d ) === getDateNumber( o.currentDate ) );
+
+			label.data( 'day', d );
+			label.data( 'isActive', isActive );
+			label.data( 'hasEvents', hasEvents );
+
+			label[isCurrentDay ? 'addClass' : 'removeClass']( styles.current );
+			label.removeClass( styles.cellHover );
+
+			//do not modify dom if nothing changed
+			if( oldIsActive && ( oldIsActive === isActive ) &&
+					oldDay && ( getDateNumber( d ) === getDateNumber( oldDay ) ) &&
+					oldHasEvents && ( oldHasEvents === hasEvents )
+			) {
+				return;
+			}
+
+			if( !isActive ) {
+				label.addClass( styles.inactive ).html(d.getDate());
+			} else if( hasEvents || o.allRefs ) {
+				label
+					.removeClass( styles.inactive )
+					.html( jQuery( '<a />', {
+						html: d.getDate(),
+						href: Calendar._formatDate( d, o.dayRef )
+					} ) );
+			} else {
+				label.removeClass( styles.inactive ).html(d.getDate());
+			}
+		};
+
+		this.disabledStyle = function (sws)
+		{
+			if(sws === 'prevMonth' || sws === 'prevYear') {
+				return styles.prevDisabled;
+			} else {
+				return styles.nextDisabled;
+			}
+		};
+	}
+
+	var Calendar = {
+		options: {}, //all options were move to the default options object
+
+		_create: function() {
+			this.options = jQuery.extend( true, {}, defaultOptions, this.options );
+
+			if( !this.options.displayedMonth ) {
+				this.options.displayedMonth = new Date( this.options.currentDate );
+			}
+
+			this._events = this.options.events;
+			this._hideTimer = null;
+			this._nodes = this._nodes || { container: this.element, root: this.element };
+			this._invalidateTimer = null;
+
+			this._bindNodes();
+
+			this.options.startMonth.setDate( 1 );
+
+			this._view = new (this._getView())( this._nodes, this.options.classNames, this.options );
+			this._view.initialize();
+			this._invalidateDisplay();
+
+			this._bindEvents();
+		},
+
+		_getView: function() {
+			return View;
+		},
+
+		_bindNodes: function() {
+			for( var i in this.options.selectors ) {
+				if( !( i in this._nodes ) ) {
+					this._nodes[ i ] = this._nodes.container.find( this.options.selectors[ i ] );
+				}
+			}
+		},
+
+		destroy: function() {
+			$.Widget.prototype.destroy.apply(this, arguments);
+		},
+
+		_bindEvents: function() {
+			var self = this;
+
+			var switcherStates = this._getSwitcherStates( this.options.currentDate ),
+				switcherMouseDown = function( item ) {
+					return function (ev) {
+						ev.preventDefault();
+						var switcherStates = self._getSwitcherStates( self.options.currentDate );
+
+						if( switcherStates[item] ) {
+							self["_" + item]();
+						}
+					};
+				};
+
+			for (var sws in switcherStates) {
+				this._nodes[sws].mousedown( switcherMouseDown(sws) );
+			}
+
+			this._nodes.tbody
+				.delegate( 'td', 'mousedown', function( ev ) {
+					self._cellSelectedEvent( $( this ), ev );
+				} );
+		},
+
+		_switchMonth: function ( go ) {
+			var event = jQuery.Event( "dateChange" );
+			event.moveForward = go > 0;
+			event.switchType = Math.abs( go ) === 12 ? "year" : ( Math.abs( go ) === 1 ? "month" : null );
+			event.date = new Date( this.options.displayedMonth );
+			event.date.setMonth( event.date.getMonth() + go );
+
+			this._nodes.container.trigger( event );
+			this._setOption( 'displayedMonth', event.date );
+		},
+
+		_prevMonth: function () { this._switchMonth( -1 ); },
+		_nextMonth: function () { this._switchMonth( 1 ); },
+
+		_prevYear : function () { this._switchMonth( -12 ); },
+		_nextYear : function () { this._switchMonth( 12 ); },
+
+		_cellSelectedEvent: function( cell, ev ) {
+			//if cell is inactive or user controls it's behavior we do not pass event to the link
+			if( !cell.data('isActive' ) || this._cellSelected( cell.data( 'day' ) ) ) {
+				ev.stopPropagation();
+				ev.preventDefault();
+			}
+		},
+
+		/**
+		 * @return {Boolean} returns true if user prevents default behaviour
+		 */
+		_cellSelected: function( date ) {
+			var event = jQuery.Event( "daySelected" );
+			this._nodes.root.trigger( event, [ date ] );
+
+			if( !event.isDefaultPrevented() ) {
+				this._setOption( 'currentDate', date );
+			}
+
+			return !event.isDefaultPrevented();
+		},
+
+		_fitDate: function( date ) {
+			date = new Date( date );
+			var enabledMonthsRange = [ this.options.startMonth, this.options.endMonth ];
+
+			if( !insideTimeRange( enabledMonthsRange, date ) ) {
+				if( getDateNumber( date, true ) < getDateNumber( enabledMonthsRange[ 0 ], true ) ) {
+					date = new Date( enabledMonthsRange[ 0 ] );
+				} else {
+					date = new Date( enabledMonthsRange[ 1 ] );
+				}
+			}
+
+			return date;
+		},
+
+		_getSwitcherStates: function () {
+			var monthDate = this.options.displayedMonth,
+				yearStart = new Date( monthDate.getFullYear(), 0, 1 ),
+				yearEnd = new Date( monthDate.getFullYear(), 11, 1 );
+
+			return {
+				prevMonth: this._isActivePrev( monthDate ) !== false,
+				prevYear: this._isActivePrev( yearStart ) !== false,
+				nextMonth: this._isActiveNext( monthDate ) !== false,
+				nextYear: this._isActiveNext( yearEnd ) !== false
+			};
+		},
+
+		_isActiveNext: function( date ) { return this._isActiveDate( date, 1 ); },
+		_isActivePrev: function( date ) { return this._isActiveDate( date, -1 ); },
+		_isActiveDate: function( date, dir ) {
+			var d = new Date( date );
+			d.setMonth( d.getMonth() + dir );
+			d.setDate( 1 );
+
+			return insideTimeRange( [ this.options.startMonth, this.options.endMonth ], d );
+		},
+
+		_invalidateDisplay: function() {
+			var self = this;
+			clearTimeout( this._invalidateTimer );
+
+			setTimeout( function() {
+				self._view.modelChanged( self.options.displayedMonth, self._events, self._getSwitcherStates() );
+			}, 50 );
+		},
+
+		_setOption: function( name, value ) {
+			switch( name ) {
+				case 'currentDate':
+					this.options.currentDate = this._fitDate( value );
+					this._setOption( 'displayedMonth', value );
+					break;
+				case 'activeFrom':
+					this.options.activeFrom = new Date( value );
+					this._invalidateDisplay();
+					break;
+				case 'activeUntil':
+					this.options.activeUntil = new Date( value );
+					this._invalidateDisplay();
+					break;
+				case 'title':
+					this._title = value;
+					this._nodes.title.html( value );
+					break;
+				case 'events':
+					this._events = value;
+					this._invalidateDisplay();
+					break;
+				case 'displayedMonth':
+					this.options.displayedMonth = this._fitDate( new Date( value ) );
+					this._invalidateDisplay();
+					break;
+				case 'startMonth':
+					this.options.startMonth = new Date( value );
+					this._invalidateDisplay();
+					break;
+				case 'endMonth':
+					this.options.endMonth = new Date( value );
+					this._invalidateDisplay();
+					break;
+			}
+		},
+
+		/**
+		 * Serialize date to string according the format string.
+		 * We suppose that every token takes place in the string only once.
+		 *
+		 * @param string Date Date object.
+		 * @param string format Date format.
+		 * @return Date|null Returns new Date object or null on parse failure.
+		 */
+		_formatDate: function( d, format ) {
+			format = format || "%Y-%M-%D";
+			var str = format;
+					
+			var subs = {
+				'%Y' : d.getFullYear(),
+				'%M' : ( "0" + ( d.getMonth() + 1 ) ).slice( -2 ),
+				'%D' : ( "0" + d.getDate() ).slice( -2 ),
+				'%S' : +d
+			};
+			
+			for( var k in subs ) {
+				if( !subs.hasOwnProperty(k) ) {
+					continue;
+				}
+				
+				str = str.replace( k, subs[k] );
+			}
+
+			return str;
+		}
+
+	};
+
+	$.widget('lj.inlineCalendar', Calendar );
+
+	$.lj.inlineCalendar.setDefaults = function ( opts ) {
+		if( opts ) {
+			jQuery.extend( defaultOptions, opts );
+		}
+	};
+
+} ( jQuery, window ) );
+

Added: trunk/htdocs/js/s2.js
===================================================================
--- trunk/htdocs/js/s2.js	                        (rev 0)
+++ trunk/htdocs/js/s2.js	2011-06-22 07:45:54 UTC (rev 10678)
@@ -0,0 +1,74 @@
+jQuery( function( $ ) {
+	var calendarWidget = {
+		init: function( node ) {
+			var widget = this;
+			this.events = Site.journal_calendar;
+			this.calendar = node;
+
+			this.calendar.inlineCalendar( {
+				selectors: {
+					prevMonth: '.sbar-cal-nav-month .sbar-cal-nav-prev',
+					nextMonth: '.sbar-cal-nav-month .sbar-cal-nav-next',
+					prevYear:  '.sbar-cal-nav-year .sbar-cal-nav-prev',
+					nextYear:  '.sbar-cal-nav-year .sbar-cal-nav-next',
+
+					monthLabel: '.sbar-cal-nav-month .sbar-cal-month',
+					yearLabel: '.sbar-cal-nav-year .sbar-cal-year'
+				},
+				classNames: {
+					current  : 'today',
+					nextDisabled : 'disabled',
+					prevDisabled : 'disabled'
+				},
+				events: this.getEvents( this.events.year, this.events.month, this.events.days ),
+				monthRef: '/%Y/%M',
+				yearRef: '/%Y',
+				startAtSunday: true
+			} ).bind( 'dateChange', function( ev ) {
+				var type = ( ev.moveForward ) ? "next_" : "prev_";
+				type += ( ev.switchType === "year" ) ? "year" : "month";
+
+				if( widget.events[ type ] ) {
+					ev.date.setFullYear( widget.events[ type ][ 0 ] );
+					ev.date.setMonth( widget.events[ type ][ 1 ] -1 );
+
+					widget.fetchEvents( widget.events[ type ][ 0 ], widget.events[ type ][ 1 ] );
+				}
+			} );
+
+			this.fixBounds();
+		},
+		getEvents: function( year, month, days ) {
+			var result = {};
+			result[ +year ] = {};
+			result[ +year ][ +month -1 ] = days;
+
+			return result;
+		},
+
+		fetchEvents: function( year, month ) {
+			var widget = this;
+			var curMonth = this.calendar.inlineCalendar( 'option', 'displayedMonth' );
+			$.getJSON( LiveJournal.getAjaxUrl( 'calendar' ), { year: year, month: month },
+					function( answer ) {
+						widget.events = answer;
+						widget.fixBounds();
+						widget.calendar.inlineCalendar( 'option', 'events',
+							widget.getEvents( widget.events.year, widget.events.month, widget.events.days )  );
+					}
+			);
+		},
+
+		fixBounds: function() {
+			var curMonth = this.calendar.inlineCalendar( 'option', 'displayedMonth' );
+			var next = this.events.next_year || this.events.next_month;
+			var prev = this.events.prev_year || this.events.prev_month;
+			this.calendar.inlineCalendar( 'option', 'endMonth', ( next ) ?
+					new Date( next[0], next[1] - 1, 1 ) : curMonth );
+			this.calendar.inlineCalendar( 'option', 'startMonth', ( prev ) ?
+					new Date( prev[0], prev[1] - 1, 1 ) : curMonth );
+		}
+	}
+
+	calendarWidget.init( $( '.sidebar-cal' ) );
+});

Tags: can3p, js, ljcom
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 0 comments