<template> <div id="app"> <header> <h1><a href="/">Ordbøkene <span style="color: #bbbbbb;">(BETA)</span></a></h1> <p class="about-link"><a href="#">OM ORDBØKENE</a></p> <p class="sub-title"><a href="/">Bokmålsordboka | Nynorskordboka – rett norsk</a></p> </header> <main> <div class="search_container"> <autocomplete :debounceTime="100" :auto-select="true" :search="search" @submit="select_result" placeholder="søk..." ref="search"> <template #result="{result, props}"> <li class="suggestion" v-bind="props">{{result.label}}</li> </template> </autocomplete> <div class="lang_select_container"> <select class="lang_select" name="lang" v-model="lang"> <option value="bob,nob">Begge målformer </option> <option value="bob">Bokmål</option> <option value="nob">Nynorsk</option> </select> </div> </div> <img id="spinner" :class="waiting ? 'show' : 'hide'" src="./assets/spinner.gif" alt="Venter på innhold" /> <SearchResults :hits="search_results" :lang="lang" @article-click="article_link_click" v-show="! waiting" /> <div id="single_article_container"> <Article :key="article_key" :article="article" @article-click="article_link_click" /> </div> <div class="welcome" v-show="! (article.error || article.lemmas.length || search_results.length || waiting)"> </div> </main> <footer> <div> <img id="uiblogo" src="./assets/uib-logo.svg" alt=""> <p>Ordbøkene eies av Universitetet i Bergen sammen med Språkrådet, under ledelse frå Språksamlingane</p> <p><strong>REDAKTØRANSVAR: HF-FAKULTETET VED UiB</strong></p> </div> <div> <img id="srlogo" src="./assets/Sprakradet_logo_neg.png" alt=""> <p> Språkrådet er statens fagorgan i språkspørsmål og følger opp den norske språkpolitikken på oppdrag fra Kulturdepartementet. Språkrådet forvalter rettskrivningen i nynorsk og bokmål.</p> </div> </footer> </div> </template> <script> import axios from "axios" import entities from './utils/entities.js' import Article from './components/Article.vue' import Preview from './components/Preview.vue' import SearchResults from './components/SearchResults.vue' import Autocomplete from '@trevoreyre/autocomplete-vue' import '@trevoreyre/autocomplete-vue/dist/style.css' var api_endpoint = 'https://beta.ordbok.uib.no/api/dict' axios.interceptors.request.use(function (config) { config.headers["x-api-key"] = "ZkYiyRVXxH86ijsvhx3cH4SY5Iik2ijI3BKVJGMm" return config; }, function (error) { return Promise.reject(error); }); window.onpopstate = function (event) { if (event.state) { // eslint-disable-next-line app.__vue__._data.article = event.state.article // eslint-disable-next-line app.__vue__._data.search_results = event.state.search_results // eslint-disable-next-line app.__vue__._data.lang = event.state.lang } } function navigate_to_article(self, source) { axios.get(api_endpoint + '/' + self.$route.params.lang + '/article/' + self.$route.params.id) .then(function(response){ self.article = response.data self.search_results = [] }) .catch(function(error){ if (error.response && error.response.status == 404) { self.article = { lemmas: [], error: "Vi har ingen artikkel med id " + article_id } } else { self.article = { lemmas: [], error: "Noe gikk galt..." } console.log(error) } }) .then(function(response){ self.waiting_for_articles = false history.replaceState({article: self.article, search_results: [], lang: self.lang}, '') if (source) { self.$plausible.trackEvent('internal link incoming', {props: {origin: source}}) } }) } function navigate_to_search(self, query) { axios.get(self.api_pref + 'search?q=' + query) .then(function(response){ self.search_results = response.data self.waiting_for_articles = false history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang}, '') }) } function navigate_to_word(self, word) { axios.get(self.api_pref + 'suggest?q=' + word) .then(function(response){ self.search_results = response.data.filter(result => result.match.length == word.length) self.waiting_for_articles = false history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang}, '') }) } export default { name: 'app', data: function() { return { article_key: 0, search_results: [], lang: 'bob,nob', waiting_for_articles: true, waiting_for_metadata: true, article: {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} } }, computed: { waiting: function() { return this.waiting_for_articles || this.waiting_for_metadata }, api_pref: function() { return api_endpoint + '/' + this.lang + '/article/' }, search: function() { let self = this return function(q) { return new Promise(resolve => { return axios.get(self.api_pref + 'suggest?q=' + q).then( function(response) { let hits = [] response.data.forEach((item, i) => { if (! hits[0] || hits[0].word != item.match) { hits.splice(0, 0, {q: q, lang_set: new Set(), word: item.match, articles: []}) } hits[0].lang_set.add(item.dictionary == 'bob' ? 'bm' : 'nn') hits[0].articles.push(item) }); hits.forEach(function (hit) { if (hit.lang_set) { hit.label = `${hit.word} (${Array.from(hit.lang_set).sort().join(', ')})` } }); hits.reverse() hits = hits.slice(0, 8) if (q) { hits.push({q: q, label: q + ' (fritekstsøk)'}) } resolve(hits) }) }) } } }, components: { Article, Autocomplete, SearchResults, Preview }, methods: { select_result: function(event) { this.$refs.search.value = '' document.activeElement.blur() if(event.articles){ this.$router.push('/' + this.lang + '/w/' + event.word) this.search_results = event.articles this.article = {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} history.replaceState({article: this.article, search_results: this.search_results, lang: this.lang}, '') this.$plausible.trackEvent('dropdown selection', {props: {query: event.q, match: event.match}}) }else{ this.waiting_for_articles = true this.article = {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} this.$router.push(`/${this.lang}/search/${event.q}`) navigate_to_search(this, event.q) this.$plausible.trackEvent('dropdown selection', {props: {query: event.q, match: '<fritekstsøk>'}}) } }, article_link_click: function(item) { if (this.article.article_id == item.article_id){ this.article_key++ history.replaceState({article: this.article, search_results: this.search_results, lang: this.lang}, '') }else{ this.article = {lemmas: [], body:{pronunciation: [], definitions: [], etymology: []}} this.waiting_for_articles = true navigate_to_article(this, item.source) } } }, mounted: function(){ let self = this this.lang = 'bob,nob' Promise.all([ axios.get(api_endpoint + '/bob').then(function(response){ let concepts = response.data.concepts entities.bob = concepts }), axios.get(api_endpoint + '/nob').then(function(response){ let concepts = response.data.concepts entities.nob = concepts }) ]).then(function(_) { self.waiting_for_metadata = false if(self.$route.name == 'word') { self.lang = self.$route.params.lang navigate_to_word(self, self.$route.params.word) } else if(self.$route.name == 'lookup'){ navigate_to_article(self, self.$route.params.id) } else if (self.$route.name == 'search') { self.lang = self.$route.params.lang navigate_to_search(self, self.$route.params.query) } else { self.waiting_for_articles = false history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang}, '') } }) }, watch: { $route() { this.$plausible.trackEvent('language', {props: {code: this.$route.params.lang}}) } } } </script> <style> @import url('https://fonts.googleapis.com/css2?family=Inria+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inria+Serif:ital,wght@0,400;0,700;1,400;1,700&family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap'); #app { font-family: 'Noto Sans', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; background-color: #FDF4F5; display: flex; flex-direction: column; height: 100%; } html, body { height: 100% } body { margin: 0px; } h1 { font-family: Inria Serif; font-size: 36px; color: #560027; } header > h1 { color: #BC477B; font-size: 40px; margin: 0px; } p.about-link { text-align: right; margin: 0px; float: right; } header a { color: inherit; text-decoration: none; } p.about-link > a{ text-decoration: none; border-bottom: solid #BC477B 4px; font-size: 12px; color: #FDF4F5; } main { background-image: url('./assets/books.jpg'); background-repeat: no-repeat; background-attachment: fixed; padding-bottom: 20px; flex: 1 0 auto; } header, #search_results, #spinner, #single_article_container, footer, div.welcome, div.search_container { padding-left: calc((100vw - 1000px) / 2); padding-right: calc((100vw - 1000px) / 2); } header { padding-top: 20px; padding-bottom: 20px; background-color: #560027; } div.welcome { font-size: 20px; } .top { color: #560027; font-weight: bold; font-size: smaller; border-bottom: solid; border-color: #BC477B; } .mission-statement { font-size: 16px; margin: 0px; color: #FDF4F5; } .sub-title { font-size: 20px; margin: 0px; color: #FDF4F5; } .show { display: block; } .hide { display: none; } .autocomplete { width: 25em; border-bottom: solid #BC477B; border-radius: 0px; } .autocomplete-input { border-radius: 0px; background-color: #FFFFFF; } .autocomplete-result-list { max-height: 500px; } .lang-select-intro { font-size: smaller; font-weight: bold; } footer { font-size: smaller; display: flex; flex-direction: row; background-color: #570B27; color: #ffffff; padding-bottom: 20px; } .search_container { display: flex; flex-direction: row; padding-top: 50px; } li.suggestion { font-weight: bold; padding-left: 20px; padding-top: 5px; padding-bottom: 5px; border: 0px; background-image: none; } footer p { padding-left: 20px; padding-right: 20px; margin: 0px; flex: 50%; } #srlogo { height: 20px; padding-top: 50px; padding-left: 20px; } #uiblogo { height: 60px; padding: 10px; padding-left: 20px; } select.lang_select { appearance: none; background-color: #EEEEEE; border: none; padding: 0 1em 0 0; width: 100%; height: 100%; font-family: inherit; font-size: inherit; font-weight: bold; cursor: inherit; line-height: inherit; background-image: url('./assets/down_arrow.png'); background-position: 98% center; background-repeat: no-repeat; background-size: 10%; border-bottom: solid #BC477B; } </style>