Skip to content
FloorPlanCraftSign in
Explore the planner
Browse the docs

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

typeWhenMessage
fpc:readyOnce 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-clickA click or tap on a table{ "type": "fpc:table-click", "v": 1, "label": "T7" }
fpc:embed-expiredThe live embed's address expired{ "type": "fpc:embed-expired" }
  • tables in fpc:ready and label in fpc:table-click are 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:ready waits 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

typeMessageEffect
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.
stateLook
availableA raised white band with dark edges along the zone's edge, and a pointer cursor over it
selectedA wide violet outline and a low, see-through violet fence around the zone
unavailableA pale, see-through veil over the zone and what stands in it
  • "dimOthers": true veils every table not in the list.
  • "tables": [] clears everything, whatever dimOthers says.
  • 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 label 1 to 64 characters and not blank; state one of the three above; dimOthers a boolean when present; v exactly 1. 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:ready within 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's embedOrigins for 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.origin is https://floorplancraft.com and that event.source is the frame's window.
  • Only table labels and states cross, in either direction.