@wcstack/geolocation 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,703 @@
1
+ const _config = {
2
+ autoTrigger: true,
3
+ triggerAttribute: "data-geotarget",
4
+ tagNames: {
5
+ geo: "wcs-geo",
6
+ },
7
+ };
8
+ function deepFreeze(obj) {
9
+ if (obj === null || typeof obj !== "object")
10
+ return obj;
11
+ Object.freeze(obj);
12
+ for (const key of Object.keys(obj)) {
13
+ deepFreeze(obj[key]);
14
+ }
15
+ return obj;
16
+ }
17
+ function deepClone(obj) {
18
+ if (obj === null || typeof obj !== "object")
19
+ return obj;
20
+ const clone = {};
21
+ for (const key of Object.keys(obj)) {
22
+ clone[key] = deepClone(obj[key]);
23
+ }
24
+ return clone;
25
+ }
26
+ let frozenConfig = null;
27
+ const config = _config;
28
+ function getConfig() {
29
+ if (!frozenConfig) {
30
+ frozenConfig = deepFreeze(deepClone(_config));
31
+ }
32
+ return frozenConfig;
33
+ }
34
+ function setConfig(partialConfig) {
35
+ if (typeof partialConfig.autoTrigger === "boolean") {
36
+ _config.autoTrigger = partialConfig.autoTrigger;
37
+ }
38
+ if (typeof partialConfig.triggerAttribute === "string") {
39
+ _config.triggerAttribute = partialConfig.triggerAttribute;
40
+ }
41
+ if (partialConfig.tagNames) {
42
+ Object.assign(_config.tagNames, partialConfig.tagNames);
43
+ }
44
+ frozenConfig = null;
45
+ }
46
+
47
+ /**
48
+ * Headless geolocation primitive. A thin, framework-agnostic wrapper around the
49
+ * Geolocation API exposed through the wc-bindable protocol.
50
+ *
51
+ * It has two phases, mirroring the two distinct shapes of the underlying API:
52
+ * - **one-shot** — `getCurrentPosition()` resolves a single fix (like FetchCore's
53
+ * one-shot `fetch()`), toggling `loading` around the async call.
54
+ * - **continuous** — `watch()` / `clearWatch()` stream fixes (like TimerCore's
55
+ * `start()` / `stop()`), toggling the `watching` flag.
56
+ *
57
+ * Every successful fix is published via the single `wcs-geo:position` event;
58
+ * `latitude` / `longitude` / `accuracy` / `coords` / `timestamp` are read from
59
+ * it through getters (mirroring how TimerCore exposes count/elapsed from one
60
+ * `wcs-timer:tick` event), so an observer that binds any of them is notified on
61
+ * every fix.
62
+ *
63
+ * Geolocation also has a permission gate absent from timer/websocket: the
64
+ * `permission` property reflects `navigator.permissions.query({name:
65
+ * "geolocation"})` (`prompt` / `granted` / `denied`, or `unsupported`) and
66
+ * tracks its live `change` event. It is a read-only sensor — there is no
67
+ * element-bound "send" path; element → state only.
68
+ */
69
+ class GeolocationCore extends EventTarget {
70
+ static wcBindable = {
71
+ protocol: "wc-bindable",
72
+ version: 1,
73
+ properties: [
74
+ { name: "position", event: "wcs-geo:position" },
75
+ { name: "latitude", event: "wcs-geo:position", getter: (e) => e.detail.latitude },
76
+ { name: "longitude", event: "wcs-geo:position", getter: (e) => e.detail.longitude },
77
+ { name: "accuracy", event: "wcs-geo:position", getter: (e) => e.detail.accuracy },
78
+ { name: "coords", event: "wcs-geo:position", getter: (e) => e.detail.coords },
79
+ { name: "timestamp", event: "wcs-geo:position", getter: (e) => e.detail.timestamp },
80
+ { name: "watching", event: "wcs-geo:watching-changed" },
81
+ { name: "loading", event: "wcs-geo:loading-changed" },
82
+ { name: "error", event: "wcs-geo:error" },
83
+ { name: "permission", event: "wcs-geo:permission-changed" },
84
+ ],
85
+ commands: [
86
+ { name: "getCurrentPosition", async: true },
87
+ { name: "watch" },
88
+ { name: "clearWatch" },
89
+ ],
90
+ };
91
+ _target;
92
+ _watchId = null;
93
+ _position = null;
94
+ _watching = false;
95
+ _loading = false;
96
+ _error = null;
97
+ _permission = "prompt";
98
+ // Live PermissionStatus handle (when the Permissions API is available), kept
99
+ // so the `change` listener can be removed on dispose().
100
+ _permissionStatus = null;
101
+ // True once a permission subscription has been (or is being) established, and
102
+ // reset by dispose(). Guards reinitPermission() so the first connect after
103
+ // construction does not double-subscribe, while a reconnect after dispose()
104
+ // does re-subscribe.
105
+ _permissionSubscribed = false;
106
+ // Monotonic id of the current permission query. Bumped by every _initPermission()
107
+ // and by dispose(). Each in-flight query captures its id and, on resolve, bails
108
+ // unless it is still current — so a query superseded by a rapid (synchronous)
109
+ // disconnect→reconnect, or one that resolves after dispose(), never attaches a
110
+ // listener. A plain boolean cannot cover this: dispose()→reinit() flips it
111
+ // false→true again, reopening the window for the stale query to slip through.
112
+ _permGen = 0;
113
+ // Monotonic id of the current acquisition lifecycle, bumped only by dispose().
114
+ // Each getCurrentPosition() captures it at start; the async success/error
115
+ // callback bails (no setters, no resolve-side effects) if it is stale, so a
116
+ // one-shot fix that resolves after the element was disconnected does not
117
+ // dispatch wcs-geo:* on a torn-down element. Unlike FetchCore, the Geolocation
118
+ // API has no AbortController, so a generation guard is the only way to neutralize
119
+ // an in-flight one-shot. (watch is already stopped by clearWatch on disconnect.)
120
+ _acqGen = 0;
121
+ // Monotonic id of the current watch lifecycle, bumped by watch(), clearWatch(),
122
+ // and dispose(). Each watch() captures it; both watch callbacks bail if it is
123
+ // stale. Unlike a live `_watchId === null` check, this distinguishes the current
124
+ // watch from a superseded one: a clearWatch()→watch() restart installs a new
125
+ // watchId (non-null), so a queued callback from the previous watch would pass a
126
+ // null-check but fails the generation compare. (The README recommends exactly
127
+ // this restart sequence to reconfigure a watch.)
128
+ _watchGen = 0;
129
+ constructor(target) {
130
+ super();
131
+ this._target = target ?? this;
132
+ // Probe the permission state up front so observers see the real value
133
+ // (granted/denied/prompt) before the first read, then keep it live.
134
+ this._initPermission();
135
+ }
136
+ get position() {
137
+ return this._position;
138
+ }
139
+ get latitude() {
140
+ return this._position ? this._position.latitude : null;
141
+ }
142
+ get longitude() {
143
+ return this._position ? this._position.longitude : null;
144
+ }
145
+ get accuracy() {
146
+ return this._position ? this._position.accuracy : null;
147
+ }
148
+ get coords() {
149
+ return this._position ? this._position.coords : null;
150
+ }
151
+ get timestamp() {
152
+ return this._position ? this._position.timestamp : null;
153
+ }
154
+ get watching() {
155
+ return this._watching;
156
+ }
157
+ get loading() {
158
+ return this._loading;
159
+ }
160
+ get error() {
161
+ return this._error;
162
+ }
163
+ get permission() {
164
+ return this._permission;
165
+ }
166
+ // --- State setters with event dispatch ---
167
+ _setPosition(position) {
168
+ this._position = position;
169
+ this._target.dispatchEvent(new CustomEvent("wcs-geo:position", {
170
+ detail: position,
171
+ bubbles: true,
172
+ }));
173
+ }
174
+ _setWatching(watching) {
175
+ if (this._watching === watching)
176
+ return;
177
+ this._watching = watching;
178
+ this._target.dispatchEvent(new CustomEvent("wcs-geo:watching-changed", {
179
+ detail: watching,
180
+ bubbles: true,
181
+ }));
182
+ }
183
+ _setLoading(loading) {
184
+ if (this._loading === loading)
185
+ return;
186
+ this._loading = loading;
187
+ this._target.dispatchEvent(new CustomEvent("wcs-geo:loading-changed", {
188
+ detail: loading,
189
+ bubbles: true,
190
+ }));
191
+ }
192
+ _setError(error) {
193
+ // Same-value guard, like the other setters. Unlike `position` (which has
194
+ // derived getters and so must re-fire even on an identical reference), `error`
195
+ // has no derived state — so suppressing redundant null→null dispatches (e.g.
196
+ // a successful fix clearing an already-null error) avoids spurious events.
197
+ if (this._error === error)
198
+ return;
199
+ this._error = error;
200
+ this._target.dispatchEvent(new CustomEvent("wcs-geo:error", {
201
+ detail: error,
202
+ bubbles: true,
203
+ }));
204
+ }
205
+ _setPermission(permission) {
206
+ if (this._permission === permission)
207
+ return;
208
+ this._permission = permission;
209
+ this._target.dispatchEvent(new CustomEvent("wcs-geo:permission-changed", {
210
+ detail: permission,
211
+ bubbles: true,
212
+ }));
213
+ }
214
+ // --- Public API ---
215
+ /**
216
+ * Acquire a single position fix. Resolves once the fix arrives or the request
217
+ * fails — never rejects: failures are surfaced through the `error` property so
218
+ * they flow into the declarative state, symmetrical with FetchCore.
219
+ */
220
+ getCurrentPosition(options = {}) {
221
+ return new Promise((resolve) => {
222
+ if (!this._hasGeolocation()) {
223
+ this._setError(this._unsupportedError());
224
+ resolve();
225
+ return;
226
+ }
227
+ const gen = this._acqGen;
228
+ this._setLoading(true);
229
+ this._setError(null);
230
+ navigator.geolocation.getCurrentPosition((pos) => {
231
+ // Stale: the element was disposed (disconnected) while this fix was in
232
+ // flight. Drop it so a torn-down element never dispatches wcs-geo:*.
233
+ // Still resolve() so any awaiter (e.g. connectedCallbackPromise) settles.
234
+ if (gen !== this._acqGen) {
235
+ resolve();
236
+ return;
237
+ }
238
+ // Guard normalization/dispatch so a throw never escapes this browser
239
+ // callback as an unhandled rejection, leaves the promise pending (which
240
+ // would hang SSR's connectedCallbackPromise), or leaves `loading` stuck
241
+ // true. Loading is cleared first so it holds even if a later step throws.
242
+ try {
243
+ this._setLoading(false);
244
+ this._setPosition(this._normalizePosition(pos));
245
+ }
246
+ catch {
247
+ // Surface the unexpected failure as an error so observers are not left
248
+ // silently stale, then resolve below.
249
+ this._setError(this._unexpectedError());
250
+ }
251
+ resolve();
252
+ }, (err) => {
253
+ if (gen !== this._acqGen) {
254
+ resolve();
255
+ return;
256
+ }
257
+ try {
258
+ this._setLoading(false);
259
+ this._setError(this._normalizeError(err));
260
+ }
261
+ catch {
262
+ this._setError(this._unexpectedError());
263
+ }
264
+ resolve();
265
+ }, options);
266
+ });
267
+ }
268
+ /**
269
+ * Begin continuously watching the position. Idempotent while already
270
+ * watching: a redundant watch() must not register a second `watchPosition`
271
+ * (which would leak the handle and double the fix rate). Reconfiguring is done
272
+ * via clearWatch() + watch().
273
+ */
274
+ watch(options = {}) {
275
+ if (!this._hasGeolocation()) {
276
+ this._setError(this._unsupportedError());
277
+ return;
278
+ }
279
+ if (this._watching)
280
+ return;
281
+ this._setError(null);
282
+ this._setWatching(true);
283
+ // Open a new watch generation so any queued callback from a prior watch
284
+ // (cleared then restarted) is recognized as stale below.
285
+ const wgen = ++this._watchGen;
286
+ this._watchId = navigator.geolocation.watchPosition((pos) => {
287
+ // Stale: this callback belongs to a watch that was cleared (or the element
288
+ // disposed), possibly already superseded by a restart. A live
289
+ // `_watchId === null` check cannot catch the restart case (the new watch
290
+ // re-populates _watchId), so compare the captured generation instead.
291
+ if (wgen !== this._watchGen)
292
+ return;
293
+ // Guard normalization/dispatch so an unexpected throw never escapes this
294
+ // browser callback as an unhandled rejection — symmetric with the one-shot
295
+ // path.
296
+ try {
297
+ // A recovered fix clears any prior transient error (e.g. a one-off
298
+ // TIMEOUT) so `error` reflects the current state, not a stale failure.
299
+ // The _setError same-value guard makes this free when error is already
300
+ // null.
301
+ this._setError(null);
302
+ this._setPosition(this._normalizePosition(pos));
303
+ }
304
+ catch {
305
+ this._setError(this._unexpectedError());
306
+ }
307
+ }, (err) => {
308
+ if (wgen !== this._watchGen)
309
+ return;
310
+ // An error does not implicitly release the watch — the watchId stays
311
+ // valid and clearWatch() remains the teardown path — so `watching` is
312
+ // left true to reflect "watch still registered". A terminal error (e.g.
313
+ // PERMISSION_DENIED) is surfaced via the `error` property; callers that
314
+ // want to stop on error can call clearWatch() in response.
315
+ try {
316
+ this._setError(this._normalizeError(err));
317
+ }
318
+ catch {
319
+ this._setError(this._unexpectedError());
320
+ }
321
+ }, options);
322
+ }
323
+ clearWatch() {
324
+ if (this._watchId !== null) {
325
+ navigator.geolocation.clearWatch(this._watchId);
326
+ this._watchId = null;
327
+ }
328
+ // Invalidate the current watch generation so any callback the browser may
329
+ // still deliver after teardown bails.
330
+ this._watchGen++;
331
+ this._setWatching(false);
332
+ }
333
+ /**
334
+ * Re-establish the permission `change` subscription after a dispose() — e.g.
335
+ * the Shell element was disconnected and then reconnected (reparented). No-op
336
+ * while a subscription is already live, so the first connect after
337
+ * construction does not double-subscribe. This keeps permission tracking
338
+ * symmetric with position acquisition, which the Shell also revives on
339
+ * reconnect.
340
+ */
341
+ reinitPermission() {
342
+ if (!this._permissionSubscribed) {
343
+ this._initPermission();
344
+ }
345
+ }
346
+ /**
347
+ * Detach the live permission `change` listener. Call from the Shell's
348
+ * `disconnectedCallback` so a removed element does not leak the subscription.
349
+ * A later reconnect can re-subscribe via reinitPermission().
350
+ */
351
+ dispose() {
352
+ this._permissionSubscribed = false;
353
+ // Invalidate any in-flight query so its .then() bails instead of attaching a
354
+ // listener after teardown.
355
+ this._permGen++;
356
+ // Invalidate any in-flight one-shot acquisition so its success/error callback
357
+ // bails instead of dispatching on a disconnected element.
358
+ this._acqGen++;
359
+ // Likewise invalidate the watch generation. The Shell already calls
360
+ // clearWatch() before dispose(), but a direct headless dispose() (without a
361
+ // preceding clearWatch) still neutralizes any queued watch callback.
362
+ this._watchGen++;
363
+ // Reset the loading shadow silently (no dispatch on a disposed element). The
364
+ // bailed callback above will not clear it, and leaving it true would let the
365
+ // same-value guard swallow the loading=true edge of the next acquisition after
366
+ // a reconnect.
367
+ this._loading = false;
368
+ if (this._permissionStatus) {
369
+ this._permissionStatus.removeEventListener("change", this._onPermissionChange);
370
+ this._permissionStatus = null;
371
+ }
372
+ }
373
+ // --- Internal ---
374
+ _hasGeolocation() {
375
+ return typeof navigator !== "undefined" && !!navigator.geolocation;
376
+ }
377
+ _initPermission() {
378
+ // The Permissions API is optional. When absent (or it rejects, e.g. some
379
+ // browsers don't accept the "geolocation" name), report "unsupported" and
380
+ // leave acquisition to fail loudly via the error property if attempted.
381
+ if (typeof navigator === "undefined" || !navigator.permissions || typeof navigator.permissions.query !== "function") {
382
+ // Route through _setPermission (not a bare assignment) so observers stay in
383
+ // sync with the public state. The same-value guard means no redundant
384
+ // dispatch when the state does not actually change; it does mean a
385
+ // previously-observed "granted"/"denied" being reinit'd into an environment
386
+ // that lost the Permissions API now correctly notifies observers of the
387
+ // unsupported transition instead of silently overwriting the shadow value.
388
+ this._setPermission("unsupported");
389
+ return;
390
+ }
391
+ this._permissionSubscribed = true;
392
+ const gen = ++this._permGen;
393
+ navigator.permissions.query({ name: "geolocation" }).then((status) => {
394
+ // Stale resolution: this query was superseded (rapid reconnect) or the
395
+ // element was disposed while it was in flight. Drop it so only the current
396
+ // subscription attaches a listener.
397
+ if (gen !== this._permGen)
398
+ return;
399
+ this._permissionStatus = status;
400
+ this._setPermission(status.state);
401
+ status.addEventListener("change", this._onPermissionChange);
402
+ }, () => {
403
+ if (gen !== this._permGen)
404
+ return;
405
+ this._setPermission("unsupported");
406
+ });
407
+ }
408
+ _onPermissionChange = (event) => {
409
+ const status = event.target;
410
+ this._setPermission(status.state);
411
+ };
412
+ _normalizePosition(pos) {
413
+ const c = pos.coords;
414
+ const coords = {
415
+ latitude: c.latitude,
416
+ longitude: c.longitude,
417
+ accuracy: c.accuracy,
418
+ altitude: c.altitude,
419
+ altitudeAccuracy: c.altitudeAccuracy,
420
+ heading: c.heading,
421
+ speed: c.speed,
422
+ };
423
+ return { ...coords, timestamp: pos.timestamp, coords };
424
+ }
425
+ _normalizeError(err) {
426
+ return { code: err.code, message: err.message };
427
+ }
428
+ _unsupportedError() {
429
+ // Geolocation API absent: surface it as POSITION_UNAVAILABLE (2) so consumers
430
+ // that switch on the spec error codes treat it like any other unavailable fix.
431
+ return { code: 2, message: "Geolocation API is not available in this environment." };
432
+ }
433
+ _unexpectedError() {
434
+ // An unexpected throw while normalizing/dispatching a fix. Surface it as
435
+ // POSITION_UNAVAILABLE (2) so it flows into `error` like any other failure
436
+ // instead of escaping the browser callback as an unhandled rejection.
437
+ return { code: 2, message: "Unexpected error while processing the position fix." };
438
+ }
439
+ }
440
+
441
+ let registered = false;
442
+ function handleClick(event) {
443
+ const target = event.target;
444
+ if (!(target instanceof Element))
445
+ return;
446
+ const triggerElement = target.closest(`[${config.triggerAttribute}]`);
447
+ if (!triggerElement)
448
+ return;
449
+ const geoId = triggerElement.getAttribute(config.triggerAttribute);
450
+ if (!geoId)
451
+ return;
452
+ // Resolve the registered constructor at call time instead of importing
453
+ // Geolocation as a value. The value import created a components/Geolocation.ts
454
+ // ⇄ autoTrigger.ts cycle (Geolocation.connectedCallback() calls
455
+ // registerAutoTrigger()). instanceof against the customElements registry keeps
456
+ // the exact same identity guarantee — only the registered <wcs-geo> class
457
+ // matches — without the import cycle.
458
+ const GeoCtor = customElements.get(config.tagNames.geo);
459
+ const geoElement = document.getElementById(geoId);
460
+ if (!GeoCtor || !(geoElement instanceof GeoCtor))
461
+ return;
462
+ // Suppress the element's default action so a fix can be requested without
463
+ // navigating. Intentional: do not attach data-geotarget to an element whose
464
+ // default action you also want (real <a href> link, form-submit button) — it
465
+ // will be cancelled. See README "Optional DOM Triggering".
466
+ event.preventDefault();
467
+ geoElement.getCurrentPosition();
468
+ }
469
+ function registerAutoTrigger() {
470
+ if (registered)
471
+ return;
472
+ registered = true;
473
+ document.addEventListener("click", handleClick);
474
+ }
475
+
476
+ // Named WcsGeolocation (not `Geolocation`) so the class does not shadow the
477
+ // global DOM `Geolocation` interface (the type of `navigator.geolocation`), and
478
+ // to match the <wcs-ws> convention (WcsWebSocket). The public export keeps the
479
+ // `WcsGeolocation` name unchanged, so this rename is non-breaking.
480
+ class WcsGeolocation extends HTMLElement {
481
+ static hasConnectedCallbackPromise = true;
482
+ static wcBindable = {
483
+ ...GeolocationCore.wcBindable,
484
+ properties: [
485
+ ...GeolocationCore.wcBindable.properties,
486
+ { name: "trigger", event: "wcs-geo:trigger-changed" },
487
+ ],
488
+ // Shell-level settable surface. Each input carries its mirrored `attribute`
489
+ // hint (boolean flags reflect idempotently, so a binding system that writes
490
+ // through inputs[].attribute is safe), following the <wcs-ws> convention.
491
+ // `trigger` has no attribute — it is a momentary command-property, not a
492
+ // declarative attribute. The `getCurrentPosition` / `watchPosition` /
493
+ // `clearWatch` commands are declared below.
494
+ inputs: [
495
+ { name: "highAccuracy", attribute: "high-accuracy" },
496
+ { name: "timeout", attribute: "timeout" },
497
+ { name: "maximumAge", attribute: "maximum-age" },
498
+ { name: "watch", attribute: "watch" },
499
+ { name: "manual", attribute: "manual" },
500
+ { name: "trigger" },
501
+ ],
502
+ // The Core's `watch` command is renamed to `watchPosition` on the Shell so it
503
+ // does not collide with the `watch` boolean attribute accessor (same pattern
504
+ // as <wcs-ws>, where the `send` command becomes `sendMessage` to free the
505
+ // `send` setter). `getCurrentPosition` / `clearWatch` are unchanged.
506
+ commands: [
507
+ { name: "getCurrentPosition", async: true },
508
+ { name: "watchPosition" },
509
+ { name: "clearWatch" },
510
+ ],
511
+ };
512
+ _core;
513
+ _trigger = false;
514
+ _connectedCallbackPromise = Promise.resolve();
515
+ constructor() {
516
+ super();
517
+ this._core = new GeolocationCore(this);
518
+ }
519
+ // --- Attribute accessors ---
520
+ get highAccuracy() {
521
+ return this.hasAttribute("high-accuracy");
522
+ }
523
+ set highAccuracy(value) {
524
+ if (value) {
525
+ this.setAttribute("high-accuracy", "");
526
+ }
527
+ else {
528
+ this.removeAttribute("high-accuracy");
529
+ }
530
+ }
531
+ get timeout() {
532
+ const attr = this.getAttribute("timeout");
533
+ if (attr === null || attr.trim() === "")
534
+ return Infinity;
535
+ // Strict parse via Number() (unlike parseInt, "10px" -> NaN, not 10). Fall
536
+ // back to the API default (Infinity = no timeout) for any non-finite or
537
+ // negative value, matching the README "invalid values fall back to default".
538
+ const parsed = Number(attr);
539
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : Infinity;
540
+ }
541
+ set timeout(value) {
542
+ this.setAttribute("timeout", String(value));
543
+ }
544
+ get maximumAge() {
545
+ const attr = this.getAttribute("maximum-age");
546
+ if (attr === null || attr.trim() === "")
547
+ return 0;
548
+ // Strict parse via Number() (unlike parseInt, "10px" -> NaN, not 10). Fall
549
+ // back to the API default (0 = never use a cached fix) for any non-finite or
550
+ // negative value, matching the README "invalid values fall back to default".
551
+ const parsed = Number(attr);
552
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
553
+ }
554
+ set maximumAge(value) {
555
+ this.setAttribute("maximum-age", String(value));
556
+ }
557
+ get watch() {
558
+ return this.hasAttribute("watch");
559
+ }
560
+ set watch(value) {
561
+ if (value) {
562
+ this.setAttribute("watch", "");
563
+ }
564
+ else {
565
+ this.removeAttribute("watch");
566
+ }
567
+ }
568
+ get manual() {
569
+ return this.hasAttribute("manual");
570
+ }
571
+ set manual(value) {
572
+ if (value) {
573
+ this.setAttribute("manual", "");
574
+ }
575
+ else {
576
+ this.removeAttribute("manual");
577
+ }
578
+ }
579
+ // --- Core delegated getters ---
580
+ get position() {
581
+ return this._core.position;
582
+ }
583
+ get latitude() {
584
+ return this._core.latitude;
585
+ }
586
+ get longitude() {
587
+ return this._core.longitude;
588
+ }
589
+ get accuracy() {
590
+ return this._core.accuracy;
591
+ }
592
+ get coords() {
593
+ return this._core.coords;
594
+ }
595
+ get timestamp() {
596
+ return this._core.timestamp;
597
+ }
598
+ get watching() {
599
+ return this._core.watching;
600
+ }
601
+ get loading() {
602
+ return this._core.loading;
603
+ }
604
+ get error() {
605
+ return this._core.error;
606
+ }
607
+ get permission() {
608
+ return this._core.permission;
609
+ }
610
+ // wc-bindable connectedCallbackPromise protocol: resolves once the connect-time
611
+ // acquisition settles, so SSR (@wcstack/server render.ts) waits for the first
612
+ // fix before snapshotting the HTML. Mirrors Fetch.connectedCallbackPromise. In
613
+ // `watch` / `manual` modes there is no one-shot connect-time fix to await, so it
614
+ // stays the default resolved promise.
615
+ get connectedCallbackPromise() {
616
+ return this._connectedCallbackPromise;
617
+ }
618
+ // --- Command property ---
619
+ get trigger() {
620
+ return this._trigger;
621
+ }
622
+ set trigger(value) {
623
+ // Momentary command-property: a false→true write requests a single fix.
624
+ // Mirrors the trigger flag on <wcs-timer> / <wcs-ws>. Prefer the
625
+ // command-token protocol (`command.getCurrentPosition: $command.locate`) for
626
+ // state-driven acquisition; this exists mainly for the DOM click trigger and
627
+ // simple boolean bindings.
628
+ const v = !!value;
629
+ if (v) {
630
+ this._trigger = true;
631
+ // Fire-and-forget: getCurrentPosition() never rejects (failures surface via
632
+ // the `error` property), so the returned promise is intentionally dropped.
633
+ void this.getCurrentPosition();
634
+ this._trigger = false;
635
+ this.dispatchEvent(new CustomEvent("wcs-geo:trigger-changed", {
636
+ detail: false,
637
+ bubbles: true,
638
+ }));
639
+ }
640
+ }
641
+ // --- Commands ---
642
+ getCurrentPosition() {
643
+ return this._core.getCurrentPosition(this._options());
644
+ }
645
+ watchPosition() {
646
+ this._core.watch(this._options());
647
+ }
648
+ clearWatch() {
649
+ this._core.clearWatch();
650
+ }
651
+ // --- Internal ---
652
+ _options() {
653
+ return {
654
+ enableHighAccuracy: this.highAccuracy,
655
+ timeout: this.timeout,
656
+ maximumAge: this.maximumAge,
657
+ };
658
+ }
659
+ // --- Lifecycle ---
660
+ connectedCallback() {
661
+ this.style.display = "none";
662
+ if (config.autoTrigger) {
663
+ registerAutoTrigger();
664
+ }
665
+ // Revive permission tracking after a reconnect (reparenting). No-op on the
666
+ // first connect since the constructor already subscribed; only re-subscribes
667
+ // when disconnectedCallback's dispose() tore the subscription down.
668
+ this._core.reinitPermission();
669
+ if (!this.manual) {
670
+ // `watch` attribute selects the default phase: continuous monitoring vs a
671
+ // single fix on connect.
672
+ if (this.watch) {
673
+ this.watchPosition();
674
+ }
675
+ else {
676
+ // Track only the one-shot connect-time fix so SSR can await it. watch /
677
+ // manual leave the promise at its resolved default. getCurrentPosition()
678
+ // never rejects (failures surface via `error`), so no .catch() is needed.
679
+ this._connectedCallbackPromise = this.getCurrentPosition();
680
+ }
681
+ }
682
+ }
683
+ disconnectedCallback() {
684
+ this._core.clearWatch();
685
+ this._core.dispose();
686
+ }
687
+ }
688
+
689
+ function registerComponents() {
690
+ if (!customElements.get(config.tagNames.geo)) {
691
+ customElements.define(config.tagNames.geo, WcsGeolocation);
692
+ }
693
+ }
694
+
695
+ function bootstrapGeolocation(userConfig) {
696
+ if (userConfig) {
697
+ setConfig(userConfig);
698
+ }
699
+ registerComponents();
700
+ }
701
+
702
+ export { GeolocationCore, WcsGeolocation, bootstrapGeolocation, getConfig };
703
+ //# sourceMappingURL=index.esm.js.map