React Tag Selector polish, i18n and tests

- Added icon-button UI code for the menubutton
- Upgrade to React 16 to allow non-standard attrs, such as `tooltiptext`
to support XUL tooltips
- Add i18n support for React UI elements
- Update tests for reactified tag selector
This commit is contained in:
Adomas Venčkauskas 2018-12-12 12:34:39 +02:00
parent 897e74c7f1
commit a24cada451
39 changed files with 1437 additions and 1252 deletions

View file

@ -0,0 +1,131 @@
'use strict'
const React = require('react')
const { PureComponent, createElement: create } = React
const { injectIntl, intlShape } = require('react-intl')
const { IconDownChevron } = require('./icons')
const cx = require('classnames')
const {
bool, element, func, node, number, oneOf, string
} = require('prop-types')
const ButtonGroup = ({ children }) => (
<div className="btn-group">{children}</div>
)
ButtonGroup.propTypes = {
children: node
}
class Button extends PureComponent {
componentDidMount() {
if (!Zotero.isNode && this.title) {
// Workaround for XUL tooltips
this.container.setAttribute('tooltiptext', this.title);
}
}
get classes() {
return ['btn', this.props.className, `btn-${this.props.size}`, {
'btn-icon': this.props.icon != null,
'active': this.props.isActive,
'btn-flat': this.props.isFlat,
'btn-menu': this.props.isMenu,
'disabled': this.props.isDisabled,
}]
}
get node() {
return 'button'
}
get text() {
const { intl, text } = this.props
return text ?
intl.formatMessage({ id: text }) :
null
}
get title() {
const { intl, title } = this.props
return title ?
intl.formatMessage({ id: title }) :
null
}
get menuMarker() {
if (!Zotero.isNode) {
return this.props.isMenu && <span className="menu-marker"/>
}
return this.props.isMenu && <IconDownChevron className="menu-marker"/>
}
get attributes() {
const attr = {
className: cx(...this.classes),
disabled: !this.props.noFocus && this.props.isDisabled,
onBlur: this.handleBlur,
onFocus: this.props.onFocus,
ref: this.setContainer,
title: this.title
}
if (!this.props.isDisabled) {
attr.onMouseDown = this.handleMouseDown
attr.onClick = this.handleClick
}
return attr
}
setContainer = (container) => {
this.container = container
}
handleClick = (event) => {
event.preventDefault()
if (!this.props.isDisabled && this.props.onClick) {
this.props.onClick(event)
}
}
handleMouseDown = (event) => {
event.preventDefault()
if (!this.props.isDisabled && this.props.onMouseDown) {
this.props.onMouseDown(event)
}
}
render() {
return create(this.node, this.attributes, this.props.icon, this.text, this.menuMarker)
}
static propTypes = {
className: string,
icon: element,
intl: intlShape.isRequired,
isActive: bool,
isDisabled: bool,
isMenu: bool,
size: oneOf(['sm', 'md', 'lg']),
title: string,
text: string,
onClick: func,
onMouseDown: func
}
static defaultProps = {
size: 'md'
}
}
module.exports = {
ButtonGroup,
Button: injectIntl(Button)
}

View file

