@panoramax/web-viewer 3.2.3-develop-6e69906d → 3.2.3-develop-8b82a4e5

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 (61) hide show
  1. package/build/index.css +2 -2
  2. package/build/index.css.map +1 -1
  3. package/build/index.js +535 -216
  4. package/build/index.js.map +1 -1
  5. package/build/widgets.html +1 -1
  6. package/docs/reference/components/core/PhotoViewer.md +2 -0
  7. package/docs/reference/components/core/Viewer.md +2 -0
  8. package/docs/reference/components/layout/BottomDrawer.md +35 -0
  9. package/docs/reference/components/layout/Tabs.md +45 -0
  10. package/docs/reference/components/menus/PictureLegend.md +1 -0
  11. package/docs/reference/components/ui/Button.md +3 -2
  12. package/docs/reference/components/ui/CopyButton.md +7 -4
  13. package/docs/reference/components/ui/LinkButton.md +1 -0
  14. package/docs/reference/components/ui/ListGroup.md +22 -0
  15. package/docs/reference/components/ui/widgets/Legend.md +11 -0
  16. package/docs/reference/components/ui/widgets/OSMEditors.md +15 -0
  17. package/docs/reference/components/ui/widgets/PictureLegendActions.md +32 -0
  18. package/docs/reference.md +6 -2
  19. package/mkdocs.yml +5 -1
  20. package/package.json +1 -1
  21. package/public/widgets.html +45 -9
  22. package/src/components/core/Basic.css +1 -0
  23. package/src/components/core/PhotoViewer.css +0 -23
  24. package/src/components/core/PhotoViewer.js +41 -22
  25. package/src/components/core/Viewer.css +6 -31
  26. package/src/components/core/Viewer.js +40 -11
  27. package/src/components/layout/BottomDrawer.js +204 -0
  28. package/src/components/layout/CorneredGrid.js +3 -0
  29. package/src/components/layout/Tabs.js +133 -0
  30. package/src/components/layout/index.js +2 -0
  31. package/src/components/menus/PictureLegend.js +162 -23
  32. package/src/components/menus/PictureMetadata.js +220 -110
  33. package/src/components/menus/Share.js +2 -142
  34. package/src/components/styles.js +47 -47
  35. package/src/components/ui/Button.js +4 -2
  36. package/src/components/ui/CopyButton.js +34 -5
  37. package/src/components/ui/LinkButton.js +6 -7
  38. package/src/components/ui/ListGroup.js +66 -0
  39. package/src/components/ui/Map.js +4 -1
  40. package/src/components/ui/QualityScore.js +19 -24
  41. package/src/components/ui/TogglableGroup.js +47 -53
  42. package/src/components/ui/index.js +1 -0
  43. package/src/components/ui/widgets/Legend.js +29 -6
  44. package/src/components/ui/widgets/OSMEditors.js +153 -0
  45. package/src/components/ui/widgets/PictureLegendActions.js +131 -0
  46. package/src/components/ui/widgets/index.js +5 -4
  47. package/src/translations/en.json +14 -8
  48. package/src/translations/fr.json +14 -8
  49. package/src/utils/InitParameters.js +2 -1
  50. package/src/utils/geocoder.js +3 -1
  51. package/src/utils/picture.js +1 -2
  52. package/src/utils/widgets.js +5 -43
  53. package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +12 -32
  54. package/tests/components/core/__snapshots__/Viewer.test.js.snap +5 -25
  55. package/tests/components/ui/__snapshots__/Photo.test.js.snap +6 -2
  56. package/tests/utils/InitParameters.test.js +7 -9
  57. package/tests/utils/__snapshots__/picture.test.js.snap +13 -4
  58. package/tests/utils/picture.test.js +2 -2
  59. package/tests/utils/widgets.test.js +0 -59
  60. package/docs/reference/components/ui/widgets/Share.md +0 -15
  61. package/src/components/ui/widgets/Share.js +0 -30
@@ -7,6 +7,7 @@ import URLHandler from "../../utils/URLHandler";
7
7
  import Basic from "./Basic";
8
8
  import Photo, { PSV_DEFAULT_ZOOM, PSV_ANIM_DURATION } from "../ui/Photo";
9
9
  import { createWebComp } from "../../utils/widgets";
