clr-angular-static-fix
Version:
1. Install Clarity Icons package through npm:
801 lines (697 loc) • 24.7 kB
text/typescript
/*
* Copyright (c) 2016-2018 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
import { Injectable, OnDestroy, TemplateRef } from '@angular/core';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import { Subscription } from 'rxjs';
import { GHOST_PAGE_ANIMATION } from '../../modal/utils/ghost-page-animations';
import { ClrWizardPage } from '../wizard-page';
import { ButtonHubService } from './button-hub.service';
import { PageCollectionService } from './page-collection.service';
/**
* Performs navigation functions for a wizard and manages the current page. Presented as a
* separate service to encapsulate the behavior of navigating and completing the wizard so
* that it can be shared across the wizard and its sub-components.
*
* The easiest way to access the navigation service is there a reference on your wizard. The
* Following example would allow you to access your instance of the wizard from your host
* component and thereby access the navigation service via YourHostComponent.wizard.navService.
*
* @example
* <clr-wizard #wizard ...>
*
* @example
* export class YourHostComponent {
* @ViewChild("wizard") wizard: Wizard;
* ...
* }
*
*/
export class WizardNavigationService implements OnDestroy {
/**
* Is notified when a previous button is clicked in the wizard. Performs checks
* before alerting the current page of the button click. Enacts navigation to
* the previous page if not overridden at the page level.
*
* @memberof WizardNavigationService
*/
public previousButtonSubscription: Subscription;
/**
* Is notified when a Next button is clicked in the wizard.
*
* @memberof WizardNavigationService
*/
public nextButtonSubscription: Subscription;
/**
* Is notified when a danger button is clicked in the wizard.
*
* @memberof WizardNavigationService
*/
public dangerButtonSubscription: Subscription;
/**
* Is notified when a finish button is clicked in the wizard.
*
* @memberof WizardNavigationService
*/
public finishButtonSubscription: Subscription;
/**
* Is notified when a Custom button is clicked in the wizard.
*
* @memberof WizardNavigationService
*/
public customButtonSubscription: Subscription;
/**
* Is notified when a Cancel button is clicked in the wizard. Notifies the wizard,
* which handles all cancel functionality, if cancel is not overridden at the page
* level.
*
* @memberof WizardNavigationService
*/
public cancelButtonSubscription: Subscription;
/**
* Resets navigation to make the first page current when the page collection service
* emits an event notifying WizardNavigationService that it has reset all pages
* to their pristine, incomplete state.
*
* @memberof WizardNavigationService
*/
public pagesResetSubscription: Subscription;
/**
* Creates an instance of WizardNavigationService. Also sets up subscriptions
* that listen to the button service to determine when a button has been clicked
* in the wizard. Is also responsible for taking action when the page collection
* requests that navigation be reset to its pristine state.
*
* @memberof WizardNavigationService
*/
constructor(public pageCollection: PageCollectionService, public buttonService: ButtonHubService) {
this.previousButtonSubscription = this.buttonService.previousBtnClicked.subscribe(() => {
const currentPage = this.currentPage;
if (this.currentPageIsFirst || currentPage.previousStepDisabled) {
return;
}
currentPage.previousButtonClicked.emit(currentPage);
if (!currentPage.preventDefault) {
this.previous();
}
});
this.nextButtonSubscription = this.buttonService.nextBtnClicked.subscribe(() => {
this.checkAndCommitCurrentPage('next');
});
this.dangerButtonSubscription = this.buttonService.dangerBtnClicked.subscribe(() => {
this.checkAndCommitCurrentPage('danger');
});
this.finishButtonSubscription = this.buttonService.finishBtnClicked.subscribe(() => {
this.checkAndCommitCurrentPage('finish');
});
this.customButtonSubscription = this.buttonService.customBtnClicked.subscribe((type: string) => {
if (!this.wizardStopNavigation) {
this.currentPage.customButtonClicked.emit(type);
}
});
this.cancelButtonSubscription = this.buttonService.cancelBtnClicked.subscribe(() => {
if (this.wizardStopNavigation) {
return;
}
if (this.currentPage.preventDefault) {
this.currentPage.pageOnCancel.emit(this.currentPage);
} else {
this.cancel();
}
});
this.pagesResetSubscription = this.pageCollection.pagesReset.subscribe(() => {
this.setFirstPageCurrent();
});
}
/**
*
* @memberof WizardNavigationService
*/
ngOnDestroy(): void {
this.previousButtonSubscription.unsubscribe();
this.nextButtonSubscription.unsubscribe();
this.dangerButtonSubscription.unsubscribe();
this.finishButtonSubscription.unsubscribe();
this.customButtonSubscription.unsubscribe();
this.cancelButtonSubscription.unsubscribe();
this.pagesResetSubscription.unsubscribe();
}
/**
*
* @memberof WizardNavigationService
*/
private _currentChanged = new Subject<ClrWizardPage>();
/**
* An Observable that is predominantly used amongst the subcomponents and services
* of the wizard. It is recommended that users listen to the ClrWizardPage.onLoad
* (clrWizardPageOnLoad) output instead of this Observable.
*
* @memberof WizardNavigationService
*/
public get currentPageChanged(): Observable<ClrWizardPage> {
// TODO: MAKE SURE EXTERNAL OUTPUTS SAY 'CHANGE' NOT 'CHANGED'
// A BREAKING CHANGE SO AWAITING MINOR RELEASE
return this._currentChanged.asObservable();
}
/**
* A Boolean flag used by the ClrWizardPage to avoid a race condition when pages are
* loading and there is no current page defined.
*
* @memberof WizardNavigationService
*/
public navServiceLoaded = false;
/**
* A boolean flag shared across the Wizard subcomponents that follows the value
* of the Wizard.forceForward (clrWizardForceForwardNavigation) input. When true,
* navigating backwards in the stepnav menu will reset any skipped pages' completed
* state to false.
*
* This is useful when a wizard executes validation on a page-by-page basis when
* the next button is clicked.
*
* @memberof WizardNavigationService
*/
public forceForwardNavigation = false;
/**
* @memberof WizardNavigationService
*/
public get currentPageTitle(): TemplateRef<any> {
// when the querylist of pages is empty. this is the first place it fails...
if (!this.currentPage) {
return null;
}
return this.currentPage.title;
}
/**
* Returns a Boolean that tells you whether or not the current page is the first
* page in the Wizard.
*
* This is helpful for determining whether a page is navigable.
*
* @memberof WizardNavigationService
*/
public get currentPageIsFirst(): boolean {
return this.pageCollection.firstPage === this.currentPage;
}
/**
* Returns a Boolean that tells you whether or not the current page is the
* next to last page in the Wizard.
*
* This is used to determine the animation state of ghost pages.
*
* @memberof WizardNavigationService
*/
public get currentPageIsNextToLast(): boolean {
return this.pageCollection.penultimatePage === this.currentPage;
}
/**
* Returns a Boolean that tells you whether or not the current page is the
* last page in the Wizard.
*
* This is used to determine the animation state of ghost pages as well as
* which buttons should display in the wizard footer.
*
* @memberof WizardNavigationService
*/
public get currentPageIsLast(): boolean {
return this.pageCollection.lastPage === this.currentPage;
}
/**
* @memberof WizardNavigationService
*/
private _currentPage: ClrWizardPage;
/**
* Returns the ClrWizardPage object of the current page or null.
*
* @memberof WizardNavigationService
*/
get currentPage(): ClrWizardPage {
if (!this._currentPage) {
return null;
}
return this._currentPage;
}
/**
* Accepts a ClrWizardPage object, since that object to be the current/active
* page in the wizard, and emits the ClrWizardPage.onLoad (clrWizardPageOnLoad)
* event for that page.
*
* Note that all of this work is bypassed if the ClrWizardPage object is already
* the current page.
*
* @memberof WizardNavigationService
*/
set currentPage(page: ClrWizardPage) {
if (this._currentPage !== page && !this.wizardStopNavigation) {
this._currentPage = page;
page.onLoad.emit(page.id);
this._currentChanged.next(page);
}
}
/**
* (DEPRECATED) A legacy means of setting the current page in the wizard.
* Deprecated in 0.9.4. Accepts a ClrWizardPage object as a parameter and then
* tries to set that page to be the current page in the wizard.
*
* @memberof WizardNavigationService
*/
public setCurrentPage(page: ClrWizardPage): void {
this.currentPage = page;
}
/**
* @memberof WizardNavigationService
*/
private _movedToNextPage = new Subject<boolean>();
/**
* An observable used internally to alert the wizard that forward navigation
* has occurred. It is recommended that you use the Wizard.onMoveNext
* (clrWizardOnNext) output instead of this one.
*
* @memberof WizardNavigationService
*/
public get movedToNextPage(): Observable<boolean> {
return this._movedToNextPage.asObservable();
}
/**
* @memberof WizardNavigationService
*/
private _wizardFinished = new Subject<boolean>();
/**
* An observable used internally to alert the wizard that the nav service
* has approved completion of the wizard.
*
* It is recommended that you use the Wizard.wizardFinished (clrWizardOnFinish)
* output instead of this one.
*
* @memberof WizardNavigationService
*/
public get wizardFinished(): Observable<boolean> {
return this._wizardFinished.asObservable();
}
/**
* This is a public function that can be used to programmatically advance
* the user to the next page.
*
* When invoked, this method will move the wizard to the next page after
* successful validation. Note that this method goes through all checks
* and event emissions as if Wizard.next(false) had been called.
*
* In most cases, it makes more sense to use Wizard.next(false).
*
* @memberof WizardNavigationService
*/
public next(): void {
if (this.currentPageIsLast) {
this.checkAndCommitCurrentPage('finish');
return;
}
this.checkAndCommitCurrentPage('next');
if (!this.wizardHasAltNext && !this.wizardStopNavigation) {
this._movedToNextPage.next(true);
}
}
/**
* Bypasses checks and most event emissions to force a page to navigate forward.
*
* Comparable to calling Wizard.next() or Wizard.forceNext().
*
* @memberof WizardNavigationService
*/
public forceNext(): void {
const currentPage: ClrWizardPage = this.currentPage;
const nextPage: ClrWizardPage = this.pageCollection.getNextPage(currentPage);
// catch errant null or undefineds that creep in
if (!nextPage) {
throw new Error('The wizard has no next page to go to.');
}
if (this.wizardStopNavigation) {
return;
}
if (!currentPage.completed) {
// this is a state that alt next flows can get themselves in...
this.pageCollection.commitPage(currentPage);
}
this.currentPage = nextPage;
}
/**
* Accepts a button/action type as a parameter. Encapsulates all logic for
* event emissions, state of the current page, and wizard and page level overrides.
*
* Avoid calling this function directly unless you really know what you're doing.
*
* @memberof WizardNavigationService
*/
public checkAndCommitCurrentPage(buttonType: string): void {
const currentPage: ClrWizardPage = this.currentPage;
let iAmTheLastPage: boolean;
let isNext: boolean;
let isDanger: boolean;
let isDangerNext: boolean;
let isDangerFinish: boolean;
let isFinish: boolean;
if (!currentPage.readyToComplete || this.wizardStopNavigation) {
return;
}
iAmTheLastPage = this.currentPageIsLast;
isNext = buttonType === 'next';
isDanger = buttonType === 'danger';
isDangerNext = isDanger && !iAmTheLastPage;
isDangerFinish = isDanger && iAmTheLastPage;
isFinish = buttonType === 'finish' || isDangerFinish;
if (isFinish && !iAmTheLastPage) {
return;
}
currentPage.primaryButtonClicked.emit(buttonType);
if (isFinish) {
currentPage.finishButtonClicked.emit(currentPage);
} else if (isDanger) {
currentPage.dangerButtonClicked.emit();
} else if (isNext) {
currentPage.nextButtonClicked.emit();
}
if (currentPage.stopNext || currentPage.preventDefault) {
currentPage.onCommit.emit(currentPage.id);
return;
}
// order is very important with these emitters!
if (isFinish) {
// mark page as complete
if (!this.wizardHasAltNext) {
this.pageCollection.commitPage(currentPage);
}
this._wizardFinished.next();
}
if (this.wizardHasAltNext) {
this.pageCollection.commitPage(currentPage);
if (isNext || isDangerNext) {
this._movedToNextPage.next(true);
}
// jump out here, no matter what type we're looking at
return;
}
if (isNext || isDangerNext) {
this.forceNext();
}
}
/**
* This is a public function that can be used to programmatically conclude
* the wizard.
*
* When invoked, this method will initiate the work involved with finalizing
* and finishing the wizard workflow. Note that this method goes through all
* checks and event emissions as if Wizard.finish(false) had been called.
*
* In most cases, it makes more sense to use Wizard.finish(false).
*
* @memberof WizardNavigationService
*/
public finish(): void {
this.checkAndCommitCurrentPage('finish');
}
/**
* @memberof WizardNavigationService
*/
private _movedToPreviousPage = new Subject<boolean>();
/**
* Notifies the wizard when backwards navigation has occurred via the
* previous button.
*
* @memberof WizardNavigationService
*/
public get movedToPreviousPage(): Observable<boolean> {
return this._movedToPreviousPage.asObservable();
}
/**
* Programmatically moves the wizard to the page before the current page.
*
* In most instances, it makes more sense to call Wizard.previous()
* which does the same thing.
*
* @memberof WizardNavigationService
*/
public previous(): void {
let previousPage: ClrWizardPage;
if (this.currentPageIsFirst || this.wizardStopNavigation) {
return;
}
previousPage = this.pageCollection.getPreviousPage(this.currentPage);
if (!previousPage) {
return;
}
this._movedToPreviousPage.next(true);
if (this.forceForwardNavigation) {
this.currentPage.completed = false;
}
this.currentPage = previousPage;
}
/**
* @memberof WizardNavigationService
*/
private _cancelWizard = new Subject<any>();
/**
* Notifies the wizard that a user is trying to cancel it.
*
* @memberof WizardNavigationService
*/
public get notifyWizardCancel(): Observable<any> {
return this._cancelWizard.asObservable();
}
/**
* Allows a hook into the cancel workflow of the wizard from the nav service. Note that
* this route goes through all checks and event emissions as if a cancel button had
* been clicked.
*
* In most cases, users looking for a hook into the cancel routine are actually looking
* for a way to close the wizard from their host component because they have prevented
* the default cancel action.
*
* In this instance, it is recommended that you use Wizard.close() to avoid any event
* emission loop resulting from an event handler calling back into routine that will
* again evoke the events it handles.
*
* @memberof WizardNavigationService
*/
public cancel(): void {
this._cancelWizard.next();
}
/**
* A boolean flag shared across the Wizard subcomponents that follows the value
* of the Wizard.stopCancel (clrWizardPreventDefaultCancel) input. When true, the cancel
* routine is subverted and must be reinstated in the host component calling Wizard.close()
* at some point.
*
* @memberof WizardNavigationService
*/
public wizardHasAltCancel: boolean = false;
/**
* A boolean flag shared across the Wizard subcomponents that follows the value
* of the Wizard.stopNext (clrWizardPreventDefaultNext) input. When true, the next and finish
* routines are subverted and must be reinstated in the host component calling Wizard.next(),
* Wizard.forceNext(), Wizard.finish(), or Wizard.forceFinish().
*
* @memberof WizardNavigationService
*/
public wizardHasAltNext: boolean = false;
/**
* A boolean flag shared across the Wizard subcomponents that follows the value
* of the Wizard.stopNavigation (clrWizardPreventNavigation) input. When true, all
* navigational elements in the wizard are disabled.
*
* This is intended to freeze the wizard in place. Events are not fired so this is
* not a way to implement alternate functionality for navigation.
*
* @memberof WizardNavigationService
*/
public wizardStopNavigation: boolean = false;
/**
* A boolean flag shared with the stepnav items that prevents user clicks on
* stepnav items from navigating the wizard.
*
* @memberof WizardNavigationService
*/
public wizardDisableStepnav: boolean = false;
/**
* Performs all required checks to determine if a user can navigate to a page. Checking at each
* point if a page is navigable -- completed where the page immediately after the last completed
* page.
*
* Takes two parameters. The first one must be either the ClrWizardPage object or the ID of the
* ClrWizardPage object that you want to make the current page.
*
* The second parameter is optional and is a Boolean flag for "lazy completion". What this means
* is the Wizard will mark all pages between the current page and the page you want to navigate
* to as completed. This is useful for informational wizards that do not require user action,
* allowing an easy means for users to jump ahead.
*
* To avoid checks on navigation, use ClrWizardPage.makeCurrent() instead.
*
* @memberof WizardNavigationService
*/
public goTo(pageToGoToOrId: any, lazyComplete: boolean = false) {
let pageToGoTo: ClrWizardPage;
let currentPage: ClrWizardPage;
let myPages: PageCollectionService;
let pagesToCheck: ClrWizardPage[];
let okayToMove: boolean = true;
let goingForward: boolean;
let currentPageIndex: number;
let goToPageIndex: number;
myPages = this.pageCollection;
pageToGoTo = typeof pageToGoToOrId === 'string' ? myPages.getPageById(pageToGoToOrId) : pageToGoToOrId;
currentPage = this.currentPage;
// no point in going to the current page. you're there already!
// also hard block on any navigation when stopNavigation is true
if (pageToGoTo === currentPage || this.wizardStopNavigation) {
return;
}
currentPageIndex = myPages.getPageIndex(currentPage);
goToPageIndex = myPages.getPageIndex(pageToGoTo);
goingForward = goToPageIndex > currentPageIndex;
pagesToCheck = myPages.getPageRangeFromPages(this.currentPage, pageToGoTo);
okayToMove = lazyComplete || this.canGoTo(pagesToCheck);
if (!okayToMove) {
return;
}
if (goingForward && lazyComplete) {
pagesToCheck.forEach((page: ClrWizardPage) => {
if (page !== pageToGoTo) {
page.completed = true;
}
});
} else if (!goingForward && this.forceForwardNavigation) {
pagesToCheck.forEach((page: ClrWizardPage) => {
page.completed = false;
});
}
this.currentPage = pageToGoTo;
}
/**
* Accepts a range of ClrWizardPage objects as a parameter. Performs the work of checking
* those objects to determine if navigation can be accomplished.
*
* @memberof WizardNavigationService
*/
public canGoTo(pagesToCheck: ClrWizardPage[]): boolean {
let okayToMove = true;
const myPages = this.pageCollection;
// previous page can be important when moving because if it's completed it
// allows us to move to the page even if it's incomplete...
let previousPagePasses: boolean;
if (!pagesToCheck || pagesToCheck.length < 1) {
return false;
}
pagesToCheck.forEach((page: ClrWizardPage) => {
let previousPage: ClrWizardPage;
if (!okayToMove) {
return;
}
if (page.completed) {
// default is true. just jump out instead of complicating it.
return;
}
// so we know our page is not completed...
previousPage = myPages.getPageIndex(page) > 0 ? myPages.getPreviousPage(page) : null;
previousPagePasses = previousPage === null || previousPage.completed === true;
// we are false if not the current page AND previous page is not completed
// (but must have a previous page)
if (!page.current && !previousPagePasses) {
okayToMove = false;
}
// falls through to true as default
});
return okayToMove;
}
/**
* Looks through the collection of pages to find the first one that is incomplete
* and makes that page the current/active page.
*
* @memberof WizardNavigationService
*/
public setLastEnabledPageCurrent(): void {
const allPages: ClrWizardPage[] = this.pageCollection.pagesAsArray;
let lastCompletedPageIndex: number = null;
allPages.forEach((page: ClrWizardPage, index: number) => {
if (page.completed) {
lastCompletedPageIndex = index;
}
});
if (lastCompletedPageIndex === null) {
// always is at least the first item...
lastCompletedPageIndex = 0;
} else if (lastCompletedPageIndex + 1 < allPages.length) {
lastCompletedPageIndex = lastCompletedPageIndex + 1;
}
this.currentPage = allPages[lastCompletedPageIndex];
}
/**
* Finds the first page in the collection of pages and makes that page the
* current/active page.
*
* @memberof WizardNavigationService
*/
public setFirstPageCurrent(): void {
this.currentPage = this.pageCollection.pagesAsArray[0];
}
/**
* @memberof WizardNavigationService
* @deprecated since 0.12
*/
private _wizardGhostPageState: string = GHOST_PAGE_ANIMATION.STATES.NO_PAGES;
/**
* @memberof WizardNavigationService
* @deprecated since 0.12
*/
public get wizardGhostPageState(): string {
return this._wizardGhostPageState;
}
/**
* @memberof WizardNavigationService
* @deprecated since 0.12
*/
public set wizardGhostPageState(value: string) {
if (this.hideWizardGhostPages) {
this._wizardGhostPageState = GHOST_PAGE_ANIMATION.STATES.NO_PAGES;
} else {
this._wizardGhostPageState = value;
}
}
/**
* @memberof WizardNavigationService
* @deprecated since 0.12
*/
private _hideWizardGhostPages: boolean = true;
/**
* @memberof WizardNavigationService
* @deprecated since 0.12
*/
public get hideWizardGhostPages(): boolean {
return this._hideWizardGhostPages;
}
/**
* @memberof WizardNavigationService
* @deprecated since 0.12
*/
public set hideWizardGhostPages(value: boolean) {
this._hideWizardGhostPages = value;
}
/**
* Updates the stepnav on the left side of the wizard when pages are dynamically
* added or removed from the collection of pages.
*
* @memberof WizardNavigationService
*/
public updateNavigation(): void {
let toSetCurrent: ClrWizardPage;
let currentPageRemoved: boolean;
this.pageCollection.updateCompletedStates();
currentPageRemoved = this.pageCollection.pagesAsArray.indexOf(this.currentPage) < 0;
if (currentPageRemoved) {
toSetCurrent = this.pageCollection.findFirstIncompletePage();
this.currentPage = toSetCurrent;
}
}
}