1/*! 2 * smoothState.js is jQuery plugin that progressively enhances 3 * page loads to behave more like a single-page application. 4 * 5 * @author Miguel Ãngel Pérez [email protected] 6 * @see http://smoothstate.com 7 * 8 */ 9 10(function (factory) { 11 'use strict'; 12 13 if(typeof module === 'object' && typeof module.exports === 'object') { 14 factory(require('jquery'), window, document); 15 } else { 16 factory(jQuery, window, document); 17 } 18}(function ( $, window, document, undefined ) { 19 'use strict'; 20 21 /** Abort if browser does not support pushState */ 22 if(!window.history.pushState) { 23 // setup a dummy fn, but don't intercept on link clicks 24 $.fn.smoothState = function() { return this; }; 25 $.fn.smoothState.options = {}; 26 return; 27 } 28 29 /** Abort if smoothState is already present **/ 30 if($.fn.smoothState) { return; } 31 32 var 33 /** Used later to scroll page to the top */ 34 $body = $('html, body'), 35 36 /** Used in debug mode to console out useful warnings */ 37 consl = window.console, 38 39 /** Plugin default options, will be exposed as $fn.smoothState.options */ 40 defaults = { 41 42 /** If set to true, smoothState will log useful debug information instead of aborting */ 43 debug: false, 44 45 /** jQuery selector to specify which anchors smoothState should bind to */ 46 anchors: 'a', 47 48 /** Regex to specify which href smoothState should load. If empty, every href will be permitted. */ 49 hrefRegex: '', 50 51 /** jQuery selector to specify which forms smoothState should bind to */ 52 forms: 'form', 53 54 /** If set to true, smoothState will store form responses in the cache. */ 55 allowFormCaching: false, 56 57 /** Minimum number of milliseconds between click/submit events. Events ignored beyond this rate are ignored. */ 58 repeatDelay: 500, 59 60 /** A selector that defines what should be ignored by smoothState */ 61 blacklist: '.no-smoothState', 62 63 /** If set to true, smoothState will prefetch a link's contents on hover */ 64 prefetch: false, 65 66 /** The name of the event we will listen to from anchors if we're prefetching */ 67 prefetchOn: 'mouseover touchstart', 68 69 /** jQuery selector to specify elements which should not be prefetched */ 70 prefetchBlacklist: '.no-prefetch', 71 72 /** The response header field name defining the request's final URI. Useful for resolving redirections. */ 73 locationHeader: 'X-SmoothState-Location', 74 75 /** The number of pages smoothState will try to store in memory */ 76 cacheLength: 0, 77 78 /** Class that will be applied to the body while the page is loading */ 79 loadingClass: 'is-loading', 80 81 /** Scroll to top after onStart and scroll to hash after onReady */ 82 scroll: true, 83 84 /** 85 * A function that can be used to alter the ajax request settings before it is called 86 * @param {Object} request - jQuery.ajax settings object that will be used to make the request 87 * @return {Object} Altered request object 88 */ 89 alterRequest: function (request) { 90 return request; 91 }, 92 93 /** 94 * A function that can be used to alter the state object before it is updated or added to the browsers history 95 * @param {Object} state - An object that will be assigned to history entry 96 * @param {string} title - The history entry's title. For reference only 97 * @param {string} url - The history entry's URL. For reference only 98 * @return {Object} Altered state object 99 */ 100 alterChangeState: function (state, title, url) { 101 return state; 102 }, 103 104 /** Run before a page load has been activated */ 105 onBefore: function ($currentTarget, $container) {}, 106 107 /** Run when a page load has been activated */ 108 onStart: { 109 duration: 0, 110 render: function ($container) {} 111 }, 112 113 /** Run if the page request is still pending and onStart has finished animating */ 114 onProgress: { 115 duration: 0, 116 render: function ($container) {} 117 }, 118 119 /** Run when requested content is ready to be injected into the page */ 120 onReady: { 121 duration: 0, 122 render: function ($container, $newContent) { 123 $container.html($newContent); 124 } 125 }, 126 127 /** Run when content has been injected and all animations are complete */ 128 onAfter: function($container, $newContent) {} 129 }, 130 131 /** Utility functions that are decoupled from smoothState */ 132 utility = { 133 134 /** 135 * Checks to see if the url is external 136 * @param {string} url - url being evaluated 137 * @see http://stackoverflow.com/questions/6238351/fastest-way-to-detect-external-urls 138 * 139 */ 140 isExternal: function (url) { 141 var match = url.match(/^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/); 142 if (typeof match[1] === 'string' && match[1].length > 0 && match[1].toLowerCase() !== window.location.protocol) { 143 return true; 144 } 145 if (typeof match[2] === 'string' && 146 match[2].length > 0 && 147 match[2].replace(new RegExp(':(' + {'http:': 80, 'https:': 443}[window.location.protocol] + 148 ')?$'), '') !== window.location.host) { 149 return true; 150 } 151 return false;
152 }, 153 154 /** 155 * Strips the hash from a url and returns the new href 156 * @param {string} href - url being evaluated 157 * 158 */ 159 stripHash: function(href) { 160 return href.replace(/#.*/, ''); 161 }, 162 163 /** 164 * Checks to see if the url is an internal hash 165 * @param {string} href - url being evaluated 166 * @param {string} prev - previous url (optional) 167 * 168 */ 169 isHash: function (href, prev) { 170 prev = prev || window.location.href; 171 172 var hasHash = (href.indexOf('#') > -1) ? true : false, 173 samePath = (utility.stripHash(href) === utility.stripHash(prev)) ? true : false; 174 175 return (hasHash && samePath); 176 }, 177 178 /** 179 * Translates a url string into a $.ajax settings obj 180 * @param {Object|String} request url or settings obj 181 * @return {Object} settings object 182 */ 183 translate: function(request) { 184 var defaults = { 185 dataType: 'html', 186 type: 'GET' 187 }; 188 if(typeof request === 'string') { 189 request = $.extend({}, defaults, { url: request }); 190 } else { 191 request = $.extend({}, defaults, request); 192 } 193 return request; 194 }, 195 196 /** 197 * Checks to see if we should be loading this URL 198 * @param {string} url - url being evaluated 199 * @param {string} blacklist - jquery selector 200 * 201 */ 202 shouldLoadAnchor: function ($anchor, blacklist, hrefRegex) { 203 var href = $anchor.prop('href'); 204 // URL will only be loaded if it's not an external link, hash, or 205 // blacklisted 206 return ( 207 !utility.isExternal(href) && 208 !utility.isHash(href) && 209 !$anchor.is(blacklist) && 210 !$anchor.prop('target')) && 211 ( 212 typeof hrefRegex === undefined || 213 hrefRegex === '' || 214 $anchor.prop('href').search(hrefRegex) !== -1 215 ); 216 }, 217 218 /** 219 * Resets an object if it has too many properties 220 * 221 * This is used to clear the 'cache' object that stores 222 * all of the html. This would prevent the client from 223 * running out of memory and allow the user to hit the 224 * server for a fresh copy of the content. 225 * 226 * @param {object} obj 227 * @param {number} cap 228 * 229 */ 230 clearIfOverCapacity: function (cache, cap) { 231 // Polyfill Object.keys if it doesn't exist 232 if (!Object.keys) { 233 Object.keys = function (obj) { 234 var keys = [], 235 k; 236 for (k in obj) { 237 if (Object.prototype.hasOwnProperty.call(obj, k)) { 238 keys.push(k); 239 } 240 } 241 return keys; 242 }; 243 } 244 245 if (Object.keys(cache).length > cap) { 246 cache = {}; 247 } 248 249 return cache; 250 }, 251 252 /** 253 * Stores a document fragment into an object 254 * @param {object} object - object where it will be stored 255 * @param {string} url - name of the entry 256 * @param {string|document} doc - entire html 257 * @param {string} id - the id of the fragment 258 * @param {object} [state] - the history entry's object 259 * @param {string} [destUrl] - the destination url 260 * @return {object} updated object store 261 */ 262 storePageIn: function (object, url, doc, id, state, destUrl) { 263 var $html = $( '<html></html>' ).append( $(doc) ); 264 if (typeof state === 'undefined') { 265 state = {}; 266 } 267 if (typeof destUrl === 'undefined') { 268 destUrl = url; 269 } 270 object[url] = { // Content is indexed by the url 271 status: 'loaded', 272 // Stores the title of the page, .first() prevents getting svg titles 273 title: $html.find('title').first().text(), 274 html: $html.find('#' + id), // Stores the contents of the page 275 doc: doc, // Stores the whole page document 276 state: state, // Stores the history entry for comparisons, 277 destUrl: destUrl // URL, which will be pushed to history stack 278 }; 279 return object; 280 }, 281 282 /** 283 * Triggers an 'allanimationend' event when all animations are complete 284 * @param {object} $element - jQuery object that should trigger event 285 * @param {string} resetOn - which other events to trigger allanimationend on 286 * 287 */ 288 triggerAllAnimationEndEvent: function ($element, resetOn) { 289 290 resetOn = ' ' + resetOn || ''; 291 292 var animationCount = 0, 293 animationstart = 'animationstart webkitAnimationStart oanimationstart MSAnimationStart', 294 animationend = 'animationend webkitAnimationEnd oanimationend MSAnimationEnd', 295 eventname = 'allanimationend', 296 onAnimationStart = function (e) { 297 if ($(e.delegateTarget).is($element)) { 298 e.stopPropagation(); 299 animationCount++; 300 } 301 }, 302 onAnimationEnd = function (e) { 303 if ($(e.delegateTarget).is($element)) { 304 e.stopPropagation(); 305 animationCount--; 306 if(animationCount === 0) { 307 $element.trigger(eventname); 308 } 309 } 310 }; 311 312 $element.on(animationstart, onAnimationStart); 313 $element.on(animationend, onAnimationEnd); 314 315 $element.on('allanimationend' + resetOn, function(){ 316 animationCount = 0; 317 utility.redraw($element); 318 }); 319 }, 320 321 /** Forces browser to redraw elements */ 322 redraw: function ($element) { 323 $element.height(); 324 } 325 }, 326 327 /** Handles the popstate event, like when the user hits 'back' */ 328 onPopState = function ( e ) { 329 if(e.state !== null && ! e.state.hasOwnProperty('action')) { 330 if ( ! e.state.hasOwnProperty('id') ) { 331 return; 332 } 333 var url = window.location.href, 334 $page = $('#' + e.state.id), 335 page = $page.data('smoothState'),
336 diffUrl = (page.href !== url && !utility.isHash(url, page.href)), 337 diffState = (e.state !== page.cache[page.href].state); 338 339 if(diffUrl || diffState) { 340 if (diffState) { 341 page.clear(page.href); 342 } 343 page.load(url, false); 344 } 345 } 346 }, 347 348 /** Constructor function */ 349 Smoothstate = function ( element, options ) { 350 var 351 /** Container element smoothState is run on */ 352 $container = $(element), 353 354 /** ID of the main container */ 355 elementId = $container.prop('id'), 356 357 /** If a hash was clicked, we'll store it here so we 358 * can scroll to it once the new page has been fully 359 * loaded. 360 */ 361 targetHash = null, 362 363 /** Used to prevent fetching while we transition so 364 * that we don't mistakenly override a cache entry 365 * we need. 366 */ 367 isTransitioning = false, 368 369 /** Variable that stores pages after they are requested */ 370 cache = {}, 371 372 /** Variable that stores data for a history entry */ 373 state = {}, 374 375 /** Url of the content that is currently displayed */ 376 currentHref = window.location.href, 377 378 /** 379 * Clears a given page from the cache, if no url is provided 380 * it will clear the entire cache. 381 * @param {String} url entry that is to be deleted. 382 */ 383 clear = function(url) { 384 url = url || false; 385 if(url && cache.hasOwnProperty(url)) { 386 delete cache[url]; 387 } else { 388 cache = {}; 389 } 390 $container.data('smoothState').cache = cache; 391 }, 392 393 /** 394 * Fetches the contents of a url and stores it in the 'cache' variable 395 * @param {String|Object} request - url or request settings object 396 * @param {Function} callback - function that will run as soon as it finishes 397 */ 398 fetch = function (request, callback) { 399 400 // Sets a default in case a callback is not defined 401 callback = callback || $.noop; 402 403 // Allows us to accept a url string or object as the ajax settings 404 var settings = utility.translate(request); 405 406 // Check the length of the cache and clear it if needed 407 cache = utility.clearIfOverCapacity(cache, options.cacheLength); 408 409 // Don't prefetch if we have the content already or if it's a form 410 if(cache.hasOwnProperty(settings.url) && typeof settings.data === 'undefined') { 411 return; 412 } 413 414 // Let other parts of the code know we're working on getting the content 415 cache[settings.url] = { status: 'fetching' }; 416 417 // Make the ajax request 418 var ajaxRequest = $.ajax(settings); 419 420 // Store contents in cache variable if successful 421 ajaxRequest.done(function (html) { 422 utility.storePageIn(cache, settings.url, html, elementId); 423 $container.data('smoothState').cache = cache; 424 425 jQuery( window ).trigger( 'classChange' ); 426 }); 427 428 // Mark as error to be acted on later 429 ajaxRequest.fail(function () { 430 cache[settings.url].status = 'error'; 431 }); 432 433 if (options.locationHeader) { 434 ajaxRequest.always(function(htmlOrXhr, status, errorOrXhr){ 435 // Resolve where the XHR is based on done() or fail() argument positions 436 var xhr = (htmlOrXhr.statusCode ? htmlOrXhr : errorOrXhr), 437 redirectedLocation = xhr.getResponseHeader(options.locationHeader); 438 439 if (redirectedLocation) { 440 cache[settings.url].destUrl = redirectedLocation; 441 } 442 }); 443 } 444 445 // Call fetch callback 446 if(callback) { 447 ajaxRequest.always(callback); 448 } 449 }, 450 451 repositionWindow = function(){
452 // Scroll to a hash anchor on destination page 453 if(targetHash) { 454 var $targetHashEl = $(targetHash, $container); 455 if($targetHashEl.length){ 456 var newPosition = $targetHashEl.offset().top; 457 $body.scrollTop(newPosition); 458 } 459 targetHash = null; 460 } 461 }, 462 463 /** Updates the contents from cache[url] */ 464 updateContent = function (url) { 465 // If the content has been requested and is done: 466 var containerId = '#' + elementId, 467 $newContent = cache[url] ? $(cache[url].html.html()) : null; 468 469 if($newContent.length) { 470 471 // Update the title 472 document.title = cache[url].title; 473 474 // Update current url 475 $container.data('smoothState').href = url; 476 477 // Remove loading class 478 if(options.loadingClass) { 479 $body.removeClass(options.loadingClass); 480 } 481 482 // Call the onReady callback and set delay 483 options.onReady.render($container, $newContent); 484 485 $container.one('ss.onReadyEnd', function(){ 486 487 // Allow prefetches to be made again 488 isTransitioning = false; 489 490 // Run callback 491 options.onAfter($container, $newContent); 492 493 if (options.scroll) { 494 repositionWindow(); 495 } 496 497 bindPrefetchHandlers($container); 498 499 }); 500 501 window.setTimeout(function(){ 502 $container.trigger('ss.onReadyEnd'); 503 }, options.onReady.duration); 504 505 } else if (!$newContent && options.debug && consl) { 506 // Throw warning to help debug in debug mode 507 consl.warn('No element with an id of ' + containerId + ' in response from ' + url + ' in ' + cache); 508 } else { 509 // No content availble to update with, aborting... 510 window.location = url; 511 } 512 }, 513 514 /** 515 * Loads the contents of a url into our container 516 * @param {string} url 517 * @param {bool} push - used to determine if we should 518 * add a new item into the history object 519 * @param {bool} cacheResponse - used to determine if 520 * we should allow the cache to forget this 521 * page after thid load completes. 522 */ 523 load = function (request, push, cacheResponse) { 524 525 var settings = utility.translate(request); 526 527 /** Makes these optional variables by setting defaults. */ 528 if (typeof push === 'undefined') { 529 push = true; 530 } 531 if (typeof cacheResponse === 'undefined') { 532 cacheResponse = true; 533 } 534 535 var 536 /** Used to check if the onProgress function has been run */ 537 hasRunCallback = false, 538 539 callbBackEnded = false, 540 541 /** List of responses for the states of the page request */ 542 responses = { 543 544 /** Page is ready, update the content */ 545 loaded: function () { 546 var eventName = hasRunCallback ? 'ss.onProgressEnd' : 'ss.onStartEnd'; 547 548 if (!callbBackEnded || !hasRunCallback) { 549 $container.one(eventName, function(){ 550 updateContent(settings.url); 551 if (!cacheResponse) { 552 clear(settings.url); 553 } 554 }); 555 } 556 else if (callbBackEnded) { 557 updateContent(settings.url); 558 } 559 560 if (push) { 561 var destUrl = cache[settings.url].destUrl; 562 563 /** Prepare a history entry */ 564 state = options.alterChangeState({ id: elementId }, cache[settings.url].title, destUrl); 565 566 /** Update the cache to include the history entry for future comparisons */ 567 cache[settings.url].state = state; 568 569 /** Add history entry */ 570 window.history.pushState(state, cache[settings.url].title, destUrl); 571 } 572 573 if (callbBackEnded && !cacheResponse) { 574 clear(settings.url); 575 } 576 }, 577 578 /** Loading, wait 10 ms and check again */ 579 fetching: function () { 580 581 if (!hasRunCallback) { 582 583 hasRunCallback = true; 584 585 // Run the onProgress callback and set trigger 586 $container.one('ss.onStartEnd', function(){ 587 588 // Add loading class 589 if (options.loadingClass) { 590 $body.addClass(options.loadingClass); 591 } 592 593 options.onProgress.render($container); 594 595 window.setTimeout(function (){ 596 $container.trigger('ss.onProgressEnd'); 597 callbBackEnded = true; 598 }, options.onProgress.duration); 599 600 }); 601 } 602 603 window.setTimeout(function () { 604 // Might of been canceled, better check! 605 if (cache.hasOwnProperty(settings.url)){ 606 responses[cache[settings.url].status](); 607 } 608 }, 10); 609 }, 610 611 /** Error, abort and redirect */ 612 error: function (){ 613 if(options.debug && consl) { 614 consl.log('There was an error loading: ' + settings.url); 615 } else { 616 window.location = settings.url; 617 } 618 } 619 }; 620 621 if (!cache.hasOwnProperty(settings.url)) { 622 fetch(settings); 623 } 624 625 // Run the onStart callback and set trigger 626 options.onStart.render($container); 627 628 window.setTimeout(function(){ 629 if (options.scroll) { 630 $body.scrollTop(0); 631 } 632 $container.trigger('ss.onStartEnd'); 633 }, options.onStart.duration); 634 635 // Start checking for the status of content 636 responses[cache[settings.url].status](); 637 }, 638 639 /** 640 * Binds to the hover event of a link, used for prefetching content 641 * @param {object} event 642 */ 643 hoverAnchor = function (event) { 644 var request, 645 $anchor = $(event.currentTarget); 646
647 if (utility.shouldLoadAnchor($anchor, options.blacklist, options.hrefRegex) && !isTransitioning) { 648 event.stopPropagation(); 649 request = utility.translate($anchor.prop('href')); 650 request = options.alterRequest(request); 651 fetch(request); 652 } 653 }, 654 655 /** 656 * Binds to the click event of a link, used to show the content 657 * @param {object} event 658 */ 659 clickAnchor = function (event) { 660 661 // Ctrl (or Cmd) + click must open a new tab 662 var $anchor = $(event.currentTarget); 663 if (!event.metaKey && !event.ctrlKey && utility.shouldLoadAnchor($anchor, options.blacklist, options.hrefRegex)) { 664 665 // stopPropagation so that event doesn't fire on parent containers. 666 event.stopPropagation(); 667 event.preventDefault(); 668 669 // Apply rate limiting. 670 if (!isRateLimited()) { 671 672 // Set the delay timeout until the next event is allowed. 673 setRateLimitRepeatTime(); 674 675 var request = utility.translate($anchor.prop('href')); 676 isTransitioning = true; 677 targetHash = $anchor.prop('hash'); 678 679 // Allows modifications to the request 680 request = options.alterRequest(request); 681 682 options.onBefore($anchor, $container); 683 684 load(request); 685 } 686 } 687 }, 688 689 /** 690 * Binds to form submissions 691 * @param {Event} event 692 */ 693 submitForm = function (event) { 694 var $form = $(event.currentTarget); 695 696 if (!$form.is(options.blacklist)) { 697 698 event.preventDefault(); 699 event.stopPropagation(); 700 701 // Apply rate limiting. 702 if (!isRateLimited()) { 703 704 // Set the delay timeout until the next event is allowed. 705 setRateLimitRepeatTime(); 706 707 var request = { 708 url: $form.prop('action'), 709 data: $form.serialize(), 710 type: $form.prop('method') 711 }; 712 713 isTransitioning = true; 714 request = options.alterRequest(request); 715 716 if (request.type.toLowerCase() === 'get') { 717 request.url = request.url + '?' + request.data; 718 } 719 720 // Call the onReady callback and set delay 721 options.onBefore($form, $container); 722 723 load(request, undefined, options.allowFormCaching); 724 } 725 } 726 }, 727 728 /** 729 * DigitalMachinist (Jeff Rose) 730 * I figured to keep these together with this above functions since they're all related. 731 * Feel free to move these somewhere more appropriate if you have such places. 732 */ 733 rateLimitRepeatTime = 0, 734 isRateLimited = function () { 735 var isFirstClick = (options.repeatDelay === null); 736 var isDelayOver = (parseInt(Date.now()) > rateLimitRepeatTime); 737 return !(isFirstClick || isDelayOver); 738 }, 739 setRateLimitRepeatTime = function () { 740 rateLimitRepeatTime = parseInt(Date.now()) + parseInt(options.repeatDelay); 741 }, 742 743 /** 744 * Binds prefetch events 745 * @param {object} event 746 */ 747 bindPrefetchHandlers = function ($element) { 748 749 if (options.anchors && options.prefetch) { 750 $element.find(options.anchors).not(options.prefetchBlacklist).on(options.prefetchOn, null, hoverAnchor); 751 } 752 }, 753 754 /** 755 * Binds all events and inits functionality 756 * @param {object} event 757 */ 758 bindEventHandlers = function ($element) { 759 760 if (options.anchors) { 761 $element.on('click', options.anchors, clickAnchor); 762 763 bindPrefetchHandlers($element); 764 } 765 766 if (options.forms) { 767 $element.on('submit', options.forms, submitForm); 768 } 769 }, 770 771 /** Restart the container's css animations */ 772 restartCSSAnimations = function () { 773 var classes = $container.prop('class');
774 $container.removeClass(classes); 775 utility.redraw($container); 776 $container.addClass(classes); 777 }; 778 779 /** Merge defaults and global options into current configuration */ 780 options = $.extend( {}, $.fn.smoothState.options, options ); 781 782 /** Sets a default state */ 783 if(window.history.state === null) { 784 state = options.alterChangeState({ id: elementId }, document.title, currentHref); 785 786 /** Update history entry */ 787 window.history.replaceState(state, document.title, currentHref); 788 } else { 789 state = {}; 790 } 791 792 /** Stores the current page in cache variable */ 793 utility.storePageIn(cache, currentHref, document.documentElement.outerHTML, elementId, state); 794 795 /** Bind all of the event handlers on the container, not anchors */ 796 utility.triggerAllAnimationEndEvent($container, 'ss.onStartEnd ss.onProgressEnd ss.onEndEnd'); 797 798 /** Bind all of the event handlers on the container, not anchors */ 799 bindEventHandlers($container); 800 801 802 /** Public methods */ 803 return { 804 href: currentHref, 805 cache: cache, 806 clear: clear, 807 load: load, 808 fetch: fetch, 809 restartCSSAnimations: restartCSSAnimations 810 }; 811 }, 812 813 /** Returns elements with smoothState attached to it */ 814 declaresmoothState = function ( options ) { 815 return this.each(function () { 816 var tagname = this.tagName.toLowerCase(); 817 // Checks to make sure the smoothState element has an id and isn't already bound 818 if(this.id && tagname !== 'body' && tagname !== 'html' && !$.data(this, 'smoothState')) { 819 // Makes public methods available via $('element').data('smoothState'); 820 $.data(this, 'smoothState', new Smoothstate(this, options)); 821 } else if (!this.id && consl) { 822 // Throw warning if in debug mode 823 consl.warn('Every smoothState container needs an id but the following one does not have one:', this); 824 } else if ((tagname === 'body' || tagname === 'html') && consl) { 825 // We dont support making th html or the body element the smoothstate container 826 consl.warn('The smoothstate container cannot be the ' + this.tagName + ' tag'); 827 } 828 }); 829 }; 830 831 /** Sets the popstate function */ 832 window.onpopstate = onPopState; 833 834 /** Makes utility functions public for unit tests */ 835 $.smoothStateUtility = utility; 836 837 /** Defines the smoothState plugin */ 838 $.fn.smoothState = declaresmoothState; 839 840 /* expose the default options */ 841 $.fn.smoothState.options = defaults; 842 843}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.