• Jump To … +
    modules/Behavior.js modules/Cell.js modules/Collection.js modules/Events.js modules/FormModel.js modules/FormView.js modules/ListView.js modules/Model.js modules/NestedCell.js modules/NestedModel.js modules/Router.js modules/ServiceCell.js modules/View.js modules/behaviors/DataBehavior.js modules/configure.js modules/handlebarsUtils.js modules/history.js modules/mixins/cacheMixin.js modules/mixins/cellMixin.js modules/mixins/loadingMixin.js modules/mixins/modelMixin.js modules/mixins/pollingMixin.js modules/registry.js modules/stickitUtils.js modules/templateRenderer.js modules/torso.js modules/validation.js
  • templateRenderer.js

  • ¶
    /**
     * The jQuery reference
     * @external jQuery
     * @property {external:jQuery-Deferred} Deferred
     * @see {@link https://api.jquery.com/category/selectors/|jQuery}
     */
    /**
     * The jQuery Deferred reference
     * @external jQuery-Deferred
     * @see {@link https://api.jquery.com/category/deferred-object/|jQuery.Deferred}
     */
    (function(root, factory) {
      if (typeof define === 'function' && define.amd) {
        define(['underscore', 'backbone'], factory);
      } else if (typeof exports === 'object') {
        module.exports = factory(require('underscore'), require('backbone'));
      } else {
        root.Torso = root.Torso || {};
        root.Torso.Utils = root.Torso.Utils || {};
        root.Torso.Utils.templateRenderer = factory(root._, root.Backbone);
      }
    }(this, function(_, Backbone) {
      'use strict';
    
      var $ = Backbone.$;
    
      /**
       * Changes DOM Nodes that are different, and leaves others untouched.
       *
       * Algorithm:
       * Delegates to a particular swapMethod, depending on the Node type.
       * Recurses for nested Element Nodes only.
       * There is always room for optimizing this method.
       *
       * @memberof templateRenderer
       * @param {Node} currentNode The DOM Node corresponding to the existing page content to update
       * @param {Node} newNode The detached DOM Node representing the desired DOM subtree
       * @param {Array} ignoreElements Array of jQuery selectors for DOM Elements to ignore during render. Can be an expensive check.
       */
      function hotswap(currentNode, newNode, ignoreElements) {
        var newNodeType = newNode.nodeType,
          currentNodeType = currentNode.nodeType,
          swapMethod;
    
        if(newNodeType !== currentNodeType) {
          $(currentNode).replaceWith(newNode);
        } else {
          swapMethod = swapMethods[newNodeType] || swapMethods['default'];
          swapMethod(currentNode, newNode, ignoreElements);
        }
      }
    
      /**
       * Stickit will rely on the 'stickit-bind-val' jQuery data attribute to determine the value to use for a given option.
       * If the value DOM attribute is not the same as the stickit-bind-val, then this will clear the jquery data attribute
       * so that stickit will use the value DOM attribute of the option.  This happens when templateRenderer merges
       * the attributes of the newNode into a current node of the same type when the current node has the stickit-bind-val
       * jQuery data attribute set.
       *
       * If the node value is not set, then the stickit-bind-val might be how the view is communicating the value for stickit to use
       * (possibly in the case of non-string values).  In this case trust the stickit-bind-val.
       *
       * @param {Node} node the DoM element to test and fix the stickit data on.
       */
      function cleanupStickitData(node) {
        var $node = $(node);
        var stickitValue = $node.data('stickit-bind-val');
        if (node.tagName === 'OPTION' && node.value !== undefined && stickitValue !== node.value) {
          $node.removeData('stickit-bind-val');
        }
      }
    
      /*
       * Swap method for Element Nodes
       * @param {Element} currentNode The pre-existing DOM Element to update
       * @param {Element} newNode The detached DOM Element representing the desired DOM Element subtree
       * @param {Array} ignoreElements Array of jQuery selectors for DOM Elements to ignore during render. Can be an expensive check.
       */
      function swapElementNodes(currentNode, newNode, ignoreElements) {
        var currentAttr, shouldIgnore, $currChildNodes, $newChildNodes, currentAttributes,
          $currentNode = $(currentNode),
          $newNode = $(newNode),
          idx = 0;
    
        shouldIgnore = _.some(ignoreElements, function(selector) {
          return $currentNode.is(selector);
        });
    
        if (shouldIgnore) {
          return;
        }
  • ¶

    Handle tagname changes with full replacement

        if (newNode.tagName !== currentNode.tagName) {
          $currentNode.replaceWith(newNode);
          return;
        }
  • ¶

    Remove current attributes that have changed This is necessary, because some types of attributes cannot be removed without causing a browser error.

        currentAttributes = currentNode.attributes;
        var prevLength = currentAttributes.length;
        while (idx < currentAttributes.length) {
          currentAttr = currentAttributes[idx].name;
          if (newNode.getAttribute(currentAttr)) {
            idx++;
          } else {
            currentNode.removeAttribute(currentAttr);
            if (prevLength === currentAttributes.length) {
  • ¶

    bail since we can’t remove the attribute.

              $currentNode.replaceWith(newNode);
              return;
            }
            prevLength = currentAttributes.length;
          }
        }
  • ¶

    Set new attributes

        _.each(newNode.attributes, function(attrib) {
          currentNode.setAttribute(attrib.name, attrib.value);
        });
    
        cleanupStickitData(currentNode);
  • ¶

    Quick check to see if we need to bother comparing sub-levels

        if ($currentNode.html() === $newNode.html()) {
          return;
        }
  • ¶

    Include all child nodes, including text and comment nodes

        $newChildNodes = $newNode.contents();
        $currChildNodes = $currentNode.contents();
  • ¶

    If the DOM lists are different sizes, perform a hard refresh

        if ($newChildNodes.length !== $currChildNodes.length) {
          $currentNode.html($newNode.html());
          return;
        }
  • ¶

    Perform a recursive hotswap for all children nodes

        $currChildNodes.each(function(index, currChildNode) {
          hotswap(currChildNode, $newChildNodes.get(index), ignoreElements);
        });
      }
    
      /*
       * Swap method for Text, Comment, and CDATA Section Nodes
       * @param {Node} currentNode The pre-existing DOM Node to update
       * @param {Node} newNode The detached DOM Node representing the desired DOM Node subtree
       */
      function updateIfNodeValueChanged(currentNode, newNode){
        var nodeValueChanged = newNode.nodeValue !== currentNode.nodeValue;
        if (nodeValueChanged) {
          $(currentNode).replaceWith(newNode);
        }
      }
    
      /*
       * Map of nodeType to hot swap implementations.
       * NodeTypes are hard-coded integers per the DOM Level 2 specification instead of
       * references to constants defined on the window.Node object for IE8 compatibility
       */
      var swapMethods = {
        1: swapElementNodes, // ELEMENT_NODE
        3: updateIfNodeValueChanged, // TEXT_NODE
        4: updateIfNodeValueChanged, // CDATA_SECTION_NODE
        8: updateIfNodeValueChanged, // COMMENT_NODE
        default: function(currentNode, newNode) {
          $(currentNode).replaceWith(newNode);
        }
      };
    
      /**
       * Static Template Engine.
       * All template renders should be piped through this method.
       *
       * @namespace templateRenderer
       *
       * @author ariel.wexler@vecna.com
       *
       * @see <a href="../annotated/modules/templateRenderer.html">templateRenderer Annotated Source</a>
       */
      var templateRenderer = /** @lends templateRenderer */ {
        /**
         * Performs efficient re-rendering of a template.
         * @param  {external:jQuery} $el The Element to render into
         * @param  {external:Handlebars-Template} template The HBS template to apply
         * @param  {Object} context The context object to pass to the template
         * @param  {Object} [opts] Other options
         * @param  {boolean} [opts.force=false] Will forcefully do a fresh render and not a diff-render
         * @param  {string} [opts.newHTML] If you pass in newHTML, it will not use the template or context, but use this instead.
         * @param  {Array} [opts.ignoreElements] jQuery selectors of DOM elements to ignore during render. Can be an expensive check
         */
        render: function($el, template, context, opts) {
          var newDOM, newHTML,
              el = $el.get(0);
          opts = opts || {};
    
          newHTML = opts.newHTML || template(context);
          if (opts.force) {
            $el.html(newHTML);
          } else {
            newDOM = this.copyTopElement(el);
            $(newDOM).html(newHTML);
            this.hotswapKeepCaret(el, newDOM, opts.ignoreElements);
          }
        },
    
        /**
         * Call this.hotswap but also keeps the caret position the same
         * @param {Node} currentNode The DOM Node corresponding to the existing page content to update
         * @param {Node} newNode The detached DOM Node representing the desired DOM subtree
         * @param {Array} ignoreElements Array of jQuery selectors for DOM Elements to ignore during render. Can be an expensive check.
         */
        hotswapKeepCaret: function(currentNode, newNode, ignoreElements) {
          var currentCaret, activeElement,
              currentNodeContainsActiveElement = false;
          try {
            activeElement = document.activeElement;
          } catch (error) {
            activeElement = null;
          }
          if (activeElement && currentNode && $.contains(activeElement, currentNode)) {
            currentNodeContainsActiveElement = true;
          }
          if (currentNodeContainsActiveElement && this.supportsSelection(activeElement)) {
            currentCaret = this.getCaretPosition(activeElement);
          }
          this.hotswap(currentNode, newNode, ignoreElements);
          if (currentNodeContainsActiveElement && this.supportsSelection(activeElement)) {
            this.setCaretPosition(activeElement, currentCaret);
          }
        },
  • ¶

    See above function declaration for method-level documentation

        hotswap: hotswap,
    
        /**
         * Produces a copy of the element tag with attributes but with no contents
         * @param {Element} el the DOM element to be copied
         * @returns {Element} a shallow copy of the element with no children but with attributes
         */
        copyTopElement: function(el) {
          var newDOM = document.createElement(el.tagName);
          _.each(el.attributes, function(attrib) {
            newDOM.setAttribute(attrib.name, attrib.value);
          });
          return newDOM;
        },
    
        /**
         * Determines if the element supports selection. As per spec, https://html.spec.whatwg.org/multipage/forms.html#do-not-apply
         * selection is only allowed for text, search, tel, url, password. Other input types will throw an exception in chrome
         * @param {Element} el the DOM element to check
         * @returns {boolean} boolean indicating whether or not the selection is allowed for {Element} el
         */
        supportsSelection : function (el) {
          return (/text|password|search|tel|url/).test(el.type);
        },
    
        /**
         * Method that returns the current caret (cursor) position of a given element.
         * Source: http://stackoverflow.com/questions/2897155/get-cursor-position-in-characters-within-a-text-input-field
         * @param {element} elem the DOM element to check caret position
         * @returns {Integer} the cursor index of the given element.
         */
        getCaretPosition: function(elem) {
  • ¶

    range {IE selection object} iCaretPos {Integer} will store the final caret position

          var range,
              iCaretPos = 0;
  • ¶

    IE Support

          if (document.selection) {
  • ¶

    Set focus on the element

            elem.focus();
  • ¶

    To get cursor position, get empty selection range

            range = document.selection.createRange();
  • ¶

    Move selection start to 0 position

            range.moveStart('character', -elem.value.length);
  • ¶

    The caret position is selection length

            iCaretPos = range.text.length;
          } else if (elem.selectionStart || elem.selectionStart === 0) {
  • ¶

    Firefox support

            iCaretPos = elem.selectionStart;
          }
  • ¶

    Return results

          return iCaretPos;
        },
    
        /**
         * Method that returns sets the current caret (cursor) position of a given element and puts it in focus.
         * Source: http://stackoverflow.com/questions/512528/set-cursor-position-in-html-textbox
         * @param {element} elem
         * @param {Integer} caretPos The caret index to set
         * @returns {Integer} the cursor index of the given element.
         */
        setCaretPosition: function(elem, caretPos) {
          var range;
          if (elem) {
            if (elem.createTextRange) {
  • ¶

    IE support

              range = elem.createTextRange();
              range.move('character', caretPos);
              range.select();
            } else if (elem.selectionStart || elem.selectionStart === 0) {
  • ¶

    Firefox support

              elem.focus();
              elem.setSelectionRange(caretPos, caretPos);
            } else {
  • ¶

    At least focus the element if nothing else

              elem.focus();
            }
          }
        }
      };
    
      return templateRenderer;
    }));