@internetarchive/collection-browser 2.7.2-alpha.1 → 2.7.2-alpha.2
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/.github/workflows/ci.yml +3 -2
- package/dist/src/app-root.d.ts +4 -0
- package/dist/src/app-root.js +15 -0
- package/dist/src/app-root.js.map +1 -1
- package/dist/src/assets/img/icons/filter.d.ts +2 -0
- package/dist/src/assets/img/icons/filter.js +10 -0
- package/dist/src/assets/img/icons/filter.js.map +1 -0
- package/dist/src/collection-browser.d.ts +3 -6
- package/dist/src/collection-browser.js +25 -22
- package/dist/src/collection-browser.js.map +1 -1
- package/dist/src/collection-facets/smart-facets/heuristics/query-keywords-heuristic.d.ts +5 -0
- package/dist/src/collection-facets/smart-facets/heuristics/query-keywords-heuristic.js +45 -0
- package/dist/src/collection-facets/smart-facets/heuristics/query-keywords-heuristic.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/heuristics/wikidata-heuristic.d.ts +5 -0
- package/dist/src/collection-facets/smart-facets/heuristics/wikidata-heuristic.js +135 -0
- package/dist/src/collection-facets/smart-facets/heuristics/wikidata-heuristic.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/models.d.ts +24 -0
- package/dist/src/collection-facets/smart-facets/models.js +2 -0
- package/dist/src/collection-facets/smart-facets/models.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-bar.d.ts +27 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-bar.js +240 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-bar.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-button.d.ts +10 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-button.js +108 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-button.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-dropdown.d.ts +14 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-dropdown.js +128 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-dropdown.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-equals.d.ts +2 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-equals.js +15 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-equals.js.map +1 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-heuristics.d.ts +13 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-heuristics.js +27 -0
- package/dist/src/collection-facets/smart-facets/smart-facet-heuristics.js.map +1 -0
- package/dist/src/data-source/collection-browser-data-source.js +6 -7
- package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
- package/package.json +2 -2
- package/src/app-root.ts +16 -0
- package/src/assets/img/icons/filter.ts +10 -0
- package/src/collection-browser.ts +21 -43
- package/src/collection-facets/smart-facets/heuristics/query-keywords-heuristic.ts +55 -0
- package/src/collection-facets/smart-facets/heuristics/wikidata-heuristic.ts +153 -0
- package/src/collection-facets/smart-facets/models.ts +30 -0
- package/src/collection-facets/smart-facets/smart-facet-bar.ts +281 -0
- package/src/collection-facets/smart-facets/smart-facet-button.ts +114 -0
- package/src/collection-facets/smart-facets/smart-facet-dropdown.ts +129 -0
- package/src/collection-facets/smart-facets/smart-facet-equals.ts +16 -0
- package/src/collection-facets/smart-facets/smart-facet-heuristics.ts +31 -0
- package/src/data-source/collection-browser-data-source.ts +8 -14
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SmartQueryHeuristic,
|
|
3
|
+
KeywordFacetMap,
|
|
4
|
+
SmartFacet,
|
|
5
|
+
} from '../models';
|
|
6
|
+
|
|
7
|
+
// If wikidata describes the top query result as X, recommend facet Y, e.g.:
|
|
8
|
+
// X Y
|
|
9
|
+
// written work mt:texts
|
|
10
|
+
// film mt:movies
|
|
11
|
+
// author mt:texts and creator:<query>
|
|
12
|
+
// filmmaker mt:movies and creator:<query>
|
|
13
|
+
// photographer mt:image and creator:<query>
|
|
14
|
+
// visual artist mt:image and creator:<query>
|
|
15
|
+
// etc.
|
|
16
|
+
export class WikidataHeuristic implements SmartQueryHeuristic {
|
|
17
|
+
private static readonly ENTITIES: KeywordFacetMap = {
|
|
18
|
+
'written work': [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
|
|
19
|
+
literature: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
|
|
20
|
+
book: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
|
|
21
|
+
novel: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
|
|
22
|
+
filmmaker: [
|
|
23
|
+
{
|
|
24
|
+
label: 'Films by __QUERY',
|
|
25
|
+
facets: [
|
|
26
|
+
{ facetType: 'mediatype', bucketKey: 'movies' },
|
|
27
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
author: [
|
|
32
|
+
{
|
|
33
|
+
label: 'Writing by __QUERY',
|
|
34
|
+
facets: [
|
|
35
|
+
{ facetType: 'mediatype', bucketKey: 'texts' },
|
|
36
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
writer: [
|
|
41
|
+
{
|
|
42
|
+
label: 'Writing by __QUERY',
|
|
43
|
+
facets: [
|
|
44
|
+
{ facetType: 'mediatype', bucketKey: 'texts' },
|
|
45
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
poet: [
|
|
50
|
+
{
|
|
51
|
+
label: 'Writing by __QUERY',
|
|
52
|
+
facets: [
|
|
53
|
+
{ facetType: 'mediatype', bucketKey: 'texts' },
|
|
54
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
photographer: [
|
|
59
|
+
{
|
|
60
|
+
label: 'Images by __QUERY',
|
|
61
|
+
facets: [
|
|
62
|
+
{ facetType: 'mediatype', bucketKey: 'image' },
|
|
63
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
painter: [
|
|
68
|
+
{
|
|
69
|
+
label: 'Images by __QUERY',
|
|
70
|
+
facets: [
|
|
71
|
+
{ facetType: 'mediatype', bucketKey: 'image' },
|
|
72
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
singer: [
|
|
77
|
+
{
|
|
78
|
+
label: 'Music by __QUERY',
|
|
79
|
+
facets: [
|
|
80
|
+
{ facetType: 'mediatype', bucketKey: 'audio' },
|
|
81
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
songwriter: [
|
|
86
|
+
{
|
|
87
|
+
label: 'Music by __QUERY',
|
|
88
|
+
facets: [
|
|
89
|
+
{ facetType: 'mediatype', bucketKey: 'audio' },
|
|
90
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
musician: [
|
|
95
|
+
{
|
|
96
|
+
label: 'Music by __QUERY',
|
|
97
|
+
facets: [
|
|
98
|
+
{ facetType: 'mediatype', bucketKey: 'audio' },
|
|
99
|
+
{ facetType: 'creator', bucketKey: '__QUERY' },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
async getRecommendedFacets(query: string): Promise<SmartFacet[]> {
|
|
106
|
+
const recommendations: SmartFacet[] = [];
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const urlQuery = encodeURIComponent(query);
|
|
110
|
+
|
|
111
|
+
const wikidataResponse = await fetch(
|
|
112
|
+
`https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${urlQuery}&format=json&language=en&uselang=en&origin=*&type=item&limit=5`
|
|
113
|
+
);
|
|
114
|
+
const searchResults = await wikidataResponse.json();
|
|
115
|
+
|
|
116
|
+
for (const [keyword, facets] of Object.entries(
|
|
117
|
+
WikidataHeuristic.ENTITIES
|
|
118
|
+
)) {
|
|
119
|
+
const keywordRegex = new RegExp('\\b' + keyword + '\\b');
|
|
120
|
+
if (keywordRegex.test(searchResults.search[0]?.description)) {
|
|
121
|
+
const entityName = searchResults.search[0].label;
|
|
122
|
+
recommendations.push(
|
|
123
|
+
...facets.map(
|
|
124
|
+
sf =>
|
|
125
|
+
({
|
|
126
|
+
label: sf.label?.replace('__QUERY', entityName),
|
|
127
|
+
facets: sf.facets.map(f => {
|
|
128
|
+
const replaced = {
|
|
129
|
+
...f,
|
|
130
|
+
bucketKey: f.bucketKey.replace('__QUERY', query),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (f.displayText) {
|
|
134
|
+
replaced.displayText = replaced.displayText?.replace(
|
|
135
|
+
'__QUERY',
|
|
136
|
+
entityName
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return replaced;
|
|
141
|
+
}),
|
|
142
|
+
} as SmartFacet)
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return recommendations;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FacetEventDetails, FacetOption } from '../../models';
|
|
2
|
+
|
|
3
|
+
export interface FacetRef {
|
|
4
|
+
facetType: FacetOption;
|
|
5
|
+
bucketKey: string;
|
|
6
|
+
displayText?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface LabeledSmartFacet {
|
|
10
|
+
label: string;
|
|
11
|
+
facets: FacetRef[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UnlabeledSmartFacet {
|
|
15
|
+
label?: string;
|
|
16
|
+
facets: [FacetRef];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type SmartFacet = LabeledSmartFacet | UnlabeledSmartFacet;
|
|
20
|
+
|
|
21
|
+
export interface SmartFacetEvent {
|
|
22
|
+
smartFacet: SmartFacet;
|
|
23
|
+
details: FacetEventDetails[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type KeywordFacetMap = Record<string, SmartFacet[]>;
|
|
27
|
+
|
|
28
|
+
export interface SmartQueryHeuristic {
|
|
29
|
+
getRecommendedFacets(query: string): Promise<SmartFacet[]>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/* eslint-disable no-continue */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
css,
|
|
5
|
+
html,
|
|
6
|
+
LitElement,
|
|
7
|
+
TemplateResult,
|
|
8
|
+
CSSResultGroup,
|
|
9
|
+
nothing,
|
|
10
|
+
PropertyValues,
|
|
11
|
+
} from 'lit';
|
|
12
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
13
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
14
|
+
import type { Aggregation, Bucket } from '@internetarchive/search-service';
|
|
15
|
+
import type { CollectionTitles } from '../../data-source/models';
|
|
16
|
+
import type { FacetOption, SelectedFacets } from '../../models';
|
|
17
|
+
import { updateSelectedFacetBucket } from '../../utils/facet-utils';
|
|
18
|
+
import { SmartQueryHeuristicGroup } from './smart-facet-heuristics';
|
|
19
|
+
import type { SmartFacet, SmartFacetEvent } from './models';
|
|
20
|
+
import filterIcon from '../../assets/img/icons/filter';
|
|
21
|
+
|
|
22
|
+
import './smart-facet-button';
|
|
23
|
+
import './smart-facet-dropdown';
|
|
24
|
+
|
|
25
|
+
const fieldPrefixes: Partial<Record<FacetOption, string>> = {
|
|
26
|
+
collection: 'Collection: ',
|
|
27
|
+
creator: 'By: ',
|
|
28
|
+
subject: 'About: ',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function capitalize(str: string) {
|
|
32
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@customElement('smart-facet-bar')
|
|
36
|
+
export class SmartFacetBar extends LitElement {
|
|
37
|
+
@property({ type: String }) query?: string;
|
|
38
|
+
|
|
39
|
+
@property({ type: Object }) aggregations?: Record<string, Aggregation>;
|
|
40
|
+
|
|
41
|
+
@property({ type: Object }) selectedFacets?: SelectedFacets;
|
|
42
|
+
|
|
43
|
+
/** The map from collection identifiers to their titles */
|
|
44
|
+
@property({ type: Object })
|
|
45
|
+
collectionTitles?: CollectionTitles;
|
|
46
|
+
|
|
47
|
+
@property({ type: Boolean }) filterToggleActive = false;
|
|
48
|
+
|
|
49
|
+
@state() private heuristicRecs: SmartFacet[] = [];
|
|
50
|
+
|
|
51
|
+
@state() private smartFacets: SmartFacet[] = [];
|
|
52
|
+
|
|
53
|
+
//
|
|
54
|
+
// COMPONENT LIFECYCLE METHODS
|
|
55
|
+
//
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
return html`
|
|
59
|
+
<div id="smart-facets-container">
|
|
60
|
+
${this.filtersToggleTemplate}
|
|
61
|
+
${repeat(
|
|
62
|
+
this.smartFacets,
|
|
63
|
+
f => `${f.label}|${f.facets[0].facetType}|${f.facets[0].bucketKey}`,
|
|
64
|
+
facet => this.makeSmartFacet([facet])
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected willUpdate(changed: PropertyValues): void {
|
|
71
|
+
if (
|
|
72
|
+
changed.has('query') ||
|
|
73
|
+
(changed.has('aggregations') &&
|
|
74
|
+
(!changed.get('aggregations') ||
|
|
75
|
+
Object.keys(changed.get('aggregations')).length === 0))
|
|
76
|
+
) {
|
|
77
|
+
this.updateSmartFacets();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async updateSmartFacets(): Promise<void> {
|
|
82
|
+
if (this.query) {
|
|
83
|
+
this.heuristicRecs =
|
|
84
|
+
await new SmartQueryHeuristicGroup().getRecommendedFacets(this.query);
|
|
85
|
+
this.smartFacets = this.facetsToDisplay;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//
|
|
90
|
+
// OTHER METHODS
|
|
91
|
+
//
|
|
92
|
+
|
|
93
|
+
private makeSmartFacet(facets: SmartFacet[]) {
|
|
94
|
+
if (facets.length === 0) {
|
|
95
|
+
return nothing;
|
|
96
|
+
}
|
|
97
|
+
return this.smartFacetButton(facets[0]);
|
|
98
|
+
// else {
|
|
99
|
+
// return this.smartFacetDropdown(facets);
|
|
100
|
+
// }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private smartFacetButton(facet: SmartFacet) {
|
|
104
|
+
return html`
|
|
105
|
+
<smart-facet-button
|
|
106
|
+
.facetInfo=${facet}
|
|
107
|
+
@facetClick=${this.facetClicked}
|
|
108
|
+
></smart-facet-button>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// private smartFacetDropdown(facets: SmartFacet[]) {
|
|
113
|
+
// return html`
|
|
114
|
+
// <smart-facet-dropdown
|
|
115
|
+
// .facetType=${facets[0].facets[0].facetType}
|
|
116
|
+
// .buckets=${facets.map(sf => sf.facets.map(f => ({
|
|
117
|
+
// key: f.bucketKey,
|
|
118
|
+
|
|
119
|
+
// })))}
|
|
120
|
+
// .activeBucket=${facet.buckets[0]}
|
|
121
|
+
// @facetClick=${this.facetClicked}
|
|
122
|
+
// ></smart-facet-dropdown>
|
|
123
|
+
// `;
|
|
124
|
+
// }
|
|
125
|
+
|
|
126
|
+
private get filtersToggleTemplate(): TemplateResult {
|
|
127
|
+
return html`
|
|
128
|
+
<button
|
|
129
|
+
id="filters-toggle"
|
|
130
|
+
class=${this.filterToggleActive ? 'active' : ''}
|
|
131
|
+
title="${this.filterToggleActive ? 'Hide' : 'Show'} filters pane"
|
|
132
|
+
@click=${this.filterToggleClicked}
|
|
133
|
+
>
|
|
134
|
+
${filterIcon}
|
|
135
|
+
</button>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private get facetsToDisplay(): SmartFacet[] {
|
|
140
|
+
if (!this.aggregations) return [];
|
|
141
|
+
|
|
142
|
+
const facets: SmartFacet[] = [];
|
|
143
|
+
|
|
144
|
+
if (this.heuristicRecs.length > 0) {
|
|
145
|
+
for (const rec of this.heuristicRecs) {
|
|
146
|
+
facets.push(rec);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const [key, agg] of Object.entries(this.aggregations)) {
|
|
151
|
+
if (agg.buckets.length === 0) continue;
|
|
152
|
+
if (['lending', 'year_histogram'].includes(key)) continue;
|
|
153
|
+
if (typeof agg.buckets[0] === 'number') continue;
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
key === 'mediatype' &&
|
|
157
|
+
this.selectedFacets &&
|
|
158
|
+
Object.values(this.selectedFacets.mediatype).some(
|
|
159
|
+
bucket => bucket.state !== 'none'
|
|
160
|
+
)
|
|
161
|
+
) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const facetType = key as FacetOption;
|
|
166
|
+
const buckets = agg.buckets as Bucket[];
|
|
167
|
+
|
|
168
|
+
const bestBucket = buckets.find(b => {
|
|
169
|
+
const selectedFacetBucket = this.selectedFacets?.[facetType][b.key];
|
|
170
|
+
if (selectedFacetBucket && selectedFacetBucket.state !== 'none')
|
|
171
|
+
return false;
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
if (!bestBucket) continue;
|
|
175
|
+
|
|
176
|
+
facets.push(this.toSmartFacet(facetType, [bestBucket]));
|
|
177
|
+
|
|
178
|
+
if (facetType === 'mediatype') {
|
|
179
|
+
facets.push(this.toSmartFacet(facetType, [buckets[1]]));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// if (facetType === 'collection' || facetType === 'subject') {
|
|
183
|
+
// facets[facets.length - 1].buckets.push(...buckets.slice(1, 4).map(b => this.toSmartFacet(facetType, [b], false).buckets[0]));
|
|
184
|
+
// }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return facets;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private toSmartFacet(
|
|
191
|
+
facetType: FacetOption,
|
|
192
|
+
buckets: Bucket[],
|
|
193
|
+
prefix = true
|
|
194
|
+
): SmartFacet {
|
|
195
|
+
return {
|
|
196
|
+
facets: buckets.map(bucket => {
|
|
197
|
+
let displayText = capitalize(bucket.key.toString());
|
|
198
|
+
if (facetType === 'collection') {
|
|
199
|
+
const title = this.collectionTitles?.get(bucket.key.toString());
|
|
200
|
+
if (title) displayText = title;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (prefix && fieldPrefixes[facetType]) {
|
|
204
|
+
displayText = fieldPrefixes[facetType] + displayText;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
facetType,
|
|
209
|
+
bucketKey: bucket.key.toString(),
|
|
210
|
+
displayText,
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
} as SmartFacet;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private facetClicked(e: CustomEvent<SmartFacetEvent>): void {
|
|
217
|
+
this.smartFacets = [
|
|
218
|
+
e.detail.smartFacet,
|
|
219
|
+
...this.smartFacets.filter(f => f !== e.detail.smartFacet),
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
for (const facet of e.detail.details) {
|
|
223
|
+
this.selectedFacets = updateSelectedFacetBucket(
|
|
224
|
+
this.selectedFacets,
|
|
225
|
+
facet.facetType,
|
|
226
|
+
facet.bucket,
|
|
227
|
+
true
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const event = new CustomEvent<SelectedFacets>('facetsChanged', {
|
|
232
|
+
detail: this.selectedFacets,
|
|
233
|
+
});
|
|
234
|
+
this.dispatchEvent(event);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private filterToggleClicked(): void {
|
|
238
|
+
this.dispatchEvent(new CustomEvent('filtersToggled'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//
|
|
242
|
+
// STYLES
|
|
243
|
+
//
|
|
244
|
+
|
|
245
|
+
static get styles(): CSSResultGroup {
|
|
246
|
+
return css`
|
|
247
|
+
#smart-facets-container {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
flex-wrap: wrap;
|
|
251
|
+
gap: 5px;
|
|
252
|
+
padding: 10px 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#filters-toggle {
|
|
256
|
+
margin: 0;
|
|
257
|
+
border: 0;
|
|
258
|
+
padding: 5px 10px;
|
|
259
|
+
border-radius: 15px;
|
|
260
|
+
background: #194880;
|
|
261
|
+
color: white;
|
|
262
|
+
font-size: 1.6rem;
|
|
263
|
+
font-family: inherit;
|
|
264
|
+
text-decoration: none;
|
|
265
|
+
box-shadow: 1px 1px rgba(0, 0, 0, 0.4);
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#filters-toggle.active {
|
|
270
|
+
background: #09294d;
|
|
271
|
+
box-shadow: -1px -1px rgba(0, 0, 0, 0.1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#filters-toggle > svg {
|
|
275
|
+
width: 15px;
|
|
276
|
+
filter: invert(1);
|
|
277
|
+
vertical-align: text-bottom;
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { css, html, LitElement, CSSResultGroup, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
import { mediatypeConfig } from '../../mediatype/mediatype-config';
|
|
4
|
+
import type { SmartFacet } from './models';
|
|
5
|
+
|
|
6
|
+
@customElement('smart-facet-button')
|
|
7
|
+
export class SmartFacetButton extends LitElement {
|
|
8
|
+
@property({ type: Object }) facetInfo?: SmartFacet;
|
|
9
|
+
|
|
10
|
+
@property({ type: Boolean }) selected = false;
|
|
11
|
+
|
|
12
|
+
//
|
|
13
|
+
// COMPONENT LIFECYCLE METHODS
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
render() {
|
|
17
|
+
if (!this.facetInfo) return nothing;
|
|
18
|
+
|
|
19
|
+
const isSingleFacet = this.facetInfo.facets.length === 1;
|
|
20
|
+
const firstFacet = this.facetInfo.facets[0];
|
|
21
|
+
|
|
22
|
+
const displayText =
|
|
23
|
+
this.facetInfo.label ?? firstFacet.displayText ?? firstFacet.bucketKey;
|
|
24
|
+
if (!displayText) return nothing;
|
|
25
|
+
|
|
26
|
+
const icon =
|
|
27
|
+
isSingleFacet && firstFacet.facetType === 'mediatype'
|
|
28
|
+
? mediatypeConfig[firstFacet.bucketKey].icon
|
|
29
|
+
: nothing;
|
|
30
|
+
|
|
31
|
+
return html`
|
|
32
|
+
<a
|
|
33
|
+
class="smart-facet-button ${this.selected ? 'selected' : ''}"
|
|
34
|
+
href=${this.href}
|
|
35
|
+
@click=${this.facetClicked}
|
|
36
|
+
>
|
|
37
|
+
${icon} ${displayText}
|
|
38
|
+
${this.selected
|
|
39
|
+
? html`<span style="margin-left: 5px;">×</span>`
|
|
40
|
+
: nothing}
|
|
41
|
+
</a>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//
|
|
46
|
+
// OTHER METHODS
|
|
47
|
+
//
|
|
48
|
+
|
|
49
|
+
private get href(): string {
|
|
50
|
+
const url = new URL(window.location.href);
|
|
51
|
+
if (this.facetInfo) {
|
|
52
|
+
for (const facet of this.facetInfo.facets) {
|
|
53
|
+
url.searchParams.append(
|
|
54
|
+
'and[]',
|
|
55
|
+
encodeURIComponent(`${facet.facetType}:"${facet.bucketKey}"`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return url.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private facetClicked(e: Event): void {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
if (!this.facetInfo) return;
|
|
65
|
+
|
|
66
|
+
this.selected = !this.selected;
|
|
67
|
+
|
|
68
|
+
this.dispatchEvent(
|
|
69
|
+
new CustomEvent('facetClick', {
|
|
70
|
+
detail: {
|
|
71
|
+
smartFacet: this.facetInfo,
|
|
72
|
+
details: this.facetInfo.facets.map(f => ({
|
|
73
|
+
facetType: f.facetType,
|
|
74
|
+
bucket: {
|
|
75
|
+
key: f.bucketKey,
|
|
76
|
+
count: 0,
|
|
77
|
+
state: this.selected ? 'selected' : 'none',
|
|
78
|
+
},
|
|
79
|
+
negative: false,
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//
|
|
87
|
+
// STYLES
|
|
88
|
+
//
|
|
89
|
+
|
|
90
|
+
static get styles(): CSSResultGroup {
|
|
91
|
+
return css`
|
|
92
|
+
.smart-facet-button {
|
|
93
|
+
padding: 5px 10px;
|
|
94
|
+
border-radius: 15px;
|
|
95
|
+
background: #194880;
|
|
96
|
+
color: white;
|
|
97
|
+
font-size: 1.6rem;
|
|
98
|
+
font-family: inherit;
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
box-shadow: 1px 1px rgba(0, 0, 0, 0.4);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.smart-facet-button.selected {
|
|
104
|
+
background: #4c76aa;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.smart-facet-button > svg {
|
|
108
|
+
width: 15px;
|
|
109
|
+
filter: invert(1);
|
|
110
|
+
vertical-align: text-top;
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { css, html, LitElement, CSSResultGroup, nothing } from 'lit';
|
|
2
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
3
|
+
import type { IaDropdown, optionInterface } from '@internetarchive/ia-dropdown';
|
|
4
|
+
import type { FacetRef, SmartFacet, SmartFacetEvent } from './models';
|
|
5
|
+
|
|
6
|
+
@customElement('smart-facet-dropdown')
|
|
7
|
+
export class SmartFacetDropdown extends LitElement {
|
|
8
|
+
@property({ type: Array }) facetInfo?: SmartFacet[];
|
|
9
|
+
|
|
10
|
+
@property({ type: String }) labelPrefix?: string;
|
|
11
|
+
|
|
12
|
+
@property({ type: Object }) activeFacetRef?: FacetRef;
|
|
13
|
+
|
|
14
|
+
@query('ia-dropdown') dropdown?: IaDropdown;
|
|
15
|
+
|
|
16
|
+
//
|
|
17
|
+
// COMPONENT LIFECYCLE METHODS
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
render() {
|
|
21
|
+
if (!this.facetInfo || !this.activeFacetRef) return nothing;
|
|
22
|
+
if (this.facetInfo.length === 0) return nothing;
|
|
23
|
+
|
|
24
|
+
const displayText = this.activeFacetRef.displayText ?? this.activeFacetRef.bucketKey;
|
|
25
|
+
if (!displayText) return nothing;
|
|
26
|
+
|
|
27
|
+
return html`
|
|
28
|
+
<div class="dropdown-container">
|
|
29
|
+
<ia-dropdown
|
|
30
|
+
class="dropdown"
|
|
31
|
+
displayCaret
|
|
32
|
+
openViaButton
|
|
33
|
+
closeOnSelect
|
|
34
|
+
includeSelectedOption
|
|
35
|
+
.options=${this.dropdownOptions}
|
|
36
|
+
.selectedOption=${this.activeDropdownOption}
|
|
37
|
+
@optionSelected=${this.optionSelected}
|
|
38
|
+
>
|
|
39
|
+
<span class="dropdown-label" slot="dropdown-label"
|
|
40
|
+
>${this.labelPrefix ?? nothing} ${displayText}</span
|
|
41
|
+
>
|
|
42
|
+
</ia-dropdown>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//
|
|
48
|
+
// OTHER METHODS
|
|
49
|
+
//
|
|
50
|
+
|
|
51
|
+
private get dropdownOptions(): optionInterface[] {
|
|
52
|
+
return (
|
|
53
|
+
this.facetInfo?.map(smartFacet => {
|
|
54
|
+
const firstFacet = smartFacet.facets[0];
|
|
55
|
+
return {
|
|
56
|
+
id: firstFacet.bucketKey,
|
|
57
|
+
label: smartFacet.label ?? firstFacet.displayText ?? firstFacet.bucketKey,
|
|
58
|
+
};
|
|
59
|
+
}) ?? []
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private get activeDropdownOption(): optionInterface | undefined {
|
|
64
|
+
if (!this.activeFacetRef) return undefined;
|
|
65
|
+
return this.dropdownOptions.find(opt => opt.id === this.activeFacetRef?.bucketKey);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private optionSelected(e: CustomEvent<{ option: optionInterface }>): void {
|
|
69
|
+
if (!this.facetInfo || !this.activeFacetRef) return;
|
|
70
|
+
|
|
71
|
+
let selectedSmartFacet;
|
|
72
|
+
for (const smartFacet of this.facetInfo) {
|
|
73
|
+
const selectedRef = smartFacet.facets.find(b => b.bucketKey === e.detail.option.id);
|
|
74
|
+
if (selectedRef) {
|
|
75
|
+
this.activeFacetRef = selectedRef;
|
|
76
|
+
selectedSmartFacet = smartFacet;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!selectedSmartFacet) return;
|
|
81
|
+
|
|
82
|
+
this.dispatchEvent(
|
|
83
|
+
new CustomEvent<SmartFacetEvent>('facetClick', {
|
|
84
|
+
detail: {
|
|
85
|
+
smartFacet: selectedSmartFacet,
|
|
86
|
+
details: [{
|
|
87
|
+
facetType: this.activeFacetRef.facetType,
|
|
88
|
+
bucket: {
|
|
89
|
+
key: this.activeFacetRef.bucketKey,
|
|
90
|
+
count: 0,
|
|
91
|
+
state: 'selected',
|
|
92
|
+
},
|
|
93
|
+
negative: false,
|
|
94
|
+
}],
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//
|
|
101
|
+
// STYLES
|
|
102
|
+
//
|
|
103
|
+
|
|
104
|
+
static get styles(): CSSResultGroup {
|
|
105
|
+
return css`
|
|
106
|
+
.dropdown-container {
|
|
107
|
+
padding: 5px 8px;
|
|
108
|
+
border-radius: 5px;
|
|
109
|
+
background: #194880;
|
|
110
|
+
color: white;
|
|
111
|
+
font-size: 1.6rem;
|
|
112
|
+
font-family: inherit;
|
|
113
|
+
box-shadow: 1px 1px rgba(0, 0, 0, 0.4);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.dropdown-label {
|
|
117
|
+
font-size: 1.6rem;
|
|
118
|
+
font-family: inherit;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.dropdown {
|
|
122
|
+
--dropdownBorderWidth: 5px;
|
|
123
|
+
--dropdownBorderColor: transparent;
|
|
124
|
+
--caretWidth: 14px;
|
|
125
|
+
--caretHeight: 14px;
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
}
|