2 Commits

Author SHA1 Message Date
nick comer 4f2db92534 feat: experiment with more structured query pattern
fix: change to more robust "autofocus" mechanism to fix issue where popup would
show and not be automatically focused on the input
2024-05-10 10:09:51 -04:00
nick comer 3463d25fd0 fix regression in dark mode styling 2024-05-05 21:15:27 -04:00
+115 -11
View File
@@ -249,11 +249,11 @@ const TabItem = styled.div<{ $selected: boolean; $dark: boolean }>`
gap: ${TAB_ITEM_FLEX_GAP}px; gap: ${TAB_ITEM_FLEX_GAP}px;
align-items: center; align-items: center;
&:hover { &:hover {
background-color: #efefef; background-color: ${(props) => (props.$dark ? "#4e4e4e" : "#efefef")};
} }
`; `;
const TabItemFavicon = styled.div` const TabItemFavicon = styled.div<{ $dark: boolean }>`
background-color: #d9d9d9; background-color: ${(props) => (props.$dark ? "#6e6e6e" : "#d9d9d9")};
border-radius: 5px; border-radius: 5px;
padding: 0.5em; padding: 0.5em;
width: ${TAB_ITEM_FAVICON_SIZE}px; width: ${TAB_ITEM_FAVICON_SIZE}px;
@@ -261,6 +261,9 @@ const TabItemFavicon = styled.div`
padding: 7px; padding: 7px;
flex-shrink: 0; flex-shrink: 0;
`; `;
const TabItemCloseBox = styled.div`
cursor: pointer;
`;
const TabItemMain = styled.div` const TabItemMain = styled.div`
width: ${TAB_ITEM_MAIN_WIDTH}px; width: ${TAB_ITEM_MAIN_WIDTH}px;
`; `;
@@ -271,8 +274,8 @@ const TabItemMainTitle = styled.div<{ $dark: boolean }>`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
`; `;
const TabItemMainHostname = styled.div` const TabItemMainHostname = styled.div<{ $dark: boolean }>`
color: #6e6e6e; color: ${(props) => (props.$dark ? "#a9a9a9" : "#6e6e6e")};
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -298,6 +301,73 @@ const SearchInput = styled.input`
font-size: 1.2em; font-size: 1.2em;
`; `;
type searchField = {
aliases?: string[];
filterValues: (value: string | null) => boolean;
evaluate: (t: chrome.tabs.Tab, value: string | null) => boolean;
};
const searchFields: Record<string, searchField> = {
sys: {
filterValues(value) {
return ["true", "false"].includes(value?.toLowerCase() ?? "");
},
evaluate(t, value) {
if (!t.url) {
return value === "true";
}
const ishttp = !!t.url.match(/^https?:\/\//i);
return ishttp !== (value === "true");
},
},
pinned: {
filterValues(value) {
return ["true", "false"].includes(value?.toLowerCase() ?? "");
},
evaluate(t, value) {
return t.pinned === (value === "true");
},
},
hostname: {
aliases: ["domain"],
filterValues(value) {
return true;
},
evaluate(t, value) {
if (!t.url) {
return false;
}
return hostname(t.url).includes(value ?? "");
},
},
} as const;
function getSearchField(key: string): searchField | null {
return key in searchFields
? searchFields[key]
: Object.values(searchFields).find((sf) => {
return sf.aliases?.includes(key) ?? false;
}) ?? null;
}
function structuredQuery(query: string): Map<string, string | null> {
return new Map<string, string | null>(
query
.split(/\s+/g)
.map((pair): [string, string | null] => {
const [key, value] = pair.split(":", 2);
return [key, value ?? null];
})
.filter(([key, value]) => {
const sf = getSearchField(key);
if (!sf) {
return false;
}
return sf.filterValues(value);
})
);
}
const Popup: FunctionComponent = () => { const Popup: FunctionComponent = () => {
const darkMode = useDarkMode(); const darkMode = useDarkMode();
const [tabSelector, setTabSelector] = useState(0); const [tabSelector, setTabSelector] = useState(0);
@@ -327,7 +397,15 @@ const Popup: FunctionComponent = () => {
}, [setTabSelector, searchQuery]); }, [setTabSelector, searchQuery]);
const searchIndex = useMemo(() => { const searchIndex = useMemo(() => {
const result = new Fuse(tabs, { const result = new Fuse(tabs, {
keys: ["title", "url"], keys: [
"title",
{
name: "hostname",
getFn(obj) {
return obj.url ? hostname(obj.url) : "";
},
},
],
includeMatches: true, includeMatches: true,
}); });
return result; return result;
@@ -341,6 +419,24 @@ const Popup: FunctionComponent = () => {
}) })
); );
} }
const sq = structuredQuery(searchQuery);
if (sq.size > 0) {
return tabs
.filter((tab) => {
for (let [key, value] of sq.entries()) {
if (!getSearchField(key)!.evaluate(tab, value)) {
return false;
}
}
return true;
})
.map(
(t, i): FuseResult<chrome.tabs.Tab> => ({
item: t,
refIndex: i,
})
);
}
return searchIndex.search(searchQuery); return searchIndex.search(searchQuery);
}, [tabs, searchIndex, searchQuery]); }, [tabs, searchIndex, searchQuery]);
@@ -409,7 +505,14 @@ const Popup: FunctionComponent = () => {
}, [selectedTabEle.current]); }, [selectedTabEle.current]);
const [tabHover, setTabHover] = useState<number | null>(null); const [tabHover, setTabHover] = useState<number | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
requestIdleCallback(() => {
// this is more reliable than autoFocus attribute. sometimes autoFocus
// would let the popup open and then input would not be focused.
inputRef.current?.focus();
});
}, [inputRef.current]);
return ( return (
<div> <div>
<SearchContainer> <SearchContainer>
@@ -428,8 +531,8 @@ const Popup: FunctionComponent = () => {
</SearchIconLeftContainer> </SearchIconLeftContainer>
<SearchInputRightContainer> <SearchInputRightContainer>
<SearchInput <SearchInput
ref={inputRef}
type="text" type="text"
autoFocus
placeholder={t("ui_search_tabs")} placeholder={t("ui_search_tabs")}
value={searchQuery} value={searchQuery}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -471,6 +574,7 @@ const Popup: FunctionComponent = () => {
ref={i === tabSelector ? selectedTabEle : undefined} ref={i === tabSelector ? selectedTabEle : undefined}
> >
<TabItemFavicon <TabItemFavicon
$dark={darkMode}
onMouseEnter={() => { onMouseEnter={() => {
setTabHover(t.item.id!); setTabHover(t.item.id!);
}} }}
@@ -479,7 +583,7 @@ const Popup: FunctionComponent = () => {
}} }}
> >
{showCloseAction ? ( {showCloseAction ? (
<div <TabItemCloseBox
title="Close Tab" title="Close Tab"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -499,7 +603,7 @@ const Popup: FunctionComponent = () => {
> >
<path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm4.151 17.943l-4.143-4.102-4.117 4.159-1.833-1.833 4.104-4.157-4.162-4.119 1.833-1.833 4.155 4.102 4.106-4.16 1.849 1.849-4.1 4.141 4.157 4.104-1.849 1.849z" /> <path d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm4.151 17.943l-4.143-4.102-4.117 4.159-1.833-1.833 4.104-4.157-4.162-4.119 1.833-1.833 4.155 4.102 4.106-4.16 1.849 1.849-4.1 4.141 4.157 4.104-1.849 1.849z" />
</svg> </svg>
</div> </TabItemCloseBox>
) : ( ) : (
<img width={16} height={16} src={favicURL} /> <img width={16} height={16} src={favicURL} />
)} )}
@@ -511,7 +615,7 @@ const Popup: FunctionComponent = () => {
match={t.matches?.find((m) => m.key === "title")} match={t.matches?.find((m) => m.key === "title")}
/> />
</TabItemMainTitle> </TabItemMainTitle>
<TabItemMainHostname> <TabItemMainHostname $dark={darkMode}>
{t.item.url ? hostname(t.item.url) : ""} {t.item.url ? hostname(t.item.url) : ""}
</TabItemMainHostname> </TabItemMainHostname>
</TabItemMain> </TabItemMain>