Skip to content

Commit

Permalink
clean up additional button logic, add toggleiconbutton component
Browse files Browse the repository at this point in the history
  • Loading branch information
esimkowitz committed Dec 30, 2024
1 parent 4533c81 commit b0ed9d9
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 113 deletions.
4 changes: 3 additions & 1 deletion frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { ErrorBoundary } from "@/element/errorboundary";
import { IconButton } from "@/element/iconbutton";
import { IconButton, ToggleIconButton } from "@/element/iconbutton";
import { MagnifyIcon } from "@/element/magnify";
import { MenuButton } from "@/element/menubutton";
import { NodeModel } from "@/layout/index";
Expand Down Expand Up @@ -278,6 +278,8 @@ const BlockFrame_Header = ({
const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; preview: boolean }) => {
if (elem.elemtype == "iconbutton") {
return <IconButton decl={elem} className={clsx("block-frame-header-iconbutton", elem.className)} />;
} else if (elem.elemtype == "toggleiconbutton") {
return <ToggleIconButton decl={elem} className={clsx("block-frame-header-iconbutton", elem.className)} />;
} else if (elem.elemtype == "input") {
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} preview={preview} />;
} else if (elem.elemtype == "text") {
Expand Down
13 changes: 13 additions & 0 deletions frontend/app/element/iconbutton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,17 @@
cursor: default;
opacity: 0.45 !important;
}

&.toggle {
border-radius: 3px;
padding: 1px;
&.active {
opacity: 1;
border: 1px solid var(--accent-color);
padding: 0;
}
&:hover {
background: var(--highlight-bg-color);
}
}
}
35 changes: 34 additions & 1 deletion frontend/app/element/iconbutton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import { useLongClick } from "@/app/hook/useLongClick";
import { makeIconClass } from "@/util/util";
import clsx from "clsx";
import { forwardRef, memo, useRef } from "react";
import { atom, useAtom } from "jotai";
import { forwardRef, memo, useMemo, useRef } from "react";
import "./iconbutton.scss";

type IconButtonProps = { decl: IconButtonDecl; className?: string };
Expand All @@ -21,10 +22,42 @@ export const IconButton = memo(
"no-action": decl.noAction,
})}
title={decl.title}
aria-label={decl.title}
style={{ color: decl.iconColor ?? "inherit" }}
>
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true, { spin })} /> : decl.icon}
</button>
);
})
);

type ToggleIconButtonProps = { decl: ToggleIconButtonDecl; className?: string };

export const ToggleIconButton = memo(
forwardRef<HTMLButtonElement, ToggleIconButtonProps>(({ decl, className }, ref) => {
const activeAtom = useMemo(() => decl.active ?? atom(false), [decl.active]);
const [active, setActive] = useAtom(activeAtom);
ref = ref ?? useRef<HTMLButtonElement>(null);
const spin = decl.iconSpin ?? false;
const title = `${decl.title}${active ? " (Active)" : ""}`;
return (
<button
ref={ref}
className={clsx("wave-iconbutton", "toggle", className, decl.className, {
active,
disabled: decl.disabled,
"no-action": decl.noAction,
})}
title={title}
aria-label={title}
style={{ color: decl.iconColor ?? "inherit" }}
onPointerDown={() => {
console.log("active", active);
setActive(!active);
}}
>
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true, { spin })} /> : decl.icon}
</button>
);
})
);
12 changes: 11 additions & 1 deletion frontend/app/element/search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,23 @@
}
}

