Migrate to Deno (#86)

Big bang migration to Deno 🤯
This commit is contained in:
Zef Hemel
2022-10-10 14:50:21 +02:00
committed by GitHub
parent 78f83c70d8
commit 561aa6891f
287 changed files with 4577 additions and 25087 deletions
+44
View File
@@ -0,0 +1,44 @@
import { isMacLike } from "../../common/util.ts";
import { FilterList } from "./filter.tsx";
import { faPersonRunning } from "../deps.ts";
import { AppCommand } from "../hooks/command.ts";
import { FilterOption } from "../../common/types.ts";
export function CommandPalette({
commands,
recentCommands,
onTrigger,
}: {
commands: Map<string, AppCommand>;
recentCommands: Map<string, Date>;
onTrigger: (command: AppCommand | undefined) => void;
}) {
let options: FilterOption[] = [];
const isMac = isMacLike();
for (let [name, def] of commands.entries()) {
options.push({
name: name,
hint: isMac && def.command.mac ? def.command.mac : def.command.key,
orderId: recentCommands.has(name)
? -recentCommands.get(name)!.getTime()
: 0,
});
}
return (
<FilterList
label="Run"
placeholder="Command"
options={options}
allowNew={false}
icon={faPersonRunning}
helpText="Start typing the command name to filter results, press <code>Return</code> to run."
onSelect={(opt) => {
if (opt) {
onTrigger(commands.get(opt.name));
} else {
onTrigger(undefined);
}
}}
/>
);
}
+240
View File
@@ -0,0 +1,240 @@
import { useEffect, useRef, useState } from "../deps.ts";
import { FontAwesomeIcon } from "../deps.ts";
import type { IconDefinition } from "../deps.ts";
import { FilterOption } from "../../common/types.ts";
import fuzzysort from "https://esm.sh/fuzzysort@2.0.1";
function magicSorter(a: FilterOption, b: FilterOption): number {
if (a.orderId && b.orderId) {
return a.orderId < b.orderId ? -1 : 1;
}
if (a.orderId) {
return -1;
}
if (b.orderId) {
return 1;
}
return 0;
}
type FilterResult = FilterOption & {
result?: any;
};
function simpleFilter(
pattern: string,
options: FilterOption[],
): FilterOption[] {
const lowerPattern = pattern.toLowerCase();
return options.filter((option) => {
return option.name.toLowerCase().includes(lowerPattern);
});
}
function escapeHtml(unsafe: string): string {
return unsafe
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function fuzzySorter(pattern: string, options: FilterOption[]): FilterResult[] {
return fuzzysort
.go(pattern, options, {
all: true,
key: "name",
})
.map((result: any) => ({ ...result.obj, result: result }))
.sort(magicSorter);
}
export function FilterList({
placeholder,
options,
label,
onSelect,
onKeyPress,
allowNew = false,
helpText = "",
completePrefix,
icon,
newHint,
}: {
placeholder: string;
options: FilterOption[];
label: string;
onKeyPress?: (key: string, currentText: string) => void;
onSelect: (option: FilterOption | undefined) => void;
allowNew?: boolean;
completePrefix?: string;
helpText: string;
newHint?: string;
icon?: IconDefinition;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const [text, setText] = useState("");
const [matchingOptions, setMatchingOptions] = useState(
fuzzySorter("", options),
);
const [selectedOption, setSelectionOption] = useState(0);
const selectedElementRef = useRef<HTMLDivElement>(null);
function updateFilter(originalPhrase: string) {
const foundExactMatch = false;
const results = fuzzySorter(originalPhrase, options);
if (allowNew && !foundExactMatch && originalPhrase) {
results.push({
name: originalPhrase,
hint: newHint,
});
}
setMatchingOptions(results);
setText(originalPhrase);
setSelectionOption(0);
}
useEffect(() => {
updateFilter(text);
}, [options]);
useEffect(() => {
searchBoxRef.current!.focus();
}, []);
useEffect(() => {
function closer() {
onSelect(undefined);
}
document.addEventListener("click", closer);
return () => {
document.removeEventListener("click", closer);
};
}, []);
let exiting = false;
const returnEl = (
<div className="sb-filter-wrapper">
<div className="sb-filter-box">
<div className="sb-header">
<label>{label}</label>
<input
type="text"
value={text}
placeholder={placeholder}
ref={searchBoxRef}
// onChange={filterUpdate}
onBlur={(e) => {
if (!exiting) {
searchBoxRef.current!.focus();
}
}}
onKeyDown={(e) => {
// console.log("Key up", / e);
if (onKeyPress) {
onKeyPress(e.key, text);
}
switch (e.key) {
case "ArrowUp":
setSelectionOption(Math.max(0, selectedOption - 1));
break;
case "ArrowDown":
setSelectionOption(
Math.min(matchingOptions.length - 1, selectedOption + 1),
);
break;
case "Enter":
exiting = true;
onSelect(matchingOptions[selectedOption]);
e.preventDefault();
break;
case "PageUp":
setSelectionOption(Math.max(0, selectedOption - 5));
break;
case "PageDown":
setSelectionOption(Math.max(0, selectedOption + 5));
break;
case "Home":
setSelectionOption(0);
break;
case "End":
setSelectionOption(matchingOptions.length - 1);
break;
case "Escape":
exiting = true;
onSelect(undefined);
e.preventDefault();
break;
case " ":
if (completePrefix && !text) {
updateFilter(completePrefix);
e.preventDefault();
}
break;
default:
setTimeout(() => {
updateFilter((e.target as any).value);
});
}
e.stopPropagation();
}}
onClick={(e) => e.stopPropagation()}
/>
</div>
<div
className="sb-help-text"
dangerouslySetInnerHTML={{ __html: helpText }}
>
</div>
<div className="sb-result-list">
{matchingOptions && matchingOptions.length > 0
? matchingOptions.map((option, idx) => (
<div
key={"" + idx}
ref={selectedOption === idx ? selectedElementRef : undefined}
className={selectedOption === idx
? "sb-selected-option"
: "sb-option"}
onMouseOver={(e) => {
setSelectionOption(idx);
}}
onClick={(e) => {
e.preventDefault();
onSelect(option);
}}
>
<span className="sb-icon">
{icon && <FontAwesomeIcon icon={icon} />}
</span>
<span
className="sb-name"
dangerouslySetInnerHTML={{
__html: option?.result?.indexes
? fuzzysort.highlight(option.result, "<b>", "</b>")!
: escapeHtml(option.name),
}}
>
</span>
{option.hint && <span className="sb-hint">{option.hint}</span>}
</div>
))
: null}
</div>
</div>
</div>
);
useEffect(() => {
selectedElementRef.current?.scrollIntoView({
block: "nearest",
});
});
return returnEl;
}
+53
View File
@@ -0,0 +1,53 @@
import { FilterList } from "./filter.tsx";
import { FilterOption, PageMeta } from "../../common/types.ts";
export function PageNavigator({
allPages,
onNavigate,
currentPage,
}: {
allPages: Set<PageMeta>;
onNavigate: (page: string | undefined) => void;
currentPage?: string;
}) {
const options: FilterOption[] = [];
for (const pageMeta of allPages) {
// Order by last modified date in descending order
let orderId = -pageMeta.lastModified;
// Unless it was opened in this session
if (pageMeta.lastOpened) {
orderId = -pageMeta.lastOpened;
}
// Or it's the currently open page
if (currentPage && currentPage === pageMeta.name) {
// ... then we put it all the way to the end
orderId = Infinity;
}
options.push({
...pageMeta,
orderId: orderId,
});
}
let completePrefix: string | undefined = undefined;
if (currentPage && currentPage.includes("/")) {
const pieces = currentPage.split("/");
completePrefix = pieces.slice(0, pieces.length - 1).join("/") + "/";
} else if (currentPage && currentPage.includes(" ")) {
completePrefix = currentPage.split(" ")[0] + " ";
}
return (
<FilterList
placeholder="Page"
label="Open"
options={options}
// icon={faFileLines}
allowNew={true}
helpText="Start typing the page name to filter results, press <code>Return</code> to open."
newHint="Create page"
completePrefix={completePrefix}
onSelect={(opt) => {
onNavigate(opt?.name);
}}
/>
);
}
+97
View File
@@ -0,0 +1,97 @@
import { useEffect, useRef } from "../deps.ts";
import { Editor } from "../editor.tsx";
import { PanelConfig } from "../types.ts";
const panelHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<base target="_top">
<script>
window.addEventListener("message", (message) => {
const data = message.data;
switch (data.type) {
case "html":
document.body.innerHTML = data.html;
if (data.script) {
try {
eval(data.script);
} catch (e) {
console.error("Error evaling script", e);
}
}
break;
}
});
function sendEvent(name, ...args) {
window.parent.postMessage(
{
type: "event",
name,
args,
},
"*"
);
}
</script>
</head>
<body>
Send me HTML
</body>
</html>`;
export function Panel({
config,
editor,
}: {
config: PanelConfig;
editor: Editor;
}) {
const iFrameRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
function loadContent() {
if (iFrameRef.current?.contentWindow) {
iFrameRef.current.contentWindow.postMessage({
type: "html",
html: config.html,
script: config.script,
});
}
}
if (!iFrameRef.current) {
return;
}
const iframe = iFrameRef.current;
iframe.onload = loadContent;
loadContent();
return () => {
iframe.onload = null;
};
}, [config.html, config.script]);
useEffect(() => {
const messageListener = (evt: any) => {
if (evt.source !== iFrameRef.current!.contentWindow) {
return;
}
const data = evt.data;
if (!data) {
return;
}
if (data.type === "event") {
editor.dispatchAppEvent(data.name, ...data.args);
}
};
globalThis.addEventListener("message", messageListener);
return () => {
globalThis.removeEventListener("message", messageListener);
};
}, []);
return (
<div className="sb-panel" style={{ flex: config.mode }}>
<iframe srcDoc={panelHtml} ref={iFrameRef} />
</div>
);
}
+113
View File
@@ -0,0 +1,113 @@
// import { Fragment, h } from "../deps.ts";
import {
faHome,
faMoon,
faRunning,
faSun,
} from "https://esm.sh/@fortawesome/free-solid-svg-icons@6.2.0";
import { FontAwesomeIcon } from "../deps.ts";
import { ComponentChildren, useState } from "../deps.ts";
import { Notification } from "../types.ts";
import { isMacLike } from "../../common/util.ts";
function prettyName(s: string | undefined): string {
if (!s) {
return "";
}
return s.replaceAll("/", " / ");
}
export function TopBar({
pageName,
unsavedChanges,
isLoading,
notifications,
onClick,
onThemeClick,
onHomeClick,
onActionClick,
lhs,
rhs,
}: {
pageName?: string;
unsavedChanges: boolean;
isLoading: boolean;
notifications: Notification[];
onClick: () => void;
onThemeClick: () => void;
onHomeClick: () => void;
onActionClick: () => void;
lhs?: ComponentChildren;
rhs?: ComponentChildren;
}) {
const [theme, setTheme] = useState<string>(localStorage.theme ?? "light");
const isMac = isMacLike();
return (
<div id="sb-top" onClick={onClick}>
{lhs}
<div className="main">
<div className="inner">
<span
className={`sb-current-page ${
isLoading
? "sb-loading"
: unsavedChanges
? "sb-unsaved"
: "sb-saved"
}`}
>
{prettyName(pageName)}
</span>
{notifications.length > 0 && (
<div className="sb-notifications">
{notifications.map((notification) => (
<div
key={notification.id}
className={`sb-notification-${notification.type}`}
>
{notification.message}
</div>
))}
</div>
)}
<div className="sb-actions">
<button
onClick={(e) => {
onHomeClick();
e.stopPropagation();
}}
title="Navigate to the 'index' page"
>
<FontAwesomeIcon icon={faHome} />
</button>
<button
onClick={(e) => {
onActionClick();
e.stopPropagation();
}}
title={"Open the command palette (" + (isMac ? "Cmd" : "Ctrl") +
"+/)"}
>
<FontAwesomeIcon icon={faRunning} />
</button>
<button
onClick={(e) => {
onThemeClick();
setTheme(localStorage.theme ?? "light");
e.stopPropagation();
}}
title="Toggle theme"
>
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
</button>
</div>
</div>
</div>
{rhs}
</div>
);
}