d3 Tutorial: Coordinated Views
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if(!d3.chart) d3.chart = {}; | |
d3.chart.brush = function() { | |
var g; | |
var data; | |
var width = 600; | |
var height = 30; | |
var dispatch = d3.dispatch(chart, "filter"); | |
function chart(container) { | |
g = container; | |
var extent = d3.extent(data, function(d) { | |
return d.created | |
}) | |
var scale = d3.time.scale() | |
.domain(extent) | |
.range([0, width]) | |
var brush = d3.svg.brush() | |
brush.x(scale) | |
brush(g) | |
g.selectAll("rect").attr("height", height) | |
g.selectAll(".background") | |
.style({fill: "#fff", visibility: "visible"}) | |
g.selectAll(".extent") | |
.style({fill: "#ddd", visibility: "visible"}) | |
g.selectAll(".resize rect") | |
.style({fill: "#000", visibility: "visible"}) | |
var rects = g.selectAll("rect.events") | |
.data(data) | |
rects.enter() | |
.append("rect").classed("events", true) | |
rects.attr({ | |
x: function(d) { return scale(d.created);}, | |
y: 0, | |
width: 2, | |
height: height | |
}).style("pointer-events", "none") | |
.style("fill", function(d) { return d.color }) | |
rects.exit().remove() | |
brush.on("brushend", function() { | |
var ext = brush.extent() | |
var filtered = data.filter(function(d) { | |
return (d.created > ext[0] && d.created < ext[1]) | |
}) | |
g.selectAll("rect.events") | |
.style("stroke", "") | |
g.selectAll("rect.events") | |
.data(filtered, function(d) { return d.id }) | |
.style({ | |
stroke: "#999" | |
}) | |
//emit filtered data | |
dispatch.filter(filtered) | |
}) | |
var axis = d3.svg.axis() | |
.scale(scale) | |
.orient("bottom") | |
.tickValues([new Date(extent[0]), new Date(extent[0] + (extent[1] - extent[0])/2) , new Date(extent[1])]) | |
.tickFormat(d3.time.format("%x %H:%M")) | |
var agroup = g.append("g") | |
agroup.attr("transform", "translate(" + [0, height] + ")") | |
axis(agroup) | |
agroup.selectAll("path") | |
.style({ fill: "none", stroke: "#000"}) | |
agroup.selectAll("line") | |
.style({ stroke: "#000"}) | |
} | |
chart.highlight = function(data) { | |
var rects = g.selectAll("rect.events") | |
.style("stroke", "") | |
.style("stroke-width", "") | |
rects.data(data, function(d) { return d.id }) | |
.style("stroke", "black") | |
.style("stroke-width", 1) | |
} | |
chart.data = function(value) { | |
if(!arguments.length) return data; | |
data = value; | |
return chart; | |
} | |
chart.width = function(value) { | |
if(!arguments.length) return width; | |
width = value; | |
return chart; | |
} | |
chart.height = function(value) { | |
if(!arguments.length) return height; | |
height = value; | |
return chart; | |
} | |
return d3.rebind(chart, dispatch, "on"); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<html> | |
<head> | |
<style> | |
.axis { | |
font-size: 10px; | |
} | |
.axis path { | |
fill: none; | |
stroke: #000000; | |
} | |
.axis .tick line { | |
stroke: #000; | |
} | |
.states { | |
fill: #ccc; | |
stroke: #fff; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="http://d3js.org/d3.v3.js"></script> | |
<script src="scatter.js"></script> | |
<script src="brush.js"></script> | |
<script src="symbol_map.js"></script> | |
<script src="miso.ds.deps.0.4.1.js"></script> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script src="http://d3js.org/queue.v1.min.js"></script> | |
<script> | |
var Vis = {}; | |
Vis.data = new Array(); | |
//http://bl.ocks.org/mbostock/4342045 | |
var width = 960, | |
height = 500; | |
var data; | |
var SPREADSHEET_ID = "0Agy_OAo_DS4PdFlrdVR1X2Nub1U3VnE0X0xBemI1eUE"; | |
var SPREADSHEET_TAB = "1"; | |
var json_file = "pics5.json"; | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
var mapgroup = svg.append("g") | |
; | |
//Define map projection | |
var projection = d3.geo.albersUsa() | |
.translate([700, 230]) | |
.scale([500]); | |
var path = d3.geo.path() | |
.projection(projection); | |
queue() | |
.defer(d3.json, "us.json") | |
.await(ready); | |
function ready(error, us) { | |
mapgroup.append("path") | |
.attr("class", "states") | |
.datum(topojson.feature(us, us.objects.states)) | |
.attr("d", path); | |
loadJson(json_file); | |
//loadGSS(SPREADSHEET_ID, SPREADSHEET_TAB); | |
} | |
function createDiagram() { | |
var sgroup = svg.append("g") | |
.attr("transform", "translate(50, 0)") | |
var scatter = d3.chart.scatter() | |
scatter.data(Vis.data) | |
scatter(sgroup) | |
var symbol_map = d3.chart.symbol_map() | |
symbol_map.data(Vis.data) | |
symbol_map(mapgroup) | |
//brush | |
var bgroup = svg.append("g") | |
.attr("transform", "translate(100, 430)") | |
var brush = d3.chart.brush() | |
brush | |
.data(Vis.data) | |
.width(800) | |
brush(bgroup) | |
brush.on("filter", function(filtered) { | |
scatter.data(filtered); | |
scatter.update(); | |
symbol_map.data(filtered) | |
symbol_map.update(); | |
}) | |
scatter.on("hover", function(hovered) { | |
brush.highlight(hovered) | |
symbol_map.highlight(hovered) | |
}) | |
symbol_map.on("hover", function(hovered) { | |
scatter.highlight(hovered) | |
brush.highlight(hovered) | |
}) | |
} | |
function loadJson(file_name) { | |
d3.json(file_name, function(error, file_data) { | |
Vis.data = file_data; | |
Vis.data.forEach(function(d) { | |
d.created *= 1000; | |
}); | |
createDiagram(); | |
}); | |
} | |
function loadGSS(ssid, tab) { | |
//connect to spreadsheet | |
var lsheet = new Miso.Dataset({ | |
importer : Miso.Dataset.Importers.GoogleSpreadsheet, | |
parser : Miso.Dataset.Parsers.GoogleSpreadsheet, | |
key : ssid, | |
worksheet : tab | |
}); | |
//load data from spreadsheet | |
lsheet.fetch({ | |
success : function() { | |
this.each(function(row){ | |
Vis.data.push(row); | |
}); | |
Vis.data.forEach(function(d) { | |
d.created *= 1000; | |
}); | |
createDiagram(); | |
}, | |
error : function() { | |
console.log("Data failed to load from GSS."); | |
} | |
}); | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Miso.Dataset - v0.4.1 - 3/13/2013 | |
* http://github.com/misoproject/dataset | |
* Copyright (c) 2013 Alex Graul, Irene Ros; | |
* Dual Licensed: MIT, GPL | |
* https://github.com/misoproject/dataset/blob/master/LICENSE-MIT | |
* https://github.com/misoproject/dataset/blob/master/LICENSE-GPL | |
*/ | |
// 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); | |
/*! | |
* Lo-Dash v0.9.0 <http://lodash.com> | |
* (c) 2012 John-David Dalton <http://allyoucanleet.com/> | |
* Based on Underscore.js 1.4.2 <http://underscorejs.org> | |
* (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. | |
* Available under MIT license <http://lodash.com/license> | |
*/ | |
;(function(window, undefined) { | |
/** Detect free variable `exports` */ | |
var freeExports = typeof exports == 'object' && exports; | |
/** Detect free variable `global` and use it as `window` */ | |
var freeGlobal = typeof global == 'object' && global; | |
if (freeGlobal.global === freeGlobal) { | |
window = freeGlobal; | |
} | |
/** Used for array and object method references */ | |
var arrayRef = [], | |
objectRef = {}; | |
/** Used to generate unique IDs */ | |
var idCounter = 0; | |
/** Used internally to indicate various things */ | |
var indicatorObject = {}; | |
/** Used by `cachedContains` as the default size when optimizations are enabled for large arrays */ | |
var largeArraySize = 30; | |
/** Used to restore the original `_` reference in `noConflict` */ | |
var oldDash = window._; | |
/** Used to detect delimiter values that should be processed by `tokenizeEvaluate` */ | |
var reComplexDelimiter = /[-?+=!~*%&^<>|{(\/]|\[\D|\b(?:delete|in|instanceof|new|typeof|void)\b/; | |
/** Used to match HTML entities */ | |
var reEscapedHtml = /&(?:amp|lt|gt|quot|#x27);/g; | |
/** Used to match empty string literals in compiled template source */ | |
var reEmptyStringLeading = /\b__p \+= '';/g, | |
reEmptyStringMiddle = /\b(__p \+=) '' \+/g, | |
reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; | |
/** Used to match regexp flags from their coerced string values */ | |
var reFlags = /\w*$/; | |
/** Used to insert the data object variable into compiled template source */ | |
var reInsertVariable = /(?:__e|__t = )\(\s*(?![\d\s"']|this\.)/g; | |
/** Used to detect if a method is native */ | |
var reNative = RegExp('^' + | |
(objectRef.valueOf + '') | |
.replace(/[.*+?^=!:${}()|[\]\/\\]/g, '\\$&') | |
.replace(/valueOf|for [^\]]+/g, '.+?') + '$' | |
); | |
/** Used to ensure capturing order and avoid matches for undefined delimiters */ | |
var reNoMatch = /($^)/; | |
/** Used to match HTML characters */ | |
var reUnescapedHtml = /[&<>"']/g; | |
/** Used to match unescaped characters in compiled string literals */ | |
var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; | |
/** Used to fix the JScript [[DontEnum]] bug */ | |
var shadowed = [ | |
'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', | |
'toLocaleString', 'toString', 'valueOf' | |
]; | |
/** Used to make template sourceURLs easier to identify */ | |
var templateCounter = 0; | |
/** Native method shortcuts */ | |
var ceil = Math.ceil, | |
concat = arrayRef.concat, | |
floor = Math.floor, | |
getPrototypeOf = reNative.test(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf, | |
hasOwnProperty = objectRef.hasOwnProperty, | |
push = arrayRef.push, | |
propertyIsEnumerable = objectRef.propertyIsEnumerable, | |
slice = arrayRef.slice, | |
toString = objectRef.toString; | |
/* Native method shortcuts for methods with the same name as other `lodash` methods */ | |
var nativeBind = reNative.test(nativeBind = slice.bind) && nativeBind, | |
nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, | |
nativeIsFinite = window.isFinite, | |
nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys, | |
nativeMax = Math.max, | |
nativeMin = Math.min, | |
nativeRandom = Math.random; | |
/** `Object#toString` result shortcuts */ | |
var argsClass = '[object Arguments]', | |
arrayClass = '[object Array]', | |
boolClass = '[object Boolean]', | |
dateClass = '[object Date]', | |
funcClass = '[object Function]', | |
numberClass = '[object Number]', | |
objectClass = '[object Object]', | |
regexpClass = '[object RegExp]', | |
stringClass = '[object String]'; | |
/** | |
* Detect the JScript [[DontEnum]] bug: | |
* | |
* In IE < 9 an objects own properties, shadowing non-enumerable ones, are | |
* made non-enumerable as well. | |
*/ | |
var hasDontEnumBug; | |
/** Detect if own properties are iterated after inherited properties (IE < 9) */ | |
var iteratesOwnLast; | |
/** | |
* Detect if `Array#shift` and `Array#splice` augment array-like objects | |
* incorrectly: | |
* | |
* Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` | |
* and `splice()` functions that fail to remove the last element, `value[0]`, | |
* of array-like objects even though the `length` property is set to `0`. | |
* The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` | |
* is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. | |
*/ | |
var hasObjectSpliceBug = (hasObjectSpliceBug = { '0': 1, 'length': 1 }, | |
arrayRef.splice.call(hasObjectSpliceBug, 0, 1), hasObjectSpliceBug[0]); | |
/** Detect if an `arguments` object's indexes are non-enumerable (IE < 9) */ | |
var noArgsEnum = true; | |
(function() { | |
var props = []; | |
function ctor() { this.x = 1; } | |
ctor.prototype = { 'valueOf': 1, 'y': 1 }; | |
for (var prop in new ctor) { props.push(prop); } | |
for (prop in arguments) { noArgsEnum = !prop; } | |
hasDontEnumBug = !/valueOf/.test(props); | |
iteratesOwnLast = props[0] != 'x'; | |
}(1)); | |
/** Detect if an `arguments` object's [[Class]] is unresolvable (Firefox < 4, IE < 9) */ | |
var noArgsClass = !isArguments(arguments); | |
/** Detect if `Array#slice` cannot be used to convert strings to arrays (Opera < 10.52) */ | |
var noArraySliceOnStrings = slice.call('x')[0] != 'x'; | |
/** | |
* Detect lack of support for accessing string characters by index: | |
* | |
* IE < 8 can't access characters by index and IE 8 can only access | |
* characters by index on string literals. | |
*/ | |
var noCharByIndex = ('x'[0] + Object('x')[0]) != 'xx'; | |
/** | |
* Detect if a node's [[Class]] is unresolvable (IE < 9) | |
* and that the JS engine won't error when attempting to coerce an object to | |
* a string without a `toString` property value of `typeof` "function". | |
*/ | |
try { | |
var noNodeClass = ({ 'toString': 0 } + '', toString.call(window.document || 0) == objectClass); | |
} catch(e) { } | |
/* Detect if `Function#bind` exists and is inferred to be fast (all but V8) */ | |
var isBindFast = nativeBind && /\n|Opera/.test(nativeBind + toString.call(window.opera)); | |
/* Detect if `Object.keys` exists and is inferred to be fast (IE, Opera, V8) */ | |
var isKeysFast = nativeKeys && /^.+$|true/.test(nativeKeys + !!window.attachEvent); | |
/** | |
* Detect if sourceURL syntax is usable without erroring: | |
* | |
* The JS engine in Adobe products, like InDesign, will throw a syntax error | |
* when it encounters a single line comment beginning with the `@` symbol. | |
* | |
* The JS engine in Narwhal will generate the function `function anonymous(){//}` | |
* and throw a syntax error. | |
* | |
* Avoid comments beginning `@` symbols in IE because they are part of its | |
* non-standard conditional compilation support. | |
* http://msdn.microsoft.com/en-us/library/121hztk3(v=vs.94).aspx | |
*/ | |
try { | |
var useSourceURL = (Function('//@')(), !window.attachEvent); | |
} catch(e) { } | |
/** Used to identify object classifications that `_.clone` supports */ | |
var cloneableClasses = {}; | |
cloneableClasses[argsClass] = cloneableClasses[funcClass] = false; | |
cloneableClasses[arrayClass] = cloneableClasses[boolClass] = cloneableClasses[dateClass] = | |
cloneableClasses[numberClass] = cloneableClasses[objectClass] = cloneableClasses[regexpClass] = | |
cloneableClasses[stringClass] = true; | |
/** Used to determine if values are of the language type Object */ | |
var objectTypes = { | |
'boolean': false, | |
'function': true, | |
'object': true, | |
'number': false, | |
'string': false, | |
'undefined': false | |
}; | |
/** Used to escape characters for inclusion in compiled string literals */ | |
var stringEscapes = { | |
'\\': '\\', | |
"'": "'", | |
'\n': 'n', | |
'\r': 'r', | |
'\t': 't', | |
'\u2028': 'u2028', | |
'\u2029': 'u2029' | |
}; | |
/*--------------------------------------------------------------------------*/ | |
/** | |
* The `lodash` function. | |
* | |
* @name _ | |
* @constructor | |
* @category Chaining | |
* @param {Mixed} value The value to wrap in a `lodash` instance. | |
* @returns {Object} Returns a `lodash` instance. | |
*/ | |
function lodash(value) { | |
// exit early if already wrapped | |
if (value && value.__wrapped__) { | |
return value; | |
} | |
// allow invoking `lodash` without the `new` operator | |
if (!(this instanceof lodash)) { | |
return new lodash(value); | |
} | |
this.__wrapped__ = value; | |
} | |
/** | |
* By default, the template delimiters used by Lo-Dash are similar to those in | |
* embedded Ruby (ERB). Change the following template settings to use alternative | |
* delimiters. | |
* | |
* @static | |
* @memberOf _ | |
* @type Object | |
*/ | |
lodash.templateSettings = { | |
/** | |
* Used to detect `data` property values to be HTML-escaped. | |
* | |
* @static | |
* @memberOf _.templateSettings | |
* @type RegExp | |
*/ | |
'escape': /<%-([\s\S]+?)%>/g, | |
/** | |
* Used to detect code to be evaluated. | |
* | |
* @static | |
* @memberOf _.templateSettings | |
* @type RegExp | |
*/ | |
'evaluate': /<%([\s\S]+?)%>/g, | |
/** | |
* Used to detect `data` property values to inject. | |
* | |
* @static | |
* @memberOf _.templateSettings | |
* @type RegExp | |
*/ | |
'interpolate': /<%=([\s\S]+?)%>/g, | |
/** | |
* Used to reference the data object in the template text. | |
* | |
* @static | |
* @memberOf _.templateSettings | |
* @type String | |
*/ | |
'variable': '' | |
}; | |
/*--------------------------------------------------------------------------*/ | |
/** | |
* The template used to create iterator functions. | |
* | |
* @private | |
* @param {Obect} data The data object used to populate the text. | |
* @returns {String} Returns the interpolated text. | |
*/ | |
var iteratorTemplate = template( | |
// conditional strict mode | |
'<% if (obj.useStrict) { %>\'use strict\';\n<% } %>' + | |
// the `iteratee` may be reassigned by the `top` snippet | |
'var index, value, iteratee = <%= firstArg %>, ' + | |
// assign the `result` variable an initial value | |
'result = <%= firstArg %>;\n' + | |
// exit early if the first argument is falsey | |
'if (!<%= firstArg %>) return result;\n' + | |
// add code before the iteration branches | |
'<%= top %>;\n' + | |
// array-like iteration: | |
'<% if (arrayLoop) { %>' + | |
'var length = iteratee.length; index = -1;\n' + | |
'if (typeof length == \'number\') {' + | |
// add support for accessing string characters by index if needed | |
' <% if (noCharByIndex) { %>\n' + | |
' if (toString.call(iteratee) == stringClass) {\n' + | |
' iteratee = iteratee.split(\'\')\n' + | |
' }' + | |
' <% } %>\n' + | |
// iterate over the array-like value | |
' while (++index < length) {\n' + | |
' value = iteratee[index];\n' + | |
' <%= arrayLoop %>\n' + | |
' }\n' + | |
'}\n' + | |
'else {' + | |
// object iteration: | |
// add support for iterating over `arguments` objects if needed | |
' <% } else if (noArgsEnum) { %>\n' + | |
' var length = iteratee.length; index = -1;\n' + | |
' if (length && isArguments(iteratee)) {\n' + | |
' while (++index < length) {\n' + | |
' value = iteratee[index += \'\'];\n' + | |
' <%= objectLoop %>\n' + | |
' }\n' + | |
' } else {' + | |
' <% } %>' + | |
// Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 | |
// (if the prototype or a property on the prototype has been set) | |
// incorrectly sets a function's `prototype` property [[Enumerable]] | |
// value to `true`. Because of this Lo-Dash standardizes on skipping | |
// the the `prototype` property of functions regardless of its | |
// [[Enumerable]] value. | |
' <% if (!hasDontEnumBug) { %>\n' + | |
' var skipProto = typeof iteratee == \'function\' && \n' + | |
' propertyIsEnumerable.call(iteratee, \'prototype\');\n' + | |
' <% } %>' + | |
// iterate own properties using `Object.keys` if it's fast | |
' <% if (isKeysFast && useHas) { %>\n' + | |
' var ownIndex = -1,\n' + | |
' ownProps = objectTypes[typeof iteratee] ? nativeKeys(iteratee) : [],\n' + | |
' length = ownProps.length;\n\n' + | |
' while (++ownIndex < length) {\n' + | |
' index = ownProps[ownIndex];\n' + | |
' <% if (!hasDontEnumBug) { %>if (!(skipProto && index == \'prototype\')) {\n <% } %>' + | |
' value = iteratee[index];\n' + | |
' <%= objectLoop %>\n' + | |
' <% if (!hasDontEnumBug) { %>}\n<% } %>' + | |
' }' + | |
// else using a for-in loop | |
' <% } else { %>\n' + | |
' for (index in iteratee) {<%' + | |
' if (!hasDontEnumBug || useHas) { %>\n if (<%' + | |
' if (!hasDontEnumBug) { %>!(skipProto && index == \'prototype\')<% }' + | |
' if (!hasDontEnumBug && useHas) { %> && <% }' + | |
' if (useHas) { %>hasOwnProperty.call(iteratee, index)<% }' + | |
' %>) {' + | |
' <% } %>\n' + | |
' value = iteratee[index];\n' + | |
' <%= objectLoop %>;' + | |
' <% if (!hasDontEnumBug || useHas) { %>\n }<% } %>\n' + | |
' }' + | |
' <% } %>' + | |
// Because IE < 9 can't set the `[[Enumerable]]` attribute of an | |
// existing property and the `constructor` property of a prototype | |
// defaults to non-enumerable, Lo-Dash skips the `constructor` | |
// property when it infers it's iterating over a `prototype` object. | |
' <% if (hasDontEnumBug) { %>\n\n' + | |
' var ctor = iteratee.constructor;\n' + | |
' <% for (var k = 0; k < 7; k++) { %>\n' + | |
' index = \'<%= shadowed[k] %>\';\n' + | |
' if (<%' + | |
' if (shadowed[k] == \'constructor\') {' + | |
' %>!(ctor && ctor.prototype === iteratee) && <%' + | |
' } %>hasOwnProperty.call(iteratee, index)) {\n' + | |
' value = iteratee[index];\n' + | |
' <%= objectLoop %>\n' + | |
' }' + | |
' <% } %>' + | |
' <% } %>' + | |
' <% if (arrayLoop || noArgsEnum) { %>\n}<% } %>\n' + | |
// add code to the bottom of the iteration function | |
'<%= bottom %>;\n' + | |
// finally, return the `result` | |
'return result' | |
); | |
/** | |
* Reusable iterator options shared by `forEach`, `forIn`, and `forOwn`. | |
*/ | |
var forEachIteratorOptions = { | |
'args': 'collection, callback, thisArg', | |
'top': 'callback = createCallback(callback, thisArg)', | |
'arrayLoop': 'if (callback(value, index, collection) === false) return result', | |
'objectLoop': 'if (callback(value, index, collection) === false) return result' | |
}; | |
/** Reusable iterator options for `defaults`, and `extend` */ | |
var extendIteratorOptions = { | |
'useHas': false, | |
'args': 'object', | |
'top': | |
'for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {\n' + | |
' if (iteratee = arguments[argsIndex]) {', | |
'objectLoop': 'result[index] = value', | |
'bottom': ' }\n}' | |
}; | |
/** Reusable iterator options for `forIn` and `forOwn` */ | |
var forOwnIteratorOptions = { | |
'arrayLoop': null | |
}; | |
/*--------------------------------------------------------------------------*/ | |
/** | |
* Creates a function optimized for searching large arrays for a given `value`, | |
* starting at `fromIndex`, using strict equality for comparisons, i.e. `===`. | |
* | |
* @private | |
* @param {Array} array The array to search. | |
* @param {Mixed} value The value to search for. | |
* @param {Number} [fromIndex=0] The index to start searching from. | |
* @param {Number} [largeSize=30] The length at which an array is considered large. | |
* @returns {Boolean} Returns `true` if `value` is found, else `false`. | |
*/ | |
function cachedContains(array, fromIndex, largeSize) { | |
fromIndex || (fromIndex = 0); | |
var length = array.length, | |
isLarge = (length - fromIndex) >= (largeSize || largeArraySize), | |
cache = isLarge ? {} : array; | |
if (isLarge) { | |
// init value cache | |
var index = fromIndex - 1; | |
while (++index < length) { | |
// manually coerce `value` to string because `hasOwnProperty`, in some | |
// older versions of Firefox, coerces objects incorrectly | |
var key = array[index] + ''; | |
(hasOwnProperty.call(cache, key) ? cache[key] : (cache[key] = [])).push(array[index]); | |
} | |
} | |
return function(value) { | |
if (isLarge) { | |
var key = value + ''; | |
return hasOwnProperty.call(cache, key) && indexOf(cache[key], value) > -1; | |
} | |
return indexOf(cache, value, fromIndex) > -1; | |
} | |
} | |
/** | |
* Used by `sortBy` to compare transformed `collection` values, stable sorting | |
* them in ascending order. | |
* | |
* @private | |
* @param {Object} a The object to compare to `b`. | |
* @param {Object} b The object to compare to `a`. | |
* @returns {Number} Returns the sort order indicator of `1` or `-1`. | |
*/ | |
function compareAscending(a, b) { | |
var ai = a.index, | |
bi = b.index; | |
a = a.criteria; | |
b = b.criteria; | |
// ensure a stable sort in V8 and other engines | |
// http://code.google.com/p/v8/issues/detail?id=90 | |
if (a !== b) { | |
if (a > b || a === undefined) { | |
return 1; | |
} | |
if (a < b || b === undefined) { | |
return -1; | |
} | |
} | |
return ai < bi ? -1 : 1; | |
} | |
/** | |
* Creates a function that, when called, invokes `func` with the `this` | |
* binding of `thisArg` and prepends any `partailArgs` to the arguments passed | |
* to the bound function. | |
* | |
* @private | |
* @param {Function|String} func The function to bind or the method name. | |
* @param {Mixed} [thisArg] The `this` binding of `func`. | |
* @param {Array} partialArgs An array of arguments to be partially applied. | |
* @returns {Function} Returns the new bound function. | |
*/ | |
function createBound(func, thisArg, partialArgs) { | |
var isFunc = isFunction(func), | |
isPartial = !partialArgs, | |
methodName = func; | |
// juggle arguments | |
if (isPartial) { | |
partialArgs = thisArg; | |
} | |
function bound() { | |
// `Function#bind` spec | |
// http://es5.github.com/#x15.3.4.5 | |
var args = arguments, | |
thisBinding = isPartial ? this : thisArg; | |
if (!isFunc) { | |
func = thisArg[methodName]; | |
} | |
if (partialArgs.length) { | |
args = args.length | |
? partialArgs.concat(slice.call(args)) | |
: partialArgs; | |
} | |
if (this instanceof bound) { | |
// get `func` instance if `bound` is invoked in a `new` expression | |
noop.prototype = func.prototype; | |
thisBinding = new noop; | |
// mimic the constructor's `return` behavior | |
// http://es5.github.com/#x13.2.2 | |
var result = func.apply(thisBinding, args); | |
return result && objectTypes[typeof result] | |
? result | |
: thisBinding | |
} | |
return func.apply(thisBinding, args); | |
} | |
return bound; | |
} | |
/** | |
* Produces an iteration callback bound to an optional `thisArg`. If `func` is | |
* a property name, the callback will return the property value for a given element. | |
* | |
* @private | |
* @param {Function|String} [func=identity|property] The function called per | |
* iteration or property name to query. | |
* @param {Mixed} [thisArg] The `this` binding of `callback`. | |
* @returns {Function} Returns a callback function. | |
*/ | |
function createCallback(func, thisArg) { | |
if (!func) { | |
return identity; | |
} | |
if (typeof func != 'function') { | |
return function(object) { | |
return object[func]; | |
}; | |
} | |
if (thisArg !== undefined) { | |
return function(value, index, object) { | |
return func.call(thisArg, value, index, object); | |
}; | |
} | |
return func; | |
} | |
/** | |
* Creates compiled iteration functions. | |
* | |
* @private | |
* @param {Object} [options1, options2, ...] The compile options object(s). | |
* useHas - A boolean to specify using `hasOwnProperty` checks in the object loop. | |
* args - A string of comma separated arguments the iteration function will accept. | |
* top - A string of code to execute before the iteration branches. | |
* arrayLoop - A string of code to execute in the array loop. | |
* objectLoop - A string of code to execute in the object loop. | |
* bottom - A string of code to execute after the iteration branches. | |
* | |
* @returns {Function} Returns the compiled function. | |
*/ | |
function createIterator() { | |
var data = { | |
'arrayLoop': '', | |
'bottom': '', | |
'hasDontEnumBug': hasDontEnumBug, | |
'isKeysFast': isKeysFast, | |
'objectLoop': '', | |
'noArgsEnum': noArgsEnum, | |
'noCharByIndex': noCharByIndex, | |
'shadowed': shadowed, | |
'top': '', | |
'useHas': true | |
}; | |
// merge options into a template data object | |
for (var object, index = 0; object = arguments[index]; index++) { | |
for (var key in object) { | |
data[key] = object[key]; | |
} | |
} | |
var args = data.args; | |
data.firstArg = /^[^,]+/.exec(args)[0]; | |
// create the function factory | |
var factory = Function( | |
'createCallback, hasOwnProperty, isArguments, objectTypes, nativeKeys, ' + | |
'propertyIsEnumerable, stringClass, toString', | |
'return function(' + args + ') {\n' + iteratorTemplate(data) + '\n}' | |
); | |
// return the compiled function | |
return factory( | |
createCallback, hasOwnProperty, isArguments, objectTypes, nativeKeys, | |
propertyIsEnumerable, stringClass, toString | |
); | |
} | |
/** | |
* Used by `template` to escape characters for inclusion in compiled | |
* string literals. | |
* | |
* @private | |
* @param {String} match The matched character to escape. | |
* @returns {String} Returns the escaped character. | |
*/ | |
function escapeStringChar(match) { | |
return '\\' + stringEscapes[match]; | |
} | |
/** | |
* Used by `escape` to convert characters to HTML entities. | |
* | |
* @private | |
* @param {String} match The matched character to escape. | |
* @returns {String} Returns the escaped character. | |
*/ | |
function escapeHtmlChar(match) { | |
return htmlEscapes[match]; | |
} | |
/** | |
* A no-operation function. | |
* | |
* @private | |
*/ | |
function noop() { | |
// no operation performed | |
} | |
/** | |
* Used by `unescape` to convert HTML entities to characters. | |
* | |
* @private | |
* @param {String} match The matched character to unescape. | |
* @returns {String} Returns the unescaped character. | |
*/ | |
function unescapeHtmlChar(match) { | |
return htmlUnescapes[match]; | |
} | |
/*--------------------------------------------------------------------------*/ | |
/** | |
* Checks if `value` is an `arguments` object. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is an `arguments` object, else `false`. | |
* @example | |
* | |
* (function() { return _.isArguments(arguments); })(1, 2, 3); | |
* // => true | |
* | |
* _.isArguments([1, 2, 3]); | |
* // => false | |
*/ | |
function isArguments(value) { | |
return toString.call(value) == argsClass; | |
} | |
// fallback for browsers that can't detect `arguments` objects by [[Class]] | |
if (noArgsClass) { | |
isArguments = function(value) { | |
return value ? hasOwnProperty.call(value, 'callee') : false; | |
}; | |
} | |
/** | |
* Iterates over `object`'s own and inherited enumerable properties, executing | |
* the `callback` for each property. The `callback` is bound to `thisArg` and | |
* invoked with three arguments; (value, key, object). Callbacks may exit iteration | |
* early by explicitly returning `false`. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The object to iterate over. | |
* @param {Function} callback The function called per iteration. | |
* @param {Mixed} [thisArg] The `this` binding of `callback`. | |
* @returns {Object} Returns `object`. | |
* @example | |
* | |
* function Dog(name) { | |
* this.name = name; | |
* } | |
* | |
* Dog.prototype.bark = function() { | |
* alert('Woof, woof!'); | |
* }; | |
* | |
* _.forIn(new Dog('Dagny'), function(value, key) { | |
* alert(key); | |
* }); | |
* // => alerts 'name' and 'bark' (order is not guaranteed) | |
*/ | |
var forIn = createIterator(forEachIteratorOptions, forOwnIteratorOptions, { | |
'useHas': false | |
}); | |
/** | |
* Iterates over `object`'s own enumerable properties, executing the `callback` | |
* for each property. The `callback` is bound to `thisArg` and invoked with three | |
* arguments; (value, key, object). Callbacks may exit iteration early by explicitly | |
* returning `false`. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The object to iterate over. | |
* @param {Function} callback The function called per iteration. | |
* @param {Mixed} [thisArg] The `this` binding of `callback`. | |
* @returns {Object} Returns `object`. | |
* @example | |
* | |
* _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { | |
* alert(key); | |
* }); | |
* // => alerts '0', '1', and 'length' (order is not guaranteed) | |
*/ | |
var forOwn = createIterator(forEachIteratorOptions, forOwnIteratorOptions); | |
/** | |
* A fallback implementation of `isPlainObject` that checks if a given `value` | |
* is an object created by the `Object` constructor, assuming objects created | |
* by the `Object` constructor have no inherited enumerable properties and that | |
* there are no `Object.prototype` extensions. | |
* | |
* @private | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if `value` is a plain object, else `false`. | |
*/ | |
function shimIsPlainObject(value) { | |
// avoid non-objects and false positives for `arguments` objects | |
var result = false; | |
if (!(value && typeof value == 'object') || isArguments(value)) { | |
return result; | |
} | |
// IE < 9 presents DOM nodes as `Object` objects except they have `toString` | |
// methods that are `typeof` "string" and still can coerce nodes to strings. | |
// Also check that the constructor is `Object` (i.e. `Object instanceof Object`) | |
var ctor = value.constructor; | |
if ((!noNodeClass || !(typeof value.toString != 'function' && typeof (value + '') == 'string')) && | |
(!isFunction(ctor) || ctor instanceof ctor)) { | |
// IE < 9 iterates inherited properties before own properties. If the first | |
// iterated property is an object's own property then there are no inherited | |
// enumerable properties. | |
if (iteratesOwnLast) { | |
forIn(value, function(value, key, object) { | |
result = !hasOwnProperty.call(object, key); | |
return false; | |
}); | |
return result === false; | |
} | |
// In most environments an object's own properties are iterated before | |
// its inherited properties. If the last iterated property is an object's | |
// own property then there are no inherited enumerable properties. | |
forIn(value, function(value, key) { | |
result = key; | |
}); | |
return result === false || hasOwnProperty.call(value, result); | |
} | |
return result; | |
} | |
/** | |
* A fallback implementation of `Object.keys` that produces an array of the | |
* given object's own enumerable property names. | |
* | |
* @private | |
* @param {Object} object The object to inspect. | |
* @returns {Array} Returns a new array of property names. | |
*/ | |
function shimKeys(object) { | |
var result = []; | |
forOwn(object, function(value, key) { | |
result.push(key); | |
}); | |
return result; | |
} | |
/** | |
* Used to convert characters to HTML entities: | |
* | |
* Though the `>` character is escaped for symmetry, characters like `>` and `/` | |
* don't require escaping in HTML and have no special meaning unless they're part | |
* of a tag or an unquoted attribute value. | |
* http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") | |
*/ | |
var htmlEscapes = { | |
'&': '&', | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
"'": ''' | |
}; | |
/** Used to convert HTML entities to characters */ | |
var htmlUnescapes = invert(htmlEscapes); | |
/*--------------------------------------------------------------------------*/ | |
/** | |
* Creates a clone of `value`. If `deep` is `true`, all nested objects will | |
* also be cloned otherwise they will be assigned by reference. Functions, DOM | |
* nodes, `arguments` objects, and objects created by constructors other than | |
* `Object` are **not** cloned. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to clone. | |
* @param {Boolean} deep A flag to indicate a deep clone. | |
* @param- {Object} [guard] Internally used to allow this method to work with | |
* others like `_.map` without using their callback `index` argument for `deep`. | |
* @param- {Array} [stackA=[]] Internally used to track traversed source objects. | |
* @param- {Array} [stackB=[]] Internally used to associate clones with their | |
* source counterparts. | |
* @returns {Mixed} Returns the cloned `value`. | |
* @example | |
* | |
* var stooges = [ | |
* { 'name': 'moe', 'age': 40 }, | |
* { 'name': 'larry', 'age': 50 }, | |
* { 'name': 'curly', 'age': 60 } | |
* ]; | |
* | |
* _.clone({ 'name': 'moe' }); | |
* // => { 'name': 'moe' } | |
* | |
* var shallow = _.clone(stooges); | |
* shallow[0] === stooges[0]; | |
* // => true | |
* | |
* var deep = _.clone(stooges, true); | |
* shallow[0] === stooges[0]; | |
* // => false | |
*/ | |
function clone(value, deep, guard, stackA, stackB) { | |
if (value == null) { | |
return value; | |
} | |
if (guard) { | |
deep = false; | |
} | |
// inspect [[Class]] | |
var isObj = objectTypes[typeof value]; | |
if (isObj) { | |
// don't clone `arguments` objects, functions, or non-object Objects | |
var className = toString.call(value); | |
if (!cloneableClasses[className] || (noArgsClass && isArguments(value))) { | |
return value; | |
} | |
var isArr = className == arrayClass; | |
isObj = isArr || (className == objectClass ? isPlainObject(value) : isObj); | |
} | |
// shallow clone | |
if (!isObj || !deep) { | |
// don't clone functions | |
return isObj | |
? (isArr ? slice.call(value) : extend({}, value)) | |
: value; | |
} | |
var ctor = value.constructor; | |
switch (className) { | |
case boolClass: | |
case dateClass: | |
return new ctor(+value); | |
case numberClass: | |
case stringClass: | |
return new ctor(value); | |
case regexpClass: | |
return ctor(value.source, reFlags.exec(value)); | |
} | |
// check for circular references and return corresponding clone | |
stackA || (stackA = []); | |
stackB || (stackB = []); | |
var length = stackA.length; | |
while (length--) { | |
if (stackA[length] == value) { | |
return stackB[length]; | |
} | |
} | |
// init cloned object | |
var result = isArr ? ctor(value.length) : {}; | |
// add the source value to the stack of traversed objects | |
// and associate it with its clone | |
stackA.push(value); | |
stackB.push(result); | |
// recursively populate clone (susceptible to call stack limits) | |
(isArr ? forEach : forOwn)(value, function(objValue, key) { | |
result[key] = clone(objValue, deep, null, stackA, stackB); | |
}); | |
return result; | |
} | |
/** | |
* Assigns enumerable properties of the default object(s) to the `destination` | |
* object for all `destination` properties that resolve to `null`/`undefined`. | |
* Once a property is set, additional defaults of the same property will be | |
* ignored. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The destination object. | |
* @param {Object} [default1, default2, ...] The default objects. | |
* @returns {Object} Returns the destination object. | |
* @example | |
* | |
* var iceCream = { 'flavor': 'chocolate' }; | |
* _.defaults(iceCream, { 'flavor': 'vanilla', 'sprinkles': 'rainbow' }); | |
* // => { 'flavor': 'chocolate', 'sprinkles': 'rainbow' } | |
*/ | |
var defaults = createIterator(extendIteratorOptions, { | |
'objectLoop': 'if (result[index] == null) ' + extendIteratorOptions.objectLoop | |
}); | |
/** | |
* Assigns enumerable properties of the source object(s) to the `destination` | |
* object. Subsequent sources will overwrite propery assignments of previous | |
* sources. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The destination object. | |
* @param {Object} [source1, source2, ...] The source objects. | |
* @returns {Object} Returns the destination object. | |
* @example | |
* | |
* _.extend({ 'name': 'moe' }, { 'age': 40 }); | |
* // => { 'name': 'moe', 'age': 40 } | |
*/ | |
var extend = createIterator(extendIteratorOptions); | |
/** | |
* Creates a sorted array of all enumerable properties, own and inherited, | |
* of `object` that have function values. | |
* | |
* @static | |
* @memberOf _ | |
* @alias methods | |
* @category Objects | |
* @param {Object} object The object to inspect. | |
* @returns {Array} Returns a new array of property names that have function values. | |
* @example | |
* | |
* _.functions(_); | |
* // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] | |
*/ | |
function functions(object) { | |
var result = []; | |
forIn(object, function(value, key) { | |
if (isFunction(value)) { | |
result.push(key); | |
} | |
}); | |
return result.sort(); | |
} | |
/** | |
* Checks if the specified object `property` exists and is a direct property, | |
* instead of an inherited property. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The object to check. | |
* @param {String} property The property to check for. | |
* @returns {Boolean} Returns `true` if key is a direct property, else `false`. | |
* @example | |
* | |
* _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); | |
* // => true | |
*/ | |
function has(object, property) { | |
return object ? hasOwnProperty.call(object, property) : false; | |
} | |
/** | |
* Creates an object composed of the inverted keys and values of the given `object`. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The object to invert. | |
* @returns {Object} Returns the created inverted object. | |
* @example | |
* | |
* _.invert({ 'first': 'Moe', 'second': 'Larry', 'third': 'Curly' }); | |
* // => { 'Moe': 'first', 'Larry': 'second', 'Curly': 'third' } (order is not guaranteed) | |
*/ | |
function invert(object) { | |
var result = {}; | |
forOwn(object, function(value, key) { | |
result[value] = key; | |
}); | |
return result; | |
} | |
/** | |
* Checks if `value` is an array. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is an array, else `false`. | |
* @example | |
* | |
* (function() { return _.isArray(arguments); })(); | |
* // => false | |
* | |
* _.isArray([1, 2, 3]); | |
* // => true | |
*/ | |
var isArray = nativeIsArray || function(value) { | |
return toString.call(value) == arrayClass; | |
}; | |
/** | |
* Checks if `value` is a boolean (`true` or `false`) value. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a boolean value, else `false`. | |
* @example | |
* | |
* _.isBoolean(null); | |
* // => false | |
*/ | |
function isBoolean(value) { | |
return value === true || value === false || toString.call(value) == boolClass; | |
} | |
/** | |
* Checks if `value` is a date. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a date, else `false`. | |
* @example | |
* | |
* _.isDate(new Date); | |
* // => true | |
*/ | |
function isDate(value) { | |
return toString.call(value) == dateClass; | |
} | |
/** | |
* Checks if `value` is a DOM element. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a DOM element, else `false`. | |
* @example | |
* | |
* _.isElement(document.body); | |
* // => true | |
*/ | |
function isElement(value) { | |
return value ? value.nodeType === 1 : false; | |
} | |
/** | |
* Checks if `value` is empty. Arrays, strings, or `arguments` objects with a | |
* length of `0` and objects with no own enumerable properties are considered | |
* "empty". | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Array|Object|String} value The value to inspect. | |
* @returns {Boolean} Returns `true` if the `value` is empty, else `false`. | |
* @example | |
* | |
* _.isEmpty([1, 2, 3]); | |
* // => false | |
* | |
* _.isEmpty({}); | |
* // => true | |
* | |
* _.isEmpty(''); | |
* // => true | |
*/ | |
function isEmpty(value) { | |
var result = true; | |
if (!value) { | |
return result; | |
} | |
var className = toString.call(value), | |
length = value.length; | |
if ((className == arrayClass || className == stringClass || | |
className == argsClass || (noArgsClass && isArguments(value))) || | |
(className == objectClass && typeof length == 'number' && isFunction(value.splice))) { | |
return !length; | |
} | |
forOwn(value, function() { | |
return (result = false); | |
}); | |
return result; | |
} | |
/** | |
* Performs a deep comparison between two values to determine if they are | |
* equivalent to each other. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} a The value to compare. | |
* @param {Mixed} b The other value to compare. | |
* @param- {Object} [stackA=[]] Internally used track traversed `a` objects. | |
* @param- {Object} [stackB=[]] Internally used track traversed `b` objects. | |
* @returns {Boolean} Returns `true` if the values are equvalent, else `false`. | |
* @example | |
* | |
* var moe = { 'name': 'moe', 'luckyNumbers': [13, 27, 34] }; | |
* var clone = { 'name': 'moe', 'luckyNumbers': [13, 27, 34] }; | |
* | |
* moe == clone; | |
* // => false | |
* | |
* _.isEqual(moe, clone); | |
* // => true | |
*/ | |
function isEqual(a, b, stackA, stackB) { | |
// exit early for identical values | |
if (a === b) { | |
// treat `+0` vs. `-0` as not equal | |
return a !== 0 || (1 / a == 1 / b); | |
} | |
// a strict comparison is necessary because `null == undefined` | |
if (a == null || b == null) { | |
return a === b; | |
} | |
// compare [[Class]] names | |
var className = toString.call(a); | |
if (className != toString.call(b)) { | |
return false; | |
} | |
switch (className) { | |
case boolClass: | |
case dateClass: | |
// coerce dates and booleans to numbers, dates to milliseconds and booleans | |
// to `1` or `0`, treating invalid dates coerced to `NaN` as not equal | |
return +a == +b; | |
case numberClass: | |
// treat `NaN` vs. `NaN` as equal | |
return a != +a | |
? b != +b | |
// but treat `+0` vs. `-0` as not equal | |
: (a == 0 ? (1 / a == 1 / b) : a == +b); | |
case regexpClass: | |
case stringClass: | |
// coerce regexes to strings (http://es5.github.com/#x15.10.6.4) | |
// treat string primitives and their corresponding object instances as equal | |
return a == b + ''; | |
} | |
// exit early, in older browsers, if `a` is array-like but not `b` | |
var isArr = className == arrayClass || className == argsClass; | |
if (noArgsClass && !isArr && (isArr = isArguments(a)) && !isArguments(b)) { | |
return false; | |
} | |
if (!isArr) { | |
// unwrap any `lodash` wrapped values | |
if (a.__wrapped__ || b.__wrapped__) { | |
return isEqual(a.__wrapped__ || a, b.__wrapped__ || b); | |
} | |
// exit for functions and DOM nodes | |
if (className != objectClass || (noNodeClass && ( | |
(typeof a.toString != 'function' && typeof (a + '') == 'string') || | |
(typeof b.toString != 'function' && typeof (b + '') == 'string')))) { | |
return false; | |
} | |
var ctorA = a.constructor, | |
ctorB = b.constructor; | |
// non `Object` object instances with different constructors are not equal | |
if (ctorA != ctorB && !( | |
isFunction(ctorA) && ctorA instanceof ctorA && | |
isFunction(ctorB) && ctorB instanceof ctorB | |
)) { | |
return false; | |
} | |
} | |
// assume cyclic structures are equal | |
// the algorithm for detecting cyclic structures is adapted from ES 5.1 | |
// section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3) | |
stackA || (stackA = []); | |
stackB || (stackB = []); | |
var length = stackA.length; | |
while (length--) { | |
if (stackA[length] == a) { | |
return stackB[length] == b; | |
} | |
} | |
var index = -1, | |
result = true, | |
size = 0; | |
// add `a` and `b` to the stack of traversed objects | |
stackA.push(a); | |
stackB.push(b); | |
// recursively compare objects and arrays (susceptible to call stack limits) | |
if (isArr) { | |
// compare lengths to determine if a deep comparison is necessary | |
size = a.length; | |
result = size == b.length; | |
if (result) { | |
// deep compare the contents, ignoring non-numeric properties | |
while (size--) { | |
if (!(result = isEqual(a[size], b[size], stackA, stackB))) { | |
break; | |
} | |
} | |
} | |
return result; | |
} | |
// deep compare objects | |
for (var key in a) { | |
if (hasOwnProperty.call(a, key)) { | |
// count the number of properties. | |
size++; | |
// deep compare each property value. | |
if (!(hasOwnProperty.call(b, key) && isEqual(a[key], b[key], stackA, stackB))) { | |
return false; | |
} | |
} | |
} | |
// ensure both objects have the same number of properties | |
for (key in b) { | |
// The JS engine in Adobe products, like InDesign, has a bug that causes | |
// `!size--` to throw an error so it must be wrapped in parentheses. | |
// https://github.com/documentcloud/underscore/issues/355 | |
if (hasOwnProperty.call(b, key) && !(size--)) { | |
// `size` will be `-1` if `b` has more properties than `a` | |
return false; | |
} | |
} | |
// handle JScript [[DontEnum]] bug | |
if (hasDontEnumBug) { | |
while (++index < 7) { | |
key = shadowed[index]; | |
if (hasOwnProperty.call(a, key) && | |
!(hasOwnProperty.call(b, key) && isEqual(a[key], b[key], stackA, stackB))) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
/** | |
* Checks if `value` is, or can be coerced to, a finite number. | |
* | |
* Note: This is not the same as native `isFinite`, which will return true for | |
* booleans and empty strings. See http://es5.github.com/#x15.1.2.5. | |
* | |
* @deprecated | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a finite number, else `false`. | |
* @example | |
* | |
* _.isFinite(-101); | |
* // => true | |
* | |
* _.isFinite('10'); | |
* // => true | |
* | |
* _.isFinite(true); | |
* // => false | |
* | |
* _.isFinite(''); | |
* // => false | |
* | |
* _.isFinite(Infinity); | |
* // => false | |
*/ | |
function isFinite(value) { | |
return nativeIsFinite(value ? +value : parseFloat(value)); | |
} | |
/** | |
* Checks if `value` is a function. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a function, else `false`. | |
* @example | |
* | |
* _.isFunction(_); | |
* // => true | |
*/ | |
function isFunction(value) { | |
return typeof value == 'function'; | |
} | |
// fallback for older versions of Chrome and Safari | |
if (isFunction(/x/)) { | |
isFunction = function(value) { | |
return toString.call(value) == funcClass; | |
}; | |
} | |
/** | |
* Checks if `value` is the language type of Object. | |
* (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is an object, else `false`. | |
* @example | |
* | |
* _.isObject({}); | |
* // => true | |
* | |
* _.isObject([1, 2, 3]); | |
* // => true | |
* | |
* _.isObject(1); | |
* // => false | |
*/ | |
function isObject(value) { | |
// check if the value is the ECMAScript language type of Object | |
// http://es5.github.com/#x8 | |
// and avoid a V8 bug | |
// http://code.google.com/p/v8/issues/detail?id=2291 | |
return value ? objectTypes[typeof value] : false; | |
} | |
/** | |
* Checks if `value` is `NaN`. | |
* | |
* Note: This is not the same as native `isNaN`, which will return true for | |
* `undefined` and other values. See http://es5.github.com/#x15.1.2.4. | |
* | |
* @deprecated | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is `NaN`, else `false`. | |
* @example | |
* | |
* _.isNaN(NaN); | |
* // => true | |
* | |
* _.isNaN(new Number(NaN)); | |
* // => true | |
* | |
* isNaN(undefined); | |
* // => true | |
* | |
* _.isNaN(undefined); | |
* // => false | |
*/ | |
function isNaN(value) { | |
// `NaN` as a primitive is the only value that is not equal to itself | |
// (perform the [[Class]] check first to avoid errors with some host objects in IE) | |
return toString.call(value) == numberClass && value != +value | |
} | |
/** | |
* Checks if `value` is `null`. | |
* | |
* @deprecated | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is `null`, else `false`. | |
* @example | |
* | |
* _.isNull(null); | |
* // => true | |
* | |
* _.isNull(undefined); | |
* // => false | |
*/ | |
function isNull(value) { | |
return value === null; | |
} | |
/** | |
* Checks if `value` is a number. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a number, else `false`. | |
* @example | |
* | |
* _.isNumber(8.4 * 5); | |
* // => true | |
*/ | |
function isNumber(value) { | |
return toString.call(value) == numberClass; | |
} | |
/** | |
* Checks if a given `value` is an object created by the `Object` constructor. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if `value` is a plain object, else `false`. | |
* @example | |
* | |
* function Stooge(name, age) { | |
* this.name = name; | |
* this.age = age; | |
* } | |
* | |
* _.isPlainObject(new Stooge('moe', 40)); | |
* // => false | |
* | |
* _.isPlainObject([1, 2, 3]); | |
* // => false | |
* | |
* _.isPlainObject({ 'name': 'moe', 'age': 40 }); | |
* // => true | |
*/ | |
var isPlainObject = !getPrototypeOf ? shimIsPlainObject : function(value) { | |
if (!(value && typeof value == 'object')) { | |
return false; | |
} | |
var valueOf = value.valueOf, | |
objProto = typeof valueOf == 'function' && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto); | |
return objProto | |
? value == objProto || (getPrototypeOf(value) == objProto && !isArguments(value)) | |
: shimIsPlainObject(value); | |
}; | |
/** | |
* Checks if `value` is a regular expression. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a regular expression, else `false`. | |
* @example | |
* | |
* _.isRegExp(/moe/); | |
* // => true | |
*/ | |
function isRegExp(value) { | |
return toString.call(value) == regexpClass; | |
} | |
/** | |
* Checks if `value` is a string. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is a string, else `false`. | |
* @example | |
* | |
* _.isString('moe'); | |
* // => true | |
*/ | |
function isString(value) { | |
return toString.call(value) == stringClass; | |
} | |
/** | |
* Checks if `value` is `undefined`. | |
* | |
* @deprecated | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Mixed} value The value to check. | |
* @returns {Boolean} Returns `true` if the `value` is `undefined`, else `false`. | |
* @example | |
* | |
* _.isUndefined(void 0); | |
* // => true | |
*/ | |
function isUndefined(value) { | |
return value === undefined; | |
} | |
/** | |
* Creates an array composed of the own enumerable property names of `object`. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The object to inspect. | |
* @returns {Array} Returns a new array of property names. | |
* @example | |
* | |
* _.keys({ 'one': 1, 'two': 2, 'three': 3 }); | |
* // => ['one', 'two', 'three'] (order is not guaranteed) | |
*/ | |
var keys = !nativeKeys ? shimKeys : function(object) { | |
var type = typeof object; | |
// avoid iterating over the `prototype` property | |
if (type == 'function' && propertyIsEnumerable.call(object, 'prototype')) { | |
return shimKeys(object); | |
} | |
return object && objectTypes[type] | |
? nativeKeys(object) | |
: []; | |
}; | |
/** | |
* Merges enumerable properties of the source object(s) into the `destination` | |
* object. Subsequent sources will overwrite propery assignments of previous | |
* sources. | |
* | |
* @static | |
* @memberOf _ | |
* @category Objects | |
* @param {Object} object The destination object. | |
* @param {Object} [source1, source2, ...] The source objects. | |
* @param- {Object} [indicator] Internally used to indicate that the `stack` | |
* argument is an array of traversed objects instead of another source object. | |
* @param- {Array} [stackA=[]] Internally used to track traversed source objects. | |
* @param- {Array} [stackB=[]] Internally used to associate values with their | |
* source counterparts. | |
* @returns {Object} Returns the destination object. | |
* @example | |
* | |
* var stooges = [ | |
* { 'name': 'moe' }, | |
* { 'name': 'larry' } | |
* ]; | |
* | |
* var ages = [ | |
* { 'age': 40 }, | |
* { 'age': 50 } | |
* ]; | |
* | |
* _.merge(stooges, ages); | |
* // => [{ 'name': 'moe', 'age': 40 }, { 'name': 'larry', 'age': 50 }] | |
*/ | |
function merge(object, source, indicator) { | |
var args = arguments, | |
index = 0, | |
length = 2, | |
stackA = args[3], | |
stackB = args[4]; | |
if (indicator !== indicatorObject) { | |
stackA = []; | |
stackB = []; | |
length = args.length; | |
} | |
while (++index < length) { | |
forOwn(args[index], function(source, key) { | |
var found, isArr, value; | |
if (source && ((isArr = isArray(source)) || isPlainObject(source))) { | |
// avoid merging previously merged cyclic sources | |
var stackLength = stackA.length; | |
while (stackLength--) { | |
found = stackA[stackLength] == source; | |
if (found) { | |
break; | |
} | |
} | |
if (found) { | |
object[key] = stackB[stackLength]; | |
} | |
else { | |
// add `source` and associated `value` to the stack of traversed objects | |
stackA.push(source); | |
stackB.push(value = (value = object[key], isArr) | |
? (isArray(value) ? value : []) | |
: (isPlainObject(value) ? value : {}) | |
); | |