HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/web/matomo.vdk/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue
<!--
  Matomo - free/libre analytics platform

  @link    https://matomo.org
  @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->

<template>
  <div
    class="siteSelector piwikSelector borderedControl"
    :class="{'expanded': showSitesList, 'disabled': !hasMultipleSites}"
    v-focus-anywhere-but-here="{ blur: onBlur }"
  >
    <input
      v-if="name"
      type="hidden"
      :value="displayedModelValue?.id"
      :name="name"
    />
    <a
      ref="selectorLink"
      @click="onClickSelector"
      @keydown="onPressEnter($event)"
      href="javascript:void(0)"
      :class="{'loading': isLoading}"
      class="title"
      tabindex="4"
      v-tooltips
      :title="selectorLinkTitle"
    >
      <span
        class="icon icon-chevron-down"
        :class="{'iconHidden': isLoading, 'collapsed': !showSitesList}"
      />
      <span>
        <span
          v-text="displayedModelValue?.name || firstSiteName"
          v-if="displayedModelValue?.name || !placeholder"
        />
        <span
          v-if="!displayedModelValue?.name && placeholder"
          class="placeholder"
        >{{ placeholder }}</span>
      </span>
    </a>
    <div
      v-show="showSitesList"
      class="dropdown"
    >
      <div
        class="custom_select_search"
        v-show="autocompleteMinSites <= sites.length || searchTerm"
      >
        <input
          type="text"
          @click="searchTerm = '';loadInitialSites()"
          v-model="searchTerm"
          tabindex="4"
          class="websiteSearch inp browser-default"
          v-focus-if="{ focused: shouldFocusOnSearch }"
          :placeholder="translate('General_Search')"
        />
        <img
          title="Clear"
          v-show="searchTerm"
          @click="searchTerm = '';loadInitialSites()"
          class="reset"
          src="plugins/CoreHome/images/reset_search.png"
        />
      </div>
      <div v-if="allSitesLocation === 'top' && showAllSitesItem">
        <AllSitesLink
          :href="urlAllSites"
          :all-sites-text="allSitesText"
          @click="onAllSitesClick($event)"
        />
      </div>
      <div
        class="custom_select_container"
        v-tooltips="{ content: tooltipContent }"
      >
        <ul
          class="custom_select_ul_list"
          @click="showSitesList = false"
        >
          <li
            @click="switchSite({ ...site, id: site.idsite }, $event)"
            v-show="!(!showSelectedSite && `${activeSiteId}` === `${site.idsite}`)"
            v-for="(site, index) in sites"
            :key="index"
          >
            <a
              @click="$event.preventDefault()"
              v-html="$sanitize(getMatchedSiteName(site.name))"
              tabindex="4"
              :href="getUrlForSiteId(site.idsite)"
              :title="site.name"
            />
          </li>
        </ul>
        <ul
          v-show="!sites.length && searchTerm"
          class="custom_select_ul_list"
        >
          <li>
            <div class="noresult">
              {{ translate('SitesManager_NotFound') + ' ' + searchTerm }}
            </div>
          </li>
        </ul>
      </div>
      <div v-if="allSitesLocation === 'bottom' && showAllSitesItem">
        <AllSitesLink
          :href="urlAllSites"
          :all-sites-text="allSitesText"
          @click="onAllSitesClick($event)"
        />
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { DeepReadonly, defineComponent, PropType } from 'vue';
import Tooltips from '../Tooltips/Tooltips';
import FocusAnywhereButHere from '../FocusAnywhereButHere/FocusAnywhereButHere';
import FocusIf from '../FocusIf/FocusIf';
import AllSitesLink from './AllSitesLink.vue';
import Matomo from '../Matomo/Matomo';
import MatomoUrl from '../MatomoUrl/MatomoUrl';
import { translate } from '../translate';
import SitesStore from './SitesStore';
import debounce from '../debounce';
import SiteRef from './SiteRef';
import Site from './Site';

interface SiteSelectorState {
  searchTerm: string;
  showSitesList: boolean;
  activeSiteId: string;
  isLoading: boolean;
  sites: DeepReadonly<Site[]>;
  autocompleteMinSites: number;
}

