ipsos-components
Version:
Material Design components for Angular
311 lines (264 loc) • 9.52 kB
text/typescript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
ContentChildren,
EventEmitter,
Input,
Output,
QueryList,
Directive,
ElementRef,
Component,
ContentChild,
ViewChild,
TemplateRef,
ViewEncapsulation,
Optional,
Inject,
forwardRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnChanges,
OnDestroy
} from '@angular/core';
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
import {CdkStepLabel} from './step-label';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {AbstractControl} from '@angular/forms';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {Subject} from 'rxjs/Subject';
/** Used to generate unique ID for each stepper component. */
let nextId = 0;
/**
* Position state of the content of each step in stepper that is used for transitioning
* the content into correct position upon step selection change.
*/
export type StepContentPositionState = 'previous' | 'current' | 'next';
/** Change event emitted on selection changes. */
export class StepperSelectionEvent {
/** Index of the step now selected. */
selectedIndex: number;
/** Index of the step previously selected. */
previouslySelectedIndex: number;
/** The step instance now selected. */
selectedStep: CdkStep;
/** The step instance previously selected. */
previouslySelectedStep: CdkStep;
}
export class CdkStep implements OnChanges {
/** Template for step label if it exists. */
stepLabel: CdkStepLabel;
/** Template for step content. */
content: TemplateRef<any>;
/** The top level abstract control of the step. */
stepControl: AbstractControl;
/** Whether user has seen the expanded step content or not. */
interacted = false;
/** Label of the step. */
label: string;
/** Whether the user can return to this step once it has been marked as complted. */
get editable(): boolean { return this._editable; }
set editable(value: boolean) {
this._editable = coerceBooleanProperty(value);
}
private _editable = true;
/** Whether the completion of step is optional. */
get optional(): boolean { return this._optional; }
set optional(value: boolean) {
this._optional = coerceBooleanProperty(value);
}
private _optional = false;
/** Whether step is marked as completed. */
get completed(): boolean {
return this._customCompleted == null ? this._defaultCompleted : this._customCompleted;
}
set completed(value: boolean) {
this._customCompleted = coerceBooleanProperty(value);
}
private _customCompleted: boolean | null = null;
private get _defaultCompleted() {
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted;
}
constructor( private _stepper: CdkStepper) { }
/** Selects this step component. */
select(): void {
this._stepper.selected = this;
}
ngOnChanges() {
// Since basically all inputs of the MdStep get proxied through the view down to the
// underlying MdStepHeader, we have to make sure that change detection runs correctly.
this._stepper._stateChanged();
}
}
export class CdkStepper implements OnDestroy {
/** Emits when the component is destroyed. */
protected _destroyed = new Subject<void>();
/** The list of step components that the stepper is holding. */
_steps: QueryList<CdkStep>;
/** The list of step headers of the steps in the stepper. */
_stepHeader: QueryList<ElementRef>;
/** Whether the validity of previous steps should be checked or not. */
get linear(): boolean { return this._linear; }
set linear(value: boolean) { this._linear = coerceBooleanProperty(value); }
private _linear = false;
/** The index of the selected step. */
get selectedIndex() { return this._selectedIndex; }
set selectedIndex(index: number) {
if (this._steps) {
if (this._anyControlsInvalidOrPending(index) || index < this._selectedIndex &&
!this._steps.toArray()[index].editable) {
// remove focus from clicked step header if the step is not able to be selected
this._stepHeader.toArray()[index].nativeElement.blur();
} else if (this._selectedIndex != index) {
this._emitStepperSelectionEvent(index);
this._focusIndex = this._selectedIndex;
}
} else {
this._selectedIndex = this._focusIndex = index;
}
}
private _selectedIndex: number = 0;
/** The step that is selected. */
get selected() { return this._steps.toArray()[this.selectedIndex]; }
set selected(step: CdkStep) {
this.selectedIndex = this._steps.toArray().indexOf(step);
}
/** Event emitted when the selected step has changed. */
selectionChange = new EventEmitter<StepperSelectionEvent>();
/** The index of the step that the focus can be set. */
_focusIndex: number = 0;
/** Used to track unique ID for each stepper component. */
_groupId: number;
constructor(
private _dir: Directionality,
private _changeDetectorRef: ChangeDetectorRef) {
this._groupId = nextId++;
}
ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}
/** Selects and focuses the next step in list. */
next(): void {
this.selectedIndex = Math.min(this._selectedIndex + 1, this._steps.length - 1);
}
/** Selects and focuses the previous step in list. */
previous(): void {
this.selectedIndex = Math.max(this._selectedIndex - 1, 0);
}
/** Returns a unique id for each step label element. */
_getStepLabelId(i: number): string {
return `cdk-step-label-${this._groupId}-${i}`;
}
/** Returns unique id for each step content element. */
_getStepContentId(i: number): string {
return `cdk-step-content-${this._groupId}-${i}`;
}
/** Marks the component to be change detected. */
_stateChanged() {
this._changeDetectorRef.markForCheck();
}
/** Returns position state of the step with the given index. */
_getAnimationDirection(index: number): StepContentPositionState {
const position = index - this._selectedIndex;
if (position < 0) {
return this._layoutDirection() === 'rtl' ? 'next' : 'previous';
} else if (position > 0) {
return this._layoutDirection() === 'rtl' ? 'previous' : 'next';
}
return 'current';
}
/** Returns the type of icon to be displayed. */
_getIndicatorType(index: number): 'number' | 'edit' | 'done' {
const step = this._steps.toArray()[index];
if (!step.completed || this._selectedIndex == index) {
return 'number';
} else {
return step.editable ? 'edit' : 'done';
}
}
private _emitStepperSelectionEvent(newIndex: number): void {
const stepsArray = this._steps.toArray();
this.selectionChange.emit({
selectedIndex: newIndex,
previouslySelectedIndex: this._selectedIndex,
selectedStep: stepsArray[newIndex],
previouslySelectedStep: stepsArray[this._selectedIndex],
});
this._selectedIndex = newIndex;
this._stateChanged();
}
_onKeydown(event: KeyboardEvent) {
switch (event.keyCode) {
case RIGHT_ARROW:
if (this._layoutDirection() === 'rtl') {
this._focusPreviousStep();
} else {
this._focusNextStep();
}
break;
case LEFT_ARROW:
if (this._layoutDirection() === 'rtl') {
this._focusNextStep();
} else {
this._focusPreviousStep();
}
break;
case SPACE:
case ENTER:
this.selectedIndex = this._focusIndex;
break;
default:
// Return to avoid calling preventDefault on keys that are not explicitly handled.
return;
}
event.preventDefault();
}
private _focusNextStep() {
this._focusStep((this._focusIndex + 1) % this._steps.length);
}
private _focusPreviousStep() {
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
}
private _focusStep(index: number) {
this._focusIndex = index;
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
}
private _anyControlsInvalidOrPending(index: number): boolean {
const steps = this._steps.toArray();
steps[this._selectedIndex].interacted = true;
if (this._linear && index >= 0) {
return steps.slice(0, index).some(step =>
step.stepControl && (step.stepControl.invalid || step.stepControl.pending)
);
}
return false;
}
private _layoutDirection(): Direction {
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}
}