Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontends/web/images/icons/icon_upside_down_triangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 18 additions & 1 deletion frontends/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,24 @@ <h1>Graffiti</h1>
<div class="clearfix">&nbsp;</div>
</header>
<div class="tabs">
<button id="addTab" onclick="event_addTab()">+</button>
<!-- Leftmost tab search: collapsed to icon, expands on focus -->
<div class="tab-search">
<button class="tab-search-button" aria-label="Search tabs" title="Search tabs">
<img data-icon-state="white"
src="images/icons/icon_search.svg"
/>
</button>
<input class="tab-search-input" type="text" placeholder="Tab Search" />
</div>
<!-- Rightmost tab add -->
<!-- Arrow appears when tab list is horizontally scrollable -->
<button id="tabExpand" class="tab-expand-button" onclick="event_toggleTabBarMode()" title="Show all tabs">
<img src="images/icons/icon_upside_down_triangle.svg" expand-icon-state="down" alt="Show all tabs" />
</button>
<button id="addTab" class="add-tab-button" onclick="event_addTab()">+</button>
<!-- Tab list at the middle -->
<div class="tab-list-wrapper">
</div>
</div>
<div id="context-menu">
<div class="item" id="renameTab">Rename</div>
Expand Down
95 changes: 92 additions & 3 deletions frontends/web/scripts/TabsController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const STORAGE_VERSION = 2;

class TabsController {
constructor(tabsView, contentView, contextMenu) {
constructor(tabsView, tabsSearchView, contentView, contextMenu) {
this.tabs = [];
this.tabsView = tabsView;
this.tabsSearchView = tabsSearchView;
this.contentView = contentView;
this.contextMenu = contextMenu;
this.contextMenuOpenedForTab = null;
Expand Down Expand Up @@ -83,8 +84,13 @@ class TabsController {
#addDividers(tabId) {
let index = 0;
for (const { tabElement } of this.tabs) {
const dividerElement = this.#createDivider(index, tabId);
this.tabsView.insertBefore(dividerElement, tabElement);
// Only add divider before visible tabs
// This creates a better UX when searching tabs
// but dragging from LTR or RTL is inconsistent
if (tabElement.style.display !== "none") {
const dividerElement = this.#createDivider(index, tabId);
this.tabsView.insertBefore(dividerElement, tabElement);
}
index++;
}
this.tabsView.appendChild(this.#createDivider(index, tabId));
Expand Down Expand Up @@ -306,8 +312,61 @@ class TabsController {
}
});
};


this.tabsSearchView.addEventListener("input", (event) => {
// Hide context menu
realThis.contextMenuOpenedForTab = null;
realThis.contextMenu.classList.remove("visible");

setTimeout(() => {
realThis.onSearchTabs(event);
});
});

// TODO: maybe fetch that in a nicer way for class params
const tabsScrollBtn = document.getElementsByClassName("tab-expand-button")[0];
if (!tabsScrollBtn || !this.tabsView) return;

this.updateTabArrowVisibility = () => {
// If the content is wider than the visible area, show the button.
const overflowing = this.tabsView.scrollWidth > this.tabsView.clientWidth + 1;
const inMultiRow = this.tabsView.classList.contains("multirow");
tabsScrollBtn.style.display = overflowing | inMultiRow ? "" : "none";
};

// Initial update
setTimeout(this.updateTabArrowVisibility, 0);

// Update on window resize
window.addEventListener("resize", this.updateTabArrowVisibility);

// Observe changes to the tab list (tabs added/removed, style changes from search)
try {
const mo = new MutationObserver(() => this.updateTabArrowVisibility());
mo.observe(tabListWrapper, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
} catch (err) {
// MutationObserver shouldn't fail in modern browsers; fail silently.
}
}

toggleTabBarMode() {
const tabListWrapper = this.tabsView;
tabListWrapper.classList.toggle("multirow");
const iconElement = document.getElementsByClassName("tab-expand-button")[0].querySelector("img");
iconElement.setAttribute(
"expand-icon-state",
tabListWrapper.classList.contains("multirow") ? "up" : "down"
);
this.updateTabArrowVisibility();
}


removeCurrentTab() {
this.#removeTab(this.selectedTab);
}
Expand Down Expand Up @@ -362,6 +421,36 @@ class TabsController {
}
}

onSearchTabs(event) {
// Read the current query from the provided search input element.
const query = (event.target && event.target.value)
? event.target.value.toLowerCase()
: "";
const iconElement = this.tabsSearchView.querySelector(".tab-search-button img")

var has_matches = false;
this.tabs.forEach(({ name, tabElement }) => {
const matches = !query || name.toLowerCase().includes(query);
if (matches) {
has_matches = true;
}
tabElement.style.display = matches ? "" : "none";
});

// Update the search icon color based on whether there are matches.
if (!has_matches) {
iconElement.setAttribute('data-icon-state', "gray");
} else {
if (query) {
iconElement.setAttribute('data-icon-state', "green");
} else {
iconElement.setAttribute('data-icon-state', "white");
}
}
// Search results may have changed tab list size; update arrow visibility.
this.updateTabArrowVisibility();
}

#addEmptyTab() {
const emptyTab = this.addTab("untitled");
this.selectTab(emptyTab);
Expand Down
10 changes: 10 additions & 0 deletions frontends/web/scripts/commandPalette.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,16 @@ class CommandPalette {
handler: () => {
event_copyNodeText();
}
},
{
id: "ExpandTabBar",
title: "Expand tab bar to show all tabs or collapse it",
hotkey: "Ctrl+t",
icon: iconFor("upside_down_triangle"),
section: "Tabs",
handler: () => {
event_toggleTabBarMode();
}
}
];
}
Expand Down
12 changes: 10 additions & 2 deletions frontends/web/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,10 @@ function event_showChangelog() {
});
}

