A search interface for the Performing Patents Otherwise publication as part of the Politics of Patents case study (part of Copim WP6): this parses data from the archive of RTF files and provides additional data from the European Patent Office OPS API. https://patents.copim.ac.uk
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

763 lines
21KB

  1. /*
  2. * Autocomplete - jQuery plugin 1.1pre
  3. *
  4. * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
  5. *
  6. * Dual licensed under the MIT and GPL licenses:
  7. * http://www.opensource.org/licenses/mit-license.php
  8. * http://www.gnu.org/licenses/gpl.html
  9. *
  10. * Revision: Id: jquery.autocomplete.js 5785 2008-07-12 10:37:33Z joern.zaefferer $
  11. *
  12. */
  13. ;(function($) {
  14. $.fn.extend({
  15. autocomplete: function(urlOrData, options) {
  16. var isUrl = typeof urlOrData == "string";
  17. options = $.extend({}, $.Autocompleter.defaults, {
  18. url: isUrl ? urlOrData : null,
  19. data: isUrl ? null : urlOrData,
  20. delay: isUrl ? $.Autocompleter.defaults.delay : 10,
  21. max: options && !options.scroll ? 10 : 150
  22. }, options);
  23. // if highlight is set to false, replace it with a do-nothing function
  24. options.highlight = options.highlight || function(value) { return value; };
  25. // if the formatMatch option is not specified, then use formatItem for backwards compatibility
  26. options.formatMatch = options.formatMatch || options.formatItem;
  27. return this.each(function() {
  28. new $.Autocompleter(this, options);
  29. });
  30. },
  31. result: function(handler) {
  32. return this.bind("result", handler);
  33. },
  34. search: function(handler) {
  35. return this.trigger("search", [handler]);
  36. },
  37. flushCache: function() {
  38. return this.trigger("flushCache");
  39. },
  40. setOptions: function(options){
  41. return this.trigger("setOptions", [options]);
  42. },
  43. unautocomplete: function() {
  44. return this.trigger("unautocomplete");
  45. }
  46. });
  47. $.Autocompleter = function(input, options) {
  48. var KEY = {
  49. UP: 38,
  50. DOWN: 40,
  51. DEL: 46,
  52. TAB: 9,
  53. RETURN: 13,
  54. ESC: 27,
  55. COMMA: 188,
  56. PAGEUP: 33,
  57. PAGEDOWN: 34,
  58. BACKSPACE: 8
  59. };
  60. // Create $ object for input element
  61. var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
  62. var timeout;
  63. var previousValue = "";
  64. var cache = $.Autocompleter.Cache(options);
  65. var hasFocus = 0;
  66. var lastKeyPressCode;
  67. var config = {
  68. mouseDownOnSelect: false
  69. };
  70. var select = $.Autocompleter.Select(options, input, selectCurrent, config);
  71. var blockSubmit;
  72. // prevent form submit in opera when selecting with return key
  73. $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
  74. if (blockSubmit) {
  75. blockSubmit = false;
  76. return false;
  77. }
  78. });
  79. // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
  80. $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
  81. // track last key pressed
  82. lastKeyPressCode = event.keyCode;
  83. switch(event.keyCode) {
  84. case KEY.UP:
  85. event.preventDefault();
  86. if ( select.visible() ) {
  87. select.prev();
  88. } else {
  89. onChange(0, true);
  90. }
  91. break;
  92. case KEY.DOWN:
  93. event.preventDefault();
  94. if ( select.visible() ) {
  95. select.next();
  96. } else {
  97. onChange(0, true);
  98. }
  99. break;
  100. case KEY.PAGEUP:
  101. event.preventDefault();
  102. if ( select.visible() ) {
  103. select.pageUp();
  104. } else {
  105. onChange(0, true);
  106. }
  107. break;
  108. case KEY.PAGEDOWN:
  109. event.preventDefault();
  110. if ( select.visible() ) {
  111. select.pageDown();
  112. } else {
  113. onChange(0, true);
  114. }
  115. break;
  116. // matches also semicolon
  117. case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
  118. case KEY.TAB:
  119. case KEY.RETURN:
  120. if( selectCurrent() ) {
  121. // stop default to prevent a form submit, Opera needs special handling
  122. event.preventDefault();
  123. blockSubmit = true;
  124. return false;
  125. }
  126. break;
  127. case KEY.ESC:
  128. select.hide();
  129. break;
  130. default:
  131. clearTimeout(timeout);
  132. timeout = setTimeout(onChange, options.delay);
  133. break;
  134. }
  135. }).focus(function(){
  136. // track whether the field has focus, we shouldn't process any
  137. // results if the field no longer has focus
  138. hasFocus++;
  139. }).blur(function() {
  140. hasFocus = 0;
  141. if (!config.mouseDownOnSelect) {
  142. hideResults();
  143. }
  144. }).click(function() {
  145. // show select when clicking in a focused field
  146. if ( hasFocus++ > 1 && !select.visible() ) {
  147. onChange(0, true);
  148. }
  149. }).bind("search", function() {
  150. // TODO why not just specifying both arguments?
  151. var fn = (arguments.length > 1) ? arguments[1] : null;
  152. function findValueCallback(q, data) {
  153. var result;
  154. if( data && data.length ) {
  155. for (var i=0; i < data.length; i++) {
  156. if( data[i].result.toLowerCase() == q.toLowerCase() ) {
  157. result = data[i];
  158. break;
  159. }
  160. }
  161. }
  162. if( typeof fn == "function" ) fn(result);
  163. else $input.trigger("result", result && [result.data, result.value]);
  164. }
  165. $.each(trimWords($input.val()), function(i, value) {
  166. request(value, findValueCallback, findValueCallback);
  167. });
  168. }).bind("flushCache", function() {
  169. cache.flush();
  170. }).bind("setOptions", function() {
  171. $.extend(options, arguments[1]);
  172. // if we've updated the data, repopulate
  173. if ( "data" in arguments[1] )
  174. cache.populate();
  175. }).bind("unautocomplete", function() {
  176. select.unbind();
  177. $input.unbind();
  178. $(input.form).unbind(".autocomplete");
  179. });
  180. function selectCurrent() {
  181. var selected = select.selected();
  182. if( !selected )
  183. return false;
  184. var v = selected.result;
  185. previousValue = v;
  186. if ( options.multiple ) {
  187. var words = trimWords($input.val());
  188. if ( words.length > 1 ) {
  189. v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
  190. }
  191. v += options.multipleSeparator;
  192. }
  193. $input.val(v);
  194. hideResultsNow();
  195. $input.trigger("result", [selected.data, selected.value]);
  196. return true;
  197. }
  198. function onChange(crap, skipPrevCheck) {
  199. if( lastKeyPressCode == KEY.DEL ) {
  200. select.hide();
  201. return;
  202. }
  203. var currentValue = $input.val();
  204. if ( !skipPrevCheck && currentValue == previousValue )
  205. return;
  206. previousValue = currentValue;
  207. currentValue = lastWord(currentValue);
  208. if ( currentValue.length >= options.minChars) {
  209. $input.addClass(options.loadingClass);
  210. if (!options.matchCase)
  211. currentValue = currentValue.toLowerCase();
  212. request(currentValue, receiveData, hideResultsNow);
  213. } else {
  214. stopLoading();
  215. select.hide();
  216. }
  217. };
  218. function trimWords(value) {
  219. if ( !value ) {
  220. return [""];
  221. }
  222. var words = value.split( options.multipleSeparator );
  223. var result = [];
  224. $.each(words, function(i, value) {
  225. if ( $.trim(value) )
  226. result[i] = $.trim(value);
  227. });
  228. return result;
  229. }
  230. function lastWord(value) {
  231. if ( !options.multiple )
  232. return value;
  233. var words = trimWords(value);
  234. return words[words.length - 1];
  235. }
  236. // fills in the input box w/the first match (assumed to be the best match)
  237. // q: the term entered
  238. // sValue: the first matching result
  239. function autoFill(q, sValue){
  240. // autofill in the complete box w/the first match as long as the user hasn't entered in more data
  241. // if the last user key pressed was backspace, don't autofill
  242. if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
  243. // fill in the value (keep the case the user has typed)
  244. $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
  245. // select the portion of the value not typed by the user (so the next character will erase)
  246. $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
  247. }
  248. };
  249. function hideResults() {
  250. clearTimeout(timeout);
  251. timeout = setTimeout(hideResultsNow, 200);
  252. };
  253. function hideResultsNow() {
  254. var wasVisible = select.visible();
  255. select.hide();
  256. clearTimeout(timeout);
  257. stopLoading();
  258. if (options.mustMatch) {
  259. // call search and run callback
  260. $input.search(
  261. function (result){
  262. // if no value found, clear the input box
  263. if( !result ) {
  264. if (options.multiple) {
  265. var words = trimWords($input.val()).slice(0, -1);
  266. $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
  267. }
  268. else
  269. $input.val( "" );
  270. }
  271. }
  272. );
  273. }
  274. if (wasVisible)
  275. // position cursor at end of input field
  276. $.Autocompleter.Selection(input, input.value.length, input.value.length);
  277. };
  278. function receiveData(q, data) {
  279. if ( data && data.length && hasFocus ) {
  280. stopLoading();
  281. select.display(data, q);
  282. autoFill(q, data[0].value);
  283. select.show();
  284. } else {
  285. hideResultsNow();
  286. }
  287. };
  288. function request(term, success, failure) {
  289. if (!options.matchCase)
  290. term = term.toLowerCase();
  291. var data = cache.load(term);
  292. data = null; // Avoid buggy cache and go to Solr every time
  293. // recieve the cached data
  294. if (data && data.length) {
  295. success(term, data);
  296. // if an AJAX url has been supplied, try loading the data now
  297. } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  298. var extraParams = {
  299. timestamp: +new Date()
  300. };
  301. $.each(options.extraParams, function(key, param) {
  302. extraParams[key] = typeof param == "function" ? param() : param;
  303. });
  304. $.ajax({
  305. // try to leverage ajaxQueue plugin to abort previous requests
  306. mode: "abort",
  307. // limit abortion to this input
  308. port: "autocomplete" + input.name,
  309. dataType: options.dataType,
  310. url: options.url,
  311. data: $.extend({
  312. q: lastWord(term),
  313. limit: options.max
  314. }, extraParams),
  315. success: function(data) {
  316. var parsed = options.parse && options.parse(data) || parse(data);
  317. cache.add(term, parsed);
  318. success(term, parsed);
  319. }
  320. });
  321. } else {
  322. // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
  323. select.emptyList();
  324. failure(term);
  325. }
  326. };
  327. function parse(data) {
  328. var parsed = [];
  329. var rows = data.split("\n");
  330. for (var i=0; i < rows.length; i++) {
  331. var row = $.trim(rows[i]);
  332. if (row) {
  333. row = row.split("|");
  334. parsed[parsed.length] = {
  335. data: row,
  336. value: row[0],
  337. result: options.formatResult && options.formatResult(row, row[0]) || row[0]
  338. };
  339. }
  340. }
  341. return parsed;
  342. };
  343. function stopLoading() {
  344. $input.removeClass(options.loadingClass);
  345. };
  346. };
  347. $.Autocompleter.defaults = {
  348. inputClass: "ac_input",
  349. resultsClass: "ac_results",
  350. loadingClass: "ac_loading",
  351. minChars: 1,
  352. delay: 400,
  353. matchCase: false,
  354. matchSubset: true,
  355. matchContains: false,
  356. cacheLength: 10,
  357. max: 100,
  358. mustMatch: false,
  359. extraParams: {},
  360. selectFirst: false,
  361. formatItem: function(row) { return row[0]; },
  362. formatMatch: null,
  363. autoFill: false,
  364. width: 0,
  365. multiple: false,
  366. multipleSeparator: ", ",
  367. highlight: function(value, term) {
  368. return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  369. },
  370. scroll: true,
  371. scrollHeight: 180
  372. };
  373. $.Autocompleter.Cache = function(options) {
  374. var data = {};
  375. var length = 0;
  376. function matchSubset(s, sub) {
  377. if (!options.matchCase)
  378. s = s.toLowerCase();
  379. var i = s.indexOf(sub);
  380. if (options.matchContains == "word"){
  381. i = s.toLowerCase().search("\\b" + sub.toLowerCase());
  382. }
  383. if (i == -1) return false;
  384. return i == 0 || options.matchContains;
  385. };
  386. function add(q, value) {
  387. if (length > options.cacheLength){
  388. flush();
  389. }
  390. if (!data[q]){
  391. length++;
  392. }
  393. data[q] = value;
  394. }
  395. function populate(){
  396. if( !options.data ) return false;
  397. // track the matches
  398. var stMatchSets = {},
  399. nullData = 0;
  400. // no url was specified, we need to adjust the cache length to make sure it fits the local data store
  401. if( !options.url ) options.cacheLength = 1;
  402. // track all options for minChars = 0
  403. stMatchSets[""] = [];
  404. // loop through the array and create a lookup structure
  405. for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
  406. var rawValue = options.data[i];
  407. // if rawValue is a string, make an array otherwise just reference the array
  408. rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
  409. var value = options.formatMatch(rawValue, i+1, options.data.length);
  410. if ( value === false )
  411. continue;
  412. var firstChar = value.charAt(0).toLowerCase();
  413. // if no lookup array for this character exists, look it up now
  414. if( !stMatchSets[firstChar] )
  415. stMatchSets[firstChar] = [];
  416. // if the match is a string
  417. var row = {
  418. value: value,
  419. data: rawValue,
  420. result: options.formatResult && options.formatResult(rawValue) || value
  421. };
  422. // push the current match into the set list
  423. stMatchSets[firstChar].push(row);
  424. // keep track of minChars zero items
  425. if ( nullData++ < options.max ) {
  426. stMatchSets[""].push(row);
  427. }
  428. };
  429. // add the data items to the cache
  430. $.each(stMatchSets, function(i, value) {
  431. // increase the cache size
  432. options.cacheLength++;
  433. // add to the cache
  434. add(i, value);
  435. });
  436. }
  437. // populate any existing data
  438. setTimeout(populate, 25);
  439. function flush(){
  440. data = {};
  441. length = 0;
  442. }
  443. return {
  444. flush: flush,
  445. add: add,
  446. populate: populate,
  447. load: function(q) {
  448. if (!options.cacheLength || !length)
  449. return null;
  450. /*
  451. * if dealing w/local data and matchContains than we must make sure
  452. * to loop through all the data collections looking for matches
  453. */
  454. if( !options.url && options.matchContains ){
  455. // track all matches
  456. var csub = [];
  457. // loop through all the data grids for matches
  458. for( var k in data ){
  459. // don't search through the stMatchSets[""] (minChars: 0) cache
  460. // this prevents duplicates
  461. if( k.length > 0 ){
  462. var c = data[k];
  463. $.each(c, function(i, x) {
  464. // if we've got a match, add it to the array
  465. if (matchSubset(x.value, q)) {
  466. csub.push(x);
  467. }
  468. });
  469. }
  470. }
  471. return csub;
  472. } else
  473. // if the exact item exists, use it
  474. if (data[q]){
  475. return data[q];
  476. } else
  477. if (options.matchSubset) {
  478. for (var i = q.length - 1; i >= options.minChars; i--) {
  479. var c = data[q.substr(0, i)];
  480. if (c) {
  481. var csub = [];
  482. $.each(c, function(i, x) {
  483. if (matchSubset(x.value, q)) {
  484. csub[csub.length] = x;
  485. }
  486. });
  487. return csub;
  488. }
  489. }
  490. }
  491. return null;
  492. }
  493. };
  494. };
  495. $.Autocompleter.Select = function (options, input, select, config) {
  496. var CLASSES = {
  497. ACTIVE: "ac_over"
  498. };
  499. var listItems,
  500. active = -1,
  501. data,
  502. term = "",
  503. needsInit = true,
  504. element,
  505. list;
  506. // Create results
  507. function init() {
  508. if (!needsInit)
  509. return;
  510. element = $("<div/>")
  511. .hide()
  512. .addClass(options.resultsClass)
  513. .css("position", "absolute")
  514. .appendTo(document.body);
  515. list = $("<ul/>").appendTo(element).mouseover( function(event) {
  516. if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
  517. active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
  518. $(target(event)).addClass(CLASSES.ACTIVE);
  519. }
  520. }).click(function(event) {
  521. $(target(event)).addClass(CLASSES.ACTIVE);
  522. select();
  523. // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
  524. input.focus();
  525. return false;
  526. }).mousedown(function() {
  527. config.mouseDownOnSelect = true;
  528. }).mouseup(function() {
  529. config.mouseDownOnSelect = false;
  530. });
  531. if( options.width > 0 )
  532. element.css("width", options.width);
  533. needsInit = false;
  534. }
  535. function target(event) {
  536. var element = event.target;
  537. while(element && element.tagName != "LI")
  538. element = element.parentNode;
  539. // more fun with IE, sometimes event.target is empty, just ignore it then
  540. if(!element)
  541. return [];
  542. return element;
  543. }
  544. function moveSelect(step) {
  545. listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
  546. movePosition(step);
  547. var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
  548. if(options.scroll) {
  549. var offset = 0;
  550. listItems.slice(0, active).each(function() {
  551. offset += this.offsetHeight;
  552. });
  553. if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
  554. list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
  555. } else if(offset < list.scrollTop()) {
  556. list.scrollTop(offset);
  557. }
  558. }
  559. };
  560. function movePosition(step) {
  561. active += step;
  562. if (active < 0) {
  563. active = listItems.size() - 1;
  564. } else if (active >= listItems.size()) {
  565. active = 0;
  566. }
  567. }
  568. function limitNumberOfItems(available) {
  569. return options.max && options.max < available
  570. ? options.max
  571. : available;
  572. }
  573. function fillList() {
  574. list.empty();
  575. var max = limitNumberOfItems(data.length);
  576. for (var i=0; i < max; i++) {
  577. if (!data[i])
  578. continue;
  579. var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
  580. if ( formatted === false )
  581. continue;
  582. var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
  583. $.data(li, "ac_data", data[i]);
  584. }
  585. listItems = list.find("li");
  586. if ( options.selectFirst ) {
  587. listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
  588. active = 0;
  589. }
  590. // apply bgiframe if available
  591. if ( $.fn.bgiframe )
  592. list.bgiframe();
  593. }
  594. return {
  595. display: function(d, q) {
  596. init();
  597. data = d;
  598. term = q;
  599. fillList();
  600. },
  601. next: function() {
  602. moveSelect(1);
  603. },
  604. prev: function() {
  605. moveSelect(-1);
  606. },
  607. pageUp: function() {
  608. if (active != 0 && active - 8 < 0) {
  609. moveSelect( -active );
  610. } else {
  611. moveSelect(-8);
  612. }
  613. },
  614. pageDown: function() {
  615. if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
  616. moveSelect( listItems.size() - 1 - active );
  617. } else {
  618. moveSelect(8);
  619. }
  620. },
  621. hide: function() {
  622. element && element.hide();
  623. listItems && listItems.removeClass(CLASSES.ACTIVE);
  624. active = -1;
  625. },
  626. visible : function() {
  627. return element && element.is(":visible");
  628. },
  629. current: function() {
  630. return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
  631. },
  632. show: function() {
  633. var offset = $(input).offset();
  634. element.css({
  635. width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
  636. top: offset.top + input.offsetHeight,
  637. left: offset.left
  638. }).show();
  639. if(options.scroll) {
  640. list.scrollTop(0);
  641. list.css({
  642. maxHeight: options.scrollHeight,
  643. overflow: 'auto'
  644. });
  645. if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
  646. var listHeight = 0;
  647. listItems.each(function() {
  648. listHeight += this.offsetHeight;
  649. });
  650. var scrollbarsVisible = listHeight > options.scrollHeight;
  651. list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
  652. if (!scrollbarsVisible) {
  653. // IE doesn't recalculate width when scrollbar disappears
  654. listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
  655. }
  656. }
  657. }
  658. },
  659. selected: function() {
  660. var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
  661. return selected && selected.length && $.data(selected[0], "ac_data");
  662. },
  663. emptyList: function (){
  664. list && list.empty();
  665. },
  666. unbind: function() {
  667. element && element.remove();
  668. }
  669. };
  670. };
  671. $.Autocompleter.Selection = function(field, start, end) {
  672. if( field.createTextRange ){
  673. var selRange = field.createTextRange();
  674. selRange.collapse(true);
  675. selRange.moveStart("character", start);
  676. selRange.moveEnd("character", end);
  677. selRange.select();
  678. } else if( field.setSelectionRange ){
  679. field.setSelectionRange(start, end);
  680. } else {
  681. if( field.selectionStart ){
  682. field.selectionStart = start;
  683. field.selectionEnd = end;
  684. }
  685. }
  686. field.focus();
  687. };
  688. })(jQuery);