MMOframework
Pourquoi l'IA Playground Bibliothèques Documentation Rechercher Agence
FR EN
Index
# Guided tours — `MMO.guide` and `MMO\client\Guide` A guided tour shows a user where things are, one highlighted element at a time: a spotlight around the element, a popover telling about it (previous, next, leave), the page under it dimmed and not clickable — a tour only shows, it never saves or sends anything. The `?` button of the top bar offers the page's tours, their articles and the guides. | piece | where | what it does | |---|---|---| | `MMO\client\Guide` | `src/Guide.php` (an `aModule`, listened to by `src/bootstrap.php` on `DocumentRender`) | reads the page's `{page}.tour.json`, filters it by the user's rights, translates it, writes it in the page with the tours the user saw | | `MMO.guide` | `src/js/core/Guide.js` | runs the tours in the browser: the spotlight, the popover, the `?` button and its menu, the side panel of the articles, where the user left a tour | | styles | `src/styles/parts/guided-tours.scs` | `.guide-backdrop`, `.guide-spotlight`, `.guide-popover`, `.guide-menu`, `.guide-panel` | | tests | `tests/guide/GuideTest.php` | the filtering by rights and level, the translation, `Seen()`, the script written in the page | Two kinds of tour, the same shape: **a page's** (a json file beside its `.phtml`, served by `MMO\client\Guide`) and **an element's** (given by the element's own script, `MMO.guide.add()`). Pick the page's for a page of the site (a form, a list, a dashboard page); the element's for something that is not a page of its own and travels from page to page (an editor, a widget): mmo.editor's `` is the example. ## A page's tour: `{page}.tour.json` Written beside the page's `.phtml` (`declaration.phtml` → `declaration.tour.json`). When the page is rendered, `MMO\client\Guide` reads it, takes out what the user may not see, translates it, and hands it to `MMO.guide.register()`. ```json { "id": "declaration", //where it is remembered as seen: the user's config `guides/{id}` "title": "Declare a work accident", //English keys, translated (\MMO\core\I18n) "article": "guide/declare", //optional: a page of the guides about the same task (`articleUrl`) "autostart": true, //started by itself the first time "allowed": "data#create", //optional: the scope needed (`!scope`: must not have), or a list "level": "standard", //optional: from this display level up (MMO.modes) "levels": ["simple"], //optional: at these levels only "steps": [{ "title": "The path", "text": "Plain text: a blank line starts a paragraph.", "target": "#workflow #stepper li", //css; `host >>> inner` goes into host's shadow root "contains": "Identification", //optional: the first target whose text has it "placement": "right", //bottom (default), top, left, right "optional": true, //skipped when the target is not on the page "wait": 8000, //how long to wait for the target (ms, 2000 by default), for slow lists "allowed": "admin", "level": "expert" }] } ``` (The comments are for this page only: the file is strict json.) - **Rights**: `allowed` is a scope (`admin.activeEntity`), a scope the user must *not* have (`!admin.activeEntity`), or a list, all needed. A step the user may not see is taken out; so is the whole tour when the user may not see it or no step is left. The user is mmo.oauth's (`\MMO\oauth\user()`); without mmo.oauth, a tour or a step that needs a scope is never shown (a `!scope` one is). - **Levels** (mmo.client's display modes, `MMO\client\Modes` / `MMO.modes`, [Modes.md](../Modes.md)): `level` shows it from that level up, `levels` at those levels only; a level the application does not offer says nothing. The levels are checked in the browser (`MMO.guide.matchesLevel()`), against the level shown. - **Texts**: `title`, and each step's `title` and `text`, are English keys translated by `\MMO\core\I18n` (the application's `i18n` folder): the json stays in English. `text` is plain text, never html; a blank line starts a paragraph. - **Targets**: a css selector of the page; `host >>> inner` looks inside `host`'s shadow root (as many `>>>` as there are shadow roots), `contains` picks the first match whose text holds it. A target on the page but hidden (a phone layout, a closed tab) is not waited for. A step without a target, or whose target never shows, is shown in the middle of the screen — unless it is `optional`, then it is skipped. Prefer stable hooks (an `id`, a `data-*`) to classes of the design system. - **Errors**: a broken json is a `\JsonException` (a bug, not a page without help). A file without `id` or without steps is no tour. - **PHP**: `Guide::Tour($phtml, $user)` gives the filtered tour (null: none), `Guide::Seen($user)` what the user went through (their config's `guides`). ## An element's tour: `MMO.guide.add(tour)` An element that is not a page of its own gives its tour from its script, in the same shape — but nothing filters it on the server: its texts are translated by the element (`MMO.i18n.$('…')`), and its rights are the element's to check before adding it. ```js init() { super.init(); MMO.guide?.add({id: 'mmo-my-widget', title: MMO.i18n.$('My widget'), autostart: true, steps: [ {title: MMO.i18n.$('The filters'), text: MMO.i18n.$('Narrow the list here.'), target: 'mmo-my-widget >>> [data-part="filters"]'} ]}); } disconnectedCallback() { if (super.disconnectedCallback) {super.disconnectedCallback();} MMO.guide?.remove('mmo-my-widget'); } ``` `add()` answers whether the tour was kept (one of another display level, or left without steps, is not). An element whose content depends on the level adds one tour per level and lets `level`/`levels` keep the right one: mmo.editor's `` adds `mmo-page-editor-simple`, `-standard` and `-expert` (`MMO.editor.PageEditor#tour`, its `docs/mcp/Pages.md`), and has its own `?` in its bar, since its expert level covers the top bar. ## In the browser | | | |---|---| | `MMO.guide.register(tour)` | the page's own tour (what `MMO\client\Guide` writes): first in the menu | | `MMO.guide.add(tour)`, `.remove(id)`, `.get(id)` | an element's tour | | `MMO.guide.start(idOrTour = the page's, step = 0)`, `.stop(status)` | run one; `stop('done' \| 'skipped')` remembers it, `stop()` does not | | `MMO.guide.tours`, `.current`, `.seen` | the page's tours, the one running, what the user saw (by id) | | `MMO.guide.home` | the page of the guides (`MMO\client\Guide`'s `articles`): the menu's « All the guides » | | `MMO.guide.extra.push({icon, label, href, onclick})` | another item of the `?` menu | | `MMO.guide.openPanel(url)` | a page of the guides in the shell's side panel | - Started by the `?` menu, by itself the first time (`autostart`, not `seen`, no other tour running), or by `?guide={id}` (`?guide=1`: the page's) in the url — what the guides' articles link to; the parameter is taken out of the address once used. - Keyboard: → / Enter next, ← previous, Escape leaves; the focus stays in the popover. A click outside it does nothing but bring the focus back to « Next ». - Where the user left it (`{status: done|skipped, step, title, of, date}`) is saved in their config (`MMO.user.setConfig('guides/{id}')`, mmo.oauth, quietly) and in the browser (`localStorage['mmo-guides']`, for an application without users); the server hands what the config says to every page (`MMO.guide.seen`), for the elements' tours. - The `?` button: the page's own `#btn-guide` when it has one, else one `MMO.guide` adds to ``'s `actions` slot when a tour is registered (`.has-tour` while the page has one). Its menu: each tour, its article (opened in the shell's side panel, which then browses the guides: a link to the guides opens there, a `?guide=` link to the page the user is on starts its tour), « All the guides », and `MMO.guide.extra`. An article is the guides' own page, its `.guide-article` (else its `main`), read without its scripts; « Open in full page » leaves the panel. - Styles: `src/styles/parts/guided-tours.scs` (not `guide.scs`: a namespace of Simple CSS is its file's name, and an application may have its own `guide.scs`), over the shell's popovers and under its toasts. ## Configuration ```json "MMO\\client\\Guide": {"articles": "guide", "enabled": true} ``` - `articles`: the page of the guides (a path of the site's pages, made a url by `PageController::PageUrl()`), the menu's « All the guides »; none by default. - `enabled`: false hands no tour to the pages (the elements' tours still run). Only the page's own document gets a tour, once a request: the other documents rendered for it (a preview, a mail) have none. ## Writing a tour: checklist 1. One task per tour, five to ten steps; the first says what the page is for, the last what to do next. 2. Target what is always there for that user and level; `optional` what may not be (an empty list's first row). 3. A list or a workflow drawn after the page needs `wait`. 4. Every text an English key, translated in the application's `i18n` (`fr.php`) — a page's tour on the server, an element's in its `src/js/i18n/*.js`. 5. Try it with `?guide=1` in the url, at every display level and with a user lacking each `allowed`; reset what a user saw by removing `guides/{id}` from their config and `mmo-guides` from the browser's storage.