This content originally appeared on DEV Community and was authored by Tomasz Wegrzanowski
And now it's time to add command palette to our file manager. It will be very simple at first, but we can keep adding features to it over the next few episodes.
I sort of wonder if I'm doing things backwards, as the file manager doesn't actually do anything yet, other than being a retro looking ls
. We'll get to adding all the functionality eventually.
This episodes starts where we left over in episode 36, adding command pallete feature based on episode 35.
src/commands.js
This file is shared between keyboard handler and command palette. Once we add application menu, it should hopefully use it as well.
export default [
{key: "F2", action: ["app", "openPalette"]},
{name: "Close Palette", key: "Escape", action: ["app", "closePalette"] },
{name: "Enter Directory", key: "Enter", action: ["activePanel", "activateItem"]},
{name: "Flip Selection", key: " ", action: ["activePanel", "flipItem"]},
{name: "Go to First File", key: "Home", action: ["activePanel", "firstItem"]},
{name: "Go to Last File", key: "End", action: ["activePanel", "lastItem"]},
{name: "Go to Next File", key: "ArrowDown", action: ["activePanel", "nextItem"]},
{name: "Go to Previous File", key: "ArrowUp", action: ["activePanel", "previousItem"]},
{name: "Page Down", key: "PageDown", action: ["activePanel", "pageDown"]},
{name: "Page Up", key: "PageUp", action: ["activePanel", "pageUp"]},
{name: "Quit", key: "F10", action: ["app", "quit"]},
{name: "Switch Panel", key: "Tab", action: ["app", "switchPanel"]},
]
The idea is that commands we don't want to have keyboard shortcuts for just won't have key
(currently none, but there will be a lot of them). And commands we don't want in command palette just don't have name
(currently Open Palette
as it's meaningless to open it while it's already open).
So far the system only features commands that don't require any extra arguments. At some point we'll need to extend it to more complicated commands.
src/Keyboard.svelte
We just need to do two quick changes. The component will now get active
prop, and if it's set to false
, it will ignore all key events.
I also added e.stopPropagation()
as now we have multiple keyboard handlers - this one for when palette is closed, and the one in palette when it is open. We don't need this line, but it will save us some debugging headaches as our app gets more complex.
The rest is as before.
<script>
import commands from "./commands.js"
import { getContext } from "svelte"
export let active
let { eventBus } = getContext("app")
function handleKey(e) {
if (!active) {
return
}
for (let command of commands) {
if (command.key === e.key) {
e.preventDefault()
e.stopPropagation()
eventBus.emit(...command.action)
}
}
}
<svelte:window on:keydown={handleKey} />
</script>
src/CommandPaletteEntry.svelte
This component represents a single available command. I previously called it Command
, but I don't think this is a great name.
It functions just like the one from episode 35, but styling is more in line with our app, and there's one hack to make space key be displayed as "Space"
, even though in JS it is just " "
.
<script>
import { getContext } from "svelte"
let { eventBus } = getContext("app")
export let name
export let key
export let action
function handleClick() {
eventBus.emit("app", "closePalette")
eventBus.emit(...action)
}
function keyName(key) {
if (key === " ") {
return "Space"
} else {
return key
}
}
</script>
<li on:click={handleClick}>
<span class="name">{name}</span>
{#if key}
<span class="key">{keyName(key)}</span>
{/if}
</li>
<style>
li {
display: flex;
padding: 0px 8px;
}
li:first-child {
background-color: #66b;
}
.name {
flex: 1;
}
.key {
display: inline-block;
background-color: hsl(180,100%,30%);
padding: 2px;
border: 1px solid hsl(180,100%,20%);
border-radius: 20%;
}
</style>
src/CommandPalette.svelte
This component represents a simple command palette. Compared with what we had previously, styling is changed to match the app, and command list is imported from commands.js
instead of being duplicated here.
We also need to do event.stopPropagation()
here. Otherwise we'd press Enter
to select command, but that Enter
would also be sent to the regular keyboard handler - which would then try to run it as palette is closed at this point.
In general it's helpful to stop propagation of events even when it's not needed, just to save some debugging.
<script>
import commands from "./commands.js"
import { getContext } from "svelte"
import CommandPaletteEntry from "./CommandPaletteEntry.svelte"
let { eventBus } = getContext("app")
let pattern = ""
$: matchingCommands = commands.filter(({name}) => checkMatch(pattern, name))
function handleKey(event) {
let {key} = event;
if (key === "Enter") {
event.preventDefault()
event.stopPropagation()
eventBus.emit("app", "closePalette")
if (matchingCommands[0]) {
eventBus.emit(...matchingCommands[0].action)
}
}
if (key === "Escape") {
event.preventDefault()
event.stopPropagation()
eventBus.emit("app", "closePalette")
}
}
function checkMatch(pattern, name) {
if (!name) {
return false
}
let parts = pattern.toLowerCase().replace(/[^a-z0-9]/, "")
let rx = new RegExp(parts.split("").join(".*"))
name = name.toLowerCase().replace(/[^a-z0-9]/, "")
return rx.test(name)
}
function focus(el) {
el.focus()
}
</script>
<div class="palette">
<input use:focus bind:value={pattern} placeholder="Search for command" on:keydown={handleKey}>
<ul>
{#each matchingCommands as command}
<CommandPaletteEntry {...command} />
{/each}
</ul>
</div>
<style>
.palette {
position: fixed;
left: 0;
top: 0;
right: 0;
margin: auto;
max-width: 50vw;
background: #338;
box-shadow: 0px 0px 24px #004;
}
input {
font-family: inherit;
background-color: inherit;
font-size: inherit;
font-weight: inherit;
box-sizing: border-box;
width: 100%;
margin: 0;
background: #66b;
color: inherit;
}
input::placeholder {
color: inherit;
font-style: italic;
}
ul {
list-style: none;
padding: 0;
margin: 0;
margin-top: 8px;
}
</style>
src/App.svelte
The main app component only changed slightly. The template now has CommandPalette
and passes active
flag to the Keyboard
component.
<div class="ui">
<header>
File Manager
</header>
<Panel initialDirectory={initialDirectoryLeft} id="left" />
<Panel initialDirectory={initialDirectoryRight} id="right" />
<Footer />
</div>
<Keyboard active={!paletteOpen} />
{#if paletteOpen}
<CommandPalette />
{/if}
In the script we add small bit of logic to open and close the palette:
import CommandPalette from "./CommandPalette.svelte"
let paletteOpen = false
function openPalette() {
paletteOpen = true
}
function closePalette() {
paletteOpen = false
}
eventBus.handle("app", {switchPanel, activatePanel, quit, openPalette, closePalette})
The rest is as before.
Result
Here's the results:
The most recent few episodes were fairly heavy. The next few will be much lighter, focusing on a small function at a time. In the next episode, we'll add some highlighting feedback to command palette matches.
As usual, all the code for the episode is here.
This content originally appeared on DEV Community and was authored by Tomasz Wegrzanowski
Tomasz Wegrzanowski | Sciencx (2021-08-30T02:42:49+00:00) Electron Adventures: Episode 37: File Manager Command Palette. Retrieved from https://www.scien.cx/2021/08/30/electron-adventures-episode-37-file-manager-command-palette/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.