mirror of
https://github.com/nkcmr/HyperTab.git
synced 2026-04-23 17:28:31 +02:00
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
This commit is contained in:
@@ -261,6 +261,9 @@ const TabItemFavicon = styled.div<{ $dark: boolean }>`
|
|||||||
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;
|
||||||
`;
|
`;
|
||||||
@@ -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) => {
|
||||||
@@ -480,7 +583,7 @@ const Popup: FunctionComponent = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showCloseAction ? (
|
{showCloseAction ? (
|
||||||
<div
|
<TabItemCloseBox
|
||||||
title="Close Tab"
|
title="Close Tab"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -500,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} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user