Document export-import UI and integration code (#1501)

This commit is contained in:
Adomas Ven 2019-05-15 04:06:18 +03:00 committed by Dan Stillman
parent dd8ceb93aa
commit 48778f2847
10 changed files with 259 additions and 68 deletions

View file

@ -0,0 +1,3 @@
.chevron {
font-size: 1.5em;
}

View file

@ -32,6 +32,7 @@
// Class to provide options for bibliography
// Used by rtfScan.xul, integrationDocPrefs.xul, and bibliography.xul
Components.utils.import("resource://gre/modules/Services.jsm");
var Zotero_File_Interface_Bibliography = new function() {
var _io;
@ -174,6 +175,10 @@ var Zotero_File_Interface_Bibliography = new function() {
document.getElementById("automaticCitationUpdates-checkbox").checked = !_io.delayCitationUpdates;
}
if (_io.showImportExport) {
document.querySelector('#exportImport').hidden = false;
}
}
// set style to false, in case this is cancelled
@ -237,7 +242,38 @@ var Zotero_File_Interface_Bibliography = new function() {
window.sizeToContent();
};
this.toggleAdvanced = function() {
var advancedSettings = document.querySelector("#advanced-settings");
advancedSettings.hidden = !advancedSettings.hidden;
var chevron = document.querySelector('.chevron');
chevron.classList.toggle('chevron-down');
chevron.classList.toggle('chevron-up');
window.sizeToContent();
};
this.exportDocument = function() {
const importExportWikiURL = "https://www.zotero.org/support/kb/export_import_document";
var ps = Services.prompt;
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK)
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
var result = ps.confirmEx(null,
Zotero.getString('integration.exportDocument'),
Zotero.getString('integration.exportDocument.description'),
buttonFlags,
null,
null,
Zotero.getString('general.moreInformation'), null, {});
if (result == 0) {
_io.exportDocument = true;
document.documentElement.acceptDialog();
} else if (result == 2) {
Zotero.launchURL(importExportWikiURL);
}
}
/*
* Update locale menulist when style is changed
*/

View file

@ -26,6 +26,7 @@
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
<?xml-stylesheet href="chrome://zotero/skin/bibliography.css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/bibliography.css"?>
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
<dialog
@ -81,14 +82,27 @@
</radiogroup>
</groupbox>
<vbox id="automaticJournalAbbreviations-vbox">
<vbox class="pref-vbox" id="automaticJournalAbbreviations-vbox">
<checkbox id="automaticJournalAbbreviations-checkbox" label="&zotero.integration.prefs.automaticJournalAbbeviations.label;"/>
<description class="radioDescription">&zotero.integration.prefs.automaticJournalAbbeviations.caption;</description>
</vbox>
<vbox id="automaticCitationUpdates-vbox">
<checkbox id="automaticCitationUpdates-checkbox" label="&zotero.integration.prefs.automaticCitationUpdates.label;" tooltiptext="&zotero.integration.prefs.automaticCitationUpdates.tooltip;"/>
<description class="radioDescription">&zotero.integration.prefs.automaticCitationUpdates.description;</description>
<!--<vbox id="advanced-separator" align="center">-->
<!--<hbox align="center" onclick="Zotero_File_Interface_Bibliography.toggleAdvanced()">-->
<!--<label>&zotero.general.advancedOptions.label;</label>-->
<!--<label class="chevron chevron-down">➤</label>-->
<!--</hbox>-->
<!--</vbox> -->
<vbox id="advanced-settings" hidden="false">
<vbox id="automaticCitationUpdates-vbox">
<checkbox id="automaticCitationUpdates-checkbox" label="&zotero.integration.prefs.automaticCitationUpdates.label;" tooltiptext="&zotero.integration.prefs.automaticCitationUpdates.tooltip;"/>
<description class="radioDescription">&zotero.integration.prefs.automaticCitationUpdates.description;</description>
</vbox>
<hbox id="exportImport" hidden="true">
<button label="&zotero.integration.prefs.exportDocument;" oncommand="Zotero_File_Interface_Bibliography.exportDocument()"/>
</hbox>
</vbox>
</vbox>
</dialog>

