691 lines
13 KiB
JavaScript
691 lines
13 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2006-2016 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
https://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero 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 Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
Zotero.SearchConditions = new function(){
|
|
this.get = get;
|
|
this.getStandardConditions = getStandardConditions;
|
|
this.hasOperator = hasOperator;
|
|
this.getLocalizedName = getLocalizedName;
|
|
this.parseSearchString = parseSearchString;
|
|
this.parseCondition = parseCondition;
|
|
|
|
var _initialized = false;
|
|
var _conditions;
|
|
var _standardConditions;
|
|
|
|
var self = this;
|
|
|
|
/*
|
|
* Define the advanced search operators
|
|
*/
|
|
var _operators = {
|
|
// Standard -- these need to match those in zoterosearch.xml
|
|
is: true,
|
|
isNot: true,
|
|
beginsWith: true,
|
|
contains: true,
|
|
doesNotContain: true,
|
|
isLessThan: true,
|
|
isGreaterThan: true,
|
|
isBefore: true,
|
|
isAfter: true,
|
|
isInTheLast: true,
|
|
|
|
// Special
|
|
any: true,
|
|
all: true,
|
|
true: true,
|
|
false: true
|
|
};
|
|
|
|
|
|
/*
|
|
* Define and set up the available advanced search conditions
|
|
*
|
|
* Flags:
|
|
* - special (don't show in search window menu)
|
|
* - template (special handling)
|
|
* - noLoad (can't load from saved search)
|
|
*/
|
|
this.init = Zotero.Promise.coroutine(function* () {
|
|
var conditions = [
|
|
//
|
|
// Special conditions
|
|
//
|
|
{
|
|
name: 'deleted',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
// Don't include child items
|
|
{
|
|
name: 'noChildren',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'unfiled',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'includeParentsAndChildren',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'includeParents',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'includeChildren',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
// Search recursively within collections
|
|
{
|
|
name: 'recursive',
|
|
operators: {
|
|
true: true,
|
|
false: true
|
|
}
|
|
},
|
|
|
|
// Join mode
|
|
{
|
|
name: 'joinMode',
|
|
operators: {
|
|
any: true,
|
|
all: true
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'quicksearch-titleCreatorYear',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'quicksearch-fields',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'quicksearch-everything',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
noLoad: true
|
|
},
|
|
|
|
// Deprecated
|
|
{
|
|
name: 'quicksearch',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
noLoad: true
|
|
},
|
|
|
|
// Quicksearch block markers
|
|
{
|
|
name: 'blockStart',
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'blockEnd',
|
|
noLoad: true
|
|
},
|
|
|
|
// Shortcuts for adding collections and searches by id
|
|
{
|
|
name: 'collectionID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'savedSearchID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
noLoad: true
|
|
},
|
|
|
|
|
|
//
|
|
// Standard conditions
|
|
//
|
|
|
|
// Collection id to search within
|
|
{
|
|
name: 'collection',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'collectionItems',
|
|
field: 'collectionID'
|
|
},
|
|
|
|
// Saved search to search within
|
|
{
|
|
name: 'savedSearch',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
special: false
|
|
},
|
|
|
|
{
|
|
name: 'dateAdded',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
isBefore: true,
|
|
isAfter: true,
|
|
isInTheLast: true
|
|
},
|
|
table: 'items',
|
|
field: 'dateAdded'
|
|
},
|
|
|
|
{
|
|
name: 'dateModified',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
isBefore: true,
|
|
isAfter: true,
|
|
isInTheLast: true
|
|
},
|
|
table: 'items',
|
|
field: 'dateModified'
|
|
},
|
|
|
|
// Deprecated
|
|
{
|
|
name: 'itemTypeID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'items',
|
|
field: 'itemTypeID',
|
|
special: true
|
|
},
|
|
|
|
{
|
|
name: 'itemType',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'items',
|
|
field: 'typeName'
|
|
},
|
|
|
|
{
|
|
name: 'fileTypeID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'itemAttachments',
|
|
field: 'fileTypeID'
|
|
},
|
|
|
|
{
|
|
name: 'tagID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'itemTags',
|
|
field: 'tagID',
|
|
special: true
|
|
},
|
|
|
|
{
|
|
name: 'tag',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'itemTags',
|
|
field: 'name'
|
|
},
|
|
|
|
{
|
|
name: 'note',
|
|
operators: {
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'itemNotes',
|
|
field: 'note'
|
|
},
|
|
|
|
{
|
|
name: 'childNote',
|
|
operators: {
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'items',
|
|
field: 'note'
|
|
},
|
|
|
|
{
|
|
name: 'creator',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'itemCreators',
|
|
field: "TRIM(firstName || ' ' || lastName)"
|
|
},
|
|
|
|
{
|
|
name: 'lastName',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'itemCreators',
|
|
field: 'lastName',
|
|
special: true
|
|
},
|
|
|
|
{
|
|
name: 'field',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'itemData',
|
|
field: 'value',
|
|
aliases: yield Zotero.DB.columnQueryAsync("SELECT fieldName FROM fieldsCombined "
|
|
+ "WHERE fieldName NOT IN ('accessDate', 'date', 'pages', "
|
|
+ "'section','seriesNumber','issue')"),
|
|
template: true // mark for special handling
|
|
},
|
|
|
|
{
|
|
name: 'datefield',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
isBefore: true,
|
|
isAfter: true,
|
|
isInTheLast: true
|
|
},
|
|
table: 'itemData',
|
|
field: 'value',
|
|
aliases: ['accessDate', 'date', 'dateDue', 'accepted'], // TEMP - NSF
|
|
template: true // mark for special handling
|
|
},
|
|
|
|
{
|
|
name: 'year',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'itemData',
|
|
field: 'SUBSTR(value, 1, 4)',
|
|
special: true
|
|
},
|
|
|
|
{
|
|
name: 'numberfield',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
contains: true,
|
|
doesNotContain: true,
|
|
isLessThan: true,
|
|
isGreaterThan: true
|
|
},
|
|
table: 'itemData',
|
|
field: 'value',
|
|
aliases: ['pages', 'numPages', 'numberOfVolumes', 'section', 'seriesNumber','issue'],
|
|
template: true // mark for special handling
|
|
},
|
|
|
|
{
|
|
name: 'libraryID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'items',
|
|
field: 'libraryID',
|
|
special: true,
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'key',
|
|
operators: {
|
|
is: true,
|
|
isNot: true,
|
|
beginsWith: true
|
|
},
|
|
table: 'items',
|
|
field: 'key',
|
|
special: true,
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'itemID',
|
|
operators: {
|
|
is: true,
|
|
isNot: true
|
|
},
|
|
table: 'items',
|
|
field: 'itemID',
|
|
special: true,
|
|
noLoad: true
|
|
},
|
|
|
|
{
|
|
name: 'annotation',
|
|
operators: {
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'annotations',
|
|
field: 'text'
|
|
},
|
|
|
|
{
|
|
name: 'fulltextWord',
|
|
operators: {
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
table: 'fulltextItemWords',
|
|
field: 'word',
|
|
flags: {
|
|
leftbound: true
|
|
},
|
|
special: true
|
|
},
|
|
|
|
{
|
|
name: 'fulltextContent',
|
|
operators: {
|
|
contains: true,
|
|
doesNotContain: true
|
|
},
|
|
special: false
|
|
},
|
|
|
|
{
|
|
name: 'tempTable',
|
|
operators: {
|
|
is: true
|
|
}
|
|
}
|
|
];
|
|
|
|
// Index conditions by name and aliases
|
|
_conditions = {};
|
|
for (var i in conditions) {
|
|
_conditions[conditions[i]['name']] = conditions[i];
|
|
if (conditions[i]['aliases']) {
|
|
for (var j in conditions[i]['aliases']) {
|
|
// TEMP - NSF
|
|
switch (conditions[i]['aliases'][j]) {
|
|
case 'dateDue':
|
|
case 'accepted':
|
|
if (!Zotero.ItemTypes.getID('nsfReviewer')) {
|
|
continue;
|
|
}
|
|
}
|
|
_conditions[conditions[i]['aliases'][j]] = conditions[i];
|
|
}
|
|
}
|
|
_conditions[conditions[i]['name']] = conditions[i];
|
|
}
|
|
|
|
_standardConditions = [];
|
|
|
|
var baseMappedFields = Zotero.ItemFields.getBaseMappedFields();
|
|
var locale = Zotero.locale;
|
|
|
|
// Separate standard conditions for menu display
|
|
for (var i in _conditions){
|
|
var fieldID = false;
|
|
if (['field', 'datefield', 'numberfield'].indexOf(_conditions[i]['name']) != -1) {
|
|
fieldID = Zotero.ItemFields.getID(i);
|
|
}
|
|
|
|
// If explicitly special...
|
|
if (_conditions[i]['special'] ||
|
|
// or a template master (e.g. 'field')...
|
|
(_conditions[i]['template'] && i==_conditions[i]['name']) ||
|
|
// or no table and not explicitly unspecial...
|
|
(!_conditions[i]['table'] &&
|
|
typeof _conditions[i]['special'] == 'undefined') ||
|
|
// or field is a type-specific version of a base field...
|
|
(fieldID && baseMappedFields.indexOf(fieldID) != -1)) {
|
|
// ...then skip
|
|
continue;
|
|
}
|
|
|
|
let localized = self.getLocalizedName(i);
|
|
// Hack to use a different name for "issue" in French locale,
|
|
// where 'number' and 'issue' are translated the same
|
|
// https://forums.zotero.org/discussion/14942/
|
|
if (fieldID == 5 && locale.substr(0, 2).toLowerCase() == 'fr') {
|
|
localized = "Num\u00E9ro (p\u00E9riodique)";
|
|
}
|
|
|
|
_standardConditions.push({
|
|
name: i,
|
|
localized: localized,
|
|
operators: _conditions[i]['operators'],
|
|
flags: _conditions[i]['flags']
|
|
});
|
|
}
|
|
|
|
var collation = Zotero.getLocaleCollation();
|
|
_standardConditions.sort(function(a, b) {
|
|
return collation.compareString(1, a.localized, b.localized);
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
* Get condition data
|
|
*/
|
|
function get(condition){
|
|
return _conditions[condition];
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns array of possible conditions
|
|
*
|
|
* Does not include special conditions, only ones that would show in a drop-down list
|
|
*/
|
|
function getStandardConditions(){
|
|
// TODO: return copy instead
|
|
return _standardConditions;
|
|
}
|
|
|
|
|
|
/*
|
|
* Check if an operator is valid for a given condition
|
|
*/
|
|
function hasOperator(condition, operator){
|
|
var [condition, mode] = this.parseCondition(condition);
|
|
|
|
if (!_conditions) {
|
|
throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded");
|
|
}
|
|
|
|
if (!_conditions[condition]){
|
|
let e = new Error("Invalid condition '" + condition + "' in hasOperator()");
|
|
e.name = "ZoteroUnknownFieldError";
|
|
throw e;
|
|
}
|
|
|
|
if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){
|
|
return true;
|
|
}
|
|
|
|
return !!_conditions[condition]['operators'][operator];
|
|
}
|
|
|
|
|
|
function getLocalizedName(str) {
|
|
// TEMP
|
|
if (str == 'itemType') {
|
|
str = 'itemTypeID';
|
|
}
|
|
|
|
try {
|
|
return Zotero.getString('searchConditions.' + str)
|
|
}
|
|
catch (e) {
|
|
return Zotero.ItemFields.getLocalizedString(null, str);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Compare two API JSON condition objects
|
|
*/
|
|
this.equals = function (data1, data2) {
|
|
return data1.condition === data2.condition
|
|
&& data1.operator === data2.operator
|
|
&& data1.value === data2.value;
|
|
}
|
|
|
|
|
|
/*
|
|
* Parses a search into words and "double-quoted phrases"
|
|
*
|
|
* Also strips unpaired quotes at the beginning and end of words
|
|
*
|
|
* Returns array of objects containing 'text' and 'inQuotes'
|
|
*/
|
|
function parseSearchString(str) {
|
|
var parts = str.split(/\s*("[^"]*")\s*|"\s|\s"|^"|"$|'\s|\s'|^'|'$|\s/m);
|
|
var parsed = [];
|
|
|
|
for (var i in parts) {
|
|
var part = parts[i];
|
|
if (!part || !part.length) {
|
|
continue;
|
|
}
|
|
|
|
if (part.charAt(0)=='"' && part.charAt(part.length-1)=='"') {
|
|
parsed.push({
|
|
text: part.substring(1, part.length-1),
|
|
inQuotes: true
|
|
});
|
|
}
|
|
else {
|
|
parsed.push({
|
|
text: part,
|
|
inQuotes: false
|
|
});
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
|
|
function parseCondition(condition){
|
|
var mode = false;
|
|
var pos = condition.indexOf('/');
|
|
if (pos != -1){
|
|
mode = condition.substr(pos+1);
|
|
condition = condition.substr(0, pos);
|
|
}
|
|
|
|
return [condition, mode];
|
|
}
|
|
}
|