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/PeriodSelector/PeriodSelector.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
    ref="root"
    class="periodSelector piwikSelector"
    :class="{'periodSelector-withPrevNext': canShowMovePeriod}"
    v-expand-on-click="{
      expander: 'title',
      onExpand: onExpand,
      onClosed: onClosed,
    }"
  >
    <button
      v-if="canShowMovePeriod"
      class="move-period move-period-prev"
      @click="movePeriod(-1)"
      :disabled="isPeriodMoveDisabled(-1)"
    >
      <span class="icon-chevron-left"></span>
    </button>

    <button
      ref="title"
      id="date"
      class="title"
      tabindex="4"
      v-tooltips
      :title="translate('General_ChooseDate', currentlyViewingText)"
    >
      <span class="icon icon-calendar" />
      {{ currentlyViewingText }}
    </button>

    <div
      id="periodMore"
      class="dropdown"
    >
      <div class="flex">
        <div>
          <DateRangePicker
            v-show="selectedPeriod === 'range'"
            class="period-range"
            :start-date="startRangeDate"
            :end-date="endRangeDate"
            @range-change="onRangeChange($event.start, $event.end)"
            @submit="onApplyClicked()"
          >
          </DateRangePicker>
          <div
            class="period-date"
            v-if="selectedPeriod !== 'range'"
          >
            <PeriodDatePicker
              id="datepicker"
              :period="selectedPeriod"
              :date="periodValue === selectedPeriod ? dateValue : null"
              @select="setPiwikPeriodAndDate(selectedPeriod, $event.date)"
            >
            </PeriodDatePicker>
          </div>
        </div>
        <div class="period-type">
          <h6>{{ translate('General_Period') }}</h6>
          <div id="otherPeriods">
            <p
              v-for="period in periodsFiltered"
              :key="period"
            >
              <label
                :class="{ 'selected-period-label': period === selectedPeriod }"
                @dblclick="changeViewedPeriod(period)"
                :title="period === periodValue
                  ? ''
                  : translate('General_DoubleClickToChangePeriod')"
              >
                <input
                  type="radio"
                  name="period"
                  :id="`period_id_${ period }`"
                  v-model="selectedPeriod"
                  :checked="selectedPeriod === period"
                  @change="selectedPeriod = period"
                  @dblclick="changeViewedPeriod(period)"
                />
                <span>{{ getPeriodDisplayText(period) }}</span>
              </label>
            </p>
          </div>
        </div>
      </div>
      <div
        class="compare-checkbox"
        v-if="isComparisonEnabled"
      >
        <label>
          <input
            id="comparePeriodTo"
            type="checkbox"
            v-model="isComparing"
          />
          <span>{{ translate('General_CompareTo') }}</span>
        </label>
        <div id="comparePeriodToDropdown">
          <Field
            v-model="comparePeriodType"
            :style="{'visibility': isComparing ? 'visible' : 'hidden'}"
            :name="'comparePeriodToDropdown'"
            :uicontrol="'select'"
            :options="comparePeriodDropdownOptions"
            :full-width="true"
            :disabled="!isComparing"
          />
        </div>
      </div>
      <div
        class="compare-date-range"
        v-if="isComparing && comparePeriodType === 'custom'"
      >
        <div>
          <div id="comparePeriodStartDate">
            <div>
              <Field
                v-model="compareStartDate"
                :name="'comparePeriodStartDate'"
                :uicontrol="'text'"
                :full-width="true"
                :title="translate('CoreHome_StartDate')"
                :placeholder="'YYYY-MM-DD'"
              />
            </div>
          </div>
          <span class="compare-dates-separator" />
          <div id="comparePeriodEndDate">
            <div>
              <Field
                v-model="compareEndDate"
                :name="'comparePeriodEndDate'"
                :uicontrol="'text'"
                :full-width="true"
                :title="translate('CoreHome_EndDate')"
                :placeholder="'YYYY-MM-DD'"
              />
            </div>
          </div>
        </div>
      </div>
      <div class="apply-button-container">
        <input
          type="submit"
          id="calendarApply"
          class="btn"
          @click="onApplyClicked()"
          :disabled="!isApplyEnabled()"
          :value="translate('General_Apply')"
        />
      </div>
      <div
        id="ajaxLoadingCalendar"
        v-if="isLoadingNewPage"
      >
        <ActivityIndicator
          :loading="true"
        />
        <div class="loadingSegment">
          {{ translate('SegmentEditor_LoadingSegmentedDataMayTakeSomeTime') }}
        </div>
      </div>
    </div>
    <button
      v-if="canShowMovePeriod"
      class="move-period move-period-next"
      @click="movePeriod(1)"
      :disabled="isPeriodMoveDisabled(1)"
    >
      <span class="icon-chevron-right"></span>
    </button>
  </div>
