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.circ/plugins/CoreHome/vue/src/DatePicker/DatePicker.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"/>
</template>

<script lang="ts">
import {
  defineComponent,
  ref,
  watch,
  onMounted,
} from 'vue';
import Matomo from '../Matomo/Matomo';
import { parseDate } from '../Periods';

const DEFAULT_STEP_MONTHS = 1;

const { $ } = window;

export default defineComponent({
  props: {
    selectedDateStart: Date,
    selectedDateEnd: Date,
    highlightedDateStart: Date,
    highlightedDateEnd: Date,
    viewDate: [String, Date],
    stepMonths: Number,
    disableMonthDropdown: Boolean,
    options: Object,
  },
  emits: ['cellHover', 'cellHoverLeave', 'dateSelect'],
  setup(props, context) {
    const root = ref<HTMLElement|null>(null);

    function setDateCellColor($dateCell: JQuery, dateValue: Date): void {
      const $dateCellLink = $dateCell.children('a');

      if (props.selectedDateStart
        && props.selectedDateEnd
        && dateValue >= props.selectedDateStart
        && dateValue <= props.selectedDateEnd
      ) {
        $dateCell.addClass('ui-datepicker-current-period');
      } else {
        $dateCell.removeClass('ui-datepicker-current-period');
      }

      if (props.highlightedDateStart
        && props.highlightedDateEnd
        && dateValue >= props.highlightedDateStart
        && dateValue <= props.highlightedDateEnd
      ) {
        // other-month cells don't have links, so the <td> must have the ui-state-hover class
        const elementToAddClassTo = $dateCellLink.length ? $dateCellLink : $dateCell;
        elementToAddClassTo.addClass('ui-state-hover');
      } else {
        $dateCell.removeClass('ui-state-hover');
        $dateCellLink.removeClass('ui-state-hover');
      }
    }

    function getCellDate($dateCell: JQuery, month: number, year: number): Date {
      if ($dateCell.hasClass('ui-datepicker-other-month')) {
        return getOtherMonthDate($dateCell, month, year); // eslint-disable-line
      }

      const day = parseInt($dateCell.children('a,span').text(), 10);

      return new Date(year, month, day);
    }

    function getOtherMonthDate($dateCell: JQuery, month: number, year: number) {
      let date;

      const $row = $dateCell.parent();
      const $rowCells = $row.children('td');

      // if in the first row, the date cell is before the current month
      if ($row.is(':first-child')) {
        const $firstDateInMonth = $row.children('td:not(.ui-datepicker-other-month)').first();

        date = getCellDate($firstDateInMonth, month, year);
        date.setDate($rowCells.index($dateCell) - $rowCells.index($firstDateInMonth) + 1);
        return date;
      }

      // the date cell is after the current month
      const $lastDateInMonth = $row.children('td:not(.ui-datepicker-other-month)').last();

      date = getCellDate($lastDateInMonth, month, year);
      date.setDate(date.getDate() + $rowCells.index($dateCell) - $rowCells.index($lastDateInMonth));
      return date;
    }

    function getMonthYearDisplayed(): number[] {
      const element = $(root.value!) as JQuery;

      const $firstCellWithMonth = element.find('td[data-month]');
      const month = parseInt($firstCellWithMonth.attr('data-month')!, 10);
      const year = parseInt($firstCellWithMonth.attr('data-year')!, 10);

      return [month, year];
    }

    function setDatePickerCellColors() {
      const element = $(root.value!);

      const $calendarTable = element.find('.ui-datepicker-calendar');

      const monthYear = getMonthYearDisplayed();

      // highlight the rest of the cells by first getting the date for the first cell
      // in the calendar, then just incrementing by one for the rest of the cells.
      const $cells = $calendarTable.find('td');
      const $firstDateCell = $cells.first();
      const currentDate = getCellDate($firstDateCell, monthYear[0], monthYear[1]);

      $cells.each(function setCellColor() {
        setDateCellColor($(this), currentDate);

        currentDate.setDate(currentDate.getDate() + 1);
      });
    }

    function viewDateChanged(): boolean {
      if (!props.viewDate) {
        return false;
      }

      let date: Date;
      if (typeof props.viewDate === 'string') {
        try {
          date = parseDate(props.viewDate);
        } catch (e) {
          return false;
        }
      } else {
        date = props.viewDate as Date;
      }

      const element = $(root.value!);

      // only change the datepicker date if the date is outside of the current month/year.
      // this avoids a re-render in other cases.
      const monthYear = getMonthYearDisplayed();
      if (monthYear[0] !== date.getMonth() || monthYear[1] !== date.getFullYear()) {
        element.datepicker('setDate', date);
        return true;
      }

      return false;
    }

    // remove the ui-state-active class & click handlers for every cell. we bypass
    // the datepicker's date selection logic for smoother browser rendering.
    function onJqueryUiRenderedPicker(): void {
      const element = $(root.value!);

      element.find('td[data-event]').off('click');
      element.find('.ui-state-active').removeClass('ui-state-active');
      element.find('.ui-datepicker-current-day').removeClass('ui-datepicker-current-day');

      // add href to left/right nav in calendar so they can be accessed via keyboard
      element.find('.ui-datepicker-prev,.ui-datepicker-next').attr('href', '');
    }

    function stepMonthsChanged(): boolean {
      const element = $(root.value!);

      const stepMonths = props.stepMonths || DEFAULT_STEP_MONTHS;
      if (element.datepicker('option', 'stepMonths') === stepMonths) {
        return false;
      }

      // setting stepMonths will change the month in view back to the selected date. to avoid
      // we set the selected date to the month in view.
      const currentMonth = $('.ui-datepicker-month', element).val() as number;
      const currentYear = $('.ui-datepicker-year', element).val() as number;

      element
        .datepicker('option', 'stepMonths', stepMonths)
        .datepicker('setDate', new Date(currentYear, currentMonth));

      onJqueryUiRenderedPicker();

      return true;
    }

    function enableDisableMonthDropdown(): void {
      const element = $(root.value!);
      const monthPicker = element.find('.ui-datepicker-month')[0] as HTMLInputElement;
      if (monthPicker) {
        monthPicker.disabled = props.disableMonthDropdown;
      }
    }

    function handleOtherMonthClick(this: HTMLElement) {
      if (!$(this).hasClass('ui-state-hover')) {
        return;
      }

      const $row = $(this).parent();
      const $tbody = $row.parent();

      if ($row.is(':first-child')) {
        // click on first of the month
        $tbody.find('a').first().click();
      } else {
        // click on last of month
        $tbody.find('a').last().click();
      }
    }

    function onCalendarViewChange() {
      // clicking left/right re-enables the month dropdown, so we disable it again
      enableDisableMonthDropdown();

      setDatePickerCellColors();
    }

    // on a prop change (NOTE: we can't watch just `props`, since then newProps and oldProps will
    // have the same values (since it is a proxy object). Using a copy doesn't quite work, the
    // object it returns will always be different, BUT, since we check what changes it works
    // for our purposes. The only downside is that it runs on every tick basically, but since
    // that is within the context of the date picker component, it's bearable.
    watch(() => ({ ...props }), (newProps: typeof props, oldProps: typeof props) => {
      let redraw = false;

      [
        (x: typeof props): Date|undefined => x.selectedDateStart,
        (x: typeof props): Date|undefined => x.selectedDateEnd,
        (x: typeof props): Date|undefined => x.highlightedDateStart,
        (x: typeof props): Date|undefined => x.highlightedDateEnd,
      ].forEach((selector) => {
        if (redraw) {
          return;
        }

        const newProp = selector(newProps);
        const oldProp = selector(oldProps);

        if (!newProp && oldProp) {
          redraw = true;
        }

        if (newProp && !oldProp) {
          redraw = true;
        }

        if (newProp
          && oldProp
          && newProp.getTime() !== oldProp.getTime()
        ) {
          redraw = true;
        }
      });

      if (newProps.viewDate !== oldProps.viewDate && viewDateChanged()) {
        redraw = true;
      }

      if (newProps.stepMonths !== oldProps.stepMonths) {
        stepMonthsChanged();
      }

      if (newProps.disableMonthDropdown !== oldProps.disableMonthDropdown) {
        enableDisableMonthDropdown();
      }

      // redraw when selected/highlighted dates change
      if (redraw) {
        setDatePickerCellColors();
      }
    });

    onMounted(() => {
      const element = $(root.value!);

      const customOptions = props.options || {};
      const datePickerOptions = {
        ...Matomo.getBaseDatePickerOptions(),
        ...customOptions,
        onChangeMonthYear: () => {
          // datepicker renders the HTML after this hook is called, so we use setTimeout
          // to run some code after the render.
          setTimeout(() => {
            onJqueryUiRenderedPicker();
          });
        },
      };
      element.datepicker(datePickerOptions);

      element.on('mouseover', 'tbody td a', (event) => {
        // this event is triggered when a user clicks a date as well. in that case,
        // the originalEvent is null. we don't need to redraw again for that, so
        // we ignore events like that.
        if (event.originalEvent) {
          setDatePickerCellColors();
        }
      });

      // on hover cell, execute scope.cellHover()
      element.on('mouseenter', 'tbody td', function onMouseEnter() {
        const monthYear = getMonthYearDisplayed();

        const $dateCell = $(this);
        const dateValue = getCellDate($dateCell, monthYear[0], monthYear[1]);
        context.emit('cellHover', { date: dateValue, $cell: $dateCell });
      });

      // overrides jquery UI handler that unhighlights a cell when the mouse leaves it
      element.on('mouseout', 'tbody td a', () => {
        setDatePickerCellColors();
      });

      // call scope.cellHoverLeave() when mouse leaves table body (can't do event on tbody, for
      // some reason that fails, so we do two events, one on the table & one on thead)
      element
        .on('mouseleave', 'table', () => context.emit('cellHoverLeave'))
        .on('mouseenter', 'thead', () => context.emit('cellHoverLeave'));

      // make sure whitespace is clickable when the period makes it appropriate
      element.on('click', 'tbody td.ui-datepicker-other-month', handleOtherMonthClick);

      // NOTE: using a selector w/ .on() doesn't seem to work for some reason...
      element.on('click', (e) => {
        e.preventDefault();

        const $target = $(e.target).closest('a');
        if (!$target.is('.ui-datepicker-next')
          && !$target.is('.ui-datepicker-prev')
        ) {
          return;
        }

        onCalendarViewChange();
      });

      // when a cell is clicked, invoke the onDateSelected function. this, in conjunction
      // with onJqueryUiRenderedPicker(), overrides the date picker's click behavior.
      element.on('click', 'td[data-month]', (event) => {
        const $cell = $(event.target).closest('td') as JQuery;
        const month = parseInt($cell.attr('data-month')!, 10);
        const year = parseInt($cell.attr('data-year')!, 10);
        const day = parseInt($cell.children('a,span').text(), 10);
        context.emit('dateSelect', { date: new Date(year, month, day) });
      });

      const renderPostProcessed = stepMonthsChanged();

      viewDateChanged();
      enableDisableMonthDropdown();

      if (!renderPostProcessed) {
        onJqueryUiRenderedPicker();
      }

      setDatePickerCellColors();
    });

    return {
      root,
    };
  },
});
</script>