diff --git a/ajax/libs/backbone.stickit/0.8.0/backbone.stickit.js b/ajax/libs/backbone.stickit/0.8.0/backbone.stickit.js new file mode 100644 index 000000000..ad8979175 --- /dev/null +++ b/ajax/libs/backbone.stickit/0.8.0/backbone.stickit.js @@ -0,0 +1,580 @@ +// +// backbone.stickit - v0.8.0 +// The MIT License +// Copyright (c) 2012 The New York Times, CMS Group, Matthew DeLambo +// +// Backbone.Stickit v0.8.0, MIT Licensed +// Copyright (c) 2012 The New York Times, CMS Group, Matthew DeLambo + +(function (factory) { + + // Set up Stickit appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'backbone', 'exports'], factory); + } + + // Next for Node.js or CommonJS. + else if (typeof exports === 'object') { + factory(require('underscore'), require('backbone'), exports); + } + + // Finally, as a browser global. + else { + factory(_, Backbone, {}); + } + +}(function (_, Backbone, Stickit) { + + // Stickit Namespace + // -------------------------- + + Stickit._handlers = []; + + Stickit.addHandler = function(handlers) { + // Fill-in default values. + handlers = _.map(_.flatten([handlers]), function(handler) { + return _.extend({ + updateModel: true, + updateView: true, + updateMethod: 'text' + }, handler); + }); + this._handlers = this._handlers.concat(handlers); + }; + + // Backbone.View Mixins + // -------------------- + + Stickit.ViewMixin = { + + // Collection of model event bindings. + // [{model,event,fn,config}, ...] + _modelBindings: null, + + // Unbind the model and event bindings from `this._modelBindings` and + // `this.$el`. If the optional `model` parameter is defined, then only + // delete bindings for the given `model` and its corresponding view events. + unstickit: function(model, bindingSelector) { + // Support bindings hash in place of selector. + if (_.isObject(bindingSelector)) { + _.each(_.keys(bindingSelector), function(selector) { + this.unstickit(model, selector); + }, this); + return; + } + + var models = [], destroyFns = []; + _.each(this._modelBindings, function(binding, i) { + if (model && binding.model !== model) { return; } + if (bindingSelector && binding.config.selector != bindingSelector) return; + destroyFns.push(binding.config._destroy); + binding.model.off(binding.event, binding.fn); + models.push(binding.model); + delete this._modelBindings[i]; + }, this); + + // Trigger an event for each model that was unbound. + _.invoke(_.uniq(models), 'trigger', 'stickit:unstuck', this.cid); + // Call `_destroy` on a unique list of the binding callbacks. + _.each(_.uniq(destroyFns), function(fn) { fn.call(this); }, this); + // Cleanup the null values. + this._modelBindings = _.compact(this._modelBindings); + + this.$el.off('.stickit' + (model ? '.' + model.cid : ''), bindingSelector); + }, + + // Using `this.bindings` configuration or the `optionalBindingsConfig`, binds `this.model` + // or the `optionalModel` to elements in the view. + stickit: function(optionalModel, optionalBindingsConfig) { + var model = optionalModel || this.model, + bindings = optionalBindingsConfig || _.result(this, "bindings") || {}; + + this._modelBindings || (this._modelBindings = []); + + // Iterate through the selectors in the bindings configuration and configure + // the various options for each field. + this.addBinding(model, bindings); + + // Wrap `view.remove` to unbind stickit model and dom events. + var remove = this.remove; + if (!remove.stickitWrapped) + this.remove = function() { + var ret = this; + this.unstickit(); + if (remove) ret = remove.apply(this, arguments); + return ret; + }; + this.remove.stickitWrapped = true; + }, + + // Add a single model binding to the view + addBinding: function(optionalModel, second, _binding) { + var $el, options, modelAttr, config, selector, + model = optionalModel || this.model, + namespace = '.stickit.' + model.cid, + binding = _binding || {}, + bindId = _.uniqueId(); + + // Allow jQuery-style {key: val} event maps + if (_.isString(second)) { + selector = second; + } else { + var bindings = second; + _.each(bindings, function(v, selector) { + this.addBinding(model, selector, bindings[selector]); + }, this); + return; + } + + // Support ':el' selector - special case selector for the view managed delegate. + $el = selector === ':el' ? this.$el : this.$(selector); + + this.unstickit(model, selector); + + // Fail fast if the selector didn't match an element. + if (!$el.length) return; + + // Allow shorthand setting of model attributes - `'selector':'observe'`. + if (_.isString(binding)) binding = {observe:binding}; + + // Handle case where `observe` is in the form of a function. + if (_.isFunction(binding.observe)) binding.observe = binding.observe.call(this); + + config = getConfiguration($el, binding); + config.selector = selector; + modelAttr = config.observe; + + // Create the model set options with a unique `bindId` so that we + // can avoid double-binding in the `change:attribute` event handler. + config.bindId = bindId; + + // Add a reference to the view for handlers of stickitChange events + config.view = this; + options = _.extend({stickitChange:config}, config.setOptions); + + // Add a `_destroy` callback to the configuration, in case `destroy` + // is a named function and we need a unique function when unsticking. + config._destroy = function() { + applyViewFn(this, config.destroy, $el, model, config); + }; + + initializeAttributes(this, $el, config, model, modelAttr); + + initializeVisible(this, $el, config, model, modelAttr); + + if (modelAttr) { + // Setup one-way, form element to model, bindings. + _.each(config.events, function(type) { + var event = type + namespace; + var method = function(event) { + var val = config.getVal.call(this, $el, event, config, _.rest(arguments)); + // Don't update the model if false is returned from the `updateModel` configuration. + if (evaluateBoolean(this, config.updateModel, val, event, config)) + setAttr(model, modelAttr, val, options, this, config); + }; + method = _.bind(method, this); + if (selector === ':el') this.$el.on(event, method); + else this.$el.on(event, selector, method); + }, this); + + // Setup a `change:modelAttr` observer to keep the view element in sync. + // `modelAttr` may be an array of attributes or a single string value. + _.each(_.flatten([modelAttr]), function(attr) { + observeModelEvent(model, this, 'change:'+attr, config, function(model, val, options) { + var changeId = options && options.stickitChange && options.stickitChange.bindId || null; + if (changeId !== bindId) + updateViewBindEl(this, $el, config, getAttr(model, modelAttr, config, this), model); + }); + }, this); + + updateViewBindEl(this, $el, config, getAttr(model, modelAttr, config, this), model, true); + } + + // After each binding is setup, call the `initialize` callback. + applyViewFn(this, config.initialize, $el, model, config); + } + }; + + _.extend(Backbone.View.prototype, Stickit.ViewMixin); + + // Helpers + // ------- + + // Evaluates the given `path` (in object/dot-notation) relative to the given + // `obj`. If the path is null/undefined, then the given `obj` is returned. + var evaluatePath = function(obj, path) { + var parts = (path || '').split('.'); + var result = _.reduce(parts, function(memo, i) { return memo[i]; }, obj); + return result == null ? obj : result; + }; + + // If the given `fn` is a string, then view[fn] is called, otherwise it is + // a function that should be executed. + var applyViewFn = function(view, fn) { + if (fn) return (_.isString(fn) ? evaluatePath(view,fn) : fn).apply(view, _.rest(arguments, 2)); + }; + + var getSelectedOption = function($select) { return $select.find('option').not(function(){ return !this.selected; }); }; + + // Given a function, string (view function reference), or a boolean + // value, returns the truthy result. Any other types evaluate as false. + var evaluateBoolean = function(view, reference) { + if (_.isBoolean(reference)) return reference; + else if (_.isFunction(reference) || _.isString(reference)) + return applyViewFn.apply(this, arguments); + return false; + }; + + // Setup a model event binding with the given function, and track the event + // in the view's _modelBindings. + var observeModelEvent = function(model, view, event, config, fn) { + model.on(event, fn, view); + view._modelBindings.push({model:model, event:event, fn:fn, config:config}); + }; + + // Prepares the given `val`ue and sets it into the `model`. + var setAttr = function(model, attr, val, options, context, config) { + var value = {}; + if (config.onSet) + val = applyViewFn(context, config.onSet, val, config); + + if (config.set) + applyViewFn(context, config.set, attr, val, options, config); + else { + value[attr] = val; + // If `observe` is defined as an array and `onSet` returned + // an array, then map attributes to their values. + if (_.isArray(attr) && _.isArray(val)) { + value = _.reduce(attr, function(memo, attribute, index) { + memo[attribute] = _.has(val, index) ? val[index] : null; + return memo; + }, {}); + } + model.set(value, options); + } + }; + + // Returns the given `attr`'s value from the `model`, escaping and + // formatting if necessary. If `attr` is an array, then an array of + // respective values will be returned. + var getAttr = function(model, attr, config, context) { + var val, + retrieveVal = function(field) { + return model[config.escape ? 'escape' : 'get'](field); + }, + sanitizeVal = function(val) { + return val == null ? '' : val; + }; + val = _.isArray(attr) ? _.map(attr, retrieveVal) : retrieveVal(attr); + if (config.onGet) val = applyViewFn(context, config.onGet, val, config); + return _.isArray(val) ? _.map(val, sanitizeVal) : sanitizeVal(val); + }; + + // Find handlers in `Backbone.Stickit._handlers` with selectors that match + // `$el` and generate a configuration by mixing them in the order that they + // were found with the given `binding`. + var getConfiguration = Stickit.getConfiguration = function($el, binding) { + var handlers = [{ + updateModel: false, + updateMethod: 'text', + update: function($el, val, m, opts) { if ($el[opts.updateMethod]) $el[opts.updateMethod](val); }, + getVal: function($el, e, opts) { return $el[opts.updateMethod](); } + }]; + handlers = handlers.concat(_.filter(Stickit._handlers, function(handler) { + return $el.is(handler.selector); + })); + handlers.push(binding); + var config = _.extend.apply(_, handlers); + // `updateView` is defaulted to false for configutrations with + // `visible`; otherwise, `updateView` is defaulted to true. + if (config.visible && !_.has(config, 'updateView')) config.updateView = false; + else if (!_.has(config, 'updateView')) config.updateView = true; + return config; + }; + + // Setup the attributes configuration - a list that maps an attribute or + // property `name`, to an `observe`d model attribute, using an optional + // `onGet` formatter. + // + // attributes: [{ + // name: 'attributeOrPropertyName', + // observe: 'modelAttrName' + // onGet: function(modelAttrVal, modelAttrName) { ... } + // }, ...] + // + var initializeAttributes = function(view, $el, config, model, modelAttr) { + var props = ['autofocus', 'autoplay', 'async', 'checked', 'controls', 'defer', 'disabled', 'hidden', 'indeterminate', 'loop', 'multiple', 'open', 'readonly', 'required', 'scoped', 'selected']; + + _.each(config.attributes || [], function(attrConfig) { + var lastClass = '', observed, updateAttr; + attrConfig = _.clone(attrConfig); + observed = attrConfig.observe || (attrConfig.observe = modelAttr), + updateAttr = function() { + var updateType = _.indexOf(props, attrConfig.name, true) > -1 ? 'prop' : 'attr', + val = getAttr(model, observed, attrConfig, view); + // If it is a class then we need to remove the last value and add the new. + if (attrConfig.name === 'class') { + $el.removeClass(lastClass).addClass(val); + lastClass = val; + } + else $el[updateType](attrConfig.name, val); + }; + _.each(_.flatten([observed]), function(attr) { + observeModelEvent(model, view, 'change:' + attr, config, updateAttr); + }); + updateAttr(); + }); + }; + + // If `visible` is configured, then the view element will be shown/hidden + // based on the truthiness of the modelattr's value or the result of the + // given callback. If a `visibleFn` is also supplied, then that callback + // will be executed to manually handle showing/hiding the view element. + // + // observe: 'isRight', + // visible: true, // or function(val, options) {} + // visibleFn: function($el, isVisible, options) {} // optional handler + // + var initializeVisible = function(view, $el, config, model, modelAttr) { + if (config.visible == null) return; + var visibleCb = function() { + var visible = config.visible, + visibleFn = config.visibleFn, + val = getAttr(model, modelAttr, config, view), + isVisible = !!val; + // If `visible` is a function then it should return a boolean result to show/hide. + if (_.isFunction(visible) || _.isString(visible)) isVisible = !!applyViewFn(view, visible, val, config); + // Either use the custom `visibleFn`, if provided, or execute the standard show/hide. + if (visibleFn) applyViewFn(view, visibleFn, $el, isVisible, config); + else { + $el.toggle(isVisible); + } + }; + _.each(_.flatten([modelAttr]), function(attr) { + observeModelEvent(model, view, 'change:' + attr, config, visibleCb); + }); + visibleCb(); + }; + + // Update the value of `$el` using the given configuration and trigger the + // `afterUpdate` callback. This action may be blocked by `config.updateView`. + // + // update: function($el, val, model, options) {}, // handler for updating + // updateView: true, // defaults to true + // afterUpdate: function($el, val, options) {} // optional callback + // + var updateViewBindEl = function(view, $el, config, val, model, isInitializing) { + if (!evaluateBoolean(view, config.updateView, val, config)) return; + applyViewFn(view, config.update, $el, val, model, config); + if (!isInitializing) applyViewFn(view, config.afterUpdate, $el, val, config); + }; + + // Default Handlers + // ---------------- + + Stickit.addHandler([{ + selector: '[contenteditable="true"]', + updateMethod: 'html', + events: ['input', 'change'] + }, { + selector: 'input', + events: ['propertychange', 'input', 'change'], + update: function($el, val) { $el.val(val); }, + getVal: function($el) { + return $el.val(); + } + }, { + selector: 'textarea', + events: ['propertychange', 'input', 'change'], + update: function($el, val) { $el.val(val); }, + getVal: function($el) { return $el.val(); } + }, { + selector: 'input[type="radio"]', + events: ['change'], + update: function($el, val) { + $el.filter('[value="'+val+'"]').prop('checked', true); + }, + getVal: function($el) { + return $el.filter(':checked').val(); + } + }, { + selector: 'input[type="checkbox"]', + events: ['change'], + update: function($el, val, model, options) { + if ($el.length > 1) { + // There are multiple checkboxes so we need to go through them and check + // any that have value attributes that match what's in the array of `val`s. + val || (val = []); + $el.each(function(i, el) { + var checkbox = Backbone.$(el); + var checked = _.indexOf(val, checkbox.val()) > -1; + checkbox.prop('checked', checked); + }); + } else { + var checked = _.isBoolean(val) ? val : val === $el.val(); + $el.prop('checked', checked); + } + }, + getVal: function($el) { + var val; + if ($el.length > 1) { + val = _.reduce($el, function(memo, el) { + var checkbox = Backbone.$(el); + if (checkbox.prop('checked')) memo.push(checkbox.val()); + return memo; + }, []); + } else { + val = $el.prop('checked'); + // If the checkbox has a value attribute defined, then + // use that value. Most browsers use "on" as a default. + var boxval = $el.val(); + if (boxval !== 'on' && boxval != null) { + val = val ? $el.val() : null; + } + } + return val; + } + }, { + selector: 'select', + events: ['change'], + update: function($el, val, model, options) { + var optList, + selectConfig = options.selectOptions, + list = selectConfig && selectConfig.collection || undefined, + isMultiple = $el.prop('multiple'); + + // If there are no `selectOptions` then we assume that the `