lucos_search_component 3.0.3 → 3.0.5

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
@@ -31,7 +31,7 @@ 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
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.
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"`.
package/dist/index.js CHANGED
@@ -5718,7 +5718,7 @@ function buildFilterBy(types, excludeTypes, isContact) {
5718
5718
 
5719
5719
  class LucosSearchComponent extends HTMLSpanElement {
5720
5720
  static get observedAttributes() {
5721
- return ['data-api-key','data-types','data-exclude-types','data-is-contact','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'];
5722
5722
  }
5723
5723
  constructor() {
5724
5724
  super();
@@ -5781,8 +5781,30 @@ class LucosSearchComponent extends HTMLSpanElement {
5781
5781
  margin: 0 3px;
5782
5782
  padding: 2px 6px;
5783
5783
  }
5784
+ /* Prevent dropdown overflowing into an adjacent column in multi-column layouts.
5785
+ * Consumers may embed lucos-search in a CSS column-count context (e.g. a 2-column
5786
+ * form). Because TomSelect's DOM lives inside this shadow root, page-level CSS
5787
+ * cannot fix column overflow from outside.
5788
+ *
5789
+ * The actual fix is applied in JavaScript (see fixDropdownContentPosition below):
5790
+ * Chromium has a bug where position:absolute on a shadow-DOM descendant inside a
5791
+ * multi-column container resolves the containing block to the column-box boundary
5792
+ * rather than the nearest positioned ancestor (.ts-dropdown). CSS-only approaches
5793
+ * (position:absolute with explicit top/left, contain:layout) do not override this.
5794
+ * The JS fix switches .ts-dropdown-content to position:fixed when the dropdown opens
5795
+ * inside a multi-column layout, bypassing the containing-block resolution entirely. */
5784
5796
  .ts-dropdown {
5785
- margin: 0;
5797
+ border: none;
5798
+ margin: -3px 0 0 0;
5799
+ }
5800
+ .ts-dropdown-content {
5801
+ background: #fff;
5802
+ border: 1px solid #d0d0d0;
5803
+ margin: 0.25rem 0 0;
5804
+ border-top: 0 none;
5805
+ box-sizing: border-box;
5806
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
5807
+ border-radius: 0 0 3px 3px;
5786
5808
  }
5787
5809
  .lozenge a {
5788
5810
  color: inherit;
@@ -5998,6 +6020,64 @@ class LucosSearchComponent extends HTMLSpanElement {
5998
6020
  },
5999
6021
  },
6000
6022
  });
6023
+
6024
+ // fixDropdownContentPosition: work around a Chromium bug where, inside a shadow
6025
+ // DOM + CSS multi-column context, Chromium resolves position:absolute on
6026
+ // .ts-dropdown-content to the column-box boundary rather than the .ts-dropdown
6027
+ // containing block. We detect the multi-column context and switch to position:fixed
6028
+ // with explicit viewport coordinates, bypassing containing-block resolution entirely.
6029
+ const ts = selector.tomselect;
6030
+ let dropdownScrollHandler = null;
6031
+
6032
+ function isInMultiColumnLayout() {
6033
+ let el = component.parentElement;
6034
+ while (el && el !== document.body) {
6035
+ if (parseInt(window.getComputedStyle(el).columnCount) > 1) return true;
6036
+ el = el.parentElement;
6037
+ }
6038
+ return false;
6039
+ }
6040
+
6041
+ ts.on('dropdown_open', function (dropdown) {
6042
+ if (!isInMultiColumnLayout()) return;
6043
+ const content = ts.dropdown_content;
6044
+ // Set position:fixed BEFORE reading the bounding rect. Chromium has a bug
6045
+ // where, in shadow DOM + multi-column layouts, getBoundingClientRect() on
6046
+ // .ts-dropdown returns the column-box dimensions rather than the true visual
6047
+ // position while the content is in normal flow. Applying position:fixed first
6048
+ // takes the content out of flow, which forces Chromium to recompute .ts-dropdown's
6049
+ // layout correctly. Subsequent getBoundingClientRect() then returns accurate values.
6050
+ content.style.position = 'fixed';
6051
+ const rect = dropdown.getBoundingClientRect();
6052
+ if (!rect.width) {
6053
+ content.style.position = '';
6054
+ return;
6055
+ }
6056
+ content.style.top = rect.top + 'px';
6057
+ content.style.left = rect.left + 'px';
6058
+ content.style.width = rect.width + 'px';
6059
+ dropdownScrollHandler = function () {
6060
+ const r = dropdown.getBoundingClientRect();
6061
+ content.style.top = r.top + 'px';
6062
+ content.style.left = r.left + 'px';
6063
+ content.style.width = r.width + 'px';
6064
+ };
6065
+ window.addEventListener('scroll', dropdownScrollHandler, { passive: true, capture: true });
6066
+ window.addEventListener('resize', dropdownScrollHandler, { passive: true });
6067
+ });
6068
+
6069
+ ts.on('dropdown_close', function () {
6070
+ if (!dropdownScrollHandler) return;
6071
+ const content = ts.dropdown_content;
6072
+ content.style.position = '';
6073
+ content.style.top = '';
6074
+ content.style.left = '';
6075
+ content.style.width = '';
6076
+ window.removeEventListener('scroll', dropdownScrollHandler, { capture: true });
6077
+ window.removeEventListener('resize', dropdownScrollHandler);
6078
+ dropdownScrollHandler = null;
6079
+ });
6080
+
6001
6081
  if (selector.nextElementSibling) {
6002
6082
  shadow.append(selector.nextElementSibling);
6003
6083
  }
@@ -35,6 +35,26 @@
35
35
  <option selected>https://eolas.l42.eu/metadata/place/316/</option>
36
36
  <option selected>https://eolas.l42.eu/metadata/place/317/</option>
37
37
  </select></span>
38
+ <h1>Multi-column layout</h1>
39
+ <p>Dropdowns should open within the same column as their field, not overflow into the adjacent column.</p>
40
+ <div style="column-count: 2; column-gap: 2em; border: 1px solid #ccc; padding: 1em;">
41
+ <div style="break-inside: avoid;">
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>
44
+ </div>
45
+ <div style="break-inside: avoid;">
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>
48
+ </div>
49
+ <div style="break-inside: avoid;">
50
+ <label for="col-search3">Language:</label>
51
+ <span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}" data-types="Language"><select id="col-search3"></select></span>
52
+ </div>
53
+ <div style="break-inside: avoid;">
54
+ <label for="col-search4">About:</label>
55
+ <span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}"><select id="col-search4"></select></span>
56
+ </div>
57
+ </div>
38
58
  <h1>Form submission</h1>
