view client/src/components/Search.vue @ 5627:7768f14f6535 729-node-js-newer-version

Transformed scss variables into css custom properties
author Luisa Beerboom <lbeerboom@intevation.de>
date Tue, 09 May 2023 13:17:58 +0200
parents 1cf9b043dca1
children 84d01a536bec
line wrap: on
line source

<template>
  <div :class="searchbarContainerStyle">
    <div class="input-group-prepend m-0 d-print-none">
      <span @click="toggleSearchbar" :class="searchButtonStyle" for="search">
        <font-awesome-icon icon="search" />
      </span>
    </div>
    <div
      :class="[
        'searchgroup',
        {
          searchgroupwidthbottlenecks:
            this.showSearchbar && this.contextBoxContent === 'bottlenecks',
          sgnobottlenecks:
            this.showSearchbar && this.contextBoxContent !== 'bottlenecks',
          'searchgroup-collapsed': !showSearchbar,
          big:
            showContextBox &&
            ['bottlenecks', 'staging', 'stretches'].indexOf(
              contextBoxContent
            ) !== -1
        }
      ]"
    >
      <input
        @keyup.enter="triggerEnter"
        id="search"
        v-model="searchQuery"
        type="text"
        :class="searchInputStyle"
      />
    </div>
    <div
      v-if="showSearchbar && searchResults !== null && !showContextBox"
      class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute"
    >
      <div
        v-for="(entry, index) of searchResults"
        :key="index"
        class="border-top text-left"
      >
        <a
          href="#"
          @click.prevent="moveToSearchResult(entry)"
          class="p-2 d-block text-nowrap"
        >
          <font-awesome-icon
            icon="ship"
            v-if="entry.type === 'bottleneck'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="water"
            v-if="entry.type === 'rhm'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="city"
            v-if="entry.type === 'city'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="ruler-vertical"
            v-if="entry.type === 'gauge'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="road"
            v-if="['stretch', 'section'].includes(entry.type)"
            class="mr-1"
            fixed-width
          />
          {{ entry.name }}
          <span v-if="entry.location || entry.locationcode"
            >({{ entry.location
            }}<span v-if="entry.location && entry.locationcode">, </span
            >{{ entry.locationcode }})</span
          >
        </a>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.searchcontainer {
  opacity: 0.96;
}

.searchcontainerwitdh {
  width: 860px;
  transition: 0.1s;
  transition-timing-function: ease;
}

.searchcontainerwitdhbottlenecks {
  width: 650px;
  transition: 0.1s;
  transition-timing-function: ease;
}

.searchgroupwidth {
  min-width: 852px;
  max-width: 860px;
  transition: 0.1s;
  transition-timing-function: ease;
}

.sgnobottlenecks {
  width: 817px;
  transition: 0.1s;
  transition-timing-function: ease;
}

.searchgroupwidthbottlenecks {
  width: 617px;
  transition: 0.1s;
  transition-timing-function: ease;
}

.searchcontainer .searchbar {
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}

.searchgroup {
  overflow: hidden;
}

.searchgroup-collapsed {
  width: 0;
}

.searchbar {
  height: 2rem !important;
  box-shadow: none !important;
}

.searchbar.rounded-top-right {
  border-radius: 0 !important;
  border-top-right-radius: 0.25rem !important;
}

.searchlabel.rounded-top-left {
  border-radius: 0 !important;
  border-top-left-radius: 0.25rem !important;
}

.input-group-text {
  height: 2rem;
  width: 2rem;
}

.input-group-prepend svg path {
  fill: #666;
}

.searchresults {
  box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
  top: 2rem;
  left: 0;
  right: 0;
  max-height: 24rem;
  overflow: auto;
}

.searchresults > div:first-child {
  border-top: 0 !important;
}

.searchresults a {
  text-decoration: none;
}

.searchresults a:hover {
  background: #f8f8f8;
}

.smallbox {
  width: 2rem;
}
</style>

<script>
/* This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * Markus Kottländer <markus.kottlaender@intevation.de>
 */
import debounce from "debounce";
import { mapState, mapGetters } from "vuex";

import { displayError } from "@/lib/errors";
import { HTTP } from "@/lib/http";
import { format } from "date-fns";

const setFocus = () => document.querySelector("#search").focus();

