/* ***** BEGIN LICENSE BLOCK ***** Copyright © 2011 Center for History and New Media George Mason University, Fairfax, Virginia, USA http://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 . ***** END LICENSE BLOCK ***** */ Components.utils.import("resource://gre/modules/Services.jsm"); var Zotero_QuickFormat = new function () { const pixelRe = /^([0-9]+)px$/ const specifiedLocatorRe = /^(?:,? *(p{0,2})(?:\. *| +)|:)([0-9\-]+) *$/; const yearRe = /,? *([0-9]+) *(B[. ]*C[. ]*(?:E[. ]*)?|A[. ]*D[. ]*|C[. ]*E[. ]*)?$/i; const locatorRe = /(?:,? *(p{0,2})\.?|(\:)) *([0-9\-–]+)$/i; const creatorSplitRe = /(?:,| *(?:and|\&)) +/; const charRe = /[\w\u007F-\uFFFF]/; const numRe = /^[0-9\-–]+$/; var initialized, io, qfs, qfi, qfiWindow, qfiDocument, qfe, qfb, qfbHeight, qfGuidance, keepSorted, showEditor, referencePanel, referenceBox, referenceHeight = 0, separatorHeight = 0, currentLocator, currentLocatorLabel, currentSearchTime, dragging, panel, panelPrefix, panelSuffix, panelSuppressAuthor, panelLocatorLabel, panelLocator, panelLibraryLink, panelInfo, panelRefersToBubble, panelFrameHeight = 0, accepted = false; var _searchPromise; const SEARCH_TIMEOUT = 250; const SHOWN_REFERENCES = 7; /** * Pre-initialization, when the dialog has loaded but has not yet appeared */ this.onDOMContentLoaded = function(event) { if(event.target === document) { initialized = true; io = window.arguments[0].wrappedJSObject; // Only hide chrome on Windows or Mac if(Zotero.isMac) { document.documentElement.setAttribute("drawintitlebar", true); } else if(Zotero.isWin) { document.documentElement.setAttribute("hidechrome", true); } // Include a different key combo in message on Mac if(Zotero.isMac) { var qf = document.getElementById('quick-format-guidance'); qf.setAttribute('about', qf.getAttribute('about') + "Mac"); } new WindowDraggingElement(document.getElementById("quick-format-dialog"), window); qfs = document.getElementById("quick-format-search"); qfi = document.getElementById("quick-format-iframe"); qfb = document.getElementById("quick-format-entry"); qfbHeight = qfb.scrollHeight; referencePanel = document.getElementById("quick-format-reference-panel"); referenceBox = document.getElementById("quick-format-reference-list"); if(Zotero.isWin && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')) { qfb.setAttribute("square", "true"); } // add labels to popup var locators = Zotero.Cite.labels; var menu = document.getElementById("locator-label"); var labelList = document.getElementById("locator-label-popup"); for(var locator of locators) { var locatorLabel = Zotero.getString('citation.locator.'+locator.replace(/\s/g,'')); // add to list of labels var child = document.createElement("menuitem"); child.setAttribute("value", locator); child.setAttribute("label", locatorLabel); labelList.appendChild(child); } menu.selectedIndex = 0; keepSorted = document.getElementById("keep-sorted"); showEditor = document.getElementById("show-editor"); if(io.sortable) { keepSorted.hidden = false; if(!io.citation.properties.unsorted) { keepSorted.setAttribute("checked", "true"); } } // Nodes for citation properties panel panel = document.getElementById("citation-properties"); panelPrefix = document.getElementById("prefix"); panelSuffix = document.getElementById("suffix"); panelSuppressAuthor = document.getElementById("suppress-author"); panelLocatorLabel = document.getElementById("locator-label"); panelLocator = document.getElementById("locator"); panelInfo = document.getElementById("citation-properties-info"); panelLibraryLink = document.getElementById("citation-properties-library-link"); // Don't need to set noautohide dynamically on these platforms, so do it now if(Zotero.isMac || Zotero.isWin) { referencePanel.setAttribute("noautohide", true); } } else if(event.target === qfi.contentDocument) { qfiWindow = qfi.contentWindow; qfiDocument = qfi.contentDocument; qfb.addEventListener("keypress", _onQuickSearchKeyPress, false); qfe = qfiDocument.getElementById("quick-format-editor"); qfe.addEventListener("drop", _onBubbleDrop, false); qfe.addEventListener("paste", _onPaste, false); } } /** * Initialize add citation dialog */ this.onLoad = function(event) { if(event.target !== document) return; // make sure we are visible window.setTimeout(function() { window.resizeTo(window.outerWidth, qfb.clientHeight); var screenX = window.screenX; var screenY = window.screenY; var xRange = [window.screen.availLeft, window.screen.width-window.outerWidth]; var yRange = [window.screen.availTop, window.screen.height-window.outerHeight]; if(screenX < xRange[0] || screenX > xRange[1] || screenY < yRange[0] || screenY > yRange[1]) { var targetX = Math.max(Math.min(screenX, xRange[1]), xRange[0]); var targetY = Math.max(Math.min(screenY, yRange[1]), yRange[0]); Zotero.debug("Moving window to "+targetX+", "+targetY); window.moveTo(targetX, targetY); } qfGuidance = document.getElementById('quick-format-guidance'); qfGuidance.show(); _refocusQfe(); }, 0); window.focus(); qfe.focus(); // load citation data if(io.citation.citationItems.length) { // hack to get spacing right var evt = qfiDocument.createEvent("KeyboardEvent"); evt.initKeyEvent("keypress", true, true, qfiWindow, 0, 0, 0, 0, 0, " ".charCodeAt(0)) qfe.dispatchEvent(evt); window.setTimeout(function() { var node = qfe.firstChild; node.nodeValue = ""; _showCitation(node); _resize(); }, 1); } }; function _refocusQfe() { referencePanel.blur(); window.focus(); qfe.focus(); } /** * Gets the content of the text node that the cursor is currently within */ function _getCurrentEditorTextNode() { var selection = qfiWindow.getSelection(); var range = selection.getRangeAt(0); var node = range.startContainer; if(node !== range.endContainer) return false; if(node.nodeType === Node.TEXT_NODE) return node; // Range could be referenced to the body element if(node === qfe) { var offset = range.startOffset; if(offset !== range.endOffset) return false; node = qfe.childNodes[Math.min(qfe.childNodes.length-1, offset)]; if(node.nodeType === Node.TEXT_NODE) return node; } return false; } /** * Gets text within the currently selected node * @param {Boolean} [clear] If true, also remove these nodes */ function _getEditorContent(clear) { var node = _getCurrentEditorTextNode(); return node ? node.wholeText : false; } /** * Does the dirty work of figuring out what the user meant to type */ var _quickFormat = Zotero.Promise.coroutine(function* () { var str = _getEditorContent(); var haveConditions = false; const etAl = " et al."; var m, year = false, isBC = false, dateID = false; currentLocator = false; currentLocatorLabel = false; // check for adding a number onto a previous page number if(numRe.test(str)) { // add to previous cite var node = _getCurrentEditorTextNode(); var prevNode = node.previousSibling; if(prevNode && prevNode.citationItem && prevNode.citationItem.locator) { prevNode.citationItem.locator += str; prevNode.textContent = _buildBubbleString(prevNode.citationItem); node.nodeValue = ""; _clearEntryList(); return; } } if(str && str.length > 1) { // check for specified locator m = specifiedLocatorRe.exec(str); if(m) { if(m.index === 0) { // add to previous cite var node = _getCurrentEditorTextNode(); var prevNode = node.previousSibling; if(prevNode && prevNode.citationItem) { prevNode.citationItem.locator = m[2]; prevNode.textContent = _buildBubbleString(prevNode.citationItem); node.nodeValue = ""; _clearEntryList(); return; } } // TODO support types other than page currentLocator = m[2]; str = str.substring(0, m.index); } // check for year and pages str = _updateLocator(str); m = yearRe.exec(str); if(m) { year = parseInt(m[1]); isBC = m[2] && m[2][0] === "B"; str = str.substr(0, m.index)+str.substring(m.index+m[0].length); } if(year) str += " "+year; var s = new Zotero.Search(); str = str.replace(/ (?:&|and) /g, " ", "g"); if(charRe.test(str)) { Zotero.debug("QuickFormat: QuickSearch: "+str); // Exclude feeds Zotero.Feeds.getAll() .forEach(feed => s.addCondition("libraryID", "isNot", feed.libraryID)); s.addCondition("quicksearch-titleCreatorYear", "contains", str); s.addCondition("itemType", "isNot", "attachment"); haveConditions = true; } } if(haveConditions) { var searchResultIDs = (haveConditions ? (yield s.search()) : []); // Show items list without cited items to start yield _updateItemList(false, false, str, searchResultIDs); // Check to see which search results match items already in the document var citedItems, completed = false, isAsync = false; // Save current search time so that when we get items, we know whether it's too late to // process them or not var lastSearchTime = currentSearchTime = Date.now(); // This may or may not be synchronous io.getItems().then(function(citedItems) { // Don't do anything if panel is already closed if(isAsync && ((referencePanel.state !== "open" && referencePanel.state !== "showing") || lastSearchTime !== currentSearchTime)) return; completed = true; if(str.toLowerCase() === Zotero.getString("integration.ibid").toLowerCase()) { // If "ibid" is entered, show all cited items citedItemsMatchingSearch = citedItems; } else { Zotero.debug("Searching cited items"); // Search against items. We do this here because it's possible that some of these // items are only in the doc, and not in the DB. var splits = Zotero.Fulltext.semanticSplitter(str), citedItemsMatchingSearch = []; for(var i=0, iCount=citedItems.length; i creator.firstName + " " + creator.lastName) .concat([item.getField("title"), item.getField("date", true, true).substr(0, 4)]) .join(" "); // See if words match for(var j=0, jCount=splits.length; js var elements = qfe.getElementsByTagName("br"); while(elements.length) { elements[0].parentNode.removeChild(elements[0]); } return bubble; } /** * Clear list of bubbles */ function _clearEntryList() { while(referenceBox.hasChildNodes()) referenceBox.removeChild(referenceBox.firstChild); _resize(); } /** * Converts the selected item to a bubble */ function _bubbleizeSelected() { if(!referenceBox.hasChildNodes() || !referenceBox.selectedItem) return false; var citationItem = {"id":referenceBox.selectedItem.getAttribute("zotero-item")}; if(typeof citationItem.id === "string" && citationItem.id.indexOf("/") !== -1) { var item = Zotero.Cite.getItem(citationItem.id); citationItem.uris = item.cslURIs; citationItem.itemData = item.cslItemData; } _updateLocator(_getEditorContent()); if(currentLocator) { citationItem["locator"] = currentLocator; if(currentLocatorLabel) { citationItem["label"] = currentLocatorLabel; } } // get next node and clear this one var node = _getCurrentEditorTextNode(); node.nodeValue = ""; var bubble = _insertBubble(citationItem, node); _clearEntryList(); _previewAndSort(); _refocusQfe(); return true; } /** * Ignores clicks (for use on separators in the rich list box) */ function _ignoreClick(e) { e.stopPropagation(); e.preventDefault(); } /** * Resizes window to fit content */ function _resize() { var childNodes = referenceBox.childNodes, numReferences = 0, numSeparators = 0, firstReference, firstSeparator, height; for(var i=0, n=childNodes.length; i 30) { qfe.setAttribute("multiline", true); qfs.setAttribute("multiline", true); qfs.style.height = ((Zotero.isMac ? 6 : 4)+qfe.scrollHeight)+"px"; window.sizeToContent(); } else { delete qfs.style.height; qfe.removeAttribute("multiline"); qfs.removeAttribute("multiline"); window.sizeToContent(); } var panelShowing = referencePanel.state === "open" || referencePanel.state === "showing"; if(numReferences || numSeparators) { if(((!referenceHeight && firstReference) || (!separatorHeight && firstSeparator) || !panelFrameHeight) && !panelShowing) { _openReferencePanel(); panelShowing = true; } if(!referenceHeight && firstReference) { referenceHeight = firstReference.scrollHeight + 1; } if(!separatorHeight && firstSeparator) { separatorHeight = firstSeparator.scrollHeight + 1; } if(!panelFrameHeight) { panelFrameHeight = referencePanel.boxObject.height - referencePanel.clientHeight; var computedStyle = window.getComputedStyle(referenceBox, null); for(var attr of ["border-top-width", "border-bottom-width"]) { var val = computedStyle.getPropertyValue(attr); if(val) { var m = pixelRe.exec(val); if(m) panelFrameHeight += parseInt(m[1], 10); } } } referencePanel.sizeTo(window.outerWidth-30, numReferences*referenceHeight+numSeparators*separatorHeight+panelFrameHeight); if(!panelShowing) _openReferencePanel(); } else if(panelShowing) { referencePanel.hidePopup(); referencePanel.sizeTo(window.outerWidth-30, 0); _refocusQfe(); } } /** * Opens the reference panel and potentially refocuses the main text box */ function _openReferencePanel() { if(!Zotero.isMac && !Zotero.isWin) { // noautohide and noautofocus are incompatible on Linux // https://bugzilla.mozilla.org/show_bug.cgi?id=545265 referencePanel.setAttribute("noautohide", "false"); } referencePanel.openPopup(document.documentElement, "after_start", 15, qfb.clientHeight-window.clientHeight, false, false, null); if(!Zotero.isMac && !Zotero.isWin) { // reinstate noautohide after the window is shown referencePanel.addEventListener("popupshowing", function() { referencePanel.removeEventListener("popupshowing", arguments.callee, false); referencePanel.setAttribute("noautohide", "true"); }, false); } } /** * Clears all citations */ function _clearCitation() { var citations = qfe.getElementsByClassName("quick-format-bubble"); while(citations.length) { citations[0].parentNode.removeChild(citations[0]); } } /** * Shows citations in the citation object */ function _showCitation(insertBefore) { if(!io.citation.properties.unsorted && keepSorted.hasAttribute("checked") && io.citation.sortedItems && io.citation.sortedItems.length) { for(var i=0, n=io.citation.sortedItems.length; i _quickFormat()) .then(() => { _searchPromise = null; spinner.style.visibility = 'hidden'; }); } /** * Handle return or escape */ function _onQuickSearchKeyPress(event) { // Prevent hang if another key is pressed after Enter // https://forums.zotero.org/discussion/59157/ if (accepted) { event.preventDefault(); return; } if(qfGuidance) qfGuidance.hide(); var keyCode = event.keyCode; if (keyCode === event.DOM_VK_RETURN) { event.preventDefault(); if(!_bubbleizeSelected() && !_getEditorContent()) { _accept(); } } else if(keyCode === event.DOM_VK_TAB || event.charCode === 59 /* ; */) { event.preventDefault(); _bubbleizeSelected(); } else if(keyCode === event.DOM_VK_BACK_SPACE || keyCode === event.DOM_VK_DELETE) { var bubble = _getSelectedBubble(keyCode === event.DOM_VK_DELETE); if(bubble) { event.preventDefault(); bubble.parentNode.removeChild(bubble); } _resize(); _resetSearchTimer(); } else if(keyCode === event.DOM_VK_LEFT || keyCode === event.DOM_VK_RIGHT) { var right = keyCode === event.DOM_VK_RIGHT, bubble = _getSelectedBubble(right); if(bubble) { event.preventDefault(); var nodeRange = qfiDocument.createRange(); nodeRange.selectNode(bubble); nodeRange.collapse(!right); var selection = qfiWindow.getSelection(); selection.removeAllRanges(); selection.addRange(nodeRange); } } else if(keyCode === event.DOM_VK_UP && referencePanel.state === "open") { var selectedItem = referenceBox.selectedItem; var previousSibling; // Seek the closet previous sibling that is not disabled while((previousSibling = selectedItem.previousSibling) && previousSibling.hasAttribute("disabled")) { selectedItem = previousSibling; } // If found, change to that if(previousSibling) { referenceBox.selectedItem = previousSibling; // If there are separators before this item, ensure that they are visible var visibleItem = previousSibling; while(visibleItem.previousSibling && visibleItem.previousSibling.hasAttribute("disabled")) { visibleItem = visibleItem.previousSibling; } referenceBox.ensureElementIsVisible(visibleItem); }; event.preventDefault(); } else if(keyCode === event.DOM_VK_DOWN) { if((Zotero.isMac ? event.metaKey : event.ctrlKey)) { // If meta key is held down, show the citation properties panel var bubble = _getSelectedBubble(); if(bubble) _showCitationProperties(bubble); event.preventDefault(); } else if (referencePanel.state === "open") { var selectedItem = referenceBox.selectedItem; var nextSibling; // Seek the closet next sibling that is not disabled while((nextSibling = selectedItem.nextSibling) && nextSibling.hasAttribute("disabled")) { selectedItem = nextSibling; } // If found, change to that if(nextSibling){ referenceBox.selectedItem = nextSibling; referenceBox.ensureElementIsVisible(nextSibling); }; event.preventDefault(); } } else { _resetSearchTimer(); } } /** * Adds a dummy element to make dragging work */ function _onBubbleDrag(event) { dragging = event.currentTarget; event.dataTransfer.setData("text/plain", ''); event.stopPropagation(); } /** * Get index of bubble in citations */ function _getBubbleIndex(bubble) { var nodes = qfe.childNodes, oldPosition = -1, index = 0; for(var i=0, n=nodes.length; i { let onOpen = function () { win.removeEventListener('load', onOpen); resolve(); }; win.addEventListener('load', onOpen); }); pane = win.ZoteroPane; } pane.show(); pane.selectItem(id); // Pull window to foreground Zotero.Integration.activate(pane.document.defaultView); } /** * Resizes windows * @constructor */ var Resizer = function(panel, targetWidth, targetHeight, pixelsPerStep, stepsPerSecond) { this.panel = panel; this.curWidth = panel.clientWidth; this.curHeight = panel.clientHeight; this.difX = (targetWidth ? targetWidth - this.curWidth : 0); this.difY = (targetHeight ? targetHeight - this.curHeight : 0); this.step = 0; this.steps = Math.ceil(Math.max(Math.abs(this.difX), Math.abs(this.difY))/pixelsPerStep); this.timeout = (1000/stepsPerSecond); var me = this; this._animateCallback = function() { me.animate() }; }; /** * Performs a step of the animation */ Resizer.prototype.animate = function() { if(this.stopped) return; this.step++; this.panel.sizeTo(this.curWidth+Math.round(this.step*this.difX/this.steps), this.curHeight+Math.round(this.step*this.difY/this.steps)); if(this.step !== this.steps) { window.setTimeout(this._animateCallback, this.timeout); } }; /** * Halts resizing */ Resizer.prototype.stop = function() { this.stopped = true; }; } window.addEventListener("DOMContentLoaded", Zotero_QuickFormat.onDOMContentLoaded, false); window.addEventListener("load", Zotero_QuickFormat.onLoad, false);