fx-compat: Implement tagsBox element
This commit is contained in:
parent
7e55ea59bb
commit
2e5388af5b
16 changed files with 1099 additions and 176 deletions
|
@ -27,7 +27,6 @@
|
||||||
// related with `require` not reusing the context
|
// related with `require` not reusing the context
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOM = require('react-dom');
|
var ReactDOM = require('react-dom');
|
||||||
var TagsBoxContainer = require('containers/tagsBoxContainer').default;
|
|
||||||
var NotesList = require('components/itemPane/notesList').default;
|
var NotesList = require('components/itemPane/notesList').default;
|
||||||
|
|
||||||
var ZoteroContextPane = new function () {
|
var ZoteroContextPane = new function () {
|
||||||
|
@ -852,25 +851,11 @@ var ZoteroContextPane = new function () {
|
||||||
panelInfo.append(itemBox);
|
panelInfo.append(itemBox);
|
||||||
// Tags panel
|
// Tags panel
|
||||||
var panelTags = document.createXULElement('tabpanel');
|
var panelTags = document.createXULElement('tabpanel');
|
||||||
panelTags.setAttribute('orient', 'vertical');
|
var tagsBox = new (customElements.get('tags-box'));
|
||||||
panelTags.setAttribute('context', 'tags-context-menu');
|
tagsBox.setAttribute('flex', '1');
|
||||||
panelTags.className = 'tags-pane';
|
tagsBox.className = 'zotero-editpane-tags';
|
||||||
panelTags.style.display = 'flex';
|
panelTags.append(tagsBox);
|
||||||
var div = document.createElementNS(HTML_NS, 'div');
|
|
||||||
div.className = 'tags-box-container';
|
|
||||||
div.style.display = 'flex';
|
|
||||||
div.style.flexGrow = '1';
|
|
||||||
panelTags.append(div);
|
|
||||||
var tagsBoxRef = React.createRef();
|
|
||||||
ReactDOM.render(
|
|
||||||
<TagsBoxContainer
|
|
||||||
key={'tagsBox-' + parentItem.id}
|
|
||||||
item={parentItem}
|
|
||||||
editable={!readOnly}
|
|
||||||
ref={tagsBoxRef}
|
|
||||||
/>,
|
|
||||||
div
|
|
||||||
);
|
|
||||||
// Related panel
|
// Related panel
|
||||||
var panelRelated = document.createXULElement('tabpanel');
|
var panelRelated = document.createXULElement('tabpanel');
|
||||||
var relatedBox = new (customElements.get('related-box'));
|
var relatedBox = new (customElements.get('related-box'));
|
||||||
|
@ -890,6 +875,9 @@ var ZoteroContextPane = new function () {
|
||||||
itemBox.mode = readOnly ? 'view' : 'edit';
|
itemBox.mode = readOnly ? 'view' : 'edit';
|
||||||
itemBox.item = parentItem;
|
itemBox.item = parentItem;
|
||||||
|
|
||||||
|
tagsBox.mode = readOnly ? 'view' : 'edit';
|
||||||
|
tagsBox.item = parentItem;
|
||||||
|
|
||||||
relatedBox.mode = readOnly ? 'view' : 'edit';
|
relatedBox.mode = readOnly ? 'view' : 'edit';
|
||||||
relatedBox.item = parentItem;
|
relatedBox.item = parentItem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -328,8 +328,6 @@
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let TagsBoxContainer = require('containers/tagsBoxContainer').default;
|
|
||||||
|
|
||||||
class LinksBox extends XULElement {
|
class LinksBox extends XULElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -357,7 +355,7 @@
|
||||||
<related-box id="related"/>
|
<related-box id="related"/>
|
||||||
</menupopup>
|
</menupopup>
|
||||||
<menupopup id="tags-popup" width="300" ignorekeys="true">
|
<menupopup id="tags-popup" width="300" ignorekeys="true">
|
||||||
<div style="display: flex" id="tags-box-container" xmlns="http://www.w3.org/1999/xhtml"/>
|
<tags-box id="tags"/>
|
||||||
</menupopup>
|
</menupopup>
|
||||||
</popupset>
|
</popupset>
|
||||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||||
|
@ -401,6 +399,7 @@
|
||||||
set item(val) {
|
set item(val) {
|
||||||
this._item = val;
|
this._item = val;
|
||||||
this._id('related').item = this._item;
|
this._id('related').item = this._item;
|
||||||
|
this._id('tags').item = this._item;
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
||||||
|
@ -417,6 +416,7 @@
|
||||||
set mode(val) {
|
set mode(val) {
|
||||||
this._mode = val;
|
this._mode = val;
|
||||||
this._id('related').mode = val;
|
this._id('related').mode = val;
|
||||||
|
this._id('tags').mode = val;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,22 +434,6 @@
|
||||||
this._updateParentRow();
|
this._updateParentRow();
|
||||||
this._updateTagsSummary();
|
this._updateTagsSummary();
|
||||||
this._updateRelatedSummary();
|
this._updateRelatedSummary();
|
||||||
|
|
||||||
// TODO: Update tagsBox container state via tagsBoxRef and imperative handle, instead of recreating it
|
|
||||||
let container = this._id('tags-box-container');
|
|
||||||
ReactDOM.unmountComponentAtNode(container);
|
|
||||||
if (this._item) {
|
|
||||||
var tagsBoxRef = React.createRef();
|
|
||||||
ReactDOM.render(
|
|
||||||
<TagsBoxContainer
|
|
||||||
key={'tagsBox-' + this._item.id}
|
|
||||||
item={this._item}
|
|
||||||
editable={this._mode == 'edit'}
|
|
||||||
ref={tagsBoxRef}
|
|
||||||
/>,
|
|
||||||
container
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateParentRow() {
|
_updateParentRow() {
|
||||||
|
@ -546,7 +530,7 @@
|
||||||
// If editable and no existing tags, open new empty row
|
// If editable and no existing tags, open new empty row
|
||||||
if (this._mode == 'edit' && !this._item.getTags().length) {
|
if (this._mode == 'edit' && !this._item.getTags().length) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._id('tags-popup').querySelector('.tags-box-header button').click();
|
this._id('tags').addNew();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,10 +33,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extends AutocompleteInput to fix document.activeElement checks that
|
* Extend AutocompleteInput to work around issues with shadow DOM
|
||||||
* don't work in a shadow DOM context.
|
|
||||||
*/
|
*/
|
||||||
class ShadowAutocompleteInput extends customElements.get('autocomplete-input') {
|
class ShadowAutocompleteInput extends customElements.get('autocomplete-input') {
|
||||||
|
// Fix document.activeElement checks that don't work in a shadow DOM context
|
||||||
get focused() {
|
get focused() {
|
||||||
// document.activeElement by itself doesn't traverse shadow DOMs; see
|
// document.activeElement by itself doesn't traverse shadow DOMs; see
|
||||||
// https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
|
// https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
|
||||||
|
@ -53,6 +53,21 @@
|
||||||
|
|
||||||
return this === activeElement(document);
|
return this === activeElement(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look for `autocompletepopup` popup id inside the current shadow root.
|
||||||
|
// `autocomplete-input` itself can create an autocomplete popup inside the top DOM,
|
||||||
|
// but it appears behind the tagsBox popup, because the z order of popups messes up
|
||||||
|
get popup() {
|
||||||
|
let rootNode = this.getRootNode();
|
||||||
|
if (rootNode && rootNode instanceof ShadowRoot) {
|
||||||
|
let id = this.getAttribute('autocompletepopup');
|
||||||
|
let popup = rootNode.getElementById(id);
|
||||||
|
if (popup) {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.popup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("shadow-autocomplete-input", ShadowAutocompleteInput, {
|
customElements.define("shadow-autocomplete-input", ShadowAutocompleteInput, {
|
||||||
|
|
979
chrome/content/zotero/elements/tagsBox.js
Normal file
979
chrome/content/zotero/elements/tagsBox.js
Normal file
|
@ -0,0 +1,979 @@
|
||||||
|
/*
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2022 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";
|
||||||
|
|
||||||
|
{
|
||||||
|
class TagsBox extends XULElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.count = 0;
|
||||||
|
this.clickHandler = null;
|
||||||
|
|
||||||
|
this._lastTabIndex = false;
|
||||||
|
this._tabDirection = null;
|
||||||
|
this._tagColors = [];
|
||||||
|
this._notifierID = null;
|
||||||
|
this._mode = 'view';
|
||||||
|
this._item = null;
|
||||||
|
|
||||||
|
this.content = MozXULElement.parseXULToFragment(`
|
||||||
|
<box flex="1" tooltip="html-tooltip" style="display: flex" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
|
<div id="tags-box" style="flex-grow: 1" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<div class="tags-box-header">
|
||||||
|
<label id="count"/>
|
||||||
|
<button id="add">&zotero.item.add;</button>
|
||||||
|
</div>
|
||||||
|
<ul id="rows" class="tags-box-list"/>
|
||||||
|
</div>
|
||||||
|
</box>
|
||||||
|
<popupset xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
|
<tooltip id="html-tooltip" page="true"/>
|
||||||
|
<menupopup id="tags-context-menu">
|
||||||
|
<menuitem id="remove-all-item-tags" label="&zotero.item.tags.removeAll;"/>
|
||||||
|
</menupopup>
|
||||||
|
<!-- Note: autocomplete-input can create this panel by itself, but it appears
|
||||||
|
in the top DOM and is behind the tags box popup -->
|
||||||
|
<panel
|
||||||
|
is="autocomplete-richlistbox-popup"
|
||||||
|
type="autocomplete-richlistbox"
|
||||||
|
id="PopupAutoComplete"
|
||||||
|
role="group"
|
||||||
|
noautofocus="true"
|
||||||
|
hidden="true"
|
||||||
|
overflowpadding="4"
|
||||||
|
norolluponanchor="true"
|
||||||
|
nomaxresults="true"
|
||||||
|
/>
|
||||||
|
</popupset>
|
||||||
|
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this._destroyed = false;
|
||||||
|
window.addEventListener("unload", this.destroy);
|
||||||
|
|
||||||
|
let shadow = this.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
let s1 = document.createElement("link");
|
||||||
|
s1.rel = "stylesheet";
|
||||||
|
s1.href = "chrome://zotero-platform/content/tagsBox.css";
|
||||||
|
shadow.append(s1);
|
||||||
|
|
||||||
|
let s2 = document.createElement("link");
|
||||||
|
s2.rel = "stylesheet";
|
||||||
|
s2.href = "chrome://global/skin/";
|
||||||
|
shadow.append(s2);
|
||||||
|
|
||||||
|
let s3 = document.createElement("link");
|
||||||
|
s3.rel = "stylesheet";
|
||||||
|
s3.href = "chrome://zotero/skin/overlay.css";
|
||||||
|
shadow.append(s3);
|
||||||
|
|
||||||
|
let content = document.importNode(this.content, true);
|
||||||
|
shadow.append(content);
|
||||||
|
|
||||||
|
this._id('add').addEventListener('click', this._handleAddButtonClick);
|
||||||
|
this._id('add').addEventListener('keydown', this._handleAddButtonKeyDown);
|
||||||
|
this._id('tags-box').addEventListener('click', (event) => {
|
||||||
|
if (event.target.id == 'tags-box') {
|
||||||
|
this.blurOpenField();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let removeAllItemTags = this._id('remove-all-item-tags');
|
||||||
|
this._id('remove-all-item-tags').addEventListener('command', this.removeAll);
|
||||||
|
this._id('tags-box').addEventListener('contextmenu', (event) => {
|
||||||
|
removeAllItemTags.disabled = !this.count;
|
||||||
|
this._id('tags-context-menu').openPopupAtScreen(event.screenX, event.screenY, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._notifierID = Zotero.Notifier.registerObserver(this, ['item-tag', 'setting'], 'tagsBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this._destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.removeEventListener("unload", this.destroy);
|
||||||
|
this._destroyed = true;
|
||||||
|
|
||||||
|
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.replaceChildren();
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this._mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
set mode(val) {
|
||||||
|
this.clickable = false;
|
||||||
|
this.editable = false;
|
||||||
|
|
||||||
|
switch (val) {
|
||||||
|
case 'view':
|
||||||
|
case 'merge':
|
||||||
|
case 'mergeedit':
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
this.clickable = true;
|
||||||
|
this.editable = true;
|
||||||
|
this.clickHandler = this.showEditor;
|
||||||
|
this.blurHandler = this.hideEditor;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid mode ${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._mode = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
get item() {
|
||||||
|
return this._item;
|
||||||
|
}
|
||||||
|
|
||||||
|
set item(val) {
|
||||||
|
// Don't reload if item hasn't changed
|
||||||
|
if (this._item == val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._item = val;
|
||||||
|
this._lastTabIndex = false;
|
||||||
|
this.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(event, type, ids, extraData) {
|
||||||
|
if (type == 'setting') {
|
||||||
|
if (ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
|
||||||
|
this.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == 'item-tag') {
|
||||||
|
let itemID, tagID;
|
||||||
|
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
[itemID, tagID] = ids[i].split('-').map(x => parseInt(x));
|
||||||
|
if (!this.item || itemID != this.item.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let data = extraData[ids[i]];
|
||||||
|
let tagName = data.tag;
|
||||||
|
let tagType = data.type;
|
||||||
|
|
||||||
|
if (event == 'add') {
|
||||||
|
var newTabIndex = this.add(tagName, tagType);
|
||||||
|
if (newTabIndex == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._tabDirection == -1) {
|
||||||
|
if (this._lastTabIndex > newTabIndex) {
|
||||||
|
this._lastTabIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this._tabDirection == 1) {
|
||||||
|
if (this._lastTabIndex > newTabIndex) {
|
||||||
|
this._lastTabIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event == 'modify') {
|
||||||
|
let oldTagName = data.old.tag;
|
||||||
|
this.remove(oldTagName);
|
||||||
|
this.add(tagName, tagType);
|
||||||
|
}
|
||||||
|
else if (event == 'remove') {
|
||||||
|
var oldTabIndex = this.remove(tagName);
|
||||||
|
if (oldTabIndex == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._tabDirection == -1) {
|
||||||
|
if (this._lastTabIndex > oldTabIndex) {
|
||||||
|
this._lastTabIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this._tabDirection == 1) {
|
||||||
|
if (this._lastTabIndex >= oldTabIndex) {
|
||||||
|
this._lastTabIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCount();
|
||||||
|
}
|
||||||
|
else if (type == 'tag') {
|
||||||
|
if (event == 'modify') {
|
||||||
|
this.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
Zotero.debug('Reloading tags box');
|
||||||
|
|
||||||
|
// Cancel field focusing while we're updating
|
||||||
|
this._reloading = true;
|
||||||
|
|
||||||
|
this._id('add').hidden = !this.editable;
|
||||||
|
|
||||||
|
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
|
||||||
|
|
||||||
|
let tagRows = this._id('rows');
|
||||||
|
tagRows.replaceChildren();
|
||||||
|
|
||||||
|
var tags = this.item.getTags();
|
||||||
|
|
||||||
|
// Sort tags alphabetically
|
||||||
|
var collation = Zotero.getLocaleCollation();
|
||||||
|
tags.sort((a, b) => collation.compareString(1, a.tag, b.tag));
|
||||||
|
|
||||||
|
for (let i = 0; i < tags.length; i++) {
|
||||||
|
this.addDynamicRow(tags[i], i + 1);
|
||||||
|
}
|
||||||
|
this.updateCount(tags.length);
|
||||||
|
|
||||||
|
this._reloading = false;
|
||||||
|
this._focusField();
|
||||||
|
}
|
||||||
|
|
||||||
|
addDynamicRow(tagData, tabindex, skipAppend) {
|
||||||
|
var isNew = !tagData;
|
||||||
|
var name = tagData ? tagData.tag : "";
|
||||||
|
|
||||||
|
if (!tabindex) {
|
||||||
|
tabindex = this._id('rows').childNodes.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon = document.createElement("img");
|
||||||
|
icon.className = "zotero-box-icon";
|
||||||
|
|
||||||
|
// DEBUG: Why won't just this.nextSibling.blur() work?
|
||||||
|
icon.addEventListener('click', (event) => {
|
||||||
|
event.target.nextSibling.blur();
|
||||||
|
});
|
||||||
|
|
||||||
|
var label = this.createValueElement(name, tabindex);
|
||||||
|
|
||||||
|
if (this.editable) {
|
||||||
|
var remove = document.createElement("label");
|
||||||
|
remove.setAttribute('value', '-');
|
||||||
|
remove.setAttribute('class', 'zotero-clicky zotero-clicky-minus');
|
||||||
|
remove.setAttribute('tabindex', -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = document.createElement("li");
|
||||||
|
if (isNew) {
|
||||||
|
row.setAttribute('isNew', true);
|
||||||
|
}
|
||||||
|
row.appendChild(icon);
|
||||||
|
row.appendChild(label);
|
||||||
|
if (this.editable) {
|
||||||
|
row.appendChild(remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRow(row, tagData);
|
||||||
|
|
||||||
|
if (!skipAppend) {
|
||||||
|
this._id('rows').appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Update various attributes of a row to match the given tag
|
||||||
|
// and current editability
|
||||||
|
updateRow(row, tagData) {
|
||||||
|
var tagName = tagData ? tagData.tag : "";
|
||||||
|
var tagType = (tagData && tagData.type) ? tagData.type : 0;
|
||||||
|
|
||||||
|
var icon = row.firstChild;
|
||||||
|
if (this.editable) {
|
||||||
|
var remove = row.lastChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row
|
||||||
|
row.setAttribute('tagName', tagName);
|
||||||
|
row.setAttribute('tagType', tagType);
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
var iconFile = 'tag';
|
||||||
|
if (!tagData || tagType == 0) {
|
||||||
|
icon.setAttribute('title', Zotero.getString('pane.item.tags.icon.user'));
|
||||||
|
}
|
||||||
|
else if (tagType == 1) {
|
||||||
|
iconFile += '-automatic';
|
||||||
|
icon.setAttribute('title', Zotero.getString('pane.item.tags.icon.automatic'));
|
||||||
|
}
|
||||||
|
icon.setAttribute('src', `chrome://zotero/skin/${iconFile}${Zotero.hiDPISuffix}.png`);
|
||||||
|
|
||||||
|
// "-" button
|
||||||
|
if (this.editable) {
|
||||||
|
remove.setAttribute('disabled', false);
|
||||||
|
remove.addEventListener('click', async (event) => {
|
||||||
|
this._lastTabIndex = false;
|
||||||
|
if (tagData) {
|
||||||
|
let item = this.item;
|
||||||
|
this.remove(tagName);
|
||||||
|
try {
|
||||||
|
item.removeTag(tagName);
|
||||||
|
await item.saveTx();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.reload();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove empty textbox row
|
||||||
|
else {
|
||||||
|
row.parentNode.removeChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Return focus to items pane
|
||||||
|
var tree = document.getElementById('zotero-items-tree');
|
||||||
|
if (tree) {
|
||||||
|
tree.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createValueElement(valueText, tabindex) {
|
||||||
|
var valueElement = document.createElement("label");
|
||||||
|
valueElement.setAttribute('fieldname', 'tag');
|
||||||
|
valueElement.setAttribute('flex', 1);
|
||||||
|
valueElement.className = 'zotero-box-label';
|
||||||
|
|
||||||
|
if (this.clickable) {
|
||||||
|
if (tabindex) {
|
||||||
|
valueElement.setAttribute('ztabindex', tabindex);
|
||||||
|
}
|
||||||
|
valueElement.addEventListener('click', (event) => {
|
||||||
|
/* Skip right-click on Windows */
|
||||||
|
if (event.button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clickHandler(event.target, 1, valueText);
|
||||||
|
}, false);
|
||||||
|
valueElement.className += ' zotero-clicky';
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstSpace;
|
||||||
|
if (typeof valueText == 'string') {
|
||||||
|
firstSpace = valueText.indexOf(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 29 == arbitrary length at which to chop uninterrupted text
|
||||||
|
if ((firstSpace == -1 && valueText.length > 29) || firstSpace > 29) {
|
||||||
|
valueElement.setAttribute('crop', 'end');
|
||||||
|
valueElement.setAttribute('value', valueText);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Wrap to multiple lines
|
||||||
|
valueElement.appendChild(document.createTextNode(valueText));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag color
|
||||||
|
var colorData = this._tagColors.get(valueText);
|
||||||
|
if (colorData) {
|
||||||
|
valueElement.style.color = colorData.color;
|
||||||
|
valueElement.style.fontWeight = 'bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
showEditor(elem, rows, value) {
|
||||||
|
|
||||||
|
// Blur any active fields
|
||||||
|
/*
|
||||||
|
if (this._dynamicFields) {
|
||||||
|
this._dynamicFields.focus();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Zotero.debug('Showing editor');
|
||||||
|
|
||||||
|
var fieldName = 'tag';
|
||||||
|
var tabindex = elem.getAttribute('ztabindex');
|
||||||
|
|
||||||
|
var itemID = this._item.id;
|
||||||
|
|
||||||
|
var t = document.createElement(rows > 1 ? 'textarea' : 'input', { is: 'shadow-autocomplete-input' });
|
||||||
|
t.setAttribute('class', 'editable');
|
||||||
|
t.setAttribute('value', value);
|
||||||
|
t.setAttribute('fieldname', fieldName);
|
||||||
|
t.setAttribute('ztabindex', tabindex);
|
||||||
|
t.setAttribute('ignoreblurwhilesearching', 'true');
|
||||||
|
t.setAttribute('autocompletepopup', 'PopupAutoComplete');
|
||||||
|
// Multi-line
|
||||||
|
if (rows > 1) {
|
||||||
|
t.setAttribute('rows', rows);
|
||||||
|
}
|
||||||
|
// Add auto-complete
|
||||||
|
else {
|
||||||
|
t.setAttribute('type', 'autocomplete');
|
||||||
|
t.setAttribute('autocompletesearch', 'zotero');
|
||||||
|
let params = {
|
||||||
|
fieldName: fieldName,
|
||||||
|
libraryID: this.item.libraryID
|
||||||
|
};
|
||||||
|
params.itemID = itemID ? itemID : '';
|
||||||
|
t.setAttribute(
|
||||||
|
'autocompletesearchparam', JSON.stringify(params)
|
||||||
|
);
|
||||||
|
t.setAttribute('completeselectedindex', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var box = elem.parentNode;
|
||||||
|
box.replaceChild(t, elem);
|
||||||
|
|
||||||
|
t.addEventListener('blur', this.blurHandler);
|
||||||
|
t.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
t.addEventListener('paste', this.handlePaste);
|
||||||
|
|
||||||
|
this._tabDirection = false;
|
||||||
|
this._lastTabIndex = tabindex;
|
||||||
|
|
||||||
|
// Prevent error when clicking between a changed field
|
||||||
|
// and another -- there's probably a better way
|
||||||
|
if (!t.select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.select();
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown = async (event) => {
|
||||||
|
var target = event.target;
|
||||||
|
var focused = document.activeElement;
|
||||||
|
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case event.DOM_VK_RETURN:
|
||||||
|
var multiline = target.parentNode.classList.contains('multiline');
|
||||||
|
var empty = target.value == "";
|
||||||
|
if (event.shiftKey) {
|
||||||
|
if (!multiline) {
|
||||||
|
var self = this;
|
||||||
|
setTimeout(function () {
|
||||||
|
var val = target.value;
|
||||||
|
if (val !== "") {
|
||||||
|
val += "\n";
|
||||||
|
}
|
||||||
|
self.makeMultiline(target, val, 6);
|
||||||
|
}, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Submit
|
||||||
|
}
|
||||||
|
else if (multiline) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = target.closest('li');
|
||||||
|
let blurOnly = false;
|
||||||
|
|
||||||
|
// If non-empty last row, only blur, because the open textbox will
|
||||||
|
// be cleared in hideEditor() and remain in place
|
||||||
|
if (row == row.parentNode.lastChild && !empty) {
|
||||||
|
blurOnly = true;
|
||||||
|
}
|
||||||
|
// If empty non-last row, refocus current row
|
||||||
|
else if (row != row.parentNode.lastChild && empty) {
|
||||||
|
var focusField = true;
|
||||||
|
}
|
||||||
|
// If non-empty non-last row, return focus to items pane
|
||||||
|
else {
|
||||||
|
var focusField = false;
|
||||||
|
this._lastTabIndex = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.blurHandler(event);
|
||||||
|
|
||||||
|
if (blurOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (focusField) {
|
||||||
|
this._focusField();
|
||||||
|
}
|
||||||
|
// Return focus to items pane
|
||||||
|
else {
|
||||||
|
var tree = document.getElementById('zotero-items-tree');
|
||||||
|
if (tree) {
|
||||||
|
tree.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case event.DOM_VK_ESCAPE:
|
||||||
|
// Reset field to original value
|
||||||
|
target.value = target.getAttribute('value');
|
||||||
|
|
||||||
|
var tagsbox = focused.closest('.editable');
|
||||||
|
|
||||||
|
this._lastTabIndex = false;
|
||||||
|
await this.blurHandler(event);
|
||||||
|
|
||||||
|
if (tagsbox) {
|
||||||
|
tagsbox.closePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Return focus to items pane
|
||||||
|
var tree = document.getElementById('zotero-items-tree');
|
||||||
|
if (tree) {
|
||||||
|
tree.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case event.DOM_VK_TAB:
|
||||||
|
// If already an empty last row, ignore forward tab
|
||||||
|
if (target.value == "" && !event.shiftKey) {
|
||||||
|
var row = Zotero.getAncestorByTagName(target, 'li');
|
||||||
|
if (row == row.parentNode.lastChild) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._tabDirection = event.shiftKey ? -1 : 1;
|
||||||
|
await this.blurHandler(event);
|
||||||
|
this._focusField();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intercept paste, check for newlines, and convert textbox
|
||||||
|
// to multiline if necessary
|
||||||
|
handlePaste = (event) => {
|
||||||
|
var textbox = event.target;
|
||||||
|
var str = event.clipboardData.getData('text');
|
||||||
|
|
||||||
|
var multiline = !!str.trim().match(/\n/);
|
||||||
|
if (multiline) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.makeMultiline(textbox, str.trim());
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
makeMultiline(textbox, value, rows) {
|
||||||
|
textbox.parentNode.classList.add('multiline');
|
||||||
|
// If rows not specified, use one more than lines in input
|
||||||
|
if (!rows) {
|
||||||
|
rows = value.match(/\n/g).length + 1;
|
||||||
|
}
|
||||||
|
textbox = this.showEditor(textbox, rows, textbox.getAttribute('value'));
|
||||||
|
textbox.value = value;
|
||||||
|
// Move cursor to end
|
||||||
|
textbox.selectionStart = value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideEditor = async (event) => {
|
||||||
|
var textbox = event.target;
|
||||||
|
|
||||||
|
Zotero.debug('Hiding editor');
|
||||||
|
|
||||||
|
var oldValue = textbox.getAttribute('value');
|
||||||
|
var value = textbox.value = textbox.value.trim();
|
||||||
|
|
||||||
|
var tagsbox = textbox.closest('.editable');
|
||||||
|
if (!tagsbox) {
|
||||||
|
Zotero.debug('Tagsbox not found', 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = textbox.parentNode;
|
||||||
|
|
||||||
|
var isNew = row.getAttribute('isNew');
|
||||||
|
|
||||||
|
// Remove empty row at end
|
||||||
|
if (isNew && value === "") {
|
||||||
|
row.parentNode.removeChild(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If row hasn't changed, change back to label
|
||||||
|
if (oldValue == value) {
|
||||||
|
this.textboxToLabel(textbox);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags = value.split(/\r\n?|\n/).map(val => val.trim()).filter(x => x);
|
||||||
|
|
||||||
|
// Modifying existing tag with a single new one
|
||||||
|
if (!isNew && tags.length < 2) {
|
||||||
|
if (value !== "") {
|
||||||
|
if (oldValue !== value) {
|
||||||
|
// The existing textbox will be removed in notify()
|
||||||
|
this.removeRow(row);
|
||||||
|
this.add(value);
|
||||||
|
if (event.type != 'blur') {
|
||||||
|
this._focusField();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.item.replaceTag(oldValue, value);
|
||||||
|
await this.item.saveTx();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.reload();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Existing tag cleared
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
this.removeRow(row);
|
||||||
|
if (event.type != 'blur') {
|
||||||
|
this._focusField();
|
||||||
|
}
|
||||||
|
this.item.removeTag(oldValue);
|
||||||
|
await this.item.saveTx();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.reload();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multiple tags
|
||||||
|
else if (tags.length > 1) {
|
||||||
|
var lastTag = row == row.parentNode.lastChild;
|
||||||
|
|
||||||
|
if (!isNew) {
|
||||||
|
// If old tag isn't in array, remove it
|
||||||
|
if (tags.indexOf(oldValue) == -1) {
|
||||||
|
this.item.removeTag(oldValue);
|
||||||
|
}
|
||||||
|
// If old tag is staying, restore the textbox
|
||||||
|
// immediately. This isn't strictly necessary, but it
|
||||||
|
// makes the transition nicer.
|
||||||
|
else {
|
||||||
|
textbox.value = textbox.getAttribute('value');
|
||||||
|
this.textboxToLabel(textbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.forEach(tag => this.item.addTag(tag));
|
||||||
|
await this.item.saveTx();
|
||||||
|
|
||||||
|
if (lastTag) {
|
||||||
|
this._lastTabIndex = this.item.getTags().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reload();
|
||||||
|
}
|
||||||
|
// Single tag at end
|
||||||
|
else {
|
||||||
|
if (event.type == 'blur') {
|
||||||
|
this.removeRow(row);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
textbox.value = '';
|
||||||
|
}
|
||||||
|
this.add(value);
|
||||||
|
this.item.addTag(value);
|
||||||
|
try {
|
||||||
|
await this.item.saveTx();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.reload();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
newTag() {
|
||||||
|
var rowsElement = this._id('rows');
|
||||||
|
var rows = rowsElement.childNodes;
|
||||||
|
|
||||||
|
// Don't add new row if there already is one
|
||||||
|
if (rows.length && rows[rows.length - 1].querySelector('.editable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = this.addDynamicRow();
|
||||||
|
// It needs relatively high delay to make focus-on-click work
|
||||||
|
setTimeout(() => {
|
||||||
|
row.firstChild.nextSibling.click();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
textboxToLabel(textbox) {
|
||||||
|
var elem = this.createValueElement(
|
||||||
|
textbox.value, textbox.getAttribute('ztabindex')
|
||||||
|
);
|
||||||
|
var row = textbox.parentNode;
|
||||||
|
row.replaceChild(elem, textbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(tagName, tagType) {
|
||||||
|
var rowsElement = this._id('rows');
|
||||||
|
var rows = rowsElement.childNodes;
|
||||||
|
|
||||||
|
// Get this tag's existing row, if there is one
|
||||||
|
var row = false;
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (rows[i].getAttribute('tagName') === tagName) {
|
||||||
|
return rows[i].getAttribute('ztabindex');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagData = {
|
||||||
|
tag: tagName,
|
||||||
|
type: tagType
|
||||||
|
};
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
// Update row and label
|
||||||
|
this.updateRow(row, tagData);
|
||||||
|
var elem = this.createValueElement(tagName);
|
||||||
|
|
||||||
|
// Remove the old row, which we'll reinsert at the correct place
|
||||||
|
rowsElement.removeChild(row);
|
||||||
|
|
||||||
|
// Find the current label or textbox within the row
|
||||||
|
// and replace it with the new element -- this is used
|
||||||
|
// both when creating new rows and when hiding the
|
||||||
|
// entry textbox
|
||||||
|
var oldElem = row.getElementsByAttribute('fieldname', 'tag')[0];
|
||||||
|
row.replaceChild(elem, oldElem);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Create new row, but don't insert it
|
||||||
|
row = this.addDynamicRow(tagData, false, true);
|
||||||
|
var elem = row.getElementsByAttribute('fieldname', 'tag')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move row to appropriate place, alphabetically
|
||||||
|
var collation = Zotero.getLocaleCollation();
|
||||||
|
var labels = rowsElement.getElementsByAttribute('fieldname', 'tag');
|
||||||
|
|
||||||
|
var inserted = false;
|
||||||
|
var newTabIndex = false;
|
||||||
|
for (var i = 0; i < labels.length; i++) {
|
||||||
|
let index = i + 1;
|
||||||
|
if (inserted) {
|
||||||
|
labels[i].setAttribute('ztabindex', index);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collation.compareString(1, tagName, labels[i].textContent) > 0
|
||||||
|
// Ignore textbox at end
|
||||||
|
&& labels[i].tagName != 'input') {
|
||||||
|
labels[i].setAttribute('ztabindex', index);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
elem.setAttribute('ztabindex', index);
|
||||||
|
rowsElement.insertBefore(row, labels[i].parentNode);
|
||||||
|
newTabIndex = index;
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
|
if (!inserted) {
|
||||||
|
newTabIndex = i + 1;
|
||||||
|
elem.setAttribute('ztabindex', newTabIndex);
|
||||||
|
rowsElement.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCount(this.count + 1);
|
||||||
|
|
||||||
|
return newTabIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(tagName) {
|
||||||
|
var rowsElement = this._id('rows');
|
||||||
|
var rows = rowsElement.childNodes;
|
||||||
|
var oldTabIndex = -1;
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
let value = rows[i].getAttribute('tagName');
|
||||||
|
if (value === tagName) {
|
||||||
|
oldTabIndex = this.removeRow(rows[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oldTabIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the row and update tab indexes
|
||||||
|
removeRow(row) {
|
||||||
|
var origTabIndex = row.getElementsByAttribute('fieldname', 'tag')[0].getAttribute('ztabindex');
|
||||||
|
var origRow = row;
|
||||||
|
var i = origTabIndex;
|
||||||
|
while (row = row.nextSibling) {
|
||||||
|
let elem = row.getElementsByAttribute('fieldname', 'tag')[0];
|
||||||
|
elem.setAttribute('ztabindex', i++);
|
||||||
|
}
|
||||||
|
origRow.parentNode.removeChild(origRow);
|
||||||
|
this.updateCount(this.count - 1);
|
||||||
|
return origTabIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll = () => {
|
||||||
|
if (Services.prompt.confirm(null, "", Zotero.getString('pane.item.tags.removeAll'))) {
|
||||||
|
this.item.setTags([]);
|
||||||
|
this.item.saveTx();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCount(count) {
|
||||||
|
if (!this.item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof count == 'undefined') {
|
||||||
|
var tags = this.item.getTags();
|
||||||
|
if (tags) {
|
||||||
|
count = tags.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._id('count').replaceChildren(Zotero.getString('pane.item.tags.count', count, count));
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
closePopup() {
|
||||||
|
if (this.parentNode.hidePopup) {
|
||||||
|
this.parentNode.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the textbox for a particular label
|
||||||
|
//
|
||||||
|
// Note: We're basically replicating the built-in tabindex functionality,
|
||||||
|
// which doesn't work well with the weird label/textbox stuff we're doing.
|
||||||
|
// (The textbox being tabbed away from is deleted before the blur()
|
||||||
|
// completes, so it doesn't know where it's supposed to go next.)
|
||||||
|
_focusField() {
|
||||||
|
if (this._reloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._lastTabIndex === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxIndex = this._id('rows').childNodes.length + 1;
|
||||||
|
|
||||||
|
var tabindex = parseInt(this._lastTabIndex);
|
||||||
|
var dir = this._tabDirection;
|
||||||
|
|
||||||
|
if (dir == 1) {
|
||||||
|
var nextIndex = tabindex + 1;
|
||||||
|
}
|
||||||
|
else if (dir == -1) {
|
||||||
|
if (tabindex == 1) {
|
||||||
|
// Focus Add button
|
||||||
|
this._id('add').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var nextIndex = tabindex - 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var nextIndex = tabindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIndex = Math.min(nextIndex, maxIndex);
|
||||||
|
|
||||||
|
Zotero.debug('Looking for tabindex ' + nextIndex, 4);
|
||||||
|
|
||||||
|
var next = this.shadowRoot.getElementsByAttribute('ztabindex', nextIndex);
|
||||||
|
if (next.length) {
|
||||||
|
next = next[0];
|
||||||
|
next.click();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next = this.newTag();
|
||||||
|
next = next.firstChild.nextSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!next) {
|
||||||
|
Components.utils.reportError('Next row not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleAddButtonKeyDown = (event) => {
|
||||||
|
if (event.keyCode != event.DOM_VK_TAB || event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._lastTabIndex = 0;
|
||||||
|
this._tabDirection = 1;
|
||||||
|
this._focusField();
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleAddButtonClick = async (event) => {
|
||||||
|
await this.blurOpenField();
|
||||||
|
this.newTag();
|
||||||
|
};
|
||||||
|
|
||||||
|
addNew() {
|
||||||
|
this._handleAddButtonClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
async blurOpenField(stayOpen) {
|
||||||
|
this._lastTabIndex = false;
|
||||||
|
var textboxe = this.shadowRoot.querySelector('.editable');
|
||||||
|
if (textboxe) {
|
||||||
|
await this.blurHandler({
|
||||||
|
target: textboxe,
|
||||||
|
// If coming from the Add button, pretend user pressed return
|
||||||
|
type: stayOpen ? 'keypress' : 'blur',
|
||||||
|
// DOM_VK_RETURN
|
||||||
|
keyCode: stayOpen ? 13 : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_id(id) {
|
||||||
|
return this.shadowRoot.querySelector(`[id=${id}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("tags-box", TagsBox);
|
||||||
|
}
|
|
@ -44,11 +44,8 @@ var ZoteroItemPane = new function() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fake a ref
|
|
||||||
_tagsBox = {
|
|
||||||
current: null
|
|
||||||
};
|
|
||||||
_notesBox = document.getElementById('zotero-editpane-notes');
|
_notesBox = document.getElementById('zotero-editpane-notes');
|
||||||
|
_tagsBox = document.getElementById('zotero-editpane-tags');
|
||||||
_relatedBox = document.getElementById('zotero-editpane-related');
|
_relatedBox = document.getElementById('zotero-editpane-related');
|
||||||
|
|
||||||
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
|
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
|
||||||
|
@ -86,6 +83,10 @@ var ZoteroItemPane = new function() {
|
||||||
box.parentItem = item;
|
box.parentItem = item;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
var box = _tagsBox;
|
||||||
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
var box = _relatedBox;
|
var box = _relatedBox;
|
||||||
break;
|
break;
|
||||||
|
@ -101,11 +102,6 @@ var ZoteroItemPane = new function() {
|
||||||
// DEBUG: Currently broken
|
// DEBUG: Currently broken
|
||||||
//box.scrollToTop();
|
//box.scrollToTop();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
|
||||||
// TEMP
|
|
||||||
//_tagsBox.current.blurOpenField();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,19 +132,6 @@ var ZoteroItemPane = new function() {
|
||||||
this.setTranslateButton();
|
this.setTranslateButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (index == 2) {
|
|
||||||
ReactDOM.render(
|
|
||||||
<TagsBoxContainer
|
|
||||||
key={"tagsBox-" + item.id}
|
|
||||||
item={item}
|
|
||||||
editable={mode != 'view'}
|
|
||||||
ref={_tagsBox}
|
|
||||||
onResetSelection={focusItemsList}
|
|
||||||
/>,
|
|
||||||
document.getElementById('tags-box-container'),
|
|
||||||
() => ZoteroPane.updateTagsBoxSize()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (box) {
|
if (box) {
|
||||||
if (mode) {
|
if (mode) {
|
||||||
|
@ -186,7 +169,7 @@ var ZoteroItemPane = new function() {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
var box = _tagsBox.current;
|
var box = _tagsBox;
|
||||||
if (box) {
|
if (box) {
|
||||||
box.blurOpenField();
|
box.blurOpenField();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
||||||
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
||||||
|
|
||||||
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
|
<!DOCTYPE window [
|
||||||
|
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
|
||||||
|
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd"> %zoteroDTD;
|
||||||
|
]>
|
||||||
|
|
||||||
<window
|
<window
|
||||||
id="zotero-note-window"
|
id="zotero-note-window"
|
||||||
|
@ -20,12 +23,11 @@
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
|
||||||
Services.scriptloader.loadSubScript("resource://zotero/require.js", this);
|
Services.scriptloader.loadSubScript("resource://zotero/require.js", this);
|
||||||
|
|
||||||
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/shadowAutocompleteInput.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/noteEditor.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/noteEditor.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/relatedBox.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/relatedBox.js", this);
|
||||||
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/tagsBox.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/note.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/note.js", this);
|
||||||
|
|
||||||
var React = require('react');
|
|
||||||
var ReactDOM = require('react-dom');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<keyset>
|
<keyset>
|
||||||
|
|
|
@ -445,10 +445,16 @@ class ReaderInstance {
|
||||||
|
|
||||||
_openTagsPopup(item, selector) {
|
_openTagsPopup(item, selector) {
|
||||||
let menupopup = this._window.document.createXULElement('menupopup');
|
let menupopup = this._window.document.createXULElement('menupopup');
|
||||||
|
menupopup.addEventListener('popuphidden', function (event) {
|
||||||
|
if (event.target === menupopup) {
|
||||||
|
menupopup.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
menupopup.className = 'tags-popup';
|
menupopup.className = 'tags-popup';
|
||||||
|
menupopup.style.font = 'inherit';
|
||||||
menupopup.style.minWidth = '300px';
|
menupopup.style.minWidth = '300px';
|
||||||
menupopup.setAttribute('ignorekeys', true);
|
menupopup.setAttribute('ignorekeys', true);
|
||||||
let tagsbox = this._window.document.createXULElement('tagsbox');
|
let tagsbox = new (this._window.customElements.get('tags-box'));
|
||||||
menupopup.appendChild(tagsbox);
|
menupopup.appendChild(tagsbox);
|
||||||
tagsbox.setAttribute('flex', '1');
|
tagsbox.setAttribute('flex', '1');
|
||||||
this._popupset.appendChild(menupopup);
|
this._popupset.appendChild(menupopup);
|
||||||
|
|
|
@ -80,6 +80,7 @@
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/itemBox.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/itemBox.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/noteEditor.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/noteEditor.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/notesBox.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/notesBox.js", this);
|
||||||
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/tagsBox.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/relatedBox.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/relatedBox.js", this);
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/attachmentBox.js", this);
|
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/attachmentBox.js", this);
|
||||||
|
|
||||||
|
@ -1123,8 +1124,8 @@
|
||||||
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" flex="1"/>
|
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" flex="1"/>
|
||||||
</tabpanel>
|
</tabpanel>
|
||||||
|
|
||||||
<tabpanel id="tags-pane" class="tags-pane" orient="vertical" context="tags-context-menu">
|
<tabpanel>
|
||||||
<html:div id="tags-box-container" class="tags-box-container"></html:div>
|
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" flex="1"/>
|
||||||
</tabpanel>
|
</tabpanel>
|
||||||
|
|
||||||
<tabpanel>
|
<tabpanel>
|
||||||
|
@ -1296,13 +1297,4 @@
|
||||||
this.setAttribute('checked', false);
|
this.setAttribute('checked', false);
|
||||||
event.stopPropagation();"/>
|
event.stopPropagation();"/>
|
||||||
</menupopup>
|
</menupopup>
|
||||||
|
|
||||||
<!-- Tags Box -->
|
|
||||||
<popupset>
|
|
||||||
<menupopup id="tags-context-menu"
|
|
||||||
onpopupshowing="return ZoteroItemPane.onTagsContextPopupShowing()">
|
|
||||||
<menuitem id="remove-all-item-tags" label="&zotero.item.tags.removeAll;"
|
|
||||||
oncommand="ZoteroItemPane.removeAllTags()"/>
|
|
||||||
</menupopup>
|
|
||||||
</popupset>
|
|
||||||
</window>
|
</window>
|
||||||
|
|
|
@ -421,9 +421,7 @@ pane.item.attachments.PDF.installTools.title = PDF Tools Not Installed
|
||||||
pane.item.attachments.PDF.installTools.text = To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences.
|
pane.item.attachments.PDF.installTools.text = To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences.
|
||||||
pane.item.attachments.filename = Filename
|
pane.item.attachments.filename = Filename
|
||||||
pane.item.noteEditor.clickHere = click here
|
pane.item.noteEditor.clickHere = click here
|
||||||
pane.item.tags.count.zero = %S tags:
|
pane.item.tags.count = %1$S tag;%1$S tags
|
||||||
pane.item.tags.count.singular = %S tag:
|
|
||||||
pane.item.tags.count.plural = %S tags:
|
|
||||||
pane.item.tags.icon.user = User-added tag
|
pane.item.tags.icon.user = User-added tag
|
||||||
pane.item.tags.icon.automatic = Automatically added tag
|
pane.item.tags.icon.automatic = Automatically added tag
|
||||||
pane.item.tags.removeAll = Remove all tags from this item?
|
pane.item.tags.removeAll = Remove all tags from this item?
|
||||||
|
|
2
scss/_tagsBox.scss
Normal file
2
scss/_tagsBox.scss
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@import "components/tagsBox";
|
||||||
|
@import "components/clicky";
|
|
@ -24,3 +24,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#related-popup, #tags-popup {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
|
@ -1,105 +1,72 @@
|
||||||
.tags-pane {
|
.tags-box-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding-left: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 79px;
|
||||||
|
margin: 5px 6px 3px;
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
color: ButtonText;
|
||||||
|
text-shadow: none;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-box-container {
|
.tags-box-list {
|
||||||
flex-grow: 1;
|
list-style: none;
|
||||||
display: flex;
|
margin: 0;
|
||||||
}
|
padding: 2px 0 0; // Leave space for textbox border on top tag
|
||||||
|
|
||||||
.tags-box {
|
li {
|
||||||
$item-pane-width: 330px;
|
|
||||||
$icon-width: 16px;
|
|
||||||
$delete-button-width: 20px;
|
|
||||||
$li-side-margin: 6px;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
//width: 330px;
|
|
||||||
|
|
||||||
.tags-box-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-left: 10px;
|
margin: 3px 0;
|
||||||
align-items: center;
|
margin-inline-start: 6px;
|
||||||
|
|
||||||
button {
|
|
||||||
min-width: 79px;
|
|
||||||
margin: 5px 6px 3px;
|
|
||||||
padding-top: 1px;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
color: ButtonText;
|
|
||||||
text-shadow: none;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-box-count {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.tags-box-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 2px 0 0; // Leave space for textbox border on top tag
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.tags-box-list > li {
|
|
||||||
display: flex;
|
|
||||||
margin: 3px $li-side-margin;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
|
|
||||||
|
// Shift-Enter
|
||||||
|
&.multiline {
|
||||||
|
align-items: start;
|
||||||
|
height: 9em;
|
||||||
|
|
||||||
|
textarea.editable {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.multiline) .editable {
|
||||||
|
padding: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zotero-box-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zotero-box-label {
|
||||||
|
flex-grow: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: $delete-button-width;
|
width: 20px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.editable-container {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin: 0 2px;
|
|
||||||
// width: $item-pane-width - $icon-width - $delete-button-width - ($li-side-margin * 2);
|
|
||||||
|
|
||||||
// This container shouldn't force any width for its parent,
|
|
||||||
// because tagsBox is used in more places than just item pane,
|
|
||||||
// and it can have smaller width than $item-pane-width
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.tags-box-list > li:not(.multiline) .editable-container {
|
|
||||||
padding: 0 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift-Enter
|
|
||||||
ul.tags-box-list > li.multiline {
|
|
||||||
align-items: start;
|
|
||||||
height: 9em;
|
|
||||||
|
|
||||||
.editable-container {
|
|
||||||
align-self: stretch;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editable, .input-group {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.editable-control {
|
|
||||||
flex: 1;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input.editable-control {
|
|
||||||
width: 100px; // Dummy value that somehow prevents field from going off screen at large font size
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.editable-control {
|
|
||||||
width: 100%; // DEBUG: This still runs off the screen at large font size, though it keeps the delete button visible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.tags-box li img {
|
.tags-box-list li img {
|
||||||
margin-right: 1px;
|
margin-right: 1px;
|
||||||
}
|
}
|
||||||
|
|
1
scss/tagsBox-mac.scss
Normal file
1
scss/tagsBox-mac.scss
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@import "tagsBox";
|
1
scss/tagsBox-unix.scss
Normal file
1
scss/tagsBox-unix.scss
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@import "tagsBox";
|
1
scss/tagsBox-win.scss
Normal file
1
scss/tagsBox-win.scss
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@import "tagsBox";
|
Loading…
Reference in a new issue