Skip to content

Commit 9976692

Browse files
committed
[Map] Rework SvgIcon and factory method name, UX Icon to get generated HTML, and icons rendering
1 parent 311689c commit 9976692

14 files changed

+172
-66
lines changed

src/Map/assets/dist/abstract_map_controller.d.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@ export type WithIdentifier<T extends Record<string, unknown>> = T & {
99
};
1010
export declare const IconTypes: {
1111
readonly Url: "url";
12-
readonly InlineSvg: "inline-svg";
12+
readonly Svg: "svg";
1313
readonly UxIcon: "ux-icon";
1414
};
15-
export type IconType = (typeof IconTypes)[keyof typeof IconTypes];
1615
export type Icon = {
17-
content: string;
18-
type: IconType;
1916
width: number;
2017
height: number;
21-
};
18+
} & ({
19+
type: typeof IconTypes.UxIcon;
20+
name: string;
21+
_generated_html: string;
22+
} | {
23+
type: typeof IconTypes.Url;
24+
url: string;
25+
} | {
26+
type: typeof IconTypes.Svg;
27+
html: string;
28+
});
2229
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
2330
position: Point;
2431
title: string | null;

src/Map/assets/dist/abstract_map_controller.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Controller } from '@hotwired/stimulus';
22

33
const IconTypes = {
44
Url: 'url',
5-
InlineSvg: 'inline-svg',
5+
Svg: 'svg',
66
UxIcon: 'ux-icon',
77
};
88
class default_1 extends Controller {

src/Map/assets/src/abstract_map_controller.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Ide
66

77
export const IconTypes = {
88
Url: 'url',
9-
InlineSvg: 'inline-svg',
9+
Svg: 'svg',
1010
UxIcon: 'ux-icon',
1111
} as const;
12-
export type IconType = (typeof IconTypes)[keyof typeof IconTypes];
1312
export type Icon = {
14-
content: string;
15-
type: IconType;
1613
width: number;
1714
height: number;
18-
};
15+
} & (
16+
| {
17+
type: typeof IconTypes.UxIcon;
18+
name: string;
19+
_generated_html: string,
20+
}
21+
| {
22+
type: typeof IconTypes.Url;
23+
url: string;
24+
}
25+
| {
26+
type: typeof IconTypes.Svg;
27+
html: string;
28+
}
29+
);
1930

2031
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
2132
position: Point;

src/Map/doc/index.rst

+6-9
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,12 @@ Add Marker icons
141141

142142
A ``Marker`` can be customized with an ``Icon`` instance, which can either be an UX Icon, an URL, or a SVG content::
143143

144-
// It can be a UX Icon (requires `symfony/ux-icons` package)
145-
$icon = Icon::ux('fa:map-marker');
146-
// Or an URL pointing to an image
147-
$icon = Icon::url('https://example.com/marker.png');
148-
// Or a plain SVG content
149-
$icon = Icon::svg('<svg>(...)</svg>');
150-
151-
// Configure the icon size with the `width()` and `height()` methods
152-
$icon = Icon::ux('fa:map-marker')->width(48)->height(48);
144+
// It can be a UX Icon (requires `symfony/ux-icons` package)...
145+
$icon = Icon::ux('fa:map-marker')->width(24)->height(24);
146+
// ... or an URL pointing to an image
147+
$icon = Icon::url('https://example.com/marker.png')->width(24)->height(24);
148+
// ... or a plain SVG string
149+
$icon = Icon::svg('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>');
153150

