From d566f93b2abfcd9ffd30fc74458d98ab2f489928 Mon Sep 17 00:00:00 2001 From: geta6 Date: Sat, 16 Nov 2013 22:24:00 +0900 Subject: [PATCH] backbone.stickit - v0.6.3 Source taken from https://github.com/NYTimes/backbone.stickit --- .../0.6.3/backbone.stickit.js | 505 ++++++++++++++++++ .../0.6.3/backbone.stickit.min.js | 6 + ajax/libs/backbone.stickit/package.json | 33 ++ 3 files changed, 544 insertions(+) create mode 100644 ajax/libs/backbone.stickit/0.6.3/backbone.stickit.js create mode 100644 ajax/libs/backbone.stickit/0.6.3/backbone.stickit.min.js create mode 100644 ajax/libs/backbone.stickit/package.json diff --git a/ajax/libs/backbone.stickit/0.6.3/backbone.stickit.js b/ajax/libs/backbone.stickit/0.6.3/backbone.stickit.js new file mode 100644 index 000000000..33135ac2a --- /dev/null +++ b/ajax/libs/backbone.stickit/0.6.3/backbone.stickit.js @@ -0,0 +1,505 @@ +// +// backbone.stickit - v0.6.3 +// The MIT License +// Copyright (c) 2012 The New York Times, CMS Group, Matthew DeLambo +// +(function(Backbone) { + var $ = Backbone.$ || window.jQuery || window.Zepto; + + // Backbone.Stickit Namespace + // -------------------------- + + Backbone.Stickit = { + + _handlers: [], + + 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 + // -------------------- + + _.extend(Backbone.View.prototype, { + + // Collection of model event bindings. + // [{model,event,fn}, ...] + _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) { + var models = []; + _.each(this._modelBindings, function(binding, i) { + if (model && binding.model !== model) return false; + 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); + // Cleanup the null values. + this._modelBindings = _.compact(this._modelBindings); + + this.$el.off('.stickit' + (model ? '.' + model.cid : '')); + }, + + // 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, + namespace = '.stickit.' + model.cid, + bindings = optionalBindingsConfig || this.bindings || {}; + + this._modelBindings || (this._modelBindings = []); + this.unstickit(model); + + // Iterate through the selectors in the bindings configuration and configure + // the various options for each field. + _.each(bindings, function(v, selector) { + var $el, options, modelAttr, config, + binding = bindings[selector] || {}, + bindId = _.uniqueId(); + + // Support ':el' selector - special case selector for the view managed delegate. + $el = selector === ':el' ? this.$el : this.$(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); + + 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); + + 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, config)) + setAttr(model, modelAttr, val, options, this, config); + }; + 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, 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); + } + + model.once('stickit:unstuck', function(cid) { + if (cid === this.cid) applyViewFn(this, config.destroy, $el, model, config); + }, this); + + // After each binding is setup, call the `initialize` callback. + applyViewFn(this, config.initialize, $el, model, config); + }, this); + + // Wrap `view.remove` to unbind stickit model and dom events. + var remove = this.remove; + this.remove = function() { + var ret = this; + this.unstickit(); + if (remove) ret = remove.apply(this, _.rest(arguments)); + return ret; + }; + } + }); + + // 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) ? 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, fn) { + model.on(event, fn, view); + view._modelBindings.push({model:model, event:event, fn:fn}); + }; + + // Prepares the given `val`ue and sets it into the `model`. + var setAttr = function(model, attr, val, options, context, config) { + if (config.onSet) val = applyViewFn(context, config.onSet, val, config); + model.set(attr, val, 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 = Backbone.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(Backbone.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; + delete config.selector; + 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', '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, 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, 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 + // ---------------- + + Backbone.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 = []); + _.each($el, function(el) { + if (_.indexOf(val, $(el).val()) > -1) $(el).prop('checked', true); + else $(el).prop('checked', false); + }); + } else { + if (_.isBoolean(val)) $el.prop('checked', val); + else $el.prop('checked', val === $el.val()); + } + }, + getVal: function($el) { + var val; + if ($el.length > 1) { + val = _.reduce($el, function(memo, el) { + if ($(el).prop('checked')) memo.push($(el).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 `