lucos_search_component 3.0.6 → 4.0.1

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/README.md CHANGED
@@ -36,6 +36,7 @@ The following attributes can be added to the lucos-search span:
36
36
  * **data-label-override-zxx** — Override the displayed label for the `https://eolas.l42.eu/metadata/language/zxx/` entry in both the dropdown and selected lozenge. Has no effect on whether `zxx` appears in the list — that depends on whether `zxx` is present in the search index. Only meaningful when `data-types="Language"`.
37
37
  * **data-common** — A comma separated list of item URIs to pin in a "Common" group at the top of the list, above the normal results. Only meaningful when `data-types="Language"`.
38
38
  * **data-preload** — If present, all options are loaded upfront rather than fetched on search. Suitable for small, finite datasets such as languages.
39
+ * **data-create** — Boolean presence attribute. When present, enables inline creation of new entities: if the user types a name with no matching arachne result, they can pick "Add new `<type>`: `<name>`…" from the dropdown. On form submit, the created entry is serialised as a name-only value (`[name]` set, `[uri]` omitted) so the consumer can route it to a create-on-write path. **Opt-in per instance** — controlled-vocabulary fields (e.g. language, offence) must not set this; only open entity types (e.g. Person) should allow inline creation. The `[uri]` + `[name]` serialisation for existing arachne-selected entries is unaffected.
39
40
 
40
41
  #### Language selector
41
42
 
package/dist/index.js CHANGED
@@ -5691,7 +5691,7 @@ TomSelect.define('virtual_scroll', plugin);
5691
5691
 
