Merge pull request #1606 from adomasven/feature/reactify
Reactified Tag Selector
This commit is contained in:
commit
eb5d28b626
56 changed files with 2320 additions and 2277 deletions
1
.babelrc
1
.babelrc
|
@ -18,6 +18,7 @@
|
||||||
"syntax-jsx",
|
"syntax-jsx",
|
||||||
"transform-react-jsx",
|
"transform-react-jsx",
|
||||||
"transform-react-display-name",
|
"transform-react-display-name",
|
||||||
|
"transform-class-properties",
|
||||||
[
|
[
|
||||||
"transform-es2015-modules-commonjs",
|
"transform-es2015-modules-commonjs",
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load diff
131
chrome/content/zotero/components/button.jsx
Normal file
131
chrome/content/zotero/components/button.jsx
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const React = require('react')
|
||||||
|
const { PureComponent, createElement: create } = React
|
||||||
|
const { injectIntl, intlShape } = require('react-intl')
|
||||||
|
const { IconDownChevron } = require('./icons')
|
||||||
|
const cx = require('classnames')
|
||||||
|
const {
|
||||||
|
bool, element, func, node, number, oneOf, string
|
||||||
|
} = require('prop-types')
|
||||||
|
|
||||||
|
|
||||||
|
const ButtonGroup = ({ children }) => (
|
||||||
|
<div className="btn-group">{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
ButtonGroup.propTypes = {
|
||||||
|
children: node
|
||||||
|
}
|
||||||
|
|
||||||
|
class Button extends PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
if (!Zotero.isNode && this.title) {
|
||||||
|
// Workaround for XUL tooltips
|
||||||
|
this.container.setAttribute('tooltiptext', this.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get classes() {
|
||||||
|
return ['btn', this.props.className, `btn-${this.props.size}`, {
|
||||||
|
'btn-icon': this.props.icon != null,
|
||||||
|
'active': this.props.isActive,
|
||||||
|
'btn-flat': this.props.isFlat,
|
||||||
|
'btn-menu': this.props.isMenu,
|
||||||
|
'disabled': this.props.isDisabled,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
get node() {
|
||||||
|
return 'button'
|
||||||
|
}
|
||||||
|
|
||||||
|
get text() {
|
||||||
|
const { intl, text } = this.props
|
||||||
|
|
||||||
|
return text ?
|
||||||
|
intl.formatMessage({ id: text }) :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
const { intl, title } = this.props
|
||||||
|
|
||||||
|
return title ?
|
||||||
|
intl.formatMessage({ id: title }) :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
get menuMarker() {
|
||||||
|
if (!Zotero.isNode && Zotero.isLinux) {
|
||||||
|
return this.props.isMenu && <span className="menu-marker"/>
|
||||||
|
}
|
||||||
|
return this.props.isMenu && <IconDownChevron className="menu-marker"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
get attributes() {
|
||||||
|
const attr = {
|
||||||
|
className: cx(...this.classes),
|
||||||
|
disabled: !this.props.noFocus && this.props.isDisabled,
|
||||||
|
onBlur: this.handleBlur,
|
||||||
|
onFocus: this.props.onFocus,
|
||||||
|
ref: this.setContainer,
|
||||||
|
title: this.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.isDisabled) {
|
||||||
|
attr.onMouseDown = this.handleMouseDown
|
||||||
|
attr.onClick = this.handleClick
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr
|
||||||
|
}
|
||||||
|
|
||||||
|
setContainer = (container) => {
|
||||||
|
this.container = container
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!this.props.isDisabled && this.props.onClick) {
|
||||||
|
this.props.onClick(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!this.props.isDisabled && this.props.onMouseDown) {
|
||||||
|
this.props.onMouseDown(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return create(this.node, this.attributes, this.props.icon, this.text, this.menuMarker)
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
className: string,
|
||||||
|
icon: element,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
isActive: bool,
|
||||||
|
isDisabled: bool,
|
||||||
|
isMenu: bool,
|
||||||
|
size: oneOf(['sm', 'md', 'lg']),
|
||||||
|
title: string,
|
||||||
|
text: string,
|
||||||
|
onClick: func,
|
||||||
|
onMouseDown: func
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
size: 'md'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ButtonGroup,
|
||||||
|
Button: injectIntl(Button)
|
||||||
|
}
|
155
chrome/content/zotero/components/form/input.jsx
Normal file
155
chrome/content/zotero/components/form/input.jsx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
/* eslint-disable react/no-deprecated */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const cx = require('classnames');
|
||||||
|
const { noop } = () => {};
|
||||||
|
|
||||||
|
class Input extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
value: props.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(event = null) {
|
||||||
|
this.props.onCancel && this.props.onCancel(this.hasChanged, event);
|
||||||
|
this.hasBeenCancelled = true;
|
||||||
|
this.input.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
commit(event = null) {
|
||||||
|
this.props.onCommit && 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur(event) {
|
||||||
|
const shouldCancel = this.props.onBlur && this.props.onBlur(event);
|
||||||
|
if (this.hasBeenCancelled || this.hasBeenCommitted) { return; }
|
||||||
|
shouldCancel ? this.cancel(event) : this.commit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFocus(event) {
|
||||||
|
this.props.selectOnFocus && event.target.select();
|
||||||
|
this.props.onFocus && this.props.onFocus(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
this.cancel(event);
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
this.commit(event);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasChanged() {
|
||||||
|
return this.state.value !== this.props.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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 }
|
||||||
|
/>;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
className: 'form-control',
|
||||||
|
onBlur: noop,
|
||||||
|
onCancel: noop,
|
||||||
|
onChange: noop,
|
||||||
|
onCommit: noop,
|
||||||
|
onFocus: noop,
|
||||||
|
tabIndex: -1,
|
||||||
|
type: 'text',
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
form: PropTypes.string,
|
||||||
|
id: PropTypes.string,
|
||||||
|
inputMode: PropTypes.string,
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isReadOnly: PropTypes.bool,
|
||||||
|
isRequired: PropTypes.bool,
|
||||||
|
max: PropTypes.number,
|
||||||
|
maxLength: PropTypes.number,
|
||||||
|
min: PropTypes.number,
|
||||||
|
minLength: PropTypes.number,
|
||||||
|
name: PropTypes.string,
|
||||||
|
onBlur: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onCommit: PropTypes.func,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
selectOnFocus: PropTypes.bool,
|
||||||
|
spellCheck: PropTypes.bool,
|
||||||
|
step: PropTypes.number,
|
||||||
|
tabIndex: PropTypes.number,
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Input;
|
57
chrome/content/zotero/components/icons.jsx
Normal file
57
chrome/content/zotero/components/icons.jsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const React = require('react')
|
||||||
|
const { PureComponent } = React
|
||||||
|
const { element, string } = require('prop-types')
|
||||||
|
const cx = require('classnames')
|
||||||
|
|
||||||
|
const Icon = ({ children, className, name }) => (
|
||||||
|
<span className={cx('icon', `icon-${name}`, className)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon.propTypes = {
|
||||||
|
children: element.isRequired,
|
||||||
|
className: string,
|
||||||
|
name: string.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Icon }
|
||||||
|
|
||||||
|
|
||||||
|
function i(name, svgOrSrc, hasDPI=true) {
|
||||||
|
const icon = class extends PureComponent {
|
||||||
|
render() {
|
||||||
|
const { className } = this.props
|
||||||
|
|
||||||
|
if (typeof svgOrSrc == 'string') {
|
||||||
|
if (hasDPI && window.devicePixelRatio >= 1.25) {
|
||||||
|
let parts = svgOrSrc.split('.');
|
||||||
|
parts[parts.length-2] = parts[parts.length-2] + '@2x';
|
||||||
|
svgOrSrc = parts.join('.')
|
||||||
|
}
|
||||||
|
return <Icon className={className} name={name.toLowerCase()}><img src={svgOrSrc}/></Icon>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Icon className={className} name={name.toLowerCase()}>{svgOrImg}</Icon>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.propTypes = {
|
||||||
|
className: string
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.displayName = `Icon${name}`
|
||||||
|
|
||||||
|
module.exports[icon.displayName] = icon
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
|
||||||
|
|
||||||
|
i('TagSelectorMenu', "chrome://zotero/skin/tag-selector-menu.png")
|
||||||
|
i('DownChevron', "chrome://zotero/skin/searchbar-dropmarker.png")
|
||||||
|
|
68
chrome/content/zotero/components/tag-selector.jsx
Normal file
68
chrome/content/zotero/components/tag-selector.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const TagList = require('./tag-selector/tag-list');
|
||||||
|
const Input = require('./form/input');
|
||||||
|
const { Button } = require('./button');
|
||||||
|
const { IconTagSelectorMenu } = require('./icons');
|
||||||
|
|
||||||
|
class TagSelector extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="tag-selector">
|
||||||
|
<TagList {...this.props} />
|
||||||
|
<div className="tag-selector-filter-container">
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
ref={ref => this.focusTextbox = ref && ref.focus}
|
||||||
|
value={this.props.searchString}
|
||||||
|
onChange={this.props.onSearch}
|
||||||
|
className="tag-selector-filter"
|
||||||
|
size="1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<IconTagSelectorMenu />}
|
||||||
|
title="zotero.toolbar.actions.label"
|
||||||
|
className="tag-selector-actions"
|
||||||
|
isMenu
|
||||||
|
onClick={ev => this.props.onSettings(ev)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagSelector.propTypes = {
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
color: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool
|
||||||
|
})),
|
||||||
|
dragObserver: PropTypes.shape({
|
||||||
|
onDragOver: PropTypes.func,
|
||||||
|
onDragExit: PropTypes.func,
|
||||||
|
onDrop: PropTypes.func
|
||||||
|
}),
|
||||||
|
searchString: PropTypes.string,
|
||||||
|
shouldFocus: PropTypes.bool,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
|
onTagContext: PropTypes.func,
|
||||||
|
onSearch: PropTypes.func,
|
||||||
|
onSettings: PropTypes.func,
|
||||||
|
loaded: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
TagSelector.defaultProps = {
|
||||||
|
tags: [],
|
||||||
|
searchString: '',
|
||||||
|
shouldFocus: false,
|
||||||
|
onSelect: () => Promise.resolve(),
|
||||||
|
onTagContext: () => Promise.resolve(),
|
||||||
|
onSearch: () => Promise.resolve(),
|
||||||
|
onSettings: () => Promise.resolve()
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TagSelector;
|
77
chrome/content/zotero/components/tag-selector/tag-list.jsx
Normal file
77
chrome/content/zotero/components/tag-selector/tag-list.jsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
const React = require('react');
|
||||||
|
const { FormattedMessage } = require('react-intl');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
class TagList extends React.PureComponent {
|
||||||
|
renderTag(index) {
|
||||||
|
const { tags } = this.props;
|
||||||
|
const tag = index < tags.length ?
|
||||||
|
tags[index] : {
|
||||||
|
tag: "",
|
||||||
|
};
|
||||||
|
const { onDragOver, onDragExit, onDrop } = this.props.dragObserver;
|
||||||
|
|
||||||
|
const className = cx('tag-selector-item', 'zotero-clicky', {
|
||||||
|
selected: tag.selected,
|
||||||
|
colored: tag.color,
|
||||||
|
disabled: tag.disabled
|
||||||
|
});
|
||||||
|
|
||||||
|
let props = {
|
||||||
|
className,
|
||||||
|
onClick: ev => !tag.disabled && this.props.onSelect(tag.name, ev),
|
||||||
|
onContextMenu: ev => this.props.onTagContext(tag, ev),
|
||||||
|
onDragOver,
|
||||||
|
onDragExit,
|
||||||
|
onDrop
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tag.color) {
|
||||||
|
props['style'] = {
|
||||||
|
color: tag.color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={index} {...props}>
|
||||||
|
{tag.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const totalTagCount = this.props.tags.length;
|
||||||
|
var tagList = (
|
||||||
|
<ul className="tag-selector-list">
|
||||||
|
{
|
||||||
|
[...Array(totalTagCount).keys()].map(index => this.renderTag(index))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
if (!this.props.loaded) {
|
||||||
|
tagList = (
|
||||||
|
<div className="tag-selector-message">
|
||||||
|
<FormattedMessage id="zotero.tagSelector.loadingTags" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (totalTagCount == 0) {
|
||||||
|
tagList = (
|
||||||
|
<div className="tag-selector-message">
|
||||||
|
<FormattedMessage id="zotero.tagSelector.noTagsToDisplay" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tag-selector-container"
|
||||||
|
ref={ref => { this.container = ref }}>
|
||||||
|
{tagList}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TagList;
|
59
chrome/content/zotero/containers/containers.js
Normal file
59
chrome/content/zotero/containers/containers.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2018 Center for History and New Media
|
||||||
|
George Mason University, Fairfax, Virginia, USA
|
||||||
|
http://zotero.org
|
||||||
|
|
||||||
|
This file is part of Zotero.
|
||||||
|
|
||||||
|
Zotero is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Zotero is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***** END LICENSE BLOCK *****
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { defineMessages } = require('react-intl');
|
||||||
|
|
||||||
|
ZoteroPane.Containers = {
|
||||||
|
async init() {
|
||||||
|
await this.initIntlStrings();
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPane() {
|
||||||
|
var tagSelector = document.getElementById('zotero-tag-selector');
|
||||||
|
ZoteroPane.tagSelector = Zotero.TagSelector.init(tagSelector, {
|
||||||
|
onSelection: ZoteroPane.updateTagFilter.bind(ZoteroPane)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async initIntlStrings() {
|
||||||
|
this.intlMessages = {};
|
||||||
|
const intlFiles = ['zotero.dtd'];
|
||||||
|
for (let intlFile of intlFiles) {
|
||||||
|
let localeXML = await Zotero.File.getContentsFromURLAsync(`chrome://zotero/locale/${intlFile}`);
|
||||||
|
let regexp = /<!ENTITY ([^\s]+)\s+"([^"]+)/g;
|
||||||
|
let regexpResult;
|
||||||
|
while (regexpResult = regexp.exec(localeXML)) {
|
||||||
|
this.intlMessages[regexpResult[1]] = regexpResult[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
ZoteroPane.tagSelector.unregister();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
chrome/content/zotero/containers/containers.xul
Normal file
34
chrome/content/zotero/containers/containers.xul
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2018 Center for History and New Media
|
||||||
|
George Mason University, Fairfax, Virginia, USA
|
||||||
|
http://zotero.org
|
||||||
|
|
||||||
|
This file is part of Zotero.
|
||||||
|
|
||||||
|
Zotero is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Zotero is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***** END LICENSE BLOCK *****
|
||||||
|
-->
|
||||||
|
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
||||||
|
<?xul-overlay href="chrome://zotero/content/containers/tagSelector.xul"?>
|
||||||
|
|
||||||
|
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
|
<script src="chrome://zotero/content/include.js"></script>
|
||||||
|
|
||||||
|
<script src="containers.js"></script>
|
||||||
|
<script src="tagSelector.js"></script>
|
||||||
|
</overlay>
|
412
chrome/content/zotero/containers/tagSelector.jsx
Normal file
412
chrome/content/zotero/containers/tagSelector.jsx
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
/* global Zotero: false */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const ReactDOM = require('react-dom');
|
||||||
|
const { IntlProvider } = require('react-intl');
|
||||||
|
const TagSelector = require('components/tag-selector.js');
|
||||||
|
const noop = Promise.resolve();
|
||||||
|
const defaults = {
|
||||||
|
tagColors: new Map(),
|
||||||
|
tags: [],
|
||||||
|
showAutomatic: Zotero.Prefs.get('tagSelector.showAutomatic'),
|
||||||
|
searchString: '',
|
||||||
|
inScope: new Set(),
|
||||||
|
loaded: false
|
||||||
|
};
|
||||||
|
const { Cc, Ci } = require('chrome');
|
||||||
|
|
||||||
|
Zotero.TagSelector = class TagSelectorContainer extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._notifierID = Zotero.Notifier.registerObserver(
|
||||||
|
this,
|
||||||
|
['collection-item', 'item', 'item-tag', 'tag', 'setting'],
|
||||||
|
'tagSelector'
|
||||||
|
);
|
||||||
|
this.displayAllTags = Zotero.Prefs.get('tagSelector.displayAllTags');
|
||||||
|
this.selectedTags = new Set();
|
||||||
|
this.state = defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update trigger #1 (triggered by ZoteroPane)
|
||||||
|
async onItemViewChanged({collectionTreeRow, libraryID, tagsInScope}) {
|
||||||
|
this.collectionTreeRow = collectionTreeRow || this.collectionTreeRow;
|
||||||
|
|
||||||
|
let newState = {loaded: true};
|
||||||
|
|
||||||
|
if (!this.state.tagColors.length && libraryID && this.libraryID != libraryID) {
|
||||||
|
newState.tagColors = Zotero.Tags.getColors(libraryID);
|
||||||
|
}
|
||||||
|
this.libraryID = libraryID;
|
||||||
|
|
||||||
|
newState.tags = await this.getTags(tagsInScope,
|
||||||
|
this.state.tagColors.length ? this.state.tagColors : newState.tagColors);
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update trigger #2
|
||||||
|
async notify(event, type, ids, extraData) {
|
||||||
|
if (type === 'setting') {
|
||||||
|
if (ids.some(val => val.split('/')[1] == 'tagColors')) {
|
||||||
|
let tagColors = Zotero.Tags.getColors(this.libraryID);
|
||||||
|
this.state.tagColors = tagColors;
|
||||||
|
this.setState({tagColors, tags: await this.getTags(null, tagColors)});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore anything other than deletes in duplicates view
|
||||||
|
if (this.collectionTreeRow.isDuplicates()) {
|
||||||
|
switch (event) {
|
||||||
|
case 'delete':
|
||||||
|
case 'trash':
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore item events other than 'trash'
|
||||||
|
if (type == 'item' && (event == 'trash')) {
|
||||||
|
return this.setState({tags: await this.getTags()});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a selected tag no longer exists, deselect it
|
||||||
|
if (type == 'item-tag') {
|
||||||
|
if (event == 'delete' || event == 'trash' || event == 'modify') {
|
||||||
|
for (let tag of this.selectedTags) {
|
||||||
|
if (tag == extraData[ids[0]].old.tag) {
|
||||||
|
this.selectedTags.delete(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.setState({tags: await this.getTags()});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({tags: await this.getTags()});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTags(tagsInScope, tagColors) {
|
||||||
|
if (!tagsInScope) {
|
||||||
|
tagsInScope = await this.collectionTreeRow.getChildTags();
|
||||||
|
}
|
||||||
|
this.inScope = new Set(tagsInScope.map(t => t.tag));
|
||||||
|
let tags;
|
||||||
|
if (this.displayAllTags) {
|
||||||
|
tags = await Zotero.Tags.getAll(this.libraryID, [0, 1]);
|
||||||
|
} else {
|
||||||
|
tags = tagsInScope
|
||||||
|
}
|
||||||
|
|
||||||
|
tagColors = tagColors || this.state.tagColors;
|
||||||
|
|
||||||
|
// Add colored tags that aren't already real tags
|
||||||
|
let regularTags = new Set(tags.map(tag => tag.tag));
|
||||||
|
let coloredTags = Array.from(tagColors.keys());
|
||||||
|
|
||||||
|
coloredTags.filter(ct => !regularTags.has(ct)).forEach(x =>
|
||||||
|
tags.push(Zotero.Tags.cleanData({ tag: x }))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort by name
|
||||||
|
tags.sort(function (a, b) {
|
||||||
|
let aColored = tagColors.has(a.tag),
|
||||||
|
bColored = tagColors.has(b.tag);
|
||||||
|
if (aColored && !bColored) return -1;
|
||||||
|
if (!aColored && bColored) return 1;
|
||||||
|
return Zotero.getLocaleCollation().compareString(1, a.tag, b.tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let tags = this.state.tags;
|
||||||
|
let tagMap = new Map();
|
||||||
|
if (!this.showAutomatic) {
|
||||||
|
tags = tags.filter(t => t.type != 1)
|
||||||
|
} else {
|
||||||
|
// Ensure no duplicates from auto and manual tags
|
||||||
|
tags.forEach(t => !tagMap.has() && t.type != 1 && tagMap.set(t.tag, t));
|
||||||
|
tags = Array.from(tagMap.values());
|
||||||
|
}
|
||||||
|
if (this.state.searchString) {
|
||||||
|
tags = tags.filter(tag => !!tag.tag.match(new RegExp(this.state.searchString, 'i')));
|
||||||
|
}
|
||||||
|
tags = tags.map(t => {
|
||||||
|
let name = t.tag;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
selected: this.selectedTags.has(name),
|
||||||
|
color: this.state.tagColors.has(name) ? this.state.tagColors.get(name).color : '',
|
||||||
|
disabled: !this.inScope.has(name)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return <TagSelector
|
||||||
|
tags={tags}
|
||||||
|
ref={ref => this.focusTextbox = ref && ref.focusTextbox}
|
||||||
|
searchString={this.state.searchString}
|
||||||
|
shouldFocus={this.state.shouldFocus}
|
||||||
|
dragObserver={this.dragObserver}
|
||||||
|
onSelect={this.state.viewOnly ? () => {} : this.handleTagSelected}
|
||||||
|
onTagContext={this.handleTagContext}
|
||||||
|
onSearch={this.handleSearch}
|
||||||
|
onSettings={this.handleSettings}
|
||||||
|
loaded={this.state.loaded}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(mode) {
|
||||||
|
this.state.viewOnly != (mode == 'view') && this.setState({viewOnly: mode == 'view'});
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister() {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.domEl);
|
||||||
|
if (this._notifierID) {
|
||||||
|
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uninit() {
|
||||||
|
this.setState({searchString: ''});
|
||||||
|
this.selectedTags = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTagContext = (tag, ev) => {
|
||||||
|
let tagContextMenu = document.getElementById('tag-menu');
|
||||||
|
ev.preventDefault();
|
||||||
|
tagContextMenu.openPopup(null, null, ev.clientX+2, ev.clientY+2);
|
||||||
|
this.contextTag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSettings = (ev) => {
|
||||||
|
let settingsContextMenu = document.getElementById('tag-selector-view-settings-menu');
|
||||||
|
ev.preventDefault();
|
||||||
|
settingsContextMenu.openPopup(ev.target, 'end_before', 0, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTagSelected = (tag) => {
|
||||||
|
let selectedTags = this.selectedTags;
|
||||||
|
if(selectedTags.has(tag)) {
|
||||||
|
selectedTags.delete(tag);
|
||||||
|
} else {
|
||||||
|
selectedTags.add(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(this.props.onSelection) === 'function') {
|
||||||
|
this.props.onSelection(selectedTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch = Zotero.Utilities.debounce((searchString) => {
|
||||||
|
this.setState({searchString});
|
||||||
|
})
|
||||||
|
|
||||||
|
dragObserver = {
|
||||||
|
onDragOver: function(event) {
|
||||||
|
if (!event.dataTransfer.getData('zotero/item')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elem = event.target;
|
||||||
|
|
||||||
|
// Ignore drops not on tags
|
||||||
|
if (elem.localName != 'li') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the event, because drop event does not have shiftKey attribute set
|
||||||
|
Zotero.DragDrop.currentEvent = event;
|
||||||
|
elem.classList.add('dragged-over');
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "copy";
|
||||||
|
},
|
||||||
|
onDragExit: function (event) {
|
||||||
|
Zotero.DragDrop.currentEvent = null;
|
||||||
|
event.target.classList.remove('dragged-over');
|
||||||
|
},
|
||||||
|
onDrop: async function(event) {
|
||||||
|
var elem = event.target;
|
||||||
|
|
||||||
|
// Ignore drops not on tags
|
||||||
|
if (elem.localName != 'li') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
elem.classList.remove('dragged-over');
|
||||||
|
|
||||||
|
var dt = event.dataTransfer;
|
||||||
|
var ids = dt.getData('zotero/item');
|
||||||
|
if (!ids) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Zotero.DB.executeTransaction(function* () {
|
||||||
|
ids = ids.split(',');
|
||||||
|
var items = Zotero.Items.get(ids);
|
||||||
|
var value = elem.textContent;
|
||||||
|
|
||||||
|
for (let i=0; i<items.length; i++) {
|
||||||
|
let item = items[i];
|
||||||
|
if (Zotero.DragDrop.currentEvent.shiftKey) {
|
||||||
|
item.removeTag(value);
|
||||||
|
} else {
|
||||||
|
item.addTag(value);
|
||||||
|
}
|
||||||
|
yield item.save();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagSelection() {
|
||||||
|
return this.selectedTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTagSelection() {
|
||||||
|
this.selectedTags = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
async openColorPickerWindow() {
|
||||||
|
var io = {
|
||||||
|
libraryID: this.libraryID,
|
||||||
|
name: this.contextTag.name
|
||||||
|
};
|
||||||
|
|
||||||
|
var tagColors = this.state.tagColors;
|
||||||
|
if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) {
|
||||||
|
var ps = Cc['@mozilla.org/embedcomp/prompt-service;1']
|
||||||
|
.getService(Ci.nsIPromptService);
|
||||||
|
ps.alert(null, '', Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
io.tagColors = tagColors;
|
||||||
|
|
||||||
|
window.openDialog(
|
||||||
|
'chrome://zotero/content/tagColorChooser.xul',
|
||||||
|
'zotero-tagSelector-colorChooser',
|
||||||
|
'chrome,modal,centerscreen', io
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dialog cancel
|
||||||
|
if (typeof io.color == 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openRenamePrompt() {
|
||||||
|
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
|
||||||
|
.getService(Ci.nsIPromptService);
|
||||||
|
|
||||||
|
var newName = { value: this.contextTag.name };
|
||||||
|
var result = promptService.prompt(window,
|
||||||
|
Zotero.getString('pane.tagSelector.rename.title'),
|
||||||
|
Zotero.getString('pane.tagSelector.rename.message'),
|
||||||
|
newName, '', {});
|
||||||
|
|
||||||
|
if (!result || !newName.value || this.contextTag.name == newName.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedTags = this.selectedTags;
|
||||||
|
if (selectedTags.has(this.contextTag.name)) {
|
||||||
|
var wasSelected = true;
|
||||||
|
selectedTags.delete(this.contextTag.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Zotero.Tags.getID(this.contextTag.name)) {
|
||||||
|
await Zotero.Tags.rename(this.libraryID, this.contextTag.name, newName.value);
|
||||||
|
}
|
||||||
|
// Colored tags don't need to exist, so in that case
|
||||||
|
// just rename the color setting
|
||||||
|
else {
|
||||||
|
let color = Zotero.Tags.getColor(this.libraryID, this.contextTag.name);
|
||||||
|
if (!color) {
|
||||||
|
throw new Error("Can't rename missing tag");
|
||||||
|
}
|
||||||
|
await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false);
|
||||||
|
await Zotero.Tags.setColor(this.libraryID, newName.value, color.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasSelected) {
|
||||||
|
selectedTags.add(newName.value);
|
||||||
|
}
|
||||||
|
this.setState({tags: await this.getTags()})
|
||||||
|
}
|
||||||
|
|
||||||
|
async openDeletePrompt() {
|
||||||
|
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
|
||||||
|
.getService(Ci.nsIPromptService);
|
||||||
|
|
||||||
|
var confirmed = promptService.confirm(window,
|
||||||
|
Zotero.getString('pane.tagSelector.delete.title'),
|
||||||
|
Zotero.getString('pane.tagSelector.delete.message'));
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagID = Zotero.Tags.getID(this.contextTag.name);
|
||||||
|
|
||||||
|
if (tagID) {
|
||||||
|
await Zotero.Tags.removeFromLibrary(this.libraryID, tagID);
|
||||||
|
}
|
||||||
|
// If only a tag color setting, remove that
|
||||||
|
else {
|
||||||
|
await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({tags: await this.getTags()});
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleDisplayAllTags(newValue) {
|
||||||
|
newValue = typeof(newValue) === 'undefined' ? !this.displayAllTags : newValue;
|
||||||
|
Zotero.Prefs.set('tagSelector.displayAllTags', newValue);
|
||||||
|
this.displayAllTags = newValue;
|
||||||
|
this.setState({tags: await this.getTags()});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShowAutomatic(newValue) {
|
||||||
|
newValue = typeof(newValue) === 'undefined' ? !this.showAutomatic : newValue;
|
||||||
|
Zotero.Prefs.set('tagSelector.showAutomatic', newValue);
|
||||||
|
this.setState({showAutomatic: newValue});
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectAll() {
|
||||||
|
this.selectedTags = new Set();
|
||||||
|
if('onSelection' in this.props && typeof(this.props.onSelection) === 'function') {
|
||||||
|
this.props.onSelection(this.selectedTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
let count = this.selectedTags.size;
|
||||||
|
let mod = count === 1 ? 'singular' : count === 0 ? 'none' : 'plural';
|
||||||
|
|
||||||
|
return Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showAutomatic() {
|
||||||
|
return this.state.showAutomatic;
|
||||||
|
}
|
||||||
|
|
||||||
|
static init(domEl, opts) {
|
||||||
|
var ref;
|
||||||
|
let elem = (
|
||||||
|
<IntlProvider locale={Zotero.locale} messages={ZoteroPane.Containers.intlMessages}>
|
||||||
|
<TagSelectorContainer ref={c => ref = c } {...opts} />
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
ReactDOM.render(elem, domEl);
|
||||||
|
ref.domEl = domEl;
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
58
chrome/content/zotero/containers/tagSelector.xul
Normal file
58
chrome/content/zotero/containers/tagSelector.xul
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2017 Center for History and New Media
|
||||||
|
George Mason University, Fairfax, Virginia, USA
|
||||||
|
http://zotero.org
|
||||||
|
|
||||||
|
This file is part of Zotero.
|
||||||
|
|
||||||
|
Zotero is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Zotero is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***** END LICENSE BLOCK *****
|
||||||
|
-->
|
||||||
|
<!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">
|
||||||
|
<menupopup id="tag-menu">
|
||||||
|
<menuitem label="&zotero.tagSelector.assignColor;"
|
||||||
|
oncommand="ZoteroPane.tagSelector.openColorPickerWindow(); event.stopPropagation();"/>
|
||||||
|
<menuitem label="&zotero.tagSelector.renameTag;"
|
||||||
|
oncommand="ZoteroPane.tagSelector.openRenamePrompt(); event.stopPropagation();"/>
|
||||||
|
<menuitem label="&zotero.tagSelector.deleteTag;"
|
||||||
|
oncommand="ZoteroPane.tagSelector.openDeletePrompt(); event.stopPropagation();"/>
|
||||||
|
</menupopup>
|
||||||
|
<menupopup id="tag-selector-view-settings-menu"
|
||||||
|
onpopupshowing="
|
||||||
|
document.getElementById('show-automatic').setAttribute('checked', ZoteroPane.tagSelector.showAutomatic);
|
||||||
|
document.getElementById('display-all-tags').setAttribute('checked', ZoteroPane.tagSelector.displayAllTags);
|
||||||
|
document.getElementById('num-selected').label = ZoteroPane.tagSelector.label">
|
||||||
|
<menuitem id="num-selected" disabled="true"/>
|
||||||
|
<menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;"
|
||||||
|
oncommand="ZoteroPane.tagSelector.deselectAll(); event.stopPropagation();"/>
|
||||||
|
<menuseparator/>
|
||||||
|
<menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" type="checkbox"
|
||||||
|
oncommand="ZoteroPane.tagSelector.toggleShowAutomatic(); event.stopPropagation();"/>
|
||||||
|
<menuitem
|
||||||
|
id="display-all-tags"
|
||||||
|
label="&zotero.tagSelector.displayAllInLibrary;"
|
||||||
|
type="checkbox"
|
||||||
|
oncommand="ZoteroPane.tagSelector.toggleDisplayAllTags(); event.stopPropagation();"
|
||||||
|
/>
|
||||||
|
</menupopup>
|
||||||
|
</overlay>
|
|
@ -7,4 +7,9 @@ var Zotero = Components.classes['@zotero.org/Zotero;1']
|
||||||
.getService(Components.interfaces.nsISupports)
|
.getService(Components.interfaces.nsISupports)
|
||||||
.wrappedJSObject;
|
.wrappedJSObject;
|
||||||
|
|
||||||
Components.utils.import('resource://zotero/require.js');
|
// Components.utils.import('resource://zotero/require.js');
|
||||||
|
// Not using Cu.import here since we don't want the require module to be cached
|
||||||
|
// for includes within ZoteroPane or other code, where we want the window instance available to modules.
|
||||||
|
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
|
||||||
|
.getService(Components.interfaces.mozIJSSubScriptLoader)
|
||||||
|
.loadSubScript('resource://zotero/require.js');
|
||||||
|
|
|
@ -47,7 +47,7 @@ const ZoteroStandalone = new function() {
|
||||||
}
|
}
|
||||||
return Zotero.initializationPromise;
|
return Zotero.initializationPromise;
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(async function () {
|
||||||
if (Zotero.Prefs.get('devtools.errorconsole.enabled', true)) {
|
if (Zotero.Prefs.get('devtools.errorconsole.enabled', true)) {
|
||||||
document.getElementById('menu_errorConsole').hidden = false;
|
document.getElementById('menu_errorConsole').hidden = false;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ const ZoteroStandalone = new function() {
|
||||||
ZoteroStandalone.DebugOutput.init();
|
ZoteroStandalone.DebugOutput.init();
|
||||||
|
|
||||||
Zotero.hideZoteroPaneOverlays();
|
Zotero.hideZoteroPaneOverlays();
|
||||||
|
await ZoteroPane.Containers.init();
|
||||||
ZoteroPane.init();
|
ZoteroPane.init();
|
||||||
ZoteroPane.makeVisible();
|
ZoteroPane.makeVisible();
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,8 @@
|
||||||
windowtype="navigator:browser"
|
windowtype="navigator:browser"
|
||||||
title="&brandShortName;"
|
title="&brandShortName;"
|
||||||
width="1000" height="600"
|
width="1000" height="600"
|
||||||
persist="screenX screenY width height sizemode">
|
persist="screenX screenY width height sizemode">
|
||||||
|
<script type="application/javascript" src="resource://zotero/require.js"/>
|
||||||
<script type="application/javascript" src="standalone.js"/>
|
<script type="application/javascript" src="standalone.js"/>
|
||||||
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
|
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
|
||||||
<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
|
<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
|
||||||
|
@ -287,4 +288,7 @@
|
||||||
<stack id="zotero-pane-stack" fullscreenmode="true" flex="1"/>
|
<stack id="zotero-pane-stack" fullscreenmode="true" flex="1"/>
|
||||||
</vbox>
|
</vbox>
|
||||||
</hbox>
|
</hbox>
|
||||||
|
|
||||||
|
<menupopup id="tag-menu"/>
|
||||||
|
<menupopup id="tag-selector-view-settings-menu"/>
|
||||||
</window>
|
</window>
|
||||||
|
|
|
@ -742,9 +742,10 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
||||||
session,
|
session,
|
||||||
targetID,
|
targetID,
|
||||||
requestData,
|
requestData,
|
||||||
function (topLevelItems) {
|
function (jsonItems, items) {
|
||||||
|
session.addItems(items);
|
||||||
// Only return the properties the connector needs
|
// Only return the properties the connector needs
|
||||||
topLevelItems = topLevelItems.map((item) => {
|
jsonItems = jsonItems.map((item) => {
|
||||||
let o = {
|
let o = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
|
@ -764,7 +765,7 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
||||||
};
|
};
|
||||||
return o;
|
return o;
|
||||||
});
|
});
|
||||||
resolve([201, "application/json", JSON.stringify({items: topLevelItems})]);
|
resolve([201, "application/json", JSON.stringify({items: jsonItems})]);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Add items to session once all attachments have been saved
|
// Add items to session once all attachments have been saved
|
||||||
|
|
|
@ -1850,7 +1850,7 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i
|
||||||
// Clear the quick search and tag selection and try again (once)
|
// Clear the quick search and tag selection and try again (once)
|
||||||
if (!noRecurse && this.window.ZoteroPane) {
|
if (!noRecurse && this.window.ZoteroPane) {
|
||||||
let cleared1 = yield this.window.ZoteroPane.clearQuicksearch();
|
let cleared1 = yield this.window.ZoteroPane.clearQuicksearch();
|
||||||
let cleared2 = this.window.ZoteroPane.clearTagSelection();
|
let cleared2 = this.window.ZoteroPane.tagSelector.clearTagSelection();
|
||||||
if (cleared1 || cleared2) {
|
if (cleared1 || cleared2) {
|
||||||
return this.selectItem(id, expand, true);
|
return this.selectItem(id, expand, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,7 +179,7 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
if (itemsDoneCallback) {
|
if (itemsDoneCallback) {
|
||||||
itemsDoneCallback(items.map(item => jsonByItem.get(item)));
|
itemsDoneCallback(items.map(item => jsonByItem.get(item)), items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save standalone attachments
|
// Save standalone attachments
|
||||||
|
|
|
@ -144,6 +144,25 @@ var CSL_TYPE_MAPPINGS = {
|
||||||
* @class Functions for text manipulation and other miscellaneous purposes
|
* @class Functions for text manipulation and other miscellaneous purposes
|
||||||
*/
|
*/
|
||||||
Zotero.Utilities = {
|
Zotero.Utilities = {
|
||||||
|
/**
|
||||||
|
* Returns a function which will execute `fn` with provided arguments after `delay` milliseconds and not more
|
||||||
|
* than once, if called multiple times. See
|
||||||
|
* http://stackoverflow.com/questions/24004791/can-someone-explain-the-debounce-function-in-javascript
|
||||||
|
* @param fn {Function} function to debounce
|
||||||
|
* @param delay {Integer} number of miliseconds to delay the function execution
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
debounce: function(fn, delay=500) {
|
||||||
|
var timer = null;
|
||||||
|
return function () {
|
||||||
|
let args = arguments;
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(function () {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}.bind(this), delay);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixes author name capitalization.
|
* Fixes author name capitalization.
|
||||||
* Currently for all uppercase names only
|
* Currently for all uppercase names only
|
||||||
|
|
|
@ -58,8 +58,6 @@ var ZoteroPane = new function()
|
||||||
|
|
||||||
this.document = document;
|
this.document = document;
|
||||||
|
|
||||||
const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar
|
|
||||||
|
|
||||||
var self = this,
|
var self = this,
|
||||||
_loaded = false, _madeVisible = false,
|
_loaded = false, _madeVisible = false,
|
||||||
titlebarcolorState, titleState, observerService,
|
titlebarcolorState, titleState, observerService,
|
||||||
|
@ -145,6 +143,8 @@ var ZoteroPane = new function()
|
||||||
Zotero.hiDPI = window.devicePixelRatio > 1;
|
Zotero.hiDPI = window.devicePixelRatio > 1;
|
||||||
Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
|
Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
|
||||||
|
|
||||||
|
ZoteroPane_Local.Containers.loadPane();
|
||||||
|
|
||||||
ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
|
ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
|
||||||
|
|
||||||
// Add a default progress window
|
// Add a default progress window
|
||||||
|
@ -171,11 +171,6 @@ var ZoteroPane = new function()
|
||||||
itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
|
itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
|
||||||
itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
|
itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
|
||||||
|
|
||||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
|
||||||
tagSelector.onchange = function () {
|
|
||||||
return ZoteroPane_Local.updateTagFilter();
|
|
||||||
};
|
|
||||||
|
|
||||||
Zotero.Keys.windowInit(document);
|
Zotero.Keys.windowInit(document);
|
||||||
|
|
||||||
if (Zotero.restoreFromServer) {
|
if (Zotero.restoreFromServer) {
|
||||||
|
@ -351,8 +346,7 @@ var ZoteroPane = new function()
|
||||||
this.serializePersist();
|
this.serializePersist();
|
||||||
}
|
}
|
||||||
|
|
||||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
ZoteroPane_Local.Containers.destroy();
|
||||||
tagSelector.unregister();
|
|
||||||
|
|
||||||
if(this.collectionsView) this.collectionsView.unregister();
|
if(this.collectionsView) this.collectionsView.unregister();
|
||||||
if(this.itemsView) this.itemsView.unregister();
|
if(this.itemsView) this.itemsView.unregister();
|
||||||
|
@ -395,7 +389,6 @@ var ZoteroPane = new function()
|
||||||
this.unserializePersist();
|
this.unserializePersist();
|
||||||
this.updateLayout();
|
this.updateLayout();
|
||||||
this.updateToolbarPosition();
|
this.updateToolbarPosition();
|
||||||
this.updateTagSelectorSize();
|
|
||||||
|
|
||||||
// restore saved row selection (for tab switching)
|
// restore saved row selection (for tab switching)
|
||||||
// TODO: Remove now that no tab mode?
|
// TODO: Remove now that no tab mode?
|
||||||
|
@ -1100,100 +1093,25 @@ var ZoteroPane = new function()
|
||||||
|
|
||||||
var showing = tagSelector.getAttribute('collapsed') == 'true';
|
var showing = tagSelector.getAttribute('collapsed') == 'true';
|
||||||
tagSelector.setAttribute('collapsed', !showing);
|
tagSelector.setAttribute('collapsed', !showing);
|
||||||
this.updateTagSelectorSize();
|
|
||||||
|
|
||||||
// If showing, set scope to items in current view
|
// If showing, set scope to items in current view
|
||||||
// and focus filter textbox
|
// and focus filter textbox
|
||||||
if (showing) {
|
if (showing) {
|
||||||
yield this.setTagScope();
|
yield this.setTagScope();
|
||||||
tagSelector.focusTextbox();
|
ZoteroPane_Local.tagSelector.focusTextbox();
|
||||||
}
|
}
|
||||||
// If hiding, clear selection
|
// If hiding, clear selection
|
||||||
else {
|
else {
|
||||||
tagSelector.uninit();
|
ZoteroPane_Local.tagSelector.uninit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.updateTagSelectorSize = function () {
|
|
||||||
//Zotero.debug('Updating tag selector size');
|
|
||||||
var zoteroPane = document.getElementById('zotero-pane-stack');
|
|
||||||
var splitter = document.getElementById('zotero-tags-splitter');
|
|
||||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
|
||||||
|
|
||||||
// Nothing should be bigger than appcontent's height
|
|
||||||
var max = document.getElementById('appcontent').boxObject.height
|
|
||||||
- splitter.boxObject.height;
|
|
||||||
|
|
||||||
// Shrink tag selector to appcontent's height
|
|
||||||
var maxTS = max - COLLECTIONS_HEIGHT;
|
|
||||||
if (parseInt(tagSelector.getAttribute("height")) > maxTS) {
|
|
||||||
//Zotero.debug("Limiting tag selector height to appcontent");
|
|
||||||
tagSelector.setAttribute('height', maxTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
var height = tagSelector.boxObject.height;
|
|
||||||
|
|
||||||
|
|
||||||
/*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height);
|
|
||||||
Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height'));
|
|
||||||
Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height);
|
|
||||||
Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/
|
|
||||||
|
|
||||||
|
|
||||||
// Don't let the Z-pane jump back down to its previous height
|
|
||||||
// (if shrinking or hiding the tag selector let it clear the min-height)
|
|
||||||
if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) {
|
|
||||||
//Zotero.debug("Setting Zotero pane height attribute to " + zoteroPane.boxObject.height);
|
|
||||||
zoteroPane.setAttribute('height', zoteroPane.boxObject.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagSelector.getAttribute('collapsed') == 'true') {
|
|
||||||
// 32px is the default Z pane min-height in overlay.css
|
|
||||||
height = 32;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// tS.boxObject.height doesn't exist at startup, so get from attribute
|
|
||||||
if (!height) {
|
|
||||||
height = parseInt(tagSelector.getAttribute('height'));
|
|
||||||
}
|
|
||||||
// 121px seems to be enough room for the toolbar and collections
|
|
||||||
// tree at minimum height
|
|
||||||
height = height + COLLECTIONS_HEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Zotero.debug('Setting Zotero pane minheight to ' + height);
|
|
||||||
zoteroPane.setAttribute('minheight', height);
|
|
||||||
|
|
||||||
if (this.isShowing() && !this.isFullScreen()) {
|
|
||||||
zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix bug whereby resizing the Z pane downward after resizing
|
|
||||||
// the tag selector up and then down sometimes caused the Z pane to
|
|
||||||
// stay at a fixed size and get pushed below the bottom
|
|
||||||
tagSelector.height++;
|
|
||||||
tagSelector.height--;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getTagSelection() {
|
|
||||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
|
||||||
return tagSelector.selection ? tagSelector.selection : new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.clearTagSelection = function () {
|
|
||||||
document.getElementById('zotero-tag-selector').deselectAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sets the tag filter on the items view
|
* Sets the tag filter on the items view
|
||||||
*/
|
*/
|
||||||
this.updateTagFilter = Zotero.Promise.coroutine(function* () {
|
this.updateTagFilter = Zotero.Promise.coroutine(function* () {
|
||||||
if (this.itemsView) {
|
if (this.itemsView) {
|
||||||
yield this.itemsView.setFilter('tags', getTagSelection());
|
yield this.itemsView.setFilter('tags', ZoteroPane_Local.tagSelector.getTagSelection());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1212,23 +1130,23 @@ var ZoteroPane = new function()
|
||||||
*
|
*
|
||||||
* Passed to the items tree to trigger on changes
|
* Passed to the items tree to trigger on changes
|
||||||
*/
|
*/
|
||||||
this.setTagScope = Zotero.Promise.coroutine(function* () {
|
this.setTagScope = async function () {
|
||||||
var collectionTreeRow = this.getCollectionTreeRow();
|
var collectionTreeRow = self.getCollectionTreeRow();
|
||||||
var tagSelector = document.getElementById('zotero-tag-selector');
|
if (self.tagSelectorShown()) {
|
||||||
if (this.tagSelectorShown()) {
|
|
||||||
Zotero.debug('Updating tag selector with current tags');
|
Zotero.debug('Updating tag selector with current tags');
|
||||||
if (collectionTreeRow.editable) {
|
if (collectionTreeRow.editable) {
|
||||||
tagSelector.mode = 'edit';
|
ZoteroPane_Local.tagSelector.setMode('edit');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
tagSelector.mode = 'view';
|
ZoteroPane_Local.tagSelector.setMode('view');
|
||||||
}
|
}
|
||||||
tagSelector.collectionTreeRow = collectionTreeRow;
|
ZoteroPane_Local.tagSelector.onItemViewChanged({
|
||||||
tagSelector.updateScope = () => this.setTagScope();
|
collectionTreeRow,
|
||||||
tagSelector.libraryID = collectionTreeRow.ref.libraryID;
|
libraryID: collectionTreeRow.ref.libraryID,
|
||||||
tagSelector.scope = yield collectionTreeRow.getChildTags();
|
tagsInScope: await collectionTreeRow.getChildTags()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
||||||
this.onCollectionSelected = function () {
|
this.onCollectionSelected = function () {
|
||||||
|
@ -1280,7 +1198,7 @@ var ZoteroPane = new function()
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
collectionTreeRow.setSearch('');
|
collectionTreeRow.setSearch('');
|
||||||
collectionTreeRow.setTags(getTagSelection());
|
collectionTreeRow.setTags(ZoteroPane_Local.tagSelector.getTagSelection());
|
||||||
|
|
||||||
this._updateToolbarIconsForRow(collectionTreeRow);
|
this._updateToolbarIconsForRow(collectionTreeRow);
|
||||||
|
|
||||||
|
@ -1294,17 +1212,7 @@ var ZoteroPane = new function()
|
||||||
ZoteroPane_Local.displayErrorMessage();
|
ZoteroPane_Local.displayErrorMessage();
|
||||||
};
|
};
|
||||||
this.itemsView.onRefresh.addListener(() => this.setTagScope());
|
this.itemsView.onRefresh.addListener(() => this.setTagScope());
|
||||||
if (this.tagSelectorShown()) {
|
this.itemsView.onLoad.addListener(() => Zotero.uiIsReady());
|
||||||
let tagSelector = document.getElementById('zotero-tag-selector')
|
|
||||||
let handler = function () {
|
|
||||||
tagSelector.removeEventListener('refresh', handler);
|
|
||||||
Zotero.uiIsReady();
|
|
||||||
};
|
|
||||||
tagSelector.addEventListener('refresh', handler);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.itemsView.onLoad.addListener(() => Zotero.uiIsReady());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If item data not yet loaded for library, load it now.
|
// If item data not yet loaded for library, load it now.
|
||||||
// Other data types are loaded at startup
|
// Other data types are loaded at startup
|
||||||
|
@ -4910,8 +4818,10 @@ var ZoteroPane = new function()
|
||||||
var itemsToolbar = document.getElementById("zotero-items-toolbar");
|
var itemsToolbar = document.getElementById("zotero-items-toolbar");
|
||||||
var itemPane = document.getElementById("zotero-item-pane");
|
var itemPane = document.getElementById("zotero-item-pane");
|
||||||
var itemToolbar = document.getElementById("zotero-item-toolbar");
|
var itemToolbar = document.getElementById("zotero-item-toolbar");
|
||||||
|
var tagSelector = document.getElementById("zotero-tag-selector");
|
||||||
|
|
||||||
collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px';
|
collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px';
|
||||||
|
tagSelector.style.maxWidth = collectionsPane.boxObject.width + 'px';
|
||||||
|
|
||||||
if (stackedLayout || itemPane.collapsed) {
|
if (stackedLayout || itemPane.collapsed) {
|
||||||
// The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing).
|
// The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing).
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
||||||
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
|
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
|
||||||
|
<?xul-overlay href="chrome://zotero/content/containers/containers.xul"?>
|
||||||
|
|
||||||
<!DOCTYPE overlay [
|
<!DOCTYPE overlay [
|
||||||
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
|
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
|
||||||
|
@ -35,7 +36,8 @@
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<overlay id="zotero"
|
<overlay id="zotero"
|
||||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
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="include.js"/>
|
||||||
<script src="zoteroPane.js" type="application/javascript;version=1.8"/>
|
<script src="zoteroPane.js" type="application/javascript;version=1.8"/>
|
||||||
|
@ -304,7 +306,7 @@
|
||||||
ondragover="return ZoteroPane_Local.collectionsView.onDragOver(event)"
|
ondragover="return ZoteroPane_Local.collectionsView.onDragOver(event)"
|
||||||
ondrop="return ZoteroPane_Local.collectionsView.onDrop(event)"/>
|
ondrop="return ZoteroPane_Local.collectionsView.onDrop(event)"/>
|
||||||
</tree>
|
</tree>
|
||||||
<splitter id="zotero-tags-splitter" onmouseup="ZoteroPane_Local.updateTagSelectorSize()" collapse="after"
|
<splitter id="zotero-tags-splitter" collapse="after"
|
||||||
zotero-persist="state">
|
zotero-persist="state">
|
||||||
<grippy oncommand="ZoteroPane_Local.toggleTagSelector()"/>
|
<grippy oncommand="ZoteroPane_Local.toggleTagSelector()"/>
|
||||||
</splitter>
|
</splitter>
|
||||||
|
@ -314,7 +316,9 @@
|
||||||
|
|
||||||
TODO: deal with this some other way?
|
TODO: deal with this some other way?
|
||||||
-->
|
-->
|
||||||
<zoterotagselector id="zotero-tag-selector" zotero-persist="height,collapsed,showAutomatic,filterToScope"/>
|
<vbox id="zotero-tag-selector-container" zotero-persist="height,collapsed">
|
||||||
|
<html:div id="zotero-tag-selector"/>
|
||||||
|
</vbox>
|
||||||
</vbox>
|
</vbox>
|
||||||
|
|
||||||
<splitter id="zotero-collections-splitter" resizebefore="closest" resizeafter="closest" collapse="before"
|
<splitter id="zotero-collections-splitter" resizebefore="closest" resizeafter="closest" collapse="before"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* Font sizes */
|
/* Font sizes */
|
||||||
*[zoteroFontSize=medium] #zotero-tb-search, *[zoteroFontSize=large] #zotero-tb-search,
|
*[zoteroFontSize=medium] #zotero-tb-search, *[zoteroFontSize=large] #zotero-tb-search,
|
||||||
*[zoteroFontSize=medium] zoterotagselector textbox, *[zoteroFontSize=large] zoterotagselector textbox
|
|
||||||
{
|
{
|
||||||
font-size: 1em !important;
|
font-size: 1em !important;
|
||||||
}
|
}
|
||||||
|
@ -81,11 +80,6 @@ zoterosearch
|
||||||
-moz-binding: url('chrome://zotero/content/bindings/zoterosearch.xml#search-box');
|
-moz-binding: url('chrome://zotero/content/bindings/zoterosearch.xml#search-box');
|
||||||
}
|
}
|
||||||
|
|
||||||
zoterotagselector
|
|
||||||
{
|
|
||||||
-moz-binding: url('chrome://zotero/content/bindings/tagselector.xml#tag-selector');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
zoterosearchcondition
|
zoterosearchcondition
|
||||||
{
|
{
|
||||||
|
@ -177,14 +171,15 @@ label.zotero-text-link {
|
||||||
background-image: url('chrome://zotero/skin/plus-active.png') !important;
|
background-image: url('chrome://zotero/skin/plus-active.png') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zotero-clicky:not([disabled=true]):hover
|
.zotero-clicky:not([disabled=true]):not(.disabled):hover
|
||||||
{
|
{
|
||||||
background: rgb(187, 206, 241);
|
background: rgb(187, 206, 241);
|
||||||
border: 1px solid rgb(109, 149, 224);
|
border: 1px solid rgb(109, 149, 224);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zotero-clicky:not([disabled=true]):active,
|
.zotero-clicky:not([disabled=true]):not(.disabled):active,
|
||||||
.zotero-clicky[selected="true"]
|
.zotero-clicky[selected="true"],
|
||||||
|
.zotero-clicky.selected
|
||||||
{
|
{
|
||||||
color: white;
|
color: white;
|
||||||
background: rgb(89, 139, 236);
|
background: rgb(89, 139, 236);
|
||||||
|
|
|
@ -152,7 +152,13 @@ var isFirstLoadThisSession = true;
|
||||||
var zContext = null;
|
var zContext = null;
|
||||||
var initCallbacks = [];
|
var initCallbacks = [];
|
||||||
var zInitOptions = {};
|
var zInitOptions = {};
|
||||||
Components.utils.import('resource://zotero/require.js');
|
|
||||||
|
// Components.utils.import('resource://zotero/require.js');
|
||||||
|
// Not using Cu.import here since we don't want the require module to be cached
|
||||||
|
// for includes within ZoteroPane or other code, where we want the window instance available to modules.
|
||||||
|
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
|
||||||
|
.getService(Components.interfaces.mozIJSSubScriptLoader)
|
||||||
|
.loadSubScript('resource://zotero/require.js');
|
||||||
|
|
||||||
ZoteroContext = function() {}
|
ZoteroContext = function() {}
|
||||||
ZoteroContext.prototype = {
|
ZoteroContext.prototype = {
|
||||||
|
|
|
@ -69,8 +69,10 @@ pref("extensions.zotero.lastLongTagDelimiter", ";");
|
||||||
pref("extensions.zotero.fallbackSort", "firstCreator,date,title,dateAdded");
|
pref("extensions.zotero.fallbackSort", "firstCreator,date,title,dateAdded");
|
||||||
pref("extensions.zotero.sortCreatorAsString", false);
|
pref("extensions.zotero.sortCreatorAsString", false);
|
||||||
|
|
||||||
//Tag Cloud
|
|
||||||
pref("extensions.zotero.tagCloud", false);
|
//Tag Selector
|
||||||
|
pref("extensions.zotero.tagSelector.showAutomatic", true);
|
||||||
|
pref("extensions.zotero.tagSelector.displayAllTags", false);
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
pref("extensions.zotero.keys.openZotero", "Z");
|
pref("extensions.zotero.keys.openZotero", "Z");
|
||||||
|
|
1248
package-lock.json
generated
1248
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
@ -16,21 +16,23 @@
|
||||||
"license": "",
|
"license": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bluebird": "^3.5.1",
|
"bluebird": "^3.5.1",
|
||||||
"react": "^15.6.2",
|
"classnames": "^2.2.6",
|
||||||
"react-dom": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"zotero-web-library": "^0.9.4-alpha"
|
"react": "^16.6.3",
|
||||||
|
"react-dom": "^16.6.3",
|
||||||
|
"react-intl": "^2.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.3",
|
||||||
"babel-plugin-syntax-async-generators": "^6.13.0",
|
"babel-plugin-syntax-async-generators": "^6.13.0",
|
||||||
"babel-plugin-syntax-class-properties": "^6.13.0",
|
|
||||||
"babel-plugin-syntax-decorators": "^6.13.0",
|
"babel-plugin-syntax-decorators": "^6.13.0",
|
||||||
"babel-plugin-syntax-do-expressions": "^6.13.0",
|
"babel-plugin-syntax-do-expressions": "^6.13.0",
|
||||||
"babel-plugin-syntax-export-extensions": "^6.13.0",
|
"babel-plugin-syntax-export-extensions": "^6.13.0",
|
||||||
"babel-plugin-syntax-flow": "^6.13.0",
|
"babel-plugin-syntax-flow": "^6.13.0",
|
||||||
"babel-plugin-syntax-jsx": "^6.13.0",
|
"babel-plugin-syntax-jsx": "^6.13.0",
|
||||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
|
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||||
"babel-preset-react": "^6.16.0",
|
"babel-preset-react": "^6.16.0",
|
||||||
"browserify": "^14.5.0",
|
"browserify": "^14.5.0",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
|
@ -44,7 +46,7 @@
|
||||||
"jspath": "^0.4.0",
|
"jspath": "^0.4.0",
|
||||||
"mocha": "^3.5.3",
|
"mocha": "^3.5.3",
|
||||||
"multimatch": "^2.1.0",
|
"multimatch": "^2.1.0",
|
||||||
"node-sass": "^4.9.0",
|
"node-sass": "^4.11.0",
|
||||||
"sinon": "^4.5.0",
|
"sinon": "^4.5.0",
|
||||||
"universalify": "^0.1.1"
|
"universalify": "^0.1.1"
|
||||||
}
|
}
|
||||||
|
|
1
resource/classnames.js
Symbolic link
1
resource/classnames.js
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../node_modules/classnames/index.js
|
|
@ -26,7 +26,12 @@
|
||||||
EXPORTED_SYMBOLS = ["ConcurrentCaller"];
|
EXPORTED_SYMBOLS = ["ConcurrentCaller"];
|
||||||
|
|
||||||
if (!(typeof process === 'object' && process + '' === '[object process]')) {
|
if (!(typeof process === 'object' && process + '' === '[object process]')) {
|
||||||
Components.utils.import('resource://zotero/require.js');
|
// Components.utils.import('resource://zotero/require.js');
|
||||||
|
// Not using Cu.import here since we don't want the require module to be cached
|
||||||
|
// for includes within ZoteroPane or other code where we want the window instance available to modules.
|
||||||
|
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
|
||||||
|
.getService(Components.interfaces.mozIJSSubScriptLoader)
|
||||||
|
.loadSubScript('resource://zotero/require.js');
|
||||||
var Promise = require('resource://zotero/bluebird.js');
|
var Promise = require('resource://zotero/bluebird.js');
|
||||||
} else {
|
} else {
|
||||||
Promise = require('bluebird');
|
Promise = require('bluebird');
|
||||||
|
|
1
resource/prop-types.js
Symbolic link
1
resource/prop-types.js
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../node_modules/prop-types/prop-types.js
|
2
resource/react-dom.js
vendored
2
resource/react-dom.js
vendored
|
@ -1 +1 @@
|
||||||
../node_modules/react-dom/dist/react-dom.js
|
../node_modules/react-dom/umd/react-dom.development.js
|
1
resource/react-intl.js
vendored
Symbolic link
1
resource/react-intl.js
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../node_modules/react-intl/dist/react-intl.js
|
2
resource/react.js
vendored
2
resource/react.js
vendored
|
@ -1 +1 @@
|
||||||
../node_modules/react/dist/react.js
|
../node_modules/react/umd/react.development.js
|
|
@ -1,77 +1,102 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var EXPORTED_SYMBOLS = ['require'];
|
|
||||||
|
|
||||||
var require = (function() {
|
var require = (function() {
|
||||||
|
var win, Zotero;
|
||||||
var { Loader, Require, Module } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js');
|
var { Loader, Require, Module } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js');
|
||||||
var requirer = Module('/', '/');
|
var requirer = Module('/', '/');
|
||||||
var _runningTimers = {};
|
var _runningTimers = {};
|
||||||
var window = {};
|
if (typeof window != 'undefined') {
|
||||||
|
win = window;
|
||||||
|
} else {
|
||||||
|
win = {};
|
||||||
|
|
||||||
window.setTimeout = function (func, ms) {
|
win.setTimeout = function (func, ms) {
|
||||||
var id = Math.floor(Math.random() * (1000000000000 - 1)) + 1
|
var id = Math.floor(Math.random() * (1000000000000 - 1)) + 1
|
||||||
var useMethodjit = Components.utils.methodjit;
|
var useMethodjit = Components.utils.methodjit;
|
||||||
var timer = Components.classes["@mozilla.org/timer;1"]
|
var timer = Components.classes["@mozilla.org/timer;1"]
|
||||||
.createInstance(Components.interfaces.nsITimer);
|
.createInstance(Components.interfaces.nsITimer);
|
||||||
timer.initWithCallback({"notify":function() {
|
timer.initWithCallback({"notify":function() {
|
||||||
// Remove timer from object so it can be garbage collected
|
// Remove timer from object so it can be garbage collected
|
||||||
delete _runningTimers[id];
|
delete _runningTimers[id];
|
||||||
|
|
||||||
// Execute callback function
|
// Execute callback function
|
||||||
try {
|
try {
|
||||||
func();
|
func();
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
// Rethrow errors that occur so that they appear in the error
|
// Rethrow errors that occur so that they appear in the error
|
||||||
// console with the appropriate name and line numbers. While the
|
// console with the appropriate name and line numbers. While the
|
||||||
// the errors appear without this, the line numbers get eaten.
|
// the errors appear without this, the line numbers get eaten.
|
||||||
var scriptError = Components.classes["@mozilla.org/scripterror;1"]
|
var scriptError = Components.classes["@mozilla.org/scripterror;1"]
|
||||||
.createInstance(Components.interfaces.nsIScriptError);
|
.createInstance(Components.interfaces.nsIScriptError);
|
||||||
scriptError.init(
|
scriptError.init(
|
||||||
err.message || err.toString(),
|
err.message || err.toString(),
|
||||||
err.fileName || err.filename || null,
|
err.fileName || err.filename || null,
|
||||||
null,
|
null,
|
||||||
err.lineNumber || null,
|
err.lineNumber || null,
|
||||||
null,
|
null,
|
||||||
scriptError.errorFlag,
|
scriptError.errorFlag,
|
||||||
'component javascript'
|
'component javascript'
|
||||||
);
|
);
|
||||||
Components.classes["@mozilla.org/consoleservice;1"]
|
Components.classes["@mozilla.org/consoleservice;1"]
|
||||||
.getService(Components.interfaces.nsIConsoleService)
|
.getService(Components.interfaces.nsIConsoleService)
|
||||||
.logMessage(scriptError);
|
.logMessage(scriptError);
|
||||||
typeof Zotero !== 'undefined' && Zotero.debug(err.stack, 1);
|
typeof Zotero !== 'undefined' && Zotero.debug(err.stack, 1);
|
||||||
|
}
|
||||||
|
}}, ms, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
||||||
|
_runningTimers[id] = timer;
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
win.clearTimeout = function (id) {
|
||||||
|
var timer = _runningTimers[id];
|
||||||
|
if (timer) {
|
||||||
|
timer.cancel();
|
||||||
}
|
}
|
||||||
}}, ms, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
delete _runningTimers[id];
|
||||||
_runningTimers[id] = timer;
|
};
|
||||||
return id;
|
|
||||||
};
|
win.debug = function (msg) {
|
||||||
|
dump(msg + "\n\n");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
window.clearTimeout = function (id) {
|
function getZotero() {
|
||||||
var timer = _runningTimers[id];
|
if (typeof Zotero === 'undefined') {
|
||||||
if (timer) {
|
try {
|
||||||
timer.cancel();
|
Zotero = Components.classes["@zotero.org/Zotero;1"]
|
||||||
|
.getService(Components.interfaces.nsISupports).wrappedJSObject;
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
delete _runningTimers[id];
|
return Zotero || {};
|
||||||
};
|
}
|
||||||
|
|
||||||
window.debug = function (msg) {
|
var cons;
|
||||||
dump(msg + "\n\n");
|
if (typeof win.console !== 'undefined') {
|
||||||
|
cons = console;
|
||||||
|
}
|
||||||
|
if (!cons) {
|
||||||
|
cons = {};
|
||||||
|
for (let key of ['log', 'warn', 'error']) {
|
||||||
|
cons[key] = text => {getZotero(); typeof Zotero !== 'undefined' && false && Zotero.debug(`console.${key}: ${text}`)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let globals = {
|
||||||
|
window: win,
|
||||||
|
document: typeof win.document !== 'undefined' && win.document || {},
|
||||||
|
console: cons,
|
||||||
|
navigator: typeof win.navigator !== 'undefined' && win.navigator || {},
|
||||||
|
setTimeout: win.setTimeout,
|
||||||
|
clearTimeout: win.clearTimeout,
|
||||||
};
|
};
|
||||||
|
Object.defineProperty(globals, 'Zotero', { get: getZotero });
|
||||||
var loader = Loader({
|
var loader = Loader({
|
||||||
id: 'zotero/require',
|
id: 'zotero/require',
|
||||||
paths: {
|
paths: {
|
||||||
'': 'resource://zotero/',
|
'': 'resource://zotero/',
|
||||||
|
'components/': 'chrome://zotero/content/components/'
|
||||||
},
|
},
|
||||||
globals: {
|
globals
|
||||||
document: typeof document !== 'undefined' && document || {},
|
|
||||||
console: typeof console !== 'undefined' && console || {},
|
|
||||||
navigator: typeof navigator !== 'undefined' && navigator || {},
|
|
||||||
window,
|
|
||||||
setTimeout: window.setTimeout,
|
|
||||||
clearTimeout: window.clearTimeout,
|
|
||||||
Zotero: typeof Zotero !== 'undefined' && Zotero || {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Require(loader, requirer);
|
return Require(loader, requirer);
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../node_modules/zotero-web-library/lib
|
|
|
@ -12,7 +12,7 @@ const cluster = require('cluster');
|
||||||
async function babelWorker(ev) {
|
async function babelWorker(ev) {
|
||||||
const t1 = Date.now();
|
const t1 = Date.now();
|
||||||
const sourcefile = ev.file;
|
const sourcefile = ev.file;
|
||||||
const outfile = path.join('build', sourcefile);
|
const outfile = path.join('build', sourcefile.replace('.jsx', '.js'));
|
||||||
const postError = (error) => {
|
const postError = (error) => {
|
||||||
process.send({
|
process.send({
|
||||||
sourcefile,
|
sourcefile,
|
||||||
|
@ -26,9 +26,15 @@ async function babelWorker(ev) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let contents = await fs.readFile(sourcefile, 'utf8');
|
let contents = await fs.readFile(sourcefile, 'utf8');
|
||||||
if (sourcefile === 'resource/react-dom.js') {
|
if (sourcefile === 'resource/react.js') {
|
||||||
// patch react
|
// patch react
|
||||||
transformed = contents.replace(/ownerDocument\.createElement\((.*?)\)/gi, 'ownerDocument.createElementNS(DOMNamespaces.html, $1)');
|
transformed = contents.replace('instanceof Error', '.constructor.name == "Error"')
|
||||||
|
} else if (sourcefile === 'resource/react-dom.js') {
|
||||||
|
// and react-dom
|
||||||
|
transformed = contents.replace(/ ownerDocument\.createElement\((.*?)\)/gi, 'ownerDocument.createElementNS(HTML_NAMESPACE, $1)')
|
||||||
|
.replace('element instanceof win.HTMLIFrameElement',
|
||||||
|
'typeof element != "undefined" && element.tagName.toLowerCase() == "iframe"')
|
||||||
|
.replace("isInputEventSupported = false", 'isInputEventSupported = true');
|
||||||
} else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) {
|
} else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) {
|
||||||
transformed = contents;
|
transformed = contents;
|
||||||
isSkipped = true;
|
isSkipped = true;
|
||||||
|
|
|
@ -6,7 +6,7 @@ const getJS = require('./js');
|
||||||
const getSass = require('./sass');
|
const getSass = require('./sass');
|
||||||
const getSymlinks = require('./symlinks');
|
const getSymlinks = require('./symlinks');
|
||||||
const { formatDirsForMatcher, getSignatures, writeSignatures, cleanUp, onSuccess, onError} = require('./utils');
|
const { formatDirsForMatcher, getSignatures, writeSignatures, cleanUp, onSuccess, onError} = require('./utils');
|
||||||
const { dirs, symlinkDirs, copyDirs, symlinkFiles, jsFiles, ignoreMask } = require('./config');
|
const { dirs, symlinkDirs, copyDirs, symlinkFiles, jsFiles, scssFiles, ignoreMask } = require('./config');
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -16,6 +16,7 @@ if (require.main === module) {
|
||||||
const symlinks = symlinkFiles
|
const symlinks = symlinkFiles
|
||||||
.concat(dirs.map(d => `${d}/**`))
|
.concat(dirs.map(d => `${d}/**`))
|
||||||
.concat([`!${formatDirsForMatcher(dirs)}/**/*.js`])
|
.concat([`!${formatDirsForMatcher(dirs)}/**/*.js`])
|
||||||
|
.concat([`!${formatDirsForMatcher(dirs)}/**/*.jsx`])
|
||||||
.concat([`!${formatDirsForMatcher(copyDirs)}/**`])
|
.concat([`!${formatDirsForMatcher(copyDirs)}/**`])
|
||||||
|
|
||||||
const signatures = await getSignatures();
|
const signatures = await getSignatures();
|
||||||
|
@ -23,7 +24,7 @@ if (require.main === module) {
|
||||||
getBrowserify(signatures),
|
getBrowserify(signatures),
|
||||||
getCopy(copyDirs.map(d => `${d}/**`), { ignore: ignoreMask }, signatures),
|
getCopy(copyDirs.map(d => `${d}/**`), { ignore: ignoreMask }, signatures),
|
||||||
getJS(jsFiles, { ignore: ignoreMask }, signatures),
|
getJS(jsFiles, { ignore: ignoreMask }, signatures),
|
||||||
getSass('scss/*.scss', { root: 'scss', ignore: ignoreMask }, signatures),
|
getSass(scssFiles, { ignore: ignoreMask }, signatures),
|
||||||
getSymlinks(symlinks, { nodir: true, ignore: ignoreMask }, signatures),
|
getSymlinks(symlinks, { nodir: true, ignore: ignoreMask }, signatures),
|
||||||
getSymlinks(symlinkDirs, { ignore: ignoreMask }, signatures),
|
getSymlinks(symlinkDirs, { ignore: ignoreMask }, signatures),
|
||||||
cleanUp(signatures)
|
cleanUp(signatures)
|
||||||
|
|
|
@ -4,7 +4,6 @@ const dirs = [
|
||||||
'components',
|
'components',
|
||||||
'defaults',
|
'defaults',
|
||||||
'resource',
|
'resource',
|
||||||
'resource/web-library',
|
|
||||||
'test',
|
'test',
|
||||||
'test/resource/chai',
|
'test/resource/chai',
|
||||||
'test/resource/chai-as-promised',
|
'test/resource/chai-as-promised',
|
||||||
|
@ -49,13 +48,20 @@ const browserifyConfigs = [
|
||||||
];
|
];
|
||||||
|
|
||||||
// exclude mask used for js, copy, symlink and sass tasks
|
// exclude mask used for js, copy, symlink and sass tasks
|
||||||
const ignoreMask = ['**/#*'];
|
const ignoreMask = ['**/#*', '**/_*.scss'];
|
||||||
|
|
||||||
const jsFiles = [
|
const jsFiles = [
|
||||||
`{${dirs.join(',')}}/**/*.js`,
|
`{${dirs.join(',')}}/**/*.js`,
|
||||||
`!{${symlinkDirs.concat(copyDirs).join(',')}}/**/*.js`
|
`{${dirs.join(',')}}/**/*.jsx`,
|
||||||
|
`!{${symlinkDirs.concat(copyDirs).join(',')}}/**/*.js`,
|
||||||
|
`!{${symlinkDirs.concat(copyDirs).join(',')}}/**/*.jsx`
|
||||||
|
];
|
||||||
|
|
||||||
|
const scssFiles = [
|
||||||
|
'scss/**/*.scss',
|
||||||
|
'chrome/skin/default/zotero/**/*.scss'
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
dirs, symlinkDirs, copyDirs, symlinkFiles, browserifyConfigs, jsFiles, ignoreMask
|
dirs, symlinkDirs, copyDirs, symlinkFiles, browserifyConfigs, jsFiles, scssFiles, ignoreMask
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,7 @@ async function getJS(source, options, signatures) {
|
||||||
var f;
|
var f;
|
||||||
while ((f = matchingJSFiles.pop()) != null) {
|
while ((f = matchingJSFiles.pop()) != null) {
|
||||||
const newFileSignature = await getFileSignature(f);
|
const newFileSignature = await getFileSignature(f);
|
||||||
const dest = path.join('build', f);
|
const dest = path.join('build', f.replace('.jsx', '.js'));
|
||||||
f = path.normalize(f);
|
f = path.normalize(f);
|
||||||
if (f in signatures) {
|
if (f in signatures) {
|
||||||
if (compareSignatures(newFileSignature, signatures[f])) {
|
if (compareSignatures(newFileSignature, signatures[f])) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ const sassRender = universalify.fromCallback(sass.render);
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, '..');
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
async function getSass(source, options, signatures) {
|
async function getSass(source, options, signatures={}) {
|
||||||
const t1 = Date.now();
|
const t1 = Date.now();
|
||||||
const files = await globby(source, Object.assign({ cwd: ROOT }, options ));
|
const files = await globby(source, Object.assign({ cwd: ROOT }, options ));
|
||||||
const totalCount = files.length;
|
const totalCount = files.length;
|
||||||
|
@ -20,13 +20,22 @@ async function getSass(source, options, signatures) {
|
||||||
|
|
||||||
while ((f = files.pop()) != null) {
|
while ((f = files.pop()) != null) {
|
||||||
let newFileSignature = await getFileSignature(f);
|
let newFileSignature = await getFileSignature(f);
|
||||||
const dest = path.join.apply(this, ['build', 'chrome', 'skin', 'default', 'zotero', 'components', getPathRelativeTo(f, 'scss')]);
|
let destFile = getPathRelativeTo(f, 'scss');
|
||||||
|
destFile = path.join(path.dirname(destFile), path.basename(destFile, '.scss') + '.css');
|
||||||
|
let dest = path.join.apply(this, ['build', 'chrome', 'skin', 'default', 'zotero', destFile]);
|
||||||
|
if (['win', 'mac', 'unix'].some(platform => f.endsWith(`-${platform}.scss`))) {
|
||||||
|
let platform = f.slice(f.lastIndexOf('-')+1, f.lastIndexOf('.'));
|
||||||
|
destFile = destFile.slice(0, destFile.lastIndexOf('-'))
|
||||||
|
+ destFile.slice(destFile.lastIndexOf('-')+1+platform.length);
|
||||||
|
dest = path.join.apply(this, ['build', 'chrome', 'content', 'zotero-platform', platform, destFile]);
|
||||||
|
}
|
||||||
|
|
||||||
if (f in signatures) {
|
if (f in signatures) {
|
||||||
if (compareSignatures(newFileSignature, signatures[f])) {
|
if (compareSignatures(newFileSignature, signatures[f])) {
|
||||||
try {
|
try {
|
||||||
await fs.access(dest, fs.constants.F_OK);
|
await fs.access(dest, fs.constants.F_OK);
|
||||||
continue;
|
// TODO: Doesn't recompile on partial scss file changes, so temporarily disabled
|
||||||
|
// continue;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// file does not exists in build, fallback to browserifing
|
// file does not exists in build, fallback to browserifing
|
||||||
}
|
}
|
||||||
|
@ -34,10 +43,14 @@ async function getSass(source, options, signatures) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const sass = await sassRender({
|
const sass = await sassRender({
|
||||||
file: f
|
file: f,
|
||||||
|
outFile: dest,
|
||||||
|
sourceMap: true,
|
||||||
|
outputStyle: 'compressed'
|
||||||
});
|
});
|
||||||
|
|
||||||
await fs.outputFile(dest, sass);
|
await fs.outputFile(dest, sass.css);
|
||||||
|
await fs.outputFile(`${dest}.map`, sass.map);
|
||||||
onProgress(f, dest, 'sass');
|
onProgress(f, dest, 'sass');
|
||||||
signatures[f] = newFileSignature;
|
signatures[f] = newFileSignature;
|
||||||
count++;
|
count++;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const chokidar = require('chokidar');
|
const chokidar = require('chokidar');
|
||||||
const multimatch = require('multimatch');
|
const multimatch = require('multimatch');
|
||||||
const { dirs, jsFiles, ignoreMask, copyDirs, symlinkFiles } = require('./config');
|
const { dirs, jsFiles, scssFiles, ignoreMask, copyDirs, symlinkFiles } = require('./config');
|
||||||
const { onSuccess, onError, getSignatures, writeSignatures, cleanUp, formatDirsForMatcher } = require('./utils');
|
const { onSuccess, onError, getSignatures, writeSignatures, cleanUp, formatDirsForMatcher } = require('./utils');
|
||||||
const getJS = require('./js');
|
const getJS = require('./js');
|
||||||
const getSass = require('./sass');
|
const getSass = require('./sass');
|
||||||
|
@ -34,6 +34,7 @@ const source = [
|
||||||
const symlinks = symlinkFiles
|
const symlinks = symlinkFiles
|
||||||
.concat(dirs.map(d => `${d}/**`))
|
.concat(dirs.map(d => `${d}/**`))
|
||||||
.concat([`!${formatDirsForMatcher(dirs)}/**/*.js`])
|
.concat([`!${formatDirsForMatcher(dirs)}/**/*.js`])
|
||||||
|
.concat([`!${formatDirsForMatcher(dirs)}/**/*.jsx`])
|
||||||
.concat([`!${formatDirsForMatcher(copyDirs)}/**`]);
|
.concat([`!${formatDirsForMatcher(copyDirs)}/**`]);
|
||||||
|
|
||||||
var signatures;
|
var signatures;
|
||||||
|
@ -49,8 +50,12 @@ function getWatch() {
|
||||||
try {
|
try {
|
||||||
if (multimatch(path, jsFiles).length && !multimatch(path, ignoreMask).length) {
|
if (multimatch(path, jsFiles).length && !multimatch(path, ignoreMask).length) {
|
||||||
onSuccess(await getJS(path, { ignore: ignoreMask }, signatures));
|
onSuccess(await getJS(path, { ignore: ignoreMask }, signatures));
|
||||||
} else if (multimatch(path, 'scss/*.scss').length) {
|
} else if (multimatch(path, scssFiles).length) {
|
||||||
onSuccess(await getSass(path, {}, signatures));
|
if (multimatch(path, '**/_*.scss').length) {
|
||||||
|
onSuccess(await getSass(scssFiles, { ignore: ignoreMask }));
|
||||||
|
} else {
|
||||||
|
onSuccess(await getSass(path, {}, signatures));
|
||||||
|
}
|
||||||
} else if (multimatch(path, copyDirs.map(d => `${d}/**`)).length) {
|
} else if (multimatch(path, copyDirs.map(d => `${d}/**`)).length) {
|
||||||
onSuccess(await getCopy(path, {}, signatures));
|
onSuccess(await getCopy(path, {}, signatures));
|
||||||
} else if (multimatch(path, symlinks).length) {
|
} else if (multimatch(path, symlinks).length) {
|
||||||
|
|
25
scss/_zotero-react-client.scss
Normal file
25
scss/_zotero-react-client.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// Zotero React Client
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
// Abstracts
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@import "abstracts/variables";
|
||||||
|
@import "abstracts/functions";
|
||||||
|
@import "abstracts/mixins";
|
||||||
|
@import "abstracts/placeholders";
|
||||||
|
@import "abstracts/utilities";
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@import "themes/light";
|
||||||
|
|
||||||
|
// Components
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@import "components/tag-selector";
|
||||||
|
@import "components/button";
|
||||||
|
@import "components/icons";
|
4
scss/abstracts/_functions.scss
Normal file
4
scss/abstracts/_functions.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//
|
||||||
|
// Functions
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
4
scss/abstracts/_mixins.scss
Normal file
4
scss/abstracts/_mixins.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//
|
||||||
|
// Mixins
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
3
scss/abstracts/_placeholders.scss
Normal file
3
scss/abstracts/_placeholders.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
//
|
||||||
|
// Placeholders
|
||||||
|
// --------------------------------------------------
|
4
scss/abstracts/_utilities.scss
Normal file
4
scss/abstracts/_utilities.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//
|
||||||
|
// Utilities
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
80
scss/abstracts/_variables.scss
Normal file
80
scss/abstracts/_variables.scss
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$space-min: 4px;
|
||||||
|
$space-xs: 8px;
|
||||||
|
$space-sm: 12px;
|
||||||
|
$space-md: 16px;
|
||||||
|
$space-lg: 24px;
|
||||||
|
$space-xl: 32px;
|
||||||
|
$space-xxl: 48px;
|
||||||
|
$space-max: 64px;
|
||||||
|
|
||||||
|
$space-thumb: 42px;
|
||||||
|
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
$font-family-base: $font-family-sans-serif;
|
||||||
|
|
||||||
|
$font-size-base: 13px;
|
||||||
|
|
||||||
|
$font-size-h1: 20px;
|
||||||
|
$font-size-h2: 16px;
|
||||||
|
|
||||||
|
$line-height-base: 1.539;
|
||||||
|
$line-height-computed: ceil($font-size-base * $line-height-base);
|
||||||
|
|
||||||
|
// todo: line-height-small
|
||||||
|
$line-height-large: 26px;
|
||||||
|
$line-height-large-touch: $space-thumb;
|
||||||
|
|
||||||
|
$link-hover-decoration: underline;
|
||||||
|
|
||||||
|
$headings-font-family: inherit;
|
||||||
|
$headings-font-weight: 400;
|
||||||
|
$headings-line-height: 1.2;
|
||||||
|
|
||||||
|
|
||||||
|
// Components
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$padding-base-vertical: 2px;
|
||||||
|
|
||||||
|
$padding-base-horizontal: $space-xs;
|
||||||
|
$padding-large-horizontal: $space-sm;
|
||||||
|
|
||||||
|
$border-radius-small: 3px;
|
||||||
|
$border-radius-base: 4px;
|
||||||
|
$border-radius-large: 6px;
|
||||||
|
|
||||||
|
$border-width: 1px;
|
||||||
|
|
||||||
|
$separator-width: 1px;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$btn-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
// Z-index master list
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$z-index-mobile-nav: 0;
|
||||||
|
$z-index-navbar-bg: 10;
|
||||||
|
$z-index-main: 10;
|
||||||
|
$z-index-level: 10;
|
||||||
|
$z-index-level-active: 20;
|
||||||
|
$z-index-navbar: 20;
|
||||||
|
$z-index-menu: 30;
|
||||||
|
$z-index-modal: 40;
|
||||||
|
$z-index-drag-layer: 50;
|
||||||
|
$z-index-loading-cover: 60;
|
||||||
|
|
40
scss/components/_button.scss
Normal file
40
scss/components/_button.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// Button
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font: {
|
||||||
|
family: inherit;
|
||||||
|
size: inherit;
|
||||||
|
}
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
-moz-appearance: toolbarbutton;
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
opacity: $btn-disabled-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
.icon {
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
margin-right: -5px;
|
||||||
|
}
|
||||||
|
svg, img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span.menu-marker {
|
||||||
|
-moz-appearance: toolbarbutton-dropdown;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
7
scss/components/_icons.scss
Normal file
7
scss/components/_icons.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.icon > svg, .icon > img {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.icon-downchevron > img {
|
||||||
|
width: 7px;
|
||||||
|
}
|
92
scss/components/_tag-selector.scss
Normal file
92
scss/components/_tag-selector.scss
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
//
|
||||||
|
// Tag selector
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.tag-selector {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: auto;
|
||||||
|
height: 100px;
|
||||||
|
background-color: $tag-selector-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-filter-container {
|
||||||
|
height: auto;
|
||||||
|
flex: 0 0 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0.125em 0 0.125em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-list {
|
||||||
|
list-style: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-filter {
|
||||||
|
flex: 1 0;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-actions {
|
||||||
|
flex: 0 1;
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-item {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin: .15em .05em .15em .3em;
|
||||||
|
padding: 0 .25em 0 .25em;
|
||||||
|
max-width: 250px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.colored {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: .6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragged-over {
|
||||||
|
color: $shade-0;
|
||||||
|
background: $shade-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#zotero-tag-selector-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zotero-tag-selector {
|
||||||
|
min-height: 100px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zotero-tag-selector[collapsed=true] {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
11
scss/mac/_button.scss
Normal file
11
scss/mac/_button.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
//
|
||||||
|
// Button
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
-moz-appearance: none;
|
||||||
|
|
||||||
|
span.menu-marker {
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
}
|
20
scss/mac/_tag-selector.scss
Normal file
20
scss/mac/_tag-selector.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// Tag selector
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.tag-selector-filter-container {
|
||||||
|
padding: 0.25em 0 0.25em 0.5em;
|
||||||
|
border-top: 1px solid $shade-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-filter {
|
||||||
|
-moz-appearance: searchfield;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-actions {
|
||||||
|
flex: none;
|
||||||
|
border: 0;
|
||||||
|
margin-right: 3px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
176
scss/themes/_light.scss
Normal file
176
scss/themes/_light.scss
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
//
|
||||||
|
// Light theme
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$red: #cc2936;
|
||||||
|
$blue: rgb(89, 139, 236);
|
||||||
|
$asphalt: #7a8799;
|
||||||
|
$asphalt-light: #dadee3;
|
||||||
|
|
||||||
|
$primary: $red;
|
||||||
|
$secondary: $blue;
|
||||||
|
|
||||||
|
$blue-dark: #333c66;
|
||||||
|
$blue-darkest: #3d4466;
|
||||||
|
|
||||||
|
$transparent: transparent;
|
||||||
|
|
||||||
|
|
||||||
|
// Grays
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$shade-0: #fff;
|
||||||
|
$shade-1: #f6f6f6;
|
||||||
|
$shade-2: #e6e6e6;
|
||||||
|
$shade-3: #ddd;
|
||||||
|
$shade-4: #ccc;
|
||||||
|
$shade-5: #bbb;
|
||||||
|
$shade-6: #777;
|
||||||
|
$shade-7: #555;
|
||||||
|
$shade-8: #444;
|
||||||
|
$shade-9: #222;
|
||||||
|
$shade-10: #000;
|
||||||
|
|
||||||
|
|
||||||
|
// Scaffolding
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$body-bg: $shade-0;
|
||||||
|
$text-color: $shade-9;
|
||||||
|
|
||||||
|
$headings-color: $shade-8;
|
||||||
|
|
||||||
|
$link-color: $blue;
|
||||||
|
$link-hover-color: $blue;
|
||||||
|
|
||||||
|
$focus-color: $secondary;
|
||||||
|
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
$text-selection-color: inherit;
|
||||||
|
$text-selection-bg: rgba($secondary, 0.2);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
$separator-color: $shade-3;
|
||||||
|
|
||||||
|
|
||||||
|
// Partials
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Main
|
||||||
|
$main-bg: $shade-0;
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
$sidebar-bg: $shade-1;
|
||||||
|
|
||||||
|
// Clicky
|
||||||
|
$clicky-hover-bg-color: rgb(187, 206, 241);
|
||||||
|
$clicky-active-bg-color: $secondary;
|
||||||
|
$clicky-border-radius: 6px;
|
||||||
|
$clicky-border-color: rgb(109, 149, 224);
|
||||||
|
$clicky-active-color: $shade-0;
|
||||||
|
|
||||||
|
|
||||||
|
// Components
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
$icon-color: $secondary;
|
||||||
|
$icon-active-color: $shade-9;
|
||||||
|
|
||||||
|
// Collection tree
|
||||||
|
$collection-tree-headings-color: $shade-6;
|
||||||
|
$collection-tree-link-color: $shade-8;
|
||||||
|
$collection-tree-active-link-color: $shade-9;
|
||||||
|
$collection-tree-active-icon-color: $shade-7;
|
||||||
|
$collection-tree-active-link-bg: $shade-2;
|
||||||
|
$collection-tree-focus-color: $asphalt;
|
||||||
|
$collection-tree-icon-color: $shade-6;
|
||||||
|
$touch-collection-tree-active-link-color: $shade-0;
|
||||||
|
$touch-collection-tree-active-link-bg: $blue;
|
||||||
|
$touch-collection-tree-border: $shade-2;
|
||||||
|
$touch-collection-tree-separator: $body-bg;
|
||||||
|
$collection-tree-dnd-target-link-color: $shade-0;
|
||||||
|
$collection-tree-dnd-target-bg-color: $blue;
|
||||||
|
$collection-tree-dnd-target-icon-color: $shade-0;
|
||||||
|
|
||||||
|
// Items
|
||||||
|
$items-bg: $shade-0;
|
||||||
|
$items-odd-bg: $shade-1;
|
||||||
|
$item-list-head-color: $shade-8;
|
||||||
|
$item-list-head-border: $shade-2;
|
||||||
|
|
||||||
|
// Item list
|
||||||
|
$item-odd-bg: $shade-1;
|
||||||
|
$item-active-color: $shade-0;
|
||||||
|
$item-active-bg: $secondary;
|
||||||
|
|
||||||
|
// Item details
|
||||||
|
$item-details-bg: $shade-1;
|
||||||
|
$info-view-color: $shade-5;
|
||||||
|
|
||||||
|
// Panel
|
||||||
|
$panel-bg: $shade-1;
|
||||||
|
$panel-header-color: $shade-8;
|
||||||
|
$panel-header-shadow: rgba($shade-10, 0.08);
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
$tabs-border-color: $shade-3;
|
||||||
|
$tab-active-color: $secondary;
|
||||||
|
$tab-inactive-color: $headings-color;
|
||||||
|
|
||||||
|
// Metadata list
|
||||||
|
$key-color: $shade-6;
|
||||||
|
$editable-hover-bg: rgba($shade-10, 0.04);
|
||||||
|
$handle-color-touch: $shade-4;
|
||||||
|
$handle-color-mouse: $shade-6;
|
||||||
|
$metadata-heading-color: $shade-5;
|
||||||
|
$metadata-separator-color: $shade-3;
|
||||||
|
|
||||||
|
// Button
|
||||||
|
$btn-primary-color: $shade-0;
|
||||||
|
$btn-primary-bg: $asphalt;
|
||||||
|
$btn-default-color: $text-color;
|
||||||
|
$btn-border: $shade-3;
|
||||||
|
$btn-default-bg: $shade-0;
|
||||||
|
$btn-default-active-color: rgba($btn-default-color, 0.5);
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
$input-color: $text-color;
|
||||||
|
$input-bg: $body-bg;
|
||||||
|
$input-border-color: $shade-3;
|
||||||
|
$input-focus-color: $secondary;
|
||||||
|
$placeholder-color: $shade-5;
|
||||||
|
|
||||||
|
// Editable
|
||||||
|
$editable-color: $text-color;
|
||||||
|
$editable-bg: $shade-0;
|
||||||
|
$editable-border-color: $secondary;
|
||||||
|
|
||||||
|
// Menu (select, dropdown, …)
|
||||||
|
$menu-bg: $body-bg;
|
||||||
|
$menu-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 3px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
$menu-item-hover-bg: $shade-1;
|
||||||
|
$menu-item-selected-bg: $shade-2;
|
||||||
|
$menu-devider: $shade-2;
|
||||||
|
$menu-no-results-color: $shade-5;
|
||||||
|
|
||||||
|
// Drag layer
|
||||||
|
$creator-drag-preview-shadow: rgba($shade-10, 0.2);
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
$modal-backdrop-bg: rgba($shade-10, 0.4);
|
||||||
|
$modal-content-bg: $shade-1;
|
||||||
|
$modal-header-bg: $shade-0;
|
||||||
|
$modal-footer-border-width: 1px;
|
||||||
|
$modal-footer-border-color: $shade-2;
|
||||||
|
$modal-icon-spin-color: $shade-0;
|
||||||
|
|
||||||
|
// Tag selector
|
||||||
|
$tag-selector-bg: $shade-0;
|
8
scss/zotero-react-client-mac.scss
Normal file
8
scss/zotero-react-client-mac.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@import "zotero-react-client";
|
||||||
|
|
||||||
|
//
|
||||||
|
// MacOS specific
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@import "mac/button";
|
||||||
|
@import "mac/tag-selector";
|
6
scss/zotero-react-client-unix.scss
Normal file
6
scss/zotero-react-client-unix.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@import "zotero-react-client";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Linux specific
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
6
scss/zotero-react-client-win.scss
Normal file
6
scss/zotero-react-client-win.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@import "zotero-react-client";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Windows specific
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
|
@ -213,12 +213,15 @@ var waitForTagSelector = function (win) {
|
||||||
var zp = win.ZoteroPane;
|
var zp = win.ZoteroPane;
|
||||||
var deferred = Zotero.Promise.defer();
|
var deferred = Zotero.Promise.defer();
|
||||||
if (zp.tagSelectorShown()) {
|
if (zp.tagSelectorShown()) {
|
||||||
var tagSelector = win.document.getElementById('zotero-tag-selector');
|
let tagSelector = zp.tagSelector;
|
||||||
var onRefresh = () => {
|
let componentDidUpdate = tagSelector.componentDidUpdate;
|
||||||
tagSelector.removeEventListener('refresh', onRefresh);
|
tagSelector.componentDidUpdate = function() {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
};
|
tagSelector.componentDidUpdate = componentDidUpdate;
|
||||||
tagSelector.addEventListener('refresh', onRefresh);
|
if (typeof componentDidUpdate == 'function') {
|
||||||
|
componentDidUpdate.call(this, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
describe("Tag Selector", function () {
|
describe("Tag Selector", function () {
|
||||||
var win, doc, collectionsView, tagSelector;
|
var win, doc, collectionsView, tagSelectorElem, tagSelector;
|
||||||
|
|
||||||
var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) {
|
var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) {
|
||||||
var tagColors = Zotero.Tags.getColors(libraryID);
|
var tagColors = Zotero.Tags.getColors(libraryID);
|
||||||
|
@ -11,27 +11,13 @@ describe("Tag Selector", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
function getColoredTags() {
|
function getColoredTags() {
|
||||||
var tagsBox = tagSelector.id('tags-box');
|
var elems = Array.from(tagSelectorElem.querySelectorAll('.tag-selector-item.colored'));
|
||||||
var elems = tagsBox.getElementsByTagName('button');
|
return elems.map(elem => elem.textContent);
|
||||||
var names = [];
|
|
||||||
for (let i = 0; i < elems.length; i++) {
|
|
||||||
if (elems[i].style.order < 0) {
|
|
||||||
names.push(elems[i].textContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegularTags() {
|
function getRegularTags() {
|
||||||
var tagsBox = tagSelector.id('tags-box');
|
var elems = Array.from(tagSelectorElem.querySelectorAll('.tag-selector-item:not(.colored)'));
|
||||||
var elems = tagsBox.getElementsByTagName('button');
|
return elems.map(elem => elem.textContent);
|
||||||
var names = [];
|
|
||||||
for (let i = 0; i < elems.length; i++) {
|
|
||||||
if (elems[i].style.order >= 0 && elems[i].style.display != 'none') {
|
|
||||||
names.push(elems[i].textContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,7 +25,8 @@ describe("Tag Selector", function () {
|
||||||
win = yield loadZoteroPane();
|
win = yield loadZoteroPane();
|
||||||
doc = win.document;
|
doc = win.document;
|
||||||
collectionsView = win.ZoteroPane.collectionsView;
|
collectionsView = win.ZoteroPane.collectionsView;
|
||||||
tagSelector = doc.getElementById('zotero-tag-selector');
|
tagSelectorElem = doc.getElementById('zotero-tag-selector');
|
||||||
|
tagSelector = win.ZoteroPane.tagSelector;
|
||||||
|
|
||||||
// Wait for things to settle
|
// Wait for things to settle
|
||||||
yield Zotero.Promise.delay(100);
|
yield Zotero.Promise.delay(100);
|
||||||
|
@ -49,16 +36,37 @@ describe("Tag Selector", function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
yield clearTagColors(libraryID);
|
yield clearTagColors(libraryID);
|
||||||
// Default "Display All Tags in This Library" off
|
// Default "Display All Tags in This Library" off
|
||||||
tagSelector.filterToScope = true;
|
tagSelector.displayAllTags = false;
|
||||||
tagSelector.setSearch('');
|
tagSelector.selectedTags = new Set();
|
||||||
yield tagSelector.refresh(true);
|
tagSelector.handleSearch('');
|
||||||
})
|
tagSelector.onItemViewChanged({libraryID});
|
||||||
|
});
|
||||||
|
|
||||||
after(function () {
|
after(function () {
|
||||||
win.close();
|
win.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#setSearch()", function () {
|
it('should not display duplicate tags when automatic and manual tag with same name exists', async function () {
|
||||||
|
var collection = await createDataObject('collection');
|
||||||
|
var item1 = createUnsavedDataObject('item', { collections: [collection.id] });
|
||||||
|
item1.setTags([{
|
||||||
|
tag: "A",
|
||||||
|
type: 1
|
||||||
|
}]);
|
||||||
|
var item2 = createUnsavedDataObject('item', { collections: [collection.id] });
|
||||||
|
item2.setTags(["A", "B"]);
|
||||||
|
var promise = waitForTagSelector(win);
|
||||||
|
await Zotero.DB.executeTransaction(async function () {
|
||||||
|
await item1.save();
|
||||||
|
await item2.save();
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
var tags = getRegularTags();
|
||||||
|
assert.sameMembers(tags, ['A', 'B']);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#handleSearch()", function () {
|
||||||
it("should filter to tags matching the search", function* () {
|
it("should filter to tags matching the search", function* () {
|
||||||
var collection = yield createDataObject('collection');
|
var collection = yield createDataObject('collection');
|
||||||
var item = createUnsavedDataObject('item', { collections: [collection.id] });
|
var item = createUnsavedDataObject('item', { collections: [collection.id] });
|
||||||
|
@ -67,20 +75,23 @@ describe("Tag Selector", function () {
|
||||||
yield item.saveTx();
|
yield item.saveTx();
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
var tagsSearch = doc.getElementById('tags-search');
|
promise = waitForTagSelector(win);
|
||||||
tagsSearch.value = 'a';
|
tagSelector.handleSearch('a');
|
||||||
tagsSearch.doCommand();
|
yield Zotero.Promise.delay(500);
|
||||||
|
|
||||||
|
yield promise;
|
||||||
|
|
||||||
var tags = getRegularTags();
|
var tags = getRegularTags();
|
||||||
assert.sameMembers(tags, ['a']);
|
assert.sameMembers(tags, ['a']);
|
||||||
|
|
||||||
|
tagSelector.handleSearch('');
|
||||||
|
yield Zotero.Promise.delay(500);
|
||||||
|
|
||||||
tagsSearch.value = '';
|
|
||||||
tagsSearch.doCommand();
|
|
||||||
yield item.eraseTx();
|
yield item.eraseTx();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#refresh()", function () {
|
describe("#handleTagSelected()", function () {
|
||||||
it("should remove tags not on matching items on tag click", function* () {
|
it("should remove tags not on matching items on tag click", function* () {
|
||||||
var collection = yield createDataObject('collection');
|
var collection = yield createDataObject('collection');
|
||||||
var item1 = createUnsavedDataObject('item', { collections: [collection.id] });
|
var item1 = createUnsavedDataObject('item', { collections: [collection.id] });
|
||||||
|
@ -112,13 +123,8 @@ describe("Tag Selector", function () {
|
||||||
});
|
});
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
var buttons = tagSelector.id('tags-box').getElementsByTagName('button');
|
tagSelector.handleTagSelected('A');
|
||||||
var spy = sinon.spy(win.ZoteroPane, "updateTagFilter");
|
yield waitForTagSelector(win);
|
||||||
buttons[0].click();
|
|
||||||
|
|
||||||
yield spy.returnValues[0];
|
|
||||||
|
|
||||||
spy.restore();
|
|
||||||
|
|
||||||
var tags = getRegularTags();
|
var tags = getRegularTags();
|
||||||
assert.sameMembers(tags, ['A', 'B']);
|
assert.sameMembers(tags, ['A', 'B']);
|
||||||
|
@ -126,9 +132,9 @@ describe("Tag Selector", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("#filterToScope", function () {
|
describe("#displayAllTags", function () {
|
||||||
it("should show all tags in library when false", function* () {
|
it("should show all tags in library when true", function* () {
|
||||||
tagSelector.filterToScope = false;
|
tagSelector.displayAllTags = true;
|
||||||
|
|
||||||
var collection = yield createDataObject('collection');
|
var collection = yield createDataObject('collection');
|
||||||
var item1 = createUnsavedDataObject('item');
|
var item1 = createUnsavedDataObject('item');
|
||||||
|
@ -165,7 +171,7 @@ describe("Tag Selector", function () {
|
||||||
|
|
||||||
describe("#notify()", function () {
|
describe("#notify()", function () {
|
||||||
it("should add a tag when added to an item in the library root", function* () {
|
it("should add a tag when added to an item in the library root", function* () {
|
||||||
var promise, tagSelector;
|
var promise;
|
||||||
|
|
||||||
if (collectionsView.selection.currentIndex != 0) {
|
if (collectionsView.selection.currentIndex != 0) {
|
||||||
promise = waitForTagSelector(win);
|
promise = waitForTagSelector(win);
|
||||||
|
@ -256,11 +262,14 @@ describe("Tag Selector", function () {
|
||||||
it("should show a colored tag at the top of the list even when linked to no items", function* () {
|
it("should show a colored tag at the top of the list even when linked to no items", function* () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
|
||||||
var tagElems = tagSelector.id('tags-box').getElementsByTagName('button');
|
var tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item');
|
||||||
var count = tagElems.length;
|
var count = tagElems.length;
|
||||||
|
|
||||||
|
var promise = waitForTagSelector(win);
|
||||||
yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA');
|
yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA');
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item');
|
||||||
assert.equal(tagElems.length, count + 1);
|
assert.equal(tagElems.length, count + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -288,16 +297,16 @@ describe("Tag Selector", function () {
|
||||||
var promise = waitForTagSelector(win);
|
var promise = waitForTagSelector(win);
|
||||||
yield item.saveTx();
|
yield item.saveTx();
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
var tagElems = tagSelector.id('tags-box').getElementsByTagName('button');
|
var tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item');
|
||||||
|
|
||||||
// Make sure the colored tags are still in the right position
|
// Make sure the colored tags are still in the right position
|
||||||
var tags = new Map();
|
var tags = new Map();
|
||||||
for (let i = 0; i < tagElems.length; i++) {
|
for (let i = 0; i < tagElems.length; i++) {
|
||||||
tags.set(tagElems[i].textContent, tagElems[i].style.order);
|
tags.set(tagElems[i].textContent, i);
|
||||||
}
|
}
|
||||||
assert.isBelow(parseInt(tags.get("B")), 0);
|
assert.isAbove(tags.get("B"), 0);
|
||||||
assert.isBelow(parseInt(tags.get("B")), parseInt(tags.get("A")));
|
assert.isAbove(tags.get("B"), tags.get("A"));
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should remove a tag when an item is removed from a collection", function* () {
|
it("should remove a tag when an item is removed from a collection", function* () {
|
||||||
|
@ -325,7 +334,7 @@ describe("Tag Selector", function () {
|
||||||
promise = waitForTagSelector(win);
|
promise = waitForTagSelector(win);
|
||||||
yield item.saveTx();
|
yield item.saveTx();
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
// Tag selector shouldn't show the removed item's tag
|
// Tag selector shouldn't show the removed item's tag
|
||||||
assert.equal(getRegularTags().length, 0);
|
assert.equal(getRegularTags().length, 0);
|
||||||
})
|
})
|
||||||
|
@ -379,8 +388,9 @@ describe("Tag Selector", function () {
|
||||||
|
|
||||||
// Remove tag from library
|
// Remove tag from library
|
||||||
promise = waitForTagSelector(win);
|
promise = waitForTagSelector(win);
|
||||||
var dialogPromise = waitForDialog();
|
waitForDialog();
|
||||||
yield tagSelector.deleteTag("A");
|
tagSelector.contextTag = {name: "A"};
|
||||||
|
yield tagSelector.openDeletePrompt();
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
// Tag selector shouldn't show the deleted item's tag
|
// Tag selector shouldn't show the deleted item's tag
|
||||||
|
@ -388,7 +398,7 @@ describe("Tag Selector", function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#rename()", function () {
|
describe("#openRenamePrompt", function () {
|
||||||
it("should rename a tag and update the tag selector", function* () {
|
it("should rename a tag and update the tag selector", function* () {
|
||||||
yield selectLibrary(win);
|
yield selectLibrary(win);
|
||||||
|
|
||||||
|
@ -409,7 +419,8 @@ describe("Tag Selector", function () {
|
||||||
dialog.document.getElementById('loginTextbox').value = newTag;
|
dialog.document.getElementById('loginTextbox').value = newTag;
|
||||||
dialog.document.documentElement.acceptDialog();
|
dialog.document.documentElement.acceptDialog();
|
||||||
})
|
})
|
||||||
yield tagSelector.rename(tag);
|
tagSelector.contextTag = {name: tag};
|
||||||
|
yield tagSelector.openRenamePrompt();
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
var tags = getRegularTags();
|
var tags = getRegularTags();
|
||||||
|
@ -428,11 +439,12 @@ describe("Tag Selector", function () {
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
promise = waitForTagSelector(win);
|
promise = waitForTagSelector(win);
|
||||||
var promptPromise = waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) {
|
waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) {
|
||||||
dialog.document.getElementById('loginTextbox').value = newTag;
|
dialog.document.getElementById('loginTextbox').value = newTag;
|
||||||
dialog.document.documentElement.acceptDialog();
|
dialog.document.documentElement.acceptDialog();
|
||||||
})
|
});
|
||||||
yield tagSelector.rename(oldTag);
|
tagSelector.contextTag = {name: oldTag};
|
||||||
|
yield tagSelector.openRenamePrompt();
|
||||||
yield promise;
|
yield promise;
|
||||||
|
|
||||||
var tags = getColoredTags();
|
var tags = getColoredTags();
|
||||||
|
@ -441,7 +453,7 @@ describe("Tag Selector", function () {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#_openColorPickerWindow()", function () {
|
describe("#openColorPickerWindow()", function () {
|
||||||
it("should assign a color to a tag", function* () {
|
it("should assign a color to a tag", function* () {
|
||||||
yield selectLibrary(win);
|
yield selectLibrary(win);
|
||||||
var tag = "b " + Zotero.Utilities.randomString();
|
var tag = "b " + Zotero.Utilities.randomString();
|
||||||
|
@ -463,7 +475,8 @@ describe("Tag Selector", function () {
|
||||||
|
|
||||||
var dialogPromise = waitForDialog(false, undefined, 'chrome://zotero/content/tagColorChooser.xul');
|
var dialogPromise = waitForDialog(false, undefined, 'chrome://zotero/content/tagColorChooser.xul');
|
||||||
var tagSelectorPromise = waitForTagSelector(win);
|
var tagSelectorPromise = waitForTagSelector(win);
|
||||||
yield tagSelector._openColorPickerWindow(tag);
|
tagSelector.contextTag = {name: tag};
|
||||||
|
yield tagSelector.openColorPickerWindow();
|
||||||
yield dialogPromise;
|
yield dialogPromise;
|
||||||
yield tagSelectorPromise;
|
yield tagSelectorPromise;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue