// Backbone.Inline.Template, v1.0.1 // Copyright (c) 2016 Michael Heim, Zeilenwechsel.de // Distributed under MIT license // http://github.com/hashchange/backbone.inline.template ;( 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", "backbone.declarative.views" ], factory ); } else if ( supportsExports ) { // Node module, CommonJS module factory( exports, require( "underscore" ), require( "backbone" ), require( "backbone.declarative.views" ) ); } else { // Global (browser or Rhino) factory( {}, _, Backbone ); } }( this, function ( exports, _, Backbone ) { "use strict"; var $ = Backbone.$, $document = $( document ), pluginNamespace = Backbone.InlineTemplate = { hasInlineEl: _hasInlineEl, updateTemplateSource: false, version: "1.0.1" }, rxLeadingComments = /^(\s*<!--[\s\S]*?-->)+/, rxTrailingComments = /(<!--[\s\S]*?-->\s*)+$/, rxOutermostHtmlTagWithContent = /(<\s*[a-zA-Z][\s\S]*?>)([\s\S]*)(<\s*\/\s*[a-zA-Z]+\s*>)/, rxSelfClosingHtmlTag = /<\s*[a-zA-Z][\s\S]*?\/?\s*>/, bbdvLoadTemplate = Backbone.DeclarativeViews.defaults.loadTemplate; // // Initialization // -------------- Backbone.DeclarativeViews.plugins.registerDataAttribute( "el-definition" ); Backbone.DeclarativeViews.plugins.registerDataAttribute( "bbit-internal-template-status" ); Backbone.DeclarativeViews.plugins.registerCacheAlias( pluginNamespace, "inlineTemplate" ); Backbone.DeclarativeViews.plugins.enforceTemplateLoading(); // // Template loader //---------------- Backbone.DeclarativeViews.defaults.loadTemplate = function ( templateProperty ) { var parsedTemplateData, $resultTemplate, // Check Backbone.InlineTemplate.custom.hasInlineEl first, even though it is undocumented, to catch // accidental assignments. hasInlineEl = pluginNamespace.custom.hasInlineEl || pluginNamespace.hasInlineEl || _hasInlineEl, updateTemplateContainer = pluginNamespace.updateTemplateSource, $inputTemplate = bbdvLoadTemplate( templateProperty ); if ( _isMarkedAsUpdated( $inputTemplate ) || !hasInlineEl( $inputTemplate ) ) { // No inline el definition. Just return the template as is. $resultTemplate = $inputTemplate; } else { // Parse the template data. // // NB Errors are not handled here and bubble up further. Try-catch is just used to enhance the error message // for easier debugging. try { parsedTemplateData = _parseTemplateHtml( $inputTemplate.html() ); } catch ( err ) { err.message += '\nThe template was requested for template property "' + templateProperty + '"'; throw err; } if ( updateTemplateContainer ) { // For updating the template container, it has to be a node in the DOM. Throw an error if it has been // passed in as a raw HTML string. if ( !existsInDOM( templateProperty ) ) throw new Backbone.DeclarativeViews.TemplateError( "Backbone.Inline.Template: Can't update the template container because it doesn't exist in the DOM. The template property must be a valid selector (and not, for instance, a raw HTML string). Instead, we got \"" + templateProperty + '"' ); $resultTemplate = $inputTemplate; // The template is updated and the inline `el` removed. Set a flag on the template to make sure the // template is never processed again as having an inline `el`. _markAsUpdated( $resultTemplate ); } else { // No updating of the input template. Create a new template node which will stay out of the DOM, but is // passed to the cache. $resultTemplate = $( "<script />" ).attr( "type", "text/x-template" ); } _mapElementToDataAttributes( parsedTemplateData.$elSample, $resultTemplate ); $resultTemplate.empty().text( parsedTemplateData.templateContent ); } return $resultTemplate; }; /** * Checks if a template is marked as having an inline `el`. Is also exposed as Backbone.InlineTemplate.hasInlineEl(). * * By default, a template is recognized as having an inline `el` when the container has the following data attribute: * `data-el-definition: "inline"`. * * The check can be changed by overriding Backbone.InlineTemplate.hasInlineEl with a custom function. In order to * treat all templates as having an inline `el`, for instance, the custom function just has to return true: * * Backbone.InlineTemplate.hasInlineEl = function () { return true; }; * * @param {jQuery} $templateContainer the template node (usually a <script> or a <template> tag) * @returns {boolean} */ function _hasInlineEl ( $templateContainer ) { return $templateContainer.data( "el-definition" ) === "inline"; } /** * Marks a template as updated and no longer having an inline `el`. Henceforth, the template node is treated like an * ordinary, non-inline template. * * ## Rationale: * * A template is updated when the `updateTemplateSource` option is set. After the update, the `el` markup has been * removed from the template content, and only the inner HTML of the `el` is still present in the template. * * Therefore, the template must not be processed again for having an inline `el`, even though it is still marked as * such. (The inline `el` marker - e.g. data-el-definition: "inline" - is still present.) Repeated processing would * garble the remaining template content. * * That is prevented by setting a second flag in a data attribute which is considered internal. A template which is * marked as updated, with that flag, is not processed and updated again. * * ## jQuery data cache: * * The jQuery data cache doesn't have to be updated here. That happens automatically while the template is checked * for data attributes in Backbone.Declarative.Views. * * @param {jQuery} $templateContainer */ function _markAsUpdated ( $templateContainer ) { $templateContainer.attr( "data-bbit-internal-template-status", "updated" ); } /** * Checks if a template is marked as having been updated. * * @param {jQuery} $templateContainer * @returns {boolean} */ function _isMarkedAsUpdated ( $templateContainer ) { return $templateContainer.data( "bbit-internal-template-status" ) === "updated"; } /** * Takes the raw text content of the template tag, extracts the inline `el` as well as its content, turns the `el` * string into a sample node and, finally, returns the $el sample and the inner content in a hash. * * @param {string} templateText * @returns {ParsedTemplateData} */ function _parseTemplateHtml ( templateText ) { var elDefinition, $elSample, templateContent = "", normalizedTemplateText = templateText.replace( rxLeadingComments, "" ).replace( rxTrailingComments, "" ), matches = rxOutermostHtmlTagWithContent.exec( normalizedTemplateText ) || rxSelfClosingHtmlTag.exec( normalizedTemplateText ); if ( !matches ) throw new Backbone.DeclarativeViews.TemplateError( 'Backbone.Inline.Template: Failed to parse template with inline `el` definition. No matching content found.\nTemplate text is "' + templateText + '"' ); if ( matches[3] ) { // Applied regex for outermost HTML tag with content, capturing 3 groups elDefinition = matches[1] + matches[3]; templateContent = matches[2]; } else { // Applied regex for self-closing `el` tag without template content, not capturing any groups. elDefinition = matches[0]; } try { $elSample = $( elDefinition ); } catch ( err ) { throw new Backbone.DeclarativeViews.TemplateError( 'Backbone.Inline.Template: Failed to parse template with inline `el` definition. Extracted `el` could not be turned into a sample node.\nExtracted `el` definition string is "' + elDefinition + '", full template text is "' + templateText + '"' ); } return { templateContent: templateContent, $elSample: $elSample }; } /** * Takes an element node and maps its defining characteristics - tag name, classes, id, other attributes - to data * attributes on another node, in the format used by Backbone.Declarative.Views. * * In other words, it transforms an actual `el` sample node into a set of descriptive data attributes on a template. * * @param {jQuery} $sourceNode the `el` sample node * @param {jQuery} $target the template node */ function _mapElementToDataAttributes ( $sourceNode, $target ) { var sourceProps = { tagName: $sourceNode.prop("tagName").toLowerCase(), className: $.trim( $sourceNode.attr( "class" ) ), id: $.trim( $sourceNode.attr( "id" ) ), otherAttributes: {} }; _.each( $sourceNode[0].attributes, function ( attrNode ) { var name = attrNode.nodeName, value = attrNode.nodeValue, include = value !== undefined && name !== "class" && name !== "id"; if ( include ) sourceProps.otherAttributes[attrNode.nodeName] = value; } ); if ( sourceProps.tagName !== "div" ) $target.attr( "data-tag-name", sourceProps.tagName ); if ( sourceProps.className ) $target.attr( "data-class-name", sourceProps.className ); if ( sourceProps.id ) $target.attr( "data-id", sourceProps.id ); if ( _.size( sourceProps.otherAttributes ) ) $target.attr( "data-attributes", JSON.stringify( sourceProps.otherAttributes ) ); } // // Generic helpers // --------------- /** * Checks if an entity can be passed to jQuery successfully and be resolved to a node which exists in the DOM. * * Can be used to verify * - that a string is a selector, and that it selects at least one existing element * - that a node is part of the document * * Returns false if passed e.g. a raw HTML string, or invalid data, or a detached node. * * @param {*} testedEntity can be pretty much anything, usually a string (selector) or a node * @returns {boolean} */ function existsInDOM ( testedEntity ) { try { return $document.find( testedEntity ).length !== 0; } catch ( err ) { return false; } } // 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.Inline.Template 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 ParsedTemplateData * @type {Object} * * @property {jQuery} $elSample * @property {string} templateContent */ } ) );