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' ) ); +});