mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-03 02:27:57 +09:00
196 lines
6.2 KiB
TypeScript
196 lines
6.2 KiB
TypeScript
import { getAllPages, getPagePath, isAbsoluteUrl } from "@webstudio-is/sdk";
|
|
import {
|
|
compilePathnamePattern,
|
|
tokenizePathnamePattern,
|
|
} from "@webstudio-is/project-build/runtime";
|
|
import { matchPathnameRoutes } from "@webstudio-is/wsauth";
|
|
import { toWebstudioParams } from "@webstudio-is/react-sdk";
|
|
import { $selectedPage } from "~/shared/nano-states";
|
|
import { selectPage } from "~/shared/nano-states";
|
|
import { $isPreviewMode, $selectedPageHash } from "~/shared/nano-states";
|
|
import { $pages } from "~/shared/sync/data-stores";
|
|
import { $currentSystem, updateCurrentSystem } from "~/shared/system";
|
|
|
|
const getSelectedPagePathname = () => {
|
|
const pages = $pages.get();
|
|
const page = $selectedPage.get();
|
|
if (page && pages) {
|
|
const tokens = tokenizePathnamePattern(getPagePath(page.id, pages));
|
|
const system = $currentSystem.get();
|
|
return compilePathnamePattern(tokens, system.params);
|
|
}
|
|
};
|
|
|
|
const switchPageAndUpdateSystem = (href: string, formData?: FormData) => {
|
|
const pages = $pages.get();
|
|
if (pages === undefined) {
|
|
return;
|
|
}
|
|
// preserve pathname when not specified in href/action
|
|
if (href === "" || href.startsWith("?")) {
|
|
const pathname = getSelectedPagePathname();
|
|
if (pathname) {
|
|
href = `${pathname}${href}`;
|
|
}
|
|
}
|
|
// preserve also search params when navigate with hash
|
|
if (href.startsWith("#")) {
|
|
const pathname = getSelectedPagePathname();
|
|
if (pathname) {
|
|
const system = $currentSystem.get();
|
|
const searchParams = new URLSearchParams(
|
|
system.search as Record<string, string>
|
|
);
|
|
href = `${pathname}?${searchParams}${href}`;
|
|
}
|
|
}
|
|
const pageHref = new URL(href, "https://any-valid.url");
|
|
const matchedPage = matchPathnameRoutes(
|
|
getAllPages(pages).map((page) => ({
|
|
pattern: getPagePath(page.id, pages),
|
|
value: page,
|
|
})),
|
|
pageHref.pathname
|
|
);
|
|
if (matchedPage) {
|
|
const { value: page, params } = matchedPage;
|
|
// populate search params with form data values if available
|
|
if (formData) {
|
|
for (const [key, value] of formData.entries()) {
|
|
pageHref.searchParams.set(key, value.toString());
|
|
}
|
|
}
|
|
const search = Object.fromEntries(pageHref.searchParams);
|
|
$selectedPageHash.set({ hash: pageHref.hash });
|
|
selectPage(page.id);
|
|
updateCurrentSystem({
|
|
params: toWebstudioParams(getPagePath(page.id, pages), params),
|
|
search,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const subscribeInterceptedEvents = () => {
|
|
const handleClick = (event: MouseEvent) => {
|
|
if (!(event.target instanceof Element)) {
|
|
return;
|
|
}
|
|
const isPreviewMode = $isPreviewMode.get();
|
|
|
|
// Prevent forwarding the click event on an input element when the associated label has a "for" attribute
|
|
// and prevent checkbox or radio inputs changing when clicked
|
|
if (event.target.closest("label[for]") || event.target.closest("input")) {
|
|
if (isPreviewMode) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
|
|
const a = event.target.closest("a");
|
|
if (a) {
|
|
if (isPreviewMode) {
|
|
// use attribute instead of a.href to get raw unresolved value
|
|
const href = a.getAttribute("href") ?? "";
|
|
if (isAbsoluteUrl(href)) {
|
|
window.open(href, "_blank");
|
|
// relative paths can be safely downloaded
|
|
} else if (a.hasAttribute("download")) {
|
|
return;
|
|
} else {
|
|
switchPageAndUpdateSystem(href);
|
|
}
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
// prevent invoking submit with buttons in canvas mode
|
|
// because form with prevented submit still invokes validation
|
|
if (event.target.closest("button")) {
|
|
if (isPreviewMode) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
if (!(event.target instanceof Element)) {
|
|
return;
|
|
}
|
|
const isPreviewMode = $isPreviewMode.get();
|
|
|
|
if (event.target.closest("select")) {
|
|
if (isPreviewMode) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (event: SubmitEvent) => {
|
|
if ($isPreviewMode.get()) {
|
|
const form =
|
|
event.target instanceof HTMLFormElement ? event.target : undefined;
|
|
if (form === undefined) {
|
|
return;
|
|
}
|
|
// use attribute instead of form.action to get raw unresolved value
|
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-action
|
|
const action = form.getAttribute("action") ?? "";
|
|
// lower case just for safety
|
|
const method = form.method.toLowerCase();
|
|
if (method === "get" && isAbsoluteUrl(action) === false) {
|
|
switchPageAndUpdateSystem(action, new FormData(form));
|
|
}
|
|
}
|
|
// prevent submitting the form when clicking a button type submit
|
|
event.preventDefault();
|
|
};
|
|
|
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
if (!(event.target instanceof Element)) {
|
|
return;
|
|
}
|
|
if ($isPreviewMode.get()) {
|
|
return;
|
|
}
|
|
if (
|
|
event.target instanceof HTMLInputElement ||
|
|
event.target instanceof HTMLTextAreaElement
|
|
) {
|
|
// prevent typing in inputs only in canvas mode
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
// Note: Event handlers behave unexpectedly when used inside a dialog component.
|
|
// In Dialogs, React intercepts and processes events before they reach our handlers.
|
|
// To ensure consistent behavior across all components, we're using event capturing.
|
|
// This allows us to intercept events before React gets a chance to handle them.
|
|
document.documentElement.addEventListener("click", handleClick, {
|
|
capture: true,
|
|
});
|
|
document.documentElement.addEventListener("submit", handleSubmit, {
|
|
capture: true,
|
|
});
|
|
|
|
document.documentElement.addEventListener("keydown", handleKeydown);
|
|
|
|
document.documentElement.addEventListener("pointerdown", handlePointerDown);
|
|
|
|
return () => {
|
|
document.documentElement.removeEventListener(
|
|
"pointerdown",
|
|
handlePointerDown
|
|
);
|
|
document.documentElement.removeEventListener("click", handleClick, {
|
|
capture: true,
|
|
});
|
|
document.documentElement.removeEventListener("submit", handleSubmit, {
|
|
capture: true,
|
|
});
|
|
document.documentElement.removeEventListener("keydown", handleKeydown);
|
|
};
|
|
};
|