mirror of
https://github.com/zotero/zotero.git
synced 2026-10-05 02:43:38 +00:00
citation dlg: data structure refactor, bug fixes
- replace objects with zoteroItem, citationItem, etc. as the main representation of an entry in the citation (CitationDataManager.items) with instances of a new BubbleItem class. BubbleItem has two static methods to create a new instance: BubbleItem.fromCitationItem() used to create entries from io.citation.citationItem on load, and BubbleItem.fromItem used to create entries from Zotero.Item when a new item is being added into the citation. BubbleItem.getCitationItem will return a citationItem in a format that citeproc consumes. This streamlines the flow of data, makes it easier to manage and avoids unnecessary conversions between citationItem and Zotero.Item. - This refactoring fixes the issue of added suffix/prefix sticking to an item and persisting even if the bubble is removed and re-added. Fixes: #5282 - add csl data to virtual item in io.customGetItem to remain consistent with Zotero.Integration.Citation.loadItemData. That way, csl data is always found on Zotero.Item and we don't have to check citationItems. Also: - apply deduplication to cited items, so that a cited item from the library that appears in selected or opened section does not have a duplicate entry in the cited section. - added initial test suite for citation dialog
This commit is contained in:
parent
086b0c3d84
commit
dfc31d9961
7 changed files with 654 additions and 189 deletions
|
|
@ -50,21 +50,20 @@
|
|||
* are not present, remove bubbles whose citation items were removed, rearrange bubbles
|
||||
* if the items were moved, update bubble text if locator/prefix/suffix was changed.
|
||||
* Make sure that there is an input for user to type in before and after every bubble.
|
||||
* @param {Object[]} combinedItems - array of objects { zoteroItem, citationItem, dialogReferenceID, selected }.
|
||||
* zoteroItem - Zotero.Item, citationItem - object from io.citation.citationItems
|
||||
* dialogReferenceID - String ID of this citation entry, selected - Boolean indicator if bubble should be highlighted
|
||||
* @param {Object[]} bubblesConfig - array of objects { dialogReferenceID, bubbleString, selected}
|
||||
* representing each bubble.
|
||||
*/
|
||||
refresh(combinedItems) {
|
||||
refresh(bubblesConfig) {
|
||||
// Remove bubbles of items that are no longer in the citations
|
||||
for (let bubble of this.getAllBubbles()) {
|
||||
let bubbleDialogReferenceID = bubble.getAttribute("dialogReferenceID");
|
||||
let itemExistsForBubble = combinedItems.find(({ dialogReferenceID }) => dialogReferenceID == bubbleDialogReferenceID);
|
||||
let itemExistsForBubble = bubblesConfig.find(({ dialogReferenceID }) => dialogReferenceID == bubbleDialogReferenceID);
|
||||
if (!itemExistsForBubble) {
|
||||
bubble.remove();
|
||||
}
|
||||
}
|
||||
// Ensure each item in the citation has a bubble in the right position
|
||||
for (let [index, { dialogReferenceID, bubbleString }] of Object.entries(combinedItems)) {
|
||||
for (let [index, { dialogReferenceID, bubbleString }] of Object.entries(bubblesConfig)) {
|
||||
let allBubbles = this.getAllBubbles();
|
||||
let bubbleNode = allBubbles.find(candidate => candidate.getAttribute("dialogReferenceID") == dialogReferenceID);
|
||||
// Create bubble if it does not exist and append to the input
|
||||
|
|
@ -95,7 +94,7 @@
|
|||
// Highlight bubbles selected in the library view
|
||||
for (let bubble of this.getAllBubbles()) {
|
||||
let bubbleDialogReferenceID = bubble.getAttribute("dialogReferenceID");
|
||||
let itemObj = combinedItems.find(({ dialogReferenceID }) => dialogReferenceID == bubbleDialogReferenceID);
|
||||
let itemObj = bubblesConfig.find(({ dialogReferenceID }) => dialogReferenceID == bubbleDialogReferenceID);
|
||||
if (itemObj) {
|
||||
bubble.classList.toggle("has-item-selected", !!itemObj.selected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,8 +181,8 @@ class Layout {
|
|||
let sections = [];
|
||||
|
||||
// Tell SearchHandler which currently cited items are so they are not included in results
|
||||
let citedItems = CitationDataManager.getCitationItems();
|
||||
let searchResultGroups = SearchHandler.getOrderedSearchResultGroups(citedItems);
|
||||
let citedIDs = CitationDataManager.getCitedLibraryItemIDs();
|
||||
let searchResultGroups = SearchHandler.getOrderedSearchResultGroups(citedIDs);
|
||||
for (let { key, group, isLibrary } of searchResultGroups) {
|
||||
// selected items become a collapsible deck/list if there are multiple items
|
||||
let isGroupCollapsible = key == "selected" && group.length > 1;
|
||||
|
|
@ -440,13 +440,8 @@ class LibraryLayout extends Layout {
|
|||
updateSelectedItems() {
|
||||
if (!libraryLayout.itemsView) return;
|
||||
let selectedItemIDs = new Set(libraryLayout.itemsView.getSelectedItems().map(item => item.id));
|
||||
for (let itemObj of CitationDataManager.items) {
|
||||
if (selectedItemIDs.has(itemObj.zoteroItem.id)) {
|
||||
itemObj.selected = true;
|
||||
}
|
||||
else {
|
||||
itemObj.selected = false;
|
||||
}
|
||||
for (let bubbleItem of CitationDataManager.items) {
|
||||
bubbleItem.selected = selectedItemIDs.has(bubbleItem.id);
|
||||
}
|
||||
IOManager.updateBubbleInput();
|
||||
}
|
||||
|
|
@ -711,10 +706,9 @@ class LibraryLayout extends Layout {
|
|||
if (event.key == "Backspace") {
|
||||
let itemsToRemove = this.itemsView.getSelectedItems();
|
||||
for (let item of itemsToRemove) {
|
||||
let citationItems = CitationDataManager.getItems({ zoteroItemID: item.id });
|
||||
for (let citationItem of citationItems) {
|
||||
let { dialogReferenceID } = citationItem;
|
||||
IOManager._deleteItem(dialogReferenceID);
|
||||
let items = CitationDataManager.getItems({ itemID: item.id });
|
||||
for (let item of items) {
|
||||
IOManager._deleteItem(item.dialogReferenceID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -722,12 +716,12 @@ class LibraryLayout extends Layout {
|
|||
|
||||
// Highlight/de-highlight selected rows
|
||||
async _refreshItemsViewHighlightedRows() {
|
||||
let selectedIDs = CitationDataManager.items.map(({ zoteroItem }) => zoteroItem.id).filter(id => !!id);
|
||||
let selectedIDs = CitationDataManager.getCitedLibraryItemIDs();
|
||||
// Wait for the tree to fully load to avoid a logged error that the tree is undefined
|
||||
while (!this.itemsView.tree) {
|
||||
await Zotero.Promise.delay(10);
|
||||
}
|
||||
this.itemsView.setHighlightedRows(selectedIDs);
|
||||
this.itemsView.setHighlightedRows([...selectedIDs]);
|
||||
}
|
||||
|
||||
_scrollHorizontallyOnWheel(event) {
|
||||
|
|
@ -820,13 +814,8 @@ class ListLayout extends Layout {
|
|||
|
||||
updateSelectedItems() {
|
||||
let selectedIDs = new Set([...doc.querySelectorAll(".item.selected")].map(node => parseInt(node.getAttribute("itemID"))));
|
||||
for (let itemObj of CitationDataManager.items) {
|
||||
if (selectedIDs.has(itemObj.zoteroItem.id)) {
|
||||
itemObj.selected = true;
|
||||
}
|
||||
else {
|
||||
itemObj.selected = false;
|
||||
}
|
||||
for (let bubbleItem of CitationDataManager.items) {
|
||||
bubbleItem.selected = selectedIDs.has(bubbleItem.id);
|
||||
}
|
||||
IOManager.updateBubbleInput();
|
||||
}
|
||||
|
|
@ -950,6 +939,10 @@ const IOManager = {
|
|||
let mode = _id("mode-button").getAttribute("mode");
|
||||
newMode = mode == "library" ? "list" : "library";
|
||||
}
|
||||
// Do nothing if switching to a mode that is already active
|
||||
let currentMode = _id("mode-button").getAttribute("mode");
|
||||
if (currentMode == newMode) return;
|
||||
|
||||
_id("list-layout").hidden = newMode == "library";
|
||||
_id("library-layout").hidden = newMode == "list";
|
||||
|
||||
|
|
@ -990,9 +983,15 @@ const IOManager = {
|
|||
updateBubbleInput() {
|
||||
// re-generate the bubble string for each item, in case a locator/prefix/suffix/etc. was changed
|
||||
for (let item of CitationDataManager.items) {
|
||||
item.bubbleString = Helpers.buildBubbleString({ citationItem: item.citationItem, zoteroItem: item.zoteroItem });
|
||||
item.updateBubbleString();
|
||||
}
|
||||
_id("bubble-input").refresh(CitationDataManager.items);
|
||||
_id("bubble-input").refresh(CitationDataManager.items.map((item) => {
|
||||
return {
|
||||
dialogReferenceID: item.dialogReferenceID,
|
||||
bubbleString: item.bubbleString,
|
||||
selected: item.selected,
|
||||
};
|
||||
}));
|
||||
_id("accept-button").disabled = !CitationDataManager.items.length;
|
||||
},
|
||||
|
||||
|
|
@ -1005,7 +1004,8 @@ const IOManager = {
|
|||
if (isCitingNotes) {
|
||||
if (!items[0].isNote()) return;
|
||||
CitationDataManager.items = [];
|
||||
await CitationDataManager.addItems({ citationItems: items });
|
||||
let bubbleItem = BubbleItem.fromItem(items[0]);
|
||||
await CitationDataManager.addItems({ bubbleItems: [bubbleItem] });
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1019,18 +1019,13 @@ const IOManager = {
|
|||
|
||||
// If multiple items are being added, only add ones that are not included in the citation
|
||||
if (items.length > 1) {
|
||||
items = items.filter(item => !(item.id && CitationDataManager.getItems({ zoteroItemID: item.id }).length));
|
||||
items = items.filter(item => !(item.id && CitationDataManager.getItems({ itemID: item.id }).length));
|
||||
}
|
||||
|
||||
// If the last input has a locator, add it into the item
|
||||
let input = _id("bubble-input").getCurrentInput();
|
||||
let inputValue = SearchHandler.cleanSearchQuery(input?.value || "");
|
||||
let locator = Helpers.extractLocator(inputValue);
|
||||
// If there is no locator, make sure we clear it from the citation item
|
||||
for (let item of items) {
|
||||
item.label = locator?.label || null;
|
||||
item.locator = locator?.locator || null;
|
||||
}
|
||||
// Add the item at a position based on current input if it is not explicitly specified
|
||||
if (index === null && input) {
|
||||
index = _id("bubble-input").getFutureBubbleIndex();
|
||||
|
|
@ -1040,7 +1035,15 @@ const IOManager = {
|
|||
input.remove();
|
||||
}
|
||||
|
||||
await CitationDataManager.addItems({ citationItems: items, index });
|
||||
// Add entries into the citation with the current locator if specified
|
||||
let bubbleItems = items.map(item => BubbleItem.fromItem(item));
|
||||
if (locator) {
|
||||
for (let bubbleItem of bubbleItems) {
|
||||
bubbleItem.locator = locator.locator;
|
||||
bubbleItem.label = locator.label;
|
||||
}
|
||||
}
|
||||
await CitationDataManager.addItems({ bubbleItems, index });
|
||||
// Refresh the itemTree if in library mode
|
||||
if (currentLayout.type == "library") {
|
||||
libraryLayout.refreshItemsView();
|
||||
|
|
@ -1255,8 +1258,8 @@ const IOManager = {
|
|||
let bubble = input.previousElementSibling;
|
||||
let item = CitationDataManager.getItem({ dialogReferenceID: bubble?.getAttribute("dialogReferenceID") });
|
||||
if (item && locator && locator.onlyLocator && bubble) {
|
||||
item.citationItem.locator = locator.locator;
|
||||
item.citationItem.label = locator.label;
|
||||
item.locator = locator.locator;
|
||||
item.label = locator.label;
|
||||
input.value = "";
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
this.updateBubbleInput();
|
||||
|
|
@ -1308,8 +1311,8 @@ const IOManager = {
|
|||
},
|
||||
|
||||
_openItemDetailsPopup(dialogReferenceID) {
|
||||
let { zoteroItem, citationItem } = CitationDataManager.getItem({ dialogReferenceID });
|
||||
PopupsHandler.openItemDetails(dialogReferenceID, zoteroItem, citationItem, Helpers.buildItemDescription(zoteroItem));
|
||||
let bubbleItem = CitationDataManager.getItem({ dialogReferenceID });
|
||||
PopupsHandler.openItemDetails(bubbleItem, Helpers.buildItemDescription(bubbleItem.item));
|
||||
},
|
||||
|
||||
_handleInput({ query, eventType }) {
|
||||
|
|
@ -1425,27 +1428,112 @@ const IOManager = {
|
|||
}
|
||||
};
|
||||
|
||||
// Representation of a single entry in the citation.
|
||||
class BubbleItem {
|
||||
// Can be created from either Zotero.Item or citation item from io.citation.citationItems
|
||||
static fromItem(item) {
|
||||
let citationItem = {};
|
||||
return new BubbleItem({ item, citationItem });
|
||||
}
|
||||
|
||||
static fromCitationItem(citationItem) {
|
||||
let item;
|
||||
if (io.customGetItem) {
|
||||
item = io.customGetItem(citationItem);
|
||||
}
|
||||
if (!item) {
|
||||
item = Zotero.Cite.getItem(citationItem.id);
|
||||
}
|
||||
return new BubbleItem({ item, citationItem });
|
||||
}
|
||||
|
||||
constructor({ item, citationItem }) {
|
||||
if (!item || !citationItem) {
|
||||
throw new Error("Both Zotero.Item and citation item must be provided");
|
||||
}
|
||||
this.item = item;
|
||||
this.cslItemID = item.cslItemID;
|
||||
this.cslItemData = item.cslItemData;
|
||||
this.cslURIs = item.cslURIs;
|
||||
|
||||
this.locator = citationItem.locator;
|
||||
this.label = citationItem.label;
|
||||
this.suffix = citationItem.suffix;
|
||||
this.prefix = citationItem.prefix;
|
||||
this.suppressAuthor = citationItem["suppress-author"];
|
||||
|
||||
this.bubbleString = "";
|
||||
this.selected = false;
|
||||
// Add a new ID to our citation item and set the same ID on the bubble
|
||||
// so we have a reliable way to identify which bubble refers to which citationItem.
|
||||
this.dialogReferenceID = Zotero.Utilities.randomString(5);
|
||||
this.updateBubbleString();
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.cslItemID || this.item.id;
|
||||
}
|
||||
|
||||
updateBubbleString() {
|
||||
this.bubbleString = Helpers.buildBubbleString(this);
|
||||
}
|
||||
|
||||
// Return an object with relevant fields that cipeproc can consume.
|
||||
// Can optionally include dialogReferenceID for sorting in CitationDataManager.sort()
|
||||
getCitationItem({ includeDialogReferenceID } = {}) {
|
||||
let citationItem = {
|
||||
id: this.id,
|
||||
};
|
||||
if (this.locator) {
|
||||
citationItem.locator = this.locator;
|
||||
citationItem.label = this.label;
|
||||
}
|
||||
if (this.suffix) {
|
||||
citationItem.suffix = this.suffix;
|
||||
}
|
||||
|
||||
if (this.prefix) {
|
||||
citationItem.prefix = this.prefix;
|
||||
}
|
||||
if (this.suppressAuthor) {
|
||||
citationItem["suppress-author"] = this.suppressAuthor;
|
||||
}
|
||||
if (this.cslItemData) {
|
||||
citationItem.itemData = this.cslItemData;
|
||||
}
|
||||
if (this.cslURIs) {
|
||||
citationItem.uris = this.cslURIs;
|
||||
}
|
||||
if (includeDialogReferenceID) {
|
||||
citationItem.dialogReferenceID = this.dialogReferenceID;
|
||||
}
|
||||
return citationItem;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Singleton to store and handle items in this citation.
|
||||
// CitationDataManager.items is an array of { zoteroItem, citationItem } objects,
|
||||
// where zoteroItem is Zotero.Item and citationItem is a citation item provided by io.
|
||||
// They are stored as a pair to make it easier to access both item properties (e.g. item.getDisplayTitle())
|
||||
// and properties of citation item (e.g. locator) across different components.
|
||||
// CitationDataManager.items is an array of BubbleItem objects, which maps
|
||||
// directly to the bubbles in the citation.
|
||||
//
|
||||
const CitationDataManager = {
|
||||
items: [],
|
||||
itemAddedCache: new Set(),
|
||||
|
||||
getCitationItems() {
|
||||
return this.items.map(item => item.citationItem);
|
||||
getCitedLibraryItemIDs() {
|
||||
return new Set(this.items.map(item => item.item.id).filter(id => id));
|
||||
},
|
||||
|
||||
getSelectedIDs() {
|
||||
return this.items.filter(item => item.selected).map(item => item.id);
|
||||
},
|
||||
|
||||
getItem({ dialogReferenceID }) {
|
||||
return this.items.find(item => item.dialogReferenceID === dialogReferenceID);
|
||||
return this.items.find(bubbleItem => bubbleItem.dialogReferenceID === dialogReferenceID);
|
||||
},
|
||||
|
||||
getItems({ zoteroItemID }) {
|
||||
return this.items.filter(item => item.zoteroItem.id === zoteroItemID);
|
||||
getItems({ itemID }) {
|
||||
return this.items.filter(bubbleItem => bubbleItem.item.id === itemID);
|
||||
},
|
||||
|
||||
getItemIndex({ dialogReferenceID }) {
|
||||
|
|
@ -1454,28 +1542,28 @@ const CitationDataManager = {
|
|||
|
||||
updateItemAddedCache() {
|
||||
this.itemAddedCache = new Set();
|
||||
for (let { zoteroItem } of this.items) {
|
||||
if (!zoteroItem.id) continue;
|
||||
this.itemAddedCache.add(zoteroItem.id);
|
||||
for (let bubbleItem of this.items) {
|
||||
if (!bubbleItem.item.id) continue;
|
||||
this.itemAddedCache.add(bubbleItem.item.id.id);
|
||||
}
|
||||
},
|
||||
|
||||
// Include specified items into the citation
|
||||
async addItems({ citationItems = [], index = null }) {
|
||||
for (let item of citationItems) {
|
||||
let zoteroItem = this._citationItemToZoteroItem(item);
|
||||
// Add a new ID to our citation item and set the same ID on the bubble
|
||||
// so we have a reliable way to identify which bubble refers to which citationItem.
|
||||
let dialogReferenceID = Zotero.Utilities.randomString(5);
|
||||
let toInsert = { citationItem: item, zoteroItem: zoteroItem, dialogReferenceID };
|
||||
|
||||
/**
|
||||
* Include specified items into the citation.
|
||||
|
||||
*/
|
||||
async addItems({ bubbleItems = [], index = null }) {
|
||||
for (let bubbleItem of bubbleItems) {
|
||||
if (index !== null) {
|
||||
this.items.splice(index, 0, toInsert);
|
||||
this.items.splice(index, 0, bubbleItem);
|
||||
index += 1;
|
||||
}
|
||||
else {
|
||||
this.items.push(toInsert);
|
||||
this.items.push(bubbleItem);
|
||||
}
|
||||
}
|
||||
// No sorting happens when citing notes, since the dialog is accepted right after
|
||||
if (isCitingNotes) return;
|
||||
await this.sort();
|
||||
this.updateItemAddedCache();
|
||||
},
|
||||
|
|
@ -1499,33 +1587,7 @@ const CitationDataManager = {
|
|||
|
||||
// Update io citation object based on Citation.items array
|
||||
updateCitationObject(final = false) {
|
||||
let result = [];
|
||||
for (let item of this.items) {
|
||||
let dialogReferenceID = item.dialogReferenceID;
|
||||
item = item.citationItem;
|
||||
if (item instanceof Zotero.Item) {
|
||||
let ioResult = { id: item.cslItemID || item.id };
|
||||
if (typeof ioResult.id === "string" && ioResult.id.indexOf("/") !== -1) {
|
||||
let item = Zotero.Cite.getItem(ioResult.id);
|
||||
ioResult.uris = item.cslURIs;
|
||||
ioResult.itemData = item.cslItemData;
|
||||
}
|
||||
ioResult.label = item.label || null;
|
||||
ioResult.locator = item.locator || null;
|
||||
ioResult.prefix = item.prefix || null;
|
||||
ioResult.suffix = item.suffix || null;
|
||||
ioResult['suppress-author'] = item["suppress-author"] || null;
|
||||
result.push(ioResult);
|
||||
}
|
||||
else {
|
||||
result.push(item);
|
||||
}
|
||||
if (!final) {
|
||||
result[result.length - 1].dialogReferenceID = dialogReferenceID;
|
||||
}
|
||||
}
|
||||
io.citation.citationItems = result;
|
||||
|
||||
io.citation.citationItems = this.items.map(item => item.getCitationItem({ includeDialogReferenceID: !final }));
|
||||
if (final && io.sortable) {
|
||||
io.citation.properties.unsorted = !_id("keepSorted").checked;
|
||||
}
|
||||
|
|
@ -1546,54 +1608,24 @@ const CitationDataManager = {
|
|||
|
||||
// Construct citation upon initial load
|
||||
async buildCitation() {
|
||||
let citationItems = [];
|
||||
if (!io.citation.properties.unsorted
|
||||
&& _id("keepSorted").checked
|
||||
&& io.citation.sortedItems?.length) {
|
||||
await this.addItems({ citationItems: io.citation.sortedItems.map(entry => entry[1]) });
|
||||
citationItems = io.citation.sortedItems.map(entry => entry[1]);
|
||||
}
|
||||
else {
|
||||
await this.addItems({ citationItems: io.citation.citationItems });
|
||||
citationItems = io.citation.citationItems;
|
||||
}
|
||||
let bubbleItems = citationItems.map(item => BubbleItem.fromCitationItem(item));
|
||||
await this.addItems({ bubbleItems });
|
||||
},
|
||||
|
||||
// Check if two given items are the same to prevent an item being inserted more
|
||||
// than once into the citation. Compare firstCreator and title fields, instead of just
|
||||
// itemIDs to account for cited items that may not have ids.
|
||||
potentialDuplicateExists(targetZoteroItem) {
|
||||
if (!(targetZoteroItem instanceof Zotero.Item)) {
|
||||
targetZoteroItem = this._citationItemToZoteroItem(targetZoteroItem);
|
||||
}
|
||||
for (let item of this.items) {
|
||||
let sameCreator = item.zoteroItem.getField("firstCreator") === targetZoteroItem.getField("firstCreator");
|
||||
let sameTitle = item.zoteroItem.getDisplayTitle() === targetZoteroItem.getDisplayTitle();
|
||||
if (sameCreator && sameTitle) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// check if items have the same id, comparing .cslItemID for cited items or .id for
|
||||
// usual items
|
||||
_itemsHaveSameID(itemOne, itemTwo) {
|
||||
let itemOneID = itemOne.cslItemID || itemOne.id;
|
||||
let itemTwoID = itemTwo.cslItemID || itemTwo.id;
|
||||
if (!itemOneID || !itemTwoID) return false;
|
||||
return itemOneID == itemTwoID;
|
||||
},
|
||||
|
||||
// Shortcut to fetch Zotero.Item based on citationItem
|
||||
_citationItemToZoteroItem(citationItem) {
|
||||
if (citationItem instanceof Zotero.Item) return citationItem;
|
||||
if (io.customGetItem) {
|
||||
let item = io.customGetItem(citationItem);
|
||||
if (item) return item;
|
||||
}
|
||||
if (citationItem.id) {
|
||||
return Zotero.Cite.getItem(citationItem.id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Explicitly expose singletons to global window for tests
|
||||
window.CitationDataManager = CitationDataManager;
|
||||
window.IOManager = IOManager;
|
||||
|
||||
// Top level listeners
|
||||
window.addEventListener("load", onLoad);
|
||||
window.addEventListener("unload", onUnload);
|
||||
|
|
|
|||
|
|
@ -282,13 +282,13 @@ export class CitationDialogHelpers {
|
|||
return height + margins + border;
|
||||
}
|
||||
|
||||
buildBubbleString({ citationItem, zoteroItem }) {
|
||||
buildBubbleString(bubbleItem) {
|
||||
// Creator
|
||||
var title;
|
||||
var str = zoteroItem.getField("firstCreator");
|
||||
var str = bubbleItem.item.getField("firstCreator");
|
||||
|
||||
// Title, if no creator (getDisplayTitle in order to get case, e-mail, statute which don't have a title field)
|
||||
title = zoteroItem.getDisplayTitle();
|
||||
title = bubbleItem.item.getDisplayTitle();
|
||||
title = title.substr(0, 32) + (title.length > 32 ? "…" : "");
|
||||
if (!str && title) {
|
||||
str = Zotero.getString("punctuation.openingQMark") + title + Zotero.getString("punctuation.closingQMark");
|
||||
|
|
@ -298,32 +298,32 @@ export class CitationDialogHelpers {
|
|||
}
|
||||
|
||||
// Date
|
||||
var date = zoteroItem.getField("date", true, true);
|
||||
var date = bubbleItem.item.getField("date", true, true);
|
||||
if (date && (date = date.substr(0, 4)) !== "0000") {
|
||||
str += ", " + parseInt(date);
|
||||
}
|
||||
|
||||
// Locator
|
||||
if (citationItem.locator) {
|
||||
if (bubbleItem.locator) {
|
||||
// Try to fetch the short form of the locator label. E.g. "p." for "page"
|
||||
// If there is no locator label, default to "page" for now
|
||||
let label = (Zotero.Cite.getLocatorString(citationItem.label || 'page', 'short') || '').toLocaleLowerCase();
|
||||
let label = (Zotero.Cite.getLocatorString(bubbleItem.label || 'page', 'short') || '').toLocaleLowerCase();
|
||||
|
||||
str += `, ${label} ${citationItem.locator}`;
|
||||
str += `, ${label} ${bubbleItem.locator}`;
|
||||
}
|
||||
|
||||
// Prefix
|
||||
if (citationItem.prefix && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) {
|
||||
let prefix = citationItem.prefix.substr(0, 10) + (citationItem.prefix.length > 10 ? "…" : "");
|
||||
if (bubbleItem.prefix && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) {
|
||||
let prefix = bubbleItem.prefix.substr(0, 10) + (bubbleItem.prefix.length > 10 ? "…" : "");
|
||||
str = prefix
|
||||
+ (Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? " " : "")
|
||||
+ (Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(bubbleItem.prefix) ? " " : "")
|
||||
+ str;
|
||||
}
|
||||
|
||||
// Suffix
|
||||
if (citationItem.suffix && Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP) {
|
||||
let suffix = citationItem.suffix.substr(0, 10) + (citationItem.suffix.length > 10 ? "…" : "");
|
||||
str += (Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? " " : "") + suffix;
|
||||
if (bubbleItem.suffix && Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP) {
|
||||
let suffix = bubbleItem.suffix.substr(0, 10) + (bubbleItem.suffix.length > 10 ? "…" : "");
|
||||
str += (Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP.test(bubbleItem.suffix) ? " " : "") + suffix;
|
||||
}
|
||||
|
||||
return str;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ export class CitationDialogPopupsHandler {
|
|||
constructor({ doc }) {
|
||||
this.doc = doc;
|
||||
|
||||
this.item = null;
|
||||
this.citationItem = null;
|
||||
this.bubbleItem = null;
|
||||
this.discardItemDetailsEdits = false;
|
||||
this.itemDetailsWhenOpened = {};
|
||||
this.itemDetailsTimeOpened = null;
|
||||
|
|
@ -60,7 +59,7 @@ export class CitationDialogPopupsHandler {
|
|||
let event = new CustomEvent("delete-item", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
dialogReferenceID: this.dialogReferenceID
|
||||
dialogReferenceID: this.bubbleItem.dialogReferenceID
|
||||
}
|
||||
});
|
||||
this.doc.dispatchEvent(event);
|
||||
|
|
@ -71,7 +70,7 @@ export class CitationDialogPopupsHandler {
|
|||
this._getNode("#itemDetails .show").addEventListener("click", (_) => {
|
||||
this.discardItemDetailsEdits = true;
|
||||
this._getNode("#itemDetails").hidePopup();
|
||||
Zotero.Utilities.Internal.showInLibrary(this.item);
|
||||
Zotero.Utilities.Internal.showInLibrary(this.bubbleItem.item);
|
||||
});
|
||||
this._getNode("#itemDetails .done").addEventListener("click", (_) => {
|
||||
this._getNode("#itemDetails").hidePopup();
|
||||
|
|
@ -86,20 +85,18 @@ export class CitationDialogPopupsHandler {
|
|||
}, true);
|
||||
}
|
||||
|
||||
openItemDetails(dialogReferenceID, item, citationItem, itemDescription) {
|
||||
this.item = item;
|
||||
this.citationItem = citationItem;
|
||||
this.dialogReferenceID = dialogReferenceID;
|
||||
openItemDetails(bubbleItem, itemDescription) {
|
||||
this.bubbleItem = bubbleItem;
|
||||
// record initial properties when popup is opened to be able to discard edits on Escape
|
||||
this.itemDetailsWhenOpened = {
|
||||
label: citationItem.label,
|
||||
locator: citationItem.locator,
|
||||
prefix: citationItem.prefix,
|
||||
suffix: citationItem.suffix,
|
||||
suppressAuthor: citationItem["suppress-author"]
|
||||
label: bubbleItem.label,
|
||||
locator: bubbleItem.locator,
|
||||
prefix: bubbleItem.prefix,
|
||||
suffix: bubbleItem.suffix,
|
||||
suppressAuthor: bubbleItem.suppressAuthor
|
||||
};
|
||||
|
||||
let bubble = this._getNode(`[dialogReferenceID='${dialogReferenceID}']`);
|
||||
let bubble = this._getNode(`[dialogReferenceID='${this.bubbleItem.dialogReferenceID}']`);
|
||||
let bubbleRect = bubble.getBoundingClientRect();
|
||||
let popup = this._getNode("#itemDetails");
|
||||
popup.openPopup(bubble, "after_start", 0, 4, false, false, null);
|
||||
|
|
@ -118,21 +115,21 @@ export class CitationDialogPopupsHandler {
|
|||
this._getNode("#label").appendChild(option);
|
||||
}
|
||||
}
|
||||
this._getNode("#itemDetails .show").hidden = !this.item.id;
|
||||
this._getNode("#itemDetails .show").hidden = !this.bubbleItem.item.id;
|
||||
|
||||
// Add header and fill inputs with their values
|
||||
let description = itemDescription;
|
||||
this._getNode("#itemDetails").querySelector(".description")?.remove();
|
||||
this._getNode("#itemTitle").textContent = this.item.getDisplayTitle();
|
||||
this._getNode("#itemTitle").textContent = this.bubbleItem.item.getDisplayTitle();
|
||||
this._getNode("#itemTitle").after(description);
|
||||
let dataTypeLabel = this.item.getItemTypeIconName(true);
|
||||
let dataTypeLabel = this.bubbleItem.item.getItemTypeIconName(true);
|
||||
this._getNode("#itemDetails").querySelector(".icon").setAttribute("data-item-type", dataTypeLabel);
|
||||
|
||||
this._getNode("#label").value = this.citationItem.label || "page";
|
||||
this._getNode("#locator").value = this.citationItem.locator || "";
|
||||
this._getNode("#prefix").value = this.citationItem.prefix || "";
|
||||
this._getNode("#suffix").value = this.citationItem.suffix || "";
|
||||
this._getNode("#suppress-author").checked = !!this.citationItem["suppress-author"];
|
||||
this._getNode("#label").value = this.bubbleItem.label || "page";
|
||||
this._getNode("#locator").value = this.bubbleItem.locator || "";
|
||||
this._getNode("#prefix").value = this.bubbleItem.prefix || "";
|
||||
this._getNode("#suffix").value = this.bubbleItem.suffix || "";
|
||||
this._getNode("#suppress-author").checked = !!this.bubbleItem.suppressAuthor;
|
||||
bubble.classList.add("showingDetails");
|
||||
this.itemDetailsTimeOpened = (new Date()).getTime();
|
||||
}
|
||||
|
|
@ -156,17 +153,17 @@ export class CitationDialogPopupsHandler {
|
|||
|
||||
// When item details popup is closed, sync it's data to citationItems
|
||||
handleItemDetailsClosure() {
|
||||
let bubble = this._getNode(`[dialogReferenceID='${this.dialogReferenceID}']`);
|
||||
let bubble = this._getNode(`[dialogReferenceID='${this.bubbleItem.dialogReferenceID}']`);
|
||||
if (!bubble) return;
|
||||
bubble.classList.remove("showingDetails");
|
||||
// Restore properties to what they were when popup opened
|
||||
if (this.discardItemDetailsEdits) {
|
||||
this.discardItemDetailsEdits = false;
|
||||
this.citationItem.label = this.itemDetailsWhenOpened.label;
|
||||
this.citationItem.locator = this.itemDetailsWhenOpened.locator;
|
||||
this.citationItem.prefix = this.itemDetailsWhenOpened.prefix;
|
||||
this.citationItem.suffix = this.itemDetailsWhenOpened.suffix;
|
||||
this.citationItem["suppress-author"] = this.itemDetailsWhenOpened.suppressAuthor;
|
||||
this.bubbleItem.label = this.itemDetailsWhenOpened.label;
|
||||
this.bubbleItem.locator = this.itemDetailsWhenOpened.locator;
|
||||
this.bubbleItem.prefix = this.itemDetailsWhenOpened.prefix;
|
||||
this.bubbleItem.suffix = this.itemDetailsWhenOpened.suffix;
|
||||
this.bubbleItem.suppressAuthor = this.itemDetailsWhenOpened.suppressAuthor;
|
||||
this.itemDetailsWhenOpened = {};
|
||||
this.notifyCitationDialogOfChange();
|
||||
}
|
||||
|
|
@ -182,11 +179,11 @@ export class CitationDialogPopupsHandler {
|
|||
|
||||
// Update item details and notify citation dialog about changes
|
||||
handleItemDetailsChange() {
|
||||
this.citationItem.label = this._getNode("#locator").value ? this._getNode("#label").value : null;
|
||||
this.citationItem.locator = this._getNode("#locator").value;
|
||||
this.citationItem.prefix = this._getNode("#prefix").value;
|
||||
this.citationItem.suffix = this._getNode("#suffix").value;
|
||||
this.citationItem["suppress-author"] = this._getNode("#suppress-author").checked;
|
||||
this.bubbleItem.label = this._getNode("#locator").value ? this._getNode("#label").value : null;
|
||||
this.bubbleItem.locator = this._getNode("#locator").value;
|
||||
this.bubbleItem.prefix = this._getNode("#prefix").value;
|
||||
this.bubbleItem.suffix = this._getNode("#suffix").value;
|
||||
this.bubbleItem.suppressAuthor = this._getNode("#suppress-author").checked;
|
||||
this.notifyCitationDialogOfChange();
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +192,7 @@ export class CitationDialogPopupsHandler {
|
|||
let event = new CustomEvent("item-details-updated", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
dialogReferenceID: this.dialogReferenceID
|
||||
dialogReferenceID: this.bubbleItem.dialogReferenceID
|
||||
}
|
||||
});
|
||||
this.doc.dispatchEvent(event);
|
||||
|
|
|
|||
|
|
@ -91,10 +91,9 @@ export class CitationDialogSearchHandler {
|
|||
// by the number of results in each library.
|
||||
// Items/notes in the libraries group are sorted via _createItemsSort/_createNotesSort comparators.
|
||||
// Takes citedItems as a parameter to filter them out from Selected, Opened and Cited groups.
|
||||
getOrderedSearchResultGroups(citedItems = []) {
|
||||
getOrderedSearchResultGroups(citedItemIDs = new Set()) {
|
||||
let removeItemsIncludedInCitation = (items) => {
|
||||
let citedItemsIDs = new Set(citedItems.map(item => item.cslItemID || item.id));
|
||||
return items.filter(i => !citedItemsIDs.has(i.cslItemID ? i.cslItemID : i.id));
|
||||
return items.filter(i => !citedItemIDs.has(i.cslItemID ? i.cslItemID : i.id));
|
||||
};
|
||||
let result = [];
|
||||
// selected/open/cited go first
|
||||
|
|
@ -184,6 +183,7 @@ export class CitationDialogSearchHandler {
|
|||
else {
|
||||
this.results.cited = this.searchValue ? this._filterNonMatchingItems(this.citedItems) : [];
|
||||
}
|
||||
this._deduplicate();
|
||||
}
|
||||
|
||||
// clear selected/open items cache to re-fetch those items
|
||||
|
|
@ -219,13 +219,16 @@ export class CitationDialogSearchHandler {
|
|||
|
||||
// make sure that each item appears only in one group.
|
||||
// Items that are selected are removed from opened.
|
||||
// Items that are selected or opened are removed from library results.
|
||||
// Items that are selected or opened are removed from cited.
|
||||
// Items that are selected or opened or cited are removed from library results.
|
||||
_deduplicate() {
|
||||
let selectedIDs = new Set(this.results.selected.map(item => item.id));
|
||||
let openIDs = new Set(this.results.open.map(item => item.id));
|
||||
let citedIDs = new Set(this.results.cited.map(item => item.id));
|
||||
|
||||
this.results.open = this.results.open.filter(item => !selectedIDs.has(item.id));
|
||||
this.results.found = this.results.found.filter(item => !selectedIDs.has(item.id) && !openIDs.has(item.id));
|
||||
this.results.cited = this.results.cited.filter(item => !selectedIDs.has(item.id) && !openIDs.has(item.id));
|
||||
this.results.found = this.results.found.filter(item => !selectedIDs.has(item.id) && !openIDs.has(item.id) && !citedIDs.has(item.id));
|
||||
}
|
||||
|
||||
// Run the actual search query and find all items matching query across all libraries
|
||||
|
|
|
|||
|
|
@ -1079,6 +1079,10 @@ class EditorInstance {
|
|||
else if (!citationItem.id && citationItem.itemData) {
|
||||
let item = new Zotero.Item();
|
||||
Zotero.Utilities.itemFromCSLJSON(item, citationItem.itemData);
|
||||
// Add csl data in the same format as in Zotero.Integration.Citation.loadItemData
|
||||
item.cslItemID = citationItem.id;
|
||||
item.cslURIs = citationItem.uris;
|
||||
item.cslItemData = citationItem.itemData;
|
||||
return item;
|
||||
}
|
||||
// Otherwise returns `undefined` which makes this function to be
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ describe("Citation Dialog", function () {
|
|||
sort() {},
|
||||
sortable: false,
|
||||
citation: {
|
||||
citationItems: [],
|
||||
properties: {
|
||||
unsorted: false,
|
||||
}
|
||||
|
|
@ -13,7 +14,7 @@ describe("Citation Dialog", function () {
|
|||
return [];
|
||||
}
|
||||
};
|
||||
let dialog, win;
|
||||
let dialog, win, IOManager, CitationDataManager, SearchHandler;
|
||||
|
||||
before(async function () {
|
||||
// one of helper functions of searchHandler uses zotero pane
|
||||
|
|
@ -21,11 +22,22 @@ describe("Citation Dialog", function () {
|
|||
let dialogPromise = waitForWindow("chrome://zotero/content/integration/citationDialog.xhtml");
|
||||
Services.ww.openWindow(null, "chrome://zotero/content/integration/citationDialog.xhtml", "", "", io);
|
||||
dialog = await dialogPromise;
|
||||
IOManager = dialog.IOManager;
|
||||
CitationDataManager = dialog.CitationDataManager;
|
||||
SearchHandler = dialog.SearchHandler;
|
||||
// wait for everything (e.g. itemTree/collectionTree) inside of the dialog to be loaded.
|
||||
// it is not used currently but may be required when more complex tests are added
|
||||
// while (!dialog.loaded) {
|
||||
// await Zotero.Promise.delay(10);
|
||||
// }
|
||||
while (!dialog.loaded) {
|
||||
await Zotero.Promise.delay(10);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
// Many operations (e.g. IOManager.addItemsToCitation) are disabled
|
||||
// when search runs. Search can be triggered by a variety of events
|
||||
// so before each test, we make sure that search has finished running
|
||||
while (SearchHandler.searching) {
|
||||
await Zotero.Promise.delay(10);
|
||||
}
|
||||
});
|
||||
|
||||
after(function () {
|
||||
|
|
@ -33,6 +45,424 @@ describe("Citation Dialog", function () {
|
|||
win.close();
|
||||
});
|
||||
|
||||
describe("Manage entries in the citation", function () {
|
||||
let citedItemNotInLibrary = {
|
||||
id: "o7HzMbH6/6iMXHx6s",
|
||||
itemData: {
|
||||
id: "o7HzMbH6/6iMXHx6s",
|
||||
type: "book",
|
||||
title: "cited_not_in_library_test_title",
|
||||
author: [
|
||||
{
|
||||
family: "Last",
|
||||
given: "First"
|
||||
}
|
||||
]
|
||||
},
|
||||
uris: [
|
||||
"http://zotero.org/users/11573780/items/K22KNVZL"
|
||||
],
|
||||
item: {
|
||||
id: "o7HzMbH6/6iMXHx6s",
|
||||
type: "book",
|
||||
title: "cited_not_in_library_test_title",
|
||||
author: [
|
||||
{
|
||||
family: "Last",
|
||||
given: "First"
|
||||
}
|
||||
],
|
||||
"title-main": "cited_not_in_library_test_title",
|
||||
"title-sub": "",
|
||||
"title-subjoin": ""
|
||||
},
|
||||
label: undefined,
|
||||
locator: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
"suppress-author": undefined
|
||||
};
|
||||
let citedItemOne = {
|
||||
id: null,
|
||||
item: {
|
||||
id: null,
|
||||
type: "book",
|
||||
title: "cited_in_library_test_title",
|
||||
author: [
|
||||
{
|
||||
family: "Last",
|
||||
given: "First"
|
||||
}
|
||||
],
|
||||
"title-main": "cited_in_library_test_title",
|
||||
"title-sub": "",
|
||||
"title-subjoin": ""
|
||||
},
|
||||
label: undefined,
|
||||
locator: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
"suppress-author": undefined
|
||||
};
|
||||
let itemOne, itemTwo, bubbleInput, ZoteroCiteGetItemStub, surrogateCitedItem;
|
||||
|
||||
before(async function () {
|
||||
bubbleInput = dialog.document.querySelector("bubble-input");
|
||||
|
||||
// Virtual Zotero.Item for a cited item that does not exist in the library.
|
||||
// Same logic as in Zotero.Integration.Citation.loadItemData.
|
||||
surrogateCitedItem = new Zotero.Item();
|
||||
Zotero.Utilities.itemFromCSLJSON(surrogateCitedItem, citedItemNotInLibrary.itemData);
|
||||
surrogateCitedItem.cslItemID = citedItemNotInLibrary.id;
|
||||
surrogateCitedItem.cslURIs = citedItemNotInLibrary.uris;
|
||||
surrogateCitedItem.cslItemData = citedItemNotInLibrary.itemData;
|
||||
// Zotero.Cite.getItem called with citedItemNotInLibrary returns virtual Zotero.Item from above
|
||||
ZoteroCiteGetItemStub = sinon.stub(Zotero.Cite, 'getItem').callsFake(function (id) {
|
||||
if (id === citedItemNotInLibrary.id) {
|
||||
return surrogateCitedItem;
|
||||
}
|
||||
return Zotero.Items.get(id);
|
||||
});
|
||||
|
||||
itemOne = await createDataObject('item', { title: "one" });
|
||||
itemOne.setCreators([
|
||||
{
|
||||
firstName: "First_One",
|
||||
lastName: "Last_One",
|
||||
creatorType: "author"
|
||||
}
|
||||
]);
|
||||
await itemOne.saveTx();
|
||||
// citedItemOne is an earlier cited itemOne
|
||||
citedItemOne.id = itemOne.id;
|
||||
citedItemOne.item.id = itemOne.id;
|
||||
|
||||
itemTwo = await createDataObject('item', { title: "two" });
|
||||
itemTwo.setCreators([
|
||||
{
|
||||
firstName: "First_Two",
|
||||
lastName: "Last_Two",
|
||||
creatorType: "author"
|
||||
}
|
||||
]);
|
||||
await itemTwo.saveTx();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
ZoteroCiteGetItemStub.restore();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
io.citation.citationItems = [];
|
||||
io.citation.sortable = false;
|
||||
dialog.document.getElementById("keepSorted").checked = false;
|
||||
io.sort = () => {};
|
||||
CitationDataManager.items = [];
|
||||
IOManager.updateBubbleInput();
|
||||
});
|
||||
|
||||
it("should add an item to citation", async function () {
|
||||
await IOManager.addItemsToCitation([itemOne]);
|
||||
|
||||
let bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(CitationDataManager.items.length, 1);
|
||||
assert.equal(bubbles.length, 1);
|
||||
assert.equal(bubbles[0].textContent, itemOne.getCreator(0).lastName);
|
||||
});
|
||||
|
||||
it("should remove an item from citation", async function () {
|
||||
await IOManager.addItemsToCitation([itemOne, itemTwo]);
|
||||
let bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(bubbles.length, 2);
|
||||
|
||||
let firstBubbleItem = CitationDataManager.items[0];
|
||||
IOManager._deleteItem(firstBubbleItem.dialogReferenceID);
|
||||
|
||||
bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(CitationDataManager.items.length, 1);
|
||||
assert.equal(CitationDataManager.items[0].id, itemTwo.id);
|
||||
assert.equal(bubbles.length, 1);
|
||||
assert.equal(bubbles[0].textContent, itemTwo.getCreator(0).lastName);
|
||||
});
|
||||
|
||||
it("should build citation with a cited item in library", async function () {
|
||||
io.citation.citationItems = [citedItemOne];
|
||||
|
||||
await CitationDataManager.buildCitation();
|
||||
IOManager.updateBubbleInput();
|
||||
|
||||
let bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(CitationDataManager.items.length, 1);
|
||||
assert.equal(bubbles.length, 1);
|
||||
assert.equal(bubbles[0].textContent, itemOne.getCreator(0).lastName);
|
||||
});
|
||||
|
||||
it("should build citation with a cited item not in library", async function () {
|
||||
io.citation.citationItems = [citedItemNotInLibrary];
|
||||
|
||||
await CitationDataManager.buildCitation();
|
||||
IOManager.updateBubbleInput();
|
||||
|
||||
let bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(CitationDataManager.items.length, 1);
|
||||
assert.equal(bubbles.length, 1);
|
||||
assert.equal(bubbles[0].textContent, surrogateCitedItem.getCreator(0).lastName);
|
||||
});
|
||||
|
||||
it("should add a locator/suffix/prefix to a bubble", async function () {
|
||||
// add two bubbles for the same item
|
||||
await IOManager.addItemsToCitation([itemOne, itemOne]);
|
||||
assert.equal(CitationDataManager.items.length, 2);
|
||||
|
||||
// open popup
|
||||
let firstBubble = CitationDataManager.items[0];
|
||||
IOManager._openItemDetailsPopup(firstBubble.dialogReferenceID);
|
||||
let popup = dialog.document.getElementById("itemDetails");
|
||||
|
||||
// give the popup time to open
|
||||
await Zotero.Promise.delay(50);
|
||||
assert.equal(popup.state, "open");
|
||||
|
||||
// set locator/suffix/prefix values
|
||||
popup.querySelector("#locator").value = "10";
|
||||
popup.querySelector("#suffix").value = "suffix";
|
||||
popup.querySelector("#prefix").value = "prefix";
|
||||
popup.querySelector("#prefix").dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
// make sure they are set on the bubbleItem
|
||||
assert.equal(firstBubble.locator, "10");
|
||||
assert.equal(firstBubble.suffix, "suffix");
|
||||
assert.equal(firstBubble.prefix, "prefix");
|
||||
let bubble = dialog.document.querySelector(`.bubble[dialogReferenceID="${firstBubble.dialogReferenceID}"]`);
|
||||
assert.equal(bubble.textContent, "prefix Last_One, p. 10 suffix");
|
||||
|
||||
// make sure the other bubbleItem is not affected
|
||||
let secondBubble = CitationDataManager.items[1];
|
||||
assert.notOk(secondBubble.locator);
|
||||
assert.notOk(secondBubble.suffix);
|
||||
assert.notOk(secondBubble.prefix);
|
||||
});
|
||||
|
||||
it("should change the order of bubbles", async function () {
|
||||
// add two items
|
||||
await IOManager.addItemsToCitation([itemOne, itemTwo]);
|
||||
let bubbleItemOne = CitationDataManager.items[0];
|
||||
let bubbleItemTwo = CitationDataManager.items[1];
|
||||
|
||||
// check initial order
|
||||
let bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(bubbles[0].getAttribute("dialogReferenceID"), bubbleItemOne.dialogReferenceID);
|
||||
assert.equal(bubbles[1].getAttribute("dialogReferenceID"), bubbleItemTwo.dialogReferenceID);
|
||||
|
||||
// move the second item to the first position
|
||||
IOManager._moveItem(bubbleItemTwo.dialogReferenceID, 0);
|
||||
|
||||
// ensure the order is correct
|
||||
assert.equal(CitationDataManager.items[0].dialogReferenceID, bubbleItemTwo.dialogReferenceID);
|
||||
assert.equal(CitationDataManager.items[1].dialogReferenceID, bubbleItemOne.dialogReferenceID);
|
||||
bubbles = dialog.document.querySelector("bubble-input").getAllBubbles();
|
||||
assert.equal(bubbles[0].getAttribute("dialogReferenceID"), bubbleItemTwo.dialogReferenceID);
|
||||
assert.equal(bubbles[1].getAttribute("dialogReferenceID"), bubbleItemOne.dialogReferenceID);
|
||||
});
|
||||
|
||||
it("should sort the citation", async function () {
|
||||
// Make dialog sortable
|
||||
io.citation.sortable = true;
|
||||
dialog.document.getElementById("keepSorted").checked = true;
|
||||
// Mock sort.io implementation that sorts itemOne to the first position
|
||||
io.sort = () => {
|
||||
let items = io.citation.citationItems;
|
||||
items.sort((a, b) => {
|
||||
if (a.id === itemOne.id) return -1;
|
||||
if (b.id === itemOne.id) return 1;
|
||||
return 0;
|
||||
});
|
||||
io.citation.sortedItems = [
|
||||
[null, items[0]],
|
||||
[null, items[1]]
|
||||
];
|
||||
};
|
||||
|
||||
// Add items to citation in wrong order
|
||||
await IOManager.addItemsToCitation([itemTwo, itemOne]);
|
||||
|
||||
// Make sure the bubbleItems are sorted with itemOne being first
|
||||
let firstBubbleItem = CitationDataManager.items[0];
|
||||
let secondBubbleItem = CitationDataManager.items[1];
|
||||
assert.equal(firstBubbleItem.id, itemOne.id);
|
||||
assert.equal(secondBubbleItem.id, itemTwo.id);
|
||||
let bubbles = bubbleInput.getAllBubbles();
|
||||
assert.equal(bubbles[0].getAttribute("dialogReferenceID"), firstBubbleItem.dialogReferenceID);
|
||||
assert.equal(bubbles[1].getAttribute("dialogReferenceID"), secondBubbleItem.dialogReferenceID);
|
||||
});
|
||||
|
||||
it("should update io.citation.items from bubbles", async function () {
|
||||
let bubbleItems = CitationDataManager.items;
|
||||
// Build citation with several cited items
|
||||
io.citation.citationItems = [citedItemOne, citedItemNotInLibrary];
|
||||
await CitationDataManager.buildCitation();
|
||||
|
||||
// Add another item
|
||||
await IOManager.addItemsToCitation([itemTwo], { index: 2 });
|
||||
|
||||
// Add modifications
|
||||
bubbleItems[0].label = "page";
|
||||
bubbleItems[0].locator = "10";
|
||||
|
||||
bubbleItems[1].prefix = "prefix";
|
||||
bubbleItems[1].suffix = "suffix";
|
||||
|
||||
bubbleItems[2].suppressAuthor = true;
|
||||
|
||||
// Update io.citation.items and make sure it looks right
|
||||
CitationDataManager.updateCitationObject(true);
|
||||
let expected = [
|
||||
{
|
||||
id: itemOne.id,
|
||||
locator: "10",
|
||||
label: "page"
|
||||
},
|
||||
{
|
||||
id: citedItemNotInLibrary.id,
|
||||
suffix: "suffix",
|
||||
prefix: "prefix",
|
||||
itemData: citedItemNotInLibrary.itemData,
|
||||
uris: citedItemNotInLibrary.uris,
|
||||
},
|
||||
{
|
||||
id: itemTwo.id,
|
||||
"suppress-author": true
|
||||
}
|
||||
];
|
||||
assert.deepEqual(io.citation.citationItems, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UI", function () {
|
||||
beforeEach(function () {
|
||||
CitationDataManager.items = [];
|
||||
IOManager.updateBubbleInput();
|
||||
});
|
||||
|
||||
it("should switch dialog mode", async function () {
|
||||
IOManager.toggleDialogMode("list");
|
||||
assert.isFalse(dialog.document.getElementById("list-layout").hidden);
|
||||
assert.isTrue(dialog.document.getElementById("library-layout").hidden);
|
||||
IOManager.toggleDialogMode("library");
|
||||
assert.isFalse(dialog.document.getElementById("library-layout").hidden);
|
||||
assert.isTrue(dialog.document.getElementById("list-layout").hidden);
|
||||
});
|
||||
|
||||
it("should highlight bubbles whose items are selected", async function () {
|
||||
let itemOne = await createDataObject('item');
|
||||
let itemTwo = await createDataObject('item');
|
||||
|
||||
IOManager.toggleDialogMode("library");
|
||||
await IOManager.addItemsToCitation([itemOne, itemTwo]);
|
||||
|
||||
// Select row of the first bubble
|
||||
await dialog.libraryLayout.itemsView.selectItem(itemOne.id);
|
||||
// Check that the bubble is highlighted
|
||||
let bubbleOne = dialog.document.querySelector(`.bubble[dialogReferenceID="${CitationDataManager.items[0].dialogReferenceID}"]`);
|
||||
assert.isTrue(bubbleOne.classList.contains("has-item-selected"));
|
||||
// Check the other bubble is unaffected
|
||||
let bubbleTwo = dialog.document.querySelector(`.bubble[dialogReferenceID="${CitationDataManager.items[1].dialogReferenceID}"]`);
|
||||
assert.isFalse(bubbleTwo.classList.contains("has-item-selected"));
|
||||
});
|
||||
|
||||
it("should highlight rows of items in the citation", async function () {
|
||||
let itemOne = await createDataObject('item');
|
||||
IOManager.toggleDialogMode("library");
|
||||
|
||||
// Add the item to citation
|
||||
await IOManager.addItemsToCitation([itemOne]);
|
||||
// Select the row in itemTree, so it is visible
|
||||
await dialog.libraryLayout.itemsView.selectItem(itemOne.id);
|
||||
// Make sure the row node is highlighted
|
||||
let rowIndex = dialog.libraryLayout.itemsView.getRowIndexByID(itemOne.id);
|
||||
let rowID = "item-tree-citationDialog-row-" + rowIndex;
|
||||
let rowNode = dialog.document.getElementById(rowID);
|
||||
assert.isTrue(rowNode.classList.contains("highlighted"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Search", function () {
|
||||
let selectedOne, selectedTwo, openOne, openTwo, selectedAndOpenOne, citedOne, citedAndOpenOne, libraryOne, libraryTwo;
|
||||
|
||||
before(async function () {
|
||||
selectedOne = await createDataObject('item', { title: "one_selected" });
|
||||
selectedTwo = await createDataObject('item', { title: "two_selected" });
|
||||
openOne = await createDataObject('item', { title: "one_open" });
|
||||
openTwo = await createDataObject('item', { title: "two_open" });
|
||||
selectedAndOpenOne = await createDataObject('item', { title: "one_selected_open" });
|
||||
libraryOne = await createDataObject('item', { title: "one_library" });
|
||||
libraryTwo = await createDataObject('item', { title: "two_library" });
|
||||
citedOne = await createDataObject('item', { title: "one_cited" });
|
||||
citedAndOpenOne = await createDataObject('item', { title: "one_open_cited" });
|
||||
|
||||
// Present these items are selected/open/cited
|
||||
SearchHandler.selectedItems = [selectedOne, selectedTwo, selectedAndOpenOne];
|
||||
SearchHandler.openItems = [openOne, openTwo, selectedAndOpenOne, citedAndOpenOne];
|
||||
SearchHandler.citedItems = [citedOne, citedAndOpenOne];
|
||||
});
|
||||
|
||||
after(function () {
|
||||
SearchHandler.openItems = [];
|
||||
SearchHandler.selectedItems = [];
|
||||
});
|
||||
|
||||
it("should perform search in list mode", async function () {
|
||||
IOManager.toggleDialogMode("list");
|
||||
|
||||
// Search for "one"
|
||||
await dialog.currentLayout.search("one", { skipDebounce: true });
|
||||
// Selected items should have both "one_selected" and "one_selected_open"
|
||||
let selectedIDs = SearchHandler.results.selected.map(item => item.id);
|
||||
assert.sameMembers(selectedIDs, [selectedOne.id, selectedAndOpenOne.id]);
|
||||
// Open items should have "one_open" and "one_open_cited" but not "one_selected_open", since it is selected
|
||||
let openIDs = SearchHandler.results.open.map(item => item.id);
|
||||
assert.sameMembers(openIDs, [openOne.id, citedAndOpenOne.id]);
|
||||
// Cited items should have "one_cited"
|
||||
let citedIDs = SearchHandler.results.cited.map(item => item.id);
|
||||
assert.sameMembers(citedIDs, [citedOne.id]);
|
||||
// Library items should have "one_library" but not "two_library", "one_cited", or "one_open_cited"
|
||||
let libraryIDs = SearchHandler.results.found.map(item => item.id);
|
||||
assert.include(libraryIDs, libraryOne.id);
|
||||
assert.notInclude(libraryIDs, libraryTwo.id);
|
||||
assert.notInclude(libraryIDs, citedOne.id);
|
||||
// Make sure actual nodes for search matches are rendered
|
||||
let expectedItemCardIDs = [...selectedIDs, ...openIDs, ...citedIDs, ...libraryIDs];
|
||||
for (let itemID of expectedItemCardIDs) {
|
||||
let node = dialog.document.querySelector(`.item[id="${itemID}"]`);
|
||||
assert.isOk(node);
|
||||
}
|
||||
});
|
||||
|
||||
it("should perform search in library mode", async function () {
|
||||
IOManager.toggleDialogMode("library");
|
||||
|
||||
// Search for "one"
|
||||
await dialog.currentLayout.search("one", { skipDebounce: true });
|
||||
// Selected items should have both "one_selected" and "one_selected_open"
|
||||
let selectedIDs = SearchHandler.results.selected.map(item => item.id);
|
||||
assert.sameMembers(selectedIDs, [selectedOne.id, selectedAndOpenOne.id]);
|
||||
// Open items should have "one_open" and "one_open_cited" but not "one_selected_open", since it is selected
|
||||
let openIDs = SearchHandler.results.open.map(item => item.id);
|
||||
assert.sameMembers(openIDs, [openOne.id, citedAndOpenOne.id]);
|
||||
// Cited items should have "one_cited"
|
||||
let citedIDs = SearchHandler.results.cited.map(item => item.id);
|
||||
assert.sameMembers(citedIDs, [citedOne.id]);
|
||||
// In library mode, library is searched via itemTree, so this should be empty
|
||||
assert.equal(SearchHandler.results.found.length, 0);
|
||||
// Make sure actual nodes for search matches are rendered
|
||||
let expectedItemCardIDs = [...selectedIDs, ...openIDs, ...citedIDs];
|
||||
for (let itemID of expectedItemCardIDs) {
|
||||
let node = dialog.document.querySelector(`.item[id="${itemID}"]`);
|
||||
assert.isOk(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Helpers.extractLocator", function () {
|
||||
let locator;
|
||||
describe("Invalid locators", function () {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue