// Backbone.ModelBinder v1.1.1 // (c) 2020 Bart Wood // Distributed Under MIT License (function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['underscore', 'jquery', 'backbone'], factory); } else if(typeof module !== 'undefined' && module.exports) { // CommonJS module.exports = factory( require('underscore'), require('jquery'), require('backbone') ); } else { // Browser globals factory(_, jQuery, Backbone); } }(function(_, $, Backbone){ if(!Backbone){ throw new Error('Please include Backbone.js before Backbone.ModelBinder.js'); } Backbone.ModelBinder = function(){ _.bindAll.apply(_, [this].concat(_.functions(this))); }; // Static setter for class level options Backbone.ModelBinder.SetOptions = function(options){ Backbone.ModelBinder.options = options; }; // Current version of the library. Backbone.ModelBinder.VERSION = '1.1.1'; Backbone.ModelBinder.Constants = {}; Backbone.ModelBinder.Constants.ModelToView = 'ModelToView'; Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel'; _.extend(Backbone.ModelBinder.prototype, { bind:function (model, rootEl, attributeBindings, options) { this.unbind(); this._model = model; this._rootEl = rootEl; this._setOptions(options); if (!this._model) this._throwException('model must be specified'); if (!this._rootEl) this._throwException('rootEl must be specified'); if(attributeBindings){ // Create a deep clone of the attribute bindings this._attributeBindings = $.extend(true, {}, attributeBindings); this._initializeAttributeBindings(); this._initializeElBindings(); } else { this._initializeDefaultBindings(); } this._bindModelToView(); this._bindViewToModel(); }, bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) { this._triggers = triggers; this.bind(model, rootEl, attributeBindings, modelSetOptions); }, unbind:function () { this._unbindModelToView(); this._unbindViewToModel(); if(this._attributeBindings){ delete this._attributeBindings; this._attributeBindings = undefined; } }, _setOptions: function(options){ this._options = _.extend({ boundAttribute: 'name' }, Backbone.ModelBinder.options, options); // initialize default options if(!this._options['modelSetOptions']){ this._options['modelSetOptions'] = {}; } this._options['modelSetOptions'].changeSource = 'ModelBinder'; if(!this._options['changeTriggers']){ this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'}; } if(!this._options['initialCopyDirection']){ this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView; } }, // Converts the input bindings, which might just be empty or strings, to binding objects _initializeAttributeBindings:function () { var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding; for (attributeBindingKey in this._attributeBindings) { inputBinding = this._attributeBindings[attributeBindingKey]; if (_.isString(inputBinding)) { attributeBinding = {elementBindings: [{selector: inputBinding}]}; } else if (_.isArray(inputBinding)) { attributeBinding = {elementBindings: inputBinding}; } else if(_.isObject(inputBinding)){ attributeBinding = {elementBindings: [inputBinding]}; } else { this._throwException('Unsupported type passed to Model Binder ' + attributeBinding); } // Add a linkage from the element binding back to the attribute binding for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){ elementBinding = attributeBinding.elementBindings[elementBindingCount]; elementBinding.attributeBinding = attributeBinding; } attributeBinding.attributeName = attributeBindingKey; this._attributeBindings[attributeBindingKey] = attributeBinding; } }, // If the bindings are not specified, the default binding is performed on the specified attribute, name by default _initializeDefaultBindings: function(){ var elCount, elsWithAttribute, matchedEl, name, attributeBinding; this._attributeBindings = {}; elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl); for(elCount = 0; elCount < elsWithAttribute.length; elCount++){ matchedEl = elsWithAttribute[elCount]; name = $(matchedEl).attr(this._options['boundAttribute']); // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings if(!this._attributeBindings[name]){ attributeBinding = {attributeName: name}; attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}]; this._attributeBindings[name] = attributeBinding; } else{ this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]}); } } }, _initializeElBindings:function () { var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el; for (bindingKey in this._attributeBindings) { attributeBinding = this._attributeBindings[bindingKey]; for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { elementBinding = attributeBinding.elementBindings[bindingCount]; if (elementBinding.selector === '') { foundEls = $(this._rootEl); } else { foundEls = $(elementBinding.selector, this._rootEl); } if (foundEls.length === 0) { this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector); } else { elementBinding.boundEls = []; for (elCount = 0; elCount < foundEls.length; elCount++) { el = foundEls[elCount]; elementBinding.boundEls.push(el); } } } } }, _bindModelToView: function () { this._model.on('change', this._onModelChange, this); if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){ this.copyModelAttributesToView(); } }, // attributesToCopy is an optional parameter - if empty, all attributes // that are bound will be copied. Otherwise, only attributeBindings specified // in the attributesToCopy are copied. copyModelAttributesToView: function(attributesToCopy){ var attributeName, attributeBinding; for (attributeName in this._attributeBindings) { if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){ attributeBinding = this._attributeBindings[attributeName]; this._copyModelToView(attributeBinding); } } }, copyViewValuesToModel: function(){ var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el; for (bindingKey in this._attributeBindings) { attributeBinding = this._attributeBindings[bindingKey]; for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { elementBinding = attributeBinding.elementBindings[bindingCount]; if(this._isBindingUserEditable(elementBinding)){ if(this._isBindingRadioGroup(elementBinding)){ el = this._getRadioButtonGroupCheckedEl(elementBinding); if(el){ this._copyViewToModel(elementBinding, el); } } else { for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){ el = $(elementBinding.boundEls[elCount]); if(this._isElUserEditable(el)){ this._copyViewToModel(elementBinding, el); } } } } } } }, _unbindModelToView: function(){ if(this._model){ this._model.off('change', this._onModelChange); this._model = undefined; } }, _bindViewToModel: function () { _.each(this._options['changeTriggers'], function (event, selector) { $(this._rootEl).on(event, selector, this._onElChanged); }, this); if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){ this.copyViewValuesToModel(); } }, _unbindViewToModel: function () { if(this._options && this._options['changeTriggers']){ _.each(this._options['changeTriggers'], function (event, selector) { $(this._rootEl).off(event, selector, this._onElChanged); }, this); } }, _onElChanged:function (event) { var el, elBindings, elBindingCount, elBinding; el = $(event.target)[0]; elBindings = this._getElBindings(el); for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){ elBinding = elBindings[elBindingCount]; if (this._isBindingUserEditable(elBinding)) { this._copyViewToModel(elBinding, el); } } }, _isBindingUserEditable: function(elBinding){ return elBinding.elAttribute === undefined || elBinding.elAttribute === 'text' || elBinding.elAttribute === 'html'; }, _isElUserEditable: function(el){ var isContentEditable = el.attr('contenteditable'); return isContentEditable || el.is('input') || el.is('select') || el.is('textarea'); }, _isBindingRadioGroup: function(elBinding){ var elCount, el; var isAllRadioButtons = elBinding.boundEls.length > 0; for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ el = $(elBinding.boundEls[elCount]); if(el.attr('type') !== 'radio'){ isAllRadioButtons = false; break; } } return isAllRadioButtons; }, _getRadioButtonGroupCheckedEl: function(elBinding){ var elCount, el; for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ el = $(elBinding.boundEls[elCount]); if(el.attr('type') === 'radio' && el.prop('checked')){ return el; } } return undefined; }, _getElBindings:function (findEl) { var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl; var elBindings = []; for (attributeName in this._attributeBindings) { attributeBinding = this._attributeBindings[attributeName]; for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { elementBinding = attributeBinding.elementBindings[elementBindingCount]; for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { boundEl = elementBinding.boundEls[boundElCount]; if (boundEl === findEl) { elBindings.push(elementBinding); } } } } return elBindings; }, _onModelChange:function () { var changedAttribute, attributeBinding; for (changedAttribute in this._model.changedAttributes()) { attributeBinding = this._attributeBindings[changedAttribute]; if (attributeBinding) { this._copyModelToView(attributeBinding); } } }, _copyModelToView:function (attributeBinding) { var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue; value = this._model.get(attributeBinding.attributeName); for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { elementBinding = attributeBinding.elementBindings[elementBindingCount]; for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { boundEl = elementBinding.boundEls[boundElCount]; if(!boundEl._isSetting){ convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); this._setEl($(boundEl), elementBinding, convertedValue); } } } }, _setEl: function (el, elementBinding, convertedValue) { if (elementBinding.elAttribute) { this._setElAttribute(el, elementBinding, convertedValue); } else { this._setElValue(el, convertedValue); } }, _setElAttribute:function (el, elementBinding, convertedValue) { switch (elementBinding.elAttribute) { case 'html': el.html(convertedValue); break; case 'text': el.text(convertedValue); break; case 'enabled': el.prop('disabled', !convertedValue); break; case 'displayed': el[convertedValue ? 'show' : 'hide'](); break; case 'hidden': el[convertedValue ? 'hide' : 'show'](); break; case 'css': el.css(elementBinding.cssAttribute, convertedValue); break; case 'class': var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName); var currentValue = this._model.get(elementBinding.attributeBinding.attributeName); // is current value is now defined then remove the class the may have been set for the undefined value if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){ previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue); el.removeClass(previousValue); } if(convertedValue){ el.addClass(convertedValue); } break; default: el.attr(elementBinding.elAttribute, convertedValue); } }, _setElValue:function (el, convertedValue) { if(el.attr('type')){ switch (el.attr('type')) { case 'radio': el.prop('checked', el.val() === convertedValue); break; case 'checkbox': el.prop('checked', !!convertedValue); break; case 'file': break; default: el.val(convertedValue); } } else if(el.is('input') || el.is('select') || el.is('textarea')){ el.val(convertedValue || (convertedValue === 0 ? '0' : '')); } else { el.text(convertedValue || (convertedValue === 0 ? '0' : '')); } }, _copyViewToModel: function (elementBinding, el) { var result, value, convertedValue; if (!el._isSetting) { el._isSetting = true; result = this._setModel(elementBinding, $(el)); el._isSetting = false; if(result && elementBinding.converter){ value = this._model.get(elementBinding.attributeBinding.attributeName); convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); this._setEl($(el), elementBinding, convertedValue); } } }, _getElValue: function(elementBinding, el){ switch (el.attr('type')) { case 'checkbox': return el.prop('checked') ? true : false; default: if(el.attr('contenteditable') !== undefined){ return el.html(); } else { return el.val(); } } }, _setModel: function (elementBinding, el) { var data = {}; var elVal = this._getElValue(elementBinding, el); elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal); data[elementBinding.attributeBinding.attributeName] = elVal; return this._model.set(data, this._options['modelSetOptions']); }, _getConvertedValue: function (direction, elementBinding, value) { if (elementBinding.converter) { value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); } else if(this._options['converter']){ value = this._options['converter'](direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); } return value; }, _throwException: function(message){ if(this._options.suppressThrows){ if(typeof(console) !== 'undefined' && console.error){ console.error(message); } } else { throw new Error(message); } } }); Backbone.ModelBinder.CollectionConverter = function(collection){ this._collection = collection; if(!this._collection){ throw new Error('Collection must be defined'); } _.bindAll(this, 'convert'); }; _.extend(Backbone.ModelBinder.CollectionConverter.prototype, { convert: function(direction, value){ if (direction === Backbone.ModelBinder.Constants.ModelToView) { return value ? value.id : undefined; } else { return this._collection.get(value); } } }); // A static helper function to create a default set of bindings that you can customize before calling the bind() function // rootEl - where to find all of the bound elements // attributeType - probably 'name' or 'id' in most cases // converter(optional) - the default converter you want applied to all your bindings // elAttribute(optional) - the default elAttribute you want applied to all your bindings Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){ var foundEls, elCount, foundEl, attributeName; var bindings = {}; foundEls = $('[' + attributeType + ']', rootEl); for(elCount = 0; elCount < foundEls.length; elCount++){ foundEl = foundEls[elCount]; attributeName = $(foundEl).attr(attributeType); if(!bindings[attributeName]){ var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'}; bindings[attributeName] = attributeBinding; if(converter){ bindings[attributeName].converter = converter; } if(elAttribute){ bindings[attributeName].elAttribute = elAttribute; } } } return bindings; }; // Helps you to combine 2 sets of bindings Backbone.ModelBinder.combineBindings = function(destination, source){ _.each(source, function(value, key){ var elementBinding = {selector: value.selector}; if(value.converter){ elementBinding.converter = value.converter; } if(value.elAttribute){ elementBinding.elAttribute = value.elAttribute; } if(!destination[key]){ destination[key] = elementBinding; } else { destination[key] = [destination[key], elementBinding]; } }); return destination; }; return Backbone.ModelBinder; }));