diff --git a/scm-webapp/src/main/webapp/index.mustache b/scm-webapp/src/main/webapp/index.mustache index a61b702d1c..a4bb0b8dc6 100644 --- a/scm-webapp/src/main/webapp/index.mustache +++ b/scm-webapp/src/main/webapp/index.mustache @@ -54,6 +54,9 @@ + + + diff --git a/scm-webapp/src/main/webapp/resources/js/override/ext.util.format.js b/scm-webapp/src/main/webapp/resources/js/override/ext.util.format.js index 164280df4d..7d679efe3c 100644 --- a/scm-webapp/src/main/webapp/resources/js/override/ext.util.format.js +++ b/scm-webapp/src/main/webapp/resources/js/override/ext.util.format.js @@ -32,104 +32,24 @@ Ext.apply(Ext.util.Format, { formatTimestamp: function(value){ - var date = null; var result = ''; if ( value != null && (value > 0 || value.length > 0)){ var df = state.clientConfig.dateFormat; if ( df == null || df.length == 0 || ! Ext.isDefined(value) ){ df = "Y-m-d H:i:s"; } - date = new Date(value); - result = Ext.util.Format.date(date, df); + result = moment(value).format(df); } - if (date != null && result.indexOf("{0}") >= 0){ - result = String.format( result, Ext.util.Format.timeAgo(date) ); + if (result.indexOf("{0}") >= 0){ + result = String.format(result, Ext.util.Format.timeAgo(value)); } return result; }, - timeAgoJustNow: 'just now', - timeAgoOneMinuteAgo: '1 minute ago', - timeAgoOneMinuteFromNow: '1 minute from now', - timeAgoMinutes: 'minutes', - timeAgoOneHourAgo: '1 hour ago', - timeAgoOneHourFromNow: '1 hour from now', - timeAgoHours: 'hours', - timeAgoYesterday: 'yesterday', - timeAgoTomorrow: 'tomorrow', - timeAgoDays: 'days', - timeAgoLastWeek: 'last week', - timeAgoNextWeek: 'next week', - timeAgoWeeks: 'weeks', - timeAgoLastMonth: 'last month', - timeAgoNextMonth: 'next month', - timeAgoMonths: 'months', - timeAgoLastYear: 'last year', - timeAgoNextYear: 'next year', - timeAgoYears: 'years', - timeAgoLastCentury: 'last century', - timeAgoNextCentury: 'next century', - timeAgoCenturies: 'centuries', - timeAgo : function(value){ - - var time_formats = [ - [60, this.timeAgoJustNow, 1], // 60 - [120, this.timeAgoOneMinuteAgo, this.timeAgoOneMinuteFromNow ], // 60*2 - [3600, this.timeAgoMinutes, 60], // 60*60, 60 - [7200, this.timeAgoOneHourAgo, this.timeAgoOneHourFromNow ], // 60*60*2 - [86400, this.timeAgoHours, 3600], // 60*60*24, 60*60 - [172800, this.timeAgoYesterday, this.timeAgoTomorrow ], // 60*60*24*2 - [604800, this.timeAgoDays, 86400], // 60*60*24*7, 60*60*24 - [1209600, this.timeAgoLastWeek, this.timeAgoNextWeek], // 60*60*24*7*4*2 - [2419200, this.timeAgoWeeks, 604800], // 60*60*24*7*4, 60*60*24*7 - [4838400, this.timeAgoLastMonth, this.timeAgoNextMonth], // 60*60*24*7*4*2 - [29030400, this.timeAgoMonths, 2419200], // 60*60*24*7*4*12, 60*60*24*7*4 - [58060800, this.timeAgoLastYear, this.timeAgoNextYear], // 60*60*24*7*4*12*2 - [2903040000, this.timeAgoYears, 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12 - [5806080000, this.timeAgoLastCentury, this.timeAgoNextCentury], // 60*60*24*7*4*12*100*2 - [58060800000, this.timeAgoCenturies, 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100 - ]; - - var date = value; - - if (!Ext.isDate(date)){ - date = parseInt(date); - if ( date == 'NaN' ){ - date = Ext.parseDate(value) - } else { - date = new Date(date); - } - } - - var seconds = (new Date - date) / 1000; - var token = 'ago'; - var list_choice = 1; - if (seconds < 0){ - seconds = Math.abs(seconds); - token = 'from now'; - list_choice = 2; - } - - var i = 0; - var format = null; - while (format = time_formats[i++]){ - if (seconds < format[0]) { - if (typeof format[2] == 'string'){ - return format[list_choice]; - } else { - return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; - } - } - } - - if ( date == "Invalid Date" ){ - date = value; - } - - return date; + return moment(value).fromNow(); }, id: function(value){ diff --git a/scm-webapp/src/main/webapp/resources/moment/moment.js b/scm-webapp/src/main/webapp/resources/moment/moment.js new file mode 100644 index 0000000000..e6fa145aa4 --- /dev/null +++ b/scm-webapp/src/main/webapp/resources/moment/moment.js @@ -0,0 +1,1213 @@ +// moment.js +// version : 1.7.2 +// author : Tim Wood +// license : MIT +// momentjs.com + +(function (undefined) { + + /************************************ + Constants + ************************************/ + + var moment, + VERSION = "1.7.2", + round = Math.round, i, + // internal storage for language config files + languages = {}, + currentLanguage = 'en', + + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module.exports), + + // Parameters to check for on the lang config. This list of properties + // will be inherited from English if not provided in a language + // definition. monthsParse is also a lang config property, but it + // cannot be inherited and as such cannot be enumerated here. + langConfigProperties = 'months|monthsShort|weekdays|weekdaysShort|weekdaysMin|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'), + + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + + // format tokens + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?)/g, + + // parsing tokens + parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi, + + // parsing token regexes + parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 + parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenWord = /[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/i, // any word characters or numbers + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO seperator) + + // preliminary iso regex + // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 + isoRegex = /^\s*\d{4}-\d\d-\d\d(T(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.S', /T\d\d:\d\d:\d\d\.\d{1,3}/], + ['HH:mm:ss', /T\d\d:\d\d:\d\d/], + ['HH:mm', /T\d\d:\d\d/], + ['HH', /T\d\d/] + ], + + // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, + + // getter and setter names + proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, + + // format function strings + formatFunctions = {}, + + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w M D d'.split(' '), + paddedTokens = 'M D H h m s w'.split(' '), + + /* + * moment.fn.format uses new Function() to create an inlined formatting function. + * Results are a 3x speed boost + * http://jsperf.com/momentjs-cached-format-functions + * + * These strings are appended into a function using replaceFormatTokens and makeFormatFunction + */ + formatTokenFunctions = { + // a = placeholder + // b = placeholder + // t = the current moment being formatted + // v = getValueAtKey function + // o = language.ordinal function + // p = leftZeroFill function + // m = language.meridiem value or function + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return getValueFromArray("monthsShort", this.month(), this, format); + }, + MMMM : function (format) { + return getValueFromArray("months", this.month(), this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + var a = new Date(this.year(), this.month(), this.date()), + b = new Date(this.year(), 0, 1); + return ~~(((a - b) / 864e5) + 1.5); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return getValueFromArray("weekdaysMin", this.day(), this, format); + }, + ddd : function (format) { + return getValueFromArray("weekdaysShort", this.day(), this, format); + }, + dddd : function (format) { + return getValueFromArray("weekdays", this.day(), this, format); + }, + w : function () { + var a = new Date(this.year(), this.month(), this.date() - this.day() + 5), + b = new Date(a.getFullYear(), 0, 4); + return ~~((a - b) / 864e5 / 7 + 1.5); + }, + YY : function () { + return leftZeroFill(this.year() % 100, 2); + }, + YYYY : function () { + return leftZeroFill(this.year(), 4); + }, + a : function () { + return this.lang().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.lang().meridiem(this.hours(), this.minutes(), false); + }, + H : function () { + return this.hours(); + }, + h : function () { + return this.hours() % 12 || 12; + }, + m : function () { + return this.minutes(); + }, + s : function () { + return this.seconds(); + }, + S : function () { + return ~~(this.milliseconds() / 100); + }, + SS : function () { + return leftZeroFill(~~(this.milliseconds() / 10), 2); + }, + SSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + Z : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2); + }, + ZZ : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(~~(10 * a / 6), 4); + } + }; + + function getValueFromArray(key, index, m, format) { + var lang = m.lang(); + return lang[key].call ? lang[key](m, format) : lang[key][index]; + } + + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; + } + function ordinalizeToken(func) { + return function (a) { + var b = func.call(this, a); + return b + this.lang().ordinal(b); + }; + } + + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + + + /************************************ + Constructors + ************************************/ + + + // Moment prototype object + function Moment(date, isUTC, lang) { + this._d = date; + this._isUTC = !!isUTC; + this._a = date._a || null; + this._lang = lang || false; + } + + // Duration Constructor + function Duration(duration) { + var data = this._data = {}, + years = duration.years || duration.y || 0, + months = duration.months || duration.M || 0, + weeks = duration.weeks || duration.w || 0, + days = duration.days || duration.d || 0, + hours = duration.hours || duration.h || 0, + minutes = duration.minutes || duration.m || 0, + seconds = duration.seconds || duration.s || 0, + milliseconds = duration.milliseconds || duration.ms || 0; + + // representation for dateAddRemove + this._milliseconds = milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = months + + years * 12; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + seconds += absRound(milliseconds / 1000); + + data.seconds = seconds % 60; + minutes += absRound(seconds / 60); + + data.minutes = minutes % 60; + hours += absRound(minutes / 60); + + data.hours = hours % 24; + days += absRound(hours / 24); + + days += weeks * 7; + data.days = days % 30; + + months += absRound(days / 30); + + data.months = months % 12; + years += absRound(months / 12); + + data.years = years; + + this._lang = false; + } + + + /************************************ + Helpers + ************************************/ + + + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } + + // left zero fill a number + // see http://jsperf.com/left-zero-filling for performance comparison + function leftZeroFill(number, targetLength) { + var output = number + ''; + while (output.length < targetLength) { + output = '0' + output; + } + return output; + } + + // helper function for _.addTime and _.subtractTime + function addOrSubtractDurationFromMoment(mom, duration, isAdding) { + var ms = duration._milliseconds, + d = duration._days, + M = duration._months, + currentDate; + + if (ms) { + mom._d.setTime(+mom + ms * isAdding); + } + if (d) { + mom.date(mom.date() + d * isAdding); + } + if (M) { + currentDate = mom.date(); + mom.date(1) + .month(mom.month() + M * isAdding) + .date(Math.min(currentDate, mom.daysInMonth())); + } + } + + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } + + // compare two arrays, return the number of differences + function compareArrays(array1, array2) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if (~~array1[i] !== ~~array2[i]) { + diffs++; + } + } + return diffs + lengthDiff; + } + + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function dateFromArray(input, asUTC, hoursOffset, minutesOffset) { + var i, date, forValid = []; + for (i = 0; i < 7; i++) { + forValid[i] = input[i] = (input[i] == null) ? (i === 2 ? 1 : 0) : input[i]; + } + // we store whether we used utc or not in the input array + input[7] = forValid[7] = asUTC; + // if the parser flagged the input as invalid, we pass the value along + if (input[8] != null) { + forValid[8] = input[8]; + } + // add the offsets to the time to be parsed so that we can have a clean array + // for checking isValid + input[3] += hoursOffset || 0; + input[4] += minutesOffset || 0; + date = new Date(0); + if (asUTC) { + date.setUTCFullYear(input[0], input[1], input[2]); + date.setUTCHours(input[3], input[4], input[5], input[6]); + } else { + date.setFullYear(input[0], input[1], input[2]); + date.setHours(input[3], input[4], input[5], input[6]); + } + date._a = forValid; + return date; + } + + // Loads a language definition into the `languages` cache. The function + // takes a key and optionally values. If not in the browser and no values + // are provided, it will load the language file module. As a convenience, + // this function also returns the language values. + function loadLang(key, values) { + var i, m, + parse = []; + + if (!values && hasModule) { + values = require('./lang/' + key); + } + + for (i = 0; i < langConfigProperties.length; i++) { + // If a language definition does not provide a value, inherit + // from English + values[langConfigProperties[i]] = values[langConfigProperties[i]] || + languages.en[langConfigProperties[i]]; + } + + for (i = 0; i < 12; i++) { + m = moment([2000, i]); + parse[i] = new RegExp('^' + (values.months[i] || values.months(m, '')) + + '|^' + (values.monthsShort[i] || values.monthsShort(m, '')).replace('.', ''), 'i'); + } + values.monthsParse = values.monthsParse || parse; + + languages[key] = values; + + return values; + } + + // Determines which language definition to use and returns it. + // + // With no parameters, it will return the global language. If you + // pass in a language key, such as 'en', it will return the + // definition for 'en', so long as 'en' has already been loaded using + // moment.lang. If you pass in a moment or duration instance, it + // will decide the language based on that, or default to the global + // language. + function getLangDefinition(m) { + var langKey = (typeof m === 'string') && m || + m && m._lang || + null; + + return langKey ? (languages[langKey] || loadLang(langKey)) : moment; + } + + + /************************************ + Formatting + ************************************/ + + + function removeFormattingTokens(input) { + if (input.match(/\[.*\]/)) { + return input.replace(/^\[|\]$/g, ""); + } + return input.replace(/\\/g, ""); + } + + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = ""; + for (i = 0; i < length; i++) { + output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i]; + } + return output; + }; + } + + // format date using native date object + function formatMoment(m, format) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return m.lang().longDateFormat[input] || input; + } + + while (i-- && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + } + + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } + + return formatFunctions[format](m); + } + + + /************************************ + Parsing + ************************************/ + + + // get the regex to find the next token + function getParseRegexForToken(token) { + switch (token) { + case 'DDDD': + return parseTokenThreeDigits; + case 'YYYY': + return parseTokenFourDigits; + case 'S': + case 'SS': + case 'SSS': + case 'DDD': + return parseTokenOneToThreeDigits; + case 'MMM': + case 'MMMM': + case 'dd': + case 'ddd': + case 'dddd': + case 'a': + case 'A': + return parseTokenWord; + case 'Z': + case 'ZZ': + return parseTokenTimezone; + case 'T': + return parseTokenT; + case 'MM': + case 'DD': + case 'YY': + case 'HH': + case 'hh': + case 'mm': + case 'ss': + case 'M': + case 'D': + case 'd': + case 'H': + case 'h': + case 'm': + case 's': + return parseTokenOneOrTwoDigits; + default : + return new RegExp(token.replace('\\', '')); + } + } + + // function to convert string input to date + function addTimeToArrayFromToken(token, input, datePartArray, config) { + var a, b; + + switch (token) { + // MONTH + case 'M' : // fall through to MM + case 'MM' : + datePartArray[1] = (input == null) ? 0 : ~~input - 1; + break; + case 'MMM' : // fall through to MMMM + case 'MMMM' : + for (a = 0; a < 12; a++) { + if (getLangDefinition().monthsParse[a].test(input)) { + datePartArray[1] = a; + b = true; + break; + } + } + // if we didn't find a month name, mark the date as invalid. + if (!b) { + datePartArray[8] = false; + } + break; + // DAY OF MONTH + case 'D' : // fall through to DDDD + case 'DD' : // fall through to DDDD + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + datePartArray[2] = ~~input; + } + break; + // YEAR + case 'YY' : + datePartArray[0] = ~~input + (~~input > 70 ? 1900 : 2000); + break; + case 'YYYY' : + datePartArray[0] = ~~Math.abs(input); + break; + // AM / PM + case 'a' : // fall through to A + case 'A' : + config.isPm = ((input + '').toLowerCase() === 'pm'); + break; + // 24 HOUR + case 'H' : // fall through to hh + case 'HH' : // fall through to hh + case 'h' : // fall through to hh + case 'hh' : + datePartArray[3] = ~~input; + break; + // MINUTE + case 'm' : // fall through to mm + case 'mm' : + datePartArray[4] = ~~input; + break; + // SECOND + case 's' : // fall through to ss + case 'ss' : + datePartArray[5] = ~~input; + break; + // MILLISECOND + case 'S' : + case 'SS' : + case 'SSS' : + datePartArray[6] = ~~ (('0.' + input) * 1000); + break; + // TIMEZONE + case 'Z' : // fall through to ZZ + case 'ZZ' : + config.isUTC = true; + a = (input + '').match(parseTimezoneChunker); + if (a && a[1]) { + config.tzh = ~~a[1]; + } + if (a && a[2]) { + config.tzm = ~~a[2]; + } + // reverse offsets + if (a && a[0] === '+') { + config.tzh = -config.tzh; + config.tzm = -config.tzm; + } + break; + } + + // if the input is null, the date is not valid + if (input == null) { + datePartArray[8] = false; + } + } + + // date from string and format string + function makeDateFromStringAndFormat(string, format) { + // This array is used to make a Date, either with `new Date` or `Date.UTC` + // We store some additional data on the array for validation + // datePartArray[7] is true if the Date was created with `Date.UTC` and false if created with `new Date` + // datePartArray[8] is false if the Date is invalid, and undefined if the validity is unknown. + var datePartArray = [0, 0, 1, 0, 0, 0, 0], + config = { + tzh : 0, // timezone hour offset + tzm : 0 // timezone minute offset + }, + tokens = format.match(formattingTokens), + i, parsedInput; + + for (i = 0; i < tokens.length; i++) { + parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0]; + if (parsedInput) { + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + } + // don't parse if its not a known token + if (formatTokenFunctions[tokens[i]]) { + addTimeToArrayFromToken(tokens[i], parsedInput, datePartArray, config); + } + } + // handle am pm + if (config.isPm && datePartArray[3] < 12) { + datePartArray[3] += 12; + } + // if is 12 am, change hours to 0 + if (config.isPm === false && datePartArray[3] === 12) { + datePartArray[3] = 0; + } + // return + return dateFromArray(datePartArray, config.isUTC, config.tzh, config.tzm); + } + + // date from string and array of format strings + function makeDateFromStringAndArray(string, formats) { + var output, + inputParts = string.match(parseMultipleFormatChunker) || [], + formattedInputParts, + scoreToBeat = 99, + i, + currentDate, + currentScore; + for (i = 0; i < formats.length; i++) { + currentDate = makeDateFromStringAndFormat(string, formats[i]); + formattedInputParts = formatMoment(new Moment(currentDate), formats[i]).match(parseMultipleFormatChunker) || []; + currentScore = compareArrays(inputParts, formattedInputParts); + if (currentScore < scoreToBeat) { + scoreToBeat = currentScore; + output = currentDate; + } + } + return output; + } + + // date from iso format + function makeDateFromString(string) { + var format = 'YYYY-MM-DDT', + i; + if (isoRegex.exec(string)) { + for (i = 0; i < 4; i++) { + if (isoTimes[i][1].exec(string)) { + format += isoTimes[i][0]; + break; + } + } + return parseTokenTimezone.exec(string) ? + makeDateFromStringAndFormat(string, format + ' Z') : + makeDateFromStringAndFormat(string, format); + } + return new Date(string); + } + + + /************************************ + Relative Time + ************************************/ + + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { + var rt = lang.relativeTime[string]; + return (typeof rt === 'function') ? + rt(number || 1, !!withoutSuffix, string, isFuture) : + rt.replace(/%d/i, number || 1); + } + + function relativeTime(milliseconds, withoutSuffix, lang) { + var seconds = round(Math.abs(milliseconds) / 1000), + minutes = round(seconds / 60), + hours = round(minutes / 60), + days = round(hours / 24), + years = round(days / 365), + args = seconds < 45 && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < 45 && ['mm', minutes] || + hours === 1 && ['h'] || + hours < 22 && ['hh', hours] || + days === 1 && ['d'] || + days <= 25 && ['dd', days] || + days <= 45 && ['M'] || + days < 345 && ['MM', round(days / 30)] || + years === 1 && ['y'] || ['yy', years]; + args[2] = withoutSuffix; + args[3] = milliseconds > 0; + args[4] = lang; + return substituteTimeAgo.apply({}, args); + } + + + /************************************ + Top Level Functions + ************************************/ + + + moment = function (input, format) { + if (input === null || input === '') { + return null; + } + var date, + matched; + // parse Moment object + if (moment.isMoment(input)) { + return new Moment(new Date(+input._d), input._isUTC, input._lang); + // parse string and format + } else if (format) { + if (isArray(format)) { + date = makeDateFromStringAndArray(input, format); + } else { + date = makeDateFromStringAndFormat(input, format); + } + // evaluate it as a JSON-encoded date + } else { + matched = aspNetJsonRegex.exec(input); + date = input === undefined ? new Date() : + matched ? new Date(+matched[1]) : + input instanceof Date ? input : + isArray(input) ? dateFromArray(input) : + typeof input === 'string' ? makeDateFromString(input) : + new Date(input); + } + + return new Moment(date); + }; + + // creating with utc + moment.utc = function (input, format) { + if (isArray(input)) { + return new Moment(dateFromArray(input, true), true); + } + // if we don't have a timezone, we need to add one to trigger parsing into utc + if (typeof input === 'string' && !parseTokenTimezone.exec(input)) { + input += ' +0000'; + if (format) { + format += ' Z'; + } + } + return moment(input, format).utc(); + }; + + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; + + // duration + moment.duration = function (input, key) { + var isDuration = moment.isDuration(input), + isNumber = (typeof input === 'number'), + duration = (isDuration ? input._data : (isNumber ? {} : input)), + ret; + + if (isNumber) { + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } + + ret = new Duration(duration); + + if (isDuration) { + ret._lang = input._lang; + } + + return ret; + }; + + // humanizeDuration + // This method is deprecated in favor of the new Duration object. Please + // see the moment.duration method. + moment.humanizeDuration = function (num, type, withSuffix) { + return moment.duration(num, type === true ? null : type).humanize(type === true ? true : withSuffix); + }; + + // version number + moment.version = VERSION; + + // default format + moment.defaultFormat = isoFormat; + + // This function will load languages and then set the global language. If + // no arguments are passed in, it will simply return the current global + // language key. + moment.lang = function (key, values) { + var i; + + if (!key) { + return currentLanguage; + } + if (values || !languages[key]) { + loadLang(key, values); + } + if (languages[key]) { + // deprecated, to get the language definition variables, use the + // moment.fn.lang method or the getLangDefinition function. + for (i = 0; i < langConfigProperties.length; i++) { + moment[langConfigProperties[i]] = languages[key][langConfigProperties[i]]; + } + moment.monthsParse = languages[key].monthsParse; + currentLanguage = key; + } + }; + + // returns language data + moment.langData = getLangDefinition; + + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment; + }; + + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; + + // Set default language, other languages will inherit from English. + moment.lang('en', { + months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), + monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), + weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), + weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), + weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), + longDateFormat : { + LT : "h:mm A", + L : "MM/DD/YYYY", + LL : "MMMM D YYYY", + LLL : "MMMM D YYYY LT", + LLLL : "dddd, MMMM D YYYY LT" + }, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, + calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[last] dddd [at] LT', + sameElse : 'L' + }, + relativeTime : { + future : "in %s", + past : "%s ago", + s : "a few seconds", + m : "a minute", + mm : "%d minutes", + h : "an hour", + hh : "%d hours", + d : "a day", + dd : "%d days", + M : "a month", + MM : "%d months", + y : "a year", + yy : "%d years" + }, + ordinal : function (number) { + var b = number % 10; + return (~~ (number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + } + }); + + + /************************************ + Moment Prototype + ************************************/ + + + moment.fn = Moment.prototype = { + + clone : function () { + return moment(this); + }, + + valueOf : function () { + return +this._d; + }, + + unix : function () { + return Math.floor(+this._d / 1000); + }, + + toString : function () { + return this._d.toString(); + }, + + toDate : function () { + return this._d; + }, + + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds(), + !!this._isUTC + ]; + }, + + isValid : function () { + if (this._a) { + // if the parser finds that the input is invalid, it sets + // the eighth item in the input array to false. + if (this._a[8] != null) { + return !!this._a[8]; + } + return !compareArrays(this._a, (this._a[7] ? moment.utc(this._a) : moment(this._a)).toArray()); + } + return !isNaN(this._d.getTime()); + }, + + utc : function () { + this._isUTC = true; + return this; + }, + + local : function () { + this._isUTC = false; + return this; + }, + + format : function (inputString) { + return formatMoment(this, inputString ? inputString : moment.defaultFormat); + }, + + add : function (input, val) { + var dur = val ? moment.duration(+val, input) : moment.duration(input); + addOrSubtractDurationFromMoment(this, dur, 1); + return this; + }, + + subtract : function (input, val) { + var dur = val ? moment.duration(+val, input) : moment.duration(input); + addOrSubtractDurationFromMoment(this, dur, -1); + return this; + }, + + diff : function (input, val, asFloat) { + var inputMoment = this._isUTC ? moment(input).utc() : moment(input).local(), + zoneDiff = (this.zone() - inputMoment.zone()) * 6e4, + diff = this._d - inputMoment._d - zoneDiff, + year = this.year() - inputMoment.year(), + month = this.month() - inputMoment.month(), + date = this.date() - inputMoment.date(), + output; + if (val === 'months') { + output = year * 12 + month + date / 30; + } else if (val === 'years') { + output = year + (month + date / 30) / 12; + } else { + output = val === 'seconds' ? diff / 1e3 : // 1000 + val === 'minutes' ? diff / 6e4 : // 1000 * 60 + val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60 + val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24 + val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 + diff; + } + return asFloat ? output : round(output); + }, + + from : function (time, withoutSuffix) { + return moment.duration(this.diff(time)).lang(this._lang).humanize(!withoutSuffix); + }, + + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + + calendar : function () { + var diff = this.diff(moment().sod(), 'days', true), + calendar = this.lang().calendar, + allElse = calendar.sameElse, + format = diff < -6 ? allElse : + diff < -1 ? calendar.lastWeek : + diff < 0 ? calendar.lastDay : + diff < 1 ? calendar.sameDay : + diff < 2 ? calendar.nextDay : + diff < 7 ? calendar.nextWeek : allElse; + return this.format(typeof format === 'function' ? format.apply(this) : format); + }, + + isLeapYear : function () { + var year = this.year(); + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + }, + + isDST : function () { + return (this.zone() < moment([this.year()]).zone() || + this.zone() < moment([this.year(), 5]).zone()); + }, + + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + return input == null ? day : + this.add({ d : input - day }); + }, + + startOf: function (val) { + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (val.replace(/s$/, '')) { + case 'year': + this.month(0); + /* falls through */ + case 'month': + this.date(1); + /* falls through */ + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } + return this; + }, + + endOf: function (val) { + return this.startOf(val).add(val.replace(/s?$/, 's'), 1).subtract('ms', 1); + }, + + sod: function () { + return this.clone().startOf('day'); + }, + + eod: function () { + // end of day = start of day plus 1 day, minus 1 millisecond + return this.clone().endOf('day'); + }, + + zone : function () { + return this._isUTC ? 0 : this._d.getTimezoneOffset(); + }, + + daysInMonth : function () { + return moment.utc([this.year(), this.month() + 1, 0]).date(); + }, + + // If passed a language key, it will set the language for this + // instance. Otherwise, it will return the language configuration + // variables for this instance. + lang : function (lang) { + if (lang === undefined) { + return getLangDefinition(this); + } else { + this._lang = lang; + return this; + } + } + }; + + // helper for adding shortcuts + function makeGetterAndSetter(name, key) { + moment.fn[name] = function (input) { + var utc = this._isUTC ? 'UTC' : ''; + if (input != null) { + this._d['set' + utc + key](input); + return this; + } else { + return this._d['get' + utc + key](); + } + }; + } + + // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) + for (i = 0; i < proxyGettersAndSetters.length; i ++) { + makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase(), proxyGettersAndSetters[i]); + } + + // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') + makeGetterAndSetter('year', 'FullYear'); + + + /************************************ + Duration Prototype + ************************************/ + + + moment.duration.fn = Duration.prototype = { + weeks : function () { + return absRound(this.days() / 7); + }, + + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + this._months * 2592e6; + }, + + humanize : function (withSuffix) { + var difference = +this, + rel = this.lang().relativeTime, + output = relativeTime(difference, !withSuffix, this.lang()), + fromNow = difference <= 0 ? rel.past : rel.future; + + if (withSuffix) { + if (typeof fromNow === 'function') { + output = fromNow(output); + } else { + output = fromNow.replace(/%s/i, output); + } + } + + return output; + }, + + lang : moment.fn.lang + }; + + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } + + function makeDurationAsGetter(name, factor) { + moment.duration.fn['as' + name] = function () { + return +this / factor; + }; + } + + for (i in unitMillisecondFactors) { + if (unitMillisecondFactors.hasOwnProperty(i)) { + makeDurationAsGetter(i, unitMillisecondFactors[i]); + makeDurationGetter(i.toLowerCase()); + } + } + + makeDurationAsGetter('Weeks', 6048e5); + + + /************************************ + Exposing Moment + ************************************/ + + + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + } + /*global ender:false */ + if (typeof ender === 'undefined') { + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + this['moment'] = moment; + } + /*global define:false */ + if (typeof define === "function" && define.amd) { + define("moment", [], function () { + return moment; + }); + } +}).call(this);