@ -15,13 +15,13 @@ class Input extends React.PureComponent {
} }
cancel(event = null) { cancel(event = null) {
this.props.onCancel(this.hasChanged, event); this.props.onCancel && this.props.onCancel(this.hasChanged, event);
this.hasBeenCancelled = true; this.hasBeenCancelled = true;
this.input.blur(); this.input.blur();
} }
commit(event = null) { commit(event = null) {
this.props.onCommit(this.state.value, this.hasChanged, event); this.props.onCommit && this.props.onCommit(this.state.value, this.hasChanged, event);
this.hasBeenCommitted = true; this.hasBeenCommitted = true;
} }
@ -40,18 +40,18 @@ class Input extends React.PureComponent {
handleChange({ target }) { handleChange({ target }) {
this.setState({ value: target.value }); this.setState({ value: target.value });
this.props.onChange(target.value); this.props.onChange && this.props.onChange(target.value);
} }
handleBlur(event) { handleBlur(event) {
const shouldCancel = this.props.onBlur(event); const shouldCancel = this.props.onBlur && this.props.onBlur(event);
if (this.hasBeenCancelled || this.hasBeenCommitted) { return; } if (this.hasBeenCancelled || this.hasBeenCommitted) { return; }
shouldCancel ? this.cancel(event) : this.commit(event); shouldCancel ? this.cancel(event) : this.commit(event);
} }
handleFocus(event) { handleFocus(event) {
this.props.selectOnFocus && event.target.select(); this.props.selectOnFocus && event.target.select();
this.props.onFocus(event); this.props.onFocus && this.props.onFocus(event);
} }
handleKeyDown(event) { handleKeyDown(event) {
@ -137,11 +137,11 @@ class Input extends React.PureComponent {
min: PropTypes.number, min: PropTypes.number,
minLength: PropTypes.number, minLength: PropTypes.number,
name: PropTypes.string, name: PropTypes.string,
onBlur: PropTypes.func.isRequired, onBlur: PropTypes.func,
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func,
onCommit: PropTypes.func.isRequired, onCommit: PropTypes.func,
onFocus: PropTypes.func.isRequired, onFocus: PropTypes.func,
placeholder: PropTypes.string, placeholder: PropTypes.string,
selectOnFocus: PropTypes.bool, selectOnFocus: PropTypes.bool,
spellCheck: PropTypes.bool, spellCheck: PropTypes.bool,

View file

@ -0,0 +1,57 @@
'use strict';
const React = require('react')
const { PureComponent } = React
const { element, string } = require('prop-types')
const cx = require('classnames')
const Icon = ({ children, className, name }) => (
<span className={cx('icon', `icon-${name}`, className)}>
{children}
</span>
)
Icon.propTypes = {
children: element.isRequired,
className: string,
name: string.isRequired
}
module.exports = { Icon }
function i(name, svgOrSrc) {
const icon = class extends PureComponent {
render() {
const { className } = this.props
if (typeof svgOrSrc == 'string') {
if (window.devicePixelRatio >= 0.75) {
let parts = svgOrSrc.split('.');
parts[parts.length-2] = parts[parts.length-2] + '@2x';
svgOrSrc = parts.join('.')
}
return <Icon className={className} name={name.toLowerCase()}><img src={svgOrSrc}/></Icon>
}
return (
<Icon className={className} name={name.toLowerCase()}>{svgOrImg}</Icon>
)
}
}
icon.propTypes = {
className: string
}
icon.displayName = `Icon${name}`
module.exports[icon.displayName] = icon
}
/* eslint-disable max-len */
i('TagSelectorMenu', "chrome://zotero/skin/tag-selector-menu.png")
i('DownChevron', "chrome://zotero/skin/searchbar-dropmarker.png")

View file

@ -4,21 +4,30 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const TagList = require('./tag-selector/tag-list'); const TagList = require('./tag-selector/tag-list');
const Input = require('./form/input'); const Input = require('./form/input');
const { Button } = require('./button');
const { IconTagSelectorMenu } = require('./icons');
class TagSelector extends React.Component { class TagSelector extends React.Component {
render() { render() {
return ( return (
<div className="tag-selector"> <div className="tag-selector">
<TagList { ...this.props } /> <TagList {...this.props} />
<div className="tag-selector-filter-container"> <div className="tag-selector-filter-container">
<Input <Input
type="search" type="search"
value={ this.props.searchString } ref={ref => this.focusTextbox = ref && ref.focus}
onChange={ this.props.onSearch } value={this.props.searchString}
onChange={this.props.onSearch}
className="tag-selector-filter" className="tag-selector-filter"
size="1" size="1"
/> />
<button className="tag-selector-actions" onClick={ ev => this.props.onSettings(ev) } /> <Button
icon={<IconTagSelectorMenu />}
title="zotero.toolbar.actions.label"
className="tag-selector-actions"
isMenu
onClick={ev => this.props.onSettings(ev)}
/>
</div> </div>
</div> </div>
); );
@ -32,12 +41,18 @@ TagSelector.propTypes = {
color: PropTypes.string, color: PropTypes.string,
disabled: PropTypes.bool disabled: PropTypes.bool
})), })),
dragObserver: PropTypes.shape({
onDragOver: PropTypes.func,
onDragExit: PropTypes.func,
onDrop: PropTypes.func
}),
searchString: PropTypes.string, searchString: PropTypes.string,
shouldFocus: PropTypes.bool, shouldFocus: PropTypes.bool,
onSelect: PropTypes.func, onSelect: PropTypes.func,
onTagContext: PropTypes.func, onTagContext: PropTypes.func,
onSearch: PropTypes.func, onSearch: PropTypes.func,
onSettings: PropTypes.func, onSettings: PropTypes.func,
loaded: PropTypes.bool,
}; };
TagSelector.defaultProps = { TagSelector.defaultProps = {

View file

@ -0,0 +1,77 @@
const React = require('react');
const { FormattedMessage } = require('react-intl');
const PropTypes = require('prop-types');
const cx = require('classnames');
class TagList extends React.PureComponent {
renderTag(index) {
const { tags } = this.props;
const tag = index < tags.length ?
tags[index] : {
tag: "",
};
const { onDragOver, onDragExit, onDrop } = this.props.dragObserver;
const className = cx('tag-selector-item', 'zotero-clicky', {
selected: tag.selected,
colored: tag.color,
disabled: tag.disabled
});
let props = {
className,
onClick: ev => !tag.disabled && this.props.onSelect(tag.name, ev),
onContextMenu: ev => this.props.onTagContext(tag, ev),
onDragOver,
onDragExit,
onDrop
};
if (tag.color) {
props['style'] = {
color: tag.color,
};
}
return (
<li key={index} {...props}>
{tag.name}
</li>
);
}
render() {
const totalTagCount = this.props.tags.length;
var tagList = (
<ul className="tag-selector-list">
{
[...Array(totalTagCount).keys()].map(index => this.renderTag(index))
}
</ul>
);
if (!this.props.loaded) {
tagList = (
<div className="tag-selector-message">
<FormattedMessage id="zotero.tagSelector.loadingTags" />
</div>
);
} else if (totalTagCount == 0) {
tagList = (
<div className="tag-selector-message">
<FormattedMessage id="zotero.tagSelector.noTagsToDisplay" />
</div>
);
}
return (
<div
className="tag-selector-container"
ref={ref => { this.container = ref }}>
{tagList}
</div>
)
}
}
module.exports = TagList;

View file

@ -25,16 +25,35 @@
'use strict'; 'use strict';
ZoteroPane.React = { const { defineMessages } = require('react-intl');
init() {
ZoteroPane.Containers = {
async init() {
await this.initIntlStrings();
},
loadPane() {
var tagSelector = document.getElementById('zotero-tag-selector'); var tagSelector = document.getElementById('zotero-tag-selector');
ZoteroPane_Local.tagSelector = ZoteroPane.React.TagSelector.init(tagSelector, { ZoteroPane.tagSelector = Zotero.TagSelector.init(tagSelector, {
onSelection: ZoteroPane_Local.updateTagFilter.bind(ZoteroPane_Local) onSelection: ZoteroPane.updateTagFilter.bind(ZoteroPane)
}); });
}, },
async initIntlStrings() {
this.intlMessages = {};
const intlFiles = ['zotero.dtd'];
for (let intlFile of intlFiles) {
let localeXML = await Zotero.File.getContentsFromURLAsync(`chrome://zotero/locale/${intlFile}`);
let regexp = /<!ENTITY ([^\s]+)\s+"([^"]+)/g;
let regexpResult;
while (regexpResult = regexp.exec(localeXML)) {
this.intlMessages[regexpResult[1]] = regexpResult[2];
}
}
},
destroy() { destroy() {
ZoteroPane_Local.tagSelector.unregister(); ZoteroPane.tagSelector.unregister();
} }
} }

View file

@ -23,13 +23,12 @@
***** END LICENSE BLOCK ***** ***** END LICENSE BLOCK *****
--> -->
<?xml-stylesheet href="chrome://zotero/skin/zotero-react-client.css"?> <?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
<?xul-overlay href="chrome://zotero/content/reactUI/tagSelector.xul"?> <?xul-overlay href="chrome://zotero/content/containers/tagSelector.xul"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://zotero/content/include.js"></script> <script src="chrome://zotero/content/include.js"></script>
<script src="zoteroPane.js"></script> <script src="containers.js"></script>
<script src="tagSelector.js"></script>
<script src="containers/tag-selector.js"></script>
</overlay> </overlay>

View file

@ -0,0 +1,412 @@
/* global Zotero: false */
'use strict';
(function() {
const React = require('react');
const ReactDOM = require('react-dom');
const { IntlProvider } = require('react-intl');
const TagSelector = require('components/tag-selector.js');
const noop = Promise.resolve();
const defaults = {
tagColors: new Map(),
tags: [],
showAutomatic: Zotero.Prefs.get('tagSelector.showAutomatic'),
searchString: '',
inScope: new Set(),
loaded: false
};
const { Cc, Ci } = require('chrome');
Zotero.TagSelector = class TagSelectorContainer extends React.Component {
constructor(props) {
super(props);
this._notifierID = Zotero.Notifier.registerObserver(
this,
['collection-item', 'item', 'item-tag', 'tag', 'setting'],
'tagSelector'
);
this.displayAllTags = Zotero.Prefs.get('tagSelector.displayAllTags');
this.selectedTags = new Set();
this.state = defaults;
}
// Update trigger #1 (triggered by ZoteroPane)
async onItemViewChanged({collectionTreeRow, libraryID, tagsInScope}) {
this.collectionTreeRow = collectionTreeRow || this.collectionTreeRow;
let newState = {loaded: true};
if (!this.state.tagColors.length && libraryID && this.libraryID != libraryID) {
newState.tagColors = Zotero.Tags.getColors(libraryID);
}
this.libraryID = libraryID;
newState.tags = await this.getTags(tagsInScope,
this.state.tagColors.length ? this.state.tagColors : newState.tagColors);
this.setState(newState);
}
// Update trigger #2
async notify(event, type, ids, extraData) {
if (type === 'setting') {
if (ids.some(val => val.split('/')[1] == 'tagColors')) {
let tagColors = Zotero.Tags.getColors(this.libraryID);
this.state.tagColors = tagColors;
this.setState({tagColors, tags: await this.getTags(null, tagColors)});
}
return;
}
// Ignore anything other than deletes in duplicates view
if (this.collectionTreeRow.isDuplicates()) {
switch (event) {
case 'delete':
case 'trash':
break;
default:
return;
}
}
// Ignore item events other than 'trash'
if (type == 'item' && (event == 'trash')) {
return this.setState({tags: await this.getTags()});
}
// If a selected tag no longer exists, deselect it
if (type == 'item-tag') {
if (event == 'delete' || event == 'trash' || event == 'modify') {
for (let tag of this.selectedTags) {
if (tag == extraData[ids[0]].old.tag) {
this.selectedTags.delete(tag);
}
}
}
return this.setState({tags: await this.getTags()});
}
this.setState({tags: await this.getTags()});
}
async getTags(tagsInScope, tagColors) {
if (!tagsInScope) {
tagsInScope = await this.collectionTreeRow.getChildTags();
}
this.inScope = new Set(tagsInScope.map(t => t.tag));
let tags;
if (this.displayAllTags) {
tags = await Zotero.Tags.getAll(this.libraryID, [0, 1]);
} else {
tags = tagsInScope
}
tagColors = tagColors || this.state.tagColors;
// Add colored tags that aren't already real tags
let regularTags = new Set(tags.map(tag => tag.tag));
let coloredTags = Array.from(tagColors.keys());
coloredTags.filter(ct => !regularTags.has(ct)).forEach(x =>
tags.push(Zotero.Tags.cleanData({ tag: x }))
);
// Sort by name
tags.sort(function (a, b) {
let aColored = tagColors.has(a.tag),
bColored = tagColors.has(b.tag);
if (aColored && !bColored) return -1;
if (!aColored && bColored) return 1;
return Zotero.getLocaleCollation().compareString(1, a.tag, b.tag);
});
return tags;
}
render() {
let tags = this.state.tags;
let tagMap = new Map();
if (!this.showAutomatic) {
tags = tags.filter(t => t.type != 1)
} else {
// Ensure no duplicates from auto and manual tags
tags.forEach(t => !tagMap.has() && t.type != 1 && tagMap.set(t.tag, t));
tags = Array.from(tagMap.values());
}
if (this.state.searchString) {
tags = tags.filter(tag => !!tag.tag.match(new RegExp(this.state.searchString, 'i')));
}
tags = tags.map(t => {
let name = t.tag;
return {
name,
selected: this.selectedTags.has(name),
color: this.state.tagColors.has(name) ? this.state.tagColors.get(name).color : '',
disabled: !this.inScope.has(name)
}
});
return <TagSelector
tags={tags}
ref={ref => this.focusTextbox = ref && ref.focusTextbox}
searchString={this.state.searchString}
shouldFocus={this.state.shouldFocus}
dragObserver={this.dragObserver}
onSelect={this.state.viewOnly ? () => {} : this.handleTagSelected}
onTagContext={this.handleTagContext}
onSearch={this.handleSearch}
onSettings={this.handleSettings}
loaded={this.state.loaded}
/>;
}
setMode(mode) {
this.state.viewOnly != (mode == 'view') && this.setState({viewOnly: mode == 'view'});
}
unregister() {
ReactDOM.unmountComponentAtNode(this.domEl);
if (this._notifierID) {
Zotero.Notifier.unregisterObserver(this._notifierID);
}
}
uninit() {
this.setState({searchString: ''});
this.selectedTags = new Set();
}
handleTagContext = (tag, ev) => {
let tagContextMenu = document.getElementById('tag-menu');
ev.preventDefault();
tagContextMenu.openPopup(null, null, ev.clientX+2, ev.clientY+2);
this.contextTag = tag;
}
handleSettings = (ev) => {
let settingsContextMenu = document.getElementById('tag-selector-view-settings-menu');
ev.preventDefault();
settingsContextMenu.openPopup(ev.target, 'end_before', 0, 0, true);
}
handleTagSelected = (tag) => {
let selectedTags = this.selectedTags;
if(selectedTags.has(tag)) {
selectedTags.delete(tag);
} else {
selectedTags.add(tag);
}
if (typeof(this.props.onSelection) === 'function') {
this.props.onSelection(selectedTags);
}
}
handleSearch = Zotero.Utilities.debounce((searchString) => {
this.setState({searchString});
})
dragObserver = {
onDragOver: function(event) {
if (!event.dataTransfer.getData('zotero/item')) {
return;
}
var elem = event.target;
// Ignore drops not on tags
if (elem.localName != 'li') {
return;
}
// Store the event, because drop event does not have shiftKey attribute set
Zotero.DragDrop.currentEvent = event;
elem.classList.add('dragged-over');
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
onDragExit: function (event) {
Zotero.DragDrop.currentEvent = null;
event.target.classList.remove('dragged-over');
},
onDrop: async function(event) {
var elem = event.target;
// Ignore drops not on tags
if (elem.localName != 'li') {
return;
}
elem.classList.remove('dragged-over');
var dt = event.dataTransfer;
var ids = dt.getData('zotero/item');
if (!ids) {
return;
}
return Zotero.DB.executeTransaction(function* () {
ids = ids.split(',');
var items = Zotero.Items.get(ids);
var value = elem.textContent;
for (let i=0; i<items.length; i++) {
let item = items[i];
if (Zotero.DragDrop.currentEvent.shiftKey) {
item.removeTag(value);
} else {
item.addTag(value);
}
yield item.save();
}
}.bind(this));
}
}
getTagSelection() {
return this.selectedTags;
}
clearTagSelection() {
this.selectedTags = new Set();
}
async openColorPickerWindow() {
var io = {
libraryID: this.libraryID,
name: this.contextTag.name
};
var tagColors = this.state.tagColors;
if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) {
var ps = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
ps.alert(null, '', Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS));
return;
}
io.tagColors = tagColors;
window.openDialog(
'chrome://zotero/content/tagColorChooser.xul',
'zotero-tagSelector-colorChooser',
'chrome,modal,centerscreen', io
);
// Dialog cancel
if (typeof io.color == 'undefined') {
return;
}
await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position);
}
async openRenamePrompt() {
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
var newName = { value: this.contextTag.name };
var result = promptService.prompt(window,
Zotero.getString('pane.tagSelector.rename.title'),
Zotero.getString('pane.tagSelector.rename.message'),
newName, '', {});
if (!result || !newName.value || this.contextTag.name == newName.value) {
return;
}
let selectedTags = this.selectedTags;
if (selectedTags.has(this.contextTag.name)) {
var wasSelected = true;
selectedTags.delete(this.contextTag.name);
}
if (Zotero.Tags.getID(this.contextTag.name)) {
await Zotero.Tags.rename(this.libraryID, this.contextTag.name, newName.value);
}
// Colored tags don't need to exist, so in that case
// just rename the color setting
else {
let color = Zotero.Tags.getColor(this.libraryID, this.contextTag.name);
if (!color) {
throw new Error("Can't rename missing tag");
}
await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false);
await Zotero.Tags.setColor(this.libraryID, newName.value, color.color);
}
if (wasSelected) {
selectedTags.add(newName.value);
}
this.setState({tags: await this.getTags()})
}
async openDeletePrompt() {
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
var confirmed = promptService.confirm(window,
Zotero.getString('pane.tagSelector.delete.title'),
Zotero.getString('pane.tagSelector.delete.message'));
if (!confirmed) {
return;
}
var tagID = Zotero.Tags.getID(this.contextTag.name);
if (tagID) {
await Zotero.Tags.removeFromLibrary(this.libraryID, tagID);
}
// If only a tag color setting, remove that
else {
await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false);
}
this.setState({tags: await this.getTags()});
}
async toggleDisplayAllTags(newValue) {
newValue = typeof(newValue) === 'undefined' ? !this.displayAllTags : newValue;
Zotero.Prefs.set('tagSelector.displayAllTags', newValue);
this.displayAllTags = newValue;
this.setState({tags: await this.getTags()});
}
toggleShowAutomatic(newValue) {
newValue = typeof(newValue) === 'undefined' ? !this.showAutomatic : newValue;
Zotero.Prefs.set('tagSelector.showAutomatic', newValue);
this.setState({showAutomatic: newValue});
}
deselectAll() {
this.selectedTags = new Set();
if('onSelection' in this.props && typeof(this.props.onSelection) === 'function') {
this.props.onSelection(this.selectedTags);
}
}
get label() {
let count = this.selectedTags.size;
let mod = count === 1 ? 'singular' : count === 0 ? 'none' : 'plural';
return Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]);
}
get showAutomatic() {
return this.state.showAutomatic;
}
static init(domEl, opts) {
var ref;
let elem = (
<IntlProvider locale={Zotero.locale} messages={ZoteroPane.Containers.intlMessages}>
<TagSelectorContainer ref={c => ref = c } {...opts} />
</IntlProvider>
);
ReactDOM.render(elem, domEl);
ref.domEl = domEl;
return ref;
}
}
})();

