@panoramax/web-viewer 4.0.2 → 4.0.3-develop-54221cf0

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 (58) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/build/index.css +2 -2
  3. package/build/index.css.map +1 -1
  4. package/build/index.js +287 -72
  5. package/build/index.js.map +1 -1
  6. package/config/jest/mocks.js +5 -0
  7. package/docs/09_Develop.md +6 -0
  8. package/docs/reference/components/core/PhotoViewer.md +1 -0
  9. package/docs/reference/components/core/Viewer.md +2 -1
  10. package/docs/reference/components/menus/AnnotationsList.md +16 -0
  11. package/docs/reference/components/ui/HashTags.md +15 -0
  12. package/docs/reference/components/ui/ListItem.md +38 -0
  13. package/docs/reference/components/ui/Photo.md +78 -1
  14. package/docs/reference/components/ui/SemanticsTable.md +32 -0
  15. package/docs/reference/components/ui/widgets/GeoSearch.md +5 -1
  16. package/docs/reference/utils/PresetsManager.md +35 -0
  17. package/docs/reference.md +4 -0
  18. package/mkdocs.yml +4 -0
  19. package/package.json +2 -1
  20. package/src/components/core/Basic.css +2 -0
  21. package/src/components/core/PhotoViewer.js +11 -0
  22. package/src/components/core/Viewer.js +8 -1
  23. package/src/components/layout/Tabs.js +1 -1
  24. package/src/components/menus/AnnotationsList.js +151 -0
  25. package/src/components/menus/PictureLegend.js +6 -5
  26. package/src/components/menus/PictureMetadata.js +80 -8
  27. package/src/components/menus/index.js +1 -0
  28. package/src/components/styles.js +34 -0
  29. package/src/components/ui/HashTags.js +98 -0
  30. package/src/components/ui/ListItem.js +83 -0
  31. package/src/components/ui/Photo.js +188 -0
  32. package/src/components/ui/SemanticsTable.js +87 -0
  33. package/src/components/ui/index.js +3 -0
  34. package/src/components/ui/widgets/GeoSearch.js +13 -5
  35. package/src/img/osm.svg +49 -0
  36. package/src/img/wd.svg +1 -0
  37. package/src/translations/de.json +50 -3
  38. package/src/translations/en.json +23 -0
  39. package/src/translations/fr.json +21 -0
  40. package/src/translations/it.json +28 -1
  41. package/src/translations/nl.json +14 -4
  42. package/src/translations/zh_Hant.json +6 -1
  43. package/src/utils/PresetsManager.js +137 -0
  44. package/src/utils/URLHandler.js +1 -1
  45. package/src/utils/geocoder.js +135 -83
  46. package/src/utils/index.js +3 -1
  47. package/src/utils/picture.js +28 -0
  48. package/src/utils/semantics.js +162 -0
  49. package/src/utils/services.js +39 -1
  50. package/src/utils/widgets.js +18 -1
  51. package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +10 -0
  52. package/tests/components/core/__snapshots__/Viewer.test.js.snap +10 -0
  53. package/tests/data/Map_geocoder_nominatim.json +25 -40
  54. package/tests/utils/PresetsManager.test.js +123 -0
  55. package/tests/utils/URLHandler.test.js +42 -0
  56. package/tests/utils/__snapshots__/geocoder.test.js.snap +5 -16
  57. package/tests/utils/geocoder.test.js +1 -1
  58. package/tests/utils/semantics.test.js +125 -0
