Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement autocomplete for search #1589

Merged
merged 94 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
65a840d
Mockup version for prototyping
joelit Jan 30, 2024
e7f0f60
Mockup version of autocomplete dropdown list
joelit Jan 31, 2024
e1e189e
Dropdown functionality for the autocomplete list
joelit Jan 31, 2024
7db9310
Fixed JS styling
joelit Jan 31, 2024
93eb5eb
Search functionality for the autocomplete list
joelit Jan 31, 2024
43e2603
Minor fixes
joelit Jan 31, 2024
88a7a06
Merge branch 'main' into issue1514-search-bar-autocomplete
joelit Jan 31, 2024
bcbb281
Mockup version for prototyping
joelit Jan 30, 2024
9372684
Mockup version of autocomplete dropdown list
joelit Jan 31, 2024
0a39115
Dropdown functionality for the autocomplete list
joelit Jan 31, 2024
ab93fbf
Fixed JS styling
joelit Jan 31, 2024
7b36a8c
Search functionality for the autocomplete list
joelit Jan 31, 2024
fe95e0c
Minor fixes
joelit Jan 31, 2024
287bb27
Scrollbar for autocomplete
joelit Feb 1, 2024
fb1f882
Basic structure for cache, translations from SKOSMOS object, tweaked …
joelit Feb 1, 2024
0537559
Added UI messages for Vue to scripts.inc
joelit Feb 1, 2024
6221b8b
Merge commit
joelit Feb 1, 2024
c914321
Added no results message and formatted wildcards for the search term
joelit Feb 1, 2024
a4e5eff
Fixed code styling
joelit Feb 1, 2024
36c5c23
English as default language for language strings, autocomplete result…
joelit Feb 1, 2024
e0c291b
Removed the mockup cache implementation, changed result links
joelit Feb 1, 2024
e8963da
Added cypress tests
joelit Feb 1, 2024
e5d02de
Tweaked cypress tests
joelit Feb 1, 2024
58ace93
Removed whitespace
joelit Feb 1, 2024
b5b6064
Merge branch 'main' into issue1514-search-bar-autocomplete
joelit Feb 1, 2024
1d8d81d
Adjusted autocomplete styling
joelit Feb 1, 2024
423daab
Removed styling, removed double attribute and replaced it with a cust…
joelit Feb 1, 2024
e41a6b0
Merge branch 'issue1514-search-bar-autocomplete' of github.com:NatLib…
joelit Feb 1, 2024
5fd43f0
Added cypress tests
joelit Feb 1, 2024
c9b3ba7
Render only the result related to the lastest search term
joelit Mar 5, 2024
5fbd180
Fix typo
joelit Mar 5, 2024
e945131
Search results are rendered according to preferredLabel, altLabel and…
joelit Mar 6, 2024
950d72c
CSS, labels, result types, and JS styling
joelit Mar 6, 2024
b8a2e97
Closing the dropdown on click outside the element. Making the bold pa…
joelit Mar 6, 2024
392345c
Style tweaks
joelit Mar 6, 2024
97242f1
Removed unnecessary binary expression
joelit Mar 6, 2024
9c576d2
Switched to rarr-arrow
joelit Mar 6, 2024
33f129b
Mockup version for prototyping
joelit Jan 30, 2024
4b87a98
Mockup version of autocomplete dropdown list
joelit Jan 31, 2024
ca32567
Dropdown functionality for the autocomplete list
joelit Jan 31, 2024
026a490
Fixed JS styling
joelit Jan 31, 2024
7b70bad
Search functionality for the autocomplete list
joelit Jan 31, 2024
89c75c2
Minor fixes
joelit Jan 31, 2024
10234d3
Scrollbar for autocomplete
joelit Feb 1, 2024
4758300
Basic structure for cache, translations from SKOSMOS object, tweaked …
joelit Feb 1, 2024
25d718a
Added UI messages for Vue to scripts.inc
joelit Feb 1, 2024
0bdd9ea
Mockup version for prototyping
joelit Jan 30, 2024
700cab7
Added no results message and formatted wildcards for the search term
joelit Feb 1, 2024
b4737ec
Fixed code styling
joelit Feb 1, 2024
019ea5a
English as default language for language strings, autocomplete result…
joelit Feb 1, 2024
677917e
Removed the mockup cache implementation, changed result links
joelit Feb 1, 2024
f15bd2a
Added cypress tests
joelit Feb 1, 2024
c6f7c1a
Tweaked cypress tests
joelit Feb 1, 2024
aa872c1
Removed whitespace
joelit Feb 1, 2024
f3cfd25
Adjusted autocomplete styling
joelit Feb 1, 2024
3ca1a14
Removed styling, removed double attribute and replaced it with a cust…
joelit Feb 1, 2024
7a58872
Added cypress tests
joelit Feb 1, 2024
42155e5
Render only the result related to the lastest search term
joelit Mar 5, 2024
e8e8992
Fix typo
joelit Mar 5, 2024
4627bcb
Search results are rendered according to preferredLabel, altLabel and…
joelit Mar 6, 2024
cace282
CSS, labels, result types, and JS styling
joelit Mar 6, 2024
7885e32
Closing the dropdown on click outside the element. Making the bold pa…
joelit Mar 6, 2024
77f9dce
Style tweaks
joelit Mar 6, 2024
bfa9423
Removed unnecessary binary expression
joelit Mar 6, 2024
0854758
Switched to rarr-arrow
joelit Mar 6, 2024
39ed662
Updated tests. Some of them broke down on their own. Probably the fra…
joelit Mar 7, 2024
d202ee1
Merge commit
joelit Mar 7, 2024
99f254c
Added a test that could not be tested locally due to cypress hickups
joelit Mar 7, 2024
f3d2f85
More cypress tests
joelit Mar 7, 2024
7e8ed8c
Adjusted comments, use result.pageUrl instead of re-using result.uri
joelit Mar 7, 2024
a11e65c
Addded TODO, removed logging
joelit Mar 7, 2024
322f73b
Removed definitions that came accidentally from rebase
joelit Mar 7, 2024
b14ae27
Removed an unnecessary verison number
joelit Mar 7, 2024
8ce7567
Encoded URI components for both enter search and autocomplete (and fi…
joelit Mar 7, 2024
e68ecaa
Fixed URL encoding, objectify URL parameters
joelit Mar 13, 2024
5535518
Implement all url parameters as URLSearchParams objects
joelit Mar 13, 2024
718e6b9
Make the whole autocomplete search result row a clickable link
joelit Mar 13, 2024
7651bbd
Changed test for URL parameters. Added a test for special characters
joelit Mar 13, 2024
70d64bf
Merge branch 'main' into issue1514-search-bar-autocomplete
joelit Mar 13, 2024
e35ca62
SKOSMOS -> window.SKOSMOS
joelit Mar 13, 2024
f2d2021
Merge branch 'issue1514-search-bar-autocomplete' of github.com:NatLib…
joelit Mar 13, 2024
59b4133
Togge the visibility of the autocomplete results list by clicking out…
joelit Mar 13, 2024
285f1b1
Adjusted CSS selector for search bar, new focus border for search ele…
joelit Mar 13, 2024
7813536
Render the HTML tags of a search result in the template
joelit Mar 13, 2024
d0a825f
Fix JS style
joelit Mar 13, 2024
db07ba2
Fix search result text wrap for altLabel hits
joelit Mar 13, 2024
f74d745
Mockup translation of hierarchical concepts in the search result list
joelit Mar 13, 2024
1b656e5
Removed trailing commas from json
joelit Mar 14, 2024
7f9043b
Search bar focus tweak
joelit Mar 14, 2024
b871bb9
Apply suggestions from code review
joelit Mar 14, 2024
0818e81
Use an object property for displaying the autocomplete result list
joelit Mar 14, 2024
1dcddd6
Move event listener to custom vue directive
joelit Mar 14, 2024
f7a8ad5
Added forceUpdate to toggling the result list visibility
joelit Mar 14, 2024
ddf2603
Fix JS styling
joelit Mar 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions resource/css/skosmos.css
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ body {
height: 100%;
}

#headerbar .dropdown-toggle {
#search-wrapper .dropdown-toggle {
background-color: var(--search-bg);
color: var(--headerbar-text-2);
border: 1px solid var(--search-border);
Expand All @@ -241,51 +241,92 @@ body {
padding-right: 0.5rem;
}

#headerbar .dropdown-toggle:hover, #headerbar .dropdown-toggle:active {
#search-wrapper .dropdown-toggle:hover, #search-wrapper .dropdown-toggle:active {
background-color: var(--search-dropdown-toggle-hover); /* What color to use? */
}