function event_toggleTabBarMode() {
tabsController.toggleTabBarMode();
}

function event_showManageTokenDialog(isFromAuthEvent = false) {
const currentToken = localStorage.getItem(LOCAL_STORAGE_TOKEN_KEY);
const isWindows = navigator.userAgent.includes("Windows NT");
Expand Down Expand Up @@ -701,7 +705,8 @@ function main() {

// Initiate tabs
const tabsController = new TabsController(
document.getElementsByClassName("tabs")[0],
document.getElementsByClassName("tab-list-wrapper")[0],
document.getElementsByClassName("tab-search")[0],
document.getElementsByClassName("view")[0],
document.getElementById("context-menu")
);
Expand Down Expand Up @@ -755,7 +760,7 @@ function initiateConnectionUrl() {

function initiateHotkeys() {
hotkeys(
"esc,ctrl+z,ctrl+shift+z,ctrl+y,ctrl+s,ctrl+alt+s,ctrl+o,ctrl+i,ctrl+alt+shift+i,ctrl+q,ctrl+f,ctrl+shift+f,ctrl+k,ctrl+e,ctrl+shift+q,ctrl+shift+p,delete,ctrl+delete,ctrl+c,home,ctrl+home,shift+`,/,shift+/,ctrl+shift+/,ctrl+a,1,2,3,4,5,6,7,8,9",
"esc,ctrl+z,ctrl+shift+z,ctrl+y,ctrl+s,ctrl+alt+s,ctrl+o,ctrl+i,ctrl+alt+shift+i,ctrl+q,ctrl+f,ctrl+shift+f,ctrl+k,ctrl+e,ctrl+shift+q,ctrl+shift+p,ctrl+t,delete,ctrl+delete,ctrl+c,home,ctrl+home,shift+`,/,shift+/,ctrl+shift+/,ctrl+a,1,2,3,4,5,6,7,8,9",
function (event, handler) {
if (!window.commandPalette.isOpen()) {
switch (handler.key) {
Expand Down Expand Up @@ -793,6 +798,9 @@ function initiateHotkeys() {
case "ctrl+shift+q":
event_addTextNode();
return false;
case "ctrl+t":
event_toggleTabBarMode();
return false;
case "delete":
event_delete();
return false;
Expand Down
141 changes: 137 additions & 4 deletions frontends/web/styles/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,157 @@ body {
background: #343434;
}

#addTab {
float: right;
.add-tab-button {
/* don't float when using flex layout for .tabs; keep as fixed-size control */
float: none;
order: 2;
flex: 0 0 auto;
align-self: flex-start;
height: 52px;
width: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* Style the tab */
.tabs {
overflow: hidden;
display: flex;
float: none;
order: 2;
flex: 0 0 auto;
background-color: var(--primary-color);
}

/* The container that holds the tab buttons. It will stay on a single row
and allow horizontal scrolling when there are too many tabs. */
.tab-list-wrapper {
order: 1;
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
flex: 1 1 auto;
}

/* When `.multirow` is applied the tab list is allowed to wrap into
multiple rows instead of remaining on a single horizontally-scrollable
row. */
.tab-list-wrapper.multirow {
flex-wrap: wrap;
overflow-x: visible;
}

/* Make sure individual buttons inside the wrapper don't float/wrap */
.tab-list-wrapper button {
float: none;
display: inline-flex;
}

.tab-list-wrapper.multirow button {
flex: 0 0 auto;
}

/* Collapsible search box inside the tabs area. It shows only the icon
when collapsed and expands to a fixed width when focused
*/
.tab-search {
order: 0;
display: inline-flex;
align-items: center;
background: transparent;
border-radius: 6px;
overflow: hidden;
width: 36px;
transition: width 200ms ease;
flex: 0 0 auto;
align-self: flex-start;
}
.tab-search:focus-within {
width: 200px;
}
.tab-search:hover {
width: 200px;
background-color: var(--primary-color-hover);
}

.tab-search-button img {
filter: none;
}

.tab-search-button img[data-icon-state="white"] {
filter: brightness(0) saturate(100%) invert(100%) sepia(11%) saturate(0%) hue-rotate(14deg) brightness(103%) contrast(100%)
}
.tab-search-button img[data-icon-state="gray"] {
filter: brightness(0) saturate(100%) invert(67%) sepia(82%) saturate(0%) hue-rotate(133deg) brightness(95%) contrast(93%)
}
.tab-search-button img[data-icon-state="green"] {
filter: brightness(0) saturate(100%) invert(92%) sepia(15%) saturate(461%) hue-rotate(46deg) brightness(102%) contrast(102%);
}

.tab-search-button {
width: 36px;
background: transparent;
border: none;
color: var(--text-color);
font-size: 16px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* Arrow button shown when tab list is scrollable */
.tab-expand-button {
float: none;
order: 2;
flex: 0 0 auto;
align-self: flex-start;
width: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tab-expand-button img {
display: block;
transform: none;
filter: brightness(0) saturate(100%) invert(100%) sepia(11%) saturate(0%) hue-rotate(14deg) brightness(103%) contrast(100%)
}
.tab-expand-button img[expand-icon-state="down"] {
transform: none;
}
.tab-expand-button img[expand-icon-state="up"] {
transform: scaleY(-1);;
}

.tab-search-input {
background: transparent;
border: none;
outline: none;
color: var(--text-color);
padding: 8px 10px;
font-size: 14px;
width: 100%;
min-width: 0;
}
.tab-search-input::placeholder {
color: rgba(255,255,255,0.7);
}

.tabs .divider {
background-color: inherit;
float: left;
/* Use flex sizing instead of float so the divider keeps its intended
dimensions inside the .tabs flex container. */
float: none;
border: white solid 2px;
outline: none;
cursor: grabbing;
height: 48px;
width: 20px;
min-width: 20px;
box-sizing: content-box;
flex: 0 0 20px; /* don't grow or shrink; keep fixed width */
align-self: stretch; /* allow it to span the container height if needed */
margin: 0 6px;
}

.tabs .divider.dragover {
Expand Down
Loading