10
+ import { isNullId } from "../../utils/utils";
10
11
  import { default as InitParameters, alterPSVState, alterMapState, alterPhotoViewerState } from "../../utils/InitParameters";
11
12
 
12
13
 
@@ -40,6 +41,7 @@ const PSV_MOVE_DELTA = Math.PI / 6;
40
41
  * @slot `bottom-left` The bottom-left corner
41
42
  * @slot `bottom` The bottom middle corner
42
43
  * @slot `bottom-right` The bottom-right corner
44
+ * @slot `editors` External links to map editors, or any tool that may be helpful. Defaults to OSM tools (iD & JOSM).
43
45
  * @example
44
46
  * ```html
45
47
  * <!-- Basic example -->
@@ -54,6 +56,7 @@ const PSV_MOVE_DELTA = Math.PI / 6;
54
56
  * style="width: 300px; height: 250px"
55
57
  * >
56
58
  * <p slot="top-right">My custom text</p>
59
+ * <p slot="editors"><a href="https://my.own.tool/">Edit in my own tool</a></p>
57
60
  * </pnx-photo-viewer>
58
61
  *
59
62
  * <!-- With only your custom widgets -->
@@ -124,29 +127,41 @@ export default class PhotoViewer extends Basic {
124
127
  /** @private */
125
128
  _initWidgets() {
126
129
  if(this._initParams.getParentPostInit().widgets !== "false") {
130
+ this.grid.appendChild(createWebComp("pnx-widget-player", {
131
+ slot: "top",
132
+ _parent: this,
133
+ class: "pnx-only-psv pnx-print-hidden",
134
+ size: this.isHeightSmall() ? "md": "xl",
135
+ }));
136
+
127
137
  if(!this.isWidthSmall()) {
138
+ this.legend = createWebComp("pnx-widget-legend", {
139
+ slot: !this.isWidthSmall() ? "top-left" : undefined,
140
+ _parent: this,
141
+ focus: this._initParams.getParentPostInit().focus,
142
+ picture: this._initParams.getParentPostInit().picture,
143
+ });
128
144
  this.grid.appendChild(createWebComp("pnx-widget-zoom", {
129
145
  slot: "bottom-right",
130
146
  class: "pnx-print-hidden",
131
147
  _parent: this
132
148
  }));
149
+ this.grid.appendChild(this.legend);
150
+ }
151
+ else {
152
+ this.legend = createWebComp("pnx-picture-legend", { _parent: this });
153
+ this.bottomDrawer = createWebComp("pnx-bottom-drawer", {
154
+ slot: "bottom",
155
+ _parent: this,
156
+ class: this._initParams.getParentPostInit().picture ? undefined: "pnx-hidden",
157
+ });
158
+ this.bottomDrawer.appendChild(this.legend);
159
+ this.grid.appendChild(this.bottomDrawer);
160
+ this.addEventListener("select", e => {
161
+ if(isNullId(e.detail.picId)) { this.bottomDrawer.classList.add("pnx-hidden"); }
162
+ else { this.bottomDrawer.classList.remove("pnx-hidden"); }
163
+ });
133
164
  }
134
-
135
- this.grid.appendChild(createWebComp("pnx-widget-share", {slot: "bottom-right", class: "pnx-print-hidden", _parent: this}));
136
-
137
- this.legend = createWebComp("pnx-widget-legend", {
138
- slot: this.isWidthSmall() ? "top" : "top-left",
139
- _parent: this,
140
- focus: this._initParams.getParentPostInit().focus,
141
- picture: this._initParams.getParentPostInit().picture,
142
- });
143
- this.grid.appendChild(this.legend);
144
- this.grid.appendChild(createWebComp("pnx-widget-player", {
145
- slot: "top",
146
- _parent: this,
147
- class: "pnx-only-psv pnx-print-hidden",
148
- size: this.isHeightSmall() ? "md": "xl",
149
- }));
150
165
  }
151
166
  }
152
167
 
@@ -323,7 +338,14 @@ export default class PhotoViewer extends Basic {
323
338
  n._parent = this;
324
339
  n._t = this._t;
325
340
  }
326
- this.grid.appendChild(n);
341
+ // Editors slot -> legend
342
+ if(n.getAttribute("slot") === "editors") {
343
+ this.onceReady().then(() => this.legend.appendChild(n));
344
+ }
345
+ // Add to grid for other cases
346
+ else {
347
+ this.grid.appendChild(n);
348
+ }
327
349
  }
328
350
  }
329
351
  }
@@ -357,14 +379,11 @@ export default class PhotoViewer extends Basic {
357
379
  _showReportForm() {
358
380
  if(!this.psv.getPictureMetadata()) { throw new Error("No picture currently selected"); }
359
381
  this.setPopup(true, [createWebComp("pnx-report-form", {_parent: this})]);
360
- this.dispatchEvent(new CustomEvent("focus-changed", { detail: { focus: "meta" } }));
361
382
  }
362
383
 
363
384
  /** @private */
364
- _showPictureMetadata() {
365
- if(!this.psv.getPictureMetadata()) { throw new Error("No picture currently selected"); }
366
- this.setPopup(true, [createWebComp("pnx-picture-metadata", {_parent: this})]);
367
- this.dispatchEvent(new CustomEvent("focus-changed", { detail: { focus: "meta" } }));
385
+ _showShareOptions() {
386
+ this.setPopup(true, [createWebComp("pnx-share-menu", {_parent: this})]);
368
387
  }
369
388
 
370
389
  /**
@@ -39,17 +39,18 @@ pnx-viewer pnx-cornered-grid::part(corner-bottom-right) {
39
39
  pnx-viewer pnx-mini {
40
40
  box-sizing: border-box;
41
41
  aspect-ratio: 1/1;
42
- width: 250px;
43
- height: 250px;
42
+ min-width: 250px;
43
+ min-height: 250px;
44
44
  position: relative;
45
45
  display: block;
46
46
  z-index: 120;
47
47
  }
48
48
 
49
- @media screen and (max-width: 576px) {
49
+ @media screen and ((max-height: 400px) or (max-width: 576px)) {
50
50
  pnx-viewer pnx-mini {
51
- width: unset;
52
- height: unset;
51
+ min-width: unset;
52
+ min-height: unset;
53
+ margin-bottom: 40px;
53
54
  }
54
55
  }
55
56
 
@@ -65,32 +66,6 @@ pnx-viewer:not(pnx-viewer[focus="pic"]) .pnx-only-psv {
65
66
  }
66
67
 
67
68
  /* Override legend positioning */
68
- @media screen and (max-width: 576px) {
69
- pnx-viewer[focus="map"] pnx-widget-legend {
70
- display: none;
71
- }
72
-
73
- pnx-viewer[focus="pic"] pnx-widget-legend {
74
- position: fixed;
75
- top: 0;
76
- left: 0;
77
- right: 0;
78
- }
79
-
80
- pnx-viewer[focus="pic"] pnx-widget-legend::part(panel) {
81
- border-radius: 0;
82
- border-top: none;
83
- border-left: none;
84
- border-right: none;
85
- width: unset;
86
- max-width: unset;
87
- }
88
-
89
- pnx-viewer[focus="pic"] pnx-widget-player {
90
- margin-top: 60px;
91
- }
92
- }
93
-
94
69
  @media screen and (min-width: 576px) {
95
70
  pnx-viewer pnx-widget-legend {
96
71
  position: absolute;
@@ -49,6 +49,7 @@ const MAP_MOVE_DELTA = 100;
49
49
  * @slot `bottom-left` The bottom-left corner
50
50
  * @slot `bottom` The bottom middle corner
51
51
  * @slot `bottom-right` The bottom-right corner
52
+ * @slot `editors` External links to map editors, or any tool that may be helpful. Defaults to OSM tools (iD & JOSM).
52
53
  * @example
53
54
  * ```html
