lucos_search_component 4.0.0 → 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 +1 -0
- package/dist/index.js +88 -12
- package/example/index.html +17 -0
- package/package.json +1 -1
- package/test/form-serialise.test.mjs +111 -0
- package/web-components/form-serialise.js +29 -0
- package/web-components/lucos-search.js +58 -11
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.
|
|
@@ -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
|
-
|
|
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>…</div>`;
|
|
6100
|
+
}
|
|
6101
|
+
return `<div class="create">Add <strong>${escape(data.input)}</strong>…</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
|
-
|
|
6100
|
-
|
|
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);
|
package/example/index.html
CHANGED
|
@@ -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
|
@@ -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
|
+
});
|
|
@@ -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
|
-
|
|
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>…</div>`;
|
|
358
|
+
}
|
|
359
|
+
return `<div class="create">Add <strong>${escape(data.input)}</strong>…</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
|
-
|
|
387
|
-
|
|
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);
|