mirror of
https://github.com/wahyd4/cdnjs.git
synced 2026-08-15 15:56:28 +10:00
Merge pull request #2151 from geta6/master
Add backbone.stickit - v0.6.3
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
//
|
||||
// backbone.stickit - v0.6.3
|
||||
// The MIT License
|
||||
// Copyright (c) 2012 The New York Times, CMS Group, Matthew DeLambo <delambo@gmail.com>
|
||||
//
|
||||
(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 `<select>`
|
||||
// is pre-rendered and that we need to generate the collection.
|
||||
if (!selectConfig) {
|
||||
selectConfig = {};
|
||||
var getList = function($el) {
|
||||
return $el.map(function() {
|
||||
return {value:this.value, label:this.text};
|
||||
}).get();
|
||||
};
|
||||
if ($el.find('optgroup').length) {
|
||||
list = {opt_labels:[]};
|
||||
// Search for options without optgroup
|
||||
if ($el.find('> option').length) {
|
||||
list.opt_labels.push(undefined);
|
||||
_.each($el.find('> option'), function(el) {
|
||||
list[undefined] = getList($(el));
|
||||
});
|
||||
}
|
||||
_.each($el.find('optgroup'), function(el) {
|
||||
var label = $(el).attr('label');
|
||||
list.opt_labels.push(label);
|
||||
list[label] = getList($(el).find('option'));
|
||||
});
|
||||
} else {
|
||||
list = getList($el.find('option'));
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in default label and path values.
|
||||
selectConfig.valuePath = selectConfig.valuePath || 'value';
|
||||
selectConfig.labelPath = selectConfig.labelPath || 'label';
|
||||
|
||||
var addSelectOptions = function(optList, $el, fieldVal) {
|
||||
_.each(optList, function(obj) {
|
||||
var option = $('<option/>'), optionVal = obj;
|
||||
|
||||
var fillOption = function(text, val) {
|
||||
option.text(text);
|
||||
optionVal = val;
|
||||
// Save the option value as data so that we can reference it later.
|
||||
option.data('stickit_bind_val', optionVal);
|
||||
if (!_.isArray(optionVal) && !_.isObject(optionVal)) option.val(optionVal);
|
||||
};
|
||||
|
||||
if (obj === '__default__')
|
||||
fillOption(selectConfig.defaultOption.label, selectConfig.defaultOption.value);
|
||||
else
|
||||
fillOption(evaluatePath(obj, selectConfig.labelPath), evaluatePath(obj, selectConfig.valuePath));
|
||||
|
||||
// Determine if this option is selected.
|
||||
if (!isMultiple && optionVal != null && fieldVal != null && optionVal === fieldVal || (_.isObject(fieldVal) && _.isEqual(optionVal, fieldVal)))
|
||||
option.prop('selected', true);
|
||||
else if (isMultiple && _.isArray(fieldVal)) {
|
||||
_.each(fieldVal, function(val) {
|
||||
if (_.isObject(val)) val = evaluatePath(val, selectConfig.valuePath);
|
||||
if (val === optionVal || (_.isObject(val) && _.isEqual(optionVal, val)))
|
||||
option.prop('selected', true);
|
||||
});
|
||||
}
|
||||
|
||||
$el.append(option);
|
||||
});
|
||||
};
|
||||
|
||||
$el.html('');
|
||||
|
||||
// The `list` configuration is a function that returns the options list or a string
|
||||
// which represents the path to the list relative to `window` or the view/`this`.
|
||||
var evaluate = function(view, list) {
|
||||
var context = window;
|
||||
if (list.indexOf('this.') === 0) context = view;
|
||||
list = list.replace(/^[a-z]*\.(.+)$/, '$1');
|
||||
return evaluatePath(context, list);
|
||||
};
|
||||
if (_.isString(list)) optList = evaluate(this, list);
|
||||
else if (_.isFunction(list)) optList = applyViewFn(this, list, $el, options);
|
||||
else optList = list;
|
||||
|
||||
// Support Backbone.Collection and deserialize.
|
||||
if (optList instanceof Backbone.Collection) optList = optList.toJSON();
|
||||
|
||||
if (selectConfig.defaultOption) {
|
||||
addSelectOptions(["__default__"], $el);
|
||||
}
|
||||
|
||||
if (_.isArray(optList)) {
|
||||
addSelectOptions(optList, $el, val);
|
||||
} else if (optList.opt_labels) {
|
||||
// To define a select with optgroups, format selectOptions.collection as an object
|
||||
// with an 'opt_labels' property, as in the following:
|
||||
//
|
||||
// {
|
||||
// 'opt_labels': ['Looney Tunes', 'Three Stooges'],
|
||||
// 'Looney Tunes': [{id: 1, name: 'Bugs Bunny'}, {id: 2, name: 'Donald Duck'}],
|
||||
// 'Three Stooges': [{id: 3, name : 'moe'}, {id: 4, name : 'larry'}, {id: 5, name : 'curly'}]
|
||||
// }
|
||||
//
|
||||
_.each(optList.opt_labels, function(label) {
|
||||
var $group = $('<optgroup/>').attr('label', label);
|
||||
addSelectOptions(optList[label], $group, val);
|
||||
$el.append($group);
|
||||
});
|
||||
// With no 'opt_labels' parameter, the object is assumed to be a simple value-label map.
|
||||
// Pass a selectOptions.comparator to override the default order of alphabetical by label.
|
||||
} else {
|
||||
var opts = [], opt;
|
||||
for (var i in optList) {
|
||||
opt = {};
|
||||
opt[selectConfig.valuePath] = i;
|
||||
opt[selectConfig.labelPath] = optList[i];
|
||||
opts.push(opt);
|
||||
}
|
||||
addSelectOptions(_.sortBy(opts, selectConfig.comparator || selectConfig.labelPath), $el, val);
|
||||
}
|
||||
},
|
||||
getVal: function($el) {
|
||||
var val;
|
||||
if ($el.prop('multiple')) {
|
||||
val = $(getSelectedOption($el).map(function() {
|
||||
return $(this).data('stickit_bind_val');
|
||||
})).get();
|
||||
} else {
|
||||
val = getSelectedOption($el).data('stickit_bind_val');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}]);
|
||||
|
||||
})(Backbone);
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "backbone.stickit",
|
||||
"filename": "backbone.stickit.min.js",
|
||||
"description": "Model binding in Backbone style.",
|
||||
"version": "0.6.3",
|
||||
"author": "Matthew DeLambo <delambo@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/nytimes/backbone.stickit"
|
||||
},
|
||||
"main": "backbone.stickit.js",
|
||||
"dependencies": {
|
||||
"backbone": "~0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt-contrib": "~0.4.0",
|
||||
"grunt-docco": "~0.1.2",
|
||||
"grunt": "~0.4.0",
|
||||
"grunt-contrib-concat": "~0.1.3",
|
||||
"grunt-contrib-jshint": "~0.2.0",
|
||||
"grunt-contrib-qunit": "~0.2.0",
|
||||
"grunt-contrib-uglify": "~0.1.2",
|
||||
"grunt-contrib-clean": "~0.4.0",
|
||||
"grunt-contrib-compress": "~0.4.1"
|
||||
},
|
||||
"bugs": "http://github.com/nytimes/backbone.stickit/issues",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/nytimes/backbone.stickit/blob/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user