| /* |
| * Copyright 2011 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /** |
| * @fileoverview Code for deferring javascript on client side. |
| * This javascript is part of JsDefer filter. |
| * |
| * @author atulvasu@google.com (Atul Vasu) |
| * @author ksimbili@google.com (Kishore Simbili) |
| */ |
| |
| goog.require('pagespeedutils'); |
| |
| |
| /** |
| * Defer javascript will be executed in two phases. First phase will be for |
| * high priority scripts and second phase is for low priority scripts. Order |
| * of execution will be: |
| * 1) High priority scripts. |
| * 2) Low priority scripts. |
| * 3) Onload of high priority scripts. |
| * 4) Onload of low priority scripts. |
| * |
| * In case of blink, order will be: |
| * 1) High priority scripts present in above the fold. |
| * 2) Low priority scripts present in above the fold. |
| * 3) High priority scripts which are below the fold. |
| * 4) Low priority scripts which are below the fold. |
| * 5) Onload of high priority scripts. |
| * 6) Onload of low priority scripts. |
| * |
| * Exporting functions using quoted attributes to prevent js compiler from |
| * renaming them. |
| * See http://code.google.com/closure/compiler/docs/api-tutorial3.html#dangers |
| */ |
| window['pagespeed'] = window['pagespeed'] || {}; |
| var pagespeed = window['pagespeed']; |
| |
| pagespeed['deferJsNs'] = {}; |
| var deferJsNs = pagespeed['deferJsNs']; |
| |
| |
| |
| /** |
| * @constructor |
| */ |
| deferJsNs.DeferJs = function() { |
| /** |
| * Queue of tasks that need to be executed in order. |
| * @type {!Array<function()>} |
| * @private |
| */ |
| this.queue_ = []; |
| |
| /** |
| * Array of logs, for debugging. |
| * @type {Array<string>} |
| */ |
| this.logs = []; |
| |
| /** |
| * Next item in the queue to be executed. |
| * @type {!number} |
| * @private |
| */ |
| this.next_ = 0; |
| |
| /** |
| * Number of scripts dynamically inserted which are not yet executed. |
| * @type {!number} |
| * @private |
| */ |
| this.dynamicInsertedScriptCount_ = 0; |
| |
| /** |
| * Scripts dynamically inserted by other scripts. |
| * @type {Array<Element>} |
| * @private |
| */ |
| this.dynamicInsertedScript_ = []; |
| |
| /** |
| * document.write() strings get buffered here, they get rendered when the |
| * current script is finished executing. |
| * @private |
| */ |
| this.documentWriteHtml_ = ''; |
| |
| // TODO(sriharis): Can we have a listener module that is used for the |
| // following. |
| |
| /** |
| * Map of events to event listeners. |
| * @type {!Object} |
| * @private |
| */ |
| this.eventListenersMap_ = {}; |
| |
| /** |
| * Valid Mime types for Javascript. |
| */ |
| this.jsMimeTypes = |
| ['application/ecmascript', |
| 'application/javascript', |
| 'application/x-ecmascript', |
| 'application/x-javascript', |
| 'text/ecmascript', |
| 'text/javascript', |
| 'text/javascript1.0', |
| 'text/javascript1.1', |
| 'text/javascript1.2', |
| 'text/javascript1.3', |
| 'text/javascript1.4', |
| 'text/javascript1.5', |
| 'text/jscript', |
| 'text/livescript', |
| 'text/x-ecmascript', |
| 'text/x-javascript']; |
| |
| /** |
| * We override certain builtin browser functions, such as document.write. |
| * After OnLoad, however, these should go back to behaving as they originally |
| * did. This flag deals with the case where client JS code in turn overrides |
| * our overridden implementations. |
| * @type {boolean} |
| * @private |
| */ |
| this.overrideDefaultImplementation_ = true; |
| |
| /** |
| * Original document.getElementById handler. |
| * @private |
| */ |
| this.origGetElementById_ = document.getElementById; |
| |
| /** |
| * Original document.getElementsByTagName handler. |
| * @private |
| */ |
| this.origGetElementsByTagName_ = document.getElementsByTagName; |
| |
| /** |
| * Original document.write handler. |
| * @private |
| */ |
| this.origDocWrite_ = document.write; |
| |
| /** |
| * Original document.open handler. |
| * @private |
| */ |
| this.origDocOpen_ = document.open; |
| |
| /** |
| * Original document.close handler. |
| * @private |
| */ |
| this.origDocClose_ = document.close; |
| |
| /** |
| * Original document.addEventListener handler. |
| * @private |
| */ |
| this.origDocAddEventListener_ = document.addEventListener; |
| |
| /** |
| * Original window.addEventListener handler. |
| * @private |
| */ |
| this.origWindowAddEventListener_ = window.addEventListener; |
| |
| /** |
| * Original document.addEventListener handler. |
| * @private |
| */ |
| this.origDocAttachEvent_ = document.attachEvent; |
| |
| /** |
| * Original window.addEventListener handler. |
| * @private |
| */ |
| this.origWindowAttachEvent_ = window.attachEvent; |
| |
| /** |
| * Original document.createElement handler. |
| * @private |
| */ |
| this.origCreateElement_ = document.createElement; |
| |
| /** |
| * Maintains the current state for the deferJs. |
| * @type {!number} |
| * @private |
| */ |
| this.state_ = deferJsNs.DeferJs.STATES.NOT_STARTED; |
| |
| /** |
| * Maintains the last fired event. |
| * @type {!number} |
| * @private |
| */ |
| this.eventState_ = deferJsNs.DeferJs.EVENT.NOT_STARTED; |
| |
| /** |
| * This variable indicates whether deferJs is called for the first time. |
| * This is set to false if deferJs is called again. |
| * @private |
| */ |
| this.firstIncrementalRun_ = true; |
| |
| /** |
| * This variable indicates whether deferJs is called for the last time. |
| * This is set to false in registerScriptTags if deferJs will be called again |
| * @private |
| */ |
| this.lastIncrementalRun_ = true; |
| |
| /** |
| * Callback to call when current incremental scripts are done executing. |
| * @private |
| */ |
| this.incrementalScriptsDoneCallback_ = null; |
| |
| /** |
| * This variable counts the total number of async scripts created by no defer |
| * scripts. |
| * @type {!number} |
| * @private |
| */ |
| this.noDeferAsyncScriptsCount_ = 0; |
| |
| /** |
| * Async scripts created by no defer scripts. |
| * @type {Array<Element>} |
| * @private |
| */ |
| this.noDeferAsyncScripts_ = []; |
| |
| /** |
| * Type of the javascript node that will get executed. |
| * @type {string} |
| * @private |
| */ |
| this.psaScriptType_ = ''; |
| |
| /** |
| * Attribute added for nodes which are not processed yet. |
| * @type {string} |
| * @private |
| */ |
| this.psaNotProcessed_ = ''; |
| |
| /** |
| * Last Index until incremental scripts will be executed, rest scripts will |
| * be executed after the execution of incrementalScriptsDoneCallback_. |
| * @type {!number} |
| * @private |
| */ |
| this.optLastIndex_ = -1; |
| }; |
| |
| |
| /** |
| * Indicates if experimental js in deferJS is active. |
| * @type {boolean} |
| */ |
| deferJsNs.DeferJs.isExperimentalMode = false; |
| |
| |
| /** |
| * State Machine |
| * NOT_STARTED --> SCRIPTS_REGISTERED --> SCRIPTS_EXECUTING ---------- |
| * ^ | |
| * | | |
| * WAITING_FOR_NEXT_RUN <-- | |
| * \ | |
| * \ | |
| * SCRIPTS_DONE <--- WAITING_FOR_ONLOAD <--- SYNC_SCRIPTS_DONE <------ |
| * |
| * Constants for different states of deferJs exeuction. |
| * @enum {number} |
| */ |
| deferJsNs.DeferJs.STATES = { |
| /** |
| * Start state. |
| */ |
| NOT_STARTED: 0, |
| /** |
| * This state used to mark when critical scripts are done. |
| */ |
| WAITING_FOR_NEXT_RUN: 1, |
| /** |
| * In this state all script tags with type as 'text/psajs' are registered for |
| * deferred execution. |
| */ |
| SCRIPTS_REGISTERED: 2, |
| /** |
| * Script execution is in process. |
| */ |
| SCRIPTS_EXECUTING: 3, |
| /** |
| * All the sync scripts are executed but some async scripts may not executed |
| * till now. |
| */ |
| SYNC_SCRIPTS_DONE: 4, |
| /** |
| * Waiting for onload event to be triggered. |
| */ |
| WAITING_FOR_ONLOAD: 5, |
| /** |
| * Final state. |
| */ |
| SCRIPTS_DONE: 6 |
| }; |
| |
| |
| /** |
| * Constants for different events used by deferJs. |
| * @enum {number} |
| */ |
| deferJsNs.DeferJs.EVENT = { |
| /** |
| * Start event state. |
| */ |
| NOT_STARTED: 0, |
| /** |
| * Event triggered before executing deferred scripts. |
| */ |
| BEFORE_SCRIPTS: 1, |
| /** |
| * Event corresponding to DOMContentLoaded. |
| */ |
| DOM_READY: 2, |
| /** |
| * Event corresponding to onload. |
| */ |
| LOAD: 3, |
| /** |
| * Event triggered after executing deferred scripts. |
| */ |
| AFTER_SCRIPTS: 4 |
| }; |
| |
| |
| /** |
| * Name of the attribute set for the nodes that are not reached so far during |
| * priority scripts execution. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PRIORITY_PSA_NOT_PROCESSED = 'priority_psa_not_processed'; |
| |
| |
| /** |
| * Name of the attribute set for the nodes that are not reached so far during |
| * scripts execution. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_NOT_PROCESSED = 'psa_not_processed'; |
| |
| |
| /** |
| * Name of the attribute set for the current node to mark the current location. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_CURRENT_NODE = 'psa_current_node'; |
| |
| |
| /** |
| * Name of the attribute to mark the script node for deletion after the |
| * execution. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_TO_BE_DELETED = 'psa_to_be_deleted'; |
| |
| |
| /** |
| * Value for psa dummy script nodes. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_SCRIPT_TYPE = 'text/psajs'; |
| |
| |
| /** |
| * Value of psa dummmy priority script nodes. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PRIORITY_PSA_SCRIPT_TYPE = 'text/prioritypsajs'; |
| |
| |
| /** |
| * Name of orig_type attribute in deferred script node. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_ORIG_TYPE = 'data-pagespeed-orig-type'; |
| |
| |
| /** |
| * Name of orig_src attribute in deferred script node. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_ORIG_SRC = 'data-pagespeed-orig-src'; |
| |
| |
| /** |
| * Name of orig_index attribute in deferred script node. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PSA_ORIG_INDEX = 'orig_index'; |
| |
| |
| /** |
| * Name of the deferred onload attribute. |
| * @const {string} |
| */ |
| deferJsNs.DeferJs.PAGESPEED_ONLOAD = 'data-pagespeed-onload'; |
| |
| |
| /** |
| * Add to defer_logs if logs are enabled. |
| * @param {string} line line to be added to log. |
| * @param {Error=} opt_exception optional exception to pass to log. |
| */ |
| deferJsNs.DeferJs.prototype.log = function(line, opt_exception) { |
| if (this.logs) { |
| this.logs.push('' + line); |
| if (opt_exception) { |
| this.logs.push(opt_exception.message); |
| if (typeof(console) != 'undefined' && |
| typeof(console.log) != 'undefined') { |
| console.log('PSA ERROR: ' + line + opt_exception.message); |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * Adds task to the end of queue, unless position is explicitly given. |
| * @param {!function()} task Function closure to be executed later. |
| * @param {number=} opt_pos optional position for ordering of jobs. |
| */ |
| deferJsNs.DeferJs.prototype.submitTask = function(task, opt_pos) { |
| var pos = opt_pos ? opt_pos : this.queue_.length; |
| this.queue_.splice(pos, 0, task); |
| }; |
| |
| |
| /** |
| * @param {string} str to be evaluated. |
| * @param {Element=} opt_script_elem Script element to copy attributes into the |
| * new script node. |
| * @return {Element} Script cloned element which is created. |
| */ |
| deferJsNs.DeferJs.prototype.globalEval = function(str, opt_script_elem) { |
| var script = this.cloneScriptNode(opt_script_elem); |
| script.text = str; |
| script.setAttribute('type', 'text/javascript'); |
| var currentElem = this.getCurrentDomLocation(); |
| currentElem.parentNode.insertBefore(script, currentElem); |
| return script; |
| }; |
| |
| |
| /** |
| * Defines a new var in the name of id's present in the doc. This is the fix for |
| * IE, where setting value to the var with same name as an id in the doc throws |
| * exception. While creating vars, skip the names which have '-', ':', '.'. |
| * Also, variable names cannot start with digits. |
| * These characters are allowed in id names but not allowed in variable |
| * names. |
| */ |
| deferJsNs.DeferJs.prototype.createIdVars = function() { |
| var elems = document.getElementsByTagName('*'); |
| var idVarsString = ''; |
| for (var i = 0; i < elems.length; i++) { |
| // Don't use elem.id since it leads to problem in forms. |
| if (elems[i].hasAttribute('id')) { |
| var idStr = elems[i].getAttribute('id'); |
| if (idStr && idStr.search(/[-:.]/) == -1 && |
| idStr.search(/^[0-9]/) == -1) { |
| try { |
| if (window[idStr] && window[idStr].tagName) { |
| idVarsString += 'var ' + idStr + |
| '=document.getElementById("' + idStr + '");'; |
| } |
| } catch (err) { |
| this.log('Exception while evaluating.', err); |
| } |
| } |
| } |
| } |
| if (idVarsString) { |
| var script = this.globalEval(idVarsString); |
| script.setAttribute(deferJsNs.DeferJs.PSA_NOT_PROCESSED, ''); |
| script.setAttribute(deferJsNs.DeferJs.PRIORITY_PSA_NOT_PROCESSED, ''); |
| } |
| }; |
| |
| |
| /** |
| * Tries to prefetch the file using rel-preload. |
| * @param {!string} url Url to be downloaded. |
| */ |
| deferJsNs.DeferJs.prototype.attemptPrefetchOrQueue = function(url) { |
| var newLink = this.origCreateElement_.call(document, 'link'); |
| newLink.setAttribute('rel', 'preload'); |
| newLink.setAttribute('as', 'script'); |
| newLink.setAttribute('href', url); |
| document.head.appendChild(newLink); |
| }; |
| |
| |
| /** |
| * Defers execution of scriptNode, by adding it to the queue. |
| * @param {Element} script script node. |
| * @param {number=} opt_pos Optional position for ordering. |
| * @param {boolean=} opt_prefetch Script file is prefetched if true. |
| */ |
| deferJsNs.DeferJs.prototype.addNode = function(script, opt_pos, opt_prefetch) { |
| var src = script.getAttribute(deferJsNs.DeferJs.PSA_ORIG_SRC) || |
| script.getAttribute('src'); |
| if (src) { |
| if (opt_prefetch) { |
| this.attemptPrefetchOrQueue(src); |
| } |
| this.addUrl(src, script, opt_pos); |
| } else { |
| // ||'ed with empty string to make sure the the value of str is not |
| // undefined or null. |
| var str = script.innerHTML || script.textContent || script.data || ''; |
| this.addStr(str, script, opt_pos); |
| } |
| }; |
| |
| |
| /** |
| * Defers execution of 'str', by adding it to the queue. |
| * @param {!string} str valid javascript snippet. |
| * @param {Element} script_elem Psa inserted script used as context element. |
| * @param {number=} opt_pos Optional position for ordering. |
| */ |
| deferJsNs.DeferJs.prototype.addStr = function(str, script_elem, opt_pos) { |
| if (this.isFireFox()) { |
| // This is due to some bug identified in firefox. |
| // Got this workaround from the bug raised on firefox. |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=728151 |
| this.addUrl('data:text/javascript,' + encodeURIComponent(str), |
| script_elem, |
| opt_pos); |
| return; |
| } |
| this.logs.push('Add to queue str: ' + str); |
| var me = this; // capture closure. |
| this.submitTask(function() { |
| me.removeNotProcessedAttributeTillNode(script_elem); |
| |
| var node = me.nextPsaJsNode(); |
| node.setAttribute(deferJsNs.DeferJs.PSA_CURRENT_NODE, ''); |
| try { |
| me.globalEval(str, script_elem); |
| } catch (err) { |
| me.log('Exception while evaluating.', err); |
| } |
| me.log('Evaluated: ' + str); |
| // TODO(ksimbili): Detach stack here to prevent recursion issues. |
| me.runNext(); |
| }, opt_pos); |
| }; |
| deferJsNs.DeferJs.prototype['addStr'] = deferJsNs.DeferJs.prototype.addStr; |
| |
| |
| /** |
| * Clones a Script Node. This is equivalent of 'cloneNode'. |
| * @param {Element=} opt_script_elem Psa inserted script used for cloning the |
| * new element. |
| * @return {Element} Script Element with all attributes copied from |
| * opt_script_elem. |
| */ |
| deferJsNs.DeferJs.prototype.cloneScriptNode = function(opt_script_elem) { |
| var newScript = this.origCreateElement_.call(document, 'script'); |
| if (opt_script_elem) { |
| // Copy attributes. |
| for (var a = opt_script_elem.attributes, n = a.length, i = n - 1; |
| i >= 0; --i) { |
| // Ignore 'type' and 'src' as they are set later. |
| // Ignore 'async' and 'defer', as our current. |
| // TODO(ksimbili): If a script has async then don't wait for it to load. |
| if (a[i].name != 'type' && a[i].name != 'src' && |
| a[i].name != 'async' && a[i].name != 'defer' && |
| a[i].name != deferJsNs.DeferJs.PSA_ORIG_TYPE && |
| a[i].name != deferJsNs.DeferJs.PSA_ORIG_SRC && |
| a[i].name != deferJsNs.DeferJs.PSA_ORIG_INDEX && |
| a[i].name != deferJsNs.DeferJs.PSA_CURRENT_NODE && |
| a[i].name != this.psaNotProcessed_) { |
| newScript.setAttribute(a[i].name, a[i].value); |
| opt_script_elem.removeAttribute(a[i].name); |
| } |
| } |
| } |
| return newScript; |
| }; |
| |
| |
| /** |
| * Creates a dummy script node on load of which the next deferred script is |
| * executed. |
| * Note, this function needs to be called after a synchronous script node. |
| * @param {!string} url returns javascript when fetched. |
| */ |
| deferJsNs.DeferJs.prototype.scriptOnLoad = function(url) { |
| var script = this.origCreateElement_.call(document, 'script'); |
| script.setAttribute('type', 'text/javascript'); |
| script.async = false; |
| script.setAttribute(deferJsNs.DeferJs.PSA_TO_BE_DELETED, ''); |
| script.setAttribute(deferJsNs.DeferJs.PSA_NOT_PROCESSED, ''); |
| script.setAttribute(deferJsNs.DeferJs.PRIORITY_PSA_NOT_PROCESSED, ''); |
| var me = this; // capture closure. |
| var runNextHandler = function() { |
| if (document.querySelector) { |
| // Find the script node we inserted and remove it. |
| var node = document.querySelector( |
| '[' + deferJsNs.DeferJs.PSA_TO_BE_DELETED + ']'); |
| if (node) { |
| node.parentNode.removeChild(node); |
| } |
| } |
| me.log('Executed: ' + url); |
| me.runNext(); |
| }; |
| // The onload of the new script node is guaranteed which marks as the load |
| // completion of 'url'. |
| deferJsNs.addOnload(script, runNextHandler); |
| pagespeedutils.addHandler(script, 'error', runNextHandler); |
| script.src = 'data:text/javascript,' + |
| encodeURIComponent('window.pagespeed.psatemp=0;'); |
| var currentElem = this.getCurrentDomLocation(); |
| currentElem.parentNode.insertBefore(script, currentElem); |
| }; |
| |
| |
| /** |
| * Defers execution of contents of 'url'. |
| * @param {!string} url returns javascript when fetched. |
| * @param {Element} script_elem Psa inserted script used as context element. |
| * @param {number=} opt_pos Optional position for ordering. |
| */ |
| deferJsNs.DeferJs.prototype.addUrl = function(url, script_elem, opt_pos) { |
| this.logs.push('Add to queue url: ' + url); |
| var me = this; // capture closure. |
| this.submitTask(function() { |
| me.removeNotProcessedAttributeTillNode(script_elem); |
| |
| var script = me.cloneScriptNode(script_elem); |
| script.setAttribute('type', 'text/javascript'); |
| var useSyncScript = true; |
| if ('async' in script) { |
| script.async = false; |
| } else if (script.readyState) { |
| useSyncScript = false; |
| var stateChangeHandler = function() { |
| if (script.readyState == 'complete' || |
| script.readyState == 'loaded') { |
| script.onreadystatechange = null; |
| me.log('Executed: ' + url); |
| me.runNext(); |
| } |
| }; |
| pagespeedutils.addHandler(script, 'readystatechange', stateChangeHandler); |
| } |
| script.setAttribute('src', url); |
| // If a script node with src also has a node inside it |
| // (as innerHTML etc.), we simply create an equivalent text node so |
| // that the DOM remains the same. Note that we do not try to execute |
| // the contents of this node. |
| var str = script_elem.innerHTML || |
| script_elem.textContent || |
| script_elem.data; |
| if (str) { |
| script.appendChild(document.createTextNode(str)); |
| } |
| var currentElem = me.nextPsaJsNode(); |
| currentElem.setAttribute(deferJsNs.DeferJs.PSA_CURRENT_NODE, ''); |
| currentElem.parentNode.insertBefore(script, currentElem); |
| if (useSyncScript) { |
| // We cannot depend on the onload of the script node we just inserted, |
| // because of the way we preload the js resources. Hence we are using |
| // 'scriptOnLoad' to insert a dummy script tag whose onload is fired |
| // reliably. |
| me.scriptOnLoad(url); |
| } |
| }, opt_pos); |
| }; |
| deferJsNs.DeferJs.prototype['addUrl'] = deferJsNs.DeferJs.prototype.addUrl; |
| |
| |
| /** |
| * Remove psaNotProcessed_ attribute till the given node. |
| * @param {Node=} opt_node Stop node. |
| */ |
| deferJsNs.DeferJs.prototype.removeNotProcessedAttributeTillNode = function( |
| opt_node) { |
| if (document.querySelectorAll && !(this.getIEVersion() <= 8)) { |
| var nodes = document.querySelectorAll( |
| '[' + this.psaNotProcessed_ + ']'); |
| for (var i = 0; i < nodes.length; i++) { |
| var dom_node = nodes.item(i); |
| if (dom_node == opt_node) { |
| return; |
| } |
| if (dom_node.getAttribute('type') != this.psaScriptType_) { |
| dom_node.removeAttribute(this.psaNotProcessed_); |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * Set 'psa_not_processed' attribute to all Nodes in DOM. |
| */ |
| deferJsNs.DeferJs.prototype.setNotProcessedAttributeForNodes = function() { |
| var nodes = this.origGetElementsByTagName_.call(document, '*'); |
| for (var i = 0; i < nodes.length; i++) { |
| var dom_node = nodes.item(i); |
| dom_node.setAttribute(this.psaNotProcessed_, ''); |
| } |
| }; |
| |
| |
| /** |
| * Get the next script psajs node to be executed. |
| * @return {Element} Element having type attribute set to 'text/psajs'. |
| */ |
| deferJsNs.DeferJs.prototype.nextPsaJsNode = function() { |
| var current_node = null; |
| if (document.querySelector) { |
| current_node = document.querySelector( |
| '[type="' + this.psaScriptType_ + '"]'); |
| } |
| return current_node; |
| }; |
| |
| |
| /** |
| * Get the current location in DOM for the new nodes insertion. New nodes are |
| * inserted before the returned node. |
| * @return {Element} Element having 'psa_current_node' attribute. |
| */ |
| deferJsNs.DeferJs.prototype.getCurrentDomLocation = function() { |
| var current_node; |
| if (document.querySelector) { |
| current_node = document.querySelector( |
| '[' + deferJsNs.DeferJs.PSA_CURRENT_NODE + ']'); |
| } |
| return current_node || |
| this.origGetElementsByTagName_.call(document, 'psanode')[0]; |
| }; |
| |
| |
| /** |
| * Removes the processed script node with 'text/psajs'. |
| */ |
| deferJsNs.DeferJs.prototype.removeCurrentDomLocation = function() { |
| var oldNode = this.getCurrentDomLocation(); |
| // getCurrentDomLocation can return 'psanode' which is not a script. |
| if (oldNode.nodeName == 'SCRIPT') { |
| oldNode.parentNode.removeChild(oldNode); |
| } |
| }; |
| |
| |
| /** |
| * Called when the script Queue execution is finished. |
| */ |
| deferJsNs.DeferJs.prototype.onComplete = function() { |
| if (this.state_ >= deferJsNs.DeferJs.STATES.WAITING_FOR_ONLOAD) { |
| return; |
| } |
| |
| if (this.lastIncrementalRun_ && this.isLowPriorityDeferJs()) { |
| // ReadyState should be restored only during the last onComplete, |
| // so that document.readyState returns 'loading' till the last deferred |
| // script is executed. |
| if (this.getIEVersion() && document.documentElement['originalDoScroll']) { |
| document.documentElement.doScroll = |
| document.documentElement['originalDoScroll']; |
| } |
| if (Object.defineProperty) { |
| // Delete document.readyState so that browser can restore it. |
| delete document['readyState']; |
| } |
| if (this.getIEVersion()) { |
| if (Object.defineProperty) { |
| // Delete document.all so that browser can restore it. |
| delete document['all']; |
| } |
| } |
| } |
| |
| this.overrideDefaultImplementation_ = false; |
| |
| if (this.lastIncrementalRun_) { |
| this.state_ = deferJsNs.DeferJs.STATES.WAITING_FOR_ONLOAD; |
| |
| if (this.isLowPriorityDeferJs()) { |
| var me = this; |
| if (document.readyState != 'complete') { |
| deferJsNs.addOnload(window, function() { |
| me.fireOnload(); |
| }); |
| } else { |
| // Here there is a chance that window.onload is triggered twice. |
| // But we have no way of finding this. |
| // TODO(ksimbili): Fix the above scenario. |
| if (document.onreadystatechange) { |
| this.exec(document.onreadystatechange, document); |
| } |
| // Execute window.onload |
| if (window.onload) { |
| psaAddEventListener(window, 'onload', window.onload); |
| window.onload = null; |
| } |
| this.fireOnload(); |
| } |
| } else { |
| this.highPriorityFinalize(); |
| } |
| } else { |
| // The following state change is only for debugging. |
| this.state_ = deferJsNs.DeferJs.STATES.WAITING_FOR_NEXT_RUN; |
| this.firstIncrementalRun_ = false; |
| if (this.incrementalScriptsDoneCallback_ && this.isLowPriorityDeferJs()) { |
| pagespeed.deferJs = pagespeed.highPriorityDeferJs; |
| pagespeed['deferJs'] = pagespeed.highPriorityDeferJs; |
| this.exec(this.incrementalScriptsDoneCallback_); |
| this.incrementalScriptsDoneCallback_ = null; |
| } else { |
| this.highPriorityFinalize(); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Fires 'onload' event. |
| */ |
| deferJsNs.DeferJs.prototype.fireOnload = function() { |
| // Add all the elements whose onloads are deferred. |
| // Note, by the time we come here, all the images, iframes etc all should have |
| // been loaded. Because main page onload gets triggered when all it's |
| // resources are loaded.So we can blindly trigger all those onloads. |
| if (this.isLowPriorityDeferJs()) { |
| this.addDeferredOnloadListeners(); |
| pagespeed.highPriorityDeferJs.fireOnload(); |
| } |
| |
| this.fireEvent(deferJsNs.DeferJs.EVENT.LOAD); |
| |
| if (this.isLowPriorityDeferJs()) { |
| // Clean up psanode elements from the DOM. |
| var psanodes = document.body.getElementsByTagName('psanode'); |
| for (var i = (psanodes.length - 1); i >= 0; i--) { |
| document.body.removeChild(psanodes[i]); |
| } |
| |
| // Clean up all prefetch containers we added. |
| var prefetchContainers = |
| document.body.getElementsByClassName('psa_prefetch_container'); |
| for (var i = (prefetchContainers.length - 1); i >= 0; i--) { |
| prefetchContainers[i].parentNode.removeChild(prefetchContainers[i]); |
| } |
| } |
| |
| this.state_ = deferJsNs.DeferJs.STATES.SCRIPTS_DONE; |
| this.fireEvent(deferJsNs.DeferJs.EVENT.AFTER_SCRIPTS); |
| }; |
| |
| |
| /** |
| * Finalize function for high priority scripts execution that start the |
| * execution of low priority scripts. |
| */ |
| deferJsNs.DeferJs.prototype.highPriorityFinalize = function() { |
| var me = this; |
| window.setTimeout(function() { |
| pagespeed.deferJs = pagespeed.lowPriorityDeferJs; |
| pagespeed['deferJs'] = pagespeed.lowPriorityDeferJs; |
| if (me.incrementalScriptsDoneCallback_) { |
| pagespeed.deferJs.registerScriptTags(me.incrementalScriptsDoneCallback_, |
| me.optLastIndex_); |
| me.incrementalScriptsDoneCallback_ = null; |
| } else { |
| pagespeed.deferJs.registerScriptTags(); |
| } |
| pagespeed.deferJs.execute(); |
| }, 0); |
| }; |
| |
| |
| /** |
| * Checks if node is present in the dom. |
| * @param {Node} node Node whose presence in the dom is checked. |
| * @return {boolean} returns true if node is present in the Node, false |
| * otherwise. |
| */ |
| deferJsNs.DeferJs.prototype.checkNodeInDom = function(node) { |
| while (node = node.parentNode) { |
| if (node == document) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Script onload is not triggered if src is empty because such scripts |
| * are not async scripts as these scripts will be executed while parsing |
| * the dom. |
| * @param {Array<Element>} dynamicInsertedScriptList is the list of dynamic |
| * inserted scripts. |
| * @return {number} returns the number of scripts whose onload will not get |
| * triggered. |
| */ |
| deferJsNs.DeferJs.prototype.getNumScriptsWithNoOnload = |
| function(dynamicInsertedScriptList) { |
| var count = 0; |
| var len = dynamicInsertedScriptList.length; |
| for (var i = 0; i < len; ++i) { |
| var node = dynamicInsertedScriptList[i]; |
| var parent = node.parentNode; |
| var src = node.src; |
| var text = node.textContent; |
| // IE behaves differently for async scripts compared to other browsers. |
| // IE triggeres script onload only if parent is not null and src or |
| // textContent is not empty. But other browsers trigger onload only if |
| // node is present in the dom and src is not empty. |
| if (((this.getIEVersion() > 8) && |
| (!parent || (src == '' && text == ''))) || |
| (!this.getIEVersion() && |
| (!this.checkNodeInDom(node) || src == ''))) { |
| count++; |
| } |
| } |
| return count; |
| }; |
| |
| |
| /** |
| * Checks if onComplete() function can be called or not. |
| * @return {boolean} returns true if onComplete() can be called. |
| */ |
| deferJsNs.DeferJs.prototype.canCallOnComplete = function() { |
| // TODO(pulkitg): Handle scenario where somebody sets innetHTML and references |
| // in the this.dynamicInsertedScriptCount_ become invalid. |
| if (this.state_ != deferJsNs.DeferJs.STATES.SYNC_SCRIPTS_DONE) { |
| return false; |
| } |
| var count = 0; |
| if (this.dynamicInsertedScriptCount_ != 0) { |
| count = this.getNumScriptsWithNoOnload(this.dynamicInsertedScript_); |
| } |
| if (this.dynamicInsertedScriptCount_ == count) { |
| return true; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Whether all the deferred scripts are done executing. |
| * @return {boolean} true if all deferred scripts are done executing, false |
| * otherwise. |
| */ |
| deferJsNs.DeferJs.prototype.scriptsAreDone = function() { |
| return this.state_ === deferJsNs.DeferJs.STATES.SCRIPTS_DONE; |
| }; |
| deferJsNs.DeferJs.prototype['scriptsAreDone'] = |
| deferJsNs.DeferJs.prototype.scriptsAreDone; |
| |
| |
| /** |
| * Schedules the next task in the queue. |
| */ |
| deferJsNs.DeferJs.prototype.runNext = function() { |
| this.handlePendingDocumentWrites(); |
| this.removeCurrentDomLocation(); |
| if (this.next_ < this.queue_.length) { |
| // Done here to prevent another _run_next() in stack from |
| // seeing the same value of next, and get into infinite |
| // loop. |
| this.next_++; |
| this.queue_[this.next_ - 1].call(window); |
| } else { |
| if (this.lastIncrementalRun_) { |
| this.state_ = deferJsNs.DeferJs.STATES.SYNC_SCRIPTS_DONE; |
| this.removeNotProcessedAttributeTillNode(); |
| this.fireEvent(deferJsNs.DeferJs.EVENT.DOM_READY); |
| if (this.canCallOnComplete()) { |
| this.onComplete(); |
| } |
| } else { |
| this.onComplete(); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Converts from NodeList to array of nodes. |
| * @param {!NodeList} nodeList NodeList from a DOM node. |
| * @return {!Array<Node>} Array of nodes returned. |
| */ |
| deferJsNs.DeferJs.prototype.nodeListToArray = function(nodeList) { |
| var arr = []; |
| var len = nodeList.length; |
| for (var i = 0; i < len; ++i) { |
| arr.push(nodeList.item(i)); |
| } |
| return arr; |
| }; |
| |
| |
| /** |
| * SetUp needed before deferrred scripts execution. |
| */ |
| deferJsNs.DeferJs.prototype.setUp = function() { |
| var me = this; |
| if (this.firstIncrementalRun_ && !this.isLowPriorityDeferJs()) { |
| // TODO(ksimbili): Remove this once context is not optional. |
| // Place where document.write() happens if there is no context element |
| // present. Happens if there is no context registering that happened in |
| // registerNoScriptTags. |
| var initialContextNode = document.createElement('psanode'); |
| initialContextNode.setAttribute('psa_dw_target', 'true'); |
| document.body.appendChild(initialContextNode); |
| if (this.getIEVersion()) { |
| this.createIdVars(); |
| } |
| |
| if (Object.defineProperty) { |
| try { |
| // Shadow document.readyState |
| var propertyDescriptor = { configurable: true }; |
| propertyDescriptor.get = function() { |
| return (me.state_ >= deferJsNs.DeferJs.STATES.SYNC_SCRIPTS_DONE) ? |
| 'interactive' : 'loading'; |
| }; |
| Object.defineProperty(document, 'readyState', propertyDescriptor); |
| } catch (err) { |
| this.log('Exception while overriding document.readyState.', err); |
| } |
| } |
| if (this.getIEVersion()) { |
| // In IE another approach for identifying DOMContentLoaded is popularly |
| // used. It is described in http://javascript.nwbox.com/IEContentLoaded/ . |
| // And JQuery is one of the libraries which employs this strategy. |
| document.documentElement['originalDoScroll'] = |
| document.documentElement.doScroll; |
| document.documentElement.doScroll = function() { |
| throw ('psa exception'); |
| }; |
| if (Object.defineProperty) { |
| try { |
| // Shadow document.all |
| var propertyDescriptor = { configurable: true }; |
| propertyDescriptor.get = function() { return undefined; }; |
| Object.defineProperty(document, 'all', propertyDescriptor); |
| } catch (err) { |
| this.log('Exception while overriding document.all.', err); |
| } |
| } |
| } |
| } |
| |
| // override AddEventListeners. |
| this.overrideAddEventListeners(); |
| |
| // TODO(ksimbili): Restore the following functions to their original. |
| document.writeln = function(x) { |
| me.writeHtml(x + '\n'); |
| }; |
| document.write = function(x) { |
| me.writeHtml(x); |
| }; |
| document.open = function() { |
| if (!me.overrideDefaultImplementation_) { |
| me.origDocOpen_.call(document); |
| } |
| }; |
| document.close = function() { |
| if (!me.overrideDefaultImplementation_) { |
| me.origDocClose_.call(document); |
| } |
| }; |
| |
| document.getElementById = function(str) { |
| me.handlePendingDocumentWrites(); |
| var node = me.origGetElementById_.call(document, str); |
| return (node == null || |
| node.hasAttribute(me.psaNotProcessed_)) ? null : node; |
| }; |
| |
| if (document.querySelectorAll && !(me.getIEVersion() <= 8)) { |
| // TODO(ksimbili): Support IE8 |
| // TODO(jmaessen): More to the point, this only sort of works even on modern |
| // browsers; origGetElementsByTagName returns a live list that changes to |
| // reflect DOM changes and querySelectorAll does not (and is known to be |
| // massively slower on many browsers as a result). We might be able to get |
| // around this using delegates a la document.readyState, but it'll be hard. |
| document.getElementsByTagName = function(tagName) { |
| if (me.overrideDefaultImplementation_) { |
| try { |
| return document.querySelectorAll( |
| tagName + ':not([' + me.psaNotProcessed_ + '])'); |
| } catch (err) { |
| // Fall through and emulate original behavior |
| } |
| } |
| return me.origGetElementsByTagName_.call(document, tagName); |
| }; |
| } |
| |
| // Overriding createElement(). |
| // Attaching onload & onerror function if script node is created. |
| document.createElement = function(str) { |
| var elem = me.origCreateElement_.call(document, str); |
| if (me.overrideDefaultImplementation_ && |
| str.toLowerCase() == 'script') { |
| me.dynamicInsertedScript_.push(elem); |
| me.dynamicInsertedScriptCount_++; |
| var onload = function() { |
| me.dynamicInsertedScriptCount_--; |
| var index = me.dynamicInsertedScript_.indexOf(this); |
| if (index != -1) { |
| me.dynamicInsertedScript_.splice(index, 1); |
| if (me.canCallOnComplete()) { |
| me.onComplete(); |
| } |
| } |
| }; |
| deferJsNs.addOnload(elem, onload); |
| pagespeedutils.addHandler(elem, 'error', onload); |
| } |
| return elem; |
| }; |
| }; |
| |
| |
| /** |
| * Start the execution of the deferred script only if there is no async script |
| * pending that was created by non deferred. |
| */ |
| deferJsNs.DeferJs.prototype.execute = function() { |
| if (this.state_ != deferJsNs.DeferJs.STATES.SCRIPTS_REGISTERED) { |
| return; |
| } |
| var count = 0; |
| if (this.noDeferAsyncScriptsCount_ != 0) { |
| count = this.getNumScriptsWithNoOnload(this.noDeferAsyncScripts_); |
| } |
| if (this.noDeferAsyncScriptsCount_ == count) { |
| this.run(); |
| } |
| }; |
| deferJsNs.DeferJs.prototype['execute'] = deferJsNs.DeferJs.prototype.execute; |
| |
| |
| /** |
| * Starts the execution of all the deferred scripts. |
| */ |
| deferJsNs.DeferJs.prototype.run = function() { |
| if (this.state_ != deferJsNs.DeferJs.STATES.SCRIPTS_REGISTERED) { |
| return; |
| } |
| if (this.firstIncrementalRun_) { |
| this.fireEvent(deferJsNs.DeferJs.EVENT.BEFORE_SCRIPTS); |
| } |
| this.state_ = deferJsNs.DeferJs.STATES.SCRIPTS_EXECUTING; |
| this.setUp(); |
| // Starts executing the defer_js closures. |
| this.runNext(); |
| }; |
| deferJsNs.DeferJs.prototype['run'] = deferJsNs.DeferJs.prototype.run; |
| |
| |
| /** |
| * Parses the given html snippet. |
| * @param {!string} html to be parsed. |
| * @return {!Node} returns a DIV containing parsed nodes as children. |
| */ |
| deferJsNs.DeferJs.prototype.parseHtml = function(html) { |
| var div = this.origCreateElement_.call(document, 'div'); |
| // IE HACK -- Two options. |
| // 1) Either add a dummy character at the start and delete it after parsing. |
| // 2) Add some non-empty node infront of html. |
| div.innerHTML = '<div>_</div>' + html; |
| div.removeChild(div.firstChild); |
| return div; |
| }; |
| |
| |
| /** |
| * Removes the node from its parent if it has one. |
| * @param {Node} node Node to be disowned from parent. |
| */ |
| deferJsNs.DeferJs.prototype.disown = function(node) { |
| var parentNode = node.parentNode; |
| if (parentNode) { |
| parentNode.removeChild(node); |
| } |
| }; |
| |
| |
| /** |
| * Inserts all the nodes before elem. It must have a parentNode for this |
| * operation to succeed. (either actually inserted in DOM) |
| * @param {!NodeList} nodes to insert. |
| * @param {!Node} elem context element. |
| */ |
| deferJsNs.DeferJs.prototype.insertNodesBeforeElem = function(nodes, elem) { |
| var nodeArray = this.nodeListToArray(nodes); |
| var len = nodeArray.length; |
| var parentNode = elem.parentNode; |
| for (var i = 0; i < len; ++i) { |
| var node = nodeArray[i]; |
| this.disown(node); |
| parentNode.insertBefore(node, elem); |
| } |
| }; |
| |
| |
| /** |
| * Returns if the node is JavaScript Node. |
| * @param {!Node} node valid script Node. |
| * @return {boolean} true if script node is javascript node. |
| */ |
| deferJsNs.DeferJs.prototype.isJSNode = function(node) { |
| if (node.nodeName != 'SCRIPT') { |
| return false; |
| } |
| |
| if (node.hasAttribute('type')) { |
| var type = node.getAttribute('type'); |
| return !type || (this.jsMimeTypes.indexOf(type) != -1); |
| } else if (node.hasAttribute('language')) { |
| var lang = node.getAttribute('language'); |
| return !lang || |
| (this.jsMimeTypes.indexOf('text/' + lang.toLowerCase()) != -1); |
| } |
| return true; |
| }; |
| |
| |
| /** |
| * Given the list of nodes, sets the not_processed attributes to all nodes and |
| * generates list of script nodes. |
| * @param {!Node} node starting node for DFS. |
| * @param {!Array<!Element>} scriptNodes array of script elements (output). |
| */ |
| deferJsNs.DeferJs.prototype.markNodesAndExtractScriptNodes = function( |
| node, scriptNodes) { |
| if (!node.childNodes) { |
| return; |
| } |
| var nodeArray = this.nodeListToArray(node.childNodes); |
| var len = nodeArray.length; |
| for (var i = 0; i < len; ++i) { |
| var child = nodeArray[i]; |
| // <script id='A'> |
| // command1 |
| // document.write('<script id='B'>command2<script>'); |
| // command3 |
| // <\/script> |
| // Browser behaviour for above script node is as follows- first execute |
| // command1 and after the execution of command1, scriptB starts executing |
| // and only after the complete execution of script B, command3 will execute. |
| // But with deferJs turned on, after the execution of command 1, command3 |
| // gets executed and only after the complete execution of script A, script B |
| // will execute. |
| // TODO(pulkitg): Make both behaviour consistent. |
| if (child.nodeName == 'SCRIPT') { |
| if (this.isJSNode(child)) { |
| // TODO(jud): Rewrite this function to just iterate over children, |
| // instead of childNodes, and then we only have to operate over elements |
| // instead of nodes. |
| scriptNodes.push(/** @type {!Element} */ (child)); |
| child.setAttribute(deferJsNs.DeferJs.PSA_ORIG_TYPE, child.type); |
| child.setAttribute('type', this.psaScriptType_); |
| child.setAttribute(deferJsNs.DeferJs.PSA_ORIG_SRC, child.src); |
| child.setAttribute('src', ''); |
| child.setAttribute(this.psaNotProcessed_, ''); |
| } |
| } else { |
| this.markNodesAndExtractScriptNodes(child, scriptNodes); |
| } |
| } |
| }; |
| |
| |
| /** |
| * @param {!Array<Element>} scripts Array of script nodes to be deferred. |
| * @param {!number} pos position for script ordering. |
| */ |
| deferJsNs.DeferJs.prototype.deferScripts = function(scripts, pos) { |
| var len = scripts.length; |
| for (var i = 0; i < len; ++i) { |
| this.addNode(scripts[i], pos + i, !!i); |
| } |
| }; |
| |
| |
| /** |
| * Inserts html in the before elem, with scripts inside added to queue at pos. |
| * @param {!string} html contains the snippet. |
| * @param {!number} pos optional position to add to queue. |
| * @param {Element=} opt_elem optional context element. |
| */ |
| deferJsNs.DeferJs.prototype.insertHtml = function(html, pos, opt_elem) { |
| // Parse the html. |
| var node = this.parseHtml(html); |
| |
| // Extract script nodes out for deferring them. |
| var scriptNodes = []; |
| this.markNodesAndExtractScriptNodes(node, scriptNodes); |
| |
| // Add non-script nodes before elem |
| if (opt_elem) { |
| this.insertNodesBeforeElem(node.childNodes, opt_elem); |
| } else { |
| this.log('Unable to insert nodes, no context element found'); |
| } |
| |
| // Add script nodes for deferring. |
| this.deferScripts(scriptNodes, pos); |
| }; |
| |
| |
| /** |
| * Renders the document.write() buffer before the context |
| * element. |
| */ |
| deferJsNs.DeferJs.prototype.handlePendingDocumentWrites = function() { |
| if (this.documentWriteHtml_ == '') { |
| return; |
| } |
| this.log('handle_dw: ' + this.documentWriteHtml_); |
| |
| var html = this.documentWriteHtml_; |
| // Reset early because insertHtml may internally end up calling this function |
| // recursively. |
| this.documentWriteHtml_ = ''; |
| |
| var currentElem = this.getCurrentDomLocation(); |
| this.insertHtml(html, this.next_, currentElem); |
| }; |
| |
| |
| /** |
| * Writes html like document.write to the current context item. |
| * @param {string} html Html to be written before current context elem. |
| */ |
| deferJsNs.DeferJs.prototype.writeHtml = function(html) { |
| if (this.overrideDefaultImplementation_) { |
| this.log('dw: ' + html); |
| this.documentWriteHtml_ += html; |
| } else { |
| this.origDocWrite_.call(document, html); |
| } |
| }; |
| |
| |
| /** |
| * Adds page onload event listeners to our own list and called them later. |
| */ |
| deferJsNs.DeferJs.prototype.addDeferredOnloadListeners = function() { |
| var onloadDeferredElements; |
| if (document.querySelectorAll) { |
| onloadDeferredElements = document.querySelectorAll( |
| '[' + deferJsNs.DeferJs.PAGESPEED_ONLOAD + '][data-pagespeed-loaded]'); |
| } |
| for (var i = 0; i < onloadDeferredElements.length; i++) { |
| var elem = onloadDeferredElements.item(i); |
| var handlerStr = elem.getAttribute(deferJsNs.DeferJs.PAGESPEED_ONLOAD); |
| var functionStr = 'var psaFunc=function() {' + handlerStr + '};'; |
| // Define a function with the string above. |
| window['eval'].call(window, functionStr); |
| if (typeof window['psaFunc'] != 'function') { |
| this.log('Function is not defined', new Error('')); |
| continue; |
| } |
| |
| psaAddEventListener(elem, 'onload', window['psaFunc']); |
| } |
| }; |
| |
| |
| /** |
| * Adds functions that run as the first thing in run(). |
| * @param {!function()} func onload listener. |
| */ |
| deferJsNs.DeferJs.prototype.addBeforeDeferRunFunctions = function(func) { |
| psaAddEventListener(window, 'onbeforescripts', func); |
| }; |
| deferJsNs.DeferJs.prototype['addBeforeDeferRunFunctions'] = |
| deferJsNs.DeferJs.prototype.addBeforeDeferRunFunctions; |
| |
| |
| /** |
| * Adds functions that run after all the deferred scripts, DOM ready listeners |
| * and onload listeners have run. |
| * @param {!function()} func onload listener. |
| */ |
| deferJsNs.DeferJs.prototype.addAfterDeferRunFunctions = function(func) { |
| psaAddEventListener(window, 'onafterscripts', func); |
| }; |
| deferJsNs.DeferJs.prototype['addAfterDeferRunFunctions'] = |
| deferJsNs.DeferJs.prototype.addAfterDeferRunFunctions; |
| |
| |
| /** |
| * Firing event will execute all listeners registered for the event. |
| * @param {!deferJsNs.DeferJs.EVENT.<number>} evt Event to be fired. |
| */ |
| deferJsNs.DeferJs.prototype.fireEvent = function(evt) { |
| this.eventState_ = evt; |
| this.log('Firing Event: ' + evt); |
| var eventListeners = this.eventListenersMap_[evt] || []; |
| for (var i = 0; i < eventListeners.length; ++i) { |
| this.exec(eventListeners[i]); |
| } |
| eventListeners.length = 0; |
| }; |
| |
| |
| /** |
| * Execute function under try catch. |
| * @param {!function()} func Function to be executed. |
| * @param {Window|Element|Document=} opt_scopeObject Element to be used as |
| * scope. |
| */ |
| deferJsNs.DeferJs.prototype.exec = function(func, opt_scopeObject) { |
| try { |
| func.call(opt_scopeObject || window); |
| } catch (err) { |
| this.log('Exception while evaluating.', err); |
| } |
| }; |
| |
| |
| /** |
| * Override native event registration function on window and document objects. |
| */ |
| deferJsNs.DeferJs.prototype.overrideAddEventListeners = function() { |
| var me = this; |
| // override AddEventListeners. |
| if (window.addEventListener) { |
| document.addEventListener = function(eventName, func, capture) { |
| psaAddEventListener(document, eventName, func, |
| me.origDocAddEventListener_, capture); |
| }; |
| window.addEventListener = function(eventName, func, capture) { |
| psaAddEventListener(window, eventName, func, |
| me.origWindowAddEventListener_, capture); |
| }; |
| } else if (window.attachEvent) { |
| document.attachEvent = function(eventName, func) { |
| psaAddEventListener(document, eventName, func, |
| me.origDocAttachEvent_); |
| }; |
| window.attachEvent = function(eventName, func) { |
| psaAddEventListener(window, eventName, func, |
| me.origWindowAttachEvent_); |
| }; |
| } |
| }; |
| |
| |
| /** |
| * Registers an event with the element. |
| * @param {!(Window|Node|Document)} elem Element which is registering for the |
| * event. |
| * @param {!string} eventName Name of the event. |
| * @param {(Function|EventListener|function())} func Event handler. |
| * @param {Function=} opt_originalAddEventListener Original Add event Listener |
| * function. |
| * @param {boolean=} opt_capture Capture event. |
| */ |
| var psaAddEventListener = function(elem, eventName, func, |
| opt_originalAddEventListener, opt_capture) { |
| var deferJs = pagespeed['deferJs']; |
| if (deferJs.state_ >= deferJsNs.DeferJs.STATES.WAITING_FOR_ONLOAD) { |
| // At this point we ought to revert to the original event listener |
| // behavior. |
| if (opt_originalAddEventListener) { |
| opt_originalAddEventListener.call(elem, eventName, func, opt_capture); |
| return; |
| } |
| // Unless there wasn't an event listener provided, in which case we are |
| // calling psaAddEventListener internally and should check whether we have |
| // work to do and fall through if so. (Note that if we return |
| // unconditionally here we miss event registrations and break pages.) |
| if (deferJs.state_ >= deferJsNs.DeferJs.STATES.SCRIPTS_DONE) { |
| return; |
| } |
| } |
| var deferJsEvent; |
| var deferJsEventName; |
| |
| if (deferJs.eventState_ < deferJsNs.DeferJs.EVENT.DOM_READY && |
| (eventName == 'DOMContentLoaded' || eventName == 'readystatechange' || |
| eventName == 'onDOMContentLoaded' || |
| eventName == 'onreadystatechange')) { |
| deferJsEvent = deferJsNs.DeferJs.EVENT.DOM_READY; |
| deferJsEventName = 'DOMContentLoaded'; |
| } else if (deferJs.eventState_ < deferJsNs.DeferJs.EVENT.LOAD && |
| (eventName == 'load' || eventName == 'onload')) { |
| deferJsEvent = deferJsNs.DeferJs.EVENT.LOAD; |
| deferJsEventName = 'load'; |
| } else if (eventName == 'onbeforescripts') { |
| deferJsEvent = deferJsNs.DeferJs.EVENT.BEFORE_SCRIPTS; |
| } else if (eventName == 'onafterscripts') { |
| deferJsEvent = deferJsNs.DeferJs.EVENT.AFTER_SCRIPTS; |
| } else { |
| if (opt_originalAddEventListener) { |
| opt_originalAddEventListener.call(elem, eventName, func, opt_capture); |
| } |
| return; |
| } |
| var eventListenerClosure = function() { |
| // HACK HACK: This is specifically to solve for jquery libraries, who try |
| // to read the event being passed. |
| // Note we are not setting any of the other params in event. We don't see |
| // them as a need for now. |
| // This is set based on documentation from |
| // https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/DOMContentLoaded#Cross-browser_fallback |
| var customEvent = {}; |
| customEvent['bubbles'] = false; |
| customEvent['cancelable'] = false; |
| customEvent['eventPhase'] = 2; // Event.AT_TARGET |
| customEvent['timeStamp'] = new Date().getTime(); |
| customEvent['type'] = deferJsEventName; |
| // event.target has to be some element in DOM. It can never be window. |
| customEvent['target'] = (elem != window) ? elem : document; |
| // This can be safely 'elem' because the two events "DOMContentLoaded' and |
| // 'load' cannot bubble. |
| customEvent['currentTarget'] = elem; |
| func.call(elem, customEvent); |
| }; |
| if (!deferJs.eventListenersMap_[deferJsEvent]) { |
| deferJs.eventListenersMap_[deferJsEvent] = []; |
| } |
| deferJs.eventListenersMap_[deferJsEvent].push( |
| eventListenerClosure); |
| }; |
| |
| |
| /** |
| * Registers all script tags which are marked text/psajs, by adding themselves |
| * as the context element to the script embedded inside them. |
| * @param {function()=} opt_callback Called when critical scripts are |
| * done executing. |
| * @param {number=} opt_last_index till where its safe to run scripts. |
| */ |
| deferJsNs.DeferJs.prototype.registerScriptTags = |
| function(opt_callback, opt_last_index) { |
| if (this.state_ >= deferJsNs.DeferJs.STATES.SCRIPTS_REGISTERED) { |
| return; |
| } |
| if (opt_callback) { |
| if (!deferJsNs.DeferJs.isExperimentalMode) { |
| opt_callback(); |
| return; |
| } |
| this.lastIncrementalRun_ = false; |
| this.incrementalScriptsDoneCallback_ = opt_callback; |
| if (opt_last_index) { |
| this.optLastIndex_ = opt_last_index; |
| } |
| } else { |
| this.lastIncrementalRun_ = true; |
| } |
| |
| this.state_ = deferJsNs.DeferJs.STATES.SCRIPTS_REGISTERED; |
| var scripts = document.getElementsByTagName('script'); |
| var len = scripts.length; |
| for (var i = 0; i < len; ++i) { |
| var isFirstScript = (this.queue_.length == this.next_); |
| var script = scripts[i]; |
| // TODO(ksimbili): Use orig_type |
| if (script.getAttribute('type') == this.psaScriptType_) { |
| if (opt_callback) { |
| var scriptIndex = |
| script.getAttribute(deferJsNs.DeferJs.PSA_ORIG_INDEX); |
| if (scriptIndex <= this.optLastIndex_) { |
| this.addNode(script, undefined, !isFirstScript); |
| } |
| } else { |
| if (script.getAttribute(deferJsNs.DeferJs.PSA_ORIG_INDEX) < |
| this.optLastIndex_) { |
| this.log('Executing a script twice. Orig_Index: ' + |
| script.getAttribute(deferJsNs.DeferJs.PSA_ORIG_INDEX), |
| new Error('')); |
| } |
| this.addNode(script, undefined, !isFirstScript); |
| } |
| } |
| } |
| }; |
| deferJsNs.DeferJs.prototype['registerScriptTags'] = |
| deferJsNs.DeferJs.prototype.registerScriptTags; |
| |
| |
| /** |
| * Runs the function when element is loaded. |
| * @param {Window|Element} elem Element to attach handler. |
| * @param {!function()} func New onload handler. |
| */ |
| deferJsNs.addOnload = function(elem, func) { |
| pagespeedutils.addHandler(elem, 'load', func); |
| }; |
| pagespeed['addOnload'] = deferJsNs.addOnload; |
| |
| |
| /** |
| * @return {boolean} true if browser is Firefox. |
| */ |
| deferJsNs.DeferJs.prototype.isFireFox = function() { |
| return (navigator.userAgent.indexOf('Firefox') != -1); |
| }; |
| |
| |
| /** |
| * @return {boolean} true if browser is WebKit based. |
| */ |
| deferJsNs.DeferJs.prototype.isWebKit = function() { |
| return (navigator.userAgent.indexOf('AppleWebKit') != -1); |
| }; |
| |
| |
| /** |
| * @return {number} version number if browser is IE. |
| */ |
| deferJsNs.DeferJs.prototype.getIEVersion = function() { |
| var version = /(?:MSIE.(\d+\.\d+))/.exec(navigator.userAgent); |
| return (version && version[1]) ? |
| (document.documentMode || parseFloat(version[1])) : |
| NaN; |
| }; |
| |
| |
| /** |
| * Set the type of the scripts which will be executed. |
| * @param {string} type of psa dummy nodes that need to be processed. |
| */ |
| deferJsNs.DeferJs.prototype.setType = function(type) { |
| this.psaScriptType_ = type; |
| }; |
| |
| |
| /** |
| * Set the psaNotProcessed marker. |
| * @param {string} psaNotProcessed marker. |
| */ |
| deferJsNs.DeferJs.prototype.setPsaNotProcessed = function(psaNotProcessed) { |
| this.psaNotProcessed_ = psaNotProcessed; |
| }; |
| |
| |
| /** |
| * Whether defer javascript is executing low priority scripts. |
| * @return {boolean} returns true if defer javascript is executing low priority |
| * scripts. |
| */ |
| deferJsNs.DeferJs.prototype.isLowPriorityDeferJs = function() { |
| if (this.psaScriptType_ == deferJsNs.DeferJs.PSA_SCRIPT_TYPE) { |
| return true; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Overrides createElement for the non-deferred scripts. Any async script |
| * created by non-deferred script should be executed before deferred scripts |
| * gets executed. |
| */ |
| deferJsNs.DeferJs.prototype.noDeferCreateElementOverride = function() { |
| // Attaching onload & onerror function if script node is created. |
| var me = this; |
| document.createElement = function(str) { |
| var elem = me.origCreateElement_.call(document, str); |
| if (me.overrideDefaultImplementation_ && |
| str.toLowerCase() == 'script') { |
| me.noDeferAsyncScripts_.push(elem); |
| me.noDeferAsyncScriptsCount_++; |
| var onload = function() { |
| var index = me.noDeferAsyncScripts_.indexOf(this); |
| if (index != -1) { |
| me.noDeferAsyncScripts_.splice(index, 1); |
| me.noDeferAsyncScriptsCount_--; |
| me.execute(); |
| } |
| }; |
| deferJsNs.addOnload(elem, onload); |
| pagespeedutils.addHandler(elem, 'error', onload); |
| } |
| return elem; |
| }; |
| }; |
| |
| |
| /** |
| * @return {boolean} true if deferJs is running with experimental flag. |
| */ |
| deferJsNs.DeferJs.prototype.isExperimentalMode = function() { |
| return deferJsNs.DeferJs.isExperimentalMode; |
| }; |
| deferJsNs.DeferJs.prototype['isExperimentalMode'] = |
| deferJsNs.DeferJs.prototype.isExperimentalMode; |
| |
| |
| /** |
| * Initialize defer javascript. |
| */ |
| deferJsNs.deferInit = function() { |
| if (pagespeed['deferJs']) { |
| return; |
| } |
| |
| deferJsNs.DeferJs.isExperimentalMode = pagespeed['defer_js_experimental']; |
| pagespeed.highPriorityDeferJs = new deferJsNs.DeferJs(); |
| pagespeed.highPriorityDeferJs.setType( |
| deferJsNs.DeferJs.PRIORITY_PSA_SCRIPT_TYPE); |
| pagespeed.highPriorityDeferJs.setPsaNotProcessed( |
| deferJsNs.DeferJs.PRIORITY_PSA_NOT_PROCESSED); |
| pagespeed.highPriorityDeferJs.setNotProcessedAttributeForNodes(); |
| pagespeed.lowPriorityDeferJs = new deferJsNs.DeferJs(); |
| pagespeed.lowPriorityDeferJs.setType(deferJsNs.DeferJs.PSA_SCRIPT_TYPE); |
| pagespeed.lowPriorityDeferJs.setPsaNotProcessed( |
| deferJsNs.DeferJs.PSA_NOT_PROCESSED); |
| pagespeed.lowPriorityDeferJs.setNotProcessedAttributeForNodes(); |
| pagespeed.deferJs = pagespeed.highPriorityDeferJs; |
| |
| pagespeed.deferJs.noDeferCreateElementOverride(); |
| pagespeed['deferJs'] = pagespeed.deferJs; |
| }; |
| |
| deferJsNs.deferInit(); |
| |
| |
| /** |
| * Indicates if deferJs started executing. |
| * @type {boolean} |
| */ |
| pagespeed.deferJsStarted = false; |
| |
| |
| /** |
| * Starts deferJs execution. |
| */ |
| deferJsNs.startDeferJs = function() { |
| if (pagespeed.deferJsStarted) { |
| return; |
| } |
| pagespeed.deferJsStarted = true; |
| pagespeed.deferJs.registerScriptTags(); |
| pagespeed.deferJs.execute(); |
| }; |
| deferJsNs['startDeferJs'] = deferJsNs.startDeferJs; |
| pagespeedutils.addHandler(document, 'DOMContentLoaded', deferJsNs.startDeferJs); |
| deferJsNs.addOnload(window, deferJsNs.startDeferJs); |