PageRenderTime 44ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/type-ahead.js

http://chrome-type-ahead.googlecode.com/
JavaScript | 636 lines | 610 code | 5 blank | 21 comment | 0 complexity | cf191877fcda828d15733a38aeebca8f MD5 | raw file
  1. /*
  2. type-ahead-find: find text or links as you write.
  3. This script is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 3 of the License, or
  6. (at your option) any later version.
  7. This script is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this script. If not, see <http://www.gnu.org/licenses/>.
  13. Website: http://code.google.com/p/chrome-type-ahead/
  14. Author: Arnau Sanchez <tokland@gmail.com> (2009)
  15. */
  16. /* Styles and addStyle borrowed from nice-alert.js project */
  17. var styles = '\
  18. #type-ahead-box {\
  19. position: fixed;\
  20. top: 0;\
  21. right: 0;\
  22. margin: 0;\
  23. text-align: left;\
  24. z-index: 2147483647;\
  25. color: #000;\
  26. border-bottom: 1px solid #ccc;\
  27. border-bottom: 1px solid rgba(0,0,0,0.3);\
  28. padding: 4px 8px;\
  29. opacity: 0.9;\
  30. float: right;\
  31. clear: both;\
  32. overflow: hidden;\
  33. font-size: 18px;\
  34. font-family: Arial, Verdana, Georgia, Serif;\
  35. white-space: pre-wrap;\
  36. min-width: 60px;\
  37. outline: 0;\
  38. -webkit-box-shadow: 0px 2px 8px rgba(0,0,0,0.2);\
  39. -moz-box-shadow: 0px 2px 8px rgba(0,0,0,0.3);\
  40. }\
  41. \
  42. #type-ahead-box small {\
  43. letter-spacing: -0.12em;\
  44. color: #444;\
  45. }'
  46. /* Generic functions */
  47. function max(a, b) {
  48. return ((a > b) ? a : b);
  49. }
  50. function escape_regexp(s, ignore) {
  51. var special = ["\\", "?", ".", "+", "(", ")", "{", "}", "[", "]", "$", "^", "*"];
  52. special.forEach(function(re) {
  53. if (!ignore || ignore.indexOf(re) < 0)
  54. s = s.replace(new RegExp("\\" + re, "g"), "\\" + re);
  55. });
  56. return s;
  57. }
  58. function get_current_zoom(doc) {
  59. var s = doc.body.parentElement.style.zoom;
  60. var zoom;
  61. if (s.match('%$'))
  62. zoom = parseFloat(s.slice(0, s.length - 1)) / 100;
  63. else if (s)
  64. zoom = parseFloat(s.replace(',', '.'));
  65. else
  66. zoom = 1;
  67. return zoom;
  68. }
  69. function addStyle(css) {
  70. var head = document.getElementsByTagName('head')[0];
  71. if (head) {
  72. var style = document.createElement("style");
  73. style.type = "text/css";
  74. style.appendChild(document.createTextNode(css));
  75. head.appendChild(style);
  76. }
  77. }
  78. function stopEvent(ev) {
  79. ev.preventDefault();
  80. ev.stopPropagation();
  81. }
  82. function up(element, tagName) {
  83. var upTagName = tagName.toUpperCase();
  84. while (element && (!element.tagName ||
  85. element.tagName.toUpperCase() != upTagName)) {
  86. element = element.parentNode;
  87. }
  88. return element;
  89. }
  90. function upMatch(element, matchFunction) {
  91. while (element) {
  92. var res = matchFunction(element);
  93. if (res == null)
  94. return null;
  95. else if (res)
  96. return element;
  97. element = element.parentNode;
  98. }
  99. return element;
  100. }
  101. function isVisible(element) {
  102. while (element) {
  103. style = window.getComputedStyle(element);
  104. if (style && (style.getPropertyValue('display') == 'none' ||
  105. style.getPropertyValue('visibility') == 'hidden'))
  106. return false;
  107. element = element.parentNode;
  108. }
  109. return true;
  110. }
  111. function getRootNodes() {
  112. var rootNodes = new Array();
  113. var frames = document.getElementsByTagName('frame');
  114. for (var i = 0; i < frames.length; i++)
  115. rootNodes.push(frames[i]);
  116. return rootNodes;
  117. }
  118. function clearRanges() {
  119. var rootNodes = [window].concat(getRootNodes());
  120. for (var i = 0; i < rootNodes.length; i++) {
  121. var w = rootNodes[i].contentDocument || rootNodes[i];
  122. if (w && w.getSelection) {
  123. var selection = w.getSelection();
  124. selection.removeAllRanges();
  125. }
  126. }
  127. }
  128. function getActiveElement(doc) {
  129. return doc.activeElement || doc._tafActiveElement;
  130. }
  131. function getSelectedAnchor(doc) {
  132. var rootNodes = [document].concat(getRootNodes());
  133. for (var i = 0; i < rootNodes.length; i++) {
  134. var doc = rootNodes[i].contentDocument || rootNodes[i];
  135. var element = getActiveElement(doc);
  136. if (element && element.tagName == "A")
  137. return(element);
  138. }
  139. }
  140. function isInputElementActive(doc) {
  141. var element = getActiveElement(doc);
  142. if (!element)
  143. return;
  144. var name = element.tagName.toLowerCase();
  145. if (["input", "select", "textarea", "object", "embed"].indexOf(name) >= 0)
  146. return true;
  147. return (upMatch(element, function(el) {
  148. if (!el.getAttribute || el.getAttribute('contenteditable') == 'false')
  149. return null;
  150. return el.getAttribute('contenteditable');
  151. }))
  152. }
  153. function selectOnchange(select) {
  154. if (select) {
  155. select.focus();
  156. if (select.onchange)
  157. select.onchange();
  158. return true;
  159. }
  160. }
  161. function setAlternativeActiveDocument(doc) {
  162. function dom_trackActiveElement(evt) {
  163. if (evt && evt.target) {
  164. doc._tafActiveElement = (evt.target == doc) ? null : evt.target;
  165. }
  166. }
  167. function dom_trackActiveElementLost(evt) {
  168. doc._tafActiveElement = null;
  169. }
  170. if (doc._tafActiveElement == undefined && !doc.activeElement) {
  171. doc._tafActiveElement = null;
  172. doc.addEventListener("focus", dom_trackActiveElement, true);
  173. doc.addEventListener("blur", dom_trackActiveElementLost, true);
  174. }
  175. }
  176. /* Type-ahead specific functions */
  177. function clearSearchBox() {
  178. var box = document.getElementById('type-ahead-box');
  179. if (box) {
  180. box.style.display = 'none';
  181. }
  182. }
  183. function isSearchBoxVisible() {
  184. var box = document.getElementById('type-ahead-box');
  185. return (box && box.style.display != 'none');
  186. }
  187. function showSearchBox(search, clear_function) {
  188. var colors = {
  189. text: {ok: search.options.color_text, ko: search.options.color_notfound},
  190. links: {ok: search.options.color_link, ko: search.options.color_notfound},
  191. }
  192. var box = document.getElementById('type-ahead-box');
  193. if (!box) {
  194. box = document.createElement('TABOX');
  195. box.id = 'type-ahead-box';
  196. document.documentElement.appendChild(box);
  197. addStyle(styles);
  198. }
  199. box.style.display = 'block';
  200. if (search.mode) {
  201. var color = colors[search.mode][(search.total < 1 && search.text) ? 'ko' : 'ok']
  202. box.style['background-color'] = color;
  203. }
  204. box.innerHTML = search.text ?
  205. (search.text + ' <small>(' + search.nmatch + ' of ' + search.total + ')</small>') : '&nbsp;';
  206. box.style['top'] = ''
  207. if (search.nmatch >= 1 && search.range) {
  208. var sel = search.range.getBoundingClientRect();
  209. if (sel.right >= box.offsetLeft && sel.top <= box.offsetTop + box.offsetHeight) {
  210. topval = (sel.bottom + 10);
  211. box.style['top'] = ((topval < 100) ? topval : 100) + 'px';
  212. }
  213. }
  214. if (search.timeout && search.timeout > 0) {
  215. if (search.timeout_id)
  216. clearTimeout(search.timeout_id);
  217. search.timeout_id = setTimeout(function() {
  218. clear_function(true);
  219. }, search.timeout * 1000);
  220. }
  221. }
  222. function elementInViewport(el) {
  223. var top = el.offsetTop;
  224. var left = el.offsetLeft;
  225. var width = el.offsetWidth;
  226. var height = el.offsetHeight;
  227. while(el.offsetParent) {
  228. el = el.offsetParent;
  229. top += el.offsetTop;
  230. left += el.offsetLeft;
  231. }
  232. return (top >= window.pageYOffset &&
  233. left >= window.pageXOffset &&
  234. (top + height) <= (window.pageYOffset + window.innerHeight) &&
  235. (left + width) <= (window.pageXOffset + window.innerWidth));
  236. }
  237. function processSearch(search, options) {
  238. var selected = false;
  239. var selectedAnchor = getSelectedAnchor();
  240. if (selectedAnchor)
  241. selectedAnchor.blur();
  242. if (search.text.length <= 0 || !search.mode) {
  243. clearRanges();
  244. return(selected);
  245. }
  246. var matchedElements = new Array();
  247. var string = escape_regexp(search.text).replace(/\s+/g, "(\\s|\240)+");
  248. if (options.starts_link_only)
  249. string = '^' + string;
  250. // If string is lower case, search will be case-unsenstive.
  251. // If string is upper case, search will be case-senstive.
  252. var regexp = new RegExp(string, string == string.toLowerCase() ? 'ig' : 'g')
  253. // currently Xpath does not support regexp matches. That would be great:
  254. // document.evaluate('//a//*[matches(text(), "regexp")]', document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(N);
  255. var rootNodes = [window].concat(getRootNodes());
  256. for (var i = 0; i < rootNodes.length; i++) {
  257. var doc = rootNodes[i].document || rootNodes[i].contentDocument;
  258. if (!doc || !doc.body)
  259. continue;
  260. var frame = rootNodes[i].contentWindow || rootNodes[i];
  261. var regexp2 = new RegExp(regexp);
  262. function match(textNode) {
  263. if (!textNode.data.match(regexp2))
  264. return NodeFilter.FILTER_REJECT;
  265. var anchor = up(textNode, 'a');
  266. if ((search.mode == 'links' && !anchor) ||
  267. !isVisible(textNode.parentNode) ||
  268. up(textNode.parentNode, 'script'))
  269. return NodeFilter.FILTER_REJECT;
  270. return NodeFilter.FILTER_ACCEPT;
  271. }
  272. var nodeIterator = doc.createNodeIterator(doc.body, NodeFilter.SHOW_TEXT, match, true);
  273. while ((textNode = nodeIterator.nextNode()) != null) {
  274. var anchor = up(textNode, 'a');
  275. var option = up(textNode.parentNode, 'option');
  276. var regexp2 = new RegExp(regexp);
  277. var result;
  278. while (match = regexp2.exec(textNode.data)) {
  279. result = {doc: doc, frame: frame, node: textNode,
  280. anchor: anchor, option: option,
  281. start: match.index, end: regexp2.lastIndex};
  282. matchedElements.push(result);
  283. }
  284. }
  285. }
  286. search.total = matchedElements.length;
  287. if (matchedElements.length > 0) {
  288. var index;
  289. // If this is the first search, start on the first match on viewport
  290. if (search.search_in_viewport) {
  291. index = 0;
  292. while (index < matchedElements.length &&
  293. !elementInViewport(matchedElements[index].node.parentNode)) {
  294. index += 1;
  295. }
  296. } else {
  297. index = search.index;
  298. }
  299. index = index % matchedElements.length;
  300. if (index < 0)
  301. index += matchedElements.length;
  302. var result = matchedElements[index];
  303. search.index = index;
  304. search.nmatch = index + 1;
  305. search.element = result.node.parentNode;
  306. if (result.option) {
  307. result.option.selected = 'selected';
  308. search.select = up(result.option, 'select');
  309. } else {
  310. search.select = null;
  311. }
  312. clearRanges();
  313. var range = result.doc.createRange();
  314. range.setStart(result.node, result.start);
  315. range.setEnd(result.node, result.end);
  316. var selection = window.getSelection();
  317. selection.addRange(range);
  318. search.range = range;
  319. selected = true;
  320. if (result.anchor)
  321. result.anchor.focus();
  322. // Apparently A's with empty href don't trigger the window scroll when focused
  323. if (!result.anchor || !result.anchor.href) {
  324. var rect = range.getBoundingClientRect();
  325. var doc_height = window.innerHeight;
  326. var zoom = get_current_zoom(result.doc);
  327. if (zoom*rect.top < (doc_height * 1) / 6.0 || zoom*rect.bottom > (doc_height * 5) / 6.0) {
  328. var y = max(0, window.pageYOffset + zoom*rect.top - doc_height / 3.0);
  329. window.scrollTo(window.pageXOffset + zoom*rect.left, y);
  330. }
  331. }
  332. } else {
  333. search.nmatch = 0;
  334. clearRanges();
  335. }
  336. return(selected);
  337. }
  338. function is_shortcut(ev) {
  339. var is_mac = navigator.appVersion.indexOf("Mac") !== -1;
  340. var is_windows = navigator.appVersion.indexOf("Windows") !== -1;
  341. var is_alternative_input = (is_mac && ev.altKey) || (is_windows && ev.ctrlKey && ev.altKey);
  342. return !is_alternative_input && (ev.altKey || ev.metaKey || ev.ctrlKey);
  343. }
  344. function check_blacklist(sites_blacklist) {
  345. if (sites_blacklist) {
  346. var url = window.location.href;
  347. var urls = options.sites_blacklist.split('\n');
  348. for (var i=0; i < urls.length; i++) {
  349. var s = urls[i].replace(/^\s+|\s+$/g, '');
  350. if (s[0] == '#') {
  351. // If URL starts with #, ignore it
  352. continue;
  353. } else if (s[0] == '|') {
  354. // If URL starts with | assume it is a real regexp.
  355. var regexp = new RegExp('^' + s.slice(1) + '$');
  356. if (url.match(regexp))
  357. return true;
  358. } else {
  359. // Otherwise compare with the URL ('*' is the only wildcard available)
  360. var s2 = escape_regexp(s, ["*"]).replace(new RegExp("\\*", "g"), ".*");
  361. var regexp = new RegExp('^' + s2 + '$');
  362. if (url.match(regexp))
  363. return true;
  364. }
  365. }
  366. }
  367. return false;
  368. }
  369. function init(options) {
  370. var keycodes = {
  371. "backspace": 8,
  372. "tab": 9,
  373. "enter": 13,
  374. "spacebar": 32,
  375. "escape": 27,
  376. "n": 78,
  377. "p": 80,
  378. "g": 71,
  379. "f3": 114,
  380. "f4": 115,
  381. };
  382. var search = {};
  383. function clearSearch(clearRanges) {
  384. if (search.timeout_id) {
  385. clearTimeout(search.timeout_id);
  386. }
  387. search = {mode: null, text: '', index: 0, search_in_viewport: true,
  388. matches: 0, total: 0, select: null,
  389. timeout: parseInt(options.timeout), options: options};
  390. if (clearRanges) {
  391. selection = window.getSelection();
  392. selection.removeAllRanges();
  393. }
  394. clearSearchBox();
  395. }
  396. function processSearchWithOptions(blur_unless_found) {
  397. return processSearch(search, {
  398. search_links: (search.mode == 'links'),
  399. starts_link_only: options["starts_link_only"],
  400. blur_unless_found: blur_unless_found
  401. });
  402. }
  403. function setEvents(rootNode) {
  404. var doc = rootNode.contentDocument || rootNode;
  405. var body = rootNode.body;
  406. if (!body || !body.addEventListener) {
  407. return;
  408. }
  409. setAlternativeActiveDocument(doc);
  410. doc.addEventListener('keydown', function(ev) {
  411. if (isInputElementActive(doc))
  412. return;
  413. var code = ev.keyCode;
  414. var selectedAnchor = getSelectedAnchor(doc);
  415. var blacklisted = check_blacklist(options.sites_blacklist);
  416. // Disable blacklisted sites completely
  417. //if (blacklisted && !search.mode)
  418. if (blacklisted)
  419. return;
  420. if (code == keycodes.backspace && search.mode) {
  421. if (search.text) {
  422. if (!ev.ctrlKey) {
  423. search.text = search.text.substr(0, search.text.length-1);
  424. } else { /* delete last word */
  425. var index = search.text.lastIndexOf(' ');
  426. search.text = (index == -1) ? "": search.text.substr(0, index+1);
  427. }
  428. processSearchWithOptions(true);
  429. showSearchBox(search, clearSearchBox);
  430. }
  431. } else if (code == keycodes.escape && search.mode) {
  432. clearSearch(true);
  433. processSearchWithOptions(true);
  434. } else if (code == keycodes.enter &&
  435. (selectedAnchor || selectOnchange(search.select))) {
  436. clearSearch(true);
  437. return;
  438. } else if (search.mode && code == keycodes.tab && !selectedAnchor) {
  439. var nodeIterator = doc.createNodeIterator(
  440. doc.body,
  441. NodeFilter.SHOW_ELEMENT,
  442. null,
  443. false
  444. );
  445. var textNode;
  446. while ((textNode = nodeIterator.nextNode()) != search.element);
  447. var nextNodeFuncName = ev.shiftKey ? "previousNode" : "nextNode";
  448. while ((textNode = nodeIterator[nextNodeFuncName]()) != null) {
  449. if ((textNode.tagName == 'A' && textNode.href) ||
  450. textNode.tagName == "INPUT" ||
  451. textNode.tagName == "TEXTAREA" ||
  452. textNode.tagName == "SELECT") {
  453. // Hack: If we directly textNode.focus() the cursor is not
  454. // set in the field. Why? As a workaround, defer to a timeout
  455. setTimeout(function() { textNode.focus(); }, 0);
  456. break;
  457. }
  458. }
  459. clearSearch(true);
  460. stopEvent(ev);
  461. return;
  462. } else if (search.mode && code == keycodes.tab) {
  463. clearSearch(true);
  464. return;
  465. } else if (code == keycodes.f4 && search.mode) {
  466. search.mode = (search.mode == 'text') ? 'links' : 'text'
  467. search.index = 0;
  468. processSearchWithOptions(true);
  469. showSearchBox(search, clearSearchBox);
  470. } else if (search.text && (code == keycodes.f3 ||
  471. (code == keycodes.g && (ev.ctrlKey || ev.metaKey)) ||
  472. (code == keycodes.n && ev.altKey) ||
  473. (code == keycodes.p && ev.altKey))) {
  474. search.search_in_viewport = false;
  475. search.index += (ev.shiftKey || code == keycodes.p) ? -1 : +1;
  476. processSearchWithOptions(true);
  477. showSearchBox(search, clearSearchBox);
  478. } else {
  479. return;
  480. }
  481. stopEvent(ev);
  482. }, false);
  483. doc.addEventListener('keypress', function(ev) {
  484. if (isInputElementActive(doc))
  485. return;
  486. var blacklisted = check_blacklist(options.sites_blacklist);
  487. if (blacklisted)
  488. return;
  489. var code = ev.keyCode;
  490. var ascii = String.fromCharCode(code);
  491. if (!is_shortcut(ev) && ascii && [keycodes.enter].indexOf(code) == -1 &&
  492. (code != keycodes.spacebar || search.mode)) {
  493. if (!search.mode && ascii == "/") {
  494. search.mode = 'text';
  495. } else if (!search.mode && ascii == "'") {
  496. search.mode = 'links';
  497. } else if (!search.mode && blacklisted) {
  498. return;
  499. } else if (!search.mode && options.direct_search_mode == 'disabled') {
  500. return;
  501. } else if (!search.mode) {
  502. search.mode = options.direct_search_mode == 'links' ? 'links' : 'text';
  503. search.text += ascii;
  504. } else if (search.mode) {
  505. if (isSearchBoxVisible()) {
  506. search.text += ascii;
  507. } else {
  508. search.text = ascii;
  509. }
  510. }
  511. processSearchWithOptions(true)
  512. showSearchBox(search, clearSearchBox);
  513. stopEvent(ev)
  514. }
  515. }, false);
  516. doc.addEventListener('mousedown', function(ev) {
  517. if (search.mode)
  518. clearSearch(false);
  519. }, false);
  520. }
  521. clearSearch(false);
  522. var rootNodes = [document].concat(getRootNodes());
  523. for (var i = 0; i < rootNodes.length; i++) {
  524. var rootNode = rootNodes[i];
  525. if (rootNode.contentDocument) {
  526. rootNode.addEventListener('load', function(ev) {
  527. setEvents(ev.target.contentDocument);
  528. });
  529. }
  530. else if (!rootNode.contentDocument || rootNode.contentDocument.readyState == 'complete') {
  531. setEvents(rootNode.contentDocument ? rootNode.contentDocument : rootNode);
  532. }
  533. }
  534. }
  535. /* Default options */
  536. var default_options = {
  537. direct_search_mode: 'text',
  538. starts_link_only: false,
  539. sites_blacklist: '',
  540. color_link: '#DDF',
  541. color_text: '#FF5',
  542. color_notfound: 'F55'
  543. };
  544. function main(options) {
  545. // Use setInterval to add events to document.body as soon as possible
  546. interval_id = setInterval(function() {
  547. if (document.body) {
  548. clearInterval(interval_id);
  549. init(options);
  550. }
  551. }, 100);
  552. }
  553. setAlternativeActiveDocument(document);
  554. options = default_options;
  555. if (typeof(chrome) == "object" && chrome.extension) {
  556. chrome.extension.sendRequest({'get_options': true}, function(response) {
  557. options = {
  558. direct_search_mode: response.direct_search_mode,
  559. sites_blacklist: response.sites_blacklist,
  560. starts_link_only: (response.starts_link_only == '1'),
  561. timeout: response.timeout,
  562. color_link: response.color_link || default_options.color_link,
  563. color_text: response.color_text || default_options.color_text,
  564. color_notfound: response.color_notfound || default_options.color_notfound
  565. };
  566. main(options);
  567. });
  568. } else {
  569. main(options);
  570. }