diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css
index d457d308ff..acb230d456 100644
--- a/chrome/content/zotero-platform/mac/overlay.css
+++ b/chrome/content/zotero-platform/mac/overlay.css
@@ -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;
diff --git a/chrome/content/zotero/components/editable.jsx b/chrome/content/zotero/components/editable.jsx
new file mode 100644
index 0000000000..eb170f9110
--- /dev/null
+++ b/chrome/content/zotero/components/editable.jsx
@@ -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 .
+
+ ***** 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 (
+
+ {
+ hasChildren ?
+ this.props.children :
+
+ }
+
+ );
+ }
+
+ renderControls() {
+ const { input: InputElement, inputComponent: InputComponent } = this.props;
+ if(InputElement) {
+ return InputElement;
+ } else {
+ const { className, innerRef, ...props } = this.props;
+ props.ref = innerRef;
+
+ return
+ }
+ }
+
+ render() {
+ const { isDisabled } = this.props;
+ return (
+
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() }
+
+ );
+ }
+ 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) => );
\ No newline at end of file
diff --git a/chrome/content/zotero/components/editable/content.jsx b/chrome/content/zotero/components/editable/content.jsx
new file mode 100644
index 0000000000..d10dcab285
--- /dev/null
+++ b/chrome/content/zotero/components/editable/content.jsx
@@ -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 .
+
+ ***** 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 { this.displayValue }
;
+ }
+
+ 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;
diff --git a/chrome/content/zotero/components/form/input.jsx b/chrome/content/zotero/components/form/input.jsx
index d70b131b56..12dfc56d2f 100644
--- a/chrome/content/zotero/components/form/input.jsx
+++ b/chrome/content/zotero/components/form/input.jsx
@@ -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
+ {suggestion}
+ ;
+ }
+
+ 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 = 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 ? (
+ }
+ focusInputOnSuggestionClick={false}
+ shouldRenderSuggestions={this.shouldRenderSuggestions.bind(this)}
+ inputProps={inputProps}
+ />
+ ) : (
+
+ );
+
+ if(this.props.resize) {
+ /*input = (
+
+ { input }
+
+ );*/
+ }
+
return input;
}
+ render() {
+ const className = cx({
+ 'input-group': true,
+ 'input': true,
+ 'busy': this.props.isBusy
+ }, this.props.inputGroupClassName);
+ return (
+
+ { this.renderInput() }
+
+ );
+ }
+
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) => );
\ No newline at end of file
diff --git a/chrome/content/zotero/components/form/select.jsx b/chrome/content/zotero/components/form/select.jsx
new file mode 100644
index 0000000000..d6144b94a6
--- /dev/null
+++ b/chrome/content/zotero/components/form/select.jsx
@@ -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 .
+
+ ***** 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 (
+
+
+
+ { (options.find(o => o.value === value) || options[0] || {}).label }
+
+
+
+ );
+ } else {
+ const props = {
+ ...this.defaultSelectProps,
+ ...this.props,
+ ...commonProps,
+ onInputKeyDown: this.handleKeyDown.bind(this),
+ onChange: this.handleChange.bind(this),
+ };
+
+ return ;
+ }
+ }
+
+ renderSpinner() {
+ return null;
+ //return this.props.isBusy ? : null;
+ }
+
+ render() {
+ const { userType, viewport } = this.props.device;
+ const className = cx({
+ 'input-group': true,
+ 'select': true,
+ 'busy': this.props.isBusy
+ }, this.props.inputGroupClassName);
+ return (
+
+ { this.renderInput(userType, viewport) }
+ { this.renderSpinner() }
+
+ );
+ }
+
+ 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;
diff --git a/chrome/content/zotero/components/form/textArea.jsx b/chrome/content/zotero/components/form/textArea.jsx
new file mode 100644
index 0000000000..56c25fd298
--- /dev/null
+++ b/chrome/content/zotero/components/form/textArea.jsx
@@ -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 .
+
+ ***** 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 = ;
+
+ return this.props.resize ?
+ //
+ { input }
+ /* */:
+ input;
+ }
+
+ renderSpinner() {
+ return null;
+ //return this.props.isBusy ? : null;
+ }
+
+ render() {
+ const className = cx({
+ 'input-group': true,
+ 'textarea': true,
+ 'busy': this.props.isBusy
+ }, this.props.inputGroupClassName);
+ return (
+
+ { this.renderInput() }
+ { this.renderSpinner() }
+
+ );
+ }
+
+ 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) => );
diff --git a/chrome/content/zotero/components/itemPane/tagsBox.jsx b/chrome/content/zotero/components/itemPane/tagsBox.jsx
new file mode 100644
index 0000000000..0d5ae12a47
--- /dev/null
+++ b/chrome/content/zotero/components/itemPane/tagsBox.jsx
@@ -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 .
+
+ ***** 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 (
+
+
setSelectedTag(tag.tag)}
+ />
+
+
+
+ {props.editable
+ && ()}
+
+ );
+ }
+
+ // 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 (
+
+
+
{renderCount()}
+
+
+
+ {displayTags.map(tag => renderTagRow(tag))}
+
+
+
+ );
+});
+
+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;
\ No newline at end of file
diff --git a/chrome/content/zotero/components/utils.js b/chrome/content/zotero/components/utils.js
new file mode 100644
index 0000000000..5de953e8f6
--- /dev/null
+++ b/chrome/content/zotero/components/utils.js
@@ -0,0 +1,5 @@
+const noop = () => {};
+
+export {
+ noop
+};
diff --git a/chrome/content/zotero/containers/containers.xul b/chrome/content/zotero/containers/containers.xul
index 91cfbd4f07..bb5b6f9d54 100644
--- a/chrome/content/zotero/containers/containers.xul
+++ b/chrome/content/zotero/containers/containers.xul
@@ -25,6 +25,7 @@
-->
+
diff --git a/chrome/content/zotero/containers/tagsBox.xul b/chrome/content/zotero/containers/tagsBox.xul
new file mode 100644
index 0000000000..ad1a4065a1
--- /dev/null
+++ b/chrome/content/zotero/containers/tagsBox.xul
@@ -0,0 +1,38 @@
+
+
+ %globalDTD;
+ %zoteroDTD;
+]>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chrome/content/zotero/containers/tagsBoxContainer.jsx b/chrome/content/zotero/containers/tagsBoxContainer.jsx
new file mode 100644
index 0000000000..0810065a18
--- /dev/null
+++ b/chrome/content/zotero/containers/tagsBoxContainer.jsx
@@ -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 .
+
+ ***** 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 ;
+}
+
+export default React.forwardRef(TagsBoxContainer);
diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js
index 9eb4e3fa23..ad303e5a25 100644
--- a/chrome/content/zotero/itemPane.js
+++ b/chrome/content/zotero/itemPane.js
@@ -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(
+ ,
+ 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();
diff --git a/chrome/content/zotero/itemPane.xul b/chrome/content/zotero/itemPane.xul
index ee0e62e791..81796b2649 100644
--- a/chrome/content/zotero/itemPane.xul
+++ b/chrome/content/zotero/itemPane.xul
@@ -28,9 +28,11 @@
-
+
-
+
@@ -95,8 +97,8 @@
-
-
+
+
diff --git a/chrome/content/zotero/modules/immutable.js b/chrome/content/zotero/modules/immutable.js
new file mode 100644
index 0000000000..ef6c3832ad
--- /dev/null
+++ b/chrome/content/zotero/modules/immutable.js
@@ -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 .
+
+ ***** 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 };
diff --git a/package-lock.json b/package-lock.json
index 1fff4413e2..79bc46b6b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index d50739e9ce..20bab0cbd5 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/resource/loader.jsm b/resource/loader.jsm
index e7cc887f1b..f26bcaea8a 100644
--- a/resource/loader.jsm
+++ b/resource/loader.jsm
@@ -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.
diff --git a/resource/react-autosuggest.js b/resource/react-autosuggest.js
new file mode 120000
index 0000000000..0f94f6338f
--- /dev/null
+++ b/resource/react-autosuggest.js
@@ -0,0 +1 @@
+../node_modules/react-autosuggest/dist/standalone/autosuggest.min.js
\ No newline at end of file
diff --git a/resource/require.js b/resource/require.js
index 04c73f8580..2485d2e9a0 100644
--- a/resource/require.js
+++ b/resource/require.js
@@ -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
});
diff --git a/scripts/config.js b/scripts/config.js
index a504b503bd..280969a427 100644
--- a/scripts/config.js
+++ b/scripts/config.js
@@ -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',
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss
index fa6e91d2c7..c45fffc2d2 100644
--- a/scss/_zotero-react-client.scss
+++ b/scss/_zotero-react-client.scss
@@ -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";
diff --git a/scss/components/_autosuggest.scss b/scss/components/_autosuggest.scss
new file mode 100644
index 0000000000..77f0faddd5
--- /dev/null
+++ b/scss/components/_autosuggest.scss
@@ -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;
+}
diff --git a/scss/components/_editable.scss b/scss/components/_editable.scss
new file mode 100644
index 0000000000..c09315afc0
--- /dev/null
+++ b/scss/components/_editable.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/scss/components/_tag-manager.scss b/scss/components/_tag-manager.scss
new file mode 100644
index 0000000000..5342ccc1f9
--- /dev/null
+++ b/scss/components/_tag-manager.scss
@@ -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;
+ }
+}
diff --git a/scss/components/_tagsBox.scss b/scss/components/_tagsBox.scss
new file mode 100644
index 0000000000..ee534c7ae2
--- /dev/null
+++ b/scss/components/_tagsBox.scss
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/test/tests/tagsboxTest.js b/test/tests/tagsboxTest.js
index dcefb05b7b..8858be962d 100644
--- a/test/tests/tagsboxTest.js
+++ b/test/tests/tagsboxTest.js
@@ -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);
})
})