@panoramax/web-viewer 3.2.3 → 4.0.0-develop-39167b4d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/.gitlab-ci.yml +13 -6
  2. package/CHANGELOG.md +53 -1
  3. package/CODE_OF_CONDUCT.md +1 -1
  4. package/README.md +1 -1
  5. package/build/editor.html +10 -1
  6. package/build/index.css +12 -12
  7. package/build/index.css.map +1 -1
  8. package/build/index.html +1 -1
  9. package/build/index.js +2126 -14
  10. package/build/index.js.map +1 -1
  11. package/build/map.html +1 -1
  12. package/build/photo.html +1 -0
  13. package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff +0 -0
  14. package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff2 +0 -0
  15. package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff +0 -0
  16. package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff2 +0 -0
  17. package/build/viewer.html +12 -1
  18. package/build/widgets.html +1 -0
  19. package/config/jest/mocks.js +201 -0
  20. package/config/paths.js +2 -0
  21. package/config/webpack.config.js +52 -0
  22. package/docs/03_URL_settings.md +14 -16
  23. package/docs/05_Compatibility.md +59 -76
  24. package/docs/09_Develop.md +46 -11
  25. package/docs/90_Releases.md +2 -2
  26. package/docs/images/class_diagram.drawio +60 -45
  27. package/docs/images/class_diagram.jpg +0 -0
  28. package/docs/images/screenshot.jpg +0 -0
  29. package/docs/index.md +135 -0
  30. package/docs/reference/components/core/Basic.md +196 -0
  31. package/docs/reference/components/core/CoverageMap.md +210 -0
  32. package/docs/reference/components/core/Editor.md +224 -0
  33. package/docs/reference/components/core/PhotoViewer.md +307 -0
  34. package/docs/reference/components/core/Viewer.md +350 -0
  35. package/docs/reference/components/layout/BottomDrawer.md +35 -0
  36. package/docs/reference/components/layout/CorneredGrid.md +29 -0
  37. package/docs/reference/components/layout/Mini.md +45 -0
  38. package/docs/reference/components/layout/Tabs.md +45 -0
  39. package/docs/reference/components/menus/MapBackground.md +32 -0
  40. package/docs/reference/components/menus/MapFilters.md +15 -0
  41. package/docs/reference/components/menus/MapLayers.md +15 -0
  42. package/docs/reference/components/menus/MapLegend.md +15 -0
  43. package/docs/reference/components/menus/PictureLegend.md +16 -0
  44. package/docs/reference/components/menus/PictureMetadata.md +15 -0
  45. package/docs/reference/components/menus/PlayerOptions.md +15 -0
  46. package/docs/reference/components/menus/QualityScoreDoc.md +15 -0
  47. package/docs/reference/components/menus/ReportForm.md +15 -0
  48. package/docs/reference/components/menus/ShareMenu.md +15 -0
  49. package/docs/reference/components/ui/Button.md +40 -0
  50. package/docs/reference/components/ui/ButtonGroup.md +36 -0
  51. package/docs/reference/components/ui/CopyButton.md +38 -0
  52. package/docs/reference/components/ui/Grade.md +32 -0
  53. package/docs/reference/components/ui/LinkButton.md +45 -0
  54. package/docs/reference/components/ui/ListGroup.md +22 -0
  55. package/docs/reference/components/ui/Loader.md +56 -0
  56. package/docs/reference/components/ui/Map.md +239 -0
  57. package/docs/reference/components/ui/MapMore.md +256 -0
  58. package/docs/reference/components/ui/Photo.md +385 -0
  59. package/docs/reference/components/ui/Popup.md +56 -0
  60. package/docs/reference/components/ui/ProgressBar.md +32 -0
  61. package/docs/reference/components/ui/QualityScore.md +45 -0
  62. package/docs/reference/components/ui/SearchBar.md +63 -0
  63. package/docs/reference/components/ui/TogglableGroup.md +39 -0
  64. package/docs/reference/components/ui/widgets/GeoSearch.md +32 -0
  65. package/docs/reference/components/ui/widgets/Legend.md +49 -0
  66. package/docs/reference/components/ui/widgets/MapFiltersButton.md +33 -0
  67. package/docs/reference/components/ui/widgets/MapLayersButton.md +15 -0
  68. package/docs/reference/components/ui/widgets/OSMEditors.md +15 -0
  69. package/docs/reference/components/ui/widgets/PictureLegendActions.md +32 -0
  70. package/docs/reference/components/ui/widgets/Player.md +33 -0
  71. package/docs/reference/components/ui/widgets/Zoom.md +15 -0
  72. package/docs/reference/utils/API.md +334 -0
  73. package/docs/reference/utils/InitParameters.md +68 -0
  74. package/docs/reference/utils/URLHandler.md +107 -0
  75. package/docs/reference.md +79 -0
  76. package/docs/shortcuts.md +11 -0
  77. package/docs/tutorials/aerial_imagery.md +19 -0
  78. package/docs/tutorials/authentication.md +10 -0
  79. package/docs/tutorials/custom_widgets.md +59 -0
  80. package/docs/tutorials/map_style.md +39 -0
  81. package/docs/tutorials/migrate_v4.md +153 -0
  82. package/docs/tutorials/synced_coverage.md +43 -0
  83. package/mkdocs.yml +66 -5
  84. package/package.json +22 -17
  85. package/public/editor.html +21 -29
  86. package/public/index.html +17 -12
  87. package/public/map.html +19 -18
  88. package/public/photo.html +55 -0
  89. package/public/viewer.html +22 -26
  90. package/public/widgets.html +306 -0
  91. package/scripts/doc.js +79 -0
  92. package/src/components/core/Basic.css +48 -0
  93. package/src/components/core/Basic.js +349 -0
  94. package/src/components/core/CoverageMap.css +9 -0
  95. package/src/components/core/CoverageMap.js +139 -0
  96. package/src/components/core/Editor.css +23 -0
  97. package/src/components/core/Editor.js +390 -0
  98. package/src/components/core/PhotoViewer.css +48 -0
  99. package/src/components/core/PhotoViewer.js +499 -0
  100. package/src/components/core/Viewer.css +98 -0
  101. package/src/components/core/Viewer.js +564 -0
  102. package/src/components/core/index.js +12 -0
  103. package/src/components/index.js +13 -0
  104. package/src/components/layout/BottomDrawer.js +257 -0
  105. package/src/components/layout/CorneredGrid.js +112 -0
  106. package/src/components/layout/Mini.js +117 -0
  107. package/src/components/layout/Tabs.js +133 -0
  108. package/src/components/layout/index.js +9 -0
  109. package/src/components/menus/MapBackground.js +106 -0
  110. package/src/components/menus/MapFilters.js +400 -0
  111. package/src/components/menus/MapLayers.js +143 -0
  112. package/src/components/menus/MapLegend.js +34 -0
  113. package/src/components/menus/PictureLegend.js +257 -0
  114. package/src/components/menus/PictureMetadata.js +317 -0
  115. package/src/components/menus/PlayerOptions.js +95 -0
  116. package/src/components/menus/QualityScoreDoc.js +36 -0
  117. package/src/components/menus/ReportForm.js +133 -0
  118. package/src/components/menus/Share.js +100 -0
  119. package/src/components/menus/index.js +15 -0
  120. package/src/components/styles.js +383 -0
  121. package/src/components/ui/Button.js +77 -0
  122. package/src/components/ui/ButtonGroup.css +57 -0
  123. package/src/components/ui/ButtonGroup.js +68 -0
  124. package/src/components/ui/CopyButton.js +106 -0
  125. package/src/components/ui/Grade.js +54 -0
  126. package/src/components/ui/LinkButton.js +67 -0
  127. package/src/components/ui/ListGroup.js +66 -0
  128. package/src/components/ui/Loader.js +203 -0
  129. package/src/components/{Map.css → ui/Map.css} +5 -17
  130. package/src/components/{Map.js → ui/Map.js} +148 -156
  131. package/src/components/ui/MapMore.js +324 -0
  132. package/src/components/{Photo.css → ui/Photo.css} +6 -6
  133. package/src/components/{Photo.js → ui/Photo.js} +313 -101
  134. package/src/components/ui/Popup.js +145 -0
  135. package/src/components/ui/ProgressBar.js +104 -0
  136. package/src/components/ui/QualityScore.js +147 -0
  137. package/src/components/ui/SearchBar.js +367 -0
  138. package/src/components/ui/TogglableGroup.js +157 -0
  139. package/src/components/ui/index.js +22 -0
  140. package/src/components/ui/widgets/GeoSearch.css +21 -0
  141. package/src/components/ui/widgets/GeoSearch.js +139 -0
  142. package/src/components/ui/widgets/Legend.js +113 -0
  143. package/src/components/ui/widgets/MapFiltersButton.js +104 -0
  144. package/src/components/ui/widgets/MapLayersButton.js +79 -0
  145. package/src/components/ui/widgets/OSMEditors.js +155 -0
  146. package/src/components/ui/widgets/PictureLegendActions.js +117 -0
  147. package/src/components/ui/widgets/Player.css +7 -0
  148. package/src/components/ui/widgets/Player.js +151 -0
  149. package/src/components/ui/widgets/Zoom.js +82 -0
  150. package/src/components/ui/widgets/index.js +13 -0
  151. package/src/img/loader_base.jpg +0 -0
  152. package/src/img/panoramax.svg +13 -0
  153. package/src/img/switch_big.svg +20 -10
  154. package/src/index.js +7 -9
  155. package/src/translations/br.json +1 -0
  156. package/src/translations/da.json +38 -15
  157. package/src/translations/de.json +5 -3
  158. package/src/translations/en.json +35 -15
  159. package/src/translations/eo.json +38 -15
  160. package/src/translations/es.json +1 -1
  161. package/src/translations/fr.json +36 -16
  162. package/src/translations/hu.json +1 -1
  163. package/src/translations/it.json +39 -16
  164. package/src/translations/ja.json +182 -1
  165. package/src/translations/nl.json +106 -6
  166. package/src/translations/pl.json +1 -1
  167. package/src/translations/sv.json +182 -0
  168. package/src/translations/zh_Hant.json +35 -14
  169. package/src/utils/API.js +109 -49
  170. package/src/utils/InitParameters.js +388 -0
  171. package/src/utils/PhotoAdapter.js +1 -0
  172. package/src/utils/URLHandler.js +362 -0
  173. package/src/utils/geocoder.js +152 -0
  174. package/src/utils/{I18n.js → i18n.js} +7 -3
  175. package/src/utils/index.js +11 -0
  176. package/src/utils/{Map.js → map.js} +256 -77
  177. package/src/utils/picture.js +442 -0
  178. package/src/utils/utils.js +324 -0
  179. package/src/utils/widgets.js +55 -0
  180. package/tests/components/core/Basic.test.js +121 -0
  181. package/tests/components/core/BasicMock.js +25 -0
  182. package/tests/components/core/CoverageMap.test.js +20 -0
  183. package/tests/components/core/Editor.test.js +20 -0
  184. package/tests/components/core/PhotoViewer.test.js +57 -0
  185. package/tests/components/core/Viewer.test.js +84 -0
  186. package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +73 -0
  187. package/tests/components/core/__snapshots__/Viewer.test.js.snap +145 -0
  188. package/tests/components/ui/CopyButton.test.js +52 -0
  189. package/tests/components/ui/Loader.test.js +55 -0
  190. package/tests/components/{Map.test.js → ui/Map.test.js} +73 -61
  191. package/tests/components/{Photo.test.js → ui/Photo.test.js} +97 -63
  192. package/tests/components/ui/Popup.test.js +26 -0
  193. package/tests/components/ui/QualityScore.test.js +18 -0
  194. package/tests/components/ui/SearchBar.test.js +110 -0
  195. package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +33 -0
  196. package/tests/components/ui/__snapshots__/Loader.test.js.snap +56 -0
  197. package/tests/components/{__snapshots__ → ui/__snapshots__}/Map.test.js.snap +11 -38
  198. package/tests/components/{__snapshots__ → ui/__snapshots__}/Photo.test.js.snap +70 -6
  199. package/tests/components/ui/__snapshots__/Popup.test.js.snap +29 -0
  200. package/tests/components/ui/__snapshots__/QualityScore.test.js.snap +11 -0
  201. package/tests/components/ui/__snapshots__/SearchBar.test.js.snap +65 -0
  202. package/tests/utils/API.test.js +83 -83
  203. package/tests/utils/InitParameters.test.js +499 -0
  204. package/tests/utils/URLHandler.test.js +401 -0
  205. package/tests/utils/__snapshots__/API.test.js.snap +10 -0
  206. package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
  207. package/tests/utils/__snapshots__/{Map.test.js.snap → geocoder.test.js.snap} +1 -1
  208. package/tests/utils/__snapshots__/map.test.js.snap +11 -0
  209. package/tests/utils/__snapshots__/picture.test.js.snap +327 -0
  210. package/tests/utils/__snapshots__/widgets.test.js.snap +19 -0
  211. package/tests/utils/geocoder.test.js +37 -0
  212. package/tests/utils/{I18n.test.js → i18n.test.js} +8 -8
  213. package/tests/utils/map.test.js +126 -0
  214. package/tests/utils/picture.test.js +745 -0
  215. package/tests/utils/utils.test.js +288 -0
  216. package/tests/utils/widgets.test.js +31 -0
  217. package/docs/01_Start.md +0 -149
  218. package/docs/02_Usage.md +0 -831
  219. package/docs/04_Advanced_examples.md +0 -216
  220. package/src/Editor.css +0 -37
  221. package/src/Editor.js +0 -361
  222. package/src/StandaloneMap.js +0 -114
  223. package/src/Viewer.css +0 -203
  224. package/src/Viewer.js +0 -1246
  225. package/src/components/CoreView.css +0 -70
  226. package/src/components/CoreView.js +0 -175
  227. package/src/components/Loader.css +0 -74
  228. package/src/components/Loader.js +0 -120
  229. package/src/img/loader_hd.jpg +0 -0
  230. package/src/utils/Exif.js +0 -193
  231. package/src/utils/Utils.js +0 -631
  232. package/src/utils/Widgets.js +0 -562
  233. package/src/viewer/URLHash.js +0 -469
  234. package/src/viewer/Widgets.css +0 -880
  235. package/src/viewer/Widgets.js +0 -1470
  236. package/tests/Editor.test.js +0 -126
  237. package/tests/StandaloneMap.test.js +0 -45
  238. package/tests/Viewer.test.js +0 -366
  239. package/tests/__snapshots__/Editor.test.js.snap +0 -298
  240. package/tests/__snapshots__/StandaloneMap.test.js.snap +0 -30
  241. package/tests/__snapshots__/Viewer.test.js.snap +0 -195
  242. package/tests/components/CoreView.test.js +0 -92
  243. package/tests/components/Loader.test.js +0 -38
  244. package/tests/components/__snapshots__/Loader.test.js.snap +0 -15
  245. package/tests/utils/Exif.test.js +0 -124
  246. package/tests/utils/Map.test.js +0 -113
  247. package/tests/utils/Utils.test.js +0 -300
  248. package/tests/utils/Widgets.test.js +0 -107
  249. package/tests/utils/__snapshots__/Exif.test.js.snap +0 -43
  250. package/tests/utils/__snapshots__/Utils.test.js.snap +0 -41
  251. package/tests/utils/__snapshots__/Widgets.test.js.snap +0 -44
  252. package/tests/viewer/URLHash.test.js +0 -559
  253. package/tests/viewer/Widgets.test.js +0 -127
  254. package/tests/viewer/__snapshots__/URLHash.test.js.snap +0 -108
  255. package/tests/viewer/__snapshots__/Widgets.test.js.snap +0 -403