5692
5692
  var tomSelectStylesheet = "/**\n * tom-select.css (v2.6.1)\n * Copyright (c) contributors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this\n * file except in compliance with the License. You may obtain a copy of the License at:\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF\n * ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n *\n */\n.ts-control {\n border: 1px solid #d0d0d0;\n padding: 8px 8px;\n width: 100%;\n overflow: hidden;\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);\n border-radius: 3px;\n display: flex;\n flex-wrap: wrap;\n}\n.ts-wrapper.multi.has-items .ts-control {\n padding: calc(8px - 2px - 1px) 8px calc(8px - 2px - 3px - 1px);\n}\n.full .ts-control {\n background-color: #fff;\n}\n.disabled .ts-control, .disabled .ts-control * {\n cursor: default !important;\n}\n.focus .ts-control {\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);\n}\n.ts-control > * {\n vertical-align: baseline;\n display: inline-block;\n}\n.ts-wrapper.multi .ts-control > div {\n cursor: pointer;\n margin: 0 3px 3px 0;\n padding: 2px 6px;\n background: #1da7ee;\n color: #fff;\n border: 1px solid #0073bb;\n overflow: auto;\n}\n.ts-wrapper.multi .ts-control > div.active {\n background: #92c836;\n color: #fff;\n border: 1px solid #00578d;\n}\n.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {\n color: hsl(0, 0%, 130%);\n background: #d2d2d2;\n border: 1px solid #aaaaaa;\n}\n.ts-control > input {\n flex: 1 1 auto;\n min-width: 7rem;\n display: inline-block !important;\n padding: 0 !important;\n min-height: 0 !important;\n max-height: none !important;\n max-width: 100% !important;\n margin: 0 !important;\n text-indent: 0 !important;\n border: 0 none !important;\n background: none !important;\n line-height: inherit !important;\n -webkit-user-select: auto !important;\n -moz-user-select: auto !important;\n -ms-user-select: auto !important;\n user-select: auto !important;\n box-shadow: none !important;\n}\n.ts-control > input::-ms-clear {\n display: none;\n}\n.ts-control > input:focus {\n outline: none !important;\n}\n.has-items .ts-control > input {\n margin: 0px 4px !important;\n}\n.ts-control.rtl {\n text-align: right;\n}\n.ts-control.rtl.single .ts-control:after {\n left: 15px;\n right: auto;\n}\n.ts-control.rtl .ts-control > input {\n margin: 0px 4px 0px -2px !important;\n}\n.disabled .ts-control {\n opacity: 0.5;\n background-color: #fafafa;\n}\n.input-hidden .ts-control > input {\n opacity: 0;\n position: absolute;\n left: -10000px;\n}\n\n.ts-dropdown {\n position: absolute;\n top: 100%;\n left: 0;\n width: 100%;\n z-index: 10;\n border: 1px solid #d0d0d0;\n background: #fff;\n margin: 0.25rem 0 0;\n border-top: 0 none;\n box-sizing: border-box;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n border-radius: 0 0 3px 3px;\n}\n.ts-dropdown [data-selectable] {\n cursor: pointer;\n overflow: hidden;\n}\n.ts-dropdown [data-selectable] .highlight {\n background: rgba(125, 168, 208, 0.2);\n border-radius: 1px;\n}\n.ts-dropdown .option,\n.ts-dropdown .optgroup-header,\n.ts-dropdown .no-results,\n.ts-dropdown .create {\n padding: 5px 8px;\n}\n.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {\n cursor: inherit;\n opacity: 0.5;\n}\n.ts-dropdown [data-selectable].option {\n opacity: 1;\n cursor: pointer;\n}\n.ts-dropdown .optgroup:first-child .optgroup-header {\n border-top: 0 none;\n}\n.ts-dropdown .optgroup-header {\n color: #303030;\n background: #fff;\n cursor: default;\n}\n.ts-dropdown .active {\n background-color: #f5fafd;\n color: #495c68;\n}\n.ts-dropdown .active.create {\n color: #495c68;\n}\n.ts-dropdown .create {\n color: rgba(48, 48, 48, 0.5);\n}\n.ts-dropdown .spinner {\n display: inline-block;\n width: 30px;\n height: 30px;\n margin: 5px 8px;\n}\n.ts-dropdown .spinner::after {\n content: \" \";\n display: block;\n width: 24px;\n height: 24px;\n margin: 3px;\n border-radius: 50%;\n border: 5px solid #d0d0d0;\n border-color: #d0d0d0 transparent #d0d0d0 transparent;\n animation: lds-dual-ring 1.2s linear infinite;\n}\n@keyframes lds-dual-ring {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n\n.ts-dropdown-content {\n overflow: hidden auto;\n max-height: 200px;\n scroll-behavior: smooth;\n}\n\n.ts-wrapper.plugin-drag_drop .ts-dragging {\n color: transparent !important;\n}\n.ts-wrapper.plugin-drag_drop .ts-dragging > * {\n visibility: hidden !important;\n}\n\n.plugin-checkbox_options:not(.rtl) .option input {\n margin-right: 0.5rem;\n}\n\n.plugin-checkbox_options.rtl .option input {\n margin-left: 0.5rem;\n}\n\n/* stylelint-disable function-name-case */\n.plugin-clear_button {\n --ts-pr-clear-button: 1em;\n}\n.plugin-clear_button .clear-button {\n opacity: 0;\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n right: calc(8px - 6px);\n margin-right: 0 !important;\n background: transparent !important;\n transition: opacity 0.5s;\n cursor: pointer;\n}\n.plugin-clear_button.form-select .clear-button, .plugin-clear_button.single .clear-button {\n right: max(var(--ts-pr-caret), 8px);\n}\n.plugin-clear_button.focus.has-items .clear-button, .plugin-clear_button:not(.disabled):hover.has-items .clear-button {\n opacity: 1;\n}\n\n.ts-wrapper .dropdown-header {\n position: relative;\n padding: 10px 8px;\n border-bottom: 1px solid #d0d0d0;\n background: color-mix(#fff, #d0d0d0, 85%);\n border-radius: 3px 3px 0 0;\n}\n.ts-wrapper .dropdown-header-close {\n position: absolute;\n right: 8px;\n top: 50%;\n color: #303030;\n opacity: 0.4;\n margin-top: -12px;\n line-height: 20px;\n font-size: 20px !important;\n}\n.ts-wrapper .dropdown-header-close:hover {\n color: hsl(0, 0%, -6.1764705882%);\n}\n\n.plugin-dropdown_input.focus.dropdown-active .ts-control {\n box-shadow: none;\n border: 1px solid #d0d0d0;\n}\n.plugin-dropdown_input .dropdown-input {\n border: 1px solid #d0d0d0;\n border-width: 0 0 1px;\n display: block;\n padding: 8px 8px;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);\n width: 100%;\n background: transparent;\n}\n.plugin-dropdown_input .items-placeholder {\n border: 0 none !important;\n box-shadow: none !important;\n width: 100%;\n}\n.plugin-dropdown_input.has-items .items-placeholder, .plugin-dropdown_input.dropdown-active .items-placeholder {\n display: none !important;\n}\n\n.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input {\n min-width: 0;\n}\n.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input {\n flex: none;\n min-width: 4px;\n}\n.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder {\n color: transparent;\n}\n.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder {\n color: transparent;\n}\n\n.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content {\n display: flex;\n}\n.ts-dropdown.plugin-optgroup_columns .optgroup {\n border-right: 1px solid #f2f2f2;\n border-top: 0 none;\n flex-grow: 1;\n flex-basis: 0;\n min-width: 0;\n}\n.ts-dropdown.plugin-optgroup_columns .optgroup:last-child {\n border-right: 0 none;\n}\n.ts-dropdown.plugin-optgroup_columns .optgroup::before {\n display: none;\n}\n.ts-dropdown.plugin-optgroup_columns .optgroup-header {\n border-top: 0 none;\n}\n\n.ts-wrapper.plugin-remove_button .item {\n display: inline-flex;\n align-items: center;\n}\n.ts-wrapper.plugin-remove_button .item .remove {\n color: inherit;\n text-decoration: none;\n vertical-align: middle;\n display: inline-block;\n padding: 0 6px;\n border-radius: 0 2px 2px 0;\n box-sizing: border-box;\n}\n.ts-wrapper.plugin-remove_button .item .remove:hover {\n background: rgba(0, 0, 0, 0.05);\n}\n.ts-wrapper.plugin-remove_button.disabled .item .remove:hover {\n background: none;\n}\n.ts-wrapper.plugin-remove_button .remove-single {\n position: absolute;\n right: 0;\n top: 0;\n font-size: 23px;\n}\n\n.ts-wrapper.plugin-remove_button:not(.rtl) .item {\n padding-right: 0 !important;\n}\n.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {\n border-left: 1px solid #0073bb;\n margin-left: 6px;\n}\n.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove {\n border-left-color: #00578d;\n}\n.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove {\n border-left-color: #aaaaaa;\n}\n\n.ts-wrapper.plugin-remove_button.rtl .item {\n padding-left: 0 !important;\n}\n.ts-wrapper.plugin-remove_button.rtl .item .remove {\n border-right: 1px solid #0073bb;\n margin-right: 6px;\n}\n.ts-wrapper.plugin-remove_button.rtl .item.active .remove {\n border-right-color: #00578d;\n}\n.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove {\n border-right-color: #aaaaaa;\n}\n\n:root {\n --ts-pr-clear-button: 0px;\n --ts-pr-caret: 0px;\n --ts-pr-min: .75rem;\n}\n\n.ts-wrapper.single .ts-control, .ts-wrapper.single .ts-control input {\n cursor: pointer;\n}\n\n.ts-control:not(.rtl) {\n padding-right: max(var(--ts-pr-min), var(--ts-pr-clear-button) + var(--ts-pr-caret)) !important;\n}\n\n.ts-control.rtl {\n padding-left: max(var(--ts-pr-min), var(--ts-pr-clear-button) + var(--ts-pr-caret)) !important;\n}\n\n.ts-wrapper {\n position: relative;\n}\n\n.ts-dropdown,\n.ts-control,\n.ts-control input {\n color: #303030;\n font-family: inherit;\n font-size: 13px;\n line-height: 18px;\n}\n\n.ts-control,\n.ts-wrapper.single.input-active .ts-control {\n background: #fff;\n cursor: text;\n}\n\n.ts-hidden-accessible {\n border: 0 !important;\n clip: rect(0 0 0 0) !important;\n -webkit-clip-path: inset(50%) !important;\n clip-path: inset(50%) !important;\n overflow: hidden !important;\n padding: 0 !important;\n position: absolute !important;\n width: 1px !important;\n white-space: nowrap !important;\n}\n\n.ts-wrapper.single .ts-control {\n --ts-pr-caret: 2rem;\n}\n.ts-wrapper.single .ts-control::after {\n content: \" \";\n display: block;\n position: absolute;\n top: 50%;\n margin-top: -3px;\n width: 0;\n height: 0;\n border-style: solid;\n border-width: 5px 5px 0 5px;\n border-color: #808080 transparent transparent transparent;\n}\n.ts-wrapper.single .ts-control:not(.rtl)::after {\n right: 15px;\n}\n.ts-wrapper.single .ts-control.rtl::after {\n left: 15px;\n}\n.ts-wrapper.single.dropdown-active .ts-control::after {\n margin-top: -4px;\n border-width: 0 5px 5px 5px;\n border-color: transparent transparent #808080 transparent;\n}\n.ts-wrapper.single.input-active .ts-control, .ts-wrapper.single.input-active .ts-control input {\n cursor: text;\n}\n\n.ts-wrapper {\n display: flex;\n min-height: 36px;\n}\n.ts-wrapper.multi.has-items .ts-control {\n padding-left: 5px;\n --ts-pr-min: 5px;\n}\n.ts-wrapper.multi .ts-control [data-value] {\n text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);\n border-radius: 3px;\n background-color: color-mix(#1da7ee, #178ee9, 60%);\n background-image: linear-gradient(to bottom, #1da7ee, #178ee9);\n background-repeat: repeat-x;\n box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 1px rgba(255, 255, 255, 0.03);\n}\n.ts-wrapper.multi .ts-control [data-value].active {\n background-color: color-mix(#008fd8, #0075cf, 60%);\n background-image: linear-gradient(to bottom, #008fd8, #0075cf);\n background-repeat: repeat-x;\n}\n.ts-wrapper.multi.disabled .ts-control [data-value] {\n color: #999;\n text-shadow: none;\n background: none;\n box-shadow: none;\n}\n.ts-wrapper.multi.disabled .ts-control [data-value], .ts-wrapper.multi.disabled .ts-control [data-value] .remove {\n border-color: #e6e6e6;\n}\n.ts-wrapper.multi.disabled .ts-control [data-value] .remove {\n background: none;\n}\n.ts-wrapper.single .ts-control {\n box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.8);\n background-color: color-mix(#fefefe, #f2f2f2, 60%);\n background-image: linear-gradient(to bottom, #fefefe, #f2f2f2);\n background-repeat: repeat-x;\n}\n\n.ts-wrapper.single .ts-control, .ts-dropdown.single {\n border-color: #b8b8b8;\n}\n\n.dropdown-active .ts-control {\n border-radius: 3px 3px 0 0;\n}\n\n.ts-dropdown .optgroup-header {\n padding-top: 7px;\n font-weight: bold;\n font-size: 0.85em;\n}\n.ts-dropdown .optgroup {\n border-top: 1px solid #f0f0f0;\n}\n.ts-dropdown .optgroup:first-child {\n border-top: 0 none;\n}\n/*# sourceMappingURL=tom-select.default.css.map */";