54
55
  * <!-- Basic example -->
@@ -63,6 +64,7 @@ const MAP_MOVE_DELTA = 100;
63
64
  * style="width: 300px; height: 250px"
64
65
  * >
65
66
  * <p slot="top-right">My custom text</p>
67
+ * <p slot="editors"><a href="https://my.own.tool/">Edit in my own tool</a></p>
66
68
  * </pnx-viewer>
67
69
  *
68
70
  * <!-- With only your custom widgets -->
@@ -145,15 +147,31 @@ export default class Viewer extends PhotoViewer {
145
147
  class: this.isWidthSmall() ? "pnx-only-map pnx-print-hidden" : "pnx-print-hidden",
146
148
  _parent: this
147
149
  }));
148
- this.grid.appendChild(createWebComp("pnx-widget-share", {slot: "bottom-right", class: "pnx-print-hidden", _parent: this}));
149
150
 
150
- this.legend = createWebComp("pnx-widget-legend", {
151
- slot: this.isWidthSmall() ? "top" : "top-left",
152
- _parent: this,
153
- focus: this._initParams.getParentPostInit().focus,
154
- picture: this._initParams.getParentPostInit().picture,
155
- });
156
- this.grid.appendChild(this.legend);
151
+ if(!this.isWidthSmall()) {
152
+ this.legend = createWebComp("pnx-widget-legend", {
153
+ slot: this.isWidthSmall() ? "top" : "top-left",
154
+ _parent: this,
155
+ focus: this._initParams.getParentPostInit().focus,
156
+ picture: this._initParams.getParentPostInit().picture,
157
+ });
158
+ this.grid.appendChild(this.legend);
159
+ }
160
+ else {
161
+ this.legend = createWebComp("pnx-picture-legend", { _parent: this });
162
+ this.bottomDrawer = createWebComp("pnx-bottom-drawer", {
163
+ slot: "bottom",
164
+ _parent: this,
165
+ class: this._initParams.getParentPostInit().picture ? undefined: "pnx-hidden",
166
+ });
167
+ this.bottomDrawer.appendChild(this.legend);
168
+ this.grid.appendChild(this.bottomDrawer);
169
+ this.addEventListener("select", e => {
170
+ if(isNullId(e.detail.picId)) { this.bottomDrawer.classList.add("pnx-hidden"); }
171
+ else { this.bottomDrawer.classList.remove("pnx-hidden"); }
172
+ });
173
+ }
174
+
157
175
  this.grid.appendChild(createWebComp("pnx-widget-player", {
158
176
  slot: "top",
159
177
  _parent: this,
@@ -196,7 +214,10 @@ export default class Viewer extends PhotoViewer {
196
214
  this._handleKeyboardManagement();
197
215
 
198
216
  if(myPostInitParams.picture) {
199
- this.psv.addEventListener("picture-loaded", () => this.loader.dismiss(), {once: true});
217
+ this.psv.addEventListener("picture-loaded", () => {
218
+ alterViewerState(this, myPostInitParams); // Do it again for forcing focus
219
+ this.loader.dismiss();
220
+ }, {once: true});
200
221
  }
201
222
  else {
202
223
  this.loader.dismiss();
@@ -236,8 +257,16 @@ export default class Viewer extends PhotoViewer {
236
257
  if(isNullId(old) && !isNullId(value)) {
237
258
  this.mini.removeAttribute("collapsed");
238
259
  }
239
- if(isNullId(value) && this.map && this.isMapWide()) {
240
- this.mini.classList.add("pnx-hidden");
260
+
261
+ // Unselect -> show map wide instead
262
+ if(isNullId(value)) {
263
+ if(this.map && this.isMapWide()) { this.mini.classList.add("pnx-hidden"); }
264
+ else if(this.map && !this.isMapWide()) { this._setFocus("map"); }
265
+ }
266
+ // Select after none selected -> show pic wide
267
+ else {
268
+ this.mini.classList.remove("pnx-hidden");
269
+ if(isNullId(old)) { this._setFocus("pic"); }
241
270
  }
242
271
  }
243
272
 
@@ -0,0 +1,204 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { classMap } from "lit/directives/class-map.js";
3
+
4
+ const OPENNESS_Y_PCT = { "opened": 0, "half-opened": 0.7, "closed": 1 };
5
+ const OPENNESS_Y_PCT_RANGE = { "opened": [0, 0.4], "half-opened": [0.4, 0.8], "closed": [0.8, 1] };
6
+
7
+ /**
8
+ * BottomDrawer layout offers a mobile-like drawer menu, coming from bottom of the screen.
9
+ * @class Panoramax.components.layout.BottomDrawer
10
+ * @element pnx-bottom-drawer
11
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
12
+ * @slot `default` The drawer content
13
+ * @example
14
+ * ```html
15
+ * <pnx-bottom-drawer openness="opened">
16
+ * <p>My drawer content</p>
17
+ * </pnx-bottom-drawer>
18
+ * ```
19
+ */
20
+ export default class BottomDrawer extends LitElement {
21
+ /** @private */
22
+ static styles = css`
23
+ :host {
24
+ position: fixed;
25
+ bottom: 0;
26
+ left: 0;
27
+ width: 100%;
28
+ z-index: 130;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .drawer {
33
+ background: var(--widget-bg);
34
+ border-top-left-radius: 16px;
35
+ border-top-right-radius: 16px;
36
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
37
+ display: flex;
38
+ flex-direction: column;
39
+ transition: transform 0.3s ease;
40
+ will-change: transform;
41
+ touch-action: none;
42
+ pointer-events: auto;
43
+ }
44
+
45
+ .drawer.dragging { transition: none; }
46
+ .drawer.closed { transform: translateY(calc(100% - 30px)); }
47
+ .drawer.half-opened { transform: translateY(70%); }
48
+ .drawer.half-opened .content { overflow-y: hidden; }
49
+ .drawer.opened { transform: translateY(0%); }
50
+
51
+ .handle {
52
+ height: 30px;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ cursor: grab;
57
+ touch-action: none;
58
+ pointer-events: auto;
59
+ }
60
+
61
+ .handle-bar {
62
+ width: 40px;
63
+ height: 5px;
64
+ background: var(--grey-pale);
65
+ border-radius: 3px;
66
+ }
67
+
68
+ .content {
69
+ overflow: auto;
70
+ flex: 1;
71
+ }
72
+ `;
73
+
74
+ /**
75
+ * Component properties.
76
+ * @memberof Panoramax.components.layout.BottomDrawer#
77
+ * @type {Object}
78
+ * @property {string} [openness=half-opened] How much the drawer should be opened (closed, half-opened, opened)
79
+ */
80
+ static properties = {
81
+ _fingerY: {state: true},
82
+ _deltaFingerY: {state: true},
83
+ _drawerY: {state: true},
84
+ _isDragging: {state: true},
85
+ _drawerHeight: {state: true},
86
+ openness: {type: String, reflect: true},
87
+ };
88
+
89
+ constructor() {
90
+ super();
91
+ this._isDragging = false;
92
+ this.openness = "half-opened";
93
+ }
94
+
95
+ /** @private */
96
+ firstUpdated() {
97
+ super.firstUpdated();
98
+ this._boundTouchMove = this._onTouchMove.bind(this);
99
+ this._boundTouchEnd = this._onTouchEnd.bind(this);
100
+
101
+ this._drawerHeight = window.innerHeight - 30;
102
+ const drawer = this.shadowRoot?.querySelector(".drawer");
103
+ if (!drawer) { return; }
104
+ drawer.style.height = `${this._drawerHeight}px`;
105
+ drawer.style.maxHeight = `${this._drawerHeight}px`;
106
+ }
107
+
108
+ /** @private */
109
+ disconnectedCallback() {
110
+ super.disconnectedCallback();
111
+ this._cleanupTouchListeners();
112
+ }
113
+
114
+ /** @private */
115
+ _onHandleClick() {
116
+ if(this.openness === "opened" || this.openness === "closed") { this.openness = "half-opened"; }
117
+ else if(this.openness === "half-opened") { this.openness = "opened"; }
118
+ }
119
+
120
+ /** @private */
121
+ _onTouchStart(e) {
122
+ this._isDragging = true;
123
+ this._startFingerY = e.touches[0].clientY;
124
+ this._deltaFingerY = 0;
125
+ this._drawerY = this._drawerHeight * OPENNESS_Y_PCT[this.openness];
126
+ window.addEventListener("touchmove", this._boundTouchMove, { passive: true });
127
+ window.addEventListener("touchend", this._boundTouchEnd);
128
+ window.addEventListener("touchcancel", this._boundTouchEnd);
129
+ }
130
+
131
+ /** @private */
132
+ _onTouchMove(e) {
133
+ if (!this._isDragging) return;
134
+ this._deltaFingerY = e.touches[0].clientY - this._startFingerY;
135
+ this._updateDrawerTransform(this._drawerY + this._deltaFingerY);
136
+ }
137
+
138
+ /** @private */
139
+ _onTouchEnd() {
140
+ if (!this._isDragging) return;
141
+ this._isDragging = false;
142
+ this._updateDrawerTransform(this._drawerY + this._deltaFingerY, true);
143
+
144
+ this._cleanupTouchListeners();
145
+ this._startFingerY = null;
146
+ this._deltaFingerY = null;
147
+ }
148
+
149
+ /** @private */
150
+ _cleanupTouchListeners() {
151
+ window.removeEventListener("touchmove", this._boundTouchMove);
152
+ window.removeEventListener("touchend", this._boundTouchEnd);
153
+ window.removeEventListener("touchcancel", this._boundTouchCancel);
154
+ }
155
+
156
+ /** @private */
157
+ _updateDrawerTransform(y, snap = false) {
158
+ const drawer = this.shadowRoot?.querySelector(".drawer");
159
+ if (!drawer) { return; }
160
+
161
+ y = Math.max(0, Math.min(y, this._drawerHeight - 30));
162
+
163
+ // Snap to nearest static position
164
+ if(snap) {
165
+ const pct = y / (this._drawerHeight - 30);
166
+ if(pct > OPENNESS_Y_PCT_RANGE.opened[0] && pct <= OPENNESS_Y_PCT_RANGE.opened[1]) { this.openness = "opened"; }
167
+ if(pct > OPENNESS_Y_PCT_RANGE["half-opened"][0] && pct <= OPENNESS_Y_PCT_RANGE["half-opened"][1]) { this.openness = "half-opened"; }
168
+ if(pct > OPENNESS_Y_PCT_RANGE.closed[0] && pct <= OPENNESS_Y_PCT_RANGE.closed[1]) { this.openness = "closed"; }
169
+ this._drawerY = null;
170
+ drawer.style.transform = null;
171
+ }
172
+ // Live drag
173
+ else {
174
+ drawer.style.transform = `translateY(${y}px)`;
175
+ }
176
+ }
177
+
178
+ /** @private */
179
+ render() {
180
+ const classes = {
181
+ "drawer": true,
182
+ [this.openness]: true,
183
+ "dragging": this._isDragging,
184
+ };
185
+
186
+ return html`
187
+ <div
188
+ class=${classMap(classes)}
189
+ @touchstart="${this._onTouchStart}"
190
+ @touchmove="${this._onTouchMove}"
191
+ @touchend="${this._onTouchEnd}"
192
+ >
193
+ <div class="handle" @click=${this._onHandleClick}>
194
+ <div class="handle-bar"></div>
195
+ </div>
196
+ <div class="content">
197
+ <slot></slot>
198
+ </div>
199
+ </div>
200
+ `;
201
+ }
202
+ }
203
+
204
+ customElements.define("pnx-bottom-drawer", BottomDrawer);
@@ -50,11 +50,14 @@ export default class CorneredGrid extends LitElement {
50
50
  pointer-events: none;
51
51
  }
52
52
 
53
+ .row.bottom { align-items: flex-end; }
54
+
53
55
  .corner {
54
56
  position: relative;
55
57
  flex: 1 1 33%;
56
58
  display: flex;
57
59
  gap: 10px;
60
+ height: min-content;
58
61
  }
59
62
 
60
63
  .corner slot {
@@ -0,0 +1,133 @@
1
+ import { LitElement, html, css } from "lit";
2
+
3
+ /**
4
+ * Tabs offers a nice paged content rendering based on tabs buttons.
5
+ * The list of tab names are passed through `title` slots, and content using `content` slots.
6
+ * Note that tab names and contents should respect a coherent order.
7
+ * @class Panoramax.components.layout.Tabs
8
+ * @element pnx-tabs
9
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
10
+ * @slot `title` A single tab name
11
+ * @slot `content` A single tab content
12
+ * @example
13
+ * ```html
14
+ * <pnx-tabs>
15
+ * <h4 slot="title">Tab 1</h4>
16
+ * <div slot="content">Tab 1 content</div>
17
+ *
18
+ * <h4 slot="title">Tab 2</h4>
19
+ * <div slot="content">Tab 2 content</div>
20
+ *
21
+ * <h4 slot="title">Tab 3</h4>
22
+ * <div slot="content">Tab 3 content</div>
23
+ * </pnx-tabs>
24
+ * ```
25
+ */
26
+ export default class Tabs extends LitElement {
27
+ /** @private */
28
+ static styles = css`
29
+ /* Tabs */
30
+ nav {
31
+ display: flex;
32
+ gap: 5px;
33
+ align-items: stretch;
34
+ overflow-x: auto;
35
+ touch-action: pan-x;
36
+ }
37
+ nav ::slotted(*) {
38
+ color: var(--grey-dark);
39
+ border-bottom: 2px solid rgba(0,0,0,0);
40
+ cursor: pointer;
41
+ transition: all 0.1s;
42
+ flex: 1;
43
+ }
44
+ nav ::slotted(*:hover:not(.active)) { background-color: var(--blue-pale); }
45
+ nav ::slotted(*.active) {
46
+ color: var(--blue-dark);
47
+ border-bottom: 2px solid var(--blue-dark);
48
+ }
49
+
50
+ /* Content */
51
+ .contents ::slotted(div) { display: none !important; }
52
+ .contents ::slotted(.active) { display: block !important; }
53
+ `;
54
+
55
+ /**
56
+ * Component properties.
57
+ * @memberof Panoramax.components.layout.Tabs#
58
+ * @type {Object}
59
+ * @property {string} [activeTabIndex=0] The selected tab index
60
+ */
61
+ static properties = {
62
+ activeTabIndex: {type: Number},
63
+ };
64
+
65
+ constructor() {
66
+ super();
67
+ this.activeTabIndex = 0;
68
+ }
69
+
70
+ /** @private */
71
+ _getTabs() {
72
+ return this.shadowRoot.querySelector("slot[name='title']").assignedNodes();
73
+ }
74
+
75
+ /** @private */
76
+ _getContents() {
77
+ return this.shadowRoot.querySelector("slot[name='content']").assignedNodes();
78
+ }
79
+
80
+ /** @private */
81
+ _changeTab(tabTarget, tabIndex) {
82
+ const tabs = this._getTabs();
83
+ const contents = this._getContents();
84
+
85
+ // Check if tab change is possible
86
+ if(tabTarget !== undefined || tabIndex !== undefined) {
87
+ // For tab target, check if a nav tab has really been clicked
88
+ if(tabTarget) { tabIndex = tabs.findIndex(tab => (
89
+ tab === tabTarget || tab === tabTarget.parentNode
90
+ )); }
91
+
92
+ if(!isNaN(tabIndex) && tabIndex >= 0 && tabIndex < tabs.length) {
93
+ tabs.forEach((tab, index) => {
94
+ if (index == tabIndex) {
95
+ this.activeTabIndex = index;
96
+ contents[index].classList.add("active");
97
+ tab.classList.add("active");
98
+ } else {
99
+ contents[index].classList.remove("active");
100
+ tab.classList.remove("active");
101
+ }
102
+ });
103
+ }
104
+ }
105
+
106
+ }
107
+
108
+ /** @private */
109
+ _handleTabClick(event) {
110
+ this._changeTab(event.target);
111
+ }
112
+
113
+ /** @private */
114
+ updated(changedProperties) {
115
+ if(changedProperties.has("activeTabIndex")) {
116
+ this._changeTab(undefined, this.activeTabIndex);
117
+ }
118
+ }
119
+
120
+ /** @private */
121
+ render() {
122
+ return html`
123
+ <nav @click="${this._handleTabClick}">
124
+ <slot name="title"></slot>
125
+ </nav>
126
+ <div class="contents">
127
+ <slot name="content"></slot>
128
+ </div>
129
+ `;
130
+ }
131
+ }
132
+
133
+ customElements.define("pnx-tabs", Tabs);
@@ -3,5 +3,7 @@
3
3
  * @module Panoramax:components:layout
4
4
  */
5
5
 
6
+ export {default as BottomDrawer} from "./BottomDrawer";
6
7
  export {default as CorneredGrid} from "./CorneredGrid";
7
8
  export {default as Mini} from "./Mini";
9
+ export {default as Tabs} from "./Tabs";