Reactify item tags box

Improvements:

- Fixes autocomplete text remaining in field after selection in Fx60
- No more text or icon shifting on select (tested on macOS)

Changes:

- Tags are now selected on mousedown with no active state, as in web
  library

Regressions:

- Tooltip with tag type doesn't appear when hovering over icon
- Pressing Tab after modifying a tag loses focus
- Right-click in textbox shows custom menu instead of default text
  editing context menu (Cut/Copy/Paste)

To-do:

- Switch to this version for note tags box
- Style colored tags in autocomplete drop-down? Sort to top?
- Only show delete button on row hover, as in web library?
This commit is contained in:
Dan Stillman 2019-11-08 03:40:20 -05:00
parent 963329df28
commit 5791ffeb16
26 changed files with 2179 additions and 145 deletions

View file

@ -1,3 +1,8 @@
/* Force use of Lucida for HTML input elements (e.g., Editable) */
input {
font-family: Lucida Grande, Lucida Sans Unicode, Lucida Sans, Geneva, -apple-system, sans-serif !important;
}
#zotero-items-toolbar[state=collapsed]
{
margin-left: -8px !important;

View file

@ -0,0 +1,117 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import EditableContent from './editable/content';
import Input from './form/input';
import TextAreaInput from './form/textArea';
import SelectInput from './form/select';
import { noop } from './utils';
class Editable extends React.PureComponent {
get isActive() {
return (this.props.isActive || this.props.isBusy) && !this.props.isDisabled;
}
get isReadOnly() {
return this.props.isReadOnly || this.props.isBusy;
}
get className() {
const { input, inputComponent } = this.props;
return {
'editable': true,
'editing': this.isActive,
'textarea': inputComponent === TextAreaInput || input && input.type === TextAreaInput,
'select': inputComponent === SelectInput || input && input.type === SelectInput,
};
}
renderContent() {
const hasChildren = typeof this.props.children !== 'undefined';
return (
<React.Fragment>
{
hasChildren ?
this.props.children :
<EditableContent { ...this.props } />
}
</React.Fragment>
);
}
renderControls() {
const { input: InputElement, inputComponent: InputComponent } = this.props;
if(InputElement) {
return InputElement;
} else {
const { className, innerRef, ...props } = this.props;
props.ref = innerRef;
return <InputComponent
className={ cx(className, "editable-control") }
{ ...props }
/>
}
}
render() {
const { isDisabled } = this.props;
return (
<div
tabIndex={ isDisabled ? null : this.isActive ? null : 0 }
onClick={ event => this.props.onClick(event) }
onFocus={ event => this.props.onFocus(event) }
onMouseDown={ event => this.props.onMouseDown(event) }
className={ cx(this.className) }
>
{ this.isActive ? this.renderControls() : this.renderContent() }
</div>
);
}
static defaultProps = {
inputComponent: Input,
onClick: noop,
onFocus: noop,
onMouseDown: noop,
};
static propTypes = {
children: PropTypes.oneOfType([PropTypes.element, PropTypes.array]),
input: PropTypes.element,
inputComponent: PropTypes.elementType,
isActive: PropTypes.bool,
isBusy: PropTypes.bool,
isDisabled: PropTypes.bool,
isReadOnly: PropTypes.bool,
};
}
export default React.forwardRef((props, ref) => <Editable
innerRef={ref} {...props}
/>);

View file

@ -0,0 +1,91 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import TextAreaInput from '../form/textArea';
import Select from '../form/select';
class EditableContent extends React.PureComponent {
get hasValue() {
const { input, value } = this.props;
return !!(value || input && input.props.value);
}
get isSelect() {
const { input, inputComponent } = this.props;
return inputComponent === Select || input && input.type == Select;
}
get isTextarea() {
const { input, inputComponent } = this.props;
return inputComponent === TextAreaInput || input && input.type === TextAreaInput;
}
get displayValue() {
const { options, display, input } = this.props;
const value = this.props.value || input && input.props.value;
const placeholder = this.props.placeholder || input && input.props.placeholder;
if(!this.hasValue) { return placeholder; }
if(display) { return display; }
if(this.isSelect && options) {
const displayValue = options.find(e => e.value == value);
return displayValue ? displayValue.label : value;
}
return value;
}
render() {
const className = {
'editable-content': true,
'placeholder': !this.hasValue
};
return <div className={ cx(this.props.className, className) }>{ this.displayValue }</div>;
}
static defaultProps = {
value: '',
placeholder: ''
};
static propTypes = {
display: PropTypes.string,
input: PropTypes.element,
inputComponent: PropTypes.elementType,
options: PropTypes.array,
placeholder: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
};
}
export default EditableContent;

View file

@ -1,116 +1,209 @@
/* eslint-disable react/no-deprecated */
'use strict';
const React = require('react');
const PropTypes = require('prop-types');
const cx = require('classnames');
const { noop } = () => {};
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { noop } from '../utils';
import { pickKeys } from '@zotero/immutable';
//import AutoResizer from './auto-resizer';
import Autosuggest from 'react-autosuggest';
class Input extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
suggestions: [],
value: props.value
};
this.suggestions = React.createRef();
this.showSuggestions = React.createRef(false);
this.preSuggestionValue = React.createRef();
this.selectedSuggestion = React.createRef();
}
cancel(event = null) {
this.props.onCancel && this.props.onCancel(this.hasChanged, event);
this.props.onCancel(this.hasChanged, event);
this.hasBeenCancelled = true;
this.input.blur();
this.props.innerRef.current && this.props.innerRef.current.blur();
}
commit(event = null) {
this.props.onCommit && this.props.onCommit(this.state.value, this.hasChanged, event);
this.props.onCommit(this.state.value, this.hasChanged, event);
this.hasBeenCommitted = true;
}
focus() {
if(this.input != null) {
this.input.focus();
this.props.selectOnFocus && this.input.select();
if (this.props.innerRef.current != null) {
this.props.innerRef.current.focus();
this.props.selectOnFocus && this.props.innerRef.current.select();
}
}
componentWillReceiveProps({ value }) {
UNSAFE_componentWillReceiveProps({ value }) {
if (value !== this.props.value) {
this.setState({ value });
}
}
handleChange({ target }) {
this.setState({ value: target.value });
this.props.onChange && this.props.onChange(target.value);
handleChange({ target }, options) {
var newValue = options.newValue || target.value;
this.setState({
value: newValue,
});
this.props.onChange(newValue);
}
handleBlur(event) {
const shouldCancel = this.props.onBlur && this.props.onBlur(event);
if (this.selectedSuggestion.current) {
this.selectedSuggestion.current = null;
return;
}
if (this.hasBeenCancelled || this.hasBeenCommitted) { return; }
const shouldCancel = this.props.onBlur(event);
shouldCancel ? this.cancel(event) : this.commit(event);
}
handleFocus(event) {
this.props.selectOnFocus && event.target.select();
this.props.onFocus && this.props.onFocus(event);
!this.focused && this.props.selectOnFocus && event.target.select();
// Only focus the input once so that the entered text doesn't get selected when it matches
// a suggestion and the input gets rerendered with the suggestions drop-down
this.focused = true;
this.showSuggestions.current = false;
this.props.onFocus(event);
}
handleKeyDown(event) {
this.showSuggestions.current = true;
switch (event.key) {
case 'Escape':
this.cancel(event);
break;
case 'Enter':
this.commit(event);
if (this.selectedSuggestion.current) {
let value = this.selectedSuggestion.current;
this.selectedSuggestion.current = null;
this.setState({ value });
}
else {
this.commit(event);
}
break;
default:
return;
}
this.props.onKeyDown(event);
}
handlePaste(event) {
this.props.onPaste && this.props.onPaste(event);
}
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
async handleSuggestionsFetchRequested({ value }) {
this.setState({
suggestions: await this.props.getSuggestions(value)
});
}
// Autosuggest will call this function every time you need to clear suggestions.
handleSuggestionsClearRequested() {
this.setState({
suggestions: []
});
}
getSuggestionValue(suggestion) {
return suggestion;
}
shouldRenderSuggestions(value) {
return value.length && this.showSuggestions.current;
}
renderSuggestion(suggestion) {
return <span>
{suggestion}
</span>;
}
handleSuggestionSelected = (event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
this.selectedSuggestion.current = suggestionValue;
// focusInputOnSuggestionClick in Autosuggest doesn't work with a custom renderInputComponent,
// so refocus the textbox manually
setTimeout(() => this.props.innerRef.current.focus());
}
get value() {
return this.state.value;
}
get hasChanged() {
return this.state.value !== this.props.value;
}
render() {
renderInput() {
this.hasBeenCancelled = false;
this.hasBeenCommitted = false;
const extraProps = Object.keys(this.props).reduce((aggr, key) => {
if(key.match(/^(aria-|data-).*/)) {
aggr[key] = this.props[key];
}
return aggr;
}, {});
const input = <input
autoFocus={ this.props.autoFocus }
className={ this.props.className }
disabled={ this.props.isDisabled }
form={ this.props.form }
id={ this.props.id }
inputMode={ this.props.inputMode }
max={ this.props.max }
maxLength={ this.props.maxLength }
min={ this.props.min }
minLength={ this.props.minLength }
name={ this.props.name }
onBlur={ this.handleBlur.bind(this) }
onChange={ this.handleChange.bind(this) }
onFocus={ this.handleFocus.bind(this) }
onKeyDown={ this.handleKeyDown.bind(this) }
placeholder={ this.props.placeholder }
readOnly={ this.props.isReadOnly }
ref={ input => this.input = input }
required={ this.props.isRequired }
size={ this.props.size }
spellCheck={ this.props.spellCheck }
step={ this.props.step }
tabIndex={ this.props.tabIndex }
type={ this.props.type }
value={ this.state.value }
{ ...extraProps }
/>;
const inputProps = {
disabled: this.props.isDisabled,
onBlur: this.handleBlur.bind(this),
onChange: this.handleChange.bind(this),
onFocus: this.handleFocus.bind(this),
onKeyDown: this.handleKeyDown.bind(this),
onPaste: this.handlePaste.bind(this),
readOnly: this.props.isReadOnly,
required: this.props.isRequired,
value: this.state.value,
...pickKeys(this.props, ['autoFocus', 'className', 'form', 'id', 'inputMode', 'max',
'maxLength', 'min', 'minLength', 'name', 'placeholder', 'type', 'spellCheck',
'step', 'tabIndex']),
...pickKeys(this.props, key => key.match(/^(aria-|data-).*/))
};
var input = this.props.autoComplete ? (
<Autosuggest
suggestions={this.state.suggestions}
onSuggestionsFetchRequested={this.handleSuggestionsFetchRequested.bind(this)}
onSuggestionsClearRequested={this.handleSuggestionsClearRequested.bind(this)}
onSuggestionSelected={this.handleSuggestionSelected}
getSuggestionValue={this.getSuggestionValue.bind(this)}
renderSuggestion={this.renderSuggestion.bind(this)}
// https://github.com/moroshko/react-autosuggest/issues/474
renderInputComponent={(inputProps) => <input {...inputProps} ref={this.props.innerRef} />}
focusInputOnSuggestionClick={false}
shouldRenderSuggestions={this.shouldRenderSuggestions.bind(this)}
inputProps={inputProps}
/>
) : (
<input { ...inputProps } />
);
if(this.props.resize) {
/*input = (
<AutoResizer
content={ this.state.value }
vertical={ this.props.resize === 'vertical' }
>
{ input }
</AutoResizer>
);*/
}
return input;
}
render() {
const className = cx({
'input-group': true,
'input': true,
'busy': this.props.isBusy
}, this.props.inputGroupClassName);
return (
<div className={ className }>
{ this.renderInput() }
</div>
);
}
static defaultProps = {
className: 'form-control',
onBlur: noop,
@ -118,17 +211,23 @@ class Input extends React.PureComponent {
onChange: noop,
onCommit: noop,
onFocus: noop,
onKeyDown: noop,
onPaste: noop,
tabIndex: -1,
type: 'text',
value: '',
};
static propTypes = {
autoComplete: PropTypes.bool,
autoFocus: PropTypes.bool,
className: PropTypes.string,
form: PropTypes.string,
getSuggestions: PropTypes.func,
id: PropTypes.string,
inputGroupClassName: PropTypes.string,
inputMode: PropTypes.string,
isBusy: PropTypes.bool,
isDisabled: PropTypes.bool,
isReadOnly: PropTypes.bool,
isRequired: PropTypes.bool,
@ -137,12 +236,15 @@ class Input extends React.PureComponent {
min: PropTypes.number,
minLength: PropTypes.number,
name: PropTypes.string,
onBlur: PropTypes.func,
onCancel: PropTypes.func,
onChange: PropTypes.func,
onCommit: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onCommit: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func,
placeholder: PropTypes.string,
resize: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
selectOnFocus: PropTypes.bool,
spellCheck: PropTypes.bool,
step: PropTypes.number,
@ -152,4 +254,6 @@ class Input extends React.PureComponent {
};
}
module.exports = Input;
export default React.forwardRef((props, ref) => <Input
innerRef={ref} {...props}
/>);

View file

@ -0,0 +1,221 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { noop } from '../utils';
//import Spinner from '../ui/spinner';
import Select from 'react-select';
class SelectInput extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
value: props.value
};
}
cancel(event = null) {
this.props.onCancel(this.hasChanged, event);
}
commit(event = null, value = null, force = false) {
this.props.onCommit(value || this.state.value, force ? true : this.hasChanged, event);
}
focus() {
if(this.input != null) {
this.input.focus();
}
}
UNSAFE_componentWillReceiveProps({ value }) {
if (value !== this.props.value) {
this.setState({ value });
}
}
handleChange(value, ev) {
value = value !== null || (value === null && this.props.clearable) ?
value : this.props.value;
this.setState({ value });
if(this.props.onChange(value) || this.forceCommitOnNextChange) {
if(!ev) {
//@NOTE: this is using undocumeneted feature of react-selct v1, but see #131
const source = typeof this.input.input.getInput === 'function' ?
this.input.input.getInput() : this.input.input;
ev = {
type: 'change',
currentTarget: source,
target: source
}
}
this.commit(ev, value, value !== this.props.value);
}
this.forceCommitOnNextChange = false;
}
handleBlur(event) {
this.props.onBlur(event);
this.cancel(event);
if(this.props.autoBlur) {
this.forceCommitOnNextChange = true;
}
}
handleFocus(event) {
this.props.onFocus(event);
}
handleKeyDown(event) {
switch (event.key) {
case 'Escape':
this.cancel(event);
break;
default:
return;
}
}
get hasChanged() {
return this.state.value !== this.props.value;
}
get defaultSelectProps() {
return {
simpleValue: true,
clearable: false,
};
}
renderInput(userType, viewport) {
const {
options,
autoFocus,
className,
id,
placeholder,
tabIndex,
value,
} = this.props;
const commonProps = {
disabled: this.props.isDisabled,
onBlur: this.handleBlur.bind(this),
onFocus: this.handleFocus.bind(this),
readOnly: this.props.isReadOnly,
ref: input => this.input = input,
required: this.props.isRequired,
};
if(userType === 'touch' || viewport.xxs || viewport.xs || viewport.sm) {
const props = {
...commonProps,
onKeyDown: this.handleKeyDown.bind(this),
onChange: ev => this.handleChange(ev.target.value, ev),
autoFocus, id, placeholder, tabIndex, value
};
return (
<div className="native-select-wrap" >
<select { ...props }>
{ options.map(({ value, label }) => (
<option key={ value } value={ value }>{ label }</option>)
)}
</select>
<div className={ className }>
{ (options.find(o => o.value === value) || options[0] || {}).label }
</div>
</div>
);
} else {
const props = {
...this.defaultSelectProps,
...this.props,
...commonProps,
onInputKeyDown: this.handleKeyDown.bind(this),
onChange: this.handleChange.bind(this),
};
return <Select { ...props } />;
}
}
renderSpinner() {
return null;
//return this.props.isBusy ? <Spinner className="small" /> : null;
}
render() {
const { userType, viewport } = this.props.device;
const className = cx({
'input-group': true,
'select': true,
'busy': this.props.isBusy
}, this.props.inputGroupClassName);
return (
<div className={ className }>
{ this.renderInput(userType, viewport) }
{ this.renderSpinner() }
</div>
);
}
static defaultProps = {
className: 'form-control',
onBlur: noop,
onCancel: noop,
onChange: noop,
onCommit: noop,
onFocus: noop,
options: [],
tabIndex: -1,
value: '',
};
static propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
inputGroupClassName: PropTypes.string,
isBusy: PropTypes.bool,
isDisabled: PropTypes.bool,
isReadOnly: PropTypes.bool,
isRequired: PropTypes.bool,
onBlur: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onCommit: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
placeholder: PropTypes.string,
tabIndex: PropTypes.number,
value: PropTypes.string.isRequired,
};
}
export default SelectInput;

View file

@ -0,0 +1,208 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { noop } from '../utils';
//import AutoResizer from './auto-resizer';
//import Spinner from '../ui/spinner';
class TextAreaInput extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
value: props.value
};
}
cancel(event = null) {
this.props.onCancel(this.hasChanged, event);
}
commit(event = null) {
this.props.onCommit(this.state.value, this.hasChanged, event);
}
focus() {
if (this.props.innerRef != null) {
this.props.innerRef.focus();
this.props.selectOnFocus && this.props.innerRef.select();
}
}
UNSAFE_componentWillReceiveProps({ value }) {
if (value !== this.props.value) {
this.setState({ value });
}
}
handleChange({ target }) {
this.setState({ value: target.value });
this.props.onChange(target.value);
}
handleBlur(event) {
const shouldCancel = this.props.onBlur(event);
shouldCancel ? this.cancel(event) : this.commit(event);
}
handleFocus(event) {
this.props.selectOnFocus && event.target.select();
this.props.onFocus(event);
}
handleKeyDown(event) {
const { isSingleLine } = this.props;
switch (event.key) {
case 'Escape':
this.cancel(event);
break;
case 'Enter':
if(event.shiftKey || isSingleLine) {
event.preventDefault();
this.commit(event);
}
break;
default:
return;
}
}
get hasChanged() {
return this.state.value !== this.props.value;
}
renderInput() {
const extraProps = Object.keys(this.props).reduce((aggr, key) => {
if(key.match(/^(aria-|data-).*/)) {
aggr[key] = this.props[key];
}
return aggr;
}, {});
const input = <textarea
//autoComplete={ this.props.autoComplete }
autoFocus={ this.props.autoFocus }
className={ this.props.className }
cols={ this.props.cols }
disabled={ this.props.isDisabled }
form={ this.props.form }
id={ this.props.id }
maxLength={ this.props.maxLength }
minLength={ this.props.minLength }
name={ this.props.name }
onBlur={ this.handleBlur.bind(this) }
onChange={ this.handleChange.bind(this) }
onFocus={ this.handleFocus.bind(this) }
onKeyDown={ this.handleKeyDown.bind(this) }
placeholder={ this.props.placeholder }
readOnly={ this.props.isReadOnly }
ref={ this.props.innerRef }
required={ this.props.isRequired }
rows={ this.props.rows }
spellCheck={ this.props.spellCheck }
tabIndex={ this.props.tabIndex }
value={ this.state.value }
wrap={ this.props.wrap }
{ ...extraProps }
/>;
return this.props.resize ?
//<AutoResizer
// content={ this.state.value }
// vertical={ this.props.resize === 'vertical' }
//>
{ input }
/*</AutoResizer> */:
input;
}
renderSpinner() {
return null;
//return this.props.isBusy ? <Spinner /> : null;
}
render() {
const className = cx({
'input-group': true,
'textarea': true,
'busy': this.props.isBusy
}, this.props.inputGroupClassName);
return (
<div className={ cx(className) }>
{ this.renderInput() }
{ this.renderSpinner() }
</div>
);
}
static defaultProps = {
className: 'form-control',
onBlur: noop,
onCancel: noop,
onChange: noop,
onCommit: noop,
onFocus: noop,
tabIndex: -1,
value: '',
};
static propTypes = {
//autoComplete: PropTypes.bool,
autoFocus: PropTypes.bool,
className: PropTypes.string,
cols: PropTypes.number,
form: PropTypes.string,
id: PropTypes.string,
inputGroupClassName: PropTypes.string,
isBusy: PropTypes.bool,
isDisabled: PropTypes.bool,
isReadOnly: PropTypes.bool,
isRequired: PropTypes.bool,
isSingleLine: PropTypes.bool,
maxLength: PropTypes.number,
minLength: PropTypes.number,
name: PropTypes.string,
onBlur: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onCommit: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
placeholder: PropTypes.string,
resize: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
rows: PropTypes.number,
selectOnFocus: PropTypes.bool,
spellCheck: PropTypes.bool,
tabIndex: PropTypes.number,
value: PropTypes.string.isRequired,
wrap: PropTypes.bool,
};
}
export default React.forwardRef((props, ref) => <TextAreaInput
innerRef={ref} {...props}
/>);