@@ -0,0 +1,367 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { classMap } from "lit/directives/class-map.js";
3
+ import { map } from "lit/directives/map.js";
4
+ import { fa, listenForMenuClosure } from "../../utils/widgets";
5
+ import { faSvg } from "../styles";
6
+ import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons/faCircleExclamation";
7
+ import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons/faMagnifyingGlass";
8
+ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
9
+ import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
10
+
11
+
12
+ /**
13
+ * Search Bar Element displays an interactive search widget.
14
+ * @class Panoramax.components.ui.SearchBar
15
+ * @element pnx-search-bar
16
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
17
+ * @fires Panoramax.components.ui.SearchBar#result
18
+ * @example
19
+ * ```html
20
+ * <pnx-search-bar
21
+ * id="my-search-bar"
22
+ * placeholder="Search something..."
23
+ * .searcher=${mysearchfct}
24
+ * ._parent=${viewer}
25
+ * reduceable="false"
26
+ * reduced="false"
27
+ * size="xxl" @result=${e => console.log(e.detail)}
28
+ * >
29
+ * <!-- Optional icon for display on left-side of search bar -->
30
+ * </pnx-search-bar>
31
+ * ```
32
+ */
33
+ export default class SearchBar extends LitElement {
34
+ /** @private */
35
+ static styles = [ faSvg, css`
36
+ /* Container */
37
+ .sb {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+ gap: 5px;
42
+ border: 1px solid var(--widget-border-div);
43
+ border-radius: 5px;
44
+ background-color: var(--widget-bg);
45
+ color: var(--widget-font);
46
+ height: 30px;
47
+ border-radius: 20px;
48
+ position: relative;
49
+ padding: 0 0 0 10px;
50
+ width: fit-content;
51
+ max-width: 100%;
52
+ box-sizing: border-box;
53
+ font-family: var(--font-family);
54
+ }
55
+ .sb.sb-xxl {
56
+ height: 40px;
57
+ line-height: 40px;
58
+ }
59
+ .sb.sb-reduceable {
60
+ width: 360px;
61
+ }
62
+ .sb.sb-reduceable.sb-reduced {
63
+ width: fit-content;
64
+ padding: 0;
65
+ gap: 0;
66
+ }
67
+
68
+ /* Text field */
69
+ .sb input {
70
+ background: none;
71
+ border: none !important;
72
+ outline: none;
73
+ height: 20px;
74
+ width: 100%;
75
+ font-family: var(--font-family);
76
+ }
77
+ .sb.sb-xxl input {
78
+ font-size: 1.1em;
79
+ }
80
+ .sb.sb-reduceable.sb-reduced input {
81
+ display: none;
82
+ }
83
+
84
+ /* Status icon */
85
+ .sb-icon {
86
+ cursor: pointer;
87
+ height: 30px;
88
+ line-height: 30px;
89
+ width: 30px;
90
+ min-width: 30px;
91
+ text-align: center;
92
+ }
93
+ .sb.sb-xxl .sb-icon {
94
+ height: 40px;
95
+ line-height: 40px;
96
+ width: 40px;
97
+ min-width: 40px;
98
+ }
99
+ .sb-icon svg {
100
+ pointer-events: none;
101
+ width: 14px;
102
+ height: 14px;
103
+ }
104
+
105
+ /* Search results */
106
+ .sb-results {
107
+ position: absolute;
108
+ top: 35px;
109
+ list-style: none;
110
+ margin: 0;
111
+ padding: 0;
112
+ max-width: calc(100% - 20px);
113
+ border: 1px solid var(--widget-border-div);
114
+ border-radius: 10px;
115
+ background-color: var(--widget-bg);
116
+ color: var(--widget-font);
117
+ z-index: 130;
118
+ font-size: 1.05em;
119
+ line-height: normal;
120
+ font-family: var(--font-family);
121
+ }
122
+ .sb.sb-xxl .sb-results {
123
+ top: 45px;
124
+ }
125
+ .sb-result,
126
+ .sb-empty {
127
+ display: block;
128
+ padding: 5px 15px;
129
+ white-space: nowrap;
130
+ overflow: hidden;
131
+ text-overflow: ellipsis;
132
+ cursor: pointer;
133
+ border-radius: 0;
134
+ }
135
+ .sb-result:hover {
136
+ background-color: var(--widget-bg-hover);
137
+ }
138
+ .sb-result:first-child {
139
+ border-top-right-radius: 10px;
140
+ border-top-left-radius: 10px;
141
+ padding-top: 15px;
142
+ }
143
+ .sb-result:last-child {
144
+ border-bottom-right-radius: 10px;
145
+ border-bottom-left-radius: 10px;
146
+ padding-bottom: 15px;
147
+ }
148
+ ` ];
149
+
150
+ /**
151
+ * Component properties.
152
+ * @memberof Panoramax.components.ui.SearchBar#
153
+ * @type {Object}
154
+ * @property {string} [id] The ID attribute set on component and prefix for input as well
155
+ * @property {string} [placeholder] Default text to display on empty field
156
+ * @property {boolean} [reduceable=false] Can the search bar be collapsed (for mobile view)
157
+ * @property {boolean} [reduced=false] Is the search bar currently collapsed ?
158
+ * @property {string} [value] The default input value
159
+ * @property {string} [size=md] The component sizing (md, xxl)
160
+ * @property {function} [searcher] Search callback, function that takes as parameter the input text value, and resolves on list of results ({title, subtitle} and any other data you'd like to get on validation)
161
+ * @property {boolean} [no-menu-closure=false] Set to true to ignore menu closure events
162
+ */
163
+ static properties = {
164
+ id: {type: String},
165
+ placeholder: {type: String},
166
+ reduceable: {type: Boolean, reflect: true},
167
+ reduced: {type: Boolean, reflect: true},
168
+ value: {type: String},
169
+ size: {type: String},
170
+ _icon: {state: true},
171
+ _results: {state: true},
172
+ searcher: {type: Function},
173
+ "no-menu-closure": {type: Boolean},
174
+ };
175
+
176
+ constructor() {
177
+ super();
178
+
179
+ // State properties
180
+ this.reduceable = false;
181
+ this.reduced = false;
182
+ this.placeholder = nothing;
183
+ this.size = "md";
184
+ this._icon = "search";
185
+ this._results = null;
186
+ this["no-menu-closure"] = false;
187
+
188
+ // Other properties
189
+ this._throttler = null;
190
+ delete this._lastSearch;
191
+ }
192
+
193
+ /** @private */
194
+ connectedCallback() {
195
+ super.connectedCallback();
196
+ if(!this["no-menu-closure"]) { listenForMenuClosure(this, this.reset.bind(this)); }
197
+ }
198
+
199
+ /** @private */
200
+ attributeChangedCallback(name, _old, value) {
201
+ super.attributeChangedCallback(name, _old, value);
202
+ if(name == "value" && this._icon == "search") {
203
+ this._icon = "empty";
204
+ }
205
+ }
206
+
207
+ /** @private */
208
+ _onIconClick() {
209
+ if(["empty", "warn"].includes(this._icon)) {
210
+ this.reset();
211
+ }
212
+ if(this.reduceable && this._icon == "search") {
213
+ this.reduced = !this.reduced;
214
+ }
215
+ }
216
+
217
+ /** @private */
218
+ _onInputChange(e) {
219
+ this.value = e.target.value;
220
+ this._throttledSearch();
221
+ }
222
+
223
+ /** @private */
224
+ _onResultClick(item) {
225
+ /**
226
+ * Event for search result clicked
227
+ * @event Panoramax.components.ui.SearchBar#result
228
+ * @type {CustomEvent}
229
+ * @property {object|null} detail The data associated to clicked item (format based on searcher function results), or null on reset
230
+ */
231
+ this.dispatchEvent(new CustomEvent("result", {bubbles: true, composed: true, detail: item}));
232
+
233
+ this._results = null;
234
+ if(this._throttler) {
235
+ clearTimeout(this._throttler);
236
+ this._throttler = null;
237
+ }
238
+
239
+ if(!this.reduceable && item) {
240
+ this.value = item?.title;
241
+ this._icon = "empty";
242
+ }
243
+ else {
244
+ this.value = "";
245
+ this._icon = "search";
246
+ if(this.reduceable) { this.reduced = true; }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Limit search calls to every 500ms
252
+ * @private
253
+ */
254
+ _throttledSearch() {
255
+ if(this._throttler) {
256
+ clearTimeout(this._throttler);
257
+ delete this._throttler;
258
+ }
259
+
260
+ this._throttler = setTimeout(this._search.bind(this), 500);
261
+ }
262
+
263
+ /**
264
+ * Perform real search
265
+ * @private
266
+ */
267
+ _search() {
268
+ if(!this.value || this.value.length == 0) {
269
+ this.reset();
270
+ return;
271
+ }
272
+
273
+ if(!this.searcher) {
274
+ console.warn("No search handler defined");
275
+ return;
276
+ }
277
+
278
+ this._icon = "loading";
279
+ this._results = null;
280
+
281
+ this.searcher(this.value).then(data => {
282
+ if(this._icon !== "loading") { return; }
283
+
284
+ this._icon = "empty";
285
+ if(!data || data.length == 0) {
286
+ this._results = [];
287
+ }
288
+ else if(data === true) {
289
+ this._results = null;
290
+ }
291
+ else {
292
+ this._results = data;
293
+ if(!this["no-menu-closure"]) {
294
+ this._parent?.dispatchEvent(new CustomEvent("menu-opened", { detail: { menu: this }}));
295
+ }
296
+ }
297
+ }).catch(e => {
298
+ console.error(e);
299
+ this._icon = "warn";
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Empty results list and reset search bar content.
305
+ */
306
+ reset() {
307
+ this._onResultClick(null);
308
+ }
309
+
310
+ /** @private */
311
+ render() {
312
+ const classes = {
313
+ sb: true,
314
+ "sb-xxl": this.size === "xxl",
315
+ "sb-reduceable": this.reduceable,
316
+ "sb-reduced": this.reduced,
317
+ };
318
+
319
+ return html`<div
320
+ id=${this.id}
321
+ class=${classMap(classes)}
322
+ part="container"
323
+ >
324
+ <slot name="pre" class=${classMap({"sb-reduced": this.reduced})}></slot>
325
+
326
+ <input
327
+ id="${this.id}-input"
328
+ type="text"
329
+ placeholder=${this.placeholder}
330
+ autocomplete="off"
331
+ @change=${this._onInputChange.bind(this)}
332
+ @keypress=${this._onInputChange.bind(this)}
333
+ @paste=${this._onInputChange.bind(this)}
334
+ @input=${this._onInputChange.bind(this)}
335
+ .value=${this.value || ""}
336
+ part="input"
337
+ />
338
+
339
+ <span
340
+ class="sb-icon"
341
+ @click=${this._onIconClick.bind(this)}
342
+ >
343
+ ${this._icon == "search" ? fa(faMagnifyingGlass) : nothing}
344
+ ${this._icon == "loading" ? fa(faCircleNotch, { classes: ["fa-spin"] }) : nothing}
345
+ ${this._icon == "empty" ? fa(faXmark) : nothing}
346
+ ${this._icon == "warn" ? fa(faCircleExclamation) : nothing}
347
+ </span>
348
+
349
+ ${!this.reduced && this._results ? html`
350
+ <div class="sb-results">
351
+ ${this._results.length === 0 ? html`
352
+ <div class="sb-empty">${this._parent?._t?.pnx.search_empty}</div>
353
+ ` : nothing}
354
+
355
+ ${map(this._results, i => html`
356
+ <div class="sb-result" @click=${() => this._onResultClick(i)}>
357
+ ${i.title}
358
+ ${i.subtitle && i.subtitle != "" ? html`<br /><small>${i.subtitle}</small>` : nothing}
359
+ </div>
360
+ `)}
361
+ </div>
362
+ ` : nothing}
363
+ </div>`;
364
+ }
365
+ }
366
+
367
+ customElements.define("pnx-search-bar", SearchBar);
@@ -0,0 +1,157 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { panel } from "../styles";
3
+ import { listenForMenuClosure } from "../../utils/widgets";
4
+ import { classMap } from "lit/directives/class-map.js";
5
+
6
+ /**
7
+ * Togglable Group element allows to make a menu appear/disappear based on button click.
8
+ * @class Panoramax.components.ui.TogglableGroup
9
+ * @element pnx-togglable-group
10
+ * @slot `button` The button on which click leads to open/close menu
11
+ * @slot `default` The menu to make shown/hidden on button click
12
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
13
+ * @example
14
+ * ```html
15
+ * <pnx-togglable-group padded="false" ._parent=${viewer}>
16
+ * <pnx-button slot="button">
17
+ * Open menu
18
+ * </pnx-button>
19
+ * <div>Menu content</div>
20
+ * </pnx-togglable-group>
21
+ * ```
22
+ */
23
+ export default class TogglableGroup extends LitElement {
24
+ /** @private */
25
+ static styles = [ panel, css`
26
+ div.container {
27
+ position: relative;
28
+ height: 100%;
29
+ }
30
+
31
+ .pnx-panel { z-index: 130; }
32
+ .pnx-panel-fixed { position: fixed; }
33
+ ` ];
34
+
35
+ /**
36
+ * Component properties.
37
+ * @memberof Panoramax.components.ui.TogglableGroup#
38
+ * @type {Object}
39
+ * @property {string} [padded] If set, adds a 15px padding to the menu panel.
40
+ */
41
+ static properties = {
42
+ _opened: {state: true},
43
+ padded: {type: String},
44
+ };
45
+
46
+ constructor() {
47
+ super();
48
+ this._opened = false;
49
+ }
50
+
51
+ /** @private */
52
+ open() {
53
+ this._opened = true;
54
+ this._parent?.dispatchEvent(new CustomEvent("menu-opened", { detail: { menu: this }}));
55
+ this.adjustMenuPosition(this._btn);
56
+ if(this._chevron) { this._chevron.style.rotate = "180deg"; }
57
+ }
58
+
59
+ /** @private */
60
+ close() {
61
+ this._opened = false;
62
+ if(this._chevron) { this._chevron.style.rotate = ""; }
63
+ }
64
+
65
+ /** @private */
66
+ connectedCallback() {
67
+ super.connectedCallback();
68
+ listenForMenuClosure(this, this.close.bind(this));
69
+ }
70
+
71
+ /** @private */
72
+ handleButtonSlotChange(e) {
73
+ this._btn = e.target.assignedNodes().pop();
74
+ if(this._btn) {
75
+ // Chevron
76
+ this._chevron = this._btn.querySelector("svg.fa-chevron-down");
77
+ if(this._chevron) {
78
+ this._chevron.style.transition = "rotate 0.2s";
79
+ this._chevron.style.rotate = this._opened ? "180deg" : "";
80
+ }
81
+
82
+ this._btn.addEventListener("click", () => {
83
+ if(this._opened) { this.close(); }
84
+ else { this.open(); }
85
+ });
86
+ }
87
+ }
88
+
89
+ /** @private */
90
+ adjustMenuPosition(btn) {
91
+ const btnMenuMargin = 5;
92
+ const borderMargin = 10;
93
+
94
+ // Reset menu position
95
+ const menu = this.shadowRoot.querySelector("div[part='menu']");
96
+ menu.style.top = null;
97
+ menu.style.bottom = null;
98
+ menu.style.right = null;
99
+ menu.style.left = null;
100
+ menu.style.overflowY = null;
101
+ menu.style.marginTop = null;
102
+
103
+ // Get positions on screen
104
+ const btnRect = btn.getBoundingClientRect();
105
+ let menuRect = menu.getBoundingClientRect();
106
+ const fitsWidth = menuRect.right <= window.innerWidth - borderMargin;
107
+ const fitsHeight = menuRect.bottom <= window.innerHeight - borderMargin;
108
+
109
+ // No overflow = space a bit under button
110
+ if(fitsWidth && fitsHeight) {
111
+ menu.style.top = `${btnRect.bottom+btnMenuMargin}px`;
112
+ }
113
+ // Overflows width+height = put at button's top+left
114
+ else if(!fitsWidth && !fitsHeight) {
115
+ menu.style.right = `${window.innerWidth - btnRect.left + btnMenuMargin}px`;
116
+ menu.style.bottom = `${window.innerHeight - btnRect.bottom}px`;
117
+
118
+ // Check if it doesn't overflow on top
119
+ menuRect = menu.getBoundingClientRect();
120
+ if(menuRect.top - borderMargin < 0) {
121
+ menu.style.bottom = `${borderMargin}px`;
122
+ menu.style.top = `${btnRect.top}px`;
123
+ menu.style.overflowY = "auto";
124
+ }
125
+ }
126
+ // Overflows height = limit height
127
+ else if(!fitsHeight) {
128
+ menu.style.bottom = `${borderMargin}px`;
129
+ menu.style.top = `${btnRect.bottom+btnMenuMargin}px`;
130
+ menu.style.overflowY = "auto";
131
+ }
132
+ // Overflows width = move to left
133
+ else if(!fitsWidth) {
134
+ menu.style.marginTop = `${btnMenuMargin}px`;
135
+ menu.style.right = `${window.innerWidth - btnRect.right}px`;
136
+ }
137
+ }
138
+
139
+ /** @private */
140
+ render() {
141
+ const panelClasses = {
142
+ "pnx-panel": true,
143
+ "pnx-panel-fixed": true,
144
+ "pnx-hidden": !this._opened,
145
+ "pnx-padded": this.padded !== "false",
146
+ };
147
+
148
+ return html`<div class="container">
149
+ <slot name="button" @slotchange=${this.handleButtonSlotChange}></slot>
150
+ <div class=${classMap(panelClasses)} part="menu">
151
+ <slot></slot>
152
+ </div>
153
+ </div>`;
154
+ }
155
+ }
156
+
157
+ customElements.define("pnx-togglable-group", TogglableGroup);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Smaller UI pieces components
3
+ * @module Panoramax:components:ui
4
+ */
5
+
6
+ export {default as ButtonGroup} from "./ButtonGroup";
7
+ export {default as Button} from "./Button";
8
+ export {default as CopyButton} from "./CopyButton";
9
+ export {default as Grade} from "./Grade";
10
+ export {default as LinkButton} from "./LinkButton";
11
+ export {default as ListGroup} from "./ListGroup";
12
+ export {default as Loader} from "./Loader";
13
+ export {default as Map} from "./Map";
14
+ export {default as MapMore} from "./MapMore";
15
+ export {default as Photo} from "./Photo";
16
+ export {default as Popup} from "./Popup";
17
+ export {default as ProgressBar} from "./ProgressBar";
18
+ export {default as QualityScore} from "./QualityScore";
19
+ export {default as SearchBar} from "./SearchBar";
20
+ export {default as TogglableGroup} from "./TogglableGroup";
21
+ import * as widgets from "./widgets";
22
+ export {widgets};
@@ -0,0 +1,21 @@
1
+ pnx-widget-geosearch .maplibregl-ctrl {
2
+ box-shadow: none !important;
3
+ background: none !important;
4
+ }
5
+
6
+ pnx-widget-geosearch .maplibregl-ctrl-geolocate {
7
+ border-radius: 15px;
8
+ }
9
+
10
+ pnx-widget-geosearch pnx-search-bar[reduced] .maplibregl-ctrl-geolocate {
11
+ width: 40px;
12
+ }
13
+
14
+ pnx-widget-geosearch pnx-search-bar:not([reduced]) .maplibregl-ctrl-geolocate {
15
+ margin-left: -5px;
16
+ }
17
+
18
+ pnx-widget-geosearch pnx-search-bar:not([reduceable])::part(input) {
19
+ width: 300px;
20
+ max-width: 30vw;
21
+ }
@@ -0,0 +1,139 @@
1
+ // DO NOT REMOVE THE "!": bundled builds breaks otherwise !!!
2
+ import maplibregl from "!maplibre-gl";
3
+
4
+ import { LitElement, html } from "lit";
5
+ import { forwardGeocodingBAN, forwardGeocodingNominatim } from "../../../utils/geocoder";
6
+ import "./GeoSearch.css";
7
+
8
+ const GEOCODER_ENGINES = { "ban": forwardGeocodingBAN, "nominatim": forwardGeocodingNominatim };
9
+
10
+ /**
11
+ * Ready-to-use geocoder search bar.
12
+ * @class Panoramax.components.ui.widgets.GeoSearch
13
+ * @element pnx-widget-geosearch
14
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
15
+ * @example
16
+ * ```html
17
+ * <pnx-widget-geosearch _parent=${viewer} />
18
+ * ```
19
+ */
20
+ export default class GeoSearch extends LitElement {
21
+ /**
22
+ * Component properties.
23
+ * @memberof Panoramax.components.ui.widgets.GeoSearch#
24
+ * @type {Object}
25
+ * @property {string} [geocoder=nominatim] The geocoder engine to use (nominatim, ban)
26
+ */
27
+ static properties = {
28
+ geocoder: {type: String},
29
+ _geolocate: {state: true},
30
+ };
31
+
32
+ constructor() {
33
+ super();
34
+ this.geocoder = "nominatim";
35
+
36
+ this._geolocateCtrl = new maplibregl.GeolocateControl({
37
+ positionOptions: {
38
+ enableHighAccuracy: true,
39
+ timeout: 60000, // Max 1 minute for first position
40
+ maximumAge: 300000, // Accepts 5 minutes old position
41
+ },
42
+ showAccuracyCircle: true,
43
+ showUserLocation: true,
44
+ trackUserLocation: true,
45
+ });
46
+ }
47
+
48
+ /** @private */
49
+ createRenderRoot() {
50
+ return this;
51
+ }
52
+
53
+ /** @private */
54
+ connectedCallback() {
55
+ super.connectedCallback();
56
+
57
+ this._geocoderEngine = GEOCODER_ENGINES[this.geocoder];
58
+ this._parent?.onceMapReady?.().then(() => {
59
+ this._geolocate = this._geolocateCtrl.onAdd(this._parent.map);
60
+ this._geolocate.setAttribute("slot", "pre");
61
+ });
62
+ }
63
+
64
+ /** @private */
65
+ _onInput(query) {
66
+ const rgxCoords = /([-+]?\d{1,2}\.\d+),\s*([-+]?\d{1,3}\.\d+)/;
67
+ const coordsMatch = query.match(rgxCoords);
68
+ const rgxUuid = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
69
+ const uuidMatch = query.match(rgxUuid);
70
+
71
+ if(coordsMatch) {
72
+ const lat = parseFloat(coordsMatch[1]);
73
+ const lon = parseFloat(coordsMatch[2]);
74
+ this._parent.map.jumpTo({
75
+ center: [lon, lat],
76
+ zoom: 16,
77
+ });
78
+ return Promise.resolve(true);
79
+ }
80
+ else if(uuidMatch) {
81
+ this._parent.select(null, query);
82
+ return Promise.resolve(true);
83
+ }
84
+ else {
85
+ return this._geocoderEngine({
86
+ query,
87
+ limit: 3,
88
+ //bbox: this._parent.map.getBounds().toArray().map(d => d.join(",")).join(","),
89
+ proximity: this._parent.map.getCenter().lat+","+this._parent.map.getCenter().lng,
90
+ }).then(data => {
91
+ data = data.features.map(f => ({
92
+ title: f.place_name.split(",")[0],
93
+ subtitle: f.place_name.split(",").slice(1).join(", "),
94
+ data: f
95
+ }));
96
+ return data;
97
+ });
98
+ }
99
+ }
100
+
101
+ /** @private */
102
+ _onSelect(e) {
103
+ const entry = e.detail;
104
+ if(entry) {
105
+ if(entry.data.bounds) {
106
+ this._parent?.map.fitBounds(entry.data.bounds, {animate: false});
107
+ }
108
+ else {
109
+ this._parent?.map.jumpTo({
110
+ center: entry.data.center,
111
+ zoom: entry.data.zoom || 13,
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ /** @private */
118
+ render() {
119
+ const isSmall = this._parent?.isWidthSmall() || false;
120
+
121
+ return html`
122
+ <pnx-search-bar
123
+ id="pnx-widget-search-bar"
124
+ placeholder=${this._parent?._t.pnx.search_address}
125
+ ._parent=${this._parent}
126
+ .searcher=${this._onInput.bind(this)}
127
+ .reduceable=${isSmall}
128
+ .reduced=${isSmall}
129
+ size="xxl"
130
+ class="pnx-print-hidden"
131
+ @result=${this._onSelect.bind(this)}
132
+ >
133
+ ${this._geolocate}
134
+ </pnx-search-bar>
135
+ `;
136
+ }
137
+ }
138
+
139
+ customElements.define("pnx-widget-geosearch", GeoSearch);