Initial tab bar implementation

This commit is contained in:
Dan Stillman 2020-09-15 03:30:17 -04:00
parent 199619f40e
commit 875e9f674f
10 changed files with 929 additions and 490 deletions

View file

@ -0,0 +1,110 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.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";
import React, { forwardRef, useState, useImperativeHandle, useEffect } from 'react';
import cx from 'classnames';
const TabBar = forwardRef(function (props, ref) {
const [tabs, setTabs] = useState(props.initialTabs);
const [selectedIndex, setSelectedIndex] = useState(0);
useImperativeHandle(ref, () => ({
addTab({ title, type }) {
var newTabs = [...tabs];
newTabs.push({ title, type });
setTabs(newTabs);
setSelectedIndex(newTabs.length - 1);
},
renameTab(title, index) {
var newTabs = tabs.map((tab, currentIndex) => {
let newTab = Object.assign({}, tab);
if (index == currentIndex) {
newTab.title = title;
}
return newTab;
});
setTabs(newTabs);
}
}));
useEffect(() => {
if (props.onTabSelected) {
props.onTabSelected(selectedIndex);
}
}, [selectedIndex]);
function removeTab(index) {
var newTabs = [...tabs];
newTabs.splice(index, 1);
setTabs(newTabs);
setSelectedIndex(selectedIndex - 1);
if (props.onTabClosed) {
props.onTabClosed(index);
}
}
function handleTabClick(event, index) {
setSelectedIndex(index);
event.stopPropagation();
}
function handleTabClose(event, index) {
removeTab(index);
if (props.onTabClose) {
props.onTabClose(index);
}
event.stopPropagation();
}
function renderTab(tab, index) {
return (
<div
key={index}
className={cx("tab", { selected: index == selectedIndex })}
onClick={(event) => handleTabClick(event, index)}
>
<div className="tab-name">{tab.title}</div>
<div
className="tab-close"
onClick={(event) => handleTabClose(event, index)}
>
X
</div>
</div>
);
}
return (
<div className="tabs">
{ tabs.map((tab, index) => renderTab(tab, index)) }
</div>
);
});
export default TabBar;

View file

@ -43,9 +43,13 @@
<window id="main-window"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="navigator:browser"
title="&brandShortName;"
width="1000" height="600"
drawintitlebar="true"
tabsintitlebar="true"
chromemargin="0,-1,-1,-1"
persist="screenX screenY width height sizemode">
<script type="application/javascript" src="resource://zotero/require.js"/>
<script type="application/javascript" src="standalone.js"/>
@ -111,7 +115,16 @@
accesskey="&selectAllCmd.accesskey;"
command="cmd_selectAll"/>
</menupopup>
<vbox id="titlebar">
<hbox id="titlebar-buttonbox-container" skipintoolbarset="true">
<hbox id="titlebar-buttonbox">
<toolbarbutton class="titlebar-button titlebar-min" oncommand="window.minimize();"/>
<toolbarbutton class="titlebar-button titlebar-max" oncommand="onTitlebarMaxClick();"/>
<toolbarbutton class="titlebar-button titlebar-close" command="cmd_closeWindow"/>
</hbox>
</hbox>
</vbox>
<vbox id="tab-bar-container" style="-moz-window-dragging: drag"/>
<toolbox id="navigator-toolbox" class="toolbox-top" mode="icons" defaultmode="icons">
<!-- Menu -->
<toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" customizable="true"

View file

@ -0,0 +1,105 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.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";
// Using 'import' breaks hooks
var React = require('react');
var ReactDOM = require('react-dom');
import TabBar from 'components/tabBar';
var Zotero_Tabs = new function () {
const HTML_NS = 'http://www.w3.org/1999/xhtml';
Object.defineProperty(this, 'deck', {
get: () => document.getElementById('tabs-deck')
});
this._tabBarRef = {};
this._tabs = [
{
title: "",
type: 'library'
}
];
this.init = function () {
ReactDOM.render(
<TabBar
ref={this._tabBarRef}
initialTabs={this._tabs}
onTabSelected={this._onTabSelected.bind(this)}
onTabClosed={this._onTabClosed.bind(this)}
/>,
document.getElementById('tab-bar-container')
);
};
/**
* @return {Element} - The element created in the deck
*/
this.add = function ({ title, type, url, index }) {
this._tabBarRef.current.addTab({ title, type });
var elem;
if (url) {
elem = document.createElement('iframe');
elem.setAttribute('type', 'content');
elem.setAttribute('src', url);
}
else {
elem = document.createElementNS(HTML_NS, 'div');
elem.textContent = title;
}
var deck = this.deck;
deck.insertBefore(elem, index === undefined ? null : deck.childNodes[index]);
return elem;
};
this.rename = function (title, index) {
if (index === undefined) {
index = this.deck.selectedIndex;
}
this._tabs[index].title = title;
this._tabBarRef.current.renameTab(title, index);
};
this._onTabSelected = function (index) {
this.deck.selectedIndex = index;
};
this._onTabClosed = function (index) {
this._tabs.splice(index, 1);
var deck = this.deck;
deck.removeChild(deck.childNodes[index]);
};
};