.right-buttons {
.right-buttons:not(:empty) {
display: flex;
gap: 5px;
padding-left: 5px;
border-left: 1px solid var(--modal-border-color);
button {
font-size: 12px;
}

&.additional {
gap: 2px;
button {
font-size: 10px;
i {
margin: auto 1px;
}
}
}
}
}
45 changes: 35 additions & 10 deletions frontend/app/element/search.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,35 @@ const meta: Meta<typeof Search> = {
export default meta;
type Story = StoryObj<typeof Popover>;

export const DefaultSearch: Story = {
export const Default: Story = {
render: (args) => {
const props = useSearch();
const setIsOpen = useSetAtom(props.isOpenAtom);
const setIsOpen = useSetAtom(props.isOpen);
useEffect(() => {
setIsOpen(true);
}, []);
return (
<div
className="viewbox"
ref={props.anchorRef as React.RefObject<HTMLDivElement>}
style={{
border: "2px solid black",
width: "100%",
height: "200px",
background: "var(--main-bg-color)",
}}
>
<Search {...args} {...props} />
</div>
);
},
args: {},
};

export const AdditionalButtons: Story = {
render: (args) => {
const props = useSearch({ regex: true, caseSensitive: true, wholeWord: true });
const setIsOpen = useSetAtom(props.isOpen);
useEffect(() => {
setIsOpen(true);
}, []);
Expand All @@ -44,8 +69,8 @@ export const DefaultSearch: Story = {
export const Results10: Story = {
render: (args) => {
const props = useSearch();
const setIsOpen = useSetAtom(props.isOpenAtom);
const setNumResults = useSetAtom(props.numResultsAtom);
const setIsOpen = useSetAtom(props.isOpen);
const setNumResults = useSetAtom(props.resultsCount);
useEffect(() => {
setIsOpen(true);
setNumResults(10);
Expand All @@ -71,9 +96,9 @@ export const Results10: Story = {
export const InputAndResults10: Story = {
render: (args) => {
const props = useSearch();
const setIsOpen = useSetAtom(props.isOpenAtom);
const setNumResults = useSetAtom(props.numResultsAtom);
const setSearch = useSetAtom(props.searchAtom);
const setIsOpen = useSetAtom(props.isOpen);
const setNumResults = useSetAtom(props.resultsCount);
const setSearch = useSetAtom(props.searchValue);
useEffect(() => {
setIsOpen(true);
setNumResults(10);
Expand All @@ -100,9 +125,9 @@ export const InputAndResults10: Story = {
export const LongInputAndResults10: Story = {
render: (args) => {
const props = useSearch();
const setIsOpen = useSetAtom(props.isOpenAtom);
const setNumResults = useSetAtom(props.numResultsAtom);
const setSearch = useSetAtom(props.searchAtom);
const setIsOpen = useSetAtom(props.isOpen);
const setNumResults = useSetAtom(props.resultsCount);
const setSearch = useSetAtom(props.searchValue);
useEffect(() => {
setIsOpen(true);
setNumResults(10);
Expand Down
82 changes: 62 additions & 20 deletions frontend/app/element/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { autoUpdate, FloatingPortal, Middleware, offset, useFloating } from "@fl
import clsx from "clsx";
import { atom, useAtom } from "jotai";
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
import { IconButton } from "./iconbutton";
import { IconButton, ToggleIconButton } from "./iconbutton";
import { Input } from "./input";
import "./search.scss";

Expand All @@ -16,21 +16,22 @@ type SearchProps = SearchAtoms & {
onSearch?: (search: string) => void;
onNext?: () => void;
onPrev?: () => void;
additionalButtons?: IconButtonDecl[];
};

const SearchComponent = ({
searchAtom,
indexAtom,
numResultsAtom,
isOpenAtom,
searchValue: searchAtom,
resultsIndex: indexAtom,
resultsCount: numResultsAtom,
regex: regexAtom,
caseSensitive: caseSensitiveAtom,
wholeWord: wholeWordAtom,
isOpen: isOpenAtom,
anchorRef,
offsetX = 10,
offsetY = 10,
onSearch,
onNext,
onPrev,
additionalButtons,
}: SearchProps) => {
const [isOpen, setIsOpen] = useAtom<boolean>(isOpenAtom);
const [search, setSearch] = useAtom<string>(searchAtom);
Expand Down Expand Up @@ -131,6 +132,31 @@ const SearchComponent = ({
click: () => setIsOpen(false),
};

const regexDecl: ToggleIconButtonDecl = regexAtom
? {
elemtype: "toggleiconbutton",
icon: "asterisk",
title: "Regex Search",
active: regexAtom,
}
: null;
const wholeWordDecl: ToggleIconButtonDecl = caseSensitiveAtom
? {
elemtype: "toggleiconbutton",
icon: "w",
title: "Whole Word",
active: wholeWordAtom,
}
: null;
const caseSensitiveDecl: ToggleIconButtonDecl = caseSensitiveAtom
? {
elemtype: "toggleiconbutton",
icon: "font-case",
title: "Case Sensitive",
active: caseSensitiveAtom,
}
: null;

return (
<>
{isOpen && (
Expand All @@ -150,13 +176,13 @@ const SearchComponent = ({
>
{index + 1}/{numResults}
</div>
{additionalButtons?.length && (
<div className="right-buttons">
{additionalButtons.map((decl, i) => (
<IconButton key={i} decl={decl} />
))}
</div>
)}

<div className="right-buttons additional">
{caseSensitiveDecl && <ToggleIconButton decl={caseSensitiveDecl} />}
{wholeWordDecl && <ToggleIconButton decl={wholeWordDecl} />}
{regexDecl && <ToggleIconButton decl={regexDecl} />}
</div>

<div className="right-buttons">
<IconButton decl={prevDecl} />
<IconButton decl={nextDecl} />
Expand All @@ -171,16 +197,32 @@ const SearchComponent = ({

export const Search = memo(SearchComponent) as typeof SearchComponent;

export function useSearch(anchorRef?: React.RefObject<HTMLElement>, viewModel?: ViewModel): SearchProps {
type SearchOptions = {
anchorRef?: React.RefObject<HTMLElement>;
viewModel?: ViewModel;
regex?: boolean;
caseSensitive?: boolean;
wholeWord?: boolean;
};

export function useSearch(options?: SearchOptions): SearchProps {
const searchAtoms: SearchAtoms = useMemo(
() => ({ searchAtom: atom(""), indexAtom: atom(0), numResultsAtom: atom(0), isOpenAtom: atom(false) }),
() => ({
searchValue: atom(""),
resultsIndex: atom(0),
resultsCount: atom(0),
isOpen: atom(false),
regex: options?.regex !== undefined ? atom(options.regex) : undefined,
caseSensitive: options?.caseSensitive !== undefined ? atom(options.caseSensitive) : undefined,
wholeWord: options?.wholeWord !== undefined ? atom(options.wholeWord) : undefined,
}),
[]
);
anchorRef ??= useRef(null);
const anchorRef = options?.anchorRef ?? useRef(null);
useEffect(() => {
if (viewModel) {
viewModel.searchAtoms = searchAtoms;
if (options?.viewModel) {
options.viewModel.searchAtoms = searchAtoms;
}
}, [viewModel]);
}, [options?.viewModel]);
return { ...searchAtoms, anchorRef };
}
6 changes: 3 additions & 3 deletions frontend/app/store/keymodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,16 +326,16 @@ function registerGlobalKeys() {
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
if (bcm.viewModel.searchAtoms) {
console.log("activateSearch2");
globalStore.set(bcm.viewModel.searchAtoms.isOpenAtom, true);
globalStore.set(bcm.viewModel.searchAtoms.isOpen, true);
return true;
}
return false;
}
function deactivateSearch(): boolean {
console.log("deactivateSearch");
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
if (bcm.viewModel.searchAtoms && globalStore.get(bcm.viewModel.searchAtoms.isOpenAtom)) {
globalStore.set(bcm.viewModel.searchAtoms.isOpenAtom, false);
if (bcm.viewModel.searchAtoms && globalStore.get(bcm.viewModel.searchAtoms.isOpen)) {
globalStore.set(bcm.viewModel.searchAtoms.isOpen, false);
return true;
}
return false;
Expand Down
Loading

0 comments on commit b0ed9d9

Please sign in to comment.