Initial annotation support
This commit is contained in:
parent
92ba393488
commit
1c366de546
15 changed files with 1109 additions and 28 deletions
|
@ -652,7 +652,7 @@
|
|||
|
||||
for (var i=0; i<itemTypes.length; i++) {
|
||||
var name = itemTypes[i].name;
|
||||
if (name != 'attachment' && name != 'note') {
|
||||
if (name != 'attachment' && name != 'note' && name != 'annotation') {
|
||||
this.itemTypeMenu.appendItem(itemTypes[i].localized, itemTypes[i].id);
|
||||
}
|
||||
}
|
||||
|
|
129
chrome/content/zotero/xpcom/annotations.js
Normal file
129
chrome/content/zotero/xpcom/annotations.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2020 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
https://www.zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Zotero.Annotations = new function () {
|
||||
// Keep in sync with items.js::loadAnnotations()
|
||||
Zotero.defineProperty(this, 'ANNOTATION_TYPE_HIGHLIGHT', { value: 1 });
|
||||
Zotero.defineProperty(this, 'ANNOTATION_TYPE_NOTE', { value: 2 });
|
||||
Zotero.defineProperty(this, 'ANNOTATION_TYPE_AREA', { value: 3 });
|
||||
|
||||
|
||||
this.toJSON = function (item) {
|
||||
var o = {};
|
||||
o.key = item.key;
|
||||
o.type = item.annotationType;
|
||||
o.isAuthor = !item.createdByUserID || item.createdByUserID == Zotero.Users.getCurrentUserID();
|
||||
if (!o.isAuthor) {
|
||||
o.authorName = Zotero.Users.getName(item.createdByUserID);
|
||||
}
|
||||
if (o.type == 'highlight') {
|
||||
o.text = item.annotationText;
|
||||
}
|
||||
else if (o.type == 'area') {
|
||||
o.imageURL = item.annotationImageURL;
|
||||
}
|
||||
o.comment = item.annotationComment;
|
||||
o.pageLabel = item.annotationPageLabel;
|
||||
o.color = item.annotationColor;
|
||||
o.sortIndex = item.annotationSortIndex;
|
||||
o.position = item.annotationPosition;
|
||||
|
||||
// Add tags and tag colors
|
||||
var tagColors = Zotero.Tags.getColors(item.libraryID);
|
||||
var tags = item.getTags().map((t) => {
|
||||
let obj = {
|
||||
name: t.tag
|
||||
};
|
||||
if (tagColors.has(t.tag)) {
|
||||
obj.color = tagColors.get(t.tag).color;
|
||||
// Add 'position' for sorting
|
||||
obj.position = tagColors.get(t.tag).position;
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
// Sort colored tags by position and other tags by name
|
||||
tags.sort((a, b) => {
|
||||
if (!a.color && !b.color) return Zotero.localeCompare(a.name, b.name);
|
||||
if (!a.color && !b.color) return -1;
|
||||
if (!a.color && b.color) return 1;
|
||||
return a.position - b.position;
|
||||
});
|
||||
// Remove temporary 'position' value
|
||||
tags.forEach(t => delete t.position);
|
||||
if (tags.length) {
|
||||
o.tags = tags;
|
||||
}
|
||||
|
||||
o.dateModified = item.dateModified;
|
||||
return o;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Zotero.Item} attachment - Saved parent attachment item
|
||||
* @param {Object} json
|
||||
* @return {Promise<Zotero.Item>} - Promise for an annotation item
|
||||
*/
|
||||
this.saveFromJSON = async function (attachment, json, saveOptions = {}) {
|
||||
if (!attachment) {
|
||||
throw new Error("'attachment' not provided");
|
||||
}
|
||||
if (!attachment.libraryID) {
|
||||
throw new Error("'attachment' is not saved");
|
||||
}
|
||||
if (!json.key) {
|
||||
throw new Error("'key' not provided in JSON");
|
||||
}
|
||||
|
||||
var item = Zotero.Items.getByLibraryAndKey(attachment.libraryID, json.key);
|
||||
if (!item) {
|
||||
item = new Zotero.Item('annotation');
|
||||
item.libraryID = attachment.libraryID;
|
||||
item.key = json.key;
|
||||
await item.loadPrimaryData();
|
||||
}
|
||||
item.parentID = attachment.id;
|
||||
|
||||
item._requireData('annotation');
|
||||
item._requireData('annotationDeferred');
|
||||
item.annotationType = json.type;
|
||||
if (json.type == 'highlight') {
|
||||
item.annotationText = json.text;
|
||||
}
|
||||
item.annotationComment = json.comment;
|
||||
item.annotationColor = json.color;
|
||||
item.annotationPageLabel = json.pageLabel;
|
||||
item.annotationSortIndex = json.sortIndex;
|
||||
item.annotationPosition = Object.assign({}, json.position);
|
||||
// TODO: Can colors be set?
|
||||
item.setTags((json.tags || []).map(t => ({ tag: t.name })));
|
||||
|
||||
await item.saveTx(saveOptions);
|
||||
|
||||
return item;
|
||||
};
|
||||
};
|
|
@ -24,11 +24,13 @@
|
|||
*/
|
||||
|
||||
Zotero.Attachments = new function(){
|
||||
// Keep in sync with Zotero.Schema.integrityCheck()
|
||||
// Keep in sync with Zotero.Schema.integrityCheck() and this.linkModeToName()
|
||||
this.LINK_MODE_IMPORTED_FILE = 0;
|
||||
this.LINK_MODE_IMPORTED_URL = 1;
|
||||
this.LINK_MODE_LINKED_FILE = 2;
|
||||
this.LINK_MODE_LINKED_URL = 3;
|
||||
this.LINK_MODE_EMBEDDED_IMAGE = 4;
|
||||
|
||||
this.BASE_PATH_PLACEHOLDER = 'attachments:';
|
||||
|
||||
var _findPDFQueue = [];
|
||||
|
@ -351,6 +353,71 @@ Zotero.Attachments = new function(){
|
|||
});
|
||||
|
||||
|
||||
/**
|
||||
* Saves an image for a parent note or area annotation
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Blob} params.blob - Image to save
|
||||
* @param {Integer} params.parentItemID - Annotation item to add item to
|
||||
* @param {Object} [params.saveOptions] - Options to pass to Zotero.Item::save()
|
||||
* @return {Promise<Zotero.Item>}
|
||||
*/
|
||||
this.importEmbeddedImage = async function ({ blob, parentItemID, saveOptions }) {
|
||||
Zotero.debug('Importing annotation image');
|
||||
|
||||
var contentType = blob.type;
|
||||
var fileExt;
|
||||
switch (contentType) {
|
||||
case 'image/png':
|
||||
fileExt = 'png';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported embedded image content type '${contentType}`);
|
||||
}
|
||||
var filename = 'image.' + fileExt;
|
||||
|
||||
var attachmentItem;
|
||||
var destDir;
|
||||
try {
|
||||
await Zotero.DB.executeTransaction(async function () {
|
||||
// Create a new attachment
|
||||
attachmentItem = new Zotero.Item('attachment');
|
||||
let { libraryID: parentLibraryID } = Zotero.Items.getLibraryAndKeyFromID(parentItemID);
|
||||
attachmentItem.libraryID = parentLibraryID;
|
||||
attachmentItem.parentID = parentItemID;
|
||||
attachmentItem.attachmentLinkMode = this.LINK_MODE_EMBEDDED_IMAGE;
|
||||
attachmentItem.attachmentPath = 'storage:' + filename;
|
||||
attachmentItem.attachmentContentType = contentType;
|
||||
await attachmentItem.save(saveOptions);
|
||||
|
||||
// Write blob to file in attachment directory
|
||||
destDir = await this.createDirectoryForItem(attachmentItem);
|
||||
let file = OS.Path.join(destDir, filename);
|
||||
await Zotero.File.putContentsAsync(file, blob);
|
||||
await Zotero.File.setNormalFilePermissions(file);
|
||||
}.bind(this));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError("Failed importing image:\n\n" + e);
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
if (destDir) {
|
||||
await OS.File.removeDir(destDir, { ignoreAbsent: true });
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return attachmentItem;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Integer} options.libraryID
|
||||
|
@ -2102,6 +2169,9 @@ Zotero.Attachments = new function(){
|
|||
if (!(item instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
if (!item.key) {
|
||||
throw new Error("Item key must be set");
|
||||
}
|
||||
return this.getStorageDirectoryByLibraryAndKey(item.libraryID, item.key);
|
||||
}
|
||||
|
||||
|
@ -2787,6 +2857,8 @@ Zotero.Attachments = new function(){
|
|||
return 'linked_file';
|
||||
case this.LINK_MODE_LINKED_URL:
|
||||
return 'linked_url';
|
||||
case this.LINK_MODE_EMBEDDED_IMAGE:
|
||||
return 'embedded_image';
|
||||
default:
|
||||
throw new Error(`Invalid link mode ${linkMode}`);
|
||||
}
|
||||
|
|
|
@ -354,6 +354,8 @@ Zotero.ItemTypes = new function() {
|
|||
var _primaryTypeNames = ['book', 'bookSection', 'journalArticle', 'newspaperArticle', 'document'];
|
||||
var _primaryTypes;
|
||||
var _secondaryTypes;
|
||||
// Item types hidden from New Item menu
|
||||
var _hiddenTypeNames = ['webpage', 'attachment', 'note', 'annotation'];
|
||||
var _hiddenTypes;
|
||||
|
||||
var _numPrimary = 5;
|
||||
|
@ -371,12 +373,13 @@ Zotero.ItemTypes = new function() {
|
|||
|
||||
// Secondary types
|
||||
_secondaryTypes = yield this._getTypesFromDB(
|
||||
`WHERE display != 0 AND display NOT IN ('${_primaryTypeNames.join("', '")}')`
|
||||
+ " AND name != 'webpage'"
|
||||
`WHERE typeName NOT IN ('${_primaryTypeNames.concat(_hiddenTypeNames).join("', '")}')`
|
||||
);
|
||||
|
||||
// Hidden types
|
||||
_hiddenTypes = yield this._getTypesFromDB('WHERE display=0')
|
||||
_hiddenTypes = yield this._getTypesFromDB(
|
||||
`WHERE typeName IN ('${_hiddenTypeNames.join("', '")}')`
|
||||
);
|
||||
|
||||
// Custom labels and icons
|
||||
var sql = "SELECT customItemTypeID AS id, label, icon FROM customItemTypes";
|
||||
|
@ -402,8 +405,8 @@ Zotero.ItemTypes = new function() {
|
|||
mru.split(',')
|
||||
.slice(0, _numPrimary)
|
||||
.map(name => this.getName(name))
|
||||
// Ignore 'webpage' item type
|
||||
.filter(name => name && name != 'webpage')
|
||||
// Ignore hidden item types and 'webpage'
|
||||
.filter(name => name && !_hiddenTypeNames.concat('webpage').includes(name))
|
||||
);
|
||||
|
||||
// Add types from defaults until we reach our limit
|
||||
|
|
|
@ -763,7 +763,7 @@ Zotero.DataObject.prototype._getLatestField = function (field) {
|
|||
*/
|
||||
Zotero.DataObject.prototype._markFieldChange = function (field, value) {
|
||||
// New method (changedData)
|
||||
if (['deleted', 'tags'].includes(field)) {
|
||||
if (['deleted', 'tags'].includes(field) || field.startsWith('annotation')) {
|
||||
if (Array.isArray(value)) {
|
||||
this._changedData[field] = [...value];
|
||||
}
|
||||
|
|
|
@ -486,9 +486,14 @@ Zotero.DataObjects.prototype.loadDataTypes = Zotero.Promise.coroutine(function*
|
|||
* @param {Integer[]} [ids]
|
||||
*/
|
||||
Zotero.DataObjects.prototype._loadDataTypeInLibrary = Zotero.Promise.coroutine(function* (dataType, libraryID, ids) {
|
||||
var funcName = "_load" + dataType[0].toUpperCase() + dataType.substr(1)
|
||||
// note → loadNotes
|
||||
// itemData → loadItemData
|
||||
// annotationDeferred → loadAnnotationsDeferred
|
||||
var baseDataType = dataType.replace('Deferred', '');
|
||||
var funcName = "_load" + dataType[0].toUpperCase() + baseDataType.substr(1)
|
||||
// Single data types need an 's' (e.g., 'note' -> 'loadNotes()')
|
||||
+ ((dataType.endsWith('s') || dataType.endsWith('Data') ? '' : 's'));
|
||||
+ ((baseDataType.endsWith('s') || baseDataType.endsWith('Data') ? '' : 's'))
|
||||
+ (dataType.endsWith('Deferred') ? 'Deferred' : '');
|
||||
if (!this[funcName]) {
|
||||
throw new Error(`Zotero.${this._ZDO_Objects}.${funcName} is not a function`);
|
||||
}
|
||||
|
|
|
@ -64,6 +64,16 @@ Zotero.Item = function(itemTypeOrID) {
|
|||
this._attachments = null;
|
||||
this._notes = null;
|
||||
|
||||
// loadAnnotation
|
||||
this._annotationType = null;
|
||||
this._annotationText = null;
|
||||
this._annotationImage = null;
|
||||
this._annotationComment = null;
|
||||
this._annotationColor = null;
|
||||
this._annotationPageLabel = null;
|
||||
this._annotationSortIndex = null;
|
||||
this._annotationPosition = null;
|
||||
|
||||
this._tags = [];
|
||||
this._collections = [];
|
||||
|
||||
|
@ -91,6 +101,8 @@ Zotero.Item.prototype._dataTypes = Zotero.Item._super.prototype._dataTypes.conca
|
|||
'creators',
|
||||
'itemData',
|
||||
'note',
|
||||
'annotation',
|
||||
'annotationDeferred',
|
||||
'childItems',
|
||||
// 'relatedItems', // TODO: remove
|
||||
'tags',
|
||||
|
@ -122,6 +134,9 @@ for (let name of ['libraryID', 'key', 'dateAdded', 'dateModified', 'version', 's
|
|||
Zotero.defineProperty(Zotero.Item.prototype, 'itemTypeID', {
|
||||
get: function() { return this._itemTypeID; }
|
||||
});
|
||||
Zotero.defineProperty(Zotero.Item.prototype, 'itemType', {
|
||||
get: function() { return Zotero.ItemTypes.getName(this._itemTypeID); }
|
||||
});
|
||||
|
||||
// .parentKey and .parentID defined in dataObject.js, but create aliases
|
||||
Zotero.defineProperty(Zotero.Item.prototype, 'parentItemID', {
|
||||
|
@ -177,8 +192,8 @@ Zotero.Item.prototype._set = function () {
|
|||
}
|
||||
|
||||
Zotero.Item.prototype._setParentKey = function() {
|
||||
if (!this.isNote() && !this.isAttachment()) {
|
||||
throw new Error("_setParentKey() can only be called on items of type 'note' or 'attachment'");
|
||||
if (!this.isNote() && !this.isAttachment() && !this.isAnnotation()) {
|
||||
throw new Error("_setParentKey() can only be called on items of type 'note', 'attachment', or 'annotation'");
|
||||
}
|
||||
|
||||
Zotero.Item._super.prototype._setParentKey.apply(this, arguments);
|
||||
|
@ -1426,6 +1441,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
|||
switch (Zotero.ItemTypes.getName(itemTypeID)) {
|
||||
case 'note':
|
||||
case 'attachment':
|
||||
case 'annotation':
|
||||
reloadParentChildItems[parentItemID] = true;
|
||||
break;
|
||||
}
|
||||
|
@ -1741,6 +1757,62 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Annotation
|
||||
//
|
||||
if (this._changed.annotation || this._changed.annotationDeferred) {
|
||||
if (!parentItemID) {
|
||||
throw new Error("Annotation item must have a parent item");
|
||||
}
|
||||
let parentItem = Zotero.Items.get(parentItemID);
|
||||
if (!parentItem.isAttachment()) {
|
||||
throw new Error("Annotation parent must be an attachment item");
|
||||
}
|
||||
if (!parentItem.isFileAttachment()) {
|
||||
throw new Error("Annotation parent must be a file attachment");
|
||||
}
|
||||
if (parentItem.attachmentContentType != 'application/pdf') {
|
||||
throw new Error("Annotation parent must be a PDF");
|
||||
}
|
||||
let type = this._getLatestField('annotationType');
|
||||
let typeID = Zotero.Annotations[`ANNOTATION_TYPE_${type.toUpperCase()}`];
|
||||
if (!typeID) {
|
||||
throw new Error(`Invalid annotation type '${type}'`);
|
||||
}
|
||||
|
||||
let text = this._getLatestField('annotationText');
|
||||
let comment = this._getLatestField('annotationComment');
|
||||
let color = this._getLatestField('annotationColor');
|
||||
let pageLabel = this._getLatestField('annotationPageLabel');
|
||||
let sortIndex = this._getLatestField('annotationSortIndex');
|
||||
let position = this._getLatestField('annotationPosition');
|
||||
// This gets stringified, so make sure it's not null
|
||||
if (!position) {
|
||||
throw new Error("Annotation position not set");
|
||||
}
|
||||
|
||||
let sql = "REPLACE INTO itemAnnotations "
|
||||
+ "(itemID, parentItemID, type, text, comment, color, pageLabel, sortIndex, position) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
yield Zotero.DB.queryAsync(
|
||||
sql,
|
||||
[
|
||||
itemID,
|
||||
parentItemID,
|
||||
typeID,
|
||||
text || null,
|
||||
comment || null,
|
||||
color || null,
|
||||
pageLabel || null,
|
||||
sortIndex,
|
||||
JSON.stringify(position)
|
||||
]
|
||||
);
|
||||
|
||||
// Clear cached child items of the parent attachment
|
||||
reloadParentChildItems[parentItemID] = true;
|
||||
}
|
||||
|
||||
// Add to new collections
|
||||
if (env.collectionsAdded) {
|
||||
let toAdd = env.collectionsAdded;
|
||||
|
@ -1895,7 +1967,9 @@ Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
|
|||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
// Methods dealing with note items
|
||||
//
|
||||
// Note methods
|
||||
//
|
||||
//
|
||||
////////////////////////////////////////////////////////
|
||||
/**
|
||||
|
@ -2083,10 +2157,12 @@ Zotero.Item.prototype.getNotes = function(includeTrashed) {
|
|||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
// Methods dealing with attachments
|
||||
//
|
||||
// Attachment methods
|
||||
//
|
||||
// save() is not required for attachment functions
|
||||
//
|
||||
//
|
||||
///////////////////////////////////////////////////////
|
||||
/**
|
||||
* Determine if an item is an attachment
|
||||
|
@ -2104,8 +2180,11 @@ Zotero.Item.prototype.isImportedAttachment = function() {
|
|||
return false;
|
||||
}
|
||||
var linkMode = this.attachmentLinkMode;
|
||||
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE || linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) {
|
||||
return true;
|
||||
switch (linkMode) {
|
||||
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
||||
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
||||
case Zotero.Attachments.LINK_MODE_EMBEDDED_IMAGE:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -2310,8 +2389,7 @@ Zotero.Item.prototype.getFilePathAsync = Zotero.Promise.coroutine(function* () {
|
|||
}
|
||||
|
||||
// Imported file with relative path
|
||||
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
|
||||
linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
||||
if (this.isImportedAttachment()) {
|
||||
if (!path.includes("storage:")) {
|
||||
Zotero.logError("Invalid attachment path '" + path + "'");
|
||||
this._updateAttachmentStates(false);
|
||||
|
@ -2731,6 +2809,7 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentLinkMode', {
|
|||
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
||||
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
|
||||
case Zotero.Attachments.LINK_MODE_LINKED_URL:
|
||||
case Zotero.Attachments.LINK_MODE_EMBEDDED_IMAGE:
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -3360,6 +3439,180 @@ Zotero.Item.prototype.clearBestAttachmentState = function () {
|
|||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
//
|
||||
// Annotation methods
|
||||
//
|
||||
//
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
// Main annotation properties (required for items list display)
|
||||
for (let name of ['type', 'text', 'comment', 'color', 'pageLabel', 'sortIndex']) {
|
||||
let field = 'annotation' + name[0].toUpperCase() + name.substr(1);
|
||||
Zotero.defineProperty(Zotero.Item.prototype, field, {
|
||||
get: function () {
|
||||
this._requireData('annotation');
|
||||
return this._getLatestField(field);
|
||||
},
|
||||
set: function (value) {
|
||||
this._requireData('annotation');
|
||||
|
||||
if (this._getLatestField(field) === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case 'type': {
|
||||
let currentType = this._getLatestField('annotationType');
|
||||
if (currentType && currentType != value) {
|
||||
throw new Error("Cannot change annotation type");
|
||||
}
|
||||
if (!['highlight', 'note', 'area'].includes(value)) {
|
||||
throw new Error(`Invalid annotation type '${value}'`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'text':
|
||||
if (this._getLatestField('annotationType') != 'highlight') {
|
||||
throw new Error("'annotationText' can only be set for highlight annotations");
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sortIndex':
|
||||
if (!/^\d{6}\|\d{7}\|\d{6}\.\d{3}$/.test(value)) {
|
||||
throw new Error(`Invalid sortIndex '${value}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this._markFieldChange(field, value);
|
||||
this._changed.annotation = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Deferred annotation properties (not necessary until viewed)
|
||||
for (let name of ['position']) {
|
||||
let field = 'annotation' + name[0].toUpperCase() + name.substr(1);
|
||||
Zotero.defineProperty(Zotero.Item.prototype, field, {
|
||||
get: function () {
|
||||
this._requireData('annotationDeferred');
|
||||
return this._getLatestField(field);
|
||||
},
|
||||
set: function (value) {
|
||||
this._requireData('annotationDeferred');
|
||||
if (this._getLatestField(field) === value) {
|
||||
return;
|
||||
}
|
||||
this._markFieldChange(field, value);
|
||||
this._changed.annotationDeferred = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @property {Zotero.Item} annotationImageAttachment
|
||||
*/
|
||||
Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageAttachment', {
|
||||
get: function () {
|
||||
if (!this.isAreaAnnotation()) {
|
||||
throw new Error("'annotationImageAttachment' is only valid for area annotations");
|
||||
}
|
||||
var attachments = this.getAttachments();
|
||||
if (!attachments.length) {
|
||||
throw new Error("No attachments found for area annotation");
|
||||
}
|
||||
return Zotero.Items.get(attachments[0]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* @property {String} annotationImageURL
|
||||
*/
|
||||
Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageURL', {
|
||||
get: function () {
|
||||
if (!this.isAreaAnnotation()) {
|
||||
throw new Error("'annotationImageURL' is only valid for area annotations");
|
||||
}
|
||||
var attachments = this.getAttachments();
|
||||
if (!attachments.length) {
|
||||
throw new Error("No attachments found for area annotation");
|
||||
}
|
||||
|
||||
var { libraryID, key } = Zotero.Items.getLibraryAndKeyFromID(attachments[0]);
|
||||
var url = 'zotero://attachment/';
|
||||
if (libraryID == Zotero.Libraries.userLibraryID) {
|
||||
url += 'library';
|
||||
}
|
||||
else {
|
||||
url += Zotero.URI.getLibraryPath(libraryID);
|
||||
}
|
||||
url += '/items/' + key;
|
||||
|
||||
return url;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Determine if an item is an annotation
|
||||
*
|
||||
* @return {Boolean}
|
||||
**/
|
||||
Zotero.Item.prototype.isAnnotation = function() {
|
||||
return Zotero.ItemTypes.getName(this.itemTypeID) == 'annotation';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if an item is an annotation
|
||||
*
|
||||
* @return {Boolean}
|
||||
**/
|
||||
Zotero.Item.prototype.isAreaAnnotation = function() {
|
||||
return this.isAnnotation() && this._getLatestField('annotationType') == 'area';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns child annotations for an attachment item
|
||||
*
|
||||
* @param {Boolean} [includeTrashed=false] - Include annotations in trash
|
||||
* @return {Zotero.Item[]}
|
||||
*/
|
||||
Zotero.Item.prototype.getAnnotations = function (includeTrashed) {
|
||||
if (!this.isAttachment()) {
|
||||
throw new Error("getAnnotations() can only be called on attachment items");
|
||||
}
|
||||
|
||||
this._requireData('childItems');
|
||||
|
||||
if (!this._annotations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var cacheKey = 'with' + (includeTrashed ? '' : 'out') + 'Trashed';
|
||||
|
||||
if (this._annotations[cacheKey]) {
|
||||
return [...this._annotations[cacheKey]];
|
||||
}
|
||||
|
||||
var rows = this._annotations.rows.concat();
|
||||
// Remove trashed items if necessary
|
||||
if (!includeTrashed) {
|
||||
rows = rows.filter(row => !row.trashed);
|
||||
}
|
||||
var ids = rows.map(row => row.itemID);
|
||||
this._annotations[cacheKey] = ids;
|
||||
return ids;
|
||||
};
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Methods dealing with item tags
|
||||
//
|
||||
|
|
|
@ -518,7 +518,9 @@ Zotero.ItemFields = new function() {
|
|||
var rows = yield Zotero.DB.queryAsync(sql);
|
||||
|
||||
_itemTypeFields = {
|
||||
[Zotero.ItemTypes.getID('note')]: [] // Notes have no fields
|
||||
// Notes and annotations have no fields
|
||||
[Zotero.ItemTypes.getID('note')]: [],
|
||||
[Zotero.ItemTypes.getID('annotation')]: []
|
||||
};
|
||||
|
||||
for (let i=0; i<rows.length; i++) {
|
||||
|
|
|
@ -38,6 +38,7 @@ Zotero.Items = function() {
|
|||
get: function () {
|
||||
var itemTypeAttachment = Zotero.ItemTypes.getID('attachment');
|
||||
var itemTypeNote = Zotero.ItemTypes.getID('note');
|
||||
var itemTypeAnnotation = Zotero.ItemTypes.getID('annotation');
|
||||
|
||||
return {
|
||||
itemID: "O.itemID",
|
||||
|
@ -58,10 +59,16 @@ Zotero.Items = function() {
|
|||
deleted: "DI.itemID IS NOT NULL AS deleted",
|
||||
inPublications: "PI.itemID IS NOT NULL AS inPublications",
|
||||
|
||||
parentID: `(CASE O.itemTypeID WHEN ${itemTypeAttachment} THEN IAP.itemID `
|
||||
+ `WHEN ${itemTypeNote} THEN INoP.itemID END) AS parentID`,
|
||||
parentKey: `(CASE O.itemTypeID WHEN ${itemTypeAttachment} THEN IAP.key `
|
||||
+ `WHEN ${itemTypeNote} THEN INoP.key END) AS parentKey`,
|
||||
parentID: `(CASE O.itemTypeID `
|
||||
+ `WHEN ${itemTypeAttachment} THEN IAP.itemID `
|
||||
+ `WHEN ${itemTypeNote} THEN INoP.itemID `
|
||||
+ `WHEN ${itemTypeAnnotation} THEN IAnP.itemID `
|
||||
+ `END) AS parentID`,
|
||||
parentKey: `(CASE O.itemTypeID `
|
||||
+ `WHEN ${itemTypeAttachment} THEN IAP.key `
|
||||
+ `WHEN ${itemTypeNote} THEN INoP.key `
|
||||
+ `WHEN ${itemTypeAnnotation} THEN IAnP.key `
|
||||
+ `END) AS parentKey`,
|
||||
|
||||
attachmentCharset: "CS.charset AS attachmentCharset",
|
||||
attachmentLinkMode: "IA.linkMode AS attachmentLinkMode",
|
||||
|
@ -80,6 +87,8 @@ Zotero.Items = function() {
|
|||
+ "LEFT JOIN items IAP ON (IA.parentItemID=IAP.itemID) "
|
||||
+ "LEFT JOIN itemNotes INo ON (O.itemID=INo.itemID) "
|
||||
+ "LEFT JOIN items INoP ON (INo.parentItemID=INoP.itemID) "
|
||||
+ "LEFT JOIN itemAnnotations IAn ON (O.itemID=IAn.itemID) "
|
||||
+ "LEFT JOIN items IAnP ON (IAn.parentItemID=IAnP.itemID) "
|
||||
+ "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID) "
|
||||
+ "LEFT JOIN publicationsItems PI ON (O.itemID=PI.itemID) "
|
||||
+ "LEFT JOIN charsets CS ON (IA.charsetID=CS.charsetID)"
|
||||
|
@ -483,6 +492,92 @@ Zotero.Items = function() {
|
|||
});
|
||||
|
||||
|
||||
this._loadAnnotations = async function (libraryID, ids, idSQL) {
|
||||
var sql = "SELECT itemID, IA.parentItemID, IA.type, IA.text, IA.comment, IA.color, IA.sortIndex "
|
||||
+ "FROM items JOIN itemAnnotations IA USING (itemID) "
|
||||
+ "WHERE libraryID=?" + idSQL;
|
||||
var params = [libraryID];
|
||||
await Zotero.DB.queryAsync(
|
||||
sql,
|
||||
params,
|
||||
{
|
||||
noCache: ids.length != 1,
|
||||
onRow: function (row) {
|
||||
let itemID = row.getResultByIndex(0);
|
||||
|
||||
let item = this._objectCache[itemID];
|
||||
if (!item) {
|
||||
throw new Error("Item " + itemID + " not found");
|
||||
}
|
||||
|
||||
item._parentItemID = row.getResultByIndex(1);
|
||||
var typeID = row.getResultByIndex(2);
|
||||
var type;
|
||||
switch (typeID) {
|
||||
case Zotero.Annotations.ANNOTATION_TYPE_HIGHLIGHT:
|
||||
type = 'highlight';
|
||||
break;
|
||||
|
||||
case Zotero.Annotations.ANNOTATION_TYPE_NOTE:
|
||||
type = 'note';
|
||||
break;
|
||||
|
||||
case Zotero.Annotations.ANNOTATION_TYPE_AREA:
|
||||
type = 'area';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown annotation type id ${typeID}`);
|
||||
}
|
||||
item._annotationType = type;
|
||||
item._annotationText = row.getResultByIndex(3);
|
||||
item._annotationComment = row.getResultByIndex(4);
|
||||
item._annotationColor = row.getResultByIndex(5);
|
||||
item._annotationSortIndex = row.getResultByIndex(6);
|
||||
|
||||
item._loaded.annotation = true;
|
||||
item._clearChanged('annotation');
|
||||
}.bind(this)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
this._loadAnnotationsDeferred = async function (libraryID, ids, idSQL) {
|
||||
var sql = "SELECT itemID, IA.position, IA.pageLabel FROM items "
|
||||
+ "JOIN itemAnnotations IA USING (itemID) "
|
||||
+ "WHERE libraryID=?" + idSQL;
|
||||
var params = [libraryID];
|
||||
await Zotero.DB.queryAsync(
|
||||
sql,
|
||||
params,
|
||||
{
|
||||
noCache: ids.length != 1,
|
||||
onRow: function (row) {
|
||||
let itemID = row.getResultByIndex(0);
|
||||
|
||||
let item = this._objectCache[itemID];
|
||||
if (!item) {
|
||||
throw new Error("Item " + itemID + " not found");
|
||||
}
|
||||
|
||||
try {
|
||||
item._annotationPosition = JSON.parse(row.getResultByIndex(1));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(`Error parsing 'position' for item ${item.libraryKey}`);
|
||||
item._annotationPosition = {};
|
||||
}
|
||||
item._annotationPageLabel = row.getResultByIndex(2);
|
||||
|
||||
item._loaded.annotationDeferred = true;
|
||||
item._clearChanged('annotationDeferred');
|
||||
}.bind(this)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
this._loadChildItems = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) {
|
||||
var params = [libraryID];
|
||||
var rows = [];
|
||||
|
@ -606,6 +701,51 @@ Zotero.Items = function() {
|
|||
ids.forEach(id => setNoteItem(id, []));
|
||||
}
|
||||
|
||||
//
|
||||
// Annotations
|
||||
//
|
||||
sql = "SELECT parentItemID, IAn.itemID, "
|
||||
+ "text || ' - ' || comment AS title, " // TODO: Make better
|
||||
+ "CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed "
|
||||
+ "FROM itemAnnotations IAn "
|
||||
+ "JOIN items I ON (IAn.parentItemID=I.itemID) "
|
||||
+ "LEFT JOIN deletedItems DI USING (itemID) "
|
||||
+ "WHERE libraryID=?"
|
||||
+ (ids.length ? " AND parentItemID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : "")
|
||||
+ " ORDER BY parentItemID, sortIndex";
|
||||
var setAnnotationItem = function (itemID, rows) {
|
||||
var item = this._objectCache[itemID];
|
||||
if (!item) {
|
||||
throw new Error("Item " + itemID + " not loaded");
|
||||
}
|
||||
rows.sort((a, b) => a.sortIndex - b.sortIndex);
|
||||
item._annotations = {
|
||||
rows,
|
||||
withTrashed: null,
|
||||
withoutTrashed: null
|
||||
};
|
||||
}.bind(this);
|
||||
lastItemID = null;
|
||||
rows = [];
|
||||
yield Zotero.DB.queryAsync(
|
||||
sql,
|
||||
params,
|
||||
{
|
||||
noCache: ids.length != 1,
|
||||
onRow: function (row) {
|
||||
onRow(row, setAnnotationItem);
|
||||
}
|
||||
}
|
||||
);
|
||||
// Process unprocessed rows
|
||||
if (lastItemID) {
|
||||
setAnnotationItem(lastItemID, rows);
|
||||
}
|
||||
// Otherwise clear existing entries for passed items
|
||||
else if (ids.length) {
|
||||
ids.forEach(id => setAnnotationItem(id, []));
|
||||
}
|
||||
|
||||
// Mark all top-level items as having child items loaded
|
||||
sql = "SELECT itemID FROM items I WHERE libraryID=?" + idSQL + " AND itemID NOT IN "
|
||||
+ "(SELECT itemID FROM itemAttachments UNION SELECT itemID FROM itemNotes)";
|
||||
|
|
|
@ -41,7 +41,7 @@ Zotero.Schema = new function(){
|
|||
|
||||
// If updating from this userdata version or later, don't show "Upgrading database…" and don't make
|
||||
// DB backup first. This should be set to false when breaking compatibility or making major changes.
|
||||
const minorUpdateFrom = 107;
|
||||
const minorUpdateFrom = false;
|
||||
|
||||
var _dbVersions = [];
|
||||
var _schemaVersions = [];
|
||||
|
@ -344,9 +344,18 @@ Zotero.Schema = new function(){
|
|||
* @return {Object}
|
||||
*/
|
||||
async function _readGlobalSchemaFromFile() {
|
||||
return JSON.parse(
|
||||
var data = JSON.parse(
|
||||
await Zotero.File.getResourceAsync('resource://zotero/schema/global/schema.json')
|
||||
);
|
||||
// TEMP: Add annotation to schema
|
||||
// TODO: Move to schema.json
|
||||
data.itemTypes.push({
|
||||
itemType: "annotation",
|
||||
fields: [],
|
||||
creatorTypes: []
|
||||
});
|
||||
data.locales['en-US'].itemTypes.annotation = 'Annotation';
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1944,8 +1953,8 @@ Zotero.Schema = new function(){
|
|||
],
|
||||
// Invalid link mode -- set to imported url
|
||||
[
|
||||
"SELECT COUNT(*) > 0 FROM itemAttachments WHERE linkMode NOT IN (0,1,2,3)",
|
||||
"UPDATE itemAttachments SET linkMode=1 WHERE linkMode NOT IN (0,1,2,3)"
|
||||
"SELECT COUNT(*) > 0 FROM itemAttachments WHERE linkMode NOT IN (0,1,2,3,4)",
|
||||
"UPDATE itemAttachments SET linkMode=1 WHERE linkMode NOT IN (0,1,2,3,4)"
|
||||
],
|
||||
// Creators with first name can't be fieldMode 1
|
||||
[
|
||||
|
@ -3224,6 +3233,9 @@ Zotero.Schema = new function(){
|
|||
|
||||
yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS users");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE users (\n userID INTEGER PRIMARY KEY,\n name TEXT NOT NULL\n)");
|
||||
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE itemAnnotations (\n itemID INTEGER PRIMARY KEY,\n parentItemID INT NOT NULL,\n type INTEGER NOT NULL,\n text TEXT,\n comment TEXT,\n color TEXT,\n pageLabel TEXT,\n sortIndex TEXT NOT NULL,\n position TEXT NOT NULL,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX itemAnnotations_parentItemID ON itemAnnotations(parentItemID)");
|
||||
}
|
||||
|
||||
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
|
||||
|
|
|
@ -64,6 +64,7 @@ const xpcomFilesLocal = [
|
|||
'libraryTreeView',
|
||||
'collectionTreeView',
|
||||
'collectionTreeRow',
|
||||
'annotations',
|
||||
'api',
|
||||
'attachments',
|
||||
'cite',
|
||||
|
|
|
@ -113,6 +113,21 @@ CREATE INDEX itemAttachments_charsetID ON itemAttachments(charsetID);
|
|||
CREATE INDEX itemAttachments_contentType ON itemAttachments(contentType);
|
||||
CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
|
||||
|
||||
CREATE TABLE itemAnnotations (
|
||||
itemID INTEGER PRIMARY KEY,
|
||||
parentItemID INT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
text TEXT,
|
||||
comment TEXT,
|
||||
color TEXT,
|
||||
pageLabel TEXT,
|
||||
sortIndex TEXT NOT NULL,
|
||||
position TEXT NOT NULL,
|
||||
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX itemAnnotations_parentItemID ON itemAnnotations(parentItemID);
|
||||
|
||||
CREATE TABLE tags (
|
||||
tagID INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
|
|
|
@ -689,7 +689,7 @@ function generateAllTypesAndFieldsData() {
|
|||
};
|
||||
|
||||
// Item types that should not be included in sample data
|
||||
let excludeItemTypes = ['note', 'attachment'];
|
||||
let excludeItemTypes = ['note', 'attachment', 'annotation'];
|
||||
|
||||
for (let i = 0; i < itemTypes.length; i++) {
|
||||
if (excludeItemTypes.indexOf(itemTypes[i].name) != -1) continue;
|
||||
|
@ -921,6 +921,28 @@ function importHTMLAttachment() {
|
|||
}
|
||||
|
||||
|
||||
async function createAnnotation(type, parentItem) {
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.parentID = parentItem.id;
|
||||
annotation.annotationType = type;
|
||||
if (type == 'highlight') {
|
||||
annotation.annotationText = Zotero.Utilities.randomString();
|
||||
}
|
||||
annotation.annotationComment = Zotero.Utilities.randomString();
|
||||
var page = Zotero.Utilities.rand(1, 100).toString().padStart(6, '0');
|
||||
var pos = Zotero.Utilities.rand(1, 10000).toString().padStart(7, '0');
|
||||
annotation.annotationSortIndex = `${page}|${pos}|000000.000`;
|
||||
annotation.annotationPosition = {
|
||||
pageIndex: 123,
|
||||
rects: [
|
||||
[314.4, 412.8, 556.2, 609.6]
|
||||
]
|
||||
};
|
||||
await annotation.saveTx();
|
||||
return annotation;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the fake XHR server to response to a given response
|
||||
*
|
||||
|
|
266
test/tests/annotationsTest.js
Normal file
266
test/tests/annotationsTest.js
Normal file
|
@ -0,0 +1,266 @@
|
|||
describe("Zotero.Annotations", function() {
|
||||
var exampleHighlight = {
|
||||
"key": "92JLMCVT",
|
||||
"type": "highlight",
|
||||
"isAuthor": true,
|
||||
"text": "This is an <b>extracted</b> text with rich-text\nAnd a new line",
|
||||
"comment": "This is a comment with <i>rich-text</i>\nAnd a new line",
|
||||
"color": "#ffec00",
|
||||
"pageLabel": "15",
|
||||
"sortIndex": "000015|0002431|000000.000",
|
||||
"position": {
|
||||
"pageIndex": 1,
|
||||
"rects": [
|
||||
[231.284, 402.126, 293.107, 410.142],
|
||||
[54.222, 392.164, 293.107, 400.18],
|
||||
[54.222, 382.201, 293.107, 390.217],
|
||||
[54.222, 372.238, 293.107, 380.254],
|
||||
[54.222, 362.276, 273.955, 370.292]
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "math",
|
||||
"color": "#ff0000"
|
||||
},
|
||||
{
|
||||
"name": "chemistry"
|
||||
}
|
||||
],
|
||||
"dateModified": "2019-05-14 06:50:40"
|
||||
};
|
||||
|
||||
var exampleNote = {
|
||||
"key": "5TKU34XX",
|
||||
"type": "note",
|
||||
"isAuthor": true,
|
||||
"comment": "This is a note",
|
||||
"color": "#ffec00",
|
||||
"pageLabel": "14",
|
||||
"sortIndex": "000014|0001491|000283.000",
|
||||
"position": {
|
||||
"pageIndex": 0,
|
||||
"rects": [
|
||||
[371.395, 266.635, 486.075, 274.651]
|
||||
]
|
||||
},
|
||||
"dateModified": "2019-05-14 06:50:54"
|
||||
};
|
||||
|
||||
var exampleArea = {
|
||||
"key": "QD32MQJF",
|
||||
"type": "area",
|
||||
"isAuthor": true,
|
||||
"imageURL": "zotero://attachment/library/items/LB417FR4",
|
||||
"comment": "This is a comment",
|
||||
"color": "#ffec00",
|
||||
"pageLabel": "XVI",
|
||||
"sortIndex": "000016|0003491|000683.000",
|
||||
"position": {
|
||||
"pageIndex": 123,
|
||||
"rects": [
|
||||
[314.4, 412.8, 556.2, 609.6]
|
||||
],
|
||||
"width": 400,
|
||||
"height": 200
|
||||
},
|
||||
"dateModified": "2019-05-14 06:51:22"
|
||||
};
|
||||
|
||||
var exampleGroupHighlight = {
|
||||
"key": "PE57YAYH",
|
||||
"type": "highlight",
|
||||
"isAuthor": false,
|
||||
"authorName": "Kate Smith",
|
||||
"text": "This is an <b>extracted</b> text with rich-text\nAnd a new line",
|
||||
"comment": "This is a comment with <i>rich-text</i>\nAnd a new line",
|
||||
"color": "#ffec00",
|
||||
"pageLabel": "15",
|
||||
"sortIndex": "000015|0002431|000000.000",
|
||||
"position": {
|
||||
"pageIndex": 1,
|
||||
"rects": [
|
||||
[231.284, 402.126, 293.107, 410.142],
|
||||
[54.222, 392.164, 293.107, 400.18],
|
||||
[54.222, 382.201, 293.107, 390.217],
|
||||
[54.222, 372.238, 293.107, 380.254],
|
||||
[54.222, 362.276, 273.955, 370.292]
|
||||
]
|
||||
},
|
||||
"dateModified": "2019-05-14 06:50:40"
|
||||
};
|
||||
|
||||
var item;
|
||||
var attachment;
|
||||
var group;
|
||||
var groupItem;
|
||||
var groupAttachment;
|
||||
|
||||
before(async function () {
|
||||
item = await createDataObject('item');
|
||||
attachment = await importFileAttachment('test.pdf', { parentID: item.id });
|
||||
|
||||
group = await getGroup();
|
||||
groupItem = await createDataObject('item', { libraryID: group.libraryID });
|
||||
groupAttachment = await importFileAttachment(
|
||||
'test.pdf',
|
||||
{ libraryID: group.libraryID, parentID: groupItem.id }
|
||||
);
|
||||
});
|
||||
|
||||
describe("#toJSON()", function () {
|
||||
it("should generate an object for a highlight", async function () {
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.libraryID = attachment.libraryID;
|
||||
annotation.key = exampleHighlight.key;
|
||||
await annotation.loadPrimaryData();
|
||||
annotation.parentID = attachment.id;
|
||||
annotation.annotationType = 'highlight';
|
||||
for (let prop of ['text', 'comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
annotation[itemProp] = exampleHighlight[prop];
|
||||
}
|
||||
annotation.addTag("math");
|
||||
annotation.addTag("chemistry");
|
||||
await annotation.saveTx();
|
||||
await Zotero.Tags.setColor(annotation.libraryID, "math", "#ff0000", 0);
|
||||
var json = Zotero.Annotations.toJSON(annotation);
|
||||
|
||||
assert.sameMembers(Object.keys(json), Object.keys(exampleHighlight));
|
||||
for (let prop of Object.keys(exampleHighlight)) {
|
||||
if (prop == 'dateModified') {
|
||||
continue;
|
||||
}
|
||||
assert.deepEqual(json[prop], exampleHighlight[prop], `'${prop}' doesn't match`);
|
||||
}
|
||||
|
||||
await annotation.eraseTx();
|
||||
});
|
||||
|
||||
it("should generate an object for a note", async function () {
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.libraryID = attachment.libraryID;
|
||||
annotation.key = exampleNote.key;
|
||||
await annotation.loadPrimaryData();
|
||||
annotation.parentID = attachment.id;
|
||||
annotation.annotationType = 'note';
|
||||
for (let prop of ['comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
annotation[itemProp] = exampleNote[prop];
|
||||
}
|
||||
await annotation.saveTx();
|
||||
var json = Zotero.Annotations.toJSON(annotation);
|
||||
|
||||
assert.sameMembers(Object.keys(json), Object.keys(exampleNote));
|
||||
for (let prop of Object.keys(exampleNote)) {
|
||||
if (prop == 'dateModified') {
|
||||
continue;
|
||||
}
|
||||
assert.deepEqual(json[prop], exampleNote[prop], `'${prop}' doesn't match`);
|
||||
}
|
||||
|
||||
await annotation.eraseTx();
|
||||
});
|
||||
|
||||
it("should generate an object for an area", async function () {
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.libraryID = attachment.libraryID;
|
||||
annotation.key = exampleArea.key;
|
||||
await annotation.loadPrimaryData();
|
||||
annotation.parentID = attachment.id;
|
||||
annotation.annotationType = 'area';
|
||||
for (let prop of ['comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
annotation[itemProp] = exampleArea[prop];
|
||||
}
|
||||
await annotation.saveTx();
|
||||
|
||||
// Get Blob from file and attach it
|
||||
var path = OS.Path.join(getTestDataDirectory().path, 'test.png');
|
||||
var imageData = await Zotero.File.getBinaryContentsAsync(path);
|
||||
var array = new Uint8Array(imageData.length);
|
||||
for (let i = 0; i < imageData.length; i++) {
|
||||
array[i] = imageData.charCodeAt(i);
|
||||
}
|
||||
var imageAttachment = await Zotero.Attachments.importEmbeddedImage({
|
||||
blob: new Blob([array], { type: 'image/png' }),
|
||||
parentItemID: annotation.id
|
||||
});
|
||||
|
||||
var json = Zotero.Annotations.toJSON(annotation);
|
||||
|
||||
assert.sameMembers(Object.keys(json), Object.keys(exampleArea));
|
||||
for (let prop of Object.keys(exampleArea)) {
|
||||
if (prop == 'imageURL'
|
||||
|| prop == 'dateModified') {
|
||||
continue;
|
||||
}
|
||||
assert.deepEqual(json[prop], exampleArea[prop], `'${prop}' doesn't match`);
|
||||
}
|
||||
assert.equal(json.imageURL, `zotero://attachment/library/items/${imageAttachment.key}`);
|
||||
|
||||
await annotation.eraseTx();
|
||||
});
|
||||
|
||||
it("should generate an object for a highlight by another user in a group library", async function () {
|
||||
await Zotero.Users.setName(12345, 'Kate Smith');
|
||||
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.libraryID = group.libraryID;
|
||||
annotation.key = exampleGroupHighlight.key;
|
||||
await annotation.loadPrimaryData();
|
||||
annotation.createdByUserID = 12345;
|
||||
annotation.parentID = groupAttachment.id;
|
||||
annotation.annotationType = 'highlight';
|
||||
for (let prop of ['text', 'comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
annotation[itemProp] = exampleGroupHighlight[prop];
|
||||
}
|
||||
await annotation.saveTx();
|
||||
var json = Zotero.Annotations.toJSON(annotation);
|
||||
|
||||
assert.isFalse(json.isAuthor);
|
||||
assert.equal(json.authorName, 'Kate Smith');
|
||||
|
||||
await annotation.eraseTx();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("#saveFromJSON()", function () {
|
||||
it("should create an item from a highlight", async function () {
|
||||
var annotation = await Zotero.Annotations.saveFromJSON(attachment, exampleHighlight);
|
||||
|
||||
assert.equal(annotation.key, exampleHighlight.key);
|
||||
for (let prop of ['text', 'comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
assert.deepEqual(annotation[itemProp], exampleHighlight[prop], `'${prop}' doesn't match`);
|
||||
}
|
||||
var itemTags = annotation.getTags().map(t => t.tag);
|
||||
var jsonTags = exampleHighlight.tags.map(t => t.name);
|
||||
assert.sameMembers(itemTags, jsonTags);
|
||||
});
|
||||
|
||||
it("should create an item from a note", async function () {
|
||||
var annotation = await Zotero.Annotations.saveFromJSON(attachment, exampleNote);
|
||||
|
||||
assert.equal(annotation.key, exampleNote.key);
|
||||
for (let prop of ['comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
assert.deepEqual(annotation[itemProp], exampleNote[prop], `'${prop}' doesn't match`);
|
||||
}
|
||||
});
|
||||
|
||||
it("should create an item from an area", async function () {
|
||||
var annotation = await Zotero.Annotations.saveFromJSON(attachment, exampleArea);
|
||||
|
||||
// Note: Image is created separately using Zotero.Attachments.importEmbeddedImage()
|
||||
|
||||
assert.equal(annotation.key, exampleArea.key);
|
||||
for (let prop of ['comment', 'color', 'pageLabel', 'sortIndex', 'position']) {
|
||||
let itemProp = 'annotation' + prop[0].toUpperCase() + prop.substr(1);
|
||||
assert.deepEqual(annotation[itemProp], exampleArea[prop], `'${prop}' doesn't match`);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
|
@ -709,6 +709,27 @@ describe("Zotero.Item", function () {
|
|||
assert.equal(attachments[0], attachment.id);
|
||||
})
|
||||
|
||||
it("should update after an attachment is moved to the trash", async function () {
|
||||
var item = await createDataObject('item');
|
||||
var attachment = new Zotero.Item("attachment");
|
||||
attachment.parentID = item.id;
|
||||
attachment.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_FILE;
|
||||
await attachment.saveTx();
|
||||
|
||||
// Attachment should show up initially
|
||||
var attachments = item.getAttachments();
|
||||
assert.lengthOf(attachments, 1);
|
||||
assert.equal(attachments[0], attachment.id);
|
||||
|
||||
// Move attachment to trash
|
||||
attachment.deleted = true;
|
||||
await attachment.saveTx();
|
||||
|
||||
// Attachment should not show up without includeTrashed=true
|
||||
attachments = item.getAttachments();
|
||||
assert.lengthOf(attachments, 0);
|
||||
});
|
||||
|
||||
it("#should return an empty array for an item with no attachments", function* () {
|
||||
var item = yield createDataObject('item');
|
||||
assert.lengthOf(item.getAttachments(), 0);
|
||||
|
@ -1138,6 +1159,146 @@ describe("Zotero.Item", function () {
|
|||
});
|
||||
|
||||
|
||||
describe("Annotations", function () {
|
||||
var item;
|
||||
var attachment;
|
||||
|
||||
before(async function () {
|
||||
item = await createDataObject('item');
|
||||
attachment = await importFileAttachment('test.pdf', { parentID: item.id });
|
||||
});
|
||||
|
||||
describe("#annotationText", function () {
|
||||
it("should not be changeable", async function () {
|
||||
var a = new Zotero.Item('annotation');
|
||||
a.annotationType = 'highlight';
|
||||
assert.doesNotThrow(() => a.annotationType = 'highlight');
|
||||
assert.throws(() => a.annotationType = 'note');
|
||||
});
|
||||
});
|
||||
|
||||
describe("#annotationText", function () {
|
||||
it("should only be allowed for highlights", async function () {
|
||||
var a = new Zotero.Item('annotation');
|
||||
a.annotationType = 'highlight';
|
||||
assert.doesNotThrow(() => a.annotationText = "This is highlighted text.");
|
||||
|
||||
a = new Zotero.Item('annotation');
|
||||
a.annotationType = 'note';
|
||||
assert.throws(() => a.annotationText = "This is highlighted text.");
|
||||
|
||||
a = new Zotero.Item('annotation');
|
||||
a.annotationType = 'area';
|
||||
assert.throws(() => a.annotationText = "This is highlighted text.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#saveTx()", function () {
|
||||
it("should save a highlight annotation", async function () {
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.parentID = attachment.id;
|
||||
annotation.annotationType = 'highlight';
|
||||
annotation.annotationText = "This is highlighted text.";
|
||||
annotation.annotationSortIndex = '000015|0002431|000000.000';
|
||||
annotation.annotationPosition = {
|
||||
pageIndex: 123,
|
||||
rects: [
|
||||
[314.4, 412.8, 556.2, 609.6]
|
||||
]
|
||||
};
|
||||
await annotation.saveTx();
|
||||
});
|
||||
|
||||
it("should save a note annotation", async function () {
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.parentID = attachment.id;
|
||||
annotation.annotationType = 'note';
|
||||
annotation.annotationComment = "This is a comment.";
|
||||
annotation.annotationSortIndex = '000015|0002431|000000.000';
|
||||
annotation.annotationPosition = {
|
||||
pageIndex: 123,
|
||||
rects: [
|
||||
[314.4, 412.8, 556.2, 609.6]
|
||||
]
|
||||
};
|
||||
await annotation.saveTx();
|
||||
});
|
||||
|
||||
it("should save an area annotation", async function () {
|
||||
// Create a Blob from a PNG
|
||||
var path = OS.Path.join(getTestDataDirectory().path, 'test.png');
|
||||
var imageData = await Zotero.File.getBinaryContentsAsync(path);
|
||||
var array = new Uint8Array(imageData.length);
|
||||
for (let i = 0; i < imageData.length; i++) {
|
||||
array[i] = imageData.charCodeAt(i);
|
||||
}
|
||||
|
||||
var annotation = new Zotero.Item('annotation');
|
||||
annotation.parentID = attachment.id;
|
||||
annotation.annotationType = 'area';
|
||||
annotation.annotationSortIndex = '000015|0002431|000000.000';
|
||||
annotation.annotationPosition = {
|
||||
pageIndex: 123,
|
||||
rects: [
|
||||
[314.4, 412.8, 556.2, 609.6]
|
||||
],
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
await annotation.saveTx();
|
||||
|
||||
await Zotero.Attachments.importEmbeddedImage({
|
||||
blob: new Blob([array], { type: 'image/png' }),
|
||||
parentItemID: annotation.id
|
||||
});
|
||||
|
||||
var attachments = annotation.getAttachments();
|
||||
assert.lengthOf(attachments, 1);
|
||||
var imageAttachment = Zotero.Items.get(attachments[0]);
|
||||
var imagePath = await imageAttachment.getFilePathAsync();
|
||||
assert.ok(imagePath);
|
||||
assert.equal(OS.Path.basename(imagePath), 'image.png');
|
||||
assert.equal(
|
||||
await Zotero.File.getBinaryContentsAsync(imagePath),
|
||||
imageData
|
||||
);
|
||||
assert.equal(imageAttachment.attachmentContentType, 'image/png');
|
||||
|
||||
assert.equal(
|
||||
annotation.annotationImageURL,
|
||||
`zotero://attachment/library/items/${imageAttachment.key}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getAnnotations()", function () {
|
||||
var item;
|
||||
var attachment;
|
||||
var annotation1;
|
||||
var annotation2;
|
||||
|
||||
before(async function () {
|
||||
item = await createDataObject('item');
|
||||
attachment = await importFileAttachment('test.pdf', { parentID: item.id });
|
||||
annotation1 = await createAnnotation('highlight', attachment);
|
||||
annotation2 = await createAnnotation('highlight', attachment);
|
||||
annotation2.deleted = true;
|
||||
await annotation2.saveTx();
|
||||
});
|
||||
|
||||
it("should return ids of annotations not in trash", async function () {
|
||||
var ids = attachment.getAnnotations();
|
||||
assert.sameMembers(ids, [annotation1.id]);
|
||||
});
|
||||
|
||||
it("should return ids of annotations in trash if includeTrashed=true", async function () {
|
||||
var ids = attachment.getAnnotations(true);
|
||||
assert.sameMembers(ids, [annotation1.id, annotation2.id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("#setTags", function () {
|
||||
it("should save an array of tags in API JSON format", function* () {
|
||||
var tags = [
|
||||
|
|
Loading…
Reference in a new issue