Skip to content

Commit

Permalink
Use Command palette for commands only
Browse files Browse the repository at this point in the history
  • Loading branch information
penge committed Jul 29, 2022
1 parent 1d6988b commit 4e63d63
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 334 deletions.
5 changes: 0 additions & 5 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,6 @@
"Options": "Options",
"Sync notes to and from Google Drive": "Sync notes to and from Google Drive",
"Command palette": "Command palette",
"Command palette detail": {
"line1": "By default, Command palette looks for notes <b>by their name.</b>",
"line2": "Type {{symbol}} first, and continue to start search for <b>commands.</b>",
"line3": "Type {{symbol}} first, and continue to start search for notes <b>by their content.</b>"
},
"Repeat last command": "Repeat last command",
"Toggle Focus mode": "Toggle Focus mode",
"Toggle Sidebar": "Toggle Sidebar",
Expand Down
68 changes: 10 additions & 58 deletions src/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
/* eslint-disable react/jsx-pascal-case */
import { h, render, Fragment } from "preact";
import {
useState, useEffect, useRef, useCallback, useMemo,
useState, useEffect, useRef, useCallback,
} from "preact/hooks";

import {
Os,
Storage,
Expand Down Expand Up @@ -49,12 +48,9 @@ import getFirstAvailableNoteName from "notes/filters/get-first-available-note-na
import notesHistory from "notes/history";
import keyboardShortcuts, { KeyboardShortcut } from "notes/keyboard-shortcuts";
import { useKeyboardShortcut } from "notes/hooks/use-keyboard-shortcut";
import {
Command, commands, toggleSidebar, toggleToolbar,
} from "notes/commands";
import { Command, toggleSidebar, toggleToolbar } from "notes/commands";
import { exportNote } from "notes/export";
import { notesToSidebarNotes } from "notes/adapters";
import { t } from "i18n";

let autoSyncIntervalId: number | undefined;

Expand Down Expand Up @@ -504,73 +500,29 @@ const Notes = (): h.JSX.Element => {
chrome.storage.local.set({ active: noteName });
}, [notesProps]);

// Command Palette
const commandPaletteCommands: { name: string, translation: h.JSX.Element, command: Command }[] = useMemo(() => [
{
name: "Insert current Date",
translation: t("Insert current Date"),
command: commands.InsertCurrentDate,
},
{
name: "Insert current Time",
translation: t("Insert current Time"),
command: commands.InsertCurrentTime,
},
{
name: "Insert current Date and Time",
translation: t("Insert current Date and Time"),
command: commands.InsertCurrentDateAndTime,
},
{
name: "Toggle Sidebar",
translation: t("Shortcuts descriptions.Toggle Sidebar"),
command: toggleSidebar,
},
{
name: "Toggle Toolbar",
translation: t("Shortcuts descriptions.Toggle Toolbar"),
command: toggleToolbar,
},
], []);

// Repeat last executed command
const [setOnRepeatLastExecutedCommandHandler] = useKeyboardShortcut(KeyboardShortcut.OnRepeatLastExecutedCommand);

// Command Palette
const [setOnToggleCommandPaletteHandler] = useKeyboardShortcut(KeyboardShortcut.OnToggleCommandPalette);
useEffect(() => {
if (!notesOrder) {
return;
}

// Detach when there are no notes
if (!Object.keys(notesProps.notes).length) {
setOnToggleCommandPaletteHandler(undefined);
setCommandPaletteProps(null);
return;
}

// Start preparing props for Command Palette
const currentNoteLocked: boolean = notesProps.active in notesProps.notes && notesProps.notes[notesProps.active].locked === true;
const newCommands = currentNoteLocked ? [] : commandPaletteCommands;

// Props for Command Palette
const currentNoteLocked: boolean = notesProps.active in notesProps.notes && notesProps.notes[notesProps.active].locked === true;
const props: CommandPaletteProps = {
notes: notesToSidebarNotes(notesProps.notes, notesOrder, order),
commands: newCommands,
onActivateNote: (noteName: string) => {
includeContentCommands: !currentNoteLocked,
onCommandToExecute: (command: Command) => {
setCommandPaletteProps(null);
range.restore(() => handleOnActivateNote(noteName));
},
onExecuteCommand: (commandName: string) => {
const foundCommand = commandPaletteCommands.find((command) => command.name === commandName);
if (foundCommand) {
setCommandPaletteProps(null);
range.restore(() => {
foundCommand.command();
setOnRepeatLastExecutedCommandHandler(foundCommand.command);
});
}
range.restore(() => {
command();
setOnRepeatLastExecutedCommandHandler(command);
});
},
};

Expand All @@ -589,7 +541,7 @@ const Notes = (): h.JSX.Element => {

// Update props for already visible Command Palette
setCommandPaletteProps((prev) => (!prev ? prev : props));
}, [os, notesProps, notesOrder, order, handleOnActivateNote, commandPaletteCommands]);
}, [notesProps]);

