@angular/core#AfterContentChecked TypeScript Examples
The following examples show how to use
@angular/core#AfterContentChecked.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: table-head.component.ts From canopy with Apache License 2.0 | 6 votes |
@Component({
selector: '[lg-table-head]',
templateUrl: './table-head.component.html',
styleUrls: [ './table-head.component.scss' ],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LgTableHeadComponent implements AfterContentChecked {
@HostBinding('class') class = 'lg-table-head';
@ContentChild(LgTableRowComponent, { static: false }) headRow: LgTableRowComponent;
ngAfterContentChecked() {
if (this.headRow) {
this.headRow.isHeadRow = true;
}
}
}
Example #2
Source File: breadcrumb.component.ts From canopy with Apache License 2.0 | 5 votes |
@Component({
selector: 'lg-breadcrumb',
templateUrl: './breadcrumb.component.html',
styleUrls: [ './breadcrumb.component.scss' ],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LgBreadcrumbComponent implements AfterContentChecked {
private _variant = BreadcrumbVariant.dark;
private contentHasInit = false;
@Input() set variant(variant: BreadcrumbVariant) {
this._variant = variant;
if (this.contentHasInit) {
this.setVariantOnChildren();
}
}
get variant(): BreadcrumbVariant {
return this._variant;
}
@HostBinding('class.lg-breadcrumb') class = true;
@HostBinding('attr.aria-label') attr = 'breadcrumb';
@HostBinding('attr.role') role = 'navigation';
@ContentChildren(forwardRef(() => LgBreadcrumbItemComponent), {
descendants: true,
})
crumbs: QueryList<LgBreadcrumbItemComponent>;
@ContentChildren(forwardRef(() => LgBreadcrumbItemEllipsisComponent), {
descendants: true,
})
ellipsis: QueryList<LgBreadcrumbItemEllipsisComponent>;
ngAfterContentChecked(): void {
this.setVariantOnChildren();
this.setCrumbProperties();
this.contentHasInit = true;
}
private setVariantOnChildren(): void {
this.crumbs.forEach(crumb => (crumb.variant = this.variant));
this.ellipsis.forEach(ellipsisItem => (ellipsisItem.variant = this.variant));
}
private setCrumbProperties(): void {
this.crumbs.forEach((crumb, index) => {
const totalCrumbCount = this.crumbs.length;
crumb.index = index;
crumb.hideIcons = totalCrumbCount === 2 && !index;
crumb.showBackChevron = totalCrumbCount > 1;
crumb.showForwardChevron = index + 1 !== totalCrumbCount;
crumb.isSmScreenFeaturedItem =
(!index && totalCrumbCount === 1) || index + 2 === totalCrumbCount;
});
}
}
Example #3
Source File: accordion.directive.ts From flingo with MIT License | 5 votes |
@Directive({
selector: '[appAccordion]'
})
export class AccordionDirective implements AfterContentChecked {
protected navlinks: Array<AccordionLinkDirective> = [];
closeOtherLinks(selectedLink: AccordionLinkDirective): void {
this.navlinks.forEach((link: AccordionLinkDirective) => {
if (link !== selectedLink) {
link.selected = false;
}
});
}
addLink(link: AccordionLinkDirective): void {
this.navlinks.push(link);
}
removeGroup(link: AccordionLinkDirective): void {
const index = this.navlinks.indexOf(link);
if (index !== -1) {
this.navlinks.splice(index, 1);
}
}
checkOpenLinks() {
this.navlinks.forEach((link: AccordionLinkDirective) => {
if (link.group) {
const routeUrl = this.router.url;
const currentUrl = routeUrl.split('/');
if (currentUrl.indexOf(link.group) > 0) {
link.selected = true;
this.closeOtherLinks(link);
}
}
});
}
ngAfterContentChecked(): void {}
constructor(private router: Router) {
setTimeout(() => this.checkOpenLinks());
}
}
Example #4
Source File: table.component.ts From canopy with Apache License 2.0 | 4 votes |
@Component({
selector: '[lg-table]',
templateUrl: './table.component.html',
styleUrls: [ './table.component.scss' ],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LgTableComponent implements AfterContentChecked {
private _showColumnsAt: TableColumnLayoutBreakpoints;
isExpandable = false;
id = nextUniqueId++;
columns = new Map<number, TableColumn>();
_variant: TableVariant;
@Input()
set showColumnsAt(columnsBreakpoint: TableColumnLayoutBreakpoints) {
this.addColumnsBreakpoint(columnsBreakpoint);
this._showColumnsAt = columnsBreakpoint;
}
get showColumnsAt(): TableColumnLayoutBreakpoints {
return this._showColumnsAt;
}
@Input()
set variant(variant: TableVariant) {
if (this._variant) {
this.renderer.removeClass(
this.hostElement.nativeElement,
`lg-table--${this.variant}`,
);
}
this.renderer.addClass(this.hostElement.nativeElement, `lg-table--${variant}`);
this._variant = variant;
}
get variant() {
return this._variant;
}
@HostBinding('class') class = 'lg-table';
@ContentChild(LgTableHeadComponent, { static: false }) tableHead: LgTableHeadComponent;
@ContentChild(LgTableBodyComponent, { static: false }) tableBody: LgTableBodyComponent;
@HostBinding('class.lg-table--expandable')
get expandableClass() {
return this.isExpandable;
}
constructor(private renderer: Renderer2, private hostElement: ElementRef) {
this.variant = 'striped';
this.showColumnsAt = TableColumnLayoutBreakpoints.Medium;
}
ngAfterContentChecked() {
if (this.tableHead && this.tableBody) {
this.tableBody.rows.forEach(row =>
row.bodyCells.forEach(cell => {
if (cell.expandableDetail) {
row.isDetailRow = true;
}
}),
);
this.isExpandable = this.tableBody.rows.some(row => row.isDetailRow);
this.handleHeadCells();
this.handleDetailRows();
this.handleBodyRows();
}
}
private addColumnsBreakpoint(columnsBreakpoint: TableColumnLayoutBreakpoints) {
this.renderer.removeClass(
this.hostElement.nativeElement,
`lg-table--columns-${this._showColumnsAt}`,
);
this.renderer.addClass(
this.hostElement.nativeElement,
`lg-table--columns-${columnsBreakpoint}`,
);
}
private handleHeadCells() {
const headCells = this.tableHead.headRow.headCells;
headCells.forEach((cell, cellIndex) => {
this.columns.set(cellIndex, {
align: cell.align,
label: cell.element.nativeElement.innerHTML,
showLabel: cell.showLabel,
});
});
}
private handleDetailRows() {
this.tableBody.rows
.filter(row => row.isDetailRow)
.forEach((detailRow, index) => {
detailRow.ariaId = `lg-table-${this.id}-detail-row-${index}`;
detailRow.ariaLabelledBy = `lg-table-${this.id}-toggle-row-${index}`;
});
}
private handleBodyRows() {
this.tableBody.rows
.filter(row => !row.isDetailRow)
.forEach((row, index) => {
row.bodyCells
.filter(cell => !cell.expandableDetail)
.forEach((cell, cellIndex) => {
const { align, showLabel, label } = this.columns.get(cellIndex);
cell.align = align;
cell.showLabel = showLabel;
cell.label.nativeElement.innerHTML = label;
cell.tableId = this.id;
});
let toggleContext = '';
row.bodyCells.forEach((cell, cellIndex) => {
if (cellIndex === 1) {
// assume column index 0 is for the toggle
toggleContext = cell.content.nativeElement.innerHTML;
}
});
row.bodyCells
.filter(cell => !!cell.toggle)
.forEach(cell => {
cell.toggle.tableId = this.id;
cell.toggle.rowId = index;
cell.toggle.context = toggleContext;
});
});
}
}
Example #5
Source File: tab-nav-bar.component.ts From canopy with Apache License 2.0 | 4 votes |
@Component({
selector: 'lg-tab-nav-bar',
templateUrl: './tab-nav-bar.component.html',
styleUrls: [ './tab-nav-bar.component.scss' ],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LgTabNavBarComponent implements AfterContentChecked, OnDestroy {
private ngUnsubscribe: Subject<void> = new Subject<void>();
selectedIndex = 0;
tabs: Array<LgTabNavBarLinkDirective>;
@Input() label = 'Tabs';
@ContentChildren(forwardRef(() => LgTabNavBarLinkDirective), {
descendants: true,
})
tabQueryList: QueryList<LgTabNavBarLinkDirective>;
@HostBinding('class.lg-tab-nav-bar') class = true;
@HostBinding('attr.role') ariaRole = 'tablist';
@HostBinding('attr.aria-label')
get ariaLabel() {
return this.label
? this.label
: 'Tabs';
}
constructor(private cd: ChangeDetectorRef) {}
@HostListener('keyup', [ '$event' ]) onKeyUp(event: KeyboardEvent): void {
const isPreviousKey = isKeyLeft(event) || isKeyUp(event);
const isNextKey = isKeyRight(event) || isKeyDown(event);
if (!isPreviousKey && !isNextKey) {
return;
}
event.preventDefault();
const currentSelectedTabIndex = this.tabs.findIndex(tab => tab.isActive);
if (isPreviousKey) {
this.selectedIndex =
currentSelectedTabIndex === 0
? this.tabs.length - 1
: currentSelectedTabIndex - 1;
}
if (isNextKey) {
this.selectedIndex =
currentSelectedTabIndex === this.tabs.length - 1
? 0
: currentSelectedTabIndex + 1;
}
this.tabs[this.selectedIndex].selectByKeyboard();
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
ngAfterContentChecked() {
this.setTabs();
this.tabQueryList.changes.pipe(takeUntil(this.ngUnsubscribe)).subscribe(() => {
this.setTabs();
this.cd.detectChanges();
});
}
setTabs() {
this.tabs = this.tabQueryList.toArray();
// Set the tab indexes and initial selected tab index
this.tabs.forEach((tab, index: number) => {
tab.index = index;
});
// Update tab link active states when active tab changes
const tabOutputs = this.tabs.map(tab => tab.selectedTabIndexChange);
merge(...tabOutputs)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((nextSelectedIndex: number) =>
this.updateSelectedTab(nextSelectedIndex),
);
}
updateSelectedTab(index: number) {
this.tabs.forEach((tab, i: number) => {
tab.isActive = i === index;
});
this.selectedIndex = index;
}
}
Example #6
Source File: command-bar.component.ts From leapp with Mozilla Public License 2.0 | 4 votes |
@Component({
selector: "app-command-bar",
templateUrl: "./command-bar.component.html",
styleUrls: ["./command-bar.component.scss"],
})
export class CommandBarComponent implements OnInit, OnDestroy, AfterContentChecked {
@ViewChild("parent") parent: ElementRef;
@ViewChild("child") child: ElementRef;
overflowed = false;
filterForm = new FormGroup({
searchFilter: new FormControl(""),
dateFilter: new FormControl(true),
providerFilter: new FormControl([]),
profileFilter: new FormControl([]),
regionFilter: new FormControl([]),
integrationFilter: new FormControl([]),
typeFilter: new FormControl([]),
});
providers: { show: boolean; id: string; name: string; value: boolean }[];
profiles: { show: boolean; id: string; name: string; value: boolean }[];
integrations: { show: boolean; id: string; name: string; value: boolean }[];
types: { show: boolean; id: SessionType; category: string; name: string; value: boolean }[];
regions: { show: boolean; name: string; value: boolean }[];
filterExtended: boolean;
compactMode: boolean;
eConstants = constants;
private subscription;
private subscription2;
private subscription3;
private subscription4;
private subscription5;
private subscription6;
private currentSegment: Segment;
private behaviouralSubjectService: BehaviouralSubjectService;
constructor(
public optionsService: OptionsService,
private bsModalService: BsModalService,
public appService: AppService,
public electronService: AppNativeService,
private leappCoreService: AppProviderService,
private windowService: WindowService
) {
this.behaviouralSubjectService = leappCoreService.behaviouralSubjectService;
this.filterExtended = false;
this.compactMode = false;
globalFilteredSessions.next(this.behaviouralSubjectService.sessions);
globalColumns.next({
role: true,
provider: true,
namedProfile: true,
region: true,
});
this.setInitialArrayFilters();
}
private static changeSessionsTableHeight() {
document.querySelector(".sessions").classList.toggle("filtered");
}
ngOnInit(): void {
this.subscription = this.filterForm.valueChanges.subscribe((values: GlobalFilters) => {
globalFilterGroup.next(values);
this.applyFiltersToSessions(values, this.behaviouralSubjectService.sessions);
});
this.subscription2 = globalHasFilter.subscribe((value) => {
this.filterExtended = value;
});
this.subscription3 = globalResetFilter.subscribe(() => {
this.setInitialArrayFilters();
this.filterForm.get("searchFilter").setValue("");
this.filterForm.get("dateFilter").setValue(true);
this.filterForm.get("providerFilter").setValue(this.providers);
this.filterForm.get("profileFilter").setValue(this.profiles);
this.filterForm.get("regionFilter").setValue(this.regions);
this.filterForm.get("integrationFilter").setValue(this.integrations);
this.filterForm.get("typeFilter").setValue(this.types);
});
this.subscription4 = this.behaviouralSubjectService.sessions$.subscribe((sessions) => {
const actualFilterValues: GlobalFilters = {
dateFilter: this.filterForm.get("dateFilter").value,
integrationFilter: this.filterForm.get("integrationFilter").value,
profileFilter: this.filterForm.get("profileFilter").value,
providerFilter: this.filterForm.get("providerFilter").value,
regionFilter: this.filterForm.get("regionFilter").value,
searchFilter: this.filterForm.get("searchFilter").value,
typeFilter: this.filterForm.get("typeFilter").value,
};
this.applyFiltersToSessions(actualFilterValues, sessions);
});
this.subscription5 = globalSegmentFilter.subscribe((segment: Segment) => {
if (segment) {
const values = segment.filterGroup;
globalFilterGroup.next(values);
this.updateFilterForm(values);
this.applyFiltersToSessions(values, this.behaviouralSubjectService.sessions);
}
});
this.subscription6 = globalOrderingFilter.subscribe((sessions: Session[]) => {
globalFilteredSessions.next(sessions);
});
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
this.subscription2.unsubscribe();
this.subscription3.unsubscribe();
this.subscription4.unsubscribe();
this.subscription5.unsubscribe();
this.subscription6.unsubscribe();
}
ngAfterContentChecked(): void {
if (this.parent && this.child) {
const parentW = parseInt(this.parent.nativeElement.clientWidth, 10);
const childW = parseInt(this.child.nativeElement.clientWidth, 10);
this.overflowed = childW > parentW;
}
}
showOptionDialog(): void {
this.bsModalService.show(OptionsDialogComponent, { animated: false, class: "option-modal" });
}
showCreateDialog(): void {
this.bsModalService.show(CreateDialogComponent, { animated: false, class: "create-modal", backdrop: "static", keyboard: false });
}
toggleCompactMode(): void {
this.compactMode = !this.compactMode;
this.filterExtended = false;
this.windowService.getCurrentWindow().unmaximize();
this.windowService.getCurrentWindow().restore();
if (this.appService.detectOs() === constants.mac && this.windowService.getCurrentWindow().isFullScreen()) {
this.windowService.getCurrentWindow().setFullScreen(false);
this.windowService.getCurrentWindow().setMaximizable(false);
}
compactMode.next(this.compactMode);
globalHasFilter.next(this.filterExtended);
document.querySelector(".sessions").classList.remove("filtered");
}
toggleFilters(): void {
this.filterExtended = !this.filterExtended;
globalHasFilter.next(this.filterExtended);
CommandBarComponent.changeSessionsTableHeight();
}
toggleDateFilter(): void {
this.filterForm.get("dateFilter").setValue(!this.filterForm.get("dateFilter").value);
}
openSaveSegmentDialog(): void {
this.bsModalService.show(SegmentDialogComponent, { animated: false, class: "segment-modal" });
}
checkFormIsDirty(): boolean {
return (
this.filterForm.get("dateFilter").value ||
this.filterForm.get("providerFilter").value.length > 0 ||
this.filterForm.get("profileFilter").value.length > 0 ||
this.filterForm.get("regionFilter").value.length > 0 ||
this.filterForm.get("integrationFilter").value.length > 0 ||
this.filterForm.get("typeFilter").value.length > 0
);
}
syncAll(): void {
syncAllEvent.next(true);
}
windowButtonDetectTheme(): string {
if (
this.optionsService.colorTheme === constants.darkTheme ||
(this.optionsService.colorTheme === constants.systemDefaultTheme && this.appService.isDarkMode())
) {
return "_dark";
} else {
return "";
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
windowMaximizeAction() {
if (!this.compactMode) {
if (this.windowService.getCurrentWindow().isMaximized()) {
this.windowService.getCurrentWindow().restore();
} else {
this.windowService.getCurrentWindow().maximize();
}
}
}
private applyFiltersToSessions(values: GlobalFilters, sessions: Session[]) {
let filteredSessions = sessions;
const searchText = this.filterForm.get("searchFilter").value;
if (searchText !== "") {
filteredSessions = filteredSessions.filter((session) => {
let test = false;
test ||= session.sessionName.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
test ||= session.type.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
test ||= session.region.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
test ||= (session as any).email?.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
test ||= (session as any).roleArn?.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
test ||= (session as any).idpArn?.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
test ||= (session as any).roleSessionName?.toLowerCase().indexOf(searchText.toLowerCase()) > -1;
try {
test ||=
this.leappCoreService.namedProfileService
.getProfileName((session as any).profileId)
?.toLowerCase()
.indexOf(searchText.toLowerCase()) > -1;
} catch (e) {
test ||= false;
}
return test;
});
}
if (this.filterForm.get("dateFilter").value) {
filteredSessions = this.orderByDate(filteredSessions);
} else {
filteredSessions = filteredSessions.sort((a, b) => a.sessionName.localeCompare(b.sessionName));
}
if (this.filterForm.get("providerFilter").value.filter((v) => v.value).length > 0) {
filteredSessions = filteredSessions.filter((session) => {
let test = false;
this.providers.forEach((provider) => {
if (provider.value) {
test ||= session.type.indexOf(provider.id) > -1;
}
});
return test;
});
}
if (this.filterForm.get("profileFilter").value.filter((v) => v.value).length > 0) {
filteredSessions = filteredSessions.filter((session) => {
let test = false;
this.profiles.forEach((profile) => {
if (profile.value) {
if ((session as any).profileId) {
test ||= (session as any).profileId.indexOf(profile.id) > -1;
}
}
});
return test;
});
}
if (this.filterForm.get("regionFilter").value.filter((v) => v.value).length > 0) {
filteredSessions = filteredSessions.filter((session) => {
let test = false;
this.regions.forEach((region) => {
if (region.value) {
test ||= session.region.indexOf(region.name) > -1;
}
});
return test;
});
}
if (this.filterForm.get("integrationFilter").value.filter((v) => v.value).length > 0) {
filteredSessions = filteredSessions.filter((session) => {
let test = false;
this.integrations.forEach((integration) => {
if (integration.value) {
test ||= session.type === SessionType.awsSsoRole && (session as AwsSsoRoleSession).awsSsoConfigurationId.indexOf(integration.id) > -1;
}
});
return test;
});
}
if (this.filterForm.get("typeFilter").value.filter((v) => v.value).length > 0) {
filteredSessions = filteredSessions.filter((session) => {
let test = false;
this.types.forEach((type) => {
if (type.value) {
test ||= session.type.indexOf(type.id) > -1;
}
});
return test;
});
}
filteredSessions = filteredSessions.sort((x, y) => {
const pinnedList = this.optionsService.pinned;
if ((pinnedList.indexOf(x.sessionId) !== -1) === (pinnedList.indexOf(y.sessionId) !== -1)) {
return 0;
} else if (this.optionsService.pinned.indexOf(x.sessionId) !== -1) {
return -1;
} else {
return 1;
}
});
return globalFilteredSessions.next(filteredSessions);
}
private orderByDate(filteredSession: Session[]) {
return filteredSession.sort((a, b) => new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime());
}
private updateFilterForm(values: GlobalFilters) {
console.log("inside filter form", values);
this.filterForm.get("searchFilter").setValue(values.searchFilter);
this.filterForm.get("dateFilter").setValue(values.dateFilter);
this.filterForm.get("providerFilter").setValue(values.providerFilter);
this.filterForm.get("profileFilter").setValue(values.profileFilter);
this.filterForm.get("regionFilter").setValue(values.regionFilter);
this.filterForm.get("integrationFilter").setValue(values.integrationFilter);
this.filterForm.get("typeFilter").setValue(values.typeFilter);
if (values.providerFilter.length > 0) {
this.providers = values.providerFilter;
this.providers.forEach((provider) => {
provider.show = true;
});
}
if (values.profileFilter.length > 0) {
this.profiles = values.profileFilter;
this.profiles.forEach((profile) => {
profile.show = true;
});
}
if (values.regionFilter.length > 0) {
this.regions = values.regionFilter;
this.regions.forEach((region) => {
region.show = true;
});
}
if (values.typeFilter.length > 0) {
this.types = values.typeFilter;
this.types.forEach((type) => {
type.show = true;
});
}
}
private setInitialArrayFilters() {
this.providers = [
{ show: true, id: "aws", name: "Amazon AWS", value: false },
{ show: true, id: "azure", name: "Microsoft Azure", value: false },
];
this.integrations = [];
this.types = [
// eslint-disable-next-line max-len
{ show: true, id: SessionType.awsIamRoleFederated, category: "Amazon AWS", name: "IAM Role Federated", value: false },
{ show: true, id: SessionType.awsIamUser, category: "Amazon AWS", name: "IAM User", value: false },
{ show: true, id: SessionType.awsIamRoleChained, category: "Amazon AWS", name: "IAM Role Chained", value: false },
{ show: true, id: SessionType.awsSsoRole, category: "Amazon AWS", name: "IAM Single Sign-On", value: false },
{ show: true, id: SessionType.azure, category: "Microsoft Azure", name: "Azure Subscription", value: false },
];
this.profiles = this.leappCoreService.namedProfileService
.getNamedProfiles()
.map((element) => ({ name: element.name, id: element.id, value: false, show: true }));
this.regions = this.leappCoreService.awsCoreService.getRegions().map((element) => ({ name: element.region, value: false, show: true }));
}
private saveTemporarySegmentAndApply() {
if (!this.filterExtended) {
this.currentSegment = JSON.parse(
JSON.stringify({
name: "temp",
filterGroup: {
dateFilter: this.filterForm.get("dateFilter").value,
integrationFilter: this.filterForm.get("integrationFilter").value,
profileFilter: this.filterForm.get("profileFilter").value,
providerFilter: this.filterForm.get("providerFilter").value,
regionFilter: this.filterForm.get("regionFilter").value,
searchFilter: this.filterForm.get("searchFilter").value,
typeFilter: this.filterForm.get("typeFilter").value,
},
})
);
globalResetFilter.next(true);
} else {
globalSegmentFilter.next(this.currentSegment);
}
}
}
Example #7
Source File: tab-group.component.ts From alauda-ui with MIT License | 4 votes |
@Component({
selector: 'aui-tab-group',
exportAs: 'auiTabGroup',
templateUrl: './tab-group.component.html',
styleUrls: ['./tab-group.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
preserveWhitespaces: false,
})
export class TabGroupComponent
implements OnChanges, AfterContentChecked, AfterContentInit, OnDestroy
{
bem: Bem = buildBem('aui-tab-group');
@ContentChildren(TabComponent)
_tabs: QueryList<TabComponent>;
@ContentChild(TabHeaderAddonDirective, { static: false })
_headerAddon: TabHeaderAddonDirective;
@ViewChild(TabHeaderComponent, { static: false })
_tabHeader: TabHeaderComponent;
@ContentChild(TabTitleDirective, { static: false })
_tabTitle: TabTitleDirective;
/** The tab index that should be selected after the content has been checked. */
private _indexToSelect: number | null = 0;
/** Subscription to tabs being added/removed. */
private _tabsSubscription = Subscription.EMPTY;
/** Subscription to changes in the tab labels. */
private _tabLabelSubscription = Subscription.EMPTY;
private _selectedIndex: number | null = null;
private _tab: string = null;
private _type: TabType = TabType.Line;
private _size: TabSize = TabSize.Medium;
/** true lazy mode for template ref children */
private _lazy: boolean;
private _previousHeaderAddon: TabHeaderAddonDirective;
/** Emits whenever the type changes */
readonly _typeChange = new Subject<void>();
/** Emits whenever the size changes */
readonly _sizeChange = new Subject<void>();
/** The index of the active tab. */
@Input()
get selectedIndex(): number | null {
return this._selectedIndex;
}
set selectedIndex(value: number | null) {
this._indexToSelect = coerceNumber(value, null);
this._changeActivatedTabs();
}
@Input()
get tab(): string {
return this._tab;
}
set tab(value: string) {
this._tab = value;
if (this._tabs) {
this.selectedIndex = this._findIndexByTab(value);
}
}
@Input()
get type() {
return this._type;
}
set type(type: TabType) {
this._type = type;
}
@Input()
title: string | TemplateRef<unknown>;
@Input()
get size() {
return this._size;
}
set size(val) {
if (!val || this._size === val) {
return;
}
this._size = val;
}
@Input()
get lazy() {
return this._lazy;
}
set lazy(lazy: boolean) {
if (this._lazy === lazy) {
return;
}
this._lazy = lazy;
if (lazy) {
this._changeActivatedTabs();
} else {
this.activatedTabs.length = 0;
}
}
/** Output to enable support for two-way binding on `[(selectedIndex)]` */
@Output()
readonly selectedIndexChange = new EventEmitter<number>();
@Output()
readonly tabChange = new EventEmitter<string>();
/** Event emitted when the tab selection has changed. */
@Output()
readonly selectedTabChange = new EventEmitter<TabChangeEvent>(true);
/** Event emitted when focus has changed within a tab group. */
@Output()
readonly focusChange: EventEmitter<TabChangeEvent> = new EventEmitter<TabChangeEvent>();
constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {}
activatedTabs: TabComponent[] = [];
get activeTab() {
return this._tabs.length > 0 && this.selectedIndex !== null
? this._tabs.toArray()[this.selectedIndex]
: null;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty('type')) {
this._typeChange.next();
}
if (changes.hasOwnProperty('size')) {
this._sizeChange.next();
}
}
/**
* After the content is checked, this component knows what tabs have been defined
* and what the selected index should be. This is where we can know exactly what position
* each tab should be in according to the new selected index.
*/
ngAfterContentChecked(): void {
// Don't clamp the `indexToSelect` immediately in the setter because it can happen that
// the amount of tabs changes before the actual change detection runs.
const indexToSelect = (this._indexToSelect = this._clampTabIndex(
this._indexToSelect,
));
// If there is a change in selected index, emit a change event. Should not trigger if
// the selected index has not yet been initialized.
if (this._selectedIndex !== indexToSelect && this._selectedIndex != null) {
this._changeActivatedTabs();
const tabChangeEvent = this._createChangeEvent(indexToSelect);
this.selectedTabChange.emit(tabChangeEvent);
// Emitting this value after change detection has run
// since the checked content may contain this variable'
Promise.resolve().then(() => {
this.selectedIndexChange.emit(indexToSelect);
this.tabChange.emit(this._tabs.get(indexToSelect).name);
});
}
// Setup the position for each tab and optionally setup an origin on the next selected tab.
this._tabs.forEach((tab: TabComponent, index: number) => {
tab.position = index - indexToSelect;
tab.tabContext.isActive = index === indexToSelect;
// If there is already a selected tab, then set up an origin for the next selected tab
// if it doesn't have one already.
if (this._selectedIndex != null && tab.position === 0 && !tab.origin) {
tab.origin = indexToSelect - this._selectedIndex;
}
});
if (this._selectedIndex !== indexToSelect) {
this._selectedIndex = indexToSelect;
this._changeDetectorRef.markForCheck();
}
if (this._previousHeaderAddon !== this._headerAddon) {
this._previousHeaderAddon = this._headerAddon;
this._changeDetectorRef.markForCheck();
}
}
ngAfterContentInit() {
if (this.tab) {
this._indexToSelect = this._findIndexByTab(this.tab);
}
this._changeActivatedTabs();
this._subscribeToTabLabels();
// Subscribe to changes in the amount of tabs, in order to be
// able to re-render the content as new tabs are added or removed.
this._tabsSubscription = this._tabs.changes.subscribe(() => {
const tabs = this._tabs.toArray();
if (this._lazy) {
this.activatedTabs = this.activatedTabs.filter(tab =>
tabs.includes(tab),
);
}
const indexToSelect = this._clampTabIndex(this._indexToSelect);
// Maintain the previously-selected tab if a new tab is added or removed and there is no
// explicit change that selects a different tab.
if (indexToSelect === this._selectedIndex) {
for (const [i, tab] of tabs.entries()) {
if (tab.tabContext.isActive) {
// Assign both to the `_indexToSelect` and `_selectedIndex` so we don't fire a changed
// event, otherwise the consumer may end up in an infinite loop in some edge cases like
// adding a tab within the `selectedIndexChange` event.
this._indexToSelect = this._selectedIndex = i;
break;
}
}
}
this._changeActivatedTabs();
this._subscribeToTabLabels();
this._changeDetectorRef.markForCheck();
});
}
ngOnDestroy() {
this._tabsSubscription.unsubscribe();
this._tabLabelSubscription.unsubscribe();
this._typeChange.complete();
this._sizeChange.complete();
}
/** Handle click events, setting new selected index if appropriate. */
_handleClick(tab: TabComponent, idx: number) {
if (!tab.disabled) {
this.selectedIndex = this._tabHeader.focusIndex = idx;
}
}
_focusChanged(index: number) {
this.focusChange.emit(this._createChangeEvent(index));
}
/** Re-aligns the ink bar to the selected tab element. */
realignActiveIndicator() {
if (this._tabHeader) {
this._tabHeader._alignActiveIndicatorToSelectedTab();
}
}
private _changeActivatedTabs() {
if (
!this._lazy ||
!this._tabs ||
this._indexToSelect < 0 ||
this._indexToSelect >= this._tabs.length
) {
return;
}
const tab = this._tabs.find((_, index) => index === this._indexToSelect);
if (tab && !this.activatedTabs.includes(tab)) {
this.activatedTabs.push(tab);
}
}
/** Clamps the given index to the bounds of 0 and the tabs length. */
private _clampTabIndex(index: number | null): number {
// Note the `|| 0`, which ensures that values like NaN can't get through
// and which would otherwise throw the component into an infinite loop
// (since Math.max(NaN, 0) === NaN).
return Math.min(this._tabs.length - 1, Math.max(index || 0, 0));
}
private _findIndexByTab(name: string) {
return Math.max(
this._tabs.toArray().findIndex(tab => tab.name === name),
0,
);
}
private _createChangeEvent(index: number): TabChangeEvent {
const event = new TabChangeEvent();
event.index = index;
if (this._tabs?.length > 0) {
event.tab = this._tabs.toArray()[index];
}
return event;
}
/**
* Subscribes to changes in the tab labels. This is needed, because the @Input for the label is
* on the Tab component, whereas the data binding is inside the TabGroup. In order for the
* binding to be updated, we need to subscribe to changes in it and trigger change detection
* manually.
*/
private _subscribeToTabLabels() {
if (this._tabLabelSubscription) {
this._tabLabelSubscription.unsubscribe();
}
this._tabLabelSubscription = merge(
...this._tabs.map(tab => tab._stateChanges),
this._typeChange,
this._sizeChange,
).subscribe(() => {
this.realignActiveIndicator();
this._changeDetectorRef.markForCheck();
});
}
/** Retrieves the tabindex for the tab. */
_getTabIndex(tab: TabComponent, idx: number): number | null {
if (tab.disabled) {
return null;
}
return this.selectedIndex === idx ? 0 : -1;
}
}
Example #8
Source File: tab-header.component.ts From alauda-ui with MIT License | 4 votes |
@Component({
selector: 'aui-tab-header',
templateUrl: './tab-header.component.html',
styleUrls: ['./tab-header.component.scss'],
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabHeaderComponent
implements OnDestroy, AfterContentChecked, AfterContentInit
{
bem: Bem = buildBem('aui-tab-header');
_showAddon = false;
/** Whether the controls for pagination should be displayed */
_showPaginationControls = false;
/** Whether the tab list can be scrolled more towards the end of the tab label list. */
_disableScrollAfter = true;
/** Whether the tab list can be scrolled more towards the beginning of the tab label list. */
_disableScrollBefore = true;
/** Whether the scroll distance has changed and should be applied after the view is checked. */
private _scrollDistanceChanged: boolean;
/** Whether the header should scroll to the selected index after the view has been checked. */
private _selectedIndexChanged = false;
/** The distance in pixels that the tab labels should be translated to the left. */
private _scrollDistance = 0;
private _selectedIndex = 0;
/** Emits when the component is destroyed. */
private readonly _destroyed = new Subject<void>();
/** Used to manage focus between the tabs. */
private _keyManager: FocusKeyManager<TabLabelWrapperDirective>;
/**
* The number of tab labels that are displayed on the header. When this changes, the header
* should re-evaluate the scroll position.
*/
private _tabLabelCount: number;
@Input()
type: TabType = TabType.Line;
@Input()
size: TabSize = TabSize.Medium;
/** The index of the active tab. */
@Input()
get selectedIndex(): number {
return this._selectedIndex;
}
set selectedIndex(value: number) {
value = coerceNumber(value);
this._selectedIndexChanged = this._selectedIndex !== value;
this._selectedIndex = value;
if (this._keyManager) {
this._keyManager.updateActiveItem(value);
}
}
/** Event emitted when the option is selected. */
@Output()
readonly selectFocusedIndex = new EventEmitter<number>();
/** Event emitted when a label is focused. */
@Output()
readonly indexFocused = new EventEmitter<number>();
@ContentChild(TabHeaderAddonDirective, { static: false })
_headerAddon: TabHeaderAddonDirective;
@ContentChild(TabTitleDirective, { static: false })
_title: TabTitleDirective;
@ContentChildren(TabLabelWrapperDirective)
/**
* workaround for @link https://github.com/microsoft/TypeScript/pull/42425
*/
_labelWrappers: QueryList<TabLabelWrapperDirective & { disabled?: boolean }>;
@ViewChild('tabListContainer', { static: true })
_tabListContainer: ElementRef<HTMLElement>;
@ViewChild('tabList', { static: true })
_tabList: ElementRef<HTMLElement>;
@ViewChild('paginationWrapper', { static: true })
_paginationWrapper: ElementRef<HTMLElement>;
@ViewChild(TabHeaderActiveIndicatorComponent, { static: true })
_activeIndicator: TabHeaderActiveIndicatorComponent;
ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}
ngAfterContentChecked(): void {
// If the number of tab labels have changed, check if scrolling should be enabled
if (this._tabLabelCount !== this._labelWrappers.length) {
this._updatePagination();
this._tabLabelCount = this._labelWrappers.length;
this._changeDetectorRef.markForCheck();
}
// If the selected index has changed, scroll to the label and check if the scrolling controls
// should be disabled.
if (this._selectedIndexChanged) {
this._scrollToLabel(this._selectedIndex);
this._checkScrollingControls();
this._alignActiveIndicatorToSelectedTab();
this._selectedIndexChanged = false;
this._changeDetectorRef.markForCheck();
}
// If the scroll distance has been changed (tab selected, focused, scroll controls activated),
// then translate the header to reflect this.
if (this._scrollDistanceChanged) {
this._updateTabScrollPosition();
this._scrollDistanceChanged = false;
this._changeDetectorRef.markForCheck();
}
if (this._showAddon !== !!this._headerAddon) {
this._showAddon = !!this._headerAddon;
this._updatePagination();
this._changeDetectorRef.markForCheck();
}
}
/**
* Aligns the ink bar to the selected tab on load.
*/
ngAfterContentInit() {
const resize = this._viewportRuler.change(150);
const realign = () => {
this._updatePagination();
this._alignActiveIndicatorToSelectedTab();
};
this._keyManager = new FocusKeyManager(this._labelWrappers)
.withHorizontalOrientation('ltr')
.withWrap();
this._keyManager.updateActiveItem(0);
// Defer the first call in order to allow for slower browsers to lay out the elements.
// This helps in cases where the user lands directly on a page with paginated tabs.
typeof requestAnimationFrame !== 'undefined'
? requestAnimationFrame(realign)
: realign();
// On window resize, realign the ink bar and update the orientation of
// the key manager if the direction has changed.
resize.pipe(takeUntil(this._destroyed)).subscribe(() => {
realign();
});
// If there is a change in the focus key manager we need to emit the `indexFocused`
// event in order to provide a public event that notifies about focus changes. Also we realign
// the tabs container by scrolling the new focused tab into the visible section.
this._keyManager.change
.pipe(takeUntil(this._destroyed))
.subscribe(newFocusIndex => {
this.indexFocused.emit(newFocusIndex);
this._setTabFocus(newFocusIndex);
});
}
/** Sets the distance in pixels that the tab header should be transformed in the X-axis. */
get scrollDistance(): number {
return this._scrollDistance;
}
set scrollDistance(v: number) {
this._scrollDistance = Math.max(
0,
Math.min(this._getMaxScrollDistance(), v),
);
// Mark that the scroll distance has changed so that after the view is checked, the CSS
// transformation can move the header.
this._scrollDistanceChanged = true;
this._checkScrollingControls();
}
/** Tracks which element has focus; used for keyboard navigation */
get focusIndex(): number {
return this._keyManager ? this._keyManager.activeItemIndex : 0;
}
/** When the focus index is set, we must manually send focus to the correct label */
set focusIndex(value: number) {
if (
!this._isValidIndex(value) ||
this.focusIndex === value ||
!this._keyManager
) {
return;
}
this._keyManager.setActiveItem(value);
}
/**
* Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
* providing a valid index and return true.
*/
_isValidIndex(index: number): boolean {
if (!this._labelWrappers) {
return true;
}
const tab = this._labelWrappers
? this._labelWrappers.toArray()[index]
: null;
return !!tab && !tab.disabled;
}
_handleKeydown(event: KeyboardEvent) {
switch (event.key) {
case 'Home':
this._keyManager.setFirstItemActive();
event.preventDefault();
break;
case 'End':
this._keyManager.setLastItemActive();
event.preventDefault();
break;
case 'Enter':
case 'Space':
this.selectFocusedIndex.emit(this.focusIndex);
event.preventDefault();
break;
default:
this._keyManager.onKeydown(event);
}
}
/**
* Sets focus on the HTML element for the label wrapper and scrolls it into the view if
* scrolling is enabled.
*/
_setTabFocus(tabIndex: number) {
if (this._showPaginationControls) {
this._scrollToLabel(tabIndex);
}
if (this._labelWrappers?.length > 0) {
this._labelWrappers.toArray()[tabIndex].focus();
// Do not let the browser manage scrolling to focus the element, this will be handled
// by using translation. In LTR, the scroll left should be 0. In RTL, the scroll width
// should be the full width minus the offset width.
const containerEl = this._tabListContainer.nativeElement;
containerEl.scrollLeft = 0;
}
}
/**
* Moves the tab list such that the desired tab label (marked by index) is moved into view.
*
* This is an expensive call that forces a layout reflow to compute box and scroll metrics and
* should be called sparingly.
*/
_scrollToLabel(labelIndex: number) {
const selectedLabel = this._labelWrappers
? this._labelWrappers.toArray()[labelIndex]
: null;
if (!selectedLabel) {
return;
}
// The view length is the visible width of the tab labels.
const viewLength = this._tabListContainer.nativeElement.offsetWidth;
const labelBeforePos = selectedLabel.getOffsetLeft();
const labelAfterPos = labelBeforePos + selectedLabel.getOffsetWidth();
const beforeVisiblePos = this.scrollDistance;
const afterVisiblePos = this.scrollDistance + viewLength;
if (labelBeforePos < beforeVisiblePos) {
// Scroll header to move label to the before direction
this.scrollDistance -=
beforeVisiblePos - labelBeforePos + EXAGGERATED_OVERSCROLL;
} else if (labelAfterPos > afterVisiblePos) {
// Scroll header to move label to the after direction
this.scrollDistance +=
labelAfterPos - afterVisiblePos + EXAGGERATED_OVERSCROLL;
}
}
/**
* Moves the tab list in the 'before' or 'after' direction (towards the beginning of the list or
* the end of the list, respectively). The distance to scroll is computed to be a third of the
* length of the tab list view window.
*
* This is an expensive call that forces a layout reflow to compute box and scroll metrics and
* should be called sparingly.
*/
_scrollHeader(scrollDir: ScrollDirection) {
const viewLength = this._tabListContainer.nativeElement.offsetWidth;
// Move the scroll distance one-third the length of the tab list's viewport.
this.scrollDistance += ((scrollDir === 'before' ? -1 : 1) * viewLength) / 3;
}
/**
* Callback for when the MutationObserver detects that the content has changed.
*/
_onContentChanges() {
this._updatePagination();
this._alignActiveIndicatorToSelectedTab();
this._changeDetectorRef.markForCheck();
}
/**
* Updating the view whether pagination should be enabled or not
*/
_updatePagination() {
this._checkPaginationEnabled();
this._checkScrollingControls();
this._updateTabScrollPosition();
}
/**
* Evaluate whether the pagination controls should be displayed. If the scroll width of the
* tab list is wider than the size of the header container, then the pagination controls should
* be shown.
*
* This is an expensive call that forces a layout reflow to compute box and scroll metrics and
* should be called sparingly.
*/
_checkPaginationEnabled() {
const isEnabled =
this._tabList.nativeElement.scrollWidth >
this._paginationWrapper.nativeElement.offsetWidth + 2; // 2 is the border size
if (!isEnabled) {
this.scrollDistance = 0;
}
const detectChanges = isEnabled !== this._showPaginationControls;
this._showPaginationControls = isEnabled;
if (detectChanges) {
this._changeDetectorRef.markForCheck();
}
}
/**
* Evaluate whether the before and after controls should be enabled or disabled.
* If the header is at the beginning of the list (scroll distance is equal to 0) then disable the
* before button. If the header is at the end of the list (scroll distance is equal to the
* maximum distance we can scroll), then disable the after button.
*
* This is an expensive call that forces a layout reflow to compute box and scroll metrics and
* should be called sparingly.
*/
_checkScrollingControls() {
// Check if the pagination arrows should be activated.
this._disableScrollBefore = this.scrollDistance === 0;
this._disableScrollAfter =
this.scrollDistance === this._getMaxScrollDistance();
this._changeDetectorRef.markForCheck();
}
/**
* Determines what is the maximum length in pixels that can be set for the scroll distance. This
* is equal to the difference in width between the tab list container and tab header container.
*
* This is an expensive call that forces a layout reflow to compute box and scroll metrics and
* should be called sparingly.
*/
_getMaxScrollDistance(): number {
const lengthOfTabList = this._tabList.nativeElement.scrollWidth;
const viewLength = this._tabListContainer.nativeElement.offsetWidth;
return lengthOfTabList - viewLength || 0;
}
/** Performs the CSS transformation on the tab list that will cause the list to scroll. */
_updateTabScrollPosition() {
const scrollDistance = this.scrollDistance;
const translateX = -scrollDistance;
// Don't use `translate3d` here because we don't want to create a new layer. A new layer
// seems to cause flickering and overflow in Internet Explorer. For example, the ink bar
// and ripples will exceed the boundaries of the visible tab bar.
// See: https://github.com/angular/material2/issues/10276
this._tabList.nativeElement.style.transform = `translateX(${translateX}px)`;
}
/** Tells the active indicator to align itself to the current label wrapper */
_alignActiveIndicatorToSelectedTab(): void {
const selectedLabelWrapper =
this._labelWrappers?.length > 0
? this._labelWrappers.toArray()[this.selectedIndex].elementRef
.nativeElement
: null;
this._activeIndicator.alignToElement(selectedLabelWrapper);
}
constructor(
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _viewportRuler: ViewportRuler,
) {}
}
Example #9
Source File: hero.component.ts From angular-component-library with BSD 3-Clause "New" or "Revised" License | 4 votes |
/**
* [Hero Component](https://brightlayer-ui-components.github.io/angular/?path=/info/components-hero--readme)
*
* The `<blui-hero>` components are used to call attention to particular values that are of the most importance to the user.
* These are typically displayed in a banner.
*/
@Component({
selector: 'blui-hero',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
styleUrls: ['./hero.component.scss'],
template: `
<div class="blui-hero-content">
<div
class="blui-hero-primary-wrapper"
#primaryContainer
[style.backgroundColor]="iconBackgroundColor"
[style.lineHeight.px]="iSize"
[style.fontSize.px]="iSize"
[style.width.px]="iSize"
[style.height.px]="iSize"
[class.blui-hero-svgIcon]="hasMatSvgIcon"
>
<ng-content select="[blui-primary]"></ng-content>
</div>
<span class="blui-hero-channel-value-wrapper">
<ng-content select="blui-channel-value" *ngIf="value === undefined"></ng-content>
<blui-channel-value *ngIf="value !== undefined" [value]="value" [units]="units" [unitSpace]="unitSpace">
<ng-content select="[blui-secondary]"></ng-content>
</blui-channel-value>
</span>
<h5 class="blui-hero-label">{{ label }}</h5>
</div>
`,
host: {
class: 'blui-hero',
},
})
export class HeroComponent implements OnChanges, AfterViewInit, AfterContentChecked {
/** Color of the hero icon */
@Input() color: string;
/** Color of the hero background */
@Input() iconBackgroundColor: string;
/** The size of the primary icon (10-48) */
@Input() iconSize = 36;
/** The text shown below the Channel Value */
@Input() label: string;
/** Transforms an SVG icon based on the provided `iconSize` */
@Input() scaleSvgIcon = true;
/** Text to show after the value */
@Input() units: string;
/** The value for the channel */
@Input() value: string;
/** Show a space between the value and units for the channel, default is `auto` */
@Input() unitSpace: UnitSpaceType = 'auto';
@ViewChild('primaryContainer') primaryContainer: ElementRef;
iSize: number;
iconString: string;
hasMatSvgIcon: boolean;
constructor(private readonly _ref: ChangeDetectorRef) {}
ngOnChanges(): void {
requireInput<HeroComponent>(['label'], this);
this.iSize = this.iconSize;
}
ngAfterViewInit(): void {
this.hasMatSvgIcon = Boolean(this.getMatSvgIcon());
this._ref.detectChanges();
}
ngAfterContentChecked(): void {
this.scaleSvgIconOnChanges();
}
// Used to listen for changes in the primary icon content.
// SVG icon content might be loaded post-render (fetching icon via http)
scaleSvgIconOnChanges(): void {
if (!this.hasMatSvgIcon || !this.primaryContainer || !this.scaleSvgIcon) {
return;
}
// If the primary ng-content did not change, no action required.
const iconHtml = this.primaryContainer.nativeElement.innerHTML;
if (iconHtml === this.iconString) {
return;
}
// Update icon data.
this.iconString = iconHtml;
const matIcon = this.getMatSvgIcon();
if (matIcon) {
this.hasMatSvgIcon = true;
const svg = matIcon.querySelector('svg');
if (svg) {
svg.style.setProperty('transform', `scale(${this.iSize / 24})`);
}
}
}
// Returns projected <mat-icon> with attribute [svgIcon] if it exists
getMatSvgIcon(): HTMLElement {
return this.primaryContainer.nativeElement.querySelector('div mat-icon[svgicon]');
}
}
Example #10
Source File: list-edit.component.ts From attack-workbench-frontend with Apache License 2.0 | 4 votes |
@Component({
selector: 'app-list-edit',
templateUrl: './list-edit.component.html',
styleUrls: ['./list-edit.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ListEditComponent implements OnInit, AfterContentChecked {
@Input() public config: ListPropertyConfig;
// allowed values (editType: 'select')
public allAllowedValues: any;
public selectControl: FormControl;
public disabledTooltip: string = "a valid domain must be selected first";
public dataLoaded: boolean = false;
// prevent async issues
private sub: Subscription = new Subscription();
public fieldToStix = {
"platforms": "x_mitre_platforms",
"tactic_type": "x_mitre_tactic_type",
"impact_type": "x_mitre_impact_type",
"effective_permissions": "x_mitre_effective_permissions",
"permissions_required": "x_mitre_permissions_required",
"collection_layers": "x_mitre_collection_layers",
"data_sources": "x_mitre_data_sources"
}
public domains = [
"enterprise-attack",
"mobile-attack",
"ics-attack"
]
// selection model (editType: 'stixList')
public select: SelectionModel<string>;
public type: string;
public allObjects: StixObject[] = [];
// any value (editType: 'any')
public inputControl: FormControl;
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(public dialog: MatDialog, private restAPIConnectorService: RestApiConnectorService, private ref: ChangeDetectorRef) { }
ngOnInit(): void {
this.selectControl = new FormControl({value: this.config.object[this.config.field], disabled: this.config.disabled ? this.config.disabled : false});
this.inputControl = new FormControl(null, this.config.required ? [Validators.required] : undefined);
if (this.config.field == 'platforms'
|| this.config.field == 'tactic_type'
|| this.config.field == 'permissions_required'
|| this.config.field == 'effective_permissions'
|| this.config.field == 'impact_type'
|| this.config.field == 'domains'
|| this.config.field == 'collection_layers'
|| this.config.field == 'data_sources') {
if (!this.dataLoaded) {
let data$ = this.restAPIConnectorService.getAllAllowedValues();
this.sub = data$.subscribe({
next: (data) => {
let stixObject = this.config.object as StixObject;
this.allAllowedValues = data.find(obj => { return obj.objectType == stixObject.attackType; });
this.dataLoaded = true;
},
complete: () => { this.sub.unsubscribe(); }
});
}
}
else if (this.config.field == 'defense_bypassed') { } //any
else if (this.config.field == 'system_requirements') { } //any
else if (this.config.field == 'contributors') { } //any
else if (this.config.field == 'tactics') {
this.type = 'tactic';
let subscription = this.restAPIConnectorService.getAllTactics().subscribe({
next: (tactics) => {
this.allObjects = tactics.data;
// retrieve currently selected tactics
let object = this.config.object as any;
let selectedTactics = this.shortnameToTactic(object.domains);
let selectedTacticIDs = selectedTactics.map(tactic => tactic.stixID);
// set up domain & tactic tracking
this.domainState = [];
this.tacticState = [];
object.domains.forEach(domain => this.domainState.push(domain));
selectedTactics.forEach(tactic => this.tacticState.push(tactic));
// set selection model with initial values
this.select = new SelectionModel<string>(true, selectedTacticIDs);
this.dataLoaded = true;
},
complete: () => { subscription.unsubscribe(); }
})
}
}
ngAfterContentChecked() {
this.ref.detectChanges();
}
/** Retrieves a list of selected tactics */
private shortnameToTactic(domains: string[]): Tactic[] {
let allObjects = this.allObjects as Tactic[];
let tactics = this.config.object[this.config.field].map(shortname => {
let tactic = allObjects.find(tactic => {
return tactic.shortname == shortname && this.tacticInDomain(tactic, domains)
});
return tactic;
})
return tactics;
}
/** Retrieves a list of selected tactic shortnames */
private stixIDToShortname(tacticID: string): string[] {
let allObjects = this.allObjects as Tactic[];
let tactic = allObjects.find(object => object.stixID == tacticID)
return [tactic.shortname, tactic.domains[0]];
}
/** Update current stix-list selection on domain change */
private domainState: string[];
private tacticState: Tactic[];
public selectedValues(): string[] {
if (!this.dataLoaded) return null;
let isEqual = function(arr1:string[], arr2:string[]) {
return (arr1.length == arr2.length) && arr1.every(function(element, index) {
return element === arr2[index];
});
}
if (this.config.field == 'tactics') {
let object = this.config.object as Tactic;
if (!isEqual(this.domainState, object.domains)) {
// get selected tactics
let selectedTactics = this.tacticState;
if (object.domains.length < this.domainState.length) { // a domain was removed
// filter selected tactics with updated domain selection
selectedTactics = selectedTactics.filter(tactic => this.tacticInDomain(tactic));
let selectedTacticIDs = selectedTactics.map(tactic => tactic.stixID);
let tacticShortnames = selectedTacticIDs.map(id => this.stixIDToShortname(id));
// udpate object & selection model
this.config.object[this.config.field] = tacticShortnames;
this.select.clear();
selectedTacticIDs.forEach(tactic=>this.select.select(tactic));
}
// reset domain & tactic selection state with a copy of current state
this.domainState = [];
this.tacticState = [];
object.domains.forEach(domain => this.domainState.push(domain));
selectedTactics.forEach(tactic => this.tacticState.push(tactic));
}
}
return this.config.object[this.config.field];
}
/** Get allowed values for this field */
public getAllowedValues(): string[] {
if (this.config.field == 'domains') return this.domains;
if (!this.dataLoaded) {
this.selectControl.disable();
return null;
};
// filter values
let values: string[] = [];
let property = this.allAllowedValues.properties.find(p => {return p.propertyName == this.fieldToStix[this.config.field]});
if (!property) { // property not found
this.selectControl.disable();
return null;
}
if ("domains" in this.config.object) {
let object = this.config.object as any;
property.domains.forEach(domain => {
if (object.domains.includes(domain.domainName)) {
values = values.concat(domain.allowedValues);
}
})
}
else { // domains not specified on object
property.domains.forEach(domain => {
values = values.concat(domain.allowedValues);
});
}
// check for existing data
if (this.selectControl.value) {
for (let value of this.selectControl.value) {
if (!values.includes(value)) values.push(value);
}
}
if (!values.length) {
// disable field and reset selection
this.selectControl.disable();
this.selectControl.reset();
this.config.object[this.config.field] = [];
}
else {
this.selectControl.enable(); // re-enable field
}
return values;
}
/** Add value to object property list */
public add(event: MatChipInputEvent): void {
if (event.value && event.value.trim()) {
this.config.object[this.config.field].push(event.value.trim());
this.inputControl.setValue(this.config.object[this.config.field]);
}
if (event.input) {
event.input.value = ''; // reset input value
}
}
/** Remove value from object property list */
public remove(value: string): void {
let i = this.config.object[this.config.field].indexOf(value);
if (i >= 0) {
this.config.object[this.config.field].splice(i, 1);
}
this.inputControl.setValue(this.config.object[this.config.field])
}
/** Remove selection from via chip cancel button */
public removeSelection(value: string): void {
let values = this.selectControl.value as string[];
let i = values.indexOf(value);
if (i >= 0) {
values.splice(i, 1);
}
this.selectControl.setValue([]); // reset selection
this.selectControl.setValue(values);
this.remove(value); // remove value from object property
}
/** Add or remove selection from object property list via select-list */
public change(event: MatOptionSelectionChange): void {
if (event.isUserInput) {
if (event.source.selected) this.config.object[this.config.field].push(event.source.value);
else this.remove(event.source.value);
}
}
/** Check if the given tactic is in the same domain as this object */
private tacticInDomain(tactic: Tactic, domains?: string[]) {
let object = this.config.object as Tactic;
let checkDomains = domains ? domains : object.domains;
for (let domain of tactic.domains) {
if (checkDomains.includes(domain)) return true;
}
}
/** Open stix list selection window */
public openStixList() {
// filter tactic objects by domain
let tactics = this.allObjects as Tactic[];
let selectableObjects = tactics.filter(tactic => this.tacticInDomain(tactic));
let dialogRef = this.dialog.open(AddDialogComponent, {
maxWidth: "70em",
maxHeight: "70em",
data: {
selectableObjects: selectableObjects,
select: this.select,
type: this.type,
buttonLabel: "OK"
}
});
let selectCopy = new SelectionModel(true, this.select.selected);
let subscription = dialogRef.afterClosed().subscribe({
next: (result) => {
if (result) {
let tacticShortnames = this.select.selected.map(id => this.stixIDToShortname(id));
this.config.object[this.config.field] = tacticShortnames;
// reset tactic selection state
this.tacticState = [];
let allObjects = this.allObjects as Tactic[];
let tactics = this.select.selected.map(tacticID => allObjects.find(tactic => tactic.stixID == tacticID));
tactics.forEach(tactic => this.tacticState.push(tactic));
} else { // user cancel
this.select = selectCopy; // reset selection
}
},
complete: () => { subscription.unsubscribe(); }
});
}
}
Example #11
Source File: configuration.component.ts From HeartBeat with MIT License | 4 votes |
@Component({
selector: 'app-configuration',
templateUrl: './configuration.component.html',
styleUrls: ['./configuration.component.scss'],
})
export class ConfigurationComponent implements OnInit, AfterContentChecked {
@Output() stepOneSubmit = new EventEmitter();
importData: any;
metricsConfig: MetricsConfig = Config.metricsConfig;
metrics: Metric[] = Config.metrics;
metricsSource: MetricsSource = {};
filterMetricsConfig: MetricsConfig = {};
matcher = new DoraErrorStateMatcher();
sourceControl: { type: string; data: Array<string> };
doneColumnsControlName = controlNames.doneStatus;
doneKeyFromBackend = metricsConstant.doneKeyFromBackend;
configForm = this.formBuilder.group(
{
projectName: ['', Validators.required],
metrics: ['', Validators.required],
},
{
validators: this.tokenVerifyService.verifyTokenValidator(),
}
);
get selectedMetrics(): Metric[] {
return this.configForm.get('metrics').value.map((name) => this.metrics.find((metric) => metric.name === name));
}
get selectedMetricRoles() {
return new Set(this.selectedMetrics.flatMap((metric) => metric.roles));
}
constructor(
private formBuilder: FormBuilder,
private tokenVerifyService: TokenVerifyService,
private utils: UtilsService,
private activatedRoute: ActivatedRoute,
private cdref: ChangeDetectorRef,
private router: Router,
public dialog: MatDialog,
private stepsFetchService: StepsFetchService,
private cycleDoneService: CycleDoneService
) {}
ngOnInit(): void {
this.importMetrics();
}
ngAfterContentChecked(): void {
this.cdref.detectChanges();
}
importMetrics() {
this.activatedRoute.paramMap.pipe(map(() => window.history.state)).subscribe((result) => {
if (!result || !result.data) {
return;
}
this.importData = JSON.parse(result.data);
this.configForm.patchValue({
metrics: this.importData.metrics || [],
projectName: this.importData.projectName || '',
});
this.selectedMetricRoles.forEach((formGroupName) => {
this.filterMetricsConfig[formGroupName] = this.metricsConfig[formGroupName];
});
});
}
selectionChange({ value: metrics }) {
this.filterMetricsConfig = {};
this.selectedMetricRoles.forEach((formGroupName) => {
this.filterMetricsConfig[formGroupName] = this.metricsConfig[formGroupName];
});
if (!this.selectedMetricRoles.has(sourceControlMetricKey)) {
this.sourceControl = null;
}
}
onVerify({ formGroupName, value }) {
if (formGroupName === sourceControlMetricKey) {
this.sourceControl = value;
return;
}
this.metricsSource[formGroupName] = value;
}
getRequestParams(formValue): ReportParams {
const reportRequestParams = new ReportParams(formValue);
if (formValue.pipelineTool) {
reportRequestParams.pipeline = new PipelineParams(formValue.pipelineTool);
}
if (formValue.sourceControl) {
reportRequestParams.codebaseSetting = new CodebaseParams(formValue.sourceControl);
}
if (formValue.board) {
reportRequestParams.kanbanSetting = new BoardParams(formValue.board);
}
return reportRequestParams;
}
getMetricsSource() {
Object.keys(this.metricsSource).forEach((key) => {
if (![...this.selectedMetricRoles].includes(key)) {
delete this.metricsSource[key];
}
});
if (this.sourceControl) {
const pipelines = this.filterMetricSourceForSourceControl(this.sourceControl.data, this.metricsSource);
this.metricsSource.sourceControl = {
type: this.sourceControl.type,
data: pipelines,
};
this.sourceControl = null;
}
return this.metricsSource;
}
filterMetricSourceForSourceControl(sourceControlRepos: Array<string>, metricsSource: MetricsSource) {
const repos = sourceControlRepos.map((repo) => {
const urlParse = GitUrlParse(repo);
return `${urlParse.source}/${urlParse.full_name}`;
});
const pipelines = metricsSource.pipelineTool.data.filter((pipeline) => {
const urlParse = GitUrlParse(pipeline.repository);
const url = `${urlParse.source}/${urlParse.full_name}`;
return repos.includes(url);
});
return pipelines;
}
onSubmit() {
const metricsSource = this.getMetricsSource();
const reportsParams = this.getRequestParams(this.configForm.value);
this.saveDataForNextPage(metricsSource, reportsParams);
this.stepOneSubmit.emit({ metricsSource, reportsParams, configFormValue: this.configForm.value });
}
saveDataForNextPage(metricsSource: MetricsSource, reportsParams: ReportParams) {
if (reportsParams.pipeline) {
const {
pipeline: { token, type },
startTime,
endTime,
} = reportsParams;
this.stepsFetchService.setValue({ token, type, startTime, endTime });
}
if (metricsSource && metricsSource.board && metricsSource.board.data.jiraColumns) {
const jiraColumnsData = this.metricsSource.board.data.jiraColumns;
const importDoneStatus = this.importData && this.importData[this.doneColumnsControlName];
let doneStatusArr = [];
if (importDoneStatus && importDoneStatus.length > 0) {
doneStatusArr = importDoneStatus
.map((item) => jiraColumnsData.find((i) => i.value.statuses.includes(item)))
.flatMap((item) => item && item.value.statuses);
} else {
doneStatusArr = jiraColumnsData
.filter((item) => item.key === this.doneKeyFromBackend)
.flatMap((item) => item && item.value.statuses);
}
this.cycleDoneService.setValue([...new Set(doneStatusArr)]);
}
}
trackByItems(index: number, config: { key; value }): number {
return config.key;
}
saveConfig() {
this.utils.exportToJsonFile({ fileName: 'config.json', json: this.configForm.value });
}
backToHome() {
const dialogRef = this.dialog.open(ConfirmDialogComponent, { disableClose: true });
dialogRef.afterClosed().subscribe((isConfirm) => {
isConfirm && this.router.navigate(['/dora/home'], { replaceUrl: true });
});
}
}
Example #12
Source File: metrics.component.ts From HeartBeat with MIT License | 4 votes |
@Component({
selector: 'app-metrics',
templateUrl: './metrics.component.html',
styleUrls: ['./metrics.component.scss'],
})
export class MetricsComponent implements OnInit, AfterContentChecked, OnChanges {
@Input() metricsSource: MetricsSource;
@Input() metricsParams: string[];
@Input() configFormValue: any;
@Output() stepTwoSubmit = new EventEmitter();
leadTimeControlName = controlNames.leadTime;
doneStatusControlName = controlNames.doneStatus;
cycleTimeControlName = controlNames.cycleTime;
crewsControlName = controlNames.crews;
jiraColumnsControlName = controlNames.jiraColumns;
treatFlagCardAsBlockControlName = controlNames.treatFlagCardAsBlock;
classificationsControlName = controlNames.classifications;
deploymentControlName = controlNames.deployment;
displayItems: string[];
importConfig = null;
metricsForm = this.formBuilder.group({});
get pipeline() {
return this.metricsForm.get(this.deploymentControlName) as FormArray;
}
get leadTime() {
return this.metricsForm.get(this.leadTimeControlName) as FormArray;
}
constructor(
private formBuilder: FormBuilder,
private cdref: ChangeDetectorRef,
private utils: UtilsService,
private importConfigService: ImportConfigService
) {}
ngAfterContentChecked(): void {
this.cdref.detectChanges();
}
formatJiraColumns(columns) {
return columns
.map((item) => {
const returnItem = {
name: Object.keys(item)[0],
value: Object.values(item)[0],
};
return returnItem;
})
.filter((item) => item.value !== metricsConstant.cycleTimeEmptyStr);
}
formatCrews(crews: string[]) {
const isAllChecked = crews.includes(metricsConstant.crewAll);
if (!isAllChecked) {
return crews;
}
const index = crews.findIndex((item) => item === metricsConstant.crewAll);
crews.splice(index, 1);
return crews;
}
formatClassifications(classifications: string[]) {
const isAllChecked = classifications.includes(metricsConstant.crewAll);
const originalList = this.metricsSource.board.data.targetFields;
if (!classifications) {
return originalList;
}
if (isAllChecked) {
originalList.map((item) => (item.flag = true));
} else {
originalList.forEach((item) => {
item.flag = !!classifications.find((classification) => {
return classification === item.key;
});
});
}
return originalList;
}
formatDoneColumns() {
if (this.metricsSource.board && this.metricsSource.board.data.jiraColumns) {
const doneColumn = this.metricsSource.board.data.jiraColumns.find(
(item) => item.key === metricsConstant.doneKeyFromBackend
);
return doneColumn ? doneColumn.value.statuses : [];
}
return [];
}
formatPipeline(selectedPipelines) {
return selectedPipelines.map(({ pipelineId, step }) => {
const pipeline = this.metricsSource.pipelineTool.data.find((pipelineItem) => pipelineItem.id === pipelineId);
return {
orgId: pipeline.orgId,
orgName: pipeline.orgName,
id: pipeline.id,
name: pipeline.name,
step,
repository: pipeline.repository,
};
});
}
onSubmit() {
const deployment = this.metricsForm.value[this.deploymentControlName]
? this.formatPipeline(this.metricsForm.value[this.deploymentControlName])
: [];
const leadTime = this.metricsForm.value[this.leadTimeControlName]
? this.formatPipeline(this.metricsForm.value[this.leadTimeControlName])
: [];
const boardColumns = this.metricsForm.value[this.cycleTimeControlName]
? this.formatJiraColumns(this.metricsForm.value[this.cycleTimeControlName][this.jiraColumnsControlName])
: [];
const treatFlagCardAsBlock = this.metricsForm.value[this.cycleTimeControlName]
? this.metricsForm.value[this.cycleTimeControlName][this.treatFlagCardAsBlockControlName]
: false;
const doneColumn = this.metricsForm.value[this.doneStatusControlName] || this.formatDoneColumns();
const users = this.metricsForm.value[this.crewsControlName]
? this.formatCrews(this.metricsForm.value[this.crewsControlName])
: [];
const targetFields = this.metricsForm.value[this.classificationsControlName]
? this.formatClassifications(this.metricsForm.value[this.classificationsControlName])
: this.metricsSource.board?.data.targetFields;
this.stepTwoSubmit.emit({
deployment,
boardColumns,
treatFlagCardAsBlock,
users,
leadTime,
targetFields,
doneColumn,
});
}
ngOnInit(): void {
this.displayItems = this.displayList();
}
displayList() {
if (!this.metricsParams) {
return [];
}
const array = this.metricsParams
.map((metricName) => metricFormConfig.find((config) => config.name === metricName))
.flatMap((config) => config.displayItems);
return [...new Set(array)];
}
ngOnChanges(changes: SimpleChanges): void {
const metricsParamsChanges = changes.metricsParams;
if (
metricsParamsChanges &&
metricsParamsChanges.currentValue &&
metricsParamsChanges.currentValue !== metricsParamsChanges.previousValue
) {
this.displayItems = this.displayList();
}
this.initImportConfig();
}
initImportConfig() {
if (this.importConfigService && this.importConfigService.get()) {
this.importConfig = Object.assign({}, this.importConfigService.get());
}
}
saveConfig() {
this.utils.exportToJsonFile({
fileName: 'config.json',
json: { ...this.configFormValue, ...this.metricsForm.value },
});
}
}
Example #13
Source File: table.ts From halstack-angular with Apache License 2.0 | 3 votes |
/**
* A data table that can render a header row, data rows, and a footer row.
* Uses the dataSource input to determine the data to be rendered. The data can be provided either
* as a data array, an Observable stream that emits the data array to render, or a DataSource with a
* connect function that will return an Observable stream that emits the data array to render.
*/
@Component({
selector: "dxc-resultset-table, table[dxc-resultset-table]",
exportAs: "dxcResultsetTable",
template: `
<dxc-table [margin]="margin">
<ng-container headerOutlet></ng-container>
<ng-container rowOutlet></ng-container>
</dxc-table>
<dxc-paginator
*ngIf="totalItems !== null"
[totalItems]="totalItems"
[itemsPerPage]="itemsPerPage"
[itemsPerPageOptions]="itemsPerPageOptions"
[currentPage]="page"
[showGoToPage]="showGoToPage"
(onGoToPage)="navigate($event)"
(itemsPerPageFunction)="handleItemsPerPageSelect($event)"
></dxc-paginator>
`,
encapsulation: ViewEncapsulation.None,
// The "OnPush" status for the `MatTable` component is effectively a noop, so we are removing it.
// The view for `MatTable` consists entirely of templates declared in other views. As they are
// declared elsewhere, they are checked when their declaration points are checked.
// tslint:disable-next-line:validate-decorators
changeDetection: ChangeDetectionStrategy.Default,
providers: [
{ provide: DXC_RESULTSET_TABLE, useExisting: DxcResultTable },
PaginationService,
SortService,
],
})
export class DxcResultTable<T>
implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit
{
/**
* Number of items per page.
*/
@Input()
itemsPerPage: number = 5;
/**
* An array of objects with the values to display in the table.
* The key is the column and the value is the property to be displayed in the cell.
*/
@Input()
get collectionResource(): Array<any> {
return this._collectionResource;
}
set collectionResource(value: Array<any>) {
this._collectionResource = coerceArray(value);
}
private _collectionResource;
/**
* Size of the margin to be applied to the component
* ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge').
* You can pass an object with 'top', 'bottom', 'left' and 'right' properties in
* order to specify different padding sizes.
*/
@Input() margin: Space | Spacing;
/**
* An array of numbers representing the items per page options.
*/
@Input() public itemsPerPageOptions: number[];
/**
* Show page navigation select.
*/
@Input() public showGoToPage: boolean = true;
/**
* Value of the tabindex attribute given to the sortable icon.
*/
@Input()
get tabIndexValue(): number {
return this._tabIndexValue;
}
set tabIndexValue(value: number) {
this._tabIndexValue = coerceNumberProperty(value);
}
private _tabIndexValue = 0;
/**
* This event will emit in case of the user selects an item per page
* option. The value selected will be passed as a parameter.
*/
@Output() itemsPerPageFunction: EventEmitter<number> =
new EventEmitter<number>();
collectionData: BehaviorSubject<Array<any>> = new BehaviorSubject([]);
displayedColumns: string[] = [];
totalItems: Number = 0;
fetchStatus;
page: number = 1;
@HostBinding("class") className;
/** List of ordering directives. */
private _allOrderingRefs: ElementRef[] = [];
private _document: Document;
/** Latest data provided by the data source. */
protected _data: T[] | ReadonlyArray<T>;
/** Subject that emits when the component has been destroyed. */
private _onDestroy = new Subject<void>();
/** List of the rendered rows as identified by their `RenderRow` object. */
private _renderRows: RenderRow<T>[];
/** Subscription that listens for the data provided by the data source. */
private _renderChangeSubscription: Subscription | null;
/**
* Map of all the user's defined columns (header, data, and footer cell template) identified by
* name. Collection populated by the column definitions gathered by `ContentChildren` as well as
* any custom column definitions added to `_customColumnDefs`.
*/
private _columnDefsByName = new Map<string, DxcColumnDef>();
/**
* Set of all row definitions that can be used by this table. Populated by the rows gathered by
* using `ContentChildren` as well as any custom row definitions added to `_customRowDefs`.
*/
//private _rowDefs: CdkRowDef<T>[];
/** Differ used to find the changes in the data provided by the data source. */
private _dataDiffer: IterableDiffer<RenderRow<T>>;
/**
* Column definitions that were defined outside of the direct content children of the table.
* These will be defined when, e.g., creating a wrapper around the cdkTable that has
* column definitions as *its* content child.
*/
private _customColumnDefs = new Set<DxcColumnDef>();
/**
* Cache of the latest rendered `RenderRow` objects as a map for easy retrieval when constructing
* a new list of `RenderRow` objects for rendering rows. Since the new list is constructed with
* the cached `RenderRow` objects when possible, the row identity is preserved when the data
* and row template matches, which allows the `IterableDiffer` to check rows by reference
* and understand which rows are added/moved/removed.
*
* Implemented as a map of maps where the first key is the `data: T` object and the second is the
* `CdkRowDef<T>` object. With the two keys, the cache points to a `RenderRow<T>` object that
* contains an array of created pairs. The array is necessary to handle cases where the data
* array contains multiple duplicate data objects and each instantiated `RenderRow` must be
* stored.
*/
private _cachedRenderRowsMap = new Map<T, WeakMap<Object, RenderRow<T>[]>>();
/** Whether the table is applied to a native `<table>`. */
private _isNativeHtmlTable: boolean;
/**
* Tracking function that will be used to check the differences in data changes. Used similarly
* to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data
* relative to the function to know if a row should be added/removed/moved.
* Accepts a function that takes two parameters, `index` and `item`.
*/
@Input()
get trackBy(): TrackByFunction<T> {
return this._trackByFn;
}
set trackBy(fn: TrackByFunction<T>) {
if (
isDevMode() &&
fn != null &&
typeof fn !== "function" &&
<any>console &&
<any>console.warn
) {
console.warn(
`trackBy must be a function, but received ${JSON.stringify(fn)}.`
);
}
this._trackByFn = fn;
}
private _trackByFn: TrackByFunction<T>;
private dataSource: dxcResultsetTableDataSourceInput<T>;
// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
/**
* Stream containing the latest information on what rows are being displayed on screen.
* Can be used by the data source to as a heuristic of what data should be provided.
*
* @docs-private
*/
readonly viewChange = new BehaviorSubject<{ start: number; end: number }>({
start: 0,
end: Number.MAX_VALUE,
});
// Outlets in the table's template where the header, data rows, and footer will be inserted.
@ViewChild(HeaderOutlet, { static: true }) _headerOutlet: HeaderOutlet;
@ViewChild(DataRowOutlet, { static: true }) _rowOutlet: DataRowOutlet;
/**
* The column definitions provided by the user that contain what the header, data, and footer
* cells should render for each column.
*/
@ContentChildren(DxcColumnDef, { descendants: true })
_contentColumnDefs: QueryList<DxcColumnDef>;
constructor(
protected readonly _differs: IterableDiffers,
protected readonly _changeDetectorRef: ChangeDetectorRef,
protected readonly _elementRef: ElementRef,
@Attribute("role") role: string,
@Optional() protected readonly _dir: Directionality,
@Inject(DOCUMENT) _document: any,
private resolver: ComponentFactoryResolver,
private paginationService: PaginationService,
private sortService: SortService
) {
if (!role) {
this._elementRef.nativeElement.setAttribute("role", "grid");
}
this._document = _document;
this._isNativeHtmlTable =
this._elementRef.nativeElement.nodeName === "TABLE";
this.setClassName();
}
ngOnInit() {
this.collectionData.next(
this.collectionResource.slice(0, this.itemsPerPage)
);
this.dataSource = new TableDataSource(this.collectionData);
this.totalItems = this.collectionResource.length;
if (this._isNativeHtmlTable) {
this._applyNativeTableSections();
}
// Set up the trackBy function so that it uses the `RenderRow` as its identity by default. If
// the user has provided a custom trackBy, return the result of that function as evaluated
// with the values of the `RenderRow`'s data and index.
this._dataDiffer = this._differs
.find([])
.create((_i: number, dataRow: RenderRow<T>) => {
return this.trackBy
? this.trackBy(dataRow.dataIndex, dataRow.data)
: dataRow;
});
}
ngOnChanges() {
this.page = 1;
this.navigate(this.page, "");
}
ngAfterContentChecked() {
// Cache the row and column definitions gathered by ContentChildren and programmatic injection.
//this._cacheRowDefs();
this._cacheColumnDefs();
// Render updates if the list of columns have been changed for the header, row, or footer defs.
// If there is a data source and row definitions, connect to the data source unless a
// connection has already been made.
if (this.dataSource && !this._renderChangeSubscription) {
this._observeRenderChanges();
}
}
ngOnDestroy() {
this._headerOutlet.viewContainer.clear();
this._rowOutlet.viewContainer.clear();
this._cachedRenderRowsMap.clear();
this._onDestroy.next();
this._onDestroy.complete();
if (isDataSource(this.dataSource)) {
this.dataSource.disconnect(this);
}
}
handleItemsPerPageSelect($event) {
this.itemsPerPageFunction.emit($event);
}
renderHeaders() {
this._headerOutlet.viewContainer.clear();
if (this._columnDefsByName !== null) {
this._columnDefsByName.forEach((value: DxcColumnDef, key: string) => {
const factory = this.resolver.resolveComponentFactory(
DxcHeaderRowComponent
);
const viewRef =
this._headerOutlet.viewContainer.createComponent(factory);
viewRef.instance.columnName = key;
viewRef.instance.isSortable = value.sortable.isSortable; //Save if header is sortable in the created component
viewRef.instance.tabIndexValue = this.tabIndexValue;
viewRef.instance.state = this.getMapStateHeaders().get(key); //Get header's current state for sorting and save it in the created component
viewRef.instance.parentClassName = this.className; // just in case there are more tables in the page
viewRef.instance.propertyName = value.sortable.propertyName;
if (!this.displayedColumns.includes(key)) {
this.displayedColumns.push(key);
}
});
}
}
/**
* Renders rows based on the table's latest set of data, which was either provided directly as an
* input or retrieved through an Observable stream (directly or from a DataSource).
* Checks for differences in the data since the last diff to perform only the necessary
* changes (add/remove/move rows).
*
* If the table's data source is a DataSource or Observable, this will be invoked automatically
* each time the provided Observable stream emits a new data array. Otherwise if your data is
* an array, this function will need to be called to render any changes.
*/
renderRows() {
this._renderRows = this._getAllRenderRows();
const changes = this._dataDiffer.diff(this._renderRows);
if (!changes) {
return;
}
const viewContainer = this._rowOutlet.viewContainer;
changes.forEachOperation(
(
record: IterableChangeRecord<RenderRow<T>>,
prevIndex: number | null,
currentIndex: number | null
) => {
if (record.previousIndex == null) {
this._insertRow(record.item, currentIndex!);
} else if (currentIndex == null) {
viewContainer.remove(prevIndex!);
} else {
const view = <RowViewRef<T>>viewContainer.get(prevIndex!);
viewContainer.move(view!, currentIndex);
}
}
);
// Update the meta context of a row's context data (index, count, first, last, ...)
this._updateRowIndexContext();
// Update rows that did not get added/removed/moved but may have had their identity changed,
// e.g. if trackBy matched data on some property but the actual data reference changed.
changes.forEachIdentityChange(
(record: IterableChangeRecord<RenderRow<T>>) => {
const rowView = <RowViewRef<T>>viewContainer.get(record.currentIndex!);
rowView.context.$implicit = record.item.data;
}
);
}
/**
* Get the list of RenderRow objects to render according to the current list of data and defined
* row definitions. If the previous list already contained a particular pair, it should be reused
* so that the differ equates their references.
*/
private _getAllRenderRows(): RenderRow<T>[] {
const renderRows: RenderRow<T>[] = [];
// Store the cache and create a new one. Any re-used RenderRow objects will be moved into the
// new cache while unused ones can be picked up by garbage collection.
const prevCachedRenderRows = this._cachedRenderRowsMap;
this._cachedRenderRowsMap = new Map();
// For each data object, get the list of rows that should be rendered, represented by the
// respective `RenderRow` object which is the pair of `data` and `CdkRowDef`.
for (let i = 0; i < this._data.length; i++) {
let data = this._data[i];
const renderRowsForData = this._getRenderRowsForData(
data,
i,
prevCachedRenderRows.get(data)
);
if (!this._cachedRenderRowsMap.has(data)) {
this._cachedRenderRowsMap.set(data, new WeakMap());
}
for (let j = 0; j < renderRowsForData.length; j++) {
let renderRow = renderRowsForData[j];
const cache = this._cachedRenderRowsMap.get(renderRow.data)!;
if (cache.has(renderRow.data)) {
cache.get(renderRow.data)!.push(renderRow);
} else {
cache.set(renderRow.data, [renderRow]);
renderRows.push(renderRow);
}
}
}
return renderRows;
}
/**
* Gets a list of `RenderRow<T>` for the provided data object and any `DxcRowDef` objects that
* should be rendered for this data. Reuses the cached RenderRow objects if they match the same
* `(T, DxcRowDef)` pair.
*/
private _getRenderRowsForData(
data: T,
dataIndex: number,
cache?: WeakMap<Object, RenderRow<T>[]>
): RenderRow<T>[] {
return this.displayedColumns.map((rowDef) => {
const cachedRenderRows =
cache && cache.has(rowDef) ? cache.get(rowDef)! : [];
if (cachedRenderRows.length) {
const dataRow = cachedRenderRows.shift()!;
dataRow.dataIndex = dataIndex;
return dataRow;
} else {
return { data, rowDef, dataIndex };
}
});
}
/** Update the map containing the content's column definitions. */
private _cacheColumnDefs() {
this._columnDefsByName.clear();
const columnDefs = this.mergeArrayAndSet(
this._getOwnDefs(this._contentColumnDefs),
this._customColumnDefs
);
columnDefs.forEach((columnDef) => {
// if (this._columnDefsByName.has(columnDef.name)) {
// throw getTableDuplicateColumnNameError(columnDef.name);
// }
this._columnDefsByName.set(columnDef.name, columnDef);
});
}
/** Set up a subscription for the data provided by the data source. */
private _observeRenderChanges() {
// If no data source has been set, there is nothing to observe for changes.
if (!this.dataSource) {
return;
}
let dataStream: Observable<T[] | ReadonlyArray<T>> | undefined;
if (isDataSource(this.dataSource)) {
dataStream = this.dataSource.connect(this);
} else if (isObservable(this.dataSource)) {
dataStream = this.dataSource;
} else if (Array.isArray(this.dataSource)) {
dataStream = observableOf(this.dataSource);
}
if (dataStream === undefined) {
throw getTableUnknownDataSourceError();
}
this._renderChangeSubscription = dataStream
.pipe(takeUntil(this._onDestroy))
.subscribe((data) => {
this._data = data || [];
this.renderHeaders();
this.renderRows();
});
}
/**
* Create the embedded view for the data row template and place it in the correct index location
* within the data row view container.
*/
private _insertRow(renderRow: RenderRow<T>, renderIndex: number) {
const context: RowContext<T> = { $implicit: renderRow.data };
this._renderRow(this._rowOutlet, renderRow, renderIndex, context);
}
/**
* Creates a new row template in the outlet and fills it with the set of cell templates.
* Optionally takes a context to provide to the row and cells, as well as an optional index
* of where to place the new row template in the outlet.
*/
private _renderRow(
outlet: RowOutlet,
renderRow: Object,
index: number,
context: RowContext<T> = {}
) {
// TODO(andrewseguin): enforce that one outlet was instantiated from createEmbeddedView
const factory = this.resolver.resolveComponentFactory(DxcRowComponent);
outlet.viewContainer.createComponent(factory, index);
//outlet.viewContainer.createEmbeddedView(this.cdkRow.template, context, index);
for (let cellTemplate of this._getCellTemplates(renderRow)) {
if (DxcCellOutlet.mostRecentCellOutlet) {
DxcCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(
cellTemplate,
context
);
}
}
this._changeDetectorRef.markForCheck();
}
/**
* Updates the index-related context for each row to reflect any changes in the index of the rows,
* e.g. first/last/even/odd.
*/
private _updateRowIndexContext() {
const viewContainer = this._rowOutlet.viewContainer;
for (
let renderIndex = 0, count = viewContainer.length;
renderIndex < count;
renderIndex++
) {
const viewRef = viewContainer.get(renderIndex) as RowViewRef<T>;
if (viewRef.context) {
const context = viewRef.context as RowContext<T>;
context.count = count;
context.first = renderIndex === 0;
context.last = renderIndex === count - 1;
context.even = renderIndex % 2 === 0;
context.odd = !context.even;
context.index = this._renderRows[renderIndex].dataIndex;
}
}
}
/** Gets the column definitions for the provided row def. */
private _getCellTemplates(rowDef: Object): TemplateRef<any>[] {
if (!rowDef || !this.displayedColumns) {
return [];
}
return Array.from(this.displayedColumns, (columnId) => {
const column = this._columnDefsByName.get(columnId);
return column.cell.template;
});
}
/** Adds native table sections (e.g. tbody) and moves the row outlets into them. */
private _applyNativeTableSections() {
const documentFragment = this._document.createDocumentFragment();
const sections = [
// {tag: 'thead', outlet: this._headerRowOutlet},
{ tag: "tbody", outlet: this._rowOutlet },
];
for (const section of sections) {
const element = this._document.createElement(section.tag);
element.setAttribute("role", "rowgroup");
element.appendChild(section.outlet.elementRef.nativeElement);
documentFragment.appendChild(element);
}
// Use a DocumentFragment so we don't hit the DOM on each iteration.
this._elementRef.nativeElement.appendChild(documentFragment);
}
/** Filters definitions that belong to this table from a QueryList. */
private _getOwnDefs<I extends { _table?: any }>(items: QueryList<I>): I[] {
return items.filter((item) => !item._table || item._table === this);
}
/** Calculate pagination for the data displayed in the table after click event */
navigate(page: number, operation?: string) {
this.page = page;
this.paginationService.calculatePagination(
this.page,
this.itemsPerPage,
(parameters) => {
this.collectionData.next(
this.collectionResource.slice(parameters.start, parameters.end)
);
}
);
}
/** Set to default others header's states if they are different to default state ("up" or "down"). */
removeOtherSorts(actualIdHeader) {
this._allOrderingRefs.forEach((element) => {
let nativeElement = element.nativeElement;
if (actualIdHeader != nativeElement.id) {
let stateElement = nativeElement.getAttribute("state");
if (stateElement === "up" || stateElement === "down") {
this.sortService.removeOtherSortings(nativeElement.id);
}
}
});
}
ngAfterViewInit(): void {
this.setDefaultStateHeaders();
}
/** Set to default all headers that are sortable. */
setDefaultStateHeaders() {
this._allOrderingRefs.forEach((element) => {
let id = element.nativeElement.id;
let columnName = id.split("-")[1];
this.sortService.mapStatesHeaders.set(columnName, "default");
});
}
/** Register all ordering directives references. */
registerOrderingRef(ref: ElementRef) {
this._allOrderingRefs.push(ref);
}
/** Sort row elements from given column depending on given state. */
sortCells(columnName, state) {
let start = this.paginationService.getCurrentStart();
let end = this.paginationService.getCurrentEnd();
let list;
if (state === "up") {
list = this.ascSort(columnName, start, end);
} else if (state === "down") {
list = this.descSort(columnName, start, end);
}
this.collectionData.next(list);
}
/** Sort row elements by ascendant */
ascSort(columnName, start, end) {
return this.sortService
.getSortedList(this.collectionResource, columnName, "asc")
.slice(start, end);
}
/** Sort row elements by descendant */
descSort(columnName, start, end) {
return this.sortService
.getSortedList(this.collectionResource, columnName, "desc")
.slice(start, end);
}
/** Change icon to up icon */
changeAscIcon(el: ElementRef) {
this.sortService.setAscIconSort(el);
}
/** Change icon to down icon */
changeDescIcon(el: ElementRef) {
this.sortService.setDescIconSort(el);
}
/** Change icon to default icon */
changeDefaultIcon(el: ElementRef) {
this.sortService.setDefaultIconSort(el);
}
/** Return map with header's states */
getMapStateHeaders() {
return this.sortService.mapStatesHeaders;
}
//It is needed to give a unique id to the resultset table
setClassName() {
this.className = `${Math.round(Math.random() * 100)}`;
}
/** Utility function that gets a merged list of the entries in an array and values of a Set. */
mergeArrayAndSet<T>(array: T[], set: Set<T>): T[] {
return array.concat(Array.from(set));
}
}