#headerbar .dropdown-toggle.show {
#search-wrapper .dropdown-toggle.show {
background-color: var(--search-dropdown-toggle-show); /* What color to use? */
}

#headerbar .dropdown-menu {
#search-wrapper .dropdown-menu {
border: 1px solid var(--search-border);
border-radius: 0;
padding: 0;
width: 114%;
max-height: 20rem;
overflow: auto;
}

#headerbar .dropdown-item:active {
#search-wrapper .dropdown-item:active {
background-color: var(--search-dropdown-item-active);
}

#headerbar .dropdown-item:hover {
#search-wrapper .dropdown-item:hover {
background-color: var(--search-dropdown-item-hover); /* What color to use? */
}

#headerbar .form-check-input {
#search-wrapper .form-check-input {
background-color: var(--search-bg);
border: 1px solid var(--search-border);

border-radius: 0;
}

#headerbar .form-check-input:checked {
#search-wrapper .form-check-input:checked {
background-color: var(--headerbar-text-2);
}

#headerbar input {
#search-wrapper input {
background-color: var(--search-bg);
border: 1px solid var(--search-border);
border-color: var(--search-border);
border-radius: 0;
}

#search-wrapper span.dropdown {
width: 25rem;
z-index: 4;
}

#search-wrapper .form-control:focus, #search-wrapper select:focus {
border-color: var(--search-border) !important;
box-shadow: inset 0 0 5px rgba(196, 205, 217, 1) !important;
}

