UNPKG

nitropage

Version:

A free and open source, extensible visual page builder based on SolidStart.

219 lines (189 loc) 6.52 kB
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)}`;