lucos_search_component 3.0.2 → 3.0.4

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.
@@ -2,11 +2,27 @@ version: 2.1
2
2
  orbs:
3
3
  lucos: lucos/deploy@0
4
4
 
5
+ jobs:
6
+ test:
7
+ docker:
8
+ - image: node:22-alpine
9
+ steps:
10
+ - checkout
11
+ - run:
12
+ name: Install Dependencies
13
+ command: npm ci
14
+ - run:
15
+ name: Run Tests
16
+ command: npm test
17
+
5
18
  workflows:
6
19
  version: 2
7
20
  release:
8
21
  jobs:
22
+ - test
9
23
  - lucos/release-npm:
24
+ requires:
25
+ - test
10
26
  filters:
11
27
  branches:
12
28
  only: main
@@ -13,7 +13,7 @@ permissions:
13
13
 
14
14
  jobs:
15
15
  reusable:
16
- uses: lucas42/.github/.github/workflows/reusable-code-reviewer-auto-merge.yml@v1.18.0
16
+ uses: lucas42/.github/.github/workflows/reusable-code-reviewer-auto-merge.yml@v1.19.0
17
17
  secrets:
18
18
  LUCOS_CI_APP_ID: ${{ secrets.LUCOS_CI_APP_ID }}
19
19
  LUCOS_CI_PRIVATE_KEY: ${{ secrets.LUCOS_CI_PRIVATE_KEY }}
@@ -9,4 +9,4 @@ permissions:
9
9
 
10
10
  jobs:
11
11
  convention-check:
12
- uses: lucas42/.github/.github/workflows/reusable-convention-check.yml@v1.18.0
12
+ uses: lucas42/.github/.github/workflows/reusable-convention-check.yml@v1.19.0
@@ -9,7 +9,7 @@ permissions:
9
9
 
10
10
  jobs:
11
11
  dependabot:
12
- uses: lucas42/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v1.18.0
12
+ uses: lucas42/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v1.19.0
13
13
  secrets:
14
14
  LUCOS_CI_APP_ID: ${{ secrets.LUCOS_CI_APP_ID }}
15
15
  LUCOS_CI_PRIVATE_KEY: ${{ secrets.LUCOS_CI_PRIVATE_KEY }}
package/README.md CHANGED
@@ -31,7 +31,8 @@ Selected options use the item's URI as their value.
31
31
  The following attributes can be added to the lucos-search span:
