nitropage
Version:
A free and open source, extensible visual page builder based on SolidStart.
219 lines (189 loc) • 6.52 kB
text/typescript
import { useIsAppleDevice } from "#lib/primitives/device";
import { useContext } from "solid-js";
import { EditorUiContext, SettingsContext } from "../../../lib/context/admin";
import { StateContext } from "../../../lib/context/page";
import { makeEventListener } from "../../../lib/primitives/eventListener";
import { Message, PostMessage } from "../../../lib/primitives/iframeSync";
import { isEditableElement } from "../../../utils/dom/isEditableElement";
import { throttleWheel } from "../../../utils/function/throttle";
import { tools } from "../components/toolbar/tools";
import { clampZoom, zoomStep } from "../components/toolbar/zoom";
import { makeCancelElementCreation } from "./element/makeCancelElementCreation";
import { makeSavePageHandler } from "./page";
import { makeViewportToolToggle } from "./viewport";
/**
* Normally defaultPrevented events will not be processed as keyboard shortcuts
* This utility can be used to process such events anyways
*/
export const allowShortcuts = (
evt: KeyboardEvent & { __shortcutsAllowed?: true },
) => {
evt.__shortcutsAllowed = true;
return evt;
};
const defaultPrevented = (
evt: KeyboardEvent & { __shortcutsAllowed?: true },
) => (evt.__shortcutsAllowed ? false : evt.defaultPrevented);
const toolShortcuts = Object.entries(tools).reduce(
(acc, [k, v]) => {
acc[v.shortcut.toLowerCase()] = k as any;
return acc;
},
{} as Record<string, keyof typeof tools>,
);
const compactKeyboardEvent = (evt: KeyboardEvent) => {
const targetEl = evt.target as HTMLElement;
if (
evt.key === "Escape" &&
(targetEl.classList.contains("ql-editor") ||
targetEl instanceof HTMLInputElement)
) {
window.getSelection()?.removeAllRanges();
targetEl.blur();
return;
}
const modifierKey = useIsAppleDevice() ? evt.metaKey : evt.ctrlKey;
const isSaveShortcut = modifierKey && (evt.key === "s" || evt.key === "p");
const overridesOS = isSaveShortcut;
if (overridesOS) {
evt.preventDefault();
}
if (!modifierKey && isEditableElement(targetEl)) {
return;
}
return {
overridesOS,
key: evt.key,
modifier: modifierKey,
alt: evt.altKey,
shift: evt.shiftKey,
};
};
const cancellableKeyboardEvent = (
cb: (data: ReturnType<typeof compactKeyboardEvent>) => void,
) => {
return async (evt: KeyboardEvent) => {
const data = compactKeyboardEvent(evt);
if (!data) return;
await new Promise(requestAnimationFrame);
if (!data.overridesOS && defaultPrevented(evt)) return;
cb(data);
};
};
type Shortcut = ReturnType<typeof compactKeyboardEvent>;
export const makeShortcutListener = (cb: (shortcut: Shortcut) => void) => {
makeEventListener(() => document, "keydown", cancellableKeyboardEvent(cb));
const throttledCb = throttleWheel((evt) => {
const key = evt.deltaY < 0 ? "+" : "-";
cb({ overridesOS: true, key, modifier: false, alt: false, shift: false });
});
makeEventListener(
() => document,
"wheel",
(e) => {
if (!e.ctrlKey) return;
if (e.defaultPrevented) return;
e.preventDefault();
throttledCb(e);
},
{ passive: false },
);
};
const SHORTCUT_TYPE = "npShortcut";
export const makeViewportShortcutListener = (postMessage: PostMessage) => {
makeShortcutListener((data) => postMessage({ type: SHORTCUT_TYPE, data }));
};
export const makeParentShortcutListener = (
applyShortcut: ReturnType<typeof makeApplyShortcut>,
) => {
makeShortcutListener(applyShortcut);
return (message: Message) => {
if (message.type !== SHORTCUT_TYPE) return;
applyShortcut(message.data);
return true;
};
};
export const makeApplyShortcut = ({
stateContext = useContext(StateContext),
editorUiContext = useContext(EditorUiContext),
settingsContext = useContext(SettingsContext),
} = {}) => {
const [state] = stateContext!;
const [editorUi, setEditorUi] = editorUiContext!;
const savePage = makeSavePageHandler({
stateContext,
editorUiContext,
settingsContext,
});
const cancelElementCreation = makeCancelElementCreation({
stateContext,
editorUiContext,
});
const toggleViewportTool = makeViewportToolToggle({
stateContext,
editorUiContext,
});
return (shortcut: Shortcut) => {
if (!shortcut || editorUi.movingElementId || editorUi.deletingElementId) {
return;
}
if (shortcut.modifier) {
const publish = shortcut.key === "p";
const save = shortcut.key === "s";
if (!editorUi.creatingElementId && (save || publish)) {
savePage(publish);
return;
}
}
// Originally the plan here was to bind these to shortcut.modifier
// But on OSX, if CMD+Shift+Number is pressed, the numberic key value is not resolved to the keyboard layout
// E.g. CMD+Shift+Digit1 would be "+" with CH-Layout, but KeyboardEvent.key does list "1"
if (shortcut.key === "-" || shortcut.key === "+") {
const nextZoom =
shortcut.key === "+"
? editorUi.viewport.zoom + zoomStep
: editorUi.viewport.zoom - zoomStep;
setEditorUi((d) => (d.viewport.zoom = clampZoom(nextZoom)));
return;
}
if (shortcut.key === "Backspace" && editorUi.selectedElementId) {
const parentSlotId =
state.elements[editorUi.selectedElementId].parentSlotId;
const parentElementId = parentSlotId
? state.slots[parentSlotId].parentElementId
: undefined;
setEditorUi(
(d) =>
(d.selectedElementId =
parentElementId !== "root" ? parentElementId : undefined),
);
return;
}
if (!shortcut.modifier && editorUi.viewport.tool !== "locked") {
const tool = toolShortcuts[shortcut.key];
if (tool) {
toggleViewportTool(tool);
return;
}
}
if (shortcut.key === "Escape") {
if (editorUi.creatingElementId) {
cancelElementCreation();
return;
}
if (editorUi.viewport.tool !== "idle") {
toggleViewportTool("idle");
return;
}
setEditorUi((d) => (d.selectedElementId = undefined));
}
};
};
export const MODIFIER_PLACEHOLDER = "Modifier";
/**
* Replaces generic "Modifier" placeholder in shortcuts with the platform specific one
*/
export const localizeModifier = (shortcut: string) =>
shortcut.replace(MODIFIER_PLACEHOLDER, useIsAppleDevice() ? "Cmd" : "Ctrl");
export const shortcutTitle = (text: string, shortcut: string) =>
`${text} — ${localizeModifier(shortcut)}`;