/type-ahead.js
JavaScript | 636 lines | 610 code | 5 blank | 21 comment | 0 complexity | cf191877fcda828d15733a38aeebca8f MD5 | raw file
- /*
- type-ahead-find: find text or links as you write.
-
- This script is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This script is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this script. If not, see <http://www.gnu.org/licenses/>.
-
- Website: http://code.google.com/p/chrome-type-ahead/
- Author: Arnau Sanchez <tokland@gmail.com> (2009)
- */
- /* Styles and addStyle borrowed from nice-alert.js project */
- var styles = '\
- #type-ahead-box {\
- position: fixed;\
- top: 0;\
- right: 0;\
- margin: 0;\
- text-align: left;\
- z-index: 2147483647;\
- color: #000;\
- border-bottom: 1px solid #ccc;\
- border-bottom: 1px solid rgba(0,0,0,0.3);\
- padding: 4px 8px;\
- opacity: 0.9;\
- float: right;\
- clear: both;\
- overflow: hidden;\
- font-size: 18px;\
- font-family: Arial, Verdana, Georgia, Serif;\
- white-space: pre-wrap;\
- min-width: 60px;\
- outline: 0;\
- -webkit-box-shadow: 0px 2px 8px rgba(0,0,0,0.2);\
- -moz-box-shadow: 0px 2px 8px rgba(0,0,0,0.3);\
- }\
- \
- #type-ahead-box small {\
- letter-spacing: -0.12em;\
- color: #444;\
- }'
- /* Generic functions */
- function max(a, b) {
- return ((a > b) ? a : b);
- }
- function escape_regexp(s, ignore) {
- var special = ["\\", "?", ".", "+", "(", ")", "{", "}", "[", "]", "$", "^", "*"];
- special.forEach(function(re) {
- if (!ignore || ignore.indexOf(re) < 0)
- s = s.replace(new RegExp("\\" + re, "g"), "\\" + re);
- });
- return s;
- }
- function get_current_zoom(doc) {
- var s = doc.body.parentElement.style.zoom;
- var zoom;
- if (s.match('%$'))
- zoom = parseFloat(s.slice(0, s.length - 1)) / 100;
- else if (s)
- zoom = parseFloat(s.replace(',', '.'));
- else
- zoom = 1;
- return zoom;
- }
- function addStyle(css) {
- var head = document.getElementsByTagName('head')[0];
- if (head) {
- var style = document.createElement("style");
- style.type = "text/css";
- style.appendChild(document.createTextNode(css));
- head.appendChild(style);
- }
- }
- function stopEvent(ev) {
- ev.preventDefault();
- ev.stopPropagation();
- }
-
- function up(element, tagName) {
- var upTagName = tagName.toUpperCase();
- while (element && (!element.tagName ||
- element.tagName.toUpperCase() != upTagName)) {
- element = element.parentNode;
- }
- return element;
- }
- function upMatch(element, matchFunction) {
- while (element) {
- var res = matchFunction(element);
- if (res == null)
- return null;
- else if (res)
- return element;
- element = element.parentNode;
- }
- return element;
- }
- function isVisible(element) {
- while (element) {
- style = window.getComputedStyle(element);
- if (style && (style.getPropertyValue('display') == 'none' ||
- style.getPropertyValue('visibility') == 'hidden'))
- return false;
- element = element.parentNode;
- }
- return true;
- }
- function getRootNodes() {
- var rootNodes = new Array();
- var frames = document.getElementsByTagName('frame');
- for (var i = 0; i < frames.length; i++)
- rootNodes.push(frames[i]);
- return rootNodes;
- }
- function clearRanges() {
- var rootNodes = [window].concat(getRootNodes());
- for (var i = 0; i < rootNodes.length; i++) {
- var w = rootNodes[i].contentDocument || rootNodes[i];
- if (w && w.getSelection) {
- var selection = w.getSelection();
- selection.removeAllRanges();
- }
- }
- }
- function getActiveElement(doc) {
- return doc.activeElement || doc._tafActiveElement;
- }
- function getSelectedAnchor(doc) {
- var rootNodes = [document].concat(getRootNodes());
- for (var i = 0; i < rootNodes.length; i++) {
- var doc = rootNodes[i].contentDocument || rootNodes[i];
- var element = getActiveElement(doc);
- if (element && element.tagName == "A")
- return(element);
- }
- }
- function isInputElementActive(doc) {
- var element = getActiveElement(doc);
- if (!element)
- return;
- var name = element.tagName.toLowerCase();
- if (["input", "select", "textarea", "object", "embed"].indexOf(name) >= 0)
- return true;
- return (upMatch(element, function(el) {
- if (!el.getAttribute || el.getAttribute('contenteditable') == 'false')
- return null;
- return el.getAttribute('contenteditable');
- }))
- }
- function selectOnchange(select) {
- if (select) {
- select.focus();
- if (select.onchange)
- select.onchange();
- return true;
- }
- }
- function setAlternativeActiveDocument(doc) {
- function dom_trackActiveElement(evt) {
- if (evt && evt.target) {
- doc._tafActiveElement = (evt.target == doc) ? null : evt.target;
- }
- }
- function dom_trackActiveElementLost(evt) {
- doc._tafActiveElement = null;
- }
- if (doc._tafActiveElement == undefined && !doc.activeElement) {
- doc._tafActiveElement = null;
- doc.addEventListener("focus", dom_trackActiveElement, true);
- doc.addEventListener("blur", dom_trackActiveElementLost, true);
- }
- }
- /* Type-ahead specific functions */
- function clearSearchBox() {
- var box = document.getElementById('type-ahead-box');
- if (box) {
- box.style.display = 'none';
- }
- }
- function isSearchBoxVisible() {
- var box = document.getElementById('type-ahead-box');
- return (box && box.style.display != 'none');
- }
- function showSearchBox(search, clear_function) {
- var colors = {
- text: {ok: search.options.color_text, ko: search.options.color_notfound},
- links: {ok: search.options.color_link, ko: search.options.color_notfound},
- }
- var box = document.getElementById('type-ahead-box');
- if (!box) {
- box = document.createElement('TABOX');
- box.id = 'type-ahead-box';
- document.documentElement.appendChild(box);
- addStyle(styles);
- }
- box.style.display = 'block';
- if (search.mode) {
- var color = colors[search.mode][(search.total < 1 && search.text) ? 'ko' : 'ok']
- box.style['background-color'] = color;
- }
- box.innerHTML = search.text ?
- (search.text + ' <small>(' + search.nmatch + ' of ' + search.total + ')</small>') : ' ';
- box.style['top'] = ''
- if (search.nmatch >= 1 && search.range) {
- var sel = search.range.getBoundingClientRect();
- if (sel.right >= box.offsetLeft && sel.top <= box.offsetTop + box.offsetHeight) {
- topval = (sel.bottom + 10);
- box.style['top'] = ((topval < 100) ? topval : 100) + 'px';
- }
- }
- if (search.timeout && search.timeout > 0) {
- if (search.timeout_id)
- clearTimeout(search.timeout_id);
- search.timeout_id = setTimeout(function() {
- clear_function(true);
- }, search.timeout * 1000);
- }
- }
- function elementInViewport(el) {
- var top = el.offsetTop;
- var left = el.offsetLeft;
- var width = el.offsetWidth;
- var height = el.offsetHeight;
- while(el.offsetParent) {
- el = el.offsetParent;
- top += el.offsetTop;
- left += el.offsetLeft;
- }
- return (top >= window.pageYOffset &&
- left >= window.pageXOffset &&
- (top + height) <= (window.pageYOffset + window.innerHeight) &&
- (left + width) <= (window.pageXOffset + window.innerWidth));
- }
- function processSearch(search, options) {
- var selected = false;
- var selectedAnchor = getSelectedAnchor();
-
- if (selectedAnchor)
- selectedAnchor.blur();
-
- if (search.text.length <= 0 || !search.mode) {
- clearRanges();
- return(selected);
- }
-
- var matchedElements = new Array();
- var string = escape_regexp(search.text).replace(/\s+/g, "(\\s|\240)+");
- if (options.starts_link_only)
- string = '^' + string;
- // If string is lower case, search will be case-unsenstive.
- // If string is upper case, search will be case-senstive.
- var regexp = new RegExp(string, string == string.toLowerCase() ? 'ig' : 'g')
- // currently Xpath does not support regexp matches. That would be great:
- // document.evaluate('//a//*[matches(text(), "regexp")]', document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(N);
- var rootNodes = [window].concat(getRootNodes());
- for (var i = 0; i < rootNodes.length; i++) {
- var doc = rootNodes[i].document || rootNodes[i].contentDocument;
- if (!doc || !doc.body)
- continue;
- var frame = rootNodes[i].contentWindow || rootNodes[i];
- var regexp2 = new RegExp(regexp);
- function match(textNode) {
- if (!textNode.data.match(regexp2))
- return NodeFilter.FILTER_REJECT;
- var anchor = up(textNode, 'a');
- if ((search.mode == 'links' && !anchor) ||
- !isVisible(textNode.parentNode) ||
- up(textNode.parentNode, 'script'))
- return NodeFilter.FILTER_REJECT;
- return NodeFilter.FILTER_ACCEPT;
- }
- var nodeIterator = doc.createNodeIterator(doc.body, NodeFilter.SHOW_TEXT, match, true);
-
- while ((textNode = nodeIterator.nextNode()) != null) {
- var anchor = up(textNode, 'a');
- var option = up(textNode.parentNode, 'option');
- var regexp2 = new RegExp(regexp);
- var result;
- while (match = regexp2.exec(textNode.data)) {
- result = {doc: doc, frame: frame, node: textNode,
- anchor: anchor, option: option,
- start: match.index, end: regexp2.lastIndex};
- matchedElements.push(result);
- }
- }
- }
-
- search.total = matchedElements.length;
- if (matchedElements.length > 0) {
- var index;
-
- // If this is the first search, start on the first match on viewport
- if (search.search_in_viewport) {
- index = 0;
- while (index < matchedElements.length &&
- !elementInViewport(matchedElements[index].node.parentNode)) {
- index += 1;
- }
- } else {
- index = search.index;
- }
- index = index % matchedElements.length;
- if (index < 0)
- index += matchedElements.length;
- var result = matchedElements[index];
- search.index = index;
- search.nmatch = index + 1;
- search.element = result.node.parentNode;
- if (result.option) {
- result.option.selected = 'selected';
- search.select = up(result.option, 'select');
- } else {
- search.select = null;
- }
- clearRanges();
-
- var range = result.doc.createRange();
- range.setStart(result.node, result.start);
- range.setEnd(result.node, result.end);
- var selection = window.getSelection();
- selection.addRange(range);
- search.range = range;
- selected = true;
- if (result.anchor)
- result.anchor.focus();
- // Apparently A's with empty href don't trigger the window scroll when focused
- if (!result.anchor || !result.anchor.href) {
- var rect = range.getBoundingClientRect();
- var doc_height = window.innerHeight;
- var zoom = get_current_zoom(result.doc);
- if (zoom*rect.top < (doc_height * 1) / 6.0 || zoom*rect.bottom > (doc_height * 5) / 6.0) {
- var y = max(0, window.pageYOffset + zoom*rect.top - doc_height / 3.0);
- window.scrollTo(window.pageXOffset + zoom*rect.left, y);
- }
- }
- } else {
- search.nmatch = 0;
- clearRanges();
- }
-
- return(selected);
- }
- function is_shortcut(ev) {
- var is_mac = navigator.appVersion.indexOf("Mac") !== -1;
- var is_windows = navigator.appVersion.indexOf("Windows") !== -1;
- var is_alternative_input = (is_mac && ev.altKey) || (is_windows && ev.ctrlKey && ev.altKey);
- return !is_alternative_input && (ev.altKey || ev.metaKey || ev.ctrlKey);
- }
- function check_blacklist(sites_blacklist) {
- if (sites_blacklist) {
- var url = window.location.href;
- var urls = options.sites_blacklist.split('\n');
- for (var i=0; i < urls.length; i++) {
- var s = urls[i].replace(/^\s+|\s+$/g, '');
- if (s[0] == '#') {
- // If URL starts with #, ignore it
- continue;
- } else if (s[0] == '|') {
- // If URL starts with | assume it is a real regexp.
- var regexp = new RegExp('^' + s.slice(1) + '$');
- if (url.match(regexp))
- return true;
- } else {
- // Otherwise compare with the URL ('*' is the only wildcard available)
- var s2 = escape_regexp(s, ["*"]).replace(new RegExp("\\*", "g"), ".*");
- var regexp = new RegExp('^' + s2 + '$');
- if (url.match(regexp))
- return true;
- }
- }
- }
- return false;
- }
- function init(options) {
- var keycodes = {
- "backspace": 8,
- "tab": 9,
- "enter": 13,
- "spacebar": 32,
- "escape": 27,
- "n": 78,
- "p": 80,
- "g": 71,
- "f3": 114,
- "f4": 115,
- };
-
- var search = {};
- function clearSearch(clearRanges) {
- if (search.timeout_id) {
- clearTimeout(search.timeout_id);
- }
- search = {mode: null, text: '', index: 0, search_in_viewport: true,
- matches: 0, total: 0, select: null,
- timeout: parseInt(options.timeout), options: options};
- if (clearRanges) {
- selection = window.getSelection();
- selection.removeAllRanges();
- }
- clearSearchBox();
- }
-
- function processSearchWithOptions(blur_unless_found) {
- return processSearch(search, {
- search_links: (search.mode == 'links'),
- starts_link_only: options["starts_link_only"],
- blur_unless_found: blur_unless_found
- });
- }
- function setEvents(rootNode) {
- var doc = rootNode.contentDocument || rootNode;
- var body = rootNode.body;
-
- if (!body || !body.addEventListener) {
- return;
- }
- setAlternativeActiveDocument(doc);
- doc.addEventListener('keydown', function(ev) {
- if (isInputElementActive(doc))
- return;
-
- var code = ev.keyCode;
- var selectedAnchor = getSelectedAnchor(doc);
- var blacklisted = check_blacklist(options.sites_blacklist);
- // Disable blacklisted sites completely
- //if (blacklisted && !search.mode)
- if (blacklisted)
- return;
-
- if (code == keycodes.backspace && search.mode) {
- if (search.text) {
- if (!ev.ctrlKey) {
- search.text = search.text.substr(0, search.text.length-1);
- } else { /* delete last word */
- var index = search.text.lastIndexOf(' ');
- search.text = (index == -1) ? "": search.text.substr(0, index+1);
- }
- processSearchWithOptions(true);
- showSearchBox(search, clearSearchBox);
- }
- } else if (code == keycodes.escape && search.mode) {
- clearSearch(true);
- processSearchWithOptions(true);
- } else if (code == keycodes.enter &&
- (selectedAnchor || selectOnchange(search.select))) {
- clearSearch(true);
- return;
- } else if (search.mode && code == keycodes.tab && !selectedAnchor) {
- var nodeIterator = doc.createNodeIterator(
- doc.body,
- NodeFilter.SHOW_ELEMENT,
- null,
- false
- );
- var textNode;
- while ((textNode = nodeIterator.nextNode()) != search.element);
- var nextNodeFuncName = ev.shiftKey ? "previousNode" : "nextNode";
- while ((textNode = nodeIterator[nextNodeFuncName]()) != null) {
- if ((textNode.tagName == 'A' && textNode.href) ||
- textNode.tagName == "INPUT" ||
- textNode.tagName == "TEXTAREA" ||
- textNode.tagName == "SELECT") {
- // Hack: If we directly textNode.focus() the cursor is not
- // set in the field. Why? As a workaround, defer to a timeout
- setTimeout(function() { textNode.focus(); }, 0);
- break;
- }
- }
- clearSearch(true);
- stopEvent(ev);
- return;
- } else if (search.mode && code == keycodes.tab) {
- clearSearch(true);
- return;
- } else if (code == keycodes.f4 && search.mode) {
- search.mode = (search.mode == 'text') ? 'links' : 'text'
- search.index = 0;
- processSearchWithOptions(true);
- showSearchBox(search, clearSearchBox);
- } else if (search.text && (code == keycodes.f3 ||
- (code == keycodes.g && (ev.ctrlKey || ev.metaKey)) ||
- (code == keycodes.n && ev.altKey) ||
- (code == keycodes.p && ev.altKey))) {
- search.search_in_viewport = false;
- search.index += (ev.shiftKey || code == keycodes.p) ? -1 : +1;
- processSearchWithOptions(true);
- showSearchBox(search, clearSearchBox);
- } else {
- return;
- }
-
- stopEvent(ev);
- }, false);
-
- doc.addEventListener('keypress', function(ev) {
- if (isInputElementActive(doc))
- return;
- var blacklisted = check_blacklist(options.sites_blacklist);
- if (blacklisted)
- return;
- var code = ev.keyCode;
- var ascii = String.fromCharCode(code);
- if (!is_shortcut(ev) && ascii && [keycodes.enter].indexOf(code) == -1 &&
- (code != keycodes.spacebar || search.mode)) {
- if (!search.mode && ascii == "/") {
- search.mode = 'text';
- } else if (!search.mode && ascii == "'") {
- search.mode = 'links';
- } else if (!search.mode && blacklisted) {
- return;
- } else if (!search.mode && options.direct_search_mode == 'disabled') {
- return;
- } else if (!search.mode) {
- search.mode = options.direct_search_mode == 'links' ? 'links' : 'text';
- search.text += ascii;
- } else if (search.mode) {
- if (isSearchBoxVisible()) {
- search.text += ascii;
- } else {
- search.text = ascii;
- }
- }
- processSearchWithOptions(true)
- showSearchBox(search, clearSearchBox);
- stopEvent(ev)
- }
- }, false);
-
- doc.addEventListener('mousedown', function(ev) {
- if (search.mode)
- clearSearch(false);
- }, false);
- }
- clearSearch(false);
- var rootNodes = [document].concat(getRootNodes());
- for (var i = 0; i < rootNodes.length; i++) {
- var rootNode = rootNodes[i];
- if (rootNode.contentDocument) {
- rootNode.addEventListener('load', function(ev) {
- setEvents(ev.target.contentDocument);
- });
- }
- else if (!rootNode.contentDocument || rootNode.contentDocument.readyState == 'complete') {
- setEvents(rootNode.contentDocument ? rootNode.contentDocument : rootNode);
- }
- }
- }
- /* Default options */
- var default_options = {
- direct_search_mode: 'text',
- starts_link_only: false,
- sites_blacklist: '',
- color_link: '#DDF',
- color_text: '#FF5',
- color_notfound: 'F55'
- };
- function main(options) {
- // Use setInterval to add events to document.body as soon as possible
- interval_id = setInterval(function() {
- if (document.body) {
- clearInterval(interval_id);
- init(options);
- }
- }, 100);
- }
- setAlternativeActiveDocument(document);
- options = default_options;
- if (typeof(chrome) == "object" && chrome.extension) {
- chrome.extension.sendRequest({'get_options': true}, function(response) {
- options = {
- direct_search_mode: response.direct_search_mode,
- sites_blacklist: response.sites_blacklist,
- starts_link_only: (response.starts_link_only == '1'),
- timeout: response.timeout,
- color_link: response.color_link || default_options.color_link,
- color_text: response.color_text || default_options.color_text,
- color_notfound: response.color_notfound || default_options.color_notfound
- };
- main(options);
- });
- } else {
- main(options);
- }