<template> <main> <div class="search_container" v-if="!article"> <Autocomplete v-on:submit="select_result" :api="get_search_endpoint"> </Autocomplete> <SearchToolbar v-if="$store.state.showSearchToolbar" @updatePos="update_pos" @updateScope="update_scope"/> </div> <div id="above_results"> <div id="suggestions" v-if="!article && inflection_suggestions && inflection_suggestions.length">Se også <em>{{$route.query.q}}</em> som bøyd form av <span v-for="(item,index) in inflection_suggestions" :key="index"><router-link :to="generate_path(item[0])" @click.native="select_suggestion(item[0])">{{item[0]}}</router-link>{{index == inflection_suggestions.length? '.' : ', '}}</span> </div> <div class="return_to_results" v-if="total_results && article"> <router-link v-if="previous" :to="previous" @click.native="return_to_results()"> <v-icon left class="nav_arrow">chevron_left</v-icon>{{$t("back_to_results")}} </router-link> <a v-if="!this.previous" href="/"> <v-icon left class="nav_arrow">chevron_left</v-icon>Søk etter andre ord </a> </div> </div> <SearchResults :results_bm="search_results.bm || []" :results_nn="search_results.nn || []" :lang="lang" @article-click="article_link_click" @details-click="details_click" @update-page="update_page" v-if="total_results() && ! article" /> <div id="spinner" v-if="waiting"> <v-progress-circular indeterminate color="secondary" size="120"></v-progress-circular> </div> <div id="single_article_container" v-if="article"> <Article :key="article_key" :article="article" @article-click="article_link_click" /> </div> <div class="welcome" v-if="! (article || error || total_results() || waiting)"> <div class="monthly" :class="$vuetify.breakpoint.name"> <div> <Article :article="monthly_bm" @article-click="article_link_click" /> </div> <div> <Article :article="monthly_nn" @article-click="article_link_click" /> </div> </div> </div> <div class="error" v-if="error"> <h1>Ingen treff</h1> <p>{{error}}</p> </div> </main> </template> <script> import axios from "axios" import entities from '../utils/entities.js' import Article from './Article.vue' import SearchResults from './SearchResults.vue' import Autocomplete from './Autocomplete.vue' import SearchToolbar from './SearchToolbar.vue' import { setup } from 'axios-cache-adapter' const SEARCH_ENDPOINT = process.env.VUE_APP_SEARCH_ENDPOINT const ARTICLE_ENDPOINT= process.env.VUE_APP_ARTICLE_ENDPOINT const dicts = {'nn': 'Nynorskordboka', 'bm': 'Bokmålsordboka', 'bm,nn': 'ordbøkene'} const api = setup({ baseURL: SEARCH_ENDPOINT, cache: { maxAge: 15 * 60 * 1000, exclude: { query: false, paths: ["articles"] // Disable caching for articles } } }) function QueryException(params) { this.name = "QueryException" this.params = params } function navigate_to_article(self, source) { self.article = null self.waiting_for_articles = true const lang = self.$route.params.lang axios.get(ARTICLE_ENDPOINT + lang + '/article/' + self.$route.params.id + ".json") .then(function(response){ self.article = Object.assign(response.data, {'dictionary': lang, results: self.search_results}) self.error = null }) .catch(function(error){ console.log(error) if (error.response && error.response.status == 404) { self.error = self.$t('error.no_article', {id: self.$route.params.id}) } else { self.error = self.$t('error.generic') } }) .then(function(response){ self.waiting_for_articles = false history.replaceState({article: self.article, search_results: {}, lang: self.lang, error: self.error, pos_selected: self.pos_selected, scope: self.scope, article_info: self.article_info, page: self.page, perPage: self.perPage}, '') if (source) { self.$plausible.trackEvent('internal link incoming', {props: {origin: source}}) } }) } function load_articles(self, query, offset, n, dict) { let article_IDs = self.article_info.articles[dict] if (article_IDs) { if (offset > article_IDs.length) { n = 0 } else if (offset + n > article_IDs.length) { n = article_IDs.length % n } if (n > 0 && (self.lang == dict || self.lang == "bm,nn")) { article_IDs = article_IDs.slice(offset, offset + n) return Promise.all(article_IDs.map((article_id) => { return axios.get(`${ARTICLE_ENDPOINT}${dict}/article/${article_id}.json`) })) .then((response) => { let results = response.map((element, index) => { return Object.assign(element.data, { dictionary: dict }) }) // TODO: Error handling must be moved self.article = null self.search_results[dict] = results }) .catch(error => { self.waiting_for_articles = false self.error_message(error) }) } else { self.search_results[dict] = [] } } return Promise.resolve() } function navigate_to_query(self, word) { self.error = null self.waiting_for_articles = true let query = self.event ? self.event : {match: word} self.query = query // Get article IDs let params = {w: query.match, dict: self.lang, scope: self.scope, meta: 'y'} let offset = 0 if (self.page) { offset = self.perPage * (self.page -1) } if (self.pos_selected) params.wc = self.pos_selected api.get('articles?', {params}).then((response) => { self.article_info = response.data self.search_results = {} let total = response.data.articles.bm ? response.data.articles.bm.length : 0 total += response.data.articles.nn ? response.data.articles.nn.length : 0 if (total == 0) { if (self.event && query.inflection_suggestions) { self.inflection_suggestions = query.inflection_suggestions } else { self.inflection_suggestions = [] } throw new QueryException(query) } else { self.error = null } Promise.all([ load_articles(self, query, offset, self.perPage, "bm"), load_articles(self, query, offset, self.perPage, "nn") ]) .then(() => { if (self.event && self.event.inflection_suggestions) { self.inflection_suggestions = self.event.inflection_suggestions.filter((item) => { if ((item[1] == 1 || item[1] == 3) && self.search_results.bm) { return !self.search_results.bm[0].suggest.includes(item[0]) } if ((item[1] == 2 || item[1] == 3) && self.search_results.nn) { return !self.search_results.nn[0].suggest.includes(item[0]) } }) } else { self.inflection_suggestions = [] } self.waiting_for_articles = false history.replaceState({ article: self.article, lang: self.lang, error: self.error, pos_selected: self.pos_selected, scope: self.scope, article_info: self.article_info, search_results: self.search_results, page: self.page, perPage: self.perPage }, '') self.previous = self.$route.fullPath }) }).catch(error =>{ self.waiting_for_articles = false console.log(error) self.error_message(error) }) } export default { name: 'DictionaryView', data: function() { return { article_key: 0, search_results: {}, lang: this.$store.state.defaultDict, waiting_for_articles: true, waiting_for_metadata: true, article: null, error: null, monthly_bm: null, monthly_nn: null, event: null, previous: this.$route.fullPath, scope: "wb", pos_selected: "ALL", article_info: null, page: 1, perPage: 10, inflection_suggestions: null, selected: null } }, computed: { waiting: function() { return (this.waiting_for_articles || this.waiting_for_metadata) && this.$route.name != 'root' }, get_search_endpoint: function() { return api } }, components: { Article, Autocomplete, SearchResults, SearchToolbar }, methods: { total_results: function() { if (this.article_info) { let total = 0 if (this.article_info.articles.bm) { total += this.article_info.articles.bm.length } if (this.article_info.articles.nn) { total += this.article_info.articles.nn.length } return total } }, error_message: function(error) { this.search_results = {} console.log(error) if (error instanceof QueryException) { if (error.params.match) { // If a suggestion from the dropdown isn't found. The message should be revised this.error = this.$t('error.no_word', {q: error.params.match, dict: this.$t('dicts_inline.'+this.lang)}) } else { this.error = this.$t('error.search_error', {q: error.params.q, dict: this.$t('dicts_inline.'+this.lang)}) if (this.lang != "bm,nn") this.error += "\r\n"+this.$t('error.other_dict', {otherDict: this.$t('inline_dict.'+this.lang == "bm"? "nn" : "bm")}) if (error.params.search != 2) this.error += "\r\n"+this.$t('error.search_advice') } } else if (error.message == "Network Error") { this.error = this.$t('error.network') } else if (error.response) { this.error = this.$t('error.server', {code: error.response.status}) } else { this.error = this.$t('error.generic') } }, select_suggestion: function (word) { this.pos_selected = null this.event = null this.inflection_suggestions = null navigate_to_query(this, word) }, select_result: function (event) { this.event = event let path = `/${this.lang}/search` let pos = this.pos_param() let query = {q: event.match || event.q} if (pos) query["pos"] = pos if (this.scope) query["scope"] = this.scope this.$router.push({path, query}) navigate_to_query(this) // Tracking let track_props = {query: event.q} if (event.match) track_props.match = event.match this.$plausible.trackEvent(event.update_lang ? "language" : 'dropdown selection', { props: track_props }) }, pos_param: function() { if (this.pos_selected) return this.pos_selected.toLowerCase() return null }, update_page: function() { let q = (this.$route.query || this.$route.params).q let path = `/${this.lang}/search` let pos = this.pos_param() let query = {q: q, page: this.page} if (pos != 'all') query.pos = pos if (this.scope) query.scope = this.scope this.$router.push({path, query}) let offset = 0 if (this.page) { offset = this.perPage * (this.page -1) } let self = this Promise.all([ load_articles(this, query, offset, this.perPage, "bm"), load_articles(this, query, offset, this.perPage, "nn")]).then(() => { history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang, error: self.error, pos_selected: self.pos_selected, scope: self.scope, article_info: self.article_info, page: self.page, perPage: self.perPage, selected: self.selected, inflection_suggestions: self.inflection_suggestions}, ''), self.$forceUpdate() } ) }, generate_path: function(q) { if (q) { let path = `/${this.lang}/search?q=${q}` if (this.scope) path += "&scope=" + this.scope return path } }, reload_params: function() { let q = (this.$route.query || this.$route.params).q if (q) { let path = `/${this.lang}/search` let pos = this.pos_param() let query = {q: q} if (pos) query.pos = pos if (this.scope) query.scope = this.scope this.$router.push({path, query}) navigate_to_query(this, q) } }, update_lang_form: function (lang) { this.lang = lang this.page = 1 this.reload_params() }, update_scope: function(scope) { this.scope = scope this.page = 1 this.reload_params() }, update_pos: function (pos) { this.pos_selected = pos this.page = 1 this.reload_params() }, article_link_click: function(item) { if (this.article && this.article.article_id == item.article_id){ this.article_key++ history.replaceState({article: this.article, search_results: this.search_results, lang: this.lang, error: this.error, pos_selected: this.pos_selected, scope: this.scope, article_info: this.article_info, page: this.page, perPage: this.perPage, selected: this.selected, inflection_suggestions: this.inflection_suggestions}, '') }else{ navigate_to_article(this, item.source) } }, details_click: function(item) { item.article.source = this.$route.fullPath this.previous = this.previous.split('#')[0] + "#" + item.title_id this.article = item.article history.replaceState({article: this.article, search_results: {}, lang: this.lang, error: null, pos_selected: this.pos_selected, scope: this.scope, article_info: this.article_info, page: this.page, perPage: this.perPage, selected: this.selected, inflection_suggestions: this.inflection_suggestions}, '') }, return_to_results: function() { this.article = null } }, mounted: function(){ let self = this self.lang = self.$route.params.lang || this.$store.state.defaultDict if (self.$route.query.pos) { self.pos_selected = self.$route.query.pos.toUpperCase() } else self.pos_selected = null if (self.$route.query.scope) { self.scope = self.$route.query.scope } if (self.$route.query.page) self.page = parseInt(self.$route.query.page) if (self.$route.query.perPage) self.perPage = self.$route.query.perPage Promise.all([ axios.get(ARTICLE_ENDPOINT + 'bm/concepts.json').then(function(response){ let concepts = response.data.concepts entities.bm = concepts }), axios.get(ARTICLE_ENDPOINT + 'nn/concepts.json').then(function(response){ let concepts = response.data.concepts entities.nn = concepts }) ]).then(function(_) { self.waiting_for_metadata = false if (self.$route.name == 'query') { navigate_to_query(self, self.$route.query.q) } else if(self.$route.name == 'search') { navigate_to_query(self, self.$route.params.query) } else if(self.$route.name == 'word') { navigate_to_query(self, self.$route.params.query) } else if(self.$route.name == 'lookup'){ navigate_to_article(self, self.$route.path) } else { self.waiting_for_articles = false history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang, error: self.error, pos_selected: self.pos_selected, scope: self.scope, article_info: self.article_info, page: self.page, perPage: self.perPage, selected: self.selected, inflection_suggestions: self.inflection_suggestions}, '') // words of the month axios.get(ARTICLE_ENDPOINT + 'bm/article/5607.json').then(function(response){ self.monthly_bm = Object.assign(response.data, {dictionary: 'bm'}) }) axios.get(ARTICLE_ENDPOINT + 'nn/article/78569.json').then(function(response){ self.monthly_nn = Object.assign(response.data, {dictionary: 'nn'}) }) } }).catch(function(error){ self.error = null if (self.lang !== 'bm') self.error.push(`Eit nettverksproblem hindra lasting av sida. Prøv å laste sida på nytt`) if (self.lang !== 'nn') self.error.push(`Et nettverksproblem hindret lasting av siden. Prøv å laste siden på nytt`) self.waiting_for_metadata = false self.waiting_for_articles = false }) }, created: function() { let self = this window.onpopstate = function (event) { if (event.state) { self.article = event.state.article self.search_results = event.state.search_results self.lang = event.state.lang self.pos_selected = event.pos_selected || self.pos_selected self.scope = event.scope || self.scope self.error = event.state.error self.page = event.state.page, self.perPage = event.state.perPage self.selected = event.state.selected self.inflection_suggestions = event.state.inflection_suggestions } } } } </script> <style> main { flex: 1 0 auto; background-color: var(--v-tertiary-base); display: flex; flex-direction: column; } div.welcome { padding-top: 10px; flex-grow: 10; background-image: url('../assets/books.jpg'); background-repeat: no-repeat; background-size: cover; } div.welcome article { border-style: none; } .search_container { background-color: var(--v-tertiary-base); padding-top: 10px; } #spinner { margin: auto; } #search_results, #spinner, #above_results, #single_article_container, div.welcome, div.search_container, .error { padding-left: calc((100vw - 1200px) / 2); padding-right: calc((100vw - 1200px) / 2); } #above_results div { padding-top: 10px; margin-left: 10px; font-size: 18px; } #suggestions { padding-left: 10px; } .error > p { margin-left: 15px; } #spinner { padding-top: 40px; } div.monthly { display: flex; width: 100%; } div.monthly > div { flex: 50%; } div.monthly.sm, div.monthly.xs { flex-direction: column; } div.monthly article.bm .dict-label::before { content: "fra "; } div.monthly article.nn .dict-label::before { content: "frå "; } div.monthly details, div.monthly h3 { display: none; } .v-label span { color: var(--v-primary-base); } .lang_select_container { padding-left: 10px; } .pos_select_container { padding-left: 10px; padding-right: 10px; padding-bottom: 0px; padding-top: 10px; } li.suggestion { font-weight: bold; padding-left: 20px; padding-top: 5px; padding-bottom: 5px; border: 0px; background-image: none; } ::selection { background: var(--v-secondary-base); color: white; } .return_to_results { padding-top: 10px; display: table-cell; } .return_to_results a { color: var(--v-text-base) !important; text-decoration: none; } .nav_arrow { vertical-align: top !important; color: var(--v-primary-base) !important; } .col { padding: 10px; } </style>