Refactor and renaming
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { isMacLike } from "../../common/util";
|
||||
import { FilterList } from "./filter";
|
||||
import { faPersonRunning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { AppCommand } from "../hooks/command";
|
||||
import { FilterOption } from "@silverbulletmd/common/types";
|
||||
|
||||
export function CommandPalette({
|
||||
commands,
|
||||
onTrigger,
|
||||
}: {
|
||||
commands: Map<string, AppCommand>;
|
||||
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,
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FilterOption } from "@silverbulletmd/common/types";
|
||||
|
||||
function magicSorter(a: FilterOption, b: FilterOption): number {
|
||||
if (a.orderId && b.orderId) {
|
||||
return a.orderId < b.orderId ? -1 : 1;
|
||||
}
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
|
||||
function escapeRegExp(str: string): string {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
}
|
||||
|
||||
function fuzzyFilter(pattern: string, options: FilterOption[]): FilterOption[] {
|
||||
let closeMatchRegex = escapeRegExp(pattern);
|
||||
closeMatchRegex = closeMatchRegex.split(/\s+/).join(".*?");
|
||||
closeMatchRegex = closeMatchRegex.replace(/\\\//g, ".*?\\/.*?");
|
||||
const distantMatchRegex = escapeRegExp(pattern).split("").join(".*?");
|
||||
const r1 = new RegExp(closeMatchRegex, "i");
|
||||
const r2 = new RegExp(distantMatchRegex, "i");
|
||||
let matches = [];
|
||||
if (!pattern) {
|
||||
return options;
|
||||
}
|
||||
for (let option of options) {
|
||||
let m = r1.exec(option.name);
|
||||
if (m) {
|
||||
matches.push({
|
||||
...option,
|
||||
orderId: 100000 - (options.length - m[0].length - m.index),
|
||||
});
|
||||
} else {
|
||||
// Let's try the distant matcher
|
||||
var m2 = r2.exec(option.name);
|
||||
if (m2) {
|
||||
matches.push({
|
||||
...option,
|
||||
orderId: 10000 - (options.length - m2[0].length - m2.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function simpleFilter(
|
||||
pattern: string,
|
||||
options: FilterOption[]
|
||||
): FilterOption[] {
|
||||
const lowerPattern = pattern.toLowerCase();
|
||||
return options.filter((option) => {
|
||||
return option.name.toLowerCase().includes(lowerPattern);
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
options.sort(magicSorter)
|
||||
);
|
||||
const [selectedOption, setSelectionOption] = useState(0);
|
||||
|
||||
let selectedElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function filterUpdate(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
updateFilter(e.target.value);
|
||||
}
|
||||
|
||||
function updateFilter(originalPhrase: string) {
|
||||
const searchPhrase = originalPhrase.toLowerCase();
|
||||
|
||||
if (searchPhrase) {
|
||||
let foundExactMatch = false;
|
||||
let results = simpleFilter(searchPhrase, options);
|
||||
results = results.sort(magicSorter);
|
||||
if (allowNew && !foundExactMatch) {
|
||||
results.push({
|
||||
name: originalPhrase,
|
||||
hint: newHint,
|
||||
});
|
||||
}
|
||||
setMatchingOptions(results);
|
||||
} else {
|
||||
let results = options.sort(magicSorter);
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const returnEl = (
|
||||
<div className="filter-box">
|
||||
<div className="header">
|
||||
<label>{label}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
ref={searchBoxRef}
|
||||
onChange={filterUpdate}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
// 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":
|
||||
onSelect(matchingOptions[selectedOption]);
|
||||
e.preventDefault();
|
||||
break;
|
||||
case "Escape":
|
||||
onSelect(undefined);
|
||||
break;
|
||||
case " ":
|
||||
if (completePrefix && !text) {
|
||||
updateFilter(completePrefix);
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="help-text"
|
||||
dangerouslySetInnerHTML={{ __html: helpText }}
|
||||
></div>
|
||||
<div className="result-list">
|
||||
{matchingOptions && matchingOptions.length > 0
|
||||
? matchingOptions.map((option, idx) => (
|
||||
<div
|
||||
key={"" + idx}
|
||||
ref={selectedOption === idx ? selectedElementRef : undefined}
|
||||
className={
|
||||
selectedOption === idx ? "selected-option" : "option"
|
||||
}
|
||||
onMouseOver={(e) => {
|
||||
setSelectionOption(idx);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(option);
|
||||
}}
|
||||
>
|
||||
<span className="icon">
|
||||
{icon && <FontAwesomeIcon icon={icon} />}
|
||||
</span>
|
||||
<span className="name">{option.name}</span>
|
||||
{option.hint && <span className="hint">{option.hint}</span>}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
selectedElementRef.current?.scrollIntoView({
|
||||
block: "nearest",
|
||||
});
|
||||
});
|
||||
|
||||
return returnEl;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FilterList } from "./filter";
|
||||
import { FilterOption, PageMeta } from "@silverbulletmd/common/types";
|
||||
|
||||
export function PageNavigator({
|
||||
allPages,
|
||||
onNavigate,
|
||||
currentPage,
|
||||
}: {
|
||||
allPages: Set<PageMeta>;
|
||||
onNavigate: (page: string | undefined) => void;
|
||||
currentPage?: string;
|
||||
}) {
|
||||
let options: FilterOption[] = [];
|
||||
for (let pageMeta of allPages) {
|
||||
if (currentPage && currentPage === pageMeta.name) {
|
||||
continue;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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("/") + "/";
|
||||
}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base target="_top">
|
||||
<script src="panel_page.ts"/>
|
||||
</head>
|
||||
<body>
|
||||
Send me HTML
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
// @ts-ignore
|
||||
import iframeHtml from "bundle-text:./panel.html";
|
||||
|
||||
export function Panel({ html, flex }: { html: string; flex: number }) {
|
||||
const iFrameRef = useRef<HTMLIFrameElement>(null);
|
||||
useEffect(() => {
|
||||
function loadContent() {
|
||||
if (iFrameRef.current?.contentWindow) {
|
||||
iFrameRef.current.contentWindow.postMessage({
|
||||
type: "html",
|
||||
html: html,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!iFrameRef.current) {
|
||||
return;
|
||||
}
|
||||
let iframe = iFrameRef.current;
|
||||
iframe.onload = loadContent;
|
||||
loadContent();
|
||||
return () => {
|
||||
iframe.onload = null;
|
||||
};
|
||||
}, [html]);
|
||||
|
||||
useEffect(() => {
|
||||
let messageListener = (evt: any) => {
|
||||
if (evt.source !== iFrameRef.current!.contentWindow) {
|
||||
return;
|
||||
}
|
||||
let data = evt.data;
|
||||
if (!data) return;
|
||||
console.log("Got message from panel", data);
|
||||
};
|
||||
window.addEventListener("message", messageListener);
|
||||
return () => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="panel" style={{ flex }}>
|
||||
<iframe srcDoc={iframeHtml} ref={iFrameRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
window.addEventListener("message", (message) => {
|
||||
const data = message.data;
|
||||
switch (data.type) {
|
||||
case "html":
|
||||
document.body.innerHTML = data.html;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function sendEvent(data: any) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "event",
|
||||
data: data,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
//
|
||||
// setInterval(() => {
|
||||
// self.sendEvent("testing");
|
||||
// }, 2000);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import * as util from "../../common/util";
|
||||
|
||||
export function StatusBar({ editorView }: { editorView?: EditorView }) {
|
||||
let wordCount = 0,
|
||||
readingTime = 0;
|
||||
if (editorView) {
|
||||
let text = editorView.state.sliceDoc();
|
||||
wordCount = util.countWords(text);
|
||||
readingTime = util.readingTime(wordCount);
|
||||
}
|
||||
return (
|
||||
<div id="bottom">
|
||||
<div className="inner">
|
||||
{wordCount} words | {readingTime} min
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Notification } from "../types";
|
||||
|
||||
function prettyName(s: string | undefined): string {
|
||||
if (!s) {
|
||||
return "";
|
||||
}
|
||||
return s.replaceAll("/", " / ");
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
pageName,
|
||||
unsavedChanges,
|
||||
notifications,
|
||||
onClick,
|
||||
lhs,
|
||||
rhs,
|
||||
}: {
|
||||
pageName?: string;
|
||||
unsavedChanges: boolean;
|
||||
notifications: Notification[];
|
||||
onClick: () => void;
|
||||
lhs?: React.ReactNode;
|
||||
rhs?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div id="top" onClick={onClick}>
|
||||
{lhs}
|
||||
<div className="main">
|
||||
<div className="inner">
|
||||
{/*<span className={`icon ${unsavedChanges ? "unsaved" : "saved"}`}>*/}
|
||||
{/* <FontAwesomeIcon icon={faFileLines} />*/}
|
||||
{/*</span>*/}
|
||||
|
||||
<span
|
||||
className={`current-page ${unsavedChanges ? "unsaved" : "saved"}`}
|
||||
>
|
||||
{prettyName(pageName)}
|
||||
</span>
|
||||
{/*<button*/}
|
||||
{/* onClick={(e) => {*/}
|
||||
{/* // @ts-ignore*/}
|
||||
{/* window.syncer();*/}
|
||||
{/* e.stopPropagation();*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* Sync*/}
|
||||
{/*</button>*/}
|
||||
{notifications.length > 0 && (
|
||||
<div className="status">
|
||||
{notifications.map((notification) => (
|
||||
<div key={notification.id}>{notification.message}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{rhs}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user