View file

@ -0,0 +1,433 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React, { useState, useEffect, useRef, useMemo, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Editable from '../editable';
import Input from '../form/input';
import TextAreaInput from '../form/textArea';
//import Button from '../form/button';
const TagsBox = React.forwardRef((props, ref) => {
const [prevInitialTags, setPrevInitialTags] = useState([]);
const [tags, setTags] = useState([]);
const tagNames = useMemo(() => new Set(tags.map(t => t.tag)), [tags]);
const [selectedTag, setSelectedTag] = useState('');
const [newRow, setNewRow] = useState(false);
const [currentValue, setCurrentValue] = useState('');
const [isMultiline, setIsMultiline] = useState(false);
const [rows, setRows] = useState(1);
const rootRef = useRef(null);
const textboxRef = useRef(null);
const requestID = useRef(1);
const newRowID = useRef(1);
const resetSelectionOnRender = useRef(false);
const skipNextEdit = useRef(false);
const removeStr = Zotero.getString('general.remove');
useEffect(() => {
// Move cursor to end of textarea after paste
if (isMultiline) {
//let textarea = window.getSelection().anchorNode.querySelector('textarea');
let textarea = rootRef.current && rootRef.current.querySelector('textarea');
if (textarea) {
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}
}
if (resetSelectionOnRender.current) {
resetSelectionOnRender.current = false;
if (props.onResetSelection) {
props.onResetSelection();
}
}
});
useImperativeHandle(ref, () => ({
blurOpenField
}));
function handleAddTag() {
setSelectedTag('');
showNewRow(true);
requestID.current++;
}
function showNewRow(show) {
setNewRow(show);
if (show) {
setSelectedTag('');
newRowID.current++;
}
}
function handleEdit(event) {
if (skipNextEdit.current) {
skipNextEdit.current = false;
return;
}
var tag = event.currentTarget.closest('[data-tag]').dataset.tag;
if (tag === '') {
return;
}
// If switching from input to textarea, don't change anything
if (isMultiline) {
return;
}
setCurrentValue(tag);
setSelectedTag(tag);
showNewRow(false);
}
function handleKeyDown(event) {
// With the delete button set to tabindex=-1, Tab doesn't work in the last tag for some
// reason, so blur it manually
if (!isMultiline && event.key == 'Tab' && !event.shiftKey) {
let target = event.currentTarget || event.target;
let oldTag = target.closest('[data-tag]').dataset.tag;
if (oldTag === '' && target.value === '') {
textboxRef.current.blur();
}
}
}
function handleMouseDown(event) {
// Prevent right-click on a tag from switching to edit mode
if (event.button != 0) {
event.stopPropagation();
event.preventDefault();
// The above works on its own, but setting the XUL context popup allows the event to go
// through if the confirmation prompt for "Remove All Tags" is cancelled, so we need
// to skip the next edit event as well
skipNextEdit.current = true;
}
}
function handleCommit(newTag, hasChanged, event) {
var oldTag = (event.currentTarget || event.target).closest('[data-tag]').dataset.tag;
var oldTags = tags;
var sortedTags = getSortedTags(oldTags);
var lastTag = sortedTags.length ? sortedTags[oldTags.length - 1] : null;
if (!isMultiline
&& event.key == 'Enter'
&& event.shiftKey) {
let trimmed = newTag.trim();
if (trimmed !== '') {
trimmed += "\n";
}
setCurrentValue(trimmed);
setIsMultiline(true);
setRows(6);
event.preventDefault();
return;
}
setCurrentValue('');
setSelectedTag('');
setIsMultiline(false);
// Tag hasn't changed
if (oldTag === newTag) {
// If Enter was pressed in an empty text box, hide it
if (newTag === '') {
showNewRow(false);
}
/*else if (oldTag == lastTag.tag) {
showNewRow(true);
}*/
resetSelectionOnRender.current = event.key == 'Enter';
return;
}
var newTags = [];
if (newTag !== '') {
// Split by newlines
let splitTags = newTag.split(/\r\n?|\n/)
.map(val => val.trim())
.filter(x => x);
let newTagsMap = new Map();
// Get all tags
for (let i = 0; i < oldTags.length; i++) {
let tag = oldTags[i];
// If this was the tag being edited, add the new value(s)
if (tag.tag == oldTag) {
for (let t of splitTags) {
newTagsMap.set(t, { tag: t });
}
if (oldTag == lastTag) {
showNewRow(true);
}
}
// Otherwise add the old one
else {
newTagsMap.set(tag.tag, tag);
}
}
// New tag at end
if (oldTag === '') {
for (let t of splitTags) {
newTagsMap.set(t, { tag: t });
}
// Call this again to increment the ref and avoid reusing the entered value in the
// next new row
showNewRow(true);
}
else {
resetSelectionOnRender.current = event.key == 'Enter';
}
newTags = [...newTagsMap.values()];
}
// Tag cleared
else {
newTags = oldTags.filter(tag => tag.tag != oldTag);
showNewRow(false);
resetSelectionOnRender.current = event.key == 'Enter';
}
setTags(getSortedTags(newTags));
props.onTagsUpdate(newTags);
}
function handleCancel() {
setCurrentValue('');
setSelectedTag('');
setIsMultiline(false);
showNewRow(false);
//setSuggestions([]);
resetSelectionOnRender.current = true;
requestID.current++;
}
function handleDelete(event) {
var tag = event.currentTarget.closest('[data-tag]').dataset.tag;
var oldTags = tags;
setSelectedTag('');
var newTags = oldTags.filter(t => t.tag !== tag);
setTags(newTags);
props.onTagsUpdate(newTags);
}
function handlePaste(event) {
var text = event.clipboardData.getData('text');
//paste = paste.toUpperCase();
var multiline = !!text.trim().match(/\n/);
if (multiline) {
//setCurrentValue(str.trim());
let field = event.target;
let newValue;
// TODO: Add newlines before and after if necessary
if (field.selectionStart || field.selectionStart == '0') {
let startPos = field.selectionStart;
let endPos = field.selectionEnd;
newValue = field.value.substring(0, startPos)
+ text
+ field.value.substring(endPos, field.value.length);
}
else {
newValue = field.value + text;
}
setCurrentValue(newValue);
setIsMultiline(true);
setRows(newValue.split(/\n/).length);
event.preventDefault();
}
}
function blurOpenField(event) {
if (textboxRef.current && event.target != textboxRef.current) {
textboxRef.current.blur();
}
}
function getSortedTags(tags) {
var sortedTags = [...tags];
sortedTags.sort((a, b) => a.tag.localeCompare(b.tag));
return sortedTags;
}
async function getFilteredSuggestions(value) {
var suggestions = await props.getSuggestions(value);
return suggestions.filter(s => !tagNames.has(s));
}
function tagsEqual(a, b) {
if (a.length != b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].tag !== b[i].tag || a[i].type !== b[i].type) {
return false;
}
}
return true;
}
function renderCount() {
var count = tags.length;
var str = 'pane.item.tags.count.';
// TODO: Switch to plural rules
switch (count) {
case 0:
str += 'zero';
break;
case 1:
str += 'singular';
break;
default:
str += 'plural';
break;
}
return Zotero.getString(str, [count]);
}
function renderTagRow(tag) {
// Icon
var iconFile = 'tag';
var title = '';
if (!tag.type || tag.newRow) {
title = Zotero.getString('pane.item.tags.icon.user');
}
else if (tag.type == 1) {
title = Zotero.getString('pane.item.tags.icon.automatic');
iconFile += '-automatic';
}
var selected = tag.tag === selectedTag;
// Style colored tags
var style = {};
if (!selected) {
let colorData = props.colors.get(tag.tag);
if (colorData) {
style.fontWeight = 'bold';
style.color = colorData.color;
}
}
return (
<li
className={cx({ tag: true, multiline: selected && isMultiline })}
key={tag.newRow ? newRowID.current + '' : tag.tag}
data-tag={tag.tag}
>
<img
src={`chrome://zotero/skin/${iconFile}${Zotero.hiDPISuffix}.png`}
alt={title}
title={title}
tooltiptext={title}
style={{ width: "16px", height: "16px" }}
onClick={() => setSelectedTag(tag.tag)}
/>
<div className="editable-container" style={style}>
<Editable
autoComplete={!isMultiline}
autoFocus
className={cx({ 'zotero-clicky': !selected })}
getSuggestions={getFilteredSuggestions}
inputComponent={isMultiline ? TextAreaInput : Input}
isActive={selected}
isReadOnly={!props.editable}
onCancel={handleCancel}
onClick={handleEdit}
onCommit={handleCommit}
onFocus={handleEdit}
onKeyDown={handleKeyDown}
onMouseDown={handleMouseDown}
onPaste={handlePaste}
ref={textboxRef}
selectOnFocus={!isMultiline}
value={(selected && isMultiline) ? currentValue : tag.tag}
/>
</div>
{props.editable
&& (<button
onClick={handleDelete}
tabIndex="-1"
>
<img
alt={removeStr}
height="18"
width="18"
title={removeStr}
tooltiptext={removeStr}
src={`chrome://zotero/skin/minus${Zotero.hiDPISuffix}.png`}/>
</button>)}
</li>
);
}
// When the initial tags change (because the item was updated), update state with those
var initialTags = getSortedTags(props.initialTags);
if (!tagsEqual(initialTags, prevInitialTags)) {
setTags(initialTags);
setPrevInitialTags(initialTags);
}
var displayTags = [...tags];
if (newRow) {
displayTags.push({
tag: '',
newRow: true
});
}
return (
<div className="tags-box" ref={rootRef} onClick={blurOpenField}>
<div className="tags-box-header">
<div className="tags-box-count">{renderCount()}</div>
<div><button onClick={handleAddTag}>Add</button></div>
</div>
<ul className="tags-box-list">
{displayTags.map(tag => renderTagRow(tag))}
</ul>
<span
tabIndex="0"
onFocus={handleAddTag}
/>
</div>
);
});
TagsBox.propTypes = {
colors: PropTypes.instanceOf(Map),
editable: PropTypes.bool,
getSuggestions: PropTypes.func,
initialTags: PropTypes.array.isRequired,
onResetSelection: PropTypes.func,
onTagsUpdate: PropTypes.func
};
export default TagsBox;

