@guardian/interactive-component-library 0.3.2 → 0.4.1

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.
@@ -1,3 +1,5 @@
1
+ import { Layer } from '../lib/layers';
2
+ import { ReactNode } from 'preact/compat';
1
3
  /**
2
4
  * @param {Object} params
3
5
  * @param {import('../lib/Map').Map} params.map
@@ -8,10 +10,18 @@ export function MapProvider({ map, children }: {
8
10
  children: import('preact').ComponentChildren;
9
11
  }): import("preact").JSX.Element;
10
12
  export type MapContext = {
11
- registerLayer: ((layer: import('../lib/layers').Layer) => void) | null;
13
+ registerLayer: ((layer: Layer, comp: ReactNode) => void) | null;
14
+ unregisterLayer: ((layer: Layer) => void) | null;
12
15
  };
13
16
  /**
14
- * @typedef {{ registerLayer: ((layer: import("../lib/layers").Layer) => void) | null }} MapContext
17
+ * @import { Layer } from "../lib/layers"
18
+ * @import { ReactNode } from "preact/compat"
19
+ */
20
+ /**
21
+ * @typedef {{
22
+ * registerLayer: ((layer: Layer, comp: ReactNode) => void) | null,
23
+ * unregisterLayer: ((layer: Layer) => void) | null
24
+ * }} MapContext
15
25
  */
16
26
  /**
17
27
  * @type {import('preact').Context<MapContext | null>}
@@ -1,18 +1,52 @@
1
1
  import { jsx } from "preact/jsx-runtime";
2
2
  import { createContext } from "preact";
3
- import { useEffect } from "preact/hooks";
3
+ import { useState, useEffect } from "preact/hooks";
4
4
  const MapContext = createContext(null);
5
5
  function MapProvider({ map, children }) {
6
- const registeredLayers = [];
7
- const registerLayer = (layer) => {
8
- registeredLayers.push(layer);
6
+ const [layers, setLayers] = useState([]);
7
+ const registerLayer = (layer, comp) => {
8
+ if (!layers.includes(layer)) {
9
+ const position = getCompTreePosition(comp, children);
10
+ if (position === null) return;
11
+ setLayers((prevLayers) => {
12
+ const newLayers = [...prevLayers];
13
+ newLayers.splice(position, 0, layer);
14
+ return newLayers;
15
+ });
16
+ }
17
+ };
18
+ const unregisterLayer = (layerToRemove) => {
19
+ setLayers(
20
+ (prevLayers) => prevLayers.filter((layer) => layer !== layerToRemove)
21
+ );
9
22
  };
10
23
  useEffect(() => {
11
- if (map && !map.hasLayers(registeredLayers)) {
12
- map.setLayers(registeredLayers);
24
+ if (!map) return;
25
+ map.setLayers(layers);
26
+ }, [map, layers]);
27
+ return /* @__PURE__ */ jsx(MapContext.Provider, { value: { registerLayer, unregisterLayer }, children });
28
+ }
29
+ function getCompTreePosition(targetComponent, children) {
30
+ let index = 0;
31
+ function traverse(nodes) {
32
+ var _a, _b;
33
+ for (const node of nodes) {
34
+ const childNodes = (_b = (_a = node == null ? void 0 : node.__c) == null ? void 0 : _a.__v) == null ? void 0 : _b.__k;
35
+ if (childNodes && childNodes.length > 1 && childNodes[0] !== null) {
36
+ const result = traverse(childNodes);
37
+ if (result !== null) {
38
+ return result;
39
+ }
40
+ } else {
41
+ if ((node == null ? void 0 : node.__c) === targetComponent) {
42
+ return index;
43
+ }
44
+ index++;
45
+ }
13
46
  }
14
- }, [map, children]);
15
- return /* @__PURE__ */ jsx(MapContext.Provider, { value: { registerLayer }, children });
47
+ return null;
48
+ }
49
+ return traverse(Array.isArray(children) ? children : [children]);
16
50
  }
