mirror of
https://github.com/ryanamay/inkdrop-command-palette.git
synced 2024-09-20 01:40:34 +00:00
base: make the basic things work
This commit is contained in:
parent
dbf66e6b42
commit
ed9c586156
6 changed files with 387 additions and 9 deletions
94
lib/commands/application.js
Normal file
94
lib/commands/application.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
"use babel";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: ">About",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:about",
|
||||||
|
shortcut: ["test", "red"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Inspect",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:inspect",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Logout",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:logout",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Open preferences",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:open-preferences",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Open website",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:open-website",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Quit",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:quit",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Report issue",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:report-issue",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Show credits",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:show-credits",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Show and focus main window",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:show-and-focus-main-window",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Toggle main window",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:toggle-main-window",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">View Help",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:view-help",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Sync database (last checkpoint)",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:sync-db",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Sync database (full)",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:quit",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: ">Quick note",
|
||||||
|
category: "Application",
|
||||||
|
command: "application:quick-note",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "!Toggle Strong/Bold",
|
||||||
|
category: "Core",
|
||||||
|
command: "core:strong",
|
||||||
|
shortcut: [""],
|
||||||
|
},
|
||||||
|
];
|
54
lib/components/command.js
Normal file
54
lib/components/command.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
"use babel";
|
||||||
|
|
||||||
|
import React, { useEffect, useCallback, useLayoutEffect } from "react";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
export default Option = (props) => {
|
||||||
|
let { name, category, command, shortcut, modal, idx } = props;
|
||||||
|
|
||||||
|
function execute() {
|
||||||
|
ipcRenderer.send("command", command, {});
|
||||||
|
modal.close();
|
||||||
|
// hide modal after executing
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkfocus() {
|
||||||
|
if (document.activeElement.id === "cpInput") {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHighlighted = {
|
||||||
|
backgroundColor: idx === 0 ? "var(--highlight-background)" : "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className="option flex-row" onClick={execute} href="#">
|
||||||
|
<p className="nomargin">
|
||||||
|
{category}
|
||||||
|
{category != "" ? ": " : ""}
|
||||||
|
{name.replace(/!/g, "").replace(/>/g, "")}
|
||||||
|
</p>
|
||||||
|
<p className="nomargin">
|
||||||
|
{idx === 0 ? (
|
||||||
|
<span className="nomargin topresult">
|
||||||
|
<em>top result</em>
|
||||||
|
</span>
|
||||||
|
) : null}{" "}
|
||||||
|
{shortcut.map((key, index) => {
|
||||||
|
if (key == "") return null;
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<kbd key={index}>{key}</kbd>
|
||||||
|
{index !== shortcut.length - 1 ? " + " : ""}
|
||||||
|
{console.log(key)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,15 +1,22 @@
|
||||||
"use babel";
|
"use babel";
|
||||||
|
|
||||||
import React, { useEffect, useCallback, useRef, useLayoutEffect } from "react";
|
import React, { useEffect, useCallback, useRef, useLayoutEffect } from "react";
|
||||||
|
import Option from "./command.js";
|
||||||
|
import Commands from "../commands/application.js";
|
||||||
import { logger, useModal } from "inkdrop";
|
import { logger, useModal } from "inkdrop";
|
||||||
|
import useArrowKeyNavigation from "../navigation/hook.js";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
const CommandPalette = (props) => {
|
const CommandPalette = (props) => {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const { Dialog } = inkdrop.components.classes;
|
const { Dialog } = inkdrop.components.classes;
|
||||||
|
const parentRef = useArrowKeyNavigation({ selectors: "a,input" });
|
||||||
|
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
modal.show();
|
modal.show();
|
||||||
logger.debug("Dialog was toggled!");
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -25,13 +32,37 @@ const CommandPalette = (props) => {
|
||||||
const textbox = document.getElementById("cpInput");
|
const textbox = document.getElementById("cpInput");
|
||||||
textbox.focus();
|
textbox.focus();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
console.log("use layout effect triggered");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filter = (commands, query) => {
|
||||||
|
if (!query) return commands;
|
||||||
|
return commands.filter((command) => {
|
||||||
|
const commandText = command.name.toLowerCase();
|
||||||
|
const commandCat = command.category.toLowerCase();
|
||||||
|
let textFilter = commandText.includes(query.toLowerCase());
|
||||||
|
let categoryFilter = commandCat.includes(query.toLowerCase());
|
||||||
|
return textFilter || categoryFilter;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchQuery(e.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredResults = filter(Commands, searchQuery);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog {...modal.state} onBackdropClick={modal.close}>
|
<Dialog
|
||||||
|
{...modal.state}
|
||||||
|
onBackdropClick={modal.close}
|
||||||
|
className="commandpalette flex-col"
|
||||||
|
ref={parentRef}
|
||||||
|
>
|
||||||
<Dialog.Content className="commandpalette">
|
<Dialog.Content className="commandpalette">
|
||||||
<div role="dialog" aria-modal="true">
|
<div className="commandpalettewrapper flex-col" ref={parentRef}>
|
||||||
<div>
|
<div className="flex-col contents">
|
||||||
<div className="ui small input">
|
<div className="ui small input">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -39,8 +70,26 @@ const CommandPalette = (props) => {
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="cpInput"
|
className="cpInput"
|
||||||
id="cpInput"
|
id="cpInput"
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={changeHandler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="cpContents">
|
||||||
|
{filteredResults.length === 0 ? (
|
||||||
|
<div className="nomatch">
|
||||||
|
<p className="nomatchemoji">{"(。>﹏<)"}</p>
|
||||||
|
<p className="nomatchtext">no matching commands</p>
|
||||||
|
<p className="nomatchtext">
|
||||||
|
try searching for a different term
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{filteredResults.map((Command, index) => {
|
||||||
|
return (
|
||||||
|
<Option idx={index} key={index} {...Command} modal={modal} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
|
72
lib/navigation/handleEvents.js
Normal file
72
lib/navigation/handleEvents.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
"use babel";
|
||||||
|
|
||||||
|
function handleEnter({ event, currentIndex, availableElements }) {
|
||||||
|
let clickElement;
|
||||||
|
if (currentIndex === 0) {
|
||||||
|
clickElement = availableElements[currentIndex + 1];
|
||||||
|
} else {
|
||||||
|
clickElement = availableElements[currentIndex];
|
||||||
|
}
|
||||||
|
clickElement.click();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowKey({ event, currentIndex, availableElements }) {
|
||||||
|
// If the focus isn't in the container, focus on the first thing
|
||||||
|
if (currentIndex === -1) availableElements[0].focus();
|
||||||
|
|
||||||
|
// Move the focus up or down
|
||||||
|
let nextElement;
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
nextElement = availableElements[currentIndex + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
nextElement = availableElements[currentIndex - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(nextElement);
|
||||||
|
nextElement && nextElement.focus();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement arrow key navigation for the given parentNode
|
||||||
|
* @param {object} options
|
||||||
|
* @param {Event} options.e Keydown event
|
||||||
|
* @param {DOMNode} options.parentNode The parent node to operate on. Arrow keys won't navigate outside of this node
|
||||||
|
* @param {String} options.selectors Selectors for elements we want to be able to key through
|
||||||
|
*/
|
||||||
|
export default function handleEvents({
|
||||||
|
event,
|
||||||
|
parentNode,
|
||||||
|
selectors = "a,button,input",
|
||||||
|
}) {
|
||||||
|
if (!parentNode) return;
|
||||||
|
|
||||||
|
const key = event.key;
|
||||||
|
if (!["ArrowUp", "ArrowDown", "Enter"].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
|
||||||
|
// If we're not inside the container, don't do anything
|
||||||
|
if (!parentNode.contains(activeElement)) return;
|
||||||
|
|
||||||
|
// Get the list of elements we're allowed to scroll through
|
||||||
|
const availableElements = parentNode.querySelectorAll(selectors);
|
||||||
|
|
||||||
|
// No elements are available to loop through.
|
||||||
|
if (!availableElements.length) return;
|
||||||
|
|
||||||
|
// Which index is currently selected
|
||||||
|
const currentIndex = Array.from(availableElements).findIndex(
|
||||||
|
(availableElement) => availableElement === activeElement
|
||||||
|
);
|
||||||
|
|
||||||
|
if (key === "Enter") {
|
||||||
|
handleEnter({ event, currentIndex, availableElements });
|
||||||
|
}
|
||||||
|
handleArrowKey({ event, currentIndex, availableElements });
|
||||||
|
}
|
24
lib/navigation/hook.js
Normal file
24
lib/navigation/hook.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
"use babel";
|
||||||
|
|
||||||
|
import handleEvents from "./handleEvents";
|
||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A react hook to enable arrow key navigation on a component.
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns a useRef, which can be applied to a component
|
||||||
|
*/
|
||||||
|
export default function useArrowKeyNavigation(props) {
|
||||||
|
const { selectors } = props || {};
|
||||||
|
const parentNode = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventHandler = (event) => {
|
||||||
|
handleEvents({ event, parentNode: parentNode.current, selectors });
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", eventHandler);
|
||||||
|
return () => document.removeEventListener("keydown", eventHandler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return parentNode;
|
||||||
|
}
|
|
@ -1,9 +1,94 @@
|
||||||
.commandpalette {
|
.commandpalette {
|
||||||
padding: 0rem !important ;
|
padding: 5px !important ;
|
||||||
background-color: red;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpInput {
|
.commandpalettewrapper {
|
||||||
width: 400px;
|
height: 255px;
|
||||||
background-color: var(--page-background) !important ;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpInput,
|
||||||
|
.cpContents {
|
||||||
|
width: 600px;
|
||||||
|
background-color: var(--page-background) !important ;
|
||||||
|
margin: 4px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpContents {
|
||||||
|
overflow: hidden scroll;
|
||||||
|
height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
padding: 4px 13px;
|
||||||
|
width: 600px;
|
||||||
|
height: 25px;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-top: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover,
|
||||||
|
.option:focus {
|
||||||
|
padding: 4px 13px;
|
||||||
|
width: 600px;
|
||||||
|
height: 25px;
|
||||||
|
background-color: var(--highlight-background);
|
||||||
|
color: var(--selected-text-color);
|
||||||
|
margin-top: 5px;
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: top;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomargin {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topresult {
|
||||||
|
color: var(--disabled-text-color);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomatch {
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomatchemoji {
|
||||||
|
font-size: 50px;
|
||||||
|
color: var(--disabled-text-color);
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomatchtext {
|
||||||
|
color: var(--disabled-text-color);
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue