@tileflow/vue 0.1.0-alpha.14 → 0.1.0-alpha.17

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.
package/README.md CHANGED
@@ -28,4 +28,17 @@ Tileflow resolves from the map name, manifest, or explicit style props. Direct
28
28
  Tileflow props such as `center`, `zoom`, and `interactive` take priority when
29
29
  provided.
30
30
 
31
+ ## Headless capture readiness
32
+
33
+ Pass `capture-id` when a page contains multiple copies of the same named map:
34
+
35
+ ```vue
36
+ <TileflowMap map="madrid" capture-id="checkout-map" />
37
+ ```
38
+
39
+ The root exposes `data-tileflow-map`, optional `data-tileflow-capture-id`, and
40
+ `data-tileflow-state="loading|idle|error"`. It becomes idle only after MapLibre idle plus two
41
+ animation frames (or image decode/load in image mode), returns to loading for new style/data work,
42
+ and marks terminal load errors. Application capture selects exactly one ready target.
43
+
31
44
  Docs: https://tileflow.dev/docs
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ type TileflowMapProps = {
9
9
  alt?: string;
10
10
  analytics?: TileflowAnalytics;
11
11
  center?: [number, number];
12
+ captureId?: string;
12
13
  className?: string;
13
14
  config?: TileflowConfig;
14
15
  height?: number | string;
@@ -35,6 +36,7 @@ declare const TileflowMap: vue.DefineComponent<vue.ExtractPropTypes<{
35
36
  };
36
37
  analytics: PropType<TileflowAnalytics>;
37
38
  center: PropType<[number, number]>;
39
+ captureId: StringConstructor;
38
40
  className: StringConstructor;
39
41
  config: PropType<TileflowConfig>;
40
42
  height: {
@@ -85,6 +87,7 @@ declare const TileflowMap: vue.DefineComponent<vue.ExtractPropTypes<{
85
87
  };
86
88
  analytics: PropType<TileflowAnalytics>;
87
89
  center: PropType<[number, number]>;
90
+ captureId: StringConstructor;
88
91
  className: StringConstructor;
89
92
  config: PropType<TileflowConfig>;
90
93
  height: {
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  computed,
5
5
  defineComponent,
6
6
  h,
7
+ nextTick,
7
8
  onBeforeUnmount,
8
9
  onMounted,
9
10
  ref,
@@ -15,15 +16,20 @@ import {
15
16
  defaultTileflowManifestUrl,
16
17
  loadTileflowManifest,
17
18
  mergeTileflowAnalytics,
19
+ normalizeTileflowCaptureId,
18
20
  normalizeTileflowStaticImageSize,
19
- resolveTileflowAnalyticsRequestUrl,
20
21
  resolveTileflowManifestMap,
21
22
  resolveTileflowMapMode,
22
23
  resolveTileflowRuntimeStyle,
23
24
  resolveTileflowStaticImageUrl,
24
- shouldLoadTileflowManifest,
25
- startTileflowSession
25
+ shouldLoadTileflowManifest
26
26
  } from "@tileflow/core";
27
+ import {
28
+ attachTileflowMapLifecycle,
29
+ createTileflowMarkerController,
30
+ createTileflowSessionStarter,
31
+ createTileflowTransformRequest
32
+ } from "@tileflow/core/browser";
27
33
  var defaultCenter = [0, 20];
28
34
  var defaultZoom = 2;
29
35
  var TileflowMap = defineComponent({
@@ -36,6 +42,7 @@ var TileflowMap = defineComponent({
36
42
  },
37
43
  analytics: Object,
38
44
  center: Array,
45
+ captureId: String,
39
46
  className: String,
40
47
  config: Object,
41
48
  height: {
@@ -84,12 +91,27 @@ var TileflowMap = defineComponent({
84
91
  const manifestMap = shallowRef(null);
85
92
  const imageSize = shallowRef(null);
86
93
  const mapRef = shallowRef(null);
94
+ const captureState = ref("loading");
87
95
  const sessionId = createTileflowSessionId();
88
- const markerInstances = [];
89
- const sessionStarts = /* @__PURE__ */ new Set();
96
+ const markerController = createTileflowMarkerController({
97
+ attach(markerInstance, map, marker) {
98
+ markerInstance.setLngLat(marker.coordinates).addTo(map);
99
+ markerInstance.getElement().title = marker.label ?? marker.id;
100
+ },
101
+ create: (marker) => new maplibregl.Marker({
102
+ color: marker.color ?? "#C6A15B"
103
+ }),
104
+ remove: (marker) => marker.remove()
105
+ });
106
+ const sessionStarter = createTileflowSessionStarter({
107
+ sessionId,
108
+ source: "vue"
109
+ });
90
110
  let imageResizeObserver = null;
111
+ let mapLifecycle = null;
91
112
  let mapResizeObserver = null;
92
113
  let manifestLoadId = 0;
114
+ let readinessRunId = 0;
93
115
  const resolvedMode = computed(
94
116
  () => resolveTileflowMapMode({
95
117
  imageUrl: props.imageUrl,
@@ -138,6 +160,7 @@ var TileflowMap = defineComponent({
138
160
  position: "relative",
139
161
  width: "100%"
140
162
  }));
163
+ const resolvedCaptureId = computed(() => normalizeTileflowCaptureId(props.captureId));
141
164
  const refreshManifest = async () => {
142
165
  const shouldLoad = shouldLoadTileflowManifest({
143
166
  config: props.config,
@@ -186,18 +209,35 @@ var TileflowMap = defineComponent({
186
209
  updateImageSize();
187
210
  };
188
211
  const destroyMap = () => {
189
- mapResizeObserver?.disconnect();
212
+ readinessRunId += 1;
213
+ const lifecycle = mapLifecycle;
214
+ const resizeObserver = mapResizeObserver;
215
+ const map = mapRef.value;
216
+ mapLifecycle = null;
190
217
  mapResizeObserver = null;
191
- clearMarkers(markerInstances);
192
- mapRef.value?.remove();
193
218
  mapRef.value = null;
219
+ try {
220
+ lifecycle?.dispose();
221
+ } finally {
222
+ try {
223
+ resizeObserver?.disconnect();
224
+ } finally {
225
+ try {
226
+ markerController.clear();
227
+ } finally {
228
+ map?.remove();
229
+ }
230
+ }
231
+ }
194
232
  };
195
233
  const recreateMap = () => {
196
234
  destroyMap();
235
+ captureState.value = "loading";
197
236
  if (!containerRef.value || !runtimeStyle.value || isImageMode.value) {
198
237
  return;
199
238
  }
200
239
  const runtime = runtimeStyle.value;
240
+ const analyticsForMap = resolvedAnalytics.value;
201
241
  const map = new maplibregl.Map({
202
242
  ...props.mapOptions,
203
243
  attributionControl: props.mapOptions?.attributionControl ?? { compact: true },
@@ -205,11 +245,11 @@ var TileflowMap = defineComponent({
205
245
  container: containerRef.value,
206
246
  interactive: resolvedInteractive.value,
207
247
  style: runtime.style,
208
- transformRequest: createTransformRequest(
209
- resolvedAnalytics.value,
248
+ transformRequest: createTileflowTransformRequest({
249
+ getAnalytics: () => analyticsForMap,
210
250
  sessionId,
211
- props.mapOptions?.transformRequest
212
- ),
251
+ transformRequest: props.mapOptions?.transformRequest ?? void 0
252
+ }),
213
253
  zoom: resolvedZoom.value
214
254
  });
215
255
  mapRef.value = map;
@@ -220,21 +260,27 @@ var TileflowMap = defineComponent({
220
260
  map.resize();
221
261
  });
222
262
  mapResizeObserver.observe(containerRef.value);
223
- map.on("load", () => {
224
- const analyticsForLoad = resolvedAnalytics.value;
225
- const styleId = analyticsForLoad?.styleId ?? (typeof runtime.style === "string" ? runtime.style : props.map);
226
- emit("load", map);
227
- if (analyticsForLoad?.mapId) {
228
- const sessionKey = `${sessionId}:${analyticsForLoad.mapId}:${styleId ?? ""}`;
229
- if (!sessionStarts.has(sessionKey)) {
230
- sessionStarts.add(sessionKey);
231
- startTileflowSession(analyticsForLoad, {
232
- mapId: analyticsForLoad.mapId,
233
- sessionId,
234
- source: "vue",
235
- styleId
236
- });
237
- }
263
+ mapLifecycle = attachTileflowMapLifecycle({
264
+ getSession: () => {
265
+ const analyticsForLoad = resolvedAnalytics.value;
266
+ return {
267
+ analytics: analyticsForLoad,
268
+ styleId: analyticsForLoad?.styleId ?? (typeof runtime.style === "string" ? runtime.style : props.map)
269
+ };
270
+ },
271
+ map,
272
+ onLoad: (loadedMap) => emit("load", loadedMap),
273
+ scheduler: {
274
+ cancelFrame: (frame) => cancelAnimationFrame(frame),
275
+ requestFrame: (callback) => requestAnimationFrame(callback)
276
+ },
277
+ sessionStarter,
278
+ setState: (state) => {
279
+ captureState.value = state;
280
+ },
281
+ subscribe: (subscribedMap, event, listener) => {
282
+ const subscription = subscribedMap.on(event, listener);
283
+ return () => subscription.unsubscribe();
238
284
  }
239
285
  });
240
286
  syncMarkers();
@@ -247,22 +293,43 @@ var TileflowMap = defineComponent({
247
293
  };
248
294
  const syncMarkers = () => {
249
295
  const map = mapRef.value;
250
- clearMarkers(markerInstances);
251
296
  if (!map) {
297
+ markerController.clear();
252
298
  return;
253
299
  }
254
- for (const marker of props.markers) {
255
- const markerInstance = new maplibregl.Marker({
256
- color: marker.color ?? "#C6A15B"
257
- }).setLngLat(marker.coordinates).addTo(map);
258
- markerInstance.getElement().title = marker.label ?? marker.id;
259
- markerInstances.push(markerInstance);
300
+ markerController.replace(map, props.markers);
301
+ };
302
+ const markImageReady = async (image) => {
303
+ const runId = ++readinessRunId;
304
+ try {
305
+ if (typeof image.decode === "function") await image.decode();
306
+ } catch {
307
+ if (!image.complete || image.naturalWidth === 0) {
308
+ if (runId === readinessRunId) captureState.value = "error";
309
+ return;
310
+ }
311
+ }
312
+ if (runId === readinessRunId) captureState.value = "idle";
313
+ };
314
+ const resetImageReadiness = async () => {
315
+ if (!isImageMode.value) return;
316
+ const runId = ++readinessRunId;
317
+ captureState.value = "loading";
318
+ await nextTick();
319
+ if (runId !== readinessRunId) return;
320
+ const image = containerRef.value?.querySelector("img");
321
+ if (!image?.complete) return;
322
+ if (image.naturalWidth === 0) {
323
+ captureState.value = "error";
324
+ return;
260
325
  }
326
+ await markImageReady(image);
261
327
  };
262
328
  onMounted(() => {
263
329
  void refreshManifest();
264
330
  updateImageResizeObserver();
265
331
  recreateMap();
332
+ void resetImageReadiness();
266
333
  });
267
334
  onBeforeUnmount(() => {
268
335
  imageResizeObserver?.disconnect();
@@ -284,6 +351,13 @@ var TileflowMap = defineComponent({
284
351
  void refreshManifest();
285
352
  }
286
353
  );
354
+ watch(
355
+ () => [isImageMode.value, runtimeImageUrl.value],
356
+ () => {
357
+ void resetImageReadiness();
358
+ },
359
+ { flush: "post" }
360
+ );
287
361
  watch(
288
362
  () => [props.imageUrl, props.mode, props.preferLocalDev],
289
363
  () => updateImageResizeObserver(),
@@ -329,6 +403,9 @@ var TileflowMap = defineComponent({
329
403
  {
330
404
  ...rootAttrs,
331
405
  class: classValue,
406
+ "data-tileflow-capture-id": resolvedCaptureId.value,
407
+ "data-tileflow-map": props.map,
408
+ "data-tileflow-state": captureState.value,
332
409
  ref: containerRef,
333
410
  style: styleValue
334
411
  },
@@ -336,6 +413,11 @@ var TileflowMap = defineComponent({
336
413
  alt: props.alt,
337
414
  decoding: "async",
338
415
  loading: props.imageLoading,
416
+ onError: () => {
417
+ readinessRunId += 1;
418
+ captureState.value = "error";
419
+ },
420
+ onLoad: (event) => void markImageReady(event.currentTarget),
339
421
  src: runtimeImageUrl.value,
340
422
  style: {
341
423
  display: "block",
@@ -349,40 +431,9 @@ var TileflowMap = defineComponent({
349
431
  }
350
432
  });
351
433
  var index_default = TileflowMap;
352
- function createTransformRequest(analytics, sessionId, userTransformRequest) {
353
- if ((!analytics || analytics.enabled === false) && !userTransformRequest) {
354
- return void 0;
355
- }
356
- return (url, resourceType) => {
357
- const request = userTransformRequest?.(url, resourceType);
358
- if (isPromiseLike(request)) {
359
- return request.then(
360
- (resolvedRequest) => applyTileflowAnalyticsRequest(url, resolvedRequest, analytics, sessionId) ?? resolvedRequest
361
- );
362
- }
363
- return applyTileflowAnalyticsRequest(url, request, analytics, sessionId);
364
- };
365
- }
366
- function applyTileflowAnalyticsRequest(url, request, analytics, sessionId) {
367
- const requestUrl = request?.url ?? url;
368
- const nextUrl = resolveTileflowAnalyticsRequestUrl(requestUrl, analytics, sessionId);
369
- if (!nextUrl) {
370
- return request;
371
- }
372
- return request ? { ...request, url: nextUrl } : { url: nextUrl };
373
- }
374
- function isPromiseLike(value) {
375
- return Boolean(value && typeof value.then === "function");
376
- }
377
434
  function formatHeight(height) {
378
435
  return typeof height === "number" ? `${height}px` : height;
379
436
  }
380
- function clearMarkers(markerInstances) {
381
- for (const marker of markerInstances) {
382
- marker.remove();
383
- }
384
- markerInstances.splice(0);
385
- }
386
437
  export {
387
438
  TileflowMap,
388
439
  index_default as default
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@tileflow/vue",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "Vue component for rendering Tileflow maps with MapLibre.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "homepage": "https://tileflow.dev",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/tileflow/tileflow.git",
10
+ "url": "git+https://github.com/tileflow/tileflow-sdk.git",
11
11
  "directory": "packages/vue"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/tileflow/tileflow/issues"
14
+ "url": "https://github.com/tileflow/tileflow-sdk/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "tileflow",
@@ -36,18 +36,22 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@tileflow/core": "0.1.0-alpha.14"
39
+ "@tileflow/core": ">=0.1.0-alpha.17 <0.1.0-beta.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "maplibre-gl": ">=5",
43
43
  "vue": ">=3.3"
44
44
  },
45
45
  "devDependencies": {
46
+ "@vue/server-renderer": "3.5.39",
46
47
  "maplibre-gl": "^5.24.0",
48
+ "tsx": "^4.23.1",
47
49
  "vue": "^3.5.26"
48
50
  },
49
51
  "scripts": {
50
52
  "build": "tsup src/index.ts --format esm --dts --clean --external vue --external maplibre-gl",
53
+ "test": "tsx --test test/*.test.ts",
54
+ "verify": "pnpm run build && pnpm run test",
51
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
52
56
  "lint": "tsc -p tsconfig.json --noEmit"
53
57
  }