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 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
- const searchParams = new URLSearchParams({
5840
- q: '*',
5841
- filter_by: `id:[${ids.join(",")}]`,
5842
- per_page: ids.length,
5843
- });
5844
- const results = await component.searchRequest(searchParams);
5845
- results.forEach(result => {
5846
- this.updateOption(result.id, result);
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
- async searchRequest(searchParams) {
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: AbortSignal.timeout(8000),
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.';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "description": "Web Components for searching lucOS data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- const searchParams = new URLSearchParams({
197
- q: '*',
198
- filter_by: `id:[${ids.join(",")}]`,
199
- per_page: ids.length,
200
- });
201
- const results = await component.searchRequest(searchParams);
202
- results.forEach(result => {
203
- this.updateOption(result.id, result);
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
- async searchRequest(searchParams) {
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: AbortSignal.timeout(8000),
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.';