Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Previous revision
dev:client_coding:javascript_api [2017/12/18 09:04]
dev:client_coding:javascript_api [2019/08/18 05:41] (current)
zuphilip [Running Ad Hoc JavaScript in Zotero] Add examples
Line 1: Line 1:
 +====== Zotero JavaScript API ======
  
 +Whereas Zotero'​s [[dev:​web_api|Web API]] allows read and write access to online Zotero libraries, it is also possible to access the local Zotero client through the local JavaScript API. (It is also possible to [[dev/​client_coding/​direct_sqlite_database_access|directly access the local SQLite database]], but that approach is much more fragile.)
 +
 +Note that the (mostly user-contributed) documentation of the JavaScript API is not comprehensive. If you use the JavaScript API in ways beyond what's described here, please consider expanding this wiki page.
 +
 +
 +===== Running Ad Hoc JavaScript in Zotero =====
 +
 +Zotero includes an option to run arbitrary privileged JavaScript:
 +
 +  - In the Tools → Developer menu, select Run JavaScript. Opening the Error Console, which appears in the same menu, will also be helpful.
 +  - In the window that opens, enter JavaScript in the Code textbox and click Run or press Cmd-R/​Ctrl-R.
 +
 +When running **asynchronous** code containing ''​await'',​ the entered code is wrapped in an [[https://​developer.mozilla.org/​en-US/​docs/​Web/​JavaScript/​Reference/​Statements/​async_function|async function]], allowing you to wait for the resolution of promises returned by functions. Most Zotero functions that access the database, disk, or network are asynchronous. In this mode, the value of a ''​return''​ statement will be displayed in the right-hand pane upon successful completion. E.g. returning the currently selected item(s)
 +
 +<code javascript>​
 +var items = Zotero.getActiveZoteroPane().getSelectedItems();​
 +return items;
 +</​code>​
 +
 +In **synchronous** mode, the value of the final line will appear in the right-hand pane. The same result as above could be achieved in synchonous mode with
 +
 +<code javascript>​
 +var items = Zotero.getActiveZoteroPane().getSelectedItems();​
 +items;
 +</​code>​
 +===== Zotero Code Architecture =====
 +
 +==== Window Scope vs. Non-Window Scope ===
 +
 +Zotero code exists in either window scope and non-window scope.
 +
 +Window scope applies to code that runs within either the main Zotero window or a secondary window (e.g., the Advanced Search window). It has access to the window'​s DOM and can interact with the UI. The main window-scope object in Zotero is ''​ZoteroPane'',​ from zoteroPane.js,​ which controls most interactions triggered in the main Zotero window.
 +
 +Non-window scope applies to lower-level code that doesn'​t have access to the DOM. This includes the core ''​Zotero''​ object, which contains all other non-window code, including the data layer used for retrieving and modifying library data. In Zotero, non-window code is contained within the ''​xpcom''​ subdirectory.
 +
 +Overlays and windows in Zotero can import the core ''​Zotero''​ object via the include.js script. Zotero methods can then be called from anywhere within the window'​s scope simply by calling, for example, ''​var item = Zotero.Items.get(1);''​.
 +
 +To access Zotero functionality from your own extension, you will need access to the core ''​Zotero''​ object. If your extension operates within the main browser overlay, you already have access to the ''​Zotero''​ object and don’t need to take further steps. Otherwise, you can import the Zotero object into other windows by including the script %%chrome://​zotero/​content/​include.js%% within an HTML or XUL file:
 +
 +<code html>
 +<script src="​chrome://​zotero/​content/​include.js"><​script>​
 +</​code>​
 +
 +Once you have ''​Zotero'',​ you can get the current ''​ZoteroPane''​ object:
 +<code javascript>​
 +var zp = Zotero.getActiveZoteroPane();​
 +var items = zp.getSelectedItems();​
 +</​code> ​
 +(The Zotero pane will always be available unless the main window is closed, as is possible on macOS.)
 +
 +==== Notification System ====
 +
 +Zotero has a built-in notification system that allows other privileged code to be notified when a change is made via the data layer — for example, when an item is added to the library. Within Zotero itself, this is used mostly to update the UI when items change, but extensions can use the system to perform additional operations when specific events occur — say, to sync item metadata with a web-based API.
 +
 +Available events:
 +
 +  * add (c, s, i, t, ci, it)
 +  * modify (c, s, i, t)
 +  * delete (c, s, i, t)
 +  * remove (ci, it)
 +  * move (c, for changing collection parent)
 +
 +c = collection, s = search (saved search), i = item, t = tag, ci = collection-item,​ it = item-tag
 +
 +See the Zotero [[dev/​Sample Plugin]] for a demonstration.
 +
 +===== API Methods =====
 +
 +//The Zotero JavaScript API is under-documented,​ and at present requires a lot of looking around in the source code.  The most useful parts of the source code are in chrome/​content/​zotero/​xpcom and xpcom/data, and the chrome/​content/​zotero (particularly zoteroPane.js and fileInterface.js).//​
 +
 +==== Adding items and modifying data ====
 +
 +A typical operation might include a call to ''​Zotero.Items.get()''​ to retrieve a ''​Zotero.Item''​ instance, calls to ''​Zotero.Item''​ methods on the retrieved object to modify data, and finally a ''​save()''​ (within a transaction) or ''​saveTx()''​ (outside a transaction) to save the modified data to the database.
 +
 +<code javascript>​
 +var item = new Zotero.Item('​book'​);​
 +item.setField('​title',​ 'Much Ado About Nothing'​);​
 +item.setCreators(
 +    [
 +        {
 +            firstName: "​William",​
 +            lastName: "​Shakespeare",​
 +            creatorType:​ "​author"​
 +        }
 +    ]
 +);
 +var itemID = await item.save();​
 +return itemID;
 +</​code>​
 +
 +<code javascript>​
 +
 +// Fetch saved items with Items.get(itemID)
 +var item = Zotero.Items.get(itemID);​
 +
 +alert(item.getField('​title'​));​ // "Much Ado About Nothing"​
 +
 +alert(item.getCreator(0));​ // {'​firstName'​=>'​William',​ '​lastName'​=>'​Shakespeare',​
 +                           // ​  '​creatorTypeID'​=>​1,​ '​fieldMode'​=>​0}
 +// Alternative format
 +alert(item.getCreatorJSON(0));​ // {'​firstName'​=>'​William',​ '​lastName'​=>'​Shakespeare',​
 +                           // ​  '​creatorType'​=>'​author'​}
 +
 +item.setField('​place',​ '​England'​);​
 +item.setField('​date',​ 1599);
 +await item.saveTx();​ // update database with new data
 +
 +</​code>​
 +
 +==== Get the Zotero Pane to interact with the Zotero UI ====
 +
 +<code javascript>​
 +var ZoteroPane = Zotero.getActiveZoteroPane();​
 +</​code>​
 +
 +Then grab the currently selected items from the Zotero pane:
 +
 +<code javascript>​
 +// Get first selected item
 +var selectedItems = ZoteroPane.getSelectedItems();​
 +var item = selectedItems[0];​
 +
 +// Proceed if an item is selected and it isn't a note
 +if (item && !item.isNote()) {
 +    if (item.isAttachment()) {
 +        // find out about attachment
 +    }
 +    if (item.isRegularItem()) {
 +        // We could grab attachments:​
 +        // let attachmentIDs = item.getAttachments();​
 +        // let attachment = Zotero.Items.get(attachmentIDs[0]);​
 +    }
 +    alert(item.id);​
 +}
 +</​code>​
 +
 +==== Collection Operations ====
 +
 +=== Get the items in the selected collection ===
 +
 +<code javascript>​
 +var collection = ZoteroPane.getSelectedCollection();​
 +var items = collection.getChildItems();​
 +// or you can obtain an array of itemIDs instead:
 +var itemIDs = collection.getChildItems(true);​
 +</​code>​
 +
 +=== Create a subcollection of the selected collection ===
 +
 +<code javascript>​
 +var currentCollection = ZoteroPane.getSelectedCollection();​
 +var collection = new Zotero.Collection();​
 +collection.name = name;
 +collection.parentID = currentCollection.id;​
 +var collectionID = await collection.saveTx();​
 +return collectionID;​
 +</​code>​
 +
 +==== Zotero Search Basics ====
 +
 +If you are focused on data access, the first thing you will want to do
 +will be to retrieve items from your Zotero library. Creating an in-memory search is a
 +good start.
 +
 +<code javascript>​
 +var s = new Zotero.Search();​
 +s.libraryID = Zotero.Libraries.userLibraryID;​
 +</​code>​
 +
 +=== Search for items containing a specific tag ===
 +
 +Starting with the above code, we then use the following code to retrieve items in My Library with a particular tag:
 +
 +<code javascript>​
 +s.addCondition('​tag',​ '​is',​ 'tag name here'​);​
 +var itemIDs = await s.search();
 +</​code>​
 +
 +=== Advanced searches ===
 +
 +<code javascript>​
 +var s = new Zotero.Search();​
 +s.libraryID = Zotero.Libraries.userLibraryID;​
 +s.addCondition('​joinMode',​ '​any'​);​ // joinMode defaults to '​all'​ as per the 
 +                                        // advanced search UI
 +</​code>​
 +
 +To add the other conditions available in the advanced search UI, use the following:
 +
 +<code javascript>​
 +s.addCondition('​recursive',​ '​true'​); ​ // equivalent of "​Search subfolders"​ checked
 +s.addCondition('​noChildren',​ '​true'​);​ //               "​Only show top level children
 +s.addCondition('​includeParentsAndChildren',​ '​true'​); ​  "​Include parent and child ..."
 +</​code>​
 +
 +There are also some "​hidden"​ search conditions around line 1735 of chrome/​content/​zotero/​xpcom/​search.js in the Zotero source code (although some should only be used internally). ​ [TODO remove mention of source code and enumerate all conditions]
 +
 +One example is:
 +
 +<code javascript>​
 +s.addCondition('​deleted',​ '​true'​); ​ // Include deleted items
 +</​code>​
 +
 +=== Search by collection ===
 +
 +To search for a collection or a saved search you need to know the ID or key:
 +
 +<code javascript>​
 +s.addCondition('​collectionID',​ '​is',​ collectionID);​ // e.g., 52
 +s.addCondition('​savedSearchID',​ '​is',​ savedSearchID);​
 +</​code>​
 +
 +<code javascript>​
 +s.addCondition('​collection',​ '​is',​ collectionKey);​ // e.g., '​C72FDAP2'​
 +s.addCondition('​savedSearch',​ '​is',​ savedSearchKey);​
 +</​code>​
 +
 +=== Search by tag ===
 +
 +To search by tag, you use the tag text:
 +
 +<code javascript>​
 +var tagName = '​something';​
 +s.addCondition('​tag',​ '​is',​ tagName);
 +</​code>​
 +
 +=== Search by other fields ===
 +
 +The complete list of other fields available to search on is on the [[dev/​client_coding/​javascript_api/​search fields]] page.
 +
 +==== Executing the search ====
 +
 +Once the search conditions have been set up, then it's time to execute the
 +results:
 +
 +<code javascript>​var results = await s.search();</​code>​
 +
 +This returns the item ids in the search as an array. The next thing to do is to get the Zotero items for the array of IDs:
 +
 +<code javascript>​var items = await Zotero.Items.getAsync(results);</​code>​
 +
 +===== Managing citations and bibliographies =====
 +
 +TODO:  this is pretty sparse. ​ the rtfscan code is a good place to look for some guidance.
 +
 +==== Getting a bibliography for an array of items: ====
 +
 +Here we use Zotero'​s Quick Copy functions to get a bibliography in the style
 +specified in Zotero'​s preferences.
 +
 +First we start with a list of as in the previous entry.
 +
 +<code javascript>​var qc = Zotero.QuickCopy;​
 +var biblio = qc.getContentFromItems([item],​ Zotero.Prefs.get("​export.quickCopy.setting"​));​
 +var biblio_html_format = cite.html;
 +var biblio_txt ​ = cite.text; ​
 +</​code>​
 +
 +==== Get a list of available styles ====
 +
 +<code javascript> ​
 +await Zotero.Schema.schemeUpdatePromise;​
 +var styles = Zotero.Styles.getVisible();​
 +var styleInfo = [];
 +for (let style of styles) {
 +    styleInfo.push({ id: style.styleID,​ name: style.title });
 +}
 +return styleInfo;
 +</​code>​
 +
 +TODO:  get citations. ​ change the style. ​ get stuff in other formats,
 +especially RTF
 +
 +==== Get information about an item. ====
 +
 +TODO:  need to list all the possible fields here, and what kind of entry they
 +belong to.
 +
 +To get an item's abstract, we get the '​abstractNote'​ field from the Zotero item:
 +
 +<code javascript>​var abstract = item.getField('​abstractNote'​);</​code>​
 +
 +==== Get child notes for an item ====
 +
 +To get the child notes for an item, we use the following code:
 +
 +<code javascript>​var noteIDs = item.getNotes();</​code>​
 +
 +This returns an array of ids of note items. To get each note in turn we just iterate through the array:
 +
 +<code javascript>​
 +for (let id of noteIDs) {
 +    let note = Zotero.Items.get(id);​
 +    let noteHTML = note.getNote();​
 +
 +</​code>​
 +
 +==== Get an item's related items ====
 +
 +<code javascript>​var relatedItems = item.relatedItems;</​code>​
 +
 +==== Set two items as related to each other  ====
 +
 +Given two items ''​itemA''​ and ''​itemB''​. We can set them as related items
 +to each other by using the ''​addRelatedItem''​ function:
 +
 +<code javascript>​itemA.addRelatedItem(itemB);​
 +await itemA.saveTx();​
 +itemB.addRelatedItem(itemA);​
 +await itemB.saveTx();</​code>​
 +
 +
 +==== Get an item's attachments ===
 +
 +Here's some example code to get the full text of HTML and PDF items in storage and put the data in an array:
 +
 +<code javascript>​
 +var item = ZoteroPane.getSelectedItems()[0];​
 +var fulltext = [];
 +if (item.isRegularItem()) { // not an attachment already
 +    let attachmentIDs = item.getAttachments();​
 +    for (let id of attachmentIDs) {
 +        let attachment = Zotero.Items.get(id);​
 +        if (attachment.attachmentContentType == '​application/​pdf'​
 +                || attachment.attachmentContentType == '​text/​html'​) {
 +            fulltext.push(await attachment.attachmentText);​
 +        }
 +    }
 +}
 +return fulltext;
 +</​code>​
 +
 +==== To Do ===
 +
 +  * Select a Zotero saved search
 +  * Combining search terms
 +  * Complete list of search operators
 +  * Complete list of search fields - with description of what the more obscure fields mean - e.g. abstractNote for abstract, and how do we search the fulltext archive?
 +  * fulltext for an item
 +  * Get stored attachments for an item
 +
 +
 +=====  Batch Editing =====
 +
 +The JavaScript API can provide a powerful way to script changes to your Zotero library. The common case of search-and-replace is accomplished easily using a basic script.
 +
 +Before proceeding, back up your [[:​zotero_data|Zotero data directory]] and temporarily disable auto-sync in the Sync pane of the Zotero preferences.
 +
 +==== Example: Item Field Changes ====
 +
 +Edit the first three lines as necessary:
 +
 +<code javascript>​var fieldName = "​publicationTitle";​
 +var oldValue = "​Foo";​
 +var newValue = "​Foo2";​
 +
 +var fieldID = Zotero.ItemFields.getID(fieldName);​
 +var s = new Zotero.Search();​
 +s.libraryID = Zotero.Libraries.userLibraryID;​
 +s.addCondition(fieldName,​ '​is',​ oldValue);
 +var ids = await s.search();
 +if (!ids.length) {
 +    return "No items found";​
 +}
 +await Zotero.DB.executeTransaction(async function () {
 +    for (let id of ids) {
 +        let item = await Zotero.Items.getAsync(id);​
 +        let mappedFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(item.itemTypeID,​ fieldName);
 +        item.setField(mappedFieldID ? mappedFieldID : fieldID, newValue);
 +        await item.save();​
 +    }
 +});
 +return ids.length + " item(s) updated";</​code>​
 +
 +The list of field names to use can be retrieved via the web API:
 +
 +https://​api.zotero.org/​itemFields?​pprint=1.
 +
 +==== Example: Delete Tags By Name ====
 +
 +<​html><​p id="​zotero-5-update-warning"​ style="​color:​ red; font-weight:​ bold">​This example has not been updated for Zotero 5 and should not currently be used.</​p></​html>​
 +
 +<code javascript>​var tags = ["​foo",​ "​bar",​ "​baz"​];​
 +var ids = [];
 +var allTags = Zotero.Tags.search();​
 +tags = tags.map(tag => tag.toLowerCase());​
 +for (var id in allTags) {
 +    if (tags.indexOf(allTags[id].name.toLowerCase()) != -1) {
 +      ids.push(id);​
 +    }
 +}
 +Zotero.Tags.erase(ids);​
 +</​code>​
 +
 +==== Example: Delete Tags By Part of Name ====
 +
 +<​html><​p id="​zotero-5-update-warning"​ style="​color:​ red; font-weight:​ bold">​This example has not been updated for Zotero 5 and should not currently be used.</​p></​html>​
 +
 +<code javascript>​var tags = ["​foo",​ "​bar",​ "​baz"​];​
 +var ids = [];
 +tags.forEach(function (tag) {
 +    ids = ids.concat(Object.keys(Zotero.Tags.search(tag)));​
 +});
 +Zotero.Tags.erase(ids);</​code>​