View file

@ -0,0 +1,5 @@
const noop = () => {};
export {
noop
};

View file

@ -25,6 +25,7 @@
-->
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
<?xul-overlay href="chrome://zotero/content/containers/tagSelector.xul"?>
<?xul-overlay href="chrome://zotero/content/containers/tagsBox.xul"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://zotero/content/include.js"></script>

View file

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<!--
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
-->
<!DOCTYPE overlay [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd"> %zoteroDTD;
]>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<popupset>
<menupopup id="tags-context-menu">
<menuitem id="remove-all-item-tags" label="&zotero.item.tags.removeAll;"
oncommand="ZoteroItemPane.removeAllTags()"/>
</menupopup>
</popupset>
</overlay>

View file

@ -0,0 +1,123 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React, { useState, useEffect } from 'react';
//import PropTypes from 'prop-types';
import { Cc, Ci } from 'chrome';
import TagsBox from 'components/itemPane/tagsBox.js';
var search = Cc["@mozilla.org/autocomplete/search;1?name=zotero"]
.createInstance(Ci.nsIAutoCompleteSearch);
function TagsBoxContainer(props, ref) {
var map = Zotero.Tags.getColors(props.item.libraryID);
const [tags, setTags] = useState(props.item.getTags());
const [colors, setColors] = useState(Zotero.Tags.getColors(props.item.libraryID));
useEffect(() => {
var observer = {
notify: async function (action, type, ids, extraData) {
if (type == 'setting') {
if (ids.some(val => val.split("/")[1] == 'tagColors')) {
setColors(Zotero.Tags.getColors(props.item.libraryID));
}
}
else if (type == 'item-tag') {
for (let i = 0; i < ids.length; i++) {
let [itemID, _tagID] = ids[i].split('-').map(x => parseInt(x));
if (itemID == props.item.id) {
setTags(props.item.getTags());
break;
}
}
}
}
};
var id = Zotero.Notifier.registerObserver(observer, ['item-tag', 'setting'], 'tagsBox');
return function cleanup() {
Zotero.Notifier.unregisterObserver(id);
};
});
async function getSuggestions(value) {
return new Zotero.Promise(function (resolve, reject) {
var results = [];
search.startSearch(
value,
JSON.stringify({
libraryID: props.item.libraryID,
fieldName: 'tag',
itemID: props.item.id
}),
[],
{
onSearchResult: function (search, result) {
if (result.searchResult == result.RESULT_IGNORED
|| result.searchResult == result.RESULT_FAILURE) {
reject(result.errorDescription);
return;
}
if (result.searchResult == result.RESULT_SUCCESS
|| result.searchResult == result.RESULT_SUCCESS_ONGOING) {
for (let i = 0; i < result.matchCount; i++) {
results.push(result.getValueAt(i));
}
}
if (result.searchResult != result.RESULT_SUCCESS_ONGOING &&
result.searchResult != result.RESULT_NOMATCH_ONGOING) {
resolve(results);
}
}
}
);
});
}
function handleResetSelection() {
if (props.onResetSelection) {
props.onResetSelection();
}
}
async function handleTagsUpdate(newTags) {
var item = props.item;
item.setTags(newTags);
await item.saveTx();
}
return <TagsBox
colors={colors}
editable={props.editable}
getSuggestions={getSuggestions}
initialTags={tags}
onResetSelection={handleResetSelection}
onTagsUpdate={handleTagsUpdate}
ref={ref}
/>;
}
export default React.forwardRef(TagsBoxContainer);

