diff --git a/resource/css/skosmos.css b/resource/css/skosmos.css index c510225f7..d2e76eda1 100644 --- a/resource/css/skosmos.css +++ b/resource/css/skosmos.css @@ -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); @@ -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); diff --git a/resource/js/vocab-search.js b/resource/js/vocab-search.js index 76c28c203..de876f845 100644 --- a/resource/js/vocab-search.js +++ b/resource/js/vocab-search.js @@ -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: ` -