Skip to content

Commit 304f541

Browse files
authored
WEBDEV-7003 Smart facet bar (#405)
* Checkpoint * Fix type imports * Dedupe smart facets, improve models/organization, and add language heuristic * Ensure facet pane toggle only effective when smart facets shown
1 parent 9b4e36e commit 304f541

File tree

13 files changed

+1026
-2
lines changed

13 files changed

+1026
-2
lines changed

src/app-root.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,14 @@ export class AppRoot extends LitElement {
327327
/>
328328
<label for="enable-management">Enable manage mode</label>
329329
</div>
330+
<div class="checkbox-control">
331+
<input
332+
type="checkbox"
333+
id="enable-smart-facet-bar"
334+
@click=${this.smartFacetBarCheckboxChanged}
335+
/>
336+
<label for="enable-smart-facet-bar">Enable smart facet bar</label>
337+
</div>
330338
</fieldset>
331339
332340
<fieldset class="cb-visual-appearance">
@@ -696,6 +704,14 @@ export class AppRoot extends LitElement {
696704
'Select items to remove (customizable texts)';
697705
}
698706

707+
/**
708+
* Handler for when the dev panel's "Enable smart facet bar" checkbox is changed.
709+
*/
710+
private smartFacetBarCheckboxChanged(e: Event) {
711+
const target = e.target as HTMLInputElement;
712+
this.collectionBrowser.showSmartFacetBar = target.checked;
713+
}
714+
699715
/**
700716
* Handler for when the dev panel's "Show facet top slot" checkbox is changed.
701717
*/

src/assets/img/icons/filter.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { html } from 'lit';
2+
3+
export default html`
4+
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
5+
<path
6+
d="m 91.666668,8.3333328 v 0.9708343 l -35.3625,39.2916669 -2.137502,2.375 v 3.195832 32.350001 L 45.833334,82.35 V 54.166666 50.970834 l -2.1375,-2.375 L 8.3333328,9.3041671 V 8.3333328 H 91.666668 M 100,0 H 0 V 12.5 L 37.500001,54.166666 V 87.5 l 25,12.5 V 54.166666 L 100,12.5 Z"
7+
fill="#000"
8+
/>
9+
</svg>
10+
`;

src/collection-browser.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import './sort-filter-bar/sort-filter-bar';
7777
import './manage/manage-bar';
7878
import './collection-facets';
7979
import './circular-activity-indicator';
80+
import './collection-facets/smart-facets/smart-facet-bar';
8081

8182
@customElement('collection-browser')
8283
export class CollectionBrowser
@@ -95,7 +96,7 @@ export class CollectionBrowser
9596
/**
9697
* Which backend should be targeted by searches (e.g., metadata or FTS)
9798
*/
98-
@property({ type: String }) searchType: SearchType = SearchType.METADATA;
99+
@property({ type: Number }) searchType: SearchType = SearchType.METADATA;
99100

100101
/**
101102
* The identifier of the collection that searches should be performed within
@@ -147,6 +148,8 @@ export class CollectionBrowser
147148

148149
@property({ type: Object }) selectedFacets?: SelectedFacets;
149150

151+
@property({ type: Boolean }) showSmartFacetBar = false;
152+
150153
/**
151154
* Whether to show the date picker (above the facets)
152155
*/
@@ -205,6 +208,8 @@ export class CollectionBrowser
205208
*/
206209
@property({ type: String }) facetLoadStrategy: FacetLoadStrategy = 'eager';
207210

211+
@property({ type: Boolean }) facetPaneVisible = false;
212+
208213
@property({ type: Boolean }) clearResultsOnEmptyQuery = false;
209214

210215
@property({ type: String }) collectionPagePath: string = '/details/';
@@ -496,6 +501,20 @@ export class CollectionBrowser
496501

497502
render() {
498503
return html`
504+
${this.showSmartFacetBar
505+
? html` <smart-facet-bar
506+
.query=${this.baseQuery}
507+
.aggregations=${this.dataSource.aggregations}
508+
.selectedFacets=${this.selectedFacets}
509+
.collectionTitles=${this.dataSource.collectionTitles}
510+
.filterToggleActive=${this.facetPaneVisible}
511+
@facetsChanged=${this.facetsChanged}
512+
@filtersToggled=${() => {
513+
this.facetPaneVisible = !this.facetPaneVisible;
514+
}}
515+
></smart-facet-bar>`
516+
: nothing}
517+
499518
<div
500519
id="content-container"
501520
class=${this.mobileView ? 'mobile' : 'desktop'}
@@ -599,7 +618,11 @@ export class CollectionBrowser
599618
*/
600619
private get desktopLeftColumnTemplate(): TemplateResult {
601620
return html`
602-
<div id="left-column" class="column">
621+
<div
622+
id="left-column"
623+
class="column"
624+
?hidden=${this.showSmartFacetBar && !this.facetPaneVisible}
625+
>
603626
${this.facetTopViewSlot}
604627
<div id="facets-header-container">
605628
<h2 id="facets-header" class="sr-only">Filters</h2>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { SmartFacet } from './models';
2+
import { smartFacetEquals } from './smart-facet-equals';
3+
4+
/**
5+
* Removes any duplicated smart facets from the given array.
6+
* Smart facets are equal if they have the same `label` and same
7+
* set of facet refs. Only the first occurrence of a given smart
8+
* facet is kept.
9+
* @param facets The array of smart facets to deduplicate
10+
* @returns A new array containing the deduplicated set of facets
11+
*/
12+
export function dedupe<T extends SmartFacet[] | SmartFacet[][]>(facets: T): T {
13+
if (!Array.isArray(facets[0])) {
14+
const facetsUnnested = facets as SmartFacet[];
15+
16+
let result: SmartFacet[] = [...facetsUnnested];
17+
for (const curFacet of facetsUnnested) {
18+
result = result.filter(
19+
sf => curFacet === sf || !smartFacetEquals(curFacet, sf)
20+
);
21+
}
22+
23+
return result as T;
24+
}
25+
26+
const facetsNested = facets as SmartFacet[][];
27+
28+
const result: SmartFacet[][] = [];
29+
for (const curFacetArray of facetsNested) {
30+
const subresult: SmartFacet[] = [];
31+
for (const curFacet of curFacetArray) {
32+
const existing = result.find(sfa =>
33+
sfa.find(sf => smartFacetEquals(curFacet, sf))
34+
);
35+
if (!existing) subresult.push(curFacet);
36+
}
37+
if (subresult.length > 0) {
38+
result.push(subresult);
39+
}
40+
}
41+
42+
return result as T;
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { SmartQueryHeuristic, SmartFacet } from '../models';
2+
3+
export class BrowserLanguageHeuristic implements SmartQueryHeuristic {
4+
async getRecommendedFacets(): Promise<SmartFacet[]> {
5+
const browserLanguageCode = navigator.language;
6+
const languageName =
7+
BrowserLanguageHeuristic.getLanguageDisplayName(browserLanguageCode);
8+
if (!languageName) return [];
9+
10+
return [
11+
{
12+
facets: [
13+
{
14+
facetType: 'language',
15+
bucketKey: languageName,
16+
},
17+
],
18+
},
19+
];
20+
}
21+
22+
private static getLanguageDisplayName(langCode: string): string | undefined {
23+
// Strip off any script/region/variant codes for greater generality
24+
const languageOnly = langCode.split('-')[0];
25+
return new Intl.DisplayNames(['en'], { type: 'language' }).of(languageOnly);
26+
}
27+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type {
2+
SmartQueryHeuristic,
3+
KeywordFacetMap,
4+
SmartFacet,
5+
} from '../models';
6+
7+
// If the query contains X word but Y facet isn't selected, recommend facet Y
8+
export class QueryKeywordsHeuristic implements SmartQueryHeuristic {
9+
private static readonly KEYWORDS: KeywordFacetMap = {
10+
text: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
11+
book: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
12+
pdf: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
13+
epub: [{ facets: [{ facetType: 'mediatype', bucketKey: 'texts' }] }],
14+
audio: [{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] }],
15+
song: [{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] }],
16+
music: [{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] }],
17+
listen: [{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] }],
18+
podcast: [{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] }],
19+
radio: [{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] }],
20+
stream: [
21+
{ facets: [{ facetType: 'mediatype', bucketKey: 'audio' }] },
22+
{ facets: [{ facetType: 'mediatype', bucketKey: 'movies' }] },
23+
],
24+
video: [{ facets: [{ facetType: 'mediatype', bucketKey: 'movies' }] }],
25+
movie: [{ facets: [{ facetType: 'mediatype', bucketKey: 'movies' }] }],
26+
film: [{ facets: [{ facetType: 'mediatype', bucketKey: 'movies' }] }],
27+
image: [{ facets: [{ facetType: 'mediatype', bucketKey: 'image' }] }],
28+
photo: [{ facets: [{ facetType: 'mediatype', bucketKey: 'image' }] }],
29+
picture: [{ facets: [{ facetType: 'mediatype', bucketKey: 'image' }] }],
30+
software: [{ facets: [{ facetType: 'mediatype', bucketKey: 'software' }] }],
31+
app: [{ facets: [{ facetType: 'mediatype', bucketKey: 'software' }] }],
32+
program: [{ facets: [{ facetType: 'mediatype', bucketKey: 'software' }] }],
33+
game: [{ facets: [{ facetType: 'mediatype', bucketKey: 'software' }] }],
34+
etree: [{ facets: [{ facetType: 'mediatype', bucketKey: 'etree' }] }],
35+
concert: [{ facets: [{ facetType: 'mediatype', bucketKey: 'etree' }] }],
36+
'live music': [
37+
{ facets: [{ facetType: 'mediatype', bucketKey: 'etree' }] },
38+
],
39+
dataset: [{ facets: [{ facetType: 'mediatype', bucketKey: 'data' }] }],
40+
};
41+
42+
async getRecommendedFacets(query: string): Promise<SmartFacet[]> {
43+
const recommendations: SmartFacet[] = [];
44+
45+
for (const [keyword, facets] of Object.entries(
46+
QueryKeywordsHeuristic.KEYWORDS
47+
)) {
48+
if (query.includes(keyword)) {
49+
recommendations.push(...facets);
50+
}
51+
}
52+
53+
return recommendations;
54+
}
55+
}

0 commit comments

Comments
 (0)