www

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

searchConditions.js (13848B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2006-2016 Center for History and New Media
      5                           George Mason University, Fairfax, Virginia, USA
      6                           https://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 Zotero.SearchConditions = new function(){
     27 	this.get = get;
     28 	this.getStandardConditions = getStandardConditions;
     29 	this.hasOperator = hasOperator;
     30 	this.getLocalizedName = getLocalizedName;
     31 	this.parseSearchString = parseSearchString;
     32 	this.parseCondition = parseCondition;
     33 	
     34 	var _initialized = false;
     35 	var _conditions;
     36 	var _standardConditions;
     37 	
     38 	var self = this;
     39 	
     40 	/*
     41 	 * Define the advanced search operators
     42 	 */
     43 	var _operators = {
     44 		// Standard -- these need to match those in zoterosearch.xml
     45 		is: true,
     46 		isNot: true,
     47 		beginsWith: true,
     48 		contains: true,
     49 		doesNotContain: true,
     50 		isLessThan: true,
     51 		isGreaterThan: true,
     52 		isBefore: true,
     53 		isAfter: true,
     54 		isInTheLast: true,
     55 		
     56 		// Special
     57 		any: true,
     58 		all: true,
     59 		true: true,
     60 		false: true
     61 	};
     62 	
     63 	
     64 	/*
     65 	 * Define and set up the available advanced search conditions
     66 	 *
     67 	 * Flags:
     68 	 *  - special (don't show in search window menu)
     69 	 *  - template (special handling)
     70 	 *  - noLoad (can't load from saved search)
     71 	 */
     72 	this.init = Zotero.Promise.coroutine(function* () {
     73 		var conditions = [
     74 			//
     75 			// Special conditions
     76 			//
     77 			{
     78 				name: 'deleted',
     79 				operators: {
     80 					true: true,
     81 					false: true
     82 				}
     83 			},
     84 			
     85 			// Don't include child items
     86 			{
     87 				name: 'noChildren',
     88 				operators: {
     89 					true: true,
     90 					false: true
     91 				}
     92 			},
     93 			
     94 			{
     95 				name: 'unfiled',
     96 				operators: {
     97 					true: true,
     98 					false: true
     99 				}
    100 			},
    101 			
    102 			{
    103 				name: 'publications',
    104 				operators: {
    105 					true: true,
    106 					false: true
    107 				}
    108 			},
    109 			
    110 			{
    111 				name: 'includeParentsAndChildren',
    112 				operators: {
    113 					true: true,
    114 					false: true
    115 				}
    116 			},
    117 			
    118 			{
    119 				name: 'includeParents',
    120 				operators: {
    121 					true: true,
    122 					false: true
    123 				}
    124 			},
    125 			
    126 			{
    127 				name: 'includeChildren',
    128 				operators: {
    129 					true: true,
    130 					false: true
    131 				}
    132 			},
    133 			
    134 			// Search recursively within collections
    135 			{
    136 				name: 'recursive',
    137 				operators: {
    138 					true: true,
    139 					false: true
    140 				}
    141 			},
    142 			
    143 			// Join mode
    144 			{
    145 				name: 'joinMode',
    146 				operators: {
    147 					any: true,
    148 					all: true
    149 				}
    150 			},
    151 			
    152 			{
    153 				name: 'quicksearch-titleCreatorYear',
    154 				operators: {
    155 					is: true,
    156 					isNot: true,
    157 					contains: true,
    158 					doesNotContain: true
    159 				},
    160 				noLoad: true
    161 			},
    162 			
    163 			{
    164 				name: 'quicksearch-fields',
    165 				operators: {
    166 					is: true,
    167 					isNot: true,
    168 					contains: true,
    169 					doesNotContain: true
    170 				},
    171 				noLoad: true
    172 			},
    173 			
    174 			{
    175 				name: 'quicksearch-everything',
    176 				operators: {
    177 					is: true,
    178 					isNot: true,
    179 					contains: true,
    180 					doesNotContain: true
    181 				},
    182 				noLoad: true
    183 			},
    184 			
    185 			// Deprecated
    186 			{
    187 				name: 'quicksearch',
    188 				operators: {
    189 					is: true,
    190 					isNot: true,
    191 					contains: true,
    192 					doesNotContain: true
    193 				},
    194 				noLoad: true
    195 			},
    196 			
    197 			// Quicksearch block markers
    198 			{
    199 				name: 'blockStart',
    200 				noLoad: true
    201 			},
    202 			
    203 			{
    204 				name: 'blockEnd',
    205 				noLoad: true
    206 			},
    207 			
    208 			// Shortcuts for adding collections and searches by id
    209 			{
    210 				name: 'collectionID',
    211 				operators: {
    212 					is: true,
    213 					isNot: true
    214 				},
    215 				noLoad: true
    216 			},
    217 			
    218 			{
    219 				name: 'savedSearchID',
    220 				operators: {
    221 					is: true,
    222 					isNot: true
    223 				},
    224 				noLoad: true
    225 			},
    226 			
    227 			
    228 			//
    229 			// Standard conditions
    230 			//
    231 			
    232 			// Collection id to search within
    233 			{
    234 				name: 'collection',
    235 				operators: {
    236 					is: true,
    237 					isNot: true
    238 				},
    239 				table: 'collectionItems',
    240 				field: 'collectionID'
    241 			},
    242 			
    243 			// Saved search to search within
    244 			{
    245 				name: 'savedSearch',
    246 				operators: {
    247 					is: true,
    248 					isNot: true
    249 				},
    250 				special: true
    251 			},
    252 			
    253 			{
    254 				name: 'dateAdded',
    255 				operators: {
    256 					is: true,
    257 					isNot: true,
    258 					isBefore: true,
    259 					isAfter: true,
    260 					isInTheLast: true
    261 				},
    262 				table: 'items',
    263 				field: 'dateAdded'
    264 			},
    265 			
    266 			{
    267 				name: 'dateModified',
    268 				operators: {
    269 					is: true,
    270 					isNot: true,
    271 					isBefore: true,
    272 					isAfter: true,
    273 					isInTheLast: true
    274 				},
    275 				table: 'items',
    276 				field: 'dateModified'
    277 			},
    278 			
    279 			// Deprecated
    280 			{
    281 				name: 'itemTypeID',
    282 				operators: {
    283 					is: true,
    284 					isNot: true
    285 				},
    286 				table: 'items',
    287 				field: 'itemTypeID',
    288 				special: true
    289 			},
    290 			
    291 			{
    292 				name: 'itemType',
    293 				operators: {
    294 					is: true,
    295 					isNot: true
    296 				},
    297 				table: 'items',
    298 				field: 'typeName'
    299 			},
    300 			
    301 			{
    302 				name: 'fileTypeID',
    303 				operators: {
    304 					is: true,
    305 					isNot: true
    306 				},
    307 				table: 'itemAttachments',
    308 				field: 'fileTypeID'
    309 			},
    310 			
    311 			{
    312 				name: 'tagID',
    313 				operators: {
    314 					is: true,
    315 					isNot: true
    316 				},
    317 				table: 'itemTags',
    318 				field: 'tagID',
    319 				special: true
    320 			},
    321 			
    322 			{
    323 				name: 'tag',
    324 				operators: {
    325 					is: true,
    326 					isNot: true,
    327 					contains: true,
    328 					doesNotContain: true
    329 				},
    330 				table: 'itemTags',
    331 				field: 'name'
    332 			},
    333 			
    334 			{
    335 				name: 'note',
    336 				operators: {
    337 					contains: true,
    338 					doesNotContain: true
    339 				},
    340 				table: 'itemNotes',
    341 				// Exclude note prefix and suffix
    342 				field: `SUBSTR(note, ${1 + Zotero.Notes.notePrefix.length}, `
    343 					+ `LENGTH(note) - ${Zotero.Notes.notePrefix.length + Zotero.Notes.noteSuffix.length})`
    344 			},
    345 			
    346 			{
    347 				name: 'childNote',
    348 				operators: {
    349 					contains: true,
    350 					doesNotContain: true
    351 				},
    352 				table: 'items',
    353 				// Exclude note prefix and suffix
    354 				field: `SUBSTR(note, ${1 + Zotero.Notes.notePrefix.length}, `
    355 					+ `LENGTH(note) - ${Zotero.Notes.notePrefix.length + Zotero.Notes.noteSuffix.length})`
    356 			},
    357 			
    358 			{
    359 				name: 'creator',
    360 				operators: {
    361 					is: true,
    362 					isNot: true,
    363 					contains: true,
    364 					doesNotContain: true
    365 				},
    366 				table: 'itemCreators',
    367 				field: "TRIM(firstName || ' ' || lastName)"
    368 			},
    369 			
    370 			{
    371 				name: 'lastName',
    372 				operators: {
    373 					is: true,
    374 					isNot: true,
    375 					contains: true,
    376 					doesNotContain: true
    377 				},
    378 				table: 'itemCreators',
    379 				field: 'lastName',
    380 				special: true
    381 			},
    382 			
    383 			{
    384 				name: 'field',
    385 				operators: {
    386 					is: true,
    387 					isNot: true,
    388 					contains: true,
    389 					doesNotContain: true
    390 				},
    391 				table: 'itemData',
    392 				field: 'value',
    393 				aliases: yield Zotero.DB.columnQueryAsync("SELECT fieldName FROM fieldsCombined "
    394 					+ "WHERE fieldName NOT IN ('accessDate', 'date', 'pages', "
    395 					+ "'section','seriesNumber','issue')"),
    396 				template: true // mark for special handling
    397 			},
    398 			
    399 			{
    400 				name: 'datefield',
    401 				operators: {
    402 					is: true,
    403 					isNot: true,
    404 					isBefore: true,
    405 					isAfter: true,
    406 					isInTheLast: true
    407 				},
    408 				table: 'itemData',
    409 				field: 'value',
    410 				aliases: ['accessDate', 'date', 'dateDue', 'accepted'], // TEMP - NSF
    411 				template: true // mark for special handling
    412 			},
    413 			
    414 			{
    415 				name: 'year',
    416 				operators: {
    417 					is: true,
    418 					isNot: true,
    419 					contains: true,
    420 					doesNotContain: true
    421 				},
    422 				table: 'itemData',
    423 				field: 'SUBSTR(value, 1, 4)',
    424 				special: true
    425 			},
    426 			
    427 			{
    428 				name: 'numberfield',
    429 				operators: {
    430 					is: true,
    431 					isNot: true,
    432 					contains: true,
    433 					doesNotContain: true,
    434 					isLessThan: true,
    435 					isGreaterThan: true
    436 				},
    437 				table: 'itemData',
    438 				field: 'value',
    439 				aliases: ['pages', 'numPages', 'numberOfVolumes', 'section', 'seriesNumber','issue'],
    440 				template: true // mark for special handling
    441 			},
    442 			
    443 			{
    444 				name: 'libraryID',
    445 				operators: {
    446 					is: true,
    447 					isNot: true
    448 				},
    449 				table: 'items',
    450 				field: 'libraryID',
    451 				special: true,
    452 				noLoad: true
    453 			},
    454 			
    455 			{
    456 				name: 'key',
    457 				operators: {
    458 					is: true,
    459 					isNot: true,
    460 					beginsWith: true
    461 				},
    462 				table: 'items',
    463 				field: 'key',
    464 				special: true,
    465 				noLoad: true,
    466 				inlineFilter: function (val) {
    467 					return Zotero.Utilities.isValidObjectKey(val) ? `'${val}'` : false;
    468 				}
    469 			},
    470 			
    471 			{
    472 				name: 'itemID',
    473 				operators: {
    474 					is: true,
    475 					isNot: true
    476 				},
    477 				table: 'items',
    478 				field: 'itemID',
    479 				special: true,
    480 				noLoad: true
    481 			},
    482 			
    483 			{
    484 				name: 'annotation',
    485 				operators: {
    486 					contains: true,
    487 					doesNotContain: true
    488 				},
    489 				table: 'annotations',
    490 				field: 'text'
    491 			},
    492 			
    493 			{
    494 				name: 'fulltextWord',
    495 				operators: {
    496 					contains: true,
    497 					doesNotContain: true
    498 				},
    499 				table: 'fulltextItemWords',
    500 				field: 'word',
    501 				flags: {
    502 					leftbound: true
    503 				},
    504 				special: true
    505 			},
    506 			
    507 			{
    508 				name: 'fulltextContent',
    509 				operators: {
    510 					contains: true,
    511 					doesNotContain: true
    512 				},
    513 				special: false
    514 			},
    515 			
    516 			{
    517 				name: 'tempTable',
    518 				operators: {
    519 					is: true
    520 				}
    521 			}
    522 		];
    523 		
    524 		// Index conditions by name and aliases
    525 		_conditions = {};
    526 		for (var i in conditions) {
    527 			_conditions[conditions[i]['name']] = conditions[i];
    528 			if (conditions[i]['aliases']) {
    529 				for (var j in conditions[i]['aliases']) {
    530 					// TEMP - NSF
    531 					switch (conditions[i]['aliases'][j]) {
    532 						case 'dateDue':
    533 						case 'accepted':
    534 							if (!Zotero.ItemTypes.getID('nsfReviewer')) {
    535 								continue;
    536 							}
    537 					}
    538 					_conditions[conditions[i]['aliases'][j]] = conditions[i];
    539 				}
    540 			}
    541 			_conditions[conditions[i]['name']] = conditions[i];
    542 		}
    543 		
    544 		_standardConditions = [];
    545 		
    546 		var baseMappedFields = Zotero.ItemFields.getBaseMappedFields();
    547 		var locale = Zotero.locale;
    548 		
    549 		// Separate standard conditions for menu display
    550 		for (var i in _conditions){
    551 			var fieldID = false;
    552 			if (['field', 'datefield', 'numberfield'].indexOf(_conditions[i]['name']) != -1) {
    553 				fieldID = Zotero.ItemFields.getID(i);
    554 			}
    555 			
    556 			// If explicitly special...
    557 			if (_conditions[i]['special'] ||
    558 				// or a template master (e.g. 'field')...
    559 				(_conditions[i]['template'] && i==_conditions[i]['name']) ||
    560 				// or no table and not explicitly unspecial...
    561 				(!_conditions[i]['table'] &&
    562 					typeof _conditions[i]['special'] == 'undefined') ||
    563 				// or field is a type-specific version of a base field...
    564 				(fieldID && baseMappedFields.indexOf(fieldID) != -1)) {
    565 				// ...then skip
    566 				continue;
    567 			}
    568 			
    569 			let localized = self.getLocalizedName(i);
    570 			// Hack to use a different name for "issue" in French locale,
    571 			// where 'number' and 'issue' are translated the same
    572 			// https://forums.zotero.org/discussion/14942/
    573 			if (fieldID == 5 && locale.substr(0, 2).toLowerCase() == 'fr') {
    574 				localized = "Num\u00E9ro (p\u00E9riodique)";
    575 			}
    576 			
    577 			_standardConditions.push({
    578 				name: i,
    579 				localized: localized,
    580 				operators: _conditions[i]['operators'],
    581 				flags: _conditions[i]['flags']
    582 			});
    583 		}
    584 		
    585 		var collation = Zotero.getLocaleCollation();
    586 		_standardConditions.sort(function(a, b) {
    587 			return collation.compareString(1, a.localized, b.localized);
    588 		});
    589 	});
    590 	
    591 	
    592 	/*
    593 	 * Get condition data
    594 	 */
    595 	function get(condition){
    596 		return _conditions[condition];
    597 	}
    598 	
    599 	
    600 	/*
    601 	 * Returns array of possible conditions
    602 	 *
    603 	 * Does not include special conditions, only ones that would show in a drop-down list
    604 	 */
    605 	function getStandardConditions(){
    606 		// TODO: return copy instead
    607 		return _standardConditions;
    608 	}
    609 	
    610 	
    611 	/*
    612 	 * Check if an operator is valid for a given condition
    613 	 */
    614 	function hasOperator(condition, operator){
    615 		var [condition, mode] = this.parseCondition(condition);
    616 		
    617 		if (!_conditions) {
    618 			throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded");
    619 		}
    620 		
    621 		if (!_conditions[condition]){
    622 			let e = new Error("Invalid condition '" + condition + "' in hasOperator()");
    623 			e.name = "ZoteroUnknownFieldError";
    624 			throw e;
    625 		}
    626 		
    627 		if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){
    628 			return true;
    629 		}
    630 		
    631 		return !!_conditions[condition]['operators'][operator];
    632 	}
    633 	
    634 	
    635 	function getLocalizedName(str) {
    636 		// TEMP
    637 		if (str == 'itemType') {
    638 			str = 'itemTypeID';
    639 		}
    640 		
    641 		try {
    642 			return Zotero.getString('searchConditions.' + str)
    643 		}
    644 		catch (e) {
    645 			return Zotero.ItemFields.getLocalizedString(null, str);
    646 		}
    647 	}
    648 	
    649 	
    650 	/**
    651 	 * Compare two API JSON condition objects
    652 	 */
    653 	this.equals = function (data1, data2) {
    654 		return data1.condition === data2.condition
    655 			&& data1.operator === data2.operator
    656 			&& data1.value === data2.value;
    657 	}
    658 	
    659 	
    660 	/*
    661 	 * Parses a search into words and "double-quoted phrases"
    662 	 *
    663 	 * Also strips unpaired quotes at the beginning and end of words
    664 	 *
    665 	 * Returns array of objects containing 'text' and 'inQuotes'
    666 	 */
    667 	function parseSearchString(str) {
    668 		var parts = str.split(/\s*("[^"]*")\s*|"\s|\s"|^"|"$|'\s|\s'|^'|'$|\s/m);
    669 		var parsed = [];
    670 		
    671 		for (var i in parts) {
    672 			var part = parts[i];
    673 			if (!part || !part.length) {
    674 				continue;
    675 			}
    676 			
    677 			if (part.charAt(0)=='"' && part.charAt(part.length-1)=='"') {
    678 				parsed.push({
    679 					text: part.substring(1, part.length-1),
    680 					inQuotes: true
    681 				});
    682 			}
    683 			else {
    684 				parsed.push({
    685 					text: part,
    686 					inQuotes: false
    687 				});
    688 			}
    689 		}
    690 		
    691 		return parsed;
    692 	}
    693 	
    694 	
    695 	function parseCondition(condition){
    696 		var mode = false;
    697 		var pos = condition.indexOf('/');
    698 		if (pos != -1){
    699 			mode = condition.substr(pos+1);
    700 			condition = condition.substr(0, pos);
    701 		}
    702 		
    703 		return [condition, mode];
    704 	}
    705 }