1/** 2 * booking-context.js â socle composable du front de réservation. 3 * 4 * Miroir vanilla (servi brut au navigateur, AUCUN build) du contexte partagé 5 * entre un `BookingProvider` et ses enfants (calendar, slots, participants, 6 * summary, checkoutâ¦). Trois responsabilités, équivalent vanilla d'un React 7 * Context + store : 8 * - createStore() : conteneur d'état pub/sub (généralise BookingState) 9 * - registre providers : un store par id de provider â OcoBooking.providers 10 * - resolveProvider(el) : résolution robuste enfant â provider (fix bug A5) 11 * 12 * Contrat public stable (cf. docs/component-architecture.md §4, 13 * docs/booking-implementation-spec.md §2). Testé dans 14 * src/lib/__tests__/bookingContext.test.ts (Vitest + jsdom). 15 */ 16(function (root, factory) { 17 if (typeof module === 'object' && module.exports) { 18 module.exports = factory(); 19 } else { 20 root.OcoBooking = factory(); 21 } 22}(typeof self !== 'undefined' ? self : this, function () { 23 'use strict'; 24 25 /** Registre global : un store par identifiant de provider. */ 26 var providers = {}; 27 28 /** 29 * createStore â conteneur d'état pub/sub. 30 * @param {object} [initial] état initial 31 * @returns {{getState:Function, setState:Function, subscribe:Function, notify:Function}} 32 */ 33 function createStore(initial) { 34 var state = assign({}, initial); 35 var listeners = []; 36 var events = {}; // bus d'événements nommés (on/emit), distinct des abonnés d'état 37 38 function getState() { return state; } 39 40 function setState(updates) { 41 state = assign(assign({}, state), updates); 42 notify(); 43 } 44 45 function notify() { 46 // snapshot : un (dés)abonnement pendant la diffusion ne perturbe pas l'itération 47 var snapshot = listeners.slice(); 48 for (var i = 0; i < snapshot.length; i++) { 49 snapshot[i](state); 50 } 51 } 52 53 function subscribe(cb) { 54 if (typeof cb !== 'function') { return function () {}; } 55 listeners.push(cb); 56 cb(state); // appel immédiat avec l'état courant (aligné sur BookingState.subscribe) 57 return function unsubscribe() { 58 var idx = listeners.indexOf(cb); 59 if (idx !== -1) { listeners.splice(idx, 1); } 60 }; 61 } 62 63 // --- Bus d'événements nommés (on/emit) --------------------------------- 64 // Canal distinct du flux d'état : porte les intentions ponctuelles (ex. 65 // 'oco:open' émis par le launcher pour ouvrir le flux à une étape précise), 66 // qui ne sont pas un changement d'état durable et ne doivent pas re-rendre 67 // les abonnés via notify(). 68 function on(event, cb) { 69 if (!event || typeof cb !== 'function') { return function () {}; } 70 (events[event] || (events[event] = [])).push(cb); 71 return function off() { 72 var arr = events[event]; 73 if (!arr) { return; } 74 var idx = arr.indexOf(cb); 75 if (idx !== -1) { arr.splice(idx, 1); } 76 }; 77 } 78 79 function emit(event, payload) { 80 var arr = events[event]; 81 if (!arr) { return; } 82 var snapshot = arr.slice(); // (dés)abonnement pendant la diffusion sans casser l'itération 83 for (var i = 0; i < snapshot.length; i++) { snapshot[i](payload); } 84 } 85 86 return { 87 getState: getState, setState: setState, subscribe: subscribe, notify: notify, 88 on: on, emit: emit, 89 }; 90 } 91 92 /** Enregistre/expose le store d'un provider. */ 93 function registerProvider(id, store) { 94 if (id == null || id === '') { return store; } 95 providers[String(id)] = store; 96 return store; 97 } 98 function getProvider(id) { 99 if (id == null || id === '') { return null; } 100 return providers[String(id)] || null; 101 } 102 function unregisterProvider(id) { 103 if (id != null) { delete providers[String(id)]; } 104 } 105 106 /** 107 * resolveProvider â retrouve le store du provider d'un élément enfant. 108 * Robuste à l'aplatissement / déplacement du DOM par Bricks (bug A5) : 109 * 1. imbriqué : closest('.oco-booking-provider') â data-oco-provider 110 * 2. frère/aplati : closest('[data-oco-provider-ref]') â registre 111 * 3. cas trivial : un seul provider enregistré sur la page 112 * @param {Element|null} element 113 * @returns {object|null} store ou null 114 */ 115 function resolveProvider(element) { 116 if (!element) { return null; }
117 var hasClosest = typeof element.closest === 'function'; 118 119 // 1. ascendance DOM (cas nominal imbriqué) 120 if (hasClosest) { 121 var host = element.closest('.oco-booking-provider'); 122 if (host) { 123 var byHost = getProvider(attr(host, 'data-oco-provider')); 124 if (byHost) { return byHost; } 125 } 126 } 127 128 // 2. référence explicite portée par le composant (cas frère / rendu aplati) 129 var refHost = hasClosest ? element.closest('[data-oco-provider-ref]') : null; 130 var byRef = getProvider(attr(refHost || element, 'data-oco-provider-ref')); 131 if (byRef) { return byRef; } 132 133 // 3. un seul provider sur la page â résolution sans ambiguïté 134 var ids = keys(providers); 135 if (ids.length === 1) { return providers[ids[0]]; } 136 137 return null; 138 } 139 140 // --- helpers (sans polyfills, compat navigateurs servis brut) --- 141 function assign(target, src) { 142 if (src) { 143 for (var k in src) { 144 if (Object.prototype.hasOwnProperty.call(src, k)) { target[k] = src[k]; } 145 } 146 } 147 return target; 148 } 149 function keys(o) { 150 var r = []; 151 for (var k in o) { if (Object.prototype.hasOwnProperty.call(o, k)) { r.push(k); } } 152 return r; 153 } 154 function attr(el, name) { 155 return el && typeof el.getAttribute === 'function' ? el.getAttribute(name) : null; 156 } 157 158 // --- Intention de recherche persistée ----------------------------------- 159 // Pré-remplit la pill search ET les modales de réservation, et survit au 160 // reload / changement de page. On ne persiste QUE l'intention non sensible 161 // (date, participants, service, durée) â JAMAIS de coordonnées (nom, email, 162 // téléphone). TTL court + validation à la lecture (une date passée est 163 // ignorée) pour ne pas pré-remplir une intention périmée. 164 var INTENT_KEY = 'oco_booking_intent'; 165 var INTENT_TTL_MS = 60 * 60 * 1000; // 1 heure 166 var INTENT_FIELDS = ['date', 'pax', 'serviceId', 'duration', 'slot']; 167 168 function globalRef() { 169 if (typeof self !== 'undefined') { return self; } 170 if (typeof window !== 'undefined') { return window; } 171 if (typeof globalThis !== 'undefined') { return globalThis; } 172 return null; 173 } 174 function nowMs() { return (new Date()).getTime(); } 175 function intentTodayIso() { 176 var d = new Date(); 177 var m = d.getMonth() + 1, day = d.getDate(); 178 return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day; 179 } 180 // Accès localStorage défensif (mode privé / quota / storage désactivé â null). 181 function safeStorage() { 182 try { 183 var g = globalRef(); 184 var s = g && g.localStorage; 185 if (!s) { return null; } 186 var probe = '__oco_probe__'; 187 s.setItem(probe, '1'); s.removeItem(probe); 188 return s; 189 } catch (e) { return null; } 190 } 191 192 function readIntent() { 193 var s = safeStorage(); 194 if (!s) { return {}; } 195 var raw; try { raw = s.getItem(INTENT_KEY); } catch (e) { return {}; } 196 if (!raw) { return {}; } 197 var obj; try { obj = JSON.parse(raw); } catch (e) { obj = null; }
198 if (!obj || typeof obj !== 'object') { try { s.removeItem(INTENT_KEY); } catch (e2) {} return {}; } 199 // TTL : intention périmée â purge + ignore. 200 if (!obj.ts || (nowMs() - obj.ts) > INTENT_TTL_MS) { 201 try { s.removeItem(INTENT_KEY); } catch (e3) {} 202 return {}; 203 } 204 var out = {}; 205 // Validation à la lecture : on ne ressort que des valeurs encore valides. 206 if (typeof obj.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(obj.date) && obj.date >= intentTodayIso()) { 207 out.date = obj.date; 208 } 209 if (obj.pax != null) { var p = parseInt(obj.pax, 10); if (!isNaN(p) && p >= 1) { out.pax = p; } } 210 if (obj.serviceId != null && String(obj.serviceId) !== '') { out.serviceId = String(obj.serviceId); } 211 if (obj.duration != null) { var d = parseInt(obj.duration, 10); if (!isNaN(d) && d > 0) { out.duration = d; } } 212 // Créneau choisi (heure de début) : sync barre OBV â modale de réservation. 213 if (obj.slot != null && String(obj.slot) !== '') { out.slot = String(obj.slot); } 214 return out; 215 } 216 217 // Ãcrit un sous-ensemble de champs d'intention (merge). null/'' efface le champ. 218 function writeIntent(partial) { 219 var s = safeStorage(); 220 if (!s || !partial || typeof partial !== 'object') { return; } 221 var cur = {}; 222 try { var raw = s.getItem(INTENT_KEY); if (raw) { cur = JSON.parse(raw) || {}; } } catch (e) { cur = {}; } 223 if (!cur || typeof cur !== 'object') { cur = {}; } 224 var touched = false; 225 for (var i = 0; i < INTENT_FIELDS.length; i++) { 226 var k = INTENT_FIELDS[i]; 227 if (Object.prototype.hasOwnProperty.call(partial, k)) { 228 var v = partial[k]; 229 if (v == null || v === '') { delete cur[k]; } else { cur[k] = v; } 230 touched = true; 231 } 232 } 233 if (!touched) { return; } 234 cur.ts = nowMs(); 235 try { s.setItem(INTENT_KEY, JSON.stringify(cur)); } catch (e4) {} 236 // Notifie les consommateurs d'un changement d'intention (ex. le listing 237 // date-first se re-rend quand la pill change date/participants). Bus 238 // découplé : la pill écrit l'intention, le listing l'écoute. 239 try { 240 var g = globalRef(); 241 if (g && typeof g.dispatchEvent === 'function' && typeof g.CustomEvent === 'function') { 242 g.dispatchEvent(new g.CustomEvent('oco:intent', { detail: readIntent() })); 243 } 244 } catch (e5) {} 245 } 246 247 function clearIntent() { 248 var s = safeStorage(); 249 if (!s) { return; } 250 try { s.removeItem(INTENT_KEY); } catch (e) {} 251 } 252 253 var intent = { 254 read: readIntent, 255 write: writeIntent, 256 clear: clearIntent, 257 KEY: INTENT_KEY, 258 TTL_MS: INTENT_TTL_MS, 259 }; 260 261 return { 262 providers: providers, 263 createStore: createStore, 264 registerProvider: registerProvider, 265 getProvider: getProvider, 266 unregisterProvider: unregisterProvider, 267 resolveProvider: resolveProvider, 268 intent: intent, 269 }; 270}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.