32
32
  * **data-api-key** \[required\] — a valid API key for the production instance of [lucos_arachne](https://github.com/lucas42/lucos_arachne), as generated by [lucos_creds](https://github.com/lucas42/lucos_creds).
33
33
  * **data-types** — A comma separated list of item types to search for (defaults to all types).
34
- * **data-exclude-types** — A comma separated list of item types to exclude from the search (ignored if `data-types` is set).
34
+ * **data-exclude_types** — A comma separated list of item types to exclude from the search (ignored if `data-types` is set).
35
+ * **data-is-contact** — Filter results to contacts only (`"true"`) or non-contacts only (`"false"`). Omitting the attribute applies no filter. Composes with `data-types` when both are set.
35
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"`.
36
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"`.
37
38
  * **data-preload** — If present, all options are loaded upfront rather than fetched on search. Suitable for small, finite datasets such as languages.
package/dist/index.js CHANGED
@@ -1654,9 +1654,10 @@ function getSettings(input, settings_user) {
1654
1654
  *
1655
1655
  */
1656
1656
  var init_textbox = () => {
1657
+ var _a, _b;
1657
1658
  const data_raw = input.getAttribute(attr_data);
1658
1659
  if (!data_raw) {
1659
- var value = input.value.trim() || '';
1660
+ var value = (_b = (_a = input === null || input === void 0 ? void 0 : input.value) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
1660
1661
  if (!settings.allowEmptyOption && !value.length)
1661
1662
  return;
1662
1663
  const values = value.split(settings.delimiter);
@@ -1966,15 +1967,6 @@ class TomSelect extends MicroPlugin(MicroEvent) {
1966
1967
  self.close(false);
1967
1968
  self.inputState();
1968
1969
  self.isSetup = true;
1969
- if (input.disabled) {
1970
- self.disable();
1971
- }
1972
- else if (input.readOnly) {
1973
- self.setReadOnly(true);
1974
- }
1975
- else {
1976
- self.enable(); //sets tabIndex
1977
- }
1978
1970
  self.on('change', this.onChange);
1979
1971
  addClasses$2(input, 'tomselected', 'ts-hidden-accessible');
1980
1972
  self.trigger('initialize');
@@ -2075,6 +2067,16 @@ class TomSelect extends MicroPlugin(MicroEvent) {
2075
2067
  const settings = get_settings ? getSettings(self.input, { delimiter: self.settings.delimiter, allowEmptyOption: self.settings.allowEmptyOption }) : self.settings;
2076
2068
  self.setupOptions(settings.options, settings.optgroups);
2077
2069
  self.setValue(settings.items || [], true); // silent prevents recursion
2070
+ if (self.input.disabled) {
2071
+ self.disable();
2072
+ }
2073
+ else if (self.input.readOnly) {
2074
+ self.setReadOnly(true);
2075
+ }
2076
+ else {
2077
+ self.enable(); //sets tabIndex
2078
+ }
2079
+ self.lastQuery = null; // so updated options will be displayed in dropdown
2078
2080
  }
2079
2081
  /**
2080
2082
  * Triggered when the main control element
@@ -2724,15 +2726,19 @@ class TomSelect extends MicroPlugin(MicroEvent) {
2724
2726
  if (self.isDisabled || self.isReadOnly)
2725
2727
  return;
2726
2728
  self.ignoreFocus = true;
2727
- if (self.control_input.offsetWidth) {
2728
- self.control_input.focus();
2729
- }
2730
- else {
2731
- self.focus_node.focus();
2732
- }
2729
+ const focusTarget = this.control_input.offsetWidth ? this.control_input : this.focus_node;
2730
+ focusTarget.focus();
2733
2731
  setTimeout(() => {
2734
2732
  self.ignoreFocus = false;
2735
- self.onFocus();
2733
+ // Fix https://github.com/orchidjs/tom-select/issues/806
2734
+ // Only proceed if this instance's element is still the active element. If Edge autofill
2735
+ // (or anything else) has moved focus to a different element in the interim, calling
2736
+ // onFocus() here would steal focus back and restart the cascade loop.
2737
+ const root = focusTarget.getRootNode();
2738
+ if (root.activeElement !== focusTarget) {
2739
+ return;
2740
+ }
2741
+ this.onFocus();
2736
2742
  }, 0);
2737
2743
  }
2738
2744
  /**
@@ -3977,7 +3983,7 @@ class TomSelect extends MicroPlugin(MicroEvent) {
3977
3983
  }
3978
3984
 
3979
3985
  /**
3980
- * Tom Select v2.6.0
3986
+ * Tom Select v2.6.1
3981
3987
  * Licensed under the Apache License, Version 2.0 (the "License");
3982
3988
  */
3983
3989
 
@@ -4026,7 +4032,7 @@ function plugin$d () {
4026
4032
  }
4027
4033
 
4028
4034
  /**
4029
- * Tom Select v2.6.0
4035
+ * Tom Select v2.6.1
4030
4036
  * Licensed under the Apache License, Version 2.0 (the "License");
4031
4037
  */
4032
4038
 
@@ -4203,7 +4209,7 @@ function plugin$c (userOptions) {
4203
4209
  }
4204
4210
 
4205
4211
  /**
4206
- * Tom Select v2.6.0
4212
+ * Tom Select v2.6.1
4207
4213
  * Licensed under the Apache License, Version 2.0 (the "License");
4208
4214
  */
4209
4215
 
@@ -4277,7 +4283,7 @@ function plugin$b (userOptions) {
4277
4283
  }
4278
4284
 
4279
4285
  /**
4280
- * Tom Select v2.6.0
4286
+ * Tom Select v2.6.1
4281
4287
  * Licensed under the Apache License, Version 2.0 (the "License");
4282
4288
  */
4283
4289
 
@@ -4495,7 +4501,7 @@ function plugin$a () {
4495
4501
  }
4496
4502
 
4497
4503
  /**
4498
- * Tom Select v2.6.0
4504
+ * Tom Select v2.6.1
4499
4505
  * Licensed under the Apache License, Version 2.0 (the "License");
4500
4506
  */
4501
4507
 
@@ -4595,7 +4601,7 @@ function plugin$9 (userOptions) {
4595
4601
  }
4596
4602
 
4597
4603
  /**
4598
- * Tom Select v2.6.0
4604
+ * Tom Select v2.6.1
4599
4605
  * Licensed under the Apache License, Version 2.0 (the "License");
4600
4606
  */
4601
4607
 
@@ -4756,7 +4762,7 @@ function plugin$8 () {
4756
4762
  }
4757
4763
 
4758
4764
  /**
4759
- * Tom Select v2.6.0
4765
+ * Tom Select v2.6.1
4760
4766
  * Licensed under the Apache License, Version 2.0 (the "License");
4761
4767
  */
4762
4768
 
@@ -4978,7 +4984,7 @@ function plugin$7 () {
4978
4984
  }
4979
4985
 
4980
4986
  /**
4981
- * Tom Select v2.6.0
4987
+ * Tom Select v2.6.1
4982
4988
  * Licensed under the Apache License, Version 2.0 (the "License");
4983
4989
  */
4984
4990
 
@@ -5050,7 +5056,7 @@ function plugin$6 () {
5050
5056
  }
5051
5057
 
5052
5058
  /**
5053
- * Tom Select v2.6.0
5059
+ * Tom Select v2.6.1
5054
5060
  * Licensed under the Apache License, Version 2.0 (the "License");
5055
5061
  */
5056
5062
 
@@ -5080,7 +5086,7 @@ function plugin$5 () {
5080
5086
  }
5081
5087
 
5082
5088
  /**
5083
- * Tom Select v2.6.0
5089
+ * Tom Select v2.6.1
5084
5090
  * Licensed under the Apache License, Version 2.0 (the "License");
5085
5091
  */
5086
5092
 
@@ -5104,7 +5110,7 @@ function plugin$4 () {
5104
5110
  }
5105
5111
 
5106
5112
  /**
5107
- * Tom Select v2.6.0
5113
+ * Tom Select v2.6.1
5108
5114
  * Licensed under the Apache License, Version 2.0 (the "License");
5109
5115
  */
5110
5116
 
@@ -5188,7 +5194,7 @@ function plugin$3 () {
5188
5194
  }
5189
5195
 
5190
5196
  /**
5191
- * Tom Select v2.6.0
5197
+ * Tom Select v2.6.1
5192
5198
  * Licensed under the Apache License, Version 2.0 (the "License");
5193
5199
  */
5194
5200
 
@@ -5320,7 +5326,7 @@ function plugin$2 (userOptions) {
5320
5326
  }
5321
5327
 
5322
5328
  /**
5323
- * Tom Select v2.6.0
5329
+ * Tom Select v2.6.1
5324
5330
  * Licensed under the Apache License, Version 2.0 (the "License");
5325
5331
  */
5326
5332
 
@@ -5360,7 +5366,7 @@ function plugin$1 (userOptions) {
5360
5366
  }
5361
5367
 
5362
5368
  /**
5363
- * Tom Select v2.6.0
5369
+ * Tom Select v2.6.1
5364
5370
  * Licensed under the Apache License, Version 2.0 (the "License");
5365
5371
  */
5366
5372
 
@@ -5468,6 +5474,8 @@ function plugin () {
5468
5474
  var loading_more = false;
5469
5475
  var load_more_opt;
5470
5476
  var default_values = [];
5477
+ var default_values_loaded = false;
5478
+ var default_pagination;
5471
5479
  if (!self.settings.shouldLoadMore) {
5472
5480
  // return true if additional results should be loaded
5473
5481
  self.settings.shouldLoadMore = () => {
@@ -5567,6 +5575,16 @@ function plugin () {
5567
5575
  }
5568
5576
  }
5569
5577
  orig_loadCallback.call(self, options, optgroups);
5578
+
5579
+ // After the initial preload (empty query), update default_values to include
5580
+ // preloaded options, not just the HTML <option> elements captured on initialize
5581
+ if (!loading_more && !default_values_loaded) {
5582
+ default_values_loaded = true;
5583
+ if (self.lastValue === '') {
5584
+ default_values = Object.keys(self.options);
5585
+ default_pagination = pagination[''];
5586
+ }
5587
+ }
5570
5588
  loading_more = false;
5571
5589
  });
5572
5590
 
@@ -5604,6 +5622,24 @@ function plugin () {
5604
5622
  }
5605
5623
  });
5606
5624
 
5625
+ // Restore preloaded options and pagination when clearing search
5626
+ const restoreDefaults = () => {
5627
+ if (!default_values_loaded) {
5628
+ return;
5629
+ }
5630
+ self.clearOptions(clearFilter);
5631
+ if (default_pagination) {
5632
+ pagination[''] = default_pagination;
5633
+ }
5634
+ };
5635
+ self.on('type', query => {
5636
+ if (query === '') {
5637
+ restoreDefaults();
5638
+ self.refreshOptions(false);
5639
+ }
5640
+ });
5641
+ self.on('dropdown_close', restoreDefaults);
5642
+
5607
5643
  // add scroll listener and default templates
5608
5644
  self.on('initialize', () => {
5609
5645
  default_values = Object.keys(self.options);
@@ -5653,13 +5689,36 @@ TomSelect.define('remove_button', plugin$2);
5653
5689
  TomSelect.define('restore_on_backspace', plugin$1);
5654
5690
  TomSelect.define('virtual_scroll', plugin);
5655
5691
 
5656
- var tomSelectStylesheet = "/**\n * tom-select.css (v2.6.0)\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: white;\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: black;\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 */";
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 */";
5657
5693
 
5658
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";
5659
5695
 
5696
+ /**
5697
+ * Builds a Typesense filter_by expression from the search component's filter attributes.
5698
+ *
5699
+ * @param {string|null} types - Value of data-types attribute (e.g. "Person,Place")
5700
+ * @param {string|null} excludeTypes - Value of data-exclude-types attribute (e.g. "Language")
5701
+ * @param {string|null} isContact - Value of data-is-contact attribute ("true" or "false")
5702
+ * @returns {string|null} filter expression, or null if no filters apply
5703
+ */
5704
+ function buildFilterBy(types, excludeTypes, isContact) {
5705
+ const parts = [];
5706
+ if (types) {
5707
+ parts.push(`type:=[${types}]`);
5708
+ } else if (excludeTypes) {
5709
+ parts.push(`type:!=[${excludeTypes}]`);
5710
+ }
5711
+ if (isContact === 'true') {
5712
+ parts.push('is_contact:=true');
5713
+ } else if (isContact === 'false') {
5714
+ parts.push('is_contact:=false');
5715
+ }
5716
+ return parts.length > 0 ? parts.join(' && ') : null;
5717
+ }
5718
+
5660
5719
  class LucosSearchComponent extends HTMLSpanElement {
5661
5720
  static get observedAttributes() {
5662
- return ['data-api-key','data-types','data-exclude-types','data-label-override-zxx','data-common','data-preload'];
5721
+ return ['data-api-key','data-types','data-exclude_types','data-is-contact','data-label-override-zxx','data-common','data-preload'];
5663
5722
  }
5664
5723
  constructor() {
5665
5724
  super();
@@ -5782,11 +5841,12 @@ class LucosSearchComponent extends HTMLSpanElement {
5782
5841
  const queryParams = new URLSearchParams({
5783
5842
  q: query,
5784
5843
  });
5785
- if (component.getAttribute("data-types")) {
5786
- queryParams.set("filter_by",`type:=[${component.getAttribute("data-types")}]`);
5787
- } else if (component.getAttribute("data-exclude_types")) {
5788
- queryParams.set("filter_by",`type:!=[${component.getAttribute("data-exclude_types")}]`);
5789
- }
5844
+ const filterBy = buildFilterBy(
5845
+ component.getAttribute("data-types"),
5846
+ component.getAttribute("data-exclude_types"),
5847
+ component.getAttribute("data-is-contact"),
5848
+ );
5849
+ if (filterBy) queryParams.set("filter_by", filterBy);
5790
5850
  try {
5791
5851
  let results = await component.searchRequest(queryParams, abortController.signal);
5792
5852
  if (abortController.signal.aborted) return;
@@ -5859,11 +5919,11 @@ class LucosSearchComponent extends HTMLSpanElement {
5859
5919
  }
5860
5920
  // Preload all matching options (after groups are registered so options slot correctly)
5861
5921
  if (component.hasAttribute("data-preload")) {
5862
- const filterValue = component.getAttribute("data-types")
5863
- ? `type:=[${component.getAttribute("data-types")}]`
5864
- : component.getAttribute("data-exclude_types")
5865
- ? `type:!=[${component.getAttribute("data-exclude_types")}]`
5866
- : null;
5922
+ const filterValue = buildFilterBy(
5923
+ component.getAttribute("data-types"),
5924
+ component.getAttribute("data-exclude_types"),
5925
+ component.getAttribute("data-is-contact"),
5926
+ );
5867
5927
  // per_page: 250 acts as an upper bound — data-preload is intended for finite datasets
5868
5928
  const preloadParams = new URLSearchParams({ q: '*', per_page: 250 });
5869
5929
  if (filterValue) preloadParams.set("filter_by", filterValue);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Web Components for searching lucOS data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 1",
8
+ "test": "node --test test/*.test.mjs",
9
9
  "example": "source .env && KEY_LUCOS_ARACHNE=$KEY_LUCOS_ARACHNE envsubst < example/index.html > example/built.html && npm run build && webpack -c example/webpack.config.js && open example/built.html",
10
10
  "build": "node scripts/generate-colours.js && rollup -c"
11
11
  },
@@ -0,0 +1,23 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildFilterBy } from '../web-components/filter.js';
4
+
5
+ test('data-is-contact="true" produces is_contact:=true filter', () => {
6
+ const result = buildFilterBy(null, null, 'true');
7
+ assert.equal(result, 'is_contact:=true');
8
+ });
9
+
10
+ test('data-is-contact="false" produces is_contact:=false filter', () => {
11
+ const result = buildFilterBy(null, null, 'false');
12
+ assert.equal(result, 'is_contact:=false');
13
+ });
14
+
15
+ test('omitting data-is-contact produces no is_contact filter', () => {
16
+ const result = buildFilterBy(null, null, null);
17
+ assert.equal(result, null);
18
+ });
19
+
20
+ test('data-is-contact combines correctly with data-types', () => {
21
+ const result = buildFilterBy('Person', null, 'true');
22
+ assert.equal(result, 'type:=[Person] && is_contact:=true');
23
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Builds a Typesense filter_by expression from the search component's filter attributes.
3
+ *
4
+ * @param {string|null} types - Value of data-types attribute (e.g. "Person,Place")
5
+ * @param {string|null} excludeTypes - Value of data-exclude-types attribute (e.g. "Language")
6
+ * @param {string|null} isContact - Value of data-is-contact attribute ("true" or "false")
7
+ * @returns {string|null} filter expression, or null if no filters apply
8
+ */
9
+ export function buildFilterBy(types, excludeTypes, isContact) {
10
+ const parts = [];
11
+ if (types) {
12
+ parts.push(`type:=[${types}]`);
13
+ } else if (excludeTypes) {
14
+ parts.push(`type:!=[${excludeTypes}]`);
15
+ }
16
+ if (isContact === 'true') {
17
+ parts.push('is_contact:=true');
18
+ } else if (isContact === 'false') {
19
+ parts.push('is_contact:=false');
20
+ }
21
+ return parts.length > 0 ? parts.join(' && ') : null;
22
+ }
@@ -1,10 +1,11 @@
1
1
  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
+ import { buildFilterBy } from './filter.js';
4
5
 
5
6
  class LucosSearchComponent extends HTMLSpanElement {
6
7
  static get observedAttributes() {
7
- return ['data-api-key','data-types','data-exclude-types','data-label-override-zxx','data-common','data-preload'];
8
+ return ['data-api-key','data-types','data-exclude_types','data-is-contact','data-label-override-zxx','data-common','data-preload'];
8
9
  }
9
10
  constructor() {
10
11
  super();
@@ -127,11 +128,12 @@ class LucosSearchComponent extends HTMLSpanElement {
127
128
  const queryParams = new URLSearchParams({
128
129
  q: query,
129
130
  });
130
- if (component.getAttribute("data-types")) {
131
- queryParams.set("filter_by",`type:=[${component.getAttribute("data-types")}]`);
132
- } else if (component.getAttribute("data-exclude_types")) {
133
- queryParams.set("filter_by",`type:!=[${component.getAttribute("data-exclude_types")}]`);
134
- }
131
+ const filterBy = buildFilterBy(
132
+ component.getAttribute("data-types"),
133
+ component.getAttribute("data-exclude_types"),
134
+ component.getAttribute("data-is-contact"),
135
+ );
136
+ if (filterBy) queryParams.set("filter_by", filterBy);
135
137
  try {
136
138
  let results = await component.searchRequest(queryParams, abortController.signal);
137
139
  if (abortController.signal.aborted) return;
@@ -204,11 +206,11 @@ class LucosSearchComponent extends HTMLSpanElement {
204
206
  }
205
207
  // Preload all matching options (after groups are registered so options slot correctly)
206
208
  if (component.hasAttribute("data-preload")) {
207
- const filterValue = component.getAttribute("data-types")
208
- ? `type:=[${component.getAttribute("data-types")}]`
209
- : component.getAttribute("data-exclude_types")
210
- ? `type:!=[${component.getAttribute("data-exclude_types")}]`
211
- : null;
209
+ const filterValue = buildFilterBy(
210
+ component.getAttribute("data-types"),
211
+ component.getAttribute("data-exclude_types"),
212
+ component.getAttribute("data-is-contact"),
213
+ );
212
214
  // per_page: 250 acts as an upper bound — data-preload is intended for finite datasets
213
215
  const preloadParams = new URLSearchParams({ q: '*', per_page: 250 });
214
216
  if (filterValue) preloadParams.set("filter_by", filterValue);