@clr/angular
Version:
Angular components for Clarity
1,295 lines (1,286 loc) • 136 kB
JavaScript
import * as i6 from '@angular/common';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, Output, Input, Component, Directive, ContentChild, ViewChildren, PLATFORM_ID, ViewChild, ContentChildren, Inject, NgModule } from '@angular/core';
import * as i3 from '@clr/angular/utils';
import { uniqueIdFactory } from '@clr/angular/utils';
import { filter } from 'rxjs/operators';
import { Subject, startWith, debounceTime, tap } from 'rxjs';
import * as i5 from '@clr/angular/icon';
import { ClarityIcons, errorStandardIcon, successStandardIcon, ClrIcon } from '@clr/angular/icon';
import * as i8 from '@clr/angular/modal';
import { ClrModalModule } from '@clr/angular/modal';
import { ClrAlertModule } from '@clr/angular/emphasis/alert';
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
var ClrWizardFooterAlign;
(function (ClrWizardFooterAlign) {
ClrWizardFooterAlign["START"] = "start";
ClrWizardFooterAlign["END"] = "end";
})(ClrWizardFooterAlign || (ClrWizardFooterAlign = {}));
const CLR_WIZARD_FOOTER_ALIGN_VALUES = new Set(Object.values(ClrWizardFooterAlign));
function footerAlignAttribute(value) {
if (CLR_WIZARD_FOOTER_ALIGN_VALUES.has(value)) {
return value;
}
throw new Error(`Invalid ClrWizardFooterAlign: "${value}". Expected one of: ${[...CLR_WIZARD_FOOTER_ALIGN_VALUES].join(', ')}`);
}
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
var ClrWizardStepnavLayout;
(function (ClrWizardStepnavLayout) {
ClrWizardStepnavLayout["VERTICAL"] = "vertical";
ClrWizardStepnavLayout["HORIZONTAL"] = "horizontal";
})(ClrWizardStepnavLayout || (ClrWizardStepnavLayout = {}));
const CLR_WIZARD_STEPNAV_LAYOUT_VALUES = new Set(Object.values(ClrWizardStepnavLayout));
function stepnavLayoutAttribute(value) {
if (CLR_WIZARD_STEPNAV_LAYOUT_VALUES.has(value)) {
return value;
}
throw new Error(`Invalid ClrWizardStepnavLayout: "${value}". Expected one of: ${[...CLR_WIZARD_STEPNAV_LAYOUT_VALUES].join(', ')}`);
}
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ButtonHubService {
constructor() {
this.buttonsReady = false;
this._previousBtnClicked = new Subject();
this._nextBtnClicked = new Subject();
this._dangerBtnClicked = new Subject();
this._cancelBtnClicked = new Subject();
this._finishBtnClicked = new Subject();
this._customBtnClicked = new Subject();
}
get previousBtnClicked() {
return this._previousBtnClicked.asObservable();
}
get nextBtnClicked() {
return this._nextBtnClicked.asObservable();
}
get dangerBtnClicked() {
return this._dangerBtnClicked.asObservable();
}
get cancelBtnClicked() {
return this._cancelBtnClicked.asObservable();
}
get finishBtnClicked() {
return this._finishBtnClicked.asObservable();
}
get customBtnClicked() {
return this._customBtnClicked.asObservable();
}
buttonClicked(buttonType) {
if ('previous' === buttonType) {
this._previousBtnClicked.next();
}
else if ('next' === buttonType) {
this._nextBtnClicked.next();
}
else if ('finish' === buttonType) {
this._finishBtnClicked.next();
}
else if ('danger' === buttonType) {
this._dangerBtnClicked.next();
}
else if ('cancel' === buttonType) {
this._cancelBtnClicked.next();
}
else {
this._customBtnClicked.next(buttonType);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ButtonHubService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ButtonHubService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ButtonHubService, decorators: [{
type: Injectable
}] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
/**
* PageCollectionService manages the collection of pages assigned to the wizard and offers
* a number of functions useful across the wizards providers and subcomponents -- all related
* to essentially lookups on the collection of pages.
*
* The easiest way to access PageCollectionService is via the wizard. The
* following example would allow you to access your instance of the wizard from your host
* component and thereby access the page collection via YourHostComponent.wizard.pageCollection.
*
* @example
* <clr-wizard #wizard ...>
*
* @example
* export class YourHostComponent {
* @ViewChild("wizard") wizard: Wizard;
* ...
* }
*
* The heart of the page collection is the query list of pages, which it is assigned as a
* reference to the Wizard.pages QueryList when the wizard is created.
*
*/
class PageCollectionService {
constructor() {
/**
*
* @memberof PageCollectionService
*/
this._pagesReset = new Subject();
}
/**
* Converts the PageCollectionService.pages QueryList to an array and returns it.
*
* Useful for many instances when you would prefer a QueryList to act like an array.
*
* @memberof PageCollectionService
*/
get pagesAsArray() {
return this.pages ? this.pages.toArray() : [];
}
/**
* Returns the length of the pages query list.
*
* @memberof PageCollectionService
*/
get pagesCount() {
return this.pages ? this.pages.length : 0;
}
/**
* Returns the next-to-last page in the query list of pages. Operates as a getter
* so that it isn't working with stale data.
*
* @memberof PageCollectionService
*/
get penultimatePage() {
const pageCount = this.pagesCount;
if (pageCount < 2) {
return null;
}
return this.pagesAsArray[pageCount - 2];
}
/**
* Returns the last page in the query list of pages. Operates as a getter
* so that it isn't working with stale data.
*
* @memberof PageCollectionService
*/
get lastPage() {
const pageCount = this.pagesCount;
if (pageCount < 1) {
return null;
}
return this.pagesAsArray[pageCount - 1];
}
/**
* Returns the first page in the query list of pages. Operates as a getter
* so that it isn't working with stale data.
*
* @memberof PageCollectionService
*/
get firstPage() {
if (!this.pagesCount) {
return null;
}
return this.pagesAsArray[0];
}
/**
* An observable that the navigation service listens to in order to know when
* the page collection completed states have been reset to false so that way it
* can also reset the navigation to make the first page in the page collection
* current/active.
*
* @memberof PageCollectionService
*/
get pagesReset() {
return this._pagesReset.asObservable();
}
/**
* Used mostly internally, but accepts a string ID and returns a ClrWizardPage
* object that matches the ID passed. Note that IDs here should include the prefix
* "clr-wizard-page-".
*
* Returns the next-to-last page in the query list of pages. Operates as a getter
* so that it isn't working with stale data.
*
* @memberof PageCollectionService
*/
getPageById(id) {
const foundPages = this.pages.filter((page) => id === page.id);
return this.checkResults(foundPages, id);
}
/**
* Accepts s number as a parameter and treats that number as the index of the page
* you're looking for in the collection of pages. Returns a wizard page object.
*
* @memberof PageCollectionService
*/
getPageByIndex(index) {
const pageCount = this.pagesCount;
const pagesLastIndex = pageCount > 1 ? pageCount - 1 : 0;
if (index < 0) {
throw new Error('Cannot retrieve page with index of ' + index);
}
if (index > pagesLastIndex) {
throw new Error('Page index is greater than length of pages array.');
}
return this.pagesAsArray[index];
}
/**
* Takes a wizard page object as a parameter and returns its index in the
* collection of pages.
*
* @memberof PageCollectionService
*/
getPageIndex(page) {
const index = this.pagesAsArray.indexOf(page);
if (index < 0) {
throw new Error('Requested page cannot be found in collection of pages.');
}
return index;
}
/**
* Accepts two numeric indexes and returns an array of wizard page objects that include
* all wizard pages in the page collection from the first index to the second.
*
* @memberof PageCollectionService
*/
pageRange(start, end) {
let pages = [];
if (start < 0 || end < 0) {
return [];
}
if (start === null || typeof start === 'undefined' || isNaN(start)) {
return [];
}
if (end === null || typeof end === 'undefined' || isNaN(end)) {
return [];
}
if (end > this.pagesCount) {
end = this.pagesCount;
}
pages = this.pagesAsArray;
if (end - start === 0) {
// just return the one page they want
return [this.getPageByIndex(start)];
}
// slice end does not include item referenced by end index, which is weird for users
// incrementing end index here to correct that so users and other methods
// don't have to think about it
end = end + 1;
// slice does not return the last one in the range but it does include the first one
// does not modify original array
return pages.slice(start, end);
}
/**
* Accepts two wizard page objects and returns those page objects with all other page
* objects between them in the page collection. It doesn't care which page is ahead of the
* other in the parameters. It will be smart enough to figure that out on its own.
*
* @memberof PageCollectionService
*/
getPageRangeFromPages(page, otherPage) {
const pageIndex = this.getPageIndex(page);
const otherPageIndex = this.getPageIndex(otherPage);
let startIndex;
let endIndex;
if (pageIndex <= otherPageIndex) {
startIndex = pageIndex;
endIndex = otherPageIndex;
}
else {
startIndex = otherPageIndex;
endIndex = pageIndex;
}
return this.pageRange(startIndex, endIndex);
}
/**
* Takes a wizard page object as a parameter and returns the wizard page object of
* the page immediately before it in the page collection. Returns null if there is
* no page before the page it is passed.
*
* @memberof PageCollectionService
*/
getPreviousPage(page) {
const myPageIndex = this.getPageIndex(page);
const previousPageIndex = myPageIndex - 1;
if (previousPageIndex < 0) {
return null;
}
return this.getPageByIndex(previousPageIndex);
}
/**
* Accepts a wizard page object as a parameter and returns a Boolean that says if
* the page you sent it is complete.
*
* @memberof PageCollectionService
*/
previousPageIsCompleted(page) {
if (!page) {
return false;
}
const previousPage = this.getPreviousPage(page);
if (null === previousPage) {
// page is the first page. no previous page.
return true;
}
return previousPage.completed;
}
/**
* Takes a wizard page object as a parameter and returns the wizard page object of
* the page immediately after it in the page collection. Returns null if there is
* no page after the page it is passed.
*
* @memberof PageCollectionService
*/
getNextPage(page) {
const myPageIndex = this.getPageIndex(page);
const nextPageIndex = myPageIndex + 1;
if (nextPageIndex >= this.pagesAsArray.length) {
return null;
}
return this.getPageByIndex(nextPageIndex);
}
/**
* Takes a wizard page object as a parameter and generates a step item id from the
* page ID. Returns the generated step item ID as a string.
*
* @memberof PageCollectionService
*/
getStepItemIdForPage(page) {
const pageId = page.id;
const pageIdParts = pageId.split('-').reverse();
pageIdParts[1] = 'step';
return pageIdParts.reverse().join('-');
}
/**
* Generally only used internally to mark that a specific page has been "committed".
* This involves marking the page complete and firing the ClrWizardPage.onCommit
* (clrWizardPageOnCommit) output. Takes the wizard page object that you intend to
* mark completed as a parameter.
*
* @memberof PageCollectionService
*/
commitPage(page) {
const pageHasOverrides = page.stopNext || page.preventDefault;
page.completed = true;
if (!pageHasOverrides) {
// prevent loop of event emission; alternate flows work off
// of event emitters this is how they break that cycle.
page.onCommit.emit(page.id);
}
}
/**
* Sets all completed states of the pages in the page collection to false and
* notifies the navigation service to likewise reset the navigation.
*
* @memberof PageCollectionService
*/
reset() {
this.pagesAsArray.forEach((page) => {
page.completed = false;
});
this._pagesReset.next(true);
}
/**
* Rolls through all the pages in the page collection to make sure there are no
* incomplete pages sandwiched between completed pages in the workflow. Identifies
* the first incomplete page index and sets all pages behind it to a completed
* state of false.
*
* @memberof PageCollectionService
*/
updateCompletedStates() {
const firstIncompleteIndex = this.findFirstIncompletePageIndex();
if (firstIncompleteIndex === this.pagesAsArray.length - 1) {
// all complete no need to do anything
return;
}
this.pagesAsArray.forEach((page, index) => {
if (index > firstIncompleteIndex) {
page.completed = false;
}
});
}
/**
* Retrieves the index of the first incomplete page in the page collection.
*
* @memberof PageCollectionService
*/
findFirstIncompletePageIndex() {
let returnIndex = null;
this.pagesAsArray.forEach((page, index) => {
if (null === returnIndex && false === page.completed) {
returnIndex = index;
}
});
// fallthrough, all completed, return last page
if (null === returnIndex) {
returnIndex = this.pagesCount - 1;
}
return returnIndex;
}
findFirstIncompletePage() {
const myIncompleteIndex = this.findFirstIncompletePageIndex();
return this.pagesAsArray[myIncompleteIndex];
}
/**
* Consolidates guard logic that prevents a couple of unfortunate edge cases with
* look ups on the collection of pages.
*
* @memberof PageCollectionService
*/
checkResults(results, requestedPageId) {
const foundPagesCount = results.length || 0;
if (foundPagesCount > 1) {
throw new Error('More than one page has the requested id ' + requestedPageId + '.');
}
else if (foundPagesCount < 1) {
throw new Error('No page can be found with the id ' + requestedPageId + '.');
}
else {
return results[0];
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PageCollectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PageCollectionService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PageCollectionService, decorators: [{
type: Injectable
}] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
/**
* 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;
* ...
* }
*
*/
class WizardNavigationService {
/**
* 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(pageCollection, buttonService) {
this.pageCollection = pageCollection;
this.buttonService = buttonService;
/**
* 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
*/
this.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
*/
this.forceForwardNavigation = false;
/**
* 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
*/
this.wizardHasAltCancel = 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
*/
this.wizardHasAltNext = 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
*/
this.wizardStopNavigation = false;
/**
* A boolean flag shared with the stepnav items that prevents user clicks on
* stepnav items from navigating the wizard.
*
* @memberof WizardNavigationService
*/
this.wizardDisableStepnav = false;
/**
* The layout of the wizard stepnav, either 'vertical' or 'horizontal'.
*/
this.stepnavLayout = ClrWizardStepnavLayout.VERTICAL;
/**
*
* @memberof WizardNavigationService
*/
this._currentChanged = new Subject();
/**
* @memberof WizardNavigationService
*/
this._movedToNextPage = new Subject();
/**
* @memberof WizardNavigationService
*/
this._wizardFinished = new Subject();
/**
* @memberof WizardNavigationService
*/
this._movedToPreviousPage = new Subject();
/**
* @memberof WizardNavigationService
*/
this._cancelWizard = new Subject();
this.previousButtonSubscription = buttonService.previousBtnClicked.subscribe(() => {
const currentPage = this.currentPage;
if (this.currentPageIsFirst || currentPage.previousStepDisabled) {
return;
}
currentPage.previousButtonClicked.emit(currentPage);
if (!currentPage.preventDefault) {
this.previous();
}
});
this.nextButtonSubscription = buttonService.nextBtnClicked.subscribe(() => {
this.checkAndCommitCurrentPage('next');
});
this.dangerButtonSubscription = buttonService.dangerBtnClicked.subscribe(() => {
this.checkAndCommitCurrentPage('danger');
});
this.finishButtonSubscription = buttonService.finishBtnClicked.subscribe(() => {
this.checkAndCommitCurrentPage('finish');
});
this.customButtonSubscription = buttonService.customBtnClicked.subscribe((type) => {
if (!this.wizardStopNavigation) {
this.currentPage.customButtonClicked.emit(type);
}
});
this.cancelButtonSubscription = buttonService.cancelBtnClicked.subscribe(() => {
if (this.wizardStopNavigation) {
return;
}
if (this.currentPage.preventDefault) {
this.currentPage.pageOnCancel.emit(this.currentPage);
}
else {
this.cancel();
}
});
this.pagesResetSubscription = pageCollection.pagesReset.subscribe(() => {
this.setFirstPageCurrent();
});
}
/**
* 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
*/
get currentPageChange() {
return this._currentChanged.asObservable();
}
/**
* @memberof WizardNavigationService
*/
get currentPageTitle() {
// 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
*/
get currentPageIsFirst() {
return this.pageCollection.firstPage === 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 which buttons should display in the wizard footer.
*
* @memberof WizardNavigationService
*/
get currentPageIsLast() {
return this.pageCollection.lastPage === this.currentPage;
}
/**
* Returns the ClrWizardPage object of the current page or null.
*
* @memberof WizardNavigationService
*/
get currentPage() {
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) {
if (this._currentPage !== page && !this.wizardStopNavigation) {
this._currentPage = page;
page.onLoad.emit(page.id);
this._currentChanged.next(page);
}
}
/**
* 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
*/
get movedToNextPage() {
return this._movedToNextPage.asObservable();
}
/**
* 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
*/
get wizardFinished() {
return this._wizardFinished.asObservable();
}
/**
* Notifies the wizard when backwards navigation has occurred via the
* previous button.
*
* @memberof WizardNavigationService
*/
get movedToPreviousPage() {
return this._movedToPreviousPage.asObservable();
}
/**
* Notifies the wizard that a user is trying to cancel it.
*
* @memberof WizardNavigationService
*/
get notifyWizardCancel() {
return this._cancelWizard.asObservable();
}
/**
*
* @memberof WizardNavigationService
*/
ngOnDestroy() {
this.previousButtonSubscription.unsubscribe();
this.nextButtonSubscription.unsubscribe();
this.dangerButtonSubscription.unsubscribe();
this.finishButtonSubscription.unsubscribe();
this.customButtonSubscription.unsubscribe();
this.cancelButtonSubscription.unsubscribe();
this.pagesResetSubscription.unsubscribe();
}
/**
* 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
*/
next() {
if (this.currentPageIsLast) {
this.checkAndCommitCurrentPage('finish');
}
else {
this.checkAndCommitCurrentPage('next');
}
}
/**
* Bypasses checks and most event emissions to force a page to navigate forward.
*
* Comparable to calling Wizard.next() or Wizard.forceNext().
*
* @memberof WizardNavigationService
*/
forceNext() {
const currentPage = this.currentPage;
const nextPage = 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
*/
checkAndCommitCurrentPage(buttonType) {
const currentPage = this.currentPage;
if (!currentPage.readyToComplete || this.wizardStopNavigation) {
return;
}
const iAmTheLastPage = this.currentPageIsLast;
const isNext = buttonType === 'next';
const isDanger = buttonType === 'danger';
const isDangerNext = isDanger && !iAmTheLastPage;
const isDangerFinish = isDanger && iAmTheLastPage;
const 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();
}
if (!this.wizardHasAltNext && !this.wizardStopNavigation) {
this._movedToNextPage.next(true);
}
}
/**
* 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
*/
finish() {
this.checkAndCommitCurrentPage('finish');
}
/**
* 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
*/
previous() {
if (this.currentPageIsFirst || this.wizardStopNavigation) {
return;
}
const previousPage = this.pageCollection.getPreviousPage(this.currentPage);
if (!previousPage) {
return;
}
this._movedToPreviousPage.next(true);
if (this.forceForwardNavigation) {
this.currentPage.completed = false;
}
this.currentPage = previousPage;
}
/**
* 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
*/
cancel() {
this._cancelWizard.next();
}
/**
* 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
*/
goTo(pageToGoToOrId, lazyComplete = false) {
const myPages = this.pageCollection;
const pageToGoTo = typeof pageToGoToOrId === 'string' ? myPages.getPageById(pageToGoToOrId) : pageToGoToOrId;
const 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;
}
const currentPageIndex = myPages.getPageIndex(currentPage);
const goToPageIndex = myPages.getPageIndex(pageToGoTo);
const goingForward = goToPageIndex > currentPageIndex;
const pagesToCheck = myPages.getPageRangeFromPages(this.currentPage, pageToGoTo);
const okayToMove = lazyComplete || this.canGoTo(pagesToCheck);
if (!okayToMove) {
return;
}
if (goingForward && lazyComplete) {
pagesToCheck.forEach((page) => {
if (page !== pageToGoTo) {
page.completed = true;
}
});
}
else if (!goingForward && this.forceForwardNavigation) {
pagesToCheck.forEach((page) => {
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
*/
canGoTo(pagesToCheck) {
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;
if (!pagesToCheck || pagesToCheck.length < 1) {
return false;
}
pagesToCheck.forEach((page) => {
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...
const 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
*/
setLastEnabledPageCurrent() {
const allPages = this.pageCollection.pagesAsArray;
let lastCompletedPageIndex = null;
allPages.forEach((page, index) => {
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
*/
setFirstPageCurrent() {
this.currentPage = this.pageCollection.pagesAsArray[0];
}
/**
* Updates the stepnav on the left side of the wizard when pages are dynamically
* added or removed from the collection of pages.
*
* @memberof WizardNavigationService
*/
updateNavigation() {
let toSetCurrent;
this.pageCollection.updateCompletedStates();
const currentPageRemoved = this.pageCollection.pagesAsArray.indexOf(this.currentPage) < 0;
if (currentPageRemoved) {
toSetCurrent = this.pageCollection.findFirstIncompletePage();
this.currentPage = toSetCurrent;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: WizardNavigationService, deps: [{ token: PageCollectionService }, { token: ButtonHubService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: WizardNavigationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: WizardNavigationService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: PageCollectionService }, { type: ButtonHubService }] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class HeaderActionService {
constructor(navService) {
this.navService = navService;
}
get wizardHasHeaderActions() {
const wizardHdrActions = this.wizardHeaderActions;
if (!wizardHdrActions) {
return false;
}
return wizardHdrActions.toArray().length > 0;
}
get currentPageHasHeaderActions() {
return this.navService.currentPage ? this.navService.currentPage.hasHeaderActions : false;
}
get showWizardHeaderActions() {
return !this.currentPageHasHeaderActions && this.wizardHasHeaderActions;
}
get displayHeaderActionsWrapper() {
return this.currentPageHasHeaderActions || this.wizardHasHeaderActions;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HeaderActionService, deps: [{ token: WizardNavigationService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HeaderActionService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HeaderActionService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: WizardNavigationService }] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
const DEFAULT_BUTTON_TYPES = {
cancel: 'cancel',
previous: 'previous',
next: 'next',
finish: 'finish',
danger: 'danger',
};
const CUSTOM_BUTTON_TYPES = {
cancel: 'custom-cancel',
previous: 'custom-previous',
next: 'custom-next',
finish: 'custom-finish',
danger: 'custom-danger',
};
class ClrWizardButton {
constructor(navService, buttonService) {
this.navService = navService;
this.buttonService = buttonService;
this.type = '';
this.disabled = false;
this.hidden = false;
// EventEmitter which is emitted when a button is clicked.
this.wasClicked = new EventEmitter(false);
}
get isCancel() {
return this.checkDefaultAndCustomType(this.type, 'cancel');
}
get isNext() {
return this.checkDefaultAndCustomType(this.type, 'next');
}
get isPrevious() {
return this.checkDefaultAndCustomType(this.type, 'previous');
}
get isFinish() {
return this.checkDefaultAndCustomType(this.type, 'finish');
}
get isDanger() {
return this.checkDefaultAndCustomType(this.type, 'danger');
}
get isPrimaryAction() {
return this.isNext || this.isDanger || this.isFinish;
}
get _disabledAttribute() {
if (this.isDisabled) {
return '';
}
return null;
}
get isDisabled() {
// dealing with negatives here. cognitively easier to think of it like this...
const disabled = true;
const nav = this.navService;
const page = this.navService.currentPage;
// Ensure we don't change the response until buttons are ready to avoid chocolate
if (!this.buttonService.buttonsReady) {
return !disabled;
}
if (this.disabled || nav.wizardStopNavigation || !page) {
return true;
}
if (this.isCancel) {
return !disabled;
}
if (this.isPrevious && (nav.currentPageIsFirst || page.previousStepDisabled)) {
return disabled;
}
if (this.isDanger && !page.readyToComplete) {
return disabled;
}
if (this.isNext && (nav.currentPageIsLast || !page.readyToComplete)) {
return disabled;
}
if (this.isFinish && (!nav.currentPageIsLast || !page.readyToComplete)) {
return disabled;
}
return !disabled;
}
get isHidden() {
// dealing with negatives here. cognitively easier to think of it like this...
const hidden = true;
const nav = this.navService;
// Ensure we don't change the response until buttons are ready to avoid chocolate
if (!this.buttonService.buttonsReady) {
return !hidden;
}
if (this.hidden) {
return true;
}
if (this.isCancel) {
return !hidden;
}
if (this.isPrevious && nav.currentPageIsFirst) {
return hidden;
}
if (this.isNext && nav.currentPageIsLast) {
return hidden;
}
if (this.isFinish && !nav.currentPageIsLast) {
return hidden;
}
return !hidden;
}
click() {
if (this.isDisabled) {
return;
}
this.wasClicked.emit(this.type);
this.buttonService.buttonClicked(this.type);
}
checkDefaultAndCustomType(valueToCheck = '', typeToLookUp) {
if (DEFAULT_BUTTON_TYPES[typeToLookUp] === valueToCheck) {
return true;
}
if (CUSTOM_BUTTON_TYPES[typeToLookUp] === valueToCheck) {
return true;
}
return false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrWizardButton, deps: [{ token: WizardNavigationService }, { token: ButtonHubService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: ClrWizardButton, isStandalone: false, selector: "clr-wizard-button", inputs: { type: "type", disabled: ["clrWizardButtonDisabled", "disabled"], hidden: ["clrWizardButtonHidden", "hidden"] }, outputs: { wasClicked: "clrWizardButtonClicked" }, host: { properties: { "attr.aria-hidden": "isHidden" }, classAttribute: "clr-wizard-btn-wrapper" }, ngImport: i0, template: `
<button
type="button"
class="btn clr-wizard-btn"
[class.btn-link]="isCancel"
[class.clr-wizard-btn--tertiary]="isCancel"
[class.btn-outline]="isPrevious"
[class.clr-wizard-btn--secondary]="isPrevious"
[class.btn-primary]="isPrimaryAction"
[class.clr-wizard-btn--primary]="isPrimaryAction"
[class.btn-success]="isFinish"
[class.btn-danger]="isDanger"
[class.disabled]="isDisabled"
[attr.disabled]="_disabledAttribute"
(click)="click()"
>
<ng-content></ng-content>
</button>
`, isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrWizardButton, decorators: [{
type: Component,
args: [{
selector: 'clr-wizard-button',
template: `
<button
type="button"
class="btn clr-wizard-btn"
[class.btn-link]="isCancel"
[class.clr-wizard-btn--tertiary]="isCancel"
[class.btn-outline]="isPrevious"
[class.clr-wizard-btn--secondary]="isPrevious"
[class.btn-primary]="isPrimaryAction"
[class.clr-wizard-btn--primary]="isPrimaryAction"
[class.btn-success]="isFinish"
[class.btn-danger]="isDanger"
[class.disabled]="isDisabled"
[attr.disabled]="_disabledAttribute"
(click)="click()"
>
<ng-content></ng-content>
</button>
`,
host: { class: 'clr-wizard-btn-wrapper', '[attr.aria-hidden]': 'isHidden' },
standalone: false,
}]
}], ctorParameters: () => [{ type: WizardNavigationService }, { type: ButtonHubService }], propDecorators: { type: [{
type: Input,
args: ['type']
}], disabled: [{
type: Input,
args: ['clrWizardButtonDisabled']
}], hidden: [{
type: Input,
args: ['clrWizardButtonHidden']
}], wasClicked: [{
type: Output,
args: ['clrWizardButtonClicked']
}] } });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
let wizardHeaderActionIndex = 0;
class ClrWizardHeaderAction {
constructor() {
// title is explanatory text added to the header action
this.title = '';
// If our host has an ID attribute, we use this instead of our index.
this._id = (wizardHeaderActionIndex++).toString();
this.disabled = false;
this.headerActionClicked = new EventEmitter(false);
}
get id() {
return `clr-wizard-header-action-${this._id}`;
}
click() {
if (this.disabled) {
return;
}
// passing the header action id allows users to have one method that
// routes to many different actions based on the type of header action
// clicked. this is further aided by users being able to specify ids
// for their header actions.
this.headerActionClicked.emit(this._id);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrWizardHeaderAction, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: ClrWizardHeaderAction, isStandalone: false, selector: "clr-wizard-header-action", inputs: { title: "title", _id: ["id", "_id"], disabled: ["clrWizardHeaderActionDisabled", "disabled"] }, outputs: { headerActionClicked: "actionClicked" }, host: { classAttribute: "clr-wizard-header-action-wrapper" }, ngImport: i0, template: `
<button
type="button"
class="btn clr-wizard-header-action btn-link"