Display mode options dropdown (#5923)

* Separate ZoomSlider into own component
* Turn ListViewOptions into dropdown
Also puts zoom slider in the dropdown
* Move ZoomSlider into separate file
* Add title
* Restyle slider
This commit is contained in:
WithoutPants 2025-06-13 11:45:10 +10:00 committed by GitHub
parent 574fd680c9
commit a145576f39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 223 additions and 110 deletions

View file

@ -1,21 +1,17 @@
import React, { useEffect } from "react"; import React, { useEffect, useRef, useState } from "react";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { import { Button, Dropdown, Overlay, Popover } from "react-bootstrap";
Button,
ButtonGroup,
Form,
OverlayTrigger,
Tooltip,
} from "react-bootstrap";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { import {
faChevronDown,
faList, faList,
faSquare, faSquare,
faTags, faTags,
faThLarge, faThLarge,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { ZoomSelect } from "./ZoomSlider";
interface IListViewOptionsProps { interface IListViewOptionsProps {
zoomIndex?: number; zoomIndex?: number;
@ -25,6 +21,38 @@ interface IListViewOptionsProps {
displayModeOptions: DisplayMode[]; displayModeOptions: DisplayMode[];
} }
function getIcon(option: DisplayMode) {
switch (option) {
case DisplayMode.Grid:
return faThLarge;
case DisplayMode.List:
return faList;
case DisplayMode.Wall:
return faSquare;
case DisplayMode.Tagger:
return faTags;
}
}
function getLabelId(option: DisplayMode) {
let displayModeId = "unknown";
switch (option) {
case DisplayMode.Grid:
displayModeId = "grid";
break;
case DisplayMode.List:
displayModeId = "list";
break;
case DisplayMode.Wall:
displayModeId = "wall";
break;
case DisplayMode.Tagger:
displayModeId = "tagger";
break;
}
return `display_mode.${displayModeId}`;
}
export const ListViewOptions: React.FC<IListViewOptionsProps> = ({ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
zoomIndex, zoomIndex,
onSetZoom, onSetZoom,
@ -37,6 +65,9 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
const intl = useIntl(); const intl = useIntl();
const overlayTarget = useRef(null);
const [showOptions, setShowOptions] = useState(false);
useEffect(() => { useEffect(() => {
Mousetrap.bind("v g", () => { Mousetrap.bind("v g", () => {
if (displayModeOptions.includes(DisplayMode.Grid)) { if (displayModeOptions.includes(DisplayMode.Grid)) {
@ -53,82 +84,16 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
onSetDisplayMode(DisplayMode.Wall); onSetDisplayMode(DisplayMode.Wall);
} }
}); });
Mousetrap.bind("+", () => {
if (onSetZoom && zoomIndex !== undefined && zoomIndex < maxZoom) {
onSetZoom(zoomIndex + 1);
}
});
Mousetrap.bind("-", () => {
if (onSetZoom && zoomIndex !== undefined && zoomIndex > minZoom) {
onSetZoom(zoomIndex - 1);
}
});
return () => { return () => {
Mousetrap.unbind("v g"); Mousetrap.unbind("v g");
Mousetrap.unbind("v l"); Mousetrap.unbind("v l");
Mousetrap.unbind("v w"); Mousetrap.unbind("v w");
Mousetrap.unbind("+");
Mousetrap.unbind("-");
}; };
}); });
function maybeRenderDisplayModeOptions() { function getLabel(option: DisplayMode) {
function getIcon(option: DisplayMode) { return intl.formatMessage({ id: getLabelId(option) });
switch (option) {
case DisplayMode.Grid:
return faThLarge;
case DisplayMode.List:
return faList;
case DisplayMode.Wall:
return faSquare;
case DisplayMode.Tagger:
return faTags;
}
}
function getLabel(option: DisplayMode) {
let displayModeId = "unknown";
switch (option) {
case DisplayMode.Grid:
displayModeId = "grid";
break;
case DisplayMode.List:
displayModeId = "list";
break;
case DisplayMode.Wall:
displayModeId = "wall";
break;
case DisplayMode.Tagger:
displayModeId = "tagger";
break;
}
return intl.formatMessage({ id: `display_mode.${displayModeId}` });
}
if (displayModeOptions.length < 2) {
return;
}
return (
<ButtonGroup className="ml-2">
{displayModeOptions.map((option) => (
<OverlayTrigger
key={option}
overlay={
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
}
>
<Button
variant="secondary"
active={displayMode === option}
onClick={() => onSetDisplayMode(option)}
>
<Icon icon={getIcon(option)} />
</Button>
</OverlayTrigger>
))}
</ButtonGroup>
);
} }
function onChangeZoom(v: number) { function onChangeZoom(v: number) {
@ -137,29 +102,60 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
} }
} }
function maybeRenderZoom() {
if (onSetZoom && displayMode === DisplayMode.Grid) {
return (
<div className="ml-2 d-none d-sm-inline-flex">
<Form.Control
className="zoom-slider ml-1"
type="range"
min={minZoom}
max={maxZoom}
value={zoomIndex}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
}
/>
</div>
);
}
}
return ( return (
<> <>
{maybeRenderDisplayModeOptions()} <Button
{maybeRenderZoom()} className="display-mode-select"
ref={overlayTarget}
variant="secondary"
title={intl.formatMessage(
{ id: "display_mode.label_current" },
{ current: getLabel(displayMode) }
)}
onClick={() => setShowOptions(!showOptions)}
>
<Icon icon={getIcon(displayMode)} />
<Icon size="xs" icon={faChevronDown} />
</Button>
<Overlay
target={overlayTarget.current}
show={showOptions}
placement="bottom"
rootClose
onHide={() => setShowOptions(false)}
>
{({ placement, arrowProps, show: _show, ...props }) => (
<div className="popover" {...props} style={{ ...props.style }}>
<Popover.Content className="display-mode-popover">
<div className="display-mode-menu">
{onSetZoom &&
zoomIndex !== undefined &&
displayMode === DisplayMode.Grid ? (
<div className="zoom-slider-container">
<ZoomSelect
minZoom={minZoom}
maxZoom={maxZoom}
zoomIndex={zoomIndex}
onChangeZoom={onChangeZoom}
/>
</div>
) : null}
{displayModeOptions.map((option) => (
<Dropdown.Item
key={option}
active={displayMode === option}
onClick={() => {
onSetDisplayMode(option);
}}
>
<Icon icon={getIcon(option)} /> {getLabel(option)}
</Dropdown.Item>
))}
</div>
</Popover.Content>
</div>
)}
</Overlay>
</> </>
); );
}; };

View file

@ -0,0 +1,50 @@
import React, { useEffect } from "react";
import Mousetrap from "mousetrap";
import { Form } from "react-bootstrap";
export interface IZoomSelectProps {
minZoom: number;
maxZoom: number;
zoomIndex: number;
onChangeZoom: (v: number) => void;
}
export const ZoomSelect: React.FC<IZoomSelectProps> = ({
minZoom,
maxZoom,
zoomIndex,
onChangeZoom,
}) => {
useEffect(() => {
Mousetrap.bind("+", () => {
if (zoomIndex !== undefined && zoomIndex < maxZoom) {
onChangeZoom(zoomIndex + 1);
}
});
Mousetrap.bind("-", () => {
if (zoomIndex !== undefined && zoomIndex > minZoom) {
onChangeZoom(zoomIndex - 1);
}
});
return () => {
Mousetrap.unbind("+");
Mousetrap.unbind("-");
};
});
return (
<Form.Control
className="zoom-slider"
type="range"
min={minZoom}
max={maxZoom}
value={zoomIndex}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChangeZoom(Number.parseInt(e.currentTarget.value, 10));
e.preventDefault();
e.stopPropagation();
}}
/>
);
};