View file

@ -30,29 +30,29 @@
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<menupopup id="tag-menu"> <menupopup id="tag-menu">
<menuitem label="&zotero.tagSelector.assignColor;" <menuitem label="&zotero.tagSelector.assignColor;"
oncommand="ZoteroPane_Local.tagSelector.openColorPickerWindow(); event.stopPropagation();"/> oncommand="ZoteroPane.tagSelector.openColorPickerWindow(); event.stopPropagation();"/>
<menuitem label="&zotero.tagSelector.renameTag;" <menuitem label="&zotero.tagSelector.renameTag;"
oncommand="ZoteroPane_Local.tagSelector.openRenamePrompt(); event.stopPropagation();"/> oncommand="ZoteroPane.tagSelector.openRenamePrompt(); event.stopPropagation();"/>
<menuitem label="&zotero.tagSelector.deleteTag;" <menuitem label="&zotero.tagSelector.deleteTag;"
oncommand="ZoteroPane_Local.tagSelector.openDeletePrompt(); event.stopPropagation();"/> oncommand="ZoteroPane.tagSelector.openDeletePrompt(); event.stopPropagation();"/>
</menupopup> </menupopup>
<menupopup id="tag-selector-view-settings-menu" <menupopup id="tag-selector-view-settings-menu"
onpopupshowing=" onpopupshowing="
document.getElementById('show-automatic').setAttribute('checked', ZoteroPane_Local.tagSelector.showAutomatic); document.getElementById('show-automatic').setAttribute('checked', ZoteroPane.tagSelector.showAutomatic);
document.getElementById('display-all-tags').setAttribute('checked', !ZoteroPane_Local.tagSelector.filterToScope); document.getElementById('display-all-tags').setAttribute('checked', ZoteroPane.tagSelector.displayAllTags);
document.getElementById('num-selected').label = ZoteroPane_Local.tagSelector.label"> document.getElementById('num-selected').label = ZoteroPane.tagSelector.label">
<menuitem id="num-selected" disabled="true"/> <menuitem id="num-selected" disabled="true"/>
<menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;" <menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;"
oncommand="ZoteroPane_Local.tagSelector.deselectAll(); event.stopPropagation();"/> oncommand="ZoteroPane.tagSelector.deselectAll(); event.stopPropagation();"/>
<menuseparator/> <menuseparator/>
<menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" type="checkbox" <menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" type="checkbox"
oncommand="ZoteroPane_Local.tagSelector.toggleShowAutomatic(); event.stopPropagation();"/> oncommand="ZoteroPane.tagSelector.toggleShowAutomatic(); event.stopPropagation();"/>
<menuitem <menuitem
id="display-all-tags" id="display-all-tags"
label="&zotero.tagSelector.displayAllInLibrary;" label="&zotero.tagSelector.displayAllInLibrary;"
type="checkbox" type="checkbox"
oncommand="ZoteroPane_Local.tagSelector.toggleFilterToScope(); event.stopPropagation();" oncommand="ZoteroPane.tagSelector.toggleDisplayAllTags(); event.stopPropagation();"
/> />
</menupopup> </menupopup>
</overlay> </overlay>

View file