17
51
  export {
18
52
  MapContext,
@@ -1,6 +1,6 @@
1
- import { Style } from "../styles/Style.js";
2
1
  import { Stroke } from "../styles/Stroke.js";
3
2
  import { Fill } from "../styles/Fill.js";
3
+ import { Style } from "../styles/Style.js";
4
4
  function interpolateStyles(firstStyle, secondStyle, interpolateColors, interpolateNumbers) {
5
5
  const fillInterpolator = interpolateFill(
6
6
  firstStyle.fill,
@@ -6,7 +6,7 @@ import { Dispatcher } from '../events/Dispatcher';
6
6
  /** @typedef {TextLayerOptions & { features: import("../Feature").Feature[] | import("../FeatureCollection").FeatureCollection }} TextLayerComponentProps */
7
7
  export class TextLayer {
8
8
  /** @param {TextLayerComponentProps} props */
9
- static Component({ features: featureCollection, style, minZoom, opacity, declutter, drawCollisionBoxes, }: TextLayerComponentProps): any;
9
+ static Component({ features: featureCollection, style, minZoom, opacity, declutter, drawCollisionBoxes, }: TextLayerComponentProps): boolean;
10
10
  /**
11
11
  * @param {import("../Feature").Feature[]} features
12
12
  * @param {TextLayerOptions} options
@@ -1,6 +1,6 @@
1
1
  import { TextLayerRenderer } from "../renderers/TextLayerRenderer.js";
2
- import { Style } from "../styles/Style.js";
3
2
  import { Text } from "../styles/Text.js";
3
+ import { Style } from "../styles/Style.js";
4
4
  import { Dispatcher } from "../events/Dispatcher.js";
5
5
  import { combineExtents } from "../util/extent.js";
6
6
  import { MapEvent } from "../events/MapEvent.js";
@@ -18,7 +18,7 @@ class TextLayer {
18
18
  declutter,
19
19
  drawCollisionBoxes
20
20
  }) {
21
- const { registerLayer } = useContext(MapContext);
21
+ const { registerLayer, unregisterLayer } = useContext(MapContext);
22
22
  const layer = useMemo(
23
23
  () => {
24
24
  const features = featureCollection instanceof FeatureCollection ? featureCollection.features : (
@@ -36,11 +36,16 @@ class TextLayer {
36
36
  // eslint-disable-next-line react-hooks/exhaustive-deps
37
37
  [featureCollection, minZoom, opacity, declutter, drawCollisionBoxes]
38
38
  );
39
- registerLayer(layer);
39
+ useEffect(() => {
40
+ registerLayer(layer, this);
41
+ return () => {
42
+ unregisterLayer(layer);
43
+ };
44
+ }, [layer]);
40
45
  useEffect(() => {
41
46
  layer.style = style;
42
47
  }, [style]);
43
- return null;
48
+ return false;
44
49
  }
45
50
  /**
46
51
  * @param {import("../Feature").Feature[]} features
@@ -6,7 +6,7 @@ import { VectorSource } from '../sources/VectorSource';
6
6
  /** @typedef {VectorLayerOptions & { features: import("../Feature").Feature[] | import("../FeatureCollection").FeatureCollection }} VectorLayerComponentProps */
7
7
  export class VectorLayer {
8
8
  /** @param {VectorLayerComponentProps} props */
9
- static Component({ features: featureCollection, style, minZoom, opacity, hitDetectionEnabled, }: VectorLayerComponentProps): any;
9
+ static Component({ features: featureCollection, style, minZoom, opacity, hitDetectionEnabled, }: VectorLayerComponentProps): boolean;
10
10
  /**
11
11
  * @param {import("../Feature").Feature[]} features
12
12
  * @param {VectorLayerOptions} options
@@ -1,6 +1,6 @@
1
1
  import { VectorLayerRenderer } from "../renderers/VectorLayerRenderer.js";
2
- import { Style } from "../styles/Style.js";
3
2
  import { Stroke } from "../styles/Stroke.js";
3
+ import { Style } from "../styles/Style.js";
4
4
  import { combineExtents } from "../util/extent.js";
5
5
  import { Dispatcher } from "../events/Dispatcher.js";
6
6
  import { MapEvent } from "../events/MapEvent.js";
@@ -17,7 +17,7 @@ class VectorLayer {
17
17
  opacity,
18
18
  hitDetectionEnabled = true
19
19
  }) {
20
- const { registerLayer } = useContext(MapContext);
20
+ const { registerLayer, unregisterLayer } = useContext(MapContext);
21
21
  const layer = useMemo(
22
22
  () => {
23
23
  const features = featureCollection instanceof FeatureCollection ? featureCollection.features : (
@@ -34,11 +34,16 @@ class VectorLayer {
34
34
  // eslint-disable-next-line react-hooks/exhaustive-deps
35
35
  [featureCollection, minZoom, opacity, hitDetectionEnabled]
36
36
  );
37
- registerLayer(layer);
37
+ useEffect(() => {
38
+ registerLayer(layer, this);
39
+ return () => {
40
+ unregisterLayer(layer);
41
+ };
42
+ }, [layer]);
38
43
  useEffect(() => {
39
44
  layer.style = style;
40
45
  }, [style]);
41
- return null;
46
+ return false;
42
47
  }
43
48
  /**
44
49
  * @param {import("../Feature").Feature[]} features
@@ -33,8 +33,7 @@ class FeatureRenderer {
33
33
  }
34
34
  this.drawPath(geometries, context);
35
35
  if (fill) {
36
- context.fillStyle = fill.getRgba();
37
- context.fill();
36
+ fill.drawInContext(context, transform.k);
38
37
  }
39
38
  if (stroke) {
40
39
  context.save();
@@ -1,7 +1,33 @@
1
+ /**
2
+ * @class Fill
3
+ * @description Represents a fill style for a feature
4
+ * @property {string} color - The color of the fill
5
+ * @property {number} opacity - The opacity of the fill
6
+ * @property {HashPattern} pattern - The pattern of the fill
7
+ */
1
8
  export class Fill {
2
- constructor(options: any);
3
- color: any;
4
- opacity: any;
9
+ /**
10
+ * @constructor
11
+ * @description Creates a new instance of the Fill class
12
+ * @param {Object} [options] - The options for the fill
13
+ * @param {string} [options.color="#CCC"] - The color of the fill
14
+ * @param {number} [options.opacity=1] - The opacity of the fill
15
+ * @param {Object} [options.pattern=null] - The pattern of the fill
16
+ */
17
+ constructor(options?: {
18
+ color?: string;
19
+ opacity?: number;
20
+ pattern?: any;
21
+ });
22
+ color: string;
23
+ opacity: number;
24
+ pattern: any;
5
25
  _getRgba: (...arg0: any[]) => string;
26
+ /**
27
+ * @function getRgba
28
+ * @description Returns the fill color as an RGBA string
29
+ * @returns {string} The fill color with opacity applied
30
+ */
6
31
  getRgba(): string;
32
+ drawInContext(context: any, scale: any): void;
7
33
  }
@@ -1,14 +1,36 @@
1
1
  import { memoise } from "../util/memoise.js";
2
2
  import { toRgba } from "../util/toRgba.js";
3
3
  class Fill {
4
+ /**
5
+ * @constructor
6
+ * @description Creates a new instance of the Fill class
7
+ * @param {Object} [options] - The options for the fill
8
+ * @param {string} [options.color="#CCC"] - The color of the fill
9
+ * @param {number} [options.opacity=1] - The opacity of the fill
10
+ * @param {Object} [options.pattern=null] - The pattern of the fill
11
+ */
4
12
  constructor(options) {
5
13
  this.color = (options == null ? void 0 : options.color) || "#CCC";
6
14
  this.opacity = (options == null ? void 0 : options.opacity) || 1;
15
+ this.pattern = (options == null ? void 0 : options.pattern) || null;
7
16
  this._getRgba = memoise(toRgba);
8
17
  }
18
+ /**
19
+ * @function getRgba
20
+ * @description Returns the fill color as an RGBA string
21
+ * @returns {string} The fill color with opacity applied
22
+ */
9
23
  getRgba() {
10
24
  return this._getRgba(this.color, this.opacity);
11
25
  }
26
+ drawInContext(context, scale) {
27
+ if (this.pattern) {
28
+ context.fillStyle = this.pattern.createPatternInContext(context, scale);
29
+ } else {
30
+ context.fillStyle = this.getRgba();
31
+ }
32
+ context.fill();
33
+ }
12
34
  }
13
35
  export {
14
36
  Fill
@@ -0,0 +1,38 @@
1
+ /**
2
+ * HashPattern creates a diagonally-striped pattern used to fill a canvas element.
3
+ *
4
+ * Use `createPatternInContext` to create a pattern that can be used as a `fillStyle`.
5
+ */
6
+ export class HashPattern {
7
+ /**
8
+ * @param {Object} options
9
+ * @param {number} [options.stripeWidth=4] - width of the pattern's stripes
10
+ * @param {number} [options.gapWidth =10] - width of the gap between the stripes
11
+ * @param {string} [options.stripeColor="#eee"]
12
+ * @param {string} [options.gapColor="#777"]
13
+ */
14
+ constructor({ stripeWidth, gapWidth, gapColor, stripeColor, }: {
15
+ stripeWidth?: number;
16
+ gapWidth?: number;
17
+ stripeColor?: string;
18
+ gapColor?: string;
19
+ });
20
+ stripeWidth: number;
21
+ stripeColor: string;
22
+ gapwidth: number;
23
+ gapColor: string;
24
+ tileSize: number;
25
+ offscreenCanvas: HTMLCanvasElement;
26
+ lastDrawnScale: any;
27
+ lastDrawnPattern: CanvasPattern;
28
+ /**
29
+ * @param {CanvasRenderingContext2D} ctx
30
+ * @param {number} scale
31
+ */
32
+ createPatternInContext(ctx: CanvasRenderingContext2D, scale: number): CanvasPattern;
33
+ /**
34
+ * @param {CanvasRenderingContext2D} ctx
35
+ * @param {number} scale
36
+ */
37
+ _createPattern(ctx: CanvasRenderingContext2D, scale: number): CanvasPattern;
38
+ }
@@ -0,0 +1,72 @@
1
+ class HashPattern {
2
+ /**
3
+ * @param {Object} options
4
+ * @param {number} [options.stripeWidth=4] - width of the pattern's stripes
5
+ * @param {number} [options.gapWidth =10] - width of the gap between the stripes
6
+ * @param {string} [options.stripeColor="#eee"]
7
+ * @param {string} [options.gapColor="#777"]
8
+ */
9
+ constructor({
10
+ stripeWidth = 4,
11
+ gapWidth = 8,
12
+ gapColor = "#eee",
13
+ stripeColor = "#777"
14
+ }) {
15
+ this.stripeWidth = stripeWidth;
16
+ this.stripeColor = stripeColor;
17
+ this.gapwidth = gapWidth;
18
+ this.gapColor = gapColor;
19
+ const hypot = stripeWidth * 2 + gapWidth * 2;
20
+ this.tileSize = Math.round(hypot / Math.SQRT2);
21
+ this.offscreenCanvas = document.createElement("canvas");
22
+ this.lastDrawnScale = null;
23
+ this.lastDrawnPattern = null;
24
+ }
25
+ /**
26
+ * @param {CanvasRenderingContext2D} ctx
27
+ * @param {number} scale
28
+ */
29
+ createPatternInContext(ctx, scale) {
30
+ const roundedScale = Math.round(scale);
31
+ if (roundedScale === this.lastDrawnScale) {
32
+ return this.lastDrawnPattern;
33
+ }
34
+ this.lastDrawnPattern = this._createPattern(ctx, roundedScale);
35
+ this.lastDrawnScale = roundedScale;
36
+ return this.lastDrawnPattern;
37
+ }
38
+ /**
39
+ * @param {CanvasRenderingContext2D} ctx
40
+ * @param {number} scale
41
+ */
42
+ _createPattern(ctx, scale) {
43
+ const size = this.tileSize * scale;
44
+ this.offscreenCanvas.width = size;
45
+ this.offscreenCanvas.height = size;
46
+ const offCtx = this.offscreenCanvas.getContext("2d");
47
+ if (this.gapColor) {
48
+ offCtx.fillStyle = this.gapColor;
49
+ offCtx.fillRect(0, 0, size, size);
50
+ }
51
+ const lineWidth = this.stripeWidth * scale;
52
+ offCtx.strokeStyle = this.stripeColor;
53
+ offCtx.lineWidth = lineWidth;
54
+ offCtx.lineCap = "square";
55
+ offCtx.beginPath();
56
+ offCtx.moveTo(-1, 1);
57
+ offCtx.lineTo(1, -1);
58
+ offCtx.moveTo(0, size);
59
+ offCtx.lineTo(size, 0);
60
+ offCtx.moveTo(size - 1, size + 1);
61
+ offCtx.lineTo(size + 1, size - 1);
62
+ offCtx.stroke();
63
+ const pattern = ctx.createPattern(this.offscreenCanvas, "repeat");
64
+ pattern.setTransform(
65
+ new DOMMatrix().scale(1 / Math.round(scale), 1 / Math.round(scale))
66
+ );
67
+ return pattern;
68
+ }
69
+ }
70
+ export {
71
+ HashPattern
72
+ };
@@ -8,7 +8,7 @@
8
8
  * @class
9
9
  * @property {Object} properties - The properties of the style
10
10
  * @property {Object} properties.stroke - The stroke color of the style
11
- * @property {Object} properties.fill - The fill color of the style
11
+ * @property {Fill} properties.fill - The fill color of the style
12
12
  * @property {Object} properties.text - The text color of the style
13
13
  */
14
14
  export class Style {
@@ -1,4 +1,5 @@
1
- export * from './Style';
2
1
  export * from './Stroke';
2
+ export * from './HashPattern';
3
3
  export * from './Fill';
4
4
  export * from './Text';
5
+ export * from './Style';
package/dist/index.js CHANGED
@@ -42,10 +42,11 @@ import { MapEvent } from "./components/molecules/canvas-map/lib/events/MapEvent.
42
42
  import { Projection } from "./components/molecules/canvas-map/lib/projection/index.js";
43
43
  import { TextLayer } from "./components/molecules/canvas-map/lib/layers/TextLayer.js";
44
44
  import { VectorLayer } from "./components/molecules/canvas-map/lib/layers/VectorLayer.js";
45
- import { Style } from "./components/molecules/canvas-map/lib/styles/Style.js";
46
45
  import { Stroke, StrokePosition } from "./components/molecules/canvas-map/lib/styles/Stroke.js";
46
+ import { HashPattern } from "./components/molecules/canvas-map/lib/styles/HashPattern.js";
47
47
  import { Fill } from "./components/molecules/canvas-map/lib/styles/Fill.js";
48
48
  import { Text } from "./components/molecules/canvas-map/lib/styles/Text.js";
49
+ import { Style } from "./components/molecules/canvas-map/lib/styles/Style.js";
49
50
  import { GeoCoordinate } from "./components/molecules/canvas-map/lib/util/coordinate.js";
50
51
  import { GeoBounds } from "./components/molecules/canvas-map/lib/util/bounds.js";
51
52
  import { Extent, combineExtents, containsCoordinate, containsXY, extentForCoordinates } from "./components/molecules/canvas-map/lib/util/extent.js";
@@ -89,6 +90,7 @@ export {
89
90
  GeoJSON,
90
91
  GradientIcon,
91
92
  GridType,
93
+ HashPattern,
92
94
  InfoButton,
93
95
  LabelOverlapConfig,
94
96
  LabelType,
@@ -8,7 +8,11 @@ function useContainerSize(containerRef) {
8
8
  for (let entry of entries) {
9
9
  setContainerSize({
10
10
  width: entry.contentRect.width,
11
- height: entry.contentRect.height
11
+ height: entry.contentRect.height,
12
+ left: entry.contentRect.left,
13
+ right: entry.contentRect.right,
14
+ bottom: entry.contentRect.bottom,
15
+ top: entry.contentRect.top
12
16
  });
13
17
  }
14
18
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@guardian/interactive-component-library",
3
3
  "private": false,
4
- "version": "0.3.2",
4
+ "version": "0.4.1",
5
5
  "packageManager": "pnpm@8.4.0",
6
6
  "repository": {
7
7
  "type": "git",