diff --git a/src/popup/main.tsx b/src/popup/main.tsx index f47a7f6..dd0f3e5 100644 --- a/src/popup/main.tsx +++ b/src/popup/main.tsx @@ -261,6 +261,9 @@ const TabItemFavicon = styled.div<{ $dark: boolean }>` padding: 7px; flex-shrink: 0; `; +const TabItemCloseBox = styled.div` + cursor: pointer; +`; const TabItemMain = styled.div` width: ${TAB_ITEM_MAIN_WIDTH}px; `; @@ -298,6 +301,73 @@ const SearchInput = styled.input` 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 = { + 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 { + return new Map( + 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 darkMode = useDarkMode(); const [tabSelector, setTabSelector] = useState(0); @@ -327,7 +397,15 @@ const Popup: FunctionComponent = () => { }, [setTabSelector, searchQuery]); const searchIndex = useMemo(() => { const result = new Fuse(tabs, { - keys: ["title", "url"], + keys: [ + "title", + { + name: "hostname", + getFn(obj) { + return obj.url ? hostname(obj.url) : ""; + }, + }, + ], includeMatches: true, }); 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 => ({ + item: t, + refIndex: i, + }) + ); + } return searchIndex.search(searchQuery); }, [tabs, searchIndex, searchQuery]); @@ -409,7 +505,14 @@ const Popup: FunctionComponent = () => { }, [selectedTabEle.current]); const [tabHover, setTabHover] = useState(null); - + const inputRef = useRef(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 (
@@ -428,8 +531,8 @@ const Popup: FunctionComponent = () => { { @@ -480,7 +583,7 @@ const Popup: FunctionComponent = () => { }} > {showCloseAction ? ( -
{ e.stopPropagation(); @@ -500,7 +603,7 @@ const Popup: FunctionComponent = () => { > -
+ ) : ( )}