Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 3021 additions and 553 deletions
<template>
<div class="autocomplete-container">
<v-autocomplete
v-model="select"
:loading="loading"
:items="items"
:search-input.sync="search"
item-text="label"
:menu-props="{maxHeight: 500, transition: 'fade-transition'}"
prepend-inner-icon="search"
append-icon="undefined"
return-object
hide-no-data
no-filter
hide-details
label="Søk..."
solo
auto-select-first
placeholder="Søk..."
ref="autocomplete"
color="secondary"
dense
>
<template v-slot:item="data">
<span class="search-hit">{{data.item.label}} </span> ({{data.item.lang_set ? Array.from(data.item.lang_set).sort().join(', ') : 'fritekstsøk'}})
</template>
</v-autocomplete>
</div>
</template>
<script>
import axios from "axios"
import debounce from "debounce"
export default {
props: {
endpoint: String
},
data: function() {
return {
loading: false,
items: [],
search: null,
select: null,
debounced: debounce(function(q, self) {
self.loading = true
return axios.get(self.endpoint + 'suggest?q=' + encodeURIComponent(q))
.then(
function(response) {
let hits = []
if (q == self.search) {
response.data.forEach((item, i) => {
let match = encodeURIComponent(item.match)
if (! hits[0] || hits[0].word != match) {
hits.splice(0, 0, {q: encodeURIComponent(q), lang_set: new Set(), word: 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 = decodeURIComponent(hit.word)
}
});
hits.reverse()
hits = hits.slice(0, 9)
if (q) {
hits.push({q: encodeURIComponent(q), label: q + ' '})
}
}
self.items = hits
self.loading = false
})
}, 100)
}
},
watch: {
search (val) {
if (! val) {
this.items = []
} else {
this.run_query(val)
}
},
select(item) {
this.$emit('submit', item)
let self = this
setTimeout(() => self.$refs.autocomplete.$refs.input.select(), 1)
}
},
methods: {
run_query(q) {
this.debounced(q, this)
}
},
}
</script>
<style scoped>
.search-hit {
font-weight: bold;
margin-right: 5px;
}
.autocomplete-container {
max-width: 500px;
}
</style>
......@@ -2,11 +2,13 @@
<li class="compound_list">
<ul>
<DefElement :body="body.intro" v-if="body.intro" :dictionary="dictionary" />
<li :key="index" v-for="(item, index) in body.elements">
<router-link :to="'/' + dictionary + '/' + item.article_id + (item.definition_id ? '#def'+item.definition_id : '')" @click.native="article_link_click(item)">
<span class="homograph" :key="index">{{item.lemmas[0].hgno ? String.fromCharCode(0x215f + item.lemmas[0].hgno) : ''}}</span>
{{item.lemmas[0].lemma}};
</router-link>
<li
:key="index"
v-for="(item, index) in body.elements"
>{{' '}}<router-link
:to="'/' + dictionary + '/' + item.article_id + (item.definition_id ? '#def'+item.definition_id : '')"
@click.native="article_link_click(item)"
>{{item.lemmas[0].lemma}}</router-link>
</li>
</ul>
</li>
......@@ -35,4 +37,12 @@ export default {
li.compound_list ul li {
display: inline;
}
li.compound_list li:not(:last-child):not(:first-child):after {
content: ",";
}
ul {
padding-left: 0px !important;
}
</style>
<template>
<v-list>
<v-list-item>
<div>
<h2>{{$t('contact.content[0]')}}</h2><p>
{{$t('contact.content[2]')}} <a href="mailto:ordbok@uib.no">ordbok@uib.no</a></p>
<h2>{{$t('contact.content[1]')}}</h2><p>
{{$t('contact.content[3]')}} <span style="white-space: nowrap;"><a href="mailto:ordbok-teknisk@uib.no">ordbok-teknisk@uib.no</a></span></p>
</div>
</v-list-item>
<v-list-item>
<v-list-item-title>
<h2>{{$t('contact.faq.title')}}</h2>
</v-list-item-title>
</v-list-item>
<v-list-group dense prepend-icon="help">
<template v-slot:activator>
<v-list-item-content>{{$t('contact.faq.items[0].title')}}</v-list-item-content>
</template>
<v-list-item><p><em>{{$t('dicts.bm')}}</em>{{$t('and')}}<em>{{$t('dicts.nn')}}</em>{{$t('contact.faq.items[0].text[0]')}}<router-link to="/om" @click.native="$emit('close')">{{$t('contact.faq.items[0].text[1]')}}</router-link></p></v-list-item>
</v-list-group>
<v-list-group prepend-icon="help">
<template v-slot:activator>
<v-list-item-content>{{$t('contact.faq.items[1].title')}}</v-list-item-content>
</template>
<v-list-item>{{$t('contact.faq.items[1].text')}}</v-list-item>
</v-list-group>
<v-list-group prepend-icon="help">
<template v-slot:activator>
<v-list-item-content>{{$t('contact.faq.items[2].title')}}</v-list-item-content>
</template>
<v-list-item>{{$t('contact.faq.items[2].text')}}</v-list-item>
</v-list-group>
</v-list>
</template>
<script>
export default {
name: "Contact",
}
</script>
......@@ -2,12 +2,19 @@
<li :is="tag" :class="body.type_"><!--
--><span :is="item.tag || 'span'" v-for="(item, index) in assemble_text"
:class="item.type"
@error="article_error"
:key="index"
v-bind="item.props"><!--
-->{{item.html}}<!--
--><router-link v-if="item.type == 'article_ref'" :to="item.ref" @click.native="article_link_click(item)"><!--
-->{{item.link_text}} {{item.definition_order ? ` (${item.definition_order})` : ''}}<!--
--></router-link><!--
--><router-link class="article_ref" v-if="item.type == 'article_ref'" :to="item.ref" @click.native="article_link_click(item)" :key="index"><!--
--><DefElement tag='span' v-if="item.link_text.type_" :dictionary="dictionary" :key="item.id+'_sub'" :body='item.link_text'/><span v-else>{{item.link_text}}</span><!--
--><span class="homograph" v-if="item.lemmas[0].hgno" :aria-label="`${dictionary=='bm'? 'Betydning': 'Tyding'} ${item.lemmas[0].hgno}`" :title="`${dictionary=='bm'? 'Betydning': 'Tyding'} ${item.lemmas[0].hgno}`" :key="index"><!--
--> ({{roman_hgno(item.lemmas[0])}}{{item.definition_order ? '': ')'}}</span>
<span class="def_order" v-if="item.definition_order" :aria-label="'definisjon '+item.definition_order">{{item.lemmas[0].hgno ? ', ': ' ('}}{{item.definition_order}})</span>
</router-link>
<!--
--><span class="numerator" v-if="item.type == 'fraction'">{{item.num}}</span><!--
-->{{item.type == 'fraction' ? '' : ''}}<!--
--><span class="denominator" v-if="item.type == 'fraction'">{{item.denom}}</span><!--
......@@ -18,6 +25,15 @@
import entities from '../utils/entities.js'
import helpers from '../utils/helpers.js'
function replace_grammar_id(item, lang) {
let content = item.content
if (content.includes('$') && (item.items[0].id)){
let replacement_item = entities[lang][item.items[0].id]['expansion']
content = content.replace('$', replacement_item)}
return content
}
export default {
name: 'DefElement',
props: {
......@@ -34,34 +50,58 @@ export default {
}
},
computed: {
fulltext_highlight: function() {
return this.$store.state.fulltextHighlight
},
unparsed: function(){
let lang = this.dictionary
let path = this.path
return this.body.items.map(
function(item){
if (item.type_ == 'usage') return {type: item.type_, html: item.text, tag: 'mark'}
else if (item.type_ == 'article_ref') return {
type: item.type_,
html: '',
link_text: item.word_form || item.lemmas[0].lemma,
ref: '/' + lang + '/' + item.article_id + '/' + encodeURIComponent(item.word_form || item.lemmas[0].lemma) + (item.definition_id ? '#def' + item.definition_id : ''),
article_id: item.article_id,
definition_id: item.definition_id,
definition_order: item.definition_order,
source: path
}
else if (item.type_ == 'pronunciation') return {type: item.type_, html: item.string}
else if (item.type_ == 'superscript') return {type: item.type_, html: item.text, tag: 'sup'}
else if (item.type_ == 'subscript') return {type: item.type_, html: item.text, 'tag': 'sub'}
else if (item.type_ == 'quote_inset') return {type: item.type_, body: item, html: '', tag: 'DefElement', props: {body: item, tag: 'i'}}
else if (item.type_ == 'fraction') return helpers.fraction(item.numerator, item.denominator)
else if (item.id) return {type: item.type_, html: (entities[lang][item.id] || {})['expansion'] || item.id}
else return {type: item.type_ || 'plain', html: item}
try {
let lang = this.dictionary
let path = this.path
return this.body.items.map(
function(item){
if (item.type_ == 'usage') {
if (item.items) {
item.content = item.text
return {type: item.type_, html: '', tag: 'DefElement', props: {body: item, tag: 'i', dictionary: lang}}
}
else {
return {type: item.type_, html: item.text, tag: 'i'}
}
}
else if (item.type_ == 'article_ref') {
return {
type: item.type_,
html: '',
lemmas: item.lemmas,
link_text: item.word_form || item.lemmas[0].annotated_lemma || item.lemmas[0].lemma,
ref: '/' + lang + '/' + item.article_id + (item.definition_id ? '#def' + item.definition_id : ''),
article_id: item.article_id,
definition_id: item.definition_id,
definition_order: item.definition_order,
source: path
}
}
else if (item.type_ == 'pronunciation') return {type: item.type_, html: item.string}
else if (item.type_ == 'pronunciation_guide') return {type: item.type_, body: item, html: '', tag: 'DefElement', props: {body: item, tag: 'i', dictionary: lang}}
else if (item.type_ == 'superscript') return {type: item.type_, html: item.text, tag: 'sup'}
else if (item.type_ == 'subscript') return {type: item.type_, html: item.text, tag: 'sub'}
else if (item.type_ == 'quote_inset') return {type: item.type_, body: item, html: '', tag: 'DefElement', props: {body: item, tag: 'i', dictionary: lang}}
else if (item.type_ == 'fraction') return helpers.fraction(item.numerator, item.denominator)
else if (item.id) return {type: item.type_, html: (entities[lang][item.id] || {})['expansion'] || item.id}
else return {type: item.type_ || 'plain', html: item}
}
)
}
catch(error) {
this.$emit('error', {location: "unparsed", message: error.message} )
return {type: 'plain', html: item}
}
},
assemble_text: function(){
var old_parts = this.body.content.split(/(\$)/)
try {
var old_parts = this.body.content.split(/(\$)/)
var text_items = this.unparsed.slice(0).reverse()
var new_parts = []
old_parts.forEach(function(item){
......@@ -72,22 +112,37 @@ export default {
}
})
return new_parts
}
catch(error) {
this.$emit('error', {location: "assemble_text", message: error.message} )
return []
}
}
},
methods: {
article_link_click: function(item) {
this.$emit('article-click', item)
}
},
article_error: function(payload) {
this.$emit('error', payload)
},
roman_hgno: helpers.roman_hgno
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.usage, .pronunciation {
.usage {
font-style: italic;
}
.pronunciation_guide {
font-size: smaller;
}
.numerator{
vertical-align: super;
padding-right: 2px;
......@@ -103,19 +158,23 @@ export default {
mark {
background: rgba(255, 255, 255, 0);
}
span.language {
font-weight: bold;
color: inherit;
}
i {
font-style: normal;
font-family: monospace;
}
.homograph {
vertical-align: sub;
.link_text {
text-decoration: underline;
}
.homograph, .def_order{
text-decoration: none !important;
color: black
}
q:before {
......
<template>
<li :class="['definition', 'level'+level]" :ref="'def' + body.id" :id="'def' + body.id">
<ul class="explanation">
<li :is="element_wrapper.template" :body="element_wrapper.element" :dictionary="element_wrapper.dictionary" v-for="(element_wrapper, index) in template_name_added" :key="index" @article-click="article_link_click">{{element_wrapper.element}}</li>
<li :class="['definition', 'level'+level]" :ref="level != 9 ? 'def' + body.id : ''" :id="level != 9? 'def' + body.id : ''">
<span v-if="level!=9"/>
<ul class="explanations">
<DefElement :body="explanation" :dictionary="dictionary" :has_article_ref=has_article_ref(explanation) v-for="(explanation, index) in explanations" :key="index" @article-click="article_link_click" />
</ul>
<div :is="level < 3 ? 'ol' : 'ul'" class="sub_definitions" v-if="subdefs.length">
<Definition :level="level+1" :body="subdef" v-for="(subdef, index) in subdefs" :dictionary="dictionary" :key="index" @article-click="article_link_click" />
<div v-if="examples.length">
<h5 :lang="lang_tag_locale" v-if="level < 3">{{$t('article.headings.examples', content_locale)}}</h5>
<ul class="examples">
<Example :body="example" :dictionary="dictionary" v-for="(example, index) in examples" :key="index" @article-click="article_link_click" />
</ul>
</div>
<ul class="sub_articles" v-if="subArticles.length">
<SubArticle :body="subart" v-for="(subart, index) in subArticles" :dictionary="dictionary" :key="index" @article-click="article_link_click" />
<ul class="compound_lists">
<CompoundList :body="compound_list" :dictionary="dictionary" v-for="(compound_list, index) in compund_lists" :key="index" @article-click="article_link_click" />
</ul>
<div :is="level < 3 ? 'ol' : 'ul'" class="sub_definitions" v-if="subdefs.length">
<Definition :def_number='index+1' :level="level+1" :body="subdef" v-for="(subdef, index) in subdefs" :dictionary="dictionary" :key="index" @article-click="article_link_click" />
</div>
</li>
</template>
<script>
import DefElement from './DefElement.vue'
import Example from './Example.vue'
import SubArticle from './SubArticle.vue'
import CompoundList from './CompoundList.vue'
var Definition = {
......@@ -23,57 +29,92 @@ var Definition = {
props: {
body: Object,
level: Number,
dictionary: String
dictionary: String,
def_number: Number
},
components: {
DefElement,
Definition,
Example,
SubArticle,
CompoundList
},
computed: {
template_name_added: function(){
let dictionary = this.dictionary
return this.body.elements.filter(el => ! ['definition', 'sub_article'].includes(el.type_)).map(
function(element){
return {
'template': {
'explanation': 'DefElement',
'example': 'Example',
'compound_list': 'CompoundList'
}[element.type_] || 'li',
'element': element,
'dictionary': dictionary
}
})
explanations: function() {
try {
return this.body.elements.filter(el => el.type_ == 'explanation')
} catch (error) {
this.$emit('error', {location: "explanations", message: error.message})
return []
}
},
examples: function() {
try {
return this.body.elements.filter(el => el.type_ == 'example')
} catch (error) {
this.$emit('error', {location: "examples", message: error.message})
return []
}
},
compund_lists: function() {
try {
return this.body.elements.filter(el => el.type_ == 'compound_list')
} catch (error) {
this.$emit('error', {location: "compound_lists", message: error.message})
return []
}
},
subdefs: function() {
return this.body.elements.filter(el => el.type_ == 'definition')
try {
return this.body.elements.filter(el => el.type_ == 'definition').filter(def => def.elements.filter(el => el.type_ != 'sub_article').length > 0)
} catch (error) {
this.$emit('error', {location: "subdefs", message: error.message})
return []
}
// filtrerer bort definisjoner som bare inneholder underartikler
},
content_locale: function() {
return this.$parent.content_locale
},
subArticles: function() {
return this.body.elements.filter(el => el.type_ == 'sub_article')
lang_tag_locale: function() {
return this.$parent.lang_tag_locale
}
},
},
mounted: function() {
let ref = 'def' + this.body.id
if(location.hash.substring(1) == ref){
this.$refs[ref].scrollIntoView()
this.$refs[ref].scrollIntoView({block: 'center'})
this.$refs[ref].classList.add('highlighted')
}
},
methods: {
article_link_click: function(item) {
this.$emit('article-click', item)
}
},
has_article_ref: function(item){
if(item.items.length && item.items[0].type_ == "article_ref" && item.items[0].definition_id === undefined)
{
return "true";
}
else{
return "false";
}
}
},
watch:{
$route(to, from) {
let ref = 'def' + this.body.id
if(location.hash.substring(1) == ref){
this.$refs[ref].classList.add('highlighted')
}else{
this.$refs[ref].classList.remove('highlighted')
if (this.$refs[ref]) {
if(location.hash.substring(1) == ref){
this.$refs[ref].classList.add('highlighted')
}else {
this.$refs[ref].classList.remove('highlighted')
}
}
}
}
......@@ -86,8 +127,18 @@ q {
font-style: italic;
}
.highlighted {
background-color: var(--v-tertiary-base);
border-radius: 5px;
.highlighted, mark {
background-color: var(--v-tertiary-darken1);
}
mark {
font-weight: bold;
}
li[has_article_ref="true"] {
margin-top: 8px;
margin-left: -25px;
}
</style>
<template>
<main :class="(article || waiting || error) ? '' : 'welcome '">
<div class="search_container">
<div class="lang_select_container">
<v-radio-group row v-model="lang">
<template v-slot:label>
<span>VIS TREFF I</span>
</template>
<v-radio value="bob,nob" color="secondary">
<template v-slot:label>
<span>begge<span class="verbose"> ordbøkene</span></span>
</template>
</v-radio>
<v-radio value="bob" color="secondary">
<template v-slot:label>
<span><span class="verbose">bokmål (</span>bm<span class="verbose">)</span></span>
</template>
</v-radio>
<v-radio value="nob" color="secondary">
<template v-slot:label>
<span><span class="verbose">nynorsk (</span>nn<span class="verbose">)</span></span>
</template>
</v-radio>
</v-radio-group>
<main tabindex="-1" ref="main" id="main" class="dict-container">
<span class="chosen_api" v-if="chosen_api">API: {{chosen_api}}</span>
<SearchForm ref="SearchForm"
v-on:submit="select_result"
v-on:update-lang-form="update_lang_form"
@updatePos="update_pos"
@updateScope="update_scope">
</SearchForm>
<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 && this.queryString.slice(-1) != "."'>
<v-icon left
color="primary">info</v-icon><em>{{queryString}}</em> {{$t('notifications.inflected')}}<!--
--><span v-for="(item,index) in inflection_suggestions"
:key="index"><!--
--><router-link :to="generate_path({q: item[0]})"
@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="other_dict(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="other_dict(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>
<Autocomplete @submit="select_result" :endpoint="api_pref">
</Autocomplete>
<div class="no_results"
v-if="no_results && !error">
<div>
<p>
<v-icon left
color=primary>error</v-icon> <strong role="heading" aria-level="2" id="result0">{{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">
<em>{{this.queryString}}</em>{{$t('notifications.inflected')}}
<span v-for="(item,index) in inflection_suggestions"
:key="index"><!--
--><router-link :to="generate_path({q: item[0]})"
@click.native="inflection_link(item[0])">{{item[0]}}</router-link><!--
-->{{index == inflection_suggestions.length-1? '.' : ', '}}</span>
</p><p class="below-notification"
v-if="lang=='bm' && suggest_other_dict">{{$t('notifications.suggest_dict[1]')}}
<router-link :to="generate_lang_path('nn')"
@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]')}}
<router-link :to="generate_lang_path('bm')"
@click.native="language_link('bm')">{{$t('dicts_inline.bm')}}</router-link>
</p>
<div v-if="suggest_exact.length"
:class="'v-sheet v-card rounded-xl did_you_mean article ' + $vuetify.breakpoint.name">
<span class="similar-label" role="heading" aria-level="1">{{$t('notifications.similar')}}</span>
<v-list>
<template v-for="(item, index) in suggest_exact">
<v-list-item :key="index">
<router-link :to="generate_path({q: item[0], scope})"
@click.native="exact_link()">{{item[0]}}</router-link>
<span class="dict-parentheses"
v-if="lang=='bm,nn'">&nbsp;({{["bokmål","nynorsk","bokmål, nynorsk"][item[1]-1]}})</span>
</v-list-item>
</template>
</v-list>
</div>
<div v-if="suggest_fulltext.length"
:class="'v-sheet v-card rounded-xl did_you_mean article ' + $vuetify.breakpoint.name">
<span class="similar-label" role="heading" aria-level="1">{{$t('notifications.fulltext')}}</span>
<v-list>
<template v-for="(item, index) in suggest_fulltext">
<v-list-item :key="index">
<router-link :to="generate_path({q: item[0], scope: scope+'f'})"
@click.native="fulltext_link()">{{item[0]}}</router-link>
<span class="dict-parentheses"
v-if="lang=='bm,nn'">&nbsp;({{["bokmål","nynorsk","bokmål, nynorsk"][item[1]-1]}})</span>
</v-list-item>
</template>
</v-list>
</div>
<div v-if="similar && similar.length"
:class="'v-sheet v-card rounded-xl did_you_mean article ' + $vuetify.breakpoint.name">
<span class="similar-label" role="heading" aria-level="1">{{$t('notifications.similar')}}</span>
<v-list>
<template v-for="(item, index) in similar">
<v-list-item :key="index">
<router-link :to="generate_path({q: item[0]})"
@click.native="similar_link(item[0])">{{item[0]}}</router-link>
<span class="dict-parentheses"
v-if="lang=='bm,nn'">&nbsp;({{["bokmål","nynorsk","bokmål, nynorsk"][item[1]-1]}})</span>
</v-list-item>
</template>
</v-list>
</div>
</div>
</div>
</div>
<div id="spinner" v-if="waiting">
<v-progress-circular indeterminate color="secondary" size="120"></v-progress-circular>
<SearchResults :results_bm="search_results.bm || []"
:results_nn="search_results.nn || []"
:lang="lang"
:queryPattern="queryPattern"
:scope="scope"
@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>
<SearchResults :hits="search_results" :lang="lang" @article-click="article_link_click" v-if="! waiting" />
<div id="single_article_container" v-if="article">
<Article :key="article_key" :article="article" @article-click="article_link_click" />
<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>
<div class="welcome" v-if="! (article || error || search_results.length || waiting)">
<div class="monthly">
<div class="welcome-container" :class="$vuetify.breakpoint.name"
v-show="!error && ($route.name=='/' || !$route.name)">
<div class="welcome" :class="$vuetify.breakpoint.name">
<div class="monthly-title"><h2><span>{{$t('monthly')}}</span></h2></div>
<div class="monthly"
:class="$vuetify.breakpoint.name">
<div>
<Article :article="monthly_bm" @article-click="article_link_click" />
<Article :article="monthly_bm"
title_id="result0"
@article-click="article_link_click"
@details-click="details_click" />
</div>
<div>
<Article :article="monthly_nn" @article-click="article_link_click" />
<Article :article="monthly_nn"
title_id="result1"
@article-click="article_link_click"
@details-click="details_click" />
</div>
</div>
</div>
<div class="error" v-if="error">
<h1>Oooops...</h1>
{{error}}
</div>
<div class="error"
v-if="error">
<div>
<h1 tabindex="0"
id="result0">{{error.title}}</h1>
<p>{{error.description}}</p>
</div>
</div>
<div v-if="$route.name && $route.name != 'lookup' && $store.state.currentLocale != 'ukr' && !$parent.waiting" class="betalink notification mx-auto mb-10"
centered
>
<div class="d-flex flex-column align-md-center flex-md-row mx-2">
<div class="shrink justify-top mr-8" cols="12" sm="6">
<img aria-hidden="true" width="96" height="96" src="https://test.ordbokene.no/favicon.ico">
</div>
<div>
<div v-if="$store.state.currentLocale == 'nob'">
<h2>Betaversjon</h2>
Vi har lansert en betaversjon av denne nettsiden med bedre søkeforslag og forslag til oversettelser mellom bokmål og nynorsk.<div class="mt-2"> <a :href="beta_search">Søk i betaversjonen</a><v-icon small color="white">launch</v-icon></div>
</div>
<div v-if="$store.state.currentLocale == 'nno'">
<h2>Betaversjon</h2>
Vi har lansert ein betaversjon av denne nettstaden med betre søkjeforslag og forslag til omsetjingar mellom bokmål og nynorsk. <div class="mt-2"><a :href="beta_search">Søk i betaversjonen</a><v-icon small color="white">launch</v-icon></div>
</div>
<div v-if="$store.state.currentLocale == 'eng'">
<h2>Beta version</h2>
We have launched a beta version of this website with better search suggestions and suggested translations between Bokmål and Nynorsk.<div class="mt-2"><a :href="beta_search">Search in the beta version</a><v-icon small color="white">launch</v-icon></div>
</div>
</div>
</div>
</div>
</main>
</template>
......@@ -55,237 +224,887 @@ 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 SearchForm from './SearchForm.vue'
const ENDPOINTS = {
'oda_prod': process.env.VUE_APP_ODA_PROD,
'oda_dev': process.env.VUE_APP_ODA_DEV,
'odd_prod': process.env.VUE_APP_ODD_PROD,
'odd_dev': process.env.VUE_APP_ODD_DEV,
'ida_dev': process.env.VUE_APP_IDA_DEV,
'ida_prod': process.env.VUE_APP_IDA_PROD,
}
const ENDPOINT = ENDPOINTS[process.env.VUE_APP_ENDPOINT]
const FALLBACK_ENDPOINT = ENDPOINTS[process.env.VUE_APP_FALLBACK_ENDPOINT]
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);
});
function navigate_to_article(self, origin) {
self.article = null
self.waiting_for_articles = true
function navigate_to_article(self, source) {
axios.get(api_endpoint + '/' + self.$route.params.lang + '/article/' + self.$route.params.id)
const lang = self.$route.params.lang
self.api.get(lang + '/article/' + self.$route.params.id + ".json")
//self.api.get("https://httpstat.us/502")
.then(function(response){
self.article = Object.assign(response.data, {'dictionary': self.$route.params.lang})
self.search_results = []
self.article = Object.assign(response.data, {'dictionary': lang, results: self.search_results})
self.error = null
})
.catch(function(error){
if (error.response && error.response.status == 404) {
self.error = "Vi har ingen artikkel med id " + self.$route.params.id
} else {
self.error = "Noe gikk galt..."
console.log(error)
}
self.handle_error(error, {retry: navigate_to_article, arg: origin, article: true})
})
.then(function(response){
self.waiting_for_articles = false
history.replaceState({article: self.article, search_results: [], lang: self.lang, error: self.error}, '')
if (source) {
self.$plausible.trackEvent('internal link incoming', {props: {origin: source}})
}
self.replace_history()
if (origin) self.$plausible.trackEvent(' incoming', {props: {origin}})
})
}
function navigate_to_search(self, query) {
axios.get(self.api_pref + 'search?q=' + query)
.then(function(response){
self.search_results = response.data
if (! self.search_results.length) {
self.error = "Vi fant ingen resultater for '" + decodeURIComponent(query) + "'. (Søkeforlag kommer i en senere oppatering av Ordbøkene)"
} else {
self.error = null
}
})
.catch(function(error){
if (error.response && error.response.status == 400) {
self.error = "Søkeuttrykket inneholder feil"
} else if (error.response) {
self.error = "Noe gikk galt på serversiden"
} else {
self.error = "Nettverksproblemer, prøv igjen"
}
})
.then(function(_){
self.waiting_for_articles = false
history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang, error: self.error}, '')
})
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 => {
self.handle_error(error, {})
})
}
else {
self.search_results[dict] = []
}
}
return Promise.resolve()
}
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)
if (! self.search_results.length) {
self.error = "Ordet '" + decodeURIComponent(word) + "' finnes ikke i ordbøkene"
} else {
self.error = null
function navigate_to_query(self, word, keep_page) {
self.error = null
self.no_results = null
self.waiting_for_articles = true
self.inflection_suggestions = []
self.similar = []
self.suggest_fulltext = []
self.suggest_exact = []
self.suggest_other_dict = false
if (!self.event) {
self.event = {match: word}
}
let query = self.event
let q = query.match
let words = q.split(/ |\|/)
if (words.length > 20) {
self.no_results = self.$t('notifications.ignored_words')
self.waiting_for_articles = false
self.replace_history()
return
}
for (let i = 0; i < words.length; i++) {
if (words[i].length > 40) {
self.no_results = self.$t('notifications.ignored_chars')
self.waiting_for_articles = false
self.replace_history()
return
}
})
.catch(function(error){
if (error.response) {
self.error = "Noe gikk galt på serversiden"
} else {
self.error = "Nettverksproblemer, prøv igjen"
}
if (!keep_page) {
self.page = 1
}
let advanced_search = /[?_*%|]/.test(q)
// Get inflections
if (!advanced_search && self.$route.name == 'search') {
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')
.then((response) => {
self.inflection_suggestions = response.data.a.inflect && response.data.a.inflect.filter((item) => item[0][0] != "-" && item[0][item.length-1] != "-")
console.log(self.inflection_suggestions)
}).catch(error =>{
self.handle_error(error, {retry: navigate_to_query, arg: q})
self.replace_history()
})
}
})
.then(function(_){
self.waiting_for_articles = false
history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang, error: self.error}, '')
})
// Get article IDs
let params = {w: query.match, dict: self.lang, scope: self.scope}
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}
self.api.get('api/suggest?', {params})
//axios.get('https://httpstat.us/502')
.then((response) => {
if (self.suggest_exact && self.suggest_exact.length == 0) {
self.similar = response.data.a.similar
}
self.replace_history()
}).catch(error => {
self.handle_error(error, {retry: navigate_to_query, arg: q})
self.replace_history()
})
} else {
self.similar = []
}
}
if (total_length == 0) {
self.waiting_for_articles = false
self.no_results = self.$t('notifications.no_results')
let params = {q, dict, n: 2, dform: 'int', include: 'ef', wc: self.pos_selected}
self.api.get('api/suggest?', {params}).then((response) => {
if (response.data.a.exact && response.data.a.exact[0][0].toUpperCase() == q) {
console.log(response.data.a.exact)
self.suggest_exact = response.data.a.exact || []
self.suggest_fulltext = []
self.similar = []
}
else {
self.suggest_fulltext = response.data.a.freetext || []
self.suggest_exact = []
}
}).catch(error => {
self.handle_error(error, {retry: navigate_to_query, arg: q})
self.replace_history()
})
if (dict != 'bm,nn') {
let params = {q, n: 2, dict: dict=='bm'?'nn':'bm', dform: 'int', include: 'e', wc: self.pos_selected}
self.api.get('api/suggest?', {params}).then((response) => {
self.suggest_other_dict = response.data.cnt > 0 && response.data.a.exact[0][0] == q
}).catch(error => {
self.handle_error(error, {retry: navigate_to_query, arg: q})
self.replace_history()
})
}
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()
})
}
}).catch(error =>{
self.handle_error(error, {retry: navigate_to_query, arg: q})
self.replace_history()
})
}
export default {
name: 'DictionaryView',
data: function() {
return {
api: null,
chosen_api: null,
fallback: false,
article_key: 0,
search_results: [],
lang: 'bob,nob',
search_results: {},
lang: this.$store.state.defaultDict,
waiting_for_articles: true,
waiting_for_metadata: true,
article: null,
error: null,
no_results: false,
monthly_nn: null,
monthly_bm: null,
monthly_nn: null
event: null,
scope: "ei",
pos_selected: "ALL",
article_info: null,
page: 1,
perPage: 10,
inflection_suggestions: null,
similar: null,
selected: null,
suggest_fulltext: [],
suggest_exact: [],
suggest_other_dict: false
}
},
computed: {
beta_search: function(){
const q = this.queryPattern
const advanced_search = this.scope.includes('f') || this.pos_selected || /[?_*%|]/.test(q)
const base = "https://beta.ordbokene.no/" + this.$i18n.locale
if (advanced_search) {
return `${base}/search?q=${q}&dict=${this.lang}&scope=${this.scope}${this.pos ? '&pos=' + this.pos : ''}`
}
else {
return `${base}/${this.lang}?q=${q}`
}
},
queryString: function() {
let q = this.$route.query.q || this.$route.params.q
return q ? q.trim() : ""
},
queryPattern: function() {
if (this.queryString) {
if (/[_%|]/.test(this.queryString)) {
return this.queryString.replaceAll(/[*%]/g, ".*").replaceAll(/[_?]/g, ".")
}
else {
return this.queryString
}
}
},
waiting: function() {
return (this.waiting_for_articles || this.waiting_for_metadata) && this.$route.name != 'root'
},
api_pref: function() {
return api_endpoint + '/' + this.lang + '/article/'
}
},
metaInfo() {
let advanced = /[?_*%|]/.test(this.queryString)
if (this.no_results || advanced || this.scope.includes("f")) {
return {meta: [{name: "robots", content: 'noindex'}]}
}
else if (!this.articleLookup) {
let title
let meta
let link = []
let q = ""
if (this.queryString) {
q = this.queryString + (this.lang == 'bm,nn' ? ' - ' : ' | ')
link = [{rel: "canonical", href: `https://ordbokene.no/${this.lang}/${this.queryString}`} ]
}
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."
if (this.lang == 'bm,nn') {
title = q+'ordbøkene.no'
meta = [{name: "description", vmid: 'description', content: "Bokmålsordboka og Nynorskordboka"+desc}]
}
if (this.lang == 'bm') {
title = q+"Bokmålsordboka"
meta = [{name: "description", vmid: 'description', content: "Bokmålsordboka"+desc}]
}
if (this.lang == 'nn') {
title = q+"Nynorskordboka"
meta = [{name: "description", vmid: 'description', content: "Nynorskordboka"+desc}]
}
return {title,
meta,
link}
}
},
components: {
Article,
Autocomplete,
SearchForm,
SearchResults
},
methods: {
select_result: function(event) {
if(event.articles){
this.$router.push('/' + this.lang + '/w/' + event.word)
this.search_results = event.articles
this.article = null
this.error = null
history.replaceState({article: this.article, search_results: this.search_results, lang: this.lang, error: this.error}, '')
this.$plausible.trackEvent('dropdown selection', {props: {query: event.label, match: event.match}})
}else{
this.waiting_for_articles = true
this.article = null
this.$router.push(`/${this.lang}/search/${event.q}`)
navigate_to_search(this, event.q)
this.$plausible.trackEvent('dropdown selection', {props: {query: event.label, match: '<fritekstsøk>'}})
}
},
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}, '')
}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){
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.bob = concepts
entities.bm = concepts
}),
axios.get(api_endpoint + '/nob').then(function(response){
self.api.get('nn/concepts.json').then(function(response){
let concepts = response.data.concepts
entities.nob = concepts
entities.nn = 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)
if (self.$route.name == 'search') {
if (self.queryString) {
navigate_to_query(self, self.queryString, true)
}
else {
self.$router.push( "/"+self.lang)
self.waiting_for_articles = false
self.replace_history()
self.load_monthly("bm")
self.load_monthly("nn")
}
}
else if(self.$route.name == 'lookup'){
navigate_to_article(self, self.$route.params.id)
else if(self.$route.name == 'word') {
self.scope = 'ei'
self.pos = null
navigate_to_query(self, self.queryString, true)
}
else if (self.$route.name == 'search') {
self.lang = self.$route.params.lang
navigate_to_search(self, self.$route.params.query)
else if(self.$route.name == 'lookup'){
navigate_to_article(self, self.$route.path)
}
else {
self.lang = self.$route.params.lang || 'bob,nob'
self.waiting_for_articles = false
history.replaceState({article: self.article, search_results: self.search_results, lang: self.lang, error: self.error}, '')
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
}
else {
self.fallback = true
self.load_welcome_and_metadata()
}
}
})
},
replace_history: function() {
history.replaceState({article: this.article,
search_results: this.search_results,
article_info: this.article_info,
lang: this.lang,
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_exact: this.suggest_exact,
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
}
},
// words of the month
axios.get(api_endpoint + '/bob/article/5607').then(function(response){
self.monthly_bm = Object.assign(response.data, {dictionary: 'bob'})
load_monthly: function(dict) {
let self = this
this.api.get(dict + '/parameters.json').then(function(response) {
let front_article_id = response.data.front_article.value
self.api.get(dict + `/article/${front_article_id}.json`).then(function(response){
if (dict == "nn") {
self.monthly_nn = Object.assign(response.data, {dictionary: 'nn'})
}
else {
self.monthly_bm = Object.assign(response.data, {dictionary: 'bm'})
}
})
})
axios.get(api_endpoint + '/nob/article/78569').then(function(response){
self.monthly_nn = Object.assign(response.data, {dictionary: 'nob'})
},
handle_error: function(error, retry_params) {
console.log(error)
this.waiting_for_articles = false
this.no_results = false
this.search_results = {}
this.inflection_suggestions = []
this.similar = []
this.suggest_fulltext = []
this.suggest_exact = []
this.suggest_other_dict = false
if (!this.chosen_api) {
this.api = axios.create({baseURL: FALLBACK_ENDPOINT})
}
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}
}
else {
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}
}
else {
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)
}
}
},
inflection_link: function (word) {
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)
},
other_dict: function(word) {
let lang = this.previous.params.lang
let from = this.previous.query.q
let to = word
this.$plausible.trackEvent('other dict', {props: {lang, from, to , words: lang+": "+from + " => " + to}})
this.event = null
navigate_to_query(this, this.queryString)
},
similar_link: function (word) {
let lang = this.previous.params.lang
let from = this.previous.query.q
let to = word
this.$plausible.trackEvent('similar link', {props: {lang, from, to , words: lang+": "+from + " => " + to}})
this.event = null
navigate_to_query(this, this.queryString)
},
fulltext_link: function () {
this.event = null
this.scope = this.scope + "f"
navigate_to_query(this, this.queryString)
},
exact_link: function () {
this.event = null
this.scope = this.scope + "ei"
navigate_to_query(this, this.queryString)
},
language_link: function (lang) {
this.lang = lang
this.event = null
navigate_to_query(this, this.queryString)
},
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 q = this.queryString
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
})
}).catch(function(_){
self.error = "Et nettverksproblem hindret lasting av siden. Prøv å laste siden på nytt"
self.waiting_for_metadata = false
self.waiting_for_articles = false
})
},
generate_path: function(params) {
if (this.$route.name == "word") {
return this.$router.resolve({name: "search", query: {q: this.queryString, ...params}}).href
}
else {
return this.$router.resolve({query: {...this.queryString, ...params}}).href
}
},
generate_lang_path: function(dict) {
return this.$route.fullPath.replace(/\/(bm|nn|bm,nn)\//, "/"+dict+"/")
},
reload_params: function() {
let q = this.queryString
if (q) {
let path = `/${this.lang}/search`
let pos = this.pos_param()
let query = {q}
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)
}
else {
this.$router.push({path: '/'+this.lang})
this.replace_history()
}
},
update_lang_form: function (lang) {
this.lang = lang
this.$store.commit("setDefaultDict", lang)
this.page = 1
},
update_scope: function(scope) {
this.scope = scope
if (this.$route.name && this.$route.name != 'lookup') {
this.page = 1
this.reload_params()
}
},
update_pos: function (pos) {
this.pos_selected = pos
if (this.$route.name && this.$route.name != 'lookup') {
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()
},
article_link_click: function(item) {
let event = window.event
if (!(event.ctrlKey || event.shiftKey || event.metaKey)) {
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 || event.metaKey )) {
this.article = item.article
this.replace_history()
}
},
return_to_results: function() {
this.article = null
this.replace_history()
},
set_fulltext_highlight: function() {
if (this.queryString && this.scope.includes("f")) {
let q = this.queryString
q = q.replace(/\*|%/, "[^\\s]*")
q = q.replace(/_|\?/, "[^\\s]")
this.$store.commit('setFulltextHighlight', q)
}
else {
this.$store.commit('setFulltextHighlight', false)
}
}
},
mounted: function(){
this.chosen_api = this.$route.query.api
if (this.chosen_api) {
this.api = axios.create({baseURL: ENDPOINTS[this.chosen_api]})
}
else {
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)
}
else {
this.perPage = parseInt(this.$store.state.perPage)
}
this.load_welcome_and_metadata()
},
watch: {
$route() {
this.$plausible.trackEvent('language', {props: {code: this.$route.params.lang}})
$route(to, from) {
this.previous = from
if (to.fullPath == "/") {
this.load_monthly("bm")
this.load_monthly("nn")
}
if (to.name == 'lookup' && from.fullPath == '/') {
this.$store.commit('setSearchRoute', null)
}
if (to.name == 'search') {
this.set_fulltext_highlight()
}
}
},
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.error = event.state.error
if (event.state.lang) {
if (self.$refs.SearchForm.$refs.autocomplete.$refs.input == document.activeElement) {
self.$refs.SearchForm.$refs.autocomplete.$refs.input.select()
}
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_exact = event.state.suggest_exact,
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 error")
}
}
}
}
}
</script>
<style>
main {
padding-bottom: 20px;
flex: 1 0 auto;
background-color: var(--v-tertiary-base);
.dict-container {
display: flex;
flex-flow: column;
height: 100%;
}
main.welcome {
background-image: url('../assets/books.jpg');
div.welcome-container.lg, div.welcome-container.md, div.welcome-container.xl {
background-image: url("../assets/background.jpg");
background-repeat: no-repeat;
background-attachment: fixed;
background-position-x: center;
background-size: cover !important;
flex: 1 1 auto;
}
div.welcome.lg, div.welcome.md, div.welcome.xl {
padding-top: 10px;
margin-bottom: auto;
flex: 1 1 auto;
flex-direction: column;
}
div.welcome-container.sm {
background-position-y: -128px;
}
div.welcome-container.md {
background-position-y: -160px;
}
div.welcome-comtainer.lg {
background-position-y: -256px;
}
#search_results, #spinner, #single_article_container, div.welcome, div.search_container, .error {
padding-left: calc((100vw - 1000px) / 2);
padding-right: calc((100vw - 1000px) / 2);
div.welcome-container.xl {
background-position-y: -512px;
}
div.welcome .article {
border-style: none;
}
#spinner {
padding-top: 40px;
margin: auto;
}
.dict-container>div, .dict-container>section, .welcome {
padding-left: calc((100vw - 1200px) / 2);
padding-right: calc((100vw - 1200px) / 2);
}
.dict-container>.welcome-container {
padding: 0 !important;
}
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.bm .article{
width: 50%;
}
#notifications .search_notification {
padding-top: 10px;
padding-bottom: 0px;
margin-left: 10px;
font-size: 18px;
}
#suggestions {
padding-left: 10px;
}
.error p, .no_results p {
margin-left: 15px;
}
.no_results {
padding-top: 24px;
}
.error div{
padding: 10px;
padding-top: 24px;
}
#spinner {
padding-top: 40px;
}
div.monthly {
padding: 20px;
border-radius: 10px;
display: flex;
width: 100%;
}
......@@ -294,24 +1113,27 @@ div.monthly > div {
flex: 50%;
}
div.monthly article.bob .dict-label::before {
content: "månedens ";
}
div.monthly article.nob .dict-label::before {
content: "månadens ";
div.monthly.sm, div.monthly.xs {
flex-direction: column;
}
.search_container {
max-width: 1500px;
padding-top: 50px;
}
.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;
......@@ -326,4 +1148,130 @@ li.suggestion {
color: white;
}
#return_to_results {
padding-left: 10px;
padding-top: 10px;
padding-bottom: 10px;
display: table-cell;
}
#return_to_results a {
color: var(--v-text-base) !important;
text-decoration: none;
}
.nav_arrow {
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%;
}
.monthly-title {
text-align: center;
font-size: 1.17em;
color: var(--v-primary-base);
margin-bottom: 10px;
margin-top: 10px;
}
.monthly-title h2 {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
text-align: center;
}
.xl .monthly-title h2, .lg .monthly-title h2, .md .monthly-title h2 {
padding-right: 10px;
padding-left: 10px;
}
.xl .monthly-title, .lg .monthly-title, .md .monthly-title {
background: linear-gradient(90deg, rgba(255,255,255,0) 10%, rgba(255, 255, 255, 0.75) 50%, rgba(255,255,255,0) 90%);
margin: 24px;
}
.monthly-title h2:after {
margin: 0 0 0 20px;
}
.xl .monthly-title, .lg .monthly-title, .md .monthly-title {
margin-top: 24px;
margin-bottom: 24px;
}
.xs .monthly-title h2:before,
.sm .monthly-title h2:before,
.xs .monthly-title h2:after,
.sm .monthly-title h2:after {
content: '';
margin: 0 24px 0 24px;
border-top: 2px solid;
flex: 1 0 0px;
color: var(--v-secondary-base);
}
.similar-label {
color: var(--v-primary-base) ;
font-weight: bold;
position: absolute;
padding-left: 10px;
top: 0;
font-variant-caps: all-small-caps;
font-size: 1.17em;
}
.chosen_api {
position: absolute;
top: 0px;
left: 0px;
color: white;
width: 100px;
}
.v-text-field--rounded > .v-input__control > .v-input__slot {
padding-left: 12px !important;
}
.betalink>div {
border-radius: 16px;
margin-top: 10px;
padding-left: 32px;
padding-right: 32px;
padding-top: 10px;
padding-bottom: 24px;
color: white;
background: black;
}
.betalink h2 {
font-variant: all-small-caps;
font-size: 2rem;
margin-bottom: 6px;
}
.betalink a {
color: white !important;
font-size: 1.25rem;
}
</style>
<template>
<li class="example">
<DefElement tag="q" :body="body.quote" @article-click="article_link_click" :dictionary="dictionary" /><span v-if="body.explanation && body.explanation.content.length"> - </span>
<DefElement tag="q" :body="body.quote" @article-click="article_link_click" :dictionary="dictionary" /><span v-if="body.explanation && body.explanation.content.length"> </span>
<DefElement tag="span" :body="body.explanation" v-if="body.explanation && body.explanation.content.length" @article-click="article_link_click" :dictionary="dictionary" />
</li>
</template>
......
<template>
<span>
<v-btn v-if="$route.name!='about'" to="/om" small dark text>
<v-icon small left>info</v-icon>{{$t('menu.about')}}
</v-btn>
<v-btn v-if="$route.name=='about'" to="/" small dark text>
<v-icon small left>home</v-icon>{{$t('home')}}
</v-btn>
<v-dialog :fullscreen="$vuetify.breakpoint.smAndDown" max-width="800px" v-model="help_dialog">
<template v-slot:activator="{ on, attrs }">
<v-btn small dark text v-on="on" v-bind="attrs">
<v-icon small left>help</v-icon>{{$t('menu.help')}}
</v-btn>
</template>
<v-card>
<v-toolbar elevation="0" dark color="primary">
<v-toolbar-title>{{$t('menu.help')}}</v-toolbar-title><v-spacer></v-spacer>
<v-toolbar-items><v-btn @click.native="help_dialog=false" text>{{$t('close')}}<v-icon right>close</v-icon></v-btn></v-toolbar-items>
</v-toolbar>
<v-card-text class="text--primary">
<br>
<Help/>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog :fullscreen="$vuetify.breakpoint.smAndDown" max-width="800px" v-model="settings_dialog">
<template v-slot:activator="{ on, attrs }">
<v-btn small dark text v-on="on" v-bind="attrs">
<v-icon small left>settings</v-icon><span>{{$t('settings.title')}}</span>
</v-btn>
</template>
<v-card>
<v-toolbar elevation="0" dark color="primary">
<v-toolbar-title><span role="heading" aria-level="1">{{$t('settings.title')}}</span></v-toolbar-title><v-spacer></v-spacer>
<v-toolbar-items><v-btn @click.native="settings_dialog=false" text>{{$t('close')}}<v-icon right>close</v-icon></v-btn></v-toolbar-items>
</v-toolbar>
<Settings/>
</v-card>
</v-dialog>
<v-dialog :fullscreen="$vuetify.breakpoint.smAndDown" max-width="800px" v-model="contact_dialog">
<template v-slot:activator="{ on, attrs }">
<v-btn small dark text v-on="on" v-bind="attrs">
<v-icon small left>mail</v-icon><span>{{$t('contact.title')}}</span>
</v-btn>
</template>
<v-card>
<v-toolbar elevation="0" dark color="primary">
<v-toolbar-title><span role="heading" aria-level="1">{{$t('contact.title')}}</span></v-toolbar-title><v-spacer></v-spacer>
<v-toolbar-items><v-btn @click.native="contact_dialog=false" text>{{$t('close')}}<v-icon right>close</v-icon></v-btn></v-toolbar-items>
</v-toolbar>
<Contact @close="contact_dialog = false"/>
</v-card>
</v-dialog>
</span>
</div>
</template>
<script>
import Help from './Help.vue'
import Contact from './Contact.vue'
import Settings from './Settings.vue'
export default {
name: "FooterMenu",
components: {
Help,
Contact,
Settings
},
data: function() {
return {
contact_dialog: false,
help_dialog: false,
settings_dialog: false
}
}
}
</script>
\ No newline at end of file
<!-- eslint-disable -->
<template>
<div>
<h1 class="article_header">{{header_text}}</h1>
{{group_list}}
<details :title="inflect_tooltip" @toggle="toggle()" v-if="group_list">
<summary>Bøying</summary>
<div class="inflection">
<div v-for="(lemma, index) in inflection_groups_by_lemma" :key="index">
<h4>{{lemma.lemma}}</h4>
<component v-for="grp in Object.keys(lemma.inflection_groups)"
:key="grp.replace('/', '_')"
:is="grp.replace('/', '_')"
:dictionary="dictionary"
:standardisations="lemma.inflection_groups[grp]"></component>
</div>
</div>
</details>
<span class="dict-label">{{dict_label}}</span>
<div class="header">
<router-link v-if="$route.name != 'lookup'"
:id="title_id"
:to="$parent.link_to_self.ref"
@click.native="heading_click"
v-bind:class="{'long_lemma': long_lemma}"
class="article_header">
<HeaderTitle :lemma_groups="lemma_groups" :secondary_header_text="secondary_header_text"/>
</router-link>
<span v-else>
<HeaderTitle :lemma_groups="lemma_groups" :secondary_header_text="secondary_header_text"/>
</span>
<InflectionButton :lemmas="$parent.article.lemmas" :dictionary="dictionary" :article_id="$parent.article.article_id"/>
<SplitInf v-if="!$parent.collapsed" :lemmas="lemmas"/>
<p v-if="!lemma_groups[1] && $parent.collapsed && $parent.snippet && $parent.has_content" v-bind:class="{'under_inflection_button': !$store.state.inflectionExpanded}">
{{$parent.snippet}}
</p>
<span v-if="$parent.collapsable" >
<v-btn class="expand_icon" v-if="$parent.collapsed"
text
small
right
@click="$emit('toggle-collapse')">
{{$t('article.show')}}
<v-icon right>expand_more</v-icon></v-btn>
<v-btn class="expand_icon" v-else
text
small
@click="$emit('toggle-collapse')">
<v-icon small>expand_less</v-icon></v-btn>
</span>
</div>
</template>
<script>
/* eslint-disable */
import helpers from '../utils/helpers.js'
import ADJ from './inflection/Adjective.vue'
import ADJ_adv from './inflection/AdjectiveAdv.vue'
import ADJ_masc_fem from './inflection/AdjectiveMF.vue'
import ADJ_masc_fem_fem from './inflection/AdjectiveMF_F.vue'
import DET from './inflection/Determinative.vue'
import DET_adj from './inflection/DeterminativeAdj.vue'
import PRON from './inflection/Pronoun.vue'
import NOUN from './inflection/Noun.vue'
import NOUN_reg_fem from './inflection/NounRegFem.vue'
import VERB from './inflection/Verb.vue'
import VERB_sPass from './inflection/VerbSPass.vue'
import SplitInf from './SplitInf.vue'
import InflectionButton from './InflectionButton.vue'
import HeaderTitle from './HeaderTitle.vue'
export default {
name: 'Header',
props: {
lemmas: Array,
dictionary: String,
article_id: Number
article_id: Number,
title_id: String
},
computed: {
header_text: function() {
return this.lemmas.map(lemma => lemma.lemma).join(', ')
},
inflect_tooltip: function() {
return this.dictionary == 'bob' ? 'Klikk for å se bøyinger' : 'Klikk for å sjå bøyingar'
},
dict_label: function() {
return {
'bob': 'BOKMÅL',
'nob': 'NYNORSK'
}[this.dictionary] || ''
},
group_list: function() {
return helpers.group_list(this.lemmas, this.dictionary)
},
inflection_groups_by_lemma: function() {
let components = Object.keys(this.$options.components)
return this.lemmas.map(
function(lemma_){
let inflection_groups = lemma_.paradigm_info.reduce((acc, std) => Object.assign(acc, {[std.inflection_group]: []}), {})
lemma_.paradigm_info.forEach(std => inflection_groups[std.inflection_group].push(std))
return {
lemma: lemma_.lemma,
inflection_groups: Object.fromEntries(Object.entries(inflection_groups).filter(e => components.includes(e[0].replace('/', '_'))))
}
}
)
data: function() {
return {
long_lemma: false
}
},
components: {
ADJ,
ADJ_adv,
ADJ_masc_fem,
ADJ_masc_fem_fem,
DET,
DET_adj,
PRON,
NOUN,
NOUN_reg_fem,
VERB,
VERB_sPass
components: {
SplitInf,
InflectionButton,
HeaderTitle
},
data: function() {
return {
inflect_reported: false
mounted: function() {
if (this.$el.scrollWidth > this.$el.offsetWidth) {
this.long_lemma = true
}
},
methods: {
toggle: function() {
if (! this.inflect_reported) {
this.$plausible.trackEvent('open inflection', {props: {article: `/${this.dictionary}/${this.article_id}/${this.lemmas[0].lemma}`}})
heading_click: function() {
this.$plausible.trackEvent('article head click', {props: {article: `${this.dictionary} ${this.article_id}`}})
this.$parent.details_click(this.$parent.link_to_self)
},
inflection_classes: function(lemmas) {
let inf_classes = new Set()
let ureg = false
lemmas.forEach((lemma, i) => {
if (lemma.inflection_class) inf_classes.add(lemma.inflection_class)
else ureg = true
})
if (inf_classes.size){
let class_array = Array.from(inf_classes).sort()
if (ureg) class_array.push("ureg.")
let class_list
if (class_array.length < 3) {
class_list = class_array.join(" og ")
}
else {
class_list = class_array.slice(0, -1).join(", ") + " og " + class_array[class_array.length -1]
}
return " ("+ class_list +")"
}
this.inflect_reported = true
}
},
computed: {
content_locale: function() {
return this.$parent.content_locale
},
lang_tag_locale: function() {
return this.$parent.lang_tag_locale
},
secondary_header_text: function() {
let a_forms = []
this.lemmas.forEach((lemma, i) => {
if (lemma.paradigm_info[0] && lemma.paradigm_info[0].inflection[1] && lemma.paradigm_info[0].inflection[1].tags[0] == 'Inf') {
let inf2 = lemma.paradigm_info[0].inflection[1].word_form
if (inf2 && inf2.length) {
a_forms.push(inf2)
}
}
});
return a_forms.join(', ')
},
hgno_arabic: function() {
let hgnos = []
this.lemmas.forEach(lemma => {
let hgint = parseInt(lemma.hgno)
if (hgint > 0) {
hgnos.push(hgint)
}
})
return hgnos
},
lemma_groups: function() {
let groups = [{lemmas: this.lemmas}]
try {
if (this.lemmas[0].paradigm_info[0].tags[0] == "DET" && this.lemmas[0].paradigm_info[0].tags.length > 1) {
groups = [{description: this.$t('tags.'+this.lemmas[0].paradigm_info[0].tags[0], this.content_locale), pos_group: ["Quant", "Dem", "Poss"].includes(this.lemmas[0].paradigm_info[0].tags[1]) ? this.$t('determiner.' + this.lemmas[0].paradigm_info[0].tags[1], this.content_locale) : '', lemmas: this.lemmas}]
}
else if (this.lemmas[0].paradigm_info[0].tags[0] == 'NOUN') {
let genus_map = {}
let self = this
this.lemmas.forEach(lemma =>{
let genera = new Set()
lemma.paradigm_info.forEach(paradigm => {
if (paradigm.tags[1]) {
genera.add(paradigm.tags[1])
}
})
let genus_description = ""
if (genera.size == 3) {
genus_description += self.$t('tags.Masc') + ', ' + self.$t('tags.Fem', self.content_locale) + self.$t('or') + self.$t('tags.Neuter', self.content_locale)
} else {
genus_description += Array.from(genera).map(code => self.$t('tags.'+code, self.content_locale)).sort().join(self.$t('or'))
}
if (genus_map[genus_description]) {
genus_map[genus_description].push(lemma)
}
else {
genus_map[genus_description] = [lemma]
}
})
groups = Object.keys(genus_map).map(key => {
return {description: self.$t('tags.NOUN', self.content_locale), pos_group: key, lemmas: genus_map[key], }
})
}
else if (this.lemmas[0].paradigm_info[0].tags[0] != 'EXPR') {
groups = [{description: this.$t('tags.'+this.lemmas[0].paradigm_info[0].tags[0], this.content_locale), lemmas: this.lemmas}]
}
groups.forEach((lemma_group, index) => {
groups[index]['inflection_classes'] = this.inflection_classes(lemma_group.lemmas)
})
} catch(error) {
console.log("lemma_groups",this.article_id, this.dictionary, '"'+error.message+'"')
this.$parent.invalid = true
//console.error(error)
}
return groups
},
}
}
......@@ -104,91 +181,108 @@ export default {
<style>
article (table, th, td) {
border: solid 1px;
.article h3 {
padding-top: 4px !important;
padding-bottom: 0px !important;
font-family: Inria Serif;
}
summary {
width: 30em;
.article .sm h3, .article .xs h3, .long_lemma h3 {
font-size: 1.3em;
}
article h1 {
padding-top: 7px;
.sm .long_lemma, .xs .long_lemma h3 {
font-size: 1em;
}
.word-classification {
text-decoration: underline dashed;
.article h3 {
font-size: 1.5em;
}
.dict-label {
color: var(--v-primary-base) ;
font-weigth: bold;
padding-left: 5px;
position: absolute;
top: 0px;
font-variant-caps: all-small-caps;
h3 a {
text-decoration: none !important;
color: var(--v-primary-base) !important;
}
details {
margin-top: 10px;
.article h1.secondary_header {
padding-top: 0px;
padding-bottom: 4px;
}
details > summary {
position: relative;
max-width: 80px;
list-style: none;
border: solid 2px var(--v-primary-base);
border-radius: 4px;
padding: 3px;
color: var(--v-primary-base);
cursor: pointer;
.info-button {
margin-left: 6px;
margin-right: -2px;
height: 10px;
width: 10px;
}
details > summary:after {
content: "⌄";
font-weight: bold;
position: absolute;
right: 0;
top: 1px;
margin-right: 3px;
.word-classification {
text-decoration: underline dashed;
}
@keyframes open {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
div.lemma {
display: none;
}
.article_header {
overflow:auto;
word-wrap: normal;
text-decoration: none;
}
details[open] summary ~ * {
animation: open 0.3s ease-in-out;
.hgno {
color: var(--v-text-base) !important;
font-family: unset;
font-size: 16px;
font-weight: normal;
}
.inflection table {
border-collapse: collapse;
margin: 10px;
.expand_icon {
justify-content: center;
position: absolute;
bottom: 0px;
left: 0px;
padding-right: 24px !important;
width: 100%;
border-bottom-right-radius: 28px;
border-bottom-left-radius: 28px;
}
.inflection td, .inflection th {
border: solid;
border-width: 1px;
margin: 0px 0px 0px 0px;
padding: 3px;
.subheader {
color: var(--v-text-base);
font-weight: normal;
font-style: italic;
font-size: 14px;
padding-right: 10px;
}
.inflection_classes {
font-style: normal;
}
details > summary::-webkit-details-marker {
display: none;
.header_group_list {
font-variant: all-small-caps;
font-style: normal;
font-size: 18px;
}
details > div {
border-radius: 0 0 10px 10px;
background-color: #eee;
padding: 2px 6px;
margin: 0;
box-shadow: 3px 3px 4px black;
p.under_inflection_button {
padding-top:10px !important;
}
</style>
<template>
<span>
<span v-bind:class="{ 'lookup': $route.name=='lookup'}" v-for="(lemma_group, i) in lemma_groups" :key="i">
<h3>
<!--
--><span v-for="(lemma, index) in lemma_group.lemmas"
:key="index"><DefElement v-if="lemma.annotated_lemma" :body="lemma.annotated_lemma" tag="span"/><span v-else>{{lemma.lemma}}</span><!--
--><span v-if="lemma.hgno"
:aria-label="$t('accessibility.homograph') + parseInt(lemma.hgno)"
:title="$t('accessibility.homograph')+parseInt(lemma.hgno)"
class="hgno">{{" "+roman_hgno(lemma)}}</span><!--
--><span
class="title_comma"
v-if="lemma_group.lemmas[1] && index < lemma_group.lemmas.length-1">{{", "}}
</span>
</span>
</h3>
<h3 v-if="secondary_header_text">{{secondary_header_text}}</h3>
<span :lang="$parent.$parent.lang_tag_locale" v-if="lemma_group.description" class="subheader">
<span class="header_group_list">{{lemma_group.description}}</span>
{{lemma_group.pos_group}}
<span v-if="$store.state.showInflectionNo" class="inflection_classes">{{lemma_group.inflection_classes}}</span>
</span>
</span>
</span>
</template>
<script>
import helpers from '../utils/helpers.js'
import DefElement from './DefElement.vue'
export default {
name: 'HeaderTitle',
components: {
DefElement
},
props: {
lemma_groups: Array,
secondary_header_text: String
},
methods: {
roman_hgno: helpers.roman_hgno,
}
}
</script>
<template id="">
<div class='help' :class="$vuetify.breakpoint.name">
<div v-if="$store.state.currentLocale == 'nno'">
<p>Du søkjer ved å skrive inn ordet og trykkje på returtasten (<kbd>enter</kbd>) eller velje eit søkjeforslag i nedtrekksmenyen. Tast <kbd>Shift</kbd>+<kbd>7</kbd> dersom du vil hoppe tilbake til søkjefeltet etter å ha søkt.
Overskrifta i toppen av kvart søkjetreff er ei lenkje til ei ny artikkelside for det aktuelle ordet. Om du ikkje får meir enn eitt treff i kvar ordbok, viser heile artikkelen i resultatlista, og du treng ikkje trykkje på lenkja. Er det fleire treff, kan du òg sjå artikkelinnhaldet i resultatlista ved å trykkje på "Vis artikkel" i botnen av søkjetreffet. Sjå innstillingane nedanfor om du ønskjer at resultatlista alltid skal vise fullstendige artiklar.
</p><p>
Kva ordbok du får treff i, avheng av kva ordbok du har valt å søkje i, og om ordet blir brukt i begge skriftspråka. Det ligg per i dag ikkje inne koplingar mellom ordbøkene, men vi har utvikla ein meinte-du-funksjon, som vi håpar blir til hjelp for å finne dei riktige orda både i bokmål og nynorsk.</p>
<p>Du kan søkje direkte på faste uttrykk, t.d. <em>gi katten i</em>. Alternativt finn du uttrykka samla til slutt i artikkelen eller artiklane dei er knytte til. Når du skriv inn eit enkelt ord i søkjefeltet, viser nedtrekksmenyen òg dei faste uttrykka som ordet er ein del av.</p>
<h2>Søk med jokerteikn</h2>
<p>Dersom du er usikker på skrivemåten av eit ord eller ønskjer treff i meir
enn éin artikkel, kan du bruke <kbd>%</kbd> og <kbd>*</kbd> for å få treff på null, eitt eller fleire teikn.
Symbolet <kbd>_</kbd> erstattar berre eitt teikn. Du kan plassere jokerteikna fleire stader i søkjeordet.</p>
Døme:
<p>Søkjer du på «<a href="https://ordbokene.no/bm,nn/search?q=arbeid%2ar&scope=ei">arbeid*r</a>», er dei første treffa i nedtrekksmenyen «arbeider» i <em>Bokmålsordboka</em> og «arbeidar» i <em>Nynorskordboka</em>.</p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=inter%25es%25ant&scope=ei">inter%es%ant</a>» gjev treff på «interessant» i begge ordbøkene. </p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=f%C3%B8rsk%25l%C3%A6r_r&scope=ei">førsk%lær_r</a>» gjev treff på «førskolelærer» i <em>Bokmålsordboka</em> og «førskolelærar/førskulelærar» i <em>Nynorskordboka</em>.</p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=ku__&scope=e">ku__</a>» gjev treff på ord på fire bokstavar som startar på «ku».</p>
<h2>Kombiner søkjeord</h2>
<p>Symbolet <kbd>|</kbd> gjer det mogleg å søkje med fleire søkjeord samtidig. Det er òg mogleg å kombinere søkjeord som inneheld jokerteikn.</p>
Døme:
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=kj%C3%A6rlighet%7Ckj%C3%A6rleik&scope=ei">kjærlighet|kjærleik</a>» gjev treff på «kjærlighet» i <em>Bokmålsordboka</em> og «kjærleik» i <em>Nynorskordboka</em>.</p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=undervis_r%7Cl%C3%A6r_r&scope=ei">undervis_r|lær_r</a>» gjev treff på «underviser» og «lærer» i <em>Bokmålsordboka</em> og «undervisar» og «lærar» i <em>Nynorskordboka</em>.</p>
</div>
<div v-if="$store.state.currentLocale == 'nob'">
<p>Du søker ved å skrive inn ordet og trykke på returtasten (<kbd>enter</kbd>) eller velge et søkeforslag i nedtrekksmenyen. Tast <kbd>Shift</kbd>+<kbd>7</kbd> hvis du vil hoppe tilbake til søkefeltet etter å ha søkt.
Overskriften i toppen av hvert søketreff er en lenke til en ny artikkelside for det aktuelle ordet. Hvis du ikke får mer enn ett treff i hver ordbok, vises hele artikkelen i resultatlisten, og du trenger ikke trykke på lenken. Er det flere treff, kan du også vise artikkelinnholdet i resultatlisten ved å trykke på "Vis artikkel" i bunnen av søketreffet. Se innstillingene nedenfor hvis du ønsker at resultatlisten alltid skal vise fullstendige artikler.
</p><p>
Hvilken ordbok du får treff i, avhenger av hvilken ordbok du har valgt å søke i, og om ordet blir brukt i begge skriftspråkene. Det ligger per i dag ikke inne koblinger mellom ordbøkene, men vi har utviklet en mente-du-funksjon, som vi håper blir til hjelp for å finne de riktige ordene både i bokmål og nynorsk.
</p><p>Du kan søke direkte på faste uttrykk, f.eks. <em>gi katten i</em>. Alternativt finner du uttrykkene samlet til slutt i artikkelen eller artiklene de er knyttet til. Når du skriver inn et enkelt ord i søkefeltet, viser nedtrekksmenyen også de faste uttrykkene som ordet inngår i.</p>
<h2>Søk med jokertegn</h2>
<p>Dersom du er usikker på skrivemåten av et ord eller ønsker treff i mer enn én artikkel, kan du bruke <kbd>%</kbd> og <kbd>*</kbd> for å få treff på null, ett eller flere tegn.
Symbolet <kbd>_</kbd> erstatter kun ett tegn. Du kan plassere jokertegnene flere steder i søkeordet.</p>
Eksempler:
<p>Søker du på «<a href="https://ordbokene.no/bm,nn/search?q=arbeid%2ar&scope=ei">arbeid*r</a>», er de første treffene i nedtrekksmenyen «arbeider» i <em>Bokmålsordboka</em> og «arbeidar» i <em>Nynorskordboka</em>.</p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=inter%25es%25ant&scope=ei">inter%es%ant</a>» gir treff på «interessant» i begge ordbøkene. </p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=f%C3%B8rsk%25l%C3%A6r_r&scope=ei">førsk%lær_r</a>» gir treff på «førskolelærer» i <em>Bokmålsordboka</em> og «førskolelærar/førskulelærar» i <em>Nynorskordboka</em>.</p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=ku__&scope=e">ku__</a>» gir treff på ord på fire bokstaver som begynner med «ku».</p>
<h2>Kombiner søkeord</h2>
<p>Symbolet <kbd>|</kbd> gjør det mulig å søke med flere søkeord samtidig. Det er også mulig å kombinere søkeord som inneholder jokertegn.</p>
Eksempler:
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=kj%C3%A6rlighet%7Ckj%C3%A6rleik&scope=ei">kjærlighet|kjærleik</a>» gir treff på «kjærlighet» i <em>Bokmålsordboka</em> og «kjærleik» i <em>Nynorskordboka</em>.</p>
<p>Søket «<a href="https://ordbokene.no/bm,nn/search?q=undervis_r%7Cl%C3%A6r_r&scope=ei">undervis_r|lær_r</a>» gir treff på «underviser» og «lærer» i <em>Bokmålsordboka</em> og «undervisar» og «lærar» i <em>Nynorskordboka</em>.</p>
</div>
<div v-if="$store.state.currentLocale == 'eng'">
<p>Search the website by entering a word, press <kbd>return</kbd> or pick a search suggestion in the drop-down menu. Press <kbd>/</kbd> whenever you want to jump back to the search box.
The heading of each search result is a link to a lookup page for the article in question. If you get no more than one result in each dictionary, the entire article is shown in the list of results, and you won't need to click the link. If there are more resutls, you may expand the article content in the results list by clicking "Show article" at the bottom of the result item. You can make the articles expand by default in the settings below.
</p><p>
The dictionary from which to get search results depends on the dictionary selected, and if the word is used in both written languages. Currently, the two dictionaries are not linked at word level. Thus a ‘Did you mean’ function has been developed. The hope is that this function will be helpful in finding the right words in both Bokmål and Nynorsk. It is possible to search for fixed expressions, e.g.
<em>gi katten i</em> (not give a damn). It is also possible to find such expressions at the end of each article or the articles they are linked to. When typing a single word in the search field, the drop-down menu also shows the common expressions the word is part of.
</p>
<h2>Wildcard search</h2>
<p>If unsure of the spelling of a word or want results from more than one article, it is possible to use <kbd>%</kbd> and <kbd>*</kbd> to replace zero, one or more characters.
Underscore (<kbd>_</kbd>) replaces only one character. You may use the wilcard symbols multiple times in your search expression.</p>
Examples:
<p>The search query “<a href="https://ordbokene.no/bm,nn/search?q=arbeid%2ar&scope=ei">arbeid*r</a>”, will result in results in the drop-down menu with “arbeider” (worker) in the Bokmål dictionary and “arbeidar” (worker) in the Nynorsk Dictionary.</p>
<p>The search query “<a href="https://ordbokene.no/bm,nn/search?q=inter%25es%25ant&scope=ei">inter%es%ant</a>” returns the result “interessant” (interesting) in both dictionaries. </p>
<p>The search query “<a href="https://ordbokene.no/bm,nn/search?q=f%C3%B8rsk%25l%C3%A6r_r&scope=ei">førsk%lær_r</a>” gives resutls for “førskolelærer” (pre-school teacher) in the Bokmål dictionary and “førskolelærar/førskulelærar” (pre-school teacher) in the Nynorsk dictionary.</p>
<p>The search query «<a href="https://ordbokene.no/bm,nn/search?q=ku__&scope=e">ku__</a>» returns four letter words starting with «ku».</p>
<h2>Combine search terms</h2>
<p>The symbol <kbd>|</kbd> allows you to use in several search words at the same time. You can also combine search words that contain wildcard symbols.</p>
Examples:
<p>The search “<a href="https://ordbokene.no/bm,nn/search?q=kj%C3%A6rlighet%7Ckj%C3%A6rleik&scope=ei">kjærlighet|kjærleik</a>” yields the result “kjærlighet” in the Bokmål dictionary and “kjærleik” (love) in the Nynorsk dictionary.</p>
<p>The search “<a href="https://ordbokene.no/bm,nn/search?q=undervis_r%7Cl%C3%A6r_r&scope=ei">undervis_r|lær_r</a>” results in “underviser” (teacher) and “lærer” (teacher) in the Bokmål dictionary and “undervisar” and “lærar” in the Nynorsk dictionary.</p>
</div>
<div v-if="$store.state.currentLocale == 'ukr'">
<p>Для пошуку введіть слово і натисніть кнопку <kbd>enter</kbd> або виберіть варіант пошуку у спадному меню. Натисніть <kbd>Shift</kbd>+<kbd>7</kbd>, якщо ви хочете повернутися до поля пошуку після виконання пошуку.
Заголовок у верхній частині кожного результату пошуку - це посилання на сторінку нової статті про це слово. Якщо ви не знайдете більше одного збігу в кожному словнику, вся стаття з’явиться у списку результатів, і вам не потрібно переходити за посиланням. Якщо результатів декілька, ви також можете переглянути вміст статті у списку результатів, натиснувши "показати статтю" внизу результату пошуку. Дивіться налаштування нижче, якщо ви хочете, щоб у списку результатів завжди випадали повні статті.
</p>
<p>
У якому словнику ви отримаєте збіги, залежить від того, який словник ви вибрали для пошуку,
і чи використовується це слово в обох письмових мовах. Наразі між словниками немає посилань,
але ми розробили для вас функцію, яка, сподіваємось, допоможе знайти потрібні слова як у <em>букмолі</em>,
так і в <em>нюношку</em>.
</p>
<p>
Ви можете здійснювати пошук безпосередньо за фіксованими виразами,
напр., <em>gi katten i</em> (не заморочуватись нічим або ніким).
Крім того, ви знайдете вирази разом у статті чи статтях, з якими вони пов’язані.
Коли ви вводите в поле пошуку одне слово, у спадному меню також показуються фіксовані вирази, до
яких входить це слово.
</p>
<h2>Пошук за допомогою символів підстановки</h2>
<p>Якщо ви не впевнені в написанні слова або хочете знайти збіги в більш ніж одній статті, ви можете використовувати <kbd>%</kbd> і <kbd>*</kbd> щоб отримати збіги від нуля, одного або декількох символів.
Символ <kbd>_</kbd> замінює лише один символ. Ви можете розмістити символи підстановки в декількох місцях ключового слова.</p>
Приклади:
<p>Пошук за запитом “<a href="https://ordbokene.no/bm,nn/search?q=arbeid%2ar&scope=ei">arbeid*r</a>” покаже у спадному меню результатів слова “arbeider” (працівник) у <em>Словнику букмола</em> і “arbeidar” (працівник) у <em>Словнику нюношка</em>.</p>
<p>Пошук “<a href="https://ordbokene.no/bm,nn/search?q=f%C3%B8rsk%25l%C3%A6r_r&scope=ei">førsk%lær_r</a>” надає результати за словом “førskolelærer” (вчитель дошкільної освіти) у <em>Словнику букмола</em> та “førskolelærar/førskulelærar” (вчитель дошкільної освіти) у <em>Словнику нюношка</em>.</p>
<p>Пошук “<a href="https://ordbokene.no/bm,nn/search?q=ku__&scope=e">ku__</a>” забезпечує відповідність словами з чотирьох букв, що починаються на “ku”.</p>
<h2>Поєднайте ключові слова</h2>
<p>Символ <kbd>|</kbd> дозволяє здійснювати пошук за кількома ключовими словами одночасно. Також можливо комбінувати ключові слова, що містять символи підстановки.</p>
Приклади:
<p>Пошук “<a href="https://ordbokene.no/bm,nn/search?q=kj%C3%A6rlighet%7Ckj%C3%A6rleik&scope=ei">kjærlighet|kjærleik</a>” надає результати за словом “kjærlighet” (любов) у <em>Словнику букмола</em> і “kjærleik” (любов) у <em>Словнику нюношка</em>.</p>
<p>Пошук “<a href="https://ordbokene.no/bm,nn/search?q=undervis_r%7Cl%C3%A6r_r&scope=ei">undervis_r|lær_r</a>” надає результати за словом “underviser” (навчання) та “lærer” (вчитель) у <em>Словнику букмола</em> і “undervisar” (навчання) та “lærar” (вчитель) у <em>Словнику нюношка</em>.</p>
</div>
</div>
</template>
<style scoped>
ul.bullet > li {
list-style: disc;
margin-left: 20px;
}
a {
text-decoration: none;
}
blockquote {
margin-left:50px;
}
kbd {
border-radius: 5px !important;
background-color: var(--v-tertiary-darken1) !important;
color: var(--v-text-base) !important;
}
</style>
<!-- eslint-disable -->
<template>
<span :is="$vuetify.breakpoint.smAndDown ? 'div' : 'span'" class="inflection-wrapper" v-if="inflected">
<v-btn :lang="$parent.$parent.lang_tag_locale" :aria-controls="'inflection-canvas'+$parent.title_id" :aria-expanded="inflection_expanded ? 'true' : 'false'" v-if="!($store.state.inflectionExpanded && $route.name)" class="show-inflection" small depressed rounded @click.native="toggle" :class="$vuetify.breakpoint.name">
<span :lang="$parent.lang_tag_locale">{{inflection_expanded? $t('article.hide_inflection', $parent.content_locale):$t('article.show_inflection', $parent.content_locale)}}</span><span class = "inflection-arrow"><v-icon small right>{{this.inflection_expanded? "remove" : "add"}}</v-icon></span>
</v-btn>
<div :id="'inflection-canvas'+$parent.title_id" class="inflection-canvas">
<inflectionTable v-if="inflection_expanded || always_expand" :eng="$store.state.currentLocale == 'eng'" :ukr="$store.state.currentLocale == 'ukr'" :lemmaList="lemmas_with_word_class_and_lang" :mq="$vuetify.breakpoint.name" :context="$store.state.inflectionTableContext" :key="$store.state.currentLocale + $store.state.inflectionTableContext"/>
</div>
</span>
</template>
<script>
/* eslint-disable */
import inflectionTable from 'inflection-table'
export default {
name: 'InflectionButton',
props: {
lemmas: Array,
dictionary: String,
article_id: Number
},
computed: {
lemmas_with_word_class_and_lang: function() {
return this.lemmas.map(lemma => Object.assign({language: this.dictionary == 'bm' ? 'nob' : 'nno',
word_class: lemma.paradigm_info[0].inflection_group.split('_')[0]}, lemma))
},
inflected: function() {
return this.lemmas.reduce((acc, lemma) => acc + lemma.paradigm_info.reduce((acc2, digm) => digm.inflection_group.includes("uninfl") ? acc2 : acc2 + digm.inflection.length, 0), 0) > this.lemmas.length
},
always_expand: function() {
return !this.$parent.$parent.collapsed && this.$store.state.inflectionExpanded && this.$route.name
}
},
components: {
inflectionTable
},
data: function() {
return {
inflection_expanded: false
}
},
methods: {
toggle: function() {
if (!this.inflection_expanded) {
this.$plausible.trackEvent('inflection click', {props: {article: `${this.dictionary} ${this.article_id}`}})
}
this.inflection_expanded = !this.inflection_expanded
}
},
}
</script>
<style>
.show-inflection .v-icon {
color: var(--v-primary-base) !important;
margin-left: 4px !important;
}
@keyframes open {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
div.show-inflection {
padding-top: 10px;
}
.inflection-canvas {
overflow-x: auto;
/*position: absolute;*/
z-index: 5;
background-color: rgba(255, 255, 255, 0);
/*max-width: 100vw;*/
}
.inflection-canvas > div {
padding-top: 10px;
padding-bottom: 10px;
}
.inflection-wrapper {
color: var(--v-text-base);
font-size: 14px;
margin-top: 10px;
}
.context {
color: #545454 !important;
}
div.lemma {
display: none;
}
table {
border-collapse: collapse;
margin-top: 5px;
}
th, td {
border: solid 2px var(--v-button-darken1);
padding: 5px;
}
th {
background-color: var(--v-button-base);
}
.infl-label {
text-align: center;
vertical-align: top;
}
td.hilite {
background-color: var(--v-tertiary-base);
text-align: center
}
</style>
<template>
<v-card>
<v-list>
<v-list-group prepend-icon="help">
<template v-slot:activator>
<v-list-item-title>{{$t('menu.help')}}</v-list-item-title>
</template>
<v-list-item><Help/></v-list-item>
</v-list-group>
<v-list-item link to="/om" v-on:click="$emit('close')">
<v-list-item-icon><v-icon>info</v-icon></v-list-item-icon>
<v-list-item-title>{{$t('menu.about')}}</v-list-item-title>
<v-list-item-icon><v-icon>chevron_right</v-icon></v-list-item-icon>
</v-list-item>
<v-list-group sub prepend-icon="language">
<template v-slot:activator>
<v-list-item-title>{{$t('settings.locale.title')}}</v-list-item-title>
</template>
<v-list-item>
<v-list subheader>
<v-radio-group v-model="updateLocale" :label="$t('settings.locale.description')">
<v-radio value="nob" label="Bokmål (nob)"></v-radio>
<v-radio value="nno" label="Nynorsk (nno)"></v-radio>
<v-radio value="eng" label="English (eng)"></v-radio>
<v-radio value="ukr" label="Українська (ukr)"></v-radio>
</v-radio-group>
</v-list></v-list-item>
</v-list-group>
<v-list-group v-if="false" prepend-icon="book">
<template v-slot:activator>
<v-list-item-title>{{$t('settings.dict.title')}}</v-list-item-title>
</template>
<v-list-item>
<v-list subheader>
<v-radio-group mandatory v-model="updateDefaultDict" :label="$t('settings.dict.description')">
<v-radio value="bm,nn" :label="$t('dicts.bm,nn')"></v-radio>
<v-radio value="bm" :label="$t('dicts.bm')"></v-radio>
<v-radio value="nn" :label="$t('dicts.nn')"></v-radio>
</v-radio-group>
</v-list></v-list-item>
</v-list-group>
<v-list-group prepend-icon="settings">
<template v-slot:activator>
<v-list-item-title>{{$t('settings.title')}}</v-list-item-title>
</template>
<v-list-item><Settings/></v-list-item>
</v-list-group>
<v-list-item v-if="!$store.state.unavailable" v-on:click='$store.commit("resetStore")'>
<v-list-item-icon><v-icon>delete</v-icon></v-list-item-icon>
<v-list-item-title>{{$t("settings.reset")}}</v-list-item-title>
</v-list-item>
<v-list-group prepend-icon="mail">
<template v-slot:activator>
<v-list-item-title>{{$t('contact.title')}}</v-list-item-title>
</template>
<v-list-item><Contact @close="$emit('close')"/></v-list-item>
</v-list-group>
</v-list>
</v-card>
</template>
<script>
import Settings from './Settings.vue'
import Help from './Help.vue'
import Contact from './Contact.vue'
export default {
name: "Menu",
components: {
Settings,
Help,
Contact
},
computed: {
updateLocale: {
get () { return this.$store.state.currentLocale},
set(value) {
this.$store.commit("setLocale", {value: value, i18n: this._i18n})
}
},
updateDefaultDict: {
get () { return this.$store.state.defaultDict},
set(value) {
this.$store.commit("setDefaultDict", value)
}
}
},
}
</script>
<template>
<div v-bind:class="{'centered': bottom}">
<span color="tertiary" class = "pagination">
<v-pagination @input="update" v-model="$parent.$parent.page" :class="$vuetify.breakpoint.name" :total-visible="$vuetify.breakpoint.smAndDown && !bottom ? 5 : 8" circle :length="Math.ceil(Math.max($parent.count_bm, $parent.count_nn)/$parent.$parent.perPage)"></v-pagination>
<span class="result-counts" v-if="!bottom && $vuetify.breakpoint.smAndDown">
<span class="total-results">{{$parent.$parent.total_results()}} treff </span>
<span v-if="$parent.$parent.lang=='bm,nn'" class="dict-counts"> | {{$vuetify.breakpoint.mdAndDown? 'bokmål': 'Bokmålsordboka'}}: {{$parent.count_bm}} | {{$vuetify.breakpoint.mdAndDown? 'nynorsk': 'Nynorskordboka'}}: {{$parent.count_nn}}</span>
<span v-if="$parent.$parent.lang!='bm,nn'" class="dict-counts"> | {{$t('dicts_inline.'+$parent.lang)}}</span>
</span>
</span>
<v-btn v-if="bottom && $parent.$parent.total_results() > 6" text @click.native="to_top"><v-icon left>expand_less</v-icon>{{$t('to_top')}}</v-btn>
</div>
</template>
<script>
export default ({
name: "Pagination",
props: {
bottom: Boolean
},
methods: {
update: function(item) {
if (this.bottom) {
this.to_top()
}
this.$emit('update-page', item)
},
to_top: function() {
let first = document.getElementById("result0")
if (first) first.focus();
window.scrollTo(0,0)
}
},
})
</script>
<style scoped>
.centered {
text-align: center;
padding-bottom: 10px;
}
.centered > span {
justify-content: center;
padding-bottom: 10px
}
</style>
\ No newline at end of file
<template>
<div :class="$vuetify.breakpoint.name">
<form @submit.prevent="submit">
<v-combobox
v-model="select"
prepend-inner-icon="search"
autocapitalize = 'off'
:loading="loading"
:items="items"
append-icon=""
:search-input.sync="search"
item-text="match"
:menu-props="{maxHeight: $vuetify.breakpoint.name === 'xs' ? 190 : 500, transition: 'fade-transition', allowOverflow: true}"
return-object
rounded
clearable
:autofocus="!$route.hash && !$store.state.noMouse && $vuetify.breakpoint.mdAndUp"
hide-no-data
no-filter
auto-select-first
hide-details
:label="$t('search_placeholder') + $t(`dicts.${$parent.lang}`)"
:aria-label="$t('search_placeholder') + $t(`dicts.${$parent.lang}`)"
solo
full-width
:placeholder="$t('search_placeholder') + $t(`dicts.${$parent.lang}`)"
ref="autocomplete"
color="primary"
:dense="$vuetify.breakpoint.mdAndDown"
>
<template v-slot:prepend-inner>
<v-menu allowOverflow offsetY v-model="dictMenuOpened">
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs"
v-on="on"
plain
depressed
color = "primary"
text
@click.native="items=[]">
<v-icon left>book</v-icon>
<span v-if="$vuetify.breakpoint.mdAndUp">
{{$t(`dicts.${$parent.lang}`)}}
<v-icon right>{{ dictMenuOpened? 'expand_less' : 'expand_more'}}</v-icon>
</span><span v-if="$vuetify.breakpoint.smAndDown">{{$t(`dicts_short.${$parent.lang}`)}}</span></v-btn>
</template>
<v-list>
<v-list-item v-for="item in ['bm,nn','bm','nn'].map(l => {return {label: $t(`dicts.${l}`), tag: l}})" :key="item.tag"
active-class="v-list-item--active" @click="update_lang_form(item.tag)">
<v-list-item-icon v-if="$parent.lang == item.tag"><v-icon color="primary">radio_button_checked</v-icon></v-list-item-icon>
<v-list-item-icon v-else><v-icon>radio_button_unchecked</v-icon></v-list-item-icon>
<v-list-item-title >{{ item.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-divider vertical aria-hidden="true"/><v-icon right>search</v-icon>
</template>
<template v-slot:item="data">
<span v-if="data.item.type == 'empty' || data.item.type == 0">
Søk: <strong>{{data.item.match}}</strong> </span>
<span v-if="data.item.type == 'exact'">
<span class="searchLemma">
<strong>{{data.item.match}}</strong>
</span>
<span class="dict-parentheses" v-if="(get_lang()=='bm,nn')">
({{["bokmål","nynorsk","bokmål, nynorsk"][data.item.lang-1]}})
</span>
</span>
<span v-if="data.item.type == 'other'">
{{data.item.match}} </span>
</template>
</v-combobox>
<SearchToolbar @updatePos="update_pos"
@updateScope="update_scope" />
</form>
</div>
</template>
<script>
import SearchToolbar from './SearchToolbar.vue'
export default {
data: function() {
return {
loading: false,
items: [],
search: null,
select: this.$parent.queryString || null,
suggesting: null,
menuDialog: false,
dictMenuOpened: false
}
},
components: {
SearchToolbar
},
watch: {
search (val) {
const time = Date.now()
if (! val) {
this.items = []
} else {
this.run_query(val, time)
}
},
select(item) {
if (item) {
if (typeof item != 'string') {
this.items = []
this.suggesting = false
this.submit(item)
}
}
},
$route (to, from) {
this.select = to.query.q || ""
}
},
methods: {
hotkey(e) {
if (e.key !== "/") return;
if (this.$store.state.disableHotkey) return
if (e.altKey || e.ctrlKey || e.metaKey) return;
if (/^(?:input|textarea|select)$/i.test(e.target.tagName)) return;
e.preventDefault()
this.$refs.autocomplete.$refs.input.focus()
},
update_lang_form(lang) {
this.$emit('update-lang-form', lang)
// Submit if switching on front page
if (this.search && this.search!=this.$parent.queryString && !this.items[0]) {
this.submit({q: this.search, match: this.search, time: Date.now()})
}
else {
this.$parent.reload_params()
}
},
update_scope(scope) {
this.$emit('updateScope', scope)
},
update_pos(pos) {
this.$emit('updatePos', pos)
},
get_lang() {
return this.$parent.lang
},
run_query(q, time) {
q = q.trim()
if (this.items[0]) {
if (this.items[0].time < time) {
// Whitespace necessary in case option already exists in dropdown
this.items.splice(0,1, {q, match: q, time, type: 0})
}
}
else {
this.items.push({q, match: q, time, type: 0})
}
let words = q.split(/ |\|/)
if (words.length > 20) {
this.items = [{q, match: q, time, type: 0}]
return
}
for (let i = 0; i < words.length; i++) {
if (words[i].length > 40) {
this.items = [{q, match: q, time, type: 0}]
return
}
}
this.suggesting = true
let self = this
let params = {q, dict: self.get_lang(), n: 20, dform: 'int', meta: 'n', include: this.$parent.scope, wc: self.$parent.pos_selected}
this.$parent.api.get('api/suggest?', {params})
.then(async (response) => {
if (self.$refs.autocomplete.searchInput == q & self.suggesting) {
let suggestions = []
if (response.data.a.exact) {
suggestions = response.data.a.exact.map(item => ({q: q, match: item[0], time: time, lang: [item[1]], type: "exact"}))
}
if (response.data.a.inflect) {
let inf = response.data.a.inflect.map(item => ({q: q, match: q, inflected: item[0], time: time, type: "other"}))
suggestions = suggestions.concat(inf)
}
if (response.data.a.freetext) {
let fr = response.data.a.freetext.map(item => ({q: q, match: item[0], time: time, type: "other"}))
suggestions = suggestions.concat(fr)
}
if (suggestions.length) {
if (suggestions[0].q.toLowerCase() != suggestions[0].match.toLowerCase()) {
suggestions.unshift({q, match: q, time, type: "empty"})
}
self.items = suggestions
}
else {
self.items = [{q, match: q, time, type: "empty"}]
}
}
})
},
submit(item) {
if (!item.q) {
item = this.items[0] || {q: this.search, match: this.search, time: Date.now() }
this.items = []
}
this.$emit('submit', item)
let self = this
setTimeout(() => {
if (!self.$store.state.noMouse && this.$vuetify.breakpoint.mdAndUp) self.$refs.autocomplete.$refs.input.select()
else self.$refs.autocomplete.$refs.input.blur()
self.suggesting = false
}, 1)
}
},
mounted: function() {
document.addEventListener("keydown", this.hotkey);
}
}
</script>
<style scoped>
.searchResult {
margin-right: 5px;
}
.searchLemma {
color: var(--v-primary-base);
}
form {
padding-left: 10px;
padding-right: 10px;
padding-top: 10px;
}
.accessibility_link {
display: inline;
position: absolute;
left: -10000px;
}
.accessibility_link:focus {
position: absolute;
left: 48px;
top : 48px;
background-color: white;
padding: 10px;
z-index: 10000;
}
</style>
<template>
<section id="search_results">
<div class="flex-container">
<ul class="hits" v-if="results_bob.length">
<li class="article_container" v-for="(result, index) in results_bob" :key="index + results_hash">
<Article :article="result" @article-click="article_link_click">
<section id="search_results" :class="$vuetify.breakpoint.name">
<div v-if="show_per_page" class="pagination-container">
<Pagination v-if="show_pagination" @update-page="$emit('update-page')"/>
<span id="per-page">
<span id="per-page-label">{{$t('per_page')}}</span>
<v-select class="per-page-select"
aria-labelledby="per-page-label"
v-model="$parent.perPage"
:items="[10, 20, 50, 100]"
@input="$parent.update_per_page"
hide-details
dense
:menu-props="{ offsetY: true }"
append-icon="expand_more"/>
</span>
</div>
<div class="flex-container" v-if="!$parent.waiting" :class="$vuetify.breakpoint.name">
<div class="hits" v-if="$vuetify.breakpoint.smAndDown">
<div class="article_container" v-for="(result, index) in results_bm.concat(results_nn)" :key="index + both_hash"
:style="'order:'+(index < results_bm.length? 2*index : ((index-results_bm.length)*2) + 1)">
<Article
:article="result"
:title_id="`result${index}`"
:queryPattern="queryPattern"
:scope="scope"
@article-click="article_link_click"
@details-click="details_click">
</Article>
</div>
</div>
<div class="hits" v-if="$vuetify.breakpoint.mdAndUp && lang!= 'nn'" v-bind:class="{'wide': lang=='bm'}">
<div class="dict-label-top"> <span role="heading" aria-level="2" class="dict-label-title"> bokmålsordboka </span><span v-if="count_bm != 1"> | <span v-if="count_bm == 0">{{$t('notifications.no_results')}}</span><span v-else>{{count_bm}} {{$t('notifications.results')}}</span></span></div>
<div class="article_container" v-for="(result, index) in results_bm" :key="index + bm_hash">
<Article
:article="result"
:title_id="`result${index}`"
:queryPattern="queryPattern"
:scope="scope"
@article-click="article_link_click"
@details-click="details_click">
</Article>
</li>
</ul>
<ul class="hits" v-if="results_nob.length">
<li class="article_container" v-for="(result, index) in results_nob" :key="index + results_hash">
<Article :article="result" @article-click="article_link_click">
</div>
</div>
<div class="hits" v-if="$vuetify.breakpoint.mdAndUp && lang!='bm'" v-bind:class="{'wide': lang=='nn'}">
<div class="dict-label-top"> <span role="heading" aria-level="2" class="dict-label-title"> nynorskordboka </span><span v-if="count_nn != 1"> | <span v-if="count_nn == 0">{{$t('notifications.no_results')}}</span><span v-else>{{count_nn}} {{$t('notifications.results')}}</span></span></div>
<div class="article_container" v-for="(result, index) in results_nn" :key="index + nn_hash">
<Article
:article="result"
:title_id="`result${index + results_bm.length}`"
:queryPattern="queryPattern"
:scope="scope"
@article-click="article_link_click"
@details-click="details_click">
</Article>
</li>
</ul>
</div>
</div>
</div>
<Pagination v-if="show_pagination && !$parent.waiting" @update-page="$emit('update-page')" bottom/>
<div>
</div>
</section>
</template>
<script>
import Article from './Article.vue'
import Pagination from './Pagination.vue'
export default {
name: 'SearchResults',
props: {
hits: Array,
lang: String
results_bm: Array,
results_nn: Array,
lang: String,
meta: Object,
queryPattern: String,
scope: String
},
computed: {
results_bob: function(){
return this.hits.filter(hit => hit.dictionary == 'bob')
both_hash: function(){
return this.results_bm.concat(this.results_nn).reduce((hash, hit) => (hash + hit.article_id) % 10000, 0)
},
bm_hash: function(){
return this.results_bm.reduce((hash, hit) => (hash + hit.article_id) % 10000, 0)
},
results_nob: function(){
return this.hits.filter(hit => hit.dictionary == 'nob')
nn_hash: function(){
return this.results_nn.reduce((hash, hit) => (hash + hit.article_id) % 10000, 0)
},
results_hash: function(){
return this.hits.reduce((hash, hit) => (hash + hit.article_id) % 10000, 0)
}
count_bm: function(){
if (this.$parent.article_info.articles.bm ){
return this.$parent.article_info.articles.bm.length
}
else {
return 0
}
},
count_nn: function(){
if (this.$parent.article_info.articles.nn ){
return this.$parent.article_info.articles.nn.length
}
else {
return 0
}
},
show_pagination: function() {
return !this.$parent.article && this.$parent.article_info && (this.count_bm > this.$parent.perPage || this.count_nn > this.$parent.perPage)
},
show_per_page: function() {
return this.$parent.article_info && (this.count_bm > 10 || this.count_nn > 10) && !this.$parent.article
},
},
methods: {
article_link_click: function(item) {
this.$emit('article-click', item)
}
},
details_click: function(item) {
this.$emit('details-click', item)
},
},
components: {
Article
Article,
Pagination
}
}
</script>
<style >
h2 {
<style>
.article h3 {
color: var(--v-primary-base);
margin: 0px;
padding: 20px 0px 20px 0px;
}
.flex-container {
padding-top: 0px;
display: flex;
}
.flex-container.sm, .flex-container.xs {
padding-top: 0px;
}
.hits {
margin-top:0px;
min-width: 50%;
}
.flex-container {
.hits.wide {
min-width: 50%;
}
.flex-container.sm .hits, .flex-container.xs .hits {
min-width: 100%;
display: flex;
flex-direction: column;
}
.results-count {
color: var(--v-primary-base);
margin-left: 24px;
.flex-container > ul {
padding: 0px !important;
}
.flex-container h4 {
margin: 0px;
.pagination {
display: flex;
flex-wrap: wrap;
}
.total-results {
font-weight: bold;
color: var(--v-primary-base);
}
.result-counts {
padding: 10px;
justify-content: right !important;
}
.total-results, .dict-counts {
white-space: nowrap;
}
.pagination ul {
padding-inline-start: 0px !important;
justify-content: right;
}
#per-page {
padding-bottom: 10px;
padding-right: 10px;
margin-left: 10px;
display: flex;
flex-direction: row;
gap: 10px;
align-items: baseline;
}
.per-page-select {
max-width: 64px;;
}
.pagination-container {
margin-left: 10px;
padding-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.lg .pagination-container, .xl .pagination-container {
padding-top: 10px;
}
.dict-label-title {
color: var(--v-primary-base) ;
font-weight: bold;
font-size: 24px;
font-variant-caps: all-small-caps;
}
.dict-label-top {
padding: 10px;
padding-left: 24px;
}
</style>
<template>
<div id="search_toolbar" :class="$vuetify.breakpoint">
<v-card v-if="$vuetify.breakpoint.smAndDown" rounded="xl" min-height="38px">
<v-list two-line v-if="expanded" id="settings-list">
<v-list-item>
<v-select
v-model="$parent.$parent.pos_selected"
hide-details
:menu-props="{ offsetY: true }"
:aria-label="$t('options.pos')"
:items="pos_items"
append-icon="expand_more"
@input="updatePos">
</v-select>
</v-list-item>
<v-list-item>
<v-checkbox
v-model="fulltext"
hide-details
:label="$t('options.fulltext')"/>
</v-list-item>
<v-list-item>
<v-checkbox
v-model="inflected_results"
hide-details=""
:label="$t('options.inflected')"/>
</v-list-item>
<v-list-item v-if="($route.name == 'search' || $route.name == 'word') && $store.state.collapseArticles != 'auto'">
<v-switch hide-details dense v-model="updateCollapse" :label="$t('options.collapse')"></v-switch>
</v-list-item>
</v-list>
<v-btn v-if="$vuetify.breakpoint.smAndDown"
class="search_options"
v-bind:class="{'expanded': expanded}"
height="38px"
rounded
text
@click="expanded=!expanded"
:aria-expanded="expanded ? 'true' : 'false'"
aria-controls="settings-list">
{{expanded? '' : $t('options.title')}}<v-icon small color="primary" :right="!expanded">{{expanded ? 'expand_less': 'expand_more'}}</v-icon></v-btn>
</v-card>
<div id="desktop_toolbar" v-else>
<span>
<v-checkbox
v-model="inflected_results"
dense
hide-details
:label="$t('options.inflected')"/>
</span>
<span>
<v-checkbox
v-model="fulltext"
dense
hide-details
:label="$t('options.fulltext')"/>
</span>
<span id="toolbar-pos-select">
<v-select
v-model="$parent.$parent.pos_selected"
hide-details
:menu-props="{ offsetY: true }"
dense
:aria-label="$t('options.pos')"
:items="pos_items"
append-icon="expand_more"
@input="updatePos">
</v-select>
</span>
<span id="display-options" v-if="($route.name == 'search' || $route.name == 'word') && $store.state.collapseArticles != 'auto'">
<v-switch v-model="updateCollapse" hide-details dense :label="$t('options.collapse')"></v-switch>
</span>
</div>
</div>
</template>
<script>
export default {
name: "SearchToolbar",
data: function() {
return {
expanded: false
}
},
computed: {
updateCollapse: {
get () { return this.$store.state.collapseArticles != "never"},
set(value) {
if (value) this.$store.commit("setCollapse", "always")
else this.$store.commit("setCollapse", "never")
}
},
inflected_results: {
get() {
return this.$parent.$parent.scope.includes("i")
},
set() {
if (this.$parent.$parent.scope.includes("i")) {
this.$emit('updateScope', this.$parent.$parent.scope.replace("i",""))
}
else {
this.$emit('updateScope', this.$parent.$parent.scope + "i")
}
}
},
fulltext: {
get() {
return this.$parent.$parent.scope.includes("f")
},
set() {
if (this.$parent.$parent.scope.includes("f")) {
this.$emit('updateScope', this.$parent.$parent.scope.replace("f",""))
}
else {
this.$emit('updateScope', this.$parent.$parent.scope + "f")
}
}
},
pos_items: function() {
let items = ["VERB", "NOUN", "ADJ", "PRON", "DET", "ADV", "ADP", "CCONJ", "SCONJ", "INTJ"].map(tag =>{
return {text: this.$t("pos_tags_plural." + tag ), value: tag}
})
items.unshift({text: this.$t('all_pos'), value: null})
return items
}
},
methods: {
reset () {
this.$emit('updatePos', "ALL")
this.$emit('updateScope', "ei")
},
updatePos (pos) {
this.$emit('updatePos', pos)
}
}
}
</script>
<style scoped>
.v-card {
padding-top: 0px !important;
padding-bottom: 24px !important;
padding-top: 10px;
margin-top: 10px;
min-height: 36px;
}
.v-list-item {
padding-left: 24px;
padding-right: 24px;
}
.search_options {
position: absolute;
min-height: 36px;
bottom: 0px;
left: 0px;
border-radius: 28px;
float: left;
width: 100%;
}
.search_options.expanded {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
min-height: 24px;
}
#desktop_toolbar {
display: flex;
padding-left: 10px;
margin-right: 10px;
gap: 24px;
}
#search_toolbar {
margin-bottom: 10px;
}
.md#search_toolbar, .lg#search_toolbar, .xl#search_toolbar {
padding-top: 10px;
}
#desktop_toolbar v-combobox {
width: 300px;
}
#toolbar-pos-select {
max-width: 200px;
}
.v-input {
margin-top: 0px;
}
</style>
<template>
<div>
<v-list subheader>
<v-list-item>
<v-checkbox
v-model="toggleInflectionExpanded"
:label="$t('settings.inflection_expanded')"
hide-details>
</v-checkbox>
</v-list-item>
<v-list-item>
<v-checkbox
v-model="toggleInflectionNo"
:label="$t('settings.inflection_no')"
hide-details>
</v-checkbox>
</v-list-item>
<v-list-item>
<v-checkbox
v-model="disableHotkey"
:label="$t('settings.disable_hotkey')"
hide-details>
</v-checkbox>
</v-list-item>
<v-list-item>
<v-checkbox
v-model="toggleInflectionTableContext"
:label="$t('settings.inflection_table_context')"
hide-details>
</v-checkbox>
</v-list-item>
<v-list-item>
<v-radio-group mandatory two-line v-model="updateCollapse" :label="$t('settings.collapse.description')">
<v-radio value="auto" :label="$t('settings.collapse.auto')"></v-radio>
<v-radio value="always" :label="$t('settings.collapse.always')"></v-radio>
<v-radio value="never" :label="$t('settings.collapse.never')"></v-radio>
</v-radio-group>
</v-list-item>
</v-list>
</div>
</template>
<script>
export default {
name: "Settings",
computed: {
toggleInflectionNo: {
get () { return this.$store.state.showInflectionNo},
set () { this.$store.commit("toggleInflectionNo")
}
},
disableHotkey: {
get () { return this.$store.state.disableHotkey},
set () { this.$store.commit("disableHotkey")
}
},
toggleInflectionExpanded: {
get () { return this.$store.state.inflectionExpanded},
set () { this.$store.commit("toggleInflectionExpanded")
}
},
toggleInflectionTableContext: {
get () {
return this.$store.state.inflectionTableContext},
set () {
this.$store.commit("toggleInflectionTableContext")
}
},
updateCollapse: {
get () { return this.$store.state.collapseArticles},
set(value) {
this.$store.commit("setCollapse", value)
}
},
},
}
</script>
<template>
<div :lang="$parent.lang_tag_locale" class = "split-inf" v-if="split_inf">kløyvd infinitiv: <em>-a</em>
<v-menu tile
v-model="menu"
offset-x max-width="200px">
<template v-slot:activator="{ on, attrs }">
<v-btn :aria-label="$t('split_inf.label', $parent.content_locale)"
small
icon
v-on="on"
v-bind="attrs"
class="info-button">
<v-icon color="primary"
small>info</v-icon>
</v-btn>
</template>
<v-card tile
class="info-card">
{{$t('split_inf.content[0]', $parent.content_locale)}} <em>-a</em> {{$t('split_inf.content[1]', $parent.content_locale)}}
<a target="_blank"
href="https://www.sprakradet.no/svardatabase/sporsmal-og-svar/kloyvd-infinitiv-/">{{$t('split_inf.content[2]', $parent.content_locale)}}</a>
</v-card>
</v-menu>
</div>
</template>
<script>
export default {
name: "SplitInf",
props: {
lemmas: Array
},
data: function() {
return {
menu: false,
long_lemma: false
}
},
computed: {
split_inf: function() {
return this.lemmas[0].split_inf
}
}
}
</script>
<style>
.split-inf {
padding-top: 10px;
}
</style>
\ No newline at end of file
<template>
<li class="sub_article">
<span class="sub_article_header">
<router-link :to="'/' + dictionary + '/' + body.article_id" @click.native="article_link_click(body)">
{{body.lemmas[0]}}
</router-link>
{{body.lemmas[0]}}
</span>
<ul>
<DefElement :body="body.intro" v-if="body.intro" :dictionary="dictionary" @article-click="article_link_click" />
<Definition :level="9" :body="body.article.body.definitions[0]" :dictionary="dictionary" @article-click="article_link_click" />
<DefElement :body="body.intro" v-if="body.intro" :dictionary="dictionary" @article-click="article_link_click" @error="article_error"/>
<Definition :level="9" :body="body.article.body.definitions[0]" :dictionary="dictionary" @article-click="article_link_click" @error="article_error"/>
</ul>
</li>
</template>
......@@ -35,6 +33,9 @@ export default {
methods: {
article_link_click: function(item) {
this.$emit('article-click', item)
},
article_error: function(payload) {
this.$emit('error', payload)
}
}
}
......@@ -43,6 +44,7 @@ export default {
<style scoped>
.sub_article_header {
font-weight: bold;
color: var(--v-primary-base);
}
li.sub_article {
......