@@ -0,0 +1,151 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { faSvg, iconify } from "../styles";
3
+ import { fa, moreIcons } from "../../utils/widgets";
4
+ import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
5
+ import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft";
6
+ import "iconify-icon";
7
+
8
+ /**
9
+ * Annotations list shows listing of tagged picture areas.
10
+ * It uses the parent component currently selected picture.
11
+ *
12
+ * @class Panoramax.components.menus.AnnotationsList
13
+ * @element pnx-annotations-list
14
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
15
+ * @example
16
+ * ```html
17
+ * <pnx-annotations-list _parent=${viewer} />
18
+ * ```
19
+ */
20
+ export default class AnnotationsList extends LitElement {
21
+ /** @private */
22
+ static styles = [ faSvg, iconify, css`
23
+ .annotation { background: var(--white); }
24
+ ` ];
25
+
26
+ /** @private */
27
+ static properties = {
28
+ _meta: {state: true},
29
+ _selectedAnnotation: {state: true},
30
+ _presets: {state: true},
31
+ };
32
+
33
+ constructor() {
34
+ super();
35
+ this._selectedAnnotation = null;
36
+ this._presets = {};
37
+ moreIcons();
38
+ }
39
+
40
+ /** @private */
41
+ connectedCallback() {
42
+ super.connectedCallback();
43
+
44
+ this._onPicChange();
45
+ this._parent?.psv?.addEventListener("picture-loaded", this._onPicChange.bind(this));
46
+
47
+ this._parent?.psv?.addEventListener("annotation-click", e => {
48
+ const aPos = this._meta.properties?.annotations?.findIndex(a => a.id === e.detail.annotationId);
49
+ if(aPos >= 0) { this._onListItemClick(Object.assign({nb: aPos+1}, this._meta.properties.annotations[aPos])); }
50
+ });
51
+
52
+ this._parent?.psv?.addEventListener("annotations-unfocused", () => {
53
+ this._onListItemClick(null);
54
+ });
55
+ }
56
+
57
+ /** @private */
58
+ _onPicChange() {
59
+ this._meta = this._parent?.psv?.getPictureMetadata();
60
+ delete this._prevPsvView;
61
+ this._selectedAnnotation = null;
62
+
63
+ // Load presets for annotations
64
+ if(this._meta && this._parent?.presetsManager) {
65
+ this._presets = {};
66
+ if(this._meta.properties?.annotations?.length > 0) {
67
+ this._meta.properties.annotations.map(a => this._parent.presetsManager.getPreset(a).then(p => {
68
+ this._presets[a.id] = p;
69
+ this.requestUpdate();
70
+ }));
71
+ }
72
+ }
73
+ }
74
+
75
+ /** @private */
76
+ _onListItemHover(a) {
77
+ if(a) {
78
+ // Save position before hover to allow reset after
79
+ if(!this._prevPsvView) { this._prevPsvView = [this._parent.psv.getZoomLevel(), this._parent.psv.getPosition()]; }
80
+
81
+ this._parent.psv.focusOnAnnotation(a.id);
82
+ }
83
+ else {
84
+ this._parent.psv.unfocusAnnotation();
85
+
86
+ // Restore previous PSV position
87
+ if(this._prevPsvView) {
88
+ this._parent.psv.zoom(this._prevPsvView[0]);
89
+ this._parent.psv.rotate(this._prevPsvView[1]);
90
+ delete this._prevPsvView;
91
+ }
92
+ }
93
+ }
94
+
95
+ /** @private */
96
+ _onListItemClick(a) {
97
+ this._selectedAnnotation = a;
98
+ this._onListItemHover(a);
99
+ }
100
+
101
+ /** @private */
102
+ render() {
103
+ /* eslint-disable indent */
104
+ if(!this._meta) { return nothing; }
105
+
106
+ return this._selectedAnnotation === null
107
+ ? html`<div class="list">
108
+ ${this._meta.properties.annotations.map((a,i) => html`
109
+ <pnx-list-item
110
+ title=${
111
+ this._presets[a.id]?.name
112
+ || this._parent?._t.pnx.semantics_features_default_title.replace("{nb}", i+1)
113
+ }
114
+ subtitle=${this._parent?._t.pnx.semantics_features_subtitle.replace("{nb}", a.semantics.length)}
115
+ @click=${() => this._onListItemClick(Object.assign({nb: i+1}, a))}
116
+ @mouseover=${() => this._onListItemHover(Object.assign({nb: i+1}, a))}
117
+ @mouseout=${() => this._onListItemHover(null)}
118
+ >
119
+ <iconify-icon
120
+ slot="icon"
121
+ icon=${this._presets[a.id]?.iconify || "fa6-solid:cube"}
122
+ style="font-size: 1.5em"
123
+ ></iconify-icon>
124
+ ${fa(faChevronRight, {transform: {size: 24}, attributes: {slot: "action"}})}
125
+ </pnx-list-item>
126
+ `)}
127
+ </div>`
128
+ : html`<div class="annotation">
129
+ <pnx-list-item
130
+ title=${
131
+ this._presets[this._selectedAnnotation.id]?.name
132
+ || this._parent?._t.pnx.semantics_features_default_title.replace("{nb}", this._selectedAnnotation.nb)
133
+ }
134
+ @click=${() => this._onListItemClick(null)}
135
+ >
136
+ ${fa(faArrowLeft, {transform: {size: 24}, attributes: {slot: "icon"}})}
137
+ <iconify-icon
138
+ slot="icon"
139
+ icon=${this._presets[this._selectedAnnotation.id]?.iconify || "fa6-solid:cube"}
140
+ style="font-size: 1.5em"
141
+ ></iconify-icon>
142
+ </pnx-list-item>
143
+ <pnx-semantics-table
144
+ ._t=${this._parent?._t}
145
+ .source=${this._selectedAnnotation}
146
+ />
147
+ </div>`;
148
+ }
149
+ }
150
+
151
+ customElements.define("pnx-annotations-list", AnnotationsList);
@@ -8,7 +8,7 @@ import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
8
8
  import { faCalendarAlt } from "@fortawesome/free-solid-svg-icons/faCalendarAlt";
