nitropage
Version:
A free and open source, extensible visual page builder based on SolidStart.
163 lines (146 loc) • 4.11 kB
text/typescript
import { Accessor, createEffect, startTransition, untrack } from "solid-js";
import { reconcile, SetStoreFunction } from "solid-js/store";
import { EditorUiState, State } from "../../../../types";
import { SettingsContext } from "../../../lib/context/admin";
import {
createIframeSync,
makeSyncedSetStore,
Message,
PostMessage,
} from "../../../lib/primitives/iframeSync";
import { deserialize, serialize } from "../../../utils/object/serialize";
import { makeStateReconciler } from "./reconcile";
import {
makeApplyShortcut,
makeParentShortcutListener,
makeViewportShortcutListener,
} from "./shortcuts";
export const makeSetters = (
postMessage: PostMessage,
setState_: SetStoreFunction<State>,
setEditorUi_: SetStoreFunction<EditorUiState>,
) => {
const [setState, onMessage1] = makeSyncedSetStore(
"NP_SET_STATE",
setState_,
postMessage,
);
const [setEditorUi, onMessage2] = makeSyncedSetStore(
"NP_SET_EDITORUI",
setEditorUi_,
postMessage,
);
const onMessage = (message: Message) => {
if (onMessage1(message)) return true;
return onMessage2(message);
};
return [setState, setEditorUi, onMessage] as const;
};
const FOCUS_ELEMENT_TYPE = "npFocusElement";
const RECONCILE_STATE_TYPE = "npReconcileState";
const RECONCILE_EDITOR_UI_TYPE = "npReconcileEditorUi";
export const iframeSyncId = "npEditor";
export const createParentSync = ({
iframeRef,
state,
setStateLocal,
editorUi,
setEditorUiLocal,
settingsContext,
}: {
iframeRef: Accessor<HTMLIFrameElement>;
state: State;
editorUi: EditorUiState;
setStateLocal: SetStoreFunction<State>;
setEditorUiLocal: SetStoreFunction<EditorUiState>;
settingsContext: (typeof SettingsContext)["defaultValue"];
}) => {
const [connected, postMessage] = createIframeSync({
id: iframeSyncId,
iframeRef,
onMessage(message) {
if (onSetterMessage(message)) return;
if (onShortcutMessage(message)) return;
},
});
const [setState, setEditorUi, onSetterMessage] = makeSetters(
postMessage,
setStateLocal,
setEditorUiLocal,
);
const onShortcutMessage = makeParentShortcutListener(
makeApplyShortcut({
stateContext: [state, setState],
editorUiContext: [editorUi, setEditorUi],
settingsContext,
}),
);
const postReconcileState = (state: Partial<State>) =>
postMessage({ type: RECONCILE_STATE_TYPE, data: serialize(state) });
const focusElement = (id: string) => {
postMessage({
type: FOCUS_ELEMENT_TYPE,
data: id,
});
};
createEffect(() => {
if (!connected()) {
return;
}
untrack(() => {
postMessage({
type: RECONCILE_EDITOR_UI_TYPE,
data: serialize(editorUi),
});
postReconcileState(state);
});
});
return [
connected,
setState,
setEditorUi,
postReconcileState,
focusElement,
] as const;
};
export const createViewportSync = ({
state,
setStateLocal,
setEditorUiLocal,
focusElementById,
}: {
state: State;
setStateLocal: SetStoreFunction<State>;
setEditorUiLocal: SetStoreFunction<EditorUiState>;
focusElementById: (id: string) => void;
}) => {
const reconcileState = makeStateReconciler(state, setStateLocal);
const [connected, postMessage] = createIframeSync({
id: iframeSyncId,
onMessage(message) {
if (onMessage(message)) return;
switch (message.type) {
case FOCUS_ELEMENT_TYPE:
focusElementById(message.data);
break;
case RECONCILE_STATE_TYPE:
startTransition(() => reconcileState(deserialize(message.data)));
break;
case RECONCILE_EDITOR_UI_TYPE:
startTransition(() =>
setEditorUiLocal(
reconcile(deserialize(message.data), { key: null, merge: true }),
),
);
break;
}
},
});
const [setState, setEditorUi, onMessage] = makeSetters(
postMessage,
setStateLocal,
setEditorUiLocal,
);
makeViewportShortcutListener(postMessage);
return [connected, setState, setEditorUi] as const;
};