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 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
- const results = await component.searchRequest(queryParams, abortController.signal);
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
- if (noLang) results.unshift(noLang);
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 (noLang) this.addOption(noLang);
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
- // Always make the no-lang option available for new selections
5857
- if (noLang) this.addOption(noLang);
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 idsToFetch = noLang ? ids.filter(id => id !== noLang.id) : ids;
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");
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
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','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
- const results = await component.searchRequest(queryParams, abortController.signal);
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
- if (noLang) results.unshift(noLang);
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 (noLang) this.addOption(noLang);
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
- // Always make the no-lang option available for new selections
214
- if (noLang) this.addOption(noLang);
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 idsToFetch = noLang ? ids.filter(id => id !== noLang.id) : ids;
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");