-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Implemented keybind
module to handle keyboard shortcuts
#186
Conversation
Drafting as my main branch was out of sync so this needs merge help but will still take feedback in the meantime @mikkeldenker! |
keybind
module to handle keyboard shortcutskeybind
module to handle keyboard shortcuts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functionality seems to be absolutely spot-on, but we need to make some changes for maintainability. In general, we should avoid using query selectors and instead give references to binded elements to the relevant functions. This way, it's very easy to see directly in the code which components depend on eachother and avoid them coming out of sync.
I think you can bind multiple results in search/+page.svelte
like this
let resultElems: Result[] = [];
...
{#each results.webpages as webpage, resultIndex (`${query}-${webpage.url}`)}
<div animate:flip={{ duration: 150 }}>
<Result
bind:this={resultElems[resultIndex]}
{webpage}
{resultIndex}
on:modal={openSearchModal(webpage)}
/>
</div>
{/each}
I know there is a ton of information here, but please feel free to ask if anything is unclear.
frontend/src/lib/keybind.ts
Outdated
private keyEnumFromString(str: string): Keys | undefined { | ||
for (const [_, key] of Object.entries(Keys)) { | ||
if (key == str) return key; | ||
} | ||
|
||
return undefined; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When Key
is a type, this can be written as
private keyFromString(str: string): Key | undefined {
return Keys.find((key) => key === str);
}
frontend/src/lib/keybind.ts
Outdated
/** | ||
* Used to indicate what direction {@link navigateResults} should go (up/down) | ||
*/ | ||
type Direction = 1 | -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type Direction = "up" | "down";
is a bit more descriptive. The type is also pretty self-explanatory and the TsDoc string therefore only clutters it a bit and could be removed
frontend/src/lib/keybind.ts
Outdated
const openResult = (e: KeyboardEvent) => { | ||
if ((e.target as HTMLElement).className.includes('result-main-link')) { | ||
const resultLink = e.target as HTMLAnchorElement; | ||
window.location.href = resultLink.href; | ||
} else { | ||
const results = document.getElementsByClassName('result-main-link'); | ||
if (results.length > 0) { | ||
window.location.href = (results[0] as HTMLAnchorElement).href; | ||
} | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be written as
const openResult = (_e: KeyboardEvent, refs: Refs) => {
const focusedResult: Result | undefined = refs.results.find((result) => result.hasFocus());
if (focusedResult) {
focusedResult.open();
}
};
with related .open()
and .hasFocus()
functions in Result.svelte
frontend/src/lib/keybind.ts
Outdated
const openResultInNewTab = (e: KeyboardEvent) => { | ||
// Ensure target event is a result url | ||
if ((e.target as HTMLElement).className.includes('result-main-link')) { | ||
const resultLink = e.target as HTMLAnchorElement; | ||
window.open(resultLink.href, '_blank', 'noopener'); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be written as
const openResultInNewTab = (_e: KeyboardEvent, refs: Refs) => {
const focusedResult: Result | undefined = refs.results.find((result) => result.hasFocus());
if (focusedResult) {
focusedResult.openInNewTab();
}
}
with related .openResultInNewTab()
and .hasFocus()
functions in Result.svelte
frontend/src/lib/keybind.ts
Outdated
const msLink = document.getElementById('misspell-link'); | ||
if (msLink) window.location.href = (msLink as HTMLAnchorElement).href; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use Refs
with a binding for the mispell link added to the type instead of .getElementById
and could then also use a .open()
method
frontend/src/lib/keybind.ts
Outdated
// Ensure target event is a result url | ||
if ((e.target as HTMLElement).className.includes('result-main-link')) { | ||
const searchBar = document.getElementById('searchbar') as HTMLInputElement; | ||
if (searchBar) { | ||
// Parse the result url, pull out the hostname then resubmit search | ||
const resultLink = new URL((e.target as HTMLAnchorElement)?.href); | ||
searchBar.value += ` site:${resultLink.hostname}`; | ||
searchBar.form?.submit(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should not directly reference the dom elements but instead call related .userQuery()
and search(`${query} site:${resultLink.hostname}`)
methods. ResultLink.svelte
should also have a .url()
method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you expand on .userQuery()
& search(...)
? I couldn't find any userQuery
methods anywhere and was just going to pull it from PageData
and pass it in a wrapper. As for search
I was going to use this if that's what you were referring to. Wasn't sure if these are existing need to be built out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I think it would be easiest to create those as new methods in Searchbar.svelte
export const userQuery = () => lastRealQuery;
export const search = (q: string) => {
if (formElem && inputElem) {
inputElem.value = q;
formElem.submit();
}
};
with an added formElem
bind
let formElem: HTMLFormElement | undefined;
...
<form
action="/search"
class="flex w-full justify-center"
id="searchbar-form"
bind:this={formElem}
method={$postSearchStore ? 'POST' : 'GET'}
>
The search function in search.ts
effectively just calls the api and returns the results. Calling the search function in Searchbar.svelte
would ensure that the user interface gets updated correctly with the new results.
domainSearch
can then be something like
const domainSearch = (_e: KeyboardEvent, refs: Refs) => {
const focusedResult: Result | undefined = refs.results.find((result) => result.hasFocus());
if (refs.searchbar && focusedResult) {
const query: string = refs.searchbar.userQuery();
const url: string = focusedResult.getMainLink().url();
const domain = new URL(url).hostname;
const domainQuery = `site:${domain}`;
if (query.includes(domainQuery)) {
refs.searchbar.search(query);
} else {
refs.searchbar.search(`${query} site:${domain}`);
}
}
}
Where getMainLink
would be a new method in Result.svelte
export const getMainLink = () => mainLink
and url
would be a new method in ResultLink.svelte
export const url = () => href;
if (!((event.target as HTMLElement).nodeName === 'INPUT')) { | ||
keybind.onKeyDown(event, $useKeyboardShortcuts); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could be
if (event.target != searchbar?.getInputElem()) {
keybind.onKeyDown(event, $useKeyboardShortcuts);
}
This feedback is phenomenal! I really appreciate you taking the time to give in depth feedback. Svelte is completely new to me as I'm coming from an Angular/Vue/Django background so these pointers have been incredibly helpful. Especially cool all of the things you can do with bindings in regards to HTML Elements. I had no idea and wince a bit whenever I had to do DOM manipulation. I'll have some revisions up shortly! |
Changes will be up tomorrow - didn't catch the keybindings in |
frontend/src/lib/keybind.ts
Outdated
callback: (e: KeyboardEvent) => void; | ||
key: Key; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
callback: (e: KeyboardEvent, context: any) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had a bit of a battle with typing here. I tried to create a generic and use it both here (ie. context: Context
) and then again on line 39 with export class Keybind<Context>
but everything I tried complained or didn't register that they would be the same type when implemented. this has worked fine but totally open to any suggestions
frontend/src/lib/keybind.ts
Outdated
/** | ||
* Redirect to the `Did you mean:` link (if exists) | ||
*/ | ||
const goToMisspellLink = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also the only one that I can't get working. I'm pretty sure if has to do with the logic of the spell correction and it's conditional rendering - let me know if you look over this and see anything right off the bat
Was hoping this method of implementation didn't clutter things but the aim was re-usability as Stract grows. Implementing it in the search bar felt like some overhead was definitely eliminated, along with code not being duplicated which was nice. Once feedback is given on those two comments I'll move the PR out of draft. Cheers! |
It's always used with 'Refs' as context, so there is no need to have it generic
The functionality didn't work (for instance enter didn't trigger a search). It would require a lot of aditional complexity in 'Keybind' to also support the use case from searchbar. It's okay to have some code duplication if this results in a simpler solution that will therefore be more readable and mantainable long term
This forces us to not rely on direct manipulation of the event, but instead implement the necesarry functionality in helper methods in the different components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! I hope it's okay I went ahead and finished the last few things so we could get this merged. I tried to document it a bit in the commit messages, but in broad strokes:
- The callbacks in
Keybind
always useRefs
so there is no need to make it generic. - I think it's better to not use
Keybind
in the searchbar and have a simple keydown match instead. It would add a fair bit of complexity toKeybind
to be able to use it both in the shortcuts and searchbar, and I think this added complexity wouldn't be worth it compared to the (very small) code replication. - The callbacks can all be implemented without having to know about the keyboard event that triggered them.
Great work!
This is great, thanks a ton for the fixes! Are you sure the keybind class isn't adding bloat? I can totally do a quick refactor and toss it all in the |
I actually quite like to have it in a separate file to avoid |
Closes #146. The goal here was to create a simple system that could be reused to handle keyboard shortcuts. Thus as
Stract grows and potentially implements more features/views, new shortcuts can be easily added. Most of the TSDoc comments speak for themselves but in summary:
Keybind
to handle all needed Keybindings for a routekeybind.ts
/search/+page.svelte
/search/+page.svelte
Let me know whatever feedback you've got, totally willing to refactor to your preferences. Cheers!