// Backbone.Declarative.Views, v4.1.2 // Copyright (c) 2014-2017 Michael Heim, Zeilenwechsel.de // Distributed under MIT license // http://github.com/hashchange/backbone.declarative.views ;( function ( root, factory ) { "use strict"; // UMD for a Backbone plugin. Supports AMD, Node.js, CommonJS and globals. // // - Code lives in the Backbone namespace. // - The module does not export a meaningful value. // - The module does not create a global. var supportsExports = typeof exports === "object" && exports && !exports.nodeType && typeof module === "object" && module && !module.nodeType; // AMD: // - Some AMD build optimizers like r.js check for condition patterns like the AMD check below, so keep it as is. // - Check for `exports` after `define` in case a build optimizer adds an `exports` object. // - The AMD spec requires the dependencies to be an array **literal** of module IDs. Don't use a variable there, // or optimizers may fail. if ( typeof define === "function" && typeof define.amd === "object" && define.amd ) { // AMD module define( [ "exports", "underscore", "backbone" ], factory ); } else if ( supportsExports ) { // Node module, CommonJS module factory( exports, require( "underscore" ), require( "backbone" ) ); } else { // Global (browser or Rhino) factory( {}, _, Backbone ); } }( this, function ( exports, _, Backbone ) { "use strict"; var originalClearCache, // for Marionette only originalConstructor = Backbone.View, templateCache = {}, instanceCacheAliases = [], enforceTemplateLoading = false, isMarionetteInitialized = false, registeredDataAttributes = { primitives: [], json: [] }, rxElDefinitionComment, rxRegisteredDataAttributes = {}, GenericError = createCustomErrorType( "Backbone.DeclarativeViews.Error" ), TemplateError = createCustomErrorType( "Backbone.DeclarativeViews.TemplateError" ), CompilerError = createCustomErrorType( "Backbone.DeclarativeViews.CompilerError" ), CustomizationError = createCustomErrorType( "Backbone.DeclarativeViews.CustomizationError" ), ConfigurationError = createCustomErrorType( "Backbone.DeclarativeViews.ConfigurationError" ), events = _.clone( Backbone.Events ), $ = Backbone.$; // // Core functionality and API // -------------------------- _.extend( Backbone.View.prototype, { tagName: function () { var data = getViewTemplateData( this ) || {}; return data.tagName || "div"; }, className: function () { var data = getViewTemplateData( this ) || {}; return data.className || undefined; }, id: function () { var data = getViewTemplateData( this ) || {}; return data.id || undefined; }, attributes: function () { var data = getViewTemplateData( this ) || {}; return data.attributes || undefined; } } ); Backbone.View = Backbone.View.extend( { constructor: function ( options ) { if ( options && options.template !== undefined ) this.template = options.template; this.declarativeViews = { meta: { viewId: _.uniqueId( "view-" ) }, getCachedTemplate: _.partial( getViewTemplateData, this ), clearCachedTemplate: _.partial( clearViewTemplateCache, this ) }; _.each( instanceCacheAliases, function ( alias ) { this[alias] = this.declarativeViews; }, this ); if ( enforceTemplateLoading ) getViewTemplateData( this, options ); originalConstructor.apply( this, arguments ); } } ); Backbone.DeclarativeViews = { getCachedTemplate: getTemplateData, clearCachedTemplate: clearCachedTemplate, clearCache: clearCache, joinMarionette: joinMarionette, Error: GenericError, TemplateError: TemplateError, CompilerError: CompilerError, CustomizationError: CustomizationError, ConfigurationError: ConfigurationError, plugins: { registerDataAttribute: _registerDataAttribute, getDataAttributes: _getDataAttributes, updateJqueryDataCache: _updateJQueryDataCache, registerCacheAlias: _registerCacheAlias, enforceTemplateLoading: _enforceTemplateLoading, tryCompileTemplate: _tryCompileTemplate, events: events }, defaults: { loadTemplate: loadTemplate }, custom: { /** @type {Function|undefined} */ loadTemplate: undefined, /** @type {Function|undefined} */ compiler: undefined }, version: "4.1.2" }; // // Initialization // -------------- _registerDataAttribute( "tag-name" ); _registerDataAttribute( "class-name" ); _registerDataAttribute( "id" ); _registerDataAttribute( "attributes", { isJSON: true } ); // // Cache management // ---------------- /** * Returns the template data associated with a template property string. Caches it in the process, or retrieves it * from the cache if already available. Returns undefined if there is no cacheable template data. * * When retrieved from the cache, a copy of the cache entry is returned. It protects the cache entry from * modification in case the data is manipulated later on. The protection also extends to the nested `attributes` * hash. The `_pluginData` property, however, must remain writable, and is returned as is. * * The template data is returned as a hash. For a list of properties, see readme. * * @param {string} templateProp template selector, or raw template HTML, identifying the cache entry * @param {Backbone.View} [view] the view which requested the template * @param {Object} [viewOptions] the options passed to the view during instantiation. For availability, * see getViewTemplateData() * @returns {CachedTemplateData|undefined} */ function getTemplateData ( templateProp, view, viewOptions ) { var data; if ( templateProp && _.isString( templateProp ) ) { data = templateCache[ templateProp ]; if ( ! data ) data = _createTemplateCache( templateProp, view, viewOptions ); if ( data.invalid ) data = undefined; if ( data ) data = _copyCacheEntry( data ); } return data; } /** * Returns the template data associated with a given view, provided that the template is set to a non-empty string. * Otherwise, it returns undefined. Manages caching behind the scenes. * * The template data is returned as a hash. For a list of properties, see readme. * * Events * ------ * * The method fires two events: * * - cacheEntry:view:process * * Fires only once per view, on first access to the template from that particular view. Fires whether or not the * template is already in the cache. * * The event handler receives a **copy** of the returned cache data. Modifications of the data are ineffective, * they don't show up anywhere outside of the handler. * * (That is by design. When setting up the el of a view, the cache is accessed several times - once for each * el-related property. The handler would be able to modify the data during the very first access, when the * `attributes` property is requested, but not for `className`, `tagName`, `id`. That behaviour can be confusing * and cause bugs which are difficult to track down. Hence data modification is actively prevented even during * first access, making the behaviour consistent.) * * But there is an exception: the _pluginData property. If the handler needs to change or store data, it can write * to the _pluginData hash. Changes to the hash are persistent. They are stored in the original cache entry and * hence show up in every subsequent cache query for that entry. * * NB: When `el`-related properties from the cache, like tagName or html, need to be manipulated on the fly, it * must be done in a handler for another event: cacheEntry:view:fetch. That handler _is_ allowed to change the * returned data. It also has access to the _pluginData created during the cacheEntry:view:process event. * * - cacheEntry:view:fetch * * Fires every time data is requested from the cache in the context of a querying view. The event fires on first * access as well. On that occasion, it is preceded by the cacheEntry:view:process event. * * The event handler receives the returned cache data and has full access to it. If the handler modifies the data, * the modifications show up in the returned result. * * However, the original cache entry is protected from modification (with the exception of the _pluginData * property, see above), so changes made by the event handler do not alter the values stored in the cache. * * The events fire only if the cache is accessed with getViewTemplateData(), ie when the cache is queried from a * view: during view instantiation, or when called with `view.declarativeViews.getCachedTemplate()`. * * The events do **not** fire when the cache is queried from the global API, even if a view is provided as an * additional argument, as in `Backbone.DeclarativeViews.getCachedTemplate( "#template", view )`. * * @param {Backbone.View} view * @param {Object} [viewOptions] the options passed to the view during instantiation. Only available when * called during view instantiation, and only if the component has been * configured to enforce template loading, with _enforceTemplateLoading() * @returns {CachedTemplateData|undefined} */ function getViewTemplateData ( view, viewOptions ) { var data, meta = view.declarativeViews.meta; if ( ! meta.processed ) { if ( view.template && _.isString( view.template ) ) { meta.originalTemplateProp = view.template; data = getTemplateData( view.template, view, viewOptions ); meta.processed = true; meta.inGlobalCache = true; if ( data ) events.trigger( "cacheEntry:view:process", _copyCacheEntry( data ), meta.originalTemplateProp, view, viewOptions ); } else { data = undefined; meta.processed = true; meta.inGlobalCache = false; } } else { data = meta.inGlobalCache ? getTemplateData( meta.originalTemplateProp, view, viewOptions ) : undefined; } if ( data ) events.trigger( "cacheEntry:view:fetch", data, meta.originalTemplateProp, view, viewOptions ); return data; } /** * Clears the cache as a whole. * * Also clears the Marionette cache (if Marionette is available). * * @param {boolean} [fromMarionette=false] internal flag to prevent circular calls to and from Marionette */ function clearCache ( fromMarionette ) { templateCache = {}; if ( ! fromMarionette && Backbone.Marionette && Backbone.Marionette.TemplateCache ) Backbone.Marionette.TemplateCache.clear(); } /** * Removes one or more cache entries. * * Arguments * --------- * * The strings identifying the cache entries can be passed in as individual arguments (prop1, prop2, ...), or as an * array. Each string must be * * - a template selector * - raw HTML of a template, if that's what the template property held when a view made use of it. * * A template selector must be identical to the one which was used when creating the cache entry, ie the selector * specified in the template property or template option of a view. Mere selector equivalence (e.g. "#template" and * "script#template") won't match the cache. * * Strings not matching a cache entry are ignored, as are non-string arguments. * * Marionette.TemplateCache * ------------------------ * * When templates are cleared here, they are removed from the Marionette template cache as well (if Marionette is * loaded). * * @param {...string|string[]} [templateProp] template selector(s), or raw template HTML, identifying the cache * entry. NB The last argument can also be an internal "fromMarionette" * flag to prevent circular calls to and from Marionette */ function clearCachedTemplate ( templateProp ) { var fromMarionette = false, args = _.toArray( arguments ), lastArg = _.last( args ); // When called from Marionette, or called recursively, the last argument is a "fromMarionette" boolean. Splice // it off before proceeding. if ( args.length && _.isBoolean( lastArg ) ) fromMarionette = args.pop(); // Handle multiple template props passed in as a varargs list, or as an array, with recursive calls for each // template property. if ( args.length > 1 ) { _.each( args, function ( singleProp ) { clearCachedTemplate( singleProp, fromMarionette ); } ); } else if ( _.isArray( templateProp ) || _.isArguments( templateProp ) ) { _.each( templateProp, function ( singleProp ) { clearCachedTemplate( singleProp, fromMarionette ); } ); } else { if ( ! templateProp ) throw new GenericError( "Missing argument: string identifying the template. The string should be a template selector or the raw HTML of a template, as provided to the template property of a view when the cache entry was created" ); // Dealing with a single templateProp argument. // // Delete the corresponding cache entry. Try to clear it from the Marionette cache as well. The // templateProp must be a string - non-string arguments are quietly ignored. if ( _.isString( templateProp ) ) { _clearCachedTemplate( templateProp ); if ( ! fromMarionette && Backbone.Marionette && Backbone.Marionette.TemplateCache ) { try { Backbone.Marionette.TemplateCache.clear( templateProp ); } catch ( err ) {} } } } } /** * Removes the template cache entry associated with a given view, provided that a cache entry exists. * * @param {Backbone.View} view */ function clearViewTemplateCache ( view ) { var meta = view.declarativeViews.meta; if ( meta.processed ) { if ( meta.inGlobalCache ) _clearCachedTemplate( meta.originalTemplateProp ); } else if ( view.template && _.isString( view.template ) ) { _clearCachedTemplate( view.template ); } } /** * Defines the default template loader. Accepts a selector string and returns the template node (usually a <script> * or <template> node) in a jQuery wrapper. * * Is only ever called with a string argument. There is no need to handle other argument types here, or guard * against them. * * Interprets the argument as a selector first and returns the corresponding node if it exists. If not, the argument * is interpreted as a raw HTML/template string and wrapped in a script tag (of type text/x-template). If the raw * template string contains a comment describing the el, the related data attributes are created on the script tag. * * NB Raw template strings are never altered, and not interpreted (apart from looking for the el-related comment). * * @param {string} templateProperty * @returns {jQuery} */ function loadTemplate ( templateProperty ) { var $template; try { $template = $( templateProperty ); // If the template is not in the DOM, treat the template property as a raw template string instead. That // part is handled in `catch`, and should not be guarded against further errors here. To switch to that // process, just throw an error. if ( !$.contains( document.documentElement, $template[0] ) ) throw new Error(); } catch ( err ) { $template = _wrapRawTemplate( templateProperty ); // If the template string cannot be retrieved unaltered even after wrapping it in a script tag, bail out by // throwing a silent error (will be caught, and not propagated further, in _createTemplateCache()). if ( $template.html() !== templateProperty ) throw new Error( "Failed to wrap template string in script tag without altering it" ); } return $template; } /** * Takes a raw HTML/template string and wraps it in a script tag (of type "text/x-template"). In the process, it * detects el-related data attributes which are contained in an HTML comment, and sets them on the script tag. * Returns the script element, as a jQuery object. * * @param {string} templateString * @returns {jQuery} */ function _wrapRawTemplate( templateString ) { var $wrapper = $( "<script />" ) .attr( "type", "text/x-template" ) .text( templateString ), elDataAttributes = _getEmbeddedElAttributes( templateString ); if ( elDataAttributes ) $wrapper.attr( elDataAttributes ); return $wrapper; } /** * Takes a raw HTML/template string and looks for el-related data attributes which are contained in a comment. * Returns the attributes hash, or undefined if no attributes are found. * * The keys in the hash are the data attribute names, ie they include the "data-" prefix. * * @param {string} templateString * @returns {Object|undefined} */ function _getEmbeddedElAttributes ( templateString ) { var elDataAttributes = {}, elDefinitionMatch = rxElDefinitionComment.exec( templateString ), elDefinitionComment = elDefinitionMatch && elDefinitionMatch[0]; if ( elDefinitionComment ) { _.each( rxRegisteredDataAttributes, function ( rxAttributeMatcher, attributeName ) { var match = rxAttributeMatcher.exec( elDefinitionComment ), attributeValue = match && match[2]; if ( attributeValue ) elDataAttributes[attributeName] = attributeValue; } ); } return _.size( elDataAttributes ) ? elDataAttributes : undefined; } /** * Creates a copy of a cache entry and returns it. Protects the original cache entry from modification, except for * the _pluginData property, which remains writable and can be accessed from the copy. * * NB The `attribute` property is cloned and protected, too, if it exists. * * @param {CachedTemplateData} cacheEntry * @returns {CachedTemplateData} */ function _copyCacheEntry ( cacheEntry ) { var copy = _.clone( cacheEntry ); if ( _.isObject( copy.attributes ) ) copy.attributes = _.clone( copy.attributes ); return copy; } /** * Creates a cache entry for a given template property. * * Returns the cached entry if creating it has succeeded. In case of failure, it returns the hash { invalid: true }. * It signals that the template has been processed, but that the returned hash, as well as the cache itself, does * not contain valid data for the template property. * * The creation of a cache entry can fail if the template property is an empty string, or a selector which doesn't * match anything, or a string which jQuery can't process. * * Uses a custom loader if specified, instead of loading the template with jQuery (default). * * Events * ------ * * The method fires the following event: * * - cacheEntry:create * * Fires only once per cache entry, when a valid cache entry is created. That may happen in the context of a view, * or with a global query. * * The event handler receives the original cache entry. If the handler modifies the data, the modifications show * up in the returned result **and** alter the content in the cache. * * If a valid cache entry can't be created (flagged as invalid), the event does NOT fire. * * @param {string} templateProp template selector, or raw template HTML, identifying the cache entry * @param {Backbone.View} [view] the view which requested the template * @param {Object} [viewOptions] the options passed to the view during instantiation. For availability, * see getViewTemplateData() * @returns {CachedTemplateData|Uncacheable} */ function _createTemplateCache( templateProp, view, viewOptions ) { var $template, data, html, customLoader = Backbone.DeclarativeViews.custom.loadTemplate, defaultLoader = Backbone.DeclarativeViews.defaults.loadTemplate, modifiedDefaultLoader = defaultLoader !== loadTemplate, cacheId = templateProp; // Load the template try { $template = customLoader ? customLoader( templateProp, view, viewOptions ) : defaultLoader( templateProp, view, viewOptions ); } catch ( err ) { // Rethrow and exit if the alarm has been raised deliberately, using an error type of Backbone.DeclarativeViews. if( _isDeclarativeViewsErrorType( err ) ) throw err; // Otherwise, continue without having fetched a template. $template = ""; } if ( ( customLoader || modifiedDefaultLoader ) && $template !== "" && ! ( $template instanceof Backbone.$ ) ) { throw new CustomizationError( "Invalid return value. The " + ( customLoader ? "custom" : "default" ) + " loadTemplate function must return a jQuery instance, but it hasn't" ); } // Create cache entry if ( $template.length ) { // Read the el-related data attributes of the template. data = _getDataAttributes( $template ) ; html = $template.html(); templateCache[cacheId] = { html: html, compiled: _tryCompileTemplate( html, $template ), tagName: data.tagName, className: data.className, id: data.id, attributes: data.attributes, // Data store for plugins. Plugins should create their own namespace in the store, with the plugin name // as key. _pluginData: {} }; events.trigger( "cacheEntry:create", templateCache[cacheId], templateProp, view, viewOptions ); } else { templateCache[cacheId] = { invalid: true }; } return templateCache[cacheId]; } /** * Returns the compiled template if a custom compiler is set in Backbone.DeclarativeViews.custom.compiler, or * undefined if no compiler is set. * * The compiler function is passed the inner HTML of the template node as first argument, and the $template node * itself, in a jQuery wrapper, as the second argument. * * The template node argument is always present when the function is invoked by Backbone.Declarative.Views. When * invoked by plugins for individual template string snippets, the $template node might be missing when there is no * node for such a snippet. * * The compiler should return a function which can be called with the template vars as arguments, producing the * final HTML. This is not enforced, though - the compiler can in fact return anything because who knows what hacks * people come up with. * * @param {string} html * @param {jQuery} [$template] * @returns {Function|undefined} */ function _tryCompileTemplate ( html, $template ) { var compiled, customCompiler = Backbone.DeclarativeViews.custom.compiler; if ( customCompiler ) { if ( customCompiler && !_.isFunction( customCompiler ) ) throw new CustomizationError( "Invalid custom template compiler set in Backbone.DeclarativeViews.custom.compiler: compiler is not a function" ); try { compiled = customCompiler( html, $template ); } catch ( err ) { throw new CompilerError( 'An error occurred while compiling the template. The compiler had been passed the HTML string "' + html + ( $template ? '" as the first argument, and the corresponding template node, wrapped in a jQuery object, as the second argument.' : '" as the only argument.' ) + "\nOriginal error thrown by the compiler:\n" + err.message ); } } return compiled; } /** * Removes a cache entry. * * @param {string} templateProp template selector, or raw template HTML, identifying the cache entry */ function _clearCachedTemplate ( templateProp ) { if ( templateCache[ templateProp ] ) delete templateCache[ templateProp ]; } /** * Adds a name to the list of data attributes which are used and managed by Backbone.Declarative.Views. The name * must be passed without the "data-" prefix, but written as in the data attribute (ie "tag-name", not "tagName"). * * When a data attribute is used to store stringified JSON objects, the flag `{ isJSON: true }` must be set in the * options. Primitive data attributes (of type string, number, boolean) don't need a flag. * * The names "html" and "compiled" are illegal because they are reserved. They are already in use in the cache * object, so there could be a conflict further down the line. Also, a name can only be registered once. And, as * said before, it must not be prefixed with "data-". Violations of these rules cause an error to be thrown. * * Registering a data attribute has the following effects: * * - When a registered data attribute is queried by Backbone.Declarative.Views, the attribute is refreshed from the * DOM and updated in the jQuery data cache. Changes to the attribute in the DOM are picked up that way. The * update can also be triggered externally, e.g. by a plugin, with `updateJqueryDataCache()`. * * - A registered data attribute is detected in a raw HTML/template string, provided that it is placed into a * comment. It must be written as it would appear on a script or template tag, ie in dashed form and including the * "data-" prefix, just like the standard `el`-defining attributes. Custom attributes and `el`-defining attributes * must be placed into the same, single comment. * * The registered attribute is then created on the temporary script tag which is wrapped around the template * string, along with the `el`-defining data attributes. But unlike these, the custom attribute does not make it * into the cache (nor does the script tag). * * However, the script tag can be accessed and examined by a custom loader. For that to happen, the custom loader * has to invoke the default loader before processing the result further. Custom attributes can be read this way. * * @param {string} name as in the data attribute (e.g. "tag-name", not "tagName"), and without "data-" prefix * @param {object} [options] * @param {boolean} [options.isJSON=false] */ function _registerDataAttribute ( name, options ) { var existingNames = _getRegisteredDataAttributeNames(), fullName = "data-" + name, type = options && options.isJSON ? "json" : "primitives", names = registeredDataAttributes[type]; if ( name.indexOf( "data-" ) === 0 ) throw new CustomizationError( 'registerDataAttribute(): Illegal attribute name "' + name + '", must be registered without "data-" prefix' ); if ( name === "html" || name === "compiled" ) throw new CustomizationError( 'registerDataAttribute(): Cannot register attribute name "' + name + '" because it is reserved' ); if ( _.contains( existingNames, name ) ) throw new CustomizationError( 'registerDataAttribute(): Cannot register attribute name "' + name + '" because it has already been registered' ); // Add the name to the list of registered data attributes names.push( name ); registeredDataAttributes[type] = _.uniq( names ); // Create amd store a regex matching the attribute and its value in an HTML/template string, for transfer onto a // wrapper node (see _wrapRawTemplate()) rxRegisteredDataAttributes[fullName] = new RegExp( fullName + "\\s*=\\s*(['\"])([\\s\\S]+?)\\1" ); // Update the regular expression which tests an HTML/template string and detects a comment containing registered // attributes. rxElDefinitionComment = _createElDefinitionCommentRx(); } /** * Returns the names of all registered attributes. These names are dashed but don't include the "data-" prefix. * * @returns {string[]} */ function _getRegisteredDataAttributeNames () { return registeredDataAttributes.primitives.concat( registeredDataAttributes.json ); } /** * Returns a regular expression which tests an HTML/template string and detects a comment containing at least one * registered attribute. Stops after the first matching comment is found (no /g flag). * * NB When the default el-related attributes are registered, this is the resulting regex: * /<!--(?:(?!-->)[\s\S])*?data-(?:tag-name|class-name|id|attributes)\s*=\s*(['"])[\s\S]+?\1[\s\S]*?-->/ * * @returns {RegExp} */ function _createElDefinitionCommentRx () { return new RegExp( "<!--(?:(?!-->)[\\s\\S])*?data-(?:" + _getRegisteredDataAttributeNames().join( "|" ) + ")\\s*=\\s*(['\"])[\\s\\S]+?\\1[\\s\\S]*?-->" ); } /** * Returns the data attributes of an element. * * Makes sure that the registered data attributes, which describe a Backbone el, are read from the DOM directly, * circumventing a potentially stale jQuery cache. The jQuery cache is updated in the process (but for these * attributes only). * * With registerDataAttribute(), plugins can register additional data attributes to have them handled the same way. * * See _updateJQueryDataCache() for more about updating the jQuery data cache. * * @param {jQuery} $elem * @returns {Object} */ function _getDataAttributes ( $elem ) { _updateJQueryDataCache( $elem ); return $elem.data(); } /** * Reads registered data attributes of a given element from the DOM, and updates an existing jQuery data cache with * these values. * * If no jQuery data cache exists, it is NOT created by the call. (This function is meant to be used as an efficient, * internal tool.) If you need to make sure the jQuery data cache is current and in sync with the DOM, and also * create it if it doesn't exist, just call _getDataAttributes() instead. * * The function is needed because jQuery keeps its own cache of data attributes, but there is no API to clear or * circumvent that cache. The jQuery functions $.fn.removeData() and $.removeData() don't do that job: despite their * name, they don't actually remove the cached values but set them to undefined. So undefined is returned on next * access - not the actual values in the DOM. * * Here, we force-update the jQuery cache, making sure that changes of the HTML5 data-* attributes in the DOM are * picked up. * * The implementation circumvents the numerous bugs of jQuery.fn.data(), in particular when removing data. The * behaviour and bugs of a .data() call vary by jQuery version. For an overview of that mess, see * http://jsbin.com/venuqo/4/edit?html,js,console * * NB The cache update is limited to the data attributes which have been registered with _registerDataAttribute(). * By default, only attributes which are "owned" by Backbone.Declarative.Views are updated - ie, the ones describing * the `el` of a view. Other HTML5 data-* attributes are not updated in the jQuery cache because it would interfere * with the responsibilities of other code. * * @param {jQuery} $elem * @returns {Object} */ function _updateJQueryDataCache ( $elem ) { var add = {}, remove = []; if ( $.hasData( $elem[0] ) ) { // A jQuery data cache exists. Update it for the el properties (and attribute names registered by a plugin). // Primitive data types. Normally, this will read the "data-tag-name", "data-class-name" and "data-id" // attributes. _.each( registeredDataAttributes.primitives, function ( attributeName ) { var attributeValue = $elem.attr( "data-" + attributeName ); if ( attributeValue === undefined ) { remove.push( attributeName ); } else { add[toCamelCase( attributeName )] = attributeValue; } } ); // Stringified JSON data. Normally, this just deals with "data-attributes". _.each( registeredDataAttributes.json, function ( attributeName ) { var attributeValue = $elem.attr( "data-" + attributeName ); if ( attributeValue === undefined ) { remove.push( attributeName ); } else { try { add[toCamelCase( attributeName )] = $.parseJSON( attributeValue ); } catch ( err ) { remove.push( attributeName ); } } } ); if ( remove.length ) $elem.removeData( remove ); if ( _.size( add ) ) $elem.data( add ); } } /** * Registers an alternative way to access the cache and set up a custom compiler and loader. Intended for use by * plugins. * * A cache alias just adds syntactic sugar for users wanting to manage and access the cache from a plugin namespace. * The registration creates references to `getCachedTemplate`, `clearCachedTemplate`, `clearCache`, and the `custom` * object in the alternative namespace. * * You can also register the name of an alias to use on view instances (optional). A property of that name will be * created on each view. It references the declarativeViews property of the view. * * @param {Object} namespaceObject e.g. Backbone.InlineTemplate * @param {string} [instanceCachePropertyName] the name of the cache property on a view instance, e.g. "inlineTemplate" */ function _registerCacheAlias( namespaceObject, instanceCachePropertyName ) { namespaceObject.getCachedTemplate = Backbone.DeclarativeViews.getCachedTemplate; namespaceObject.clearCachedTemplate = Backbone.DeclarativeViews.clearCachedTemplate; namespaceObject.clearCache = Backbone.DeclarativeViews.clearCache; namespaceObject.custom = Backbone.DeclarativeViews.custom; if ( instanceCachePropertyName ) { instanceCacheAliases.push( instanceCachePropertyName ); instanceCacheAliases = _.unique( instanceCacheAliases ); } } /** * Sets a flag to enforce template loading when a view is instantiated. Intended for use by plugins. * * When all `el` properties (`tagName`, `className`, ...) are overridden by properties set in a view, there normally * is no need to examine the template, and the loader is not called. (It will be called as soon as the data is * needed, ie when the template cache is queried; that is early enough.) * * However, if the loader does not just fetch the template but also transforms the template element, that * transformation would not happen if all `el` properties are overridden. Calling _enforceTemplateLoading() and * setting the flag makes sure that the loader is called, even in that case. * * _enforceTemplateLoading() must also be called if the loader, or a cache creation helper, needs access to the view * options which are passed to the constructor. */ function _enforceTemplateLoading () { enforceTemplateLoading = true; } /** * Checks if an error belongs to the error types of Backbone.DeclarativeViews. * * ATTN Update this check as new error types are added to Backbone.DeclarativeViews. * * @param {Object} error * @returns {boolean} */ function _isDeclarativeViewsErrorType ( error ) { return error instanceof GenericError || error instanceof TemplateError || error instanceof CompilerError || error instanceof CustomizationError || error instanceof ConfigurationError; } // // Marionette integration // ---------------------- function joinMarionette () { if ( Backbone.Marionette && Backbone.Marionette.TemplateCache && !isMarionetteInitialized ) { originalClearCache = Backbone.Marionette.TemplateCache.clear; // Custom implementation of Marionette.TemplateCache.clear() // // When the Marionette cache is cleared, the DeclarativeViews cache is cleared as well. This is not technically // necessary, but makes sense. If there is a reason to invalidate a cached template, it applies to all caches. Backbone.Marionette.TemplateCache.clear = function () { if ( arguments.length ) { Backbone.DeclarativeViews.clearCachedTemplate( arguments, true ); } else { Backbone.DeclarativeViews.clearCache( true ); } originalClearCache.apply( this, arguments ); }; isMarionetteInitialized = true; // Removed: integration of the Marionette and Backbone.Declarative.Views template loading mechanisms // // Integrating the template loaders turned out to be of little or no benefit, and could potentially have caused // problems with other custom loaders. In detail: // // - Integration saved exactly one DOM access per *template*. Given the limited number of templates in a project, // the performance gain had often been too small to even be measurable. // // - During testing with just a single template, the net effect was even negative (!) - integration and the // associated overhead seemed to slow things down. // // - With integration, custom loaders like the one for Marionette/Handlebars had been trickier to use. Load // order suddenly mattered. The code setting up a custom loader had to be run after integrating // Backbone.Declarative.Views with Marionette. Otherwise, the custom loader would haven been overwritten, // breaking the application. // // In a nutshell, loader integration has proven to be more trouble than it is worth. } } // // Generic helpers // --------------- /** * Turns a dashed string into a camelCased one. * * Simple implementation, but good enough for data attributes. * * @param {string} dashed * @returns {string} */ function toCamelCase ( dashed ) { return dashed.replace( /([^-])-([a-z])/g, function ( $0, $1, $2 ) { return $1 + $2.toUpperCase(); } ); } /** * Creates and returns a custom error type. * * See gist at https://gist.github.com/hashchange/4c1ce239570c77e698c1d2df09d0e540 * * @param {string} name of the error type * @returns {Error} */ function createCustomErrorType ( name ) { function CustomError ( message ) { this.message = message; if ( Error.captureStackTrace ) { Error.captureStackTrace( this, this.constructor ); } else { this.stack = ( new Error() ).stack; } } CustomError.prototype = new Error(); CustomError.prototype.name = name; CustomError.prototype.constructor = CustomError; return CustomError; } // Module return value // ------------------- // // A return value may be necessary for AMD to detect that the module is loaded. It ony exists for that reason and is // purely symbolic. Don't use it in client code. The functionality of this module lives in the Backbone namespace. exports.info = "Backbone.Declarative.Views has loaded. Don't use the exported value of the module. Its functionality is available inside the Backbone namespace."; // // Custom types // ------------ // // For easier documentation and type inference. /** * @name CachedTemplateData * @type {Object} * * @property {string} html * @property {Function|undefined} compiled * @property {string|undefined} tagName * @property {string|undefined} className * @property {string|undefined} id * @property {Object|undefined} attributes * @property {Object} _pluginData */ /** * @name Uncacheable * @type {Object} * * @property {boolean} invalid always true */ } ) );