diff --git a/lib/commands/application.js b/lib/commands/application.js new file mode 100644 index 0000000..9719b9e --- /dev/null +++ b/lib/commands/application.js @@ -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: [""], + }, +]; diff --git a/lib/components/command.js b/lib/components/command.js new file mode 100644 index 0000000..7cc7735 --- /dev/null +++ b/lib/components/command.js @@ -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 ( + +

+ {category} + {category != "" ? ": " : ""} + {name.replace(/!/g, "").replace(/>/g, "")} +

+

+ {idx === 0 ? ( + + top result + + ) : null}{" "} + {shortcut.map((key, index) => { + if (key == "") return null; + else + return ( + + {key} + {index !== shortcut.length - 1 ? " + " : ""} + {console.log(key)} + + ); + })} +

+
+ ); +}; diff --git a/lib/components/palette.js b/lib/components/palette.js index 572467e..0337aba 100644 --- a/lib/components/palette.js +++ b/lib/components/palette.js @@ -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 ( - + -
-
+
+
{ spellCheck="false" className="cpInput" id="cpInput" + ref={inputRef} + onChange={changeHandler} />
+
+ {filteredResults.length === 0 ? ( +
+

{"(。>﹏<)"}

+

no matching commands

+

+ try searching for a different term +

+
+ ) : null} + {filteredResults.map((Command, index) => { + return ( +
diff --git a/lib/navigation/handleEvents.js b/lib/navigation/handleEvents.js new file mode 100644 index 0000000..0f1dd34 --- /dev/null +++ b/lib/navigation/handleEvents.js @@ -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 }); +} diff --git a/lib/navigation/hook.js b/lib/navigation/hook.js new file mode 100644 index 0000000..a10deff --- /dev/null +++ b/lib/navigation/hook.js @@ -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; +} diff --git a/styles/palettemodal.less b/styles/palettemodal.less index c5726fd..770e58e 100644 --- a/styles/palettemodal.less +++ b/styles/palettemodal.less @@ -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; }