From d3e689aa81b29179b3aa6c665e3c0eab5326395a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Apr 2021 08:32:49 +0200 Subject: [PATCH] MOBILE-3641 feedback: Migrate index page --- .../feedback/components/components.module.ts | 34 ++ .../index/addon-mod-feedback-index.html | 230 ++++++++ .../mod/feedback/components/index/index.ts | 503 ++++++++++++++++++ .../mod/feedback/feedback-lazy.module.ts | 38 ++ .../mod/feedback/pages/index/index.html | 22 + src/addons/mod/feedback/pages/index/index.ts | 43 ++ src/core/components/tabs/core-tabs.html | 2 +- src/core/components/tabs/tab.ts | 21 +- 8 files changed, 883 insertions(+), 10 deletions(-) create mode 100644 src/addons/mod/feedback/components/components.module.ts create mode 100644 src/addons/mod/feedback/components/index/addon-mod-feedback-index.html create mode 100644 src/addons/mod/feedback/components/index/index.ts create mode 100644 src/addons/mod/feedback/feedback-lazy.module.ts create mode 100644 src/addons/mod/feedback/pages/index/index.html create mode 100644 src/addons/mod/feedback/pages/index/index.ts diff --git a/src/addons/mod/feedback/components/components.module.ts b/src/addons/mod/feedback/components/components.module.ts new file mode 100644 index 000000000..ce2b48392 --- /dev/null +++ b/src/addons/mod/feedback/components/components.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { AddonModFeedbackIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModFeedbackIndexComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + ], + providers: [ + ], + exports: [ + AddonModFeedbackIndexComponent, + ], +}) +export class AddonModFeedbackComponentsModule {} diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html new file mode 100644 index 000000000..e5c8a6962 --- /dev/null +++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + + +

{{ 'addon.mod_feedback.completed_feedbacks' | translate }}

+
+ {{completedCount}} +
+ + +

{{ 'addon.mod_feedback.show_nonrespondents' | translate }}

+
+
+ + +

{{ 'addon.mod_feedback.questions' | translate }}

+
+ {{itemsCount}} +
+
+
+ + + + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + + + + + + + {{ 'addon.mod_feedback.feedback_is_not_open' | translate }} + + + + + + + {{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }} + + + + + + +

{{ 'addon.mod_feedback.feedbackopen' | translate }}

+

{{overview.openTimeReadable}}

+
+
+ + +

{{ 'addon.mod_feedback.feedbackclose' | translate }}

+

{{overview.closeTimeReadable}}

+
+
+ + +

{{ 'addon.mod_feedback.page_after_submit' | translate }}

+ + +
+
+ + + +

{{ 'addon.mod_feedback.mode' | translate }}

+

{{ 'addon.mod_feedback.anonymous' | translate }}

+

{{ 'addon.mod_feedback.non_anonymous' | translate }}

+
+
+ + + + + + {{ 'addon.mod_feedback.preview' | translate }} + + + + + + {{ 'addon.mod_feedback.complete_the_form' | translate }} + + + {{ 'addon.mod_feedback.continue_the_form' | translate }} + + + + + + + +
+
+
+
+ + + + + + + + + + + {{ warning }} + + + + + + +

+ {{item.num}}. + +

+

+ + +

+ + +
    +
  • {{ data }}
  • +
+

{{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}}

+
+ +
    + +
  • {{ data }}
  • +
    +
+
+ + + +

+ {{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}} +

