Repository: zotero/zotero (JavaScript)

Issue:
Citation dialog: Change `last-closed` pref value to `last-used`
I think I made a comment about this, but it looks like it didn't happen. The pref value should match what we're calling it in the UI.
Add a pref migration to set library mode as last-used for people using the classic dialog
In prefs.js, add a pref migration so that if `integration.useClassicAddCitationDialog` is true, `integration.citationDialogLastClosedMode` is to set to `library`. Then clear `integration.useClassicAddCitationDialog`.


Discussion: Sorry about that - it must have gotten lost during rebasing and squashing
And `citationDialogLastClosedMode` too





Fix (patch):
diff --git a/chrome/content/zotero/integration/citationDialog.js b/chrome/content/zotero/integration/citationDialog.js
index 349734332f8..3ec70d94354 100644
--- a/chrome/content/zotero/integration/citationDialog.js
+++ b/chrome/content/zotero/integration/citationDialog.js
@@ -30,6 +30,9 @@ const { COLUMNS } = require('zotero/itemTreeColumns');
 
 var doc, io, isCitingNotes, accepted;
 
+// used for tests
+var loaded = false;
+
 var currentLayout, libraryLayout, listLayout;
 
 var Helpers, SearchHandler, PopupsHandler, KeyboardHandler;
@@ -106,6 +109,7 @@ async function onLoad() {
 			delete multiselectable.dataset.multiselectable;
 		}
 	}
+	loaded = true;
 }
 
 
