> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elementlabs.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Pages

> Add your own screen to the map, described as content the map draws or as your own interface embedded in it.

A page is a screen of your own reached from a tab along the top of the map. While it is open the map
is a blurred backdrop: it does not pan, its markers are hidden, and every click and key belongs to
the page.

Use a page for a list, a form or a summary that belongs next to the map — a job board, a garage, a
property list. For a different **map** — other markers, another camera — use a
[view](/products/map/reference/views) instead.

## Contract

```lua theme={null}
exports.element_map:RegisterPage(page)
exports.element_map:OpenPage('jobs')
```

| Export           | Signature                                 |
| ---------------- | ----------------------------------------- |
| `RegisterPage`   | `(page: table) => boolean`                |
| `UpdatePage`     | `(id: string, patch: table) => boolean`   |
| `SetPageContent` | `(id: string, content: table) => boolean` |
| `RemovePage`     | `(id: string) => boolean`                 |
| `OpenPage`       | `(id: string) => void`                    |
| `ClosePage`      | `() => boolean`                           |
| `OpenPageId`     | `() => string \| nil`                     |

`RegisterPage` answers `false` when the page it was given cannot be drawn, which is also what
happens to a page whose fields are of the wrong type: what the map cannot render it refuses rather
than showing an empty screen. The reason is written to the console, so a refused page does not have
to be guessed at. `RemovePage` answers whether there was a page of that id to remove.

Registering an id twice replaces the first. `OpenPage` opens the map when it is closed, so a page can
be reached from a command or a key of your own resource.

## Inputs

### The page

| Field      | Type                 | Default               | Meaning                                                              |
| ---------- | -------------------- | --------------------- | -------------------------------------------------------------------- |
| `id`       | `string`             | required              | The id used by `OpenPage` and reported by the events. No spaces.     |
| `label`    | `string`             | required              | The text on the tab.                                                 |
| `order`    | `number`             | `0`                   | Where the tab sits among the others.                                 |
| `badge`    | `number` or `string` | none                  | Drawn on the tab. A count of what is waiting inside.                 |
| `disabled` | `boolean`            | `false`               | The tab is shown but cannot be chosen.                               |
| `blur`     | `boolean`            | `visual.settingsBlur` | Whether the map behind the page is blurred.                          |
| `content`  | table                | required              | A document, below, or `{ url = '...' }` to embed your own interface. |

### The document

```lua theme={null}
content = {
  schemaVersion = 1,
  title = 'Jobs',
  subtitle = 'Three contracts waiting',
  sections = { { title = 'Available', wide = true, items = { ... } } },
}
```

A section is a panel. Two sit side by side; `wide = true` makes one take the full width.

| Item     | Fields                                | Meaning                                                     |
| -------- | ------------------------------------- | ----------------------------------------------------------- |
| `text`   | `value`, `muted`                      | A line of prose.                                            |
| `stat`   | `label`, `value`                      | A label and a number, read as a pair.                       |
| `list`   | `id`, `rows`, `selectable`, `empty`   | Rows, optionally choosable.                                 |
| `button` | `id`, `label`, `variant`, `disabled`  | `variant` is `primary`, `ghost` or `danger`.                |
| `toggle` | `id`, `label`, `value`, `disabled`    | An on/off switch.                                           |
| `input`  | `id`, `label`, `placeholder`, `value` | Reported when the player leaves the field or presses Enter. |
| `select` | `id`, `label`, `value`, `options`     | `options` is a list of `{ value, label }`.                  |

A row of a list is `{ id, label, meta, icon, color, disabled, actions }`. `icon` and `color` are a
marker sprite and colour, drawn the way the legend draws them. `actions` is a list of
`{ id, label, variant }` shown at the end of the row.

An item the map does not know is skipped, so a page written for a newer map still shows the rest of
its content.

### Embedding your own interface

```lua theme={null}
content = { url = 'https://cfx-nui-myresource/page.html' }
```

The address must be `https`, and its host must be the NUI origin of a resource on this server —
`cfx-nui-<resource>` and nothing more, so a registrable domain such as `cfx-nui-x.example.com` is not
one — or a host listed whole in `hud.pageEmbedHosts`. An address carrying a userinfo section
(`https://cfx-nui-x@example.com/`) loads `example.com` and is refused, as is one whose host is
percent-encoded or non-ASCII. Anything else is refused.

The frame is sandboxed and is not same-origin with the map, so the embedded page cannot reach the
map's interface or its callbacks. It talks to its own resource, the way any NUI page does.

## Outputs

| Event                | Arguments       | When                                                   |
| -------------------- | --------------- | ------------------------------------------------------ |
| `el:map:page:enter`  | `id: string`    | The page is on screen.                                 |
| `el:map:page:leave`  | `id: string`    | The page was left, closed, or the map closed under it. |
| `el:map:page:action` | `action: table` | Something in the page was used.                        |

The action table is `{ page, action, row, value }`.

| Field    | Present when                                                      |
| -------- | ----------------------------------------------------------------- |
| `page`   | Always. The id of the page.                                       |
| `action` | Always. The id of the item, or of the list a row was chosen from. |
| `row`    | A row was chosen, or an action belonging to a row was pressed.    |
| `value`  | A toggle, an input or a select changed.                           |

The map holds no state of its own for a page: answer an action by sending the page back with
`SetPageContent`.

A [view](/products/map/reference/views) has the last word on which pages it hosts: a page its
`surface.pages` does not list is neither drawn as a tab nor opened by `OpenPage` while that view is
on screen. The page stays registered and comes back with the next view that allows it.

## Errors

`RegisterPage` answers `false` and says why in the console when a page has no id, no label, or
content that is neither a document nor an embeddable address. Nothing is registered: there is no
half-registered page and no tab.

An item the map does not understand is dropped from the page rather than refusing the whole of it,
so a page written for a newer map still shows the rest of its content. A section whose `items` is
missing is dropped the same way.

An embed whose address does not pass the rule above is refused at registration, so it never reaches
the interface. The interface applies the same rule again before it draws the frame, and shows
"Embedded page refused" in place of the page if it ever disagrees.

`SetPageContent` and `RemovePage` answer `false` for a page that is not registered.

## Example

```lua theme={null}
local function jobsPage()
  return {
    schemaVersion = 1,
    title = 'Jobs',
    sections = { {
      title = 'Available',
      wide = true,
      items = { {
        kind = 'list',
        id = 'contracts',
        selectable = true,
        rows = {
          { id = 'trucker', label = 'Long haul', meta = '$1,240', icon = 477, color = 5,
            actions = { { id = 'accept', label = 'Accept', variant = 'primary' } } },
        },
      } },
    } },
  }
end

AddEventHandler('el:map:ready', function()
  exports.element_map:RegisterPage({ id = 'jobs', label = 'Jobs', order = 1, content = jobsPage() })
end)

AddEventHandler('el:map:page:action', function(event)
  if event.page ~= 'jobs' then
    return
  end
  if event.action == 'accept' then
    startJob(event.row)
    exports.element_map:ClosePage()
  end
end)
```

## Related

* [Exports](/products/map/reference/exports)
* [Events](/products/map/reference/events)
* [Views](/products/map/reference/views)
