Table clicks and highlights
Messages between the 3D embeds and your page: learn which table a guest clicked, and colour the tables your app offers, selects or blocks.
Updated · View as Markdown
The 3D view of both embeds talks to the page that frames it with window.postMessage. Your app draws every card, form and decision. FloorPlanCraft only reports which table was clicked and colours the tables you name.
For a booking app the tables are its areas, and for a play venue its zones. Every message is a plain object with a type and "v": 1. Unknown types and fields are ignored on both sides.
Messages from the embed
type | When | Message |
|---|---|---|
fpc:ready | Once per load, when the scene is drawn and its tables can be clicked; again when the scene reloads | { "type": "fpc:ready", "v": 1, "tables": ["T1", "T2"] } |
fpc:table-click | A click or tap on a table | { "type": "fpc:table-click", "v": 1, "label": "T7" } |
fpc:embed-expired | The live embed's address expired | { "type": "fpc:embed-expired" } |
tablesinfpc:readyandlabelinfpc:table-clickare zone names exactly as the plan draws them, in plan order.- The tables are the zones of the shown level whose names your pushes report as tables, compared ignoring case. Stations and other zones never are.
- In the live embed, the scene reloads when the viewer moves the replay to another stored minute, presses Live or picks another service day.
fpc:readywaits for the first floor, and does not come while a replay plays.
What counts as a click
One press and release of the primary mouse button, or of one finger, that never moves 6 CSS pixels or more. Orbiting, panning, wheel or pinch zoom and a second finger are never clicks. The table, its chairs and the zone's floor inside its outline all count as that table; a wall in the way does not, unless the view's see-through walls are on. A click only posts the message: the view opens nothing itself.
Messages to the embed
type | Message | Effect |
|---|---|---|
fpc:highlight | { "type": "fpc:highlight", "v": 1, "tables": [{ "label": "T2", "state": "available" }, { "label": "T7", "state": "selected" }], "dimOthers": true } | Replaces the whole highlight |
fpc:focus | { "type": "fpc:focus", "v": 1, "label": "T7" } | Eases the camera to frame that table in 450 ms, at once with reduced motion, keeping its direction. A press on the view stops it. An unknown label does nothing. |
state | Look |
|---|---|
available | A raised white band with dark edges along the zone's edge, and a pointer cursor over it |
selected | A wide violet outline and a low, see-through violet fence around the zone |
unavailable | A pale, see-through veil over the zone and what stands in it |
"dimOthers": trueveils every table not in the list."tables": []clears everything, whateverdimOtherssays.- Labels are matched ignoring case. A label with no table is ignored, and a later entry for the same table wins.
- In the live embed, the highlight sits on each zone's edge or is see-through, so the live colour stays readable.
- A highlight stays in that frame only: it is never stored or seen by another viewer. It lasts through live updates and replay moves until you replace or clear it; a reload starts with none.
- Limits: at most 500 entries; each
label1 to 64 characters and not blank;stateone of the three above;dimOthersa boolean when present;vexactly1. A message that breaks any of them is ignored whole, without an error.
Example
const ORIGIN = "https://floorplancraft.com";
const frame = document.querySelector<HTMLIFrameElement>("#live-floor")!;
function send(message: object): void {
frame.contentWindow?.postMessage(message, ORIGIN);
}
window.addEventListener("message", (event: MessageEvent) => {
if (event.origin !== ORIGIN || event.source !== frame.contentWindow) return;
const message = event.data as { type?: string; tables?: string[]; label?: string };
if (message.type === "fpc:ready") {
// Offer the tables that are free in your app, and fade the rest.
send({ type: "fpc:highlight", v: 1, tables: [{ label: "T2", state: "available" }], dimOthers: true });
}
if (message.type === "fpc:table-click" && typeof message.label === "string") {
send({ type: "fpc:highlight", v: 1, tables: [{ label: message.label, state: "selected" }], dimOthers: true });
send({ type: "fpc:focus", v: 1, label: message.label });
// Then open your own card for that table.
}
});Where messages work
- The live embed always has them.
- The plan embed has them only in its 3D view (
?view=3d), while a connection that holds the plan's share has Live 3D, and only when the plan's allowed origins name at least one origin. This is decided when the page loads: reload the plan embed when a new session starts. - Without them, the plan embed sends nothing and ignores highlights. Treat no
fpc:readywithin 10 seconds as no 3D picking.
Security
- The embed posts only to
window.parent, once per allowed origin with that origin as the target, never"*". The allowed origins are your connection'sembedOriginsfor the live embed, and the plan's allowed origins for the plan embed. - It reads a message only from
window.parent, from one of those origins, and only when the message's shape is exactly right. Anything else is dropped without an error. - Before you trust a message, check that
event.originishttps://floorplancraft.comand thatevent.sourceis the frame's window. - Only table labels and states cross, in either direction.