Restyle Tag Selector
* Render a colored dot for colored tags * Improve spacing and positioning, especially on 2x screens * Add scss map for tag colors in light/dark scheme * Add support for compact/comfortable in tag selector * Restyle filter box, add a placeholder and a new icon
This commit is contained in:
parent
ff115b0873
commit
9ff76d2dd9
21 changed files with 334 additions and 131 deletions
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const { PureComponent, createElement: create } = React
|
const { PureComponent, createElement: create } = React
|
||||||
const { IconDownChevron } = require('./icons')
|
const { CSSIcon } = require('./icons')
|
||||||
const cx = require('classnames')
|
const cx = require('classnames')
|
||||||
const {
|
const {
|
||||||
bool, element, func, node, number, oneOf, string
|
bool, element, func, node, number, oneOf, string
|
||||||
|
@ -77,7 +77,7 @@ class Button extends PureComponent {
|
||||||
if (!Zotero.isNode && Zotero.isLinux) {
|
if (!Zotero.isNode && Zotero.isLinux) {
|
||||||
return this.props.isMenu && <span className="menu-marker"/>
|
return this.props.isMenu && <span className="menu-marker"/>
|
||||||
}
|
}
|
||||||
return this.props.isMenu && <IconDownChevron className="menu-marker"/>
|
return this.props.isMenu && <CSSIcon name="chevron-6" className="menu-marker icon-8"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
get attributes() {
|
get attributes() {
|
||||||
|
|
|
@ -29,7 +29,7 @@ const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const TagList = require('./tagSelector/tagSelectorList');
|
const TagList = require('./tagSelector/tagSelectorList');
|
||||||
const { Button } = require('./button');
|
const { Button } = require('./button');
|
||||||
const { IconTagSelectorMenu } = require('./icons');
|
const { CSSIcon } = require('./icons');
|
||||||
const Search = require('./search');
|
const Search = require('./search');
|
||||||
|
|
||||||
class TagSelector extends React.PureComponent {
|
class TagSelector extends React.PureComponent {
|
||||||
|
@ -46,21 +46,26 @@ class TagSelector extends React.PureComponent {
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
height={this.props.height}
|
height={this.props.height}
|
||||||
fontSize={this.props.fontSize}
|
fontSize={this.props.fontSize}
|
||||||
|
lineHeight={this.props.lineHeight}
|
||||||
|
uiDensity={this.props.uiDensity}
|
||||||
/>
|
/>
|
||||||
<div className="tag-selector-filter-container">
|
<div className="tag-selector-filter-pane">
|
||||||
<Search
|
<div className="tag-selector-filter-container">
|
||||||
ref={this.props.searchBoxRef}
|
<Search
|
||||||
value={this.props.searchString}
|
ref={this.props.searchBoxRef}
|
||||||
onSearch={this.props.onSearch}
|
value={this.props.searchString}
|
||||||
className="tag-selector-filter"
|
onSearch={this.props.onSearch}
|
||||||
/>
|
className="tag-selector-filter"
|
||||||
<Button
|
data-l10n-id="tagselector-search"
|
||||||
icon={<IconTagSelectorMenu />}
|
/>
|
||||||
title="zotero.toolbar.actions.label"
|
<Button
|
||||||
className="tag-selector-actions"
|
icon={<CSSIcon name="filter" className="icon-16" />}
|
||||||
isMenu
|
title="zotero.toolbar.actions.label"
|
||||||
onMouseDown={ev => this.props.onSettings(ev)}
|
className="tag-selector-actions"
|
||||||
/>
|
isMenu
|
||||||
|
onMouseDown={ev => this.props.onSettings(ev)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -88,6 +93,8 @@ TagSelector.propTypes = {
|
||||||
width: PropTypes.number.isRequired,
|
width: PropTypes.number.isRequired,
|
||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number.isRequired,
|
||||||
fontSize: PropTypes.number.isRequired,
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
lineHeight: PropTypes.number.isRequired,
|
||||||
|
uiDensity: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
searchBoxRef: PropTypes.object,
|
searchBoxRef: PropTypes.object,
|
||||||
|
|
|
@ -26,21 +26,21 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
var { Collection } = require('react-virtualized');
|
var { Collection } = require('react-virtualized');
|
||||||
|
const { props } = require("bluebird");
|
||||||
|
|
||||||
// See also .tag-selector-item in _tag-selector.scss
|
// See also .tag-selector-item in _tag-selector.scss
|
||||||
var filterBarHeight = 32;
|
var filterBarHeight = 36;
|
||||||
var tagPaddingTop = 4;
|
var tagPaddingLeft = 4;
|
||||||
var tagPaddingLeft = 2;
|
var tagPaddingRight = 4;
|
||||||
var tagPaddingRight = 2;
|
var tagSpaceBetweenX = 2;
|
||||||
var tagPaddingBottom = 4;
|
|
||||||
var tagSpaceBetweenX = 7;
|
|
||||||
var tagSpaceBetweenY = 4;
|
var tagSpaceBetweenY = 4;
|
||||||
var panePaddingTop = 2;
|
var panePaddingTop = 8;
|
||||||
var panePaddingLeft = 2;
|
var panePaddingLeft = 8;
|
||||||
var panePaddingRight = 25;
|
var panePaddingRight = 2; // + scrollbar width
|
||||||
//var panePaddingBottom = 2;
|
//var panePaddingBottom = 2;
|
||||||
var minHorizontalPadding = panePaddingLeft + tagPaddingLeft + tagPaddingRight + panePaddingRight;
|
var minHorizontalPadding = panePaddingLeft + tagPaddingLeft + tagPaddingRight + panePaddingRight;
|
||||||
|
|
||||||
|
|
||||||
class TagList extends React.PureComponent {
|
class TagList extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -53,11 +53,13 @@ class TagList extends React.PureComponent {
|
||||||
// Redraw all tags on every refresh
|
// Redraw all tags on every refresh
|
||||||
if (this.collectionRef && this.collectionRef.current) {
|
if (this.collectionRef && this.collectionRef.current) {
|
||||||
// If width or height changed, recompute positions. It seems like this should happen
|
// If width or height changed, recompute positions. It seems like this should happen
|
||||||
// automatically, but it doesn't as of 9.21.0.
|
// automatically, but it doesn't as of 9.21.0. Also check for density change.
|
||||||
|
|
||||||
if (prevProps.height != this.props.height
|
if (prevProps.height != this.props.height
|
||||||
|| prevProps.width != this.props.width
|
|| prevProps.width != this.props.width
|
||||||
|| prevProps.fontSize != this.props.fontSize
|
|| prevProps.lineHeight != this.props.lineHeight
|
||||||
|| prevProps.tags != this.props.tags) {
|
|| prevProps.tags != this.props.tags
|
||||||
|
|| prevProps.uiDensity !== this.props.uiDensity) {
|
||||||
this.collectionRef.current.recomputeCellSizesAndPositions();
|
this.collectionRef.current.recomputeCellSizesAndPositions();
|
||||||
}
|
}
|
||||||
// If dimensions didn't change, just redraw at current positions. Without this, clicking
|
// If dimensions didn't change, just redraw at current positions. Without this, clicking
|
||||||
|
@ -94,8 +96,12 @@ class TagList extends React.PureComponent {
|
||||||
* Calculate the x,y coordinates of all tags
|
* Calculate the x,y coordinates of all tags
|
||||||
*/
|
*/
|
||||||
updatePositions() {
|
updatePositions() {
|
||||||
var tagMaxWidth = this.props.width - minHorizontalPadding;
|
const tagPaddingTop = this.props.uiDensity === 'comfortable' ? 2 : 1;
|
||||||
var rowHeight = tagPaddingTop + this.props.fontSize + tagPaddingBottom + tagSpaceBetweenY;
|
const tagPaddingBottom = tagPaddingTop;
|
||||||
|
this.scrollbarWidth = Zotero.Utilities.Internal.getScrollbarWidth();
|
||||||
|
|
||||||
|
var tagMaxWidth = this.props.width - minHorizontalPadding - this.scrollbarWidth;
|
||||||
|
var rowHeight = tagPaddingTop + this.props.lineHeight + tagPaddingBottom + tagSpaceBetweenY;
|
||||||
var positions = [];
|
var positions = [];
|
||||||
var row = 0;
|
var row = 0;
|
||||||
let rowX = panePaddingLeft;
|
let rowX = panePaddingLeft;
|
||||||
|
@ -113,9 +119,11 @@ class TagList extends React.PureComponent {
|
||||||
shouldAddSeparator = true;
|
shouldAddSeparator = true;
|
||||||
forceNewLine = true;
|
forceNewLine = true;
|
||||||
}
|
}
|
||||||
let tagWidth = tagPaddingLeft + Math.min(tag.width, tagMaxWidth) + tagPaddingRight;
|
// size of the colored dot + space between the dot and the tag name always sums up to fontSize (e.g., 8px + 3px at 11px fontSize)
|
||||||
|
const tagColorWidth = (tag.color && !Zotero.Utilities.Internal.isOnlyEmoji(tag.name)) ? this.props.fontSize : 0;
|
||||||
|
let tagWidth = tagPaddingLeft + Math.min(tag.width, tagMaxWidth) + tagPaddingRight + tagColorWidth;
|
||||||
// If first row or cell fits, add to current row
|
// If first row or cell fits, add to current row
|
||||||
if (!forceNewLine && (i == 0 || ((rowX + tagWidth) < (this.props.width - panePaddingLeft - panePaddingRight)))) {
|
if (!forceNewLine && (i == 0 || ((rowX + tagWidth) < (this.props.width - panePaddingRight - this.scrollbarWidth)))) {
|
||||||
positions[i] = [rowX, panePaddingTop + (row * rowHeight)];
|
positions[i] = [rowX, panePaddingTop + (row * rowHeight)];
|
||||||
}
|
}
|
||||||
// Otherwise, start new row
|
// Otherwise, start new row
|
||||||
|
@ -135,10 +143,10 @@ class TagList extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
cellSizeAndPositionGetter = ({ index }) => {
|
cellSizeAndPositionGetter = ({ index }) => {
|
||||||
var tagMaxWidth = this.props.width - minHorizontalPadding;
|
var tagMaxWidth = this.props.width - minHorizontalPadding - this.scrollbarWidth;
|
||||||
return {
|
return {
|
||||||
width: Math.min(this.props.tags[index].width, tagMaxWidth),
|
width: Math.min(this.props.tags[index].width, tagMaxWidth),
|
||||||
height: this.props.fontSize,
|
height: this.props.lineHeight,
|
||||||
x: this.positions[index][0],
|
x: this.positions[index][0],
|
||||||
y: this.positions[index][1]
|
y: this.positions[index][1]
|
||||||
};
|
};
|
||||||
|
@ -149,7 +157,7 @@ class TagList extends React.PureComponent {
|
||||||
|
|
||||||
const { onDragOver, onDragExit, onDrop } = this.props.dragObserver;
|
const { onDragOver, onDragExit, onDrop } = this.props.dragObserver;
|
||||||
|
|
||||||
var className = 'tag-selector-item zotero-clicky';
|
var className = 'tag-selector-item';
|
||||||
if (tag.selected) {
|
if (tag.selected) {
|
||||||
className += ' selected';
|
className += ' selected';
|
||||||
}
|
}
|
||||||
|
@ -159,6 +167,9 @@ class TagList extends React.PureComponent {
|
||||||
if (tag.disabled) {
|
if (tag.disabled) {
|
||||||
className += ' disabled';
|
className += ' disabled';
|
||||||
}
|
}
|
||||||
|
if (Zotero.Utilities.Internal.isOnlyEmoji(tag.name)) {
|
||||||
|
className += ' emoji';
|
||||||
|
}
|
||||||
|
|
||||||
let props = {
|
let props = {
|
||||||
className,
|
className,
|
||||||
|
@ -176,7 +187,7 @@ class TagList extends React.PureComponent {
|
||||||
// Don't specify explicit width unless we're truncating, because for some reason the width
|
// Don't specify explicit width unless we're truncating, because for some reason the width
|
||||||
// from canvas can sometimes be slightly smaller than the actual width, resulting in an
|
// from canvas can sometimes be slightly smaller than the actual width, resulting in an
|
||||||
// unnecessary ellipsis.
|
// unnecessary ellipsis.
|
||||||
var tagMaxWidth = this.props.width - minHorizontalPadding;
|
var tagMaxWidth = this.props.width - minHorizontalPadding - this.scrollbarWidth;
|
||||||
if (props.style.width < tagMaxWidth) {
|
if (props.style.width < tagMaxWidth) {
|
||||||
delete props.style.width;
|
delete props.style.width;
|
||||||
}
|
}
|
||||||
|
@ -190,11 +201,12 @@ class TagList extends React.PureComponent {
|
||||||
|
|
||||||
if (tag.color) {
|
if (tag.color) {
|
||||||
props.style.color = tag.color;
|
props.style.color = tag.color;
|
||||||
|
props['data-color'] = tag.color.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={tag.name} {...props}>
|
<div key={tag.name} {...props}>
|
||||||
{tag.name}
|
<span>{tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -265,6 +277,8 @@ class TagList extends React.PureComponent {
|
||||||
width: PropTypes.number.isRequired,
|
width: PropTypes.number.isRequired,
|
||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number.isRequired,
|
||||||
fontSize: PropTypes.number.isRequired,
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
lineHeight: PropTypes.number.isRequired,
|
||||||
|
uiDensity: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,9 @@ const defaults = {
|
||||||
};
|
};
|
||||||
const { Cc, Ci } = require('chrome');
|
const { Cc, Ci } = require('chrome');
|
||||||
|
|
||||||
|
// first n tags will be measured using DOM method for more accurate measurment (at the cost of performance)
|
||||||
|
const FORCE_DOM_TAGS_FOR_COUNT = 200;
|
||||||
|
|
||||||
Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -50,7 +53,10 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
['collection-item', 'item', 'item-tag', 'tag', 'setting'],
|
['collection-item', 'item', 'item-tag', 'tag', 'setting'],
|
||||||
'tagSelector'
|
'tagSelector'
|
||||||
);
|
);
|
||||||
this._prefObserverID = Zotero.Prefs.registerObserver('fontSize', this.handleFontChange.bind(this));
|
this._prefObserverID = Zotero.Prefs.registerObserver('fontSize', this.handleUIPropertiesChange.bind(this));
|
||||||
|
this._prefObserverID = Zotero.Prefs.registerObserver('uiDensity', this.handleUIPropertiesChange.bind(this));
|
||||||
|
this._mediaQueryList = window.matchMedia("(min-resolution: 1.5dppx)");
|
||||||
|
this._mediaQueryList.addEventListener("change", this.handleUIPropertiesChange.bind(this));
|
||||||
|
|
||||||
this.tagListRef = React.createRef();
|
this.tagListRef = React.createRef();
|
||||||
this.searchBoxRef = React.createRef();
|
this.searchBoxRef = React.createRef();
|
||||||
|
@ -66,7 +72,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
...defaults,
|
...defaults,
|
||||||
...this.getContainerDimensions(),
|
...this.getContainerDimensions(),
|
||||||
...this.getFontInfo()
|
...this.getFontInfo(),
|
||||||
|
isHighDensity: this._mediaQueryList.matches
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,6 +364,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
container.appendChild(elem);
|
container.appendChild(elem);
|
||||||
var style = window.getComputedStyle(elem);
|
var style = window.getComputedStyle(elem);
|
||||||
var props = {
|
var props = {
|
||||||
|
lineHeight: style.getPropertyValue('line-height'),
|
||||||
fontSize: style.getPropertyValue('font-size'),
|
fontSize: style.getPropertyValue('font-size'),
|
||||||
fontFamily: style.getPropertyValue('font-family')
|
fontFamily: style.getPropertyValue('font-family')
|
||||||
};
|
};
|
||||||
|
@ -365,36 +373,61 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recompute tag widths based on the current font settings
|
* Recompute tag widths when either font, UI density or pixel density changes
|
||||||
*/
|
*/
|
||||||
handleFontChange() {
|
handleUIPropertiesChange(ev) {
|
||||||
this.widths.clear();
|
this.widths.clear();
|
||||||
this.widthsBold.clear();
|
this.widthsBold.clear();
|
||||||
|
const isHighDensity = ev.target instanceof MediaQueryList ? ev.matches : this.state.isHighDensity;
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.getFontInfo()
|
...this.getFontInfo(),
|
||||||
|
uiDensity: Zotero.Prefs.get('uiDensity'),
|
||||||
|
isHighDensity
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||||
|
* Except for emoji tags, where, on high-density screens, we use actual DOM element for more accurate
|
||||||
|
* measurement (which is 4-5x slower) because canvas method can be off by enough to cause visible artifacts.
|
||||||
|
* It's possible to force use of DOM method for other tags using forceUseDOM parameter.
|
||||||
*
|
*
|
||||||
* @param {String} text The text to be rendered.
|
* @param {String} text The text to be rendered.
|
||||||
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
|
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
|
||||||
|
* @param {String} forceUseDOM Force use of DOM method for measuring text width
|
||||||
*
|
*
|
||||||
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||||
*/
|
*/
|
||||||
getTextWidth(text, font) {
|
getTextWidth(text, font, forceUseDOM = false) {
|
||||||
// re-use canvas object for better performance
|
let width;
|
||||||
var canvas = this.canvas || (this.canvas = document.createElement("canvas"));
|
const useDOM = forceUseDOM || (this.state.isHighDensity && Zotero.Utilities.Internal.includesEmoji(text));
|
||||||
var context = canvas.getContext("2d");
|
if (useDOM) {
|
||||||
context.font = font;
|
if (!this.divMeasure) {
|
||||||
// Add a little more to make sure we don't crop
|
this.divMeasure = document.createElement('div');
|
||||||
var metrics = context.measureText(text);
|
this.divMeasure.style.position = 'absolute';
|
||||||
return Math.ceil(metrics.width);
|
this.divMeasure.style.top = '-9999px';
|
||||||
|
this.divMeasure.whiteSpace = 'nowrap';
|
||||||
|
document.querySelector('#zotero-tag-selector').appendChild(this.divMeasure);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.divMeasure.style.font = font;
|
||||||
|
this.divMeasure.textContent = text;
|
||||||
|
width = this.divMeasure.clientWidth;
|
||||||
|
this.divMeasure.textContent = '';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// re-use canvas object for better performance
|
||||||
|
var canvas = this.canvas || (this.canvas = document.createElement("canvas"));
|
||||||
|
var context = canvas.getContext("2d");
|
||||||
|
context.font = font;
|
||||||
|
var metrics = context.measureText(text);
|
||||||
|
width = metrics.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWidth(name) {
|
getWidth(name, forceUseDOM = false) {
|
||||||
var num = 0;
|
|
||||||
var font = this.state.fontSize + ' ' + this.state.fontFamily;
|
var font = this.state.fontSize + ' ' + this.state.fontFamily;
|
||||||
// Colored tags are shown in bold, which results in a different width
|
// Colored tags are shown in bold, which results in a different width
|
||||||
var fontBold = 'bold ' + font;
|
var fontBold = 'bold ' + font;
|
||||||
|
@ -402,8 +435,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
let widths = hasColor ? this.widthsBold : this.widths;
|
let widths = hasColor ? this.widthsBold : this.widths;
|
||||||
let width = widths.get(name);
|
let width = widths.get(name);
|
||||||
if (width === undefined) {
|
if (width === undefined) {
|
||||||
width = this.getTextWidth(name, hasColor ? fontBold : font);
|
width = this.getTextWidth(name, hasColor ? fontBold : font, forceUseDOM);
|
||||||
//Zotero.debug(`Calculated ${hasColor ? 'bold ' : ''}width of ${width} for tag '${name}'`);
|
// Zotero.debug(`Calculated ${hasColor ? 'bold ' : ''}width of ${width} for tag '${name}' using ${forceUseDOM ? 'DOM' : 'hybrid'} method`);
|
||||||
widths.set(name, width);
|
widths.set(name, width);
|
||||||
}
|
}
|
||||||
return width;
|
return width;
|
||||||
|
@ -460,7 +493,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
// Prepare tag objects for list component
|
// Prepare tag objects for list component
|
||||||
//var d = new Date();
|
//var d = new Date();
|
||||||
var inTagColors = true;
|
var inTagColors = true;
|
||||||
tags = tags.map((tag) => {
|
tags = tags.map((tag, i) => {
|
||||||
let name = tag.tag;
|
let name = tag.tag;
|
||||||
tag = {
|
tag = {
|
||||||
name,
|
name,
|
||||||
|
@ -480,10 +513,14 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
if ((this.displayAllTags || inTagColors) && !this.state.scope.has(name)) {
|
if ((this.displayAllTags || inTagColors) && !this.state.scope.has(name)) {
|
||||||
tag.disabled = true;
|
tag.disabled = true;
|
||||||
}
|
}
|
||||||
tag.width = this.getWidth(name);
|
const forceUseDOM = this.state.isHighDensity && i < FORCE_DOM_TAGS_FOR_COUNT;
|
||||||
|
tag.width = this.getWidth(name, forceUseDOM);
|
||||||
return tag;
|
return tag;
|
||||||
});
|
});
|
||||||
//Zotero.debug(`Prepared tags in ${new Date() - d} ms`);
|
// clean up divMeasure, which might have been used for measuring emoji tags
|
||||||
|
this.divMeasure?.parentNode?.removeChild?.(this.divMeasure);
|
||||||
|
this.divMeasure = null;
|
||||||
|
// Zotero.debug(`Prepared ${tags.length} tags in ${new Date() - d} ms`);
|
||||||
return <TagSelector
|
return <TagSelector
|
||||||
tags={tags}
|
tags={tags}
|
||||||
searchBoxRef={this.searchBoxRef}
|
searchBoxRef={this.searchBoxRef}
|
||||||
|
@ -498,6 +535,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
|
||||||
width={this.state.width}
|
width={this.state.width}
|
||||||
height={this.state.height}
|
height={this.state.height}
|
||||||
fontSize={parseInt(this.state.fontSize.replace('px', ''))}
|
fontSize={parseInt(this.state.fontSize.replace('px', ''))}
|
||||||
|
lineHeight={parseInt(this.state.lineHeight.replace('px', ''))}
|
||||||
|
uiDensity={Zotero.Prefs.get('uiDensity')}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -417,6 +417,12 @@ Zotero.Utilities.Internal = {
|
||||||
return !str.replace(re, '');
|
return !str.replace(re, '');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
includesEmoji: function (str) {
|
||||||
|
// Remove emoji, Zero Width Joiner, and Variation Selector-16 and compare lengths
|
||||||
|
const re = /\p{Extended_Pictographic}|\u200D|\uFE0F/gu;
|
||||||
|
return str.replace(re, '').length !== str.length;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a prompt from an error with custom buttons and a callback
|
* Display a prompt from an error with custom buttons and a callback
|
||||||
*/
|
*/
|
||||||
|
@ -2304,6 +2310,21 @@ Zotero.Utilities.Internal = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
getScrollbarWidth() {
|
||||||
|
let document = Zotero.getMainWindow().document;
|
||||||
|
let scrollDiv = document.createElement('div');
|
||||||
|
scrollDiv.style.position = 'absolute';
|
||||||
|
scrollDiv.style.top = '-9999px';
|
||||||
|
scrollDiv.style.width = '50px';
|
||||||
|
scrollDiv.style.height = '50px';
|
||||||
|
scrollDiv.style.overflow = 'scroll';
|
||||||
|
document.documentElement.appendChild(scrollDiv);
|
||||||
|
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
|
||||||
|
document.documentElement.removeChild(scrollDiv);
|
||||||
|
|
||||||
|
return scrollbarWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -240,3 +240,6 @@ sidenav-related =
|
||||||
|
|
||||||
abstract-field =
|
abstract-field =
|
||||||
.label = Add abstract…
|
.label = Add abstract…
|
||||||
|
|
||||||
|
tagselector-search =
|
||||||
|
.placeholder = Filter Tags
|
4
chrome/skin/default/zotero/16/dark/filter.svg
Normal file
4
chrome/skin/default/zotero/16/dark/filter.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.99998 1.70711C1.37001 1.07714 1.81618 0 2.70708 0H14.2929C15.1838 0 15.6299 1.07714 15 1.70711L9.99998 6.70711V12.7071L6.99998 15.7071V6.70711L1.99998 1.70711ZM14.2929 1L2.70708 1L7.99998 6.29289V13.2929L8.99998 12.2929V6.29289L14.2929 1Z" fill="#FFFFFF8C" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 423 B |
4
chrome/skin/default/zotero/16/light/filter.svg
Normal file
4
chrome/skin/default/zotero/16/light/filter.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.99998 1.70711C1.37001 1.07714 1.81618 0 2.70708 0H14.2929C15.1838 0 15.6299 1.07714 15 1.70711L9.99998 6.70711V12.7071L6.99998 15.7071V6.70711L1.99998 1.70711ZM14.2929 1L2.70708 1L7.99998 6.29289V13.2929L8.99998 12.2929V6.29289L14.2929 1Z" fill="#00000080" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 423 B |
3
chrome/skin/default/zotero/8/dark/chevron-6.svg
Normal file
3
chrome/skin/default/zotero/8/dark/chevron-6.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.70711 2L1 2.70711L4 5.70711L7 2.70711L6.29289 2L4 4.29289L1.70711 2Z" fill="#FFFFFF8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 200 B |
3
chrome/skin/default/zotero/8/light/chevron-6.svg
Normal file
3
chrome/skin/default/zotero/8/light/chevron-6.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.70711 2L1 2.70711L4 5.70711L7 2.70711L6.29289 2L4 4.29289L1.70711 2Z" fill="#00000080"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 200 B |
|
@ -88,3 +88,15 @@ $item-pane-sections: (
|
||||||
"tags": var(--accent-orange),
|
"tags": var(--accent-orange),
|
||||||
"related": var(--accent-wood),
|
"related": var(--accent-wood),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$tagColorsLookup: (
|
||||||
|
'#ff6666': --tag-red,
|
||||||
|
'#ff8c19': --tag-orange,
|
||||||
|
'#999999': --tag-gray,
|
||||||
|
'#5fb236': --tag-green,
|
||||||
|
'#009980': --tag-teal,
|
||||||
|
'#2ea8e5': --tag-blue,
|
||||||
|
'#576dd9': --tag-indigo,
|
||||||
|
'#a28ae5': --tag-purple,
|
||||||
|
'#a6507b': --tag-plum,
|
||||||
|
);
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#zotero-collections-pane {
|
#zotero-collections-pane, #zotero-item-pane {
|
||||||
background: var(--material-sidepane);
|
background: var(--material-sidepane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,37 +3,31 @@
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
font: {
|
font: {
|
||||||
family: inherit;
|
family: inherit;
|
||||||
size: inherit;
|
size: inherit;
|
||||||
}
|
}
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
-moz-appearance: toolbarbutton;
|
-moz-appearance: toolbarbutton;
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: $btn-disabled-opacity;
|
opacity: $btn-disabled-opacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
.icon {
|
.icon {
|
||||||
&:first-child {
|
svg, img {
|
||||||
margin-left: -5px;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
&:last-child {
|
}
|
||||||
margin-right: -5px;
|
span.menu-marker {
|
||||||
}
|
-moz-appearance: toolbarbutton-dropdown;
|
||||||
svg, img {
|
display: inline-block;
|
||||||
vertical-align: middle;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
span.menu-marker {
|
|
||||||
-moz-appearance: toolbarbutton-dropdown;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: -5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,49 @@
|
||||||
.icon > svg, .icon > img {
|
.icon > svg, .icon > img {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-bg {
|
.icon-bg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-css {
|
.icon-css {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.icon-downchevron {
|
.icon.icon-downchevron {
|
||||||
width: 7px !important;
|
width: 7px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
-moz-appearance: none !important;
|
-moz-appearance: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-16 {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-8 {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
$-icons: (
|
||||||
|
filter: 16,
|
||||||
|
chevron-6: 8
|
||||||
|
);
|
||||||
|
|
||||||
|
@each $icon, $size in $-icons {
|
||||||
|
.icon-#{$icon} {
|
||||||
|
@include color-scheme using($color) {
|
||||||
|
@include svgicon($icon, $color, $size);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -136,11 +136,10 @@
|
||||||
|
|
||||||
.zotero-toolbar {
|
.zotero-toolbar {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zotero-layout-switcher .zotero-toolbar {
|
||||||
background: var(--material-tabbar);
|
background: var(--material-tabbar);
|
||||||
border-bottom: var(--material-panedivider);
|
border-bottom: var(--material-panedivider);
|
||||||
}
|
}
|
||||||
|
|
||||||
#zotero-collections-tree > div, #zotero-item-pane {
|
|
||||||
background: var(--material-sidepane);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.search input {
|
.search input {
|
||||||
|
background: var(--material-background);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: var(--material-border-quinary);
|
||||||
|
color: var(--fill-primary);
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
font-size: 1em;
|
margin: 6px 4px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
padding-left: 4px;
|
padding: 3px 7px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--fill-tertiary);
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: SelectedItem;
|
||||||
|
box-shadow: 0 0 0 2px SelectedItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search .search-cancel-button {
|
.search .search-cancel-button {
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
background: var(--material-sidepane);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-selector-list-container > div {
|
.tag-selector-list-container > div {
|
||||||
|
@ -43,18 +42,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-selector-list {
|
.tag-selector-list {
|
||||||
list-style: none;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-selector-filter-pane {
|
||||||
|
padding: 0 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-selector-filter-container {
|
.tag-selector-filter-container {
|
||||||
height: 30px;
|
border-top: var(--material-panedivider);
|
||||||
flex: 0 0 1em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: 0.125em 0 0.125em 0.5em;
|
flex: 0 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-selector-filter-container .search {
|
.tag-selector-filter-container .search {
|
||||||
|
@ -63,21 +66,79 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-selector-actions {
|
.tag-selector-actions {
|
||||||
flex: 0 1;
|
align-self: center;
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
flex: 0 1;
|
||||||
|
padding: 3px 4px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-selector-item {
|
.tag-selector-item {
|
||||||
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 0.916666667em;
|
||||||
|
line-height: 1.272727273;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 1px 4px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
padding: 1px 4px 3px; // See also TagSelectorList.jsx
|
|
||||||
|
@include comfortable {
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--fill-quinary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--fill-secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.colored {
|
&.colored {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: currentcolor;
|
||||||
|
|
||||||
|
@each $colorHex, $colorVar in $tagColorsLookup {
|
||||||
|
@include state('.tag-selector-item[data-color="#{$colorHex}"]') {
|
||||||
|
background-color: var($colorVar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.emoji) {
|
||||||
|
&::before {
|
||||||
|
content: " ";
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.636363636em; // 7px (+ 1px border = 8px)
|
||||||
|
height: 0.636363636em;
|
||||||
|
margin-right: .272727273em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: currentcolor; // fallback for non-standard colors
|
||||||
|
vertical-align: -0.363636364em; // -4px
|
||||||
|
border: var(--material-border-transparent);
|
||||||
|
|
||||||
|
@include state('.tag-selector-item.selected') {
|
||||||
|
border-color: var(--color-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $colorHex, $colorVar in $tagColorsLookup {
|
||||||
|
@include state('.tag-selector-item[data-color="#{$colorHex}"]') {
|
||||||
|
&::before {
|
||||||
|
background-color: var($colorVar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
@ -89,4 +150,12 @@
|
||||||
color: var(--color-background);
|
color: var(--color-background);
|
||||||
background-color: var(--fill-secondary);
|
background-color: var(--fill-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--fill-primary);
|
||||||
|
|
||||||
|
@include state('.tag-selector-item.selected') {
|
||||||
|
color: var(--color-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.search input {
|
.search input {
|
||||||
-moz-appearance: searchfield;
|
-moz-appearance: searchfield;
|
||||||
height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search .search-cancel-button {
|
.search .search-cancel-button {
|
||||||
|
|
|
@ -2,18 +2,4 @@
|
||||||
// Tag selector
|
// Tag selector
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
.tag-selector-filter-container {
|
|
||||||
padding: 0.25em 0 0.25em 0.5em;
|
|
||||||
border-top: var(--material-panedivider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-selector-item {
|
|
||||||
padding-bottom: .3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-selector-actions {
|
|
||||||
flex: none;
|
|
||||||
border: 0;
|
|
||||||
margin-right: 3px;
|
|
||||||
padding: 1px 6px 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,6 +31,9 @@
|
||||||
--color-sidepane: #303030;
|
--color-sidepane: #303030;
|
||||||
--color-tabbar: #1e1e1e;
|
--color-tabbar: #1e1e1e;
|
||||||
--color-toolbar: #272727;
|
--color-toolbar: #272727;
|
||||||
|
--color-scrollbar: rgb(117, 117, 117);
|
||||||
|
--color-scrollbar-hover: rgb(158, 158, 158);
|
||||||
|
--color-scrollbar-background: transparent;
|
||||||
--tag-blue: #55a6dfd9;
|
--tag-blue: #55a6dfd9;
|
||||||
--tag-gray: #aaac;
|
--tag-gray: #aaac;
|
||||||
--tag-green: #74b04ad9;
|
--tag-green: #74b04ad9;
|
||||||
|
@ -59,5 +62,6 @@
|
||||||
--material-border: 1px solid var(--color-border);
|
--material-border: 1px solid var(--color-border);
|
||||||
--material-border50: 1px solid var(--color-border50);
|
--material-border50: 1px solid var(--color-border50);
|
||||||
--material-panedivider: 1px solid var(--color-panedivider);
|
--material-panedivider: 1px solid var(--color-panedivider);
|
||||||
|
--material-border-quinary: 1px solid var(--fill-quinary);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -31,6 +31,9 @@
|
||||||
--color-sidepane: #f2f2f2;
|
--color-sidepane: #f2f2f2;
|
||||||
--color-tabbar: #f2f2f2;
|
--color-tabbar: #f2f2f2;
|
||||||
--color-toolbar: #f9f9f9;
|
--color-toolbar: #f9f9f9;
|
||||||
|
--color-scrollbar: rgb(194, 194, 194);
|
||||||
|
--color-scrollbar-hover: rgb(125, 125, 125);
|
||||||
|
--color-scrollbar-background: transparent;
|
||||||
--tag-blue: #55a6df;
|
--tag-blue: #55a6df;
|
||||||
--tag-gray: #aaa;
|
--tag-gray: #aaa;
|
||||||
--tag-green: #74b04a;
|
--tag-green: #74b04a;
|
||||||
|
@ -59,5 +62,6 @@
|
||||||
--material-border: 1px solid var(--color-border);
|
--material-border: 1px solid var(--color-border);
|
||||||
--material-border50: 1px solid var(--color-border50);
|
--material-border50: 1px solid var(--color-border50);
|
||||||
--material-panedivider: 1px solid var(--color-panedivider);
|
--material-panedivider: 1px solid var(--color-panedivider);
|
||||||
|
--material-border-quinary: 1px solid var(--fill-quinary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue