Skip to content

Commit

Permalink
replace TagInput
Browse files Browse the repository at this point in the history
- replace evergreen-ui's TagInput with a custom TagInput
- fix issue where 2-3 tags in search would cause the TagInput to overflow the header
- add a 'ghost' style to tag input on edit page to get a slightly cleaner look
  • Loading branch information
cloverich committed Sep 9, 2024
1 parent 8efd03b commit 6c59924
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 74 deletions.
142 changes: 142 additions & 0 deletions src/components/TagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { cn } from "@udecode/cn";
import { observable } from "mobx";
import { observer } from "mobx-react-lite";
import * as React from "react";

// todo(chris): Refactor this to accept a list of options / details dynanmically
const availableTags = ["in:", "tag:", "title:", "text:", "before:"];

interface TagInputProps {
tokens: string[];
onAdd: (token: string) => void;
onRemove: (token: string) => void;
/** Whether to show the dropdown on focus */
dropdownEnabled?: boolean;
/** placeholder text */
placeholder?: string;
/** When true, hide the borders / disable padding */
ghost?: boolean;
}

const TagInput = observer((props: TagInputProps) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const [dropdown, _] = React.useState(observable({ open: false }));

// Close the typeahead menu when clicking outside of the dropdown
React.useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
dropdown.open = false;
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

return (
<div
className={cn(
"flex w-0 max-w-full flex-grow flex-col rounded-sm border bg-background text-xs drag-none",
props.ghost && "border-none",
)}
ref={containerRef}
onClick={() => inputRef.current?.focus()}
>
<div
className={cn(
"flex flex-grow items-center p-1.5",
props.ghost && "p-0",
)}
>
{props.tokens.map((token, idx) => (
<Tag key={idx} token={token} remove={props.onRemove} />
))}
<input
ref={inputRef}
className="w-0 min-w-8 flex-shrink flex-grow outline-none"
type="text"
placeholder={props.tokens.length ? "" : props.placeholder}
onKeyDown={(e) => {
if (e.key === "Backspace" && e.currentTarget.value === "") {
// remove the last search token, if any
if (props.tokens.length) {
props.onRemove(props.tokens[props.tokens.length - 1]);
setTimeout(() => inputRef.current?.focus(), 0); // Refocus the input
}
}

if (e.key === "Enter" && e.currentTarget.value.trim() !== "") {
props.onAdd(e.currentTarget.value.trim()); // Add the token
e.currentTarget.value = ""; // Clear input

// Unfocus and close the dropdown; after entering a tag, the user
// likely wants to view the search results
// e.currentTarget.blur();
// dropdown.open = false;
}

// I'm angry, get me out of here! (close dropdown)
if (e.key === "Escape") {
e.currentTarget.value = "";
e.currentTarget.blur();
dropdown.open = false;
}
}}
onFocus={() => (dropdown.open = true)}
onInput={() => (dropdown.open = true)} // open menu anytime user types
/>
</div>
<div className="relative">
{props.dropdownEnabled && dropdown.open && (
<div className="absolute left-0 top-1 z-10 mt-2 w-full border bg-white shadow-md">
{availableTags.slice(0, 5).map((tag, idx) => (
<div
key={idx}
className="flex cursor-pointer justify-between p-2 hover:bg-gray-200"
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur
inputRef.current!.value = tag; // Set input to tag
}}
>
<span>{tag}</span>
<span className="text-gray-400">
{tag === "in:" && "Filter to specific journal"}
{tag === "tag:" && "Filter to specific tag"}
{tag === "title:" && "Filter by title"}
{tag === "text:" && "Search body text"}
{tag === "before:" &&
"Filter to notes before date (YYYY-MM-DD)"}
</span>
</div>
))}
</div>
)}
</div>
</div>
);
});

export default TagInput;

interface TagProps {
token: string;
remove: (token: string) => void;
}

const Tag = ({ token, remove }: TagProps) => {
return (
<span className="mr-2 flex flex-shrink items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-sm border border-slate-800 bg-violet-200 px-1 py-0.5 text-xs text-slate-600">
<span className="flex-shrink overflow-hidden text-ellipsis">{token}</span>
<button
className="text-grey-400 ml-1 flex-shrink-0"
onClick={() => remove(token)}
>
×
</button>
</span>
);
};
5 changes: 3 additions & 2 deletions src/views/documents/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SheetTrigger } from "../../components/Sidesheet";
import Titlebar from "../../titlebar/macos";
import * as Base from "../layout";
import { SearchStore } from "./SearchStore";
import SearchDocuments from "./search";
import { SearchInput } from "./search";
import JournalSelectionSidebar from "./sidebar/Sidebar";

interface Props {
Expand Down Expand Up @@ -55,7 +55,8 @@ export function Layout(props: Props) {
Create new note
</IconButton>

<SearchDocuments store={props.store} />
<SearchInput />

<IconButton
backgroundColor="transparent"
border="none"
Expand Down
50 changes: 10 additions & 40 deletions src/views/documents/search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,18 @@
import { TagInput } from "evergreen-ui";
import { observer } from "mobx-react-lite";
import React from "react";
import { SearchStore } from "../SearchStore";
import TagInput from "../../../components/TagInput";
import { useSearchStore } from "../SearchStore";

interface Props {
store: SearchStore;
}

/**
* SearchDocuments is a component that provides a search input for searching documents.
*/
const SearchDocuments = (props: Props) => {
function onRemove(tag: string | React.ReactNode, idx: number) {
if (typeof tag !== "string") return;
props.store.removeToken(tag);
}

function onAdd(tokens: string[]) {
if (tokens.length > 1) {
// https://evergreen.segment.com/components/tag-input
// Documents say this is single value, Type says array
// Testing says array but with only one value... unsure how multiple
// values end up in the array.
console.warn(
"TagInput.onAdd called with > 1 token? ",
tokens,
"ignoring extra tokens",
);
}

const token = tokens[0];
props.store.addToken(token);
}
export const SearchInput = observer(() => {
const searchStore = useSearchStore()!;

return (
<TagInput
className="drag-none"
flexGrow={1}
inputProps={{ placeholder: "Search journals" }}
values={props.store.searchTokens}
onAdd={onAdd}
onRemove={onRemove}
tokens={searchStore.searchTokens}
onAdd={(token) => searchStore.addToken(token)}
onRemove={(token) => searchStore.removeToken(token)}
placeholder="Search notes"
dropdownEnabled={true}
/>
);
};

export default observer(SearchDocuments);
});
12 changes: 0 additions & 12 deletions src/views/documents/search/loading.tsx

This file was deleted.

28 changes: 8 additions & 20 deletions src/views/edit/FrontMatter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Menu, Popover, Position, TagInput } from "evergreen-ui";
import { Menu, Popover, Position } from "evergreen-ui";
import { observer } from "mobx-react-lite";
import React from "react";
import { DayPicker } from "react-day-picker";
import TagInput from "../../components/TagInput";
import { JournalResponse } from "../../preload/client/journals";
import { TagTokenParser } from "../documents/search/parsers/tag";
import { EditableDocument } from "./EditableDocument";
Expand All @@ -14,20 +15,8 @@ const FrontMatter = observer(
document: EditableDocument;
journals: JournalResponse[];
}) => {
function onAddTag(tokens: string[]) {
if (tokens.length > 1) {
// https://evergreen.segment.com/components/tag-input
// Documents say this is single value, Type says array
// Testing says array but with only one value... unsure how multiple
// values end up in the array.
console.warn(
"TagInput.onAdd called with > 1 token? ",
tokens,
"ignoring extra tokens",
);
}

let tag = new TagTokenParser().parse(tokens[0])?.value;
function onAddTag(token: string) {
let tag = new TagTokenParser().parse(token)?.value;
if (!tag) return;

if (!document.tags.includes(tag)) {
Expand All @@ -36,8 +25,7 @@ const FrontMatter = observer(
}
}

function onRemoveTag(tag: string | React.ReactNode, idx: number) {
if (typeof tag !== "string") return;
function onRemoveTag(tag: string) {
document.tags = document.tags.filter((t) => t !== tag);
document.save();
}
Expand Down Expand Up @@ -151,11 +139,11 @@ const FrontMatter = observer(
{/* Tags */}
<div className="-mt-2 mb-4 flex justify-start pl-0.5 text-sm">
<TagInput
flexGrow={1}
inputProps={{ placeholder: "Document tags" }}
values={document.tags}
tokens={document.tags}
onAdd={onAddTag}
onRemove={onRemoveTag}
placeholder="Add tags"
ghost={true}
/>
</div>
</>
Expand Down

0 comments on commit 6c59924

Please sign in to comment.