@guardian/interactive-component-library 0.5.3 → 0.5.5
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/dist/components/molecules/canvas-map/lib/layers/TextLayer.d.ts +17 -2
- package/dist/components/molecules/canvas-map/lib/layers/TextLayer.js +24 -3
- package/dist/components/molecules/canvas-map/lib/renderers/FeatureRenderer.js +0 -2
- package/dist/components/molecules/canvas-map/lib/renderers/TextLayerRenderer.d.ts +12 -1
- package/dist/components/molecules/canvas-map/lib/renderers/TextLayerRenderer.js +82 -31
- package/dist/components/molecules/canvas-map/lib/styles/Style.d.ts +3 -1
- package/dist/components/molecules/result-summary/index.d.ts +2 -1
- package/dist/components/molecules/result-summary/index.js +26 -18
- package/package.json +1 -1
|
@@ -19,14 +19,26 @@ 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
43
|
/**
|
|
32
44
|
* @type {VectorSource}
|
|
@@ -57,6 +69,9 @@ export class TextLayer {
|
|
|
57
69
|
* @public
|
|
58
70
|
*/
|
|
59
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;
|
|
60
75
|
/**
|
|
61
76
|
* @type {TextLayerRenderer}
|
|
62
77
|
* @public
|
|
@@ -80,7 +95,7 @@ export class TextLayer {
|
|
|
80
95
|
}
|
|
81
96
|
export namespace TextLayer {
|
|
82
97
|
/** @param {TextLayerComponentProps} props */
|
|
83
|
-
function Component({ features: featureCollection, style, minZoom, opacity, declutter, drawCollisionBoxes, }: TextLayerComponentProps): any;
|
|
98
|
+
function Component({ features: featureCollection, style, minZoom, opacity, declutter, drawCollisionBoxes, onClick, onHover, restyleOnHover, }: TextLayerComponentProps): any;
|
|
84
99
|
namespace Component {
|
|
85
100
|
let displayName: string;
|
|
86
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
|
|
@@ -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
|
}
|
|
@@ -10,12 +10,21 @@ export class TextLayerRenderer {
|
|
|
10
10
|
layer: import('../layers/TextLayer').TextLayer;
|
|
11
11
|
featureRenderer: FeatureRenderer;
|
|
12
12
|
_element: HTMLDivElement;
|
|
13
|
+
_mouseInteractionsEnabled: boolean | ((feature: import('../Feature').Feature, event: MouseEvent) => void);
|
|
13
14
|
/**
|
|
14
15
|
* @param {import("./MapRenderer").CanvasSingleton} canvasSingleton
|
|
15
16
|
*/
|
|
16
17
|
renderFrame(frameState: any, canvasSingleton: import('./MapRenderer').CanvasSingleton): HTMLDivElement;
|
|
17
18
|
getTextElementWithID(id: any): Element;
|
|
18
|
-
|
|
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
|
+
}): {
|
|
19
28
|
width: any;
|
|
20
29
|
height: any;
|
|
21
30
|
};
|
|
@@ -63,4 +72,6 @@ export class TextLayerRenderer {
|
|
|
63
72
|
* @param {import("../styles/Text").IconOptions} icon
|
|
64
73
|
*/
|
|
65
74
|
drawTextIcon(context: CanvasRenderingContext2D, icon: import('../styles/Text').IconOptions): void;
|
|
75
|
+
attachClickAndHoverListeners(): void;
|
|
76
|
+
_hoveredFeature: import('../Feature').Feature;
|
|
66
77
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FeatureRenderer } from "./FeatureRenderer.js";
|
|
2
2
|
import { replaceChildren } from "../util/dom.js";
|
|
3
|
+
import { MapEvent } from "../events/MapEvent.js";
|
|
3
4
|
class TextLayerRenderer {
|
|
4
5
|
/**
|
|
5
6
|
* @param {import("../layers/TextLayer").TextLayer} layer
|
|
@@ -13,8 +14,10 @@ class TextLayerRenderer {
|
|
|
13
14
|
style.position = "absolute";
|
|
14
15
|
style.width = "100%";
|
|
15
16
|
style.height = "100%";
|
|
16
|
-
style.pointerEvents = "none";
|
|
17
17
|
style.overflow = "hidden";
|
|
18
|
+
style.pointerEvents = "none";
|
|
19
|
+
this._mouseInteractionsEnabled = this.layer.onClick || this.layer.onHover || this.layer.restyleOnHover;
|
|
20
|
+
this.attachClickAndHoverListeners();
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* @param {import("./MapRenderer").CanvasSingleton} canvasSingleton
|
|
@@ -38,7 +41,11 @@ class TextLayerRenderer {
|
|
|
38
41
|
);
|
|
39
42
|
}
|
|
40
43
|
const styleFunction = feature.getStyleFunction() || this.layer.getStyleFunction();
|
|
41
|
-
const featureStyle = styleFunction(
|
|
44
|
+
const featureStyle = styleFunction(
|
|
45
|
+
feature,
|
|
46
|
+
transform.k,
|
|
47
|
+
this._hoveredFeature === feature
|
|
48
|
+
);
|
|
42
49
|
const textElement = this.getTextElementWithID(feature.uid);
|
|
43
50
|
textElement.innerText = featureStyle.text.content;
|
|
44
51
|
const [canvasX, canvasY] = transform.apply(point.coordinates);
|
|
@@ -120,9 +127,19 @@ class TextLayerRenderer {
|
|
|
120
127
|
if (!textElement) {
|
|
121
128
|
textElement = document.createElement("div");
|
|
122
129
|
textElement.id = elementId;
|
|
130
|
+
textElement.dataset.featureId = id;
|
|
131
|
+
}
|
|
132
|
+
if (this._mouseInteractionsEnabled) {
|
|
133
|
+
textElement.style.pointerEvents = "auto";
|
|
134
|
+
textElement.style.cursor = "pointer";
|
|
123
135
|
}
|
|
124
136
|
return textElement;
|
|
125
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* @param {HTMLDivElement} element
|
|
140
|
+
* @param {import("../styles/Text").Text} textStyle
|
|
141
|
+
* @param {{left: string, top: string}} position
|
|
142
|
+
*/
|
|
126
143
|
styleTextElement(element, textStyle, position) {
|
|
127
144
|
const style = element.style;
|
|
128
145
|
style.position = "absolute";
|
|
@@ -136,7 +153,20 @@ class TextLayerRenderer {
|
|
|
136
153
|
style.lineHeight = textStyle.lineHeight;
|
|
137
154
|
style.color = textStyle.color;
|
|
138
155
|
style.textShadow = textStyle.textShadow;
|
|
139
|
-
|
|
156
|
+
let { width, height } = this.getElementSize(element);
|
|
157
|
+
if (textStyle.icon) {
|
|
158
|
+
const iconSize = textStyle.icon.size;
|
|
159
|
+
if (textStyle.icon.position === "left") {
|
|
160
|
+
style.paddingLeft = `${iconSize + textStyle.icon.padding * 2}px`;
|
|
161
|
+
} else if (textStyle.icon.position === "right") {
|
|
162
|
+
style.paddingRight = `${iconSize + textStyle.icon.padding * 2}px`;
|
|
163
|
+
}
|
|
164
|
+
const iconSizeHeightDiff = iconSize - height;
|
|
165
|
+
if (iconSizeHeightDiff > 0) {
|
|
166
|
+
style.paddingTop = `${iconSizeHeightDiff / 2}px`;
|
|
167
|
+
style.paddingBottom = `${iconSizeHeightDiff / 2}px`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
140
170
|
style.transform = textStyle.getTransform(width, height);
|
|
141
171
|
return { width, height };
|
|
142
172
|
}
|
|
@@ -176,16 +206,6 @@ class TextLayerRenderer {
|
|
|
176
206
|
maxX += textStyle.callout.offsetBy.x;
|
|
177
207
|
maxY += textStyle.callout.offsetBy.y;
|
|
178
208
|
}
|
|
179
|
-
if (textStyle.icon) {
|
|
180
|
-
if (textStyle.icon.position === "left" || textStyle.icon.position === "right") {
|
|
181
|
-
maxX += textStyle.icon.size + textStyle.icon.padding;
|
|
182
|
-
}
|
|
183
|
-
const iconSizeHeightDiff = textStyle.icon.size - dimens.height;
|
|
184
|
-
if (iconSizeHeightDiff > 0) {
|
|
185
|
-
minY -= iconSizeHeightDiff / 2;
|
|
186
|
-
maxY += iconSizeHeightDiff / 2;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
209
|
minX = Math.floor(minX);
|
|
190
210
|
minY = Math.floor(minY);
|
|
191
211
|
maxX = Math.ceil(maxX);
|
|
@@ -203,29 +223,11 @@ class TextLayerRenderer {
|
|
|
203
223
|
*/
|
|
204
224
|
getElementPosition(textStyle, position) {
|
|
205
225
|
if (textStyle.callout) {
|
|
206
|
-
if (textStyle.icon.position === "left") {
|
|
207
|
-
return {
|
|
208
|
-
left: `calc(${position.x * 100}% + ${textStyle.callout.offsetBy.x + (textStyle.icon.size + textStyle.icon.padding * 2)}px)`,
|
|
209
|
-
top: `calc(${position.y * 100}% + ${textStyle.callout.offsetBy.y}px)`
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
if (textStyle.icon.position === "right") {
|
|
213
|
-
return {
|
|
214
|
-
left: `calc(${position.x * 100}% + ${textStyle.callout.offsetBy.x}px)`,
|
|
215
|
-
top: `calc(${position.y * 100}% + ${textStyle.callout.offsetBy.y}px)`
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
226
|
return {
|
|
219
227
|
left: `calc(${position.x * 100}% + ${textStyle.callout.offsetBy.x}px)`,
|
|
220
228
|
top: `calc(${position.y * 100}% + ${textStyle.callout.offsetBy.y}px)`
|
|
221
229
|
};
|
|
222
230
|
}
|
|
223
|
-
if (textStyle.icon && (textStyle.icon.position === "left" || textStyle.icon.position === "right")) {
|
|
224
|
-
return {
|
|
225
|
-
left: `calc(${position.x * 100}% + ${textStyle.icon.size + textStyle.icon.padding * 2}px)`,
|
|
226
|
-
top: `${position.y * 100}%`
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
231
|
return {
|
|
230
232
|
left: `${position.x * 100}%`,
|
|
231
233
|
top: `${position.y * 100}%`
|
|
@@ -276,6 +278,55 @@ class TextLayerRenderer {
|
|
|
276
278
|
}
|
|
277
279
|
}
|
|
278
280
|
}
|
|
281
|
+
attachClickAndHoverListeners() {
|
|
282
|
+
if (this.layer.onClick) {
|
|
283
|
+
this._element.addEventListener("click", (event) => {
|
|
284
|
+
if (!event.target) return;
|
|
285
|
+
const clickedFeature = this.layer.source.getFeatures().find((feature) => {
|
|
286
|
+
var _a;
|
|
287
|
+
return ((_a = event.target.dataset) == null ? void 0 : _a.featureId) === feature.uid;
|
|
288
|
+
});
|
|
289
|
+
if (!clickedFeature) return;
|
|
290
|
+
this.layer.onClick(clickedFeature, event);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (this.layer.onHover) {
|
|
294
|
+
this._element.addEventListener("mouseover", (event) => {
|
|
295
|
+
if (!event.target) return;
|
|
296
|
+
const hoveredFeature = this.layer.source.getFeatures().find((feature) => {
|
|
297
|
+
var _a;
|
|
298
|
+
return ((_a = event.target.dataset) == null ? void 0 : _a.featureId) === feature.uid;
|
|
299
|
+
});
|
|
300
|
+
if (!hoveredFeature) return;
|
|
301
|
+
const onHoverLeave = this.layer.onHover(hoveredFeature, event);
|
|
302
|
+
if (onHoverLeave) {
|
|
303
|
+
this._element.addEventListener("mouseout", onHoverLeave, {
|
|
304
|
+
once: true
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (this.layer.restyleOnHover) {
|
|
310
|
+
this._element.addEventListener("mouseover", (event) => {
|
|
311
|
+
if (!event.target) return;
|
|
312
|
+
const hoveredFeature = this.layer.source.getFeatures().find((feature) => {
|
|
313
|
+
var _a;
|
|
314
|
+
return ((_a = event.target.dataset) == null ? void 0 : _a.featureId) === feature.uid;
|
|
315
|
+
});
|
|
316
|
+
if (!hoveredFeature) return;
|
|
317
|
+
this._hoveredFeature = hoveredFeature;
|
|
318
|
+
this.layer.dispatcher.dispatch(MapEvent.CHANGE);
|
|
319
|
+
this._element.addEventListener(
|
|
320
|
+
"mouseout",
|
|
321
|
+
() => {
|
|
322
|
+
this._hoveredFeature = void 0;
|
|
323
|
+
this.layer.dispatcher.dispatch(MapEvent.CHANGE);
|
|
324
|
+
},
|
|
325
|
+
{ once: true }
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
279
330
|
}
|
|
280
331
|
export {
|
|
281
332
|
TextLayerRenderer
|
|
@@ -9,6 +9,8 @@ import { Text } from './Text';
|
|
|
9
9
|
* @callback StyleFunction
|
|
10
10
|
* @param {import("../Feature").Feature} feature The feature to style.
|
|
11
11
|
* @param {number} zoom The current map zoom level
|
|
12
|
+
* @param {boolean} [isHovering] If the layer has `restyleOnHover` enabled, this will be true if the
|
|
13
|
+
* feature is currently being hovered over.
|
|
12
14
|
* @returns {Style}
|
|
13
15
|
*/
|
|
14
16
|
/**
|
|
@@ -52,4 +54,4 @@ export class Style {
|
|
|
52
54
|
pointRadius: number;
|
|
53
55
|
clone(): Style;
|
|
54
56
|
}
|
|
55
|
-
export type StyleFunction = (feature: import('../Feature').Feature, zoom: number) => Style;
|
|
57
|
+
export type StyleFunction = (feature: import('../Feature').Feature, zoom: number, isHovering?: boolean) => Style;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export function ResultSummary({ previous, next, title, text, timestamp, isSlim, styles, }: {
|
|
1
|
+
export function ResultSummary({ previous, next, title, text, timestamp, onClick, isSlim, styles, }: {
|
|
2
2
|
previous: any;
|
|
3
3
|
next: any;
|
|
4
4
|
title: any;
|
|
5
5
|
text: any;
|
|
6
6
|
timestamp: any;
|
|
7
|
+
onClick: any;
|
|
7
8
|
isSlim?: boolean;
|
|
8
9
|
styles: any;
|
|
9
10
|
}): import("preact").JSX.Element;
|
|
@@ -28,30 +28,38 @@ function ResultSummary({
|
|
|
28
28
|
title,
|
|
29
29
|
text,
|
|
30
30
|
timestamp,
|
|
31
|
+
onClick,
|
|
31
32
|
isSlim = false,
|
|
32
33
|
styles
|
|
33
34
|
}) {
|
|
34
35
|
styles = mergeStyles({ ...defaultStyles }, styles);
|
|
35
36
|
if (isSlim) {
|
|
36
37
|
let hasChanged = next !== previous;
|
|
37
|
-
return /* @__PURE__ */ jsxs(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
return /* @__PURE__ */ jsxs(
|
|
39
|
+
"div",
|
|
40
|
+
{
|
|
41
|
+
className: `${styles.container} ${styles.containerSlim}`,
|
|
42
|
+
onClick,
|
|
43
|
+
children: [
|
|
44
|
+
hasChanged ? /* @__PURE__ */ jsx(
|
|
45
|
+
GradientIcon,
|
|
46
|
+
{
|
|
47
|
+
previous,
|
|
48
|
+
next,
|
|
49
|
+
styles: {
|
|
50
|
+
previous: styles.previous,
|
|
51
|
+
next: styles.next
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
) : /* @__PURE__ */ jsx(CircleIcon, { styles: { circle: `fill-color--${next}` } }),
|
|
55
|
+
/* @__PURE__ */ jsxs("p", { className: styles.titleSlim, children: [
|
|
56
|
+
title,
|
|
57
|
+
" "
|
|
58
|
+
] }),
|
|
59
|
+
/* @__PURE__ */ jsx(RelativeTimeSentence, { timeStamp: timestamp })
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
);
|
|
55
63
|
} else {
|
|
56
64
|
return /* @__PURE__ */ jsxs("div", { className: styles.container, children: [
|
|
57
65
|
/* @__PURE__ */ jsx(ControlChange, { previous, next, text: title }),
|