openvibers/openvibe.shared guide.js
guide.js
| 1 | ; |
| 2 | |
| 3 | (function (root) { |
| 4 | |
| 5 | /** Point at a real hooked control without activating it or covering it. The host owns pointer styling. */ |
| 6 | function startQuestGuide({ step, nextHref, root = document, onDone = () => {}, checkDone = null, pollMs = 5000 } = {}) { |
| 7 | if (!step || !/^[a-z][a-z0-9_.-]{1,63}$/.test(step.hook || '') || !step.event_type) return () => {}; |
| 8 | const control = root.querySelector(`[data-ov-guide="${CSS.escape(step.hook)}"]`); |
| 9 | if (!control) return () => {}; |
| 10 | control.scrollIntoView({ block: 'center', behavior: 'smooth' }); |
| 11 | const note = root.createElement('aside'); |
| 12 | note.setAttribute('data-ov-quest-guide', ''); |
| 13 | note.setAttribute('role', 'status'); |
| 14 | note.textContent = step.help; |
| 15 | control.insertAdjacentElement('beforebegin', note); |
| 16 | let finished = false; |
| 17 | const done = (event) => { |
| 18 | if (finished || !event || event.detail?.event_type !== step.event_type) return; |
| 19 | finished = true; |
| 20 | if (poll) clearInterval(poll); |
| 21 | note.replaceChildren(root.createTextNode('Done. ')); |
| 22 | if (nextHref) { |
| 23 | const link = root.createElement('a'); link.href = nextHref; link.textContent = 'Next step'; note.append(link); |
| 24 | } |
| 25 | onDone(step); |
| 26 | }; |
| 27 | root.addEventListener('ov:quest:step-done', done); |
| 28 | const poll = typeof checkDone === 'function' && !step.done ? setInterval(async () => { |
| 29 | try { const latest = await checkDone(); if (latest?.step?.done) done({ detail: { event_type: step.event_type } }); } |
| 30 | catch { /* the Events receipt may still be arriving; the next read retries */ } |
| 31 | }, pollMs) : null; |
| 32 | if (poll && typeof poll.unref === 'function') poll.unref(); |
| 33 | const dismiss = (event) => { |
| 34 | if (event.key === 'Escape' || (event.type === 'pointerdown' && !note.contains(event.target) && !control.contains(event.target))) stop(); |
| 35 | }; |
| 36 | function stop() { if (poll) clearInterval(poll); root.removeEventListener('ov:quest:step-done', done); root.removeEventListener('keydown', dismiss); root.removeEventListener('pointerdown', dismiss); note.remove(); } |
| 37 | root.addEventListener('keydown', dismiss); root.addEventListener('pointerdown', dismiss); |
| 38 | if (step.done) done({ detail: { event_type: step.event_type } }); |
| 39 | return stop; |
| 40 | } |
| 41 | |
| 42 | /** Resolve only a short opaque step id. The host supplies an authenticated reader. */ |
| 43 | async function startFromLocation({ load, location = root.location, document: page = root.document, onDone } = {}) { |
| 44 | if (typeof load !== 'function' || !location || !page) return () => {}; |
| 45 | const id = new URL(location.href).searchParams.get('qstep'); |
| 46 | if (!id || !/^[A-Za-z0-9_-]{12,48}$/.test(id)) return () => {}; |
| 47 | const answer = await load(id); |
| 48 | if (!answer || !answer.step || !answer.step.hook) return () => {}; |
| 49 | // Many sites render the target after their page data arrives. Watch briefly instead of losing the guide. |
| 50 | const selector = `[data-ov-guide="${CSS.escape(answer.step.hook)}"]`; |
| 51 | if (!page.querySelector(selector) && page.defaultView?.MutationObserver) await new Promise((resolve) => { |
| 52 | const observer = new page.defaultView.MutationObserver(() => { |
| 53 | if (page.querySelector(selector)) { observer.disconnect(); clearTimeout(timer); resolve(); } |
| 54 | }); |
| 55 | const timer = setTimeout(() => { observer.disconnect(); resolve(); }, 15000); |
| 56 | observer.observe(page.documentElement, { childList: true, subtree: true }); |
| 57 | }); |
| 58 | return startQuestGuide({ step: answer.step, nextHref: answer.next_href, root: page, onDone, checkDone: () => load(id) }); |
| 59 | } |
| 60 | |
| 61 | const api = { startQuestGuide, startFromLocation }; |
| 62 | if (typeof module === 'object' && module.exports) module.exports = api; |
| 63 | if (root && root.document) root.OpenVibeQuestGuide = api; |
| 64 | })(typeof window !== 'undefined' ? window : globalThis); |
| 65 |