PageRenderTime 63ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/ajax/libs/smoothState.js/0.2.1/jquery.smoothState.js

https://gitlab.com/Mirros/cdnjs
JavaScript | 424 lines | 214 code | 53 blank | 157 comment | 46 complexity | c2751e9a428b90d47efb43a2f9a50d3b MD5 | raw file
  1. /*!
  2. * jQuery htmlDoc "fixer" - v0.2pre - 8/8/2011
  3. * http://benalman.com/projects/jquery-misc-plugins/
  4. *
  5. * Copyright (c) 2010 "Cowboy" Ben Alman
  6. * Dual licensed under the MIT and GPL licenses.
  7. * http://benalman.com/about/license/
  8. */
  9. (function ($) {
  10. "use strict";
  11. /**
  12. * This is jQuery plugin that progressively enhances page loads
  13. * to behave more like single-page application.
  14. *
  15. * The approach taken here is that of a mix of ajax, pushstate,
  16. * and a series of render functions that output the scafolding markup
  17. * needed for CSS animations on an interval. The jquery plugin is run
  18. * on a container element. This container will listen for links that are
  19. * interacted with and fetch the content, run the render functions, and
  20. * update the URL of the page.
  21. *
  22. * @author Miguel Ángel Pérez reachme@miguel-perez.com
  23. * @param {object} options - List of configuarable variables
  24. *
  25. */
  26. $.fn.smoothState = function (options) {
  27. var poppedState = false, // used later to check if we need to update the URL
  28. hasPopped = false,
  29. cache = {}, // used to store the contents that we fetch with ajax
  30. $body = $("body"),
  31. $wind = $(window),
  32. consl = (window.console || false),
  33. matchTag = /<(\/?)(html|head|body|title|base|meta)(\s+[^>]*)?>/ig,
  34. prefix = 'ss' + Math.round(Math.random() * 100 * 100);
  35. // Defaults
  36. options = $.extend({
  37. prefetch : false,
  38. blacklist : ".no-smoothstate, [rel='nofollow'], [target]",
  39. loadingBodyClass : "loading-cursor", //@todo: We don't need this if we provide right hooks
  40. development : false,
  41. pageCacheSize : 5,
  42. frameDelay : 400,
  43. renderFrame : [
  44. function ($content) {
  45. return $("<div/>").append($content).html();
  46. }
  47. ],
  48. alterRequestUrl : function (url) {
  49. return url;
  50. },
  51. onAfter : function () {},
  52. onBefore : function () {
  53. $wind.scrollTop(0);
  54. }
  55. }, options);
  56. /**
  57. * Loads the contents of a url into a specified container
  58. *
  59. * @todo Don't wait until the response is done to start animating content
  60. * @param {string} url
  61. * @param {jQuery} $container - container the new content
  62. * will be injected into.
  63. *
  64. */
  65. function load(url, $container) {
  66. // Checks to see if we already have the contents of this URL
  67. if (cache.hasOwnProperty(url)) {
  68. // Null is an indication that the Ajax request has been
  69. // fired but has not completed.
  70. if (cache[url] === null) {
  71. // If the content has been request but is not done,
  72. // wait 10ms and try again and add a loading indicator.
  73. setTimeout(function () {
  74. $body.addClass(options.loadingBodyClass);
  75. load(url, $container);
  76. }, 10);
  77. } else if (cache[url] === "error") {
  78. // If there was an error, abort and redirect
  79. window.location = url;
  80. } else {
  81. // If the content has been requested and is done:
  82. // 1. Remove loading class
  83. $body.removeClass(options.loadingBodyClass);
  84. // 2. Run onBefore function
  85. options.onBefore(url, $container);
  86. // 3. Start to update the page
  87. updatePage(url, $container);
  88. }
  89. } else {
  90. // Starts to fetch and load the content if we haven't started
  91. // to load the content.
  92. fetch(url);
  93. load(url, $container);
  94. }
  95. }
  96. /**
  97. * Fetches the contents of a url and stores it in the 'cache' varible
  98. * @param {string} url
  99. *
  100. */
  101. function fetch(url) {
  102. if (!cache.hasOwnProperty(url)) {
  103. cache[url] = null;
  104. var requestUrl = options.alterRequestUrl(url),
  105. request = $.ajax(requestUrl);
  106. // Store contents in cache variable if successful
  107. request.success(function (html) {
  108. // Clear cache varible if it's getting too big
  109. cache = clearIfOverCapacity(cache, options.pageCacheSize);
  110. cache[url] = { // Content is indexed by the url
  111. title: $(html).filter("title").text(), // Stores the title of the page
  112. html: html // Stores the contents of the page
  113. };
  114. });
  115. // Mark as error
  116. request.error(function () {
  117. cache[url] = "error";
  118. });
  119. }
  120. }
  121. /**
  122. * Resets an object if it has too many properties
  123. *
  124. * This is used to clear the 'cache' object that stores
  125. * all of the html. This would prevent the client from
  126. * running out of memory and allow the user to hit the
  127. * server for a fresh copy of the content.
  128. *
  129. * @param {object} obj
  130. * @param {number} cap
  131. *
  132. */
  133. function clearIfOverCapacity(obj, cap) {
  134. // Polyfill Object.keys if it doesn't exist
  135. if (!Object.keys) {
  136. Object.keys = function (obj) {
  137. var keys = [],
  138. k;
  139. for (k in obj) {
  140. if (Object.prototype.hasOwnProperty.call(obj, k)) {
  141. keys.push(k);
  142. }
  143. }
  144. return keys;
  145. };
  146. }
  147. if (Object.keys(obj).length > cap) {
  148. obj = {};
  149. }
  150. return obj;
  151. }
  152. /**
  153. * Fetches the contents of a url and stores it in the 'cache' varible
  154. * @param {string} url
  155. * @todo $content jquery object should be stored, speed improvment
  156. *
  157. */
  158. function updatePage(url, $container) {
  159. var containerId = $container.prop("id"),
  160. $html = htmlDoc(cache[url].html),
  161. $content = (containerId.length) ? $html.find("#" + containerId).html() : "";
  162. // We check to see if the container we hope to update is
  163. // returned in the request so that we can replace existing
  164. // content with the updated markup.
  165. if (containerId.length && $content.length) {
  166. animateContent($content, $container);
  167. updateState(cache[url].title, url, containerId);
  168. } else if (options.development && consl) { // Throw warning to help debug
  169. if (!containerId.length) { // No container ID
  170. consl.warn("The following container has no ID: ", $container[0]);
  171. } else if (!$content.length) { // No container in the response
  172. consl.warn("No element with an ID of '#" + containerId + "' in response from " + url);
  173. }
  174. } else {
  175. // If the container isn't in the response, just abort.
  176. window.location = url;
  177. }
  178. }
  179. /**
  180. * Begins to loop through all of the render functions that alter the DOM
  181. * @param {jquery} $content - the markup that will replace the
  182. * contents of the container.
  183. * @param {jquery} $container - the container that is listening for
  184. * interactions to links.
  185. *
  186. */
  187. function animateContent($content, $container) {
  188. var i, isLastFrame;
  189. for (i = 0; i < options.renderFrame.length; i += 1) {
  190. isLastFrame = (i === options.renderFrame.length - 1);
  191. showFrame(i, $content, $container, isLastFrame);
  192. }
  193. }
  194. /**
  195. * Updates the page title and URL
  196. * @param {string} title - title of the page we fetched content from
  197. * @param {string} url - url that we just fetched content from
  198. * @param {string} id - the id of the container that was updated
  199. *
  200. */
  201. function updateState(title, url, id) {
  202. document.title = title;
  203. if (!poppedState) {
  204. // the id is used to know what needs to be updated on the popState event
  205. history.pushState({ id: id }, title, url);
  206. hasPopped = true;
  207. } else {
  208. poppedState = false;
  209. }
  210. }
  211. /**
  212. * Defines when the render functions will run
  213. * @param {number} i - index of the function in options.renderFrame
  214. * @param {jquery} $content - the markup that will replace the
  215. * contents of the container.
  216. * @param {jquery} $container - the container that is listening for
  217. * interactions to links.
  218. * @param {bool} isLastFrame - used to determine if the callback should fire
  219. *
  220. */
  221. function showFrame(i, $content, $container, isLastFrame) {
  222. var timing = options.frameDelay * i;
  223. setTimeout(function () {
  224. var html = options.renderFrame[i]($content, $container);
  225. $container.html(html);
  226. if (isLastFrame) {
  227. options.onAfter($content, $container);
  228. }
  229. }, timing);
  230. }
  231. /**
  232. * Checks to see if the url is external
  233. * @param {string} url - url being evaluated
  234. * @see http://stackoverflow.com/questions/6238351/fastest-way-to-detect-external-urls
  235. *
  236. */
  237. function isExternal(url) {
  238. var match = url.match(/^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/);
  239. if (typeof match[1] === "string" && match[1].length > 0 && match[1].toLowerCase() !== location.protocol) {
  240. return true;
  241. }
  242. if (typeof match[2] === "string" && match[2].length > 0 && match[2].replace(new RegExp(":(" + {"http:": 80, "https:": 443}[location.protocol] + ")?$"), "") !== location.host) {
  243. return true;
  244. }
  245. return false;
  246. }
  247. /**
  248. * Checks to see if the url is an internal hash
  249. * @param {string} url - url being evaluated
  250. *
  251. */
  252. function isHash(url) {
  253. var hasPathname = (url.indexOf(window.location.pathname) > 0) ? true : false,
  254. hasHash = (url.indexOf("#") > 0) ? true : false;
  255. return (hasPathname && hasHash) ? true : false;
  256. }
  257. /**
  258. * Checks to see if we should be loading this URL
  259. * @param {string} url - url being evaluated
  260. *
  261. */
  262. function shouldLoad($anchor) {
  263. var url = $anchor.prop("href");
  264. // URL will only be loaded if it's not an external link, hash, or blacklisted
  265. return (!isExternal(url) && !isHash(url) && !$anchor.is(options.blacklist));
  266. }
  267. /**
  268. * Prevents jQuery from stripping elements from $(html)
  269. * @param {string} url - url being evaluated
  270. * @author Ben Alman http://benalman.com/
  271. * @see https://gist.github.com/cowboy/742952
  272. *
  273. */
  274. function htmlDoc (html) {
  275. var parent,
  276. elems = $(),
  277. htmlParsed = html.replace(matchTag, function(tag, slash, name, attrs) {
  278. var obj = {};
  279. if (!slash) {
  280. elems = elems.add('<' + name + '/>');
  281. if (attrs) {
  282. $.each($('<div' + attrs + '/>')[0].attributes, function(i, attr) {
  283. obj[attr.name] = attr.value;
  284. });
  285. }
  286. elems.eq(-1).attr(obj);
  287. }
  288. return '<' + slash + 'div' + (slash ? '' : ' id="' + prefix + (elems.length - 1) + '"') + '>';
  289. });
  290. // If no placeholder elements were necessary, just return normal
  291. // jQuery-parsed HTML.
  292. if (!elems.length) {
  293. return $(html);
  294. }
  295. // Create parent node if it hasn't been created yet.
  296. if (!parent) {
  297. parent = $('<div/>');
  298. }
  299. // Create the parent node and append the parsed, place-held HTML.
  300. parent.html(htmlParsed);
  301. // Replace each placeholder element with its intended element.
  302. $.each(elems, function(i) {
  303. var elem = parent.find('#' + prefix + i).before(elems[i]);
  304. elems.eq(i).html(elem.contents());
  305. elem.remove();
  306. });
  307. return parent.children().unwrap();
  308. }
  309. /**
  310. * Binds to the hover event of a link, used for prefetching content
  311. *
  312. * @param {object} event
  313. *
  314. */
  315. function hoverAnchor(event) {
  316. event.stopPropagation();
  317. var $anchor = $(event.currentTarget),
  318. url = $anchor.prop("href");
  319. if (shouldLoad($anchor)) {
  320. fetch(url);
  321. }
  322. }
  323. /**
  324. * Binds to the click event of a link, used to show the content
  325. *
  326. * @param {object} event
  327. * @todo Allow loading from a template in addition to an ajax request
  328. *
  329. */
  330. function clickAnchor(event) {
  331. // stopPropagation so that event doesn't fire on parent containers.
  332. event.stopPropagation();
  333. var $anchor = $(event.currentTarget),
  334. url = $anchor.prop("href"),
  335. $container = $(event.delegateTarget);
  336. if (shouldLoad($anchor)) {
  337. event.preventDefault();
  338. load(url, $container);
  339. }
  340. }
  341. /**
  342. * Handles the popstate event, like when the user hits 'back'
  343. *
  344. * @param {object} event
  345. * @see https://developer.mozilla.org/en-US/docs/Web/API/Window.onpopstate
  346. *
  347. */
  348. function onPopState() {
  349. var url = window.location.href;
  350. if (!isHash(url) && history.state) {
  351. // Sets the flag that we've begun to pop states
  352. poppedState = true;
  353. // Update content if we know what needs to be updated
  354. load(url, $("#" + history.state.id));
  355. } else if (history.state === null && hasPopped) {
  356. window.location = url;
  357. }
  358. }
  359. // Sets the popstate function
  360. window.onpopstate = onPopState;
  361. // Returns the jquery object
  362. return this.each(function () {
  363. //@todo: Handle form submissions
  364. var $this = $(this);
  365. $this.on("click", "a", clickAnchor);
  366. if (options.prefetch) {
  367. $this.on("mouseover touchstart", "a", hoverAnchor);
  368. }
  369. });
  370. };
  371. })(jQuery);