Closes #470, Add tag add/modification/delete targets to Notifier
Closes #471, Tag selector should update when tags are added/removed Tag Selector overhaul: - Right-click to rename/delete tags globally - Filter tags to only those associated with currently visible items, with a Display All checkbox to show others in gray -- scope list set via new callback mechanism in the items tree - Drag and drop items onto tags to batch assign - Tag Notifier events, currently unused (tag selector currently just refreshes on all item events, since doing granular tag updates is considerably more complicated) - Performance improvements, offset by the new features that make it slower There should probably be an option to use either an ANY or an ALL search in the tag selector... (It's ALL by default now.) New methods: - Zotero.hasValues(obj) -- return true if an object (/associative array) has at least one value, false if not - Zotero.Item.addTagByID() - Zotero.Item.hasTag() - Zotero.Tags.getAllWithinSearch(search) - Zotero.Tags.rename(tagID, tag) - Zotero.Tags.remove(tagID) - ItemTreeView.addCallback() - ItemTreeView.setFilter('search'|'tags', data) -- replaces searchText() - CollectionTreeView.getSearchObject() -- search object used to generate the items list - CollectionTreeView.getChildTags()
This commit is contained in:
parent
cac436a7ee
commit
ecab0e5785
|
@ -33,117 +33,285 @@
|
|||
</resources>
|
||||
|
||||
<implementation>
|
||||
<field name="_initialized">false</field>
|
||||
<field name="_notifierID">false</field>
|
||||
<field name="_tags">null</field>
|
||||
<field name="_dirty">null</field>
|
||||
<field name="selection"/>
|
||||
|
||||
<field name="_hasFilter">false</field>
|
||||
<field name="_filter">null</field>
|
||||
<method name="setFilterTags">
|
||||
<parameter name="val"/>
|
||||
<parameter name="skipRefresh"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (Zotero.hasValues(val)) {
|
||||
this._hasFilter = true;
|
||||
this._filter = val;
|
||||
}
|
||||
else {
|
||||
this._hasFilter = !!val;
|
||||
this._filter = {};
|
||||
}
|
||||
|
||||
if (!skipRefresh) {
|
||||
this.refresh();
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<field name="_hasScope">false</field>
|
||||
<field name="_scope">null</field>
|
||||
<property name="scope" onget="return this._scope">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
if (Zotero.hasValues(val)) {
|
||||
this._hasScope = true;
|
||||
this._scope = val;
|
||||
}
|
||||
else {
|
||||
this._hasScope = !!val;
|
||||
this._scope = {};
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<property name="filterToScope">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
return this.getAttribute('filterToScope') == 'true';
|
||||
]]>
|
||||
</getter>
|
||||
<setter>
|
||||
<![CDATA[
|
||||
this.setAttribute('filterToScope', val);
|
||||
this.refresh();
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
||||
<constructor>
|
||||
<![CDATA[
|
||||
this.init();
|
||||
this.setAttribute('filterToScope', true);
|
||||
this.dragObserver = new this._dragObserverConstructor;
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
|
||||
<method name="init">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.selection = {};
|
||||
this.setTagList(Zotero.Tags.getAll());
|
||||
]]>
|
||||
<![CDATA[
|
||||
this._initialized = true;
|
||||
this.selection = {};
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this);
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="setTagList">
|
||||
<parameter name="tags"/>
|
||||
|
||||
<method name="uninit">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tagsToggleDiv = this.id('tags-toggle');
|
||||
|
||||
// Save selection
|
||||
var labels = tagsToggleDiv.getElementsByTagName('label');
|
||||
for (var i=0; i<labels.length; i++){
|
||||
//this.selection[labels[i].value] = labels[i].getAttribute('selected')=='true';
|
||||
}
|
||||
<![CDATA[
|
||||
if (!this._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._initialized = false;
|
||||
this.unregister();
|
||||
this.selection = {};
|
||||
this.doCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="unregister">
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (this._notifierID) {
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="refresh">
|
||||
<parameter name="fetch"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (!this._initialized) {
|
||||
this.init();
|
||||
fetch = true;
|
||||
}
|
||||
|
||||
Zotero.debug('Refreshing tags selector');
|
||||
var tagsToggleBox = this.id('tags-toggle');
|
||||
|
||||
if (fetch || this._dirty) {
|
||||
this._tags = Zotero.Tags.getAll();
|
||||
|
||||
// Remove children
|
||||
while (tagsToggleDiv.hasChildNodes()){
|
||||
tagsToggleDiv.removeChild(tagsToggleDiv.firstChild);
|
||||
while (tagsToggleBox.hasChildNodes()){
|
||||
tagsToggleBox.removeChild(tagsToggleBox.firstChild);
|
||||
}
|
||||
|
||||
// Regenerate list
|
||||
for (var tagID in tags){
|
||||
for (var tagID in this._tags) {
|
||||
var label = document.createElement('label');
|
||||
label.setAttribute('onclick', "this.parentNode.parentNode.parentNode.handleTagClick(this)");
|
||||
label.className = 'zotero-clicky';
|
||||
label.setAttribute('value', tags[tagID]);
|
||||
tagsToggleDiv.appendChild(label);
|
||||
label.setAttribute('value', this._tags[tagID]);
|
||||
label.setAttribute('tagID', tagID);
|
||||
label.setAttribute('context', 'tag-menu');
|
||||
label.setAttribute('ondragover', 'nsDragAndDrop.dragOver(event, this.parentNode.parentNode.parentNode.dragObserver)');
|
||||
label.setAttribute('ondragexit', 'nsDragAndDrop.dragExit(event, this.parentNode.parentNode.parentNode.dragObserver)');
|
||||
label.setAttribute('ondragdrop', 'nsDragAndDrop.drop(event, this.parentNode.parentNode.parentNode.dragObserver)');
|
||||
tagsToggleBox.appendChild(label);
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
}
|
||||
|
||||
// Set attributes
|
||||
var labels = tagsToggleBox.getElementsByTagName('label');
|
||||
for (var i=0; i<labels.length; i++){
|
||||
var tagID = labels[i].getAttribute('tagID');
|
||||
|
||||
// Restore selection
|
||||
var labels = tagsToggleDiv.getElementsByTagName('label');
|
||||
for (var i=0; i<labels.length; i++){
|
||||
if (this.selection[labels[i].value]){
|
||||
labels[i].setAttribute('selected', 'true');
|
||||
}
|
||||
if (this.selection[labels[i].value]){
|
||||
labels[i].setAttribute('selected', 'true');
|
||||
}
|
||||
else {
|
||||
labels[i].setAttribute('selected', 'false');
|
||||
}
|
||||
|
||||
this.doCommand();
|
||||
]]>
|
||||
// If not in filter, hide
|
||||
if (this._hasFilter && !this._filter[tagID]) {
|
||||
//Zotero.debug(1);
|
||||
labels[i].setAttribute('hidden', true);
|
||||
}
|
||||
else if (this.filterToScope) {
|
||||
if (this._hasScope && this.scope[tagID]) {
|
||||
//Zotero.debug(2);
|
||||
labels[i].setAttribute('inScope', true);
|
||||
labels[i].setAttribute('hidden', false);
|
||||
}
|
||||
else {
|
||||
//Zotero.debug(3);
|
||||
labels[i].setAttribute('hidden', true);
|
||||
labels[i].setAttribute('inScope', false);
|
||||
}
|
||||
}
|
||||
// Display all
|
||||
else {
|
||||
if (this._hasScope && this.scope[tagID]) {
|
||||
//Zotero.debug(4);
|
||||
labels[i].setAttribute('inScope', true);
|
||||
}
|
||||
else {
|
||||
//Zotero.debug(5);
|
||||
labels[i].setAttribute('inScope', false);
|
||||
}
|
||||
|
||||
labels[i].setAttribute('hidden', false);
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="notify">
|
||||
<parameter name="event"/>
|
||||
<parameter name="type"/>
|
||||
<parameter name="ids"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (type == 'item') {
|
||||
var t = this.id('tags-search').inputField;
|
||||
this.setFilterTags(Zotero.Tags.search(t.value), true);
|
||||
this._dirty = true;
|
||||
this.doCommand();
|
||||
}
|
||||
|
||||
// TODO: optimize for tag notifications?
|
||||
// Would need separate additional Notifier events
|
||||
// to distinguish between item adds and item collection adds
|
||||
// and for tag changes on items separate from item-modify,
|
||||
// plus code to insert new/changed tags at the appropriate spot
|
||||
|
||||
return;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="selectVisible">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tagsToggleDiv = this.id('tags-toggle');
|
||||
var tagsToggleBox = this.id('tags-toggle');
|
||||
|
||||
var labels = tagsToggleDiv.getElementsByTagName('label');
|
||||
var labels = tagsToggleBox.getElementsByTagName('label');
|
||||
for (var i=0; i<labels.length; i++){
|
||||
labels[i].setAttribute('selected', 'true');
|
||||
this.selection[labels[i].value] = true;
|
||||
if (labels[i].getAttribute('hidden') != 'true'
|
||||
&& labels[i].getAttribute('inScope') == 'true') {
|
||||
labels[i].setAttribute('selected', 'true');
|
||||
this.selection[labels[i].value] = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.doCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="clearVisible">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tagsToggleDiv = this.id('tags-toggle');
|
||||
var tagsToggleBox = this.id('tags-toggle');
|
||||
|
||||
var labels = tagsToggleDiv.getElementsByTagName('label');
|
||||
var labels = tagsToggleBox.getElementsByTagName('label');
|
||||
for (var i=0; i<labels.length; i++){
|
||||
labels[i].setAttribute('selected', 'false');
|
||||
this.selection[labels[i].value] = false;
|
||||
}
|
||||
|
||||
this.doCommand();
|
||||
// Bubbles up to command
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="clearAll">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.clearVisible();
|
||||
this.selection = [];
|
||||
this.doCommand();
|
||||
this.clearVisible();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="handleKeyPress">
|
||||
<parameter name="clear"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var t = document.commandDispatcher.focusedElement;
|
||||
var textbox = t.parentNode.parentNode;
|
||||
Zotero.debug(clear);
|
||||
|
||||
var textbox = this.id('tags-search');
|
||||
var t = textbox.inputField;
|
||||
|
||||
textbox.firstChild.hidden = (t.value == "");
|
||||
|
||||
if (clear != undefined){
|
||||
if (clear){
|
||||
t.value = '';
|
||||
this.setTagList(Zotero.Tags.getAll());
|
||||
this.setFilterTags(false);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
|
@ -151,17 +319,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
this.setTagList(Zotero.Tags.search(t.value));
|
||||
|
||||
this.setFilterTags(Zotero.Tags.search(t.value));
|
||||
return true;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="handleTagClick">
|
||||
<parameter name="label"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
// Ignore clicks on tags not in scope
|
||||
if (label.getAttribute('inScope') == 'false') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deselect
|
||||
if (label.getAttribute('selected')=='true'){
|
||||
this.selection[label.value] = false;
|
||||
|
@ -172,11 +345,55 @@
|
|||
this.selection[label.value] = true;
|
||||
label.setAttribute('selected', 'true');
|
||||
}
|
||||
|
||||
this.doCommand();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="rename">
|
||||
<parameter name="tagID"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var oldName = Zotero.Tags.getName(tagID);
|
||||
|
||||
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Components.interfaces.nsIPromptService);
|
||||
|
||||
var newName = { value: oldName };
|
||||
var result = promptService.prompt(window,
|
||||
Zotero.getString('pane.tagSelector.rename.title'),
|
||||
Zotero.getString('pane.tagSelector.rename.message'),
|
||||
newName, '', {});
|
||||
|
||||
if (result && newName.value)
|
||||
{
|
||||
Zotero.Tags.rename(tagID, newName.value);
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="delete">
|
||||
<parameter name="tagID"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Components.interfaces.nsIPromptService);
|
||||
|
||||
var confirmed = promptService.confirm(window,
|
||||
Zotero.getString('pane.tagSelector.delete.title'),
|
||||
Zotero.getString('pane.tagSelector.delete.message'));
|
||||
|
||||
if (confirmed) {
|
||||
Zotero.Tags.remove(tagID);
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="focusTextbox">
|
||||
<body>
|
||||
<![CDATA[
|
||||
|
@ -185,6 +402,51 @@
|
|||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<property name="dragObserver"/>
|
||||
<method name="_dragObserverConstructor">
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.onDragOver = onDragOver;
|
||||
this.onDragExit = onDragExit;
|
||||
this.onDrop = onDrop;
|
||||
this.getSupportedFlavours = getSupportedFlavours;
|
||||
|
||||
|
||||
function onDragOver(event, flavour, session) {
|
||||
event.target.setAttribute('draggedOver', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onDragExit(event, session) {
|
||||
event.target.setAttribute('draggedOver', false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function onDrop(event, dropData, session) {
|
||||
event.target.setAttribute('draggedOver', false);
|
||||
|
||||
var ids = dropData.data.split(',');
|
||||
var items = Zotero.Items.get(ids);
|
||||
var unlock = Zotero.Notifier.begin(true);
|
||||
for each(var item in items) {
|
||||
item.addTagByID(event.target.getAttribute('tagID'));
|
||||
}
|
||||
Zotero.Notifier.commit(unlock);
|
||||
}
|
||||
|
||||
|
||||
function getSupportedFlavours() {
|
||||
var flavours = new FlavourSet();
|
||||
flavours.appendFlavour("zotero/item");
|
||||
return flavours;
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="id">
|
||||
<parameter name="id"/>
|
||||
<body>
|
||||
|
@ -197,14 +459,22 @@
|
|||
|
||||
<content>
|
||||
<xul:groupbox flex="1">
|
||||
<xul:menupopup id="tag-menu">
|
||||
<xul:menuitem label="&zotero.tagSelector.renameTag;" class="menuitem-non-iconic" oncommand="this.parentNode.parentNode.parentNode.rename(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
|
||||
<xul:menuitem label="&zotero.tagSelector.deleteTag;" class="menuitem-non-iconic" oncommand="this.parentNode.parentNode.parentNode.delete(document.popupNode.getAttribute('tagID')); event.stopPropagation()"/>
|
||||
</xul:menupopup>
|
||||
|
||||
<xul:vbox id="tags-toggle" flex="1"/>
|
||||
<xul:checkbox label="&zotero.tagSelector.displayAll;"
|
||||
oncommand="this.parentNode.parentNode.filterToScope = !this.checked; event.stopPropagation();">
|
||||
</xul:checkbox>
|
||||
<xul:hbox>
|
||||
<xul:label value="&zotero.tagSelector.filter;"/>
|
||||
<xul:textbox id="tags-search" flex="1" type="timed" timeout="250" dir="reverse"
|
||||
oncommand="this.parentNode.parentNode.parentNode.handleKeyPress()"
|
||||
onkeypress="this.parentNode.parentNode.parentNode.handleKeyPress(event.keyCode == event.DOM_VK_ESCAPE)">
|
||||
oncommand="this.parentNode.parentNode.parentNode.handleKeyPress(); event.stopPropagation()"
|
||||
onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) { this.parentNode.parentNode.parentNode.handleKeyPress(true); }">
|
||||
<xul:toolbarbutton id="search-cancel"
|
||||
oncommand="this.parentNode.focus(); this.parentNode.value='';" hidden="true"/>
|
||||
oncommand="this.parentNode.focus(); this.parentNode.parentNode.parentNode.parentNode.handleKeyPress(true)" hidden="true"/>
|
||||
</xul:textbox>
|
||||
</xul:hbox>
|
||||
<xul:hbox>
|
||||
|
@ -213,7 +483,7 @@
|
|||
<xul:toolbarbutton label="&zotero.tagSelector.clearVisible;" class="zotero-clicky"
|
||||
oncommand="this.parentNode.parentNode.parentNode.clearVisible()"/>
|
||||
<xul:toolbarbutton label="&zotero.tagSelector.clearAll;" class="zotero-clicky"
|
||||
oncommand="this.parentNode.parentNode.parentNode.clearAll()"/>
|
||||
oncommand="this.parentNode.parentNode.parentNode.clearAll();"/>
|
||||
</xul:hbox>
|
||||
</xul:groupbox>
|
||||
</content>
|
||||
|
|
|
@ -148,6 +148,9 @@ var ZoteroPane = new function()
|
|||
*/
|
||||
function onUnload()
|
||||
{
|
||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
||||
tagSelector.unregister();
|
||||
|
||||
collectionsView.unregister();
|
||||
if(itemsView)
|
||||
itemsView.unregister();
|
||||
|
@ -310,14 +313,18 @@ var ZoteroPane = new function()
|
|||
function toggleTagSelector(){
|
||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
||||
var collapsed = tagSelector.getAttribute('collapsed')=='true';
|
||||
// If hiding, clear selection
|
||||
if (!collapsed){
|
||||
tagSelector.init();
|
||||
}
|
||||
tagSelector.setAttribute('collapsed', !collapsed);
|
||||
// If showing, set scope to items in current view
|
||||
// and focus filter textbox
|
||||
if (collapsed) {
|
||||
tagSelector.init();
|
||||
_setTagScope();
|
||||
tagSelector.focusTextbox();
|
||||
}
|
||||
// If hiding, clear selection
|
||||
else {
|
||||
tagSelector.uninit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -327,18 +334,25 @@ var ZoteroPane = new function()
|
|||
}
|
||||
|
||||
|
||||
/*
|
||||
* Sets the tag filter on the items view
|
||||
*/
|
||||
function updateTagFilter(){
|
||||
if (itemsView)
|
||||
{
|
||||
itemsView.unregister();
|
||||
}
|
||||
|
||||
if (collectionsView){
|
||||
var itemgroup = collectionsView._getItemAtRow(collectionsView.selection.currentIndex);
|
||||
itemgroup.setTags(getTagSelection());
|
||||
|
||||
itemsView = new Zotero.ItemTreeView(itemgroup);
|
||||
document.getElementById('zotero-items-tree').view = itemsView;
|
||||
itemsView.setFilter('tags', getTagSelection());
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Set the tags scope to the items in the current view
|
||||
*
|
||||
* Passed to the items tree to trigger on changes
|
||||
*/
|
||||
function _setTagScope() {
|
||||
var itemgroup = collectionsView._getItemAtRow(collectionsView.selection.currentIndex);
|
||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
||||
if (tagSelector.getAttribute('collapsed') == 'false') {
|
||||
Zotero.debug('Updating tag selector with current tags');
|
||||
tagSelector.scope = itemgroup.getChildTags();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,6 +371,7 @@ var ZoteroPane = new function()
|
|||
itemgroup.setTags(getTagSelection());
|
||||
|
||||
itemsView = new Zotero.ItemTreeView(itemgroup);
|
||||
itemsView.addCallback(_setTagScope);
|
||||
document.getElementById('zotero-items-tree').view = itemsView;
|
||||
itemsView.selection.clearSelection();
|
||||
}
|
||||
|
@ -632,7 +647,7 @@ var ZoteroPane = new function()
|
|||
if(itemsView)
|
||||
{
|
||||
var searchVal = document.getElementById('zotero-tb-search').value;
|
||||
itemsView.searchText(searchVal);
|
||||
itemsView.setFilter('search', searchVal);
|
||||
|
||||
document.getElementById('zotero-tb-search-cancel').hidden = searchVal == "";
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ function onSearch()
|
|||
if(itemsView)
|
||||
{
|
||||
var searchVal = document.getElementById('zotero-tb-search').value;
|
||||
itemsView.searchText(searchVal);
|
||||
itemsView.setFilter('search', searchVal);
|
||||
|
||||
document.getElementById('zotero-tb-search-cancel').hidden = searchVal == "";
|
||||
}
|
||||
|
|
|
@ -678,6 +678,18 @@ Zotero.ItemGroup.prototype.getName = function()
|
|||
|
||||
Zotero.ItemGroup.prototype.getChildItems = function()
|
||||
{
|
||||
var s = this.getSearchObject();
|
||||
var ids = s.search();
|
||||
return Zotero.Items.get(ids);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Returns the search object for the currently display
|
||||
*
|
||||
* This accounts for the collection, saved search, quicksearch, tags, etc.
|
||||
*/
|
||||
Zotero.ItemGroup.prototype.getSearchObject = function() {
|
||||
var s = new Zotero.Search();
|
||||
|
||||
if (this.searchText){
|
||||
|
@ -716,10 +728,19 @@ Zotero.ItemGroup.prototype.getChildItems = function()
|
|||
}
|
||||
}
|
||||
|
||||
var ids = s.search();
|
||||
return Zotero.Items.get(ids);
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Returns all the tags used by items in the current view
|
||||
*/
|
||||
Zotero.ItemGroup.prototype.getChildTags = function() {
|
||||
var s = this.getSearchObject();
|
||||
return Zotero.Tags.getAllWithinSearch(s);
|
||||
}
|
||||
|
||||
|
||||
Zotero.ItemGroup.prototype.setSearch = function(searchText)
|
||||
{
|
||||
this.searchText = searchText;
|
||||
|
|
|
@ -1528,11 +1528,11 @@ Zotero.Item.prototype.getBestSnapshot = function(){
|
|||
//
|
||||
Zotero.Item.prototype.addTag = function(tag){
|
||||
if (!this.getID()){
|
||||
this.save();
|
||||
throw ('Cannot add tag to unsaved item in Item.addTag()');
|
||||
}
|
||||
|
||||
if (!tag){
|
||||
Zotero.debug('Not saving empty tag', 2);
|
||||
Zotero.debug('Not saving empty tag in Item.addTag()', 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1541,11 +1541,41 @@ Zotero.Item.prototype.addTag = function(tag){
|
|||
if (!tagID){
|
||||
var tagID = Zotero.Tags.add(tag);
|
||||
}
|
||||
try {
|
||||
var result = this.addTagByID(tagID);
|
||||
Zotero.DB.commitTransaction();
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.DB.rollbackTransaction();
|
||||
throw (e);
|
||||
}
|
||||
|
||||
return result ? tagID : false;
|
||||
}
|
||||
|
||||
|
||||
Zotero.Item.prototype.addTagByID = function(tagID) {
|
||||
if (!this.getID()) {
|
||||
throw ('Cannot add tag to unsaved item in Item.addTagByID()');
|
||||
}
|
||||
|
||||
if (!tagID) {
|
||||
Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
var sql = "SELECT COUNT(*) FROM tags WHERE tagID = ?";
|
||||
var count = !!Zotero.DB.valueQuery(sql, tagID);
|
||||
|
||||
if (!count) {
|
||||
throw ('Cannot add invalid tag id ' + tagID + ' in Item.addTagByID()');
|
||||
}
|
||||
|
||||
Zotero.DB.beginTransaction();
|
||||
|
||||
// If INSERT OR IGNORE gave us affected rows, we wouldn't need this...
|
||||
var sql = "SELECT COUNT(*) FROM itemTags WHERE itemID=? AND tagID=?";
|
||||
var exists = Zotero.DB.valueQuery(sql, [this.getID(), tagID]);
|
||||
if (exists){
|
||||
if (this.hasTag(tagID)) {
|
||||
Zotero.debug('Item ' + this.getID() + ' already has tag ' + tagID + ' in Item.addTagByID()');
|
||||
Zotero.DB.commitTransaction();
|
||||
return false;
|
||||
}
|
||||
|
@ -1554,12 +1584,14 @@ Zotero.Item.prototype.addTag = function(tag){
|
|||
Zotero.DB.query(sql, [this.getID(), tagID]);
|
||||
|
||||
Zotero.DB.commitTransaction();
|
||||
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
||||
|
||||
if (!Zotero.DB.transactionInProgress()){
|
||||
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
||||
}
|
||||
|
||||
return tagID;
|
||||
return true;
|
||||
}
|
||||
|
||||
Zotero.Item.prototype.hasTag = function(tagID) {
|
||||
var sql = "SELECT COUNT(*) FROM itemTags WHERE itemID=? AND tagID=?";
|
||||
return !!Zotero.DB.valueQuery(sql, [this.getID(), tagID]);
|
||||
}
|
||||
|
||||
Zotero.Item.prototype.getTags = function(){
|
||||
|
@ -1609,10 +1641,7 @@ Zotero.Item.prototype.removeTag = function(tagID){
|
|||
Zotero.DB.query(sql, [this.getID(), tagID]);
|
||||
Zotero.Tags.purge();
|
||||
Zotero.DB.commitTransaction();
|
||||
|
||||
if (!Zotero.DB.transactionInProgress()){
|
||||
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
||||
}
|
||||
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
||||
}
|
||||
|
||||
|
||||
|
@ -3010,8 +3039,12 @@ Zotero.Tags = new function(){
|
|||
this.getName = getName;
|
||||
this.getID = getID;
|
||||
this.getAll = getAll;
|
||||
this.getAllWithinSearch = getAllWithinSearch;
|
||||
this.getTagItems = getTagItems;
|
||||
this.search = search;
|
||||
this.add = add;
|
||||
this.rename = rename;
|
||||
this.remove = remove;
|
||||
this.purge = purge;
|
||||
|
||||
/*
|
||||
|
@ -3067,6 +3100,30 @@ Zotero.Tags = new function(){
|
|||
}
|
||||
|
||||
|
||||
/*
|
||||
* Get all tags within the items of a Zotero.Search object
|
||||
*/
|
||||
function getAllWithinSearch(search) {
|
||||
var searchSQL = search.getSQL();
|
||||
var searchParams = search.getSQLParams();
|
||||
|
||||
var sql = "SELECT DISTINCT tagID, tag FROM itemTags NATURAL JOIN tags "
|
||||
+ "WHERE itemID IN (" + searchSQL + ") ORDER BY tag COLLATE NOCASE";
|
||||
var tags = Zotero.DB.query(sql, searchParams);
|
||||
var indexed = {};
|
||||
for (var i in tags){
|
||||
indexed[tags[i]['tagID']] = tags[i]['tag'];
|
||||
}
|
||||
return indexed;
|
||||
}
|
||||
|
||||
|
||||
function getTagItems(tagID) {
|
||||
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
|
||||
return Zotero.DB.columnQuery(sql, tagID);
|
||||
}
|
||||
|
||||
|
||||
function search(str){
|
||||
var sql = 'SELECT tagID, tag FROM tags WHERE tag LIKE ? '
|
||||
+ 'ORDER BY tag COLLATE NOCASE';
|
||||
|
@ -3094,10 +3151,50 @@ Zotero.Tags = new function(){
|
|||
Zotero.DB.query(sql, [{int: rnd}, {string: tag}]);
|
||||
|
||||
Zotero.DB.commitTransaction();
|
||||
Zotero.Notifier.trigger('add', 'tag', rnd);
|
||||
return rnd;
|
||||
}
|
||||
|
||||
|
||||
function rename(tagID, tag) {
|
||||
Zotero.debug('Renaming tag', 4);
|
||||
|
||||
Zotero.DB.beginTransaction();
|
||||
var sql = "UPDATE tags SET tag=? WHERE tagID=?";
|
||||
Zotero.DB.query(sql, [{string: tag}, {int: tagID}]);
|
||||
|
||||
var itemIDs = this.getTagItems(tagID);
|
||||
|
||||
delete _tags[_tagsByID[tagID]];
|
||||
delete _tagsByID[tagID];
|
||||
|
||||
Zotero.DB.commitTransaction();
|
||||
|
||||
Zotero.Notifier.trigger('modify', 'item', itemIDs);
|
||||
Zotero.Notifier.trigger('modify', 'tag', tagID);
|
||||
}
|
||||
|
||||
|
||||
function remove(tagID) {
|
||||
Zotero.DB.beginTransaction();
|
||||
|
||||
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
|
||||
var items = Zotero.DB.columnQuery(sql, tagID);
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sql = "DELETE FROM itemTags WHERE tagID=?";
|
||||
Zotero.DB.query(sql, tagID);
|
||||
Zotero.Notifier.trigger('modify', 'item', items)
|
||||
|
||||
this.purge();
|
||||
Zotero.DB.commitTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Delete obsolete tags from database and clear internal array entries
|
||||
*
|
||||
|
@ -3123,6 +3220,8 @@ Zotero.Tags = new function(){
|
|||
+ '(SELECT tagID FROM itemTags);';
|
||||
var result = Zotero.DB.query(sql);
|
||||
|
||||
Zotero.Notifier.trigger('delete', 'tag', toDelete);
|
||||
|
||||
return toDelete;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,12 +36,27 @@ Zotero.ItemTreeView = function(itemGroup, sourcesOnly)
|
|||
this._itemGroup = itemGroup;
|
||||
this._sourcesOnly = sourcesOnly;
|
||||
|
||||
this._callbacks = [];
|
||||
|
||||
this._treebox = null;
|
||||
this.refresh();
|
||||
|
||||
this._unregisterID = Zotero.Notifier.registerObserver(this, 'item');
|
||||
}
|
||||
|
||||
|
||||
Zotero.ItemTreeView.prototype.addCallback = function(callback) {
|
||||
this._callbacks.push(callback);
|
||||
}
|
||||
|
||||
|
||||
Zotero.ItemTreeView.prototype._runCallbacks = function() {
|
||||
for each(var cb in this._callbacks) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Called by the tree itself
|
||||
*/
|
||||
|
@ -59,8 +74,12 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox)
|
|||
{
|
||||
this.sort();
|
||||
}
|
||||
|
||||
//Zotero.debug('Running callbacks in itemTreeView.setTree()', 4);
|
||||
this._runCallbacks();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Reload the rows from the data access methods
|
||||
* (doesn't call the tree.invalidate methods, etc.)
|
||||
|
@ -629,15 +648,24 @@ Zotero.ItemTreeView.prototype.deleteSelection = function(eraseChildren, force)
|
|||
this._treebox.endUpdateBatch();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Set the search filter on the view
|
||||
* Set the tags filter on the view
|
||||
*/
|
||||
Zotero.ItemTreeView.prototype.searchText = function(search)
|
||||
{
|
||||
Zotero.ItemTreeView.prototype.setFilter = function(type, data) {
|
||||
this.selection.selectEventsSuppressed = true;
|
||||
var savedSelection = this.saveSelection();
|
||||
|
||||
this._itemGroup.setSearch(search);
|
||||
switch (type) {
|
||||
case 'search':
|
||||
this._itemGroup.setSearch(data);
|
||||
break;
|
||||
case 'tags':
|
||||
this._itemGroup.setTags(data);
|
||||
break;
|
||||
default:
|
||||
throw ('Invalid filter type in setFilter');
|
||||
}
|
||||
var oldCount = this.rowCount;
|
||||
this.refresh();
|
||||
this._treebox.rowCountChanged(0,this.rowCount-oldCount);
|
||||
|
@ -647,8 +675,11 @@ Zotero.ItemTreeView.prototype.searchText = function(search)
|
|||
this.rememberSelection(savedSelection);
|
||||
this.selection.selectEventsSuppressed = false;
|
||||
this._treebox.invalidate();
|
||||
//Zotero.debug('Running callbacks in itemTreeView.setFilter()', 4);
|
||||
this._runCallbacks();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Called by various view functions to show a row
|
||||
*
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
Zotero.Notifier = new function(){
|
||||
var _observers = new Zotero.Hash();
|
||||
var _disabled = false;
|
||||
var _types = ['collection', 'search', 'item'];
|
||||
var _types = ['collection', 'search', 'item', 'tag'];
|
||||
var _inTransaction;
|
||||
var _locked = false;
|
||||
var _queue = [];
|
||||
|
@ -69,7 +69,7 @@ Zotero.Notifier = new function(){
|
|||
while (_observers.get(hash));
|
||||
|
||||
Zotero.debug('Registering observer for '
|
||||
+ (types ? '[' + types.join() + ']' : ' all types')
|
||||
+ (types ? '[' + types.join() + ']' : 'all types')
|
||||
+ ' in notifier with hash ' + hash + "'", 4);
|
||||
_observers.set(hash, {ref: ref, types: types});
|
||||
return hash;
|
||||
|
@ -114,9 +114,18 @@ Zotero.Notifier = new function(){
|
|||
* type - 'collection', 'search', 'item'
|
||||
* ids - single id or array of ids
|
||||
*
|
||||
* c = collection, s = search, i = item
|
||||
* c = collection, s = search, i = item, t = tag
|
||||
*
|
||||
* New events and types should be added to the order arrays in commit()
|
||||
*
|
||||
* Notes:
|
||||
*
|
||||
* - add-item is currently used for both item creation and adding an
|
||||
* existing item to a collection
|
||||
*
|
||||
* - If event queuing is on, events will not fire until commit() is called
|
||||
* unless _force_ is true.
|
||||
*
|
||||
* - New events and types should be added to the order arrays in commit()
|
||||
**/
|
||||
function trigger(event, type, ids, force){
|
||||
if (_disabled){
|
||||
|
@ -206,7 +215,7 @@ Zotero.Notifier = new function(){
|
|||
function sorter(a, b) {
|
||||
return order.indexOf(a) - order.indexOf(b);
|
||||
}
|
||||
var order = ['collection', 'search', 'items'];
|
||||
var order = ['collection', 'search', 'items', 'tags'];
|
||||
_queue.sort();
|
||||
|
||||
var order = ['add', 'modify', 'remove', 'move', 'delete'];
|
||||
|
|
|
@ -57,6 +57,7 @@ var Zotero = new function(){
|
|||
this.inArray = inArray;
|
||||
this.arraySearch = arraySearch;
|
||||
this.arrayToHash = arrayToHash;
|
||||
this.hasValues = hasValues;
|
||||
this.randomString = randomString;
|
||||
this.getRandomID = getRandomID;
|
||||
this.moveToUnique = moveToUnique;
|
||||
|
@ -470,6 +471,18 @@ var Zotero = new function(){
|
|||
}
|
||||
|
||||
|
||||
/*
|
||||
* Returns true if an object (or associative array) has at least one value
|
||||
*/
|
||||
function hasValues(obj) {
|
||||
for (var i in obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a random string of length 'len' (defaults to 8)
|
||||
**/
|
||||
|
|
|
@ -56,9 +56,12 @@
|
|||
<!ENTITY zotero.toolbar.attachment.snapshot "Take Snapshot of Current Page">
|
||||
|
||||
<!ENTITY zotero.tagSelector.filter "Filter:">
|
||||
<!ENTITY zotero.tagSelector.displayAll "Display All">
|
||||
<!ENTITY zotero.tagSelector.selectVisible "Select Visible">
|
||||
<!ENTITY zotero.tagSelector.clearVisible "Clear Visible">
|
||||
<!ENTITY zotero.tagSelector.clearAll "Clear All">
|
||||
<!ENTITY zotero.tagSelector.renameTag "Rename Tag...">
|
||||
<!ENTITY zotero.tagSelector.deleteTag "Delete Tag...">
|
||||
|
||||
<!ENTITY zotero.selectitems.title "Select Items">
|
||||
<!ENTITY zotero.selectitems.intro.label "Select which items you'd like to add to your library">
|
||||
|
|
|
@ -17,6 +17,11 @@ pane.collections.menu.createBib.savedSearch = Create Bibliography From Saved Se
|
|||
pane.collections.menu.generateReport.collection = Generate Report from Collection...
|
||||
pane.collections.menu.generateReport.savedSearch = Generate Report from Saved Search...
|
||||
|
||||
pane.tagSelector.rename.title = Please enter a new name for this tag.
|
||||
pane.tagSelector.rename.message = The tag will be changed in all associated items.
|
||||
pane.tagSelector.delete.title = Are you sure you want to delete this tag?
|
||||
pane.tagSelector.delete.message = The tag will be removed from all items.
|
||||
|
||||
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?
|
||||
pane.items.delete.title = Delete
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
@namespace html url("http://www.w3.org/1999/xhtml");
|
||||
|
||||
|
||||
/* Tag selector */
|
||||
|
||||
/* Don't focus filter textbox if pane isn't open */
|
||||
zoterotagselector[collapsed=true] tags-search
|
||||
{
|
||||
|
@ -19,22 +13,45 @@ groupbox
|
|||
#tags-toggle
|
||||
{
|
||||
overflow: auto;
|
||||
margin-bottom: 10px;
|
||||
display: block; /* allow labels to wrap instead of all being in one line */
|
||||
}
|
||||
|
||||
checkbox
|
||||
{
|
||||
margin: .75em 0 .4em;
|
||||
}
|
||||
|
||||
label
|
||||
{
|
||||
margin-right: 5px;
|
||||
padding: 2px 4px;
|
||||
margin-right: .2em;
|
||||
padding: .15em .25em;
|
||||
-moz-user-focus: ignore;
|
||||
}
|
||||
|
||||
label[selected=true]
|
||||
label[selected="true"]
|
||||
{
|
||||
background: #a9c6f0 !important;
|
||||
}
|
||||
|
||||
/* Visible out-of-scope tags should be grey */
|
||||
label[inScope="false"]
|
||||
{
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
/* Don't display clicky effect to out-of-scope icons */
|
||||
label.zotero-clicky[inScope="false"]:hover,
|
||||
label.zotero-clicky[inScope="false"]:active
|
||||
{
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
label[draggedOver="true"]
|
||||
{
|
||||
color: white !important;
|
||||
background: #666;
|
||||
}
|
||||
|
||||
hbox
|
||||
{
|
||||
-moz-box-align: baseline;
|
||||
|
@ -49,6 +66,7 @@ hbox
|
|||
list-style-image: url('chrome://zotero/skin/search-cancel.png');
|
||||
}
|
||||
|
||||
/* Bottom buttons */
|
||||
toolbarbutton.zotero-clicky
|
||||
{
|
||||
margin:3px 5px;
|
||||
|
|
Loading…
Reference in New Issue
Block a user