@konce-pt/angular 0.7.11 → 0.7.12
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/CHANGELOG.md +17 -0
- package/charts/src/lib/llms.txt +30 -30
- package/grid/src/lib/llms.txt +48 -48
- package/map/src/lib/llms.txt +446 -446
- package/package.json +1 -1
- package/roadmap/src/lib/llms.txt +74 -74
- package/src/lib/accordion/llms.txt +9 -9
- package/src/lib/alert/llms.txt +10 -10
- package/src/lib/app-shell/llms.txt +59 -59
- package/src/lib/auth/llms.txt +58 -58
- package/src/lib/autocomplete/llms.txt +8 -8
- package/src/lib/avatar/llms.txt +9 -9
- package/src/lib/avatar-group/llms.txt +8 -8
- package/src/lib/badge/llms.txt +9 -9
- package/src/lib/bottom-sheet/llms.txt +14 -14
- package/src/lib/breadcrumb/llms.txt +7 -7
- package/src/lib/breakpoint/llms.txt +21 -21
- package/src/lib/button/llms.txt +34 -34
- package/src/lib/button-group/llms.txt +5 -5
- package/src/lib/card/llms.txt +27 -27
- package/src/lib/carousel/llms.txt +11 -11
- package/src/lib/checkbox/llms.txt +7 -7
- package/src/lib/chip/llms.txt +6 -6
- package/src/lib/chips-input/llms.txt +9 -9
- package/src/lib/color-picker/llms.txt +7 -7
- package/src/lib/confirm/llms.txt +11 -11
- package/src/lib/context-menu/llms.txt +10 -10
- package/src/lib/data-table/llms.txt +170 -170
- package/src/lib/data-view/llms.txt +8 -8
- package/src/lib/date-range/llms.txt +25 -25
- package/src/lib/datepicker/llms.txt +48 -48
- package/src/lib/dialog/llms.txt +62 -62
- package/src/lib/divider/llms.txt +9 -9
- package/src/lib/drawer/llms.txt +18 -18
- package/src/lib/empty/llms.txt +9 -9
- package/src/lib/fab/llms.txt +12 -12
- package/src/lib/fieldset/llms.txt +9 -9
- package/src/lib/file-upload/llms.txt +10 -10
- package/src/lib/form-field/llms.txt +45 -45
- package/src/lib/galleria/llms.txt +7 -7
- package/src/lib/i18n/llms.txt +21 -21
- package/src/lib/icon/llms.txt +20 -20
- package/src/lib/icon-button/llms.txt +8 -8
- package/src/lib/image/llms.txt +9 -9
- package/src/lib/input/llms.txt +27 -27
- package/src/lib/input-mask/llms.txt +10 -10
- package/src/lib/input-number/llms.txt +34 -34
- package/src/lib/input-otp/llms.txt +11 -11
- package/src/lib/knob/llms.txt +14 -14
- package/src/lib/listbox/llms.txt +8 -8
- package/src/lib/megamenu/llms.txt +13 -13
- package/src/lib/menu/llms.txt +26 -26
- package/src/lib/menubar/llms.txt +10 -10
- package/src/lib/meter-group/llms.txt +8 -8
- package/src/lib/order-list/llms.txt +8 -8
- package/src/lib/paginator/llms.txt +12 -12
- package/src/lib/panel/llms.txt +12 -12
- package/src/lib/password/llms.txt +12 -12
- package/src/lib/pick-list/llms.txt +8 -8
- package/src/lib/popover/llms.txt +8 -8
- package/src/lib/progress/llms.txt +6 -6
- package/src/lib/radio-group/llms.txt +5 -5
- package/src/lib/rating/llms.txt +6 -6
- package/src/lib/rich-text/llms.txt +50 -50
- package/src/lib/scroll-top/llms.txt +7 -7
- package/src/lib/select/llms.txt +26 -26
- package/src/lib/sidenav/llms.txt +24 -24
- package/src/lib/skeleton/llms.txt +6 -6
- package/src/lib/slider/llms.txt +17 -17
- package/src/lib/speed-dial/llms.txt +7 -7
- package/src/lib/spinner/llms.txt +5 -5
- package/src/lib/split-button/llms.txt +15 -15
- package/src/lib/splitter/llms.txt +11 -11
- package/src/lib/stepper/llms.txt +10 -10
- package/src/lib/switch/llms.txt +10 -10
- package/src/lib/switch-group/llms.txt +16 -16
- package/src/lib/tabs/llms.txt +10 -10
- package/src/lib/textarea/llms.txt +5 -5
- package/src/lib/timeline/llms.txt +10 -10
- package/src/lib/toast/llms.txt +10 -10
- package/src/lib/toolbar/llms.txt +31 -31
- package/src/lib/tooltip/llms.txt +21 -21
- package/src/lib/tree/llms.txt +6 -6
package/map/src/lib/llms.txt
CHANGED
|
@@ -1,153 +1,153 @@
|
|
|
1
1
|
# KptMap (kpt-map)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
popup
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
A map with configurable markers: categories carrying a color and an icon, clustering with a count,
|
|
4
|
+
a details popup whose template depends on the category, a ready-made place card, a legend, an address
|
|
5
|
+
search and adding locations with the right mouse button. The default engine is Leaflet + OpenStreetMap; the component's API
|
|
6
|
+
knows nothing about the engine, so a second adapter (Google Maps) swaps in through the provider alone.
|
|
7
7
|
|
|
8
8
|
Import: `import { KptMap, KptMapPoiCard, KptMapSearch } from '@konce-pt/angular/map';`
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Requirements (the application)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
What follows is for the default engine, Leaflet. The alternative — Google Maps — needs no package
|
|
13
|
+
and no stylesheet, but it does need an API key and a `mapId`; see the "Engines" section.
|
|
14
14
|
|
|
15
|
-
1.
|
|
15
|
+
1. The **optional** `leaflet` dependency — the library does not install it:
|
|
16
16
|
|
|
17
17
|
pnpm add leaflet
|
|
18
18
|
|
|
19
|
-
2.
|
|
20
|
-
|
|
19
|
+
2. Leaflet's stylesheet — like `@angular/cdk/overlay-prebuilt.css`, through `angular.json → styles[]`,
|
|
20
|
+
NOT as an `import` in `main.ts` (the Angular CLI would turn that into an orphaned chunk):
|
|
21
21
|
|
|
22
22
|
node_modules/leaflet/dist/leaflet.css
|
|
23
23
|
|
|
24
24
|
Vite/webpack: `import 'leaflet/dist/leaflet.css';`
|
|
25
25
|
|
|
26
|
-
3.
|
|
26
|
+
3. The engine provider at bootstrap:
|
|
27
27
|
|
|
28
28
|
bootstrapApplication(App, { providers: [provideKptLeafletMap()] });
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Leaflet
|
|
30
|
+
Without the `leaflet` package installed the component still compiles and imports without error —
|
|
31
|
+
only an attempt to render the map ends with a readable message and instructions.
|
|
32
|
+
Leaflet is loaded with a dynamic `import()`, so it does not weigh down applications that use no map.
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## Selector
|
|
35
35
|
|
|
36
|
-
`kpt-map` — element
|
|
36
|
+
`kpt-map` — a block element. The height is set by `[height]` (default `480px`).
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Inputs
|
|
39
39
|
|
|
40
|
-
- `locations`: `readonly KptMapLocation<T>[]` — **
|
|
41
|
-
- `categories`: `readonly KptMapCategory[]` — **
|
|
42
|
-
- `country`: `string` = `'PL'` —
|
|
43
|
-
- `view`: `{ center, zoom } | null` —
|
|
44
|
-
- `bounds`: `KptMapBounds | null` —
|
|
45
|
-
- `restrictToCountry`: `boolean` = `false` —
|
|
40
|
+
- `locations`: `readonly KptMapLocation<T>[]` — **required**
|
|
41
|
+
- `categories`: `readonly KptMapCategory[]` — **required**
|
|
42
|
+
- `country`: `string` = `'PL'` — the initial view preset; an unknown code falls back to Poland
|
|
43
|
+
- `view`: `{ center, zoom } | null` — an explicit view; overrides `country`
|
|
44
|
+
- `bounds`: `KptMapBounds | null` — explicit bounds; override `country` and `view`
|
|
45
|
+
- `restrictToCountry`: `boolean` = `false` — blocks panning outside the country's bounds/`bounds`
|
|
46
46
|
- `cluster`: `boolean` = `true`
|
|
47
|
-
- `clusterOptions`: `{ gridSize?, maxZoom?, minPoints? }` —
|
|
48
|
-
- `clusterColorBy`: `'dominant' | 'none'` = `'none'` —
|
|
49
|
-
- `legend`: `boolean` = `true` —
|
|
50
|
-
- `legendCounts`: `boolean` = `true` —
|
|
47
|
+
- `clusterOptions`: `{ gridSize?, maxZoom?, minPoints? }` — `60 / 16 / 2` by default
|
|
48
|
+
- `clusterColorBy`: `'dominant' | 'none'` = `'none'` — whether a cluster takes the color of its most numerous category
|
|
49
|
+
- `legend`: `boolean` = `true` — renders `kpt-map-legend` below the map
|
|
50
|
+
- `legendCounts`: `boolean` = `true` — counters next to the legend entries
|
|
51
51
|
- `showUserLocation`: `boolean` = `false`
|
|
52
|
-
- `followUser`: `boolean` = `false` —
|
|
53
|
-
- `allowCreate`: `boolean` = `false` —
|
|
54
|
-
- `search`: `boolean` = `false` —
|
|
55
|
-
`KPT_MAP_GEOCODER` **
|
|
56
|
-
- `searchPlaceholder`: `string` = `''` —
|
|
57
|
-
- `searchOptions`: `{ minLength?, debounce?, limit?, countryCodes? }` —
|
|
58
|
-
- `searchZoom`: `number` = `16` — zoom
|
|
59
|
-
- `searchMarker`: `boolean` = `true` —
|
|
60
|
-
- `geocoder`: `KptMapGeocoderPort | null` = `null` —
|
|
61
|
-
`KPT_MAP_GEOCODER`
|
|
62
|
-
|
|
63
|
-
- `popupLayout`: `'plain' | 'card'` = `'plain'` — `card`
|
|
64
|
-
|
|
65
|
-
`--kpt-map-popup-max-height
|
|
66
|
-
- `distanceFrom`: `KptLatLng | null` = `null` —
|
|
67
|
-
|
|
52
|
+
- `followUser`: `boolean` = `false` — recenters the map on every new position
|
|
53
|
+
- `allowCreate`: `boolean` = `false` — an "Add location" menu under the right mouse button
|
|
54
|
+
- `search`: `boolean` = `false` — the address search in the map's corner; it requires the
|
|
55
|
+
`KPT_MAP_GEOCODER` port **with a `search()` method**, and without it the field does not render
|
|
56
|
+
- `searchPlaceholder`: `string` = `''` — an empty value hands it over to the dictionary
|
|
57
|
+
- `searchOptions`: `{ minLength?, debounce?, limit?, countryCodes? }` — `3 / 300 / 5` by default
|
|
58
|
+
- `searchZoom`: `number` = `16` — the zoom for a result with no bounds of its own
|
|
59
|
+
- `searchMarker`: `boolean` = `true` — a temporary pin at the found point
|
|
60
|
+
- `geocoder`: `KptMapGeocoderPort | null` = `null` — **this** map's geocoder; it wins over
|
|
61
|
+
`KPT_MAP_GEOCODER` from DI and serves both directions: the search and the address suggestions
|
|
62
|
+
in the create form
|
|
63
|
+
- `popupLayout`: `'plain' | 'card'` = `'plain'` — `card` hands the popup over to the content entirely:
|
|
64
|
+
no padding, wider (`--kpt-map-popup-width-card`), scrolling once it exceeds
|
|
65
|
+
`--kpt-map-popup-max-height`, and no built-in close button (the card supplies one)
|
|
66
|
+
- `distanceFrom`: `KptLatLng | null` = `null` — the reference point for the distance handed
|
|
67
|
+
to the popup template; `null` means "the user's position from `KPT_MAP_GEOLOCATION`"
|
|
68
68
|
- `height`: `string` = `'480px'`
|
|
69
|
-
- `tileUrl`: `string | null` —
|
|
70
|
-
- `attribution`: `string | null` —
|
|
71
|
-
- `engine`: `KptMapEngineFactory | null` —
|
|
72
|
-
(
|
|
69
|
+
- `tileUrl`: `string | null` — OSM tiles by default (see "Tiles" below); **irrelevant for the Google engine**
|
|
70
|
+
- `attribution`: `string | null` — `© OpenStreetMap contributors` by default; likewise
|
|
71
|
+
- `engine`: `KptMapEngineFactory | null` — this map's engine; it wins over `KPT_MAP_ENGINE` from DI
|
|
72
|
+
(see "Engines")
|
|
73
73
|
|
|
74
|
-
## Model /
|
|
74
|
+
## Model / events
|
|
75
75
|
|
|
76
|
-
- `hiddenCategories`: `model<string[]>` —
|
|
76
|
+
- `hiddenCategories`: `model<string[]>` — the ids of categories hidden in the legend
|
|
77
77
|
- `locationClick`: `KptMapLocation<T>`
|
|
78
78
|
- `clusterClick`: `KptMapCluster<T>`
|
|
79
|
-
- `locationCreate`: `{ at, categoryId, value }` —
|
|
80
|
-
|
|
81
|
-
- `searchSelect`: `KptMapSearchResult` —
|
|
82
|
-
- `viewChange`: `{ center, zoom, bounds }` —
|
|
79
|
+
- `locationCreate`: `{ at, categoryId, value }` — the component appends **nothing** to `locations`;
|
|
80
|
+
the application is the source of truth
|
|
81
|
+
- `searchSelect`: `KptMapSearchResult` — a suggestion was picked; the map has **already** moved the view itself
|
|
82
|
+
- `viewChange`: `{ center, zoom, bounds }` — after the map's movement ends
|
|
83
83
|
- `userPositionChange`: `KptMapUserPosition | null`
|
|
84
84
|
- `userLocationError`: `KptMapGeolocationError`
|
|
85
85
|
|
|
86
86
|
## KptMapCategory
|
|
87
87
|
|
|
88
|
-
{ id: 'closure', label: '
|
|
88
|
+
{ id: 'closure', label: 'Closure', color: 'danger', icon: 'lock',
|
|
89
89
|
shape: 'pin', legend: true, fields: [ … ] }
|
|
90
90
|
|
|
91
|
-
`color`
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
`color` is a role from the semantic layer (`primary | success | warning | danger | info | muted`)
|
|
92
|
+
or a ready `var(--kpt-*)`. Literals (`#c00`, `rgb(...)`) are blocked at the type level —
|
|
93
|
+
the repo's contract requires tokens.
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
A role yields **three** variables, depending on what the element is:
|
|
96
96
|
|
|
97
|
-
|
|
|
97
|
+
| function | for what | `muted` gives |
|
|
98
98
|
|---|---|---|
|
|
99
|
-
| `colorVarOf()` |
|
|
100
|
-
| `contrastVarOf()` |
|
|
101
|
-
| `foregroundVarOf()` |
|
|
99
|
+
| `colorVarOf()` | a background (a pin, a legend dot, a cluster) | `--kpt-color-muted` (neutral-100) |
|
|
100
|
+
| `contrastVarOf()` | a glyph **on** that background | `--kpt-color-on-muted` (neutral-800) |
|
|
101
|
+
| `foregroundVarOf()` | a glyph standing straight on the surface, with no background | `--kpt-color-on-surface-muted` |
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
a
|
|
105
|
-
|
|
106
|
-
neutral-100
|
|
107
|
-
`provideKptTablerIcons()`). `shape`: `pin` (
|
|
103
|
+
Getting it wrong only hurts with `muted`, because it is the only role whose `--kpt-color-*` is a background
|
|
104
|
+
color rather than a content one — the rest are saturated and read fine in either function. Hence the third function:
|
|
105
|
+
the place card paints the category icon straight onto the card's background, and `colorVarOf()` would give it
|
|
106
|
+
neutral-100 on white. `icon` is a name from `KptIconRegistry` (the full Tabler set comes with
|
|
107
|
+
`provideKptTablerIcons()`). `shape`: `pin` (the default, anchored at the tip), `dot`, `square`.
|
|
108
108
|
|
|
109
109
|
## KptMapLocation
|
|
110
110
|
|
|
111
111
|
{ id: 'w1', at: { lat: 51.11, lng: 17.04 }, categoryId: 'opening',
|
|
112
112
|
title: '2210 · Wrocław', data: { … } }
|
|
113
113
|
|
|
114
|
-
`title`
|
|
115
|
-
|
|
114
|
+
`title` is required — it goes into the marker's `aria-label`. `data` is arbitrary and is handed
|
|
115
|
+
to the popup template.
|
|
116
116
|
|
|
117
|
-
##
|
|
117
|
+
## Templates
|
|
118
118
|
|
|
119
|
-
`kptMapPopup` — popup
|
|
120
|
-
|
|
119
|
+
`kptMapPopup` — the details popup, chosen by category. A template with no `category` attribute
|
|
120
|
+
is the fallback; with no template at all the component shows the title and the category name.
|
|
121
121
|
|
|
122
122
|
<kpt-map [locations]="locations()" [categories]="categories">
|
|
123
123
|
<ng-template kptMapPopup category="closure" let-loc let-d="data">
|
|
124
124
|
<strong>{{ loc.title }}</strong>
|
|
125
|
-
<p>
|
|
126
|
-
<kpt-button size="sm" (click)="openCard(loc)">
|
|
125
|
+
<p>Closure · {{ d.status }}</p>
|
|
126
|
+
<kpt-button size="sm" (click)="openCard(loc)">Open the project card</kpt-button>
|
|
127
127
|
</ng-template>
|
|
128
128
|
<ng-template kptMapPopup let-loc><strong>{{ loc.title }}</strong></ng-template>
|
|
129
129
|
</kpt-map>
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
Context: `$implicit` = the location, `data` = `location.data` (typed `T | undefined`, because `data`
|
|
132
|
+
is optional), `category`, `distance`, `close()`.
|
|
133
133
|
|
|
134
|
-
`distance`
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
`distance` is the point's distance in **meters** from `[distanceFrom]`, or from the user's position
|
|
135
|
+
without it. `null` when there is no reference point — geolocation is disabled or was refused.
|
|
136
|
+
Format it with `formatDistance(meters, locale)` from the core; `kpt-map-poi-card` does it itself.
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
The type of `data` is loose by default — Angular does not carry the generic from `kpt-map` onto a directive
|
|
139
|
+
embedded in its content. If you want inference, bind `[kptMapPopupTypeFor]` to the same array you feed
|
|
140
|
+
the map (the input exists solely for that and is unused at runtime):
|
|
141
141
|
|
|
142
142
|
<ng-template kptMapPopup [kptMapPopupTypeFor]="locations()" category="opening" let-d="data">
|
|
143
143
|
<p>{{ d?.address }}</p>
|
|
144
144
|
</ng-template>
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
A popup opened near the container's edge pans the map so that it fits (auto-pan measures it
|
|
147
|
+
after the first render — before that we do not know its dimensions).
|
|
148
148
|
|
|
149
|
-
`kptMapCreateForm` —
|
|
150
|
-
|
|
149
|
+
`kptMapCreateForm` — your own create form. Its presence disables the form built
|
|
150
|
+
from the category schema.
|
|
151
151
|
|
|
152
152
|
<ng-template kptMapCreateForm let-api>
|
|
153
153
|
<!-- api: { at, category, close(), submit(value) } -->
|
|
@@ -155,9 +155,9 @@ budowany ze schemy kategorii.
|
|
|
155
155
|
|
|
156
156
|
## KptMapPoiCard (kpt-map-poi-card)
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
A ready-made place card for the popup: the distance, a 16:9 photo, a primary button + share + favorite,
|
|
159
|
+
a category row, expandable opening hours with an open/closed status, and contact
|
|
160
|
+
rows (phone, website, e-mail, coordinates) with actions and copy-to-clipboard.
|
|
161
161
|
|
|
162
162
|
<kpt-map [locations]="places()" [categories]="categories" popupLayout="card">
|
|
163
163
|
<ng-template kptMapPopup [kptMapPopupTypeFor]="places()"
|
|
@@ -167,139 +167,139 @@ kontaktowe (telefon, www, e-mail, współrzędne) z akcjami i kopiowaniem do sch
|
|
|
167
167
|
[poi]="poiOf(loc, d)"
|
|
168
168
|
[distance]="distance"
|
|
169
169
|
[category]="category ?? null"
|
|
170
|
-
primaryLabel="
|
|
170
|
+
primaryLabel="Get directions"
|
|
171
171
|
(primaryAction)="planRoute($event)"
|
|
172
172
|
(closed)="close()"
|
|
173
173
|
/>
|
|
174
174
|
</ng-template>
|
|
175
175
|
</kpt-map>
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
Inputs:
|
|
178
178
|
|
|
179
|
-
- `poi`: `KptMapPoi` — **
|
|
180
|
-
- `distance`: `number | null` = `null` —
|
|
181
|
-
- `category`: `KptMapCategory | null` = `null` —
|
|
182
|
-
- `primaryLabel`: `string` = `''` —
|
|
179
|
+
- `poi`: `KptMapPoi` — **required**
|
|
180
|
+
- `distance`: `number | null` = `null` — meters; `null` hides the distance row
|
|
181
|
+
- `category`: `KptMapCategory | null` = `null` — the icon, the color and the label of the category row
|
|
182
|
+
- `primaryLabel`: `string` = `''` — an empty value hides the primary button
|
|
183
183
|
- `showShare`, `showFavourite`, `closable`: `boolean` = `true`
|
|
184
184
|
- `favourite`: `model<boolean>` = `false`
|
|
185
185
|
- `hoursExpanded`: `model<boolean>` = `false`
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Outputs: `primaryAction: KptMapPoi`, `closed: void`, `shared: KptMapPoi`.
|
|
188
188
|
|
|
189
|
-
`KptMapPoi`
|
|
190
|
-
|
|
189
|
+
`KptMapPoi` is **separate from the domain model** — the `location.data → KptMapPoi` mapping is done by
|
|
190
|
+
the application. The library does not guess which field is the phone number:
|
|
191
191
|
|
|
192
192
|
{ title, address?, photo?, photoAlt?, hours?, phone?, email?, website?, at?,
|
|
193
193
|
facts?: [{ icon?, label, value }] }
|
|
194
194
|
|
|
195
|
-
`photo`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
- **
|
|
201
|
-
|
|
202
|
-
- **
|
|
203
|
-
`KptAccordionPanel.title`
|
|
204
|
-
|
|
205
|
-
- **
|
|
206
|
-
(pointer: fine)`,
|
|
207
|
-
|
|
208
|
-
- **
|
|
209
|
-
|
|
210
|
-
- **
|
|
211
|
-
|
|
212
|
-
- **
|
|
213
|
-
|
|
214
|
-
`<h3>`
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
195
|
+
`photo` takes a URL or a `data:` string — the card does not tell them apart. Without one it draws a placeholder
|
|
196
|
+
with an icon instead of collapsing the frame to zero. `facts` are arbitrary "icon · label · value" rows below the contacts.
|
|
197
|
+
|
|
198
|
+
Behaviors worth remembering:
|
|
199
|
+
|
|
200
|
+
- **The photo** sits in a 16:9 frame with `object-fit: cover` on a plain `<img>`, not on `kpt-image` —
|
|
201
|
+
that one is `inline-block` with `height: auto` and will not hold the frame.
|
|
202
|
+
- **The hours** are a native disclosure (`aria-expanded` + `aria-controls`), not a `kpt-accordion`:
|
|
203
|
+
`KptAccordionPanel.title` accepts a plain string, while the header has to fit an icon, a colored
|
|
204
|
+
status and a chevron. The status is computed by `isOpenAt()`; days are joined by `groupWeek()`.
|
|
205
|
+
- **The row actions** are always in the DOM. They are hidden only by `@media (hover: hover) and
|
|
206
|
+
(pointer: fine)`, and `:hover`/`:focus-within` reveals them — on touch and from the keyboard
|
|
207
|
+
they are available without any tricks.
|
|
208
|
+
- **Copying** goes through `navigator.clipboard`; without that API the button does not render.
|
|
209
|
+
On success the icon changes to `check`, and a message goes to `aria-live`.
|
|
210
|
+
- **Sharing** calls `navigator.share()`, and without it copies the address. The user canceling the sheet
|
|
211
|
+
is not an error and emits no `shared`.
|
|
212
|
+
- **The title is a `<p role="heading" aria-level="3">`, not an `<h3>`.** Component styles live
|
|
213
|
+
in `@layer kpt.components`, and an application's CSS outside any layer always beats them — a plain
|
|
214
|
+
`<h3>` would catch every `h3 { … }` rule from the page's layout (in the playground that turned the place title
|
|
215
|
+
into 13-pixel uppercase). To a screen reader it is still a level 3 heading.
|
|
216
|
+
- The card does not set its own `role` on the host — the `kpt-map` popup is already `role="dialog"`.
|
|
217
|
+
|
|
218
|
+
Anatomy and scrolling:
|
|
219
219
|
|
|
220
220
|
kpt-map-poi-card
|
|
221
|
-
.kpt-map-poi__head
|
|
222
|
-
.kpt-map-poi__meta
|
|
223
|
-
.kpt-map-poi__scroll
|
|
221
|
+
.kpt-map-poi__head title + close button — fixed
|
|
222
|
+
.kpt-map-poi__meta distance + address — fixed
|
|
223
|
+
.kpt-map-poi__scroll photo, actions, category, hours, contacts, aria-live
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
The header stays put, and only `__scroll` scrolls. The distance and the address belong to the header
|
|
226
|
+
block, because they are part of the place's identity — to make them scroll with the content,
|
|
227
|
+
just move `__meta` into `__scroll`.
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
The condition for this is that `popupLayout="card"` **hands scrolling to the card**: `.kpt-map__popup-body`
|
|
230
|
+
then gets `display: flex` and `overflow: hidden`, and the card the popup's full height. As long as
|
|
231
|
+
the parent container scrolls, the header cannot be pinned without `sticky` and negative margins.
|
|
232
|
+
The popup itself does not get `overflow: hidden` — the `::after` arrow hangs at `top: 100%` and would be
|
|
233
|
+
cut off. A card outside a popup (stories, any wrapper with no height limit) does not scroll
|
|
234
|
+
at all: `__scroll` has `flex: 0 1 auto` and simply grows without a height ceiling.
|
|
235
235
|
|
|
236
|
-
##
|
|
236
|
+
## Adding locations
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
Right-click → a menu of categories → a `kpt-dialog` with a form. By default the form is built
|
|
239
|
+
from `category.fields` on Signal Forms. Field types and controls:
|
|
240
240
|
|
|
241
|
-
| `type` |
|
|
241
|
+
| `type` | control | notes |
|
|
242
242
|
|---|---|---|
|
|
243
243
|
| `text` | `kpt-input` | |
|
|
244
244
|
| `textarea` | `kpt-textarea` | |
|
|
245
|
-
| `number` | `kpt-input-number` | `min`/`max`
|
|
246
|
-
| `date` | `kpt-datepicker` | `min`/`max`
|
|
247
|
-
| `select` / `multiselect` | `kpt-select` (`multiple`) |
|
|
248
|
-
| `checkbox` | `kpt-checkbox` | `placeholder`
|
|
249
|
-
| `tel` | `kpt-input type="tel"` |
|
|
250
|
-
| `email` | `kpt-input type="email"` |
|
|
251
|
-
| `url` | `kpt-input type="url"` |
|
|
252
|
-
| `image` | `kpt-map-image-field` |
|
|
253
|
-
| `hours` | `kpt-map-hours-field` |
|
|
245
|
+
| `number` | `kpt-input-number` | `min`/`max` from the schema go through validators, not through `[min]` |
|
|
246
|
+
| `date` | `kpt-datepicker` | `min`/`max` as ISO dates |
|
|
247
|
+
| `select` / `multiselect` | `kpt-select` (`multiple`) | requires `options` |
|
|
248
|
+
| `checkbox` | `kpt-checkbox` | `placeholder` serves as the label next to the field |
|
|
249
|
+
| `tel` | `kpt-input type="tel"` | no format validation — phone numbers can be odd |
|
|
250
|
+
| `email` | `kpt-input type="email"` | the `email()` validator |
|
|
251
|
+
| `url` | `kpt-input type="url"` | the `https?://…` pattern |
|
|
252
|
+
| `image` | `kpt-map-image-field` | a file picker **or** a pasted URL; the value is a `string` |
|
|
253
|
+
| `hours` | `kpt-map-hours-field` | the value is a `KptMapOpeningHours` |
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
The coordinates from the click are shown but not editable.
|
|
256
256
|
|
|
257
|
-
`image`
|
|
258
|
-
|
|
259
|
-
|
|
257
|
+
`image` reads the file with `FileReader.readAsDataURL()` and **sends it nowhere** — the library knows
|
|
258
|
+
nothing about the application's storage. The 2 MB limit is real, not decorative: a data URL swells by about a third,
|
|
259
|
+
and the whole value travels in `locationCreate`. Typing a URL clears the chosen file and vice versa.
|
|
260
260
|
|
|
261
|
-
`hours`
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
`hours` gives seven rows starting on Monday (day names from `Intl`, not from the dictionary), two
|
|
262
|
+
ranges per day, a "closed" marker and a "same all week" shortcut that copies
|
|
263
|
+
Monday onto the rest. A day not marked as closed must have both times — otherwise
|
|
264
|
+
the form is `invalid`.
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
`HH:mm`
|
|
266
|
+
The time is picked on a `kpt-datepicker selectionMode="time"` (a clock dial, `minuteStep=5`),
|
|
267
|
+
not on a native `<input type="time">` — that one renders its own system dropdown, which
|
|
268
|
+
cannot be styled and looks alien among the library's controls. The value is the same:
|
|
269
|
+
`HH:mm` or an empty string.
|
|
270
270
|
|
|
271
|
-
**
|
|
272
|
-
|
|
271
|
+
**A limitation:** with a dynamic schema the model is a `Record<string, unknown>`, so the fields
|
|
272
|
+
are not statically typed. Anyone who needs types supplies their own `kptMapCreateForm`.
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
invalid()`),
|
|
274
|
+
The "do not scare people with red on a fresh form" gate lives in the presentation (`touched() &&
|
|
275
|
+
invalid()`), not in the validator — per this repo's Signal Forms contract.
|
|
276
276
|
|
|
277
|
-
##
|
|
277
|
+
## Address suggestions (reverse geocoding)
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
The fields of the "add location" form can fill themselves in with the clicked point's address. Two conditions:
|
|
280
|
+
the field has an `autofill` pointing at a part of the address, and the map has a port — from DI (`KPT_MAP_GEOCODER`)
|
|
281
|
+
or from the `[geocoder]` input, which wins.
|
|
282
282
|
|
|
283
|
-
{ key: 'city', type: 'text', label: '
|
|
284
|
-
{ key: 'address', type: 'text', label: '
|
|
283
|
+
{ key: 'city', type: 'text', label: 'City', autofill: 'city' },
|
|
284
|
+
{ key: 'address', type: 'text', label: 'Address', autofill: 'address' },
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
`city`, `postcode`, `state`, `county`, `country`, `countryCode`, `label` (
|
|
288
|
-
|
|
286
|
+
The address parts (`KptMapAddressPart`): `street`, `houseNumber`, `address` (street with number),
|
|
287
|
+
`city`, `postcode`, `state`, `county`, `country`, `countryCode`, `label` (the full address as one
|
|
288
|
+
string). It works only for `text` and `textarea` fields — a date or a number will not take a fragment of an address.
|
|
289
289
|
|
|
290
|
-
**
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
**A suggestion never overwrites what the user typed** — only fields that are
|
|
291
|
+
empty at the moment of the response are filled in. A missing port or a failed request does not block the form: the fields
|
|
292
|
+
stay empty, and a note appears under the coordinates.
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
The ready adapter (opt-in, NOT registered by default):
|
|
295
295
|
|
|
296
296
|
providers: [provideKptLeafletMap(), provideKptNominatimGeocoder({ language: 'pl' })]
|
|
297
297
|
|
|
298
|
-
**
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
**The Nominatim policy.** The public `nominatim.openstreetmap.org` instance allows at most
|
|
299
|
+
one request per second, requires a recognizable `Referer`/`User-Agent` and forbids bulk
|
|
300
|
+
querying; traffic can be blocked without notice, commercial traffic in particular. The adapter
|
|
301
|
+
queues requests a second apart on its own and aborts them when the dialog closes, but for production
|
|
302
|
+
put your own instance behind it (`baseUrl`) or a commercial provider through your own `KPT_MAP_GEOCODER`:
|
|
303
303
|
|
|
304
304
|
export interface KptMapGeocoderPort {
|
|
305
305
|
reverse(at: KptLatLng, options?): Observable<KptMapAddress | null>;
|
|
@@ -307,425 +307,425 @@ podstaw własną instancję (`baseUrl`) albo komercyjnego dostawcę przez własn
|
|
|
307
307
|
resolve?(suggestion: KptMapSuggestion, options?): Observable<KptMapSearchResult | null>;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
`resolve()`
|
|
310
|
+
Only `reverse()` is mandatory. Without `search()` the map shows no search field;
|
|
311
|
+
`resolve()` is described below, with suggestions that carry no coordinates.
|
|
312
312
|
|
|
313
|
-
**
|
|
314
|
-
|
|
313
|
+
**The interval between requests (`minIntervalMs`).** The one-second gap is not a property of Nominatim, it is a
|
|
314
|
+
requirement of the **public OSM instance's** terms — which is why the default depends on `baseUrl`:
|
|
315
315
|
|
|
316
316
|
provideKptNominatimGeocoder() // 1000 ms
|
|
317
317
|
provideKptNominatimGeocoder({ baseUrl: 'https://geo.local' }) // 0 ms
|
|
318
318
|
provideKptNominatimGeocoder({ baseUrl: '…', minIntervalMs: 200 })
|
|
319
319
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
On your own server there is nothing to respect, and for as-you-type suggestions a one-second
|
|
321
|
+
delay is the difference between "it works" and "it stutters". `0` also removes the queuing — the requests
|
|
322
|
+
go out in parallel. When pointing at **someone else's shared** instance, set the value explicitly.
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
Tests and Storybook: `provideKptMapGeocoderStub({ city: 'Warszawa', address: 'Marszałkowska 100' })`.
|
|
325
325
|
|
|
326
|
-
##
|
|
326
|
+
## Address search (forward geocoding)
|
|
327
327
|
|
|
328
|
-
`[search]`
|
|
329
|
-
port
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
`[search]` puts a field with suggestions in the map's top right corner. Typing a phrase queries
|
|
329
|
+
the geocoder port — the **same** one as the address suggestions in the create form, just
|
|
330
|
+
in the other direction; from DI (`KPT_MAP_GEOCODER`) or from the `[geocoder]` input, which wins.
|
|
331
|
+
The `search()` method is **optional** in the port: ports written before it existed still
|
|
332
|
+
compile, and a map without it simply does not render the field.
|
|
333
333
|
|
|
334
334
|
<kpt-map [locations]="places()" [categories]="categories" search
|
|
335
335
|
(searchSelect)="onFound($event)" />
|
|
336
336
|
|
|
337
|
-
// main.ts —
|
|
337
|
+
// main.ts — without this there will be no field
|
|
338
338
|
providers: [provideKptLeafletMap(), provideKptNominatimGeocoder({ language: 'pl' })]
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
Two types, deliberately kept apart:
|
|
341
341
|
|
|
342
|
-
KptMapSuggestion = { id, label, at?, bounds?, address? } //
|
|
343
|
-
KptMapSearchResult = KptMapSuggestion & { at: KptLatLng } //
|
|
342
|
+
KptMapSuggestion = { id, label, at?, bounds?, address? } // an entry in the list
|
|
343
|
+
KptMapSearchResult = KptMapSuggestion & { at: KptLatLng } // what comes out of the event
|
|
344
344
|
|
|
345
|
-
`address`
|
|
345
|
+
`address` is the same `KptMapAddress` as in reverse geocoding — there is no second address model.
|
|
346
346
|
|
|
347
|
-
###
|
|
347
|
+
### Suggestions with no coordinates (`resolve()`)
|
|
348
348
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
349
|
+
Most geocoders queried forward (Nominatim, Photon, the Google Geocoding API) give `lat`/`lng`
|
|
350
|
+
right in the list. **Google Places Autocomplete does not**: it returns a `placeId` and text,
|
|
351
|
+
and the coordinates come from a separate Place Details call. This is not one provider's quirk — it is what
|
|
352
|
+
its session-based billing rests on, where a whole run of suggestions plus the final Place Details counts
|
|
353
|
+
as one event.
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
That is why `search()` returns a `KptMapSuggestion` (with `at` **optional**), and a port may add:
|
|
356
356
|
|
|
357
357
|
resolve?(suggestion: KptMapSuggestion, options?: { language?, signal? })
|
|
358
358
|
: Observable<KptMapSearchResult | null>;
|
|
359
359
|
|
|
360
|
-
|
|
361
|
-
`resolve()`.**
|
|
360
|
+
The rule fits in one sentence: **if your `search()` does not return `at`, you must implement
|
|
361
|
+
`resolve()`.** Anyone returning coordinates right away does not need the method — the field will not even call it.
|
|
362
362
|
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
The handling lives in `kpt-map-search`, not in `kpt-map`, so the guarantee holds just the same for a field
|
|
364
|
+
standing on its own:
|
|
365
365
|
|
|
366
|
-
-
|
|
367
|
-
-
|
|
368
|
-
panel
|
|
369
|
-
- `null`
|
|
370
|
-
|
|
371
|
-
-
|
|
366
|
+
- a suggestion with `at` → `selected` fires **immediately**, with no extra request;
|
|
367
|
+
- a suggestion without `at` → `resolve()` once, **after the entry is picked**, never while typing;
|
|
368
|
+
the panel stays open with a "Searching…" note, and `aria-busy` on the field carries that to a screen reader;
|
|
369
|
+
- `null` or an error → the label stays in the field, the list comes back (another entry can be picked),
|
|
370
|
+
and `aria-live` says "no matching places";
|
|
371
|
+
- another pick or clearing the field aborts an in-flight request.
|
|
372
372
|
|
|
373
|
-
**`selected`
|
|
373
|
+
**`selected` and `searchSelect` always carry coordinates.** The application never checks whether `at` is there.
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
When a port **has no** `resolve()`, suggestions without `at` never reach the list at all: an entry whose
|
|
376
|
+
click would have nowhere to fly is worse than no entry.
|
|
377
377
|
|
|
378
|
-
|
|
378
|
+
The stub can pretend to be such a provider — without it there is no way to demo the path without an API key:
|
|
379
379
|
|
|
380
380
|
provideKptMapGeocoderStub(address, results, { deferCoordinates: true })
|
|
381
381
|
|
|
382
|
-
**
|
|
383
|
-
|
|
384
|
-
`searchSelect`
|
|
385
|
-
`[distanceFrom]`,
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
**
|
|
399
|
-
|
|
400
|
-
`countrycodes`
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
**
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
`debounce` (300 ms)
|
|
412
|
-
adapter
|
|
413
|
-
|
|
414
|
-
`AbortController
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
382
|
+
**After a suggestion is picked the map moves the view itself.** A result with `bounds` (a city, a region,
|
|
383
|
+
a province) gets `fitBounds`, and a result without them — `setView` at `[searchZoom]`. The
|
|
384
|
+
`searchSelect` event fires after the movement and is there for the application's own purposes (moving
|
|
385
|
+
`[distanceFrom]`, querying for nearby points), not for navigation.
|
|
386
|
+
|
|
387
|
+
The Nominatim adapter **deliberately rejects an overly tight box** and returns the point alone in that case.
|
|
388
|
+
A single address has a bounding box on the order of 0.0001° there, so `fitBounds` would jump to
|
|
389
|
+
the top zoom limit on it and every address would land at a different zoom, depending on how
|
|
390
|
+
the provider traced the building — with the threshold, addresses land at a repeatable `[searchZoom]`,
|
|
391
|
+
while cities still fit into the frame whole.
|
|
392
|
+
|
|
393
|
+
The rule is held by `isFittableBounds(bounds, minSpan?)` from the core (`KPT_MIN_FIT_SPAN` = 0.001°,
|
|
394
|
+
that is ~100 m) — it is in the public API, it has tests and it applies to every adapter, not just Nominatim.
|
|
395
|
+
The threshold only has to be exceeded on **one** axis: a long, narrow street has one microscopic dimension
|
|
396
|
+
and still deserves `fitBounds`.
|
|
397
|
+
|
|
398
|
+
**Geographic narrowing.** The query gets a `viewbox` from `[bounds]`, or from the `[country]`
|
|
399
|
+
preset without them — that is bias only, it reorders the results and cuts nothing off. A hard
|
|
400
|
+
`countrycodes` is only added with `restrictToCountry` (the map will not let the view move
|
|
401
|
+
outside the country anyway, so a foreign address would be impossible to show) or explicitly
|
|
402
|
+
through `searchOptions.countryCodes`.
|
|
403
|
+
|
|
404
|
+
**The temporary pin** (`[searchMarker]`, on by default) travels the same path as the other
|
|
405
|
+
markers — through `engine.setMarkers()` — so it moves with the map frame by frame.
|
|
406
|
+
It does not belong to `entries()`, so clicking it opens nothing. It disappears once the field
|
|
407
|
+
is cleared with the × and after a click on the map.
|
|
408
|
+
|
|
409
|
+
The Nominatim policy is **stricter here than for `reverse()`**, because as-you-type suggestions
|
|
410
|
+
generate series of requests by their nature. The adapter defends itself two ways: the field waits
|
|
411
|
+
a `debounce` (300 ms) after the last character and does not query below `minLength` (3 characters), while the
|
|
412
|
+
adapter itself keeps **one queue shared with `reverse()`** — one request per second for the whole
|
|
413
|
+
client, not per method. An abandoned request (another character, the field closing) is aborted with
|
|
414
|
+
an `AbortController`. For production, put your own instance or a commercial provider behind it anyway.
|
|
415
|
+
|
|
416
|
+
The queue measures the interval **against the clock** rather than adding a fixed delay before each request: the first one
|
|
417
|
+
after a quiet moment goes out immediately, and the next waits only for what is left of the second.
|
|
418
|
+
The upper bound of one request per second stays intact, but the first suggestion
|
|
419
|
+
no longer costs a second up front.
|
|
420
420
|
|
|
421
421
|
### KptMapSearch (kpt-map-search)
|
|
422
422
|
|
|
423
|
-
|
|
424
|
-
(
|
|
423
|
+
The field itself is a separate component — like the legend, so that it can stand outside the map
|
|
424
|
+
(an application toolbar, a side panel):
|
|
425
425
|
|
|
426
426
|
<kpt-map-search [viewbox]="bounds" (selected)="goTo($event)" (cleared)="clearPin()" />
|
|
427
427
|
|
|
428
|
-
|
|
429
|
-
`viewbox`, `disabled`, `geocoder` (
|
|
430
|
-
`kpt-map`).
|
|
431
|
-
|
|
428
|
+
Inputs: `placeholder`, `minLength` (3), `debounce` (300), `limit` (5), `countryCodes`,
|
|
429
|
+
`viewbox`, `disabled`, `geocoder` (this field's port; it wins over DI — that is how
|
|
430
|
+
`kpt-map` passes it in). Outputs: `selected: KptMapSearchResult`, `cleared: void`.
|
|
431
|
+
Without a port that has `search()` the field renders **disabled**, rather than pretending it will find anything.
|
|
432
432
|
|
|
433
|
-
**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
433
|
+
**Why not `kpt-autocomplete`** — the question keeps coming back, so in writing: that one filters options
|
|
434
|
+
client-side (`includes(q)`), so a geocoder result that tolerates typos or is phrased differently
|
|
435
|
+
would drop off the list; its value is a plain `string`, so there is nothing to carry
|
|
436
|
+
coordinates in; and it stands on CDK Overlay, which would add `@angular/cdk/overlay-prebuilt.css`
|
|
437
|
+
to the map's requirements when it otherwise needs none. The suggestion panel is therefore drawn directly
|
|
438
|
+
in the Angular layer over the map, just like the popup and the right-click menu.
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
`peerDependency`
|
|
440
|
+
The debounce stands on a `setTimeout` inside an effect with `onCleanup`, not on rxjs operators: this package's
|
|
441
|
+
ports use `Observable`/`of` exclusively, and rxjs is not declared as the library's
|
|
442
|
+
`peerDependency` (it arrives transitively through `@angular/core`).
|
|
443
443
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
The typed text and the phrase to query are **two separate signals**. Picking a suggestion writes its
|
|
445
|
+
label into the field but does not touch the phrase — otherwise a pick would immediately fire another query
|
|
446
|
+
for what we just found.
|
|
447
447
|
|
|
448
|
-
##
|
|
448
|
+
## Legend
|
|
449
449
|
|
|
450
|
-
`kpt-map-legend` —
|
|
451
|
-
|
|
450
|
+
`kpt-map-legend` — a separate component, so that it can be placed outside the map. The entries are
|
|
451
|
+
buttons with `aria-pressed`; a click hides the category (it also disappears from the clusters, and the counters drop).
|
|
452
452
|
|
|
453
453
|
<kpt-map [legend]="false" [(hiddenCategories)]="hidden" … />
|
|
454
454
|
<kpt-map-legend [categories]="categories" [(hidden)]="hidden" orientation="vertical" />
|
|
455
455
|
|
|
456
|
-
|
|
456
|
+
Inputs: `categories` (required), `hidden` (a model), `counts`, `orientation`, `interactive`.
|
|
457
457
|
|
|
458
|
-
##
|
|
458
|
+
## Geolocation
|
|
459
459
|
|
|
460
|
-
`[showUserLocation]`
|
|
461
|
-
|
|
460
|
+
`[showUserLocation]` enables the marker for the user's own position (the `user` icon) with an accuracy circle
|
|
461
|
+
and a "center on me" control. The position comes through a DI port:
|
|
462
462
|
|
|
463
|
-
KPT_MAP_GEOLOCATION → KptBrowserGeolocation (
|
|
464
|
-
provideKptMapGeolocationStub({ lat: 52.23, lng: 21.01 }) // Storybook,
|
|
463
|
+
KPT_MAP_GEOLOCATION → KptBrowserGeolocation (by default)
|
|
464
|
+
provideKptMapGeolocationStub({ lat: 52.23, lng: 21.01 }) // Storybook, tests
|
|
465
465
|
|
|
466
|
-
Geolocation API
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
466
|
+
The Geolocation API works **only in a secure context** (HTTPS or `localhost`).
|
|
467
|
+
On `http://` with an IP address the browser refuses — that is not a bug in the component. A denied permission
|
|
468
|
+
disables the control and emits `userLocationError`; the component shows no alert of its own, leaving
|
|
469
|
+
the decision about a message to the application. Under SSR the port returns `null`.
|
|
470
470
|
|
|
471
|
-
##
|
|
471
|
+
## Engines
|
|
472
472
|
|
|
473
|
-
|
|
474
|
-
|
|
473
|
+
The engine is supplied by a factory under the `KPT_MAP_ENGINE` token, and the component talks to it through
|
|
474
|
+
the `KptMapEngine` interface (zero Leaflet types in the signatures). The split of rendering:
|
|
475
475
|
|
|
476
|
-
-
|
|
477
|
-
- **popup,
|
|
476
|
+
- tiles, panning, zoom, **markers and clusters** → the engine (`L.divIcon` with HTML from the core),
|
|
477
|
+
- **the popup, the controls, the menu, the legend, the dialog** → an Angular layer over the map container.
|
|
478
478
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
479
|
+
Markers go through the engine because they have to move with the map frame by frame (Leaflet does that
|
|
480
|
+
with a single `transform` on the pane). There is one popup at a time, so recomputing its position
|
|
481
|
+
with `engine.project()` on every movement is cheap — and it buys full Angular in the content
|
|
482
|
+
and one implementation for both engines.
|
|
483
483
|
|
|
484
|
-
###
|
|
484
|
+
### Two engines
|
|
485
485
|
|
|
486
|
-
provideKptLeafletMap() //
|
|
487
|
-
provideKptGoogleMap({ apiKey, mapId }) // Google Maps JavaScript API
|
|
486
|
+
provideKptLeafletMap() // the default, no key and no account
|
|
487
|
+
provideKptGoogleMap({ apiKey, mapId }) // the Google Maps JavaScript API
|
|
488
488
|
|
|
489
|
-
Leaflet
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
489
|
+
Leaflet needs the (optional) `leaflet` package and `leaflet/dist/leaflet.css`. Google needs
|
|
490
|
+
**no npm package at all** — the API is pulled in by a script with the first map — but it does need a key,
|
|
491
|
+
a billing account with a card and a `mapId` from the console (*Map management → Create Map ID*).
|
|
492
|
+
Without `mapId` `AdvancedMarkerElement` does not work, which means there will be no markers.
|
|
493
493
|
|
|
494
494
|
| | `leaflet` | `google` |
|
|
495
495
|
| ---------------------- | ----------------------------- | ----------------------------------------- |
|
|
496
|
-
| `[tileUrl]`/`[attribution]` |
|
|
497
|
-
|
|
|
498
|
-
|
|
|
499
|
-
|
|
|
500
|
-
|
|
|
501
|
-
| zoom |
|
|
496
|
+
| `[tileUrl]`/`[attribution]` | work, swappable on the fly | **ignored** — Google supplies the tiles |
|
|
497
|
+
| attribution | Leaflet's control (ODbL) | the logo and ToS links are built in, **do not cover them** |
|
|
498
|
+
| key / account | not needed | an API key + billing with a card |
|
|
499
|
+
| cost | OSM tiles (no SLA) | the Dynamic Maps SKU: 10k loads/month free |
|
|
500
|
+
| the Google geocoder | **not allowed** | allowed |
|
|
501
|
+
| zoom | integer, max 19 | fractional on gestures, above 21 |
|
|
502
502
|
|
|
503
|
-
**
|
|
504
|
-
Specific Terms §6.2 (Geocoding API)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
503
|
+
**The last row is the reason this engine exists.** The Google Maps Platform Service
|
|
504
|
+
Specific Terms §6.2 (Geocoding API) and §14.2 (Places API) forbid using their results
|
|
505
|
+
"in conjunction with a non-Google map". A Google adapter under `KPT_MAP_GEOCODER` is therefore compliant
|
|
506
|
+
**only** with the `google` engine — not with the default Leaflet. The library itself
|
|
507
|
+
ships no such adapter; that is the application's decision and code.
|
|
508
508
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
509
|
+
That is why the geocoder is per map rather than per application: the `[geocoder]` input (below) lets you
|
|
510
|
+
put a Leaflet map with an OSM geocoder next to a Google map with a Google geocoder.
|
|
511
|
+
A global provider could not settle that — it would have to break the rule on one
|
|
512
|
+
of the two sides.
|
|
513
513
|
|
|
514
|
-
### `[engine]` —
|
|
514
|
+
### `[engine]` — two maps, two engines, one page
|
|
515
515
|
|
|
516
|
-
|
|
516
|
+
The input takes a factory and wins over `KPT_MAP_ENGINE` from DI. Without it nothing changes.
|
|
517
517
|
|
|
518
518
|
readonly google: KptMapEngineFactory = () => createGoogleEngine({ apiKey, mapId });
|
|
519
519
|
|
|
520
520
|
<kpt-map [engine]="google" [locations]="places()" [categories]="cats" />
|
|
521
521
|
|
|
522
|
-
|
|
523
|
-
|
|
522
|
+
It is read once, at initialization — swapping it after startup does not reload the map. No engine
|
|
523
|
+
in either source produces a readable error with instructions, not a `NullInjectorError`.
|
|
524
524
|
|
|
525
|
-
### `[geocoder]` — port
|
|
525
|
+
### `[geocoder]` — the port follows the engine
|
|
526
526
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
`KPT_MAP_GEOCODER`
|
|
527
|
+
Symmetrical to `[engine]` and for the same reason: a Google geocoder may only be shown
|
|
528
|
+
with a Google map, and that is a property of the **map**, not of the application. The input wins over
|
|
529
|
+
`KPT_MAP_GEOCODER` from DI and reaches both the search and the create form.
|
|
530
530
|
|
|
531
|
-
<kpt-map [engine]="leaflet" search … /> <!-- port
|
|
531
|
+
<kpt-map [engine]="leaflet" search … /> <!-- the port from DI (Nominatim, say) -->
|
|
532
532
|
<kpt-map [engine]="google" [geocoder]="googleGeocoder" search … />
|
|
533
533
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
534
|
+
Unlike `[engine]` it is read **reactively**: swapping the port clears the suggestion list
|
|
535
|
+
and queries the new provider from the next phrase on. `null` in both sources means what
|
|
536
|
+
having no port means today: the search field does not render, and the form suggests no address.
|
|
537
537
|
|
|
538
|
-
|
|
538
|
+
`kpt-map-search` and `kpt-map-create-dialog` have the same input when they stand outside a map.
|
|
539
539
|
|
|
540
|
-
**
|
|
541
|
-
`maps.googleapis.com/maps/api/geocode/json`
|
|
540
|
+
**When writing a Google adapter, go through the SDK, not through REST.** The
|
|
541
|
+
`maps.googleapis.com/maps/api/geocode/json` web service rejects keys restricted by domain:
|
|
542
542
|
|
|
543
543
|
{ "status": "REQUEST_DENIED",
|
|
544
544
|
"error_message": "API keys with referer restrictions cannot be used with this API." }
|
|
545
545
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
546
|
+
and that is the only restriction that makes sense for a key sitting in plain sight in a browser bundle. CORS on
|
|
547
|
+
that endpoint works, so there is not even a network error — a `200` arrives with `REQUEST_DENIED`
|
|
548
|
+
in the body, easy to miss. `google.maps.Geocoder` from the loaded SDK authorizes the same way the map itself
|
|
549
|
+
does, so it works with the same key — the same SKU, just a different route. Access to the API
|
|
550
|
+
comes from the exported loader (the same one the engine uses):
|
|
551
551
|
|
|
552
552
|
const maps = await loadGoogleMapsApi({ apiKey, language: 'pl' });
|
|
553
553
|
const { Geocoder } = await maps.importLibrary('geocoding');
|
|
554
554
|
const { results } = await new Geocoder().geocode({ address: 'Marszałkowska 100, Warszawa' });
|
|
555
555
|
|
|
556
|
-
|
|
557
|
-
(`lat()`, `getNorthEast()`),
|
|
558
|
-
|
|
559
|
-
|
|
556
|
+
Mind the shape of the response: the SDK returns `location` and `viewport` as objects with methods
|
|
557
|
+
(`lat()`, `getNorthEast()`), not as fields the way REST does. An SDK request **cannot be aborted**,
|
|
558
|
+
so the `signal` from the port's options has nothing behind it — unsubscribing merely stops listening.
|
|
559
|
+
An adapter from start to finish: `apps/playground/src/pg-map.google-geocoder.ts`.
|
|
560
560
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
561
|
+
The engine lands on the host as `[data-engine]` (`leaflet` | `google`) — that attribute is what the few
|
|
562
|
+
differences are styled by, such as the accuracy circle (an SVG circle in Leaflet, an element with a background in
|
|
563
|
+
Google, because `google.maps.Circle` takes colors in its options while tokens are the rule here).
|
|
564
564
|
|
|
565
|
-
|
|
566
|
-
`map.getProjection()`:
|
|
567
|
-
|
|
568
|
-
|
|
565
|
+
The Google adapter computes `project()`/`unproject()` **in the core** (`core/geo.ts`) rather than with
|
|
566
|
+
`map.getProjection()`: that one can be `null` until the first `idle` and needs converting
|
|
567
|
+
into container pixels anyway. As a result the popup lands pixel for pixel where it does with Leaflet,
|
|
568
|
+
agrees with the clustering and copes with fractional zoom.
|
|
569
569
|
|
|
570
|
-
##
|
|
570
|
+
## Core (pure TS)
|
|
571
571
|
|
|
572
|
-
`@konce-pt/angular/map`
|
|
573
|
-
|
|
572
|
+
`@konce-pt/angular/map` exports the computation core — with no Angular and no engine, portable
|
|
573
|
+
to the React port: `clusterLocations`, `dominantCategory`, `countByCategory`, `isSplittable`,
|
|
574
574
|
`project`/`unproject`/`boundsOf`/`haversine`/`metersPerPixel`/`zoomForBounds`/`isFittableBounds`,
|
|
575
575
|
`KPT_MAP_COUNTRIES`/`countryPreset`/`resolveCountryView`,
|
|
576
576
|
`renderMarkerHtml`/`renderClusterHtml`/`colorVarOf`/`contrastVarOf`/`foregroundVarOf`,
|
|
577
|
-
`formatDistance`/`formatLatLng`
|
|
577
|
+
`formatDistance`/`formatLatLng` and the opening hours: `parseTime`, `formatTime`, `isoWeekday`,
|
|
578
578
|
`isOpenAt`, `isAlwaysOpen`, `nextChange`, `weekdayName`, `groupWeek`, `emptyWeek`.
|
|
579
579
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
`24:00`
|
|
583
|
-
`isOpenAt()`
|
|
584
|
-
|
|
585
|
-
(`
|
|
586
|
-
|
|
580
|
+
The hours model: `KptMapOpeningHours` = an array of `{ day: 1–7 (ISO), closed?, ranges: [{ from, to }] }`
|
|
581
|
+
in `HH:MM` of the place's local time. A `to` lower than or equal to `from` means "past midnight";
|
|
582
|
+
`24:00` is a valid end of day and does **not** cross midnight. A missing day = closed.
|
|
583
|
+
`isOpenAt()` checks two days — the current one and the previous one — because a `22:00–01:00` range covers
|
|
584
|
+
the early hours of the next day. `groupWeek()` joins adjacent days with an identical schedule
|
|
585
|
+
(`Mon–Fri`), but it does **not** wrap the week across the Sunday–Monday boundary: a week reads
|
|
586
|
+
linearly and wrapping would be confusing.
|
|
587
587
|
|
|
588
|
-
|
|
588
|
+
Tests: `packages/angular/map/src/core/*.test.ts` (`node --test`).
|
|
589
589
|
|
|
590
|
-
##
|
|
590
|
+
## Clustering — how it works and what it does not do
|
|
591
591
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
592
|
+
A grid in pixel space: points are projected onto world pixels for the current zoom
|
|
593
|
+
and assigned to `gridSize` px cells. The algorithm is O(n) and deterministic — the same
|
|
594
|
+
data set always yields the same clusters with the same ids.
|
|
595
595
|
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
The cluster center is averaged **in pixels, not in degrees**: Mercator stretches degrees toward the
|
|
597
|
+
poles, so an average in degrees lands beside the visual center of the dots on screen.
|
|
598
598
|
|
|
599
|
-
|
|
600
|
-
-
|
|
601
|
-
|
|
602
|
-
- **
|
|
603
|
-
|
|
604
|
-
(`isSplittable()`
|
|
605
|
-
-
|
|
599
|
+
The limitations (deliberate):
|
|
600
|
+
- a cluster can "jump" when a cell boundary is crossed while zooming — that is the price
|
|
601
|
+
for skipping the preprocessing hierarchical clustering requires;
|
|
602
|
+
- **there is no spiderfy.** Points with identical coordinates stay together at every zoom,
|
|
603
|
+
so clicking such a cluster opens a popup listing the entries instead of zooming forever
|
|
604
|
+
(`isSplittable()` recognizes that case);
|
|
605
|
+
- above `clusterMaxZoom` (16 by default) clustering is switched off.
|
|
606
606
|
|
|
607
|
-
##
|
|
607
|
+
## Tiles — the OSM policy
|
|
608
608
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
pre-
|
|
612
|
-
|
|
609
|
+
The default `https://tile.openstreetmap.org/{z}/{x}/{y}.png` is a **best-effort service with no SLA**.
|
|
610
|
+
The OSM Foundation requires visible attribution and a sensible `User-Agent`/`Referer`, forbids
|
|
611
|
+
pre-fetching and reserves the right to block traffic without notice — commercial traffic
|
|
612
|
+
in particular. The default source is there so that the demo works right away.
|
|
613
613
|
|
|
614
|
-
**
|
|
615
|
-
|
|
614
|
+
**For production, put your own or a commercial tile server behind it** through `[tileUrl]` + `[attribution]`.
|
|
615
|
+
Do not remove the attribution — it is a license requirement, not decoration.
|
|
616
616
|
|
|
617
|
-
##
|
|
617
|
+
## Tokens
|
|
618
618
|
|
|
619
619
|
`--kpt-map-bg`, `--kpt-map-border`, `--kpt-map-radius`, `--kpt-map-popup-bg`,
|
|
620
620
|
`--kpt-map-popup-shadow`, `--kpt-map-popup-width`, `--kpt-map-popup-width-card`,
|
|
621
621
|
`--kpt-map-popup-max-height`, `--kpt-map-search-width`, `--kpt-map-search-panel-max-height`,
|
|
622
622
|
`--kpt-map-poi-media-radius`, `--kpt-map-poi-row-hover`,
|
|
623
623
|
`--kpt-map-cluster-bg`, `--kpt-map-cluster-fg`, `--kpt-map-marker-size`,
|
|
624
|
-
`--kpt-map-height` (
|
|
624
|
+
`--kpt-map-height` (from `[height]`).
|
|
625
625
|
|
|
626
|
-
|
|
627
|
-
info,primary,muted}`
|
|
626
|
+
Marker and cluster colors come from the category roles: `--kpt-color-{success,warning,danger,
|
|
627
|
+
info,primary,muted}` and their `-contrast` variants.
|
|
628
628
|
|
|
629
|
-
##
|
|
629
|
+
## Leaflet's stacking context — do not touch
|
|
630
630
|
|
|
631
|
-
`.kpt-map .leaflet-container`
|
|
632
|
-
(panes 200–700,
|
|
633
|
-
|
|
634
|
-
(2)
|
|
631
|
+
`.kpt-map .leaflet-container` gets `z-index: 0`, to close Leaflet's z-index scale
|
|
632
|
+
(panes 200–700, controls 800) inside its own stacking context. Without that the panes compete
|
|
633
|
+
directly with the Angular layer stretched over the map — and the popup (`z-index: 3`), the zoom controls
|
|
634
|
+
(2) and the right-click menu (4) lose and are **painted under the tiles**.
|
|
635
635
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
636
|
+
The bug is insidious, because a hit test does not show it: the panes and tiles have `pointer-events: none`,
|
|
637
|
+
so `document.elementFromPoint` returns the popup as the topmost element even though the user
|
|
638
|
+
cannot see it. The only reliable test is a screenshot.
|
|
639
639
|
|
|
640
|
-
|
|
641
|
-
|
|
640
|
+
The scale of the Angular layer over the map: zoom controls `2`, the popup `3`, the right-click menu `4`,
|
|
641
|
+
the search `5` (an expanded suggestion panel has to cover everything above).
|
|
642
642
|
|
|
643
|
-
##
|
|
643
|
+
## The map does not style child components' hosts — do not "simplify"
|
|
644
644
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
645
|
+
The search is positioned by a **wrapper**, `<div class="kpt-map__search">`, and not by a class
|
|
646
|
+
placed directly on `<kpt-map-search>`. Shortening that to a single element looks
|
|
647
|
+
innocent and **hides the field completely**.
|
|
648
648
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
`position: relative`
|
|
654
|
-
`.kpt-map__canvas` (`height: 100%`)
|
|
649
|
+
The reason: the search host has its own `.kpt-map-search { position: relative }` rule (the containing
|
|
650
|
+
block for the suggestion panel, needed outside the map too). Both rules would then land on the
|
|
651
|
+
same element with identical specificity (0,1,0) and in the same `kpt.components` layer, so
|
|
652
|
+
injection order decides — and Angular injects a child's styles **after** the parent's.
|
|
653
|
+
`position: relative` beats `absolute`, the field drops into normal flow behind
|
|
654
|
+
`.kpt-map__canvas` (`height: 100%`) and is clipped by the view's `overflow: hidden`.
|
|
655
655
|
|
|
656
|
-
|
|
657
|
-
|
|
656
|
+
The bug is insidious for the same reason as the z-index one: the element is in the DOM, has the right
|
|
657
|
+
attributes and passes every "did it render" check. It only shows up in a screenshot.
|
|
658
658
|
|
|
659
|
-
`class="kpt-map__legend"`
|
|
660
|
-
**
|
|
661
|
-
|
|
659
|
+
`class="kpt-map__legend"` on `<kpt-map-legend>` is safe, because `map.component.scss`
|
|
660
|
+
**has no rule for that class** — it is a pure hook for attaching things from outside. A rule
|
|
661
|
+
added there at any point in the future would fall into the same trap.
|
|
662
662
|
|
|
663
|
-
##
|
|
663
|
+
## A deviation from `@layer` — do not "fix" this
|
|
664
664
|
|
|
665
|
-
`map.component.scss`
|
|
666
|
-
|
|
667
|
-
`leaflet.css`
|
|
668
|
-
|
|
669
|
-
|
|
665
|
+
`map.component.scss` has two parts: its own styles in `@layer kpt.components` and a section
|
|
666
|
+
of `.leaflet-*` overrides **outside the layer**. It is the only such place in the library and it is deliberate:
|
|
667
|
+
`leaflet.css` is supplied by the application and is not layered, and CSS outside layers beats
|
|
668
|
+
every rule inside `@layer` **regardless of specificity**. Moving those rules
|
|
669
|
+
into the layer "for tidiness" will stop them from working.
|
|
670
670
|
|
|
671
671
|
## A11y
|
|
672
672
|
|
|
673
|
-
-
|
|
674
|
-
-
|
|
675
|
-
-
|
|
676
|
-
- popup: `role="dialog"`
|
|
677
|
-
(
|
|
678
|
-
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
-
|
|
682
|
-
|
|
683
|
-
`aria-activedescendant` (panel
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
-
|
|
687
|
-
-
|
|
673
|
+
- a marker: `role="img"` + `aria-label` = the location's `title`,
|
|
674
|
+
- a cluster: `aria-label` = `map.cluster` with the count,
|
|
675
|
+
- the legend: `role="group"`, entries as `<button aria-pressed>`,
|
|
676
|
+
- the popup: `role="dialog"` with `aria-label` = the title; closed by `Esc`, a click on the map and the × button
|
|
677
|
+
(with `popupLayout="card"` the card supplies the close button, so there are not two),
|
|
678
|
+
- the place card: the title as `role="heading" aria-level="3"`, the contacts as `<ul>`/`<li>`,
|
|
679
|
+
the hours as a disclosure with `aria-expanded` and `aria-controls`, copy confirmation
|
|
680
|
+
through `aria-live="polite"`,
|
|
681
|
+
- the search: `role="combobox"` + `aria-expanded`/`aria-controls`/`aria-autocomplete="list"`,
|
|
682
|
+
the list as `<ul role="listbox">` / `<li role="option">`, the active entry through
|
|
683
|
+
`aria-activedescendant` (the panel sits in the DOM next to the field, so focus stays in the `<input>`),
|
|
684
|
+
the ↑/↓/Enter keys, the first Esc closes the list, the second clears the field; the result count
|
|
685
|
+
and "no results" go to `aria-live="polite"`,
|
|
686
|
+
- the zoom controls and "center on me": `<button>` with an `aria-label` from the dictionary,
|
|
687
|
+
- the right-click menu: `role="menu"` / `role="menuitem"`.
|
|
688
688
|
|
|
689
689
|
## i18n
|
|
690
690
|
|
|
691
|
-
|
|
691
|
+
The `map.*` keys in `KptMessages` (EN + PL): `addLocation`, `chooseCategory`, `myLocation`,
|
|
692
692
|
`locateMe`, `locationDenied`, `locationUnavailable`, `zoomIn`, `zoomOut`, `legend`,
|
|
693
693
|
`showCategory`, `hideCategory`, `cluster`, `clusterItems`, `fieldRequired`, `latitude`,
|
|
694
694
|
`longitude`, `empty`, `save`, `cancel`, `lookingUpAddress`, `addressNotFound`.
|
|
695
695
|
|
|
696
|
-
|
|
696
|
+
The search: `map.search.*` — `placeholder`, `label`, `results`, `clear`, `noResults`,
|
|
697
697
|
`minLength` ({count}), `searching`, `found` ({count}).
|
|
698
698
|
|
|
699
|
-
|
|
699
|
+
The place card: `map.poi.*` — `distanceAway`, `openNow`, `closedNow`, `opensAt`, `closesAt`,
|
|
700
700
|
`alwaysOpen`, `openingHours`, `closedDay`, `call`, `sendEmail`, `openWebsite`, `coordinates`,
|
|
701
701
|
`copy`, `copied`, `share`, `addFavourite`, `removeFavourite`, `noPhoto`.
|
|
702
702
|
|
|
703
|
-
|
|
703
|
+
The newer form fields: `map.field.*` — `invalidEmail`, `invalidUrl`, `imageTooLarge`, `pickImage`,
|
|
704
704
|
`orPasteUrl`, `removeImage`, `imagePreview`, `hoursClosed`, `hoursFrom`, `hoursTo`,
|
|
705
705
|
`hoursSameAllWeek`, `hoursAddRange`, `hoursRemoveRange`, `hoursIncomplete`.
|
|
706
706
|
|
|
707
|
-
|
|
708
|
-
|
|
707
|
+
Weekday names are **not** in the dictionary — `Intl.DateTimeFormat` takes them from the locale, so the card
|
|
708
|
+
and the hours editor work for any language without adding translations.
|
|
709
709
|
|
|
710
|
-
##
|
|
710
|
+
## Example
|
|
711
711
|
|
|
712
712
|
// main.ts
|
|
713
713
|
bootstrapApplication(App, {
|
|
714
714
|
providers: [provideKptTablerIcons(), provideKptLeafletMap()],
|
|
715
715
|
});
|
|
716
716
|
|
|
717
|
-
//
|
|
717
|
+
// the component
|
|
718
718
|
readonly locations = signal<KptMapLocation<Site>[]>(SITES);
|
|
719
719
|
readonly categories: KptMapCategory[] = [
|
|
720
|
-
{ id: 'opening', label: '
|
|
721
|
-
{ id: 'closure', label: '
|
|
720
|
+
{ id: 'opening', label: 'Opening', color: 'success', icon: 'building-store', fields: [ … ] },
|
|
721
|
+
{ id: 'closure', label: 'Closure', color: 'danger', icon: 'lock', fields: [ … ] },
|
|
722
722
|
];
|
|
723
723
|
|
|
724
724
|
onCreate(event: KptMapCreateEvent): void {
|
|
725
725
|
this.locations.update((all) => [...all, toLocation(event)]);
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
-
//
|
|
728
|
+
// the template
|
|
729
729
|
<kpt-map
|
|
730
730
|
[locations]="locations()"
|
|
731
731
|
[categories]="categories"
|
|
@@ -736,6 +736,6 @@ i edytor godzin działają dla dowolnego języka bez dopisywania tłumaczeń.
|
|
|
736
736
|
>
|
|
737
737
|
<ng-template kptMapPopup category="opening" let-loc let-d="data">
|
|
738
738
|
<strong>{{ loc.title }}</strong>
|
|
739
|
-
<p>
|
|
739
|
+
<p>Opening: {{ d.openingDate }}</p>
|
|
740
740
|
</ng-template>
|
|
741
741
|
</kpt-map>
|