View file

@ -34,6 +34,68 @@
text-align: center; text-align: center;
} }
.display-mode-select {
padding-left: 0.375rem;
padding-right: 0.375rem;
text-wrap: nowrap;
> svg:first-child {
margin-right: 0;
}
}
.display-mode-menu {
.dropdown-item {
color: #f5f8fa;
font-size: 1rem;
&:hover {
background-color: rgba(138, 155, 168, 0.15);
cursor: pointer;
}
}
.zoom-slider-container {
display: flex;
justify-content: center;
margin-bottom: 0.5rem;
min-height: 1rem;
padding-bottom: 0.5rem;
padding-top: 0.25rem;
}
.zoom-slider {
&::-webkit-slider-thumb {
background-color: $primary;
}
&::-webkit-slider-runnable-track {
background-color: $body-bg;
}
&:focus::-webkit-slider-runnable-track {
background-color: lighten($body-bg, 5%);
}
&::-moz-range-thumb {
background-color: $primary;
}
&::-moz-range-track {
background-color: $body-bg;
}
&:focus::-moz-range-track {
background-color: lighten($body-bg, 5%);
}
}
}
.display-mode-popover {
padding-left: 0;
padding-right: 0;
}
input[type="range"].zoom-slider { input[type="range"].zoom-slider {
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -810,6 +872,10 @@ input[type="range"].zoom-slider {
justify-content: center; justify-content: center;
row-gap: 0.5rem; row-gap: 0.5rem;
} }
.btn.display-mode-select {
margin-left: 0.5rem;
}
} }
.sidebar-pane .filtered-list-toolbar { .sidebar-pane .filtered-list-toolbar {

View file

@ -995,6 +995,7 @@
"disambiguation": "Disambiguation", "disambiguation": "Disambiguation",
"display_mode": { "display_mode": {
"grid": "Grid", "grid": "Grid",
"label_current": "Display Mode: {current}",
"list": "List", "list": "List",
"tagger": "Tagger", "tagger": "Tagger",
"unknown": "Unknown", "unknown": "Unknown",

View file

@ -15,7 +15,7 @@ input[type="range"] {
&::-webkit-slider-runnable-track { &::-webkit-slider-runnable-track {
animate: 0.2s; animate: 0.2s;
background: #137cbd; background: $primary;
border: 0 solid #000101; border: 0 solid #000101;
border-radius: 25px; border-radius: 25px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
@ -28,12 +28,12 @@ input[type="range"] {
-webkit-appearance: none; -webkit-appearance: none;
background: #394b59; background: #394b59;
border: 0 solid #000; border: 0 solid #000;
border-radius: 5px; border-radius: 10px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
cursor: pointer; cursor: pointer;
height: 16px; height: 15px;
margin-top: -5px; margin-top: -4px;
width: 16px; width: 15px;
} }
&:focus::-webkit-slider-runnable-track { &:focus::-webkit-slider-runnable-track {
@ -42,7 +42,7 @@ input[type="range"] {
&::-moz-range-track { &::-moz-range-track {
animate: 0.2s; animate: 0.2s;
background: #137cbd; background: $primary;
border: 0 solid #000101; border: 0 solid #000101;
border-radius: 25px; border-radius: 25px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
@ -54,11 +54,11 @@ input[type="range"] {
&::-moz-range-thumb { &::-moz-range-thumb {
background: #394b59; background: #394b59;
border: 0 solid #000; border: 0 solid #000;
border-radius: 5px; border-radius: 10px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
cursor: pointer; cursor: pointer;
height: 16px; height: 15px;
width: 16px; width: 15px;
} }
&::-ms-track { &::-ms-track {
@ -72,14 +72,14 @@ input[type="range"] {
} }
&::-ms-fill-lower { &::-ms-fill-lower {
background: #137cbd; background: $primary;
border: 0 solid #000101; border: 0 solid #000101;
border-radius: 50px; border-radius: 50px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
} }
&::-ms-fill-upper { &::-ms-fill-upper {
background: #137cbd; background: $primary;
border: 0 solid #000101; border: 0 solid #000101;
border-radius: 50px; border-radius: 50px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
@ -88,11 +88,11 @@ input[type="range"] {
&::-ms-thumb { &::-ms-thumb {
background: #394b59; background: #394b59;
border: 0 solid #000; border: 0 solid #000;
border-radius: 5px; border-radius: 10px;
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
cursor: pointer; cursor: pointer;
height: 16px; height: 16px;
margin-top: 1px; margin-top: 2px;
width: 16px; width: 16px;
} }