UNPKG

nitropage

Version:

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

250 lines (216 loc) 7.25 kB
import { BlueprintData, EditorUiState, Element, State } from "#../types"; import { EditorUiContext, SettingsContext } from "#lib/context/admin"; import { StateContext } from "#lib/context/page"; import type { Settings } from "#lib/server/settings"; import { titleCase } from "#utils/string/titleCase"; import { createId } from "@paralleldrive/cuid2"; import { cloneDeep } from "es-toolkit"; import { Accessor, createEffect, createMemo, onCleanup, onMount, useContext, } from "solid-js"; import { produce } from "solid-js/store"; import { makeEventListener } from "../../../lib/primitives/eventListener"; import { useRouterPage } from "../../admin/data"; import { createSlot } from "./slot"; export const duplicateElement = function ( element: Element, draft: State, ids: Record<string, string>, parentSlotId?: string, siblings?: string[], ) { const nextId = (ids[element.id] ??= createId()); const clonedElement = cloneDeep(element); clonedElement.id = nextId; clonedElement.historyId = undefined; clonedElement.slots = {}; if (parentSlotId != null) { clonedElement.parentSlotId = parentSlotId; } draft.elements[clonedElement.id] = clonedElement; if (siblings) { siblings.push(clonedElement.id); } else { const siblingElements = draft.slots[element.parentSlotId!].elements; const origIndex = siblingElements.indexOf(element.id); siblingElements.splice(origIndex + 1, 0, clonedElement.id); } for (const [slotKey, slotId] of Object.entries(element.slots)) { const prevSlot = draft.slots[slotId]; const nextSlotId = (ids[slotId] ??= createId()); const nextSlot = createSlot(slotKey, clonedElement.id, nextSlotId); draft.slots[nextSlot.id] = nextSlot; clonedElement.slots[slotKey] = nextSlot.id; for (const childElementId of prevSlot.elements) { duplicateElement( draft.elements[childElementId], draft, ids, nextSlot.id, nextSlot.elements, ); } } }; export const useSelectedElement = function () { const [editorUi] = useContext(EditorUiContext)!; const [state] = useContext(StateContext)!; return createMemo(function () { if (!editorUi.selectedElementId) { return; } return state.elements[editorUi.selectedElementId]; }); }; export const useSelectedElementPreset = function ( element = useSelectedElement(), ) { const [state] = useContext(StateContext)!; return createMemo(function () { const presetId = element()?.presetId; if (!presetId) { return; } return state.presets[presetId]; }); }; export const blueprintDataTitle = function <T extends BlueprintData>( data: T, fallback: string, ) { const titleFn = data.title; return titleFn ? titleFn() : titleCase(fallback); }; export const useDarkMode = function (editorUi: EditorUiState) { createEffect(function () { document.documentElement.style["colorScheme"] = editorUi.viewport.darkMode ? "dark" : "light"; }); }; export const useSetting = function <T extends keyof Settings>( key: T, from: Accessor<"pages" | "projects">, ) { const [settings, setSettings] = useContext(SettingsContext)!; const getter = (): Settings[T]["value"] => settings[from()][key]?.value; const setter = function ( fn: (draft: Settings[T]["value"]) => Settings[T]["value"], ) { setSettings( produce(function (draft) { const from_ = from(); const result = fn(draft[from_][key]?.value); draft[from_][key]!.value = result; draft[from_][key]!.from = result != null ? from_ : "default"; }), ); }; return [getter, setter] as [typeof getter, typeof setter]; }; export const isElementInViewport = (el: globalThis.Element) => { const screenBorder = 150; const elRect = el.getBoundingClientRect(); const elCord = { top: elRect.y, bottom: elRect.y + elRect.height, // TODO: Also check the horizontal axis // left: elRect.x, // right: elRect.x + elRect.width, }; const vCord = { top: window.screenTop + screenBorder, bottom: window.screenTop + window.innerHeight - screenBorder, // left: window.screenLeft, // right: window.screenLeft + window.innerWidth, }; // The viewport is inside of the element (for large elements) if (elCord.top <= vCord.top && elCord.bottom >= vCord.bottom) { return true; } // The top of the element is in the viewport if (elCord.top >= vCord.top && elCord.top <= vCord.bottom) { return true; } // The bottom of the element is in the viewport if (elCord.bottom >= vCord.top && elCord.bottom <= vCord.bottom) { return true; } return false; }; export const useScrollRestoration = function ( editorUi: EditorUiState, focusElementById: (id: string) => void, ) { // Store information about the document, before it changed to a new tool // The old information is used to calculate a proper scroll position in % // This is needed because the restoration effect below might run too late, after the document already has started to change let restorationInfo = { scrollHeight: 0, scrollTop: 0 }; let lastTool = editorUi.viewport.tool; onMount(function () { let interval = setInterval(function () { if (lastTool != editorUi.viewport.tool) return; restorationInfo.scrollHeight = document.documentElement.scrollHeight; restorationInfo.scrollTop = document.documentElement.scrollTop; }, 1000 / 20); onCleanup(() => clearInterval(interval)); }); let scrollRaf: number | undefined = undefined; makeEventListener( () => window, "wheel", (evt) => { if (scrollRaf == null) return; evt.preventDefault(); }, { passive: false }, ); // Restore the scroll position when switching between different editor tools createEffect(function (initial) { lastTool = editorUi.viewport.tool; const creatingElementId = editorUi.creatingElementId; if (initial) { return; } const document = window.document.documentElement; const outerHeight = document.clientHeight; const oldScroll = restorationInfo.scrollTop; const oldHeight = restorationInfo.scrollHeight; const precision = 100; const scrollPercent = Math.round((precision / (oldHeight - outerHeight)) * oldScroll) / precision; if (scrollRaf != undefined) { cancelAnimationFrame(scrollRaf); scrollRaf = undefined; } // The slot animations take 0.08s (80ms) -> 5 frames -> we add one extra to smooth it out const restoreScroll = (frames = 6) => { scrollRaf = requestAnimationFrame(() => { if (creatingElementId) { focusElementById(creatingElementId); } else { const nextHeight = document.scrollHeight; const newScroll = (nextHeight - outerHeight) * scrollPercent; document.scrollTo({ behavior: "instant", top: newScroll }); } frames -= 1; if (frames == 0) { scrollRaf = undefined; return; } restoreScroll(frames); }); }; restoreScroll(); }, true); }; export const useIsLayout = () => { const page = useRouterPage(); return createMemo(() => page()?.type === "layout"); };