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
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var TagsBoxContainer = require('containers/tagsBoxContainer').default;
|
||||
var NotesList = require('components/itemPane/notesList').default;
|
||||
|
||||
var ZoteroContextPane = new function () {
|
||||
|
@ -852,25 +851,11 @@ var ZoteroContextPane = new function () {
|
|||
panelInfo.append(itemBox);
|
||||
// Tags panel
|
||||
var panelTags = document.createXULElement('tabpanel');
|
||||
panelTags.setAttribute('orient', 'vertical');
|
||||
panelTags.setAttribute('context', 'tags-context-menu');
|
||||
panelTags.className = 'tags-pane';
|
||||
panelTags.style.display = 'flex';
|
||||
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
|
||||
);
|
||||
var tagsBox = new (customElements.get('tags-box'));
|
||||
tagsBox.setAttribute('flex', '1');
|
||||
tagsBox.className = 'zotero-editpane-tags';
|
||||
panelTags.append(tagsBox);
|
||||
|
||||
// Related panel
|
||||
var panelRelated = document.createXULElement('tabpanel');
|
||||
var relatedBox = new (customElements.get('related-box'));
|
||||
|
@ -890,6 +875,9 @@ var ZoteroContextPane = new function () {
|
|||
itemBox.mode = readOnly ? 'view' : 'edit';
|
||||
itemBox.item = parentItem;
|
||||
|
||||
tagsBox.mode = readOnly ? 'view' : 'edit';
|
||||
tagsBox.item = parentItem;
|
||||
|
||||
relatedBox.mode = readOnly ? 'view' : 'edit';
|
||||
relatedBox.item = parentItem;
|
||||
}
|
||||
|
|
|
@ -328,8 +328,6 @@
|
|||
|
||||
|
||||
{
|
||||
let TagsBoxContainer = require('containers/tagsBoxContainer').default;
|
||||
|
||||
class LinksBox extends XULElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -357,7 +355,7 @@
|
|||
<related-box id="related"/>
|
||||
</menupopup>
|
||||
<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>
|
||||
</popupset>
|
||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
|
@ -401,6 +399,7 @@
|
|||
set item(val) {
|
||||
this._item = val;
|
||||
this._id('related').item = this._item;
|
||||
this._id('tags').item = this._item;
|
||||
|
||||
this.refresh();
|
||||
|
||||
|
@ -417,6 +416,7 @@
|
|||
set mode(val) {
|
||||
this._mode = val;
|
||||
this._id('related').mode = val;
|
||||
this._id('tags').mode = val;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
@ -434,22 +434,6 @@
|
|||
this._updateParentRow();
|
||||
this._updateTagsSummary();
|
||||
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() {
|
||||
|
@ -546,7 +530,7 @@
|
|||
// If editable and no existing tags, open new empty row
|
||||
if (this._mode == 'edit' && !this._item.getTags().length) {
|
||||
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
|
||||
* don't work in a shadow DOM context.
|
||||
* Extend AutocompleteInput to work around issues with shadow DOM
|
||||
*/
|
||||
class ShadowAutocompleteInput extends customElements.get('autocomplete-input') {
|
||||
// Fix document.activeElement checks that don't work in a shadow DOM context
|
||||
get focused() {
|
||||
// document.activeElement by itself doesn't traverse shadow DOMs; see
|
||||
// https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
|
||||
|
@ -53,6 +53,21 @@
|
|||
|
||||
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, {
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
// Fake a ref
|
||||
_tagsBox = {
|
||||
current: null
|
||||
};
|
||||
_notesBox = document.getElementById('zotero-editpane-notes');
|
||||
_tagsBox = document.getElementById('zotero-editpane-tags');
|
||||
_relatedBox = document.getElementById('zotero-editpane-related');
|
||||
|
||||
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
|
||||
|
@ -86,6 +83,10 @@ var ZoteroItemPane = new function() {
|
|||
box.parentItem = item;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
var box = _tagsBox;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
var box = _relatedBox;
|
||||
break;
|
||||
|
@ -101,11 +102,6 @@ var ZoteroItemPane = new function() {
|
|||
// DEBUG: Currently broken
|
||||
//box.scrollToTop();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// TEMP
|
||||
//_tagsBox.current.blurOpenField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,19 +132,6 @@ var ZoteroItemPane = new function() {
|
|||
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 (mode) {
|
||||
|
@ -186,7 +169,7 @@ var ZoteroItemPane = new function() {
|
|||
break;
|
||||
|
||||
case 2:
|
||||
var box = _tagsBox.current;
|
||||
var box = _tagsBox;
|
||||
if (box) {
|
||||
box.blurOpenField();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<?xml-stylesheet href="chrome://zotero/skin/zotero.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
|
||||
id="zotero-note-window"
|
||||
|
@ -20,12 +23,11 @@
|
|||
Services.scriptloader.loadSubScript("chrome://zotero/content/include.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/relatedBox.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/tagsBox.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/note.js", this);
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
</script>
|
||||
|
||||
<keyset>
|
||||
|
|
|
@ -445,10 +445,16 @@ class ReaderInstance {
|
|||
|
||||
_openTagsPopup(item, selector) {
|
||||
let menupopup = this._window.document.createXULElement('menupopup');
|
||||
menupopup.addEventListener('popuphidden', function (event) {
|
||||
if (event.target === menupopup) {
|
||||
menupopup.remove();
|
||||
}
|
||||
});
|
||||
menupopup.className = 'tags-popup';
|
||||
menupopup.style.font = 'inherit';
|
||||
menupopup.style.minWidth = '300px';
|
||||
menupopup.setAttribute('ignorekeys', true);
|
||||
let tagsbox = this._window.document.createXULElement('tagsbox');
|
||||
let tagsbox = new (this._window.customElements.get('tags-box'));
|
||||
menupopup.appendChild(tagsbox);
|
||||
tagsbox.setAttribute('flex', '1');
|
||||
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/noteEditor.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/attachmentBox.js", this);
|
||||
|
||||
|
@ -1123,8 +1124,8 @@
|
|||
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" flex="1"/>
|
||||
</tabpanel>
|
||||
|
||||
<tabpanel id="tags-pane" class="tags-pane" orient="vertical" context="tags-context-menu">
|
||||
<html:div id="tags-box-container" class="tags-box-container"></html:div>
|
||||
<tabpanel>
|
||||
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" flex="1"/>
|
||||
</tabpanel>
|
||||
|
||||
<tabpanel>
|
||||
|
@ -1296,13 +1297,4 @@
|
|||
this.setAttribute('checked', false);
|
||||
event.stopPropagation();"/>
|
||||
</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>
|
||||
|
|
|
@ -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.filename = Filename
|
||||
pane.item.noteEditor.clickHere = click here
|
||||
pane.item.tags.count.zero = %S tags:
|
||||
pane.item.tags.count.singular = %S tag:
|
||||
pane.item.tags.count.plural = %S tags:
|
||||
pane.item.tags.count = %1$S tag;%1$S tags
|
||||
pane.item.tags.icon.user = User-added tag
|
||||
pane.item.tags.icon.automatic = Automatically added tag
|
||||
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;
|
||||
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 {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
}
|
||||
.tags-box-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 2px 0 0; // Leave space for textbox border on top tag
|
||||
|
||||
.tags-box {
|
||||
$item-pane-width: 330px;
|
||||
$icon-width: 16px;
|
||||
$delete-button-width: 20px;
|
||||
$li-side-margin: 6px;
|
||||
|
||||
flex-grow: 1;
|
||||
//width: 330px;
|
||||
|
||||
.tags-box-header {
|
||||
li {
|
||||
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-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;
|
||||
margin: 3px 0;
|
||||
margin-inline-start: 6px;
|
||||
align-items: center;
|
||||
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 {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
width: $delete-button-width;
|
||||
width: 20px;
|
||||
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;
|
||||
}
|
||||
|
|
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