#search-wrapper .dropdown-menu {
transform: translateY(-1px);
}

#search-wrapper .autocomplete {
position: relative;
}

#search-wrapper .autocomplete-result {
border-top: 1px solid var(--search-border);
margin: auto;
}

#search-wrapper .autocomplete-result * {
text-decoration: none;
color: var(--dark-color);
}

#search-wrapper .autocomplete-result span.result {
color: var(--vocab-link);
}

#search-wrapper .autocomplete-result span.d-inline {
white-space: nowrap;
}

/* Remove built-in x button from Internet Explorer */
#headerbar input[type=search]::-ms-clear { display: none; width : 0; height: 0; }
#headerbar input[type=search]::-ms-reveal { display: none; width : 0; height: 0; }
#search-wrapper input[type=search]::-ms-clear { display: none; width : 0; height: 0; }
#search-wrapper input[type=search]::-ms-reveal { display: none; width : 0; height: 0; }

/* Remove built-in x button from Chrome */
#headerbar input[type="search"]::-webkit-search-decoration,
#headerbar input[type="search"]::-webkit-search-cancel-button,
#headerbar input[type="search"]::-webkit-search-results-button,
#headerbar input[type="search"]::-webkit-search-results-decoration { display: none; }
#search-wrapper input[type="search"]::-webkit-search-decoration,
#search-wrapper input[type="search"]::-webkit-search-cancel-button,
#search-wrapper input[type="search"]::-webkit-search-results-button,
#search-wrapper input[type="search"]::-webkit-search-results-decoration { display: none; }

