@panoramax/web-viewer 3.2.3-develop-d7e5a16d → 3.2.3-develop-6257391e

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 (221) hide show
  1. package/.gitlab-ci.yml +3 -0
  2. package/CHANGELOG.md +19 -0
  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 +2 -2
  7. package/build/index.css.map +1 -1
  8. package/build/index.html +1 -1
  9. package/build/index.js +1682 -5
  10. package/build/index.js.map +1 -1
  11. package/build/map.html +1 -1
  12. package/build/viewer.html +10 -1
  13. package/build/widgets.html +1 -0
  14. package/config/jest/mocks.js +172 -0
  15. package/config/paths.js +1 -0
  16. package/config/webpack.config.js +26 -0
  17. package/docs/03_URL_settings.md +3 -11
  18. package/docs/05_Compatibility.md +59 -76
  19. package/docs/09_Develop.md +30 -11
  20. package/docs/90_Releases.md +2 -2
  21. package/docs/images/class_diagram.drawio +28 -28
  22. package/docs/images/class_diagram.jpg +0 -0
  23. package/docs/index.md +112 -0
  24. package/docs/reference/components/core/Basic.md +153 -0
  25. package/docs/reference/components/core/CoverageMap.md +160 -0
  26. package/docs/reference/components/core/Editor.md +172 -0
  27. package/docs/reference/components/core/Viewer.md +288 -0
  28. package/docs/reference/components/layout/CorneredGrid.md +29 -0
  29. package/docs/reference/components/layout/Mini.md +45 -0
  30. package/docs/reference/components/menus/MapBackground.md +32 -0
  31. package/docs/reference/components/menus/MapFilters.md +15 -0
  32. package/docs/reference/components/menus/MapLayers.md +15 -0
  33. package/docs/reference/components/menus/MapLegend.md +15 -0
  34. package/docs/reference/components/menus/PictureLegend.md +15 -0
  35. package/docs/reference/components/menus/PictureMetadata.md +15 -0
  36. package/docs/reference/components/menus/PlayerOptions.md +15 -0
  37. package/docs/reference/components/menus/QualityScoreDoc.md +15 -0
  38. package/docs/reference/components/menus/ReportForm.md +15 -0
  39. package/docs/reference/components/menus/ShareMenu.md +15 -0
  40. package/docs/reference/components/ui/Button.md +39 -0
  41. package/docs/reference/components/ui/ButtonGroup.md +36 -0
  42. package/docs/reference/components/ui/CopyButton.md +35 -0
  43. package/docs/reference/components/ui/Grade.md +32 -0
  44. package/docs/reference/components/ui/LinkButton.md +44 -0
  45. package/docs/reference/components/ui/Loader.md +54 -0
  46. package/docs/reference/components/ui/Map.md +214 -0
  47. package/docs/reference/components/ui/MapMore.md +233 -0
  48. package/docs/reference/components/ui/Photo.md +369 -0
  49. package/docs/reference/components/ui/Popup.md +56 -0
  50. package/docs/reference/components/ui/QualityScore.md +45 -0
  51. package/docs/reference/components/ui/SearchBar.md +63 -0
  52. package/docs/reference/components/ui/TogglableGroup.md +39 -0
  53. package/docs/reference/components/ui/widgets/GeoSearch.md +32 -0
  54. package/docs/reference/components/ui/widgets/Legend.md +32 -0
  55. package/docs/reference/components/ui/widgets/MapFiltersButton.md +33 -0
  56. package/docs/reference/components/ui/widgets/MapLayersButton.md +15 -0
  57. package/docs/reference/components/ui/widgets/Player.md +32 -0
  58. package/docs/reference/components/ui/widgets/Share.md +15 -0
  59. package/docs/reference/components/ui/widgets/Zoom.md +15 -0
  60. package/docs/reference/utils/API.md +311 -0
  61. package/docs/reference/utils/InitParameters.md +67 -0
  62. package/docs/reference/utils/URLHandler.md +102 -0
  63. package/docs/reference.md +73 -0
  64. package/docs/shortcuts.md +11 -0
  65. package/docs/tutorials/aerial_imagery.md +19 -0
  66. package/docs/tutorials/authentication.md +10 -0
  67. package/docs/tutorials/custom_widgets.md +64 -0
  68. package/docs/tutorials/map_style.md +27 -0
  69. package/docs/tutorials/migrate_v4.md +122 -0
  70. package/docs/tutorials/synced_coverage.md +42 -0
  71. package/mkdocs.yml +60 -5
  72. package/package.json +10 -7
  73. package/public/editor.html +21 -29
  74. package/public/index.html +3 -3
  75. package/public/map.html +19 -18
  76. package/public/viewer.html +18 -24
  77. package/public/widgets.html +265 -0
  78. package/scripts/doc.js +77 -0
  79. package/src/components/core/Basic.css +44 -0
  80. package/src/components/core/Basic.js +258 -0
  81. package/src/components/core/CoverageMap.css +9 -0
  82. package/src/components/core/CoverageMap.js +105 -0
  83. package/src/components/core/Editor.css +23 -0
  84. package/src/components/core/Editor.js +354 -0
  85. package/src/components/core/Viewer.css +109 -0
  86. package/src/components/core/Viewer.js +707 -0
  87. package/src/components/core/index.js +11 -0
  88. package/src/components/index.js +13 -0
  89. package/src/components/layout/CorneredGrid.js +109 -0
  90. package/src/components/layout/Mini.js +117 -0
  91. package/src/components/layout/index.js +7 -0
  92. package/src/components/menus/MapBackground.js +106 -0
  93. package/src/components/menus/MapFilters.js +386 -0
  94. package/src/components/menus/MapLayers.js +143 -0
  95. package/src/components/menus/MapLegend.js +54 -0
  96. package/src/components/menus/PictureLegend.js +103 -0
  97. package/src/components/menus/PictureMetadata.js +188 -0
  98. package/src/components/menus/PlayerOptions.js +96 -0
  99. package/src/components/menus/QualityScoreDoc.js +36 -0
  100. package/src/components/menus/ReportForm.js +133 -0
  101. package/src/components/menus/Share.js +228 -0
  102. package/src/components/menus/index.js +15 -0
  103. package/src/components/styles.js +365 -0
  104. package/src/components/ui/Button.js +75 -0
  105. package/src/components/ui/ButtonGroup.css +49 -0
  106. package/src/components/ui/ButtonGroup.js +68 -0
  107. package/src/components/ui/CopyButton.js +71 -0
  108. package/src/components/ui/Grade.js +54 -0
  109. package/src/components/ui/LinkButton.js +68 -0
  110. package/src/components/ui/Loader.js +188 -0
  111. package/src/components/{Map.css → ui/Map.css} +5 -17
  112. package/src/components/{Map.js → ui/Map.js} +114 -138
  113. package/src/components/ui/MapMore.js +324 -0
  114. package/src/components/{Photo.css → ui/Photo.css} +6 -6
  115. package/src/components/{Photo.js → ui/Photo.js} +279 -90
  116. package/src/components/ui/Popup.js +145 -0
  117. package/src/components/ui/QualityScore.js +152 -0
  118. package/src/components/ui/SearchBar.js +363 -0
  119. package/src/components/ui/TogglableGroup.js +162 -0
  120. package/src/components/ui/index.js +20 -0
  121. package/src/components/ui/widgets/GeoSearch.css +21 -0
  122. package/src/components/ui/widgets/GeoSearch.js +139 -0
  123. package/src/components/ui/widgets/Legend.js +51 -0
  124. package/src/components/ui/widgets/MapFiltersButton.js +104 -0
  125. package/src/components/ui/widgets/MapLayersButton.js +79 -0
  126. package/src/components/ui/widgets/Player.css +7 -0
  127. package/src/components/ui/widgets/Player.js +148 -0
  128. package/src/components/ui/widgets/Share.js +30 -0
  129. package/src/components/ui/widgets/Zoom.js +82 -0
  130. package/src/components/ui/widgets/index.js +12 -0
  131. package/src/img/panoramax.svg +13 -0
  132. package/src/img/switch_big.svg +20 -10
  133. package/src/index.js +6 -9
  134. package/src/translations/da.json +1 -1
  135. package/src/translations/de.json +1 -1
  136. package/src/translations/en.json +5 -3
  137. package/src/translations/eo.json +1 -1
  138. package/src/translations/es.json +1 -1
  139. package/src/translations/fr.json +5 -3
  140. package/src/translations/hu.json +1 -1
  141. package/src/translations/it.json +1 -1
  142. package/src/translations/ja.json +1 -1
  143. package/src/translations/nl.json +1 -1
  144. package/src/translations/pl.json +1 -1
  145. package/src/translations/sv.json +1 -1
  146. package/src/translations/zh_Hant.json +1 -1
  147. package/src/utils/API.js +74 -42
  148. package/src/utils/InitParameters.js +354 -0
  149. package/src/utils/URLHandler.js +364 -0
  150. package/src/utils/geocoder.js +116 -0
  151. package/src/utils/{I18n.js → i18n.js} +3 -1
  152. package/src/utils/index.js +11 -0
  153. package/src/utils/{Map.js → map.js} +216 -80
  154. package/src/utils/picture.js +433 -0
  155. package/src/utils/utils.js +315 -0
  156. package/src/utils/widgets.js +93 -0
  157. package/tests/components/ui/CopyButton.test.js +52 -0
  158. package/tests/components/ui/Loader.test.js +54 -0
  159. package/tests/components/{Map.test.js → ui/Map.test.js} +19 -61
  160. package/tests/components/{Photo.test.js → ui/Photo.test.js} +89 -57
  161. package/tests/components/ui/Popup.test.js +24 -0
  162. package/tests/components/ui/QualityScore.test.js +17 -0
  163. package/tests/components/ui/SearchBar.test.js +107 -0
  164. package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +34 -0
  165. package/tests/components/ui/__snapshots__/Loader.test.js.snap +56 -0
  166. package/tests/components/{__snapshots__ → ui/__snapshots__}/Map.test.js.snap +11 -38
  167. package/tests/components/{__snapshots__ → ui/__snapshots__}/Photo.test.js.snap +57 -4
  168. package/tests/components/ui/__snapshots__/Popup.test.js.snap +29 -0
  169. package/tests/components/ui/__snapshots__/QualityScore.test.js.snap +11 -0
  170. package/tests/components/ui/__snapshots__/SearchBar.test.js.snap +65 -0
  171. package/tests/utils/API.test.js +1 -14
  172. package/tests/utils/InitParameters.test.js +485 -0
  173. package/tests/utils/URLHandler.test.js +350 -0
  174. package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
  175. package/tests/utils/__snapshots__/picture.test.js.snap +315 -0
  176. package/tests/utils/__snapshots__/widgets.test.js.snap +19 -0
  177. package/tests/utils/geocoder.test.js +37 -0
  178. package/tests/utils/{I18n.test.js → i18n.test.js} +1 -1
  179. package/tests/utils/map.test.js +67 -0
  180. package/tests/utils/picture.test.js +745 -0
  181. package/tests/utils/utils.test.js +288 -0
  182. package/tests/utils/widgets.test.js +90 -0
  183. package/docs/01_Start.md +0 -149
  184. package/docs/02_Usage.md +0 -831
  185. package/docs/04_Advanced_examples.md +0 -216
  186. package/src/Editor.css +0 -37
  187. package/src/Editor.js +0 -361
  188. package/src/StandaloneMap.js +0 -114
  189. package/src/Viewer.css +0 -203
  190. package/src/Viewer.js +0 -1246
  191. package/src/components/CoreView.css +0 -70
  192. package/src/components/CoreView.js +0 -175
  193. package/src/components/Loader.css +0 -74
  194. package/src/components/Loader.js +0 -120
  195. package/src/utils/Exif.js +0 -193
  196. package/src/utils/Utils.js +0 -631
  197. package/src/utils/Widgets.js +0 -562
  198. package/src/viewer/URLHash.js +0 -469
  199. package/src/viewer/Widgets.css +0 -880
  200. package/src/viewer/Widgets.js +0 -1470
  201. package/tests/Editor.test.js +0 -126
  202. package/tests/StandaloneMap.test.js +0 -45
  203. package/tests/Viewer.test.js +0 -366
  204. package/tests/__snapshots__/Editor.test.js.snap +0 -298
  205. package/tests/__snapshots__/StandaloneMap.test.js.snap +0 -30
  206. package/tests/__snapshots__/Viewer.test.js.snap +0 -195
  207. package/tests/components/CoreView.test.js +0 -92
  208. package/tests/components/Loader.test.js +0 -38
  209. package/tests/components/__snapshots__/Loader.test.js.snap +0 -15
  210. package/tests/utils/Exif.test.js +0 -124
  211. package/tests/utils/Map.test.js +0 -113
  212. package/tests/utils/Utils.test.js +0 -300
  213. package/tests/utils/Widgets.test.js +0 -107
  214. package/tests/utils/__snapshots__/Exif.test.js.snap +0 -43
  215. package/tests/utils/__snapshots__/Utils.test.js.snap +0 -41
  216. package/tests/utils/__snapshots__/Widgets.test.js.snap +0 -44
  217. package/tests/viewer/URLHash.test.js +0 -559
  218. package/tests/viewer/Widgets.test.js +0 -127
  219. package/tests/viewer/__snapshots__/URLHash.test.js.snap +0 -108
  220. package/tests/viewer/__snapshots__/Widgets.test.js.snap +0 -403
  221. /package/tests/utils/__snapshots__/{Map.test.js.snap → geocoder.test.js.snap} +0 -0