// Automatically show modal to create a new note if there are 0 notes
useEffect(() => {
Expand Down
190 changes: 63 additions & 127 deletions src/notes/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -1,136 +1,77 @@
import { h } from "preact";
import {
useRef, useState, useMemo, useCallback, useEffect,
useRef, useState, useEffect, useMemo,
} from "preact/hooks";
import clsx from "clsx";
import useBodyClass from "notes/hooks/use-body-class";
import { SidebarNote } from "notes/adapters";
import { t, tString } from "i18n";
import {
Command, commands as contentCommands, toggleSidebar, toggleToolbar,
} from "notes/commands";

export interface CommandPaletteProps {
notes: SidebarNote[]
commands: { name: string, translation: h.JSX.Element }[]
onActivateNote: (noteName: string) => void
onExecuteCommand: (commandName: string) => void
}

export enum FilterType {
CommandsByName,
NotesByContent,
NotesByName,
}

export interface Filter {
type: FilterType
input: string
}

/**
* Returns filter to be used based on the input.
*
* We can filter:
* A) CommandsByName
* => filter commands by name, when input starts with ">" (whitespace before and after ">" is allowed, whitespace at the end is allowed)
*
* B) NotesByContent
* => filter notes by the content, when input starts with "?" (whitespace before and after "?" is allowed, whitespace at the end is allowed)
*
* C) NotesByName
* => filter notes by their name, when input does NOT start with [">", "?"] (whitespace before is allowed, whitespace at the end is allowed)
*/
export const prepareFilter = (rawInput: string): Filter => {
const input = rawInput.trim().toLowerCase().replace(/^([>?])\s*(.*)/, "$1$2"); // trim whitespace, remove whitespace after [">", "?"]

// A) CommandsByName
if (input.startsWith(">")) {
return {
type: FilterType.CommandsByName,
input: input.slice(1),
};
}

// B) NotesByContent
if (input.startsWith("?")) {
return {
type: FilterType.NotesByContent,
input: input.slice(1),
};
}

// C) NotesByName
return {
type: FilterType.NotesByName,
input,
};
type CommandPaletteCommand = {
title: string
command: Command
isContentCommand?: boolean
};

export const prepareItems = (notes: SidebarNote[], commands: string[], filter: Filter | undefined): string[] => {
const noteNames = notes.map((note) => note.name);
const allCommands: CommandPaletteCommand[] = [
{
title: tString("Insert current Date"),
command: contentCommands.InsertCurrentDate,
isContentCommand: true,
},
{
title: tString("Insert current Time"),
command: contentCommands.InsertCurrentTime,
isContentCommand: true,
},
{
title: tString("Insert current Date and Time"),
command: contentCommands.InsertCurrentDateAndTime,
isContentCommand: true,
},
{
title: tString("Shortcuts descriptions.Toggle Sidebar"),
command: toggleSidebar,
},
{
title: tString("Shortcuts descriptions.Toggle Toolbar"),
command: toggleToolbar,
},
];

if (!filter) {
return noteNames;
}

const input = filter.input.trim();
const prepareFilterPredicate = (givenInput: string) => (item: string) => (givenInput ? item.toLowerCase().includes(givenInput) : item);

// A) CommandsByName
if (filter.type === FilterType.CommandsByName) {
return commands.filter(prepareFilterPredicate(input));
}

// B) NotesByContent
if (filter.type === FilterType.NotesByContent) {
const filterPredicate = prepareFilterPredicate(input);
return input
? noteNames.filter((noteName) => {
const foundNote = notes.find((note) => note.name === noteName);
return foundNote && filterPredicate(foundNote.content);
})
: noteNames;
}

// C) NotesByName
return noteNames.filter(prepareFilterPredicate(input));
};
export interface CommandPaletteProps {
includeContentCommands: boolean
onCommandToExecute: (command: Command) => void
}

const CommandPalette = ({
notes, commands, onActivateNote, onExecuteCommand,
includeContentCommands,
onCommandToExecute,
}: CommandPaletteProps): h.JSX.Element => {
useBodyClass("with-command-palette");

const inputRef = useRef<HTMLInputElement>(null);
const [filter, setFilter] = useState<Filter | undefined>(undefined);

// Items to show in Command Palette
const items: string[] = useMemo(() => prepareItems(notes, commands.map((command) => command.name), filter), [notes, commands, filter]);

// Selected item; can be changed with Up / Down arrow keys
const [selectedItemIndex, setSelectedItemIndex] = useState<number>(-1);

// What to do on Enter
const handleItem = useCallback((name: string) => {
if (filter?.type === FilterType.CommandsByName) {
onExecuteCommand(name);
return;
}

onActivateNote(name);
}, [filter, onActivateNote, onExecuteCommand]);
useEffect(() => inputRef.current?.focus(), []);

// auto-focus the input when Command Palette is open
useEffect(() => inputRef.current?.focus(), [inputRef]);
const [filter, setFilter] = useState("");
const filteredCommands = useMemo(() => (
allCommands
.filter((x) => (includeContentCommands ? true : (!x.isContentCommand)))
.filter((x) => (filter ? x.title.toLowerCase().includes(filter.trim().toLowerCase()) : true))
), [includeContentCommands, filter]);

// reset selected item when props change; auto-select it when there's just one
useEffect(() => setSelectedItemIndex(items.length === 1 ? 0 : -1), [filter, items]);
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
useEffect(() => setSelectedIndex(filteredCommands.length === 1 ? 0 : -1), [filteredCommands]);

return (
<div
id="command-palette"
onKeyDown={(event) => {
if (event.key === "Enter" && selectedItemIndex !== -1) {
const item = items[selectedItemIndex];
handleItem(item);
if (event.key === "Enter" && selectedIndex !== -1) {
const { command } = filteredCommands[selectedIndex];
onCommandToExecute(command);
return;
}

Expand All @@ -140,10 +81,10 @@ const CommandPalette = ({
}

event.preventDefault();
const activeIndexCandidate = ((selectedItemIndex + direction) % items.length);
setSelectedItemIndex(
const activeIndexCandidate = ((selectedIndex + direction) % filteredCommands.length);
setSelectedIndex(
activeIndexCandidate < 0 && direction === -1
? items.length - 1
? filteredCommands.length - 1
: activeIndexCandidate,
);
}}
Expand All @@ -155,34 +96,29 @@ const CommandPalette = ({
ref={inputRef}
onBlur={() => inputRef.current?.focus()}
onInput={(event) => {
const newFilter = prepareFilter((event.target as HTMLInputElement).value);
setFilter(newFilter);
setFilter((event.target as HTMLInputElement).value);
}}
autoComplete="off"
/>

{items.length > 0 && (
{filteredCommands.length > 0 && (
<div className="command-palette-list">
{items.map((name, index) => (
{filteredCommands.map((item, index) => (
<div
className={clsx("command-palette-list-item", index === selectedItemIndex && "active")}
onClick={() => handleItem(name)}
onMouseEnter={() => setSelectedItemIndex(index)}
className={clsx("command-palette-list-item", index === selectedIndex && "active")}
onClick={() => onCommandToExecute(item.command)}
onMouseEnter={() => setSelectedIndex(index)}
>
{(filter?.type === FilterType.CommandsByName)
? commands.find((command) => command.name === name)?.translation
: name}
{item.title}
</div>
))}
</div>
)}

{items.length === 0 && filter && (
{filteredCommands.length === 0 && (
<div className="command-palette-list">
<div className="command-palette-list-item plain">
{filter.type === FilterType.CommandsByName && t("(No matching commands)")}
{filter.type === FilterType.NotesByContent && t("(No matching notes)")}
{filter.type === FilterType.NotesByName && t("(No matching notes)")}
{t("(No matching commands)")}
</div>
</div>
)}
Expand Down
Loading

0 comments on commit 4e63d63

Please sign in to comment.