lucos_search_component 1.0.50 → 1.0.52
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.
- package/dist/index.js +104 -14
- package/example/index.html +3 -0
- package/package.json +1 -1
- package/web-components/lucos-search.js +104 -14
package/dist/index.js
CHANGED
|
@@ -5646,7 +5646,7 @@ var tomSelectStylesheet = "/**\n * tom-select.css (v2.5.2)\n * Copyright (c) con
|
|
|
5646
5646
|
|
|
5647
5647
|
class LucosSearchComponent extends HTMLSpanElement {
|
|
5648
5648
|
static get observedAttributes() {
|
|
5649
|
-
return ['data-api-key','data-types','data-exclude-types'];
|
|
5649
|
+
return ['data-api-key','data-types','data-exclude-types','data-no-lang'];
|
|
5650
5650
|
}
|
|
5651
5651
|
constructor() {
|
|
5652
5652
|
super();
|
|
@@ -5794,12 +5794,20 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5794
5794
|
if (!selector) throw new Error("Can't find select element in lucos-search");
|
|
5795
5795
|
selector.setAttribute("multiple", "multiple");
|
|
5796
5796
|
new TomSelect(selector, {
|
|
5797
|
+
...(component.isLanguageMode ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
|
|
5797
5798
|
valueField: 'id',
|
|
5798
5799
|
labelField: 'pref_label',
|
|
5799
5800
|
searchField: [],
|
|
5800
5801
|
closeAfterSelect: true,
|
|
5801
5802
|
highlight: false, // Will use typesense's hightlight (as it can consider other fields)
|
|
5802
5803
|
load: async function(query, callback) {
|
|
5804
|
+
// Cancel any in-flight search so stale responses don't overwrite newer results
|
|
5805
|
+
if (component._searchAbortController) {
|
|
5806
|
+
component._searchAbortController.abort();
|
|
5807
|
+
}
|
|
5808
|
+
const abortController = new AbortController();
|
|
5809
|
+
component._searchAbortController = abortController;
|
|
5810
|
+
|
|
5803
5811
|
errorMessage.setAttribute('hidden', '');
|
|
5804
5812
|
const queryParams = new URLSearchParams({
|
|
5805
5813
|
q: query,
|
|
@@ -5810,10 +5818,17 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5810
5818
|
queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
|
|
5811
5819
|
}
|
|
5812
5820
|
try {
|
|
5813
|
-
const results = await component.searchRequest(queryParams);
|
|
5821
|
+
const results = await component.searchRequest(queryParams, abortController.signal);
|
|
5822
|
+
if (abortController.signal.aborted) return;
|
|
5814
5823
|
this.clearOptions();
|
|
5824
|
+
if (component.isLanguageMode) {
|
|
5825
|
+
results.forEach(r => { if (!r.lang_family) r.lang_family = 'qli'; });
|
|
5826
|
+
}
|
|
5827
|
+
const noLang = component.noLangOption;
|
|
5828
|
+
if (noLang) results.unshift(noLang);
|
|
5815
5829
|
callback(results);
|
|
5816
5830
|
} catch(err) {
|
|
5831
|
+
if (err.name === 'AbortError') return;
|
|
5817
5832
|
callback([]);
|
|
5818
5833
|
errorMessage.textContent = err.userMessage || 'Search is currently unavailable — please try again later.';
|
|
5819
5834
|
errorMessage.removeAttribute('hidden');
|
|
@@ -5831,20 +5846,42 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5831
5846
|
},
|
|
5832
5847
|
onFocus: function() {
|
|
5833
5848
|
this.clearOptions();
|
|
5849
|
+
const noLang = component.noLangOption;
|
|
5850
|
+
if (noLang) this.addOption(noLang);
|
|
5834
5851
|
},
|
|
5835
5852
|
// On startup, update any existing options with latest data from search
|
|
5836
5853
|
onInitialize: async function() {
|
|
5837
5854
|
const ids = Object.keys(this.options);
|
|
5855
|
+
const noLang = component.noLangOption;
|
|
5856
|
+
// Always make the no-lang option available for new selections
|
|
5857
|
+
if (noLang) this.addOption(noLang);
|
|
5858
|
+
// In language mode, fetch families and register option groups
|
|
5859
|
+
if (component.isLanguageMode) {
|
|
5860
|
+
const families = await component.getLanguageFamilies();
|
|
5861
|
+
this.addOptionGroup('qli', { label: 'Language Isolate' });
|
|
5862
|
+
families.forEach(family => {
|
|
5863
|
+
this.addOptionGroup(family.code, { label: family.label });
|
|
5864
|
+
});
|
|
5865
|
+
}
|
|
5838
5866
|
if (ids.length < 1) return;
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5867
|
+
// Fetch real options from Typesense, excluding the synthetic no-lang option
|
|
5868
|
+
const idsToFetch = noLang ? ids.filter(id => id !== noLang.id) : ids;
|
|
5869
|
+
if (idsToFetch.length > 0) {
|
|
5870
|
+
const searchParams = new URLSearchParams({
|
|
5871
|
+
q: '*',
|
|
5872
|
+
filter_by: `id:[${idsToFetch.join(",")}]`,
|
|
5873
|
+
per_page: idsToFetch.length,
|
|
5874
|
+
});
|
|
5875
|
+
const results = await component.searchRequest(searchParams);
|
|
5876
|
+
results.forEach(result => {
|
|
5877
|
+
if (component.isLanguageMode && !result.lang_family) result.lang_family = 'qli';
|
|
5878
|
+
this.updateOption(result.id, result);
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
// Update any pre-selected no-lang option with the synthetic data
|
|
5882
|
+
if (noLang && ids.includes(noLang.id)) {
|
|
5883
|
+
this.updateOption(noLang.id, noLang);
|
|
5884
|
+
}
|
|
5848
5885
|
},
|
|
5849
5886
|
onItemSelect: function (item) {
|
|
5850
5887
|
// Tom-select prevents clicking on link in an item to work as normal, so force it here
|
|
@@ -5874,24 +5911,77 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5874
5911
|
shadow.append(selector.nextElementSibling);
|
|
5875
5912
|
}
|
|
5876
5913
|
}
|
|
5877
|
-
|
|
5914
|
+
get noLangOption() {
|
|
5915
|
+
const label = this.getAttribute("data-no-lang");
|
|
5916
|
+
if (!label) return null;
|
|
5917
|
+
return {
|
|
5918
|
+
id: 'https://eolas.l42.eu/metadata/language/zxx/',
|
|
5919
|
+
pref_label: label,
|
|
5920
|
+
type: 'Language',
|
|
5921
|
+
category: 'Anthropological',
|
|
5922
|
+
labels: [],
|
|
5923
|
+
highlight: {},
|
|
5924
|
+
};
|
|
5925
|
+
}
|
|
5926
|
+
get isLanguageMode() {
|
|
5927
|
+
const types = this.getAttribute("data-types");
|
|
5928
|
+
if (!types) return false;
|
|
5929
|
+
return types.split(",").map(t => t.trim()).includes("Language");
|
|
5930
|
+
}
|
|
5931
|
+
async getLanguageFamilies() {
|
|
5932
|
+
if (this._langFamilies) return this._langFamilies;
|
|
5933
|
+
const key = this.getAttribute("data-api-key");
|
|
5934
|
+
if (!key) { this._langFamilies = []; return []; }
|
|
5935
|
+
const searchParams = new URLSearchParams({
|
|
5936
|
+
q: '*',
|
|
5937
|
+
filter_by: 'type:=Language Family',
|
|
5938
|
+
query_by: 'pref_label',
|
|
5939
|
+
include_fields: 'id,pref_label',
|
|
5940
|
+
sort_by: 'pref_label:asc',
|
|
5941
|
+
enable_highlight_v1: false,
|
|
5942
|
+
per_page: 250,
|
|
5943
|
+
});
|
|
5944
|
+
try {
|
|
5945
|
+
const response = await fetch("https://arachne.l42.eu/search?" + searchParams.toString(), {
|
|
5946
|
+
headers: { 'X-TYPESENSE-API-KEY': key },
|
|
5947
|
+
signal: AbortSignal.timeout(8000),
|
|
5948
|
+
});
|
|
5949
|
+
if (!response.ok) { this._langFamilies = []; return []; }
|
|
5950
|
+
const data = await response.json();
|
|
5951
|
+
this._langFamilies = data.hits.map(hit => ({
|
|
5952
|
+
// The lang_family field on Language documents in Typesense stores the last
|
|
5953
|
+
// path segment of the Language Family URI (e.g. "gmw" for West Germanic at
|
|
5954
|
+
// http://id.loc.gov/vocabulary/iso639-5/gmw). filter(Boolean) strips any
|
|
5955
|
+
// trailing slash. This is consistent with lucos-lang's family code extraction.
|
|
5956
|
+
code: hit.document.id.split("/").filter(Boolean).pop(),
|
|
5957
|
+
label: hit.document.pref_label,
|
|
5958
|
+
}));
|
|
5959
|
+
} catch (_) {
|
|
5960
|
+
this._langFamilies = [];
|
|
5961
|
+
}
|
|
5962
|
+
return this._langFamilies;
|
|
5963
|
+
}
|
|
5964
|
+
async searchRequest(searchParams, abortSignal = null) {
|
|
5878
5965
|
const key = this.getAttribute("data-api-key");
|
|
5879
5966
|
if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
|
|
5880
5967
|
searchParams.set('query_by', "pref_label,labels,description,lyrics");
|
|
5881
5968
|
searchParams.set('query_by_weights', "10,8,3,1");
|
|
5882
5969
|
searchParams.set('sort_by', "_text_match:desc,pref_label:asc");
|
|
5883
5970
|
searchParams.set('prioritize_num_matching_fields', false);
|
|
5884
|
-
searchParams.set('include_fields', "id,pref_label,type,category,labels");
|
|
5971
|
+
searchParams.set('include_fields', "id,pref_label,type,category,labels,lang_family");
|
|
5885
5972
|
searchParams.set('enable_highlight_v1', false);
|
|
5886
5973
|
searchParams.set('highlight_start_tag', '<span class="highlight">');
|
|
5887
5974
|
searchParams.set('highlight_end_tag', '</span>');
|
|
5975
|
+
const timeoutSignal = AbortSignal.timeout(8000);
|
|
5976
|
+
const signal = abortSignal ? AbortSignal.any([timeoutSignal, abortSignal]) : timeoutSignal;
|
|
5888
5977
|
let response;
|
|
5889
5978
|
try {
|
|
5890
5979
|
response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
|
|
5891
5980
|
headers: { 'X-TYPESENSE-API-KEY': key },
|
|
5892
|
-
signal
|
|
5981
|
+
signal,
|
|
5893
5982
|
});
|
|
5894
5983
|
} catch(err) {
|
|
5984
|
+
if (err.name === 'AbortError') throw err; // Pass through so caller can detect cancellation
|
|
5895
5985
|
const userMessage = err.name === 'TimeoutError'
|
|
5896
5986
|
? 'Search timed out — please try again later.'
|
|
5897
5987
|
: 'Search is currently unavailable — please try again later.';
|
package/example/index.html
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
<option selected>https://media-metadata.l42.eu/tracks/13713</option>
|
|
14
14
|
<option selected>https://eolas.l42.eu/metadata/language/ain/</option>
|
|
15
15
|
</select></span>
|
|
16
|
+
<label for="search8">Languages grouped by family:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Language"><select id="search8"></select></span>
|
|
17
|
+
<label for="search6">Languages with no-lang option:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Language" data-no-lang="No Language"><select id="search6"></select></span>
|
|
18
|
+
<label for="search7">Languages with no-lang pre-selected:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Language" data-no-lang="No Language"><select id="search7" multiple><option selected>https://eolas.l42.eu/metadata/language/zxx/</option></select></span>
|
|
16
19
|
<label for="search5">More than 10:</label>
|
|
17
20
|
<span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-exclude_types="Track"><select id="search5" multiple>
|
|
18
21
|
<option selected>https://eolas.l42.eu/metadata/place/125/</option>
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import tomSelectStylesheet from 'tom-select/dist/css/tom-select.default.css';
|
|
|
3
3
|
|
|
4
4
|
class LucosSearchComponent extends HTMLSpanElement {
|
|
5
5
|
static get observedAttributes() {
|
|
6
|
-
return ['data-api-key','data-types','data-exclude-types'];
|
|
6
|
+
return ['data-api-key','data-types','data-exclude-types','data-no-lang'];
|
|
7
7
|
}
|
|
8
8
|
constructor() {
|
|
9
9
|
super();
|
|
@@ -151,12 +151,20 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
151
151
|
if (!selector) throw new Error("Can't find select element in lucos-search");
|
|
152
152
|
selector.setAttribute("multiple", "multiple");
|
|
153
153
|
new TomSelect(selector, {
|
|
154
|
+
...(component.isLanguageMode ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
|
|
154
155
|
valueField: 'id',
|
|
155
156
|
labelField: 'pref_label',
|
|
156
157
|
searchField: [],
|
|
157
158
|
closeAfterSelect: true,
|
|
158
159
|
highlight: false, // Will use typesense's hightlight (as it can consider other fields)
|
|
159
160
|
load: async function(query, callback) {
|
|
161
|
+
// Cancel any in-flight search so stale responses don't overwrite newer results
|
|
162
|
+
if (component._searchAbortController) {
|
|
163
|
+
component._searchAbortController.abort();
|
|
164
|
+
}
|
|
165
|
+
const abortController = new AbortController();
|
|
166
|
+
component._searchAbortController = abortController;
|
|
167
|
+
|
|
160
168
|
errorMessage.setAttribute('hidden', '');
|
|
161
169
|
const queryParams = new URLSearchParams({
|
|
162
170
|
q: query,
|
|
@@ -167,10 +175,17 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
167
175
|
queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
|
|
168
176
|
}
|
|
169
177
|
try {
|
|
170
|
-
const results = await component.searchRequest(queryParams);
|
|
178
|
+
const results = await component.searchRequest(queryParams, abortController.signal);
|
|
179
|
+
if (abortController.signal.aborted) return;
|
|
171
180
|
this.clearOptions();
|
|
181
|
+
if (component.isLanguageMode) {
|
|
182
|
+
results.forEach(r => { if (!r.lang_family) r.lang_family = 'qli'; });
|
|
183
|
+
}
|
|
184
|
+
const noLang = component.noLangOption;
|
|
185
|
+
if (noLang) results.unshift(noLang);
|
|
172
186
|
callback(results);
|
|
173
187
|
} catch(err) {
|
|
188
|
+
if (err.name === 'AbortError') return;
|
|
174
189
|
callback([]);
|
|
175
190
|
errorMessage.textContent = err.userMessage || 'Search is currently unavailable — please try again later.';
|
|
176
191
|
errorMessage.removeAttribute('hidden');
|
|
@@ -188,20 +203,42 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
188
203
|
},
|
|
189
204
|
onFocus: function() {
|
|
190
205
|
this.clearOptions();
|
|
206
|
+
const noLang = component.noLangOption;
|
|
207
|
+
if (noLang) this.addOption(noLang);
|
|
191
208
|
},
|
|
192
209
|
// On startup, update any existing options with latest data from search
|
|
193
210
|
onInitialize: async function() {
|
|
194
211
|
const ids = Object.keys(this.options);
|
|
212
|
+
const noLang = component.noLangOption;
|
|
213
|
+
// Always make the no-lang option available for new selections
|
|
214
|
+
if (noLang) this.addOption(noLang);
|
|
215
|
+
// In language mode, fetch families and register option groups
|
|
216
|
+
if (component.isLanguageMode) {
|
|
217
|
+
const families = await component.getLanguageFamilies();
|
|
218
|
+
this.addOptionGroup('qli', { label: 'Language Isolate' });
|
|
219
|
+
families.forEach(family => {
|
|
220
|
+
this.addOptionGroup(family.code, { label: family.label });
|
|
221
|
+
});
|
|
222
|
+
}
|
|
195
223
|
if (ids.length < 1) return;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
// Fetch real options from Typesense, excluding the synthetic no-lang option
|
|
225
|
+
const idsToFetch = noLang ? ids.filter(id => id !== noLang.id) : ids;
|
|
226
|
+
if (idsToFetch.length > 0) {
|
|
227
|
+
const searchParams = new URLSearchParams({
|
|
228
|
+
q: '*',
|
|
229
|
+
filter_by: `id:[${idsToFetch.join(",")}]`,
|
|
230
|
+
per_page: idsToFetch.length,
|
|
231
|
+
});
|
|
232
|
+
const results = await component.searchRequest(searchParams);
|
|
233
|
+
results.forEach(result => {
|
|
234
|
+
if (component.isLanguageMode && !result.lang_family) result.lang_family = 'qli';
|
|
235
|
+
this.updateOption(result.id, result);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Update any pre-selected no-lang option with the synthetic data
|
|
239
|
+
if (noLang && ids.includes(noLang.id)) {
|
|
240
|
+
this.updateOption(noLang.id, noLang);
|
|
241
|
+
}
|
|
205
242
|
},
|
|
206
243
|
onItemSelect: function (item) {
|
|
207
244
|
// Tom-select prevents clicking on link in an item to work as normal, so force it here
|
|
@@ -231,24 +268,77 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
231
268
|
shadow.append(selector.nextElementSibling);
|
|
232
269
|
}
|
|
233
270
|
}
|
|
234
|
-
|
|
271
|
+
get noLangOption() {
|
|
272
|
+
const label = this.getAttribute("data-no-lang");
|
|
273
|
+
if (!label) return null;
|
|
274
|
+
return {
|
|
275
|
+
id: 'https://eolas.l42.eu/metadata/language/zxx/',
|
|
276
|
+
pref_label: label,
|
|
277
|
+
type: 'Language',
|
|
278
|
+
category: 'Anthropological',
|
|
279
|
+
labels: [],
|
|
280
|
+
highlight: {},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
get isLanguageMode() {
|
|
284
|
+
const types = this.getAttribute("data-types");
|
|
285
|
+
if (!types) return false;
|
|
286
|
+
return types.split(",").map(t => t.trim()).includes("Language");
|
|
287
|
+
}
|
|
288
|
+
async getLanguageFamilies() {
|
|
289
|
+
if (this._langFamilies) return this._langFamilies;
|
|
290
|
+
const key = this.getAttribute("data-api-key");
|
|
291
|
+
if (!key) { this._langFamilies = []; return []; }
|
|
292
|
+
const searchParams = new URLSearchParams({
|
|
293
|
+
q: '*',
|
|
294
|
+
filter_by: 'type:=Language Family',
|
|
295
|
+
query_by: 'pref_label',
|
|
296
|
+
include_fields: 'id,pref_label',
|
|
297
|
+
sort_by: 'pref_label:asc',
|
|
298
|
+
enable_highlight_v1: false,
|
|
299
|
+
per_page: 250,
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch("https://arachne.l42.eu/search?" + searchParams.toString(), {
|
|
303
|
+
headers: { 'X-TYPESENSE-API-KEY': key },
|
|
304
|
+
signal: AbortSignal.timeout(8000),
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) { this._langFamilies = []; return []; }
|
|
307
|
+
const data = await response.json();
|
|
308
|
+
this._langFamilies = data.hits.map(hit => ({
|
|
309
|
+
// The lang_family field on Language documents in Typesense stores the last
|
|
310
|
+
// path segment of the Language Family URI (e.g. "gmw" for West Germanic at
|
|
311
|
+
// http://id.loc.gov/vocabulary/iso639-5/gmw). filter(Boolean) strips any
|
|
312
|
+
// trailing slash. This is consistent with lucos-lang's family code extraction.
|
|
313
|
+
code: hit.document.id.split("/").filter(Boolean).pop(),
|
|
314
|
+
label: hit.document.pref_label,
|
|
315
|
+
}));
|
|
316
|
+
} catch (_) {
|
|
317
|
+
this._langFamilies = [];
|
|
318
|
+
}
|
|
319
|
+
return this._langFamilies;
|
|
320
|
+
}
|
|
321
|
+
async searchRequest(searchParams, abortSignal = null) {
|
|
235
322
|
const key = this.getAttribute("data-api-key");
|
|
236
323
|
if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
|
|
237
324
|
searchParams.set('query_by', "pref_label,labels,description,lyrics");
|
|
238
325
|
searchParams.set('query_by_weights', "10,8,3,1");
|
|
239
326
|
searchParams.set('sort_by', "_text_match:desc,pref_label:asc");
|
|
240
327
|
searchParams.set('prioritize_num_matching_fields', false);
|
|
241
|
-
searchParams.set('include_fields', "id,pref_label,type,category,labels");
|
|
328
|
+
searchParams.set('include_fields', "id,pref_label,type,category,labels,lang_family");
|
|
242
329
|
searchParams.set('enable_highlight_v1', false);
|
|
243
330
|
searchParams.set('highlight_start_tag', '<span class="highlight">')
|
|
244
331
|
searchParams.set('highlight_end_tag', '</span>');
|
|
332
|
+
const timeoutSignal = AbortSignal.timeout(8000);
|
|
333
|
+
const signal = abortSignal ? AbortSignal.any([timeoutSignal, abortSignal]) : timeoutSignal;
|
|
245
334
|
let response;
|
|
246
335
|
try {
|
|
247
336
|
response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
|
|
248
337
|
headers: { 'X-TYPESENSE-API-KEY': key },
|
|
249
|
-
signal
|
|
338
|
+
signal,
|
|
250
339
|
});
|
|
251
340
|
} catch(err) {
|
|
341
|
+
if (err.name === 'AbortError') throw err; // Pass through so caller can detect cancellation
|
|
252
342
|
const userMessage = err.name === 'TimeoutError'
|
|
253
343
|
? 'Search timed out — please try again later.'
|
|
254
344
|
: 'Search is currently unavailable — please try again later.';
|