Skip to content
Snippets Groups Projects
DictionaryView.vue 20.19 KiB
<template>
  <main>
    <div class="search_container" v-if="!article">
      <Autocomplete v-on:submit="select_result" :api="get_search_endpoint">
      </Autocomplete>
      <SearchToolbar v-if="$store.state.showSearchToolbar" @updatePos="update_pos"  @updateScope="update_scope"/>

    </div>
    <div id="above_results">
    <div id="suggestions" v-if="!article && inflection_suggestions && inflection_suggestions.length">Se også <em>{{$route.query.q}}</em> som bøyd form av
    <span v-for="(item,index) in inflection_suggestions" :key="index"><router-link :to="generate_path(item[0])" @click.native="select_suggestion(item[0])">{{item[0]}}</router-link>{{index == inflection_suggestions.length? '.' : ', '}}</span>
    </div>

        <div class="return_to_results" v-if="total_results && article">
          <router-link v-if="previous" :to="previous" @click.native="return_to_results()">
            <v-icon left class="nav_arrow">chevron_left</v-icon>{{$t("back_to_results")}}
          </router-link>
          <a v-if="!this.previous" href="/">
            <v-icon left class="nav_arrow">chevron_left</v-icon>Søk etter andre ord
          </a>
        </div>

      </div>
    
    <SearchResults  :results_bm="search_results.bm || []"
                    :results_nn="search_results.nn || []"
                    :lang="lang"
                    @article-click="article_link_click"
                    @details-click="details_click"
                    @update-page="update_page"
                    v-if="total_results() && ! article" />
    <div id="spinner" v-if="waiting">
      <v-progress-circular indeterminate color="secondary" size="120"></v-progress-circular>
    </div>
    <div id="single_article_container" v-if="article">


      <Article :key="article_key" :article="article" @article-click="article_link_click" />

    </div>
    <div class="welcome" v-if="! (article || error || total_results() || waiting)">
      <div class="monthly" :class="$vuetify.breakpoint.name">
        <div>
          <Article :article="monthly_bm" @article-click="article_link_click" />
        </div>
        <div>
          <Article :article="monthly_nn" @article-click="article_link_click" />
        </div>
      </div>
    </div>
    <div class="error" v-if="error">
      <h1>Ingen treff</h1>
      <p>{{error}}</p>
    </div>
    
  </main>
</template>

<script>
import axios from "axios"
import entities from '../utils/entities.js'
import Article from './Article.vue'
import SearchResults from './SearchResults.vue'
import Autocomplete from './Autocomplete.vue'
import SearchToolbar from './SearchToolbar.vue'

import { setup } from 'axios-cache-adapter'

const SEARCH_ENDPOINT = process.env.VUE_APP_SEARCH_ENDPOINT
const ARTICLE_ENDPOINT= process.env.VUE_APP_ARTICLE_ENDPOINT

const dicts = {'nn': 'Nynorskordboka',
               'bm': 'Bokmålsordboka',
               'bm,nn': 'ordbøkene'}

const api = setup({
  baseURL: SEARCH_ENDPOINT,
  cache: {
    maxAge: 15 * 60 * 1000,
    exclude: {
      query: false,
      paths: ["articles"] // Disable caching for articles
    }
  }
})


function QueryException(params) {
  this.name = "QueryException"
  this.params = params
}

function navigate_to_article(self, source) {
  self.article = null
  self.waiting_for_articles = true

  const lang = self.$route.params.lang

  axios.get(ARTICLE_ENDPOINT + lang + '/article/' + self.$route.params.id + ".json")
  .then(function(response){
    self.article = Object.assign(response.data, {'dictionary': lang, results: self.search_results})
    self.error = null
  })
  .catch(function(error){
    console.log(error)
    if (error.response && error.response.status == 404) {
      self.error = self.$t('error.no_article', {id: self.$route.params.id})
    } else {
      self.error = self.$t('error.generic')
    }
  })
  .then(function(response){
    self.waiting_for_articles = false
    history.replaceState({article: self.article, 
                          search_results: {}, 
                          lang: self.lang, 
                          error: self.error, 
                          pos_selected: self.pos_selected, 
                          scope: self.scope, 
                          article_info: self.article_info, 
                          page: self.page,
                          perPage: self.perPage}, '')
    if (source) {
      self.$plausible.trackEvent('internal link incoming', {props: {origin: source}})
    }
  })
}