export default defineComponent({
  props: {
    modelValue: Object,
    showSelectedSite: {
      type: Boolean,
      default: false,
    },
    showAllSitesItem: {
      type: Boolean,
      default: true,
    },
    switchSiteOnSelect: {
      type: Boolean,
      default: true,
    },
    onlySitesWithAdminAccess: {
      type: Boolean,
      default: false,
    },
    name: {
      type: String,
      default: '',
    },
    allSitesText: {
      type: String,
      default: translate('General_MultiSitesSummary'),
    },
    allSitesLocation: {
      type: String,
      default: 'bottom',
    },
    placeholder: String,
    defaultToFirstSite: Boolean,
    sitesToExclude: {
      type: Array,
      default: () => [] as number[],
    },
    onlySitesWithAtLeastWriteAccess: {
      type: Boolean,
      default: false,
    },
    siteTypesToExclude: {
      type: Array as PropType<string[]>,
      default: () => [] as string[],
    },
  },
  emits: ['update:modelValue', 'blur'],
  components: {
    AllSitesLink,
  },
  directives: {
    FocusAnywhereButHere,
    FocusIf,
    Tooltips,
  },
  watch: {
    searchTerm() {
      this.onSearchTermChanged();
    },
  },
  data(): SiteSelectorState {
    return {
      searchTerm: '',
      activeSiteId: `${Matomo.idSite}`,
      showSitesList: false,
      isLoading: false,
      sites: [],
      autocompleteMinSites: parseInt(Matomo.config.autocomplete_min_sites as string, 10),
    };
  },
  created() {
    this.searchSite = debounce(this.searchSite);

    if (!this.modelValue && Matomo.idSite) {
      this.$emit('update:modelValue', {
        id: Matomo.idSite,
        name: Matomo.helper.htmlDecode(Matomo.siteName),
      });
    }
  },
  mounted() {
    window.initTopControls();

    this.loadInitialSites().then(() => {
      if (this.shouldDefaultToFirstSite) {
        this.$emit('update:modelValue', { id: this.sites[0].idsite, name: this.sites[0].name });
      }
    });

    const shortcutTitle = translate('CoreHome_ShortcutWebsiteSelector');
    Matomo.helper.registerShortcut('w', shortcutTitle, (event: KeyboardEvent) => {
      if (event.altKey) {
        return;
      }
      if (event.preventDefault) {
        event.preventDefault();
      } else {
        event.returnValue = false; // IE
      }

      const selectorLink = this.$refs.selectorLink as HTMLElement;
      if (selectorLink) {
        selectorLink.click();
        selectorLink.focus();
      }
    });
  },
  computed: {
    shouldFocusOnSearch() {
      return (this.showSitesList && this.autocompleteMinSites <= this.sites.length)
        || this.searchTerm;
    },
    selectorLinkTitle() {
      return this.hasMultipleSites && this.displayedModelValue
        ? translate('CoreHome_ChangeCurrentWebsite', this.htmlEntities(this.displayedModelValue.name))
        : '';
    },
    hasMultipleSites() {
      const initialSites = SitesStore.matchesCurrentFilteredState(
        this.onlySitesWithAdminAccess,
        (this.sitesToExclude ? this.sitesToExclude : []) as number[],
        this.onlySitesWithAtLeastWriteAccess,
        (this.siteTypesToExclude ? this.siteTypesToExclude : []) as string[],
      )
        && SitesStore.initialSitesFiltered.value
        && SitesStore.initialSitesFiltered.value.length
        ? SitesStore.initialSitesFiltered.value : SitesStore.initialSites.value;
      return initialSites && initialSites.length > 1;
    },
    firstSiteName() {
      const initialSites = SitesStore.initialSitesFiltered.value
        && SitesStore.initialSitesFiltered.value.length
        ? SitesStore.initialSitesFiltered.value : SitesStore.initialSites.value;
      return initialSites && initialSites.length > 0 ? initialSites[0].name : '';
    },
    urlAllSites() {
      const newQuery = MatomoUrl.stringify({
        ...MatomoUrl.urlParsed.value,
        module: 'MultiSites',
        action: 'index',
        date: MatomoUrl.parsed.value.date,
        period: MatomoUrl.parsed.value.period,
      });
      return `?${newQuery}`;
    },
    shouldDefaultToFirstSite() {
      return !this.modelValue?.id
        && (!this.hasMultipleSites || this.defaultToFirstSite)
        && this.sites[0];
    },
    // using an extra computed property in case SiteSelector is used directly
    // in a vue-entry, and there is no parent component with state to respond
    // to update:modelValue events
    displayedModelValue() {
      if (this.modelValue) {
        return this.modelValue;
      }

      if (Matomo.idSite) {
        return {
          id: Matomo.idSite,
          name: Matomo.helper.htmlDecode(Matomo.siteName),
        };
      }

      if (this.shouldDefaultToFirstSite) {
        return { id: this.sites[0].idsite, name: this.sites[0].name };
      }

      return null;
    },
    tooltipContent() {
      return function tooltipContent(this: HTMLElement) {
        const title = $(this).attr('title') || '';
        return Matomo.helper.htmlEntities(title);
      };
    },
  },
  methods: {
    onSearchTermChanged() {
      if (!this.searchTerm) {
        this.isLoading = false;
        this.loadInitialSites();
      } else {
        this.isLoading = true;
        this.searchSite(this.searchTerm);
      }
    },
    onAllSitesClick(event: MouseEvent) {
      this.switchSite({ id: 'all', name: this.$props.allSitesText }, event);
      this.showSitesList = false;
    },
    switchSite(site: SiteRef, event: KeyboardEvent|MouseEvent) {
      // for Mac OS cmd key needs to be pressed, ctrl key on other systems
      const controlKey = navigator.userAgent.indexOf('Mac OS X') !== -1 ? event.metaKey : event.ctrlKey;

      if (event && controlKey && event.target && (event.target as HTMLLinkElement).href) {
        window.open((event.target as HTMLLinkElement).href, '_blank');
        return;
      }

      this.$emit('update:modelValue', { id: site.id, name: site.name });

      if (!this.switchSiteOnSelect || this.activeSiteId === site.id) {
        return;
      }

      SitesStore.loadSite(site.id);
    },
    onBlur() {
      this.showSitesList = false;
      this.$emit('blur');
    },
    onClickSelector() {
      if (this.hasMultipleSites) {
        this.showSitesList = !this.showSitesList;

        if (!this.isLoading && !this.searchTerm) {
          this.loadInitialSites();
        }
      }
    },
    onPressEnter(event: KeyboardEvent) {
      if (event.key !== 'Enter') {
        return;
      }

      event.preventDefault();

      this.showSitesList = !this.showSitesList;
      if (this.showSitesList && !this.isLoading) {
        this.loadInitialSites();
      }
    },
    getMatchedSiteName(siteName: string) {
      const index = siteName.toUpperCase().indexOf(this.searchTerm.toUpperCase());
      if (index === -1
        || this.isLoading // only highlight when we know the displayed results are for a search
      ) {
        return this.htmlEntities(siteName);
      }

      const previousPart = this.htmlEntities(siteName.substring(0, index));
      const lastPart = this.htmlEntities(
        siteName.substring(index + this.searchTerm.length),
      );

      return `${previousPart}<span class="autocompleteMatched">${this.searchTerm}</span>${lastPart}`;
    },
    loadInitialSites() {
      return SitesStore.loadInitialSites(
        this.onlySitesWithAdminAccess,
        (this.sitesToExclude ? this.sitesToExclude : []) as number[],
        this.onlySitesWithAtLeastWriteAccess,
        (this.siteTypesToExclude ? this.siteTypesToExclude : []) as string[],
      ).then((sites) => {
        this.sites = sites || [];
      });
    },
    searchSite(term: string) {
      this.isLoading = true;

      SitesStore.searchSite(
        term, this.onlySitesWithAdminAccess,
        (this.sitesToExclude ? this.sitesToExclude : []) as number[],
        this.onlySitesWithAtLeastWriteAccess,
        (this.siteTypesToExclude ? this.siteTypesToExclude : []) as string[],
      ).then((sites) => {
        if (term !== this.searchTerm) {
          return; // search term changed in the meantime
        }

        if (sites) {
          this.sites = sites;
        }
      }).finally(() => {
        this.isLoading = false;
      });
    },
    getUrlForSiteId(idSite: string|number) {
      const newQuery = MatomoUrl.stringify({
        ...MatomoUrl.urlParsed.value,
        segment: '',
        idSite,
      });

      const newHash = MatomoUrl.stringify({
        ...MatomoUrl.hashParsed.value,
        segment: '',
        idSite,
      });

      return `?${newQuery}#?${newHash}`;
    },
    htmlEntities(v: string) {
      return Matomo.helper.htmlEntities(v);
    },
  },
});
</script>