Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
giswqs
GitHub Repository: giswqs/geemap
Path: blob/master/js/search_bar.ts
2313 views
1
import type { RenderProps } from "@anywidget/types";
2
import { html, css } from "lit";
3
import { property, query, queryAll } from "lit/decorators.js";
4
5
import { legacyStyles } from "./ipywidgets_styles";
6
import { LitWidget } from "./lit_widget";
7
import { materialStyles } from "./styles";
8
import { loadFonts } from "./utils";
9
10
import { TabMode } from "./tab_panel";
11
import { unsafeHTML } from "lit/directives/unsafe-html.js";
12
13
import './container';
14
15
export interface SearchTab {
16
search: string,
17
results: string[],
18
selected: string,
19
additional_html: string,
20
}
21
22
export interface SearchBarModel {
23
collapsed: boolean;
24
tab_index: number;
25
location_model: string,
26
dataset_model: string,
27
}
28
29
export class SearchBar extends LitWidget<
30
SearchBarModel,
31
SearchBar
32
> {
33
static get componentName() {
34
return `search-bar`;
35
}
36
37
static override styles = [
38
legacyStyles,
39
materialStyles,
40
css`
41
.row {
42
display: flex;
43
gap: 6px;
44
}
45
46
.input-container {
47
max-width: 320px;
48
}
49
50
.input-container > p {
51
margin: 8px 3px;
52
}
53
54
input.search {
55
margin: 2px 2px 8px 2px;
56
width: calc(100% - 4px);
57
}
58
59
ul.results {
60
list-style-type: none;
61
margin: 0;
62
margin-bottom: 4px;
63
padding: 8px 0;
64
}
65
66
label.result {
67
align-items: center;
68
display: flex;
69
margin-bottom: 4px;
70
}
71
72
.import-button, .reset-button {
73
margin: 0 2px 2px 2px;
74
padding: 0 8px;
75
white-space: nowrap;
76
}
77
78
.dataset-select {
79
margin-bottom: 2px;
80
margin-right: 2px;
81
}
82
83
.additional-html-container {
84
max-height: 300px;
85
overflow: auto;
86
padding: 8px 0;
87
}
88
89
.additional-html-container pre {
90
white-space: break-spaces;
91
}
92
`,
93
];
94
95
modelNameToViewName(): Map<keyof SearchBarModel, keyof SearchBar> {
96
return new Map([
97
["collapsed", "collapsed"],
98
["tab_index", "tab_index"],
99
["location_model", "locationModel"],
100
["dataset_model", "datasetModel"],
101
]);
102
}
103
104
@property()
105
collapsed: boolean = true;
106
107
@property()
108
tab_index: number = 0;
109
110
@property()
111
locationModel: string = JSON.stringify({
112
search: "",
113
results: [],
114
selected: "",
115
additional_html: "",
116
});
117
118
@property()
119
datasetModel: string = JSON.stringify({
120
search: "",
121
results: [],
122
selected: "",
123
additional_html: "",
124
});
125
126
@query(".location-search")
127
locationSearch!: HTMLInputElement;
128
129
@queryAll(".location-results input")
130
locationResults!: HTMLInputElement[];
131
132
@query(".dataset-search")
133
datasetSearch!: HTMLInputElement;
134
135
override render() {
136
return html`
137
<widget-container
138
icon="search"
139
title="Search"
140
.collapsed="${this.collapsed}"
141
.hideCloseButton="${true}"
142
.compactMode="${true}">
143
<tab-panel
144
.index="${this.tab_index}"
145
.tabs=${[{ name: "location", width: 110 },
146
{ name: "data", width: 110 }
147
]}
148
@tab-changed=${(e: CustomEvent<number>) => {
149
this.tab_index = e.detail;
150
}}
151
.mode="${TabMode.ALWAYS_SHOW}">
152
<div class="input-container location-container">
153
${this.renderLocationSearch()}
154
</div>
155
<div class="input-container dataset-container">
156
${this.renderDatasetSearch()}
157
</div>
158
</tab-panel>
159
</widget-container>`;
160
}
161
162
private renderLocationSearch() {
163
const locationModel = JSON.parse(this.locationModel) as SearchTab;
164
const helpText = html`<p>
165
Find your point of interest (by place name,
166
address, or coordinates, e.g. 40,-100)
167
</p>`;
168
const searchInput = html`<input
169
class="legacy-input search location-search"
170
type="search"
171
placeholder="Search by location / lat-lon"
172
@keydown="${(e: KeyboardEvent) => {
173
if (e.key === "Enter") {
174
e.preventDefault();
175
const locationModel = JSON.parse(this.locationModel) as SearchTab;
176
locationModel.search = this.locationSearch.value || "";
177
this.locationModel = JSON.stringify(locationModel);
178
}
179
}}" />`;
180
const renderedInputs = [helpText, searchInput];
181
if (locationModel.results.length) {
182
const results = html`
183
${locationModel.results.map((result) => html`
184
<li>
185
<label class="result">
186
<input
187
type="radio"
188
name="location-result"
189
value="${result}"
190
.checked="${locationModel.selected === result}"
191
@input="${(e: Event) => {
192
const input = (e.target as HTMLInputElement);
193
const locationModel = JSON.parse(this.locationModel) as SearchTab;
194
locationModel.selected = input.value || "";
195
this.locationModel = JSON.stringify(locationModel);
196
}}" />
197
<span>${result}</span>
198
</label>
199
</li>`)}
200
`;
201
renderedInputs.push(html`<ul class="results location-results">
202
${results}
203
</ul>`);
204
}
205
if (locationModel.additional_html) {
206
renderedInputs.push(html`<div class="additional-html-container">
207
${unsafeHTML(locationModel.additional_html)}
208
</div>`);
209
}
210
if (locationModel.search ||
211
locationModel.results.length ||
212
locationModel.selected) {
213
renderedInputs.push(html`<button
214
class="legacy-button primary reset-button"
215
@click="${() => {
216
this.locationModel = JSON.stringify({
217
search: "",
218
results: [],
219
selected: "",
220
additional_html: "",
221
});
222
if (this.locationSearch) {
223
this.locationSearch.value = "";
224
}
225
}}">Reset</button>`)
226
}
227
return renderedInputs;
228
}
229
230
private renderDatasetSearch() {
231
const datasetModel = JSON.parse(this.datasetModel) as SearchTab;
232
const helpText = html`<p>
233
Find a dataset by GEE data catalog name or keywords, e.g. elevation
234
</p>`;
235
const searchInput = html`<input
236
class="legacy-input search dataset-search"
237
type="search"
238
placeholder="Search dataset / keywords"
239
@keydown="${(e: KeyboardEvent) => {
240
if (e.key === "Enter") {
241
e.preventDefault();
242
const datasetModel = JSON.parse(this.datasetModel) as SearchTab;
243
datasetModel.search = this.datasetSearch?.value || "";
244
// Force a rerender.
245
this.datasetModel = JSON.stringify(datasetModel);
246
}
247
}}" />`;
248
const renderedInputs = [helpText, searchInput];
249
const importButton = html`<button
250
class="legacy-button primary import-button"
251
title="Click to import the selected asset"
252
@click="${() => {
253
this.model?.send({ type: "click", id: "import" });
254
}}">
255
Reveal Code
256
</button>`;
257
const results = html`
258
<select
259
class="legacy-select dataset-select"
260
@input="${(e: Event) => {
261
const input = (e.target as HTMLInputElement);
262
const datasetModel = JSON.parse(this.datasetModel) as SearchTab;
263
datasetModel.selected = input.value || "";
264
// Force a rerender.
265
this.datasetModel = JSON.stringify(datasetModel);
266
}}">
267
${datasetModel.results.map((result) => html`
268
<option>
269
${result}
270
</option>
271
`)}
272
</select>
273
`;
274
renderedInputs.push(
275
html`<div class="row">
276
${importButton}
277
${results}
278
</div>`);
279
if (datasetModel.additional_html) {
280
renderedInputs.push(html`<div class="additional-html-container">
281
${unsafeHTML(datasetModel.additional_html)}
282
</div>`)
283
}
284
return renderedInputs;
285
}
286
}
287
288
// Without this check, there's a component registry issue when developing locally.
289
if (!customElements.get(SearchBar.componentName)) {
290
customElements.define(SearchBar.componentName, SearchBar);
291
}
292
293
async function render({ model, el }: RenderProps<SearchBarModel>) {
294
loadFonts();
295
const row = <SearchBar>(
296
document.createElement(SearchBar.componentName)
297
);
298
row.model = model;
299
el.appendChild(row);
300
}
301
302
export default { render };
303
304