backbone-fusioncharts
Version:
Simple and Lightweight Backbone wrapper for FusionCharts JavaScript Charting Library
1,303 lines (1,120 loc) • 425 kB
JavaScript
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./example/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "../../../../../usr/local/lib/node_modules/webpack/buildin/global.js":
/*!***********************************!*\
!*** (webpack)/buildin/global.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
var g;
// This works in non-strict mode
g = (function() {
return this;
})();
try {
// This works if eval is allowed (see CSP)
g = g || Function("return this")() || (1, eval)("this");
} catch (e) {
// This works if the window reference is available
if (typeof window === "object") g = window;
}
// g can still be undefined, but nothing to do about it...
// We return undefined, instead of nothing here, so it's
// easier to handle this case. if(!global) { ...}
module.exports = g;
/***/ }),
/***/ "../../../../../usr/local/lib/node_modules/webpack/buildin/module.js":
/*!***********************************!*\
!*** (webpack)/buildin/module.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = function(module) {
if (!module.webpackPolyfill) {
module.deprecate = function() {};
module.paths = [];
// module.parent = undefined by default
if (!module.children) module.children = [];
Object.defineProperty(module, "loaded", {
enumerable: true,
get: function() {
return module.l;
}
});
Object.defineProperty(module, "id", {
enumerable: true,
get: function() {
return module.i;
}
});
module.webpackPolyfill = 1;
}
return module;
};
/***/ }),
/***/ "./dist/backbone-fusioncharts.js":
/*!***************************************!*\
!*** ./dist/backbone-fusioncharts.js ***!
\***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* WEBPACK VAR INJECTION */(function(module) {var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;
var _typeof2 = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
(function webpackUniversalModuleDefinition(root, factory) {
if (( false ? undefined : _typeof2(exports)) === 'object' && ( false ? undefined : _typeof2(module)) === 'object') module.exports = factory(__webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js"));else if (true) !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));else {}
})(window, function (__WEBPACK_EXTERNAL_MODULE_backbone__) {
return (/******/function (modules) {
// webpackBootstrap
/******/ // The module cache
/******/var installedModules = {};
/******/
/******/ // The require function
/******/function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/if (installedModules[moduleId]) {
/******/return installedModules[moduleId].exports;
/******/
}
/******/ // Create a new module (and put it into the cache)
/******/var module = installedModules[moduleId] = {
/******/i: moduleId,
/******/l: false,
/******/exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/module.l = true;
/******/
/******/ // Return the exports of the module
/******/return module.exports;
/******/
}
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/__webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/__webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/__webpack_require__.d = function (exports, name, getter) {
/******/if (!__webpack_require__.o(exports, name)) {
/******/Object.defineProperty(exports, name, {
/******/configurable: false,
/******/enumerable: true,
/******/get: getter
/******/ });
/******/
}
/******/
};
/******/
/******/ // define __esModule on exports
/******/__webpack_require__.r = function (exports) {
/******/Object.defineProperty(exports, '__esModule', { value: true });
/******/
};
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/__webpack_require__.n = function (module) {
/******/var getter = module && module.__esModule ?
/******/function getDefault() {
return module['default'];
} :
/******/function getModuleExports() {
return module;
};
/******/__webpack_require__.d(getter, 'a', getter);
/******/return getter;
/******/
};
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
/******/
/******/ // __webpack_public_path__
/******/__webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/return __webpack_require__(__webpack_require__.s = "./src/controller.js");
/******/
}(
/************************************************************************/
/******/{
/***/"./src/controller.js":
/*!***************************!*\
!*** ./src/controller.js ***!
\***************************/
/*! no static exports found */
/***/function srcControllerJs(module, exports, __webpack_require__) {
"use strict";
var _backbone = __webpack_require__( /*! backbone */"backbone");
var _backbone2 = _interopRequireDefault(_backbone);
var _view = __webpack_require__( /*! ./view */"./src/view.js");
var _view2 = _interopRequireDefault(_view);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var BackboneFusionCharts = function BackboneFusionCharts(options) {
_classCallCheck(this, BackboneFusionCharts);
this.model = new _backbone2.default.Model(options);
this.view = new _view2.default({
model: this.model
});
};
module.exports = BackboneFusionCharts;
/***/
},
/***/"./src/utils/options.js":
/*!******************************!*\
!*** ./src/utils/options.js ***!
\******************************/
/*! no static exports found */
/***/function srcUtilsOptionsJs(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = ['renderAt', 'type', 'id', 'width', 'height', 'dataFormat', 'dataSource', 'events', 'link', 'showDataLoadingMessage', 'showChartLoadingMessage', 'baseChartMessageFont', 'baseChartMessageFontSize', 'baseChartMessageColor', 'dataLoadStartMessage', 'dataLoadErrorMessage', 'dataInvalidMessage', 'dataEmptyMessage', 'typeNotSupportedMessage', 'loadMessage', 'renderErrorMessage', 'containerBackgroundColor', 'containerBackgroundOpacity', 'containerClassName', 'baseChartMessageImageHAlign', 'baseChartMessageImageVAlign', 'baseChartMessageImageAlpha', 'baseChartMessageImageScale', 'typeNotSupportedMessageImageHAalign', 'typeNotSupportedMessageImageVAlign', 'typeNotSupportedMessageImageAlpha', 'typeNotSupportedMessageImageScale', 'dataLoadErrorMessageImageHAlign', 'dataLoadErrorMessageImageVAlign', 'dataLoadErrorMessageImageAlpha', 'dataLoadErrorMessageImageScale', 'dataLoadStartMessageImageHAlign', 'dataLoadStartMessageImageVAlign', 'dataLoadStartMessageImageAlpha', 'dataLoadStartMessageImageScale', 'dataInvalidMessageImageHAlign', 'dataInvalidMessageImageVAlign', 'dataInvalidMessageImageAlpha', 'dataInvalidMessageImageScale', 'dataEmptyMessageImageHAlign', 'dataEmptyMessageImageVAlign', 'dataEmptyMessageImageAlpha', 'dataEmptyMessageImageScale', 'renderErrorMessageImageHAlign', 'renderErrorMessageImageVAlign', 'renderErrorMessageImageAlpha', 'renderErrorMessageImageScale', 'loadMessageImageHAlign', 'loadMessageImageVAlign', 'loadMessageImageAlpha', 'loadMessageImageScale'];
module.exports = exports['default'];
/***/
},
/***/"./src/utils/utils.js":
/*!****************************!*\
!*** ./src/utils/utils.js ***!
\****************************/
/*! no static exports found */
/***/function srcUtilsUtilsJs(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol" ? function (obj) {
return typeof obj === 'undefined' ? 'undefined' : _typeof2(obj);
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj === 'undefined' ? 'undefined' : _typeof2(obj);
};
exports.isObject = isObject;
exports.isCallable = isCallable;
exports.isSameObjectContent = isSameObjectContent;
exports.isUndefined = isUndefined;
exports.deepCopyOf = deepCopyOf;
function isObject(value) {
return value !== null && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object';
}
function isCallable(value) {
return typeof value === 'function';
}
function isSameObjectContent(obj1, obj2) {
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
var keys = Object.keys(obj1);
for (var i = 0; i < keys.length; i += 1) {
var key = keys[i];
if (isObject(obj1[key]) && isObject(obj2[key])) {
if (!isSameObjectContent(obj1[key], obj2[key])) {
return false;
}
} else if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
function isUndefined(value) {
// eslint-disable-next-line no-void
var UNDEFINED = void 0;
return value === UNDEFINED;
}
function deepCopyOf(obj) {
return JSON.parse(JSON.stringify(obj));
}
/***/
},
/***/"./src/view.js":
/*!*********************!*\
!*** ./src/view.js ***!
\*********************/
/*! no static exports found */
/***/function srcViewJs(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _backbone = __webpack_require__( /*! backbone */"backbone");
var _backbone2 = _interopRequireDefault(_backbone);
var _options = __webpack_require__( /*! ./utils/options */"./src/utils/options.js");
var _options2 = _interopRequireDefault(_options);
var _utils = __webpack_require__( /*! ./utils/utils */"./src/utils/utils.js");
var utils = _interopRequireWildcard(_utils);
function _interopRequireWildcard(obj) {
if (obj && obj.__esModule) {
return obj;
} else {
var newObj = {};if (obj != null) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
}
}newObj.default = obj;return newObj;
}
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
exports.default = _backbone2.default.View.extend({
initialize: function initialize() {
this.model.bind('change', this.onChange.bind(this));
this.props = this.model.toJSON();
this.render();
},
onChange: function onChange(nextProps) {
var currentOptions = this.resolveChartOptions(nextProps.toJSON());
var oldOptions = this.oldOptions;
var optionsUpdatedNatively = ['width', 'height', 'type', 'dataFormat', 'dataSource', 'events'];
this.checkAndUpdateChartRenderAt(currentOptions, oldOptions);
this.checkAndUpdateChartDimensions(currentOptions, oldOptions);
this.checkAndUpdateChartType(currentOptions, oldOptions);
this.checkAndUpdateChartData(currentOptions, oldOptions);
this.checkAndUpdateEvents(currentOptions, oldOptions);
this.checkAndUpdateRestOptions(_options2.default.filter(function (option) {
return optionsUpdatedNatively.indexOf(option) === -1;
}), currentOptions, oldOptions);
this.oldOptions = currentOptions;
},
checkAndUpdateChartRenderAt: function checkAndUpdateChartRenderAt(currentOptions, oldOptions) {
var currRenderAt = currentOptions.renderAt;
var oldRenderAt = oldOptions.renderAt;
if (String(currRenderAt) !== String(oldRenderAt)) {
this.chart.dispose();
this.props.renderAt = currRenderAt;
this.render();
}
},
checkAndUpdateChartDimensions: function checkAndUpdateChartDimensions(currentOptions, oldOptions) {
var currWidth = currentOptions.width;
var currHeight = currentOptions.height;
var oldWidth = oldOptions.width;
var oldHeight = oldOptions.height;
if (String(currWidth) !== String(oldWidth) || String(currHeight) !== String(oldHeight)) {
if (!utils.isUndefined(currWidth) && !utils.isUndefined(currHeight)) {
this.chart.resizeTo(currWidth, currHeight);
} else {
if (!utils.isUndefined(currWidth)) {
this.chart.resizeTo({
w: currWidth
});
}
if (!utils.isUndefined(currHeight)) {
this.chart.resizeTo({
h: currHeight
});
}
}
}
},
checkAndUpdateChartType: function checkAndUpdateChartType(currentOptions, oldOptions) {
var currType = currentOptions.type;
var oldType = oldOptions.type;
if (String(currType).toLowerCase() !== String(oldType).toLowerCase()) {
if (!utils.isUndefined(currType)) {
this.chart.chartType(String(currType).toLowerCase());
}
}
},
checkAndUpdateChartData: function checkAndUpdateChartData(currentOptions, oldOptions) {
var currDataFormat = currentOptions.dataFormat;
var currData = currentOptions.dataSource;
var oldDataFormat = oldOptions.dataFormat;
var oldData = oldOptions.dataSource;
if (String(currDataFormat).toLowerCase() !== String(oldDataFormat).toLowerCase()) {
if (!utils.isUndefined(currDataFormat) && !utils.isUndefined(currData)) {
this.chart.setChartData(currData, String(currDataFormat).toLowerCase());
// If the chart dataFormat is changed then
// animate the chart to show the changes
this.chart.render();
}
} else if (!this.isSameChartData(currData, oldData)) {
if (!utils.isUndefined(currData)) {
this.chart.setChartData(currData,
// When dataFormat is not given, but data is changed,
// then use 'json' as default dataFormat
currDataFormat ? String(currDataFormat).toLowerCase() : 'json');
}
}
},
isSameChartData: function isSameChartData(currData, oldData) {
if (utils.isObject(currData) && utils.isObject(oldData)) {
return utils.isSameObjectContent(currData, oldData);
}
return currData === oldData;
},
checkAndUpdateEvents: function checkAndUpdateEvents(currentOptions, oldOptions) {
var _this = this;
var currEvents = currentOptions.events;
var oldEvents = oldOptions.events;
var temp1 = void 0;
var temp2 = void 0;
if (this.detectChartEventsChange(currEvents, oldEvents)) {
if (!utils.isUndefined(currEvents)) {
temp1 = Object.assign({}, currEvents);
temp2 = utils.isUndefined(oldEvents) ? {} : Object.assign({}, oldEvents);
Object.keys(temp2).forEach(function (eventName) {
if (temp2[eventName] === temp1[eventName]) {
temp1[eventName] = undefined;
} else {
_this.chart.removeEventListener(eventName, temp2[eventName]);
}
});
Object.keys(temp1).forEach(function (eventName) {
if (temp1[eventName]) {
_this.chart.addEventListener(eventName, temp1[eventName]);
}
});
}
}
},
detectChartEventsChange: function detectChartEventsChange(currEvents, oldEvents) {
if (utils.isObject(currEvents) && utils.isObject(oldEvents)) {
return !this.isSameChartEvents(currEvents, oldEvents);
}
return !(currEvents === oldEvents);
},
isSameChartEvents: function isSameChartEvents(currEvents, oldEvents) {
if (Object.keys(currEvents).length !== Object.keys(oldEvents).length) {
return false;
}
var currEventNames = Object.keys(currEvents);
for (var i = 0; i < currEventNames.length; ++i) {
var evName = currEventNames[i];
if (currEvents[evName] !== oldEvents[evName]) {
return false;
}
}
return true;
},
checkAndUpdateRestOptions: function checkAndUpdateRestOptions(restOptions, currentOptions, oldOptions) {
var _this2 = this;
var optionsUpdated = false;
restOptions.forEach(function (optionName) {
var currValue = currentOptions[optionName];
var oldValue = oldOptions[optionName];
if (!_this2.isSameOptionValue(currValue, oldValue)) {
if (!utils.isUndefined(currValue)) {
if (_this2.chart.options && _this2.chart.options.hasOwnProperty(optionName)) {
_this2.chart.options[optionName] = currValue;
optionsUpdated = true;
}
}
}
});
if (optionsUpdated) {
this.chart.render(); // re-render the chart to reflect the changes
}
},
isSameOptionValue: function isSameOptionValue(currValue, oldValue) {
if (utils.isObject(currValue) && utils.isObject(oldValue)) {
return utils.isSameObjectContent(currValue, oldValue);
}
return String(currValue) === String(oldValue);
},
resolveChartOptions: function resolveChartOptions(props) {
var chartConfig = props.chartConfig ? props.chartConfig : {};
var inlineOptions = _options2.default.reduce(function (options, optionName) {
options[optionName] = props[optionName];
return options;
}, {});
Object.assign(inlineOptions, chartConfig);
if (utils.isObject(inlineOptions.dataSource)) {
inlineOptions.dataSource = utils.deepCopyOf(inlineOptions.dataSource);
}
if (utils.isObject(inlineOptions.link)) {
inlineOptions.link = utils.deepCopyOf(inlineOptions.link);
}
if (utils.isObject(inlineOptions.events)) {
inlineOptions.events = Object.assign({}, inlineOptions.events);
}
return inlineOptions;
},
render: function render() {
var currentOptions = this.resolveChartOptions(this.props);
this.chart = new FusionCharts(currentOptions);
this.chart.render();
this.oldOptions = currentOptions;
return this;
}
}); /* eslint-disable no-prototype-builtins, no-plusplus, no-param-reassign */
module.exports = exports['default'];
/***/
},
/***/"backbone":
/*!**************************************************************************************************!*\
!*** external {"root":"Backbone","commonjs2":"backbone","commonjs":"backbone","amd":"backbone"} ***!
\**************************************************************************************************/
/*! no static exports found */
/***/function backbone(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_backbone__;
/***/
}
/******/ })
);
});
//# sourceMappingURL=backbone-fusioncharts.js.map
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../../../../usr/local/lib/node_modules/webpack/buildin/module.js */ "../../../../../usr/local/lib/node_modules/webpack/buildin/module.js")(module)))
/***/ }),
/***/ "./example/index.js":
/*!**************************!*\
!*** ./example/index.js ***!
\**************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var _backboneFusioncharts = __webpack_require__(/*! ../dist/backbone-fusioncharts */ "./dist/backbone-fusioncharts.js");
var _backboneFusioncharts2 = _interopRequireDefault(_backboneFusioncharts);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var dataSource = {
chart: {
caption: 'Backbone FusionCharts Sample',
theme: 'fint'
},
data: [{ value: 1.9 }, { value: 2.3 }, { value: 2.1 }]
};
var fc = new _backboneFusioncharts2.default({
renderAt: 'fusioncharts',
type: 'Pie2d',
dataSource: dataSource
});
setTimeout(function () {
fc.model.set('renderAt', 'fusioncharts2');
}, 3000);
console.log(fc);
/***/ }),
/***/ "./node_modules/backbone/backbone.js":
/*!*******************************************!*\
!*** ./node_modules/backbone/backbone.js ***!
\*******************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(global) {var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Backbone.js 1.3.3
// (c) 2010-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(factory) {
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
var root = (typeof self == 'object' && self.self === self && self) ||
(typeof global == 'object' && global.global === global && global);
// Set up Backbone appropriately for the environment. Start with AMD.
if (true) {
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! underscore */ "./node_modules/underscore/underscore.js"), __webpack_require__(/*! jquery */ "./node_modules/jquery/dist/jquery.js"), exports], __WEBPACK_AMD_DEFINE_RESULT__ = (function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
}).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else { var _, $; }
})(function(root, Backbone, _, $) {
// Initial Setup
// -------------
// Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create a local reference to a common array method we'll want to use later.
var slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.3.3';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... this will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
Backbone.emulateJSON = false;
// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(length, method, attribute) {
switch (length) {
case 1: return function() {
return _[method](this[attribute]);
};
case 2: return function(value) {
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};
// Backbone.Events
// ---------------
// A module that can be mixed in to *any object* in order to provide it with
// a custom event channel. You may bind a callback to an event with `on` or
// remove with `off`; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
var Events = Backbone.Events = {};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`).
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
for (names = _.keys(name); i < names.length ; i++) {
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
// Handle space-separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
// Finally, standard events.
events = iteratee(events, name, callback, opts);
}
return events;
};
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
Events.on = function(name, callback, context) {
return internalOn(this, name, callback, context);
};
// Guard the `listening` argument from the public API.
var internalOn = function(obj, name, callback, context, listening) {
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
context: context,
ctx: obj,
listening: listening
});
if (listening) {
var listeners = obj._listeners || (obj._listeners = {});
listeners[listening.id] = listening;
}
return obj;
};
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
}
// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
return this;
};
// The reducing API that adds a callback to the `events` object.
var onApi = function(events, name, callback, options) {
if (callback) {
var handlers = events[name] || (events[name] = []);
var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
Events.off = function(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
context: context,
listeners: this._listeners
});
return this;
};
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
Events.stopListening = function(obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
var ids = obj ? [obj._listenId] : _.keys(listeningTo);
for (var i = 0; i < ids.length; i++) {
var listening = listeningTo[ids[i]];
// If listening doesn't exist, this object is not currently
// listening to obj. Break out early.
if (!listening) break;
listening.obj.off(name, callback, this);
}
return this;
};
// The reducing API that removes a callback from the `events` object.
var offApi = function(events, name, callback, options) {
if (!events) return;
var i = 0, listening;
var context = options.context, listeners = options.listeners;
// Delete all events listeners and "drop" events.
if (!name && !callback && !context) {
var ids = _.keys(listeners);
for (; i < ids.length; i++) {
listening = listeners[ids[i]];
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
return;
}
var names = name ? [name] : _.keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
// Bail out if there are no events stored.
if (!handlers) break;
// Replace events if there are any remaining. Otherwise, clean up.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
if (
callback && callback !== handler.callback &&
callback !== handler.callback._callback ||
context && context !== handler.context
) {
remaining.push(handler);
} else {
listening = handler.listening;
if (listening && --listening.count === 0) {
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
}
}
// Update tail event if the list has any events. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
return events;
};
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
if (typeof name === 'string' && context == null) callback = void 0;
return this.on(events, callback, context);
};
// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
};
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
// `offer` unbinds the `onceWrapper` after it has been called.
var onceMap = function(map, name, callback, offer) {
if (callback) {
var once = map[name] = _.once(function() {
offer(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
}
return map;
};
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
Events.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
var args = Array(length);
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
};
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
if (events && allEvents) allEvents = allEvents.slice();
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return objEvents;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
}
};
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);
// Backbone.Model
// --------------
// Backbone **Models** are the basic data object in the framework --
// frequently representing a row in a table in a database on your server.
// A discrete chunk of data and a bunch of useful, related methods for
// performing computations and transformations on that data.
// Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {};
options || (options = {});
this.cid = _.uniqueId(this.cidPrefix);
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
var defaults = _.result(this, 'defaults');
attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
changed: null,
// The value returned during the last failed validation.
validationError: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// The prefix is used to create the client id which is used to identify models locally.
// You may want to override this if you're experiencing name clashes with model ids.
cidPrefix: 'c',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Get the value of an attribute.
get: function(attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
escape: function(attr) {
return _.escape(this.get(attr));
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has: function(attr) {
return this.get(attr) != null;
},
// Special-cased proxy to underscore's `_.matches` method.
matches: function(attrs) {
return !!_.iteratee(attrs, this)(this.attributes);
},
// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
set: function(key, val, options) {
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
var unset = options.unset;
var silent = options.silent;
var changes = [];
var changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
var current = this.attributes;
var changed = this.changed;
var prev = this._previousAttributes;
// For each `set` attribute, update or delete the current value.
for (var attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
changed[attr] = val;
} else {
delete changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Update the `id`.
if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
// if the attribute doesn't exist.
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// Clear all attributes on the model, firing `"change"`.
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
for (var attr in diff) {
var val = diff[attr];
if (_.isEqual(old[attr], val)) continue;
changed[attr] = val;
}
return _.size(changed) ? changed : false;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// Fetch the model from the server, merging the response with the model's
// local attributes. Any changed attributes will trigger a "change" event.
fetch: function(options) {
options = _.extend({parse: true}, options);
var model = this;
var success = options.success;
options.success = function(resp) {
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (!model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, val, options) {
// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = _.extend({validate: true, parse: true}, options);
var wait = options.wait;
// If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
if (attrs && !wait) {
if (!this.set(attrs, options)) return false;
} else if (!this._validate(attrs, options)) {
return false;
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
var model = this;
var success = options.success;
var attributes = this.attributes;
options.success = function(resp) {
// Ensure attributes are r