#search-button {
background-color: var(--search-button-bg);
Expand Down
229 changes: 214 additions & 15 deletions resource/js/vocab-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,233 @@ const vocabSearch = Vue.createApp({
languages: [],
selectedLanguage: null,
searchTerm: null,
autoCompeteResults: [],
languageStrings: null
searchCounter: null,
renderedResultsList: [],
languageStrings: null,
msgs: null,
showDropdown: false
}
},
mounted () {
this.languages = window.SKOSMOS.languageOrder
this.selectedLanguage = window.SKOSMOS.content_lang
this.languageStrings = window.SKOSMOS.language_strings[window.SKOSMOS.lang]
this.searchCounter = 0
this.languageStrings = window.SKOSMOS.language_strings[window.SKOSMOS.lang] ?? window.SKOSMOS.language_strings.en
this.msgs = window.SKOSMOS.msgs[window.SKOSMOS.lang] ?? window.SKOSMOS.msgs.en
this.renderedResultsList = []
},
methods: {
autoComplete () {
const delayMs = 300

// when new autocomplete is fired, empty the previous result
this.renderedResultsList = []

// cancel the timer for upcoming API call
clearTimeout(this._timerId)
this.hideAutoComplete()

// TODO: if the search term is in cache, use the cache

// delay call, but don't execute if the search term is not at least two characters
if (this.searchTerm.length > 1) {
this._timerId = setTimeout(() => { this.search() }, delayMs)
}
},
search () {
const mySearchCounter = this.searchCounter + 1 // make sure we can identify this search later in case of several ongoing searches
this.searchCounter = mySearchCounter

let skosmosSearchUrl = 'rest/v1/' + window.SKOSMOS.vocab + '/search?'
const skosmosSearchUrlParams = new URLSearchParams({ query: this.formatSearchTerm(), lang: window.SKOSMOS.lang })
skosmosSearchUrl += skosmosSearchUrlParams.toString()

fetch(skosmosSearchUrl)
.then(data => data.json())
.then(data => {
if (mySearchCounter === this.searchCounter) {
this.renderedResultsList = data.results // update results (update cache if it is implemented)
this.renderResults() // render after the fetch has finished
}
})
},
formatSearchTerm () {
if (this.searchTerm.includes('*')) { return this.searchTerm }
return this.searchTerm + '*'
},
renderMatchingPart (searchTerm, label) {
if (label) {
const searchTermLowerCase = searchTerm.toLowerCase()
const labelLowerCase = label.toLowerCase()
if (labelLowerCase.includes(searchTermLowerCase)) {
const startIndex = labelLowerCase.indexOf(searchTermLowerCase)
const endIndex = startIndex + searchTermLowerCase.length
return {
before: label.substring(0, startIndex),
match: label.substring(startIndex, endIndex),
after: label.substring(endIndex)
}
}
return label
}
return null
},
translateType (type) {
return window.SKOSMOS.msgs[window.SKOSMOS.lang][type]
},
/*
* renderResults is used when the search string has been indexed in the cache
* it also shows the autocomplete results list
* TODO: Showing labels in other languages, extra concept information and such goes here
*/
renderResults () {
// TODO: get the results list form cache if it is implemented
const renderedSearchTerm = this.searchTerm // save the search term in case it changes while rendering
this.renderedResultsList.forEach(result => {
if ('hiddenLabel' in result) {
result.hitType = 'hidden'
result.hit = this.renderMatchingPart(renderedSearchTerm, result.prefLabel)
} else if ('altLabel' in result) {
result.hitType = 'alt'
result.hit = this.renderMatchingPart(renderedSearchTerm, result.altLabel)
result.hitPref = this.renderMatchingPart(renderedSearchTerm, result.prefLabel)
} else if ('prefLabel' in result) {
result.hitType = 'pref'
result.hit = this.renderMatchingPart(renderedSearchTerm, result.prefLabel)
}
if ('uri' in result) { // create relative Skosmos page URL from the search result URI
result.pageUrl = window.SKOSMOS.vocab + '/' + window.SKOSMOS.lang + '/page?'
const urlParams = new URLSearchParams({ uri: result.uri })
result.pageUrl += urlParams.toString()
}
// render search result renderedTypes
if (result.type.length > 1) { // remove the type for SKOS concepts if the result has more than one type
result.type.splice(result.type.indexOf('skos:Concept'), 1)
}
// use the translateType function to map translations for the type IRIs
result.renderedType = result.type.map(this.translateType).join(', ')
})

if (this.renderedResultsList.length === 0) { // show no results message
this.renderedResultsList.push({
prefLabel: this.msgs['No results'],
lang: window.SKOSMOS.lang
})
}
this.showAutoComplete()
},
hideAutoComplete () {
this.showDropdown = false
this.$forceUpdate()
},
gotoSearchPage () {
if (!this.searchTerm) return

const currentVocab = window.SKOSMOS.vocab + '/' + window.SKOSMOS.lang + '/'
const vocabHref = window.location.href.substring(0, window.location.href.lastIndexOf(window.SKOSMOS.vocab)) + currentVocab
let langParam = '&clang=' + window.SKOSMOS.content_lang
if (this.selectedLanguage === 'all') langParam += '&anylang=on'
const searchUrl = vocabHref + 'search?q=' + this.searchTerm + langParam
const searchUrlParams = new URLSearchParams({ clang: window.SKOSMOS.content_lang, q: this.searchTerm })
if (this.selectedLanguage === 'all') searchUrlParams.set('anylang', 'on')
const searchUrl = vocabHref + 'search?' + searchUrlParams.toString()
window.location.href = searchUrl
},
changeLang () {
window.SKOSMOS.content_lang = this.selectedLanguage
// TODO: Impelemnt partial page load to change content according to the new content language
// TODO: Implement (a normal) page load to change content according to the new content language
},
resetSearchTermAndHideDropdown () {
this.searchTerm = ''
this.renderedResultsList = []
this.hideAutoComplete()
},
/*
* Show the existing autocomplete list if it was hidden by onClickOutside()
*/
showAutoComplete () {
console.log('Show autocomplete')
this.showDropdown = true
this.$forceUpdate()
}
},
template: `
<div class="d-flex mb-2 my-auto ms-auto">
<div class="input-group" id="search-wrapper">
<div class="d-flex my-auto ms-auto">
<div class="d-flex justify-content-end input-group ms-auto" id="search-wrapper">
<select class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown-item" aria-expanded="false"
v-model="selectedLanguage"
@change="changeLang()"
>
<option class="dropdown-item" v-for="(value, key) in languageStrings" :value="key">{{ value }}</option>
</select>
<input type="search" class="form-control" aria-label="Text input with dropdown button" placeholder="Search..."
v-model="searchTerm"
@input="autoComplete()"
@keyup.enter="gotoSearchPage()"
>
<button id="clear-button" class="btn btn-danger" type="clear" v-if="searchTerm" @click="searchTerm = ''">
<span id="headerbar-search" class="dropdown">
<input type="search"
class="form-control"
id="search-field"
aria-expanded="false"
autocomplete="off"
data-bs-toggle=""
aria-label="Text input with dropdown button"
placeholder="Search..."
v-click-outside="hideAutoComplete"
v-model="searchTerm"
@input="autoComplete()"
@keyup.enter="gotoSearchPage()"
@click="showAutoComplete()">
<ul id="search-autocomplete-results" class="dropdown-menu" :class="{ 'show': showDropdown }"
aria-labelledby="search-field">
<li class="autocomplete-result container" v-for="result in renderedResultsList"
:key="result.prefLabel" >
<template v-if="result.pageUrl">
<a :href=result.pageUrl>
<div class="row pb-1">
<div class="col" v-if="result.hitType == 'hidden'">
<span class="result">
<template v-if="result.hit.hasOwnProperty('match')">
{{ result.hit.before }}<b>{{ result.hit.match }}</b>{{ result.hit.after }}
</template>
<template v-else>
{{ result.hit }}
</template>
</span>
</div>
<div class="col" v-else-if="result.hitType == 'alt'">
<span>
<template v-if="result.hit.hasOwnProperty('match')">
{{ result.hit.before }}<b>{{ result.hit.match }}</b>{{ result.hit.after }}
</template>
<template v-else>
{{ result.hit }}
</template>
</span>
<span> &rarr;&nbsp;<span class="result">
<template v-if="result.hitPref.hasOwnProperty('match')">
{{ result.hitPref.before }}<b>{{ result.hitPref.match }}</b>{{ result.hitPref.after }}
</template>
<template v-else>
{{ result.hitPref }}
</template>
</span>
</span>
</div>
<div class="col" v-else-if="result.hitType == 'pref'">
<span class="result">
<template v-if="result.hit.hasOwnProperty('match')">
{{ result.hit.before }}<b>{{ result.hit.match }}</b>{{ result.hit.after }}
</template>
<template v-else>
{{ result.hit }}
</template>
</span>
</div>
<div class="col-auto align-self-end pe-1" v-html="result.renderedType"></div>
</div>
</a>
</template>
<template v-else>
{{ result.prefLabel }}
</template>
</li>
</ul>
</span>
<button id="clear-button" class="btn btn-danger" type="clear" v-if="searchTerm" @click="resetSearchTermAndHideDropdown()">
<i class="fa-solid fa-xmark"></i>
</button>
<button id="search-button" class="btn btn-outline-secondary" @click="gotoSearchPage()">
Expand All @@ -59,4 +243,19 @@ const vocabSearch = Vue.createApp({
`
})

vocabSearch.directive('click-outside', {
beforeMount: (el, binding) => {
el.clickOutsideEvent = event => {
// Ensure the click was outside the element
if (!(el === event.target || el.contains(event.target))) {
binding.value(event) // Call the method provided in the directive's value
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted: el => {
document.removeEventListener('click', el.clickOutsideEvent)
}
})

vocabSearch.mount('#search-vocab')
Loading
Loading