www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

zotero-autocomplete.js (10704B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 const ZOTERO_AC_CONTRACTID = '@mozilla.org/autocomplete/search;1?name=zotero';
     27 const ZOTERO_AC_CLASSNAME = 'Zotero AutoComplete';
     28 const ZOTERO_AC_CID = Components.ID('{06a2ed11-d0a4-4ff0-a56f-a44545eee6ea}');
     29 
     30 const Cc = Components.classes;
     31 const Ci = Components.interfaces;
     32 const Cr = Components.results;
     33 
     34 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
     35 
     36 var Zotero = Components.classes["@zotero.org/Zotero;1"]
     37 	.getService(Components.interfaces.nsISupports)
     38 	.wrappedJSObject;
     39 
     40 /*
     41  * Implements nsIAutoCompleteSearch
     42  */
     43 function ZoteroAutoComplete() {}
     44 
     45 ZoteroAutoComplete.prototype.startSearch = Zotero.Promise.coroutine(function* (searchString, searchParams, previousResult, listener) {
     46 	// FIXME
     47 	//this.stopSearch();
     48 	
     49 	var result = Cc["@mozilla.org/autocomplete/simple-result;1"]
     50 					.createInstance(Ci.nsIAutoCompleteSimpleResult);
     51 	result.setSearchString(searchString);
     52 	
     53 	this._result = result;
     54 	this._results = [];
     55 	this._listener = listener;
     56 	this._cancelled = false;
     57 	
     58 	Zotero.debug("Starting autocomplete search with data '"
     59 		+ searchParams + "'" + " and string '" + searchString + "'");
     60 	
     61 	searchParams = JSON.parse(searchParams);
     62 	if (!searchParams) {
     63 		throw new Error("Invalid JSON passed to autocomplete");
     64 	}
     65 	var [fieldName, , subField] = searchParams.fieldName.split("-");
     66 	
     67 	var resultsCallback;
     68 	
     69 	switch (fieldName) {
     70 		case '':
     71 			break;
     72 		
     73 		case 'tag':
     74 			var sql = "SELECT DISTINCT name AS val, NULL AS comment FROM tags WHERE name LIKE ?";
     75 			var sqlParams = [searchString + '%'];
     76 			if (searchParams.libraryID !== undefined) {
     77 				sql += " AND tagID IN (SELECT tagID FROM itemTags JOIN items USING (itemID) "
     78 					+ "WHERE libraryID=?)";
     79 				sqlParams.push(searchParams.libraryID);
     80 			}
     81 			if (searchParams.itemID) {
     82 				sql += " AND name NOT IN (SELECT name FROM tags WHERE tagID IN ("
     83 					+ "SELECT tagID FROM itemTags WHERE itemID = ?))";
     84 				sqlParams.push(searchParams.itemID);
     85 			}
     86 			sql += " ORDER BY val COLLATE locale";
     87 			break;
     88 		
     89 		case 'creator':
     90 			// Valid fieldMode values:
     91 			// 		0 == search two-field creators
     92 			// 		1 == search single-field creators
     93 			// 		2 == search both
     94 			if (searchParams.fieldMode == 2) {
     95 				var sql = "SELECT DISTINCT CASE fieldMode WHEN 1 THEN lastName "
     96 					+ "WHEN 0 THEN firstName || ' ' || lastName END AS val, NULL AS comment "
     97 					+ "FROM creators ";
     98 				if (searchParams.libraryID !== undefined) {
     99 					sql += "JOIN itemCreators USING (creatorID) JOIN items USING (itemID) ";
    100 				}
    101 				sql += "WHERE CASE fieldMode "
    102 					+ "WHEN 1 THEN lastName LIKE ? "
    103 					+ "WHEN 0 THEN (firstName || ' ' || lastName LIKE ?) OR (lastName LIKE ?) END "
    104 				var sqlParams = [searchString + '%', searchString + '%', searchString + '%'];
    105 				if (searchParams.libraryID !== undefined) {
    106 					sql += " AND libraryID=?";
    107 					sqlParams.push(searchParams.libraryID);
    108 				}
    109 				sql += "ORDER BY val";
    110 			}
    111 			else
    112 			{
    113 				var sql = "SELECT DISTINCT ";
    114 				if (searchParams.fieldMode == 1) {
    115 					sql += "lastName AS val, creatorID || '-1' AS comment";
    116 				}
    117 				// Retrieve the matches in the specified field
    118 				// as well as any full names using the name
    119 				//
    120 				// e.g. "Shakespeare" and "Shakespeare, William"
    121 				//
    122 				// creatorID is in the format "12345-1" or "12345-2",
    123 				// 		- 1 means the row uses only the specified field
    124 				// 		- 2 means it uses both
    125 				else {
    126 					sql += "CASE WHEN firstName='' OR firstName IS NULL THEN lastName "
    127 						+ "ELSE lastName || ', ' || firstName END AS val, "
    128 						+ "creatorID || '-' || CASE "
    129 						+ "WHEN (firstName = '' OR firstName IS NULL) THEN 1 "
    130 						+ "ELSE 2 END AS comment";
    131 				}
    132 				
    133 				var fromSQL = " FROM creators "
    134 				if (searchParams.libraryID !== undefined) {
    135 					fromSQL += "JOIN itemCreators USING (creatorID) JOIN items USING (itemID) ";
    136 				}
    137 				fromSQL += "WHERE " + subField + " LIKE ? " + "AND fieldMode=?";
    138 				var sqlParams = [
    139 					searchString + '%',
    140 					searchParams.fieldMode ? searchParams.fieldMode : 0
    141 				];
    142 				if (searchParams.itemID) {
    143 					fromSQL += " AND creatorID NOT IN (SELECT creatorID FROM "
    144 						+ "itemCreators WHERE itemID=?";
    145 					sqlParams.push(searchParams.itemID);
    146 					if (searchParams.creatorTypeID) {
    147 						fromSQL += " AND creatorTypeID=?";
    148 						sqlParams.push(searchParams.creatorTypeID);
    149 					}
    150 					fromSQL += ")";
    151 				}
    152 				if (searchParams.libraryID !== undefined) {
    153 					fromSQL += " AND libraryID=?";
    154 					sqlParams.push(searchParams.libraryID);
    155 				}
    156 				
    157 				sql += fromSQL;
    158 				
    159 				// If double-field mode, include matches for just this field
    160 				// as well (i.e. "Shakespeare"), and group to collapse repeats
    161 				if (searchParams.fieldMode != 1) {
    162 					sql = "SELECT * FROM (" + sql + " UNION SELECT DISTINCT "
    163 						+ subField + " AS val, creatorID || '-1' AS comment"
    164 						+ fromSQL + ") GROUP BY val";
    165 					sqlParams = sqlParams.concat(sqlParams);
    166 				}
    167 				
    168 				sql += " ORDER BY val";
    169 			}
    170 			break;
    171 		
    172 		case 'dateModified':
    173 		case 'dateAdded':
    174 			var sql = "SELECT DISTINCT DATE(" + fieldName + ", 'localtime') AS val, NULL AS comment FROM items "
    175 				+ "WHERE " + fieldName + " LIKE ? ORDER BY " + fieldName;
    176 			var sqlParams = [searchString + '%'];
    177 			break;
    178 			
    179 		case 'accessDate':
    180 			var fieldID = Zotero.ItemFields.getID('accessDate');
    181 			
    182 			var sql = "SELECT DISTINCT DATE(value, 'localtime') AS val, NULL AS comment FROM itemData "
    183 				+ "WHERE fieldID=? AND value LIKE ? ORDER BY value";
    184 			var sqlParams = [fieldID, searchString + '%'];
    185 			break;
    186 		
    187 		default:
    188 			var fieldID = Zotero.ItemFields.getID(fieldName);
    189 			if (!fieldID) {
    190 				Zotero.debug("'" + fieldName + "' is not a valid autocomplete scope", 1);
    191 				this.updateResults([], false, Ci.nsIAutoCompleteResult.RESULT_IGNORED);
    192 				return;
    193 			}
    194 			
    195 			// We don't use date autocomplete anywhere, but if we're not
    196 			// disallowing it altogether, we should at least do it right and
    197 			// use the user part of the multipart field
    198 			var valueField = fieldName == 'date' ? 'SUBSTR(value, 12, 100)' : 'value';
    199 			
    200 			var sql = "SELECT DISTINCT " + valueField + " AS val, NULL AS comment "
    201 				+ "FROM itemData NATURAL JOIN itemDataValues "
    202 				+ "WHERE fieldID=?1 AND " + valueField
    203 				+ " LIKE ?2 "
    204 			
    205 			var sqlParams = [fieldID, searchString + '%'];
    206 			if (searchParams.itemID) {
    207 				sql += "AND value NOT IN (SELECT value FROM itemData "
    208 					+ "NATURAL JOIN itemDataValues WHERE fieldID=?1 AND itemID=?3) ";
    209 				sqlParams.push(searchParams.itemID);
    210 			}
    211 			sql += "ORDER BY value";
    212 	}
    213 	
    214 	var onRow = null;
    215 	// If there's a result callback (e.g., for sorting), don't use a row handler
    216 	if (!resultsCallback) {
    217 		onRow = function (row) {
    218 			if (this._cancelled) {
    219 				Zotero.debug("Cancelling query");
    220 				throw StopIteration;
    221 			}
    222 			var result = row.getResultByIndex(0);
    223 			var comment = row.getResultByIndex(1);
    224 			this.updateResult(result, comment, true);
    225 		}.bind(this);
    226 	}
    227 	var resultCode;
    228 	try {
    229 		let results = yield Zotero.DB.queryAsync(sql, sqlParams, { onRow: onRow });
    230 		// Post-process the results
    231 		if (resultsCallback) {
    232 			resultsCallback(results);
    233 			this.updateResults(
    234 				Object.values(results).map(x => x.val),
    235 				Object.values(results).map(x => x.comment),
    236 				false
    237 			);
    238 		}
    239 		resultCode = null;
    240 		Zotero.debug("Autocomplete query completed");
    241 	}
    242 	catch (e) {
    243 		Zotero.debug(e, 1);
    244 		resultCode = Ci.nsIAutoCompleteResult.RESULT_FAILURE;
    245 		Zotero.debug("Autocomplete query aborted");
    246 	}
    247 	finally {
    248 		this.updateResults(null, null, false, resultCode);
    249 	};
    250 });
    251 
    252 
    253 ZoteroAutoComplete.prototype.updateResult = function (result, comment) {
    254 	Zotero.debug("Appending autocomplete value '" + result + "'" + (comment ? " (" + comment + ")" : ""));
    255 	// Add to nsIAutoCompleteResult
    256 	this._result.appendMatch(result, comment ? comment : null);
    257 	// Add to our own list
    258 	this._results.push(result);
    259 	// Only update the UI every 10 records
    260 	if (this._result.matchCount % 10 == 0) {
    261 		this._result.setSearchResult(Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING);
    262 		this._listener.onUpdateSearchResult(this, this._result);
    263 	}
    264 }
    265 
    266 
    267 ZoteroAutoComplete.prototype.updateResults = function (results, comments, ongoing, resultCode) {
    268 	if (!results) {
    269 		results = [];
    270 	}
    271 	if (!comments) {
    272 		comments = [];
    273 	}
    274 	
    275 	for (var i=0; i<results.length; i++) {
    276 		let result = results[i];
    277 		
    278 		if (this._results.indexOf(result) == -1) {
    279 			comment = comments[i] ? comments[i] : null;
    280 			Zotero.debug("Adding autocomplete value '" + result + "'" + (comment ? " (" + comment + ")" : ""));
    281 			this._result.appendMatch(result, comment, null, null);
    282 			this._results.push(result);
    283 		}
    284 		else {
    285 			//Zotero.debug("Skipping existing value '" + result + "'");
    286 		}
    287 	}
    288 	
    289 	if (!resultCode) {
    290 		resultCode = "RESULT_";
    291 		if (!this._result.matchCount) {
    292 			resultCode += "NOMATCH";
    293 		}
    294 		else {
    295 			resultCode += "SUCCESS";
    296 		}
    297 		if (ongoing) {
    298 			resultCode += "_ONGOING";
    299 		}
    300 		resultCode = Ci.nsIAutoCompleteResult[resultCode];
    301 	}
    302 	
    303 	Zotero.debug("Found " + this._result.matchCount
    304 		+ " result" + (this._result.matchCount != 1 ? "s" : ""));
    305 	
    306 	this._result.setSearchResult(resultCode);
    307 	this._listener.onSearchResult(this, this._result);
    308 }
    309 
    310 
    311 // FIXME
    312 ZoteroAutoComplete.prototype.stopSearch = function(){
    313 	Zotero.debug('Stopping autocomplete search');
    314 	this._cancelled = true;
    315 }
    316 
    317 //
    318 // XPCOM goop
    319 //
    320 
    321 ZoteroAutoComplete.prototype.classDescription = ZOTERO_AC_CLASSNAME;
    322 ZoteroAutoComplete.prototype.classID = ZOTERO_AC_CID;
    323 ZoteroAutoComplete.prototype.contractID = ZOTERO_AC_CONTRACTID;
    324 ZoteroAutoComplete.prototype.QueryInterface = XPCOMUtils.generateQI([
    325 	Components.interfaces.nsIAutoCompleteSearch,
    326 	Components.interfaces.nsIAutoCompleteObserver,
    327 	Components.interfaces.nsISupports]);
    328 
    329 var NSGetFactory = XPCOMUtils.generateNSGetFactory([ZoteroAutoComplete]);