@@ -0,0 +1,145 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { fa } from "../../utils/widgets";
3
+ import { panel, faSvg } from "../styles";
4
+ import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
5
+
6
+ /**
7
+ * Popup Element displays a full overlay container.
8
+ * @class Panoramax.components.ui.Popup
9
+ * @element pnx-popup
10
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
11
+ * @fires Panoramax.components.ui.Popup#open
12
+ * @fires Panoramax.components.ui.Popup#close
13
+ * @example
14
+ * ```html
15
+ * <pnx-popup @close=${() => alert("closed")}>
16
+ * <h3>Popup content</h3>
17
+ * </pnx-popup>
18
+ * ```
19
+ */
20
+ export default class Popup extends LitElement {
21
+ /** @private */
22
+ static styles = [ panel, faSvg, css`
23
+ /* Backdrop */
24
+ :host {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ position: absolute;
29
+ top: 0;
30
+ right: 0;
31
+ left: 0;
32
+ bottom: 0;
33
+ opacity: 0;
34
+ visibility: hidden;
35
+ pointer-events: none;
36
+ transition: all 0.2s;
37
+ background: rgba(0, 0, 0, 0.85);
38
+ }
39
+ :host(*[visible]) {
40
+ opacity: 1;
41
+ visibility: visible;
42
+ pointer-events: unset;
43
+ z-index: 200;
44
+ }
45
+
46
+ /* Content backgroud */
47
+ .pnx-panel {
48
+ box-sizing: border-box;
49
+ position: relative;
50
+ width: 90%;
51
+ max-height: 90%;
52
+ max-width: 550px;
53
+ overflow-y: auto;
54
+ }
55
+
56
+ .btn-close {
57
+ position: absolute;
58
+ top: 15px;
59
+ right: 15px;
60
+ }
61
+ `];
62
+
63
+ /**
64
+ * Component properties.
65
+ * @memberof Panoramax.components.ui.Popup#
66
+ * @type {Object}
67
+ * @property {boolean} [visible=true] Is the popup visible to user ?
68
+ */
69
+ static properties = {
70
+ visible: {type: Boolean, reflect: true},
71
+ };
72
+
73
+ constructor() {
74
+ super();
75
+ this.visible = false;
76
+ }
77
+
78
+ /** @private */
79
+ connectedCallback() {
80
+ super.connectedCallback();
81
+
82
+ // Backdrop closing
83
+ this.addEventListener("click", e => {
84
+ if(e.target === this) { this.close(); }
85
+ });
86
+
87
+ // Esc closing
88
+ document.addEventListener("keyup", e => {
89
+ if(e.key === "Escape" && this.visible) {
90
+ this.close();
91
+ }
92
+ });
93
+ }
94
+
95
+ /** @private */
96
+ attributeChangedCallback(name, _old, value) {
97
+ super.attributeChangedCallback(name, _old, value);
98
+
99
+ if(name == "visible") {
100
+ if(value === "" || value === true) {
101
+ /**
102
+ * Event for popup opening
103
+ * @event Panoramax.components.ui.Popup#open
104
+ * @type {Event}
105
+ */
106
+ this.dispatchEvent(new Event("open"));
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Close popup
113
+ * @memberof Panoramax.components.ui.Popup#
114
+ */
115
+ close() {
116
+ this.visible = false;
117
+
118
+ /**
119
+ * Event for popup closing
120
+ * @event Panoramax.components.ui.Popup#close
121
+ * @type {Event}
122
+ */
123
+ this.dispatchEvent(new Event("close"));
124
+ }
125
+
126
+ /** @private */
127
+ render() {
128
+ return html`
129
+ <div class="pnx-panel pnx-padded">
130
+ <pnx-button
131
+ class="btn-close"
132
+ kind="flat"
133
+ size="l"
134
+ @click=${this.close.bind(this)}
135
+ >
136
+ ${fa(faXmark)}
137
+ </pnx-button>
138
+
139
+ <slot></slot>
140
+ </div>
141
+ `;
142
+ }
143
+ }
144
+
145
+ customElements.define("pnx-popup", Popup);
@@ -0,0 +1,152 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { QUALITYSCORE_VALUES } from "../../utils/utils";
3
+
4
+ /**
5
+ * Quality Score element displays the picture quality grade.
6
+ * @class Panoramax.components.ui.QualityScore
7
+ * @element pnx-quality-score
8
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
9
+ * @fires Panoramax.components.ui.QualityScore#change
10
+ * @example
11
+ * ```html
12
+ * <!-- Read-only -->
13
+ * <pnx-quality-score grade="5" ._t=${viewer._t} />
14
+ *
15
+ * <!-- Input -->
16
+ * <pnx-quality-score grade="5" input="pnx-qs-input" @change=${() => alert('changed')} ._t=${viewer._t} />
17
+ * ```
18
+ */
19
+ export default class QualityScore extends LitElement {
20
+ /** @private */
21
+ static styles = css`
22
+ :host {
23
+ gap: 0px;
24
+ justify-content: center;
25
+ height: 42px;
26
+ display: inline-flex;
27
+ align-items: center;
28
+ }
29
+
30
+ /* Single letter */
31
+ span, label {
32
+ font-size: 18px;
33
+ width: 25px;
34
+ height: 30px;
35
+ line-height: 26px;
36
+ display: inline-block;
37
+ border: 1px solid white;
38
+ text-align: center;
39
+ background-color: gray;
40
+ color: rgba(255,255,255,0.9);
41
+ font-family: sans-serif;
42
+ font-weight: bold;
43
+ vertical-align: middle;
44
+ }
45
+
46
+ /* Rounded corners for first/last */
47
+ span:first-of-type, label:first-of-type {
48
+ border-top-left-radius: 8px !important;
49
+ border-bottom-left-radius: 8px !important;
50
+ }
51
+
52
+ span:last-of-type, label:last-of-type {
53
+ border-top-right-radius: 8px !important;
54
+ border-bottom-right-radius: 8px !important;
55
+ }
56
+
57
+ /* Selected letter */
58
+ .selected, input[type="checkbox"]:checked + label {
59
+ width: 30px;
60
+ height: 42px;
61
+ line-height: 37px;
62
+ border-radius: 8px;
63
+ font-size: 27px;
64
+ color: white;
65
+ border: 2px solid white;
66
+ }
67
+
68
+ /* Clickable letter for input */
69
+ label { cursor: pointer; }
70
+
71
+ label:hover {
72
+ width: 28px;
73
+ height: 35px;
74
+ line-height: 30px;
75
+ border-radius: 3px;
76
+ font-size: 22px;
77
+ color: white;
78
+ border: 2px solid white;
79
+ }
80
+
81
+ input[type="checkbox"] { display: none; }
82
+ `;
83
+
84
+ /**
85
+ * Component properties.
86
+ * @memberof Panoramax.components.ui.QualityScore#
87
+ * @type {Object}
88
+ * @property {number|string} [grade=0] The grade (5=A, 1=E, 0=none selected), or a list of grades
89
+ * @property {string} [input=false] If set, score is an input and value is used as ID prefix
90
+ */
91
+ static properties = {
92
+ grade: {type: String},
93
+ input: {type: String},
94
+ };
95
+
96
+ constructor() {
97
+ super();
98
+ this.grade = 0;
99
+ this.input = false;
100
+ }
101
+
102
+ /** @private */
103
+ _onInput() {
104
+ // List all checked boxes
105
+ const newgrade = [];
106
+ for(let i of this.renderRoot.querySelectorAll("input")) {
107
+ if(i.checked) { newgrade.push(i.value); }
108
+ }
109
+ this.grade = newgrade.join(",");
110
+
111
+ /**
112
+ * Event for grade change
113
+ * @event Panoramax.components.ui.QualityScore#change
114
+ * @type {CustomEvent}
115
+ */
116
+ this.dispatchEvent(new CustomEvent("change", {bubbles: true, composed: true}));
117
+ }
118
+
119
+ /** @private */
120
+ render() {
121
+ const grades = `${this.grade}`.split(",").map(v => parseInt(v)).filter(v => !isNaN(v));
122
+ if(this.input) {
123
+ return QUALITYSCORE_VALUES.map((pv, index) => {
124
+ return html`
125
+ <input
126
+ id="${this.input}-${pv.label}"
127
+ type="checkbox"
128
+ name="qualityscore"
129
+ value="${5-index}"
130
+ @change="${this._onInput}"
131
+ .checked=${grades.includes(5-index)}
132
+ />
133
+ <label
134
+ for="${this.input}-${pv.label}"
135
+ title="${this._t?.pnx.filter_qualityscore_help}"
136
+ style="background-color: ${pv.color}"
137
+ >
138
+ ${pv.label}
139
+ </label>
140
+ `;});
141
+ }
142
+ else {
143
+ return QUALITYSCORE_VALUES.map((pv, index) => html`
144
+ <span class="${grades.includes(5-index) ? "selected" : ""}" style="background-color: ${pv.color}">
145
+ ${pv.label}
146
+ </span>
147
+ `);
148
+ }
149
+ }
150
+ }
151
+
152
+ customElements.define("pnx-quality-score", QualityScore);
@@ -0,0 +1,363 @@
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
+ }
54
+ .sb.sb-xxl {
55
+ height: 40px;
56
+ line-height: 40px;
57
+ }
58
+ .sb.sb-reduceable {
59
+ width: 360px;
60
+ }
61
+ .sb.sb-reduceable.sb-reduced {
62
+ width: fit-content;
63
+ padding: 0;
64
+ gap: 0;
65
+ }
66
+
67
+ /* Text field */
68
+ .sb input {
69
+ background: none;
70
+ border: none !important;
71
+ outline: none;
72
+ height: 20px;
73
+ width: 100%;
74
+ }
75
+ .sb.sb-xxl input {
76
+ font-size: 1.1em;
77
+ }
78
+ .sb.sb-reduceable.sb-reduced input {
79
+ display: none;
80
+ }
81
+
82
+ /* Status icon */
83
+ .sb-icon {
84
+ cursor: pointer;
85
+ height: 30px;
86
+ line-height: 30px;
87
+ width: 30px;
88
+ min-width: 30px;
89
+ text-align: center;
90
+ }
91
+ .sb.sb-xxl .sb-icon {
92
+ height: 40px;
93
+ line-height: 40px;
94
+ width: 40px;
95
+ min-width: 40px;
96
+ }
97
+ .sb-icon svg {
98
+ pointer-events: none;
99
+ width: 14px;
100
+ height: 14px;
101
+ }
102
+
103
+ /* Search results */
104
+ .sb-results {
105
+ position: absolute;
106
+ top: 35px;
107
+ list-style: none;
108
+ margin: 0;
109
+ padding: 0;
110
+ max-width: calc(100% - 20px);
111
+ border: 1px solid var(--widget-border-div);
112
+ border-radius: 10px;
113
+ background-color: var(--widget-bg);
114
+ color: var(--widget-font);
115
+ z-index: 130;
116
+ font-size: 1em;
117
+ line-height: normal;
118
+ font-family: sans;
119
+ }
120
+ .sb.sb-xxl .sb-results {
121
+ top: 45px;
122
+ }
123
+ .sb-result,
124
+ .sb-empty {
125
+ display: block;
126
+ padding: 5px 15px;
127
+ white-space: nowrap;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ cursor: pointer;
131
+ border-radius: 0;
132
+ }
133
+ .sb-result:hover {
134
+ background-color: var(--widget-bg-hover);
135
+ }
136
+ .sb-result:first-child {
137
+ border-top-right-radius: 10px;
138
+ border-top-left-radius: 10px;
139
+ padding-top: 15px;
140
+ }
141
+ .sb-result:last-child {
142
+ border-bottom-right-radius: 10px;
143
+ border-bottom-left-radius: 10px;
144
+ padding-bottom: 15px;
145
+ }
146
+ ` ];
147
+
148
+ /**
149
+ * Component properties.
150
+ * @memberof Panoramax.components.ui.SearchBar#
151
+ * @type {Object}
152
+ * @property {string} [id] The ID attribute set on component and prefix for input as well
153
+ * @property {string} [placeholder] Default text to display on empty field
154
+ * @property {boolean} [reduceable=false] Can the search bar be collapsed (for mobile view)
155
+ * @property {boolean} [reduced=false] Is the search bar currently collapsed ?
156
+ * @property {string} [value] The default input value
157
+ * @property {string} [size=md] The component sizing (md, xxl)
158
+ * @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)
159
+ * @property {boolean} [no-menu-closure=false] Set to true to ignore menu closure events
160
+ */
161
+ static properties = {
162
+ id: {type: String},
163
+ placeholder: {type: String},
164
+ reduceable: {type: Boolean, reflect: true},
165
+ reduced: {type: Boolean, reflect: true},
166
+ value: {type: String},
167
+ size: {type: String},
168
+ _icon: {state: true},
169
+ _results: {state: true},
170
+ searcher: {type: Function},
171
+ "no-menu-closure": {type: Boolean},
172
+ };
173
+
174
+ constructor() {
175
+ super();
176
+
177
+ // State properties
178
+ this.reduceable = false;
179
+ this.reduced = false;
180
+ this.placeholder = nothing;
181
+ this.size = "md";
182
+ this._icon = "search";
183
+ this._results = null;
184
+ this["no-menu-closure"] = false;
185
+
186
+ // Other properties
187
+ this._throttler = null;
188
+ delete this._lastSearch;
189
+ }
190
+
191
+ /** @private */
192
+ connectedCallback() {
193
+ super.connectedCallback();
194
+ if(!this["no-menu-closure"]) { listenForMenuClosure(this, this.reset.bind(this)); }
195
+ }
196
+
197
+ /** @private */
198
+ attributeChangedCallback(name, _old, value) {
199
+ super.attributeChangedCallback(name, _old, value);
200
+ if(name == "value" && this._icon == "search") {
201
+ this._icon = "empty";
202
+ }
203
+ }
204
+
205
+ /** @private */
206
+ _onIconClick() {
207
+ if(["empty", "warn"].includes(this._icon)) {
208
+ this.reset();
209
+ }
210
+ if(this.reduceable && this._icon == "search") {
211
+ this.reduced = !this.reduced;
212
+ }
213
+ }
214
+
215
+ /** @private */
216
+ _onInputChange(e) {
217
+ this.value = e.target.value;
218
+ this._throttledSearch();
219
+ }
220
+
221
+ /** @private */
222
+ _onResultClick(item) {
223
+ /**
224
+ * Event for search result clicked
225
+ * @event Panoramax.components.ui.SearchBar#result
226
+ * @type {CustomEvent}
227
+ * @property {object|null} detail The data associated to clicked item (format based on searcher function results), or null on reset
228
+ */
229
+ this.dispatchEvent(new CustomEvent("result", {bubbles: true, composed: true, detail: item}));
230
+
231
+ this._results = null;
232
+ if(this._throttler) {
233
+ clearTimeout(this._throttler);
234
+ this._throttler = null;
235
+ }
236
+
237
+ if(!this.reduceable && item) {
238
+ this.value = item?.title;
239
+ this._icon = "empty";
240
+ }
241
+ else {
242
+ this.value = "";
243
+ this._icon = "search";
244
+ if(this.reduceable) { this.reduced = true; }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Limit search calls to every 250ms
250
+ * @private
251
+ */
252
+ _throttledSearch() {
253
+ if(this._throttler) {
254
+ clearTimeout(this._throttler);
255
+ delete this._throttler;
256
+ }
257
+
258
+ this._throttler = setTimeout(this._search.bind(this), 250);
259
+ }
260
+
261
+ /**
262
+ * Perform real search
263
+ * @private
264
+ */
265
+ _search() {
266
+ if(!this.value || this.value.length == 0) {
267
+ this.reset();
268
+ return;
269
+ }
270
+
271
+ if(!this.searcher) {
272
+ console.warn("No search handler defined");
273
+ return;
274
+ }
275
+
276
+ this._icon = "loading";
277
+ this._results = null;
278
+
279
+ this.searcher(this.value).then(data => {
280
+ this._icon = "empty";
281
+ if(!data || data.length == 0) {
282
+ this._results = [];
283
+ }
284
+ else if(data === true) {
285
+ this._results = null;
286
+ }
287
+ else {
288
+ this._results = data;
289
+ if(!this["no-menu-closure"]) {
290
+ this._parent?.dispatchEvent(new CustomEvent("menu-opened", { detail: { menu: this }}));
291
+ }
292
+ }
293
+ }).catch(e => {
294
+ console.error(e);
295
+ this._icon = "warn";
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Empty results list and reset search bar content.
301
+ */
302
+ reset() {
303
+ this._onResultClick(null);
304
+ }
305
+
306
+ /** @private */
307
+ render() {
308
+ const classes = {
309
+ sb: true,
310
+ "sb-xxl": this.size === "xxl",
311
+ "sb-reduceable": this.reduceable,
312
+ "sb-reduced": this.reduced,
313
+ };
314
+
315
+ return html`<div
316
+ id=${this.id}
317
+ class=${classMap(classes)}
318
+ part="container"
319
+ >
320
+ <slot name="pre" class=${classMap({"sb-reduced": this.reduced})}></slot>
321
+
322
+ <input
323
+ id="${this.id}-input"
324
+ type="text"
325
+ placeholder=${this.placeholder}
326
+ autocomplete="off"
327
+ @change=${this._onInputChange.bind(this)}
328
+ @keypress=${this._onInputChange.bind(this)}
329
+ @paste=${this._onInputChange.bind(this)}
330
+ @input=${this._onInputChange.bind(this)}
331
+ .value=${this.value || ""}
332
+ part="input"
333
+ />
334
+
335
+ <span
336
+ class="sb-icon"
337
+ @click=${this._onIconClick.bind(this)}
338
+ >
339
+ ${this._icon == "search" ? fa(faMagnifyingGlass) : nothing}
340
+ ${this._icon == "loading" ? fa(faCircleNotch, { classes: ["fa-spin"] }) : nothing}
341
+ ${this._icon == "empty" ? fa(faXmark) : nothing}
342
+ ${this._icon == "warn" ? fa(faCircleExclamation) : nothing}
343
+ </span>
344
+
345
+ ${!this.reduced && this._results ? html`
346
+ <div class="sb-results">
347
+ ${this._results.length === 0 ? html`
348
+ <div class="sb-empty">${this._parent?._t?.pnx.search_empty}</div>
349
+ ` : nothing}
350
+
351
+ ${map(this._results, i => html`
352
+ <div class="sb-result" @click=${() => this._onResultClick(i)}>
353
+ ${i.title}
354
+ ${i.subtitle && i.subtitle != "" ? html`<br /><small>${i.subtitle}</small>` : nothing}
355
+ </div>
356
+ `)}
357
+ </div>
358
+ ` : nothing}
359
+ </div>`;
360
+ }
361
+ }
362
+
363
+ customElements.define("pnx-search-bar", SearchBar);