parent
d55b80df80
commit
89d1899690
2 changed files with 97 additions and 41 deletions
|
@ -25,32 +25,49 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect } from 'react';
|
import React, { forwardRef, useState, useRef, useImperativeHandle, useLayoutEffect } from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
const { IconXmark } = require('./icons');
|
const { IconXmark } = require('./icons');
|
||||||
|
|
||||||
const TabBar = forwardRef(function (props, ref) {
|
const TabBar = forwardRef(function (props, ref) {
|
||||||
const [tabs, setTabs] = useState([]);
|
const [tabs, setTabs] = useState([]);
|
||||||
const draggingID = useRef(null);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [draggingX, setDraggingX] = useState(0);
|
||||||
|
const draggingIDRef = useRef(null);
|
||||||
|
const draggingDeltaXRef = useRef();
|
||||||
const tabsRef = useRef();
|
const tabsRef = useRef();
|
||||||
const mouseMoveWaitUntil = useRef(0);
|
const mouseMoveWaitUntil = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('mouseup', handleWindowMouseUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mouseup', handleWindowMouseUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({ setTabs }));
|
useImperativeHandle(ref, () => ({ setTabs }));
|
||||||
|
|
||||||
function handleTabMouseDown(event, id, index) {
|
useLayoutEffect(() => {
|
||||||
|
if (!draggingIDRef.current) return;
|
||||||
|
let tab = Array.from(tabsRef.current.children).find(x => x.dataset.id === draggingIDRef.current);
|
||||||
|
if (tab) {
|
||||||
|
let x = draggingX - tab.offsetLeft - draggingDeltaXRef.current;
|
||||||
|
|
||||||
|
let firstTab = tabsRef.current.firstChild;
|
||||||
|
let lastTab = tabsRef.current.lastChild;
|
||||||
|
|
||||||
|
if (Zotero.rtl) {
|
||||||
|
if (tab.offsetLeft + x < lastTab.offsetLeft
|
||||||
|
|| tab.offsetLeft + tab.offsetWidth + x > firstTab.offsetLeft) {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tab.offsetLeft + x > lastTab.offsetLeft
|
||||||
|
|| tab.offsetLeft + x < firstTab.offsetLeft + firstTab.offsetWidth) {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.style.transform = dragging ? `translateX(${x}px)` : 'unset';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleTabMouseDown(event, id) {
|
||||||
if (event.target.closest('.tab-close')) {
|
if (event.target.closest('.tab-close')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (index != 0) {
|
|
||||||
draggingID.current = id;
|
|
||||||
}
|
|
||||||
props.onTabSelect(id);
|
props.onTabSelect(id);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
@ -61,45 +78,71 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabBarMouseMove(event) {
|
function handleDragStart(event, id, index) {
|
||||||
if (!draggingID.current || mouseMoveWaitUntil.current > Date.now()) {
|
if (index === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
// Empty drag image
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
|
event.dataTransfer.setDragImage(img, 0, 0);
|
||||||
|
event.dataTransfer.setData('zotero/tab', id);
|
||||||
|
draggingDeltaXRef.current = event.clientX - event.target.offsetLeft;
|
||||||
|
setDragging(true);
|
||||||
|
draggingIDRef.current = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
setDragging(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabBarDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = 'move';
|
||||||
|
if (!draggingIDRef.current || mouseMoveWaitUntil.current > Date.now()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraggingX(event.clientX);
|
||||||
|
|
||||||
|
let tabIndex = Array.from(tabsRef.current.children).findIndex(x => x.dataset.id === draggingIDRef.current);
|
||||||
|
let tab = tabsRef.current.children[tabIndex];
|
||||||
|
|
||||||
let points = Array.from(tabsRef.current.children).map((child) => {
|
let points = Array.from(tabsRef.current.children).map((child) => {
|
||||||
let rect = child.getBoundingClientRect();
|
return child.offsetLeft + child.offsetWidth / 2;
|
||||||
return rect.left + rect.width / 2;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x1 = event.clientX - draggingDeltaXRef.current;
|
||||||
|
let x2 = event.clientX - draggingDeltaXRef.current + tab.offsetWidth;
|
||||||
|
|
||||||
let index = null;
|
let index = null;
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
let point1 = points[i];
|
if (i === tabIndex || i + 1 === tabIndex) {
|
||||||
let point2 = points[i + 1];
|
continue;
|
||||||
if (event.clientX > Math.min(point1, point2)
|
}
|
||||||
&& event.clientX < Math.max(point1, point2)) {
|
let p1 = points[i];
|
||||||
|
let p2 = points[i + 1];
|
||||||
|
if (
|
||||||
|
Zotero.rtl && (x2 < p1 && x2 > p2 || x1 < p1 && x1 > p2)
|
||||||
|
|| !Zotero.rtl && (x2 > p1 && x2 < p2 || x1 > p1 && x1 < p2)
|
||||||
|
) {
|
||||||
index = i + 1;
|
index = i + 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === null) {
|
if (index === null) {
|
||||||
let point1 = points[0];
|
let p = points[points.length - 1];
|
||||||
let point2 = points[points.length - 1];
|
if (Zotero.rtl && x1 < p || !Zotero.rtl && x2 > p) {
|
||||||
if ((point1 < point2 && event.clientX < point1
|
|
||||||
|| point1 > point2 && event.clientX > point1)) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
index = points.length;
|
index = points.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (index == 0) {
|
|
||||||
index = 1;
|
|
||||||
}
|
|
||||||
props.onTabMove(draggingID.current, index);
|
|
||||||
mouseMoveWaitUntil.current = Date.now() + 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWindowMouseUp(event) {
|
if (index !== null) {
|
||||||
draggingID.current = null;
|
props.onTabMove(draggingIDRef.current, index);
|
||||||
event.stopPropagation();
|
}
|
||||||
|
mouseMoveWaitUntil.current = Date.now() + 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabClose(event, id) {
|
function handleTabClose(event, id) {
|
||||||
|
@ -124,10 +167,14 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className={cx('tab', { selected })}
|
data-id={id}
|
||||||
|
className={cx('tab', { selected, dragging: dragging && id === draggingIDRef.current })}
|
||||||
|
draggable={true}
|
||||||
onMouseMove={() => handleTabMouseMove(title)}
|
onMouseMove={() => handleTabMouseMove(title)}
|
||||||
onMouseDown={(event) => handleTabMouseDown(event, id, index)}
|
onMouseDown={(event) => handleTabMouseDown(event, id)}
|
||||||
onClick={(event) => handleTabClick(event, id)}
|
onClick={(event) => handleTabClick(event, id)}
|
||||||
|
onDragStart={(event) => handleDragStart(event, id, index)}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="tab-name">{title}</div>
|
<div className="tab-name">{title}</div>
|
||||||
<div
|
<div
|
||||||
|
@ -144,7 +191,7 @@ const TabBar = forwardRef(function (props, ref) {
|
||||||
<div
|
<div
|
||||||
ref={tabsRef}
|
ref={tabsRef}
|
||||||
className="tabs"
|
className="tabs"
|
||||||
onMouseMove={handleTabBarMouseMove}
|
onDragOver={handleTabBarDragOver}
|
||||||
onMouseOut={handleTabBarMouseOut}
|
onMouseOut={handleTabBarMouseOut}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => renderTab(tab, index))}
|
{tabs.map((tab, index) => renderTab(tab, index))}
|
||||||
|
|
|
@ -45,6 +45,15 @@
|
||||||
border-top: 2px solid $tab-background-color-selected;
|
border-top: 2px solid $tab-background-color-selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
border-inline-start: $tab-border;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragging + & {
|
||||||
|
border-inline-start: $tab-border;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-name {
|
.tab-name {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
Loading…
Reference in a new issue