Muriel-Salvan/rails-ajax

View on GitHub
assets/javascripts/jquery.rails-ajax.js.erb

Summary

Maintainability
Test Coverage
<% if (RailsAjax.config.enabled?) %>

//=========================================
// Browser detection code (removed from jQuery >=1.9):
// Can be removed along with setFavicon as soon as Firefox corrects bug: https://bugzilla.mozilla.org/show_bug.cgi?id=519028
var matched, browser;

// Use of jQuery.browser is frowned upon.
// More details: http://api.jquery.com/jQuery.browser
// jQuery.uaMatch maintained for back-compat
jQuery.uaMatch = function( ua ) {
    ua = ua.toLowerCase();

    var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
        /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
        /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
        /(msie) ([\w.]+)/.exec( ua ) ||
        ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
        [];

    return {
        browser: match[ 1 ] || "",
        version: match[ 2 ] || "0"
    };
};

matched = jQuery.uaMatch( navigator.userAgent );
browser = {};

if ( matched.browser ) {
    browser[ matched.browser ] = true;
    browser.version = matched.version;
}

// Chrome is Webkit, but Webkit is also Safari.
if ( browser.chrome ) {
    browser.webkit = true;
} else if ( browser.webkit ) {
    browser.safari = true;
}

jQuery.browser = browser;
//===========================================

