| // Copyright 2011 Google Inc. All Rights Reserved. |
| |
| /** |
| * @fileoverview Implements Layout and Panels for Blink client |
| * Binds JSON data to layout panels in javascript. |
| * An HTML element may have a panelId specified. |
| * This id identifies a JSON paneldata element to bind to which may be an array |
| * or a dictionary. |
| * @author ssundaram@google.com (Sridhar Sundaram) |
| */ |
| |
| var PANEL_ID = 'panel-id'; |
| var PANEL_STUBSTART = 'GooglePanel begin '; |
| var PANEL_STUBEND = 'GooglePanel end '; |
| var INSTANCE_HTML = 'instance_html'; |
| var CONTIGUOUS = 'contiguous'; |
| var XPATH = 'xpath'; |
| var DONT_BIND = 'dont_bind'; |
| var IMAGES = 'images'; |
| var BLINK_SRC = 'data-pagespeed-high-res-src'; |
| var PANEL_MARKER = 'psa_disabled'; |
| |
| // TODO(ksimbili): Convert this to DCHECK using some flag |
| function CHECK(condition) { |
| if (!condition) throw ('CHECK failed'); |
| } |
| |
| var getDocument = function() { |
| return document; |
| }; |
| |
| function isInternetExplorer() { |
| return navigator.appName == 'Microsoft Internet Explorer'; |
| } |
| |
| |
| /** |
| * Create an HTML dom element with tagName and innerHTML as specified and |
| * return the nodes corresponding to innerHTML in a document fragment |
| * @param {string} tagName |
| * @param {string} innerHTML |
| * Precondition: innerHTML is consistent with tagName. |
| * @return {Element} |
| */ |
| function createInnerHtmlElements(tagName, innerHTML) { |
| if (tagName == 'HEAD') tagName = 'div'; // innerHTML not allowed in HEAD in IE |
| var element; |
| if (isInternetExplorer() && (tagName == 'TABLE' || tagName == 'TBODY')) { |
| element = getDocument().createElement('div'); |
| element.innerHTML = '<table>' + innerHTML + '</table>'; |
| element = element.getElementsByTagName('tbody')[0]; |
| } else { |
| element = getDocument().createElement(tagName); |
| element.innerHTML = innerHTML; |
| } |
| // Transfer all instances over to a document fragment |
| var docFragment = getDocument().createDocumentFragment(); |
| while (element.childNodes.length > 0) { |
| if (element.childNodes[0].tagName) { |
| // psa_not_processed attributes are required for deferJs. Inserting these |
| // attributes before adding to DOM is much cheaper (in the order of |
| // ~500ms-2s on mobile), hence adding this code here. |
| element.setAttribute('psa_not_processed', ''); |
| element.setAttribute('priority_psa_not_processed', ''); |
| } |
| docFragment.appendChild(element.childNodes[0]); |
| } |
| return docFragment; |
| } |
| |
| |
| /** |
| * Returns ordered array of nodes which match the xpath in context of node. |
| * @param {Node} node - node within whose context xpath is found. |
| * @param {string} xpath - xpath. |
| * @return {Array.<Node>} |
| */ |
| function getMatchingXPathInDom(node, xpath) { |
| var xpathResults = |
| getDocument().evaluate(xpath, node, null, |
| XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); |
| var results = []; |
| for (var result; result = xpathResults.iterateNext(); results.push(result)) {} |
| return results; |
| } |
| |
| |
| /** |
| * Find all elements in the document which have tagName as tag and |
| * have an attribute with name attributeName. |
| * @param {string} tagName |
| * @param {string} attributeName |
| * @return {Array.<Node>} matching elements in the document. |
| */ |
| function getElementsByTagAndAttribute(tagName, attributeName) { |
| var elements = getDocument().documentElement.getElementsByTagName(tagName); |
| var elementsWithAttribute = []; |
| for (var i = 0; i < elements.length; i++) { |
| var element = elements[i]; |
| if (element.hasAttribute(attributeName)) { |
| elementsWithAttribute.push(element); |
| } |
| } |
| return elementsWithAttribute; |
| } |
| |
| |
| /** |
| * Insert panel outerHTML above the panel Stub. |
| * @param {Element} panelStub dom element indicating panel location. |
| * @param {string} panelInstanceHtml outer html corresponding to panel content. |
| */ |
| function insertPanelContents(panelStub, panelInstanceHtml) { |
| var panelHtmlNodes = |
| createInnerHtmlElements(panelStub.parentNode.tagName, panelInstanceHtml); |
| panelStub.parentNode.insertBefore(panelHtmlNodes, panelStub); |
| } |
| |
| function isComment(node) { |
| return node.nodeType == 8; // node.COMMENT_NODE is not defined in IE8 |
| } |
| |
| |
| /** |
| * Finds and returns the panel stubs within the range of nodes between |
| * [beginNode,endNode] both inclusive, corresponding to panelId. |
| * panel stub is a descendant of one of the nodes from |
| * begin to end nodes |
| * @param {Node} beginNode - begin node range. |
| * @param {Node} endNode - end node range. |
| * @param {string} panelId - id of the panel whose stub is required. |
| * @return {Array.<Node>} |
| */ |
| function getPanelStubs(beginNode, endNode, panelId) { |
| CHECK(beginNode.parentNode == endNode.parentNode); |
| var panelStubs = []; |
| for (var node = beginNode; node != endNode.nextSibling; |
| node = node.nextSibling) { |
| if (isComment(node) && endsWith(node.data, PANEL_STUBEND + panelId)) { |
| panelStubs.push(node); |
| } else if (node.tagName && node.firstChild) { |
| panelStubs = panelStubs.concat( |
| getPanelStubs(node.firstChild, node.lastChild, panelId)); |
| } |
| } |
| return panelStubs; |
| } |
| |
| |
| /** |
| * Inserts stubs at the given childIndex in given parentNode. |
| * @param {Element} parentNode - Stub is inserted as child of this node. |
| * @param {number} childIndex - Index at which stub is inserted. |
| * @param {string} panelId - panelId of the stub. |
| * @return {Node} - End stub corresponding to the panelId. |
| */ |
| function insertStubAtIndex(parentNode, childIndex, panelId) { |
| CHECK(parentNode && childIndex > 0); |
| var childNode = parentNode.children[childIndex - 1] || null; |
| // Insert stubs and proceed as normal. |
| var startStub = getDocument().createComment(PANEL_STUBSTART + panelId); |
| parentNode.insertBefore(startStub, childNode); |
| var endStub = getDocument().createComment(PANEL_STUBEND + panelId); |
| parentNode.insertBefore(endStub, childNode); |
| return endStub; |
| } |
| |
| |
| /** |
| * Inserts missing stubs at the position specified by the xpath. |
| * eg., //div[5]/span[6]/div[4] |
| * @param {string} xpath - Xpath for specific location in the DOM. |
| * @param {string} panelId - panelId of the stub. |
| * @return {Node} - End stub corresponding to the panelId. |
| * @suppress {missingReturn} |
| */ |
| function insertMissingStubUsingXpath(xpath, panelId) { |
| var xpathUnits = xpath.split('/'); |
| var idRegExp = new RegExp('(?:.*\\[@id\=\")(.*)(?:\"\\])'); |
| var indexRegExp = new RegExp('(?:.*\\[)(\\d+)(?:\\])'); |
| // ignore first 2 entries. |
| var parentNode = getDocument().body; |
| for (var i = 2; i < xpathUnits.length; ++i) { |
| var matches = idRegExp.exec(xpathUnits[i]); |
| if (matches) { |
| parentNode = getDocument().getElementById(matches[1]); |
| CHECK(parentNode); |
| } else { |
| var indexMatch = indexRegExp.exec(xpathUnits[i]); |
| if (indexMatch) { |
| if (i == xpathUnits.length - 1) { |
| return insertStubAtIndex(parentNode, Number(indexMatch[1]), panelId); |
| } |
| parentNode = parentNode.children[indexMatch[1] - 1]; |
| CHECK(parentNode); |
| } else { |
| CHECK(0); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * Checks id the string ends with the given suffix. |
| * @param {string} str - string to be used for comparison. |
| * @param {string} suffix - suffix to be used for comparison. |
| * @return {boolean} true - if str ends with suffix. |
| */ |
| function endsWith(str, suffix) { |
| var re = new RegExp(suffix + '$'); |
| return re.test(str); |
| } |
| |
| |
| /** |
| * Instantiate the child Panels for panelData in descendants of range of nodes |
| * @param {Node} beginNode - begin range of nodes of panel instances |
| * for which childpanels are being instantiated. |
| * @param {Node} endNode - end range of nodes of panel instances |
| * for which childpanels are being instantiated. |
| * @param {Object.<string, string>} panelData contains |
| * self for panel's own outerHTML if any (null, if none) |
| * repeated childPanelId - panelData for child. |
| */ |
| function instantiateChildPanels(beginNode, endNode, panelData) { |
| for (var childPanelId in panelData) { |
| if (!panelData.hasOwnProperty(childPanelId) || |
| childPanelId == XPATH || childPanelId == DONT_BIND || |
| childPanelId == INSTANCE_HTML || childPanelId == IMAGES || |
| childPanelId == CONTIGUOUS) continue; |
| var childPanelData = panelData[childPanelId]; |
| var childPanelStubs = getPanelStubs(beginNode, endNode, childPanelId); |
| CHECK(!childPanelData.length || !childPanelData[0][CONTIGUOUS]); |
| for (var i = 0, k = 0; i < childPanelData.length; i++) { |
| // If contiguous, we can continue using the old stub. |
| // check only with i>0 since k starts with 0 and first instance is always |
| // false |
| if (i > 0 && !childPanelData[i][CONTIGUOUS]) k++; |
| instantiatePanel(childPanelStubs[k], childPanelData[i], childPanelId); |
| } |
| } |
| } |
| |
| |
| /** |
| * Instantiate the panel node with its corresponding dictionary of data. |
| * @param {Node} panelStub panel stub node. |
| * @param {Object.<string, string>} panelData contains |
| * self for panel's own outerHTML if any (null, if none) |
| * repeated childPanelId - panelData for child. |
| * @param {string} panelId Panel Id. |
| */ |
| function instantiatePanel(panelStub, panelData, panelId) { |
| if (!panelData || panelData[DONT_BIND]) return; |
| |
| if (!panelStub) { |
| CHECK(panelData[XPATH]); |
| panelStub = insertMissingStubUsingXpath(panelData[XPATH], panelId); |
| CHECK(panelStub); |
| } |
| // Insert instance HTML |
| if (panelData[INSTANCE_HTML]) { |
| var panelHtmlNodes = createInnerHtmlElements( |
| panelStub.parentNode.tagName, panelData[INSTANCE_HTML]); |
| var firstNode = panelHtmlNodes.firstChild; |
| var lastNode = panelHtmlNodes.lastChild; |
| panelStub.parentNode.insertBefore(panelHtmlNodes, panelStub); |
| instantiateChildPanels(firstNode, lastNode, panelData); |
| } else { // TODO(ssundaram): Navigate to correct range of nodes here. |
| instantiateChildPanels(panelStub.parentNode, panelStub.parentNode, |
| panelData); |
| } |
| } |
| |
| |
| /** |
| * Instantiates layout with pageData to create HTML page. The layout has |
| * top level panel stubs. |
| * @param {Object.<string,string>} pageJsonDict - panelData corresponding to top |
| * level panels. |
| * @return {Array.<Node>} array of image elements instantiated due to the |
| * insertion of panel HTML content. |
| */ |
| function instantiatePanelsInPage(pageJsonDict) { |
| // Instantiate top level panels and collect their images |
| var docElement = getDocument().documentElement; |
| instantiateChildPanels(docElement.firstChild, docElement.lastChild, |
| pageJsonDict); |
| var criticalImages = getElementsByTagAndAttribute('img', BLINK_SRC); |
| return criticalImages; |
| } |
| |
| |
| /** |
| * Find all pushed image elements and collect in a dictionary |
| * ASSUMPTION: Critical images are indicated by having BLINK_SRC attribute |
| * @param {Array.<Node>} criticalImages Array of critical images. |
| * @return {Object.<String,Array.<Element>>} dictionary mapping from |
| * url --> [image] for each. <image BLINK_SRC=url> |
| */ |
| function collectCriticalImages(criticalImages) { |
| var criticalImagesByUrl = {}; |
| |
| if (!criticalImages) return criticalImagesByUrl; |
| |
| for (var i = 0; i < criticalImages.length; i++) { |
| var image = criticalImages[i]; |
| var url = image.getAttribute(BLINK_SRC); |
| // The BLINK_SRC attribute is no longer required, but we will keep it. |
| if (url) { |
| if (criticalImagesByUrl[url] == undefined) { |
| criticalImagesByUrl[url] = []; |
| } |
| criticalImagesByUrl[url].push(image); |
| } |
| } |
| return criticalImagesByUrl; |
| } |
| |
| |
| |
| /** |
| * Page Manager for the layout |
| * @constructor |
| */ |
| function PageManager() { |
| } |
| |
| |
| /** |
| * Instantiates the layout with the jsonData for the page. |
| * @param {Object.<string,string>} pageJsonData - panelData corresponding to top |
| * level panels. |
| * @return {Object.<string,Array.<Element>>} dictionary mapping from |
| * url --> [image] for each. <image BLINK_SRC=url> |
| */ |
| PageManager.prototype.instantiatePage = function(pageJsonData) { |
| var criticalImages = instantiatePanelsInPage(pageJsonData); |
| return collectCriticalImages(criticalImages); |
| }; |