blob: 8c9d54f85f39b5fcd0ba31b01ba9c45e3f410995 [file] [log] [blame]
// 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);
};