Skip to content

Instantly share code, notes, and snippets.

@edvakf
Created July 9, 2010 02:14
Show Gist options
  • Save edvakf/468941 to your computer and use it in GitHub Desktop.
Save edvakf/468941 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name searchalltabs.js
// @author edvakf (and thank you amachang for escapeXPathExpr)
// @namespace http://d.hatena.ne.jp/edvakf/
// @description An experimental UserJS to search for word from all tabs and move to the tab
// @license The MIT License
// @version 0.3
// @include *
// @released 2010-06-08
// @updated 2010-06-08
// @compatible Opera 10.5+
// @update http://gist.github.com/468941.txt
// ==/UserScript==
(function init() {
var windowFocus = window.focus;
window.focus = function() {}; // no-op
var iframeSrc = 'http://0.0.0.0/searchalltabs.js';
if (window.top !== window.self) {
if (location.href !== iframeSrc) return;
var id = Math.floor(Date.now() + Math.random() * 1000);
var pf = 'searchalltabs_'; // common prefix for storage keys
var LS = localStorage;
var c = new MessageChannel;
window.parent.postMessage({message: 'connect', src: iframeSrc}, '*', [c.port2]);
var port = c.port1;
port.onmessage = function onMessageFromTab(e) {
var data = e.data;
switch (data.message) {
case 'search':
startSearch(data.word);
break;
case 'result':
data.from = id;
if (data.exists) LS[pf + 'result'] = JSON.stringify(data);
break;
case 'focus':
LS[pf + 'focus'] = JSON.stringify(data);
break;
}
}
var search_word;
var result;
var timer;
function startSearch(word) {
LS[pf + 'search'] = JSON.stringify({word: word, from: id});
search_word = word;
result = [];
clearTimeout(timer);
timer = setTimeout(function() {
port.postMessage({message: 'result', word: word, tabs: result});
}, 500);
}
addEventListener('storage', function onStorageMessage(e) {
switch (e.key) {
case pf + 'search':
var data = JSON.parse(e.newValue);
port.postMessage({message: 'search', word: data.word, from: data.from});
break;
case pf + 'result':
var data = JSON.parse(e.newValue);
if (data.to !== id || data.word !== search_word) return;
result.push({id: data.from, title: data.title});
break;
case pf + 'focus':
var data = JSON.parse(e.newValue);
if (data.to !== id) return;
port.postMessage({message: 'focus'});
break;
}
}, false);
} else { // window.top === window.self
// open client iframe
opera.addEventListener('BeforeEvent.DOMContentLoaded', function clientDOMLoad() {
var f = document.createElement('iframe');
f.src = iframeSrc;
f.style = 'position:absolute;width:1px;height:1px;top:0;left:0;border:none;visibility:hidden;';
document.body.appendChild(f);
}, false);
// wait for first message from client
opera.addEventListener('BeforeEvent.message', function clientConnect(ev) {
var e = ev.event;
if (!(e.origin === 'http://0.0.0.0' && e.data && e.data.message === 'connect' && e.data.src === iframeSrc && e.ports)) return;
ev.preventDefault(); // prevent passing the secret port to the page
startClient(e.ports[0]);
}, false);
function startClient(port) {
port.onmessage = function clientOnMessage(e) {
if (!e.data) return;
switch(e.data.message) {
case 'search':
port.postMessage({
message: 'result', to: e.data.from, word: e.data.word,
exists: wordExists(e.data.word), title: document.title || '[no title]'
});
break;
case 'focus':
windowFocus();
break;
}
}
// searching
document.addEventListener('keydown', function(e) {
if (e.keyCode === 70 && e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) { // shift+ctrl+f (or shift+cmd+f for mac)
var box = createSearchBox();
var input = box.querySelector('input');
setTimeout(function() { input.focus(); }, 50); // timeout because sometimes can't focus otherwise
var word;
function startSearch() {
if (word === input.value) return; // search term has not been changed
word = input.value;
port.postMessage({message: 'search', word: word});
var timer = setTimeout(function searchTimeout() {
port.removeEventListener('message', waitResults, false);
box.querySelector('.searchalltabs_results').innerHTML = '';
}, 900);
port.addEventListener('message', waitResults, false);
function waitResults(e) {
var d = e.data;
if (!d || d.message !== 'result') return;
// if another query is entered, then cancel search (remove event listener and abort)
if (d.word !== word) return port.removeEventListener('message', waitResults, false);
clearTimeout(timer);
box.querySelector('.searchalltabs_results').innerHTML = d.tabs.sort(function(a,b) {
return a.id - b.id; // sort by some consistent parameter
}).map(function(tab) {
return '<p id="tabid_' + tab.id + '" '
+ 'style="margin:0;padding:3px;font:normal normal normal 12pt/120% \'Helvetica\', \'Arial\', sans-serif;'
+ 'text-indent:0;white-space:nowrap;overflow:hidden;-o-text-overflow:ellipsis;background-color:white;color:black;">'
+ escapeHTML(tab.title)
+ '</p>';
}).join('\n');
}
}
function shortcut(e) {
if (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
var keyCode = e.keyCode;
if (keyCode === 13) { // enter
e.preventDefault();
var sw = box.querySelector('.switchto');
if (!sw) return;
var tab = + sw.id.replace('tabid_',''); // number
port.postMessage({message: 'focus', to: tab});
box.parentNode.removeChild(box);
} else if (keyCode === 38 || keyCode === 40) { // up/down
e.preventDefault();
var sw = box.querySelector('.switchto');
var p = Array.prototype.slice.call(box.querySelectorAll('.searchalltabs_results > p'));
if (p.length === 0) return;
if (sw) {
sw.style.backgroundColor = '';
sw.removeAttribute('class');
}
if (keyCode === 40) { // down
sw = sw ? p[(p.indexOf(sw) + 1) % p.length] : p[0];
} else if (keyCode === 38) { // up
sw = sw ? p[(p.indexOf(sw) || p.length) - 1] : p[p.length - 1];
}
sw.style.backgroundColor = '#CCF';
sw.className = 'switchto';
}
}
startSearch(); // initial search (empty query -> collect all tabs)
input.addEventListener('input', startSearch, false);
input.addEventListener('keypress', shortcut, false); // opera still cannot preventDefault on keydown
input.addEventListener('blur', function() { box.parentNode.removeChild(box); }, false);
}
}, false);
}
// misc
function createSearchBox() {
var box = document.querySelector('#searchalltabs_box');
if (box) box.parentNode.removeChild(box);
box = document.createElement('div');
box.id = 'searchalltabs_box';
box.style = 'position:absolute;margin:0;padding:0;width:1px;height:1px;top:0;left:0;'
box.innerHTML = '<div style="position:fixed;margin:0;padding:0;top:0;left:0;width:100%;height:100%;background-color:gray;opacity:0.5;z-index:10000;"></div>'
+ '<div style="position:fixed;margin:0;padding:0;width:70%;height:auto;max-height:500px;margin-left:-35%;padding:0;left:50%;top:30%;z-index:10001;">'
+ '<input style="width:100%;margin:0;padding:3px 0;font:normal normal normal 18pt \'Helvetica\', \'Arial\', sans-serif;border:black solid 1px;">'
+ '<div class="searchalltabs_results" style="width:100%;min-height:0;margin:0 1px;padding:0;background-color:white;color:black;text-align:left;"></div>'
+ '</div>';
document.body.appendChild(box);
return box;
}
var escapeHTMLHash = {'<':'&lt;', '>':'&gt;', '\'':'&apos;', '"':'&quot;', '&':'&amp;'};
function escapeHTML(text) {
return (text+'').replace(/[<>'"&]/g, function($0){return escapeHTMLHash[$0];});
}
// search given word in the tab. return true if it exists
function wordExists(word) {
if (!word) return true; // empty query means collect all tabs' title and url (may ommit this in the future)
var TEXT = 'descendant::text()[contains(self::text(),' + escapeXPathExpr(word) + ')]';
var res = document.evaluate(TEXT, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
return !!res.iterateNext(); // can decide by the first iteration
}
// escape string token in xpath to avoid xpath injection. http://d.hatena.ne.jp/amachang/20090917/1253179486
function escapeXPathExpr(text) {
var matches = text.match(/[^"]+|"/g);
function esc(t) {
return t == '"' ? ('\'' + t + '\'') : ('"' + t + '"');
}
if (matches) {
if (matches.length == 1) {
return esc(matches[0]);
}
else {
var results = [];
for (var i = 0; i < matches.length; i ++) {
results.push(esc(matches[i]));
}
return 'concat(' + results.join(', ') + ')';
}
}
else {
return '""';
}
}
}
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment