Addresses #400, Report generation
A work in progress: - Implemented zotero:// custom protocol handler, which will likely be useful for other things too - First version of XHTML/CSS detail view -- definitely needs feedback, work, and refinement but is more or less functional - Added XUL-side interface and context menu options for loading report URLs Going forward: - Other formats (RTF, CSV) - Other views (list view, annotated bibliography, etc.) - Report options window (let the user which fields to include (with saved templates?)) - Ability to specify custom CSS files? - Extension of Zotero protocol handler to trigger Zotero events? This would allow more interactive reports with the ability to click to select items in the Z pane, run searches by clicking on tags, etc., but would have to be limited to idempotent actions. Other changes: - ZoteroPane.getSortField() and ZoteroPane.getSortDirection() - Zotero.Utilities.htmlSpecialChars(str) - Fixed sort direction in items pane (triangle icon now goes the right direction, though the default direction on clicking a new column is incorrect) - firstCreator now included in toArray(), though it's not particularly correct (#287, more or less) - ZoteroPane.getSelectedCollection/SavedSearch/Items now take asIDs parameter to return ids instead of objects
This commit is contained in:
parent
ab7f618a3e
commit
d3e29108a8
|
@ -51,6 +51,8 @@ var ZoteroPane = new function()
|
|||
this.getSelectedCollection = getSelectedCollection;
|
||||
this.getSelectedSavedSearch = getSelectedSavedSearch;
|
||||
this.getSelectedItems = getSelectedItems;
|
||||
this.getSortField = getSortField;
|
||||
this.getSortDirection = getSortDirection;
|
||||
this.buildCollectionContextMenu = buildCollectionContextMenu;
|
||||
this.buildItemContextMenu = buildItemContextMenu;
|
||||
this.onDoubleClick = onDoubleClick;
|
||||
|
@ -536,11 +538,17 @@ var ZoteroPane = new function()
|
|||
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns Zotero.ItemTreeView instance for collections pane
|
||||
*/
|
||||
function getCollectionsView()
|
||||
{
|
||||
return collectionsView;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns Zotero.ItemTreeView instance for items pane
|
||||
*/
|
||||
function getItemsView()
|
||||
{
|
||||
return itemsView;
|
||||
|
@ -568,31 +576,46 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
|
||||
function getSelectedCollection()
|
||||
function getSelectedCollection(asID)
|
||||
{
|
||||
if(collectionsView.selection.count > 0 && collectionsView.selection.currentIndex != -1)
|
||||
{
|
||||
var collection = collectionsView._getItemAtRow(collectionsView.selection.currentIndex);
|
||||
if(collection && collection.isCollection())
|
||||
return collection.ref;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getSelectedSavedSearch()
|
||||
{
|
||||
if(collectionsView.selection.count > 0 && collectionsView.selection.currentIndex != -1)
|
||||
{
|
||||
var collection = collectionsView._getItemAtRow(collectionsView.selection.currentIndex);
|
||||
if(collection && collection.isSearch())
|
||||
{
|
||||
return collection.ref;
|
||||
if (collection && collection.isCollection()) {
|
||||
if (asID) {
|
||||
return collection.ref.getID();
|
||||
}
|
||||
else {
|
||||
return collection.ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getSelectedItems()
|
||||
function getSelectedSavedSearch(asID)
|
||||
{
|
||||
if(collectionsView.selection.count > 0 && collectionsView.selection.currentIndex != -1)
|
||||
{
|
||||
var collection = collectionsView._getItemAtRow(collectionsView.selection.currentIndex);
|
||||
if (collection && collection.isSearch()) {
|
||||
if (asID) {
|
||||
return collection.ref.id;
|
||||
}
|
||||
else {
|
||||
return collection.ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return an array of Item objects for selected items
|
||||
*
|
||||
* If asIDs is true, return an array of itemIDs instead
|
||||
*/
|
||||
function getSelectedItems(asIDs)
|
||||
{
|
||||
if(itemsView)
|
||||
{
|
||||
|
@ -602,13 +625,38 @@ var ZoteroPane = new function()
|
|||
for (var i=0, len=itemsView.selection.getRangeCount(); i<len; i++)
|
||||
{
|
||||
itemsView.selection.getRangeAt(i,start,end);
|
||||
for (var j=start.value; j<=end.value; j++)
|
||||
items.push(itemsView._getItemAtRow(j).ref);
|
||||
for (var j=start.value; j<=end.value; j++) {
|
||||
if (asIDs) {
|
||||
items.push(itemsView._getItemAtRow(j).ref.getID());
|
||||
}
|
||||
else {
|
||||
items.push(itemsView._getItemAtRow(j).ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
function getSortField() {
|
||||
if (!itemsView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return itemsView.getSortField();
|
||||
}
|
||||
|
||||
|
||||
function getSortDirection() {
|
||||
if (!itemsView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return itemsView.getSortDirection();
|
||||
}
|
||||
|
||||
|
||||
function buildCollectionContextMenu()
|
||||
{
|
||||
var menu = document.getElementById('zotero-collectionmenu');
|
||||
|
@ -618,36 +666,39 @@ var ZoteroPane = new function()
|
|||
collectionsView._getItemAtRow(collectionsView.selection.currentIndex).isCollection())
|
||||
{
|
||||
var hide = [0,1,5,7,10,12,13];
|
||||
var show = [2,3,4,6,8,9,11];
|
||||
var show = [2,3,4,6,8,9,11,14];
|
||||
if (itemsView.rowCount>0)
|
||||
{
|
||||
var enable = [9,11];
|
||||
var enable = [9,11,14];
|
||||
}
|
||||
else
|
||||
{
|
||||
var disable = [9,11];
|
||||
var disable = [9,11,14];
|
||||
}
|
||||
|
||||
|
||||
menu.childNodes[14].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection'));
|
||||
}
|
||||
// Saved Search
|
||||
else if (collectionsView.selection.count == 1 &&
|
||||
collectionsView._getItemAtRow(collectionsView.selection.currentIndex).isSearch())
|
||||
{
|
||||
var hide = [0,1,2,3,4,6,9,11,13];
|
||||
var show = [5,7,8,10,12];
|
||||
var show = [5,7,8,10,12,14];
|
||||
if (itemsView.rowCount>0)
|
||||
{
|
||||
var enable = [10,12];
|
||||
var enable = [10,12,14];
|
||||
}
|
||||
else
|
||||
{
|
||||
var disable = [10,12];
|
||||
var disable = [10,12,14];
|
||||
}
|
||||
|
||||
menu.childNodes[14].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch'));
|
||||
}
|
||||
// Library
|
||||
else
|
||||
{
|
||||
var hide = [2,4,5,6,7,8,9,10,11,12];
|
||||
var hide = [2,4,5,6,7,8,9,10,11,12,14];
|
||||
var show = [0,1,3,13];
|
||||
}
|
||||
|
||||
|
@ -680,7 +731,7 @@ var ZoteroPane = new function()
|
|||
|
||||
if(itemsView && itemsView.selection.count > 0)
|
||||
{
|
||||
enable.push(4,5,6,8);
|
||||
enable.push(0,1,2,4,5,6,7,8,9);
|
||||
|
||||
// Multiple items selected
|
||||
if (itemsView.selection.count > 1)
|
||||
|
@ -707,7 +758,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
else
|
||||
{
|
||||
disable.push(4,5,7,8);
|
||||
disable.push(0,1,2,4,5,7,8,9);
|
||||
}
|
||||
|
||||
// Remove from collection
|
||||
|
@ -725,6 +776,7 @@ var ZoteroPane = new function()
|
|||
menu.childNodes[5].setAttribute('label', Zotero.getString('pane.items.menu.erase' + multiple));
|
||||
menu.childNodes[7].setAttribute('label', Zotero.getString('pane.items.menu.export' + multiple));
|
||||
menu.childNodes[8].setAttribute('label', Zotero.getString('pane.items.menu.createBib' + multiple));
|
||||
menu.childNodes[9].setAttribute('label', Zotero.getString('pane.items.menu.generateReport' + multiple));
|
||||
|
||||
for (var i in disable)
|
||||
{
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
<script src="overlay.js"/>
|
||||
<script src="fileInterface.js"/>
|
||||
<script src="reportInterface.js"/>
|
||||
|
||||
<commandset id="mainCommandSet">
|
||||
<command id="cmd_zotero_search" oncommand="ZoteroPane.search();"/>
|
||||
|
@ -73,6 +74,7 @@
|
|||
<menuitem label="&zotero.toolbar.createBibCollection.label;" oncommand="Zotero_File_Interface.bibliographyFromCollection();"/>
|
||||
<menuitem label="&zotero.toolbar.createBibSavedSearch.label;" oncommand="Zotero_File_Interface.bibliographyFromCollection()"/>
|
||||
<menuitem label="&zotero.toolbar.export.label;" oncommand="Zotero_File_Interface.exportFile()"/>
|
||||
<menuitem oncommand="Zotero_Report_Interface.loadCollectionReport()"/>
|
||||
</popup>
|
||||
<popup id="zotero-itemmenu" onpopupshowing="ZoteroPane.buildItemContextMenu();">
|
||||
<menuitem label="&zotero.items.menu.attach.note;" oncommand="ZoteroPane.newNote(false, this.parentNode.getAttribute('itemID'))"/>
|
||||
|
@ -84,6 +86,7 @@
|
|||
<menuseparator/>
|
||||
<menuitem oncommand="Zotero_File_Interface.exportItems();"/>
|
||||
<menuitem oncommand="Zotero_File_Interface.bibliographyFromItems();"/>
|
||||
<menuitem oncommand="Zotero_Report_Interface.loadItemReport()"/>
|
||||
</popup>
|
||||
</popupset>
|
||||
<vbox id="zotero-collections-pane" persist="width" flex="1">
|
||||
|
|
84
chrome/content/zotero/reportInterface.js
Normal file
84
chrome/content/zotero/reportInterface.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (c) 2006 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://chnm.gmu.edu
|
||||
|
||||
Licensed under the Educational Community License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.opensource.org/licenses/ecl1.php
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
|
||||
Zotero_Report_Interface = new function() {
|
||||
this.loadCollectionReport = loadCollectionReport;
|
||||
this.loadItemReport = loadItemReport;
|
||||
this.loadItemReportByIds = loadItemReportByIds;
|
||||
|
||||
|
||||
/*
|
||||
* Load a report for the currently selected collection
|
||||
*/
|
||||
function loadCollectionReport() {
|
||||
var queryString = '';
|
||||
|
||||
var id = ZoteroPane.getSelectedCollection(true);
|
||||
var sortColumn = ZoteroPane.getSortField();
|
||||
var sortDirection = ZoteroPane.getSortDirection();
|
||||
|
||||
if (sortColumn != 'title' || sortDirection != 'ascending') {
|
||||
queryString = '?sort=' + sortColumn +
|
||||
(sortDirection != 'ascending' ? '/d' : '');
|
||||
}
|
||||
|
||||
if (id) {
|
||||
window.loadURI('zotero://report/collection/' + id + queryString);
|
||||
return;
|
||||
}
|
||||
|
||||
var id = ZoteroPane.getSelectedSavedSearch(true);
|
||||
if (id) {
|
||||
window.loadURI('zotero://report/search/' + id + queryString);
|
||||
return;
|
||||
}
|
||||
|
||||
throw ('No collection currently selected');
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load a report for the currently selected collection
|
||||
*/
|
||||
function loadItemReport() {
|
||||
var items = ZoteroPane.getSelectedItems(true);
|
||||
|
||||
if (!items || !items.length) {
|
||||
throw ('No items currently selected');
|
||||
}
|
||||
|
||||
window.loadURI('zotero://report/items/' + items.join('-'));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load a report for the specified items
|
||||
*/
|
||||
function loadItemReportByIds(ids) {
|
||||
if (!ids || !ids.length) {
|
||||
throw ('No itemIDs provided to loadItemReportByIds()');
|
||||
}
|
||||
|
||||
window.loadURI('zotero://report/items/' + ids.join('-'));
|
||||
}
|
||||
}
|
|
@ -1758,7 +1758,7 @@ Zotero.Item.prototype.toArray = function(){
|
|||
break;
|
||||
|
||||
// Skip certain fields
|
||||
case 'firstCreator':
|
||||
//case 'firstCreator':
|
||||
case 'numNotes':
|
||||
case 'numAttachments':
|
||||
continue;
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
* Constructor the the ItemTreeView object
|
||||
* Constructor for the ItemTreeView object
|
||||
*/
|
||||
Zotero.ItemTreeView = function(itemGroup, sourcesOnly)
|
||||
{
|
||||
|
@ -526,7 +526,7 @@ Zotero.ItemTreeView.prototype.sort = function()
|
|||
// DEBUG: This may be necessary for 1.0.0b2.r1=>1.0.0b2.r2 transition
|
||||
column = this._treebox.columns.getFirstColumn();
|
||||
}
|
||||
var order = column.element.getAttribute('sortDirection') == 'descending';
|
||||
var order = column.element.getAttribute('sortDirection') == 'ascending';
|
||||
|
||||
if(column.id == 'zotero-items-typeIcon-column')
|
||||
{
|
||||
|
@ -767,6 +767,18 @@ Zotero.ItemTreeView.prototype.rememberSelection = function(selection)
|
|||
}
|
||||
}
|
||||
|
||||
Zotero.ItemTreeView.prototype.getSortField = function() {
|
||||
var col = this._treebox.columns.getSortedColumn().id;
|
||||
// zotero.items._________.column
|
||||
return col.substring(13, col.length-7);
|
||||
}
|
||||
|
||||
|
||||
Zotero.ItemTreeView.prototype.getSortDirection = function() {
|
||||
return this._treebox.columns.getSortedColumn().element.getAttribute('sortDirection');
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Command Controller:
|
||||
|
|
272
chrome/content/zotero/xpcom/report.js
Normal file
272
chrome/content/zotero/xpcom/report.js
Normal file
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (c) 2006 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://chnm.gmu.edu
|
||||
|
||||
Licensed under the Educational Community License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.opensource.org/licenses/ecl1.php
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
|
||||
Zotero.Report = new function() {
|
||||
this.generateHTMLDetails = generateHTMLDetails;
|
||||
this.generateHTMLList = generateHTMLList;
|
||||
|
||||
// Sites that don't need the query string
|
||||
// (full URL is kept for link but stripped for display)
|
||||
var _noQueryStringSites = [
|
||||
/^http:\/\/([^\.]*\.)?nytimes\.com/
|
||||
];
|
||||
|
||||
function generateHTMLDetails(items) {
|
||||
var ZU = new Zotero.Utilities();
|
||||
var escapeXML = ZU.htmlSpecialChars;
|
||||
|
||||
var content = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ';
|
||||
content += '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n';
|
||||
content += '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n';
|
||||
content += '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n';
|
||||
content += '<title>' + Zotero.getString('report.title.default') + '</title>\n';
|
||||
content += '<link rel="stylesheet" type="text/css" href="chrome://zotero/skin/report/detail.css"/>\n';
|
||||
content += '<link rel="stylesheet" type="text/css" media="print" href="chrome://zotero/skin/report/detail_print.css"/>\n';
|
||||
content += '</head>\n<body>\n';
|
||||
|
||||
content += '<ul class="report">\n';
|
||||
for each(var arr in items) {
|
||||
//Zotero.debug(arr);
|
||||
|
||||
content += '<li id="i' + arr.itemID + '" class="' + arr.itemType + '"><span>\n';
|
||||
|
||||
// Title
|
||||
if (arr.title) {
|
||||
content += '<h2>' + escapeXML(arr.title) + '</h2>\n';
|
||||
}
|
||||
|
||||
// Metadata table
|
||||
var table = false;
|
||||
var tableContent = '<table>\n';
|
||||
|
||||
// Item type
|
||||
tableContent += '<tr>\n';
|
||||
tableContent += '<th>'
|
||||
+ escapeXML(Zotero.getString('itemFields.itemType'))
|
||||
+ '</th>\n';
|
||||
tableContent += '<td>' + escapeXML(Zotero.getString('itemTypes.' + arr['itemType'])) + '</td>\n';
|
||||
tableContent += '</tr>\n';
|
||||
|
||||
// Creators
|
||||
if (arr['creators']) {
|
||||
table = true;
|
||||
var displayText;
|
||||
|
||||
for each(var creator in arr['creators']) {
|
||||
// Two fields
|
||||
if (creator['fieldMode']==0) {
|
||||
displayText = creator['firstName'] + ' ' + creator['lastName'];
|
||||
}
|
||||
// Single field
|
||||
else if (creator['fieldMode']==1) {
|
||||
displayText = creator['lastName'];
|
||||
}
|
||||
else {
|
||||
// TODO
|
||||
}
|
||||
|
||||
tableContent += '<tr>\n';
|
||||
tableContent += '<th class="' + creator.creatorType + '">'
|
||||
+ escapeXML(Zotero.getString('creatorTypes.' + creator.creatorType))
|
||||
+ '</th>\n';
|
||||
tableContent += '<td>' + escapeXML(displayText) + '</td>\n';
|
||||
tableContent += '</tr>\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Move dateAdded and dateModified to the end of the array
|
||||
var da = arr['dateAdded'];
|
||||
var dm = arr['dateModified'];
|
||||
delete arr['dateAdded'];
|
||||
delete arr['dateModified'];
|
||||
arr['dateAdded'] = dm;
|
||||
arr['dateModified'] = dm;
|
||||
|
||||
for (var i in arr) {
|
||||
// Skip certain fields
|
||||
switch (i) {
|
||||
case 'itemType':
|
||||
case 'itemID':
|
||||
case 'sourceItemID':
|
||||
case 'title':
|
||||
case 'firstCreator':
|
||||
case 'creators':
|
||||
case 'tags':
|
||||
case 'seeAlso':
|
||||
case 'notes':
|
||||
case 'note':
|
||||
case 'attachments':
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var localizedFieldName = Zotero.getString('itemFields.' + i);
|
||||
}
|
||||
// Skip fields we don't have a localized string for
|
||||
catch (e) {
|
||||
Zotero.debug('Localized string not available for ' + 'itemFields.' + i, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
table = true;
|
||||
var fieldText;
|
||||
|
||||
// Shorten long URLs manually until Firefox wraps at ?
|
||||
// (like Safari) or supports the CSS3 word-wrap property
|
||||
var firstSpace = arr[i].indexOf(' ');
|
||||
if (arr[i].indexOf('http://') === 0 &&
|
||||
((firstSpace == -1 && arr[i].length > 29) || firstSpace > 29)) {
|
||||
|
||||
var stripped = false;
|
||||
|
||||
// Strip query string for sites we know don't need it
|
||||
for each(var re in _noQueryStringSites) {
|
||||
if (re.test(arr[i])){
|
||||
var pos = arr[i].indexOf('?');
|
||||
if (pos != -1) {
|
||||
fieldText = arr[i].substr(0, pos);
|
||||
stripped = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stripped) {
|
||||
// Add a line-break after the ? of long URLs,
|
||||
fieldText = arr[i].replace('?', "?<ZOTEROBREAK/>");
|
||||
|
||||
// Strip query string variables from the end while the
|
||||
// query string is longer than the main part
|
||||
var pos = fieldText.indexOf('?');
|
||||
if (pos != -1) {
|
||||
while (pos < (fieldText.length / 2)) {
|
||||
var lastAmp = fieldText.lastIndexOf('&');
|
||||
if (lastAmp == -1) {
|
||||
break;
|
||||
}
|
||||
fieldText = fieldText.substr(0, lastAmp);
|
||||
var shortened = true;
|
||||
}
|
||||
// Append '&...' to the end
|
||||
if (shortened) {
|
||||
fieldText += "&<ZOTEROHELLIP/>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i == 'url' && firstSpace == -1) {
|
||||
fieldText = '<a href="' + escapeXML(arr[i]) + '">'
|
||||
+ escapeXML(fieldText) + '</a>';
|
||||
}
|
||||
}
|
||||
// Remove SQL date from multipart dates
|
||||
// (e.g. '2006-00-00 Summer 2006' becomes 'Summer 2006')
|
||||
else if (i=='date') {
|
||||
fieldText = escapeXML(Zotero.Date.multipartToStr(arr[i]));
|
||||
}
|
||||
// Convert dates to local format
|
||||
else if (i=='accessDate' || i=='dateAdded' || i=='dateModified') {
|
||||
var date = Zotero.Date.sqlToDate(arr[i])
|
||||
fieldText = escapeXML(date.toLocaleString());
|
||||
}
|
||||
else {
|
||||
fieldText = escapeXML(arr[i]);
|
||||
}
|
||||
|
||||
tableContent += '<tr>\n<th>' + escapeXML(localizedFieldName)
|
||||
+ '</th>\n<td>' + fieldText + '</td>\n</tr>\n';
|
||||
}
|
||||
|
||||
if (table) {
|
||||
content += tableContent + '</table>\n';
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (arr['tags'] && arr['tags'].length) {
|
||||
// TODO: localize
|
||||
content += '<h3 class="tags">' + escapeXML('Tags') + '</h3>\n';
|
||||
content += '<ul class="tags">\n';
|
||||
for each(var tag in arr['tags']) {
|
||||
content += '<li>' + escapeXML(tag) + '</li>\n';
|
||||
}
|
||||
content += '</ul>\n';
|
||||
}
|
||||
|
||||
|
||||
// Child notes
|
||||
if (arr['notes'] && arr['notes'].length) {
|
||||
// TODO: localize
|
||||
content += '<h3 class="notes">' + escapeXML(Zotero.getString('itemFields.notes')) + '</h3>\n';
|
||||
content += '<ul class="notes">\n';
|
||||
for each(var note in arr['notes']) {
|
||||
content += '<li id="i' + note.itemID + '">\n';
|
||||
content += '<p>' + escapeXML(note.note) + '</p>\n';
|
||||
content += '</li>\n';
|
||||
}
|
||||
content += '</ul>\n';
|
||||
}
|
||||
|
||||
// Independent note
|
||||
if (arr['note']) {
|
||||
content += '<p>' + escapeXML(arr['note']) + '</p>\n';
|
||||
}
|
||||
|
||||
// Attachments
|
||||
if (arr['attachments'] && arr['attachments'].length) {
|
||||
content += '<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n';
|
||||
content += '<ul class="attachments">\n';
|
||||
for each(var attachment in arr['attachments']) {
|
||||
content += '<li id="i' + attachment.itemID + '">';
|
||||
content += escapeXML(attachment.title);
|
||||
content += '</li>\n';
|
||||
}
|
||||
content += '</ul>\n';
|
||||
}
|
||||
|
||||
// Attachments
|
||||
if (arr['seeAlso'] && arr['seeAlso'].length) {
|
||||
content += '<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n';
|
||||
content += '<ul class="related">\n';
|
||||
var relateds = Zotero.Items.get(arr['seeAlso']);
|
||||
for each(var related in relateds) {
|
||||
content += '<li id="i' + related.getID() + '">';
|
||||
content += escapeXML(related.getField('title'));
|
||||
content += '</li>\n';
|
||||
}
|
||||
content += '</ul>\n';
|
||||
}
|
||||
|
||||
|
||||
content += '</span></li>\n';
|
||||
}
|
||||
content += '</ul>\n';
|
||||
content += '</body>\n</html>';
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
function generateHTMLList(items) {
|
||||
|
||||
}
|
||||
}
|
|
@ -115,6 +115,47 @@ Zotero.Utilities.prototype.cleanTags = function(x) {
|
|||
return x.replace(/<[^>]+>/g, "");
|
||||
}
|
||||
|
||||
/*
|
||||
* Encode special XML/HTML characters
|
||||
*
|
||||
* Certain entities can be inserted manually:
|
||||
*
|
||||
* <ZOTEROBREAK/> => <br/>
|
||||
* <ZOTEROHELLIP/> => …
|
||||
*/
|
||||
Zotero.Utilities.prototype.htmlSpecialChars = function(str) {
|
||||
if (typeof str != 'string') {
|
||||
throw "htmlSpecialChars: argument must be a string";
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var chars = ['&', '"',"'",'<','>'];
|
||||
var entities = ['amp', 'quot', 'apos', 'lt', 'gt'];
|
||||
|
||||
var newString = str;
|
||||
for (var i = 0; i < chars.length; i++) {
|
||||
var re = new RegExp(chars[i], 'g');
|
||||
newString = newString.replace(re, '&' + entities[i] + ';');
|
||||
}
|
||||
|
||||
newString = newString.replace(/<ZOTERO([^\/]+)\/>/g, function (str, p1, offset, s) {
|
||||
switch (p1) {
|
||||
case 'BREAK':
|
||||
return '<br/>';
|
||||
case 'HELLIP':
|
||||
return '…';
|
||||
default:
|
||||
return p1;
|
||||
}
|
||||
});
|
||||
|
||||
return newString;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Test if a string is an integer
|
||||
*/
|
||||
|
|
|
@ -4,6 +4,8 @@ pane.collections.name = Collection name:
|
|||
pane.collections.rename = Rename collection:
|
||||
pane.collections.library = My Library
|
||||
pane.collections.untitled = Untitled
|
||||
pane.collections.menu.generateReport.collection = Generate Report from Collection...
|
||||
pane.collections.menu.generateReport.savedSearch = Generate Report from Saved Search...
|
||||
|
||||
pane.items.delete = Are you sure you want to delete the selected item?
|
||||
pane.items.delete.multiple = Are you sure you want to delete the selected items?
|
||||
|
@ -17,6 +19,8 @@ pane.items.menu.export = Export Selected Item...
|
|||
pane.items.menu.export.multiple = Export Selected Items...
|
||||
pane.items.menu.createBib = Create Bibliography from Selected Item...
|
||||
pane.items.menu.createBib.multiple = Create Bibliography from Selected Items...
|
||||
pane.items.menu.generateReport = Generate Report from Selected Item...
|
||||
pane.items.menu.generateReport.multiple = Generate Report from Selected Items...
|
||||
|
||||
pane.item.selected.zero = No items selected
|
||||
pane.item.selected.multiple = %S items selected
|
||||
|
@ -79,12 +83,16 @@ itemTypes.radioBroadcast = Radio Broadcast
|
|||
itemTypes.podcast = Podcast
|
||||
itemTypes.computerProgram = Computer Program
|
||||
|
||||
itemFields.itemType = Type
|
||||
itemFields.title = Title
|
||||
itemFields.dateAdded = Date Added
|
||||
itemFields.dateModified = Modified
|
||||
itemFields.source = Source
|
||||
itemFields.notes = Notes
|
||||
itemFields.url = URL
|
||||
itemFields.notes = Notes
|
||||
itemFields.tags = Tags
|
||||
itemFields.attachments = Attachments
|
||||
itemFields.related = Related
|
||||
itemFields.url = URL
|
||||
itemFields.rights = Rights
|
||||
itemFields.series = Series
|
||||
itemFields.volume = Volume
|
||||
|
@ -243,4 +251,6 @@ date.abbreviation.month = m
|
|||
date.abbreviation.day = d
|
||||
|
||||
citation.multipleSources = Multiple Sources...
|
||||
citation.singleSource = Single Source...
|
||||
citation.singleSource = Single Source...
|
||||
|
||||
report.title.default = Zotero Report
|
107
chrome/skin/default/zotero/report/detail.css
Normal file
107
chrome/skin/default/zotero/report/detail.css
Normal file
|
@ -0,0 +1,107 @@
|
|||
/* Fonts */
|
||||
li, li > p {
|
||||
font-family: Georgia, Times New Roman, Times, serif;
|
||||
font-size: .95em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
margin: .4em 0;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
th, td {
|
||||
font-size: .95em;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ul li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
|
||||
/* Line between items */
|
||||
ul.report > li {
|
||||
display: block;
|
||||
border-bottom: 1px #333 solid;
|
||||
padding: .95em 0;
|
||||
}
|
||||
ul.report > li:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
}
|
||||
ul.report > li:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
ul.report > li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
/* Metadata table */
|
||||
table {
|
||||
border: 1px #ccc solid;
|
||||
float: right;
|
||||
width: 35%;
|
||||
margin: .1em 0 .75em 1.5em;
|
||||
padding: .2em .3em;
|
||||
background: #fff;
|
||||
}
|
||||
th {
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
padding-right: .1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
th:after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
|
||||
/* Tags and child notes */
|
||||
ul ul {
|
||||
padding: 0 .5em;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: .4em;
|
||||
}
|
||||
h3:after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
|
||||
/* Display tags as comma-separated lists */
|
||||
ul.tags li {
|
||||
display: inline;
|
||||
}
|
||||
ul.tags li:not(:last-child):after {
|
||||
content: ', ';
|
||||
}
|
||||
|
||||
|
||||
/* Child notes */
|
||||
ul.notes {
|
||||
margin-left: 1em;
|
||||
}
|
||||
ul.notes li {
|
||||
display: inline;
|
||||
}
|
||||
ul.notes li:first-child p:first-child {
|
||||
margin-top: 1em;
|
||||
}
|
||||
ul.notes li p:first-child {
|
||||
margin-top: .7em;
|
||||
display: list-item;
|
||||
list-style: square outside;
|
||||
}
|
||||
|
||||
ul.notes li p, li.note p {
|
||||
white-space: -moz-pre-wrap;
|
||||
|
||||
}
|
27
chrome/skin/default/zotero/report/detail_print.css
Normal file
27
chrome/skin/default/zotero/report/detail_print.css
Normal file
|
@ -0,0 +1,27 @@
|
|||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Firefox gets funky with floated elements that span two pages, so we pretend we're a table
|
||||
*/
|
||||
ul.report {
|
||||
display: table;
|
||||
}
|
||||
|
||||
ul.report > li {
|
||||
display: table-row;
|
||||
page-break-inside: avoid; /* Waiting on https://bugzilla.mozilla.org/show_bug.cgi?id=132035 */
|
||||
}
|
||||
|
||||
ul.report > li > span {
|
||||
display: table-cell;
|
||||
border-bottom: 1px solid #000; /* Normal <li> border disappears in table mode */
|
||||
padding: .95em 0;
|
||||
}
|
||||
|
||||
ul.report > li:last-child > span {
|
||||
border-bottom: none;
|
||||
}
|
356
components/zotero-protocol-handler.js
Normal file
356
components/zotero-protocol-handler.js
Normal file
|
@ -0,0 +1,356 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (c) 2006 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://chnm.gmu.edu
|
||||
|
||||
Licensed under the Educational Community License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.opensource.org/licenses/ecl1.php
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
Based on nsChromeExtensionHandler example code by Ed Anuff at
|
||||
http://kb.mozillazine.org/Dev_:_Extending_the_Chrome_Protocol
|
||||
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
|
||||
const ZOTERO_SCHEME = "zotero";
|
||||
const ZOTERO_PROTOCOL_CID = Components.ID("{9BC3D762-9038-486A-9D70-C997AF848A7C}");
|
||||
const ZOTERO_PROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + ZOTERO_SCHEME;
|
||||
const ZOTERO_PROTOCOL_NAME = "Zotero Chrome Extension Protocol";
|
||||
|
||||
// Dummy chrome URL used to obtain a valid chrome channel
|
||||
// This one was chosen at random and should be able to be substituted
|
||||
// for any other well known chrome URL in the browser installation
|
||||
const DUMMY_CHROME_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
|
||||
|
||||
|
||||
function ChromeExtensionHandler() {
|
||||
this.wrappedJSObject = this;
|
||||
this._systemPrincipal = null;
|
||||
this._extensions = {};
|
||||
|
||||
|
||||
/*
|
||||
* Report generation extension for Zotero protocol
|
||||
*
|
||||
* Example URLs:
|
||||
*
|
||||
* zotero://report/ -- library
|
||||
* zotero://report/collection/12345
|
||||
* zotero://report/search/12345
|
||||
* zotero://report/items/12345-23456-34567
|
||||
* zotero://report/item/12345
|
||||
*
|
||||
* Optional format can be specified after ids
|
||||
*
|
||||
* - 'html', 'rtf', 'csv'
|
||||
* - defaults to 'html' if not specified
|
||||
*
|
||||
* e.g. zotero://report/collection/12345/rtf
|
||||
*
|
||||
*
|
||||
* Sorting:
|
||||
*
|
||||
* - 'sort' query string variable
|
||||
* - format is field[/order] [, field[/order], ...]
|
||||
* - order can be 'asc', 'a', 'desc' or 'd'; defaults to ascending order
|
||||
*
|
||||
* zotero://report/collection/13245?sort=itemType/d,title
|
||||
*/
|
||||
var ReportExtension = new function(){
|
||||
this.newChannel = newChannel;
|
||||
|
||||
function newChannel(uri){
|
||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService);
|
||||
|
||||
var Zotero = Components.classes["@zotero.org/Zotero;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
|
||||
generateContent:try {
|
||||
var mimeType, content = '';
|
||||
|
||||
var [path, queryString] = uri.path.substr(1).split('?');
|
||||
var [type, ids, format] = path.split('/');
|
||||
|
||||
// Get query string variables
|
||||
if (queryString) {
|
||||
var queryVars = queryString.split('&');
|
||||
for (var i in queryVars) {
|
||||
var [key, val] = queryVars[i].split('=');
|
||||
switch (key) {
|
||||
case 'sort':
|
||||
var sortBy = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (type){
|
||||
case 'collection':
|
||||
var items = Zotero.getItems(ids);
|
||||
break;
|
||||
|
||||
case 'search':
|
||||
var s = new Zotero.Search(ids);
|
||||
var items = Zotero.Items.get(s.search());
|
||||
break;
|
||||
|
||||
case 'items':
|
||||
case 'item':
|
||||
var items = Zotero.Items.get(ids.split('-'));
|
||||
break;
|
||||
|
||||
default:
|
||||
var type = 'library';
|
||||
var s = new Zotero.Search();
|
||||
s.addCondition('noChildren', 'true');
|
||||
var items = Zotero.Items.get(s.search());
|
||||
}
|
||||
|
||||
if (!items){
|
||||
mimeType = 'text/html';
|
||||
content = 'Invalid ID';
|
||||
break generateContent;
|
||||
}
|
||||
|
||||
// Convert item objects to export arrays
|
||||
for (var i in items) {
|
||||
items[i] = items[i].toArray();
|
||||
}
|
||||
|
||||
// Sort items
|
||||
if (!sortBy) {
|
||||
sortBy = 'title';
|
||||
}
|
||||
|
||||
var sorts = sortBy.split(',');
|
||||
for (var i in sorts) {
|
||||
var [field, order] = sorts[i].split('/');
|
||||
switch (order) {
|
||||
case 'd':
|
||||
case 'desc':
|
||||
order = -1;
|
||||
break;
|
||||
|
||||
default:
|
||||
order = 1;
|
||||
}
|
||||
|
||||
sorts[i] = {
|
||||
field: field,
|
||||
order: order
|
||||
};
|
||||
}
|
||||
|
||||
var compareFunction = function(a, b) {
|
||||
var index = 0;
|
||||
|
||||
// Multidimensional sort
|
||||
do {
|
||||
var result = a[sorts[index].field] > b[sorts[index].field] ?
|
||||
sorts[index].order
|
||||
: a[sorts[index].field] < b[sorts[index].field] ?
|
||||
(sorts[index].order * -1)
|
||||
: 0;
|
||||
|
||||
index++;
|
||||
}
|
||||
while (result == 0 && sorts[index]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
items.sort(compareFunction);
|
||||
|
||||
|
||||
// Pass off to the appropriate handler
|
||||
switch (format){
|
||||
case 'rtf':
|
||||
mimeType = 'text/rtf';
|
||||
break;
|
||||
|
||||
case 'csv':
|
||||
mimeType = 'text/plain';
|
||||
break;
|
||||
|
||||
default:
|
||||
format = 'html';
|
||||
mimeType = 'application/xhtml+xml';
|
||||
content = Zotero.Report.generateHTMLDetails(items);
|
||||
}
|
||||
}
|
||||
catch (e){
|
||||
Zotero.debug(e);
|
||||
throw (e);
|
||||
}
|
||||
|
||||
var uri_str = 'data:' + (mimeType ? mimeType + ',' : '') + encodeURIComponent(content);
|
||||
var ext_uri = ioService.newURI(uri_str, null, null);
|
||||
var extChannel = ioService.newChannelFromURI(ext_uri);
|
||||
|
||||
return extChannel;
|
||||
}
|
||||
};
|
||||
|
||||
var ReportExtensionSpec = ZOTERO_SCHEME + "://report"
|
||||
ReportExtensionSpec = ReportExtensionSpec.toLowerCase();
|
||||
|
||||
this._extensions[ReportExtensionSpec] = ReportExtension;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Implements nsIProtocolHandler
|
||||
*/
|
||||
ChromeExtensionHandler.prototype = {
|
||||
scheme: ZOTERO_SCHEME,
|
||||
|
||||
defaultPort : -1,
|
||||
|
||||
protocolFlags : Components.interfaces.nsIProtocolHandler.URI_STD,
|
||||
|
||||
allowPort : function(port, scheme) {
|
||||
return false;
|
||||
},
|
||||
|
||||
newURI : function(spec, charset, baseURI) {
|
||||
var newURL = Components.classes["@mozilla.org/network/standard-url;1"]
|
||||
.createInstance(Components.interfaces.nsIStandardURL);
|
||||
newURL.init(1, -1, spec, charset, baseURI);
|
||||
|
||||
return newURL.QueryInterface(Components.interfaces.nsIURI);
|
||||
},
|
||||
|
||||
newChannel : function(uri) {
|
||||
var chromeService = Components.classes["@mozilla.org/network/protocol;1?name=chrome"]
|
||||
.getService(Components.interfaces.nsIProtocolHandler);
|
||||
|
||||
var newChannel = null;
|
||||
|
||||
try {
|
||||
var uriString = uri.spec.toLowerCase();
|
||||
|
||||
for (extSpec in this._extensions) {
|
||||
var ext = this._extensions[extSpec];
|
||||
|
||||
if (uriString.indexOf(extSpec) == 0) {
|
||||
|
||||
if (this._systemPrincipal == null) {
|
||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService);
|
||||
|
||||
var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null);
|
||||
var chromeChannel = chromeService.newChannel(chromeURI);
|
||||
|
||||
this._systemPrincipal = chromeChannel.owner;
|
||||
|
||||
var chromeRequest = chromeChannel.QueryInterface(Components.interfaces.nsIRequest);
|
||||
chromeRequest.cancel(0x804b0002); // BINDING_ABORTED
|
||||
}
|
||||
|
||||
var extChannel = ext.newChannel(uri);
|
||||
|
||||
if (this._systemPrincipal != null) {
|
||||
// applying cached system principal to extension channel
|
||||
extChannel.owner = this._systemPrincipal;
|
||||
}
|
||||
else {
|
||||
// no cached system principal to apply to extension channel
|
||||
}
|
||||
|
||||
extChannel.originalURI = uri;
|
||||
|
||||
return extChannel;
|
||||
}
|
||||
}
|
||||
|
||||
// pass request through to ChromeProtocolHandler::newChannel
|
||||
if (uriString.indexOf("chrome") != 0) {
|
||||
uriString = uri.spec;
|
||||
uriString = "chrome" + uriString.substring(uriString.indexOf(":"));
|
||||
uri = chromeService.newURI(uriString, null, null);
|
||||
}
|
||||
|
||||
newChannel = chromeService.newChannel(uri);
|
||||
}
|
||||
catch (e) {
|
||||
throw Components.results.NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
return newChannel;
|
||||
},
|
||||
|
||||
QueryInterface : function(iid) {
|
||||
if (!iid.equals(Components.interfaces.nsIProtocolHandler) &&
|
||||
!iid.equals(Components.interfaces.nsISupports)) {
|
||||
throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// XPCOM goop
|
||||
//
|
||||
|
||||
var ChromeExtensionModule = {
|
||||
cid: ZOTERO_PROTOCOL_CID,
|
||||
|
||||
contractId: ZOTERO_PROTOCOL_CONTRACTID,
|
||||
|
||||
registerSelf : function(compMgr, fileSpec, location, type) {
|
||||
compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
|
||||
compMgr.registerFactoryLocation(
|
||||
ZOTERO_PROTOCOL_CID,
|
||||
ZOTERO_PROTOCOL_NAME,
|
||||
ZOTERO_PROTOCOL_CONTRACTID,
|
||||
fileSpec,
|
||||
location,
|
||||
type
|
||||
);
|
||||
},
|
||||
|
||||
getClassObject : function(compMgr, cid, iid) {
|
||||
if (!cid.equals(ZOTERO_PROTOCOL_CID)) {
|
||||
throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
}
|
||||
if (!iid.equals(Components.interfaces.nsIFactory)) {
|
||||
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
return this.myFactory;
|
||||
},
|
||||
|
||||
canUnload : function(compMgr) {
|
||||
return true;
|
||||
},
|
||||
|
||||
myFactory : {
|
||||
createInstance : function(outer, iid) {
|
||||
if (outer != null) {
|
||||
throw Components.results.NS_ERROR_NO_AGGREGATION;
|
||||
}
|
||||
|
||||
return new ChromeExtensionHandler().QueryInterface(iid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function NSGetModule(compMgr, fileSpec) {
|
||||
return ChromeExtensionModule;
|
||||
}
|
|
@ -58,6 +58,10 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
|||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://zotero/content/xpcom/cite.js");
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://zotero/content/xpcom/report.js");
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://zotero/content/xpcom/utilities.js");
|
||||
|
|
Loading…
Reference in New Issue
Block a user