当前位置: 首页 > 工具软件 > jQuery-pjax > 使用案例 >

pjax-standalone 改版

闽鸿宝
2023-12-01
/**!
 * PJAX- Standalone
 *
 * A standalone implementation of Pushstate AJAX, for non-jQuery web pages.
 * jQuery are recommended to use the original implementation at: http://github.com/defunkt/jquery-pjax
 * 
 * @version 0.6.1
 * @author Carl
 * @source https://github.com/thybag/PJAX-Standalone
 * @license MIT
 */
(function(){ 

    // Object to store private values/methods.
    var internal = {
        // Is this the first usage of PJAX? (Ensure history entry has required values if so.)
        "firstrun": true,
        // Borrowed wholesale from https://github.com/defunkt/jquery-pjax
        // Attempt to check that a device supports pushstate before attempting to use it.
        "is_supported": window.history && window.history.pushState && window.history.replaceState && !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/),
        // Track which scripts have been included in to the page. (used if e)
        "loaded_scripts": []
    };

    // If PJAX isn't supported we can skip setting up the library all together
    // So as not to break any code expecting PJAX to be there, return a shell object containing
    // IE7 + compatible versions of connect (which needs to do nothing) and invoke ( which just changes the page)
    if(!internal.is_supported) {
        // PJAX shell, so any code expecting PJAX will work
        var pjax_shell = {
            "connect": function() { return; },
            "invoke": function() {
                var url = (arguments.length === 2) ? arguments[0] : arguments.url;
                document.location = url;
                return; 
            } 
        };
        // AMD support
        if (typeof define === 'function' && define.amd) { 
            define( function() { return pjax_shell; }); 
        } else { 
            window.pjax = pjax_shell; 
        }
        return;
    }

    /**
     * AddEvent
     *
     * @scope private
     * @param obj Object to listen on
     * @param event Event to listen for.
     * @param callback Method to run when event is detected.
     */
    internal.addEvent = function(obj, event, callback) {
        obj.addEventListener(event, callback, false);
    };

    /**
     * Clone
     * Util method to create copies of the options object (so they do not share references)
     * This allows custom settings on different links.
     *
     * @scope private
     * @param obj
     * @return obj
     */
    internal.clone = function(obj) {
        var object = {};
        // For every option in object, create it in the duplicate.
        for (var i in obj) {
            object[i] = obj[i];
        }
        return object;
    };

    /**
     * triggerEvent
     * Fire an event on a given object (used for callbacks)
     *
     * @scope private
     * @param node. Objects to fire event on
     * @return event_name. type of event
     */
    internal.triggerEvent = function(node, event_name, data) {
        // Good browsers
        var evt = document.createEvent("HTMLEvents");
        evt.initEvent(event_name, true, true);
        // If additional data was provided, add it to event
        if(typeof data !== 'undefined') evt.data = data;
        node.dispatchEvent(evt);
    };

    /**
     * popstate listener
     * Listens for back/forward button events and updates page accordingly.
     */
    internal.addEvent(window, 'popstate', function(st) {
        if(st.state !== null) {

            var opt = { 
                'url': st.state.url, 
                'container': st.state.container, 
                'title' : st.state.title,
                'history': false
            };

            // Merge original in original connect options
            if(typeof internal.options !== 'undefined'){
                for(var a in internal.options){ 
                    if(typeof opt[a] === 'undefined') opt[a] = internal.options[a];
                }
            }

            // Convert state data to PJAX options
            var options = internal.parseOptions(opt);
            // If something went wrong, return.
            if(options === false) return;
            // If there is a state object, handle it as a page load.
            internal.handle(options);
        }
    });

    /**
     * attach
     * Attach PJAX listeners to a link.
     * @scope private
     * @param link_node. link that will be clicked.
     * @param content_node. 
     */
    internal.attach = function(node, options) {

        // Ignore external links.
        if ( node.protocol !== document.location.protocol ||
            node.host !== document.location.host ) {
            return;
        }

        // Ignore anchors on the same page
        if(node.pathname === location.pathname && node.hash.length > 0) {
            return;
        }

        // Ignore common non-PJAX loadable media types (pdf/doc/zips & images) unless user provides alternate array
        var ignoreFileTypes = ['pdf','doc','docx','zip','rar','7z','gif','jpeg','jpg','png'];
        if(typeof options.ignoreFileTypes === 'undefined') options.ignoreFileTypes = ignoreFileTypes;
        // Skip link if file type is within ignored types array
        if(options.ignoreFileTypes.indexOf( node.pathname.split('.').pop().toLowerCase() ) !== -1){
            return;
        }

        // Add link HREF to object
        options.url = node.href;

        // If PJAX data is specified, use as container
        if(node.getAttribute('data-pjax')) {
            options.container = node.getAttribute('data-pjax');
        }

        // If data-title is specified, use as title.
        if(node.getAttribute('data-title')) {
            options.title = node.getAttribute('data-title');
        }

        // Check options are valid.
        options = internal.parseOptions(options);
        if(options === false) return;

        // Attach event.
        internal.addEvent(node, 'click', function(event) {
            // Allow middle click (pages in new windows)
            if ( event.which > 1 || event.metaKey || event.ctrlKey ) return;
            // Don't fire normal event
            if(event.preventDefault){ event.preventDefault(); }else{ event.returnValue = false; }
            // Take no action if we are already on said page?
            if(document.location.href === options.url) return false;
            // handle the load.
            internal.handle(options);
        });
    };

    /**
     * parseLinks
     * Parse all links within a DOM node, using settings provided in options.
     * @scope private
     * @param dom_obj. Dom node to parse for links.
     * @param options. Valid Options object.
     */
    internal.parseLinks = function(dom_obj, options) {

        var nodes;

        if(typeof options.useClass !== 'undefined'){
            // Get all nodes with the provided class name.
            nodes = dom_obj.getElementsByClassName(options.useClass);
        }else{
            // If no class was provided, just get all the links
            nodes = dom_obj.getElementsByTagName('a');
        }

        // For all returned nodes
        for(var i=0,tmp_opt; i < nodes.length; i++) {
            var node = nodes[i];
            if(typeof options.excludeClass !== 'undefined') {
                if(node.className.indexOf(options.excludeClass) !== -1) continue;
            }
            // Override options history to true, else link parsing could be triggered by back button (which runs in no-history mode)
            tmp_opt = internal.clone(options);
            tmp_opt.history = true;
            internal.attach(node, tmp_opt);
        }

        if(internal.firstrun) {
            // Store array or all currently included script src's to avoid PJAX accidentally reloading existing libraries
            var scripts = document.getElementsByTagName('script');
            for(var c=0; c < scripts.length; c++) {
                if(scripts[c].src && internal.loaded_scripts.indexOf(scripts[c].src) === -1){
                    internal.loaded_scripts.push(scripts[c].src);
                }
            }

            // Fire ready event once all links are connected
            internal.triggerEvent(internal.get_container_node(options.container), 'ready');

        }
    };

    /**
     * SmartLoad
     * Smartload checks the returned HTML to ensure PJAX ready content has been provided rather than
     * a full HTML page. If a full HTML has been returned, it will attempt to scan the page and extract
     * the correct HTML to update our container with in order to ensure PJAX still functions as expected.
     *
     * @scope private
     * @param HTML (HTML returned from AJAX)
     * @param options (Options object used to request page)
     * @return HTML to append to our page.
     */
    internal.smartLoad = function(html, options) {
        // Grab the title if there is one
        var title = html.getElementsByTagName('title')[0];
        if(title){
            document.title = title.innerHTML;
        }

        // Going by caniuse all browsers that support the pushstate API also support querySelector's
        // see: http://caniuse.com/#search=push
        // see: http://caniuse.com/#search=querySelector
        if (options.container.id=='') {
            return html;
        }
        var container = html.querySelector("#" + options.container.id);
        if(container !== null) return container;

        // If our container was not found, HTML will be returned as is.
        return html;
    };

    /**
     * Update Content
     * Updates DOM with content loaded via PJAX
     *
     * @param html DOM fragment of loaded container
     * @param options PJAX configuration options
     * return options
     */
    internal.updateContent = function(html, options){
        // Create in memory DOM node, to make parsing returned data easier
        var tmp = document.createElement('div');
        tmp.innerHTML = html; 

        // Ensure we have the correct HTML to apply to our container.
        if(options.smartLoad) tmp = internal.smartLoad(tmp, options);

        // If no title was provided, extract it
        if(typeof options.title === 'undefined'){
            // Use current doc title (this will be updated via smart load if its enabled)
            options.title = document.title;

            // Attempt to grab title from non-smart loaded page contents 
            if(!options.smartLoad){
                var tmpTitle = tmp.getElementsByTagName('title');
                if(tmpTitle.length !== 0) options.title = tmpTitle[0].innerHTML;
            }
        }

        // Update the DOM with the new content
        options.container.innerHTML = tmp.innerHTML;

        // Run included JS?
        if(options.parseJS) internal.runScripts(tmp);

        // Send data back to handle
        return options;
    };

    /**
     * runScripts
     * Execute JavaScript on pages loaded via PJAX
     *
     * Note: In-line JavaScript is run each time a page is hit, while external JavaScript
     *      is only loaded once (Although remains loaded while the user continues browsing)
     *
     * @param html DOM fragment of loaded container
     * return void
     */
    internal.runScripts = function(html){
        // Extract JavaScript & eval it (if enabled)
        var scripts = html.getElementsByTagName('script');
        for(var sc=0; sc < scripts.length;sc++) {
            // If has an src & src isn't in "loaded_scripts", load the script.
            if(scripts[sc].src && internal.loaded_scripts.indexOf(scripts[sc].src) === -1){
                // Append to head to include
                var s = document.createElement("script"); 
                s.src = scripts[sc].src;
                document.head.appendChild(s);
                // Add to loaded list
                internal.loaded_scripts.push(scripts[sc].src);
            }else{
                // If raw JS, eval it. 
                eval(scripts[sc].innerHTML);
            }
        }
    };

    /**
     * handle
     * Handle requests to load content via PJAX.
     * @scope private
     * @param url. Page to load.
     * @param node. Dom node to add returned content in to.
     * @param addtohistory. Does this load require a history event.
     */
    internal.handle = function(options) {

        // Fire beforeSend Event.
        internal.triggerEvent(options.container, 'beforeSend', options);

        // Do the request
        internal.request(options.url, function(html) {

            // Fail if unable to load HTML via AJAX
            if(html === false){
                internal.triggerEvent(options.container,'complete', options);
                internal.triggerEvent(options.container,'error', options);
                return;
            }

            // Parse page & update DOM
            options = internal.updateContent(html, options);

            // Do we need to add this to the history?
            if(options.history) {
                // If this is the first time pjax has run, create a state object for the current page.
                if(internal.firstrun){
                    window.history.replaceState({'url': document.location.href, 'container':  options.container.id, 'title': document.title}, document.title);
                    internal.firstrun = false;
                }
                // Update browser history
                window.history.pushState({'url': options.url, 'container': options.container.id, 'title': options.title }, options.title , options.url);
            }

            // Initialize any new links found within document (if enabled).
            if(options.parseLinksOnload){
                internal.parseLinks(options.container, options);
            }

            // Fire Events
            internal.triggerEvent(options.container,'complete', options);
            internal.triggerEvent(options.container,'success', options);

            // Don't track if page isn't part of history, or if autoAnalytics is disabled
            if(options.autoAnalytics && options.history) {
                // If autoAnalytics is enabled and a Google analytics tracker is detected push 
                // a trackPageView, so PJAX loaded pages can be tracked successfully.
                if(window._gaq) _gaq.push(['_trackPageview']);
                if(window.ga) ga('send', 'pageview', {'page': options.url, 'title': options.title});
            }

            // Set new title
            // document.title = options.title; // lxx //

            // Scroll page to top on new page load
            if(options.returnToTop) {
                window.scrollTo(0, 0);
            } 
        });
    };

    /**
     * Request
     * Performs AJAX request to page and returns the result..
     *
     * @scope private
     * @param location. Page to request.
     * @param callback. Method to call when a page is loaded.
     */
    internal.request = function(location, callback) {
        // Create xmlHttpRequest object.
        var xmlhttp;
        try { 
            xmlhttp = window.XMLHttpRequest? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); 
        }  catch (e) { 
            console.log("Unable to create XMLHTTP Request");
            return; 
        }
        // Add state listener.
        xmlhttp.onreadystatechange = function() {
            if ((xmlhttp.readyState === 4) && (xmlhttp.status === 200)) {
                // Success, Return HTML
                callback(xmlhttp.responseText);
            }else if((xmlhttp.readyState === 4) && (xmlhttp.status === 404 || xmlhttp.status === 500)){
                // error (return false)
                callback(false);
            }
        };
        // Secret pjax ?get param so browser doesn't return pjax content from cache when we don't want it to
        // Switch between ? and & so as not to break any URL params (Based on change by zmasek https://github.com/zmasek/)
        xmlhttp.open("GET", location + ((!/[?&]/.test(location)) ? '?_pjax' : '&_pjax'), true);
        // Add headers so things can tell the request is being performed via AJAX.
        xmlhttp.setRequestHeader('X-PJAX', 'true'); // PJAX header
        xmlhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');// Standard AJAX header.

        xmlhttp.send(null);
    };

    /**
     * parseOptions
     * Validate and correct options object while connecting up any listeners.
     *
     * @scope private
     * @param options
     * @return false | valid options object
     */
    internal.parseOptions = function(options) {

        /**  Defaults parse options. (if something isn't provided)
         *
         * - history: track event to history (on by default, set to off when performing back operation)
         * - parseLinksOnload: Enabled by default. Process pages loaded via PJAX and setup PJAX on any links found.
         * - smartLoad: Tries to ensure the correct HTML is loaded. If you are certain your back end 
         *      will only return PJAX ready content this can be disabled for a slight performance boost.
         * - autoAnalytics: Automatically attempt to log events to Google analytics (if tracker is available)
         * - returnToTop: Scroll user back to top of page, when new page is opened by PJAX
         * - parseJS: Disabled by default, when enabled PJAX will automatically run returned JavaScript
         */
        var defaults = {
            "history": true,
            "parseLinksOnload": true,
            "smartLoad" : true,
            "autoAnalytics": true,
            "returnToTop": true,
            "parseJS": false
        };

        // Ensure a URL and container have been provided.
        if(typeof options.url === 'undefined' || typeof options.container === 'undefined' || options.container === null) {
            console.log("URL and Container must be provided.");
            return false;
        }

        // Check required options are defined, if not, use default
        for(var o in defaults) {
            if(typeof options[o] === 'undefined') options[o] = defaults[o];
        }

        // Ensure history setting is a boolean.
        options.history = (options.history === false) ? false : true;

        // Get container (if its an id, convert it to a DOM node.)
        options.container = internal.get_container_node(options.container);

        // Events
        var events = ['ready', 'beforeSend', 'complete', 'error', 'success'];

        // If everything went okay thus far, connect up listeners
        for(var e in events){
            var evt = events[e];
            if(typeof options[evt] === 'function'){
                internal.addEvent(options.container, evt, options[evt]);
            }
        }

        // Return valid options
        return options;
    };

    /**
     * get_container_node
     * Returns container node
     * lxx 
     * @param container - (string) container ID | container DOM node.
     * @return container DOM node | false
     */
    internal.get_container_node = function(container) {
        if(typeof container === 'string') {
            container = document.getElementById(container);
            if(container === null){
                // console.log("Could not find container with id:" + container);
                container = document.body; // not find container and default = body
            }
        }
        return container;
    };

    /**
     * connect
     * Attach links to PJAX handlers.
     * @scope public
     *
     * Can be called in 3 ways.
     * Calling as connect(); 
     *      Will look for links with the data-pjax attribute.
     *
     * Calling as connect(container_id)
     *      Will try to attach to all links, using the container_id as the target.
     *
     * Calling as connect(container_id, class_name)
     *      Will try to attach any links with the given class name, using container_id as the target.
     *
     * Calling as connect({ 
     *                      'url':'somepage.php',
     *                      'container':'somecontainer',
     *                      'beforeSend': function(){console.log("sending");}
     *                  })
     *      Will use the provided JSON to configure the script in full (including callbacks)
     */
    this.connect = function(/* options */) {
        // connect();
        var options = {};
        // connect(container, class_to_apply_to)
        if(arguments.length === 2){
            options.container = arguments[0];
            options.useClass = arguments[1];
        }
        // Either JSON or container id
        if(arguments.length === 1){
            if(typeof arguments[0] === 'string' ) {
                //connect(container_id)
                options.container = arguments[0];
            }else{
                //Else connect({url:'', container: ''});
                options = arguments[0];
            }
        }
        // Delete history and title if provided. These options should only be provided via invoke();
        delete options.title;
        delete options.history;

        internal.options = options;
        if(document.readyState === 'complete') {
            internal.parseLinks(document, options);
        } else {
            //Don't run until the window is ready.
            internal.addEvent(window, 'load', function(){   
                //Parse links using specified options
                internal.parseLinks(document, options);
            });
        }
    };

    /**
     * invoke
     * Directly invoke a pjax page load.
     * invoke({url: 'file.php', 'container':'content'});
     *
     * @scope public
     * @param options  
     */
    this.invoke = function(/* options */) {

        var options = {};
        // url, container
        if(arguments.length === 2){
            options.url = arguments[0];
            options.container = arguments[1];
        }else{
            options = arguments[0];
        }

        // Process options
        options = internal.parseOptions(options);
        // If everything went okay, activate pjax.
        if(options !== false) internal.handle(options);
    };

    // Make object usable
    var pjax_obj = this;
    if (typeof define === 'function' && define.amd) {
        // Register pjax as AMD module
        define( function() {
            return pjax_obj;
        });
    }else{
        // Make PJAX object accessible in global name space
        window.pjax = pjax_obj;
    }


}).call({});
 类似资料: