Newer
Older
<Autocomplete v-on:submit="select_result"
v-on:update-lang-form="update_lang_form">
<SearchToolbar @updatePos="update_pos"
@updateScope="update_scope" />
<div id="notifications"
v-if="$route.name && !error"
:class="$vuetify.breakpoint.name">
<div id="suggestions"
v-if="!article && !no_results">
<div class="search_notification"
v-if="inflection_suggestions && inflection_suggestions.length">
<v-icon left
color="primary">info</v-icon><em>{{queryString}}</em> {{$t('notifications.inflected')}}<!--
--><span v-for="(item,index) in inflection_suggestions"
:key="index"><!--
@click.native="inflection_link(item[0])">{{item[0]}}</router-link><!--
-->{{index == inflection_suggestions.length-1? '.' : ', '}}</span>
</div>
<div class="search_notification"
v-if="lang=='bm,nn' && similar && similar.length > 0 && (search_results.nn && search_results.nn.length == 0)">
<v-icon left
color="primary">info</v-icon>{{$t('notifications.similar_nn')}}<!--
--><span v-for="(item,index) in similar"
:key="index"><!--
--><router-link :to="generate_path({q: queryString+'|'+item[0]})"
@click.native="similar_link(queryString+'|'+item[0])">{{item[0]}}</router-link><!--
-->{{index == similar.length-1? '.' : ', '}}
</span>
</div>
<div class="search_notification"
v-if="lang=='bm,nn' && similar && similar.length > 0 && (search_results.bm && search_results.bm.length == 0)">
<v-icon left
color="primary">info</v-icon>{{$t('notifications.similar_bm')}}<!--
--><span v-for="(item,index) in similar"
:key="index"><!--
--><router-link :to="generate_path({q: queryString+'|'+item[0]})"
@click.native="similar_link(queryString+'|'+item[0])">{{item[0]}}</router-link><!--
-->{{index == similar.length-1? '.' : ', '}}
</span>
</div>
</div>
<div id="return_to_results"
v-if="$vuetify.breakpoint.mdAndUp && article && $store.state.searchRoute">
<router-link id="return_link"
:to="$store.state.searchRoute"
@click.native="return_to_results()">
<v-icon left
class="nav_arrow">chevron_left</v-icon>{{$t("notifications.back")}}
</router-link>
</div>
<div class="no_results"
v-if="no_results && !error">
<div>
<p>
<v-icon left
color=primary>error</v-icon> <strong>{{$t('notifications.no_results')}}<span
v-if="pos_selected">{{$t('notifications.no_pos_results', {pos: $t('pos_tags_plural.'+pos_selected)})}}</span></strong>
</p>
<p class="below-notification" v-if="!article && inflection_suggestions && inflection_suggestions.length">
<span v-for="(item,index) in inflection_suggestions"
:key="index"><!--
@click.native="inflection_link(item[0])">{{item[0]}}</router-link><!--
-->{{index == inflection_suggestions.length-1? '.' : ', '}}</span>
<p class="below-notification"
v-if="lang=='bm' && suggest_other_dict">{{$t('notifications.suggest_dict[1]')}}
@click.native="language_link('nn')">{{$t('dicts.nn')}}</router-link><!--
--></p>
<p class="below-notification"
v-if="lang=='nn' && suggest_other_dict">{{$t('notifications.suggest_dict[0]')}}<br>{{$t('notifications.suggest_dict[1]')}}
@click.native="language_link('bm')">{{$t('dicts_inline.bm')}}</router-link>
</p>
<p class="below-notification"
v-if="suggest_fulltext">
{{$t('notifications.fulltext[0]')}}
@click.native="fulltext_link()">
{{$t('notifications.fulltext[1]')}}
</router-link>
</p>
<article v-if="similar && similar.length"
:class="'v-sheet v-card rounded-xl did_you_mean ' + $vuetify.breakpoint.name">
<span class="dict-label">{{$t('notifications.similar')}}</span>
<v-list>
<template v-for="(item, index) in similar">
<v-list-item :key="index">
@click.native="similar_link(item[0])">{{item[0]}}</router-link>
<span class="dict-parentheses"
v-if="lang=='bm,nn'"> ({{["bokmål","nynorsk","bokmål, nynorsk"][item[1]-1]}})</span>
</v-list-item>
</template>
</v-list>
</article>
</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="$route.name && !article && !error && !no_results" />
<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 && !error && !no_results" :class="$store.state.searchRoute && $vuetify.breakpoint.mdAndUp && lang == 'bm,nn' ? article.dictionary : null">
<Article :key="article_key"
:article="article"
title_id="result0"
@article-click="article_link_click"
@details-click="details_click"
articleLookup/>
<div class="monthly"
:class="$vuetify.breakpoint.name">
<Article :article="monthly_bm"
title_id="result0"
@article-click="article_link_click"
@details-click="details_click" />
<Article :article="monthly_nn"
title_id="result1"
@article-click="article_link_click"
@details-click="details_click" />
<div class="error"
v-if="error">
<div>
<h1 tabindex="0"
id="result0">{{error.title}}</h1>
<p>{{error.description}}</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'
const ENDPOINT = process.env.VUE_APP_ENDPOINT
const FALLBACK_ENDPOINT = process.env.VUE_APP_FALLBACK_ENDPOINT
self.api.get(lang + '/article/' + self.$route.params.id + ".json")
//self.api.get("https://httpstat.us/502")
self.article = Object.assign(response.data, {'dictionary': lang, results: self.search_results})
self.handle_error(error, {retry: navigate_to_article, arg: origin, article: true})
})
.then(function(response){
self.waiting_for_articles = false
//if (origin) self.$plausible.trackEvent('internal link incoming', {props: {origin}})
async 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 self.api.get(`${dict}/article/${article_id}.json`)
//return self.api.get(`https://httpstat.us/502`)
}))
.then((response) => {
let results = response.map((element, index) => {
return Object.assign(element.data, {
dictionary: dict
})
})
self.article = null
self.search_results[dict] = results
})
.catch(error => {
})
}
else {
self.search_results[dict] = []
}
}
return Promise.resolve()
function navigate_to_query(self, word, keep_page) {
self.error = null
self.no_results = null
self.inflection_suggestions = []
self.similar = []
self.suggest_fulltext = false
self.suggest_other_dict = false
if (!self.event) {
self.event = {match: word}
}
let query = self.event
let q = query.match
if (!keep_page) {
self.page = 1
}
let params = {q, dict: self.lang, dform: 'int', include: "i", meta: 'n', wc: self.pos_selected}
self.api.get('api/suggest?', {params})
//self.api.get('https://httpstat.us/502')
let offset = 0
if (self.page) {
offset = self.perPage * (self.page -1)
}
if (self.pos_selected) params.wc = self.pos_selected
self.api.get('api/articles?', {params}).then((response) => {
//self.api.get('https://httpstat.us/502', {params}).then((response) => {
self.article_info = response.data
self.search_results = {}
let bm_length = response.data.articles.bm ? response.data.articles.bm.length : 0
let nn_length = response.data.articles.nn ? response.data.articles.nn.length : 0
let total_length = bm_length + nn_length
let dict = self.lang
// Similar
if (bm_length == 0 || nn_length == 0) {
if (!advanced_search) {
if (dict == 'bm,nn' && total_length > 0) {
dict = bm_length == 0? 'bm' : 'nn'
}
let params = {q, dict, dform: 'int', include: "s", wc: self.pos_selected}
})
} else {
self.similar = []
}
}
if (total_length == 0) {
self.waiting_for_articles = false
self.no_results = true
if (!self.scope.includes('f')) {
let params = {q, dict, n: 1, dform: 'int', include: 'f', wc: self.pos_selected}
self.suggest_fulltext = response.data.cnt > 0
}).catch(error => {
})
}
if (dict != 'bm,nn') {
let params = {q, n: 1, dict: dict=='bm'?'nn':'bm', dform: 'int', include: 'e', wc: self.pos_selected}
self.suggest_other_dict = response.data.cnt > 0 && response.data.a.exact[0][0] == q
}).catch(error => {
})
}
self.replace_history() // fixes routing bug when going back from suggested search
}
else {
self.no_results = false
Promise.all([
load_articles(self, query, offset, self.perPage, "bm"),
load_articles(self, query, offset, self.perPage, "nn")
])
.then(() => {
self.waiting_for_articles = false
self.$store.commit('setSearchRoute', self.$route.fullPath)
self.replace_history()
})
}
export default {
name: 'DictionaryView',
data: function() {
return {
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,
scope: "ei",
pos_selected: "ALL",
article_info: null,
page: 1,
perPage: 10,
inflection_suggestions: null,
similar: null,
selected: null,
suggest_fulltext: false,
suggest_other_dict: false
}
},
computed: {
queryString: function() {
return this.$route.query.q || this.$route.params.q
},
waiting: function() {
return (this.waiting_for_articles || this.waiting_for_metadata) && this.$route.name != 'root'
if (this.no_results) {
return {meta: [{name: "robots", content: 'none'}]}
}
if (this.queryString) {
q = this.queryString + (this.lang == 'bm,nn' ? ' - ' : ' | ')
let desc = " viser skrivemåte og bøying i tråd med norsk rettskriving. Språkrådet og Universitetet i Bergen står bak ordbøkene."
switch (this.lang) {
case 'bm,nn': return {title: q+'ordbøkene.no',
meta: [{name: "description", vmid: 'description', content: "Bokmålsordboka og Nynorskordboka"+desc}]}
case 'bm': return {title: q+"Bokmålsordboka",
meta: [{name: "description", vmid: 'description', content: "Bokmålsordboka"+desc}]}
case 'nn': return {title: q+"Nynorskordboka",
meta: [{name: "description", vmid: 'description', content: "Nynorskordboka"+desc}] }
},
components: {
Article,
Autocomplete,
load_welcome_and_metadata: function() {
let self = this
Promise.all([
self.api.get('bm/concepts.json').then(function(response){
let concepts = response.data.concepts
entities.bm = concepts
}),
self.api.get('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 == 'search') {
navigate_to_query(self, self.$route.query.q, true)
}
else if(self.$route.name == 'word') {
self.scope = 'e'
self.pos = null
navigate_to_query(self, self.$route.params.q, true)
}
else if(self.$route.name == 'lookup'){
navigate_to_article(self, self.$route.path)
}
else {
self.waiting_for_articles = false
self.replace_history()
self.load_monthly_bm()
self.load_monthly_nn()
}
}).catch(function(error){
self.api = axios.create({baseURL: FALLBACK_ENDPOINT})
if (!self.error || !self.error.response) {
if (self.fallback) {
if (error.response) {
self.error = {title: self.$t('error.server.title'), description: self.$t('error.server.description', {code: error.response.status}), response: error.response}
}
else if (error.message == "Network Error") {
self.error = {title: self.$t('error.network.title'), description: self.$t('error.network.description')}
}
else {
self.error = {title: self.$t('error.generic.title'), description: self.$t('error.generic.description')}
}
self.waiting_for_metadata = false
self.waiting_for_articles = false
history.replaceState({article: this.article,
search_results: this.search_results,
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
error: this.error,
no_results: this.no_results,
pos_selected: this.pos_selected,
scope: this.scope,
page: this.page,
perPage: this.perPage,
inflection_suggestions: this.inflection_suggestions,
similar: this.similar,
suggest_fulltext: this.suggest_fulltext,
suggest_other_dict: this.suggest_other_dict}, '')
},
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
}
},
load_monthly_bm: function() {
let self = this
self.monthly_bm = Object.assign(response.data, {dictionary: 'bm'})
})
self.monthly_nn = Object.assign(response.data, {dictionary: 'nn'})
})
},
this.waiting_for_articles = false
this.no_results = false
this.search_results = {}
this.inflection_suggestions = []
this.similar = []
this.suggest_fulltext = false
this.suggest_other_dict = false
if (!this.error || !this.error.response) {
if (this.fallback || !retry_params.retry) {
this.fallback = true
if (error.response) {
if (error.response.status == 404) {
if (retry_params.article) {
this.error = {title: this.$t('error.404.title'), description: this.$t('error.no_article', {id: this.$route.params.id}), article: true, response: error.response}
}
else {
this.error = {title: this.$t('error.404.title'), description: this.$t('error.404.description'), article: retry_params.article, response: error.response}
}
}
else if (error.response.status == 503) {
this.error = {title: this.$t('error.503.title'), description: this.$t('error.503.description'), article: retry_params.article, response: error.response}
}
else if (String(error.response.status)[0] == "5") {
this.error = {title: this.$t('error.server.title'), description: this.$t('error.server.description', {code: error.response.status}), article: retry_params.article, response: error.response}
this.error = {title: this.$t('error.generic_code.title'), description: this.$t('error.generic_code.description', {code: error.response.status}), article: retry_params.article, response: error.response}
} else if (error.message == "Network Error") {
this.error = {title: this.$t('error.network.title'), description: this.$t('error.network.description'), article: retry_params.article, response: error.response}
this.error = {title: this.$t('error.generic.title'), description: this.$t('error.generic.description'), article: retry_params.article, response: error.response}
} else {
this.fallback = true
retry_params.retry(this, retry_params.arg)
//this.$plausible.trackEvent('inflection link', {props: {lang: this.previous.params.lang, from: this.previous.query.q, to: word}})
this.event = null
navigate_to_query(this, word)
},
similar_link: function (word) {
//this.$plausible.trackEvent('similar link', {props: {lang: this.previous.params.lang, from: this.previous.query.q, to: word}})
this.event = null
navigate_to_query(this, word)
},
fulltext_link: function () {
this.event = null
this.scope = this.scope + "f"
},
language_link: function (lang) {
this.lang = lang
this.event = null
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('dropdown selection', { props: track_props })
pos_param: function() {
if (this.pos_selected) return this.pos_selected.toLowerCase()
return null
},
update_page: function() {
this.waiting_for_articles = true
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
if (this.perPage) query.perPage = this.perPage
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(() => {
self.replace_history()
self.$forceUpdate()
/*
// Debugging
if (self.page < Math.ceil(Math.max(self.article_info.articles.bm.length, self.article_info.articles.nn.length)/self.perPage)) {
self.page+=1
self.update_page()
}
*/
}
).then(() => {
this.$store.commit('setSearchRoute', this.$route.fullPath)
this.waiting_for_articles = false
})
},
generate_path: function(params) {
if (this.$route.name == "word") {
return this.$router.resolve({name: "search", query: {q: this.$route.params.q, ...params}}).href
}
else {
return this.$router.resolve({query: {...this.$route.query, ...params}}).href
}
},
generate_lang_path: function(dict) {
return this.$route.fullPath.replace(/\/(bm|nn|bm,nn)\//, "/"+dict+"/")
if (q) {
let path = `/${this.lang}/search`
let pos = this.pos_param()
if (pos) query.pos = pos
if (this.scope) query.scope = this.scope
if (this.scope) query.scope = this.scope
if (this.perPage) query.perPage = this.perPage
this.$router.push({path, query})
navigate_to_query(this, q)
update_lang_form: function (lang) {
this.lang = lang
this.page = 1
},
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()
},
update_per_page: function(perPage) {
this.perPage = perPage
this.$store.commit('setPerPage', this.perPage)
this.page = 1
this.reload_params()
},
let event = window.event
if (!(event.ctrlKey || event.shiftKey)) {
if (this.article && this.article.article_id == item.article_id){
this.article_key++
this.replace_history()
}else{
navigate_to_article(this, item.source)
}
},
details_click: function(item) {
let event = window.event
if (!(event.ctrlKey || event.shiftKey)) {
this.article = item.article
this.replace_history()
}
},
return_to_results: function() {
this.article = null
},
set_fulltext_highlight: function() {
if (this.$route.query.q && this.scope.includes("f")) {
let q = this.$route.query.q
q = q.replace(/\*|%/, "[^\\s]*")
q = q.replace(/_|\?/, "[^\\s]")
this.$store.commit('setFulltextHighlight', q)
}
else {
this.$store.commit('setFulltextHighlight', false)
}
this.api = axios.create({baseURL: ENDPOINT})
this.lang = this.$route.params.lang || this.$store.state.defaultDict || 'bm,nn'
if (this.$route.query.pos) {
this.pos_selected = this.$route.query.pos.toUpperCase()
} else this.pos_selected = null
if (this.$route.query.scope) {
this.scope = this.$route.query.scope
this.set_fulltext_highlight()
if (this.$route.query.page) this.page = parseInt(this.$route.query.page)
if (this.$route.query.perPage) {
this.perPage = parseInt(this.$route.query.perPage)
watch: {
$route(to, from) {
this.previous = from
this.load_monthly_bm()
this.load_monthly_nn()
}
if (to.name == 'lookup' && from.fullPath == '/') {
this.$store.commit('setSearchRoute', null)
}
created: function() {
let self = this
window.onpopstate = function (event) {
if (event.state) {
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
if (event.state.lang) {
self.article = event.state.article
self.search_results = event.state.search_results
self.article_info = event.state.article_info
self.lang = event.state.lang
self.pos_selected = event.state.pos_selected
self.scope = event.state.scope
self.error = event.state.error
self.no_results = event.state.no_results
self.page = event.state.page,
self.perPage = event.state.perPage
self.inflection_suggestions = event.state.inflection_suggestions,
self.similar = event.state.similar,
self.suggest_fulltext = event.state.suggest_fulltext,
self.suggest_other_dict = event.state.suggest_other_dict
if (!self.$route.hash && self.$route.name != 'search') {
history.scrollRestoration = 'manual'
window.scrollTo(0,0)
}
else {
history.scrollRestoration = 'auto'
}
}
else {
console.log("Navigation errror")
}
}
}
}
}
</script>
<style>
main {
flex: 1 0 auto;
background-color: var(--v-tertiary-base);
display: flex;
flex-direction: column;
flex-grow: 10;
background-image: url('../assets/cate-brodersen-b7ukrYhnt8c-unsplash.webp');
div.welcome article {
border-style: none;
}
.search_container {
background-color: var(--v-tertiary-base);
}
#search_results, #spinner, #notifications, #single_article_container, div.search_container, .error {
padding-left: calc((100vw - 1200px) / 2);
padding-right: calc((100vw - 1200px) / 2);
div.welcome {
padding-left: calc((100vw - 917px) / 2);
padding-right: calc((100vw - 917px) / 2);
}
#single_article_container {
box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
background-color: white;
height: 100%;
#single_article_container.nn {
justify-content: flex-end !important;
display: flex;
}
#single_article_container.nn article, #single_article_container.bm article{
width: 50%;
}
padding-bottom: 0px;
margin-left: 10px;
font-size: 18px;
}
#suggestions {
padding-left: 10px;
}
.error p, .no_results p {
.no_results {
padding-top: 24px;
}
.error div{
padding: 10px;
padding-top: 24px;
}
#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 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 a {
color: var(--v-text-base) !important;
color: var(--v-primary-base) !important;
}
.col {
padding: 10px;
}
.below-notification {
padding-left: 10px;
}
.did_you_mean.md, .did_you_mean.lg, .did_you_mean.xl {
max-width: 40%;
}
.dict-parentheses {
color: rgba(0,0,0,0.6);
font-size: 85%;
}