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 }