154151
$map->addMarker(new Marker(
155152
// ...

src/Map/src/Bridge/Google/assets/dist/map_controller.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Controller } from '@hotwired/stimulus';
33

44
const IconTypes = {
55
Url: 'url',
6-
InlineSvg: 'inline-svg',
6+
Svg: 'svg',
77
UxIcon: 'ux-icon',
88
};
99
class default_1 extends Controller {
@@ -109,6 +109,7 @@ default_1.values = {
109109
};
110110

111111
let _google;
112+
const parser = new DOMParser();
112113
class map_controller extends default_1 {
113114
async connect() {
114115
if (!_google) {
@@ -282,18 +283,23 @@ class map_controller extends default_1 {
282283
return content;
283284
}
284285
doCreateIcon({ definition, element, }) {
285-
const { content, type, width, height } = definition;
286-
if (type === IconTypes.InlineSvg) {
287-
const icon = this.parser.parseFromString(content, 'image/svg+xml').documentElement;
288-
element.content = icon;
286+
const { type, width, height } = definition;
287+
if (type === IconTypes.Svg) {
288+
element.content = parser.parseFromString(definition.html, 'image/svg+xml').documentElement;
289289
}
290-
else {
290+
else if (type === IconTypes.UxIcon) {
291+
element.content = parser.parseFromString(definition._generated_html, 'image/svg+xml').documentElement;
292+
}
293+
else if (type === IconTypes.Url) {
291294
const icon = document.createElement('img');
292295
icon.width = width;
293296
icon.height = height;
294-
icon.src = content;
297+
icon.src = definition.url;
295298
element.content = icon;
296299
}
300+
else {
301+
throw new Error(`Unsupported icon type: ${type}.`);
302+
}
297303
}
298304
closeInfoWindowsExcept(infoWindow) {
299305
this.infoWindows.forEach((otherInfoWindow) => {

src/Map/src/Bridge/Google/assets/src/map_controller.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type MapOptions = Pick<
3737

3838
let _google: typeof google;
3939

40+
const parser = new DOMParser();
41+
4042
export default class extends AbstractMapController<
4143
MapOptions,
4244
google.maps.Map,
@@ -312,16 +314,20 @@ export default class extends AbstractMapController<
312314
definition: Icon;
313315
element: google.maps.marker.AdvancedMarkerElement;
314316
}): void {
315-
const { content, type, width, height } = definition;
316-
if (type === IconTypes.InlineSvg) {
317-
const icon = this.parser.parseFromString(content, 'image/svg+xml').documentElement;
318-
element.content = icon;
319-
} else {
317+
const { type, width, height } = definition;
318+
319+
if (type === IconTypes.Svg) {
320+
element.content = parser.parseFromString(definition.html, 'image/svg+xml').documentElement;
321+
} else if (type === IconTypes.UxIcon) {
322+
element.content = parser.parseFromString(definition._generated_html, 'image/svg+xml').documentElement;
323+
} else if (type === IconTypes.Url) {
320324
const icon = document.createElement('img');
321325
icon.width = width;
322326
icon.height = height;
323-
icon.src = content;
327+
icon.src = definition.url;
324328
element.content = icon;
329+
} else {
330+
throw new Error(`Unsupported icon type: ${type}.`);
325331
}
326332
}
327333

src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as L from 'leaflet';
44

55
const IconTypes = {
66
Url: 'url',
7-
InlineSvg: 'inline-svg',
7+
Svg: 'svg',
88
UxIcon: 'ux-icon',
99
};
1010
class default_1 extends Controller {
@@ -206,10 +206,20 @@ class map_controller extends default_1 {
206206
return popup;
207207
}
208208
doCreateIcon({ definition, element, }) {
209-
const { content, type, width, height } = definition;
210-
const icon = type === IconTypes.InlineSvg
211-
? L.divIcon({ html: content, iconSize: [width, height] })
212-
: L.icon({ iconUrl: content, iconSize: [width, height] });
209+
const { type, width, height } = definition;
210+
let icon;
211+
if (type === IconTypes.Svg) {
212+
icon = L.divIcon({ html: definition.html, iconSize: [width, height], className: '' });
213+
}
214+
else if (type === IconTypes.UxIcon) {
215+
icon = L.divIcon({ html: definition._generated_html, iconSize: [width, height], className: '' });
216+
}
217+
else if (type === IconTypes.Url) {
218+
icon = L.icon({ iconUrl: definition.url, iconSize: [width, height], className: '' });
219+
}
220+
else {
221+
throw new Error(`Unsupported icon type: ${type}.`);
222+
}
213223
element.setIcon(icon);
214224
}
215225
doFitBoundsToMarkers() {

src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,18 @@ export default class extends AbstractMapController<
185185
definition: Icon;
186186
element: L.Marker;
187187
}): void {
188-
const { content, type, width, height } = definition;
189-
const icon =
190-
type === IconTypes.InlineSvg
191-
? L.divIcon({ html: content, iconSize: [width, height] })
192-
: L.icon({ iconUrl: content, iconSize: [width, height] });
188+
const { type, width, height } = definition;
189+
190+
let icon: L.DivIcon | L.Icon;
191+
if (type === IconTypes.Svg) {
192+
icon = L.divIcon({ html: definition.html, iconSize: [width, height], className: '' });
193+
} else if (type === IconTypes.UxIcon) {
194+
icon = L.divIcon({ html: definition._generated_html, iconSize: [width, height], className: '' });
195+
} else if (type === IconTypes.Url) {
196+
icon = L.icon({ iconUrl: definition.url, iconSize: [width, height], className: '' });
197+
} else {
198+
throw new Error(`Unsupported icon type: ${type}.`);
199+
}
193200
element.setIcon(icon);
194201
}
195202

src/Map/src/Icon/Icon.php

+18-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
abstract class Icon
2323
{
2424
/**
25+
* Creates a new icon based on a URL (e.g.: `https://cdn.jsdelivr.net/npm/[email protected]/icons/geo-alt.svg`).
26+
*
2527
* @param non-empty-string $url
2628
*/
2729
public static function url(string $url): UrlIcon
@@ -30,6 +32,10 @@ public static function url(string $url): UrlIcon
3032
}
3133

3234
/**
35+
* Creates a new icon based on an SVG string (e.g.: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>`).
36+
* Using an SVG string may not be the best option if you want to customize the icon afterward,
37+
* it would be preferable to use {@see Icon::ux()} or {@see Icon::url()} instead.
38+
*
3339
* @param non-empty-string $html
3440
*/
3541
public static function svg(string $html): SvgIcon
@@ -38,6 +44,8 @@ public static function svg(string $html): SvgIcon
3844
}
3945

4046
/**
47+
* Creates a new icon based on a UX icon name (e.g.: `fa:map-marker`).
48+
*
4149
* @param non-empty-string $name
4250
*/
4351
public static function ux(string $name): UxIcon
@@ -56,23 +64,25 @@ protected function __construct(
5664
) {
5765
}
5866

67+
/**
68+
* Sets the width of the icon.
69+
*
70+
* @param positive-int $width
71+
*/
5972
public function width(int $width): static
6073
{
61-
if ($width <= 0) {
62-
throw new InvalidArgumentException('Width must be greater than 0.');
63-
}
64-
6574
$this->width = $width;
6675

6776
return $this;
6877
}
6978

79+
/**
80+
* Sets the height of the icon.
81+
*
82+
* @param positive-int $height
83+
*/
7084
public function height(int $height): static
7185
{
72-
if ($height <= 0) {
73-
throw new InvalidArgumentException('Height must be greater than 0.');
74-
}
75-
7686
$this->height = $height;
7787

7888
return $this;

src/Map/src/Icon/SvgIcon.php

+18-8
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,39 @@ class SvgIcon extends Icon
2323
{
2424
/**
2525
* @param non-empty-string $html
26-
* @param positive-int $width
27-
* @param positive-int $height
2826
*/
2927
protected function __construct(
3028
protected string $html,
31-
int $width = 24,
32-
int $height = 24,
3329
) {
34-
parent::__construct(IconType::Svg, $width, $height);
30+
parent::__construct(IconType::Svg);
3531
}
3632

3733
/**
38-
* @param array{ html: string, width: positive-int, height: positive-int } $data
34+
* @param array{ html: string } $data
3935
*/
4036
public static function fromArray(array $data): static
4137
{
4238
return new self(
4339
html: $data['html'],
44-
width: $data['width'],
45-
height: $data['height'],
4640
);
4741
}
4842

43+
/**
44+
* @throws \LogicException the SvgIcon can not be customized
45+
*/
46+
public function width(int $width): never
47+
{
48+
throw new \LogicException('Unable to configure the SvgIcon width, please configure it in the HTML with the "width" attribute on the root element instead.');
49+
}
50+
51+
/**
52+
* @throws \LogicException the SvgIcon can not be customized
53+
*/
54+
public function height(int $height): never
55+
{
56+
throw new \LogicException('Unable to configure the SvgIcon height, please configure it in the HTML with the "height" attribute on the root element instead.');
57+
}
58+
4959
public function toArray(): array
5060
{
5161
return [

src/Map/src/Icon/UxIconRenderer.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@ public function __construct(
2525
) {
2626
}
2727

28-
public function render(string $name): string
28+
/**
29+
* @param array<string, string|bool> $attributes
30+
*/
31+
public function render(string $name, array $attributes = []): string
2932
{
3033
if (null === $this->renderer) {
3134
throw new \LogicException('You cannot use an UX Icon as the "UX Icons" package is not installed. Try running "composer require symfony/ux-icons" to install it.');
3235
}
3336

3437
return $this->renderer->renderIcon($name, [
3538
'xmlns' => 'http://www.w3.org/2000/svg',
39+
...$attributes,
3640
]);
3741
}
3842
}

src/Map/src/Marker.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\UX\Map\Exception\InvalidArgumentException;
1515
use Symfony\UX\Map\Icon\Icon;
16+
use Symfony\UX\Map\Icon\IconType;
1617

1718
/**
1819
* Represents a marker on a map.
@@ -40,7 +41,7 @@ public function __construct(
4041
* position: array{lat: float, lng: float},
4142
* title: string|null,
4243
* infoWindow: array<string, mixed>|null,
43-
* icon: Icon|null,
44+
* icon: array{type: value-of<IconType>, width: positive-int, height: positive-int, ...}|null,
4445
* extra: array,
4546
* id: string|null
4647
* }
@@ -62,7 +63,7 @@ public function toArray(): array
6263
* position: array{lat: float, lng: float},
6364
* title: string|null,
6465
* infoWindow: array<string, mixed>|null,
65-
* icon: array<string, mixed>||null,
66+
* icon: array{type: value-of<IconType>, width: positive-int, height: positive-int, ...}|null,
6667
* extra: array,
6768
* id: string|null
6869
* } $marker

0 commit comments

Comments
 (0)