parent
7b0d6951d2
commit
5d442b706e
5 changed files with 199 additions and 19 deletions
|
@ -80,6 +80,22 @@ i('Twisty', (
|
|||
<path d="M8 13.4c-.5 0-.9-.2-1.2-.6L.4 5.2C0 4.7-.1 4.3.2 3.7S1 3 1.6 3h12.8c.6 0 1.2.1 1.4.7.3.6.2 1.1-.2 1.6l-6.4 7.6c-.3.4-.7.5-1.2.5z"/>
|
||||
</svg>
|
||||
));
|
||||
i('ArrowLeft', (
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="m5.001 8.352 5.465 5.466a.626.626 0 0 0 .884-.886L6.416 7.999l4.933-4.932a.626.626 0 0 0-.885-.885L5 7.647l.001.705z"/>
|
||||
</svg>
|
||||
));
|
||||
i('ArrowRight', (
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="m10.999 8.352-5.465 5.466a.626.626 0 0 1-.884-.886l4.935-4.934-4.934-4.931a.626.626 0 0 1 .885-.885L11 7.647l-.001.705z"/>
|
||||
</svg>
|
||||
));
|
||||
i('Cross', "chrome://zotero/skin/cross.png");
|
||||
i('Tick', "chrome://zotero/skin/tick.png");
|
||||
i('ArrowRefresh', "chrome://zotero/skin/arrow_refresh.png");
|
||||
|
|
|
@ -25,9 +25,11 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
import React, { forwardRef, useState, useRef, useImperativeHandle, useLayoutEffect } from 'react';
|
||||
import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
const { IconXmark } = require('./icons');
|
||||
const { IconXmark, IconArrowLeft, IconArrowRight } = require('./icons');
|
||||
|
||||
const SCROLL_ARROW_SCROLL_BY = 200;
|
||||
|
||||
const TabBar = forwardRef(function (props, ref) {
|
||||
const [tabs, setTabs] = useState([]);
|
||||
|
@ -36,11 +38,23 @@ const TabBar = forwardRef(function (props, ref) {
|
|||
const dragIDRef = useRef(null);
|
||||
const dragGrabbedDeltaXRef = useRef();
|
||||
const tabsRef = useRef();
|
||||
const startArrowRef = useRef();
|
||||
const endArrowRef = useRef();
|
||||
// Used to throttle mouse movement
|
||||
const mouseMoveWaitUntil = useRef(0);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setTabs }));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let handleResize = () => updateScrollArrows();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => updateScrollArrows());
|
||||
|
||||
// Use offsetLeft and offsetWidth to calculate and translate tab X position
|
||||
useLayoutEffect(() => {
|
||||
if (!dragIDRef.current) return;
|
||||
|
@ -69,6 +83,34 @@ const TabBar = forwardRef(function (props, ref) {
|
|||
tab.style.transform = dragging ? `translateX(${x}px)` : 'unset';
|
||||
}
|
||||
});
|
||||
|
||||
function updateScrollArrows() {
|
||||
let enableArrows = tabsRef.current.scrollWidth !== tabsRef.current.clientWidth;
|
||||
if (enableArrows) {
|
||||
startArrowRef.current.classList.add('enabled');
|
||||
endArrowRef.current.classList.add('enabled');
|
||||
}
|
||||
else {
|
||||
startArrowRef.current.classList.remove('enabled');
|
||||
endArrowRef.current.classList.remove('enabled');
|
||||
}
|
||||
|
||||
if (enableArrows) {
|
||||
if (tabsRef.current.scrollLeft !== 0) {
|
||||
startArrowRef.current.classList.add('active');
|
||||
}
|
||||
else {
|
||||
startArrowRef.current.classList.remove('active');
|
||||
}
|
||||
|
||||
if (tabsRef.current.scrollWidth - tabsRef.current.clientWidth !== Math.abs(tabsRef.current.scrollLeft)) {
|
||||
endArrowRef.current.classList.add('active');
|
||||
}
|
||||
else {
|
||||
endArrowRef.current.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabMouseDown(event, id) {
|
||||
if (event.button === 2) {
|
||||
|
@ -194,6 +236,41 @@ const TabBar = forwardRef(function (props, ref) {
|
|||
window.Zotero_Tooltip.stop();
|
||||
}
|
||||
|
||||
function handleWheel(event) {
|
||||
// Normalize wheel speed
|
||||
let x = event.deltaX;
|
||||
if (x && event.deltaMode) {
|
||||
if (event.deltaMode === 1) {
|
||||
x *= 20;
|
||||
}
|
||||
else {
|
||||
x *= 400;
|
||||
}
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
tabsRef.current.scrollLeft += x;
|
||||
});
|
||||
}
|
||||
|
||||
function handleClickScrollStart() {
|
||||
tabsRef.current.scrollTo({
|
||||
left: tabsRef.current.scrollLeft - (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
function handleClickScrollEnd() {
|
||||
tabsRef.current.scrollTo({
|
||||
left: tabsRef.current.scrollLeft + (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent maximizing/minimizing window
|
||||
function handleScrollArrowDoubleClick(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function renderTab({ id, title, selected }, index) {
|
||||
return (
|
||||
<div
|
||||
|
@ -219,13 +296,34 @@ const TabBar = forwardRef(function (props, ref) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={tabsRef}
|
||||
className="tabs"
|
||||
onDragOver={handleTabBarDragOver}
|
||||
onMouseOut={handleTabBarMouseOut}
|
||||
>
|
||||
{tabs.map((tab, index) => renderTab(tab, index))}
|
||||
<div>
|
||||
<div className="tab-bar-inner-container" onWheel={handleWheel}>
|
||||
<div
|
||||
ref={startArrowRef}
|
||||
className="scroll-start-arrow"
|
||||
style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }}
|
||||
onClick={handleClickScrollStart}
|
||||
onDoubleClick={handleScrollArrowDoubleClick}
|
||||
><IconArrowLeft/></div>
|
||||
<div className="tabs-wrapper">
|
||||
<div
|
||||
ref={tabsRef}
|
||||
className="tabs"
|
||||
onDragOver={handleTabBarDragOver}
|
||||
onMouseOut={handleTabBarMouseOut}
|
||||
onScroll={updateScrollArrows}
|
||||
>
|
||||
{tabs.map((tab, index) => renderTab(tab, index))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={endArrowRef}
|
||||
className="scroll-end-arrow"
|
||||
style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }}
|
||||
onClick={handleClickScrollEnd}
|
||||
onDoubleClick={handleScrollArrowDoubleClick}
|
||||
><IconArrowRight/></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -312,6 +312,19 @@ var Zotero_Tabs = new function () {
|
|||
}
|
||||
tab.lastFocusedElement = null;
|
||||
}
|
||||
let tabNode = document.querySelector(`.tab[data-id="${tab.id}"]`);
|
||||
let tabsContainerNode = document.querySelector('#tab-bar-container .tabs');
|
||||
document.querySelector(`.tab[data-id="${tab.id}"]`).scrollIntoView({ behavior: 'smooth' });
|
||||
// Border is not included when scrolling element node into view, therefore we do it manually.
|
||||
// TODO: `scroll-padding` since Firefox 68 can probably be used instead
|
||||
setTimeout(() => {
|
||||
if (tabNode.offsetLeft + tabNode.offsetWidth - tabsContainerNode.offsetWidth + 1 >= tabsContainerNode.scrollLeft) {
|
||||
document.querySelector('#tab-bar-container .tabs').scrollLeft += 1;
|
||||
}
|
||||
else if (tabNode.offsetLeft - 1 <= tabsContainerNode.scrollLeft) {
|
||||
document.querySelector('#tab-bar-container .tabs').scrollLeft -= 1;
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,8 +2,62 @@
|
|||
min-height: 30px;
|
||||
}
|
||||
|
||||
.tab-bar-inner-container {
|
||||
display: flex;
|
||||
|
||||
.tabs-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.scroll-start-arrow, .scroll-end-arrow {
|
||||
height: 30px;
|
||||
padding: 0 3px;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
color: #bebebe;
|
||||
display: none;
|
||||
box-shadow: none;
|
||||
|
||||
&.enabled {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #505050;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-start-arrow {
|
||||
border-right: 1px solid transparent;
|
||||
&.active {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 1px 0 0 0 rgba(0,0,0,0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-end-arrow {
|
||||
border-left: 1px solid transparent;
|
||||
&.active {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||
box-shadow: -1px 0 0 0 rgba(0,0,0,0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
|
@ -36,6 +90,7 @@
|
|||
color: #000;
|
||||
text-align: center;
|
||||
padding: 0 30px;
|
||||
min-width: 100px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-inline-end: $tab-border;
|
||||
|
@ -73,6 +128,10 @@
|
|||
text-align: center;
|
||||
line-height: 16px;
|
||||
border-radius: 3px;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
|
|
|
@ -6,13 +6,7 @@
|
|||
-moz-window-dragging: no-drag;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
&:before {
|
||||
width: 78px;
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
min-width: 20px;
|
||||
}
|
||||
.tab-bar-inner-container {
|
||||
margin-inline-start: 78px;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue