quickFormat.js (43643B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2011 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 Components.utils.import("resource://gre/modules/Services.jsm"); 26 27 var Zotero_QuickFormat = new function () { 28 const pixelRe = /^([0-9]+)px$/ 29 const specifiedLocatorRe = /^(?:,? *(p{0,2})(?:\. *| +)|:)([0-9\-]+) *$/; 30 const yearRe = /,? *([0-9]+) *(B[. ]*C[. ]*(?:E[. ]*)?|A[. ]*D[. ]*|C[. ]*E[. ]*)?$/i; 31 const locatorRe = /(?:,? *(p{0,2})\.?|(\:)) *([0-9\-–]+)$/i; 32 const creatorSplitRe = /(?:,| *(?:and|\&)) +/; 33 const charRe = /[\w\u007F-\uFFFF]/; 34 const numRe = /^[0-9\-–]+$/; 35 36 var initialized, io, qfs, qfi, qfiWindow, qfiDocument, qfe, qfb, qfbHeight, qfGuidance, 37 keepSorted, showEditor, referencePanel, referenceBox, referenceHeight = 0, 38 separatorHeight = 0, currentLocator, currentLocatorLabel, currentSearchTime, dragging, 39 panel, panelPrefix, panelSuffix, panelSuppressAuthor, panelLocatorLabel, panelLocator, 40 panelLibraryLink, panelInfo, panelRefersToBubble, panelFrameHeight = 0, accepted = false; 41 var _searchPromise; 42 43 const SEARCH_TIMEOUT = 250; 44 const SHOWN_REFERENCES = 7; 45 46 /** 47 * Pre-initialization, when the dialog has loaded but has not yet appeared 48 */ 49 this.onDOMContentLoaded = function(event) { 50 if(event.target === document) { 51 initialized = true; 52 io = window.arguments[0].wrappedJSObject; 53 54 // Only hide chrome on Windows or Mac 55 if(Zotero.isMac) { 56 document.documentElement.setAttribute("drawintitlebar", true); 57 } else if(Zotero.isWin) { 58 document.documentElement.setAttribute("hidechrome", true); 59 } 60 61 // Include a different key combo in message on Mac 62 if(Zotero.isMac) { 63 var qf = document.getElementById('quick-format-guidance'); 64 qf.setAttribute('about', qf.getAttribute('about') + "Mac"); 65 } 66 67 new WindowDraggingElement(document.getElementById("quick-format-dialog"), window); 68 69 qfs = document.getElementById("quick-format-search"); 70 qfi = document.getElementById("quick-format-iframe"); 71 qfb = document.getElementById("quick-format-entry"); 72 qfbHeight = qfb.scrollHeight; 73 referencePanel = document.getElementById("quick-format-reference-panel"); 74 referenceBox = document.getElementById("quick-format-reference-list"); 75 76 if(Zotero.isWin && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')) { 77 qfb.setAttribute("square", "true"); 78 } 79 80 // add labels to popup 81 var locators = Zotero.Cite.labels; 82 var menu = document.getElementById("locator-label"); 83 var labelList = document.getElementById("locator-label-popup"); 84 for(var locator of locators) { 85 var locatorLabel = Zotero.getString('citation.locator.'+locator.replace(/\s/g,'')); 86 87 // add to list of labels 88 var child = document.createElement("menuitem"); 89 child.setAttribute("value", locator); 90 child.setAttribute("label", locatorLabel); 91 labelList.appendChild(child); 92 } 93 menu.selectedIndex = 0; 94 95 keepSorted = document.getElementById("keep-sorted"); 96 showEditor = document.getElementById("show-editor"); 97 if(io.sortable) { 98 keepSorted.hidden = false; 99 if(!io.citation.properties.unsorted) { 100 keepSorted.setAttribute("checked", "true"); 101 } 102 } 103 104 // Nodes for citation properties panel 105 panel = document.getElementById("citation-properties"); 106 panelPrefix = document.getElementById("prefix"); 107 panelSuffix = document.getElementById("suffix"); 108 panelSuppressAuthor = document.getElementById("suppress-author"); 109 panelLocatorLabel = document.getElementById("locator-label"); 110 panelLocator = document.getElementById("locator"); 111 panelInfo = document.getElementById("citation-properties-info"); 112 panelLibraryLink = document.getElementById("citation-properties-library-link"); 113 114 // Don't need to set noautohide dynamically on these platforms, so do it now 115 if(Zotero.isMac || Zotero.isWin) { 116 referencePanel.setAttribute("noautohide", true); 117 } 118 } else if(event.target === qfi.contentDocument) { 119 qfiWindow = qfi.contentWindow; 120 qfiDocument = qfi.contentDocument; 121 qfb.addEventListener("keypress", _onQuickSearchKeyPress, false); 122 qfe = qfiDocument.getElementById("quick-format-editor"); 123 qfe.addEventListener("drop", _onBubbleDrop, false); 124 qfe.addEventListener("paste", _onPaste, false); 125 } 126 } 127 128 /** 129 * Initialize add citation dialog 130 */ 131 this.onLoad = function(event) { 132 if(event.target !== document) return; 133 // make sure we are visible 134 window.setTimeout(function() { 135 window.resizeTo(window.outerWidth, qfb.clientHeight); 136 var screenX = window.screenX; 137 var screenY = window.screenY; 138 var xRange = [window.screen.availLeft, window.screen.width-window.outerWidth]; 139 var yRange = [window.screen.availTop, window.screen.height-window.outerHeight]; 140 if(screenX < xRange[0] || screenX > xRange[1] || screenY < yRange[0] || screenY > yRange[1]) { 141 var targetX = Math.max(Math.min(screenX, xRange[1]), xRange[0]); 142 var targetY = Math.max(Math.min(screenY, yRange[1]), yRange[0]); 143 Zotero.debug("Moving window to "+targetX+", "+targetY); 144 window.moveTo(targetX, targetY); 145 } 146 qfGuidance = document.getElementById('quick-format-guidance'); 147 qfGuidance.show(); 148 _refocusQfe(); 149 }, 0); 150 151 window.focus(); 152 qfe.focus(); 153 154 // load citation data 155 if(io.citation.citationItems.length) { 156 // hack to get spacing right 157 var evt = qfiDocument.createEvent("KeyboardEvent"); 158 evt.initKeyEvent("keypress", true, true, qfiWindow, 159 0, 0, 0, 0, 160 0, " ".charCodeAt(0)) 161 qfe.dispatchEvent(evt); 162 window.setTimeout(function() { 163 var node = qfe.firstChild; 164 node.nodeValue = ""; 165 _showCitation(node); 166 _resize(); 167 }, 1); 168 } 169 }; 170 171 function _refocusQfe() { 172 referencePanel.blur(); 173 window.focus(); 174 qfe.focus(); 175 } 176 177 /** 178 * Gets the content of the text node that the cursor is currently within 179 */ 180 function _getCurrentEditorTextNode() { 181 var selection = qfiWindow.getSelection(); 182 if (!selection) return false; 183 var range = selection.getRangeAt(0); 184 185 var node = range.startContainer; 186 if(node !== range.endContainer) return false; 187 if(node.nodeType === Node.TEXT_NODE) return node; 188 189 // Range could be referenced to the body element 190 if(node === qfe) { 191 var offset = range.startOffset; 192 if(offset !== range.endOffset) return false; 193 node = qfe.childNodes[Math.min(qfe.childNodes.length-1, offset)]; 194 if(node.nodeType === Node.TEXT_NODE) return node; 195 } 196 return false; 197 } 198 199 /** 200 * Gets text within the currently selected node 201 * @param {Boolean} [clear] If true, also remove these nodes 202 */ 203 function _getEditorContent(clear) { 204 var node = _getCurrentEditorTextNode(); 205 return node ? node.wholeText : false; 206 } 207 208 /** 209 * Does the dirty work of figuring out what the user meant to type 210 */ 211 var _quickFormat = Zotero.Promise.coroutine(function* () { 212 var str = _getEditorContent(); 213 var haveConditions = false; 214 215 const etAl = " et al."; 216 217 var m, 218 year = false, 219 isBC = false, 220 dateID = false; 221 222 currentLocator = false; 223 currentLocatorLabel = false; 224 225 // check for adding a number onto a previous page number 226 if(numRe.test(str)) { 227 // add to previous cite 228 var node = _getCurrentEditorTextNode(); 229 var prevNode = node.previousSibling; 230 if(prevNode && prevNode.citationItem && prevNode.citationItem.locator) { 231 prevNode.citationItem.locator += str; 232 prevNode.textContent = _buildBubbleString(prevNode.citationItem); 233 node.nodeValue = ""; 234 _clearEntryList(); 235 return; 236 } 237 } 238 239 if(str && str.length > 1) { 240 // check for specified locator 241 m = specifiedLocatorRe.exec(str); 242 if(m) { 243 if(m.index === 0) { 244 // add to previous cite 245 var node = _getCurrentEditorTextNode(); 246 var prevNode = node.previousSibling; 247 if(prevNode && prevNode.citationItem) { 248 prevNode.citationItem.locator = m[2]; 249 prevNode.textContent = _buildBubbleString(prevNode.citationItem); 250 node.nodeValue = ""; 251 _clearEntryList(); 252 return; 253 } 254 } 255 256 // TODO support types other than page 257 currentLocator = m[2]; 258 str = str.substring(0, m.index); 259 } 260 261 // check for year and pages 262 str = _updateLocator(str); 263 m = yearRe.exec(str); 264 if(m) { 265 year = parseInt(m[1]); 266 isBC = m[2] && m[2][0] === "B"; 267 str = str.substr(0, m.index)+str.substring(m.index+m[0].length); 268 } 269 if(year) str += " "+year; 270 271 var s = new Zotero.Search(); 272 str = str.replace(/ (?:&|and) /g, " ", "g"); 273 if(charRe.test(str)) { 274 Zotero.debug("QuickFormat: QuickSearch: "+str); 275 // Exclude feeds 276 Zotero.Feeds.getAll() 277 .forEach(feed => s.addCondition("libraryID", "isNot", feed.libraryID)); 278 s.addCondition("quicksearch-titleCreatorYear", "contains", str); 279 s.addCondition("itemType", "isNot", "attachment"); 280 haveConditions = true; 281 } 282 } 283 284 if(haveConditions) { 285 var searchResultIDs = (haveConditions ? (yield s.search()) : []); 286 287 // Show items list without cited items to start 288 yield _updateItemList(false, false, str, searchResultIDs); 289 290 // Check to see which search results match items already in the document 291 var citedItems, completed = false, isAsync = false; 292 // Save current search time so that when we get items, we know whether it's too late to 293 // process them or not 294 var lastSearchTime = currentSearchTime = Date.now(); 295 // This may or may not be synchronous 296 io.getItems().then(function(citedItems) { 297 // Don't do anything if panel is already closed 298 if(isAsync && 299 ((referencePanel.state !== "open" && referencePanel.state !== "showing") 300 || lastSearchTime !== currentSearchTime)) return; 301 302 completed = true; 303 304 if(str.toLowerCase() === Zotero.getString("integration.ibid").toLowerCase()) { 305 // If "ibid" is entered, show all cited items 306 citedItemsMatchingSearch = citedItems; 307 } else { 308 Zotero.debug("Searching cited items"); 309 // Search against items. We do this here because it's possible that some of these 310 // items are only in the doc, and not in the DB. 311 var splits = Zotero.Fulltext.semanticSplitter(str), 312 citedItemsMatchingSearch = []; 313 for(var i=0, iCount=citedItems.length; i<iCount; i++) { 314 // Generate a string to search for each item 315 let item = citedItems[i]; 316 let itemStr = item.getCreators() 317 .map(creator => creator.firstName + " " + creator.lastName) 318 .concat([item.getField("title"), item.getField("date", true, true).substr(0, 4)]) 319 .join(" "); 320 321 // See if words match 322 for(var j=0, jCount=splits.length; j<jCount; j++) { 323 var split = splits[j]; 324 if(itemStr.toLowerCase().indexOf(split) === -1) break; 325 } 326 327 // If matched, add to citedItemsMatchingSearch 328 if(j === jCount) citedItemsMatchingSearch.push(item); 329 } 330 Zotero.debug("Searched cited items"); 331 } 332 333 _updateItemList(citedItems, citedItemsMatchingSearch, str, searchResultIDs, isAsync); 334 }); 335 336 if(!completed) { 337 // We are going to have to wait until items have been retrieved from the document. 338 Zotero.debug("Getting cited items asynchronously"); 339 isAsync = true; 340 } else { 341 Zotero.debug("Got cited items synchronously"); 342 } 343 } else { 344 // No search conditions, so just clear the box 345 _updateItemList([], [], "", []); 346 } 347 }); 348 349 /** 350 * Updates currentLocator based on a string 351 * @param {String} str String to search for locator 352 * @return {String} str without locator 353 */ 354 function _updateLocator(str) { 355 m = locatorRe.exec(str); 356 if(m && (m[1] || m[2] || m[3].length !== 4)) { 357 currentLocator = m[3]; 358 str = str.substr(0, m.index)+str.substring(m.index+m[0].length); 359 } 360 return str; 361 } 362 363 /** 364 * Updates the item list 365 */ 366 var _updateItemList = Zotero.Promise.coroutine(function* (citedItems, citedItemsMatchingSearch, 367 searchString, searchResultIDs, preserveSelection) { 368 var selectedIndex = 1, previousItemID; 369 370 // Do this so we can preserve the selected item after cited items have been loaded 371 if(preserveSelection && referenceBox.selectedIndex !== -1 && referenceBox.selectedIndex !== 2) { 372 previousItemID = parseInt(referenceBox.selectedItem.getAttribute("zotero-item"), 10); 373 } 374 375 while(referenceBox.hasChildNodes()) referenceBox.removeChild(referenceBox.firstChild); 376 377 var nCitedItemsFromLibrary = {}; 378 if(!citedItems) { 379 // We don't know whether or not we have cited items, because we are waiting for document 380 // data 381 referenceBox.appendChild(_buildListSeparator(Zotero.getString("integration.cited.loading"))); 382 selectedIndex = 2; 383 } else if(citedItems.length) { 384 // We have cited items 385 for(var i=0, n=citedItems.length; i<n; i++) { 386 var citedItem = citedItems[i]; 387 // Tabulate number of items in document for each library 388 if(!citedItem.cslItemID) { 389 var libraryID = citedItem.libraryID; 390 if(libraryID in nCitedItemsFromLibrary) { 391 nCitedItemsFromLibrary[libraryID]++; 392 } else { 393 nCitedItemsFromLibrary[libraryID] = 1; 394 } 395 } 396 } 397 398 if(citedItemsMatchingSearch && citedItemsMatchingSearch.length) { 399 referenceBox.appendChild(_buildListSeparator(Zotero.getString("integration.cited"))); 400 for(var i=0; i<Math.min(citedItemsMatchingSearch.length, 50); i++) { 401 var citedItem = citedItemsMatchingSearch[i]; 402 referenceBox.appendChild(_buildListItem(citedItem)); 403 } 404 } 405 } 406 407 // Also take into account items cited in this citation. This means that the sorting isn't 408 // exactly by # of items cited from each library, but maybe it's better this way. 409 _updateCitationObject(); 410 for(var citationItem of io.citation.citationItems) { 411 var citedItem = Zotero.Cite.getItem(citationItem.id); 412 if(!citedItem.cslItemID) { 413 var libraryID = citedItem.libraryID; 414 if(libraryID in nCitedItemsFromLibrary) { 415 nCitedItemsFromLibrary[libraryID]++; 416 } else { 417 nCitedItemsFromLibrary[libraryID] = 1; 418 } 419 } 420 } 421 422 if(searchResultIDs.length && (!citedItemsMatchingSearch || citedItemsMatchingSearch.length < 50)) { 423 // Search results might be in an unloaded library, so get items asynchronously and load 424 // necessary data 425 var items = yield Zotero.Items.getAsync(searchResultIDs); 426 yield Zotero.Items.loadDataTypes(items); 427 428 searchString = searchString.toLowerCase(); 429 var collation = Zotero.getLocaleCollation(); 430 431 items.sort(function _itemSort(a, b) { 432 var firstCreatorA = a.firstCreator, firstCreatorB = b.firstCreator; 433 434 // Favor left-bound name matches (e.g., "Baum" < "Appelbaum"), 435 // using last name of first author 436 if (firstCreatorA && firstCreatorB) { 437 let caStartsWith = firstCreatorA.toLowerCase().indexOf(searchString) == 0; 438 let cbStartsWith = firstCreatorB.toLowerCase().indexOf(searchString) == 0; 439 if (caStartsWith && !cbStartsWith) { 440 return -1; 441 } 442 else if (!caStartsWith && cbStartsWith) { 443 return 1; 444 } 445 } 446 447 var libA = a.libraryID, libB = b.libraryID; 448 if(libA !== libB) { 449 // Sort by number of cites for library 450 if(nCitedItemsFromLibrary[libA] && !nCitedItemsFromLibrary[libB]) { 451 return -1; 452 } 453 if(!nCitedItemsFromLibrary[libA] && nCitedItemsFromLibrary[libB]) { 454 return 1; 455 } 456 if(nCitedItemsFromLibrary[libA] !== nCitedItemsFromLibrary[libB]) { 457 return nCitedItemsFromLibrary[libB] - nCitedItemsFromLibrary[libA]; 458 } 459 460 // Sort by ID even if number of cites is equal 461 return libA - libB; 462 } 463 464 // Sort by last name of first author 465 if (firstCreatorA !== "" && firstCreatorB === "") { 466 return -1; 467 } else if (firstCreatorA === "" && firstCreatorB !== "") { 468 return 1 469 } else if (firstCreatorA) { 470 return collation.compareString(1, firstCreatorA, firstCreatorB); 471 } 472 473 // Sort by date 474 var yearA = a.getField("date", true, true).substr(0, 4), 475 yearB = b.getField("date", true, true).substr(0, 4); 476 return yearA - yearB; 477 }); 478 479 var previousLibrary = -1; 480 for(var i=0, n=Math.min(items.length, citedItemsMatchingSearch ? 50-citedItemsMatchingSearch.length : 50); i<n; i++) { 481 var item = items[i], libraryID = item.libraryID; 482 483 if(previousLibrary != libraryID) { 484 var libraryName = libraryID ? Zotero.Libraries.getName(libraryID) 485 : Zotero.getString('pane.collections.library'); 486 referenceBox.appendChild(_buildListSeparator(libraryName)); 487 } 488 489 referenceBox.appendChild(_buildListItem(item)); 490 previousLibrary = libraryID; 491 492 if(preserveSelection && (item.cslItemID ? item.cslItemID : item.id) === previousItemID) { 493 selectedIndex = referenceBox.childNodes.length-1; 494 } 495 } 496 } 497 498 _resize(); 499 if((citedItemsMatchingSearch && citedItemsMatchingSearch.length) || searchResultIDs.length) { 500 referenceBox.selectedIndex = selectedIndex; 501 referenceBox.ensureIndexIsVisible(selectedIndex); 502 } 503 }); 504 505 /** 506 * Builds a string describing an item. We avoid CSL here for speed. 507 */ 508 function _buildItemDescription(item, infoHbox) { 509 var nodes = []; 510 511 var author, authorDate = ""; 512 if(item.firstCreator) author = authorDate = item.firstCreator; 513 var date = item.getField("date", true, true); 514 if(date && (date = date.substr(0, 4)) !== "0000") { 515 authorDate += " ("+date+")"; 516 } 517 authorDate = authorDate.trim(); 518 if(authorDate) nodes.push(authorDate); 519 520 var publicationTitle = item.getField("publicationTitle", false, true); 521 if(publicationTitle) { 522 var label = document.createElement("label"); 523 label.setAttribute("value", publicationTitle); 524 label.setAttribute("crop", "end"); 525 label.style.fontStyle = "italic"; 526 nodes.push(label); 527 } 528 529 var volumeIssue = item.getField("volume"); 530 var issue = item.getField("issue"); 531 if(issue) volumeIssue += "("+issue+")"; 532 if(volumeIssue) nodes.push(volumeIssue); 533 534 var publisherPlace = [], field; 535 if((field = item.getField("publisher"))) publisherPlace.push(field); 536 if((field = item.getField("place"))) publisherPlace.push(field); 537 if(publisherPlace.length) nodes.push(publisherPlace.join(": ")); 538 539 var pages = item.getField("pages"); 540 if(pages) nodes.push(pages); 541 542 if(!nodes.length) { 543 var url = item.getField("url"); 544 if(url) nodes.push(url); 545 } 546 547 // compile everything together 548 var str = ""; 549 for(var i=0, n=nodes.length; i<n; i++) { 550 var node = nodes[i]; 551 552 if(i != 0) str += ", "; 553 554 if(typeof node === "object") { 555 var label = document.createElement("label"); 556 label.setAttribute("value", str); 557 label.setAttribute("crop", "end"); 558 infoHbox.appendChild(label); 559 infoHbox.appendChild(node); 560 str = ""; 561 } else { 562 str += node; 563 } 564 } 565 566 if(nodes.length && (!str.length || str[str.length-1] !== ".")) str += "."; 567 var label = document.createElement("label"); 568 label.setAttribute("value", str); 569 label.setAttribute("crop", "end"); 570 label.setAttribute("flex", "1"); 571 infoHbox.appendChild(label); 572 } 573 574 /** 575 * Creates an item to be added to the item list 576 */ 577 function _buildListItem(item) { 578 var titleNode = document.createElement("label"); 579 titleNode.setAttribute("class", "quick-format-title"); 580 titleNode.setAttribute("flex", "1"); 581 titleNode.setAttribute("crop", "end"); 582 titleNode.setAttribute("value", item.getDisplayTitle()); 583 584 var infoNode = document.createElement("hbox"); 585 infoNode.setAttribute("class", "quick-format-info"); 586 _buildItemDescription(item, infoNode); 587 588 // add to rich list item 589 var rll = document.createElement("richlistitem"); 590 rll.setAttribute("orient", "vertical"); 591 rll.setAttribute("class", "quick-format-item"); 592 rll.setAttribute("zotero-item", item.cslItemID ? item.cslItemID : item.id); 593 rll.appendChild(titleNode); 594 rll.appendChild(infoNode); 595 rll.addEventListener("click", _bubbleizeSelected, false); 596 597 return rll; 598 } 599 600 /** 601 * Creates a list separator to be added to the item list 602 */ 603 function _buildListSeparator(labelText, loading) { 604 var titleNode = document.createElement("label"); 605 titleNode.setAttribute("class", "quick-format-separator-title"); 606 titleNode.setAttribute("flex", "1"); 607 titleNode.setAttribute("crop", "end"); 608 titleNode.setAttribute("value", labelText); 609 610 // add to rich list item 611 var rll = document.createElement("richlistitem"); 612 rll.setAttribute("orient", "vertical"); 613 rll.setAttribute("disabled", true); 614 rll.setAttribute("class", loading ? "quick-format-loading" : "quick-format-separator"); 615 rll.appendChild(titleNode); 616 rll.addEventListener("mousedown", _ignoreClick, true); 617 rll.addEventListener("click", _ignoreClick, true); 618 619 return rll; 620 } 621 622 /** 623 * Builds the string to go inside a bubble 624 */ 625 function _buildBubbleString(citationItem) { 626 var item = Zotero.Cite.getItem(citationItem.id); 627 // create text for bubble 628 629 // Creator 630 var title, delimiter; 631 var str = item.getField("firstCreator"); 632 633 // Title, if no creator (getDisplayTitle in order to get case, e-mail, statute which don't have a title field) 634 if(!str) { 635 str = Zotero.getString("punctuation.openingQMark") + item.getDisplayTitle() + Zotero.getString("punctuation.closingQMark"); 636 } 637 638 // Date 639 var date = item.getField("date", true, true); 640 if(date && (date = date.substr(0, 4)) !== "0000") { 641 str += ", "+date; 642 } 643 644 // Locator 645 if(citationItem.locator) { 646 if(citationItem.label) { 647 // TODO localize and use short forms 648 var label = citationItem.label; 649 } else if(/[\-–,]/.test(citationItem.locator)) { 650 var label = "pp."; 651 } else { 652 var label = "p." 653 } 654 655 str += ", "+label+" "+citationItem.locator; 656 } 657 658 // Prefix 659 if(citationItem.prefix && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) { 660 str = citationItem.prefix 661 +(Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? " " : "") 662 +str; 663 } 664 665 // Suffix 666 if(citationItem.suffix && Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP) { 667 str += (Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? " " : "") 668 +citationItem.suffix; 669 } 670 671 return str; 672 } 673 674 /** 675 * Insert a bubble into the DOM at a specified position 676 */ 677 function _insertBubble(citationItem, nextNode) { 678 var str = _buildBubbleString(citationItem); 679 680 // It's entirely unintuitive why, but after trying a bunch of things, it looks like using 681 // a XUL label for these things works best. A regular span causes issues with moving the 682 // cursor. 683 var bubble = qfiDocument.createElement("span"); 684 bubble.setAttribute("class", "quick-format-bubble"); 685 bubble.setAttribute("draggable", "true"); 686 bubble.textContent = str; 687 bubble.addEventListener("click", _onBubbleClick, false); 688 bubble.addEventListener("dragstart", _onBubbleDrag, false); 689 bubble.citationItem = citationItem; 690 if(nextNode && nextNode instanceof Range) { 691 nextNode.insertNode(bubble); 692 } else { 693 qfe.insertBefore(bubble, (nextNode ? nextNode : null)); 694 } 695 696 // make sure that there are no rogue <br>s 697 var elements = qfe.getElementsByTagName("br"); 698 while(elements.length) { 699 elements[0].parentNode.removeChild(elements[0]); 700 } 701 return bubble; 702 } 703 704 /** 705 * Clear list of bubbles 706 */ 707 function _clearEntryList() { 708 while(referenceBox.hasChildNodes()) referenceBox.removeChild(referenceBox.firstChild); 709 _resize(); 710 } 711 712 /** 713 * Converts the selected item to a bubble 714 */ 715 var _bubbleizeSelected = Zotero.Promise.coroutine(function* () { 716 if(!referenceBox.hasChildNodes() || !referenceBox.selectedItem) return false; 717 718 var citationItem = {"id":referenceBox.selectedItem.getAttribute("zotero-item")}; 719 if(typeof citationItem.id === "string" && citationItem.id.indexOf("/") !== -1) { 720 var item = Zotero.Cite.getItem(citationItem.id); 721 citationItem.uris = item.cslURIs; 722 citationItem.itemData = item.cslItemData; 723 } 724 725 _updateLocator(_getEditorContent()); 726 if(currentLocator) { 727 citationItem["locator"] = currentLocator; 728 if(currentLocatorLabel) { 729 citationItem["label"] = currentLocatorLabel; 730 } 731 } 732 733 // get next node and clear this one 734 var node = _getCurrentEditorTextNode(); 735 node.nodeValue = ""; 736 var bubble = _insertBubble(citationItem, node); 737 _clearEntryList(); 738 yield _previewAndSort(); 739 _refocusQfe(); 740 741 return true; 742 }); 743 744 /** 745 * Ignores clicks (for use on separators in the rich list box) 746 */ 747 function _ignoreClick(e) { 748 e.stopPropagation(); 749 e.preventDefault(); 750 } 751 752 /** 753 * Resizes window to fit content 754 */ 755 function _resize() { 756 var childNodes = referenceBox.childNodes, numReferences = 0, numSeparators = 0, 757 firstReference, firstSeparator, height; 758 for(var i=0, n=childNodes.length; i<n && numReferences < SHOWN_REFERENCES; i++) { 759 if(childNodes[i].className === "quick-format-item") { 760 numReferences++; 761 if(!firstReference) { 762 firstReference = childNodes[i]; 763 if(referenceBox.selectedIndex === -1) referenceBox.selectedIndex = i; 764 } 765 } else if(childNodes[i].className === "quick-format-separator") { 766 numSeparators++; 767 if(!firstSeparator) firstSeparator = childNodes[i]; 768 } 769 } 770 771 if(qfe.scrollHeight > 30) { 772 qfe.setAttribute("multiline", true); 773 qfs.setAttribute("multiline", true); 774 qfs.style.height = ((Zotero.isMac ? 6 : 4)+qfe.scrollHeight)+"px"; 775 window.sizeToContent(); 776 } else { 777 delete qfs.style.height; 778 qfe.removeAttribute("multiline"); 779 qfs.removeAttribute("multiline"); 780 window.sizeToContent(); 781 } 782 var panelShowing = referencePanel.state === "open" || referencePanel.state === "showing"; 783 784 if(numReferences || numSeparators) { 785 if(((!referenceHeight && firstReference) || (!separatorHeight && firstSeparator) 786 || !panelFrameHeight) && !panelShowing) { 787 _openReferencePanel(); 788 panelShowing = true; 789 } 790 791 if(!referenceHeight && firstReference) { 792 referenceHeight = firstReference.scrollHeight + 1; 793 } 794 795 if(!separatorHeight && firstSeparator) { 796 separatorHeight = firstSeparator.scrollHeight + 1; 797 } 798 799 if(!panelFrameHeight) { 800 panelFrameHeight = referencePanel.boxObject.height - referencePanel.clientHeight; 801 var computedStyle = window.getComputedStyle(referenceBox, null); 802 for(var attr of ["border-top-width", "border-bottom-width"]) { 803 var val = computedStyle.getPropertyValue(attr); 804 if(val) { 805 var m = pixelRe.exec(val); 806 if(m) panelFrameHeight += parseInt(m[1], 10); 807 } 808 } 809 } 810 811 referencePanel.sizeTo(window.outerWidth-30, 812 numReferences*referenceHeight+numSeparators*separatorHeight+panelFrameHeight); 813 if(!panelShowing) _openReferencePanel(); 814 } else if(panelShowing) { 815 referencePanel.hidePopup(); 816 referencePanel.sizeTo(window.outerWidth-30, 0); 817 _refocusQfe(); 818 } 819 } 820 821 /** 822 * Opens the reference panel and potentially refocuses the main text box 823 */ 824 function _openReferencePanel() { 825 if(!Zotero.isMac && !Zotero.isWin) { 826 // noautohide and noautofocus are incompatible on Linux 827 // https://bugzilla.mozilla.org/show_bug.cgi?id=545265 828 referencePanel.setAttribute("noautohide", "false"); 829 } 830 831 referencePanel.openPopup(document.documentElement, "after_start", 15, 832 qfb.clientHeight-window.clientHeight, false, false, null); 833 834 if(!Zotero.isMac && !Zotero.isWin) { 835 // reinstate noautohide after the window is shown 836 referencePanel.addEventListener("popupshowing", function() { 837 referencePanel.removeEventListener("popupshowing", arguments.callee, false); 838 referencePanel.setAttribute("noautohide", "true"); 839 }, false); 840 } 841 } 842 843 /** 844 * Clears all citations 845 */ 846 function _clearCitation() { 847 var citations = qfe.getElementsByClassName("quick-format-bubble"); 848 while(citations.length) { 849 citations[0].parentNode.removeChild(citations[0]); 850 } 851 } 852 853 /** 854 * Shows citations in the citation object 855 */ 856 function _showCitation(insertBefore) { 857 if(!io.citation.properties.unsorted 858 && keepSorted.hasAttribute("checked") 859 && io.citation.sortedItems 860 && io.citation.sortedItems.length) { 861 for(var i=0, n=io.citation.sortedItems.length; i<n; i++) { 862 _insertBubble(io.citation.sortedItems[i][1], insertBefore); 863 } 864 } else { 865 for(var i=0, n=io.citation.citationItems.length; i<n; i++) { 866 _insertBubble(io.citation.citationItems[i], insertBefore); 867 } 868 } 869 } 870 871 /** 872 * Populates the citation object 873 */ 874 function _updateCitationObject() { 875 var nodes = qfe.childNodes; 876 io.citation.citationItems = []; 877 for(var i=0, n=nodes.length; i<n; i++) { 878 if(nodes[i].citationItem) io.citation.citationItems.push(nodes[i].citationItem); 879 } 880 881 if(io.sortable) { 882 if(keepSorted.hasAttribute("checked")) { 883 delete io.citation.properties.unsorted; 884 } else { 885 io.citation.properties.unsorted = true; 886 } 887 } 888 } 889 890 /** 891 * Move cursor to end of the textbox 892 */ 893 function _moveCursorToEnd() { 894 var nodeRange = qfiDocument.createRange(); 895 nodeRange.selectNode(qfe.lastChild); 896 nodeRange.collapse(false); 897 898 var selection = qfiWindow.getSelection(); 899 selection.removeAllRanges(); 900 selection.addRange(nodeRange); 901 } 902 903 /** 904 * Generates the preview and sorts citations 905 */ 906 var _previewAndSort = Zotero.Promise.coroutine(function* () { 907 var shouldKeepSorted = keepSorted.hasAttribute("checked"), 908 editorShowing = showEditor.hasAttribute("checked"); 909 if(!shouldKeepSorted && !editorShowing) return; 910 911 _updateCitationObject(); 912 yield io.sort(); 913 if(shouldKeepSorted) { 914 // means we need to resort citations 915 _clearCitation(); 916 _showCitation(); 917 918 // select past last citation 919 var lastBubble = qfe.getElementsByClassName("quick-format-bubble"); 920 lastBubble = lastBubble[lastBubble.length-1]; 921 922 _moveCursorToEnd(); 923 } 924 }); 925 926 /** 927 * Shows the citation properties panel for a given bubble 928 */ 929 function _showCitationProperties(target) { 930 panelRefersToBubble = target; 931 panelPrefix.value = target.citationItem["prefix"] ? target.citationItem["prefix"] : ""; 932 panelSuffix.value = target.citationItem["suffix"] ? target.citationItem["suffix"] : ""; 933 if(target.citationItem["label"]) { 934 var option = panelLocatorLabel.getElementsByAttribute("value", target.citationItem["label"]); 935 if(option.length) { 936 panelLocatorLabel.selectedItem = option[0]; 937 } else { 938 panelLocatorLabel.selectedIndex = 0; 939 } 940 } else { 941 panelLocatorLabel.selectedIndex = 0; 942 } 943 panelLocator.value = target.citationItem["locator"] ? target.citationItem["locator"] : ""; 944 panelSuppressAuthor.checked = !!target.citationItem["suppress-author"]; 945 946 Zotero.Cite.getItem(panelRefersToBubble.citationItem.id).key; 947 948 var item = Zotero.Cite.getItem(target.citationItem.id); 949 document.getElementById("citation-properties-title").textContent = item.getDisplayTitle(); 950 while(panelInfo.hasChildNodes()) panelInfo.removeChild(panelInfo.firstChild); 951 _buildItemDescription(item, panelInfo); 952 953 panelLibraryLink.hidden = !item.id; 954 if(item.id) { 955 var libraryName = item.libraryID ? Zotero.Libraries.getName(item.libraryID) 956 : Zotero.getString('pane.collections.library'); 957 panelLibraryLink.label = Zotero.getString("integration.openInLibrary", libraryName); 958 } 959 960 target.setAttribute("selected", "true"); 961 panel.openPopup(target, "after_start", 962 target.clientWidth/2, 0, false, false, null); 963 panelLocator.focus(); 964 } 965 966 /** 967 * Called when progress changes 968 */ 969 function _onProgress(percent) { 970 var meter = document.getElementById("quick-format-progress-meter"); 971 if(percent === null) { 972 meter.mode = "undetermined"; 973 } else { 974 meter.mode = "determined"; 975 meter.value = Math.round(percent); 976 } 977 } 978 979 /** 980 * Accepts current selection and adds citation 981 */ 982 function _accept() { 983 if(accepted) return; 984 accepted = true; 985 try { 986 _updateCitationObject(); 987 document.getElementById("quick-format-deck").selectedIndex = 1; 988 io.accept(_onProgress); 989 } catch(e) { 990 Zotero.debug(e); 991 } 992 } 993 994 /** 995 * Handles windows closed with the close box 996 */ 997 this.onUnload = function() { 998 if(accepted) return; 999 accepted = true; 1000 io.citation.citationItems = []; 1001 io.accept(); 1002 } 1003 1004 /** 1005 * Handle escape for entire window 1006 */ 1007 this.onKeyPress = function(event) { 1008 var keyCode = event.keyCode; 1009 if(keyCode === event.DOM_VK_ESCAPE && !accepted) { 1010 accepted = true; 1011 io.citation.citationItems = []; 1012 io.accept(); 1013 } 1014 } 1015 1016 /** 1017 * Get bubbles within the current selection 1018 */ 1019 function _getSelectedBubble(right) { 1020 var selection = qfiWindow.getSelection(), 1021 range = selection.getRangeAt(0); 1022 qfe.normalize(); 1023 1024 // Check whether the bubble is selected 1025 // Not sure whether this ever happens anymore 1026 var container = range.startContainer; 1027 if(container !== qfe) { 1028 if(container.citationItem) { 1029 return container; 1030 } else if(container.nodeType === Node.TEXT_NODE && container.wholeText == "") { 1031 if(container.parentNode === qfe) { 1032 var node = container; 1033 while((node = container.previousSibling)) { 1034 if(node.citationItem) { 1035 return node; 1036 } 1037 } 1038 } 1039 } 1040 return null; 1041 } 1042 1043 // Check whether there is a bubble anywhere to the left of this one 1044 var offset = range.startOffset, 1045 childNodes = qfe.childNodes, 1046 node = childNodes[offset-(right ? 0 : 1)]; 1047 if(node && node.citationItem) return node; 1048 return null; 1049 } 1050 1051 /** 1052 * Reset timer that controls when search takes place. We use this to avoid searching after each 1053 * keypress, since searches can be slow. 1054 */ 1055 function _resetSearchTimer() { 1056 // Show spinner 1057 var spinner = document.getElementById('quick-format-spinner'); 1058 spinner.style.visibility = ''; 1059 // Cancel current search if active 1060 if (_searchPromise && _searchPromise.isPending()) { 1061 _searchPromise.cancel(); 1062 } 1063 // Start new search 1064 _searchPromise = Zotero.Promise.delay(SEARCH_TIMEOUT) 1065 .then(() => _quickFormat()) 1066 .then(() => { 1067 _searchPromise = null; 1068 spinner.style.visibility = 'hidden'; 1069 }); 1070 } 1071 1072 /** 1073 * Handle return or escape 1074 */ 1075 var _onQuickSearchKeyPress = Zotero.Promise.coroutine(function* (event) { 1076 // Prevent hang if another key is pressed after Enter 1077 // https://forums.zotero.org/discussion/59157/ 1078 if (accepted) { 1079 event.preventDefault(); 1080 return; 1081 } 1082 if(qfGuidance) qfGuidance.hide(); 1083 1084 var keyCode = event.keyCode; 1085 if (keyCode === event.DOM_VK_RETURN) { 1086 event.preventDefault(); 1087 if(!(yield _bubbleizeSelected()) && !_getEditorContent()) { 1088 _accept(); 1089 } 1090 } else if(keyCode === event.DOM_VK_TAB || event.charCode === 59 /* ; */) { 1091 event.preventDefault(); 1092 _bubbleizeSelected(); 1093 } else if(keyCode === event.DOM_VK_BACK_SPACE || keyCode === event.DOM_VK_DELETE) { 1094 var bubble = _getSelectedBubble(keyCode === event.DOM_VK_DELETE); 1095 1096 if(bubble) { 1097 event.preventDefault(); 1098 bubble.parentNode.removeChild(bubble); 1099 } 1100 1101 _resize(); 1102 _resetSearchTimer(); 1103 } else if(keyCode === event.DOM_VK_LEFT || keyCode === event.DOM_VK_RIGHT) { 1104 var right = keyCode === event.DOM_VK_RIGHT, 1105 bubble = _getSelectedBubble(right); 1106 if(bubble) { 1107 event.preventDefault(); 1108 1109 var nodeRange = qfiDocument.createRange(); 1110 nodeRange.selectNode(bubble); 1111 nodeRange.collapse(!right); 1112 1113 var selection = qfiWindow.getSelection(); 1114 selection.removeAllRanges(); 1115 selection.addRange(nodeRange); 1116 } 1117 1118 } else if(keyCode === event.DOM_VK_UP && referencePanel.state === "open") { 1119 var selectedItem = referenceBox.selectedItem; 1120 1121 var previousSibling; 1122 1123 // Seek the closet previous sibling that is not disabled 1124 while((previousSibling = selectedItem.previousSibling) && previousSibling.hasAttribute("disabled")) { 1125 selectedItem = previousSibling; 1126 } 1127 // If found, change to that 1128 if(previousSibling) { 1129 referenceBox.selectedItem = previousSibling; 1130 1131 // If there are separators before this item, ensure that they are visible 1132 var visibleItem = previousSibling; 1133 1134 while(visibleItem.previousSibling && visibleItem.previousSibling.hasAttribute("disabled")) { 1135 visibleItem = visibleItem.previousSibling; 1136 } 1137 referenceBox.ensureElementIsVisible(visibleItem); 1138 }; 1139 event.preventDefault(); 1140 } else if(keyCode === event.DOM_VK_DOWN) { 1141 if((Zotero.isMac ? event.metaKey : event.ctrlKey)) { 1142 // If meta key is held down, show the citation properties panel 1143 var bubble = _getSelectedBubble(); 1144 1145 if(bubble) _showCitationProperties(bubble); 1146 event.preventDefault(); 1147 } else if (referencePanel.state === "open") { 1148 var selectedItem = referenceBox.selectedItem; 1149 var nextSibling; 1150 1151 // Seek the closet next sibling that is not disabled 1152 while((nextSibling = selectedItem.nextSibling) && nextSibling.hasAttribute("disabled")) { 1153 selectedItem = nextSibling; 1154 } 1155 1156 // If found, change to that 1157 if(nextSibling){ 1158 referenceBox.selectedItem = nextSibling; 1159 referenceBox.ensureElementIsVisible(nextSibling); 1160 }; 1161 event.preventDefault(); 1162 } 1163 } else { 1164 _resetSearchTimer(); 1165 } 1166 }); 1167 1168 /** 1169 * Adds a dummy element to make dragging work 1170 */ 1171 function _onBubbleDrag(event) { 1172 dragging = event.currentTarget; 1173 event.dataTransfer.setData("text/plain", '<span id="zotero-drag"/>'); 1174 event.stopPropagation(); 1175 } 1176 1177 /** 1178 * Get index of bubble in citations 1179 */ 1180 function _getBubbleIndex(bubble) { 1181 var nodes = qfe.childNodes, oldPosition = -1, index = 0; 1182 for(var i=0, n=nodes.length; i<n; i++) { 1183 if(nodes[i].citationItem) { 1184 if(nodes[i] == bubble) return index; 1185 index++; 1186 } 1187 } 1188 return -1; 1189 } 1190 1191 /** 1192 * Replaces the dummy element with a node to make dropping work 1193 */ 1194 var _onBubbleDrop = Zotero.Promise.coroutine(function* (event) { 1195 event.preventDefault(); 1196 event.stopPropagation(); 1197 1198 var range = document.createRange(); 1199 1200 // Find old position in list 1201 var oldPosition = _getBubbleIndex(dragging); 1202 range.setStart(event.rangeParent, event.rangeOffset); 1203 dragging.parentNode.removeChild(dragging); 1204 var bubble = _insertBubble(dragging.citationItem, range); 1205 1206 // If moved out of order, turn off "Keep Sources Sorted" 1207 if(io.sortable && keepSorted.hasAttribute("checked") && oldPosition !== -1 && 1208 oldPosition != _getBubbleIndex(bubble)) { 1209 keepSorted.removeAttribute("checked"); 1210 } 1211 1212 yield _previewAndSort(); 1213 _moveCursorToEnd(); 1214 }); 1215 1216 /** 1217 * Handle a click on a bubble 1218 */ 1219 function _onBubbleClick(event) { 1220 _moveCursorToEnd(); 1221 _showCitationProperties(event.currentTarget); 1222 } 1223 1224 /** 1225 * Called when the user attempts to paste 1226 */ 1227 function _onPaste(event) { 1228 event.stopPropagation(); 1229 event.preventDefault(); 1230 1231 var str = Zotero.Utilities.Internal.getClipboard("text/unicode"); 1232 if(str) { 1233 var selection = qfiWindow.getSelection(); 1234 var range = selection.getRangeAt(0); 1235 range.deleteContents(); 1236 range.insertNode(document.createTextNode(str.replace(/[\r\n]/g, " ").trim())); 1237 range.collapse(false); 1238 _resetSearchTimer(); 1239 } 1240 } 1241 1242 /** 1243 * Handle changes to citation properties 1244 */ 1245 this.onCitationPropertiesChanged = function(event) { 1246 if(panelPrefix.value) { 1247 panelRefersToBubble.citationItem["prefix"] = panelPrefix.value; 1248 } else { 1249 delete panelRefersToBubble.citationItem["prefix"]; 1250 } 1251 if(panelSuffix.value) { 1252 panelRefersToBubble.citationItem["suffix"] = panelSuffix.value; 1253 } else { 1254 delete panelRefersToBubble.citationItem["suffix"]; 1255 } 1256 if(panelLocatorLabel.selectedIndex !== 0) { 1257 panelRefersToBubble.citationItem["label"] = panelLocatorLabel.selectedItem.value; 1258 } else { 1259 delete panelRefersToBubble.citationItem["label"]; 1260 } 1261 if(panelLocator.value) { 1262 panelRefersToBubble.citationItem["locator"] = panelLocator.value; 1263 } else { 1264 delete panelRefersToBubble.citationItem["locator"]; 1265 } 1266 if(panelSuppressAuthor.checked) { 1267 panelRefersToBubble.citationItem["suppress-author"] = true; 1268 } else { 1269 delete panelRefersToBubble.citationItem["suppress-author"]; 1270 } 1271 panelRefersToBubble.textContent = _buildBubbleString(panelRefersToBubble.citationItem); 1272 }; 1273 1274 /** 1275 * Handle closing citation properties panel 1276 */ 1277 this.onCitationPropertiesClosed = function(event) { 1278 panelRefersToBubble.removeAttribute("selected"); 1279 Zotero_QuickFormat.onCitationPropertiesChanged(); 1280 } 1281 1282 /** 1283 * Makes "Enter" work in the panel 1284 */ 1285 this.onPanelKeyPress = function(event) { 1286 var keyCode = event.keyCode; 1287 if (keyCode === event.DOM_VK_RETURN) { 1288 document.getElementById("citation-properties").hidePopup(); 1289 } 1290 }; 1291 1292 /** 1293 * Handle checking/unchecking "Keep Citations Sorted" 1294 */ 1295 this.onKeepSortedCommand = function(event) { 1296 _previewAndSort(); 1297 }; 1298 1299 /** 1300 * Open classic Add Citation window 1301 */ 1302 this.onClassicViewCommand = function(event) { 1303 _updateCitationObject(); 1304 var newWindow = window.newWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 1305 .getService(Components.interfaces.nsIWindowWatcher) 1306 .openWindow(null, 'chrome://zotero/content/integration/addCitationDialog.xul', 1307 '', 'chrome,centerscreen,resizable', io); 1308 newWindow.addEventListener("focus", function() { 1309 newWindow.removeEventListener("focus", arguments.callee, true); 1310 window.close(); 1311 }, true); 1312 accepted = true; 1313 } 1314 1315 /** 1316 * Show an item in the library it came from 1317 */ 1318 this.showInLibrary = async function() { 1319 var id = panelRefersToBubble.citationItem.id; 1320 var pane = Zotero.getActiveZoteroPane(); 1321 // Open main window if it's not open (Mac) 1322 if (!pane) { 1323 let win = Zotero.openMainWindow(); 1324 await new Zotero.Promise((resolve) => { 1325 let onOpen = function () { 1326 win.removeEventListener('load', onOpen); 1327 resolve(); 1328 }; 1329 win.addEventListener('load', onOpen); 1330 }); 1331 pane = win.ZoteroPane; 1332 } 1333 pane.show(); 1334 pane.selectItem(id); 1335 1336 // Pull window to foreground 1337 Zotero.Utilities.Internal.activate(pane.document.defaultView); 1338 } 1339 1340 /** 1341 * Resizes windows 1342 * @constructor 1343 */ 1344 var Resizer = function(panel, targetWidth, targetHeight, pixelsPerStep, stepsPerSecond) { 1345 this.panel = panel; 1346 this.curWidth = panel.clientWidth; 1347 this.curHeight = panel.clientHeight; 1348 this.difX = (targetWidth ? targetWidth - this.curWidth : 0); 1349 this.difY = (targetHeight ? targetHeight - this.curHeight : 0); 1350 this.step = 0; 1351 this.steps = Math.ceil(Math.max(Math.abs(this.difX), Math.abs(this.difY))/pixelsPerStep); 1352 this.timeout = (1000/stepsPerSecond); 1353 1354 var me = this; 1355 this._animateCallback = function() { me.animate() }; 1356 }; 1357 1358 /** 1359 * Performs a step of the animation 1360 */ 1361 Resizer.prototype.animate = function() { 1362 if(this.stopped) return; 1363 this.step++; 1364 this.panel.sizeTo(this.curWidth+Math.round(this.step*this.difX/this.steps), 1365 this.curHeight+Math.round(this.step*this.difY/this.steps)); 1366 if(this.step !== this.steps) { 1367 window.setTimeout(this._animateCallback, this.timeout); 1368 } 1369 }; 1370 1371 /** 1372 * Halts resizing 1373 */ 1374 Resizer.prototype.stop = function() { 1375 this.stopped = true; 1376 }; 1377 } 1378 1379 window.addEventListener("DOMContentLoaded", Zotero_QuickFormat.onDOMContentLoaded, false); 1380 window.addEventListener("load", Zotero_QuickFormat.onLoad, false);