All files / datepicker/components si-calendar-body.component.ts

100% Statements 46/46
100% Branches 24/24
100% Functions 20/20
100% Lines 46/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246                                                                                                  192x                                                           11226x       13629x       27258x       5856x       5856x                 3216x       3978x 2384x   1594x       7964x       1434x 1149x   285x             1434x 1149x   285x                                 1x   185x   185x   185x           185x         185x           185x         185x             185x   185x           185x   185x   185x   185x 192x                 174x 174x 4776x   174x 174x           28884x       14442x       32x       41x 2x   39x       35x   27x 27x        
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { A11yModule } from '@angular/cdk/a11y';
import { NgClass } from '@angular/common';
import {
  booleanAttribute,
  Component,
  computed,
  input,
  model,
  output,
  viewChildren
} from '@angular/core';
 
import { SiCalendarDateCellDirective } from './si-calendar-date-cell.directive';
import { CompareAdapter, DayCompareAdapter } from './si-compare-adapter';
 
/** CSS classes that can be associated with a calendar cell. */
export type CellCssClasses = string | string[] | Set<string> | { [key: string]: any };
 
export interface Cell {
  value: number;
  /** Indicate that the cell is disabled */
  disabled: boolean;
  /** Cell specific aria label */
  ariaLabel: string;
  /** Display value */
  displayValue: string;
  /**
   * Indicate that the cell is a preview, this is dedicated to the calendar month
   * view to where a day could be part of the previous or next month
   */
  isPreview: boolean;
  /**
   * The cell corresponds to today.
   */
  isToday: boolean;
  /** Raw value */
  valueRaw: Date;
  /** Additional CSS classes for the cell */
  cssClasses: CellCssClasses;
}
 
/**
 * Base interface for selections.
 */
abstract class SelectionStrategy {
  constructor(protected compare: CompareAdapter) {}
 
  /**
   * Indicate whether the cell is selected
   */
  abstract isSelected(cell: Cell, start?: Date, end?: Date): boolean;
  /**
   * Cell is between start and end value.
   * start \< Cell value \< end
   */
  abstract inRange(c: Cell, start?: Date, end?: Date): boolean;
  /**
   * Cell is either startValue or endValue
   */
  abstract isRangeSelected(cell: Cell, date?: Date): boolean;
  /**
   * Preview selection range on mouse hover.
   */
  abstract previewRangeHover(cell: Cell, hoverCell?: Cell, start?: Date): boolean;
  /**
   * Preview selection range on mouse hover end of range.
   */
  abstract previewRangeHoverEnd(cell: Cell, hoverCell?: Cell, start?: Date): boolean;
}
 
/**
 * Strategy the handle single selection within the {@link SiCalendarBodyComponent}.
 */
class SingleSelectionStrategy extends SelectionStrategy {
  isSelected(cell: Cell, start?: Date, end?: Date): boolean {
    return this.compare.isEqual(cell.valueRaw, start);
  }
 
  inRange(cell: Cell, start?: Date, end?: Date): boolean {
    return false;
  }
 
  isRangeSelected(cell: Cell, date?: Date): boolean {
    return false;
  }
 
  previewRangeHover(cell: Cell, hoverCell?: Cell, start?: Date): boolean {
    return false;
  }
 
  previewRangeHoverEnd(cell: Cell, hoverCell?: Cell, start?: Date): boolean {
    return false;
  }
}
 
/**
 * Strategy the handle range selection within the {@link SiCalendarBodyComponent}.
 */
class RangeSelectionStrategy extends SelectionStrategy {
  isSelected(cell: Cell, start?: Date, end?: Date): boolean {
    return this.compare.isEqual(cell.valueRaw, start) || this.compare.isEqual(cell.valueRaw, end);
  }
 
  inRange(c: Cell, start?: Date, end?: Date): boolean {
    if (!start || !end) {
      return false;
    }
    return this.compare.isBetween(c.valueRaw, start, end);
  }
 
  isRangeSelected(cell: Cell, date?: Date): boolean {
    return this.compare.isEqual(cell.valueRaw, date);
  }
 