</template>

<script lang="ts">
import { defineComponent, watch } from 'vue';
import ExpandOnClick from '../ExpandOnClick/ExpandOnClick';
import DateRangePicker from '../DateRangePicker/DateRangePicker.vue';
import PeriodDatePicker from '../PeriodDatePicker/PeriodDatePicker.vue';
import ActivityIndicator from '../ActivityIndicator/ActivityIndicator.vue';
import Matomo from '../Matomo/Matomo';
import { translate } from '../translate';
import ComparisonsStore from '../Comparisons/Comparisons.store.instance';
import useExternalPluginComponent from '../useExternalPluginComponent';
import {
  Periods,
  parseDate,
  Range,
  format,
  datesAreInTheSamePeriod,
} from '../Periods';
import MatomoUrl from '../MatomoUrl/MatomoUrl';
import Tooltips from '../Tooltips/Tooltips';

const Field = useExternalPluginComponent('CorePluginsAdmin', 'Field');

const NBSP = Matomo.helper.htmlDecode('&nbsp;');

const COMPARE_PERIOD_TYPES = ['custom', 'previousPeriod', 'previousYear'];

const COMPARE_PERIOD_OPTIONS = [
  { key: 'custom', value: translate('General_Custom') },
  {
    key: 'previousPeriod',
    value: translate('General_PreviousPeriod').replace(/\s+/, NBSP),
  },
  {
    key: 'previousYear',
    value: translate('General_PreviousYear').replace(/\s+/, NBSP),
  },
];

// the date when the site was created
const piwikMinDate = new Date(Matomo.minDateYear, Matomo.minDateMonth - 1, Matomo.minDateDay);
// today/now
const piwikMaxDate = new Date(Matomo.maxDateYear, Matomo.maxDateMonth - 1, Matomo.maxDateDay);

function isValidDate(d: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
  if (Object.prototype.toString.call(d) !== '[object Date]') {
    return false;
  }

  return !Number.isNaN(d.getTime());
}

interface PeriodSelectorState {
  comparePeriodDropdownOptions: typeof COMPARE_PERIOD_OPTIONS;
  periodValue: string;
  dateValue: Date|null;
  selectedPeriod: string;
  startRangeDate: string|null;
  endRangeDate: string|null;
  isRangeValid: boolean|null;
  isLoadingNewPage: boolean;
  isComparing: null|boolean;
  comparePeriodType: string;
  compareStartDate: string;
  compareEndDate: string;
}