5693
5693
 
5694
- var categoryColoursCSS = "/* Generated by scripts/generate-colours.js — do not edit by hand */\n/* Run `npm run build` to regenerate from the eolas categories endpoint */\n\n.lozenge[data-category=\"People\"] {\n\t--lozenge-background: #044E00;\n\t--lozenge-border: #033100;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Anthropological\"] {\n\t--lozenge-background: #8affe7;\n\t--lozenge-border: #068900;\n\t--lozenge-text: #000000;\n}\n\n.lozenge[data-category=\"Anthropogeographical\"] {\n\t--lozenge-background: #aed0db;\n\t--lozenge-border: #3f6674;\n\t--lozenge-text: #0c1a1b;\n}\n\n.lozenge[data-category=\"Musical\"] {\n\t--lozenge-background: #000060;\n\t--lozenge-border: #000020;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Aquatic\"] {\n\t--lozenge-background: #0085fe;\n\t--lozenge-border: #0036b1;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Terrestrial\"] {\n\t--lozenge-background: #652c17;\n\t--lozenge-border: #321200;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Cosmic\"] {\n\t--lozenge-background: #15163a;\n\t--lozenge-border: #000000;\n\t--lozenge-text: #feffe8;\n}\n\n.lozenge[data-category=\"Supernatural\"] {\n\t--lozenge-background: #f1ff5f;\n\t--lozenge-border: #674800;\n\t--lozenge-text: #352005;\n}\n\n.lozenge[data-category=\"Historical\"] {\n\t--lozenge-background: #740909;\n\t--lozenge-border: #470202;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Temporal\"] {\n\t--lozenge-background: #fffc33;\n\t--lozenge-border: #7f7e00;\n\t--lozenge-text: #0f0f00;\n}\n\n.lozenge[data-category=\"Mathematical\"] {\n\t--lozenge-background: #f53b0e;\n\t--lozenge-border: #7e3d2e;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Technological\"] {\n\t--lozenge-background: #c70f7a;\n\t--lozenge-border: #8f125b;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Meteorological\"] {\n\t--lozenge-background: #ffffff;\n\t--lozenge-border: #333333;\n\t--lozenge-text: #000000;\n}\n\n.lozenge[data-category=\"Meta\"] {\n\t--lozenge-background: #4a5568;\n\t--lozenge-border: #2d3748;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Dramaturgical\"] {\n\t--lozenge-background: #5f0086;\n\t--lozenge-border: #59007d;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Literary\"] {\n\t--lozenge-background: #a22400;\n\t--lozenge-border: #5e1500;\n\t--lozenge-text: #ffffff;\n}\n";
5694
+ var categoryColoursCSS = "/* Generated by scripts/generate-colours.js — do not edit by hand */\n/* Run `npm run build` to regenerate from the eolas categories endpoint */\n\n.lozenge[data-category=\"People\"] {\n\t--lozenge-background: #044E00;\n\t--lozenge-border: #033100;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Anthropological\"] {\n\t--lozenge-background: #8affe7;\n\t--lozenge-border: #068900;\n\t--lozenge-text: #000000;\n}\n\n.lozenge[data-category=\"Advisory\"] {\n\t--lozenge-background: #010102;\n\t--lozenge-border: #000020;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Anthropogeographical\"] {\n\t--lozenge-background: #aed0db;\n\t--lozenge-border: #3f6674;\n\t--lozenge-text: #0c1a1b;\n}\n\n.lozenge[data-category=\"Musical\"] {\n\t--lozenge-background: #000060;\n\t--lozenge-border: #000020;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Aquatic\"] {\n\t--lozenge-background: #0085fe;\n\t--lozenge-border: #0036b1;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Terrestrial\"] {\n\t--lozenge-background: #652c17;\n\t--lozenge-border: #321200;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Cosmic\"] {\n\t--lozenge-background: #15163a;\n\t--lozenge-border: #000000;\n\t--lozenge-text: #feffe8;\n}\n\n.lozenge[data-category=\"Supernatural\"] {\n\t--lozenge-background: #f1ff5f;\n\t--lozenge-border: #674800;\n\t--lozenge-text: #352005;\n}\n\n.lozenge[data-category=\"Historical\"] {\n\t--lozenge-background: #740909;\n\t--lozenge-border: #470202;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Temporal\"] {\n\t--lozenge-background: #fffc33;\n\t--lozenge-border: #7f7e00;\n\t--lozenge-text: #0f0f00;\n}\n\n.lozenge[data-category=\"Mathematical\"] {\n\t--lozenge-background: #f53b0e;\n\t--lozenge-border: #7e3d2e;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Technological\"] {\n\t--lozenge-background: #c70f7a;\n\t--lozenge-border: #8f125b;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Meteorological\"] {\n\t--lozenge-background: #ffffff;\n\t--lozenge-border: #333333;\n\t--lozenge-text: #000000;\n}\n\n.lozenge[data-category=\"Meta\"] {\n\t--lozenge-background: #4a5568;\n\t--lozenge-border: #2d3748;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Dramaturgical\"] {\n\t--lozenge-background: #5f0086;\n\t--lozenge-border: #59007d;\n\t--lozenge-text: #ffffff;\n}\n\n.lozenge[data-category=\"Literary\"] {\n\t--lozenge-background: #a22400;\n\t--lozenge-border: #5e1500;\n\t--lozenge-text: #ffffff;\n}\n";
5695
5695
 