function load_articles(self, query, offset, n, dict) {
  let article_IDs = self.article_info.articles[dict]
  if (article_IDs)
  {
  if (offset > article_IDs.length) {
  
    n = 0
  }
  else if (offset + n > article_IDs.length) {
    n = article_IDs.length % n
    
  }

  if (n > 0 && (self.lang == dict || self.lang == "bm,nn")) {
    article_IDs = article_IDs.slice(offset, offset + n)

    return Promise.all(article_IDs.map((article_id) => {
      return axios.get(`${ARTICLE_ENDPOINT}${dict}/article/${article_id}.json`)

    }))
    .then((response) => {
      let results = response.map((element, index) => {
        return Object.assign(element.data, {
          dictionary: dict
        })
      })
      // TODO: Error handling must be moved
      self.article = null
      self.search_results[dict] = results
    })
    .catch(error => {
      self.waiting_for_articles = false
      self.error_message(error)
    })
  }
    else {
    self.search_results[dict] = []
  }
  }
    return Promise.resolve()
}


function navigate_to_query(self, word) {
  self.error = null
  self.waiting_for_articles = true
  let  query = self.event ? self.event : {match: word}
  self.query = query
  
  // Get article IDs
  let params = {w: query.match, dict: self.lang, scope: self.scope, meta: 'y'}
  let offset = 0
  if (self.page) {
    offset = self.perPage * (self.page -1)
  }
  if (self.pos_selected) params.wc = self.pos_selected
  api.get('articles?', {params}).then((response) => {
            self.article_info = response.data
            self.search_results = {}

            let total = response.data.articles.bm ? response.data.articles.bm.length : 0
            total += response.data.articles.nn ? response.data.articles.nn.length : 0

          if (total == 0) {
            if (self.event && query.inflection_suggestions) {
              self.inflection_suggestions = query.inflection_suggestions
            }
            else {
              self.inflection_suggestions = []
            }
              throw new QueryException(query)
            }
          else {
            self.error = null
          }



            Promise.all([
              load_articles(self, query, offset, self.perPage, "bm"),
              load_articles(self, query, offset, self.perPage, "nn")
            ])
            .then(() => {
              if (self.event && self.event.inflection_suggestions) {
                self.inflection_suggestions = self.event.inflection_suggestions.filter((item) => {
                if ((item[1] == 1 || item[1] == 3) && self.search_results.bm) {
                  return !self.search_results.bm[0].suggest.includes(item[0])
                }
                if ((item[1] == 2 || item[1] == 3) && self.search_results.nn) {
                  return !self.search_results.nn[0].suggest.includes(item[0])
                }
                })
              } else {
                self.inflection_suggestions = []
              }
              self.waiting_for_articles = false
              
              history.replaceState({
                article: self.article,
                lang: self.lang,
                error: self.error,
                pos_selected: self.pos_selected,
                scope: self.scope,
                article_info: self.article_info,
                search_results: self.search_results,
                page: self.page,
                perPage: self.perPage
              }, '')
              self.previous = self.$route.fullPath
            })
          
        }).catch(error =>{
          self.waiting_for_articles = false
          console.log(error)
            self.error_message(error)         
        })
}