@ -1,455 +0,0 @@
/* global Zotero: false */
'use strict';
(function() {
const React = require('react');
const ReactDom = require('react-dom');
const TagSelector = require('react-ui/components/tag-selector.js');
const noop = Promise.resolve();
const defaults = {
tags: [],
searchString: '',
shouldFocus: false,
onSelection: noop,
viewOnly: false
};
const { Cc, Ci } = require('chrome');
ZoteroPane.React.TagSelector = class TagSelectorContainer extends React.Component {
constructor(props) {
super(props);
this.opts = Object.assign({}, defaults, props);
this.selectedTags = new Set();
this.updateScope = Promise.resolve;
this.filterToScope = true;
this.showAutomatic = true;
this._notifierID = Zotero.Notifier.registerObserver(
this,
['collection-item', 'item', 'item-tag', 'tag', 'setting'],
'tagSelector'
);
this._scope = null;
this._allTags = null;
this.state = null;
this.update();
}
componentWillReceiveProps(nextProps) {
this.opts = Object.assign({}, this.opts, nextProps);
this.update();
}
async notify(event, type, ids, extraData) {
if (type === 'setting') {
if (ids.some(val => val.split('/')[1] == 'tagColors')) {
await this.refresh(true);
}
return;
}
// Ignore anything other than deletes in duplicates view
if (this.collectionTreeRow.isDuplicates()) {
switch (event) {
case 'delete':
case 'trash':
break;
default:
return;
}
}
// Ignore item events other than 'trash'
if (type == 'item' && event != 'trash') {
return;
}
// If a selected tag no longer exists, deselect it
if (event == 'delete' || event == 'trash' || event == 'modify') {
for (let tag of this.selectedTags) {
if (tag == extraData[ids[0]].old.tag) {
this.selectedTags.delete(tag);
break;
}
}
await this.refresh(true);
}
if (event == 'add') {
if (type == 'item-tag') {
let tagObjs = ids
// Get tag name and type
.map(x => extraData[x])
// Ignore tag adds for items not in the current library, if there is one
.filter(x => {
if (!this._libraryID) {
return true;
}
return x.libraryID == this._libraryID;
});
if (tagObjs.length) {
this.insertSorted(tagObjs);
}
}
}
// Otherwise, just update the tag selector
return this.updateScope();
}
async refresh(fetch) {
let t = new Date;
if(this._allTags === null) {
fetch = true;
}
if (fetch) {
Zotero.debug('Reloading tags selector');
} else {
Zotero.debug('Refreshing tags selector');
}
// If new data, rebuild boxes
if (fetch) {
let tagColors = Zotero.Tags.getColors(this.libraryID);
this._allTags = await Zotero.Tags.getAll(this.libraryID, this.showAutomatic ? [0, 1] : [0]);
// .tap(() => Zotero.Promise.check(this.mode));
// Add colored tags that aren't already real tags
let regularTags = new Set(this._allTags.map(tag => tag.tag));
let coloredTags = Array.from(tagColors.keys());
coloredTags.filter(ct => !regularTags.has(ct)).forEach(x =>
this._allTags.push(Zotero.Tags.cleanData({ tag: x }))
);
// Sort by name
this._allTags.sort(function (a, b) {
return Zotero.getLocaleCollation().compareString(1, a.tag, b.tag);
});
}
this.update();
Zotero.debug('Loaded tag selector in ' + (new Date - t) + ' ms');
// var event = new Event('refresh');
// this.dispatchEvent(event);
}
update() {
var tags;
let flatScopeTags = Array.isArray(this._scope) ? this._scope.map(tag => tag.tag) : [];
if('libraryID' in this) {
var tagColors = Zotero.Tags.getColors(this.libraryID);
}
if(this.filterToScope && Array.isArray(this._scope)) {
tags = this._allTags.filter(tag =>
flatScopeTags.includes(tag.tag) || (tagColors && tagColors.has(tag.tag))
);
} else {
tags = this._allTags ? this._allTags.slice(0) : [];
}
if(this.opts.searchString) {
tags = tags.filter(tag => !!tag.tag.match(new RegExp(this.opts.searchString, 'i')));
}
this.opts.tags = tags.map(t => {
let name = t.tag;
let selected = this.selectedTags.has(name);
let color = tagColors && tagColors.has(name) ? tagColors.get(name).color : '';
let disabled = !flatScopeTags.includes(name);
return { name, selected, color, disabled };
});
this.state ? this.setState(this.opts) : this.state = Object.assign({}, this.opts);
}
render() {
return <TagSelector
tags={ this.state.tags }
searchString={ this.state.searchString }
shouldFocus={ this.state.shouldFocus }
onSelect={ this.state.viewOnly ? () => {} : this.onTagSelectedHandler.bind(this) }
onTagContext={ this.onTagContextHandler.bind(this) }
onSearch={ this.onSearchHandler.bind(this) }
onSettings={ this.onTagSelectorViewSettingsHandler.bind(this) }
/>;
}
setMode(mode) {
this.setState({viewOnly: mode == 'view'});
}
focusTextbox() {
this.opts.shouldFocus = true;
this.update();
}
toggle() {
this._isCollapsed = !this._isCollapsed;
}
unregister() {
ReactDom.unmountComponentAtNode(this.domEl);
if (this._notifierID) {
Zotero.Notifier.unregisterObserver(this._notifierID);
}
}
uninit() {
this.selectedTags = new Set();
this.opts.searchString = '';
this.update();
}
onTagContextHandler(tag, ev) {
let tagContextMenu = document.getElementById('tag-menu');
ev.preventDefault();
tagContextMenu.openPopup(ev.target, 'end_before', 0, 0, true);
this.contextTag = tag;
}
onTagSelectorViewSettingsHandler(ev) {
let settingsContextMenu = document.getElementById('tag-selector-view-settings-menu');
ev.preventDefault();
settingsContextMenu.openPopup(ev.target, 'end_before', 0, 0, true);
}
onTagSelectedHandler(tag) {
if(this.selectedTags.has(tag)) {
this.selectedTags.delete(tag);
} else {
this.selectedTags.add(tag);
}
if('onSelection' in this.opts && typeof(this.opts.onSelection) === 'function') {
this.opts.onSelection(this.selectedTags).then(this.refresh.bind(this));
}
}
onSearchHandler(searchString) {
this.opts.searchString = searchString;
this.update();
}
getTagSelection() {
return this.selectedTags;
}
clearTagSelection() {
this.selectedTags = new Set();
return this.selectedTags;
}
async openColorPickerWindow() {
var io = {
libraryID: this.libraryID,
name: this.contextTag.name
};
var tagColors = Zotero.Tags.getColors(this.libraryID);
if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) {
var ps = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
ps.alert(null, '', Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS));
return;
}
io.tagColors = tagColors;
window.openDialog(
'chrome://zotero/content/tagColorChooser.xul',
'zotero-tagSelector-colorChooser',
'chrome,modal,centerscreen', io
);
// Dialog cancel
if (typeof io.color == 'undefined') {
return;
}
await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position);
this.refresh();
}
async openRenamePrompt() {
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
var newName = { value: this.contextTag.name };
var result = promptService.prompt(window,
Zotero.getString('pane.tagSelector.rename.title'),
Zotero.getString('pane.tagSelector.rename.message'),
newName, '', {});
if (!result || !newName.value || this.contextTag.name == newName.value) {
return;
}
if (this.selectedTags.has(this.contextTag.name)) {
var wasSelected = true;
this.selectedTags.delete(this.contextTag.name);
}
if (Zotero.Tags.getID(this.contextTag.name)) {
await Zotero.Tags.rename(this.libraryID, this.contextTag.name, newName.value);
}
// Colored tags don't need to exist, so in that case
// just rename the color setting
else {
let color = Zotero.Tags.getColor(this.libraryID, this.contextTag.name);
if (!color) {
throw new Error("Can't rename missing tag");
}
await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false);
await Zotero.Tags.setColor(this.libraryID, newName, color);
}
if(wasSelected) {
this.selectedTags.add(newName.value);
}
this.updateScope();
}
async openDeletePrompt() {
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
var confirmed = promptService.confirm(window,
Zotero.getString('pane.tagSelector.delete.title'),
Zotero.getString('pane.tagSelector.delete.message'));
if (!confirmed) {
return;
}
var tagID = Zotero.Tags.getID(this.contextTag.name);
if (tagID) {
await Zotero.Tags.removeFromLibrary(this.libraryID, tagID);
}
// If only a tag color setting, remove that
else {
await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false);
}
this.updateScope();
}
toggleFilterToScope(newValue) {
this.filterToScope = typeof(newValue) === 'undefined' ? !this.filterToScope : newValue;
this.refresh();
}
toggleShowAutomatic(newValue) {
this.showAutomatic = typeof(newValue) === 'undefined' ? !this.showAutomatic : newValue;
this.refresh(true);
}
deselectAll() {
this.selectedTags = new Set();
if('onSelection' in this.opts && typeof(this.opts.onSelection) === 'function') {
this.opts.onSelection(this.selectedTags).then(this.refresh.bind(this));
}
}
set scope(newScope) {
try {
this._scope = Array.from(newScope);
} catch(e) {
this._scope = null;
}
this.refresh();
}
get label() {
let count = this.selectedTags.size;
let mod = count === 1 ? 'singular' : count === 0 ? 'none' : 'plural';
return Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]);
}
onResize() {
const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar
//Zotero.debug('Updating tag selector size');
var zoteroPane = document.getElementById('zotero-pane-stack');
var splitter = document.getElementById('zotero-tags-splitter');
var tagSelector = document.getElementById('zotero-tag-selector');
// Nothing should be bigger than appcontent's height
var max = document.getElementById('appcontent').boxObject.height
- splitter.boxObject.height;
// Shrink tag selector to appcontent's height
var maxTS = max - COLLECTIONS_HEIGHT;
var tsHeight = parseInt(tagSelector.getAttribute("height"));
if (tsHeight > maxTS) {
//Zotero.debug("Limiting tag selector height to appcontent");
tagSelector.setAttribute('height', maxTS);
}
tagSelector.style.height = tsHeight + 'px';
var height = tagSelector.getBoundingClientRect().height;
/*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height);
Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height'));
Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height);
Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/
// Don't let the Z-pane jump back down to its previous height
// (if shrinking or hiding the tag selector let it clear the min-height)
if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) {
//Zotero.debug("Setting Zotero pane height attribute to " + zoteroPane.boxObject.height);
zoteroPane.setAttribute('height', zoteroPane.boxObject.height);
}
if (tagSelector.getAttribute('collapsed') == 'true') {
// 32px is the default Z pane min-height in overlay.css
height = 32;
}
else {
// tS.boxObject.height doesn't exist at startup, so get from attribute
if (!height) {
height = parseInt(tagSelector.getAttribute('height'));
}
// 121px seems to be enough room for the toolbar and collections
// tree at minimum height
height = height + COLLECTIONS_HEIGHT;
}
//Zotero.debug('Setting Zotero pane minheight to ' + height);
zoteroPane.setAttribute('minheight', height);
if (this.isShowing() && !this.isFullScreen()) {
zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height);
}
// Fix bug whereby resizing the Z pane downward after resizing
// the tag selector up and then down sometimes caused the Z pane to
// stay at a fixed size and get pushed below the bottom
tagSelector.height++;
tagSelector.height--;
}
static init(domEl, opts) {
var ref;
console.log(domEl.style);
ReactDom.render(<TagSelectorContainer ref={c => ref = c } {...opts} />, domEl);
ref.domEl = domEl;
new MutationObserver(ref.onResize).observe(domEl, {attributes: true, attributeFilter: ['height']});
return ref;
}
}
})();

View file

@ -47,7 +47,7 @@ const ZoteroStandalone = new function() {
} }
return Zotero.initializationPromise; return Zotero.initializationPromise;
}) })
.then(function () { .then(async function () {
if (Zotero.Prefs.get('devtools.errorconsole.enabled', true)) { if (Zotero.Prefs.get('devtools.errorconsole.enabled', true)) {
document.getElementById('menu_errorConsole').hidden = false; document.getElementById('menu_errorConsole').hidden = false;
} }
@ -60,6 +60,7 @@ const ZoteroStandalone = new function() {
ZoteroStandalone.DebugOutput.init(); ZoteroStandalone.DebugOutput.init();
Zotero.hideZoteroPaneOverlays(); Zotero.hideZoteroPaneOverlays();
await ZoteroPane.Containers.init();
ZoteroPane.init(); ZoteroPane.init();
ZoteroPane.makeVisible(); ZoteroPane.makeVisible();

View file

@ -1850,7 +1850,7 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i
// Clear the quick search and tag selection and try again (once) // Clear the quick search and tag selection and try again (once)
if (!noRecurse && this.window.ZoteroPane) { if (!noRecurse && this.window.ZoteroPane) {
let cleared1 = yield this.window.ZoteroPane.clearQuicksearch(); let cleared1 = yield this.window.ZoteroPane.clearQuicksearch();
let cleared2 = this.window.ZoteroPane.clearTagSelection(); let cleared2 = this.window.ZoteroPane.tagSelector.clearTagSelection();
if (cleared1 || cleared2) { if (cleared1 || cleared2) {
return this.selectItem(id, expand, true); return this.selectItem(id, expand, true);
} }

View file

@ -73,7 +73,6 @@ Zotero.Notifier = new function(){
if (priority) { if (priority) {
msg += " with priority " + priority; msg += " with priority " + priority;
} }
Zotero.debug(msg);
_observers[hash] = { _observers[hash] = {
ref: ref, ref: ref,
types: types, types: types,

View file

@ -144,6 +144,25 @@ var CSL_TYPE_MAPPINGS = {
* @class Functions for text manipulation and other miscellaneous purposes * @class Functions for text manipulation and other miscellaneous purposes
*/ */
Zotero.Utilities = { Zotero.Utilities = {
/**
* Returns a function which will execute `fn` with provided arguments after `delay` milliseconds and not more
* than once, if called multiple times. See
* http://stackoverflow.com/questions/24004791/can-someone-explain-the-debounce-function-in-javascript
* @param fn {Function} function to debounce
* @param delay {Integer} number of miliseconds to delay the function execution
* @returns {Function}
*/
debounce: function(fn, delay=500) {
var timer = null;
return function () {
let args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(this, args);
}.bind(this), delay);
};
},
/** /**
* Fixes author name capitalization. * Fixes author name capitalization.
* Currently for all uppercase names only * Currently for all uppercase names only

View file

@ -143,6 +143,8 @@ var ZoteroPane = new function()
Zotero.hiDPI = window.devicePixelRatio > 1; Zotero.hiDPI = window.devicePixelRatio > 1;
Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : ""; Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
ZoteroPane_Local.Containers.loadPane();
ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
// Add a default progress window // Add a default progress window
@ -233,7 +235,6 @@ var ZoteroPane = new function()
} }
catch (e) {} catch (e) {}
} }
ZoteroPane.React.init();
if (Zotero.openPane) { if (Zotero.openPane) {
Zotero.openPane = false; Zotero.openPane = false;
@ -345,7 +346,7 @@ var ZoteroPane = new function()
this.serializePersist(); this.serializePersist();
} }
ZoteroPane_Local.React.destroy(); ZoteroPane_Local.Containers.destroy();
if(this.collectionsView) this.collectionsView.unregister(); if(this.collectionsView) this.collectionsView.unregister();
if(this.itemsView) this.itemsView.unregister(); if(this.itemsView) this.itemsView.unregister();
@ -388,7 +389,6 @@ var ZoteroPane = new function()
this.unserializePersist(); this.unserializePersist();
this.updateLayout(); this.updateLayout();
this.updateToolbarPosition(); this.updateToolbarPosition();
this.updateTagSelectorSize();
// restore saved row selection (for tab switching) // restore saved row selection (for tab switching)
// TODO: Remove now that no tab mode? // TODO: Remove now that no tab mode?
@ -1093,7 +1093,6 @@ var ZoteroPane = new function()
var showing = tagSelector.getAttribute('collapsed') == 'true'; var showing = tagSelector.getAttribute('collapsed') == 'true';
tagSelector.setAttribute('collapsed', !showing); tagSelector.setAttribute('collapsed', !showing);
this.updateTagSelectorSize();
// If showing, set scope to items in current view // If showing, set scope to items in current view
// and focus filter textbox // and focus filter textbox
@ -1107,12 +1106,6 @@ var ZoteroPane = new function()
} }
}); });
this.updateTagSelectorSize = function () {
}
/* /*
* Sets the tag filter on the items view * Sets the tag filter on the items view
*/ */
@ -1139,7 +1132,6 @@ var ZoteroPane = new function()
*/ */
this.setTagScope = async function () { this.setTagScope = async function () {
var collectionTreeRow = self.getCollectionTreeRow(); var collectionTreeRow = self.getCollectionTreeRow();
var tagSelector = document.getElementById('zotero-tag-selector');
if (self.tagSelectorShown()) { if (self.tagSelectorShown()) {
Zotero.debug('Updating tag selector with current tags'); Zotero.debug('Updating tag selector with current tags');
if (collectionTreeRow.editable) { if (collectionTreeRow.editable) {
@ -1148,10 +1140,11 @@ var ZoteroPane = new function()
else { else {
ZoteroPane_Local.tagSelector.setMode('view'); ZoteroPane_Local.tagSelector.setMode('view');
} }
ZoteroPane_Local.tagSelector.collectionTreeRow = collectionTreeRow; ZoteroPane_Local.tagSelector.onItemViewChanged({
ZoteroPane_Local.tagSelector.updateScope = self.setTagScope; collectionTreeRow,
ZoteroPane_Local.tagSelector.libraryID = collectionTreeRow.ref.libraryID; libraryID: collectionTreeRow.ref.libraryID,
ZoteroPane_Local.tagSelector.scope = await collectionTreeRow.getChildTags(); tagsInScope: await collectionTreeRow.getChildTags()
});
} }
}; };
@ -1219,17 +1212,7 @@ var ZoteroPane = new function()
ZoteroPane_Local.displayErrorMessage(); ZoteroPane_Local.displayErrorMessage();
}; };
this.itemsView.onRefresh.addListener(() => this.setTagScope()); this.itemsView.onRefresh.addListener(() => this.setTagScope());
if (this.tagSelectorShown()) { this.itemsView.onLoad.addListener(() => Zotero.uiIsReady());
let tagSelector = document.getElementById('zotero-tag-selector')
let handler = function () {
tagSelector.removeEventListener('refresh', handler);
Zotero.uiIsReady();
};
tagSelector.addEventListener('refresh', handler);
}
else {
this.itemsView.onLoad.addListener(() => Zotero.uiIsReady());
}
// If item data not yet loaded for library, load it now. // If item data not yet loaded for library, load it now.
// Other data types are loaded at startup // Other data types are loaded at startup
@ -4835,8 +4818,10 @@ var ZoteroPane = new function()
var itemsToolbar = document.getElementById("zotero-items-toolbar"); var itemsToolbar = document.getElementById("zotero-items-toolbar");
var itemPane = document.getElementById("zotero-item-pane"); var itemPane = document.getElementById("zotero-item-pane");
var itemToolbar = document.getElementById("zotero-item-toolbar"); var itemToolbar = document.getElementById("zotero-item-toolbar");
var tagSelector = document.getElementById("zotero-tag-selector");
collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px'; collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px';
tagSelector.style.maxWidth = collectionsPane.boxObject.width + 'px';
if (stackedLayout || itemPane.collapsed) { if (stackedLayout || itemPane.collapsed) {
// The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing). // The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing).

View file

@ -27,7 +27,7 @@
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?> <?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?> <?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
<?xul-overlay href="chrome://zotero/content/reactUI/zoteroPane.xul"?> <?xul-overlay href="chrome://zotero/content/containers/containers.xul"?>
<!DOCTYPE overlay [ <!DOCTYPE overlay [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD; <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
@ -316,7 +316,9 @@
TODO: deal with this some other way? TODO: deal with this some other way?
--> -->
<html:div id="zotero-tag-selector" zotero-persist="height,collapsed,showAutomatic,filterToScope"/> <vbox id="zotero-tag-selector-container" zotero-persist="height,collapsed">
<html:div id="zotero-tag-selector"/>
</vbox>
</vbox> </vbox>
<splitter id="zotero-collections-splitter" resizebefore="closest" resizeafter="closest" collapse="before" <splitter id="zotero-collections-splitter" resizebefore="closest" resizeafter="closest" collapse="before"

View file

@ -171,14 +171,15 @@ label.zotero-text-link {
background-image: url('chrome://zotero/skin/plus-active.png') !important; background-image: url('chrome://zotero/skin/plus-active.png') !important;
} }
.zotero-clicky:not([disabled=true]):hover .zotero-clicky:not([disabled=true]):not(.disabled):hover
{ {
background: rgb(187, 206, 241); background: rgb(187, 206, 241);
border: 1px solid rgb(109, 149, 224); border: 1px solid rgb(109, 149, 224);
} }
.zotero-clicky:not([disabled=true]):active, .zotero-clicky:not([disabled=true]):not(.disabled):active,
.zotero-clicky[selected="true"] .zotero-clicky[selected="true"],
.zotero-clicky.selected
{ {
color: white; color: white;
background: rgb(89, 139, 236); background: rgb(89, 139, 236);

View file

@ -69,8 +69,10 @@ pref("extensions.zotero.lastLongTagDelimiter", ";");
pref("extensions.zotero.fallbackSort", "firstCreator,date,title,dateAdded"); pref("extensions.zotero.fallbackSort", "firstCreator,date,title,dateAdded");
pref("extensions.zotero.sortCreatorAsString", false); pref("extensions.zotero.sortCreatorAsString", false);
//Tag Cloud
pref("extensions.zotero.tagCloud", false); //Tag Selector
pref("extensions.zotero.tagSelector.showAutomatic", true);
pref("extensions.zotero.tagSelector.displayAllTags", false);
// Keyboard shortcuts // Keyboard shortcuts
pref("extensions.zotero.keys.openZotero", "Z"); pref("extensions.zotero.keys.openZotero", "Z");

962
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,11 +18,12 @@
"bluebird": "^3.5.1", "bluebird": "^3.5.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^15.6.2", "react": "^16.6.3",
"react-dom": "^15.6.2" "react-dom": "^16.6.3",
"react-intl": "^2.7.2"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.3",
"babel-plugin-syntax-async-generators": "^6.13.0", "babel-plugin-syntax-async-generators": "^6.13.0",
"babel-plugin-syntax-decorators": "^6.13.0", "babel-plugin-syntax-decorators": "^6.13.0",
"babel-plugin-syntax-do-expressions": "^6.13.0", "babel-plugin-syntax-do-expressions": "^6.13.0",
@ -31,7 +32,7 @@
"babel-plugin-syntax-jsx": "^6.13.0", "babel-plugin-syntax-jsx": "^6.13.0",
"babel-plugin-syntax-object-rest-spread": "^6.13.0", "babel-plugin-syntax-object-rest-spread": "^6.13.0",
"babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-preset-react": "^6.16.0", "babel-preset-react": "^6.16.0",
"browserify": "^14.5.0", "browserify": "^14.5.0",
"chai": "^4.1.2", "chai": "^4.1.2",
@ -45,7 +46,7 @@
"jspath": "^0.4.0", "jspath": "^0.4.0",
"mocha": "^3.5.3", "mocha": "^3.5.3",
"multimatch": "^2.1.0", "multimatch": "^2.1.0",
"node-sass": "^4.9.0", "node-sass": "^4.11.0",
"sinon": "^4.5.0", "sinon": "^4.5.0",
"universalify": "^0.1.1" "universalify": "^0.1.1"
} }

View file

@ -1 +1 @@
../node_modules/react-dom/dist/react-dom.js ../node_modules/react-dom/umd/react-dom.development.js

1
resource/react-intl.js vendored Symbolic link
View file

@ -0,0 +1 @@
../node_modules/react-intl/dist/react-intl.js

View file

@ -1,55 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const cx = require('classnames');
class TagList extends React.PureComponent {
renderTag(index) {
const { tags } = this.props;
const tag = index < tags.length ?
tags[index] : {
tag: "",
};
const className = cx('tag-selector-item', {
selected: tag.selected,
colored: tag.color,
});
let props = {
className,
onClick: ev => this.props.onSelect(tag.name, ev),
onContextMenu: ev => this.props.onTagContext(tag, ev),
};
if(tag.color) {
props['style'] = {
color: tag.color,
};
}
return (
<li key={ index } { ...props }>
{ tag.name }
</li>
);
}
render() {
const totalTagCount = this.props.tags.length;
return (
<div
className="tag-selector-container"
ref={ ref => { this.container = ref } }>
<ul className="tag-selector-list">
{
[...Array(totalTagCount).keys()].map(index => this.renderTag(index))
}
</ul>
</div>
)
}
}
module.exports = TagList;

2
resource/react.js vendored
View file

@ -1 +1 @@
../node_modules/react/dist/react.js ../node_modules/react/umd/react.development.js

View file

@ -8,9 +8,9 @@ var require = (function() {
var { Loader, Require, Module } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js'); var { Loader, Require, Module } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js');
var requirer = Module('/', '/'); var requirer = Module('/', '/');
var _runningTimers = {}; var _runningTimers = {};
var window = {}; var win = {};
window.setTimeout = function (func, ms) { win.setTimeout = function (func, ms) {
var id = Math.floor(Math.random() * (1000000000000 - 1)) + 1 var id = Math.floor(Math.random() * (1000000000000 - 1)) + 1
var useMethodjit = Components.utils.methodjit; var useMethodjit = Components.utils.methodjit;
var timer = Components.classes["@mozilla.org/timer;1"] var timer = Components.classes["@mozilla.org/timer;1"]
@ -47,7 +47,7 @@ var require = (function() {
return id; return id;
}; };
window.clearTimeout = function (id) { win.clearTimeout = function (id) {
var timer = _runningTimers[id]; var timer = _runningTimers[id];
if (timer) { if (timer) {
timer.cancel(); timer.cancel();
@ -55,7 +55,7 @@ var require = (function() {
delete _runningTimers[id]; delete _runningTimers[id];
}; };
window.debug = function (msg) { win.debug = function (msg) {
dump(msg + "\n\n"); dump(msg + "\n\n");
}; };
@ -83,15 +83,18 @@ var require = (function() {
document: typeof document !== 'undefined' && document || {}, document: typeof document !== 'undefined' && document || {},
console: cons, console: cons,
navigator: typeof navigator !== 'undefined' && navigator || {}, navigator: typeof navigator !== 'undefined' && navigator || {},
window, setTimeout: win.setTimeout,
setTimeout: window.setTimeout, clearTimeout: win.clearTimeout,
clearTimeout: window.clearTimeout,
}; };
Object.defineProperty(globals, 'Zotero', { get: getZotero }); Object.defineProperty(globals, 'Zotero', { get: getZotero });
Object.defineProperty(globals, 'window', { get: function() {
return typeof window != 'undefined' ? window : win;
} });
var loader = Loader({ var loader = Loader({
id: 'zotero/require', id: 'zotero/require',
paths: { paths: {
'': 'resource://zotero/', '': 'resource://zotero/',
'components/': 'chrome://zotero/content/components/'
}, },
globals globals
}); });

View file

@ -26,9 +26,14 @@ async function babelWorker(ev) {
try { try {
let contents = await fs.readFile(sourcefile, 'utf8'); let contents = await fs.readFile(sourcefile, 'utf8');
if (sourcefile === 'resource/react-dom.js') { if (sourcefile === 'resource/react.js') {
// patch react // patch react
transformed = contents.replace(/ownerDocument\.createElement\((.*?)\)/gi, 'ownerDocument.createElementNS(DOMNamespaces.html, $1)') transformed = contents.replace('instanceof Error', '.constructor.name == "Error"')
} else if (sourcefile === 'resource/react-dom.js') {
// and react-dom
transformed = contents.replace(/ ownerDocument\.createElement\((.*?)\)/gi, 'ownerDocument.createElementNS(HTML_NAMESPACE, $1)')
.replace('element instanceof win.HTMLIFrameElement',
'typeof element != "undefined" && element.tagName.toLowerCase() == "iframe"')
.replace("isInputEventSupported = false", 'isInputEventSupported = true'); .replace("isInputEventSupported = false", 'isInputEventSupported = true');
} else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) { } else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) {
transformed = contents; transformed = contents;

View file

@ -16,6 +16,7 @@ if (require.main === module) {
const symlinks = symlinkFiles const symlinks = symlinkFiles
.concat(dirs.map(d => `${d}/**`)) .concat(dirs.map(d => `${d}/**`))
.concat([`!${formatDirsForMatcher(dirs)}/**/*.js`]) .concat([`!${formatDirsForMatcher(dirs)}/**/*.js`])
.concat([`!${formatDirsForMatcher(dirs)}/**/*.jsx`])
.concat([`!${formatDirsForMatcher(copyDirs)}/**`]) .concat([`!${formatDirsForMatcher(copyDirs)}/**`])
const signatures = await getSignatures(); const signatures = await getSignatures();

View file

@ -4,7 +4,6 @@ const dirs = [
'components', 'components',
'defaults', 'defaults',
'resource', 'resource',
'resource/web-library',
'test', 'test',
'test/resource/chai', 'test/resource/chai',
'test/resource/chai-as-promised', 'test/resource/chai-as-promised',

View file

@ -27,7 +27,7 @@ async function getJS(source, options, signatures) {
var f; var f;
while ((f = matchingJSFiles.pop()) != null) { while ((f = matchingJSFiles.pop()) != null) {
const newFileSignature = await getFileSignature(f); const newFileSignature = await getFileSignature(f);
const dest = path.join('build', f); const dest = path.join('build', f.replace('.jsx', '.js'));
f = path.normalize(f); f = path.normalize(f);
if (f in signatures) { if (f in signatures) {
if (compareSignatures(newFileSignature, signatures[f])) { if (compareSignatures(newFileSignature, signatures[f])) {

View file

@ -28,7 +28,8 @@ async function getSass(source, options, signatures={}) {
if (compareSignatures(newFileSignature, signatures[f])) { if (compareSignatures(newFileSignature, signatures[f])) {
try { try {
await fs.access(dest, fs.constants.F_OK); await fs.access(dest, fs.constants.F_OK);
continue; // TODO: Doesn't recompile on partial scss file changes, so temporarily disabled
// continue;
} catch (_) { } catch (_) {
// file does not exists in build, fallback to browserifing // file does not exists in build, fallback to browserifing
} }

View file

@ -34,6 +34,7 @@ const source = [
const symlinks = symlinkFiles const symlinks = symlinkFiles
.concat(dirs.map(d => `${d}/**`)) .concat(dirs.map(d => `${d}/**`))
.concat([`!${formatDirsForMatcher(dirs)}/**/*.js`]) .concat([`!${formatDirsForMatcher(dirs)}/**/*.js`])
.concat([`!${formatDirsForMatcher(dirs)}/**/*.jsx`])
.concat([`!${formatDirsForMatcher(copyDirs)}/**`]); .concat([`!${formatDirsForMatcher(copyDirs)}/**`]);
var signatures; var signatures;

View file

@ -46,23 +46,27 @@ $headings-line-height: 1.2;
// Components // Components
// -------------------------------------------------- // --------------------------------------------------
$padding-x-sm: $space-sm; $padding-base-vertical: 2px;
$padding-x-md: $space-md;
$default-padding-x: $space-sm; $padding-base-horizontal: $space-xs;
$default-padding-x-touch: $space-md; $padding-large-horizontal: $space-sm;
$border-radius: 4px; $border-radius-small: 3px;
$border-radius-sm: 3px; $border-radius-base: 4px;
$border-radius-lg: 6px; $border-radius-large: 6px;
$border-width: 1px; $border-width: 1px;
$separator-width: 1px; $separator-width: 1px;
// Z-index master list // Buttons
// -------------------------------------------------- // --------------------------------------------------
$btn-icon-padding: $space-min + 1px;
$btn-disabled-opacity: 0.5;
// Z-index master list
// --------------------------------------------------
$z-index-mobile-nav: 0; $z-index-mobile-nav: 0;
$z-index-navbar-bg: 10; $z-index-navbar-bg: 10;

View file

@ -0,0 +1,42 @@
//
// Button
// --------------------------------------------------
.btn {
font: {
family: inherit;
size: inherit;
}
line-height: inherit;
color: inherit;
text-align: center;
-moz-appearance: toolbarbutton;
&[disabled],
&.disabled {
opacity: $btn-disabled-opacity;
}
}
.btn-icon {
padding: $btn-icon-padding;
.icon {
&:first-child {
margin-left: -5px;
}
&:last-child {
margin-right: -5px;
}
}
svg, img {
vertical-align: middle;
}
span.menu-marker {
-moz-appearance: toolbarbutton-dropdown;
display: inline-block;
vertical-align: middle;
margin-right: -5px;
}
}

View file

@ -2,23 +2,26 @@
// Tag selector // Tag selector
// -------------------------------------------------- // --------------------------------------------------
.library .tag-selector {
height: 160px;
}
.tag-selector { .tag-selector {
display: flex; display: flex;
flex: 1 0 100%; flex: 1 0;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background-color: $tag-selector-bg;
} }
.tag-selector-container { .tag-selector-container {
flex: 1 1 auto; flex: 1 1 auto;
justify-content: space-between; justify-content: space-between;
overflow: auto; overflow: auto;
height: auto; height: 100px;
background-color: $tag-selector-bg;
}
.tag-selector-message {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
} }
.tag-selector-filter-container { .tag-selector-filter-container {
@ -26,7 +29,7 @@
flex: 0 0 1em; flex: 0 0 1em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 0.25em; padding: 0.125em 0 0.125em 0.5em;
} }
.tag-selector-list { .tag-selector-list {
@ -42,21 +45,21 @@
} }
.tag-selector-actions { .tag-selector-actions {
flex: 0 0 32px; flex: 0 0 42px;
display: block;
white-space: nowrap;
background-color: inherit;
} }
.tag-selector-item { .tag-selector-item {
border-radius: 1em;
border: 1px solid transparent;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
padding: .05em .5em; margin: .15em .05em .15em .3em;
margin: 0; padding: 0 .25em 0 .25em;
max-width: 250px;
&.selected { overflow: hidden;
background-color: $secondary; text-overflow: ellipsis;
border: 1px solid $secondary; white-space: nowrap;
}
&.colored { &.colored {
font-weight: bold; font-weight: bold;
@ -64,14 +67,17 @@
&.disabled { &.disabled {
opacity: .6; opacity: .6;
cursor: auto; cursor: default;
pointer-events: none;
} }
&.dragged-over {
color: $shade-0;
background: $shade-6;
}
}
&:not(.disabled):hover { #zotero-tag-selector-container {
background-color: lighten($secondary, 15%); display: flex;
border: 1px solid $secondary;
}
} }
#zotero-tag-selector { #zotero-tag-selector {

View file

@ -0,0 +1,20 @@
//
// Tag selector
// --------------------------------------------------
.tag-selector-filter-container {
padding: 0.25em 0 0.25em 0.5em;
border-top: 1px solid $shade-3;
}
.tag-selector-filter {
-moz-appearance: searchfield;
height: 24px;
}
.tag-selector-actions {
flex: none;
border: 0;
margin-right: 3px;
padding: 0 6px;
}

View file

@ -7,7 +7,7 @@
// -------------------------------------------------- // --------------------------------------------------
$red: #cc2936; $red: #cc2936;
$blue: #2e4de5; // 230° 80 90 $blue: rgb(89, 139, 236);
$asphalt: #7a8799; $asphalt: #7a8799;
$asphalt-light: #dadee3; $asphalt-light: #dadee3;
@ -69,6 +69,13 @@ $main-bg: $shade-0;
// Sidebar // Sidebar
$sidebar-bg: $shade-1; $sidebar-bg: $shade-1;
// Clicky
$clicky-hover-bg-color: rgb(187, 206, 241);
$clicky-active-bg-color: $secondary;
$clicky-border-radius: 6px;
$clicky-border-color: rgb(109, 149, 224);
$clicky-active-color: $shade-0;
// Components // Components
// -------------------------------------------------- // --------------------------------------------------
@ -129,20 +136,10 @@ $metadata-separator-color: $shade-3;
// Button // Button
$btn-primary-color: $shade-0; $btn-primary-color: $shade-0;
$btn-primary-bg: $asphalt; $btn-primary-bg: $asphalt;
$btn-secondary-color: $secondary;
$btn-default-color: $text-color; $btn-default-color: $text-color;
$btn-border: $shade-3;
$btn-default-bg: $shade-0; $btn-default-bg: $shade-0;
$btn-default-active-color: rgba($btn-default-color, 0.5); $btn-default-active-color: rgba($btn-default-color, 0.5);
$btn-icon-bg: $transparent;
$btn-icon-focus-color: $shade-0;
$btn-icon-focus-bg: $asphalt;
$btn-icon-focus-active-color: rgba($shade-0, 0.5);
$btn-icon-active-color: $shade-7;
$btn-icon-active-bg: $shade-2;
$twisty-color: $shade-6;
$twisty-focus-color: $asphalt;
$twisty-selected-color: $shade-7;
$twisty-dnd-target-color: $shade-0;
// Forms // Forms
$input-color: $text-color; $input-color: $text-color;

View file

@ -21,3 +21,4 @@
// -------------------------------------------------- // --------------------------------------------------
@import "components/tag-selector"; @import "components/tag-selector";
@import "components/button";

View file

@ -213,12 +213,15 @@ var waitForTagSelector = function (win) {
var zp = win.ZoteroPane; var zp = win.ZoteroPane;
var deferred = Zotero.Promise.defer(); var deferred = Zotero.Promise.defer();
if (zp.tagSelectorShown()) { if (zp.tagSelectorShown()) {
var tagSelector = win.document.getElementById('zotero-tag-selector'); let tagSelector = zp.tagSelector;
var onRefresh = () => { let componentDidUpdate = tagSelector.componentDidUpdate;
tagSelector.removeEventListener('refresh', onRefresh); tagSelector.componentDidUpdate = function() {
deferred.resolve(); deferred.resolve();
}; tagSelector.componentDidUpdate = componentDidUpdate;
tagSelector.addEventListener('refresh', onRefresh); if (typeof componentDidUpdate == 'function') {
componentDidUpdate.call(this, arguments);
}
}
} }
else { else {
deferred.resolve(); deferred.resolve();

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
describe("Tag Selector", function () { describe("Tag Selector", function () {
var win, doc, collectionsView, tagSelector; var win, doc, collectionsView, tagSelectorElem, tagSelector;
var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) { var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) {
var tagColors = Zotero.Tags.getColors(libraryID); var tagColors = Zotero.Tags.getColors(libraryID);
@ -11,27 +11,13 @@ describe("Tag Selector", function () {
}); });
function getColoredTags() { function getColoredTags() {
var tagsBox = tagSelector.id('tags-box'); var elems = Array.from(tagSelectorElem.querySelectorAll('.tag-selector-item.colored'));
var elems = tagsBox.getElementsByTagName('button'); return elems.map(elem => elem.textContent);
var names = [];
for (let i = 0; i < elems.length; i++) {
if (elems[i].style.order < 0) {
names.push(elems[i].textContent);
}
}
return names;
} }
function getRegularTags() { function getRegularTags() {
var tagsBox = tagSelector.id('tags-box'); var elems = Array.from(tagSelectorElem.querySelectorAll('.tag-selector-item:not(.colored)'));
var elems = tagsBox.getElementsByTagName('button'); return elems.map(elem => elem.textContent);
var names = [];
for (let i = 0; i < elems.length; i++) {
if (elems[i].style.order >= 0 && elems[i].style.display != 'none') {
names.push(elems[i].textContent);
}
}
return names;
} }
@ -39,7 +25,8 @@ describe("Tag Selector", function () {
win = yield loadZoteroPane(); win = yield loadZoteroPane();
doc = win.document; doc = win.document;
collectionsView = win.ZoteroPane.collectionsView; collectionsView = win.ZoteroPane.collectionsView;
tagSelector = doc.getElementById('zotero-tag-selector'); tagSelectorElem = doc.getElementById('zotero-tag-selector');
tagSelector = win.ZoteroPane.tagSelector;
// Wait for things to settle // Wait for things to settle
yield Zotero.Promise.delay(100); yield Zotero.Promise.delay(100);
@ -49,16 +36,37 @@ describe("Tag Selector", function () {
var libraryID = Zotero.Libraries.userLibraryID; var libraryID = Zotero.Libraries.userLibraryID;
yield clearTagColors(libraryID); yield clearTagColors(libraryID);
// Default "Display All Tags in This Library" off // Default "Display All Tags in This Library" off
tagSelector.filterToScope = true; tagSelector.displayAllTags = false;
tagSelector.setSearch(''); tagSelector.selectedTags = new Set();
yield tagSelector.refresh(true); tagSelector.handleSearch('');
}) tagSelector.onItemViewChanged({libraryID});
});
after(function () { after(function () {
win.close(); win.close();
}); });
describe("#setSearch()", function () { it('should not display duplicate tags when automatic and manual tag with same name exists', async function () {
var collection = await createDataObject('collection');
var item1 = createUnsavedDataObject('item', { collections: [collection.id] });
item1.setTags([{
tag: "A",
type: 1
}]);
var item2 = createUnsavedDataObject('item', { collections: [collection.id] });
item2.setTags(["A", "B"]);
var promise = waitForTagSelector(win);
await Zotero.DB.executeTransaction(async function () {
await item1.save();
await item2.save();
});
await promise;
var tags = getRegularTags();
assert.sameMembers(tags, ['A', 'B']);
});
describe("#handleSearch()", function () {
it("should filter to tags matching the search", function* () { it("should filter to tags matching the search", function* () {
var collection = yield createDataObject('collection'); var collection = yield createDataObject('collection');
var item = createUnsavedDataObject('item', { collections: [collection.id] }); var item = createUnsavedDataObject('item', { collections: [collection.id] });
@ -67,20 +75,23 @@ describe("Tag Selector", function () {
yield item.saveTx(); yield item.saveTx();
yield promise; yield promise;
var tagsSearch = doc.getElementById('tags-search'); promise = waitForTagSelector(win);
tagsSearch.value = 'a'; tagSelector.handleSearch('a');
tagsSearch.doCommand(); yield Zotero.Promise.delay(500);
yield promise;
var tags = getRegularTags(); var tags = getRegularTags();
assert.sameMembers(tags, ['a']); assert.sameMembers(tags, ['a']);
tagSelector.handleSearch('');
yield Zotero.Promise.delay(500);
tagsSearch.value = '';
tagsSearch.doCommand();
yield item.eraseTx(); yield item.eraseTx();
}); });
}); });
describe("#refresh()", function () { describe("#handleTagSelected()", function () {
it("should remove tags not on matching items on tag click", function* () { it("should remove tags not on matching items on tag click", function* () {
var collection = yield createDataObject('collection'); var collection = yield createDataObject('collection');
var item1 = createUnsavedDataObject('item', { collections: [collection.id] }); var item1 = createUnsavedDataObject('item', { collections: [collection.id] });
@ -112,13 +123,8 @@ describe("Tag Selector", function () {
}); });
yield promise; yield promise;
var buttons = tagSelector.id('tags-box').getElementsByTagName('button'); tagSelector.handleTagSelected('A');
var spy = sinon.spy(win.ZoteroPane, "updateTagFilter"); yield waitForTagSelector(win);
buttons[0].click();
yield spy.returnValues[0];
spy.restore();
var tags = getRegularTags(); var tags = getRegularTags();
assert.sameMembers(tags, ['A', 'B']); assert.sameMembers(tags, ['A', 'B']);
@ -126,9 +132,9 @@ describe("Tag Selector", function () {
}); });
describe("#filterToScope", function () { describe("#displayAllTags", function () {
it("should show all tags in library when false", function* () { it("should show all tags in library when true", function* () {
tagSelector.filterToScope = false; tagSelector.displayAllTags = true;
var collection = yield createDataObject('collection'); var collection = yield createDataObject('collection');
var item1 = createUnsavedDataObject('item'); var item1 = createUnsavedDataObject('item');
@ -165,7 +171,7 @@ describe("Tag Selector", function () {
describe("#notify()", function () { describe("#notify()", function () {
it("should add a tag when added to an item in the library root", function* () { it("should add a tag when added to an item in the library root", function* () {
var promise, tagSelector; var promise;
if (collectionsView.selection.currentIndex != 0) { if (collectionsView.selection.currentIndex != 0) {
promise = waitForTagSelector(win); promise = waitForTagSelector(win);
@ -256,11 +262,14 @@ describe("Tag Selector", function () {
it("should show a colored tag at the top of the list even when linked to no items", function* () { it("should show a colored tag at the top of the list even when linked to no items", function* () {
var libraryID = Zotero.Libraries.userLibraryID; var libraryID = Zotero.Libraries.userLibraryID;
var tagElems = tagSelector.id('tags-box').getElementsByTagName('button'); var tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item');
var count = tagElems.length; var count = tagElems.length;
var promise = waitForTagSelector(win);
yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA'); yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA');
yield promise;
tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item');
assert.equal(tagElems.length, count + 1); assert.equal(tagElems.length, count + 1);
}); });
@ -288,16 +297,16 @@ describe("Tag Selector", function () {
var promise = waitForTagSelector(win); var promise = waitForTagSelector(win);
yield item.saveTx(); yield item.saveTx();
yield promise; yield promise;
var tagElems = tagSelector.id('tags-box').getElementsByTagName('button'); var tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item');
// Make sure the colored tags are still in the right position // Make sure the colored tags are still in the right position
var tags = new Map(); var tags = new Map();
for (let i = 0; i < tagElems.length; i++) { for (let i = 0; i < tagElems.length; i++) {
tags.set(tagElems[i].textContent, tagElems[i].style.order); tags.set(tagElems[i].textContent, i);
} }
assert.isBelow(parseInt(tags.get("B")), 0); assert.isAbove(tags.get("B"), 0);
assert.isBelow(parseInt(tags.get("B")), parseInt(tags.get("A"))); assert.isAbove(tags.get("B"), tags.get("A"));
}) })
it("should remove a tag when an item is removed from a collection", function* () { it("should remove a tag when an item is removed from a collection", function* () {
@ -325,7 +334,7 @@ describe("Tag Selector", function () {
promise = waitForTagSelector(win); promise = waitForTagSelector(win);
yield item.saveTx(); yield item.saveTx();
yield promise; yield promise;
// Tag selector shouldn't show the removed item's tag // Tag selector shouldn't show the removed item's tag
assert.equal(getRegularTags().length, 0); assert.equal(getRegularTags().length, 0);
}) })
@ -379,8 +388,9 @@ describe("Tag Selector", function () {
// Remove tag from library // Remove tag from library
promise = waitForTagSelector(win); promise = waitForTagSelector(win);
var dialogPromise = waitForDialog(); waitForDialog();
yield tagSelector.deleteTag("A"); tagSelector.contextTag = {name: "A"};
yield tagSelector.openDeletePrompt();
yield promise; yield promise;
// Tag selector shouldn't show the deleted item's tag // Tag selector shouldn't show the deleted item's tag
@ -388,7 +398,7 @@ describe("Tag Selector", function () {
}) })
}) })
describe("#rename()", function () { describe("#openRenamePrompt", function () {
it("should rename a tag and update the tag selector", function* () { it("should rename a tag and update the tag selector", function* () {
yield selectLibrary(win); yield selectLibrary(win);
@ -409,7 +419,8 @@ describe("Tag Selector", function () {
dialog.document.getElementById('loginTextbox').value = newTag; dialog.document.getElementById('loginTextbox').value = newTag;
dialog.document.documentElement.acceptDialog(); dialog.document.documentElement.acceptDialog();
}) })
yield tagSelector.rename(tag); tagSelector.contextTag = {name: tag};
yield tagSelector.openRenamePrompt();
yield promise; yield promise;
var tags = getRegularTags(); var tags = getRegularTags();
@ -428,11 +439,12 @@ describe("Tag Selector", function () {
yield promise; yield promise;
promise = waitForTagSelector(win); promise = waitForTagSelector(win);
var promptPromise = waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) { waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) {
dialog.document.getElementById('loginTextbox').value = newTag; dialog.document.getElementById('loginTextbox').value = newTag;
dialog.document.documentElement.acceptDialog(); dialog.document.documentElement.acceptDialog();
}) });
yield tagSelector.rename(oldTag); tagSelector.contextTag = {name: oldTag};
yield tagSelector.openRenamePrompt();
yield promise; yield promise;
var tags = getColoredTags(); var tags = getColoredTags();
@ -441,7 +453,7 @@ describe("Tag Selector", function () {
}); });
}) })
describe("#_openColorPickerWindow()", function () { describe("#openColorPickerWindow()", function () {
it("should assign a color to a tag", function* () { it("should assign a color to a tag", function* () {
yield selectLibrary(win); yield selectLibrary(win);
var tag = "b " + Zotero.Utilities.randomString(); var tag = "b " + Zotero.Utilities.randomString();
@ -463,7 +475,8 @@ describe("Tag Selector", function () {
var dialogPromise = waitForDialog(false, undefined, 'chrome://zotero/content/tagColorChooser.xul'); var dialogPromise = waitForDialog(false, undefined, 'chrome://zotero/content/tagColorChooser.xul');
var tagSelectorPromise = waitForTagSelector(win); var tagSelectorPromise = waitForTagSelector(win);
yield tagSelector._openColorPickerWindow(tag); tagSelector.contextTag = {name: tag};
yield tagSelector.openColorPickerWindow();
yield dialogPromise; yield dialogPromise;
yield tagSelectorPromise; yield tagSelectorPromise;