lucos_search_component 1.0.51 → 1.0.53
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 +63 -11
- package/example/index.html +1 -0
- package/package.json +1 -1
- package/web-components/lucos-search.js +63 -11
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','data-no-lang'];
|
|
5649
|
+
return ['data-api-key','data-types','data-exclude-types','data-no-lang','data-common'];
|
|
5650
5650
|
}
|
|
5651
5651
|
constructor() {
|
|
5652
5652
|
super();
|
|
@@ -5794,13 +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
|
+
...(component.isLanguageMode || component.getAttribute("data-common") ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
|
|
5798
5798
|
valueField: 'id',
|
|
5799
5799
|
labelField: 'pref_label',
|
|
5800
5800
|
searchField: [],
|
|
5801
5801
|
closeAfterSelect: true,
|
|
5802
5802
|
highlight: false, // Will use typesense's hightlight (as it can consider other fields)
|
|
5803
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
|
+
|
|
5804
5811
|
errorMessage.setAttribute('hidden', '');
|
|
5805
5812
|
const queryParams = new URLSearchParams({
|
|
5806
5813
|
q: query,
|
|
@@ -5811,15 +5818,25 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5811
5818
|
queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
|
|
5812
5819
|
}
|
|
5813
5820
|
try {
|
|
5814
|
-
|
|
5821
|
+
let results = await component.searchRequest(queryParams, abortController.signal);
|
|
5822
|
+
if (abortController.signal.aborted) return;
|
|
5815
5823
|
this.clearOptions();
|
|
5816
5824
|
if (component.isLanguageMode) {
|
|
5817
5825
|
results.forEach(r => { if (!r.lang_family) r.lang_family = 'qli'; });
|
|
5818
5826
|
}
|
|
5827
|
+
// Remove common items from results to avoid duplication (they're always shown separately)
|
|
5828
|
+
if (component._commonOptions) {
|
|
5829
|
+
const commonIds = new Set(component._commonOptions.map(o => o.id));
|
|
5830
|
+
results = results.filter(r => !commonIds.has(r.id));
|
|
5831
|
+
component._commonOptions.forEach(opt => this.addOption(opt));
|
|
5832
|
+
}
|
|
5819
5833
|
const noLang = component.noLangOption;
|
|
5820
|
-
|
|
5834
|
+
// Don't add noLang as standalone if it's already covered by a common item
|
|
5835
|
+
const noLangIsCommon = noLang && component._commonOptions && component._commonOptions.some(o => o.id === noLang.id);
|
|
5836
|
+
if (noLang && !noLangIsCommon) results.unshift(noLang);
|
|
5821
5837
|
callback(results);
|
|
5822
5838
|
} catch(err) {
|
|
5839
|
+
if (err.name === 'AbortError') return;
|
|
5823
5840
|
callback([]);
|
|
5824
5841
|
errorMessage.textContent = err.userMessage || 'Search is currently unavailable — please try again later.';
|
|
5825
5842
|
errorMessage.removeAttribute('hidden');
|
|
@@ -5837,15 +5854,35 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5837
5854
|
},
|
|
5838
5855
|
onFocus: function() {
|
|
5839
5856
|
this.clearOptions();
|
|
5857
|
+
// Re-add common items first so they own any shared IDs (e.g. zxx in both data-no-lang and data-common)
|
|
5858
|
+
if (component._commonOptions) {
|
|
5859
|
+
component._commonOptions.forEach(opt => this.addOption(opt));
|
|
5860
|
+
}
|
|
5840
5861
|
const noLang = component.noLangOption;
|
|
5841
|
-
if (
|
|
5862
|
+
// Skip noLang if its ID is already a common item (would be silently discarded as a duplicate)
|
|
5863
|
+
const noLangIsCommon = noLang && component._commonOptions && component._commonOptions.some(o => o.id === noLang.id);
|
|
5864
|
+
if (noLang && !noLangIsCommon) this.addOption(noLang);
|
|
5842
5865
|
},
|
|
5843
5866
|
// On startup, update any existing options with latest data from search
|
|
5844
5867
|
onInitialize: async function() {
|
|
5845
5868
|
const ids = Object.keys(this.options);
|
|
5846
5869
|
const noLang = component.noLangOption;
|
|
5847
|
-
//
|
|
5848
|
-
|
|
5870
|
+
// Fetch and register common items (x-common group goes first)
|
|
5871
|
+
const commonIds = component.commonIds;
|
|
5872
|
+
if (commonIds.length > 0) {
|
|
5873
|
+
this.addOptionGroup('x-common', { label: 'Common' });
|
|
5874
|
+
const commonParams = new URLSearchParams({
|
|
5875
|
+
q: '*',
|
|
5876
|
+
filter_by: `id:[${commonIds.join(",")}]`,
|
|
5877
|
+
per_page: commonIds.length,
|
|
5878
|
+
});
|
|
5879
|
+
const commonResults = await component.searchRequest(commonParams);
|
|
5880
|
+
component._commonOptions = commonResults.map(r => ({...r, lang_family: 'x-common'}));
|
|
5881
|
+
component._commonOptions.forEach(opt => this.addOption(opt));
|
|
5882
|
+
}
|
|
5883
|
+
// Add noLang option now (after common items) so we can check for overlap
|
|
5884
|
+
const noLangIsCommon = noLang && component._commonOptions && component._commonOptions.some(o => o.id === noLang.id);
|
|
5885
|
+
if (noLang && !noLangIsCommon) this.addOption(noLang);
|
|
5849
5886
|
// In language mode, fetch families and register option groups
|
|
5850
5887
|
if (component.isLanguageMode) {
|
|
5851
5888
|
const families = await component.getLanguageFamilies();
|
|
@@ -5855,8 +5892,9 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5855
5892
|
});
|
|
5856
5893
|
}
|
|
5857
5894
|
if (ids.length < 1) return;
|
|
5858
|
-
// Fetch real options from Typesense, excluding the synthetic no-lang option
|
|
5859
|
-
const
|
|
5895
|
+
// Fetch real options from Typesense, excluding the synthetic no-lang option and common items
|
|
5896
|
+
const excludeIds = new Set([...(noLang ? [noLang.id] : []), ...commonIds]);
|
|
5897
|
+
const idsToFetch = ids.filter(id => !excludeIds.has(id));
|
|
5860
5898
|
if (idsToFetch.length > 0) {
|
|
5861
5899
|
const searchParams = new URLSearchParams({
|
|
5862
5900
|
q: '*',
|
|
@@ -5873,6 +5911,12 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5873
5911
|
if (noLang && ids.includes(noLang.id)) {
|
|
5874
5912
|
this.updateOption(noLang.id, noLang);
|
|
5875
5913
|
}
|
|
5914
|
+
// Update any pre-selected common items with fresh data
|
|
5915
|
+
if (component._commonOptions) {
|
|
5916
|
+
component._commonOptions.forEach(opt => {
|
|
5917
|
+
if (ids.includes(opt.id)) this.updateOption(opt.id, opt);
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5876
5920
|
},
|
|
5877
5921
|
onItemSelect: function (item) {
|
|
5878
5922
|
// Tom-select prevents clicking on link in an item to work as normal, so force it here
|
|
@@ -5919,6 +5963,11 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5919
5963
|
if (!types) return false;
|
|
5920
5964
|
return types.split(",").map(t => t.trim()).includes("Language");
|
|
5921
5965
|
}
|
|
5966
|
+
get commonIds() {
|
|
5967
|
+
const common = this.getAttribute("data-common");
|
|
5968
|
+
if (!common) return [];
|
|
5969
|
+
return common.split(",").map(s => s.trim()).filter(Boolean);
|
|
5970
|
+
}
|
|
5922
5971
|
async getLanguageFamilies() {
|
|
5923
5972
|
if (this._langFamilies) return this._langFamilies;
|
|
5924
5973
|
const key = this.getAttribute("data-api-key");
|
|
@@ -5952,7 +6001,7 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5952
6001
|
}
|
|
5953
6002
|
return this._langFamilies;
|
|
5954
6003
|
}
|
|
5955
|
-
async searchRequest(searchParams) {
|
|
6004
|
+
async searchRequest(searchParams, abortSignal = null) {
|
|
5956
6005
|
const key = this.getAttribute("data-api-key");
|
|
5957
6006
|
if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
|
|
5958
6007
|
searchParams.set('query_by', "pref_label,labels,description,lyrics");
|
|
@@ -5963,13 +6012,16 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5963
6012
|
searchParams.set('enable_highlight_v1', false);
|
|
5964
6013
|
searchParams.set('highlight_start_tag', '<span class="highlight">');
|
|
5965
6014
|
searchParams.set('highlight_end_tag', '</span>');
|
|
6015
|
+
const timeoutSignal = AbortSignal.timeout(8000);
|
|
6016
|
+
const signal = abortSignal ? AbortSignal.any([timeoutSignal, abortSignal]) : timeoutSignal;
|
|
5966
6017
|
let response;
|
|
5967
6018
|
try {
|
|
5968
6019
|
response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
|
|
5969
6020
|
headers: { 'X-TYPESENSE-API-KEY': key },
|
|
5970
|
-
signal
|
|
6021
|
+
signal,
|
|
5971
6022
|
});
|
|
5972
6023
|
} catch(err) {
|
|
6024
|
+
if (err.name === 'AbortError') throw err; // Pass through so caller can detect cancellation
|
|
5973
6025
|
const userMessage = err.name === 'TimeoutError'
|
|
5974
6026
|
? 'Search timed out — please try again later.'
|
|
5975
6027
|
: 'Search is currently unavailable — please try again later.';
|
package/example/index.html
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
<option selected>https://eolas.l42.eu/metadata/language/ain/</option>
|
|
15
15
|
</select></span>
|
|
16
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="search9">Languages with common pinned:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Language" data-no-lang="No Language" data-common="https://eolas.l42.eu/metadata/language/en/,https://eolas.l42.eu/metadata/language/ga/,https://eolas.l42.eu/metadata/language/zxx/"><select id="search9"></select></span>
|
|
17
18
|
<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
19
|
<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>
|
|
19
20
|
<label for="search5">More than 10:</label>
|
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','data-no-lang'];
|
|
6
|
+
return ['data-api-key','data-types','data-exclude-types','data-no-lang','data-common'];
|
|
7
7
|
}
|
|
8
8
|
constructor() {
|
|
9
9
|
super();
|
|
@@ -151,13 +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
|
+
...(component.isLanguageMode || component.getAttribute("data-common") ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
|
|
155
155
|
valueField: 'id',
|
|
156
156
|
labelField: 'pref_label',
|
|
157
157
|
searchField: [],
|
|
158
158
|
closeAfterSelect: true,
|
|
159
159
|
highlight: false, // Will use typesense's hightlight (as it can consider other fields)
|
|
160
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
|
+
|
|
161
168
|
errorMessage.setAttribute('hidden', '');
|
|
162
169
|
const queryParams = new URLSearchParams({
|
|
163
170
|
q: query,
|
|
@@ -168,15 +175,25 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
168
175
|
queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
|
|
169
176
|
}
|
|
170
177
|
try {
|
|
171
|
-
|
|
178
|
+
let results = await component.searchRequest(queryParams, abortController.signal);
|
|
179
|
+
if (abortController.signal.aborted) return;
|
|
172
180
|
this.clearOptions();
|
|
173
181
|
if (component.isLanguageMode) {
|
|
174
182
|
results.forEach(r => { if (!r.lang_family) r.lang_family = 'qli'; });
|
|
175
183
|
}
|
|
184
|
+
// Remove common items from results to avoid duplication (they're always shown separately)
|
|
185
|
+
if (component._commonOptions) {
|
|
186
|
+
const commonIds = new Set(component._commonOptions.map(o => o.id));
|
|
187
|
+
results = results.filter(r => !commonIds.has(r.id));
|
|
188
|
+
component._commonOptions.forEach(opt => this.addOption(opt));
|
|
189
|
+
}
|
|
176
190
|
const noLang = component.noLangOption;
|
|
177
|
-
|
|
191
|
+
// Don't add noLang as standalone if it's already covered by a common item
|
|
192
|
+
const noLangIsCommon = noLang && component._commonOptions && component._commonOptions.some(o => o.id === noLang.id);
|
|
193
|
+
if (noLang && !noLangIsCommon) results.unshift(noLang);
|
|
178
194
|
callback(results);
|
|
179
195
|
} catch(err) {
|
|
196
|
+
if (err.name === 'AbortError') return;
|
|
180
197
|
callback([]);
|
|
181
198
|
errorMessage.textContent = err.userMessage || 'Search is currently unavailable — please try again later.';
|
|
182
199
|
errorMessage.removeAttribute('hidden');
|
|
@@ -194,15 +211,35 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
194
211
|
},
|
|
195
212
|
onFocus: function() {
|
|
196
213
|
this.clearOptions();
|
|
214
|
+
// Re-add common items first so they own any shared IDs (e.g. zxx in both data-no-lang and data-common)
|
|
215
|
+
if (component._commonOptions) {
|
|
216
|
+
component._commonOptions.forEach(opt => this.addOption(opt));
|
|
217
|
+
}
|
|
197
218
|
const noLang = component.noLangOption;
|
|
198
|
-
if (
|
|
219
|
+
// Skip noLang if its ID is already a common item (would be silently discarded as a duplicate)
|
|
220
|
+
const noLangIsCommon = noLang && component._commonOptions && component._commonOptions.some(o => o.id === noLang.id);
|
|
221
|
+
if (noLang && !noLangIsCommon) this.addOption(noLang);
|
|
199
222
|
},
|
|
200
223
|
// On startup, update any existing options with latest data from search
|
|
201
224
|
onInitialize: async function() {
|
|
202
225
|
const ids = Object.keys(this.options);
|
|
203
226
|
const noLang = component.noLangOption;
|
|
204
|
-
//
|
|
205
|
-
|
|
227
|
+
// Fetch and register common items (x-common group goes first)
|
|
228
|
+
const commonIds = component.commonIds;
|
|
229
|
+
if (commonIds.length > 0) {
|
|
230
|
+
this.addOptionGroup('x-common', { label: 'Common' });
|
|
231
|
+
const commonParams = new URLSearchParams({
|
|
232
|
+
q: '*',
|
|
233
|
+
filter_by: `id:[${commonIds.join(",")}]`,
|
|
234
|
+
per_page: commonIds.length,
|
|
235
|
+
});
|
|
236
|
+
const commonResults = await component.searchRequest(commonParams);
|
|
237
|
+
component._commonOptions = commonResults.map(r => ({...r, lang_family: 'x-common'}));
|
|
238
|
+
component._commonOptions.forEach(opt => this.addOption(opt));
|
|
239
|
+
}
|
|
240
|
+
// Add noLang option now (after common items) so we can check for overlap
|
|
241
|
+
const noLangIsCommon = noLang && component._commonOptions && component._commonOptions.some(o => o.id === noLang.id);
|
|
242
|
+
if (noLang && !noLangIsCommon) this.addOption(noLang);
|
|
206
243
|
// In language mode, fetch families and register option groups
|
|
207
244
|
if (component.isLanguageMode) {
|
|
208
245
|
const families = await component.getLanguageFamilies();
|
|
@@ -212,8 +249,9 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
212
249
|
});
|
|
213
250
|
}
|
|
214
251
|
if (ids.length < 1) return;
|
|
215
|
-
// Fetch real options from Typesense, excluding the synthetic no-lang option
|
|
216
|
-
const
|
|
252
|
+
// Fetch real options from Typesense, excluding the synthetic no-lang option and common items
|
|
253
|
+
const excludeIds = new Set([...(noLang ? [noLang.id] : []), ...commonIds]);
|
|
254
|
+
const idsToFetch = ids.filter(id => !excludeIds.has(id));
|
|
217
255
|
if (idsToFetch.length > 0) {
|
|
218
256
|
const searchParams = new URLSearchParams({
|
|
219
257
|
q: '*',
|
|
@@ -230,6 +268,12 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
230
268
|
if (noLang && ids.includes(noLang.id)) {
|
|
231
269
|
this.updateOption(noLang.id, noLang);
|
|
232
270
|
}
|
|
271
|
+
// Update any pre-selected common items with fresh data
|
|
272
|
+
if (component._commonOptions) {
|
|
273
|
+
component._commonOptions.forEach(opt => {
|
|
274
|
+
if (ids.includes(opt.id)) this.updateOption(opt.id, opt);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
233
277
|
},
|
|
234
278
|
onItemSelect: function (item) {
|
|
235
279
|
// Tom-select prevents clicking on link in an item to work as normal, so force it here
|
|
@@ -276,6 +320,11 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
276
320
|
if (!types) return false;
|
|
277
321
|
return types.split(",").map(t => t.trim()).includes("Language");
|
|
278
322
|
}
|
|
323
|
+
get commonIds() {
|
|
324
|
+
const common = this.getAttribute("data-common");
|
|
325
|
+
if (!common) return [];
|
|
326
|
+
return common.split(",").map(s => s.trim()).filter(Boolean);
|
|
327
|
+
}
|
|
279
328
|
async getLanguageFamilies() {
|
|
280
329
|
if (this._langFamilies) return this._langFamilies;
|
|
281
330
|
const key = this.getAttribute("data-api-key");
|
|
@@ -309,7 +358,7 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
309
358
|
}
|
|
310
359
|
return this._langFamilies;
|
|
311
360
|
}
|
|
312
|
-
async searchRequest(searchParams) {
|
|
361
|
+
async searchRequest(searchParams, abortSignal = null) {
|
|
313
362
|
const key = this.getAttribute("data-api-key");
|
|
314
363
|
if (!key) throw new Error("No `data-api-key` attribute set on `lucos-search` component");
|
|
315
364
|
searchParams.set('query_by', "pref_label,labels,description,lyrics");
|
|
@@ -320,13 +369,16 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
320
369
|
searchParams.set('enable_highlight_v1', false);
|
|
321
370
|
searchParams.set('highlight_start_tag', '<span class="highlight">')
|
|
322
371
|
searchParams.set('highlight_end_tag', '</span>');
|
|
372
|
+
const timeoutSignal = AbortSignal.timeout(8000);
|
|
373
|
+
const signal = abortSignal ? AbortSignal.any([timeoutSignal, abortSignal]) : timeoutSignal;
|
|
323
374
|
let response;
|
|
324
375
|
try {
|
|
325
376
|
response = await fetch("https://arachne.l42.eu/search?"+searchParams.toString(), {
|
|
326
377
|
headers: { 'X-TYPESENSE-API-KEY': key },
|
|
327
|
-
signal
|
|
378
|
+
signal,
|
|
328
379
|
});
|
|
329
380
|
} catch(err) {
|
|
381
|
+
if (err.name === 'AbortError') throw err; // Pass through so caller can detect cancellation
|
|
330
382
|
const userMessage = err.name === 'TimeoutError'
|
|
331
383
|
? 'Search timed out — please try again later.'
|
|
332
384
|
: 'Search is currently unavailable — please try again later.';
|