export default {
  name: 'DictionaryView',
  data: function() {
    return {
      article_key: 0,
      search_results: {},
      lang: this.$store.state.defaultDict,
      waiting_for_articles: true,
      waiting_for_metadata: true,
      article: null,
      error: null,
      monthly_bm: null,
      monthly_nn: null,
      event: null,
      previous: this.$route.fullPath,
      scope: "wb",
      pos_selected: "ALL",
      article_info: null,
      page: 1,
      perPage: 10,
      inflection_suggestions: null,
      selected: null
    }
  },
  computed: {
    waiting: function() {
      return (this.waiting_for_articles || this.waiting_for_metadata) && this.$route.name != 'root'
    },
    get_search_endpoint: function() {
      return api
    }    
  },
  components: {
    Article,
    Autocomplete,
    SearchResults,
    SearchToolbar
  },
  methods: {
    total_results: function() {
      if (this.article_info) {
        let total = 0
        if (this.article_info.articles.bm) {
          total += this.article_info.articles.bm.length
        }
        if (this.article_info.articles.nn) {
          total += this.article_info.articles.nn.length
        }

        return total
      }
    },
  
    error_message: function(error) {    
      this.search_results = {}
      console.log(error)
      if (error instanceof QueryException) {
        if (error.params.match) {
          // If a suggestion from the dropdown isn't found. The message should be revised
          this.error = this.$t('error.no_word', {q: error.params.match, dict: this.$t('dicts_inline.'+this.lang)})

        } else {
          this.error = this.$t('error.search_error', {q: error.params.q, dict: this.$t('dicts_inline.'+this.lang)})
          if (this.lang != "bm,nn") this.error += "\r\n"+this.$t('error.other_dict', {otherDict: this.$t('inline_dict.'+this.lang == "bm"? "nn" : "bm")})
          if (error.params.search != 2) this.error += "\r\n"+this.$t('error.search_advice')
        }
      }
      else if (error.message == "Network Error") {
        this.error = this.$t('error.network')
      }
      else if (error.response) {
        this.error = this.$t('error.server', {code: error.response.status})
      } else {
        this.error = this.$t('error.generic')
      }
    },
    select_suggestion: function (word) {
      this.pos_selected = null
      this.event = null
      this.inflection_suggestions = null
      navigate_to_query(this, word)

    },
    select_result: function (event) {
        this.event = event
        let path = `/${this.lang}/search`
        let pos = this.pos_param()
        let query = {q: event.match || event.q}
        if (pos) query["pos"] = pos
        if (this.scope) query["scope"] = this.scope
        this.$router.push({path, query})
        navigate_to_query(this)
         
        // Tracking
        let track_props = {query: event.q}
        if (event.match) track_props.match = event.match
        this.$plausible.trackEvent(event.update_lang ? "language" : 'dropdown selection', {
          props: track_props
        })
      },

      pos_param: function() {
        if (this.pos_selected) return this.pos_selected.toLowerCase()
        return null
      },
      update_page: function() {
        let q = (this.$route.query || this.$route.params).q
        let path = `/${this.lang}/search`
        let pos = this.pos_param()
        let query = {q: q, page: this.page}
        if (pos != 'all') query.pos = pos
        if (this.scope) query.scope = this.scope
        this.$router.push({path, query})
        let offset = 0
        if (this.page) {
          offset = this.perPage * (this.page -1)
        }

      let self = this
      Promise.all([
      load_articles(this, query, offset, this.perPage, "bm"),
      load_articles(this, query, offset, this.perPage, "nn")]).then(() => {
        history.replaceState({article: self.article, 
                      search_results: self.search_results, 
                      lang: self.lang, 
                      error: self.error, 
                      pos_selected: self.pos_selected, 
                      scope: self.scope, 
                      article_info: self.article_info, 
                      page: self.page,
                      perPage: self.perPage,
                      selected: self.selected,
                      inflection_suggestions: self.inflection_suggestions}, ''),
         self.$forceUpdate()

      }
      )
      },

        generate_path: function(q) {
        if (q) {
          let path = `/${this.lang}/search?q=${q}`
          if (this.scope) path += "&scope=" + this.scope
          return path
        } 
      },

      reload_params: function() {
        let q = (this.$route.query || this.$route.params).q
        if (q) {
          let path = `/${this.lang}/search`
          let pos = this.pos_param()
          let query = {q: q}
          if (pos) query.pos = pos
          if (this.scope) query.scope = this.scope
          this.$router.push({path, query})
          navigate_to_query(this, q)
        } 
      },

      update_lang_form: function (lang) {
        this.lang = lang
        this.page = 1
        this.reload_params()
      },
      update_scope: function(scope) {
        this.scope = scope
        this.page = 1
        this.reload_params()
      },
      update_pos: function (pos) {
        this.pos_selected = pos
        this.page = 1
        this.reload_params()
      },

    article_link_click: function(item) {
      if (this.article && this.article.article_id == item.article_id){
        this.article_key++
        history.replaceState({article: this.article, 
                              search_results: this.search_results, 
                              lang: this.lang, 
                              error: this.error, 
                              pos_selected: this.pos_selected, 
                              scope: this.scope, 
                              article_info: this.article_info, 
                              page: this.page,
                              perPage: this.perPage,
                              selected: this.selected,
                              inflection_suggestions: this.inflection_suggestions}, '')

      }else{
        navigate_to_article(this, item.source)
      }
    },
    details_click: function(item) {
      item.article.source = this.$route.fullPath
      this.previous = this.previous.split('#')[0] + "#" + item.title_id
      this.article = item.article
      history.replaceState({article: this.article,
                            search_results: {},
                            lang: this.lang,
                            error: null,
                            pos_selected: this.pos_selected,
                            scope: this.scope,
                            article_info: this.article_info,
                            page: this.page,
                            perPage: this.perPage,
                            selected: this.selected,
                            inflection_suggestions: this.inflection_suggestions}, '')
    },
    return_to_results: function() {
      this.article = null
    }
  },
  mounted: function(){
    let self = this
    self.lang = self.$route.params.lang || this.$store.state.defaultDict
    if (self.$route.query.pos) {
    self.pos_selected = self.$route.query.pos.toUpperCase()
    } else self.pos_selected = null

    if (self.$route.query.scope) {
      self.scope = self.$route.query.scope
    } 
    if (self.$route.query.page) self.page = parseInt(self.$route.query.page)
    if (self.$route.query.perPage) self.perPage = self.$route.query.perPage

    Promise.all([
      axios.get(ARTICLE_ENDPOINT + 'bm/concepts.json').then(function(response){
        let concepts = response.data.concepts
        entities.bm = concepts
      }),
      axios.get(ARTICLE_ENDPOINT + 'nn/concepts.json').then(function(response){
        let concepts = response.data.concepts
        entities.nn = concepts
      })
    ]).then(function(_) {
      self.waiting_for_metadata = false
      if (self.$route.name == 'query') {
        navigate_to_query(self, self.$route.query.q)
      }
      else if(self.$route.name == 'search') {
        navigate_to_query(self, self.$route.params.query)
      } 
      else if(self.$route.name == 'word') {
        navigate_to_query(self, self.$route.params.query)
      }
      else if(self.$route.name == 'lookup'){
        navigate_to_article(self, self.$route.path)
      }
      else {
        self.waiting_for_articles = false
        history.replaceState({article: self.article, 
                              search_results: self.search_results, 
                              lang: self.lang, 
                              error: self.error, 
                              pos_selected: self.pos_selected,
                              scope: self.scope,
                              article_info: self.article_info,
                              page: self.page,
                              perPage: self.perPage,
                              selected: self.selected,
                              inflection_suggestions: self.inflection_suggestions}, '')


      // words of the month
      axios.get(ARTICLE_ENDPOINT + 'bm/article/5607.json').then(function(response){
        self.monthly_bm = Object.assign(response.data, {dictionary: 'bm'})
      })

      axios.get(ARTICLE_ENDPOINT + 'nn/article/78569.json').then(function(response){
        self.monthly_nn = Object.assign(response.data, {dictionary: 'nn'})
      })

      }
    }).catch(function(error){
      self.error = null
      if (self.lang !== 'bm') self.error.push(`Eit nettverksproblem hindra lasting av sida. Prøv å laste sida på nytt`)
      if (self.lang !== 'nn') self.error.push(`Et nettverksproblem hindret lasting av siden. Prøv å laste siden på nytt`)
      self.waiting_for_metadata = false
      self.waiting_for_articles = false
    })
  },
  created: function() {
    let self = this
    window.onpopstate = function (event) {
      if (event.state) {
        self.article = event.state.article
        self.search_results = event.state.search_results
        self.lang = event.state.lang
        self.pos_selected = event.pos_selected || self.pos_selected
        self.scope = event.scope || self.scope
        self.error = event.state.error
        self.page = event.state.page,
        self.perPage = event.state.perPage
        self.selected = event.state.selected
        self.inflection_suggestions = event.state.inflection_suggestions
      }
    }
  }
}
</script>

