@guardian/interactive-component-library 0.5.2 → 0.5.4

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.
Files changed (24) hide show
  1. package/dist/components/molecules/canvas-map/lib/Feature.d.ts +4 -1
  2. package/dist/components/molecules/canvas-map/lib/Feature.js +3 -0
  3. package/dist/components/molecules/canvas-map/lib/layers/TextLayer.d.ts +65 -16
  4. package/dist/components/molecules/canvas-map/lib/layers/TextLayer.js +28 -4
  5. package/dist/components/molecules/canvas-map/lib/layers/VectorLayer.d.ts +36 -10
  6. package/dist/components/molecules/canvas-map/lib/layers/VectorLayer.js +6 -3
  7. package/dist/components/molecules/canvas-map/lib/renderers/FeatureRenderer.js +0 -2
  8. package/dist/components/molecules/canvas-map/lib/renderers/MapRenderer.d.ts +14 -0
  9. package/dist/components/molecules/canvas-map/lib/renderers/MapRenderer.js +52 -8
  10. package/dist/components/molecules/canvas-map/lib/renderers/TextLayerRenderer.d.ts +61 -5
  11. package/dist/components/molecules/canvas-map/lib/renderers/TextLayerRenderer.js +224 -26
  12. package/dist/components/molecules/canvas-map/lib/renderers/VectorLayerRenderer.d.ts +13 -6
  13. package/dist/components/molecules/canvas-map/lib/renderers/VectorLayerRenderer.js +12 -42
  14. package/dist/components/molecules/canvas-map/lib/sources/VectorSource.d.ts +24 -6
  15. package/dist/components/molecules/canvas-map/lib/sources/VectorSource.js +18 -0
  16. package/dist/components/molecules/canvas-map/lib/styles/Style.d.ts +42 -11
  17. package/dist/components/molecules/canvas-map/lib/styles/Style.js +7 -0
  18. package/dist/components/molecules/canvas-map/lib/styles/Text.d.ts +184 -11
  19. package/dist/components/molecules/canvas-map/lib/styles/Text.js +18 -3
  20. package/dist/components/molecules/result-summary/index.d.ts +3 -1
  21. package/dist/components/molecules/result-summary/index.js +38 -5
  22. package/dist/components/molecules/result-summary/style.module.css.js +12 -3
  23. package/dist/style.css +21 -2
  24. package/package.json +1 -1
