Files

269 lines
7.4 KiB
JavaScript

/**
* Copyright 2011 Google Inc. All Rights Reserved.
*
* 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.
*/
/**
* @author opensource@google.com
* @license Apache License, Version 2.0.
*/
'use strict';
// Extension namespace.
var xh = xh || {};
////////////////////////////////////////////////////////////
// Generic helper functions and constants
xh.SHIFT_KEYCODE = 16;
xh.X_KEYCODE = 88;
xh.elementsShareFamily = function(primaryEl, siblingEl) {
var p = primaryEl, s = siblingEl;
return (p.tagName === s.tagName &&
(!p.className || p.className === s.className) &&
(!p.id || p.id === s.id));
};
xh.getElementIndex = function(el) {
var index = 1; // XPath is one-indexed
var sib;
for (sib = el.previousSibling; sib; sib = sib.previousSibling) {
if (sib.nodeType === Node.ELEMENT_NODE && xh.elementsShareFamily(el, sib)) {
index++;
}
}
if (index > 1) {
return index;
}
for (sib = el.nextSibling; sib; sib = sib.nextSibling) {
if (sib.nodeType === Node.ELEMENT_NODE && xh.elementsShareFamily(el, sib)) {
return 1;
}
}
return 0;
};
xh.makeQueryForElement = function(el) {
var query = '';
for (; el && el.nodeType === Node.ELEMENT_NODE; el = el.parentNode) {
var component = el.tagName.toLowerCase();
var index = xh.getElementIndex(el);
if (el.id) {
component += '[@id=\'' + el.id + '\']';
} else if (el.className) {
component += '[@class=\'' + el.className + '\']';
}
if (index >= 1) {
component += '[' + index + ']';
}
// If the last tag is an img, the user probably wants img/@src.
if (query === '' && el.tagName.toLowerCase() === 'img') {
component += '/@src';
}
query = '/' + component + query;
}
return query;
};
xh.highlight = function(els) {
for (var i = 0, l = els.length; i < l; i++) {
els[i].classList.add('xh-highlight');
}
};
xh.clearHighlights = function() {
var els = document.querySelectorAll('.xh-highlight');
for (var i = 0, l = els.length; i < l; i++) {
els[i].classList.remove('xh-highlight');
}
};
// Returns [values, nodeCount]. Highlights result nodes, if applicable. Assumes
// no nodes are currently highlighted.
xh.evaluateQuery = function(query) {
var xpathResult = null;
var str = '';
var nodeCount = 0;
var toHighlight = [];
try {
xpathResult = document.evaluate(query, document, null,
XPathResult.ANY_TYPE, null);
} catch (e) {
str = '[INVALID XPATH EXPRESSION]';
nodeCount = 0;
}
if (!xpathResult) {
return [str, nodeCount];
}
if (xpathResult.resultType === XPathResult.BOOLEAN_TYPE) {
str = xpathResult.booleanValue ? '1' : '0';
nodeCount = 1;
} else if (xpathResult.resultType === XPathResult.NUMBER_TYPE) {
str = xpathResult.numberValue.toString();
nodeCount = 1;
} else if (xpathResult.resultType === XPathResult.STRING_TYPE) {
str = xpathResult.stringValue;
nodeCount = 1;
} else if (xpathResult.resultType ===
XPathResult.UNORDERED_NODE_ITERATOR_TYPE) {
for (var node = xpathResult.iterateNext(); node;
node = xpathResult.iterateNext()) {
if (node.nodeType === Node.ELEMENT_NODE) {
toHighlight.push(node);
}
if (str) {
str += '\n';
}
str += node.textContent;
nodeCount++;
}
if (nodeCount === 0) {
str = '[NULL]';
}
} else {
// Since we pass XPathResult.ANY_TYPE to document.evaluate(), we should
// never get back a result type not handled above.
str = '[INTERNAL ERROR]';
nodeCount = 0;
}
xh.highlight(toHighlight);
return [str, nodeCount];
};
////////////////////////////////////////////////////////////
// xh.Bar class definition
xh.Bar = function() {
this.boundHandleRequest_ = this.handleRequest_.bind(this);
this.boundMouseMove_ = this.mouseMove_.bind(this);
this.boundKeyDown_ = this.keyDown_.bind(this);
this.inDOM_ = false;
this.currEl_ = null;
this.barFrame_ = document.createElement('iframe');
this.barFrame_.src = chrome.runtime.getURL('bar.html');
this.barFrame_.id = 'xh-bar';
// Init to hidden so first showBar_() triggers fade-in.
this.barFrame_.classList.add('hidden');
document.addEventListener('keydown', this.boundKeyDown_);
chrome.runtime.onMessage.addListener(this.boundHandleRequest_);
};
xh.Bar.prototype.hidden_ = function() {
return this.barFrame_.classList.contains('hidden');
};
xh.Bar.prototype.updateQueryAndBar_ = function(el) {
xh.clearHighlights();
this.query_ = el ? xh.makeQueryForElement(el) : '';
this.updateBar_(true);
};
xh.Bar.prototype.updateBar_ = function(updateQuery) {
var results = this.query_ ? xh.evaluateQuery(this.query_) : ['', 0];
chrome.runtime.sendMessage({
type: 'update',
query: updateQuery ? this.query_ : null,
results: results
});
};
xh.Bar.prototype.showBar_ = function() {
var that = this;
function impl() {
that.barFrame_.classList.remove('hidden');
document.addEventListener('mousemove', that.boundMouseMove_);
that.updateBar_(true);
}
if (!this.inDOM_) {
this.inDOM_ = true;
document.body.appendChild(this.barFrame_);
}
window.setTimeout(impl, 0);
};
xh.Bar.prototype.hideBar_ = function() {
var that = this;
function impl() {
that.barFrame_.classList.add('hidden');
document.removeEventListener('mousemove', that.boundMouseMove_);
xh.clearHighlights();
}
window.setTimeout(impl, 0);
};
xh.Bar.prototype.toggleBar_ = function() {
if (this.hidden_()) {
this.showBar_();
} else {
this.hideBar_();
}
};
xh.Bar.prototype.handleRequest_ = function(request, sender, cb) {
if (request.type === 'evaluate') {
xh.clearHighlights();
this.query_ = request.query;
this.updateBar_(false);
} else if (request.type === 'moveBar') {
// Move iframe to a different part of the screen.
this.barFrame_.classList.toggle('bottom');
} else if (request.type === 'hideBar') {
this.hideBar_();
window.focus();
} else if (request.type === 'toggleBar') {
this.toggleBar_();
}
};
xh.Bar.prototype.mouseMove_ = function(e) {
if (this.currEl_ === e.toElement) {
return;
}
this.currEl_ = e.toElement;
if (e.shiftKey) {
this.updateQueryAndBar_(this.currEl_);
}
};
xh.Bar.prototype.keyDown_ = function(e) {
var ctrlKey = e.ctrlKey || e.metaKey;
var shiftKey = e.shiftKey;
if (e.keyCode === xh.X_KEYCODE && ctrlKey && shiftKey) {
this.toggleBar_();
}
// If the user just pressed Shift and they're not holding Ctrl, update query.
// Note that we rely on the mousemove handler to have updated this.currEl_.
// Also, note that checking e.shiftKey wouldn't work here, since Shift is the
// key that triggered this event.
if (!this.hidden_() && !ctrlKey && e.keyCode === xh.SHIFT_KEYCODE) {
this.updateQueryAndBar_(this.currEl_);
}
};
////////////////////////////////////////////////////////////
// Initialization code
if (location.href.indexOf('acid3.acidtests.org') === -1) {
window.xhBarInstance = new xh.Bar();
}