View file

@ -144,6 +144,7 @@ var ZoteroPane = new function()
Zotero.hiDPI = window.devicePixelRatio > 1;
Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
Zotero_Tabs.init();
ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
// Add a default progress window
@ -1166,6 +1167,10 @@ var ZoteroPane = new function()
return;
}
// Rename tab
// TODO: What if PDF is in front?
Zotero_Tabs.rename(collectionTreeRow.getName());
// Clear quick search and tag selector when switching views
document.getElementById('zotero-tb-search').value = "";
if (ZoteroPane.tagSelector) {

View file

@ -41,6 +41,7 @@
xmlns:html="http://www.w3.org/1999/xhtml">
<script src="include.js"/>
<script src="tabs.js"/>
<script src="zoteroPane.js" type="application/javascript"/>
<script src="fileInterface.js"/>
<script src="reportInterface.js"/>
@ -73,6 +74,7 @@
</commandset>
<stack id="zotero-pane-stack">
<deck id="tabs-deck">
<vbox id="zotero-pane"
onkeydown="ZoteroPane_Local.handleKeyDown(event, this.id)"
onkeyup="ZoteroPane_Local.handleKeyUp(event, this.id)"
@ -573,6 +575,7 @@
</box>
</hbox>
</vbox>
</deck>
<!-- Barrier to prevent tabbing into Zotero pane when busy -->
<box id="zotero-pane-tab-catcher-bottom" hidden="true" align="center" pack="center" style="opacity: 0">

View file

@ -25,8 +25,10 @@
@import "components/createParent";
@import "components/editable";
@import "components/icons";
@import "components/mainWindow";
@import "components/progressMeter";
@import "components/search";
@import "components/syncButtonTooltip";
@import "components/tabsBar";
@import "components/tagsBox";
@import "components/tagSelector";

View file

@ -0,0 +1,149 @@
:root {
--tab-min-height: 30px;
--tabs-border-color: rgba(0,0,0,.3);
--tabline-color: #0a84ff;
--toolbar-non-lwt-bgcolor: #f9f9fa;
--toolbar-non-lwt-textcolor: #0c0c0d;
--toolbar-non-lwt-bgimage: none;
--toolbar-bgcolor: var(--toolbar-non-lwt-bgcolor);
--toolbar-bgimage: var(--toolbar-non-lwt-bgimage);
--chrome-content-separator-color: hsl(0, 0%, 60%);
--toolbarbutton-border-radius: 3px;
--toolbarbutton-icon-fill-opacity: .85;
--toolbarbutton-hover-background: hsla(0, 0%, 100%, .1)
linear-gradient(hsla(0, 0%, 100%, .3),
hsla(0, 0%, 100%, .1)) no-repeat;
--toolbarbutton-hover-bordercolor: hsla(0, 0%, 0%, .2);
--toolbarbutton-header-bordercolor: hsla(0, 0%, 0%, .2);
--toolbarbutton-hover-boxshadow: 0 1px 0 hsla(0, 0%, 100%, .5),
0 1px 0 hsla(0, 0%, 100%, .5) inset;
--toolbarbutton-active-background: hsla(0, 0%, 0%, .02)
linear-gradient(hsla(0, 0%, 0%, .12),
transparent) border-box;
--toolbarbutton-active-bordercolor: hsla(0, 0%, 0%, .3);
--toolbarbutton-active-boxshadow: 0 1px 0 hsla(0, 0%, 100%, .5),
0 1px 0 hsla(0, 0%, 0%, .05) inset,
0 1px 1px hsla(0, 0%, 0%, .2) inset;
--toolbarbutton-inactive-bordercolor: rgba(0, 0, 0, 0.1);
--toolbarbutton-inactive-boxshadow: 0 1px 0 hsla(0, 0%, 0%, .05) inset;
--toolbarbutton-checkedhover-backgroundcolor: hsla(0, 0%, 0%, .09);
--toolbarbutton-icon-fill-attention: var(--lwt-toolbarbutton-icon-fill-attention, #0a84ff);
--lwt-header-image: none;
--row-grouped-header-bg-color: #d5d5d5;
--row-grouped-header-bg-color-selected: #3874d1;
--panel-separator-color: hsla(210, 4%, 10%, .14);
--arrowpanel-dimmed: hsla(0, 0%, 80%, .3);
--arrowpanel-dimmed-further: hsla(0, 0%, 80%, .45);
--splitter-color: #bdbdbd;
--urlbar-popup-url-color: hsl(210, 77%, 47%);
--urlbar-popup-action-color: hsl(178, 100%, 28%);
}
:root {
--autocomplete-popup-background: -moz-field;
--autocomplete-popup-color: -moz-fieldtext;
--autocomplete-popup-border-color: ThreeDShadow;
--autocomplete-popup-highlight-background: Highlight;
--autocomplete-popup-highlight-color: HighlightText;
/* Note: Setting this to 0 (without px) breaks CSS calculations for OSX. */
--space-above-tabbar: 0px;
}
:root:-moz-window-inactive {
--toolbar-bgcolor: -moz-mac-chrome-inactive;
}
#titlebar {
margin-bottom: -30px;
-moz-box-pack: center;
}
#personal-bookmarks {
-moz-window-dragging: inherit;
}
toolbarpaletteitem {
-moz-window-dragging: no-drag;
-moz-box-pack: start;
}
.titlebar-buttonbox-container {
-moz-box-ordinal-group: 1000;
}
#titlebar-fullscreen-button {
-moz-appearance: -moz-mac-fullscreen-button;
}
/* ::::: Tabs in Titlebar :::::: */
/**
* For tabs in titlebar on OS X, we stretch the titlebar down so that the
* tabstrip can overlap it.
*/
#main-window[tabsintitlebar] > #titlebar {
min-height: calc(var(--tab-min-height) + var(--space-above-tabbar));
}
#main-window[tabsintitlebar="true"]:not(:-moz-lwtheme) > #titlebar {
-moz-appearance: -moz-window-titlebar;
}
#main-window:not([tabsintitlebar]) .titlebar-placeholder {
visibility: collapse;
}
/* NB: these would be margin-inline-start/end if it wasn't for the fact that OS X
* doesn't reverse the order of the items in the titlebar in RTL mode. */
.titlebar-placeholder[type="caption-buttons"],
#titlebar-buttonbox {
margin-right: 12px;
margin-left: 12px;
}
.titlebar-placeholder[type="fullscreen-button"],
#titlebar-fullscreen-button {
margin-right: 7px;
margin-left: 7px;
}
#titlebar-fullscreen-button {
-moz-appearance: -moz-mac-fullscreen-button;
}
#titlebar-buttonbox {
-moz-appearance: -moz-window-button-box;
}
/* Fullscreen and caption buttons don't move with RTL on OS X so override the automatic ordering. */
#titlebar-fullscreen-button:-moz-locale-dir(ltr),
#titlebar-buttonbox-container:-moz-locale-dir(rtl),
.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(ltr),
.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(rtl) {
-moz-box-ordinal-group: 1000;
}
#titlebar-fullscreen-button:-moz-locale-dir(rtl),
#titlebar-buttonbox-container:-moz-locale-dir(ltr),
.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(ltr),
.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(rtl) {
-moz-box-ordinal-group: 0;
}
#main-window[sizemode="fullscreen"] .titlebar-placeholder[type="fullscreen-button"],
#main-window[sizemode="fullscreen"] .titlebar-placeholder[type="caption-buttons"] {
display: none;
}

View file

@ -0,0 +1,42 @@
#tab-bar-container {
min-height: 30px;
}
.tabs {
display: flex;
}
.tab {
-moz-appearance: toolbar;
width: 200px;
height: 30px;
line-height: 30px;
display: flex;
justify-content: center;
align-content: center;
position: relative;
background: #f9f9f9;
border: 1px solid black;
margin-right: -1px;
border-bottom: 1px solid transparent;
color: #000;
.tab-name {
line-height: 30px;
}
.tab-close {
position: absolute;
left: 6px;
top: 7px;
width: 16px;
height: 16px;
text-align: center;
line-height: 16px;
display: none;
}
&:hover:not(:first-child) .tab-close {
display: block;
}
}

9
scss/mac/_tabsBar.scss Normal file
View file

@ -0,0 +1,9 @@
.tabs {
-moz-window-dragging: drag;
margin-left: 78px;
}
.tab {
-moz-window-dragging: no-drag;
}

View file

@ -8,4 +8,5 @@
@import "mac/createParent";
@import "mac/editable";
@import "mac/search";
@import "mac/tabsBar";
@import "mac/tag-selector";