lucos_search_component 1.0.52 → 1.0.54
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 +80 -9
- package/example/index.html +19 -0
- package/package.json +1 -1
- package/web-components/lucos-search.js +80 -9
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,7 +5794,7 @@ 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: [],
|
|
@@ -5818,14 +5818,22 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5818
5818
|
queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
|
|
5819
5819
|
}
|
|
5820
5820
|
try {
|
|
5821
|
-
|
|
5821
|
+
let results = await component.searchRequest(queryParams, abortController.signal);
|
|
5822
5822
|
if (abortController.signal.aborted) return;
|
|
5823
5823
|
this.clearOptions();
|
|
5824
5824
|
if (component.isLanguageMode) {
|
|
5825
5825
|
results.forEach(r => { if (!r.lang_family) r.lang_family = 'qli'; });
|
|
5826
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
|
+
}
|
|
5827
5833
|
const noLang = component.noLangOption;
|
|
5828
|
-
|
|
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);
|
|
5829
5837
|
callback(results);
|
|
5830
5838
|
} catch(err) {
|
|
5831
5839
|
if (err.name === 'AbortError') return;
|
|
@@ -5846,15 +5854,35 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5846
5854
|
},
|
|
5847
5855
|
onFocus: function() {
|
|
5848
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
|
+
}
|
|
5849
5861
|
const noLang = component.noLangOption;
|
|
5850
|
-
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);
|
|
5851
5865
|
},
|
|
5852
5866
|
// On startup, update any existing options with latest data from search
|
|
5853
5867
|
onInitialize: async function() {
|
|
5854
5868
|
const ids = Object.keys(this.options);
|
|
5855
5869
|
const noLang = component.noLangOption;
|
|
5856
|
-
//
|
|
5857
|
-
|
|
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);
|
|
5858
5886
|
// In language mode, fetch families and register option groups
|
|
5859
5887
|
if (component.isLanguageMode) {
|
|
5860
5888
|
const families = await component.getLanguageFamilies();
|
|
@@ -5864,8 +5892,9 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5864
5892
|
});
|
|
5865
5893
|
}
|
|
5866
5894
|
if (ids.length < 1) return;
|
|
5867
|
-
// Fetch real options from Typesense, excluding the synthetic no-lang option
|
|
5868
|
-
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));
|
|
5869
5898
|
if (idsToFetch.length > 0) {
|
|
5870
5899
|
const searchParams = new URLSearchParams({
|
|
5871
5900
|
q: '*',
|
|
@@ -5882,6 +5911,12 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5882
5911
|
if (noLang && ids.includes(noLang.id)) {
|
|
5883
5912
|
this.updateOption(noLang.id, noLang);
|
|
5884
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
|
+
}
|
|
5885
5920
|
},
|
|
5886
5921
|
onItemSelect: function (item) {
|
|
5887
5922
|
// Tom-select prevents clicking on link in an item to work as normal, so force it here
|
|
@@ -5911,6 +5946,37 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5911
5946
|
shadow.append(selector.nextElementSibling);
|
|
5912
5947
|
}
|
|
5913
5948
|
}
|
|
5949
|
+
connectedCallback() {
|
|
5950
|
+
const form = this.closest('form');
|
|
5951
|
+
if (!form) return;
|
|
5952
|
+
this._form = form;
|
|
5953
|
+
this._formdataHandler = (event) => {
|
|
5954
|
+
const selector = this.querySelector('select');
|
|
5955
|
+
if (!selector || !selector.name) return;
|
|
5956
|
+
const ts = selector.tomselect;
|
|
5957
|
+
if (!ts) return;
|
|
5958
|
+
const name = selector.name;
|
|
5959
|
+
const values = ts.getValue();
|
|
5960
|
+
const valueArray = Array.isArray(values) ? values : (values ? [values] : []);
|
|
5961
|
+
// Remove the native select values so consumers only receive the structured pairs
|
|
5962
|
+
event.formData.delete(name);
|
|
5963
|
+
valueArray.forEach((id, idx) => {
|
|
5964
|
+
const option = ts.options[id];
|
|
5965
|
+
if (option) {
|
|
5966
|
+
event.formData.append(`${name}[${idx}][uri]`, id);
|
|
5967
|
+
event.formData.append(`${name}[${idx}][name]`, option.pref_label);
|
|
5968
|
+
}
|
|
5969
|
+
});
|
|
5970
|
+
};
|
|
5971
|
+
form.addEventListener('formdata', this._formdataHandler);
|
|
5972
|
+
}
|
|
5973
|
+
disconnectedCallback() {
|
|
5974
|
+
if (this._form && this._formdataHandler) {
|
|
5975
|
+
this._form.removeEventListener('formdata', this._formdataHandler);
|
|
5976
|
+
this._form = null;
|
|
5977
|
+
this._formdataHandler = null;
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5914
5980
|
get noLangOption() {
|
|
5915
5981
|
const label = this.getAttribute("data-no-lang");
|
|
5916
5982
|
if (!label) return null;
|
|
@@ -5928,6 +5994,11 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
5928
5994
|
if (!types) return false;
|
|
5929
5995
|
return types.split(",").map(t => t.trim()).includes("Language");
|
|
5930
5996
|
}
|
|
5997
|
+
get commonIds() {
|
|
5998
|
+
const common = this.getAttribute("data-common");
|
|
5999
|
+
if (!common) return [];
|
|
6000
|
+
return common.split(",").map(s => s.trim()).filter(Boolean);
|
|
6001
|
+
}
|
|
5931
6002
|
async getLanguageFamilies() {
|
|
5932
6003
|
if (this._langFamilies) return this._langFamilies;
|
|
5933
6004
|
const key = this.getAttribute("data-api-key");
|
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>
|
|
@@ -45,6 +46,24 @@
|
|
|
45
46
|
<label for="lang3">Common languages option</label><span is="lucos-lang" data-api-key="${KEY_LUCOS_ARACHNE}" data-no-lang="No Language" data-common="en,ga,zxx">
|
|
46
47
|
<select id="lang3"></select>
|
|
47
48
|
</span>
|
|
49
|
+
<h1>Form submission</h1>
|
|
50
|
+
<form id="test-form" onsubmit="handleSubmit(event)">
|
|
51
|
+
<label for="tags">Tags:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}"><select id="tags" name="tags" multiple></select></span>
|
|
52
|
+
<button type="submit">Submit</button>
|
|
53
|
+
</form>
|
|
54
|
+
<pre id="form-output"></pre>
|
|
55
|
+
<script>
|
|
56
|
+
function handleSubmit(event) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
const data = new FormData(event.target);
|
|
59
|
+
const output = {};
|
|
60
|
+
for (const [key, value] of data.entries()) {
|
|
61
|
+
if (!output[key]) output[key] = [];
|
|
62
|
+
output[key].push(value);
|
|
63
|
+
}
|
|
64
|
+
document.getElementById('form-output').textContent = JSON.stringify(output, null, 2);
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
48
67
|
<script src="./built.js"></script>
|
|
49
68
|
</body>
|
|
50
69
|
</html>
|
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,7 +151,7 @@ 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: [],
|
|
@@ -175,14 +175,22 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
175
175
|
queryParams.set("filter_by",`type:![${component.getAttribute("data-exclude_types")}]`);
|
|
176
176
|
}
|
|
177
177
|
try {
|
|
178
|
-
|
|
178
|
+
let results = await component.searchRequest(queryParams, abortController.signal);
|
|
179
179
|
if (abortController.signal.aborted) return;
|
|
180
180
|
this.clearOptions();
|
|
181
181
|
if (component.isLanguageMode) {
|
|
182
182
|
results.forEach(r => { if (!r.lang_family) r.lang_family = 'qli'; });
|
|
183
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
|
+
}
|
|
184
190
|
const noLang = component.noLangOption;
|
|
185
|
-
|
|
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);
|
|
186
194
|
callback(results);
|
|
187
195
|
} catch(err) {
|
|
188
196
|
if (err.name === 'AbortError') return;
|
|
@@ -203,15 +211,35 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
203
211
|
},
|
|
204
212
|
onFocus: function() {
|
|
205
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
|
+
}
|
|
206
218
|
const noLang = component.noLangOption;
|
|
207
|
-
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);
|
|
208
222
|
},
|
|
209
223
|
// On startup, update any existing options with latest data from search
|
|
210
224
|
onInitialize: async function() {
|
|
211
225
|
const ids = Object.keys(this.options);
|
|
212
226
|
const noLang = component.noLangOption;
|
|
213
|
-
//
|
|
214
|
-
|
|
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);
|
|
215
243
|
// In language mode, fetch families and register option groups
|
|
216
244
|
if (component.isLanguageMode) {
|
|
217
245
|
const families = await component.getLanguageFamilies();
|
|
@@ -221,8 +249,9 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
221
249
|
});
|
|
222
250
|
}
|
|
223
251
|
if (ids.length < 1) return;
|
|
224
|
-
// Fetch real options from Typesense, excluding the synthetic no-lang option
|
|
225
|
-
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));
|
|
226
255
|
if (idsToFetch.length > 0) {
|
|
227
256
|
const searchParams = new URLSearchParams({
|
|
228
257
|
q: '*',
|
|
@@ -239,6 +268,12 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
239
268
|
if (noLang && ids.includes(noLang.id)) {
|
|
240
269
|
this.updateOption(noLang.id, noLang);
|
|
241
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
|
+
}
|
|
242
277
|
},
|
|
243
278
|
onItemSelect: function (item) {
|
|
244
279
|
// Tom-select prevents clicking on link in an item to work as normal, so force it here
|
|
@@ -268,6 +303,37 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
268
303
|
shadow.append(selector.nextElementSibling);
|
|
269
304
|
}
|
|
270
305
|
}
|
|
306
|
+
connectedCallback() {
|
|
307
|
+
const form = this.closest('form');
|
|
308
|
+
if (!form) return;
|
|
309
|
+
this._form = form;
|
|
310
|
+
this._formdataHandler = (event) => {
|
|
311
|
+
const selector = this.querySelector('select');
|
|
312
|
+
if (!selector || !selector.name) return;
|
|
313
|
+
const ts = selector.tomselect;
|
|
314
|
+
if (!ts) return;
|
|
315
|
+
const name = selector.name;
|
|
316
|
+
const values = ts.getValue();
|
|
317
|
+
const valueArray = Array.isArray(values) ? values : (values ? [values] : []);
|
|
318
|
+
// Remove the native select values so consumers only receive the structured pairs
|
|
319
|
+
event.formData.delete(name);
|
|
320
|
+
valueArray.forEach((id, idx) => {
|
|
321
|
+
const option = ts.options[id];
|
|
322
|
+
if (option) {
|
|
323
|
+
event.formData.append(`${name}[${idx}][uri]`, id);
|
|
324
|
+
event.formData.append(`${name}[${idx}][name]`, option.pref_label);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
form.addEventListener('formdata', this._formdataHandler);
|
|
329
|
+
}
|
|
330
|
+
disconnectedCallback() {
|
|
331
|
+
if (this._form && this._formdataHandler) {
|
|
332
|
+
this._form.removeEventListener('formdata', this._formdataHandler);
|
|
333
|
+
this._form = null;
|
|
334
|
+
this._formdataHandler = null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
271
337
|
get noLangOption() {
|
|
272
338
|
const label = this.getAttribute("data-no-lang");
|
|
273
339
|
if (!label) return null;
|
|
@@ -285,6 +351,11 @@ class LucosSearchComponent extends HTMLSpanElement {
|
|
|
285
351
|
if (!types) return false;
|
|
286
352
|
return types.split(",").map(t => t.trim()).includes("Language");
|
|
287
353
|
}
|
|
354
|
+
get commonIds() {
|
|
355
|
+
const common = this.getAttribute("data-common");
|
|
356
|
+
if (!common) return [];
|
|
357
|
+
return common.split(",").map(s => s.trim()).filter(Boolean);
|
|
358
|
+
}
|
|
288
359
|
async getLanguageFamilies() {
|
|
289
360
|
if (this._langFamilies) return this._langFamilies;
|
|
290
361
|
const key = this.getAttribute("data-api-key");
|