9
9
  import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation";
10
10
  import { faShareNodes } from "@fortawesome/free-solid-svg-icons/faShareNodes";
11
- import { placeholder, panel } from "../styles";
11
+ import { placeholder, panel, hidden } from "../styles";
12
12
  import { reverseGeocodingNominatim } from "../../utils/geocoder";
13
13
 
14
14
  /**
@@ -24,7 +24,7 @@ import { reverseGeocodingNominatim } from "../../utils/geocoder";
24
24
  */
25
25
  export default class PictureLegend extends LitElement {
26
26
  /** @private */
27
- static styles = [placeholder, panel, css`
27
+ static styles = [placeholder, panel, hidden, css`
28
28
  :host {
29
29
  overflow-y: auto;
30
30
  overflow-x: hidden;
@@ -38,8 +38,6 @@ export default class PictureLegend extends LitElement {
38
38
  pnx-picture-metadata { width: 30vw; }
39
39
  }
40
40
 
41
- .pnx-hidden { display: none !important; }
42
-
43
41
  /* Top bar */
44
42
  .headline {
45
43
  display: flex;
@@ -148,6 +146,9 @@ export default class PictureLegend extends LitElement {
148
146
  this._parent.psv.addEventListener("sequence-stopped", () => {
149
147
  this._onPicChange(this._parent.psv.getPictureMetadata());
150
148
  });
149
+ this._parent.psv.addEventListener("annotation-click", () => {
150
+ this._expanded = true;
151
+ });
151
152
  });
152
153
  }
153
154
 
@@ -225,7 +226,7 @@ export default class PictureLegend extends LitElement {
225
226
 
226
227
  ${this._caption.date ? html`<div class="info-block">
227
228
  ${fa(faCalendarAlt)}
228
- ${this._caption.date.toLocaleDateString(undefined, { year: "numeric", month: "long" })}
229
+ ${this._caption.date.toLocaleDateString(this._parent?.lang || window.navigator.language, { year: "numeric", month: "long" })}
229
230
  </div>` : nothing}
230
231
  </div>
231
232
 
@@ -9,7 +9,10 @@ import { faImages } from "@fortawesome/free-solid-svg-icons/faImages";
9
9
  import { faScroll } from "@fortawesome/free-solid-svg-icons/faScroll";
10
10
  import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
11
11
  import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
12
- import { titles, textarea } from "../styles";
12
+ import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
13
+ import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
14
+ import { faTags } from "@fortawesome/free-solid-svg-icons/faTags";
15
+ import { faSvg, titles, textarea, hidden } from "../styles";
13
16
  import { createWebComp } from "../../utils/widgets";
14
17
  import { getGPSPrecision } from "../../utils/picture";
15
18
  import {
@@ -31,7 +34,7 @@ const missing = () => fa(faQuestion, {styles: {height: "16px"}});
31
34
  */
32
35
  export default class PictureMetadata extends LitElement {
33
36
  /** @private */
34
- static styles = [ titles, textarea, css`
37
+ static styles = [ faSvg, titles, textarea, hidden, css`
35
38
  div[slot="content"] {
36
39
  padding: 5px 10px;
37
40
  background-color: #ededed;
@@ -56,20 +59,37 @@ export default class PictureMetadata extends LitElement {
56
59
  .data-block div {
57
60
  font-size: 0.8em;
58
61
  }
62
+
63
+ pnx-semantics-table {
64
+ overflow-x: auto;
65
+ display: block;
66
+ }
59
67
  ` ];
60
68
 
61
69
  /** @private */
62
70
  static properties = {
63
71
  _meta: {state: true},
72
+ _semanticsPicShowAll: {state: true},
64
73
  };
65
74
 
75
+ constructor() {
76
+ super();
77
+ this._semanticsPicShowAll = false;
78
+ }
79
+
66
80
  /** @private */
67
81
  connectedCallback() {
68
82
  super.connectedCallback();
69
83
 
70
84
  this._meta = this._parent?.psv?.getPictureMetadata();
71
- this._parent?.psv?.addEventListener("picture-loaded", () => {
72
- this._meta = this._parent?.psv?.getPictureMetadata();
85
+ this._parent?.oncePSVReady?.().then(() => {
86
+ this._parent.psv.addEventListener("picture-loaded", () => {
87
+ this._meta = this._parent.psv.getPictureMetadata();
88
+ });
89
+ this._parent.psv.addEventListener("annotation-click", () => {
90
+ const tabs = this.shadowRoot.querySelector("pnx-tabs");
91
+ if(tabs) { tabs.setAttribute("activeTabIndex", 4); }
92
+ });
73
93
  });
74
94
  }
75
95
 
@@ -99,6 +119,8 @@ export default class PictureMetadata extends LitElement {
99
119
  /* eslint-disable indent */
100
120
  if(!this._meta) { return nothing; }
101
121
 
122
+ const lang = this._parent?.lang || window.navigator.language;
123
+
102
124
  // Generic information
103
125
  const persOrient = this._meta?.properties?.["pers:interior_orientation"];
104
126
  const makeModel = [persOrient.camera_manufacturer, persOrient.camera_model].filter(v => v).join(" ");
@@ -127,11 +149,11 @@ export default class PictureMetadata extends LitElement {
127
149
  this._meta?.caption?.date && {
128
150
  title: this._parent?._t.pnx.metadata_general_date,
129
151
  content: html`
130
- <strong>${new Intl.DateTimeFormat(undefined, {
152
+ <strong>${new Intl.DateTimeFormat(lang, {
131
153
  timeZone: this._meta.caption.tz,
132
154
  dateStyle: "short"
133
155
  }).format(this._meta.caption.date)}</strong>
134
- <br />${new Intl.DateTimeFormat(undefined, {
156
+ <br />${new Intl.DateTimeFormat(lang, {
135
157
  timeZone: this._meta.caption.tz,
136
158
  hour: "numeric",
137
159
  minute: "numeric",
@@ -205,6 +227,51 @@ export default class PictureMetadata extends LitElement {
205
227
  ];
206
228
  }
207
229
 
230
+ // Semantics data
231
+ const hasSemantics = (
232
+ (this._meta.properties.semantics || []).length > 0
233
+ || (this._meta.properties.annotations || []).length > 0
234
+ );
235
+ let semanticsData = [];
236
+ if(hasSemantics) {
237
+ // Full list of picture tags
238
+ semanticsData.push({
239
+ title: this._parent?._t.pnx.semantics_tags_picture,
240
+ style: "width: 100%",
241
+ content: html`${this._meta.properties.semantics?.length > 0
242
+ ? html`
243
+ <pnx-button
244
+ kind="outline"
245
+ size="sm"
246
+ style="width: 100%"
247
+ @click=${() => this._semanticsPicShowAll = !this._semanticsPicShowAll}
248
+ >
249
+ ${this._semanticsPicShowAll ? fa(faChevronUp) : fa(faChevronDown)}
250
+ ${this._semanticsPicShowAll ? this._parent?._t.pnx.semantics_hide_all_tags : this._parent?._t.pnx.semantics_show_all_tags}
251
+ </pnx-button>
252
+ <pnx-semantics-table
253
+ ._t=${this._parent?._t}
254
+ .source=${this._meta.properties}
255
+ style="margin-top: 5px"
256
+ class=${this._semanticsPicShowAll ? "":"pnx-hidden"}
257
+ />
258
+ `
259
+ : this._parent?._t.pnx.semantics_tags_picture_none
260
+ }`
261
+ });
262
+
263
+ // Annotations (features in picture)
264
+ semanticsData.push({
265
+ title: this._parent?._t.pnx.semantics_features,
266
+ style: "width: 100%",
267
+ content: html`
268
+ ${this._meta.properties.annotations?.length > 0
269
+ ? html`<pnx-annotations-list ._parent=${this._parent} />`
270
+ : this._parent?._t.pnx.semantics_features_none}
271
+ `
272
+ });
273
+ }
274
+
208
275
  // EXIF data
209
276
  const exifData = Object.entries(this._meta.properties.exif)
210
277
  .sort()
@@ -247,8 +314,8 @@ export default class PictureMetadata extends LitElement {
247
314
  this._meta?.caption?.date && {
248
315
  title: this._parent?._t.pnx.metadata_general_date,
249
316
  content: html`
250
- <strong>${new Intl.DateTimeFormat(undefined, {timeZone: this._meta.caption.tz, dateStyle: "long"}).format(this._meta.caption.date)}</strong>
251
- <br />${new Intl.DateTimeFormat(undefined, {timeZone: this._meta.caption.tz, hour: "numeric",minute:"numeric"}).format(this._meta.caption.date)}
317
+ <strong>${new Intl.DateTimeFormat(lang, {timeZone: this._meta.caption.tz, dateStyle: "long"}).format(this._meta.caption.date)}</strong>
318
+ <br />${new Intl.DateTimeFormat(lang, {timeZone: this._meta.caption.tz, hour: "numeric",minute:"numeric"}).format(this._meta.caption.date)}
252
319
  `
253
320
  },
254
321
  // Camera
@@ -327,6 +394,11 @@ export default class PictureMetadata extends LitElement {
327
394
  qualityData
328
395
  ) : nothing}
329
396
 
397
+ ${hasSemantics ? this._toTab( // Semantics
398
+ html`${fa(faTags)} ${this._parent?._t.pnx.semantics_title}`,
399
+ semanticsData
400
+ ) : nothing}
401
+
330
402
  ${this._meta.properties?.exif ? this._toTab( // EXIF
331
403
  html`${fa(faScroll)} ${this._parent?._t.pnx.metadata_exif}`,
332
404
  exifData
@@ -3,6 +3,7 @@
3
3
  * @module Panoramax:components:menus
4
4
  */
5
5
 
6
+ export {default as AnnotationsList} from "./AnnotationsList";
6
7
  export {default as MapFilters} from "./MapFilters";
7
8
  export {default as MapLayers} from "./MapLayers";
8
9
  export {default as MapBackground} from "./MapBackground";
@@ -27,6 +27,11 @@ export const panel = css`
27
27
  }
28
28
  `;
29
29
 
30
+ // Hidden
31
+ export const hidden = css`
32
+ .pnx-hidden { display: none !important; }
33
+ `;
34
+
30
35
  // Font Awesome SVG
31
36
  export const faSvg = css`
32
37
  .svg-inline--fa {
@@ -442,3 +447,32 @@ export const noprint = css`
442
447
  .pnx-print-hidden { display: none !important; }
443
448
  }
444
449
  `;
450
+
451
+ // Data table
452
+ export const table = css`
453
+ table {
454
+ border-collapse: collapse;
455
+ font-size: 0.9rem;
456
+ width: 100%;
457
+ max-width: 100%;
458
+ font-family: var(--font-family);
459
+ background: var(--white);
460
+ }
461
+
462
+ th, td {
463
+ padding: 10px;
464
+ text-align: left;
465
+ border-bottom: 1px solid #ddd;
466
+ }
467
+
468
+ th { font-weight: 600; }
469
+ `;
470
+
471
+ // Iconify icons
472
+ export const iconify = css`
473
+ iconify-icon {
474
+ display: inline-block;
475
+ width: 1em;
476
+ height: 1em;
477
+ }
478
+ `;
@@ -0,0 +1,98 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { getHashTags, hasAnnotations } from "../../utils/picture";
3
+ import { fa } from "../../utils/widgets";
4
+ import { faDrawPolygon } from "@fortawesome/free-solid-svg-icons/faDrawPolygon";
5
+
6
+ /**
7
+ * HashTags component shows the list of hashtags associated to a picture.
8
+ * @class Panoramax.components.ui.HashTags
9
+ * @element pnx-hashtags
10
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
11
+ * @example
12
+ * ```html
13
+ * <pnx-hashtags ._parent=${viewer} />
14
+ * ```
15
+ */
16
+ export default class HashTags extends LitElement {
17
+ /** @private */
18
+ static styles = css`
19
+ div {
20
+ background: linear-gradient(to bottom left, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.2));
21
+ margin-top: -10px;
22
+ margin-right: -10px;
23
+ padding: 5px 10px;
24
+ border-bottom-left-radius: 10px;
25
+ font-family: var(--font-family);
26
+ color: white;
27
+ font-size: 0.8em;
28
+ }
29
+ `;
30
+
31
+ /** @private */
32
+ static properties = {
33
+ _tags: {state: true},
34
+ _visible: {state: true},
35
+ _annotationsToggled: {state: true},
36
+ };
37
+
38
+ constructor() {
39
+ super();
40
+ this._tags = [];
41
+ this._visible = false;
42
+ this._annotationsToggled = false;
43
+ }
44
+
45
+ /** @private */
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+
49
+ this._parent.onceReady().then(() => {
50
+ this._tags = getHashTags(this._parent?.psv?.getPictureMetadata?.());
51
+
52
+ // Component visibility : only if seen at least one pic with semantics
53
+ if(
54
+ !this._visible && (
55
+ this._tags.length > 0
56
+ || hasAnnotations(this._parent?.psv?.getPictureMetadata?.())
57
+ )
58
+ ) {
59
+ this._visible = true;
60
+ this._parent.psv.toggleAllAnnotations(true);
61
+ }
62
+
63
+ this._parent.psv.addEventListener("picture-loaded", () => {
64
+ this._tags = getHashTags(this._parent.psv.getPictureMetadata());
65
+ if(
66
+ !this._visible && (
67
+ this._tags.length > 0 ||
68
+ hasAnnotations(this._parent.psv.getPictureMetadata())
69
+ )
70
+ ) {
71
+ this._visible = true;
72
+ this._parent.psv.toggleAllAnnotations(true);
73
+ }
74
+ });
75
+
76
+ this._annotationsToggled = this._parent.psv.areAnnotationsVisible() || false;
77
+ this._parent.psv.addEventListener("annotations-toggled", e => {
78
+ this._annotationsToggled = e.detail.visible;
79
+ });
80
+ });
81
+ }
82
+
83
+ /** @private */
84
+ render() {
85
+ return this._visible ? html`<div>
86
+ ${this._tags.join(" ")}
87
+ <pnx-button
88
+ kind="outline"
89
+ style="vertical-align: middle"
90
+ title=${this._annotationsToggled ? this._parent._t?.pnx.semantics_hide_annotations : this._parent._t?.pnx.semantics_show_annotations}
91
+ active=${this._annotationsToggled ? "" : nothing}
92
+ @click=${() => this._parent.psv.toggleAllAnnotations(!this._annotationsToggled)}
93
+ >${fa(faDrawPolygon)}</pnx-button>
94
+ </div>` : nothing;
95
+ }
96
+ }
97
+
98
+ customElements.define("pnx-hashtags", HashTags);
@@ -0,0 +1,83 @@
1
+ import { LitElement, html, css } from "lit";
2
+
3
+ /**
4
+ * ListItem is a list entry, in a Material Design fashion.
5
+ * @class Panoramax.components.ui.ListItem
6
+ * @element pnx-list-item
7
+ * @slot `icon` The left icon (symbol for this item)
8
+ * @slot `action` The right icon (symbol for an interactive action)
9
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
10
+ * @example
11
+ * ```html
12
+ * <pnx-list-item title="My feature" subtitle="It is very cool">
13
+ * <img src="..." slot="icon" />
14
+ * <img src="..." slot="action" />
15
+ * </pnx-list-item>
16
+ * ```
17
+ */
18
+ export default class ListItem extends LitElement {
19
+ /** @private */
20
+ static styles = css`
21
+ .list-item {
22
+ display: flex;
23
+ align-items: center;
24
+ padding: 8px 16px;
25
+ cursor: pointer;
26
+ border-bottom: 1px solid #ddd;
27
+ font-family: var(--font-family);
28
+ background: var(--white);
29
+ min-height: 50px;
30
+ box-sizing: border-box;
31
+ }
32
+ .list-item:hover { background: var(--widget-bg-hover); }
33
+ .icon {
34
+ margin-right: 16px;
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 10px;
38
+ }
39
+ .action { margin-left: 16px; }
40
+ .content {
41
+ flex: 1;
42
+ }
43
+ .title {
44
+ font-weight: 600;
45
+ }
46
+ .subtitle {
47
+ font-size: 0.9em;
48
+ color: var(--grey-dark);
49
+ }
50
+ `;
51
+
52
+ /**
53
+ * Component properties.
54
+ * @memberof Panoramax.components.ui.ListItem#
55
+ * @type {Object}
56
+ * @property {string} title The item title
57
+ * @property {string} [subtitle] The item subtitle
58
+ */
59
+ static properties = {
60
+ title: { type: String },
61
+ subtitle: { type: String },
62
+ };
63
+
64
+ /** @private */
65
+ render() {
66
+ return html`
67
+ <div class="list-item">
68
+ <div class="icon">
69
+ <slot name="icon"></slot>
70
+ </div>
71
+ <div class="content">
72
+ <div class="title">${this.title}</div>
73
+ <div class="subtitle">${this.subtitle}</div>
74
+ </div>
75
+ <div class="action">
76
+ <slot name="action"></slot>
77
+ </div>
78
+ </div>
79
+ `;
80
+ }
81
+ }
82
+
83
+ customElements.define("pnx-list-item", ListItem);