@@ -133,15 +137,19 @@ function accept() {
 function cancel() {
 	if (accepted) return;
 	accepted = true;
-	io.citation.citationItems = [];
 	cleanupBeforeDialogClosing();
-	io.accept();
+	io.cancel();
 	window.close();
 }
+// handle dialog being cancelled via window.close() (e.g. clicking X icon on windows)
+function onUnload() {
+	if (this.accepted) return;
+	cancel();
+}
 
 function cleanupBeforeDialogClosing() {
 	if (!currentLayout || !libraryLayout) return;
-	Zotero.Prefs.set("integration.citationDialogLastClosedMode", currentLayout.type);
+	Zotero.Prefs.set("integration.citationDialogLastUsedMode", currentLayout.type);
 	if (currentLayout.type == "library") {
 		Zotero.Prefs.set("integration.citationDialogCollectionLastSelected", libraryLayout.collectionsView.selectedTreeRow.id);
 	}
@@ -1191,8 +1199,8 @@ const IOManager = {
 	// Set the initial dialog mode per user's preference
 	setInitialDialogMode() {
 		let desiredMode = Zotero.Prefs.get("integration.citationDialogMode");
-		if (desiredMode == "last-closed") {
-			desiredMode = Zotero.Prefs.get("integration.citationDialogLastClosedMode");
+		if (desiredMode == "last-used") {
+			desiredMode = Zotero.Prefs.get("integration.citationDialogLastUsedMode");
 		}
 		// When the dialog is opened for the very first time, default to list mode
 		if (!desiredMode) {
@@ -1583,6 +1591,7 @@ const CitationDataManager = {
 
 // Top level listeners
 window.addEventListener("load", onLoad);
+window.addEventListener("unload", onUnload);
 // When the dialog is re-focused, run the search again in case selected or opened items changed
 let windowLostFocusOn = 0;
 window.addEventListener("blur", () => {
diff --git a/chrome/content/zotero/integration/citationDialog/helpers.mjs b/chrome/content/zotero/integration/citationDialog/helpers.mjs
index 4e4e1339abc..6e226f949d4 100644
--- a/chrome/content/zotero/integration/citationDialog/helpers.mjs
+++ b/chrome/content/zotero/integration/citationDialog/helpers.mjs
@@ -183,46 +183,92 @@ export class CitationDialogHelpers {
 		return itemNode;
 	}
 
-	// Extract locator from a string and return an object: { label: string, page: string, onlyLocator: bool}
-	// to identify the locator and pass that info to the dialog
+	// Extract locator from a string and return an object: { label: string, page: string, fullLocatorString:string, onlyLocator: bool}
+	// to identify the locator and pass that info to the dialog. If no locator is found, return null.
+	// Locator can be given in its full or short form (with or without trailing period).
+	// Locator must be followed by numbers (e.g. page 10-30) or a string in quotes (e.g. chapter "one two three").
+	// Locators are excluded from the search query, so quotes are required for textual locators to
+	// avoid conflicts with actual search terms.
+	// Whitespace between the locator and the value is not required. These are equivalent:
+	// p10-15, p.10-15, p. 10-15, p 10-15, page10-15, page 10-15, p."testing testing", page "testing testing"
+	// Locators should always be in the end of the string, to avoid conflicts with actual search terms,
+	// so page10-15 in 'history of the US page10-15 and something else' will not be treated as a locator.
 	extractLocator(string) {
-		// Check for different ways of typing the page locator
-		const pageRegex = /^(?:,? *(p{1,2})(?:\. *| *)|:)([0-9\-–]+) *$/;
-		let pageLocator = pageRegex.exec(string);
-		if (pageLocator && pageLocator.length) {
-			return {
-				label: "page",
-				locator: pageLocator[2],
-				onlyLocator: pageLocator[0].length == string.length,
-				fullLocatorString: pageLocator[0]
-			};
-		}
-		// Check for a generalized way of typing any other locator in full or short form
-		// Capture the first word (e.g. "act") followed by optional : or . with any number of whitespaces.
-		// Then, capture either any text surrounded with " or ' (e.g. book: "Book title")
-		// or just any word (e.g. l. 10)
-		const generalRegex = /(\w+)\s*[:.]?\s*(?:(['"])(.*?)\2|(\w+))$/;
-		let generalLocator = generalRegex.exec(string);
-		if (generalLocator?.length) {
-			let typedLocatorLabel = generalLocator[1].toLowerCase();
-			let existingLocators = Zotero.Cite.labels;
-			for (let existingLocator of existingLocators) {
-				let locatorLabel = Zotero.Cite.getLocatorString(existingLocator).toLowerCase();
-				// strip short locator labels of punctuation, so that e.g. for line locator, "l 10" is still counted as "l. 10"
-				let locatorLabelShort = Zotero.Cite.getLocatorString(existingLocator, "short").replace(/[.,;:{}()]/g, "").toLowerCase();
-				if (typedLocatorLabel == locatorLabel || typedLocatorLabel == locatorLabelShort) {
-					// fetch either text in quotes or the last word without quotes as locator value
-					let locatorValue = generalLocator[3] || generalLocator[4];
-					return {
-						label: existingLocator,
-						locator: locatorValue,
-						onlyLocator: generalLocator[0].length == string.length,
-						fullLocatorString: generalLocator[0]
-					};
+		string = string.trim();
+		let words = string.split(" ");
+		let locatorLabel, locatorLabelString, locatorValue;
+		let wordLocatorIndex = 0;
+		// Go through every word
+		for (let word of words) {
+			word = word.toLowerCase();
+			// Check if the current word has a locator label
+			for (let labelCandidate of Zotero.Cite.labels) {
+				let { fullLocator, shortLocator, shortLocatorNoPunctuation } = this.getLocatorLabels(labelCandidate);
+				// Potential locator value is a substring from the current word till the end
+				let potentialLocatorValue = words.slice(wordLocatorIndex).join(" ").trim();
+
+				// Check if this word has a locator label in its full or short form (e.g. "line" or "l" or "l.")
+				// If a locator string is found, check if the string without it is a valid locator value.
+				if (potentialLocatorValue.startsWith(fullLocator)) {
+					// e.g. 'US history chapter "one and two" '
+					locatorLabelString = fullLocator;
+					// potential locator value: "one and two"
+					potentialLocatorValue = potentialLocatorValue.substring(fullLocator.length).trim();
+				}
+				else if (potentialLocatorValue.startsWith(shortLocator)) {
+					// e.g. 'US history chapt."one and two" '
+					locatorLabelString = shortLocator;
+					// potential locator value: "one and two"
+					potentialLocatorValue = potentialLocatorValue.substring(shortLocator.length).trim();
+				}
+				else if (potentialLocatorValue.startsWith(shortLocatorNoPunctuation)) {
+					// e.g. 'US history chapt11-12 '
+					locatorLabelString = shortLocatorNoPunctuation;
+					// potential locator value: 11-12
+					potentialLocatorValue = potentialLocatorValue.substring(shortLocatorNoPunctuation.length).trim();
+				}
+				else {
+					continue;
+				}
+
+				// At this point, potentialLocatorValue should be a string following one of the locator labels
+				// Now, check against a two different ways that locator value can be specified.
+
+				// first candidate is numbers with optional punctuation (e.g. chapt.11-12)
+				let numericRegex = /^(\d+(?:[\s,-:]+\d+)*)(?:\s+)?$/;
+				let numericMatch = numericRegex.exec(potentialLocatorValue.trim());
+				if (numericMatch) {
+					locatorValue = numericMatch[1].trim();
+					locatorLabel = labelCandidate;
+					break;
+				}
+
+				// next candidate is text in quotes (e.g. chapter "one and two")
+				let quoteRegex = /^((?:"[^"]+"|'[^']+'))(?:\s*)$/;
+				let quoteMatch = quoteRegex.exec(potentialLocatorValue.trim());
+				if (quoteMatch) {
+					locatorValue = quoteMatch[1];
+					locatorLabel = labelCandidate;
+					break;
 				}
 			}
+			if (locatorLabel) break;
+			wordLocatorIndex += 1;
 		}
-		return null;
+		if (!locatorLabel || !locatorValue || !locatorLabelString) return null;
+
+		// find the entire locator string - "chapter 'one and two'" ensuring
+		// that fullLocatorString is strictly a part of the larger string (including whitespaces and etc.)
+		let wordsWithLocator = words.slice(wordLocatorIndex).join(" ");
+		let locatorLabelIndex = wordsWithLocator.indexOf(locatorLabelString);
+		let locatorValueIndex = wordsWithLocator.indexOf(locatorValue);
+		let fullLocatorString = wordsWithLocator.substring(locatorLabelIndex, locatorValueIndex + locatorValue.length);
+		return {
+			label: locatorLabel,
+			locator: locatorValue.replace(/['"]/g, ""),
+			onlyLocator: fullLocatorString.length == string.length,
+			fullLocatorString
+		};
 	}
 
 	// calculate the height of the #search-row accounting for margins and border
@@ -282,4 +328,12 @@ export class CitationDialogHelpers {
 		
 		return str;
 	}
+
+	getLocatorLabels(loc) {
+		return {
+			fullLocator: Zotero.Cite.getLocatorString(loc).toLowerCase(),
+			shortLocator: Zotero.Cite.getLocatorString(loc, "short").toLowerCase(),
+			shortLocatorNoPunctuation: Zotero.Cite.getLocatorString(loc, "short").toLowerCase().replace(/[.,]/g, "")
+		};
+	}
 }
diff --git a/chrome/content/zotero/integration/citationDialog/searchHandler.mjs b/chrome/content/zotero/integration/citationDialog/searchHandler.mjs
index 2ffe06e88a7..8fe6fb767d5 100644
--- a/chrome/content/zotero/integration/citationDialog/searchHandler.mjs
+++ b/chrome/content/zotero/integration/citationDialog/searchHandler.mjs
@@ -191,6 +191,7 @@ export class CitationDialogSearchHandler {
 	cleanSearchQuery(str) {
 		str = str.replace(/ (?:&|and) /g, " ", "g").replace(/^,/, '');
 		str = this._cleanYear(str);
+		str = str.trim();
 
 		// If the query is very short, treat it as empty
 		if (this.minQueryLengthEnforced && str.trim().length < MIN_QUERY_LENGTH) {
diff --git a/chrome/content/zotero/preferences/preferences_cite.xhtml b/chrome/content/zotero/preferences/preferences_cite.xhtml
index 72b99e0eb9d..be742dd6675 100644
--- a/chrome/content/zotero/preferences/preferences_cite.xhtml
+++ b/chrome/content/zotero/preferences/preferences_cite.xhtml
@@ -52,7 +52,7 @@
 				<hbox align="center">
 					<label id="citation-dialog-mode-label" data-l10n-id="preferences-citation-dialog-mode">:</label>
 					<radiogroup orient="horizontal" id="citation-dialog-mode" preference="extensions.zotero.integration.citationDialogMode">
-						<radio data-l10n-id="preferences-citation-dialog-mode-last-used" value="last-closed"/>
+						<radio data-l10n-id="preferences-citation-dialog-mode-last-used" value="last-used"/>
 						<radio data-l10n-id="preferences-citation-dialog-mode-library" value="library"/>
 						<radio data-l10n-id="preferences-citation-dialog-mode-list" value="list"/>
 					</radiogroup>
diff --git a/chrome/content/zotero/xpcom/prefs.js b/chrome/content/zotero/xpcom/prefs.js
index 62e0c93c361..a2705951ae0 100644
--- a/chrome/content/zotero/xpcom/prefs.js
+++ b/chrome/content/zotero/xpcom/prefs.js
@@ -44,7 +44,7 @@ Zotero.Prefs = new function() {
 
 		// Process pref version updates
 		var fromVersion = this.get('prefVersion');
-		var toVersion = 14;
+		var toVersion = 15;
 		if (!fromVersion) {
 			this.set('prefVersion', toVersion);
 		}
@@ -165,6 +165,21 @@ Zotero.Prefs = new function() {
 						}
 						break;
 					}
+					case 15: {
+						// If classic citation dialog was used before the redesigned citation dialog was implemented,
+						// set new dialog's last used mode to "library"
+						if (this.get('integration.useClassicAddCitationDialog')) {
+							this.set('integration.citationDialogLastUsedMode', 'library');
+							this.clear('integration.useClassicAddCitationDialog');
+						}
+						// pref citationDialogLastClosedMode was renamed to citationDialogLastUsedMode
+						// when the dialog was in beta. If the old pref is found, migrate and clear it
+						let lastClosed = this.get('integration.citationDialogLastClosedMode');
+						if (lastClosed) {
+							this.set('integration.citationDialogLastUsedMode', lastClosed);
+							this.clear('integration.citationDialogLastClosedMode');
+						}
+					}
 				}
 			}
 			this.set('prefVersion', toVersion);
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
index 02e91d161ab..435d2b0ce6b 100644
--- a/defaults/preferences/zotero.js
+++ b/defaults/preferences/zotero.js
@@ -142,7 +142,7 @@ pref("extensions.zotero.integration.useClassicAddCitationDialog", false);
 pref("extensions.zotero.integration.keepAddCitationDialogRaised", false);
 pref("extensions.zotero.integration.upgradeTemplateDelayedOn", 0);
 pref("extensions.zotero.integration.dontPromptMendeleyImport", false);
-pref("extensions.zotero.integration.citationDialogMode", "last-closed");
+pref("extensions.zotero.integration.citationDialogMode", "last-used");
 
 // Connector settings
 pref("extensions.zotero.httpServer.enabled", true);