export default {
  name: "search",
  data() {
    return {
      searchQueryIsDirty: false,
      searchResults: null,
      isSearching: false
    };
  },
  computed: {
    ...mapState("application", [
      "showSearchbar",
      "showContextBox",
      "contextBoxContent",
      "showTimeSlider",
      "currentVisibleTime"
    ]),
    ...mapState("imports", ["startDate", "endDate"]),
    ...mapGetters("imports", ["filters"]),
    ...mapGetters("map", ["openLayersMap"]),
    searchQuery: {
      get() {
        return this.$store.state.application.searchQuery;
      },
      set(value) {
        this.$store.commit("application/searchQuery", value);
      }
    },
    searchIndicator: function() {
      if (this.isSearching) {
        return "⟳";
      } else if (this.searchQueryIsDirty) {
        return "";
      } else {
        return "✓";
      }
    },
    searchbarContainerStyle() {
      return [
        "input-group searchcontainer shadow-xs rounded",
        {
          "d-flex": this.contextBoxContent !== "imports",
          "d-none": this.contextBoxContent === "imports" && this.showContextBox,
          smallbox: !this.showSearchbar,
          searchcontainerwidth:
            this.showSearchbar && this.contextBoxContent !== "bottlenecks",
          searchgroupwidth:
            this.showSearchbar && this.contextBoxContent !== "bottlenecks",
          searchcontainerwidthbottlenecks:
            this.showSearchbar && this.contextBoxContent === "bottlenecks",
          searchgroupwidthbottleneks:
            this.showSearchbar && this.contextBoxContent === "bottlenecks"
        }
      ];
    },
    searchInputStyle() {
      return [
        "form-control ui-element search searchbar d-print-none border-0",
        { "rounded-top-right": this.showContextBox || this.searchResults }
      ];
    },
    searchButtonStyle() {
      return [
        "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none",
        {
          rounded: !this.showSearchbar,
          "rounded-left": this.showSearchbar,
          "rounded-top-left":
            this.showSearchbar && (this.showContextBox || this.searchResults)
        }
      ];
    }
  },
  watch: {
    searchQuery: function() {
      this.searchQueryIsDirty = true;
      if (!this.showContextBox) this.triggerSearch();
    },
    currentVisibleTime() {
      this.doSearch();
    }
  },
  methods: {
    loadLogs() {
      this.$store
        .dispatch("imports/getImports", {
          filter: this.filters,
          from: format(this.startDate, "YYYY-MM-DDTHH:mm:ss.SSS"),
          to: format(this.endDate, "YYYY-MM-DDTHH:mm:ss.SSS"),
          query: this.searchQuery
        })
        .then(() => {})
        .catch(error => {
          let message = "Backend not reachable";
          if (error.response) {
            const { status, data } = error.response;
            message = `${status}: ${data.message || data}`;
          }
          displayError({
            title: this.$gettext("Backend Error"),
            message: message
          });
        });
    },
    triggerEnter() {
      if (this.showContextBox && this.contextBoxContent === "importoverview") {
        this.loadLogs();
      }
      if (!this.searchResults || this.searchResults.length != 1) return;
      this.moveToSearchResult(this.searchResults[0]);
    },
    triggerSearch: debounce(function() {
      this.doSearch();
    }, 500),
    doSearch() {
      this.isCalculating = true;
      this.searchResults = null;

      if (this.searchQuery == "") {
        return;
      }

      HTTP.post(
        "/search",
        this.showTimeSlider
          ? {
              string: this.searchQuery,
              time: this.currentVisibleTime.toISOString()
            }
          : { string: this.searchQuery },
        {
          headers: {
            "X-Gemma-Auth": localStorage.getItem("token"),
            "Content-type": "text/xml; charset=UTF-8"
          }
        }
      )
        .then(response => {
          this.searchResults = response.data;
        })
        .catch(error => {
          let message = "Backend not reachable";
          if (error.response) {
            const { status, data } = error.response;
            message = `${status}: ${data.message || data}`;
          }
          displayError({
            title: this.$gettext("Backend Error"),
            message: message
          });
        });

      this.isCalculating = false;
      this.searchQueryIsDirty = false;
    },
    moveToSearchResult(resultEntry) {
      let zoom = 16;
      if (resultEntry.type === "bottleneck") {
        this.openLayersMap()
          .getLayer("BOTTLENECKS")
          .setVisible(true);
        this.$store.commit(
          "bottlenecks/setBottleneckForPrint",
          resultEntry.name
        );
      }
      if (resultEntry.type === "rhm") {
        this.openLayersMap()
          .getLayer("DISTANCEMARKSAXIS")
          .setVisible(true);
      }
      if (resultEntry.type === "gauge") {
        this.openLayersMap()
          .getLayer("GAUGES")
          .setVisible(true);
      }
      if (resultEntry.type === "stretch") {
        this.$store.commit(
          "imports/selectedStretchId",
          "stretches_geoserver." + resultEntry.id
        );
        this.openLayersMap()
          .getLayer("STRETCHES")
          .setVisible(true);
      }
      if (resultEntry.type === "section") {
        this.$store.commit(
          "imports/selectedSectionId",
          "sections_geoserver." + resultEntry.id
        );
        this.openLayersMap()
          .getLayer("SECTIONS")
          .setVisible(true);
      }
      if (resultEntry.type === "city") zoom = 13;

      if (resultEntry.geom.type == "Point") {
        this.$store.dispatch("map/moveMap", {
          coordinates: resultEntry.geom.coordinates,
          zoom,
          preventZoomOut: true
        });
      } else if (resultEntry.geom.type == "Polygon") {
        const boundingBox = [
          Math.min(...resultEntry.geom.coordinates[0].map(c => c[0])),
          Math.min(...resultEntry.geom.coordinates[0].map(c => c[1])),
          Math.max(...resultEntry.geom.coordinates[0].map(c => c[0])),
          Math.max(...resultEntry.geom.coordinates[0].map(c => c[1]))
        ];
        this.$store.dispatch("map/moveToBoundingBox", { boundingBox, zoom });
      }
      // this.searchQuery = ""; // clear search query again
      this.toggleSearchbar();
    },
    toggleSearchbar() {
      if (!this.showContextBox) {
        if (!this.showSearchbar) {
          setTimeout(setFocus, 300);
        }
        this.$store.commit("application/showSearchbar", !this.showSearchbar);
      }
    }
  }
};
</script>