1
0
silverbullet/web/editor_ui.tsx

308 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2024-01-25 10:42:36 +00:00
import { safeRun } from "../common/util.ts";
2023-07-14 12:22:26 +00:00
import { Confirm, Prompt } from "./components/basic_modals.tsx";
import { CommandPalette } from "./components/command_palette.tsx";
import { FilterList } from "./components/filter.tsx";
import { PageNavigator } from "./components/page_navigator.tsx";
import { TopBar } from "./components/top_bar.tsx";
import reducer from "./reducer.ts";
import { Action, AppViewState, initialViewState } from "./types.ts";
import {
2024-01-25 18:46:08 +00:00
closeSearchPanel,
2024-01-25 10:42:36 +00:00
featherIcons,
2023-07-14 12:22:26 +00:00
preactRender,
2023-07-14 14:48:35 +00:00
runScopeHandlers,
2023-07-14 12:22:26 +00:00
useEffect,
useReducer,
} from "./deps.ts";
2023-07-14 14:56:20 +00:00
import type { Client } from "./client.ts";
2023-07-14 12:22:26 +00:00
import { Panel } from "./components/panel.tsx";
import { h } from "./deps.ts";
2023-09-04 18:02:35 +00:00
import { sleep } from "$sb/lib/async.ts";
2024-01-25 10:42:36 +00:00
import { parseCommand } from "../common/command.ts";
2023-07-14 12:22:26 +00:00
export class MainUI {
viewState: AppViewState = initialViewState;
viewDispatch: (action: Action) => void = () => {};
constructor(private client: Client) {
2023-07-14 14:48:35 +00:00
// Make keyboard shortcuts work even when the editor is in read only mode or not focused
globalThis.addEventListener("keydown", (ev) => {
if (!client.editorView.hasFocus) {
2024-01-25 18:46:08 +00:00
const target = ev.target as HTMLElement;
if (target.className === "cm-textfield" && ev.key === "Escape") {
// Search panel is open, let's close it
console.log("Closing search panel");
closeSearchPanel(client.editorView);
return;
} else if (target.closest(".cm-content")) {
2023-07-14 14:48:35 +00:00
// In some cm element, let's back out
return;
}
2024-01-24 13:03:14 +00:00
console.log("Delegated keydown", ev, "to editor");
if (runScopeHandlers(client.editorView, ev, "editor")) {
2023-07-14 14:48:35 +00:00
ev.preventDefault();
}
}
});
globalThis.addEventListener("touchstart", (ev) => {
// Launch the page picker on a two-finger tap
if (ev.touches.length === 2) {
ev.stopPropagation();
ev.preventDefault();
client.startPageNavigate("page");
2023-07-14 14:48:35 +00:00
}
// Launch the command palette using a three-finger tap
if (ev.touches.length === 3) {
ev.stopPropagation();
ev.preventDefault();
this.viewDispatch({
type: "show-palette",
context: client.getContext(),
2023-07-14 14:48:35 +00:00
});
}
});
2023-07-14 12:22:26 +00:00
}
ViewComponent() {
const [viewState, dispatch] = useReducer(reducer, initialViewState);
this.viewState = viewState;
this.viewDispatch = dispatch;
2023-12-21 17:37:50 +00:00
const client = this.client;
2023-07-14 12:22:26 +00:00
useEffect(() => {
if (viewState.currentPage) {
document.title = viewState.currentPage;
}
}, [viewState.currentPage]);
useEffect(() => {
2023-12-21 17:37:50 +00:00
client.tweakEditorDOM(
client.editorView.contentDOM,
2023-07-27 09:41:44 +00:00
);
2023-07-14 12:22:26 +00:00
}, [viewState.uiOptions.forcedROMode]);
useEffect(() => {
this.client.rebuildEditorState();
this.client.dispatchAppEvent("editor:modeswitch");
2023-07-14 12:22:26 +00:00
}, [viewState.uiOptions.vimMode]);
useEffect(() => {
document.documentElement.dataset.theme = viewState.uiOptions.darkMode
? "dark"
: "light";
}, [viewState.uiOptions.darkMode]);
useEffect(() => {
// Need to dispatch a resize event so that the top_bar can pick it up
globalThis.dispatchEvent(new Event("resize"));
}, [viewState.panels]);
return (
<>
{viewState.showPageNavigator && (
<PageNavigator
allPages={viewState.allPages}
2023-12-21 17:37:50 +00:00
currentPage={client.currentPage}
mode={viewState.pageNavigatorMode}
2023-12-21 17:37:50 +00:00
completer={client.miniEditorComplete.bind(client)}
2023-07-14 12:22:26 +00:00
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
onNavigate={(page) => {
dispatch({ type: "stop-navigate" });
setTimeout(() => {
2023-12-21 17:37:50 +00:00
client.focus();
2023-07-14 12:22:26 +00:00
});
if (page) {
safeRun(async () => {
await client.navigate({ page });
2023-07-14 12:22:26 +00:00
});
}
}}
/>
)}
{viewState.showCommandPalette && (
<CommandPalette
onTrigger={(cmd) => {
dispatch({ type: "hide-palette" });
if (cmd) {
dispatch({ type: "command-run", command: cmd.command.name });
cmd
.run()
.catch((e: any) => {
console.error("Error running command", e.message);
})
2024-01-25 18:46:08 +00:00
.then((returnValue: any) => {
2023-07-14 12:22:26 +00:00
// Always be focusing the editor after running a command
2024-01-25 18:46:08 +00:00
if (returnValue !== false) {
client.focus();
}
2023-07-14 12:22:26 +00:00
});
}
}}
2023-12-21 17:37:50 +00:00
commands={client.getCommandsByContext(viewState)}
2023-07-14 12:22:26 +00:00
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
2023-12-21 17:37:50 +00:00
completer={client.miniEditorComplete.bind(client)}
2023-07-14 12:22:26 +00:00
recentCommands={viewState.recentCommands}
settings={this.client.settings}
2023-07-14 12:22:26 +00:00
/>
)}
{viewState.showFilterBox && (
<FilterList
label={viewState.filterBoxLabel}
placeholder={viewState.filterBoxPlaceHolder}
options={viewState.filterBoxOptions}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
allowNew={false}
2023-12-21 17:37:50 +00:00
completer={client.miniEditorComplete.bind(client)}
2023-07-14 12:22:26 +00:00
helpText={viewState.filterBoxHelpText}
onSelect={viewState.filterBoxOnSelect}
/>
)}
{viewState.showPrompt && (
<Prompt
message={viewState.promptMessage!}
defaultValue={viewState.promptDefaultValue}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
2023-12-21 17:37:50 +00:00
completer={client.miniEditorComplete.bind(client)}
2023-07-14 12:22:26 +00:00
callback={(value) => {
dispatch({ type: "hide-prompt" });
viewState.promptCallback!(value);
}}
/>
)}
{viewState.showConfirm && (
<Confirm
message={viewState.confirmMessage!}
callback={(value) => {
dispatch({ type: "hide-confirm" });
viewState.confirmCallback!(value);
}}
/>
)}
<TopBar
pageName={viewState.currentPage}
notifications={viewState.notifications}
2023-08-16 09:40:31 +00:00
syncFailures={viewState.syncFailures}
2023-07-14 12:22:26 +00:00
unsavedChanges={viewState.unsavedChanges}
isLoading={viewState.isLoading}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
progressPerc={viewState.progressPerc}
2023-12-21 17:37:50 +00:00
completer={client.miniEditorComplete.bind(client)}
onClick={() => {
2023-12-21 17:37:50 +00:00
client.editorView.scrollDOM.scrollTop = 0;
}}
2023-07-14 12:22:26 +00:00
onRename={async (newName) => {
if (!newName) {
// Always move cursor to the start of the page
2023-12-21 17:37:50 +00:00
client.editorView.dispatch({
2023-07-14 12:22:26 +00:00
selection: { anchor: 0 },
});
2023-12-21 17:37:50 +00:00
client.focus();
2023-07-14 12:22:26 +00:00
return;
}
console.log("Now renaming page to...", newName);
await client.system.system.invokeFunction(
"index.renamePageCommand",
2023-07-14 12:22:26 +00:00
[{ page: newName }],
);
2023-12-21 17:37:50 +00:00
client.focus();
2023-07-14 12:22:26 +00:00
}}
actionButtons={[
2024-01-25 10:42:36 +00:00
...(!window.silverBulletConfig.syncOnly &&
!viewState.settings.hideSyncButton)
2023-08-30 15:25:54 +00:00
// If we support syncOnly, don't show this toggle button
2023-08-29 19:17:29 +00:00
? [{
2024-01-25 10:42:36 +00:00
icon: featherIcons.RefreshCw,
description: this.client.syncMode
? "Currently in Sync mode, click to switch to Online mode"
: "Currently in Online mode, click to switch to Sync mode",
class: this.client.syncMode ? "sb-enabled" : undefined,
2023-08-29 19:17:29 +00:00
callback: () => {
(async () => {
const newValue = !this.client.syncMode;
2023-08-29 19:17:29 +00:00
if (newValue) {
2023-09-04 18:02:35 +00:00
localStorage.setItem("syncMode", "true");
this.client.flashNotification(
2023-09-04 18:02:35 +00:00
"Now switching to sync mode, one moment please...",
);
await sleep(1000);
location.reload();
2023-08-29 19:17:29 +00:00
} else {
2023-09-04 18:02:35 +00:00
localStorage.removeItem("syncMode");
this.client.flashNotification(
2023-09-07 10:52:40 +00:00
"Now switching to online mode, one moment please...",
2023-09-04 18:02:35 +00:00
);
await sleep(1000);
location.reload();
2023-08-29 19:17:29 +00:00
}
})().catch(console.error);
},
}]
: [],
2024-01-25 10:42:36 +00:00
...viewState.settings.actionButtons.map((button) => {
const parsedCommand = parseCommand(button.command);
return {
icon: (featherIcons as any)[button.icon],
description: button.description || "",
callback: () => {
client.runCommandByName(
parsedCommand.name,
parsedCommand.args,
);
},
href: "",
};
}),
2023-07-14 12:22:26 +00:00
]}
rhs={!!viewState.panels.rhs.mode && (
<div
className="panel"
style={{ flex: viewState.panels.rhs.mode }}
/>
)}
lhs={!!viewState.panels.lhs.mode && (
<div
className="panel"
style={{ flex: viewState.panels.lhs.mode }}
/>
)}
/>
<div id="sb-main">
{!!viewState.panels.lhs.mode && (
2023-12-21 17:37:50 +00:00
<Panel config={viewState.panels.lhs} editor={client} />
2023-07-14 12:22:26 +00:00
)}
<div id="sb-editor" />
{!!viewState.panels.rhs.mode && (
2023-12-21 17:37:50 +00:00
<Panel config={viewState.panels.rhs} editor={client} />
2023-07-14 12:22:26 +00:00
)}
</div>
{!!viewState.panels.modal.mode && (
<div
className="sb-modal"
style={{ inset: `${viewState.panels.modal.mode}px` }}
>
2023-12-21 17:37:50 +00:00
<Panel config={viewState.panels.modal} editor={client} />
2023-07-14 12:22:26 +00:00
</div>
)}
{!!viewState.panels.bhs.mode && (
<div className="sb-bhs">
2023-12-21 17:37:50 +00:00
<Panel config={viewState.panels.bhs} editor={client} />
2023-07-14 12:22:26 +00:00
</div>
)}
</>
);
}
render(container: Element) {
// const ViewComponent = this.ui.ViewComponent.bind(this.ui);
preactRender(h(this.ViewComponent.bind(this), {}), container);
}
}