View file

@ -59,12 +59,16 @@ Zotero.HTTPIntegrationClient.Application = function() {
this.secondaryFieldType = "Http";
this.outputFormat = 'html';
this.supportedNotes = ['footnotes'];
this.supportsImportExport = false;
this.processorName = "HTTP Integration";
};
Zotero.HTTPIntegrationClient.Application.prototype = {
getActiveDocument: async function() {
let result = await Zotero.HTTPIntegrationClient.sendCommand('Application.getActiveDocument');
this.outputFormat = result.outputFormat || this.outputFormat;
this.supportedNotes = result.supportedNotes || this.supportedNotes;
this.supportsImportExport = result.supportsImportExport || this.supportsImportExport;
this.processorName = result.processorName || this.processorName;
return new Zotero.HTTPIntegrationClient.Document(result.documentID);
}
};
@ -76,7 +80,7 @@ Zotero.HTTPIntegrationClient.Document = function(documentID) {
this._documentID = documentID;
};
for (let method of ["activate", "canInsertField", "displayAlert", "getDocumentData",
"setDocumentData", "setBibliographyStyle"]) {
"setDocumentData", "setBibliographyStyle", "importDocument", "exportDocument"]) {
Zotero.HTTPIntegrationClient.Document.prototype[method] = async function() {
return Zotero.HTTPIntegrationClient.sendCommand("Document."+method,
[this._documentID].concat(Array.prototype.slice.call(arguments)));

View file

@ -74,7 +74,7 @@ Zotero.Server.Endpoints['/connector/document/respond'].prototype = {
// For managing macOS integration and progress window focus
Zotero.Server.Endpoints['/connector/sendToBack'] = function() {};
Zotero.Server.Endpoints['/connector/sendToBack'].prototype = {
supportedMethods: ["POST"],
supportedMethods: ["POST", "GET"],
supportedDataTypes: ["application/json"],
permitBookmarklet: true,
init: function() {

View file

@ -59,6 +59,8 @@ const DELAYED_CITATION_RTF_STYLING_CLEAR = "\\ulclear";
const DELAYED_CITATION_HTML_STYLING = "<div class='delayed-zotero-citation-updates'>"
const DELAYED_CITATION_HTML_STYLING_END = "</div>"
const EXPORTED_DOCUMENT_MARKER = "ZOTERO_EXPORTED_DOCUMENT";
Zotero.Integration = new function() {
Components.utils.import("resource://gre/modules/Services.jsm");
@ -210,7 +212,7 @@ Zotero.Integration = new function() {
* Executes an integration command, first checking to make sure that versions are compatible
*/
this.execCommand = async function(agent, command, docId) {
var document, session;
var document, session, documentImported;
if (Zotero.Integration.currentDoc) {
Zotero.Utilities.Internal.activate();
@ -243,12 +245,13 @@ Zotero.Integration = new function() {
}
Zotero.Integration.currentDoc = document = await documentPromise;
Zotero.Integration.currentSession = session = await Zotero.Integration.getSession(application, document, agent);
// TODO: this is a pretty awful circular dependence
session.fields = new Zotero.Integration.Fields(session, document);
[session, documentImported] = await Zotero.Integration.getSession(application, document, agent);
Zotero.Integration.currentSession = session;
// TODO: figure this out
// Zotero.Notifier.trigger('delete', 'collection', 'document');
await (new Zotero.Integration.Interface(application, document, session))[command]();
if (!documentImported) {
await (new Zotero.Integration.Interface(application, document, session))[command]();
}
await document.setDocumentData(session.data.serialize());
}
catch (e) {
@ -257,7 +260,9 @@ Zotero.Integration = new function() {
}
else {
// If user cancels we should still write the currently assigned session ID
await document.setDocumentData(session.data.serialize());
if (session) {
await document.setDocumentData(session.data.serialize());
}
}
}
finally {
@ -415,6 +420,7 @@ Zotero.Integration = new function() {
* @return {Zotero.Integration.Session} Promise
*/
this.getSession = async function (app, doc, agent) {
let documentImported = false;
try {
var progressBar = new Zotero.Integration.Progress(4, Zotero.isMac && agent != 'http');
progressBar.show();
@ -428,7 +434,7 @@ Zotero.Integration = new function() {
data = new Zotero.Integration.DocumentData();
}
if (data.prefs.fieldType) {
if (dataString != EXPORTED_DOCUMENT_MARKER && data.prefs.fieldType) {
if (data.dataVersion < DATA_VERSION) {
if (data.dataVersion == 1
&& data.prefs.fieldType == "Field"
@ -462,36 +468,54 @@ Zotero.Integration = new function() {
session = new Zotero.Integration.Session(doc, app);
session.reload = true;
}
try {
await session.setData(data);
} catch(e) {
// make sure style is defined
if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") {
if (data.style.styleID) {
let trustedSource = /^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID);
let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID);
if (trustedSource ||
await doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) {
session.agent = agent;
session._doc = doc;
session.progressBar = progressBar;
// TODO: this is a pretty awful circular dependence
session.fields = new Zotero.Integration.Fields(session, doc);
if (dataString == EXPORTED_DOCUMENT_MARKER) {
Zotero.Integration.currentSession = session;
data = await session.importDocument();
documentImported = true;
// We're slightly abusing the system here, but importing a document should cancel
// any other operation the user was trying to perform since the document will change
// significantly
} else {
try {
await session.setData(data);
} catch(e) {
// make sure style is defined
if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") {
if (data.style.styleID) {
let trustedSource =
/^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID);
let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID);
if (trustedSource ||
await doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) {
let installed = false;
try {
let { styleTitle, styleID } = await Zotero.Styles.install(
{url: data.style.styleID}, data.style.styleID, true
);
data.style.styleID = styleID;
installed = true;
}
catch (e) {
await doc.displayAlert(
Zotero.getString(
'integration.error.styleNotFound', data.style.styleID
),
DIALOG_ICON_WARNING,
DIALOG_BUTTONS_OK
);
}
if (installed) {
await session.setData(data, true);
let installed = false;
try {
let { styleTitle, styleID } = await Zotero.Styles.install(
{url: data.style.styleID}, data.style.styleID, true
);
data.style.styleID = styleID;
installed = true;
}
catch (e) {
await doc.displayAlert(
Zotero.getString(
'integration.error.styleNotFound', data.style.styleID
),
DIALOG_ICON_WARNING,
DIALOG_BUTTONS_OK
);
}
if (installed) {
await session.setData(data, true);
} else {
await session.setDocPrefs();
}
} else {
await session.setDocPrefs();
}
@ -499,24 +523,19 @@ Zotero.Integration = new function() {
await session.setDocPrefs();
}
} else {
await session.setDocPrefs();
throw e;
}
} else {
throw e;
}
}
} catch (e) {
progressBar.hide(true);
throw e;
}
session.agent = agent;
session._doc = doc;
if (session.progressBar) {
progressBar.reset();
progressBar.segments = session.progressBar.segments;
}
session.progressBar = progressBar;
return session;
return [session, documentImported];
};
}
@ -731,12 +750,12 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
if(!haveSession) {
// This is a brand new document; don't try to get fields
oldData = yield this._session.setDocPrefs();
oldData = yield this._session.setDocPrefs(true);
} else {
// Can get fields while dialog is open
oldData = yield Zotero.Promise.all([
this._session.fields.get(),
this._session.setDocPrefs()
this._session.setDocPrefs(true)
]).spread(function (fields, setDocPrefs) {
// Only return value from setDocPrefs
return setDocPrefs;
@ -861,9 +880,9 @@ Zotero.Integration.Fields.prototype.addField = async function(note) {
*/
Zotero.Integration.Fields.prototype.get = new function() {
var deferred;
return async function() {
return async function(force=false) {
// If we already have fields, just return them
if(this._fields != undefined) {
if(!force && this._fields != undefined) {
return this._fields;
}
@ -1519,10 +1538,11 @@ Zotero.Integration.Session.prototype.setData = async function (data, resetStyle)
* if there wasn't, or rejected with Zotero.Exception.UserCancelled if the dialog was
* cancelled.
*/
Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (highlightDelayCitations=false) {
Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (showImportExport=false) {
var io = new function() { this.wrappedJSObject = this; };
io.primaryFieldType = this.primaryFieldType;
io.secondaryFieldType = this.secondaryFieldType;
io.showImportExport = false;
if (this.data) {
io.style = this.data.style.styleID;
@ -1532,14 +1552,18 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
io.fieldType = this.data.prefs.fieldType;
io.delayCitationUpdates = this.data.prefs.delayCitationUpdates;
io.dontAskDelayCitationUpdates = this.data.prefs.dontAskDelayCitationUpdates;
io.highlightDelayCitations = highlightDelayCitations;
io.automaticJournalAbbreviations = this.data.prefs.automaticJournalAbbreviations;
io.requireStoreReferences = !Zotero.Utilities.isEmpty(this.embeddedItems);
io.showImportExport = showImportExport && this.data.prefs.fieldType && this._app.supportsImportExport;
}
// Make sure styles are initialized for new docs
yield Zotero.Styles.init();
yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io);
if (io.exportDocument) {
return this.exportDocument();
}
if (!io.style || !io.fieldType) {
throw new Zotero.Exception.UserCancelled("document preferences window");
@ -1581,14 +1605,61 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
return oldData || null;
})
/**
* Adds a citation based on a serialized Word field
*/
Zotero.Integration._oldCitationLocatorMap = {
p:"page",
g:"paragraph",
l:"line"
};
Zotero.Integration.Session.prototype.exportDocument = async function() {
Zotero.debug("Integration: Exporting the document");
var timer = new Zotero.Integration.Timer();
timer.start();
try {
this.data.style.bibliographyStyleHasBeenSet = false;
await this._doc.setDocumentData(this.data.serialize());
await this._doc.exportDocument(this.data.prefs.fieldType,
Zotero.getString('integration.importInstructions'));
} finally {
Zotero.debug(`Integration: Export finished in ${timer.stop()}`);
}
}
Zotero.Integration.Session.prototype.importDocument = async function() {
const importExportWikiURL = "https://www.zotero.org/support/kb/export_import_document";
var ps = Services.prompt;
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK)
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
var result = ps.confirmEx(null,
Zotero.getString('integration.importDocument'),
Zotero.getString('integration.importDocument.description'),
buttonFlags,
null,
null,
Zotero.getString('general.moreInformation'), null, {});
if (result == 1) {
throw new Zotero.Exception.UserCancelled("the document import");
} else if (result == 2) {
Zotero.launchURL(importExportWikiURL);
return;
}
Zotero.debug("Integration: Importing the document");
var timer = new Zotero.Integration.Timer();
timer.start();
try {
var importSuccessful = await this._doc.importDocument(this._app.primaryFieldType);
if (!importSuccessful) {
Zotero.debug("Integration: No importable data found in the document");
return this.displayAlert("No importable data found", DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK);
}
var data = new Zotero.Integration.DocumentData(await this._doc.getDocumentData());
data.prefs.fieldType = this._app.primaryFieldType;
await this.setData(data, true);
await this.fields.get(true);
await this.fields.updateSession(FORCE_CITATIONS_RESET_TEXT);
await this.fields.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true);
} finally {
Zotero.debug(`Integration: Import finished in ${timer.stop()}`);
}
return data;
}
/**
* Adds a citation to the arrays representing the document
@ -2228,6 +2299,16 @@ Zotero.Integration.Field.loadExisting = async function(docField) {
return field;
};
/**
* Adds a citation based on a serialized Word field
*/
Zotero.Integration._oldCitationLocatorMap = {
p:"page",
g:"paragraph",
l:"line"
};
Zotero.Integration.CitationField = class extends Zotero.Integration.Field {
constructor(field, rawCode) {
super(field, rawCode);
@ -2908,7 +2989,8 @@ Zotero.Integration.LegacyPluginWrapper = function(application) {
primaryFieldType: application.primaryFieldType,
secondaryFieldType: application.secondaryFieldType,
outputFormat: 'rtf',
supportedNotes: ['footnotes', 'endnotes']
supportedNotes: ['footnotes', 'endnotes'],
processorName: ''
}
}
Zotero.Integration.LegacyPluginWrapper.wrapField = function (field) {

View file

@ -243,10 +243,12 @@
<!ENTITY zotero.integration.prefs.automaticCitationUpdates.tooltip "Citations with pending updates will be highlighted in the document">
<!ENTITY zotero.integration.prefs.automaticCitationUpdates.description "Disabling updates can speed up citation insertion in large documents. Click Refresh to update citations manually.">
<!ENTITY zotero.integration.prefs.automaticJournalAbbeviations.label "Use MEDLINE journal abbreviations">
<!ENTITY zotero.integration.prefs.automaticJournalAbbeviations.caption "The “Journal Abbr” field will be ignored.">
<!ENTITY zotero.integration.prefs.exportDocument "Export document…">
<!ENTITY zotero.integration.prefs.importDocument "Import document…">
<!ENTITY zotero.integration.showEditor.label "Show Editor">
<!ENTITY zotero.integration.classicView.label "Classic View">

View file

@ -897,6 +897,11 @@ integration.delayCitationUpdates.alert.text2.tab = You will need to click Refres
integration.delayCitationUpdates.alert.text3 = You can change this setting later in the document preferences.
integration.delayCitationUpdates.bibliography.toolbar = Automatic citation updates are disabled. To see the bibliography, click Refresh in the Zotero toolbar.
integration.delayCitationUpdates.bibliography.tab = Automatic citation updates are disabled. To see the bibliography, click Refresh in the Zotero tab.
integration.importDocument = Import Document?
integration.importDocument.description = Would you like to import this document for use with Zotero?
integration.exportDocument = Export Document?
integration.exportDocument.description = Exporting the document will allow you to import it in a different Zotero supported word processor and retain citation links. You should make a backup of your document before exporting. Do you want to proceed?
integration.importInstructions = This document contains exported Zotero citations. Open it with a Zotero supported document editor and press "Refresh" in the Zotero plugin to import it. NOTE: Do not copy and paste the contents of the document from one processor to the other.
styles.install.title = Install Style
styles.install.unexpectedError = An unexpected error occurred while installing "%1$S"

View file

@ -25,6 +25,25 @@ radio:not(:first-child)
}
#automaticJournalAbbreviations-vbox, #automaticCitationUpdates-vbox {
#automaticJournalAbbreviations-vbox, #advanced-settings {
padding: 0 14px;
}
}
#advanced-separator * {
cursor: pointer;
}
.chevron {
color: #444;
}
.chevron.chevron-down {
transform: rotate(90deg);
}
.chevron.chevron-up {
transform: rotate(-90deg);
}
#advanced-settings > * {
margin-bottom: 10px;
}

View file

@ -21,6 +21,7 @@ describe("Zotero.Integration", function () {
this.primaryFieldType = "Field";
this.secondaryFieldType = "Bookmark";
this.supportedNotes = ['footnotes', 'endnotes'];
this.supportsImportExport = true;
this.fields = [];
};
DocumentPluginDummy.Application.prototype = {
@ -119,6 +120,31 @@ describe("Zotero.Integration", function () {
* Informs the document processor that the operation is complete
*/
complete: () => 0,
/**
* Converts field text in document to their underlying codes and appends
* document preferences and bibliography style as paragraphs at the end
* of the document. Prefixes:
* - Bibliography style: "BIBLIOGRAPHY_STYLE "
* - Document preferences: "DOCUMENT_PREFERENCES "
*
* All Zotero exported text must be converted to a hyperlink
* (with any url, e.g. http://www.zotero.org)
*/
exportDocument: (fieldType) => 0,
/**
* Converts a document from an exported form described in #exportDocument()
* to a Zotero editable form. Bibliography Style and Document Preferences
* text is removed and stored internally within the doc. The citation codes are
* also stored within the doc in appropriate representation.
*
* Note that no citation text updates are needed. Zotero will issue field updates
* manually.
*
* @returns {Boolean} whether the document contained importable data
*/
importDocument: (fieldType) => 0,
};
/**