@internetarchive/collection-browser 4.1.0 → 4.2.0-alpha-webdev8164.0
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/.editorconfig +29 -29
- package/.github/workflows/ci.yml +27 -27
- package/.github/workflows/gh-pages-main.yml +39 -39
- package/.github/workflows/npm-publish.yml +39 -39
- package/.github/workflows/pr-preview.yml +38 -38
- package/.husky/pre-commit +1 -1
- package/.prettierignore +1 -1
- package/LICENSE +661 -661
- package/README.md +83 -83
- package/dist/src/collection-browser.js +761 -761
- package/dist/src/collection-browser.js.map +1 -1
- package/dist/src/collection-facets/facets-template.js +5 -0
- package/dist/src/collection-facets/facets-template.js.map +1 -1
- package/dist/src/collection-facets/more-facets-content.d.ts +92 -8
- package/dist/src/collection-facets/more-facets-content.js +526 -84
- package/dist/src/collection-facets/more-facets-content.js.map +1 -1
- package/dist/src/collection-facets/more-facets-pagination.d.ts +12 -3
- package/dist/src/collection-facets/more-facets-pagination.js +69 -8
- package/dist/src/collection-facets/more-facets-pagination.js.map +1 -1
- package/dist/src/collection-facets/toggle-switch.js +1 -0
- package/dist/src/collection-facets/toggle-switch.js.map +1 -1
- package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
- package/dist/src/data-source/collection-browser-query-state.js.map +1 -1
- package/dist/src/sort-filter-bar/sort-filter-bar.js +280 -280
- package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
- package/dist/test/collection-browser.test.js +189 -189
- package/dist/test/collection-browser.test.js.map +1 -1
- package/dist/test/collection-facets/more-facets-content.test.js +162 -3
- package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
- package/dist/test/collection-facets/more-facets-pagination.test.js +63 -3
- package/dist/test/collection-facets/more-facets-pagination.test.js.map +1 -1
- package/dist/test/mocks/mock-search-responses.d.ts +5 -0
- package/dist/test/mocks/mock-search-responses.js +44 -0
- package/dist/test/mocks/mock-search-responses.js.map +1 -1
- package/dist/test/mocks/mock-search-service.js +2 -1
- package/dist/test/mocks/mock-search-service.js.map +1 -1
- package/dist/test/sort-filter-bar/sort-filter-bar.test.js +22 -22
- package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
- package/eslint.config.mjs +53 -53
- package/index.html +24 -24
- package/local.archive.org.cert +86 -86
- package/local.archive.org.key +27 -27
- package/package.json +121 -120
- package/renovate.json +6 -6
- package/src/collection-browser.ts +3070 -3070
- package/src/collection-facets/facets-template.ts +5 -0
- package/src/collection-facets/more-facets-content.ts +566 -96
- package/src/collection-facets/more-facets-pagination.ts +80 -9
- package/src/collection-facets/toggle-switch.ts +1 -0
- package/src/data-source/collection-browser-data-source.ts +1444 -1444
- package/src/data-source/collection-browser-query-state.ts +60 -60
- package/src/sort-filter-bar/sort-filter-bar.ts +733 -733
- package/test/collection-browser.test.ts +2402 -2402
- package/test/collection-facets/more-facets-content.test.ts +251 -4
- package/test/collection-facets/more-facets-pagination.test.ts +87 -3
- package/test/mocks/mock-search-responses.ts +48 -0
- package/test/mocks/mock-search-service.ts +2 -0
- package/test/sort-filter-bar/sort-filter-bar.test.ts +443 -443
- package/tsconfig.json +25 -25
- package/web-dev-server.config.mjs +30 -30
- package/web-test-runner.config.mjs +52 -52
- package/.claude/settings.local.json +0 -8
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
PropertyValues,
|
|
8
8
|
TemplateResult,
|
|
9
9
|
} from 'lit';
|
|
10
|
-
import { customElement, property, state } from 'lit/decorators.js';
|
|
10
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
11
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
12
|
+
import { when } from 'lit/directives/when.js';
|
|
11
13
|
import {
|
|
12
14
|
Aggregation,
|
|
13
15
|
Bucket,
|
|
@@ -40,13 +42,16 @@ import type {
|
|
|
40
42
|
TVChannelAliases,
|
|
41
43
|
} from '../data-source/models';
|
|
42
44
|
import '@internetarchive/elements/ia-status-indicator/ia-status-indicator';
|
|
43
|
-
import './more-facets-pagination';
|
|
44
45
|
import './facets-template';
|
|
45
46
|
import {
|
|
46
47
|
analyticsActions,
|
|
47
48
|
analyticsCategories,
|
|
48
49
|
} from '../utils/analytics-events';
|
|
49
50
|
import './toggle-switch';
|
|
51
|
+
import './more-facets-pagination';
|
|
52
|
+
import '@internetarchive/ia-clearable-text-input';
|
|
53
|
+
import arrowLeftIcon from '../assets/img/icons/arrow-left';
|
|
54
|
+
import arrowRightIcon from '../assets/img/icons/arrow-right';
|
|
50
55
|
import { srOnlyStyle } from '../styles/sr-only';
|
|
51
56
|
import {
|
|
52
57
|
mergeSelectedFacets,
|
|
@@ -58,6 +63,12 @@ import {
|
|
|
58
63
|
MORE_FACETS__MAX_AGGREGATIONS,
|
|
59
64
|
} from './models';
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Threshold for switching from horizontal scroll to pagination.
|
|
68
|
+
* If facet count >= this value, use pagination. Otherwise use horizontal scroll.
|
|
69
|
+
*/
|
|
70
|
+
const PAGINATION_THRESHOLD = 1000;
|
|
71
|
+
|
|
61
72
|
@customElement('more-facets-content')
|
|
62
73
|
export class MoreFacetsContent extends LitElement {
|
|
63
74
|
@property({ type: String }) facetKey?: FacetOption;
|
|
@@ -126,10 +137,38 @@ export class MoreFacetsContent extends LitElement {
|
|
|
126
137
|
getDefaultSelectedFacets();
|
|
127
138
|
|
|
128
139
|
/**
|
|
129
|
-
*
|
|
140
|
+
* Text entered by the user to filter facet buckets.
|
|
141
|
+
* Applied to bucket.key for case-insensitive matching.
|
|
142
|
+
*/
|
|
143
|
+
@state() private filterText = '';
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Current page number for pagination (when facet count >= PAGINATION_THRESHOLD).
|
|
130
147
|
*/
|
|
131
148
|
@state() private pageNumber = 1;
|
|
132
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Whether the component is narrow enough to warrant compact pagination.
|
|
152
|
+
* Updated via a ResizeObserver-based container query approach.
|
|
153
|
+
*/
|
|
154
|
+
@state() private isCompactView = false;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether the horizontal scroll is at the leftmost position.
|
|
158
|
+
*/
|
|
159
|
+
@state() private atScrollStart = true;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether the horizontal scroll is at the rightmost position.
|
|
163
|
+
*/
|
|
164
|
+
@state() private atScrollEnd = false;
|
|
165
|
+
|
|
166
|
+
@query('ia-clearable-text-input')
|
|
167
|
+
private filterInput!: HTMLElement;
|
|
168
|
+
|
|
169
|
+
@query('.facets-content')
|
|
170
|
+
private facetsContentEl!: HTMLElement;
|
|
171
|
+
|
|
133
172
|
willUpdate(changed: PropertyValues): void {
|
|
134
173
|
if (
|
|
135
174
|
changed.has('aggregations') ||
|
|
@@ -143,6 +182,13 @@ export class MoreFacetsContent extends LitElement {
|
|
|
143
182
|
this.facetGroup = this.mergedFacets;
|
|
144
183
|
}
|
|
145
184
|
|
|
185
|
+
// Reset to page 1 when filter text changes (only matters for pagination mode)
|
|
186
|
+
if (changed.has('filterText')) {
|
|
187
|
+
this.pageNumber = 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
updated(changed: PropertyValues): void {
|
|
146
192
|
// If any of the search properties change, it triggers a facet fetch
|
|
147
193
|
if (
|
|
148
194
|
changed.has('facetKey') ||
|
|
@@ -159,10 +205,149 @@ export class MoreFacetsContent extends LitElement {
|
|
|
159
205
|
|
|
160
206
|
this.updateSpecificFacets();
|
|
161
207
|
}
|
|
208
|
+
|
|
209
|
+
// Reset horizontal scroll when filter text changes (e.g., switching from
|
|
210
|
+
// horizontal-scroll mode back to pagination mode)
|
|
211
|
+
if (changed.has('filterText')) {
|
|
212
|
+
const facetsContent = this.shadowRoot?.querySelector('.facets-content');
|
|
213
|
+
if (facetsContent) {
|
|
214
|
+
facetsContent.scrollLeft = 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Manage scroll listener for horizontal scroll mode arrows.
|
|
219
|
+
// Only re-evaluate when properties that affect the displayed content change.
|
|
220
|
+
if (
|
|
221
|
+
changed.has('filterText') ||
|
|
222
|
+
changed.has('aggregations') ||
|
|
223
|
+
changed.has('facetKey') ||
|
|
224
|
+
changed.has('sortedBy') ||
|
|
225
|
+
changed.has('selectedFacets') ||
|
|
226
|
+
changed.has('unappliedFacetChanges')
|
|
227
|
+
) {
|
|
228
|
+
if (!this.usePagination) {
|
|
229
|
+
this.attachScrollListener();
|
|
230
|
+
// Refresh scroll state whenever content may have changed (e.g., filtering)
|
|
231
|
+
requestAnimationFrame(() => this.updateScrollState());
|
|
232
|
+
} else {
|
|
233
|
+
this.removeScrollListener();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
162
236
|
}
|
|
163
237
|
|
|
238
|
+
private resizeObserver?: ResizeObserver;
|
|
239
|
+
|
|
164
240
|
firstUpdated(): void {
|
|
165
241
|
this.setupEscapeListeners();
|
|
242
|
+
this.setupCompactViewObserver();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
disconnectedCallback(): void {
|
|
246
|
+
super.disconnectedCallback();
|
|
247
|
+
this.resizeObserver?.disconnect();
|
|
248
|
+
this.removeScrollListener();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private scrollHandler = () => this.updateScrollState();
|
|
252
|
+
|
|
253
|
+
private scrollListenerAttached = false;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Attaches a scroll event listener to the facets content element
|
|
257
|
+
* to track horizontal scroll position for arrow button states.
|
|
258
|
+
*/
|
|
259
|
+
private attachScrollListener(): void {
|
|
260
|
+
if (this.scrollListenerAttached || !this.facetsContentEl) return;
|
|
261
|
+
this.facetsContentEl.addEventListener('scroll', this.scrollHandler, {
|
|
262
|
+
passive: true,
|
|
263
|
+
});
|
|
264
|
+
this.scrollListenerAttached = true;
|
|
265
|
+
// Defer initial state check until after browser layout, so scrollWidth
|
|
266
|
+
// reflects the actual content dimensions.
|
|
267
|
+
requestAnimationFrame(() => this.updateScrollState());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private removeScrollListener(): void {
|
|
271
|
+
if (!this.scrollListenerAttached || !this.facetsContentEl) return;
|
|
272
|
+
this.facetsContentEl.removeEventListener('scroll', this.scrollHandler);
|
|
273
|
+
this.scrollListenerAttached = false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Updates the scroll arrow disabled states based on current scroll position.
|
|
278
|
+
*/
|
|
279
|
+
private updateScrollState(): void {
|
|
280
|
+
const el = this.facetsContentEl;
|
|
281
|
+
if (!el) return;
|
|
282
|
+
this.atScrollStart = el.scrollLeft <= 0;
|
|
283
|
+
this.atScrollEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Calculates the width of one column step (column width + gap) based on
|
|
288
|
+
* the CSS multi-column layout of the scroll container.
|
|
289
|
+
*/
|
|
290
|
+
private getColumnStep(): number {
|
|
291
|
+
const el = this.facetsContentEl;
|
|
292
|
+
if (!el) return 0;
|
|
293
|
+
|
|
294
|
+
const facetRows = el.querySelector('.facet-rows') as HTMLElement;
|
|
295
|
+
const styles = facetRows
|
|
296
|
+
? getComputedStyle(facetRows)
|
|
297
|
+
: getComputedStyle(el);
|
|
298
|
+
|
|
299
|
+
const columnCount = parseInt(styles.columnCount, 10) || 3;
|
|
300
|
+
const columnGap = parseInt(styles.columnGap, 10) || 15;
|
|
301
|
+
|
|
302
|
+
// Column width = (visible width - total gaps) / column count
|
|
303
|
+
// Column step = column width + gap = (visible width + gap) / column count
|
|
304
|
+
return (el.clientWidth + columnGap) / columnCount;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Snaps a scroll target to the nearest column boundary.
|
|
309
|
+
*/
|
|
310
|
+
private snapToColumn(target: number): number {
|
|
311
|
+
const step = this.getColumnStep();
|
|
312
|
+
if (step <= 0) return target;
|
|
313
|
+
return Math.round(target / step) * step;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Scrolls the facet content left by approximately one page, snapping to
|
|
318
|
+
* the nearest column boundary.
|
|
319
|
+
*/
|
|
320
|
+
private onScrollLeft(): void {
|
|
321
|
+
const el = this.facetsContentEl;
|
|
322
|
+
if (!el) return;
|
|
323
|
+
const rawTarget = el.scrollLeft - el.clientWidth;
|
|
324
|
+
const snapped = Math.max(0, this.snapToColumn(rawTarget));
|
|
325
|
+
el.scrollTo({ left: snapped, behavior: 'smooth' });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Scrolls the facet content right by approximately one page, snapping to
|
|
330
|
+
* the nearest column boundary.
|
|
331
|
+
*/
|
|
332
|
+
private onScrollRight(): void {
|
|
333
|
+
const el = this.facetsContentEl;
|
|
334
|
+
if (!el) return;
|
|
335
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
336
|
+
const rawTarget = el.scrollLeft + el.clientWidth;
|
|
337
|
+
const snapped = Math.min(maxScroll, this.snapToColumn(rawTarget));
|
|
338
|
+
el.scrollTo({ left: snapped, behavior: 'smooth' });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Sets up a ResizeObserver to toggle compact pagination based on component width.
|
|
343
|
+
*/
|
|
344
|
+
private setupCompactViewObserver(): void {
|
|
345
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
this.isCompactView = entry.contentRect.width <= 560;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
this.resizeObserver.observe(this);
|
|
166
351
|
}
|
|
167
352
|
|
|
168
353
|
/**
|
|
@@ -227,22 +412,6 @@ export class MoreFacetsContent extends LitElement {
|
|
|
227
412
|
}
|
|
228
413
|
}
|
|
229
414
|
|
|
230
|
-
/**
|
|
231
|
-
* Handler for page number changes from the pagination widget.
|
|
232
|
-
*/
|
|
233
|
-
private pageNumberClicked(e: CustomEvent<{ page: number }>) {
|
|
234
|
-
const page = e?.detail?.page;
|
|
235
|
-
if (page) {
|
|
236
|
-
this.pageNumber = Number(page);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
this.analyticsHandler?.sendEvent({
|
|
240
|
-
category: analyticsCategories.default,
|
|
241
|
-
action: analyticsActions.moreFacetsPageChange,
|
|
242
|
-
label: `${this.pageNumber}`,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
415
|
/**
|
|
247
416
|
* Combines the selected facets with the aggregations to create a single list of facets
|
|
248
417
|
*/
|
|
@@ -387,29 +556,81 @@ export class MoreFacetsContent extends LitElement {
|
|
|
387
556
|
}
|
|
388
557
|
|
|
389
558
|
/**
|
|
390
|
-
* Returns
|
|
559
|
+
* Returns the facet group with buckets filtered by the current filter text.
|
|
560
|
+
* Filters are applied to the full bucket list before pagination.
|
|
391
561
|
*/
|
|
392
|
-
private get
|
|
393
|
-
const { facetGroup } = this;
|
|
562
|
+
private get filteredFacetGroup(): FacetGroup | undefined {
|
|
563
|
+
const { facetGroup, filterText } = this;
|
|
394
564
|
if (!facetGroup) return undefined;
|
|
395
565
|
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
566
|
+
// If no filter text, return the full group
|
|
567
|
+
if (!filterText.trim()) {
|
|
568
|
+
return facetGroup;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Filter buckets by the text the user actually sees.
|
|
572
|
+
// For collections, match against the displayed collection title (not the identifier).
|
|
573
|
+
// For other facet types, match against the bucket key (which is also the display text).
|
|
574
|
+
const lowerFilter = filterText.toLowerCase().trim();
|
|
575
|
+
const filteredBuckets = facetGroup.buckets.filter(bucket => {
|
|
576
|
+
const displayText = this.collectionTitles?.get(bucket.key) ?? bucket.key;
|
|
577
|
+
return displayText.toLowerCase().includes(lowerFilter);
|
|
578
|
+
});
|
|
402
579
|
|
|
403
580
|
return {
|
|
404
581
|
...facetGroup,
|
|
405
|
-
buckets:
|
|
582
|
+
buckets: filteredBuckets,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Determines whether to use pagination based on the number of filtered facets.
|
|
588
|
+
* Returns true if facet count >= PAGINATION_THRESHOLD, false otherwise.
|
|
589
|
+
*/
|
|
590
|
+
private get usePagination(): boolean {
|
|
591
|
+
const facetCount = this.filteredFacetGroup?.buckets.length ?? 0;
|
|
592
|
+
return facetCount >= PAGINATION_THRESHOLD;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Returns the facet group for the current page.
|
|
597
|
+
* If using pagination (>= 1000 facets), slices to show only the current page.
|
|
598
|
+
* Otherwise, returns all facets for horizontal scrolling.
|
|
599
|
+
*/
|
|
600
|
+
private get facetGroupForCurrentPage(): FacetGroup | undefined {
|
|
601
|
+
const filteredGroup = this.filteredFacetGroup;
|
|
602
|
+
if (!filteredGroup) return undefined;
|
|
603
|
+
|
|
604
|
+
// If facet count is below threshold, show all facets with horizontal scroll
|
|
605
|
+
if (!this.usePagination) {
|
|
606
|
+
return filteredGroup;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Otherwise, use pagination - slice to current page
|
|
610
|
+
const startIndex = (this.pageNumber - 1) * this.facetsPerPage;
|
|
611
|
+
const endIndex = startIndex + this.facetsPerPage;
|
|
612
|
+
const slicedBuckets = filteredGroup.buckets.slice(startIndex, endIndex);
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
...filteredGroup,
|
|
616
|
+
buckets: slicedBuckets,
|
|
406
617
|
};
|
|
407
618
|
}
|
|
408
619
|
|
|
409
620
|
private get moreFacetsTemplate(): TemplateResult {
|
|
621
|
+
const facetGroup = this.facetGroupForCurrentPage;
|
|
622
|
+
|
|
623
|
+
// Show empty state if filtering returned no results
|
|
624
|
+
if (
|
|
625
|
+
this.filterText.trim() &&
|
|
626
|
+
(!facetGroup || facetGroup.buckets.length === 0)
|
|
627
|
+
) {
|
|
628
|
+
return this.emptyFilterResultsTemplate;
|
|
629
|
+
}
|
|
630
|
+
|
|
410
631
|
return html`
|
|
411
632
|
<facets-template
|
|
412
|
-
.facetGroup=${
|
|
633
|
+
.facetGroup=${facetGroup}
|
|
413
634
|
.selectedFacets=${this.selectedFacets}
|
|
414
635
|
.collectionTitles=${this.collectionTitles}
|
|
415
636
|
@facetClick=${(e: CustomEvent<FacetEventDetails>) => {
|
|
@@ -434,50 +655,51 @@ export class MoreFacetsContent extends LitElement {
|
|
|
434
655
|
`;
|
|
435
656
|
}
|
|
436
657
|
|
|
658
|
+
private get emptyFilterResultsTemplate(): TemplateResult {
|
|
659
|
+
return html`
|
|
660
|
+
<div class="empty-results">
|
|
661
|
+
<p>${msg('No matching values found.')}</p>
|
|
662
|
+
<p class="hint">${msg('Try a different search term.')}</p>
|
|
663
|
+
</div>
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
|
|
437
667
|
/**
|
|
438
|
-
*
|
|
668
|
+
* Number of pages for pagination (only used when facet count >= PAGINATION_THRESHOLD).
|
|
439
669
|
*/
|
|
440
670
|
private get paginationSize(): number {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Calculate the appropriate number of pages to show in the modal pagination widget
|
|
444
|
-
const length = this.aggregations[this.facetKey]?.buckets.length;
|
|
445
|
-
return Math.ceil(length / this.facetsPerPage);
|
|
671
|
+
const filteredBuckets = this.filteredFacetGroup?.buckets ?? [];
|
|
672
|
+
return Math.ceil(filteredBuckets.length / this.facetsPerPage);
|
|
446
673
|
}
|
|
447
674
|
|
|
448
|
-
|
|
675
|
+
/**
|
|
676
|
+
* Template for pagination component.
|
|
677
|
+
*/
|
|
449
678
|
private get facetsPaginationTemplate() {
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
: nothing;
|
|
679
|
+
return html`<more-facets-pagination
|
|
680
|
+
.size=${this.paginationSize}
|
|
681
|
+
.currentPage=${this.pageNumber}
|
|
682
|
+
.compact=${this.isCompactView}
|
|
683
|
+
@pageNumberClicked=${this.pageNumberClicked}
|
|
684
|
+
></more-facets-pagination>`;
|
|
457
685
|
}
|
|
458
686
|
|
|
459
687
|
private get footerTemplate() {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
Apply filters
|
|
476
|
-
</button>
|
|
477
|
-
</div> `;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return nothing;
|
|
688
|
+
return html`
|
|
689
|
+
${when(this.usePagination, () => this.facetsPaginationTemplate)}
|
|
690
|
+
<div class="footer">
|
|
691
|
+
<button class="btn btn-cancel" type="button" @click=${this.cancelClick}>
|
|
692
|
+
Cancel
|
|
693
|
+
</button>
|
|
694
|
+
<button
|
|
695
|
+
class="btn btn-submit"
|
|
696
|
+
type="button"
|
|
697
|
+
@click=${this.applySearchFacetsClicked}
|
|
698
|
+
>
|
|
699
|
+
Apply filters
|
|
700
|
+
</button>
|
|
701
|
+
</div>
|
|
702
|
+
`;
|
|
481
703
|
}
|
|
482
704
|
|
|
483
705
|
private sortFacetAggregation(facetSortType: AggregationSortType) {
|
|
@@ -487,6 +709,40 @@ export class MoreFacetsContent extends LitElement {
|
|
|
487
709
|
);
|
|
488
710
|
}
|
|
489
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Handler for filter input changes. Updates the filter text and triggers re-render.
|
|
714
|
+
*/
|
|
715
|
+
private handleFilterInput(e: Event): void {
|
|
716
|
+
const input = e.target as HTMLElement & { value: string };
|
|
717
|
+
this.filterText = input.value;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Handler for when the filter input is cleared via the clear button.
|
|
722
|
+
*/
|
|
723
|
+
private handleFilterClear(): void {
|
|
724
|
+
this.filterText = '';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Handler for pagination page number clicks.
|
|
729
|
+
* Only used when facet count >= PAGINATION_THRESHOLD.
|
|
730
|
+
*/
|
|
731
|
+
private pageNumberClicked(e: CustomEvent<{ page: number }>) {
|
|
732
|
+
this.pageNumber = e.detail.page;
|
|
733
|
+
|
|
734
|
+
// Track page navigation in analytics
|
|
735
|
+
this.analyticsHandler?.sendEvent({
|
|
736
|
+
category: analyticsCategories.default,
|
|
737
|
+
action: analyticsActions.moreFacetsPageChange,
|
|
738
|
+
label: `${this.pageNumber}`,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
this.dispatchEvent(
|
|
742
|
+
new CustomEvent('pageChanged', { detail: this.pageNumber }),
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
490
746
|
private get modalHeaderTemplate(): TemplateResult {
|
|
491
747
|
const facetSort =
|
|
492
748
|
this.sortedBy ?? defaultFacetSort[this.facetKey as FacetOption];
|
|
@@ -494,36 +750,94 @@ export class MoreFacetsContent extends LitElement {
|
|
|
494
750
|
facetSort === AggregationSortType.COUNT ? 'left' : 'right';
|
|
495
751
|
|
|
496
752
|
return html`<span class="sr-only">${msg('More facets for:')}</span>
|
|
497
|
-
<span class="title">
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
753
|
+
<span class="title"> ${this.facetGroup?.title} </span>
|
|
754
|
+
<span class="header-controls">
|
|
755
|
+
<span class="sort-controls">
|
|
756
|
+
<label class="sort-label">${msg('Sort by:')}</label>
|
|
757
|
+
${this.facetKey
|
|
758
|
+
? html`<toggle-switch
|
|
759
|
+
class="sort-toggle"
|
|
760
|
+
leftValue=${AggregationSortType.COUNT}
|
|
761
|
+
leftLabel="Count"
|
|
762
|
+
rightValue=${valueFacetSort[this.facetKey]}
|
|
763
|
+
.rightLabel=${this.facetGroup?.title}
|
|
764
|
+
side=${defaultSwitchSide}
|
|
765
|
+
@change=${(e: CustomEvent<string>) => {
|
|
766
|
+
this.sortFacetAggregation(
|
|
767
|
+
Number(e.detail) as AggregationSortType,
|
|
768
|
+
);
|
|
769
|
+
}}
|
|
770
|
+
></toggle-switch>`
|
|
771
|
+
: nothing}
|
|
772
|
+
</span>
|
|
773
|
+
|
|
774
|
+
<span class="filter-controls">
|
|
775
|
+
<label class="filter-label">${msg('Filter by:')}</label>
|
|
776
|
+
<ia-clearable-text-input
|
|
777
|
+
class="filter-input"
|
|
778
|
+
.value=${this.filterText}
|
|
779
|
+
.placeholder=${msg('Search...')}
|
|
780
|
+
.screenReaderLabel=${msg('Filter facets')}
|
|
781
|
+
.clearButtonScreenReaderLabel=${msg('Clear filter')}
|
|
782
|
+
@input=${this.handleFilterInput}
|
|
783
|
+
@clear=${this.handleFilterClear}
|
|
784
|
+
></ia-clearable-text-input>
|
|
785
|
+
</span>
|
|
516
786
|
</span>`;
|
|
517
787
|
}
|
|
518
788
|
|
|
519
789
|
render() {
|
|
790
|
+
const sectionClasses = classMap({
|
|
791
|
+
'pagination-mode': this.usePagination,
|
|
792
|
+
'horizontal-scroll-mode': !this.usePagination,
|
|
793
|
+
});
|
|
794
|
+
const contentClasses = classMap({
|
|
795
|
+
'facets-content': true,
|
|
796
|
+
'pagination-mode': this.usePagination,
|
|
797
|
+
'horizontal-scroll-mode': !this.usePagination,
|
|
798
|
+
});
|
|
799
|
+
|
|
520
800
|
return html`
|
|
521
801
|
${this.facetsLoading
|
|
522
802
|
? this.loaderTemplate
|
|
523
803
|
: html`
|
|
524
|
-
<section id="more-facets">
|
|
804
|
+
<section id="more-facets" class=${sectionClasses}>
|
|
525
805
|
<div class="header-content">${this.modalHeaderTemplate}</div>
|
|
526
|
-
|
|
806
|
+
${this.usePagination
|
|
807
|
+
? html`<div class=${contentClasses}>
|
|
808
|
+
${this.moreFacetsTemplate}
|
|
809
|
+
</div>`
|
|
810
|
+
: html`<div class="scroll-nav-container">
|
|
811
|
+
${when(
|
|
812
|
+
!this.atScrollStart || !this.atScrollEnd,
|
|
813
|
+
() =>
|
|
814
|
+
html`<button
|
|
815
|
+
class="scroll-arrow scroll-left"
|
|
816
|
+
@click=${this.onScrollLeft}
|
|
817
|
+
?disabled=${this.atScrollStart}
|
|
818
|
+
aria-label="Scroll facets left"
|
|
819
|
+
>
|
|
820
|
+
${arrowLeftIcon}
|
|
821
|
+
</button>`,
|
|
822
|
+
)}
|
|
823
|
+
<div class=${contentClasses}>
|
|
824
|
+
<div class="facets-horizontal-container">
|
|
825
|
+
${this.moreFacetsTemplate}
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
${when(
|
|
829
|
+
!this.atScrollStart || !this.atScrollEnd,
|
|
830
|
+
() =>
|
|
831
|
+
html`<button
|
|
832
|
+
class="scroll-arrow scroll-right"
|
|
833
|
+
@click=${this.onScrollRight}
|
|
834
|
+
?disabled=${this.atScrollEnd}
|
|
835
|
+
aria-label="Scroll facets right"
|
|
836
|
+
>
|
|
837
|
+
${arrowRightIcon}
|
|
838
|
+
</button>`,
|
|
839
|
+
)}
|
|
840
|
+
</div>`}
|
|
527
841
|
${this.footerTemplate}
|
|
528
842
|
</section>
|
|
529
843
|
`}
|
|
@@ -546,6 +860,9 @@ export class MoreFacetsContent extends LitElement {
|
|
|
546
860
|
// Reset the unapplied changes back to default, now that they have been applied
|
|
547
861
|
this.unappliedFacetChanges = getDefaultSelectedFacets();
|
|
548
862
|
|
|
863
|
+
// Reset filter text
|
|
864
|
+
this.filterText = '';
|
|
865
|
+
|
|
549
866
|
this.modalManager?.closeModal();
|
|
550
867
|
this.analyticsHandler?.sendEvent({
|
|
551
868
|
category: analyticsCategories.default,
|
|
@@ -558,6 +875,9 @@ export class MoreFacetsContent extends LitElement {
|
|
|
558
875
|
// Reset the unapplied changes back to default
|
|
559
876
|
this.unappliedFacetChanges = getDefaultSelectedFacets();
|
|
560
877
|
|
|
878
|
+
// Reset filter text
|
|
879
|
+
this.filterText = '';
|
|
880
|
+
|
|
561
881
|
this.modalManager?.closeModal();
|
|
562
882
|
this.analyticsHandler?.sendEvent({
|
|
563
883
|
category: analyticsCategories.default,
|
|
@@ -573,10 +893,23 @@ export class MoreFacetsContent extends LitElement {
|
|
|
573
893
|
srOnlyStyle,
|
|
574
894
|
css`
|
|
575
895
|
section#more-facets {
|
|
576
|
-
|
|
577
|
-
|
|
896
|
+
display: flex;
|
|
897
|
+
flex-direction: column;
|
|
898
|
+
max-height: calc(100vh - 16.5rem);
|
|
899
|
+
padding: 10px;
|
|
900
|
+
box-sizing: border-box;
|
|
578
901
|
--facetsColumnCount: 3;
|
|
579
902
|
}
|
|
903
|
+
|
|
904
|
+
/* Both modes need a height constraint for proper column flow */
|
|
905
|
+
section#more-facets.horizontal-scroll-mode,
|
|
906
|
+
section#more-facets.pagination-mode {
|
|
907
|
+
--facetsMaxHeight: 280px;
|
|
908
|
+
}
|
|
909
|
+
.header-content {
|
|
910
|
+
flex-shrink: 0;
|
|
911
|
+
}
|
|
912
|
+
|
|
580
913
|
.header-content .title {
|
|
581
914
|
display: block;
|
|
582
915
|
text-align: left;
|
|
@@ -585,8 +918,22 @@ export class MoreFacetsContent extends LitElement {
|
|
|
585
918
|
font-weight: bold;
|
|
586
919
|
}
|
|
587
920
|
|
|
921
|
+
.header-controls {
|
|
922
|
+
display: flex;
|
|
923
|
+
flex-wrap: wrap;
|
|
924
|
+
align-items: center;
|
|
925
|
+
gap: 4px 20px;
|
|
926
|
+
padding: 0 10px;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.sort-controls {
|
|
930
|
+
display: inline-flex;
|
|
931
|
+
align-items: center;
|
|
932
|
+
white-space: nowrap;
|
|
933
|
+
gap: 5px;
|
|
934
|
+
}
|
|
935
|
+
|
|
588
936
|
.sort-label {
|
|
589
|
-
margin-left: 20px;
|
|
590
937
|
font-size: 1.3rem;
|
|
591
938
|
}
|
|
592
939
|
|
|
@@ -594,11 +941,115 @@ export class MoreFacetsContent extends LitElement {
|
|
|
594
941
|
font-weight: normal;
|
|
595
942
|
}
|
|
596
943
|
|
|
944
|
+
.filter-controls {
|
|
945
|
+
display: inline-flex;
|
|
946
|
+
align-items: center;
|
|
947
|
+
white-space: nowrap;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.filter-label {
|
|
951
|
+
font-size: 1.3rem;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.filter-input {
|
|
955
|
+
--input-height: 2.5rem;
|
|
956
|
+
--input-font-size: 1.3rem;
|
|
957
|
+
--input-border-radius: 4px;
|
|
958
|
+
--input-padding: 4px 8px;
|
|
959
|
+
--input-focused-border-color: ${modalSubmitButton};
|
|
960
|
+
width: 150px;
|
|
961
|
+
margin-left: 5px;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.empty-results {
|
|
965
|
+
text-align: center;
|
|
966
|
+
padding: 40px 20px;
|
|
967
|
+
color: #666;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.empty-results .hint {
|
|
971
|
+
margin-top: 10px;
|
|
972
|
+
}
|
|
973
|
+
|
|
597
974
|
.facets-content {
|
|
598
975
|
font-size: 1.2rem;
|
|
599
|
-
|
|
600
|
-
|
|
976
|
+
flex: 1 1 auto;
|
|
977
|
+
min-height: 0;
|
|
978
|
+
overflow-y: auto;
|
|
979
|
+
overflow-x: hidden;
|
|
601
980
|
padding: 10px;
|
|
981
|
+
/* Force scrollbar to always be visible */
|
|
982
|
+
scrollbar-width: thin; /* Firefox */
|
|
983
|
+
scrollbar-color: #888 #f1f1f1; /* Firefox - thumb and track colors */
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* Horizontal scroll mode: horizontal scrolling only */
|
|
987
|
+
.facets-content.horizontal-scroll-mode {
|
|
988
|
+
overflow-x: auto;
|
|
989
|
+
overflow-y: hidden;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/* Webkit browsers scrollbar styling - always visible */
|
|
993
|
+
.facets-content::-webkit-scrollbar {
|
|
994
|
+
width: 12px; /* Vertical scrollbar width */
|
|
995
|
+
height: 12px; /* Horizontal scrollbar height */
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.facets-content::-webkit-scrollbar-track {
|
|
999
|
+
background: #f1f1f1;
|
|
1000
|
+
border-radius: 6px;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.facets-content::-webkit-scrollbar-thumb {
|
|
1004
|
+
background: #888;
|
|
1005
|
+
border-radius: 6px;
|
|
1006
|
+
min-height: 30px; /* Ensure thumb is always visible when scrolling is possible */
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.facets-content::-webkit-scrollbar-thumb:hover {
|
|
1010
|
+
background: #555;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* Force corner to match track color */
|
|
1014
|
+
.facets-content::-webkit-scrollbar-corner {
|
|
1015
|
+
background: #f1f1f1;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.facets-horizontal-container {
|
|
1019
|
+
display: inline-block;
|
|
1020
|
+
min-width: 100%;
|
|
1021
|
+
/* Allow natural width expansion based on content */
|
|
1022
|
+
width: fit-content;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.scroll-nav-container {
|
|
1026
|
+
display: flex;
|
|
1027
|
+
align-items: center;
|
|
1028
|
+
flex: 1 1 auto;
|
|
1029
|
+
min-height: 0;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.scroll-nav-container .facets-content {
|
|
1033
|
+
flex: 1 1 auto;
|
|
1034
|
+
min-width: 0;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.scroll-arrow {
|
|
1038
|
+
background: none;
|
|
1039
|
+
border: none;
|
|
1040
|
+
cursor: pointer;
|
|
1041
|
+
padding: 5px;
|
|
1042
|
+
flex-shrink: 0;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.scroll-arrow svg {
|
|
1046
|
+
height: 14px;
|
|
1047
|
+
fill: #2c2c2c;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.scroll-arrow:disabled {
|
|
1051
|
+
opacity: 0.3;
|
|
1052
|
+
cursor: default;
|
|
602
1053
|
}
|
|
603
1054
|
.facets-loader {
|
|
604
1055
|
--icon-width: 70px;
|
|
@@ -614,6 +1065,7 @@ export class MoreFacetsContent extends LitElement {
|
|
|
614
1065
|
width: auto;
|
|
615
1066
|
border-radius: 4px;
|
|
616
1067
|
cursor: pointer;
|
|
1068
|
+
font-family: inherit;
|
|
617
1069
|
}
|
|
618
1070
|
.btn-cancel {
|
|
619
1071
|
background-color: #2c2c2c;
|
|
@@ -623,19 +1075,37 @@ export class MoreFacetsContent extends LitElement {
|
|
|
623
1075
|
background-color: ${modalSubmitButton};
|
|
624
1076
|
color: white;
|
|
625
1077
|
}
|
|
1078
|
+
more-facets-pagination {
|
|
1079
|
+
flex-shrink: 0;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
626
1082
|
.footer {
|
|
627
1083
|
text-align: center;
|
|
628
1084
|
margin-top: 10px;
|
|
1085
|
+
flex-shrink: 0;
|
|
629
1086
|
}
|
|
630
1087
|
|
|
631
1088
|
@media (max-width: 560px) {
|
|
632
|
-
section#more-facets
|
|
633
|
-
|
|
634
|
-
--facetsColumnCount: 1;
|
|
1089
|
+
section#more-facets.horizontal-scroll-mode,
|
|
1090
|
+
section#more-facets.pagination-mode {
|
|
1091
|
+
--facetsColumnCount: 1; /* Single column on mobile */
|
|
1092
|
+
--facetsMaxHeight: none; /* Remove fixed height for vertical scrolling */
|
|
635
1093
|
}
|
|
636
|
-
|
|
1094
|
+
/* On mobile, always use vertical scrolling regardless of mode */
|
|
1095
|
+
.facets-content,
|
|
1096
|
+
.facets-content.horizontal-scroll-mode {
|
|
637
1097
|
overflow-y: auto;
|
|
638
|
-
|
|
1098
|
+
overflow-x: hidden;
|
|
1099
|
+
}
|
|
1100
|
+
.scroll-nav-container {
|
|
1101
|
+
display: contents; /* Remove wrapper from layout so section flex-column works */
|
|
1102
|
+
}
|
|
1103
|
+
.scroll-arrow {
|
|
1104
|
+
display: none;
|
|
1105
|
+
}
|
|
1106
|
+
.filter-input {
|
|
1107
|
+
width: 120px;
|
|
1108
|
+
--input-font-size: 1.2rem;
|
|
639
1109
|
}
|
|
640
1110
|
}
|
|
641
1111
|
`,
|