+
+
+
+
+
+
+
+
diff --git a/src/addons/mod/feedback/components/index/index.ts b/src/addons/mod/feedback/components/index/index.ts new file mode 100644 index 000000000..cf2799220 --- /dev/null +++ b/src/addons/mod/feedback/components/index/index.ts @@ -0,0 +1,503 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, Optional, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { IonContent } from '@ionic/angular'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreNavigator } from '@services/navigator'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { + AddonModFeedback, + AddonModFeedbackGetFeedbackAccessInformationWSResponse, + AddonModFeedbackProvider, + AddonModFeedbackWSFeedback, + AddonModFeedbackWSItem, +} from '../../services/feedback'; +import { AddonModFeedbackOffline } from '../../services/feedback-offline'; +import { + AddonModFeedbackAutoSyncData, + AddonModFeedbackSync, + AddonModFeedbackSyncProvider, + AddonModFeedbackSyncResult, +} from '../../services/feedback-sync'; +import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module'; +import { AddonModFeedbackPrefetchHandler } from '../../services/handlers/prefetch'; + +/** + * Component that displays a feedback index page. + */ +@Component({ + selector: 'addon-mod-feedback-index', + templateUrl: 'addon-mod-feedback-index.html', +}) +export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { + + @ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent; + + @Input() tab = 'overview'; + @Input() group = 0; + + component = AddonModFeedbackProvider.COMPONENT; + moduleName = 'feedback'; + feedback?: AddonModFeedbackWSFeedback; + goPage?: number; + items: AddonModFeedbackItem[] = []; + warning?: string; + showAnalysis = false; + tabsReady = false; + firstSelectedTab?: number; + access?: AddonModFeedbackGetFeedbackAccessInformationWSResponse; + completedCount = 0; + itemsCount = 0; + groupInfo?: CoreGroupInfo; + + overview = { + timeopen: 0, + openTimeReadable: '', + timeclose: 0, + closeTimeReadable: '', + }; + + tabsLoaded = { + overview: false, + analysis: false, + }; + + protected submitObserver: CoreEventObserver; + protected syncEventName = AddonModFeedbackSyncProvider.AUTO_SYNCED; + + constructor( + protected content?: IonContent, + @Optional() courseContentsPage?: CoreCourseContentsPage, + ) { + super('AddonModLessonIndexComponent', content, courseContentsPage); + + // Listen for form submit events. + this.submitObserver = CoreEvents.on(AddonModFeedbackProvider.FORM_SUBMITTED, async (data) => { + if (!this.feedback || data.feedbackId != this.feedback.id) { + return; + } + + this.tabsLoaded.analysis = false; + this.tabsLoaded.overview = false; + this.loaded = false; + + // Prefetch data if needed. + if (!data.offline && this.isPrefetched()) { + await CoreUtils.ignoreErrors(AddonModFeedbackSync.prefetchAfterUpdate( + AddonModFeedbackPrefetchHandler.instance, + this.module, + this.courseId, + )); + } + + // Load the right tab. + if (data.tab != this.tab) { + this.tabChanged(data.tab); + } else { + this.loadContent(true); + } + }, this.siteId); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + try { + await this.loadContent(false, true); + + if (this.feedback) { + CoreUtils.ignoreErrors(AddonModFeedback.logView(this.feedback.id, this.feedback.name)); + } + } finally { + this.tabsReady = true; + } + } + + /** + * @inheritdoc + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModFeedback.invalidateFeedbackData(this.courseId)); + if (this.feedback) { + promises.push(AddonModFeedback.invalidateFeedbackAccessInformationData(this.feedback.id)); + promises.push(AddonModFeedback.invalidateAnalysisData(this.feedback.id)); + promises.push(CoreGroups.invalidateActivityAllowedGroups(this.feedback.coursemodule)); + promises.push(CoreGroups.invalidateActivityGroupMode(this.feedback.coursemodule)); + promises.push(AddonModFeedback.invalidateResumePageData(this.feedback.id)); + } + + this.tabsLoaded.analysis = false; + this.tabsLoaded.overview = false; + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + protected isRefreshSyncNeeded(syncEventData: AddonModFeedbackAutoSyncData): boolean { + if (this.feedback && syncEventData.feedbackId == this.feedback.id) { + // Refresh the data. + this.content?.scrollToTop(); + + return true; + } + + return false; + } + + /** + * @inheritdoc + */ + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + try { + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.module.id); + + this.description = this.feedback.intro; + this.dataRetrieved.emit(this.feedback); + + if (sync) { + // Try to synchronize the feedback. + await this.syncActivity(showErrors); + } + + // Check if there are answers stored in offline. + this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback.id, { cmId: this.module.id }); + + this.showAnalysis = (this.access.canviewreports || this.access.canviewanalysis) && !this.access.isempty; + this.firstSelectedTab = 0; + if (!this.showAnalysis) { + this.tab = 'overview'; + } + + if (this.tab == 'analysis') { + this.firstSelectedTab = 1; + + return await this.fetchFeedbackAnalysisData(); + } + + await this.fetchFeedbackOverviewData(); + } finally { + // Now fill the context menu. + this.fillContextMenu(refresh); + + if (this.feedback) { + // Check if there are responses stored in offline. + this.hasOffline = await AddonModFeedbackOffline.hasFeedbackOfflineData(this.feedback.id); + } + + if (this.tabsReady) { + // Make sure the right tab is selected. + this.tabsComponent?.selectTab(this.tab || 'overview'); + } + } + } + + /** + * Convenience function to get feedback overview data. + * + * @return Resolved when done. + */ + protected async fetchFeedbackOverviewData(): Promise { + const promises: Promise[] = []; + + if (this.access!.cancomplete && this.access!.cansubmit && this.access!.isopen) { + promises.push(AddonModFeedback.getResumePage(this.feedback!.id, { cmId: this.module.id }).then((goPage) => { + this.goPage = goPage > 0 ? goPage : undefined; + + return; + })); + } + + if (this.access!.canedititems) { + this.overview.timeopen = (this.feedback!.timeopen || 0) * 1000; + this.overview.openTimeReadable = this.overview.timeopen ? CoreTimeUtils.userDate(this.overview.timeopen) : ''; + this.overview.timeclose = (this.feedback!.timeclose || 0) * 1000; + this.overview.closeTimeReadable = this.overview.timeclose ? CoreTimeUtils.userDate(this.overview.timeclose) : ''; + } + if (this.access!.canviewanalysis) { + // Get groups (only for teachers). + promises.push(this.fetchGroupInfo(this.module.id)); + } + + try { + await Promise.all(promises); + } finally { + this.tabsLoaded.overview = true; + } + } + + /** + * Convenience function to get feedback analysis data. + * + * @param accessData Retrieved access data. + * @return Resolved when done. + */ + protected async fetchFeedbackAnalysisData(): Promise { + try { + if (this.access!.canviewanalysis) { + // Get groups (only for teachers). + await this.fetchGroupInfo(this.module.id); + } else { + this.tabChanged('overview'); + } + + } finally { + this.tabsLoaded.analysis = true; + } + } + + /** + * Fetch Group info data. + * + * @param cmId Course module ID. + * @return Resolved when done. + */ + protected async fetchGroupInfo(cmId: number): Promise { + this.groupInfo = await CoreGroups.getActivityGroupInfo(cmId); + + await this.setGroup(CoreGroups.validateGroupId(this.group, this.groupInfo)); + } + + /** + * Parse the analysis info to show the info correctly formatted. + * + * @param item Item to parse. + * @return Parsed item. + */ + protected parseAnalysisInfo(item: AddonModFeedbackItem): AddonModFeedbackItem { + switch (item.typ) { + case 'numeric': + item.average = item.data.reduce((prev, current) => prev + Number(current), 0) / item.data.length; + item.templateName = 'numeric'; + break; + + case 'info': + item.data = item.data.map((dataItem) => { + const parsed = > CoreTextUtils.parseJSON(dataItem); + + return typeof parsed.show != 'undefined' ? parsed.show : false; + }).filter((dataItem) => dataItem); // Filter false entries. + + case 'textfield': + case 'textarea': + item.templateName = 'list'; + break; + + case 'multichoicerated': + case 'multichoice': { + const parsedData = []> item.data.map((dataItem) => { + const parsed = > CoreTextUtils.parseJSON(dataItem); + + return typeof parsed.answertext != 'undefined' ? parsed : false; + }).filter((dataItem) => dataItem); // Filter false entries. + + // Format labels. + item.labels = parsedData.map((dataItem) => { + dataItem.quotient = ( dataItem.quotient * 100).toFixed(2); + let label = ''; + + if (typeof dataItem.value != 'undefined') { + label = '(' + dataItem.value + ') '; + } + label += dataItem.answertext; + label += Number(dataItem.quotient) > 0 ? ' (' + dataItem.quotient + '%)' : ''; + + return label; + }); + + item.chartData = parsedData.map((dataItem) => dataItem.answercount); + + if (item.typ == 'multichoicerated') { + item.average = parsedData.reduce((prev, current) => prev + Number(current.avg), 0.0); + } + + const subtype = item.presentation.charAt(0); + + const single = subtype != 'c'; + item.chartType = single ? 'doughnut' : 'bar'; + item.templateName = 'chart'; + break; + } + + default: + break; + } + + return item; + } + + /** + * Function to go to the questions form. + * + * @param preview Preview or edit the form. + */ + gotoAnswerQuestions(preview: boolean = false): void { + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/form`, + { + params: { + preview, + }, + }, + ); + } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + super.ionViewDidEnter(); + + this.tabsComponent?.ionViewDidEnter(); + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + super.ionViewDidLeave(); + + this.tabsComponent?.ionViewDidLeave(); + } + + /** + * Open non respondents page. + */ + openNonRespondents(): void { + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/nonrespondents`, + { + params: { + group: this.group, + }, + }, + ); + } + + /** + * Open respondents page. + */ + openRespondents(): void { + if (!this.access!.canviewreports || this.completedCount <= 0) { + return; + } + + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/respondents`, + { + params: { + group: this.group, + }, + }, + ); + } + + /** + * Tab changed, fetch content again. + * + * @param tabName New tab name. + */ + tabChanged(tabName: string): void { + this.tab = tabName; + + if (!this.tabsLoaded[this.tab]) { + this.loadContent(false, false, true); + } + } + + /** + * Set group to see the analysis. + * + * @param groupId Group ID. + * @return Resolved when done. + */ + async setGroup(groupId: number): Promise { + this.group = groupId; + + const analysis = await AddonModFeedback.getAnalysis(this.feedback!.id, { groupId, cmId: this.module.id }); + + this.completedCount = analysis.completedcount; + this.itemsCount = analysis.itemscount; + + if (this.tab == 'analysis') { + let num = 1; + + this.items = analysis.itemsdata.map((itemData) => { + const item: AddonModFeedbackItem = Object.assign(itemData.item, { + data: itemData.data, + num: num++, + }); + + // Move data inside item. + if (item.data && item.data.length) { + return this.parseAnalysisInfo(item); + } + + return false; + }).filter((item) => item); + + this.warning = ''; + if (analysis.warnings?.length) { + const warning = analysis.warnings.find((warning) => warning.warningcode == 'insufficientresponsesforthisgroup'); + this.warning = warning?.message; + } + } + } + + /** + * @inheritdoc + */ + protected sync(): Promise { + return AddonModFeedbackSync.syncFeedback(this.feedback!.id); + } + + /** + * @inheritdoc + */ + protected hasSyncSucceed(result: AddonModFeedbackSyncResult): boolean { + return result.updated; + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + this.submitObserver?.off(); + } + +} + +type AddonModFeedbackItem = AddonModFeedbackWSItem & { + data: string[]; + num: number; + templateName?: string; + average?: number; + labels?: string[]; + chartData?: number[]; + chartType?: string; +}; diff --git a/src/addons/mod/feedback/feedback-lazy.module.ts b/src/addons/mod/feedback/feedback-lazy.module.ts new file mode 100644 index 000000000..8315c3afa --- /dev/null +++ b/src/addons/mod/feedback/feedback-lazy.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModFeedbackComponentsModule } from './components/components.module'; +import { AddonModFeedbackIndexPage } from './pages/index/index'; + +const routes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModFeedbackIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModFeedbackComponentsModule, + ], + declarations: [ + AddonModFeedbackIndexPage, + ], +}) +export class AddonModFeedbackLazyModule {} diff --git a/src/addons/mod/feedback/pages/index/index.html b/src/addons/mod/feedback/pages/index/index.html new file mode 100644 index 000000000..65b4c69e0 --- /dev/null +++ b/src/addons/mod/feedback/pages/index/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/feedback/pages/index/index.ts b/src/addons/mod/feedback/pages/index/index.ts new file mode 100644 index 000000000..a33a9b741 --- /dev/null +++ b/src/addons/mod/feedback/pages/index/index.ts @@ -0,0 +1,43 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ViewChild } from '@angular/core'; +import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { CoreNavigator } from '@services/navigator'; +import { AddonModFeedbackIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a feedback. + */ +@Component({ + selector: 'page-addon-mod-feedback-index', + templateUrl: 'index.html', +}) +export class AddonModFeedbackIndexPage extends CoreCourseModuleMainActivityPage implements OnInit { + + @ViewChild(AddonModFeedbackIndexComponent) activityComponent?: AddonModFeedbackIndexComponent; + + selectedTab?: string; + selectedGroup?: number; + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + this.selectedTab = CoreNavigator.getRouteParam('tab'); + this.selectedGroup = CoreNavigator.getRouteNumberParam('group'); + } + +} diff --git a/src/core/components/tabs/core-tabs.html b/src/core/components/tabs/core-tabs.html index 947eff9db..033c12d41 100644 --- a/src/core/components/tabs/core-tabs.html +++ b/src/core/components/tabs/core-tabs.html @@ -8,7 +8,7 @@ - diff --git a/src/core/components/tabs/tab.ts b/src/core/components/tabs/tab.ts index 452e07c68..24726538a 100644 --- a/src/core/components/tabs/tab.ts +++ b/src/core/components/tabs/tab.ts @@ -49,19 +49,21 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { @Input() icon?: string; // The tab icon. @Input() badge?: string; // A badge to add in the tab. @Input() badgeStyle?: string; // The badge color. - @Input() enabled = true; // Whether the tab is enabled. @Input() class?: string; // Class, if needed. - @Input() set show(val: boolean) { // Whether the tab should be shown. Use a setter to detect changes on the value. - if (typeof val != 'undefined') { - const hasChanged = this.isShown != val; - this.isShown = val; + @Input() set enabled(value: boolean) { // Whether the tab should be shown. + value = value === undefined ? true : value; + const hasChanged = this.isEnabled != value; + this.isEnabled = value; - if (this.initialized && hasChanged) { - this.tabs.tabVisibilityChanged(); - } + if (this.initialized && hasChanged) { + this.tabs.tabVisibilityChanged(); } } + get enabled(): boolean { + return this.isEnabled; + } + @Input() id?: string; // An ID to identify the tab. @Output() ionSelect: EventEmitter = new EventEmitter(); @@ -70,9 +72,10 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { element: HTMLElement; // The core-tab element. loaded = false; initialized = false; - isShown = true; tabElement?: HTMLElement | null; + protected isEnabled = true; + constructor( protected tabs: CoreTabsComponent, element: ElementRef,