39
59
  <form id="test-form" onsubmit="handleSubmit(event)">
40
60
  <label for="tags">Tags:</label><span is="lucos-search" data-api-key="${KEY_LUCOS_ARACHNE}"><select id="tags" name="tags" multiple></select></span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucos_search_component",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Web Components for searching lucOS data",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -5,7 +5,7 @@ import { buildFilterBy } from './filter.js';
5
5
 
6
6
  class LucosSearchComponent extends HTMLSpanElement {
7
7
  static get observedAttributes() {
8
- return ['data-api-key','data-types','data-exclude-types','data-is-contact','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'];
9
9
  }
10
10
  constructor() {
11
11
  super();
@@ -68,8 +68,30 @@ class LucosSearchComponent extends HTMLSpanElement {
68
68
  margin: 0 3px;
69
69
  padding: 2px 6px;
70
70
  }
71
+ /* Prevent dropdown overflowing into an adjacent column in multi-column layouts.
72
+ * Consumers may embed lucos-search in a CSS column-count context (e.g. a 2-column
73
+ * form). Because TomSelect's DOM lives inside this shadow root, page-level CSS
74
+ * cannot fix column overflow from outside.
75
+ *
76
+ * The actual fix is applied in JavaScript (see fixDropdownContentPosition below):
77
+ * Chromium has a bug where position:absolute on a shadow-DOM descendant inside a
78
+ * multi-column container resolves the containing block to the column-box boundary
79
+ * rather than the nearest positioned ancestor (.ts-dropdown). CSS-only approaches
80
+ * (position:absolute with explicit top/left, contain:layout) do not override this.
81
+ * The JS fix switches .ts-dropdown-content to position:fixed when the dropdown opens
82
+ * inside a multi-column layout, bypassing the containing-block resolution entirely. */
71
83
  .ts-dropdown {
72
- margin: 0;
84
+ border: none;
85
+ margin: -3px 0 0 0;
86
+ }
87
+ .ts-dropdown-content {
88
+ background: #fff;
89
+ border: 1px solid #d0d0d0;
90
+ margin: 0.25rem 0 0;
91
+ border-top: 0 none;
92
+ box-sizing: border-box;
93
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
94
+ border-radius: 0 0 3px 3px;
73
95
  }
74
96
  .lozenge a {
75
97
  color: inherit;
@@ -285,6 +307,64 @@ class LucosSearchComponent extends HTMLSpanElement {
285
307
  },
286
308
  },
287
309
  });
310
+
311
+ // fixDropdownContentPosition: work around a Chromium bug where, inside a shadow
312
+ // DOM + CSS multi-column context, Chromium resolves position:absolute on
313
+ // .ts-dropdown-content to the column-box boundary rather than the .ts-dropdown
314
+ // containing block. We detect the multi-column context and switch to position:fixed
315
+ // with explicit viewport coordinates, bypassing containing-block resolution entirely.
316
+ const ts = selector.tomselect;
317
+ let dropdownScrollHandler = null;
318
+
319
+ function isInMultiColumnLayout() {
320
+ let el = component.parentElement;
321
+ while (el && el !== document.body) {
322
+ if (parseInt(window.getComputedStyle(el).columnCount) > 1) return true;
323
+ el = el.parentElement;
324
+ }
325
+ return false;
326
+ }
327
+
328
+ ts.on('dropdown_open', function (dropdown) {
329
+ if (!isInMultiColumnLayout()) return;
330
+ const content = ts.dropdown_content;
331
+ // Set position:fixed BEFORE reading the bounding rect. Chromium has a bug
332
+ // where, in shadow DOM + multi-column layouts, getBoundingClientRect() on
333
+ // .ts-dropdown returns the column-box dimensions rather than the true visual
334
+ // position while the content is in normal flow. Applying position:fixed first
335
+ // takes the content out of flow, which forces Chromium to recompute .ts-dropdown's
336
+ // layout correctly. Subsequent getBoundingClientRect() then returns accurate values.
337
+ content.style.position = 'fixed';
338
+ const rect = dropdown.getBoundingClientRect();
339
+ if (!rect.width) {
340
+ content.style.position = '';
341
+ return;
342
+ }
343
+ content.style.top = rect.top + 'px';
344
+ content.style.left = rect.left + 'px';
345
+ content.style.width = rect.width + 'px';
346
+ dropdownScrollHandler = function () {
347
+ const r = dropdown.getBoundingClientRect();
348
+ content.style.top = r.top + 'px';
349
+ content.style.left = r.left + 'px';
350
+ content.style.width = r.width + 'px';
351
+ };
352
+ window.addEventListener('scroll', dropdownScrollHandler, { passive: true, capture: true });
353
+ window.addEventListener('resize', dropdownScrollHandler, { passive: true });
354
+ });
355
+
356
+ ts.on('dropdown_close', function () {
357
+ if (!dropdownScrollHandler) return;
358
+ const content = ts.dropdown_content;
359
+ content.style.position = '';
360
+ content.style.top = '';
361
+ content.style.left = '';
362
+ content.style.width = '';
363
+ window.removeEventListener('scroll', dropdownScrollHandler, { capture: true });
364
+ window.removeEventListener('resize', dropdownScrollHandler);
365
+ dropdownScrollHandler = null;
366
+ });
367
+
288
368
  if (selector.nextElementSibling) {
289
369
  shadow.append(selector.nextElementSibling);
290
370
  }