base: make the basic things work

This commit is contained in:
fuwaa 2022-08-22 20:20:24 +08:00
parent dbf66e6b42
commit ed9c586156
No known key found for this signature in database
GPG key ID: 7CE0BF0D3175E2DC
6 changed files with 387 additions and 9 deletions

View 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
View 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>
);
};

View file

@ -1,15 +1,22 @@
"use babel";
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 useArrowKeyNavigation from "../navigation/hook.js";
import { ipcRenderer } from "electron";
const CommandPalette = (props) => {
const modal = useModal();
const { Dialog } = inkdrop.components.classes;
const parentRef = useArrowKeyNavigation({ selectors: "a,input" });
const inputRef = useRef(null);
const [searchQuery, setSearchQuery] = React.useState("");
const toggle = useCallback(() => {
modal.show();
logger.debug("Dialog was toggled!");
}, []);
useEffect(() => {
@ -25,13 +32,37 @@ const CommandPalette = (props) => {
const textbox = document.getElementById("cpInput");
textbox.focus();
}, 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 (
<Dialog {...modal.state} onBackdropClick={modal.close}>
<Dialog
{...modal.state}
onBackdropClick={modal.close}
className="commandpalette flex-col"
ref={parentRef}
>
<Dialog.Content className="commandpalette">
<div role="dialog" aria-modal="true">
<div>
<div className="commandpalettewrapper flex-col" ref={parentRef}>
<div className="flex-col contents">
<div className="ui small input">
<input
type="text"
@ -39,8 +70,26 @@ const CommandPalette = (props) => {
spellCheck="false"
className="cpInput"
id="cpInput"
ref={inputRef}
onChange={changeHandler}
/>
</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>
</Dialog.Content>

View 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
View 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;
}

View file

@ -1,9 +1,94 @@
.commandpalette {
padding: 0rem !important ;
background-color: red;
padding: 5px !important ;
}
.cpInput {
width: 400px;
background-color: var(--page-background) !important ;
.commandpalettewrapper {
height: 255px;
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;
}