  previewRangeHover(cell: Cell, hoverCell?: Cell, start?: Date): boolean {
    if (!hoverCell || cell.disabled || !start) {
      return false;
    }
    return (
      this.compare.isAfter(cell.valueRaw, start) &&
      this.compare.isEqualOrBefore(cell.valueRaw, hoverCell.valueRaw)
    );
  }
 
  previewRangeHoverEnd(cell: Cell, hoverCell?: Cell, start?: Date): boolean {
    if (!hoverCell || cell.disabled || !start) {
      return false;
    }
    return (
      this.compare.isAfter(cell.valueRaw, start) &&
      this.compare.isEqual(cell.valueRaw, hoverCell.valueRaw)
    );
  }
}
 
@Component({
  // eslint-disable-next-line @angular-eslint/component-selector
  selector: '[si-calendar-body]',
  imports: [NgClass, A11yModule, SiCalendarDateCellDirective],
  templateUrl: './si-calendar-body.component.html',
  host: {
    class: 'si-calendar-body'
  },
  exportAs: 'siCalendarBody'
})
export class SiCalendarBodyComponent {
  /** The active date, the cell which will receive the focus. */
  readonly focusedDate = model.required<Date>();
  /** The date which shall be indicated as currently selected. */
  readonly startDate = input<Date>();
  /** Selected end value which is only considered with enableRangeSelection. */
  readonly endDate = input<Date>();
  /**
   * The cells to display in the table.
   *
   * @defaultValue []
   */
  readonly rows = input<Cell[][]>([]);
  /**
   * Labels for each row, which can be used to display the week number.
   * @defaultValue undefined
   */
  readonly rowLabels = input<string[] | undefined>(undefined);
  /**
   * Additional row label CSS class(es).
   *
   * @defaultValue []
   */
  readonly rowLabelCssClasses = input<CellCssClasses>([]);
  /**
   * Choose the selection strategy between single or range selection.
   * @defaultValue false
   */
  readonly enableRangeSelection = input(false);
  /**
   * Indicate whether a range preview shall be displayed.
   * It's necessary since to display a preview also datepicker has a valid endDate.
   *
   * @defaultValue true
   */
  readonly previewRange = input(true, { transform: booleanAttribute });
  /** The cell which which has the mouse hover. */
  readonly activeHover = model<Cell>();
  /**
   * Compare date functions which are necessary to compare a the dates according the current view.
   *
   * @defaultValue new DayCompareAdapter()
   */
  readonly compareAdapter = input<CompareAdapter>(new DayCompareAdapter());
  /** Emits when a user select a cell via click, space or enter. */
  readonly selectedValueChange = output<Date>();
 
  private readonly calendarDateCells = viewChildren(SiCalendarDateCellDirective);
 
  protected readonly selection = computed(() =>
    this.enableRangeSelection()
      ? new RangeSelectionStrategy(this.compareAdapter())
      : new SingleSelectionStrategy(this.compareAdapter())
  );
 
  /**
   * Focus calendar cell which is marked as active cell.
   */
  focusActiveCell(): void {
    setTimeout(() => {
      const focusedDateCells = this.calendarDateCells().filter(dateCell =>
        this.compareAdapter().isEqual(this.focusedDate()!, dateCell.cell().valueRaw)
      );
      if (focusedDateCells.length > 0) {
        focusedDateCells[0].ref.nativeElement.focus();
      }
    });
  }
 
  protected isActive(cell: Cell): boolean {
    return this.compareAdapter().isEqual(this.focusedDate()!, cell.valueRaw);
  }
 
  protected cellCss(cell: Cell): CellCssClasses {
    return cell.cssClasses;
  }
 
  protected emitActiveHover(cell: Cell): void {
    this.activeHover.set(cell);
  }
 
  protected emitSelectCell(selection: Cell): void {
    if (selection.disabled) {
      return;
    }
    this.selectedValueChange.emit(selection.valueRaw);
  }
 
  protected emitActiveDateChange(cell: Cell): void {
    if (!cell.disabled && !cell.isPreview) {
      // To provide a date-range preview it is necessary to maintain hoverCell also in case of keyboard usage
      this.emitActiveHover(cell);
      this.focusedDate.set(cell.valueRaw);
    }
  }
}