diff --git a/example.js b/example.js index ae0e1bb..62dd9ee 100644 --- a/example.js +++ b/example.js @@ -24,8 +24,13 @@ jQuery('document').ready(function($) { } - $('body').djax('.one-third', [], transition); + $('body').djax({ + 'selector' : '.one-third', + 'exceptions' : [], + 'replaceBlockFunction' : transition + }); + $(window).bind('djaxLoad', function(e, params) { console.log($('
'+params.response+'
')); }) -}); \ No newline at end of file +}); diff --git a/jquery.djax.js b/jquery.djax.js index 6529c15..d97eff1 100644 --- a/jquery.djax.js +++ b/jquery.djax.js @@ -21,10 +21,6 @@ * */ -/*jslint browser: true, indent: 4, maxerr: 50, sub: true */ -/*jshint bitwise:true, curly:true, eqeqeq:true, forin:true, immed:true, latedef:true, noarg:true, noempty:true, nomen:true, nonew:true, onevar:true, plusplus:true, regexp:true, smarttabs:true, strict:true, trailing:true, undef:true, white:true, browser:true, jquery:true, indent:4, maxerr:50, */ -/*global jQuery */ - // ==ClosureCompiler== // @compilation_level ADVANCED_OPTIMIZATIONS // @output_file_name jquery.djax.js @@ -33,205 +29,560 @@ // http://closure-compiler.appspot.com/home (function ($, exports) { - 'use strict'; - - $.fn.djax = function (selector, exceptions, replaceBlockWithFunc) { - - // If browser doesn't support pushState, abort now - if (!history.pushState) { - return $(this); - } - - var self = this, - blockSelector = selector, - excludes = (exceptions && exceptions.length) ? exceptions : [], - replaceBlockWith = (replaceBlockWithFunc) ? replaceBlockWithFunc : $.fn.replaceWith, - djaxing = false; - - // Ensure that the history is correct when going from 2nd page to 1st - window.history.replaceState( - { - 'url' : window.location.href, - 'title' : $('title').text() - }, - $('title').text(), - window.location.href - ); - - self.clearDjaxing = function() { - self.djaxing = false; - } - - // Exclude the link exceptions - self.attachClick = function (element, event) { - - var link = $(element), - exception = false; - - $.each(excludes, function (index, exclusion) { - if (link.attr('href').indexOf(exclusion) !== -1) { - exception = true; - } - if (window.location.href.indexOf(exclusion) !== -1) { - exception = true; - } - }); - - // If the link is one of the exceptions, return early so that - // the link can be clicked and a full page load as normal - if (exception) { - return $(element); - } - - // From this point on, we handle the behaviour - event.preventDefault(); - - // If we're already doing djaxing, return now and silently fail - if (self.djaxing) { - setTimeout(self.clearDjaxing, 1000); - return $(element); - } - - $(window).trigger('djaxClick', [element]); - self.reqUrl = link.attr('href'); - self.triggered = false; - self.navigate(link.attr('href'), true); - }; - - // Handle the navigation - self.navigate = function (url, add) { - - var blocks = $(blockSelector); - - self.djaxing = true; - - // Get the new page - $(window).trigger( - 'djaxLoading', - [{ - 'url' : url - }] - ); - - var replaceBlocks = function (response) { - if (url !== self.reqUrl) { - self.navigate(self.reqUrl, false); - return true; - } - - var result = $(response), - newBlocks = $(result).find(blockSelector); - - if (add) { - window.history.pushState( - { - 'url' : url, - 'title' : $(result).filter('title').text() - }, - $(result).filter('title').text(), - url - ); - } - - // Set page title as new page title - $('title').text($(result).filter('title').text()); - - // Loop through each block and find new page equivalent - blocks.each(function () { - - var id = '#' + $(this).attr('id'), - newBlock = newBlocks.filter(id), - block = $(this); - - $('a', newBlock).filter(function () { - return this.hostname === location.hostname; - }).addClass('dJAX_internal').on('click', function (event) { - return self.attachClick(this, event); - }); - - if (newBlock.length) { - if (block.html() !== newBlock.html()) { - replaceBlockWith.call(block, newBlock); - } - } else { - block.remove(); - } - - }); - - // Loop through new page blocks and add in as needed - $.each(newBlocks, function () { - - var newBlock = $(this), - id = '#' + $(this).attr('id'), - $previousSibling; - - // If there is a new page block without an equivalent block - // in the old page, we need to find out where to insert it - if (!$(id).length) { - - // Find the previous sibling - $previousSibling = $(result).find(id).prev(); - - if ($previousSibling.length) { - // Insert after the previous element - newBlock.insertAfter('#' + $previousSibling.attr('id')); - } else { - // There's no previous sibling, so prepend to parent instead - newBlock.prependTo('#' + newBlock.parent().attr('id')); - } - } - - // Only add a class to internal links - $('a', newBlock).filter(function () { - return this.hostname === location.hostname; - }).addClass('dJAX_internal').on('click', function (event) { - return self.attachClick(this, event); - }); - - }); - - - - // Trigger djaxLoad event as a pseudo ready() - if (!self.triggered) { - $(window).trigger( - 'djaxLoad', - [{ - 'url' : url, - 'title' : $(result).filter('title').text(), - 'response' : response - }] - ); - self.triggered = true; - self.djaxing = false; - } - }; - $.get(url, function (response) { - replaceBlocks(response); - }).error(function (response) { - // handle error - console.log('error', response); - replaceBlocks(response['responseText']); - }); - }; /* End self.navigate */ - - // Only add a class to internal links - $(this).find('a').filter(function () { - return this.hostname === location.hostname; - }).addClass('dJAX_internal').on('click', function (event) { - return self.attachClick(this, event); - }); - - // On new page load - $(window).bind('popstate', function (event) { - self.triggered = false; - if (event.originalEvent.state) { - self.reqUrl = event.originalEvent.state.url; - self.navigate(event.originalEvent.state.url, false); - } - }); - - }; - -}(jQuery, window)); \ No newline at end of file + 'use strict'; + + $.support.cors = true; + + var url_queue = []; + var djaxing = false; + var reqUrl; + var triggered; + var popstateUrl = ''; + + var _methods = { + 'clearDjaxing' : function () { + var $this = this; + djaxing = false; + + // check in the queue to see if there is something else that + // needs to be navigated + if (url_queue.length) { + var url_addToHist = url_queue.pop(); + url_queue = []; + + var url = url_addToHist[0], + addToHistory = url_addToHist[1], + urlData = url_addToHist[2], + method = url_addToHist[3], + requestParameters = url_addToHist[4]; + + methods.navigate.call($this, url, addToHistory, urlData, method, requestParameters); + } + }, + 'getUrlFromHeaders' : function (defaultUrl, jqXHR) { + var targetUrl = jqXHR.getResponseHeader("TargetUrl"); + if (typeof targetUrl === 'undefined') { + targetUrl = defaultUrl; + } + return targetUrl; + }, + 'navigate' : function (options) { + var $this = this; + + djaxing = true; + + $(window).trigger( + 'djaxLoading', [{ + 'url' : options.url + }] + ); + + $.ajax(_methods.ajaxSettings($this, options)); + }, + 'ajaxSettings' : function(element, options) { + if (options.method !== 'POST') { + options.method = 'GET'; + } + + options.blocks = $(element.data('djaxBlockSelector')); + + var settings = element.data('settings'); + + var ajax_data = settings.ajax_data_parameter; + if (typeof ajax_data === 'function') { + ajax_data = ajax_data(); + } + + return { + 'url' : options.url, + 'data' : ajax_data, + 'timeout' : settings.ajax_timeout, + 'type' : options.method, + 'crossDomain' : true, + 'success' : _methods.changePage( + element, + options.url, + options.add_to_history, + options.blocks + ), + 'error' : _methods.changePageError( + element, + options.url, + options.add_to_history, + options.blocks, + settings.ajax_timeout_callback + ) + }; + }, + 'changePage' : function (element, url, add_to_history, blocks) { + return function (responseData, textStatus, jqXHR) { + var targetUrl = _methods.getUrlFromHeaders.call(element, url, jqXHR); + + // keep url we are going to + reqUrl = targetUrl; + + _methods.replaceBlocks.call(element, targetUrl, add_to_history, blocks, responseData); + } + }, + 'changePageError' : function (element, url, add_to_history, blocks, timeout_callback) { + return function (jqXHR, textStatus, errorThrown) { + if (textStatus === 'error' + && (jqXHR.status === 404 || + errorThrown === "" || + typeof jqXHR.responseText === 'undefined')) { + + + // just "browse" to the url provided to handle redirects + window.location.href = this.url; + } + else if (textStatus === 'timeout') { + timeout_callback && timeout_callback(this); + } + else { + // handle error + // still replace blocks as we may end up here if the + // correct content type is not set by the webserver - + // (e.g., with content type set to application/json an + // error may be returned) + + // try to get url from the headers + var targetUrl = _methods.getUrlFromHeaders.call(element, url, jqXHR); + + // keep url we are going to + reqUrl = targetUrl; + + _methods.replaceBlocks.call(element, targetUrl, add_to_history, blocks, jqXHR['responseText']); + } + } + }, + 'attachClick' : function (element, event) { + var $this = this; + + var link = $(element), + exception = false, + excludes = $this.data('djaxUserExcludes'); + + $.each(excludes, function (index, exclusion) { + if (link.attr('href').indexOf(exclusion) !== -1) { + exception = true; + } + if (window.location.href.indexOf(exclusion) !== -1) { + exception = true; + } + }); + + // If the link is one of the exceptions, return early so that + // the link can be clicked and a full page load as normal + if (exception) { + return $(element); + } + + // From this point on, we handle the behaviour + event.preventDefault(); + + // If we're already doing djaxing, return now and silently fail + if (djaxing) { + setTimeout(function() { _methods.clearDjaxing.call($this); }, 1000); + return $(element); + } + + // trigger asynchronous click event + var djaxClickData = [element]; + $(window).trigger('djaxClick', djaxClickData); + + // call blocking callback + var settings = $this.data('settings'); + settings.onDjaxClickCallback.call($this, djaxClickData, {}); + + var urlDataAttribute = settings.urlDataAttribute; + if (typeof urlDataAttribute !== 'undefined') { + reqUrl = link.data(urlDataAttribute); + } + + if (typeof reqUrl === 'undefined') { + reqUrl = link.attr('href'); + } + + triggered = false; + _methods.navigate.call($this, { + url: link.attr('href'), + add_to_history: true, + type: 'GET' + }); + }, + 'replaceBlocks' : function (url, add, currentBlocks, response) { + var $this = this; + + var settings = $this.data('settings'); + + if (url !== reqUrl) { + _methods.navigate.call($this, { + url: reqUrl, + add_to_history: false, + method: 'GET' + }); + return true; + } + + // get some settings + var blockSelector = $this.data('djaxBlockSelector'); + var replaceBlockWithFunc = $this.data('djaxReplaceBlockWith'); + + var $result = $(response); + + // add them to the history if requested + if (add) { + window.history.pushState( + { + 'url' : url, + 'title' : $result.filter('title').text() + }, + $result.filter('title').text(), + url + ); + } + + // here store all replacements to be performed + // they look like: + // { + // type: 'replace', // or prependTo, insertAfter, remove + // new_block: $jquery_element, + // target: $jquery_element, // or null if a new block needs to be removed + // } + // + var replacements_config = []; + + // Set page title as new page title + // + // Set title cross-browser: + // - $('title').text(title_text); returns an error on IE7 + // + document.title = $result.filter('title').text(); + + // parse new blocks (wrap reponse to obtain all the descendants) + var $newBlocks = $('
' + response + '
').find(blockSelector); + + // + // Case in which blocks need to be replaced + // + currentBlocks.each(function () { + + var $currentBlock = $(this); + var id = '#' + $currentBlock.attr('id'); + var newBlock = $newBlocks.filter(id); + + // take all internal links in the new block + $('a:not(.' + settings.ignoreClass + ')', newBlock).filter(function () { + return this.hostname === location.hostname; + }) + // add the dJAX_internal class to them + .addClass('dJAX_internal') + + // attach the click event + .on('click.djax', function (event) { + _methods.attachClick.call($this, this, event); + }); + + if (newBlock.length) { + // compare the html of the new and the current block + var block_html = $currentBlock.clone().wrap('
').parent().html(), + newblock_html = newBlock.clone().wrap('
').parent().html(); + + if (block_html !== newblock_html) { + replacements_config.push({ + 'type': 'replace', + 'new_block' : newBlock, + 'target' : $currentBlock + }); + } + else { + // get rid of the new block if the html of the old + // block is exactly the same! + replacements_config.push({ + 'type': 'remove', + 'new_block': newBlock, + 'target' : undefined + }); + } + } + }); + + + // + // Case in which blocks need to be added/appended + // + var $previousBlock; + + $.each($newBlocks, function () { + + var $newBlock = $(this), + id = '#' + $(this).attr('id'), + $previousSibling; + + // If there is a new page block without an equivalent block + // in the old page, we need to find out where to insert it + if (!$(id).length) { + + // Find the previous sibling + $previousSibling = $result.find(id).prev(); + + if ($previousSibling.length) { + // Insert after the previous element + replacements_config.push({ + 'type' : 'insertAfter', + 'target' : $('#' + $previousSibling.attr('id')), + 'new_block' : $newBlock + }); + } else { + // There's no previous sibling, so prepend to parent instead + var parent_id = $newBlock.parent().attr('id'); + if (parent_id === undefined && $previousBlock !== undefined) { + replacements_config.push({ + 'type' : 'insertAfter', + 'target' : $('#' + $previousBlock.attr('id')), + 'new_block' : $newBlock + }); + } + else { + replacements_config.push({ + 'type' : 'prependTo', + 'target' : $('#' + parent_id), + 'new_block' : $newBlock + }); + } + } + } + + // Keep the previous block + $previousBlock = $newBlock; + + // Only add a class to internal links + // TODO: Remove this + // $('a:not(.' + settings.ignoreClass + ')', $newBlock).filter(function () { + // return this.hostname === location.hostname; + // }).addClass('dJAX_internal').on('click.djax', function (event) { + // return _methods.attachClick.call($this, this, event); + // }); + + var replace_fn = function ($newBlock) { + $('a:not(.' + settings.ignoreClass + ')', $newBlock).filter(function () { + return this.hostname === location.hostname; + }).addClass('dJAX_internal').on('click.djax', function (event) { + return _methods.attachClick.call($this, this, event); + }); + } + + replacements_config.push({ + 'type' : 'function', + 'target': replace_fn, + 'args' : [$newBlock] + }); + + }); + + // Delegate block replacement + + var done_fn = function () { + // Trigger djaxLoad event as a pseudo ready() + if (!triggered) { + $(window).trigger( + 'djaxLoad', + [{ + 'url' : url, + 'title' : $result.filter('title').text(), + 'response' : response + }] + ); + triggered = true; + djaxing = false; + } + } + + replacements_config.push({ + 'type' : 'function', + 'target': done_fn, + 'args' : [] + }); + + $(window).trigger( + 'djaxDeferReplacements', + [ replacements_config ] + ); + } + }; + + var methods = { + 'init' : function (options) { + + var settings = $.extend({ + 'selector' : undefined, + 'ignoreClass' : '', + 'exceptions' : [], + 'urlDataAttribute' : undefined, + 'replaceBlockFunction' : undefined, + 'ajax_data_parameter' : { }, + 'ajax_timeout': undefined, + 'ajax_timeout_callback' : undefined, + /* + * Called synchronously before ajax call starts. + * + * - djaxClickData: element or identifier of element the + * user interacted with; + * + * - requestParameters: parameters that will be included in the + * ajax call. This is an object. + */ + 'onDjaxClickCallback' : function (djaxClickData, requestParameters) { return; }, + 'onHistoryPopStateCallback' : function () { return; } + }, options); + + return this.each(function() { + var $this = $(this); + + // save settings + $this.data('settings', settings); + + // If browser doesn't support pushState, abort now + if (!history.pushState) { + return $(this); + } + + var excludes = settings.exceptions, + ignoreClass = settings.ignoreClass, + replaceBlockWith = (settings.replaceBlockFunction) ? settings.replaceBlockFunction: $.fn.replaceWith; + + djaxing = false; + + var blockSelector = settings.selector; + + // Save block selector internally so that we can use it in later calls + $this.data('djaxBlockSelector', blockSelector); + + // Save the replaceBlockWith function internally too... + $this.data('djaxReplaceBlockWith', replaceBlockWith); + + // Save excludes so that we can use them later... + $this.data('djaxUserExcludes', excludes); + + // Ensure that the history is correct when going from 2nd page to 1st + window.history.replaceState( + { + 'url' : window.location.href, + 'title' : $('title').text() + }, + $('title').text(), + window.location.href + ); + + // Exclude the link exceptions + // Only add a class to internal links + $this.find('a:not(.' + ignoreClass + ')').filter(function () { + return this.hostname === location.hostname; + }).addClass('dJAX_internal').on('click.djax', function (event) { + return _methods.attachClick.call($this, this, event); + }); + + // On new page load + $(window).bind('popstate', function (event) { + triggered = false; + + if (event.originalEvent.state) { + var targetUrl = event.originalEvent.state.url; + + // prevent IE <= 9 to navigate repeatedly to the current url + var url_parts = targetUrl.split("#"); + if (url_parts.length === 2) { + if (url_parts[0].indexOf(url_parts[1]) >= 0) { + popstateUrl = ''; + return; + } + } + + if (popstateUrl != targetUrl) { + settings.onHistoryPopStateCallback(); + reqUrl = targetUrl; + popstateUrl = targetUrl; + + _methods.navigate.call($this, { + url: popstateUrl, + add_to_history: false, + method: 'GET' + }); + } + else { + // second time just reset the popstate url. + popstateUrl = ''; + } + } + }); + }); + }, + 'is_djaxing' : function () { + return djaxing; + }, + 'navigate' : function (url, add_to_history, data, method, requestParameters) { + var $this = this; + + if (typeof data === 'undefined') { + data = []; + } + + if (djaxing) { + // push url in the queue and handle once the previous ajax + // request has completed + url_queue.push([url, add_to_history, data, method, requestParameters]); + + // handle queue + setTimeout(function () { _methods.clearDjaxing.call($this)} , 1000); + return $this; + } + else { + triggered = false; + $(window).trigger('djaxClick', data); + + // call blocking callback + var settings = $this.data('settings'); + settings.onDjaxClickCallback.call($this, data, requestParameters); + + reqUrl = url; + _methods.navigate.call($this, { + url: url, + add_to_history: add_to_history, + method: method + }); + } + }, + 'set_ajax_data_parameter' : function (ajax_parameters_func_or_obj) { + var $this = this; + var settings = $this.data('settings'); + + // if function: will be called when needed; if object: will be + // passed straight away to the $.ajax call. + settings.ajax_data_parameter = ajax_parameters_func_or_obj; + + // save parameters + $this.data('settings', settings); + } + }; + + $.fn.djax = function(method) { + /* + * Just a router for method calls + */ + if (methods[method]) { + // call a method + return methods[method].apply(this, + Array.prototype.slice.call(arguments, 1) + ); + } + else if (typeof method == 'object' || !method) { + // call init, user passed the settings as parameters + return methods.init.apply(this, arguments); + } + else { + $.error('Cannot call method ' + method); + } + + }; + +}(jQuery, window)); diff --git a/readme.md b/readme.md index 8897db5..c17ab54 100644 --- a/readme.md +++ b/readme.md @@ -1,139 +1,5 @@ #djax: Dynamic pjax -##Basic usage +This project is not maintained. It was diverging too much from the original project, that ended up as another project: DDJAX. -djax is very quick to set up, with a few markup requirements to let it work smoothly. - -First include it in your header after jquery: - - - - -Then instantiate on the largest page element where you will have updating content. 'body' is typically the way to go with this: - - - -Congrats, you're done! Well mostly... - -##Markup - -djax will track elements with the class you pass it as a first argument. In the above example I've passed the class 'updatable,' so my markup would look something like this: - - -
-
- Here's a div that will be monitored for changes by djax. -
-
- Here's another -
-
- - -Your markup can be laid out however you like, and your trackable sections can be anywhere in relation to one another. It's best to keep them top level (nesting is unnecessary and unsupported,) and -there are a few requirements that allow the plugin to function properly. - -###IDs - -Trackable elements must all have IDs. This is how the requested page is matched up with the current page. Only trackable elements that differ between the two pages will be loaded. -Trackable elements that do not exist on the requested page will be removed, and trackable elements that do not exist on the current page will be added. In order to support this, it -is also necessary to ensure the *parent* elements of every trackable element has an ID, as well as the sibling element immediately *prior* to each trackable element (if one exists). - -These ID's are used to add elements when necessary. If an element exists in a requested page, but not the current page, it will be inserted after the prior sibling (by ID,) or prepended to -the parent element (by ID.) - -##Parameters - -The plugin accepts only two parameters, and only one is required. - -###Tracking Class - -The first and only required parameter is the class you will use to identify trackable elements. If my code looks like the below sample, every dynamic element in my markup should have a class -of djaxable - - - -###Exceptions - -By default djax works on any internal links, but sometimes you may want to exclude certain URLs on your site. The second parameter allows you to pass an array of URL fragments to exclude from djax -loading. This is performed with a simple Javascript 'indexOf,' so the more of the URL you provide, the more specifically your exclusions will be matched. The below example will djax any internal links -that do not contain admin, resources, or ?s= in the url. - - - -##DOM Replacement Callbacks (optional) - -Pass in a reference to a function that will handle the DOM replacement logic. The default djax replacement uses the standard jQuery `replaceWith` and does an immediate replace. For transitions, fade in/outs etc, you can control when and how the new content displays on -the page. The following example fades out the old content, and fades in the new content. - - - - -##Events - -###djaxLoad - -By loading new content via ajax, your visitors will only encounter $('document').ready() the first time they land on your site, and any time they manually perform a hard refresh. To help address this, -djax triggers a window level event on each partial load it performs. Here's an example of enabling pageview tracking with Google Analytics on a djax enabled site: - - $(window).bind('djaxLoad', function(e, data) { - _gaq.push(['_trackPageview']); - }); - -As a convenience, the data object passed with the event contains the requested url, the page title for the requested page, and the contents of the requested page as a string. Use something like the following -code to work with the response as a jQuery object - - $(window).bind('djaxLoad', function(e, data) { - var responseObj = $('
'+data.response+'
'); - //do stuff here - }); - -###djaxClick - -This event is triggered when a djax'ed link is clicked. Use something like the code below to scroll top before loading in new content with ajax: - - $(window).bind('djaxClick', function(e, data) { - var bodyelem = ($.browser.safari) ? bodyelem = $("body") : bodyelem = $("html,body"); - bodyelem.scrollTop(0); - }); - -##Live Demo - -djax arose out of a desire to use [pjax](https://github.com/defunkt/jquery-pjax) with complicated and varied layouts. See [here](http://brianzeligson.com/djax) for a WordPress site using a modified version -of the [bones](http://themble.com/bones/) WordPress theme. djax enabling this theme took about [28 lines of code](https://github.com/beezee/bones-responsive/commit/58aadde224d74f8aaa3266a4bd76e961f2888ada) -(if you count adding a class to an element as a line of code.) - -There is also a small working example in the github repository. Feel free to load up any of the included html files in your browser to see how it works. +See DDJAX at https://github.com/lokku/jquery-ddjax