UNPKG

formstone

Version:

Library of modular front end components.

494 lines (403 loc) 12.5 kB
/* global define */ /* global ga */ (function(factory) { if (typeof define === "function" && define.amd) { define([ "jquery", "./core", "./analytics" ], factory); } else { factory(jQuery, Formstone); } }(function($, Formstone) { "use strict"; /** * @method private * @name initialize * @description Initializes plugin. * @param opts [object] "Plugin options" */ function initialize(options) { if (Instance || !Formstone.support.history) { return; } $Body = Formstone.$body; Instance = $.extend(Defaults, options); if (Instance.render === $.noop) { Instance.render = renderState; } if (Instance.transitionOut === $.noop) { Instance.transitionOut = function() { return $.Deferred().resolve(); }; } // Initial state if (history.state) { CurrentID = history.state.id; CurrentURL = history.state.url; } else { CurrentURL = window.location.href; replaceState(CurrentID, CurrentURL); } // Bind state events $Window.on(Events.popState, onPop); enable(); } /** * @method private * @name disable * @description Disable ASAP * @example $.asap("enable"); */ function disable() { if ($Body && $Body.hasClass(RawClasses.base)) { $Body.off(Events.click) .removeClass(RawClasses.base); } } /** * @method private * @name enable * @description Enables ASAP * @example $.asap("enable"); */ function enable() { if ($Body && !$Body.hasClass(RawClasses.base)) { $Body.on(Events.click, Instance.selector, onClick) .addClass(RawClasses.base); } } /** * @method private * @name onClick * @description Handles click events * @param e [object] "Event data" */ function onClick(e) { var url = e.currentTarget; // Ignore everything but normal click if ( (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) || (window.location.protocol !== url.protocol || window.location.host !== url.host) || url.target === "_blank" ) { return; } // Update state on hash change if (url.hash && (url.href.replace(url.hash, "") === window.location.href.replace(location.hash, "") || url.href === window.location.href + "#")) { return; } // Ignore certain file types if (url.href.match(Instance.ignoreTypes)) { return; } Functions.killEvent(e); e.stopImmediatePropagation(); if (url.href !== CurrentURL) { requestURL(url.href, true); } } /** * @method private * @name onPop * @description Handles history navigation events * @param e [object] "Event data" */ function onPop(e) { if (Request) { Request.abort(); } var state = e.originalEvent.state; // direction = (state.id > CurrentID) ? "forward" : "back"; if (state) { CurrentID = state.id; if (state.url !== CurrentURL) { requestURL(state.url, false); } } } /** * @method private * @name requestURL * @description Requests new content via AJAX * @param url [string] "URL to load" * @param doPush [boolean] "Flag to push to stack" */ function requestURL(url, doPush) { if (Request) { Request.abort(); } // Fire request event $Window.trigger(Events.requested, [doPush]); // Get transition out deferred Instance.transitionOutDeferred = Instance.transitionOut.apply(Window, [false]); var parsed = parseURL(url), params = parsed.params, hash = parsed.hash, cleanURL = parsed.clean, error = "User error", response = null, requestDeferred = $.Deferred(); params[Instance.requestKey] = true; // Request new content Request = $.ajax({ url: cleanURL, data: params, dataType: "json", cache: Instance.cache, xhr: function() { // custom xhr var xhr = new Window.XMLHttpRequest(); /* //Upload progress ? xhr.upload.addEventListener("progress", function(e) { if (e.lengthComputable) { var percent = (e.loaded / e.total) / 2; $window.trigger(Events.progress, [ percent ]); } }, false); */ //Download progress xhr.addEventListener("progress", function(e) { if (e.lengthComputable) { var percent = e.loaded / e.total; $Window.trigger(Events.progress, [percent]); } }, false); return xhr; }, success: function(resp, status, jqXHR) { response = ($.type(resp) === "string") ? $.parseJSON(resp) : resp; // handle redirects - requires passing new location with json response if (resp.location) { url = resp.location; parsed = parseURL(url); hash = parsed.hash; } requestDeferred.resolve(); }, error: function(jqXHR, status, err) { error = err; requestDeferred.reject(); } }); $.when(requestDeferred, Instance.transitionOutDeferred).done(function() { processResponse(parsed, response, doPush); }).fail(function() { $Window.trigger(Events.failed, [error]); }); } /** * @method private * @name processResponse * @description Processes a state * @param parsedURL [object] "Parsed URL" * @param data [object] "State Data" * @param doPush [boolean] "Flag to replace or add state" */ function processResponse(parsedURL, data, doPush) { // Fire load event $Window.trigger(Events.loaded, [data]); // Trigger analytics page view if ($.fsAnalytics !== undefined) { $.fsAnalytics("pageview"); } // Render before updating Instance.render.call(this, data, parsedURL.hash); // Update current url CurrentURL = parsedURL.url; if (doPush) { // Push new states to the stack CurrentID++; pushState(CurrentID, CurrentURL); } $Window.trigger(Events.rendered, [data]); var scrollTop = false; if (parsedURL.hash !== "") { var $el = $(parsedURL.hash); if ($el.length) { scrollTop = $el.offset().top; } } if (scrollTop !== false) { $Window.scrollTop(scrollTop); } } /** * @method private * @name renderHTML * @description Renders a new state * @param data [object] "State Data" * @param hash [string] "Hash" */ function renderState(data, hash) { // Update DOM if ($.type(data) !== "undefined") { var $target; for (var key in data) { if (data.hasOwnProperty(key)) { $target = $(key); if ($target.length) { $target.html(data[key]); } } } } } /** * @method private * @name loadURL * @description Loads new page * @param opts [url] <''> "URL to load" */ /** * @method * @name load * @description Loads new page * @param opts [url] <''> "URL to load" * @example $.asap("load", "http://example.com/page/"); */ function loadURL(url) { if (!Instance || !Formstone.support.history) { window.location.href = url; } else if (url) { requestURL(url, true); } return; } /** * @method private * @name replaceURL * @description Updates current url in history * @param url [string] <''> "New URL" */ /** * @method * @name replace * @description Updates current url in history * @param url [string] <''> "New URL" * @example $.asap("replace", "http://example.com/page/"); */ function replaceURL(url) { var state = history.state; CurrentURL = url; replaceState(state.id, url); } /** * @method private * @name pushState * @description Push state to the history stack * @param id [int] "State id" * @param url [string] "State url" */ function pushState(id, url) { history.pushState({ id: id, url: url }, Namespace + id, url); } /** * @method private * @name replaceState * @description Push state to the history stack * @param id [int] "State id" * @param url [string] "State url" */ function replaceState(id, url) { history.replaceState({ id: id, url: url }, Namespace + id, url); } /** * @method private * @name parseURL * @description Parse url parts * @param url [string] "URL to parse" */ function parseURL(url) { var queryIndex = url.indexOf("?"), hashIndex = url.indexOf("#"), params = {}, hash = "", cleanURL = url; if (hashIndex > -1) { hash = url.slice(hashIndex); cleanURL = url.slice(0, hashIndex); } if (queryIndex > -1) { params = Functions.parseQueryString(url.slice(queryIndex + 1, ((hashIndex > -1) ? hashIndex : url.length))); cleanURL = url.slice(0, queryIndex); } return { hash: hash, params: params, url: url, clean: cleanURL }; } /** * @plugin * @name ASAP * @description A jQuery plugin for asynchronous page loads. * @type utility * @main asap.js * @dependency jQuery * @dependency core.js * @dependency analytics.js */ var Plugin = Formstone.Plugin("asap", { utilities: { _initialize: initialize, load: loadURL, replace: replaceURL }, /** * @events * @event requested.asap "Before request is made; triggered on window; Second parameter 'true' if pop event" * @event progress.asap "As request is loaded; triggered on window; Second parameter contains percentage complete" * @event loaded.asap "After request is loaded; triggered on window" * @event rendered.asap "After state is rendered; triggered on window" * @event failed.asap "After load error; triggered on window" */ events: { failed: "failed", loaded: "loaded", popState: "popstate", progress: "progress", requested: "requested", rendered: "rendered" } }), /** * @options * @param cache [boolean] <true> "Flag to cache AJAX responses" * @param ignoreTypes [regex] <> "File types to ignore" * @param render [function] <$.noop> "Custom render function" * @param requestKey [string] <'fs-asap'> "GET variable for requests" * @param selector [string] <'a'> "Target DOM Selector" * @param transitionOut [function] <$.noop> "Transition timing callback; should return user defined $.Deferred object, which must eventually resolve" */ Defaults = { cache: true, ignoreTypes: /\.(jpg|sjpg|jpeg|png|gif|zip|exe|dmg|pdf|doc.*|xls.*|ppt.*|mp3|txt|rar|wma|mov|avi|wmv|flv|wav)$/i, render: $.noop, requestKey: "fs-asap", selector: "a", transitionOut: $.noop }, // Localize References $Window = Formstone.$window, Window = $Window[0], $Body, Functions = Plugin.functions, Events = Plugin.events, RawClasses = Plugin.classes.raw, // Internal Namespace = "asap-", CurrentURL = '', CurrentID = 1, Request, Instance; }) );