export default defineComponent({
  props: {
    periods: Array,
  },
  components: {
    DateRangePicker,
    PeriodDatePicker,
    Field,
    ActivityIndicator,
  },
  directives: {
    ExpandOnClick,
    Tooltips,
  },
  data(): PeriodSelectorState {
    const selectedPeriod = MatomoUrl.parsed.value.period as string;
    return {
      comparePeriodDropdownOptions: COMPARE_PERIOD_OPTIONS,
      periodValue: selectedPeriod,
      dateValue: null,
      selectedPeriod,
      startRangeDate: null,
      endRangeDate: null,
      isRangeValid: null,
      isLoadingNewPage: false,
      isComparing: null,
      comparePeriodType: 'previousPeriod',
      compareStartDate: '',
      compareEndDate: '',
    };
  },
  mounted() {
    Matomo.on('hidePeriodSelector', () => {
      window.$(this.$refs.root as HTMLElement).parent('#periodString').hide();
    });

    // some widgets might hide the period selector using the event above, so ensure it's
    // shown again when switching the page
    Matomo.on('matomoPageChange', () => {
      window.$(this.$refs.root as HTMLElement).parent('#periodString').show();
    });

    this.isComparing = ComparisonsStore.isComparingPeriods();
    watch(() => ComparisonsStore.isComparingPeriods(), (newVal) => {
      this.isComparing = newVal;
    });

    this.updateSelectedValuesFromHash();
    watch(() => MatomoUrl.parsed.value, this.updateSelectedValuesFromHash);

    this.updateComparisonValuesFromStore();
    watch(() => ComparisonsStore.getPeriodComparisons(), this.updateComparisonValuesFromStore);

    window.initTopControls(); // must be called when a top control changes width

    this.handleZIndexPositionRelativeCompareDropdownIssue();
  },
  computed: {
    currentlyViewingText() {
      let date;
      if (this.periodValue === 'range') {
        if (!this.startRangeDate || !this.endRangeDate) {
          return translate('General_Error');
        }

        date = `${this.startRangeDate},${this.endRangeDate}`;
      } else {
        if (!this.dateValue) {
          return translate('General_Error');
        }

        date = format(this.dateValue);
      }

      try {
        return Periods.parse(this.periodValue!, date).getPrettyString();
      } catch (e) {
        return translate('General_Error');
      }
    },
    isComparisonEnabled() {
      return ComparisonsStore.isComparisonEnabled();
    },
    periodsFiltered() {
      return (this.periods as string[] || []).filter(
        (periodLabel) => Periods.isRecognizedPeriod(periodLabel),
      );
    },
    selectedComparisonParams() {
      if (!this.isComparing) {
        return {};
      }

      if (this.comparePeriodType === 'custom') {
        return {
          comparePeriods: ['range'],
          comparePeriodType: 'custom',
          compareDates: [`${this.compareStartDate},${this.compareEndDate}`],
        };
      }

      if (this.comparePeriodType === 'previousPeriod') {
        return {
          comparePeriods: [this.selectedPeriod],
          comparePeriodType: 'previousPeriod',
          compareDates: [this.previousPeriodDateToSelectedPeriod],
        };
      }

      if (this.comparePeriodType === 'previousYear') {
        const dateStr = this.selectedPeriod === 'range'
          ? `${this.startRangeDate},${this.endRangeDate}`
          : format(this.dateValue!);

        const currentDateRange = Periods.parse(
          this.selectedPeriod as string,
          dateStr,
        ).getDateRange();
        currentDateRange[0].setFullYear(currentDateRange[0].getFullYear() - 1);
        currentDateRange[1].setFullYear(currentDateRange[1].getFullYear() - 1);

        if (this.selectedPeriod === 'range') {
          return {
            comparePeriods: ['range'],
            comparePeriodType: 'previousYear',
            compareDates: [`${format(currentDateRange[0])},${format(currentDateRange[1])}`],
          };
        }

        return {
          comparePeriods: [this.selectedPeriod],
          comparePeriodType: 'previousYear',
          compareDates: [format(currentDateRange[0])],
        };
      }

      console.warn(`Unknown compare period type: ${this.comparePeriodType}`);
      return {};
    },
    previousPeriodDateToSelectedPeriod() {
      if (this.selectedPeriod === 'range') {
        const currentStartRange = parseDate(this.startRangeDate!);
        const currentEndRange = parseDate(this.endRangeDate!);
        const newEndDate = Range.getLastNRange('day', 2, currentStartRange).startDate;

        const rangeSize = Math.floor(
          (currentEndRange.valueOf() - currentStartRange.valueOf()) / 86400000,
        );
        const newRange = Range.getLastNRange('day', 1 + rangeSize, newEndDate);

        return `${format(newRange.startDate)},${format(newRange.endDate)}`;
      }

      const newStartDate = Range.getLastNRange(this.selectedPeriod, 2, this.dateValue!).startDate;
      return format(newStartDate);
    },
    selectedDateString() {
      if (this.selectedPeriod === 'range') {
        const dateFrom = this.startRangeDate!;
        const dateTo = this.endRangeDate!;
        const oDateFrom = parseDate(dateFrom);
        const oDateTo = parseDate(dateTo);

        if (!isValidDate(oDateFrom)
          || !isValidDate(oDateTo)
          || oDateFrom > oDateTo
        ) {
          // TODO: use a notification instead?
          window.$('#alert')
            .find('h2')
            .text(translate('General_InvalidDateRange'));
          Matomo.helper.modalConfirm('#alert', {});
          return null;
        }

        return `${dateFrom},${dateTo}`;
      }

      return format(this.dateValue!);
    },
    isErrorDisplayed() {
      return this.currentlyViewingText === translate('General_Error');
    },
    isRangeSelection() {
      return this.periodValue === 'range';
    },
    canShowMovePeriod() {
      return !this.isRangeSelection && !this.isErrorDisplayed;
    },
  },
  methods: {
    onExpand(event: MouseEvent|KeyboardEvent) {
      const isKeyboardEvent = event.detail === 0;
      if (isKeyboardEvent) {
        window.$(this.$refs.root as HTMLElement).find('.ui-datepicker-month').focus();
      }
    },
    onClosed(event: MouseEvent|KeyboardEvent) {
      const isKeyboardEvent = event.detail === 0;
      if (isKeyboardEvent) {
        window.$(this.$refs.title as HTMLElement).focus();
      }
    },
    handleZIndexPositionRelativeCompareDropdownIssue() {
      const $element = window.$(this.$refs.root as HTMLElement);
      $element.on('focus', '#comparePeriodToDropdown .select-dropdown', () => {
        $element.addClass('compare-dropdown-open');
      }).on('blur', '#comparePeriodToDropdown .select-dropdown', () => {
        $element.removeClass('compare-dropdown-open');
      });
    },
    changeViewedPeriod(period: string) {
      // only change period if it's different from what's being shown currently
      if (period === this.periodValue) {
        return;
      }

      // can't just change to a range period, w/o setting two new dates
      if (period === 'range') {
        return;
      }

      this.setPiwikPeriodAndDate(period, this.dateValue!);
    },
    setPiwikPeriodAndDate(period: string, date: Date) {
      this.periodValue = period;
      this.selectedPeriod = period;
      this.dateValue = date;

      const currentDateString = format(date);
      this.setRangeStartEndFromPeriod(period, currentDateString);

      this.propagateNewUrlParams(currentDateString, this.selectedPeriod);

      window.initTopControls();
    },
    propagateNewUrlParams(date: string, period: string) {
      const compareParams = this.selectedComparisonParams;

      let baseParams: Record<string, unknown>;
      if (Matomo.helper.isReportingPage()) {
        this.closePeriodSelector();
        baseParams = MatomoUrl.hashParsed.value;
      } else {
        this.isLoadingNewPage = true;
        baseParams = MatomoUrl.parsed.value;
      }

      // get params without comparePeriods/compareSegments/compareDates
      const paramsWithoutCompare = { ...baseParams };
      delete paramsWithoutCompare.comparePeriods;
      delete paramsWithoutCompare.comparePeriodType;
      delete paramsWithoutCompare.compareDates;

      MatomoUrl.updateLocation({
        ...paramsWithoutCompare,
        date,
        period,
        ...compareParams,
      });
    },
    onApplyClicked() {
      if (this.selectedPeriod === 'range') {
        const dateString = this.selectedDateString;
        if (!dateString) {
          return;
        }

        this.periodValue = 'range';

        this.propagateNewUrlParams(dateString, 'range');
        return;
      }

      this.setPiwikPeriodAndDate(this.selectedPeriod, this.dateValue!);
    },
    updateComparisonValuesFromStore() {
      this.comparePeriodType = 'previousPeriod';
      this.compareStartDate = '';
      this.compareEndDate = '';

      // first is selected period, second is period to compare to
      const comparePeriods = ComparisonsStore.getPeriodComparisons();

      if (comparePeriods.length < 2) {
        return;
      }

      const comparePeriodType = MatomoUrl.parsed.value.comparePeriodType as string;

      if (!COMPARE_PERIOD_TYPES.includes(comparePeriodType)) {
        return;
      }

      this.comparePeriodType = comparePeriodType;

      if (this.comparePeriodType !== 'custom' || comparePeriods[1].params.period !== 'range') {
        return;
      }

      let periodObj;

      try {
        periodObj = Periods.parse(
          comparePeriods[1].params.period,
          comparePeriods[1].params.date,
        ) as Range;
      } catch {
        return;
      }

      const [startDate, endDate] = periodObj.getDateRange();

      this.compareStartDate = format(startDate);
      this.compareEndDate = format(endDate);
    },
    updateSelectedValuesFromHash() {
      const date = MatomoUrl.parsed.value.date as string;
      const period = MatomoUrl.parsed.value.period as string;

      this.periodValue = period;
      this.selectedPeriod = period;

      this.dateValue = null;
      this.startRangeDate = null;
      this.endRangeDate = null;

      try {
        Periods.parse(period, date);
      } catch (e) {
        return;
      }

      if (period === 'range') {
        const periodObj = Periods.get(period).parse(date) as Range;

        const [startDate, endDate] = periodObj.getDateRange();
        this.dateValue = startDate;
        this.startRangeDate = format(startDate);
        this.endRangeDate = format(endDate);
      } else {
        this.dateValue = parseDate(date);
        this.setRangeStartEndFromPeriod(period, date);
      }
    },
    setRangeStartEndFromPeriod(period: string, dateStr: string) {
      const dateRange = Periods.parse(period, dateStr).getDateRange();
      this.startRangeDate = format(dateRange[0] < piwikMinDate ? piwikMinDate : dateRange[0]);
      this.endRangeDate = format(dateRange[1] > piwikMaxDate ? piwikMaxDate : dateRange[1]);
    },
    getPeriodDisplayText(periodLabel: string) {
      return Periods.get(periodLabel).getDisplayText();
    },
    onRangeChange(start: string, end: string) {
      if (!start || !end) {
        this.isRangeValid = false;
        return;
      }

      this.isRangeValid = true;
      this.startRangeDate = start;
      this.endRangeDate = end;
    },
    isApplyEnabled() {
      if (this.selectedPeriod === 'range'
        && !this.isRangeValid
      ) {
        return false;
      }

      if (this.isComparing
        && this.comparePeriodType === 'custom'
        && !this.isCompareRangeValid()
      ) {
        return false;
      }

      return true;
    },
    closePeriodSelector() {
      (this.$refs.root as HTMLElement).classList.remove('expanded');
    },
    isCompareRangeValid() {
      try {
        parseDate(this.compareStartDate);
      } catch (e) {
        return false;
      }

      try {
        parseDate(this.compareEndDate);
      } catch (e) {
        return false;
      }

      return true;
    },
    movePeriod(direction: number) {
      if (!this.canMovePeriod(direction)) {
        return;
      }

      let newDate = new Date();
      if (this.dateValue != null) {
        newDate = this.dateValue;
      }

      switch (this.periodValue) {
        case 'day':
          newDate.setDate(newDate.getDate() + direction);
          break;
        case 'week':
          newDate.setDate(newDate.getDate() + direction * 7);
          break;
        case 'month':
          newDate.setMonth(newDate.getMonth() + direction);
          break;
        case 'year':
          newDate.setFullYear(newDate.getFullYear() + direction);
          break;
        default:
          break;
      }

      // Ensure the date is not outside the min and max dates
      if (this.dateValue! < piwikMinDate) {
        this.dateValue = piwikMinDate;
      }
      if (this.dateValue! > piwikMaxDate) {
        this.dateValue = piwikMaxDate;
      }

      this.onApplyClicked();
    },
    isPeriodMoveDisabled(direction: number) {
      // disable period move when date range is used or when we would go out of the min/max dates
      if (this.dateValue === null) {
        return this.isRangeSelection;
      }
      return this.isRangeSelection || !this.canMovePeriod(direction);
    },
    canMovePeriod(direction: number) {
      if (this.dateValue === null) {
        return false;
      }
      const boundaryDate = (direction === -1) ? piwikMinDate : piwikMaxDate;
      return !datesAreInTheSamePeriod(this.dateValue!, boundaryDate, this.periodValue);
    },
  },
});
</script>