5696
5696
  /**
5697
5697
  * Builds a Typesense filter_by expression from the search component's filter attributes.
@@ -5704,9 +5704,9 @@ var categoryColoursCSS = "/* Generated by scripts/generate-colours.js — do not
5704
5704
  function buildFilterBy(types, excludeTypes, isContact) {
5705
5705
  const parts = [];
5706
5706
  if (types) {
5707
- parts.push(`type:=[${types}]`);
5707
+ parts.push(`types:=[${types}]`);
5708
5708
  } else if (excludeTypes) {
5709
- parts.push(`type:!=[${excludeTypes}]`);
5709
+ parts.push(`types:!=[${excludeTypes}]`);
5710
5710
  }
5711
5711
  if (isContact === 'true') {
5712
5712
  parts.push('is_contact:=true');
@@ -5716,9 +5716,39 @@ function buildFilterBy(types, excludeTypes, isContact) {
5716
5716
  return parts.length > 0 ? parts.join(' && ') : null;
5717
5717
  }
5718
5718
 
5719
+ /**
5720
+ * Build formData key/value pairs for a lucos-search field.
5721
+ *
5722
+ * Arachne-selected entries emit both [uri] and [name]; created entries (tagged
5723
+ * with option.created = true) emit [name] only so consumers can route them to a
5724
+ * create-on-write path without a URI.
5725
+ *
5726
+ * @param {string} name - The field name (from select.name)
5727
+ * @param {string|string[]} values - Selected TomSelect values (ts.getValue())
5728
+ * @param {Object} optionMap - TomSelect options map (ts.options)
5729
+ * @returns {Array<[string, string]>} Array of [key, value] pairs to append to FormData
5730
+ */
5731
+ function buildFormDataEntries(name, values, optionMap) {
5732
+ const valueArray = Array.isArray(values) ? values : (values ? [values] : []);
5733
+ const entries = [];
5734
+ valueArray.forEach((id, idx) => {
5735
+ const option = optionMap[id];
5736
+ if (!option) return;
5737
+ if (option.created) {
5738
+ // Created (no arachne match) entry: name only, no URI
5739
+ entries.push([`${name}[${idx}][name]`, option.pref_label]);
5740
+ } else {
5741
+ // Arachne-selected entry: URI + name
5742
+ entries.push([`${name}[${idx}][uri]`, id]);
5743
+ entries.push([`${name}[${idx}][name]`, option.pref_label]);
5744
+ }
5745
+ });
5746
+ return entries;
5747
+ }
5748
+
5719
5749
  class LucosSearchComponent extends HTMLSpanElement {
5720
5750
  static get observedAttributes() {
5721
- return ['data-api-key','data-types','data-exclude_types','data-is-contact','data-label-override-zxx','data-common','data-preload'];
5751
+ return ['data-api-key','data-types','data-exclude_types','data-is-contact','data-label-override-zxx','data-common','data-preload','data-create'];
5722
5752
  }
5723
5753
  constructor() {
5724
5754
  super();
@@ -5810,6 +5840,18 @@ class LucosSearchComponent extends HTMLSpanElement {
5810
5840
  color: inherit;
5811
5841
  text-decoration: none;
5812
5842
  }
5843
+
5844
+ /* Pre-save visual indicator for unsaved created entries.
5845
+ * Cream background distinguishes from both the default unknown-category grey (#555)
5846
+ * and the white used by the Meteorological category.
5847
+ * border-style !important needed to override TomSelect's base border shorthand. */
5848
+ .lozenge.lozenge-pending {
5849
+ --lozenge-background: #fffbea;
5850
+ --lozenge-border: #999999;
5851
+ --lozenge-text: #333333;
5852
+ border-style: dashed !important;
5853
+ font-style: italic;
5854
+ }
5813
5855
  `;
5814
5856
  shadow.appendChild(mainStyle);
5815
5857
 
@@ -5820,8 +5862,33 @@ class LucosSearchComponent extends HTMLSpanElement {
5820
5862
  const selector = component.querySelector("select");
5821
5863
  if (!selector) throw new Error("Can't find select element in lucos-search");
5822
5864
  selector.setAttribute("multiple", "multiple");
5865
+
5866
+ // Derive a noun for the "Add new <type>: <name>" create prompt when data-types is a single type
5867
+ function getCreateNoun() {
5868
+ const dataTypes = component.getAttribute("data-types");
5869
+ if (!dataTypes) return null;
5870
+ const types = dataTypes.split(",").map(t => t.trim()).filter(Boolean);
5871
+ return types.length === 1 ? types[0] : null;
5872
+ }
5873
+
5874
+ if (component.hasAttribute("data-create")) {
5875
+ const createTypes = component.getAttribute("data-types");
5876
+ const createTypeList = createTypes ? createTypes.split(",").map(t => t.trim()).filter(Boolean) : [];
5877
+ if (createTypeList.length > 1) {
5878
+ console.warn(
5879
+ `lucos-search: data-create is set alongside data-types="${createTypes}" which specifies ${createTypeList.length} types. ` +
5880
+ `Server-side there will be no way to determine which type to create — data-create should only be used with a single data-types value.`
5881
+ );
5882
+ }
5883
+ }
5884
+
5823
5885
  new TomSelect(selector, {
5824
5886
  ...(component.isLanguageMode || component.getAttribute("data-common") ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
5887
+ ...(component.hasAttribute("data-create") ? {
5888
+ create: function(input) {
5889
+ return { id: input, pref_label: input, created: true };
5890
+ },
5891
+ } : {}),
5825
5892
  valueField: 'id',
5826
5893
  labelField: 'pref_label',
5827
5894
  searchField: [],
@@ -5984,8 +6051,12 @@ class LucosSearchComponent extends HTMLSpanElement {
5984
6051
  }
5985
6052
  },
5986
6053
  onItemSelect: function (item) {
5987
- // Tom-select prevents clicking on link in an item to work as normal, so force it here
5988
- window.open(item.dataset.value, '_blank').focus();
6054
+ // Tom-select prevents clicking on link in an item to work as normal, so force it here.
6055
+ // Skip navigation for created (unsaved) entries — they have no arachne URI.
6056
+ const value = item.dataset.value;
6057
+ const option = this.options[value];
6058
+ if (option && option.created) return;
6059
+ window.open(value, '_blank').focus();
5989
6060
  },
5990
6061
  render:{
5991
6062
  option: function(data, escape) {
@@ -6016,8 +6087,19 @@ class LucosSearchComponent extends HTMLSpanElement {
6016
6087
  const displayLabel = (zxxOverride && data.id === 'https://eolas.l42.eu/metadata/language/zxx/')
6017
6088
  ? zxxOverride
6018
6089
  : data.pref_label;
6090
+ // Created (unsaved) entries: no URI to link to, render with pending indicator
6091
+ if (data.created) {
6092
+ return `<div class="lozenge lozenge-pending" data-type="" data-category="">${escape(displayLabel)}</div>`;
6093
+ }
6019
6094
  return `<div class="lozenge" data-type="${escape(data.type)}" data-category="${escape(data.category)}"><a href="${data.id}" target="_blank">${escape(displayLabel)}</a></div>`;
6020
6095
  },
6096
+ option_create: function(data, escape) {
6097
+ const noun = getCreateNoun();
6098
+ if (noun) {
6099
+ return `<div class="create">Add new ${escape(noun)}: <strong>${escape(data.input)}</strong>&hellip;</div>`;
6100
+ }
6101
+ return `<div class="create">Add <strong>${escape(data.input)}</strong>&hellip;</div>`;
6102
+ },
6021
6103
  },
6022
6104
  });
6023
6105
 
@@ -6092,16 +6174,10 @@ class LucosSearchComponent extends HTMLSpanElement {
6092
6174
  const ts = selector.tomselect;
6093
6175
  if (!ts) return;
6094
6176
  const name = selector.name;
6095
- const values = ts.getValue();
6096
- const valueArray = Array.isArray(values) ? values : (values ? [values] : []);
6097
6177
  // Remove the native select values so consumers only receive the structured pairs
6098
6178
  event.formData.delete(name);
6099
- valueArray.forEach((id, idx) => {
6100
- const option = ts.options[id];
6101
- if (option) {
6102
- event.formData.append(`${name}[${idx}][uri]`, id);
6103
- event.formData.append(`${name}[${idx}][name]`, option.pref_label);
6104
- }
6179
+ buildFormDataEntries(name, ts.getValue(), ts.options).forEach(([key, value]) => {
6180
+ event.formData.append(key, value);
6105
6181
  });
6106
6182
  };
6107
6183
  form.addEventListener('formdata', this._formdataHandler);
@@ -6129,7 +6205,7 @@ class LucosSearchComponent extends HTMLSpanElement {
6129
6205
  if (!key) { this._langFamilies = []; return []; }
6130
6206
  const searchParams = new URLSearchParams({
6131
6207
  q: '*',
6132
- filter_by: 'type:=Language Family',
6208
+ filter_by: 'types:=Language Family',
6133
6209
  query_by: 'pref_label',
6134
6210
  include_fields: 'id,pref_label',
6135
6211
  sort_by: 'pref_label:asc',
@@ -40,11 +40,11 @@
40
40
  <div style="column-count: 2; column-gap: 2em; border: 1px solid #ccc; padding: 1em;">
41
41
  <div style="break-inside: avoid;">
42
42
  <label for="col-search1">Theme tune:</label>
43
- <span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="CreativeWork"><select id="col-search1"></select></span>
43
+ <span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Creative Work"><select id="col-search1"></select></span>
44
44
  </div>
45
45
  <div style="break-inside: avoid;">
46
46
  <label for="col-search2">Soundtrack:</label>
47
- <span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="CreativeWork"><select id="col-search2"></select></span>
47
+ <span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Creative Work"><select id="col-search2"></select></span>
48
48
  </div>
49
49
  <div style="break-inside: avoid;">
50
50
  <label for="col-search3">Language:</label>
@@ -61,6 +61,13 @@
61
61
  <button type="submit">Submit</button>
62
62
  </form>
63
63
  <pre id="form-output"></pre>
64
+ <h1>Inline create (data-create)</h1>
65
+ <p>Person field with <code>data-create</code>: type a name with no match and pick "Add new Person: …" to create a new entry. Created entries render with a cream dashed lozenge and serialise as <code>[name]</code>-only (no <code>[uri]</code>) on submit.</p>
66
+ <form id="create-form" onsubmit="handleCreateSubmit(event)">
67
+ <label for="composer">Composer:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Person" data-create><select id="composer" name="composer" multiple></select></span>
68
+ <button type="submit">Submit</button>
69
+ </form>
70
+ <pre id="create-output"></pre>
64
71
  <script>
65
72
  function handleSubmit(event) {
66
73
  event.preventDefault();
@@ -72,6 +79,16 @@
72
79
  }
73
80
  document.getElementById('form-output').textContent = JSON.stringify(output, null, 2);
74
81
  }
82
+ function handleCreateSubmit(event) {
83
+ event.preventDefault();
84
+ const data = new FormData(event.target);
85
+ const output = {};
86
+ for (const [key, value] of data.entries()) {
87
+ if (!output[key]) output[key] = [];
88
+ output[key].push(value);
89
+ }
90
+ document.getElementById('create-output').textContent = JSON.stringify(output, null, 2);
91
+ }
75
92
  </script>
76
93
  <script src="./built.js"></script>
77
94
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "3.0.6",
3
+ "version": "4.0.1",
4
4
  "description": "Web Components for searching lucOS data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,5 +19,15 @@ test('omitting data-is-contact produces no is_contact filter', () => {
19
19
 
20
20
  test('data-is-contact combines correctly with data-types', () => {
21
21
  const result = buildFilterBy('Person', null, 'true');
22
- assert.equal(result, 'type:=[Person] && is_contact:=true');
22
+ assert.equal(result, 'types:=[Person] && is_contact:=true');
23
+ });
24
+
25
+ test('data-types produces types:= filter', () => {
26
+ const result = buildFilterBy('City,River', null, null);
27
+ assert.equal(result, 'types:=[City,River]');
28
+ });
29
+
30
+ test('data-exclude-types produces types:!= filter', () => {
31
+ const result = buildFilterBy(null, 'Track', null);
32
+ assert.equal(result, 'types:!=[Track]');
23
33
  });
@@ -0,0 +1,111 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildFormDataEntries } from '../web-components/form-serialise.js';
4
+
5
+ // --- Arachne-selected entries ---
6
+
7
+ test('arachne-selected entry emits [uri] and [name]', () => {
8
+ const options = {
9
+ 'https://eolas.l42.eu/people/1': { pref_label: 'John Lennon', id: 'https://eolas.l42.eu/people/1' },
10
+ };
11
+ const entries = buildFormDataEntries('composer', 'https://eolas.l42.eu/people/1', options);
12
+ assert.deepEqual(entries, [
13
+ ['composer[0][uri]', 'https://eolas.l42.eu/people/1'],
14
+ ['composer[0][name]', 'John Lennon'],
15
+ ]);
16
+ });
17
+
18
+ test('arachne-selected entry with multiple values uses contiguous indices', () => {
19
+ const options = {
20
+ 'https://eolas.l42.eu/people/1': { pref_label: 'John Lennon', id: 'https://eolas.l42.eu/people/1' },
21
+ 'https://eolas.l42.eu/people/2': { pref_label: 'Paul McCartney', id: 'https://eolas.l42.eu/people/2' },
22
+ };
23
+ const entries = buildFormDataEntries(
24
+ 'composer',
25
+ ['https://eolas.l42.eu/people/1', 'https://eolas.l42.eu/people/2'],
26
+ options,
27
+ );
28
+ assert.deepEqual(entries, [
29
+ ['composer[0][uri]', 'https://eolas.l42.eu/people/1'],
30
+ ['composer[0][name]', 'John Lennon'],
31
+ ['composer[1][uri]', 'https://eolas.l42.eu/people/2'],
32
+ ['composer[1][name]', 'Paul McCartney'],
33
+ ]);
34
+ });
35
+
36
+ // --- Created entries ---
37
+
38
+ test('created entry emits [name] only, no [uri]', () => {
39
+ const options = {
40
+ 'Ringo Starr': { pref_label: 'Ringo Starr', id: 'Ringo Starr', created: true },
41
+ };
42
+ const entries = buildFormDataEntries('composer', 'Ringo Starr', options);
43
+ assert.deepEqual(entries, [
44
+ ['composer[0][name]', 'Ringo Starr'],
45
+ ]);
46
+ // Confirm [uri] is absent
47
+ const keys = entries.map(([k]) => k);
48
+ assert.ok(!keys.includes('composer[0][uri]'), 'uri key must not be present for created entries');
49
+ });
50
+
51
+ // --- Mixed entries ---
52
+
53
+ test('mixed created and arachne-selected entries serialise with contiguous indices and correct shapes', () => {
54
+ const options = {
55
+ 'https://eolas.l42.eu/people/1': { pref_label: 'John Lennon', id: 'https://eolas.l42.eu/people/1' },
56
+ 'George Harrison': { pref_label: 'George Harrison', id: 'George Harrison', created: true },
57
+ 'https://eolas.l42.eu/people/3': { pref_label: 'Paul McCartney', id: 'https://eolas.l42.eu/people/3' },
58
+ };
59
+ const entries = buildFormDataEntries(
60
+ 'composer',
61
+ ['https://eolas.l42.eu/people/1', 'George Harrison', 'https://eolas.l42.eu/people/3'],
62
+ options,
63
+ );
64
+ assert.deepEqual(entries, [
65
+ ['composer[0][uri]', 'https://eolas.l42.eu/people/1'],
66
+ ['composer[0][name]', 'John Lennon'],
67
+ ['composer[1][name]', 'George Harrison'], // created — name only
68
+ ['composer[2][uri]', 'https://eolas.l42.eu/people/3'],
69
+ ['composer[2][name]', 'Paul McCartney'],
70
+ ]);
71
+ });
72
+
73
+ // --- Edge cases ---
74
+
75
+ test('no values produces no entries', () => {
76
+ const entries = buildFormDataEntries('composer', [], {});
77
+ assert.deepEqual(entries, []);
78
+ });
79
+
80
+ test('empty string values produces no entries', () => {
81
+ const entries = buildFormDataEntries('composer', '', {});
82
+ assert.deepEqual(entries, []);
83
+ });
84
+
85
+ test('null values produces no entries', () => {
86
+ const entries = buildFormDataEntries('composer', null, {});
87
+ assert.deepEqual(entries, []);
88
+ });
89
+
90
+ test('value with no matching option is silently skipped', () => {
91
+ // Matches the existing behaviour: unknown value keys are ignored
92
+ const options = {
93
+ 'https://eolas.l42.eu/people/1': { pref_label: 'John Lennon', id: 'https://eolas.l42.eu/people/1' },
94
+ };
95
+ const entries = buildFormDataEntries(
96
+ 'composer',
97
+ ['https://eolas.l42.eu/people/1', 'https://eolas.l42.eu/people/99'],
98
+ options,
99
+ );
100
+ // Only the known option contributes entries; index gaps are absent (idx 0 only)
101
+ assert.deepEqual(entries, [
102
+ ['composer[0][uri]', 'https://eolas.l42.eu/people/1'],
103
+ ['composer[0][name]', 'John Lennon'],
104
+ ]);
105
+ });
106
+
107
+ test('field name is preserved verbatim in keys', () => {
108
+ const options = { 'https://example.com/1': { pref_label: 'Test', id: 'https://example.com/1' } };
109
+ const entries = buildFormDataEntries('my_field_name', 'https://example.com/1', options);
110
+ assert.ok(entries.every(([k]) => k.startsWith('my_field_name[')));
111
+ });
@@ -9,9 +9,9 @@
9
9
  export function buildFilterBy(types, excludeTypes, isContact) {
10
10
  const parts = [];
11
11
  if (types) {
12
- parts.push(`type:=[${types}]`);
12
+ parts.push(`types:=[${types}]`);
13
13
  } else if (excludeTypes) {
14
- parts.push(`type:!=[${excludeTypes}]`);
14
+ parts.push(`types:!=[${excludeTypes}]`);
15
15
  }
16
16
  if (isContact === 'true') {
17
17
  parts.push('is_contact:=true');
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Build formData key/value pairs for a lucos-search field.
3
+ *
4
+ * Arachne-selected entries emit both [uri] and [name]; created entries (tagged
5
+ * with option.created = true) emit [name] only so consumers can route them to a
6
+ * create-on-write path without a URI.
7
+ *
8
+ * @param {string} name - The field name (from select.name)
9
+ * @param {string|string[]} values - Selected TomSelect values (ts.getValue())
10
+ * @param {Object} optionMap - TomSelect options map (ts.options)
11
+ * @returns {Array<[string, string]>} Array of [key, value] pairs to append to FormData
12
+ */
13
+ export function buildFormDataEntries(name, values, optionMap) {
14
+ const valueArray = Array.isArray(values) ? values : (values ? [values] : []);
15
+ const entries = [];
16
+ valueArray.forEach((id, idx) => {
17
+ const option = optionMap[id];
18
+ if (!option) return;
19
+ if (option.created) {
20
+ // Created (no arachne match) entry: name only, no URI
21
+ entries.push([`${name}[${idx}][name]`, option.pref_label]);
22
+ } else {
23
+ // Arachne-selected entry: URI + name
24
+ entries.push([`${name}[${idx}][uri]`, id]);
25
+ entries.push([`${name}[${idx}][name]`, option.pref_label]);
26
+ }
27
+ });
28
+ return entries;
29
+ }
@@ -2,10 +2,11 @@ import TomSelect from 'tom-select';
2
2
  import tomSelectStylesheet from 'tom-select/dist/css/tom-select.default.css';
3
3
  import categoryColoursCSS from './generated/category-colours.css';
4
4
  import { buildFilterBy } from './filter.js';
5
+ import { buildFormDataEntries } from './form-serialise.js';
5
6
 
6
7
  class LucosSearchComponent extends HTMLSpanElement {
7
8
  static get observedAttributes() {
8
- return ['data-api-key','data-types','data-exclude_types','data-is-contact','data-label-override-zxx','data-common','data-preload'];
9
+ return ['data-api-key','data-types','data-exclude_types','data-is-contact','data-label-override-zxx','data-common','data-preload','data-create'];
9
10
  }
10
11
  constructor() {
11
12
  super();
@@ -97,6 +98,18 @@ class LucosSearchComponent extends HTMLSpanElement {
97
98
  color: inherit;
98
99
  text-decoration: none;
99
100
  }
101
+
102
+ /* Pre-save visual indicator for unsaved created entries.
103
+ * Cream background distinguishes from both the default unknown-category grey (#555)
104
+ * and the white used by the Meteorological category.
105
+ * border-style !important needed to override TomSelect's base border shorthand. */
106
+ .lozenge.lozenge-pending {
107
+ --lozenge-background: #fffbea;
108
+ --lozenge-border: #999999;
109
+ --lozenge-text: #333333;
110
+ border-style: dashed !important;
111
+ font-style: italic;
112
+ }
100
113
  `;
101
114
  shadow.appendChild(mainStyle);
102
115
 
@@ -107,8 +120,33 @@ class LucosSearchComponent extends HTMLSpanElement {
107
120
  const selector = component.querySelector("select");
108
121
  if (!selector) throw new Error("Can't find select element in lucos-search");
109
122
  selector.setAttribute("multiple", "multiple");
123
+
124
+ // Derive a noun for the "Add new <type>: <name>" create prompt when data-types is a single type
125
+ function getCreateNoun() {
126
+ const dataTypes = component.getAttribute("data-types");
127
+ if (!dataTypes) return null;
128
+ const types = dataTypes.split(",").map(t => t.trim()).filter(Boolean);
129
+ return types.length === 1 ? types[0] : null;
130
+ }
131
+
132
+ if (component.hasAttribute("data-create")) {
133
+ const createTypes = component.getAttribute("data-types");
134
+ const createTypeList = createTypes ? createTypes.split(",").map(t => t.trim()).filter(Boolean) : [];
135
+ if (createTypeList.length > 1) {
136
+ console.warn(
137
+ `lucos-search: data-create is set alongside data-types="${createTypes}" which specifies ${createTypeList.length} types. ` +
138
+ `Server-side there will be no way to determine which type to create — data-create should only be used with a single data-types value.`
139
+ );
140
+ }
141
+ }
142
+
110
143
  new TomSelect(selector, {
111
144
  ...(component.isLanguageMode || component.getAttribute("data-common") ? { optgroupField: 'lang_family', lockOptgroupOrder: true } : {}),
145
+ ...(component.hasAttribute("data-create") ? {
146
+ create: function(input) {
147
+ return { id: input, pref_label: input, created: true };
148
+ },
149
+ } : {}),
112
150
  valueField: 'id',
113
151
  labelField: 'pref_label',
114
152
  searchField: [],
@@ -271,8 +309,12 @@ class LucosSearchComponent extends HTMLSpanElement {
271
309
  }
272
310
  },
273
311
  onItemSelect: function (item) {
274
- // Tom-select prevents clicking on link in an item to work as normal, so force it here
275
- window.open(item.dataset.value, '_blank').focus();
312
+ // Tom-select prevents clicking on link in an item to work as normal, so force it here.
313
+ // Skip navigation for created (unsaved) entries — they have no arachne URI.
314
+ const value = item.dataset.value;
315
+ const option = this.options[value];
316
+ if (option && option.created) return;
317
+ window.open(value, '_blank').focus();
276
318
  },
277
319
  render:{
278
320
  option: function(data, escape) {
@@ -303,8 +345,19 @@ class LucosSearchComponent extends HTMLSpanElement {
303
345
  const displayLabel = (zxxOverride && data.id === 'https://eolas.l42.eu/metadata/language/zxx/')
304
346
  ? zxxOverride
305
347
  : data.pref_label;
348
+ // Created (unsaved) entries: no URI to link to, render with pending indicator
349
+ if (data.created) {
350
+ return `<div class="lozenge lozenge-pending" data-type="" data-category="">${escape(displayLabel)}</div>`;
351
+ }
306
352
  return `<div class="lozenge" data-type="${escape(data.type)}" data-category="${escape(data.category)}"><a href="${data.id}" target="_blank">${escape(displayLabel)}</a></div>`;
307
353
  },
354
+ option_create: function(data, escape) {
355
+ const noun = getCreateNoun();
356
+ if (noun) {
357
+ return `<div class="create">Add new ${escape(noun)}: <strong>${escape(data.input)}</strong>&hellip;</div>`;
358
+ }
359
+ return `<div class="create">Add <strong>${escape(data.input)}</strong>&hellip;</div>`;
360
+ },
308
361
  },
309
362
  });
310
363
 
@@ -379,16 +432,10 @@ class LucosSearchComponent extends HTMLSpanElement {
379
432
  const ts = selector.tomselect;
380
433
  if (!ts) return;
381
434
  const name = selector.name;
382
- const values = ts.getValue();
383
- const valueArray = Array.isArray(values) ? values : (values ? [values] : []);
384
435
  // Remove the native select values so consumers only receive the structured pairs
385
436
  event.formData.delete(name);
386
- valueArray.forEach((id, idx) => {
387
- const option = ts.options[id];
388
- if (option) {
389
- event.formData.append(`${name}[${idx}][uri]`, id);
390
- event.formData.append(`${name}[${idx}][name]`, option.pref_label);
391
- }
437
+ buildFormDataEntries(name, ts.getValue(), ts.options).forEach(([key, value]) => {
438
+ event.formData.append(key, value);
392
439
  });
393
440
  };
394
441
  form.addEventListener('formdata', this._formdataHandler);
@@ -416,7 +463,7 @@ class LucosSearchComponent extends HTMLSpanElement {
416
463
  if (!key) { this._langFamilies = []; return []; }
417
464
  const searchParams = new URLSearchParams({
418
465
  q: '*',
419
- filter_by: 'type:=Language Family',
466
+ filter_by: 'types:=Language Family',
420
467
  query_by: 'pref_label',
421
468
  include_fields: 'id,pref_label',
422
469
  sort_by: 'pref_label:asc',