<style>
main {
  flex: 1 0 auto;
  background-color: var(--v-tertiary-base);
  display: flex;
  flex-direction: column;
}


div.welcome {
  padding-top: 10px;
  flex-grow: 10;
  background-image: url('../assets/books.jpg');
  background-repeat: no-repeat;
  background-size: cover;
}

div.welcome article {
  border-style: none;
}

.search_container {
  background-color: var(--v-tertiary-base);
  padding-top: 10px;
}

#spinner {
  margin: auto;
}

#search_results, #spinner, #above_results, #single_article_container, div.welcome, div.search_container, .error {
  padding-left: calc((100vw - 1200px) / 2);
  padding-right: calc((100vw - 1200px) / 2);
}

#above_results div {
  padding-top: 10px;
  margin-left: 10px;
  font-size: 18px;
}

#suggestions {
  padding-left: 10px;
}


.error > p {
  margin-left: 15px;
}

#spinner {
  padding-top: 40px;
}

div.monthly {
  display: flex;
  width: 100%;
}

div.monthly > div {
  flex: 50%;
}

div.monthly.sm, div.monthly.xs {
  flex-direction: column;
}

div.monthly article.bm .dict-label::before {
  content: "fra ";
}

div.monthly article.nn .dict-label::before {
  content: "frå ";
}

div.monthly details, div.monthly h3 {
  display: none;
}

.v-label span {
  color: var(--v-primary-base);
}

.lang_select_container {
  padding-left: 10px;
}

.pos_select_container {
  padding-left: 10px;
  padding-right: 10px;
  padding-bottom: 0px;
  padding-top: 10px;
}

li.suggestion {
  font-weight: bold;
  padding-left: 20px;
  padding-top: 5px;
  padding-bottom: 5px;
  border: 0px;
  background-image: none;
}

::selection {
  background: var(--v-secondary-base);
  color: white;
}

.return_to_results {
  padding-top: 10px;
  display: table-cell;
}

.return_to_results a {
  color: var(--v-text-base) !important;
  text-decoration: none;
}

.nav_arrow {
  vertical-align: top !important;
  color: var(--v-primary-base) !important;
}

.col {
  padding: 10px;
}

</style>