// Encapsulate everything in a nice namespace
var railsAjax = {

  // Initialization, to be called just once when the DOM is ready
  init: function() {
    try {
      // Register bindings to every Rails remote element
      railsAjax.attachAjaxToElements('*[data-rails-ajax-remote=true]');
      var lRootURL = History.getRootUrl();
      jQuery(window).bind('statechange', function() {
        try {
          var lState = History.getState();
          <%= 'alert(\'[RailsAjax] History statechange triggered #\' + lState.data.id + \' via \' + lState.data.type + \' to \' + lState.url.replace(lRootURL, \'/\') + \' (anchor: \' + lState.data.anchor + \', dataType: \' + lState.data.dataType + \')\');' if (RailsAjax.config.debug_alerts?) %>
          // console.log('[RailsAjax] History statechange triggered ' + lState.data.type + ' to ' + lState.url.replace(lRootURL, '/') + ' (anchor: ' + lState.data.anchor + ', dataType: ' + lState.data.dataType + ')');
          jQuery.ajax({
            // TODO (History.js): Remove the anchor attribute when History.js handles anchors correctly.
            anchor: lState.data.anchor,
            // Get back all parameters set by Rails' jquery-ujs
            type: lState.data.type,
            data: lState.data.data,
            dataType: lState.data.dataType,
            crossDomain: lState.data.crossDomain,
            // Set the URL to query
            url: lState.url.replace(lRootURL, '/'),
            // And the callbacks
            // TODO (History.js): Remove the beforeSend callback when anchors will be handled correctly by History.js
            beforeSend: function(ioXHR, iSettings) {
              // console.log('[RailsAjax] Before sending request');
              // Remember the anchor used
              ioXHR.anchor = iSettings.anchor;
            },
            success: function(iData, iStatus, iXHR) {
              // console.log('[RailsAjax] Response returned success');
              railsAjax.ajaxSuccess(iXHR, iData);
            },
            error: function(iXHR, iStatus, iError) {
              // console.log('[RailsAjax] Response returned error');
              railsAjax.ajaxError(iXHR, iError);
            }
          });
        } catch (iError) {
          // console.log('[RailsAjax] Exception in statechange callback: ' + iError.name + ' - ' + iError.message);
          alert('[RailsAjax] Exception in statechange callback: ' + iError.name + ' - ' + iError.message);
        }
      });
    } catch (iError) {
      // console.log('[RailsAjax] Exception during railsAjax initialization: ' + iError.name + ' - ' + iError.message);
      alert('[RailsAjax] Exception during railsAjax initialization: ' + iError.name + ' - ' + iError.message);
    }
  },

  // Attach Ajax delegations to elements needing them
  //
  // Parameters::
  // * *iCSSSelector* (_String_): CSS selector of elements needing attachment
  attachAjaxToElements: function(iCSSSelector) {
    try {
      <%= 'alert(\'[RailsAjax] Attach Ajax bindings to elements \' + iCSSSelector + \' (\' + jQuery(iCSSSelector).length + \' elements)\');' if (RailsAjax.config.debug_alerts?) %>
      jQuery(document)
        .delegate(iCSSSelector, 'ajax:beforeSend', function(iEvent, ioXHR, iSettings) {
          try {
            var lThis = jQuery(this);
            var lURL = ((lThis.is('a')) ? lThis.attr('href') : lThis.attr('action'));
            <%= 'alert(\'[RailsAjax] Ajax before send. URL=\' + lURL + \' Method=\' + iSettings.type + \' Data=\' + iSettings.data);' if (RailsAjax.config.debug_alerts?) %>
            var lContinue = true;
            if (railsAjax.beforeSend != undefined) {
              try {
                lContinue = (railsAjax.beforeSend(ioXHR, iSettings) != false);
              } catch (iUserError) {
                alert('[RailsAjax] Exception in user callback railsAjax.beforeSend: ' + iUserError.name + ' - ' + iUserError.message);
              }
            }
            <%= 'alert(\'[RailsAjax] Continue on the Ajax load ? \' + lContinue);' if (RailsAjax.config.debug_alerts?) %>
            if (lContinue) {
              railsAjax.pushNewState(lURL,
                {
                  // Replicate the whole Rails' jquery-ujs parameters, as we will need to pass them to the new ajax options object created during onstatechange event.
                  type: iSettings.type,
                  data: iSettings.data,
                  dataType: iSettings.dataType,
                  crossDomain: iSettings.crossDomain
                },
                false
              );
            }
            // Cancel AJAX processing right now.
            // It will be taken care of by the changestate event.
            return false;
          } catch (iError) {
            alert('[RailsAjax] Exception while ajax:beforeSend: ' + iError.name + ' - ' + iError.message);
          }
        })
    } catch (iError) {
      alert('[RailsAjax] Exception while binding Ajax events to elements ' + iCSSSelector + ': ' + iError.name + ' - ' + iError.message);
    }
  },

  // Callback called when Ajax call is successfull
  //
  // Parameters::
  // * *iXHR* (_Object_): The XHR object responsible for the Ajax call
  // * *iData* (_String_): The received data (can be undefined for empty contents)
  ajaxSuccess: function(iXHR, iData) {
    try {
      <%= 'alert(\'[RailsAjax] Ajax success. Loading data: \' + iData);' if (RailsAjax.config.debug_alerts?) %>

      if (iData != undefined) {
        railsAjax.updatePage(iXHR, iData);
      }

      // Call user callbacks
      if (railsAjax.success != undefined) {
        try {
          railsAjax.success(iXHR, iData);
        } catch (iUserError) {
          alert('[RailsAjax] Exception in user callback railsAjax.success: ' + iUserError.name + ' - ' + iUserError.message);
        }
      }
      if (railsAjax.complete != undefined) {
        try {
          railsAjax.complete(iXHR);
        } catch (iUserError) {
          alert('[RailsAjax] Exception in user callback railsAjax.complete: ' + iUserError.name + ' - ' + iUserError.message);
        }
      }

      // Handle redirects
      if ((iData != undefined) &&
          ('redirect_to' in iData)) {
        railsAjax.pushNewState(iData.redirect_to, {}, true);
      }

    } catch (iError) {
      alert('[RailsAjax] Exception while Ajax success callback: ' + iError.name + ' - ' + iError.message);
    }
  },

  // Callback called when Ajax call has failed
  //
  // Parameters::
  // * *iXHR* (_Object_): The xhr object
  // * *iError* (_Object_): The error
  ajaxError: function(iXHR, iError) {

    // If the error is a 4xx one, and handled by rails-ajax (using a JSON object), still update the page.
    if ((iXHR.status >= 400) &&
        (iXHR.status < 500) &&
        (iXHR.responseJSON != undefined)) {
      railsAjax.updatePage(iXHR, iXHR.responseJSON);
    }

    // Call user callbacks
    if (railsAjax.error != undefined) {
      try {
        railsAjax.error(iXHR, iError);
      } catch (iUserError) {
        alert('[RailsAjax] Exception in user callback railsAjax.error: ' + iUserError.name + ' - ' + iUserError.message);
      }
    }
    if (railsAjax.complete != undefined) {
      try {
        railsAjax.complete(iXHR);
      } catch (iUserError) {
        alert('[RailsAjax] Exception in user callback railsAjax.complete: ' + iUserError.name + ' - ' + iUserError.message);
      }
    }

  },

  // Update the page with a given data returned by rails-ajax
  //
  // Parameters::
  // * *iXHR* (_Object_): The XHR object responsible for the Ajax call
  // * *iData* (_String_): The received data
  updatePage: function(iXHR, iData) {
    try {

      // 1. Change the window's title if needed
      if ('page_title' in iData) {
        document.title = iData.page_title;
      }

      // 2. Replace HTML data, and execute scripts in the same time
      jQuery.each(iData.div_contents, function(iCSSSelector, iContent) {
        // This replaces HTML content and also executes scripts (even document.ready ones)
        jQuery(iCSSSelector).html(iContent);
      });

      // 3. Scroll to anchor if needed
      if (iXHR.anchor) {
        railsAjax.scrollToAnchor(iXHR.anchor);
      }

      // 4. For Firefox only, workaround a bug by setting the Favicon
      if (jQuery.browser.mozilla) {
        railsAjax.setFavicon();
      }

      // 5. Execute scripts that were added
      if ('js_to_execute' in iData) {
        jQuery.each(iData.js_to_execute, function(iIdx, iJS) {
          <%= 'alert(\'[RailsAjax] Execute script #\' + iIdx + \':\\n\' + iJS);' if (RailsAjax.config.debug_alerts?) %>
          try {
            eval(iJS);
          } catch (iError) {
            alert('[RailsAjax] Exception while executing Ajax script: ' + iError.name + ' - ' + iError.message + '\nScript:\n' + iJS);
          }
        });
      }
      
      // 6. Update CSRF Token
      if ('meta_tags' in iData) {
        jQuery.each(iData.meta_tags, function(iName, iValue) {
          <%= 'alert(\'[RailsAjax] Update meta tag \' + iName + \':\\n\' + iValue);' if (RailsAjax.config.debug_alerts?) %>
          $('meta[name='+iName+']').attr('content', iValue);
        });
      }

    } catch (iError) {
      alert('[RailsAjax] Exception while updating page: ' + iError.name + ' - ' + iError.message);
    }
  },

  // Used to identify states in debugging logs
  <%= 'gStateID: 0,' if (RailsAjax.config.debug_alerts?) %>

  // Push a new History state
  //
  // Parameters::
  // * *iURL* (_String_): The URL to be pushed
  // * *iData* (_Object_): The state object to store with it
  // * *iReplaceState* (_Boolean_): Do we replace the current state ?
  pushNewState: function(iURL, iData, iReplaceState) {
    try {
      // Remove the hash if it is present, as otherwise anchorchange is triggered and statechange ignored.
      // We don't want to handle anchorchange callback as it is unable to get the real History state correctly.
      // TODO (History.js): Correct bug https://github.com/balupton/history.js/issues/42 and https://github.com/balupton/history.js/issues/173, then remove this special hash and anchor treatment
      var lSplittedURL = iURL.split('#');
      var lState = jQuery.extend(
        {
          // Used to identify states in debugging logs
          <%= 'id: railsAjax.gStateID,' if (RailsAjax.config.debug_alerts?) %>
          anchor: lSplittedURL[1]
        },
        iData
      );
      if (iReplaceState) {
        History.replaceState(lState, null, lSplittedURL[0]);
      } else {
        History.pushState(lState, null, lSplittedURL[0]);
      }
      <%= 'railsAjax.gStateID = railsAjax.gStateID + 1;' if (RailsAjax.config.debug_alerts?) %>
    } catch (iError) {
      alert('[RailsAjax] Exception in pushNewState: ' + iError.name + ' - ' + iError.message);
    }
  },

  // Scroll the web page to display a given anchor.
  // An anchor is either a div id, or a named "a"
  //
  // Parameters::
  // * *iAnchor* (_String_): Anchor name
  scrollToAnchor: function(iAnchor) {
    var lDivElement = jQuery('div#' + iAnchor)[0]
    if (lDivElement != undefined) {
      lDivElement.scrollIntoView(true);
    } else {
      var lLstElements = window.document.getElementsByName(iAnchor);
      if (lLstElements.length > 0) {
        lLstElements[0].scrollIntoView(true);
      } else {
        alert('[RailsAjax] No anchor named ' + iAnchor);
      }
    }
  },

  // Reload the favicon.
  // This is needed to workaround a Firefox bug: http://kilianvalkhof.com/2010/javascript/the-case-of-the-disappearing-favicon/
  setFavicon: function() {
    var link = jQuery('link[type="image/x-icon"]').remove().attr('href');
    jQuery('<link href="'+ link +'" rel="shortcut icon" type="image/x-icon" />').appendTo('head');
  }

};

// Initialize everything once the DOM is loaded and ready
jQuery(document).ready(function(){
  railsAjax.init();
});

<% end %>