@@ -32,7 +32,10 @@ export class Feature {
32
32
  */
33
33
  getExtent(): import('./util').Extent;
34
34
  getProjectedGeometries(projection: any): any;
35
- getStyleFunction(): any;
35
+ /**
36
+ * @returns {import("./styles/Style").StyleFunction}
37
+ */
38
+ getStyleFunction(): import('./styles/Style').StyleFunction;
36
39
  containsCoordinate(coordinate: any): boolean;
37
40
  clone(): Feature;
38
41
  /**
@@ -45,6 +45,9 @@ class Feature {
45
45
  getProjectedGeometries(projection) {
46
46
  return this._getProjectedGeometries(projection, projection.revision);
47
47
  }
48
+ /**
49
+ * @returns {import("./styles/Style").StyleFunction}
50
+ */
48
51
  getStyleFunction() {
49
52
  const style = this.style;
50
53
  if (!style) return null;
@@ -19,34 +19,83 @@ export class TextLayer {
19
19
  * @param {number} [params.opacity=1]
20
20
  * @param {boolean} [params.declutter=true]
21
21
  * @param {boolean} [params.drawCollisionBoxes=false]
22
+ * @param {(feature: import('../Feature').Feature, event: MouseEvent) => void} [params.onClick]
23
+ * @param {(feature: import('../Feature').Feature, event: MouseEvent) => (() => void) | void} [params.onHover]
24
+ * A callback that's called when the mouse hovers over a feature in this layer. The callback is
25
+ * called with the hovered feature, and the mouse event.
26
+ *
27
+ * The callback can optionally return a cleanup function that will be called when the mouse leaves this feature.
28
+ * @param {boolean} [params.restyleOnHover] If true, the layer will re-render when the mouse hovers over a feature.
29
+ *
30
+ * The provided style function will be called with the `isHovering` parameter set to `true` for the hovered feature.
22
31
  */
23
- constructor({ source, style, minZoom, opacity, declutter, drawCollisionBoxes, }: {
32
+ constructor({ source, style, minZoom, opacity, declutter, drawCollisionBoxes, onClick, onHover, restyleOnHover, }: {
24
33
  source: VectorSource;
25
34
  style?: Style | (() => Style);
26
35
  minZoom?: number;
27
36
  opacity?: number;
28
37
  declutter?: boolean;
29
38
  drawCollisionBoxes?: boolean;
39
+ onClick?: (feature: import('../Feature').Feature, event: MouseEvent) => void;
40
+ onHover?: (feature: import('../Feature').Feature, event: MouseEvent) => (() => void) | void;
41
+ restyleOnHover?: boolean;
30
42
  });
31
- source: VectorSource;
32
- _style: Style | (() => Style);
33
- minZoom: number;
34
- opacity: number;
35
- declutter: boolean;
36
- drawCollisionBoxes: boolean;
37
- renderer: TextLayerRenderer;
38
- dispatcher: Dispatcher;
43
+ /**
44
+ * @type {VectorSource}
45
+ * @public
46
+ */
47
+ public source: VectorSource;
48
+ /**
49
+ * @type {Style|import("../styles").StyleFunction}
50
+ */
51
+ _style: Style | import('../styles').StyleFunction;
52
+ /**
53
+ * @type {number}
54
+ * @public
55
+ */
56
+ public minZoom: number;
57
+ /**
58
+ * @type {number}
59
+ * @public
60
+ */
61
+ public opacity: number;
62
+ /**
63
+ * @type {boolean}
64
+ * @public
65
+ */
66
+ public declutter: boolean;
67
+ /**
68
+ * @type {boolean}
69
+ * @public
70
+ */
71
+ public drawCollisionBoxes: boolean;
72
+ onClick: (feature: import('../Feature').Feature, event: MouseEvent) => void;
73
+ onHover: (feature: import('../Feature').Feature, event: MouseEvent) => (() => void) | void;
74
+ restyleOnHover: boolean;
75
+ /**
76
+ * @type {TextLayerRenderer}
77
+ * @public
78
+ */
79
+ public renderer: TextLayerRenderer;
80
+ /**
81
+ * @type {Dispatcher}
82
+ * @public
83
+ */
84
+ public dispatcher: Dispatcher;
39
85
  tearDown(): void;
40
- set style(style: Style | (() => Style));
41
- get style(): Style | (() => Style);
42
- getExtent(): any;
43
- _extent: any;
44
- getStyleFunction(): () => Style;
45
- renderFrame(frameState: any, targetElement: any): any;
86
+ set style(style: Style | import('../styles').StyleFunction);
87
+ get style(): Style | import('../styles').StyleFunction;
88
+ getExtent(): import('../Feature').Feature;
89
+ _extent: import('../Feature').Feature;
90
+ /**
91
+ * @returns {import("../styles/Style").StyleFunction}
92
+ */
93
+ getStyleFunction(): import('../styles/Style').StyleFunction;
94
+ renderFrame(frameState: any, targetElement: any): HTMLDivElement;
46
95
  }
47
96
  export namespace TextLayer {
48
97
  /** @param {TextLayerComponentProps} props */
49
- function Component({ features: featureCollection, style, minZoom, opacity, declutter, drawCollisionBoxes, }: TextLayerComponentProps): boolean;
98
+ function Component({ features: featureCollection, style, minZoom, opacity, declutter, drawCollisionBoxes, onClick, onHover, restyleOnHover, }: TextLayerComponentProps): any;
50
99
  namespace Component {
51
100
  let displayName: string;
52
101
  }
@@ -16,7 +16,10 @@ class TextLayer {
16
16
  minZoom,
17
17
  opacity,
18
18
  declutter,
19
- drawCollisionBoxes
19
+ drawCollisionBoxes,
20
+ onClick,
21
+ onHover,
22
+ restyleOnHover
20
23
  }) {
21
24
  const { registerLayer, unregisterLayer } = useContext(MapContext);
22
25
  const layer = useMemo(
@@ -30,7 +33,10 @@ class TextLayer {
30
33
  minZoom,
31
34
  opacity,
32
35
  declutter,
33
- drawCollisionBoxes
36
+ drawCollisionBoxes,
37
+ onClick,
38
+ onHover,
39
+ restyleOnHover
34
40
  });
35
41
  },
36
42
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -45,7 +51,7 @@ class TextLayer {
45
51
  useEffect(() => {
46
52
  layer.style = style;
47
53
  }, [style]);
48
- return false;
54
+ return null;
49
55
  }
50
56
  /**
51
57
  * @param {import("../Feature").Feature[]} features
@@ -64,6 +70,15 @@ class TextLayer {
64
70
  * @param {number} [params.opacity=1]
65
71
  * @param {boolean} [params.declutter=true]
66
72
  * @param {boolean} [params.drawCollisionBoxes=false]
73
+ * @param {(feature: import('../Feature').Feature, event: MouseEvent) => void} [params.onClick]
74
+ * @param {(feature: import('../Feature').Feature, event: MouseEvent) => (() => void) | void} [params.onHover]
75
+ * A callback that's called when the mouse hovers over a feature in this layer. The callback is
76
+ * called with the hovered feature, and the mouse event.
77
+ *
78
+ * The callback can optionally return a cleanup function that will be called when the mouse leaves this feature.
79
+ * @param {boolean} [params.restyleOnHover] If true, the layer will re-render when the mouse hovers over a feature.
80
+ *
81
+ * The provided style function will be called with the `isHovering` parameter set to `true` for the hovered feature.
67
82
  */
68
83
  constructor({
69
84
  source,
@@ -71,7 +86,10 @@ class TextLayer {
71
86
  minZoom = 0,
72
87
  opacity = 1,
73
88
  declutter = true,
74
- drawCollisionBoxes = false
89
+ drawCollisionBoxes = false,
90
+ onClick,
91
+ onHover,
92
+ restyleOnHover
75
93
  }) {
76
94
  this.source = source;
77
95
  this._style = style;
@@ -79,6 +97,9 @@ class TextLayer {
79
97
  this.opacity = opacity;
80
98
  this.declutter = declutter;
81
99
  this.drawCollisionBoxes = drawCollisionBoxes;
100
+ this.onClick = onClick;
101
+ this.onHover = onHover;
102
+ this.restyleOnHover = restyleOnHover;
82
103
  this.renderer = new TextLayerRenderer(this);
83
104
  this.dispatcher = new Dispatcher(this);
84
105
  }
@@ -107,6 +128,9 @@ class TextLayer {
107
128
  this._extent = extent;
108
129
  return extent;
109
130
  }
131
+ /**
132
+ * @returns {import("../styles/Style").StyleFunction}
133
+ */
110
134
  getStyleFunction() {
111
135
  const style = this.style;
112
136
  if (typeof style === "function") return style;
@@ -25,22 +25,48 @@ export class VectorLayer {
25
25
  opacity?: number;
26
26
  hitDetectionEnabled?: boolean;
27
27
  });
28
- dispatcher: Dispatcher;
29
- renderer: VectorLayerRenderer;
30
28
  set source(source: any);
31
29
  get source(): any;
32
- _style: Style | (() => Style);
33
- minZoom: number;
34
- opacity: number;
35
- hitDetectionEnabled: boolean;
30
+ /**
31
+ * @type {Dispatcher}
32
+ * @public
33
+ */
34
+ public dispatcher: Dispatcher;
35
+ /**
36
+ * @type {VectorLayerRenderer}
37
+ * @public
38
+ */
39
+ public renderer: VectorLayerRenderer;
40
+ /**
41
+ * @type {number}
42
+ * @public
43
+ */
44
+ public minZoom: number;
45
+ /**
46
+ * @type {number}
47
+ * @public
48
+ */
49
+ public opacity: number;
50
+ /**
51
+ * @type {boolean}
52
+ * @public
53
+ */
54
+ public hitDetectionEnabled: boolean;
55
+ /**
56
+ * @type {Style|import("../styles").StyleFunction}
57
+ */
58
+ _style: Style | import('../styles').StyleFunction;
36
59
  _source: any;
37
60
  _extent: any;
38
61
  setRawProjection(projection: any): void;
39
62
  projection: any;
40
63
  tearDown(): void;
41
- set style(style: Style | (() => Style));
42
- get style(): Style | (() => Style);
43
- getStyleFunction(): () => Style;
64
+ set style(style: Style | import('../styles').StyleFunction);
65
+ get style(): Style | import('../styles').StyleFunction;
66
+ /**
67
+ * @returns {import("../styles/Style").StyleFunction}
68
+ */
69
+ getStyleFunction(): import('../styles/Style').StyleFunction;
44
70
  /**
45
71
  * Get the extent of the features in the layer.
46
72
  * @returns {import("../util").Extent | null} The extent of the features in the layer, or null if the layer is empty.
@@ -51,7 +77,7 @@ export class VectorLayer {
51
77
  }
52
78
  export namespace VectorLayer {
53
79
  /** @param {VectorLayerComponentProps} props */
54
- function Component({ features: featureCollection, style, minZoom, opacity, hitDetectionEnabled, }: VectorLayerComponentProps): boolean;
80
+ function Component({ features: featureCollection, style, minZoom, opacity, hitDetectionEnabled, }: VectorLayerComponentProps): any;
55
81
  namespace Component {
56
82
  let displayName: string;
57
83
  }
@@ -43,7 +43,7 @@ class VectorLayer {
43
43
  useEffect(() => {
44
44
  layer.style = style;
45
45
  }, [style]);
46
- return false;
46
+ return null;
47
47
  }
48
48
  /**
49
49
  * @param {import("../Feature").Feature[]} features
@@ -68,13 +68,13 @@ class VectorLayer {
68
68
  opacity = 1,
69
69
  hitDetectionEnabled = true
70
70
  }) {
71
+ this.source = source;
71
72
  this.dispatcher = new Dispatcher(this);
72
73
  this.renderer = new VectorLayerRenderer(this);
73
- this.source = source;
74
- this._style = style;
75
74
  this.minZoom = minZoom;
76
75
  this.opacity = opacity;
77
76
  this.hitDetectionEnabled = hitDetectionEnabled;
77
+ this._style = style;
78
78
  }
79
79
  get source() {
80
80
  return this._source;
@@ -106,6 +106,9 @@ class VectorLayer {
106
106
  this._style = style;
107
107
  this.dispatcher.dispatch(MapEvent.CHANGE);
108
108
  }
109
+ /**
110
+ * @returns {import("../styles/Style").StyleFunction}
111
+ */
109
112
  getStyleFunction() {
110
113
  const style = this.style;
111
114
  if (typeof style === "function") return style;
@@ -58,8 +58,6 @@ class FeatureRenderer {
58
58
  }
59
59
  if (clipPath) {
60
60
  context.clip();
61
- } else {
62
- context.closePath();
63
61
  }
64
62
  }
65
63
  drawStroke(frameState, context, { style, width: strokeWidth, position }) {
@@ -1,6 +1,20 @@
1
+ /**
2
+ * @typedef {Object} CanvasSingleton
3
+ * @property {() => CanvasRenderingContext2D} getContext2d
4
+ * @property {() => HTMLDivElement} getContainer
5
+ * @property {() => boolean} isInitialised
6
+ *
7
+ * @typedef {Object} FrameState
8
+ */
1
9
  export class MapRenderer {
2
10
  constructor(map: any);
3
11
  map: any;
4
12
  _element: HTMLDivElement;
5
13
  renderFrame(frameState: any): void;
6
14
  }
15
+ export type CanvasSingleton = {
16
+ getContext2d: () => CanvasRenderingContext2D;
17
+ getContainer: () => HTMLDivElement;
18
+ isInitialised: () => boolean;
19
+ };
20
+ export type FrameState = any;
@@ -14,25 +14,24 @@ class MapRenderer {
14
14
  container.insertBefore(this._element, container.firstChild || null);
15
15
  }
16
16
  renderFrame(frameState) {
17
- const { zoomLevel, projection } = frameState.viewState;
17
+ const { zoomLevel, projection, sizeInPixels } = frameState.viewState;
18
18
  const layers = this.map.layers;
19
19
  const mapElements = [];
20
- let previousElement = null;
21
20
  const visibleLayers = layers.filter((layer) => {
22
21
  return zoomLevel > (layer.minZoom || 0);
23
22
  });
23
+ const canvasSingleton = makeCanvasSingleton(sizeInPixels);
24
24
  const renderLayer = (layer, declutterTree2) => {
25
25
  const viewState = frameState.viewState;
26
26
  if (layer.projection) {
27
27
  viewState.projection = layer.projection;
28
28
  }
29
- const element = layer.renderFrame(
29
+ const newContainer = layer.renderFrame(
30
30
  { ...frameState, viewState, declutterTree: declutterTree2 },
31
- previousElement
31
+ canvasSingleton
32
32
  );
33
- if (element !== previousElement) {
34
- mapElements.push(element);
35
- previousElement = element;
33
+ if (newContainer) {
34
+ mapElements.push(newContainer);
36
35
  }
37
36
  viewState.projection = projection;
38
37
  };
@@ -44,9 +43,54 @@ class MapRenderer {
44
43
  renderLayer(layer);
45
44
  }
46
45
  }
47
- replaceChildren(this._element, mapElements);
46
+ if (canvasSingleton.isInitialised()) {
47
+ replaceChildren(this._element, [
48
+ canvasSingleton.getContainer(),
49
+ ...mapElements
50
+ ]);
51
+ } else {
52
+ replaceChildren(this._element, mapElements);
53
+ }
48
54
  }
49
55
  }
56
+ function makeCanvasSingleton(sizeInPixels) {
57
+ let canvasLayer;
58
+ let canvasContext2d;
59
+ return {
60
+ getContext2d: () => {
61
+ if (!canvasLayer) {
62
+ canvasLayer = createCanvasMapLayer(sizeInPixels);
63
+ canvasContext2d = canvasLayer.firstElementChild.getContext("2d");
64
+ }
65
+ return canvasContext2d;
66
+ },
67
+ getContainer: () => {
68
+ if (!canvasLayer) {
69
+ canvasLayer = createCanvasMapLayer(sizeInPixels);
70
+ canvasContext2d = canvasLayer.firstElementChild.getContext("2d");
71
+ }
72
+ return canvasLayer;
73
+ },
74
+ isInitialised: () => !!canvasLayer
75
+ };
76
+ }
77
+ function createCanvasMapLayer(sizeInPixels) {
78
+ const container = document.createElement("div");
79
+ container.className = "gv-map-layer";
80
+ let style = container.style;
81
+ style.position = "absolute";
82
+ style.width = "100%";
83
+ style.height = "100%";
84
+ const canvas = document.createElement("canvas");
85
+ style = canvas.style;
86
+ style.position = "absolute";
87
+ style.width = "100%";
88
+ style.height = "100%";
89
+ canvas.width = sizeInPixels[0];
90
+ canvas.height = sizeInPixels[1];
91
+ container.appendChild(canvas);
92
+ return container;
93
+ }
50
94
  export {
51
95
  MapRenderer
52
96
  };
@@ -1,21 +1,77 @@
1
1
  import { FeatureRenderer } from './FeatureRenderer';
2
2
  export class TextLayerRenderer {
3
- constructor(layer: any);
4
- layer: any;
3
+ /**
4
+ * @param {import("../layers/TextLayer").TextLayer} layer
5
+ */
6
+ constructor(layer: import('../layers/TextLayer').TextLayer);
7
+ /**
8
+ * @type {import("../layers/TextLayer").TextLayer}
9
+ */
10
+ layer: import('../layers/TextLayer').TextLayer;
5
11
  featureRenderer: FeatureRenderer;
6
12
  _element: HTMLDivElement;
7
- renderFrame(frameState: any, targetElement: any): any;
13
+ _mouseInteractionsEnabled: boolean | ((feature: import('../Feature').Feature, event: MouseEvent) => void);
14
+ /**
15
+ * @param {import("./MapRenderer").CanvasSingleton} canvasSingleton
16
+ */
17
+ renderFrame(frameState: any, canvasSingleton: import('./MapRenderer').CanvasSingleton): HTMLDivElement;
8
18
  getTextElementWithID(id: any): Element;
9
- styleTextElement(element: any, textStyle: any, position: any): void;
19
+ /**
20
+ * @param {HTMLDivElement} element
21
+ * @param {import("../styles/Text").Text} textStyle
22
+ * @param {{left: string, top: string}} position
23
+ */
24
+ styleTextElement(element: HTMLDivElement, textStyle: import('../styles/Text').Text, position: {
25
+ left: string;
26
+ top: string;
27
+ }): {
28
+ width: any;
29
+ height: any;
30
+ };
10
31
  getElementSize(element: any): {
11
32
  width: any;
12
33
  height: any;
13
34
  };
14
- getElementBBox(element: any, textStyle: any, position: any): {
35
+ /**
36
+ * @param {{ height: number, width: number }} dimens
37
+ * @param {import("../styles/Text").Text} textStyle
38
+ * @param {{x: number, y: number}} position
39
+ */
40
+ getElementBBox(dimens: {
41
+ height: number;
42
+ width: number;
43
+ }, textStyle: import('../styles/Text').Text, position: {
44
+ x: number;
45
+ y: number;
46
+ }): {
15
47
  minX: number;
16
48
  minY: number;
17
49
  maxX: number;
18
50
  maxY: number;
19
51
  };
52
+ /**
53
+ * @param {import("../styles/Text").Text} textStyle
54
+ * @param {{x: number, y: number}} position
55
+ */
56
+ getElementPosition(textStyle: import('../styles/Text').Text, position: {
57
+ x: number;
58
+ y: number;
59
+ }): {
60
+ left: string;
61
+ top: string;
62
+ };
20
63
  getCollisionBoxElement(bbox: any): HTMLDivElement;
64
+ /**
65
+ * Draws a `Text` element's icon on the canvas.
66
+ *
67
+ * Expects the canvas context to be translated to the text element's position.
68
+ *
69
+ * TODO: support more shapes?
70
+ *
71
+ * @param {CanvasRenderingContext2D} context
72
+ * @param {import("../styles/Text").IconOptions} icon
73
+ */
74
+ drawTextIcon(context: CanvasRenderingContext2D, icon: import('../styles/Text').IconOptions): void;
75
+ attachClickAndHoverListeners(): void;
76
+ _hoveredFeature: import('../Feature').Feature;
21
77
  }