View file

@ -23,6 +23,10 @@
***** END LICENSE BLOCK *****
*/
import React from 'react';
import ReactDOM from 'react-dom';
import TagsBoxContainer from 'containers/tagsBoxContainer';
var ZoteroItemPane = new function() {
var _lastItem, _itemBox, _notesLabel, _notesButton, _notesList, _tagsBox, _relatedBox;
var _selectedNoteID;
@ -45,7 +49,10 @@ var ZoteroItemPane = new function() {
_notesLabel = document.getElementById('zotero-editpane-notes-label');
_notesButton = document.getElementById('zotero-editpane-notes-add');
_notesList = document.getElementById('zotero-editpane-dynamic-notes');
_tagsBox = document.getElementById('zotero-editpane-tags');
// Fake a ref
_tagsBox = {
current: null
};
_relatedBox = document.getElementById('zotero-editpane-related');
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
@ -72,10 +79,6 @@ var ZoteroItemPane = new function() {
var box = _itemBox;
break;
case 2:
var box = _tagsBox;
break;
case 3:
var box = _relatedBox;
break;
@ -86,11 +89,14 @@ var ZoteroItemPane = new function() {
if (_lastItem && _lastItem != item) {
switch (index) {
case 0:
case 2:
yield box.blurOpenField();
// DEBUG: Currently broken
//box.scrollToTop();
break;
case 2:
_tagsBox.current.blurOpenField();
break;
}
}
@ -175,19 +181,33 @@ var ZoteroItemPane = new function() {
_updateNoteCount();
return;
}
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')
);
}
if (mode) {
box.mode = mode;
if (box.mode == 'view') {
box.hideEmptyFields = true;
if (box) {
if (mode) {
box.mode = mode;
if (box.mode == 'view') {
box.hideEmptyFields = true;
}
}
else {
box.mode = 'edit';
}
box.item = item;
}
else {
box.mode = 'edit';
}
box.item = item;
});
@ -214,7 +234,7 @@ var ZoteroItemPane = new function() {
break;
case 2:
var box = _tagsBox;
var box = _tagsBox.current;
break;
}
if (box) {
@ -223,6 +243,14 @@ var ZoteroItemPane = new function() {
});
function focusItemsList() {
var tree = document.getElementById('zotero-items-tree');
if (tree) {
tree.focus();
}
}
this.onNoteSelected = function (item, editable) {
_selectedNoteID = item.id;
@ -292,6 +320,14 @@ var ZoteroItemPane = new function() {
}
this.removeAllTags = async function () {
if (Services.prompt.confirm(null, "", Zotero.getString('pane.item.tags.removeAll'))) {
_lastItem.setTags([]);
await _lastItem.saveTx();
}
}
this.translateSelectedItems = Zotero.Promise.coroutine(function* () {
var collectionID = _translationTarget.objectType == 'collection' ? _translationTarget.id : undefined;
var items = ZoteroPane_Local.itemsView.getSelectedItems();

View file

@ -28,9 +28,11 @@
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<overlay
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<script src="include.js"/>
<script src="itemPane.js" type="application/javascript"/>
<script src="itemPane.js"></script>
<vbox id="zotero-item-pane" zotero-persist="width height">
<!-- My Publications -->
@ -95,8 +97,8 @@
</vbox>
</tabpanel>
<tabpanel>
<tagsbox id="zotero-editpane-tags" flex="1"/>
<tabpanel id="tags-pane" orient="vertical" context="tags-context-menu">
<html:div id="tags-box-container"></html:div>
</tabpanel>
<tabpanel>

View file

@ -0,0 +1,57 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
const removeKeys = (object, deleteKeys) => {
if(!Array.isArray(deleteKeys)) {
deleteKeys = [deleteKeys];
}
return Object.entries(object)
.reduce((aggr, [key, value]) => {
if(!deleteKeys.includes(key)) { aggr[key] = value; }
return aggr;
}, {});
}
const pickKeys = (object, pickKeys) => {
if(typeof(pickKeys) === 'function') {
return Object.entries(object)
.reduce((aggr, [key, value]) => {
if(pickKeys(key)) { aggr[key] = value; }
return aggr;
}, {});
}
if(!Array.isArray(pickKeys)) {
pickKeys = [pickKeys];
}
return Object.entries(object)
.reduce((aggr, [key, value]) => {
if(pickKeys.includes(key)) { aggr[key] = value; }
return aggr;
}, {});
}
export { removeKeys, pickKeys };

421
package-lock.json generated
View file

@ -274,6 +274,14 @@
"@babel/types": "^7.0.0"
}
},
"@babel/helper-module-imports": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz",
"integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==",
"requires": {
"@babel/types": "^7.0.0"
}
},
"@babel/helper-optimise-call-expression": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz",
@ -651,7 +659,6 @@
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.2.tgz",
"integrity": "sha512-7Bl2rALb7HpvXFL7TETNzKSAeBVCPHELzc0C//9FCxN8nsiueWSJBqaF+2oIJScyILStASR/Cx5WMkXGYTiJFA==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.2"
},
@ -659,8 +666,7 @@
"regenerator-runtime": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==",
"dev": true
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
}
}
},
@ -719,7 +725,6 @@
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
"integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.11",
@ -729,11 +734,106 @@
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@emotion/cache": {
"version": "10.0.19",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.19.tgz",
"integrity": "sha512-BoiLlk4vEsGBg2dAqGSJu0vJl/PgVtCYLBFJaEO8RmQzPugXewQCXZJNXTDFaRlfCs0W+quesayav4fvaif5WQ==",
"requires": {
"@emotion/sheet": "0.9.3",
"@emotion/stylis": "0.8.4",
"@emotion/utils": "0.11.2",
"@emotion/weak-memoize": "0.2.4"
}
},
"@emotion/core": {
"version": "10.0.22",
"resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.22.tgz",
"integrity": "sha512-7eoP6KQVUyOjAkE6y4fdlxbZRA4ILs7dqkkm6oZUJmihtHv0UBq98VgPirq9T8F9K2gKu0J/au/TpKryKMinaA==",
"requires": {
"@babel/runtime": "^7.5.5",
"@emotion/cache": "^10.0.17",
"@emotion/css": "^10.0.22",
"@emotion/serialize": "^0.11.12",
"@emotion/sheet": "0.9.3",
"@emotion/utils": "0.11.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.3.tgz",
"integrity": "sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
}
}
},
"@emotion/css": {
"version": "10.0.22",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.22.tgz",
"integrity": "sha512-8phfa5mC/OadBTmGpMpwykIVH0gFCbUoO684LUkyixPq4F1Wwri7fK5Xlm8lURNBrd2TuvTbPUGxFsGxF9UacA==",
"requires": {
"@emotion/serialize": "^0.11.12",
"@emotion/utils": "0.11.2",
"babel-plugin-emotion": "^10.0.22"
}
},
"@emotion/hash": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz",
"integrity": "sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw=="
},
"@emotion/memoize": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.3.tgz",
"integrity": "sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow=="
},
"@emotion/serialize": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.14.tgz",
"integrity": "sha512-6hTsySIuQTbDbv00AnUO6O6Xafdwo5GswRlMZ5hHqiFx+4pZ7uGWXUQFW46Kc2taGhP89uXMXn/lWQkdyTosPA==",
"requires": {
"@emotion/hash": "0.7.3",
"@emotion/memoize": "0.7.3",
"@emotion/unitless": "0.7.4",
"@emotion/utils": "0.11.2",
"csstype": "^2.5.7"
}
},
"@emotion/sheet": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.3.tgz",
"integrity": "sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A=="
},
"@emotion/stylis": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.4.tgz",
"integrity": "sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ=="
},
"@emotion/unitless": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.4.tgz",
"integrity": "sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ=="
},
"@emotion/utils": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.2.tgz",
"integrity": "sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA=="
},
"@emotion/weak-memoize": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz",
"integrity": "sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA=="
},
"@formatjs/intl-relativetimeformat": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-4.2.1.tgz",
@ -938,7 +1038,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
@ -1186,6 +1285,53 @@
"babel-runtime": "^6.22.0"
}
},
"babel-plugin-emotion": {
"version": "10.0.23",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.23.tgz",
"integrity": "sha512-1JiCyXU0t5S2xCbItejCduLGGcKmF3POT0Ujbexog2MI4IlRcIn/kWjkYwCUZlxpON0O5FC635yPl/3slr7cKQ==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/hash": "0.7.3",
"@emotion/memoize": "0.7.3",
"@emotion/serialize": "^0.11.14",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^1.0.5",
"find-root": "^1.1.0",
"source-map": "^0.5.7"
}
},
"babel-plugin-macros": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz",
"integrity": "sha512-6W2nwiXme6j1n2erPOnmRiWfObUhWH7Qw1LMi9XZy8cj+KtESu3T6asZvtk5bMQQjX8te35o7CFueiSdL/2NmQ==",
"requires": {
"@babel/runtime": "^7.4.2",
"cosmiconfig": "^5.2.0",
"resolve": "^1.10.0"
},
"dependencies": {
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"resolve": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
"integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-transform-es2015-modules-commonjs": {
"version": "6.26.2",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz",
@ -1635,6 +1781,29 @@
"integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=",
"dev": true
},
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
"integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
"requires": {
"callsites": "^2.0.0"
},
"dependencies": {
"callsites": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA="
}
}
},
"caller-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
"integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
"requires": {
"caller-callsite": "^2.0.0"
}
},
"callsites": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz",
@ -1956,7 +2125,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
"integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.1"
},
@ -1964,8 +2132,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
@ -1987,6 +2154,51 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
"cosmiconfig": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
"integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
"requires": {
"import-fresh": "^2.0.0",
"is-directory": "^0.3.1",
"js-yaml": "^3.13.1",
"parse-json": "^4.0.0"
},
"dependencies": {
"import-fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
"integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
"requires": {
"caller-path": "^2.0.0",
"resolve-from": "^3.0.0"
}
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"resolve-from": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
}
}
},
"create-ecdh": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz",
@ -2255,7 +2467,6 @@
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.1.2"
}
@ -2310,7 +2521,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"requires": {
"is-arrayish": "^0.2.1"
}
@ -2354,8 +2564,7 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint": {
"version": "5.14.1",
@ -2593,8 +2802,7 @@
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esquery": {
"version": "1.0.1",
@ -2623,8 +2831,7 @@
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
},
"events": {
"version": "1.1.1",
@ -2864,6 +3071,11 @@
}
}
},
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@ -4100,8 +4312,7 @@
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-binary-path": {
"version": "1.0.1",
@ -4169,6 +4380,11 @@
}
}
},
"is-directory": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE="
},
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@ -4348,6 +4564,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@ -4510,8 +4731,7 @@
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"lodash._baseassign": {
"version": "3.2.0",
@ -4664,6 +4884,11 @@
}
}
},
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
@ -5448,12 +5673,23 @@
"dev": true
},
"prop-types": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
},
"pseudomap": {
@ -5524,25 +5760,52 @@
}
},
"react": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
"integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==",
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.11.0.tgz",
"integrity": "sha512-M5Y8yITaLmU0ynd0r1Yvfq98Rmll6q8AxaEe88c8e7LxO8fZ2cNgmFt0aGAS9wzf1Ao32NKXtCl+/tVVtkxq6g==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.6"
"prop-types": "^15.6.2"
}
},
"react-autosuggest": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-9.4.3.tgz",
"integrity": "sha512-wFbp5QpgFQRfw9cwKvcgLR8theikOUkv8PFsuLYqI2PUgVlx186Cz8MYt5bLxculi+jxGGUUVt+h0esaBZZouw==",
"requires": {
"prop-types": "^15.5.10",
"react-autowhatever": "^10.1.2",
"shallow-equal": "^1.0.0"
}
},
"react-autowhatever": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-autowhatever/-/react-autowhatever-10.2.0.tgz",
"integrity": "sha512-dqHH4uqiJldPMbL8hl/i2HV4E8FMTDEdVlOIbRqYnJi0kTpWseF9fJslk/KS9pGDnm80JkYzVI+nzFjnOG/u+g==",
"requires": {
"prop-types": "^15.5.8",
"react-themeable": "^1.1.0",
"section-iterator": "^2.0.0"
}
},
"react-dom": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
"integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.11.0.tgz",
"integrity": "sha512-nrRyIUE1e7j8PaXSPtyRKtz+2y9ubW/ghNgqKFHHAHaeP0fpF5uXR+sq8IMRHC+ZUxw7W9NyCDTBtwWxvkb0iA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.6"
"scheduler": "^0.17.0"
}
},
"react-input-autosize": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
"integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-intl": {
@ -5571,8 +5834,73 @@
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"dev": true
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-select": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-3.0.8.tgz",
"integrity": "sha512-v9LpOhckLlRmXN5A6/mGGEft4FMrfaBFTGAnuPHcUgVId7Je42kTq9y0Z+Ye5z8/j0XDT3zUqza8gaRaI1PZIg==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/cache": "^10.0.9",
"@emotion/core": "^10.0.9",
"@emotion/css": "^10.0.9",
"memoize-one": "^5.0.0",
"prop-types": "^15.6.0",
"react-input-autosize": "^2.2.2",
"react-transition-group": "^2.2.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.3.tgz",
"integrity": "sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
}
}
},
"react-themeable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
"integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=",
"requires": {
"object-assign": "^3.0.0"
},
"dependencies": {
"object-assign": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
}
}
},
"react-transition-group": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
"requires": {
"dom-helpers": "^3.4.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
},
"react-virtualized": {
"version": "9.21.0",
@ -5875,9 +6203,9 @@
}
},
"scheduler": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.17.0.tgz",
"integrity": "sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@ -5904,6 +6232,11 @@
}
}
},
"section-iterator": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz",
"integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio="
},
"semver": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
@ -6173,8 +6506,7 @@
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
},
"source-map-resolve": {
"version": "0.5.2",
@ -6239,8 +6571,7 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sshpk": {
"version": "1.16.1",

View file

@ -17,10 +17,12 @@
"dependencies": {
"bluebird": "^3.5.1",
"classnames": "^2.2.6",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"prop-types": "^15.7.2",
"react": "^16.11.0",
"react-autosuggest": "^9.4.3",
"react-dom": "^16.11.0",
"react-intl": "^3.4.0",
"react-select": "^3.0.8",
"url": "^0.11.0"
},
"devDependencies": {

View file

@ -758,6 +758,9 @@ const Require = iced(function Require(loader, requirer) {
}
function _require(id) {
// Fix require() from react-autosuggest
if (id == 'React') id = 'react';
let { uri, requirement } = getRequirements(id);
let module = null;
// If module is already cached by loader then just use it.

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

@ -0,0 +1 @@
../node_modules/react-autosuggest/dist/standalone/autosuggest.min.js

View file

@ -93,8 +93,10 @@ var require = (function() {
id: 'zotero/require',
paths: {
'': 'resource://zotero/',
'containers/': 'chrome://zotero/content/containers/',
'components/': 'chrome://zotero/content/components/',
'zotero/': 'chrome://zotero/content/modules/'
'zotero/': 'chrome://zotero/content/modules/',
'@zotero/': 'chrome://zotero/content/modules/'
},
globals
});

View file

@ -38,6 +38,13 @@ const symlinkFiles = [
// these files will be browserified during the build
const browserifyConfigs = [
{
src: 'node_modules/react-select/dist/react-select.cjs.prod.js',
dest: 'resource/react-select.js',
config: {
standalone: 'react-select'
}
},
{
src: 'node_modules/url/url.js',
dest: 'resource/url.js',

View file

@ -20,7 +20,10 @@
// Components
// --------------------------------------------------
@import "components/autosuggest";
@import "components/button";
@import "components/editable";
@import "components/icons";
@import "components/search";
@import "components/tagsBox";
@import "components/tagSelector";

View file

@ -0,0 +1,59 @@
.react-autosuggest__container {
position: relative;
display: flex;
}
/*.react-autosuggest__input {
width: 240px;
height: 30px;
padding: 10px 20px;
font-family: Helvetica, sans-serif;
font-weight: 300;
font-size: 16px;
border: 1px solid #aaa;
border-radius: 4px;
}
.react-autosuggest__input--focused {
outline: none;
}
.react-autosuggest__input--open {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}*/
.react-autosuggest__suggestions-container {
display: none;
}
.react-autosuggest__suggestions-container--open {
display: block;
position: absolute;
top: 1.75em;
width: calc(100% + 1px);
max-height: 10em;
overflow-y: auto;
border: 1px solid #aaa;
background-color: #fff;
z-index: 2;
margin-left: -3px;
box-shadow: -1px 10px 17px -7px rgba(0,0,0,0.4);
}
.react-autosuggest__suggestions-list {
margin: 0;
padding: 0;
list-style-type: none;
}
.react-autosuggest__suggestion {
cursor: pointer;
padding: 3px;
}
.react-autosuggest__suggestion--highlighted {
background-color: Highlight;
color: HighlightText;
}

View file

@ -0,0 +1,11 @@
input.editable-control {
flex: 1 1 auto; // Stretch within react-autosuggest__container
margin-left: -3px;
padding: 2px 1px;
font-size: inherit;
}
textarea.editable-control {
font-family: inherit;
font-size: inherit;
}

View file

@ -0,0 +1,83 @@
.tag-manager {
margin-top: .5em;
flex-grow: 1;
display: flex;
flex-direction: column;
font-family: $font-family-base;
.library-menu {
display: flex;
align-items: baseline;
font-size: 14px;
select {
flex: 0 0 15em;
margin-left: .45em;
font-size: 14px;
}
}
/* Tabs */
.tabs {
display: flex;
border-left: 1px solid rgb(218, 218, 218);
border-bottom: 1px solid rgb(218, 218, 218);
margin: 1em 0 0;
padding: 0;
}
.tabs li {
flex: 0 0 auto;
display: flex;
border-top: 1px solid #ddd;
border-right: 1px solid #ddd;
background: #eee;
}
.tabs li a {
flex: 1 1 auto;
display: flex;
justify-content: center;
align-items: center;
padding: .7em .5em;
min-width: 6em;
color: #444;
height: unset;
font-size: 13px;
}
.tabs li a:hover {
}
.tabs li.active {
background: #fff;
z-index: 1;
font-weight: bold;
}
.tags-list {
width: 100%;
flex-grow: 1;
border: 1px rgb(218, 218, 218) solid;
border-top: 0;
}
.tags-list > option {
font-size: 13px;
padding: .1em .4em;
}
.button-bar {
margin-top: .7em;
display: flex;
justify-content: space-between;
margin-bottom: .1em;
}
button {
-moz-appearance: button;
font-family: $font-family-base;
font-size: 13px;
}
}

View file

@ -0,0 +1,91 @@
#tags-pane {
display: flex;
}
#tags-box-container {
flex-grow: 1;
display: flex;
}
.tags-box {
flex-grow: 1;
.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;
}
}
.tags-box-count {
margin-right: 5px;
}
ul.tags-box-list {
list-style: none;
margin: 0;
padding: 0;
}
ul.tags-box-list > li {
display: flex;
margin: 3px 6px 3px 6px;
align-items: center;
height: 1.5em;
button {
border: 0;
background: none;
padding: 0;
width: 20px;
height: 18px;
}
}
.editable-container {
flex-grow: 1;
margin: 0 2px;
}
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
}
textarea.editable-control {
width: 100%; // DEBUG: This still runs off the screen at large font size, though it keeps the delete button visible
}
}

View file

@ -32,14 +32,14 @@ describe("Item Tags Box", function () {
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 2;
var tagsbox = doc.getElementById('zotero-editpane-tags');
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
var tagsbox = doc.querySelector('.tags-box');
var rows = tagsbox.getElementsByTagName('li');
assert.equal(rows.length, 1);
assert.equal(rows[0].textContent, tag);
yield Zotero.Tags.rename(Zotero.Libraries.userLibraryID, tag, newTag);
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
var rows = tagsbox.getElementsByTagName('li');
assert.equal(rows.length, 1);
assert.equal(rows[0].textContent, newTag);
})
@ -65,18 +65,18 @@ describe("Item Tags Box", function () {
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 2;
var tagsbox = doc.getElementById('zotero-editpane-tags');
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
var tagsbox = doc.querySelector('.tags-box');
var rows = tagsbox.getElementsByTagName('li');
// Colored tags aren't sorted first, for now
assert.notOk(rows[0].getElementsByTagName('label')[0].style.color);
assert.ok(rows[1].getElementsByTagName('label')[0].style.color);
assert.notOk(rows[0].querySelector('.editable-container').style.color);
assert.ok(rows[1].querySelector('.editable-container').style.color);
assert.equal(rows[0].textContent, "_A");
assert.equal(rows[1].textContent, tag);
yield Zotero.Tags.setColor(libraryID, tag, false);
assert.notOk(rows[1].getElementsByTagName('label')[0].style.color);
assert.notOk(rows[1].querySelector('.editable-container').style.color);
})
it("should update when a tag is removed from the library", function* () {
@ -95,14 +95,14 @@ describe("Item Tags Box", function () {
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 2;
var tagsbox = doc.getElementById('zotero-editpane-tags');
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
var tagsbox = doc.querySelector('.tags-box');
var rows = tagsbox.getElementsByTagName('li');
assert.equal(rows.length, 1);
assert.equal(rows[0].textContent, tag);
yield Zotero.Tags.removeFromLibrary(Zotero.Libraries.userLibraryID, Zotero.Tags.getID(tag));
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
var rows = tagsbox.getElementsByTagName('li');
assert.equal(rows.length, 0);
})
})