From ccad29bb985266f40e8f3e02d06c0486c9cf01e2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 25 May 2018 08:54:55 +0200 Subject: [PATCH] MOBILE-2353 wiki: Implement edit page --- src/addon/mod/assign/pages/edit/edit.ts | 2 +- src/addon/mod/wiki/pages/edit/edit.html | 27 + src/addon/mod/wiki/pages/edit/edit.module.ts | 33 ++ src/addon/mod/wiki/pages/edit/edit.ts | 538 ++++++++++++++++++ src/addon/mod/wiki/providers/wiki.ts | 1 + .../rich-text-editor/rich-text-editor.ts | 29 +- 6 files changed, 623 insertions(+), 7 deletions(-) create mode 100644 src/addon/mod/wiki/pages/edit/edit.html create mode 100644 src/addon/mod/wiki/pages/edit/edit.module.ts create mode 100644 src/addon/mod/wiki/pages/edit/edit.ts diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index a3513e908..21ca6e70a 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -329,7 +329,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { * Component being destroyed. */ ngOnDestroy(): void { - this.isDestroyed = false; + this.isDestroyed = true; // Unblock the assignment. if (this.assign) { diff --git a/src/addon/mod/wiki/pages/edit/edit.html b/src/addon/mod/wiki/pages/edit/edit.html new file mode 100644 index 000000000..e41b3541c --- /dev/null +++ b/src/addon/mod/wiki/pages/edit/edit.html @@ -0,0 +1,27 @@ + + + + + + + + + + + +
+ + + + + + + + + + {{ 'addon.mod_wiki.wrongversionlock' | translate }} +
+
+
diff --git a/src/addon/mod/wiki/pages/edit/edit.module.ts b/src/addon/mod/wiki/pages/edit/edit.module.ts new file mode 100644 index 000000000..b24ab2f09 --- /dev/null +++ b/src/addon/mod/wiki/pages/edit/edit.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModWikiEditPage } from './edit'; + +@NgModule({ + declarations: [ + AddonModWikiEditPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonModWikiEditPage), + TranslateModule.forChild() + ], +}) +export class AddonModWikiEditPageModule {} diff --git a/src/addon/mod/wiki/pages/edit/edit.ts b/src/addon/mod/wiki/pages/edit/edit.ts new file mode 100644 index 000000000..fef89d36b --- /dev/null +++ b/src/addon/mod/wiki/pages/edit/edit.ts @@ -0,0 +1,538 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnDestroy } from '@angular/core'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { FormControl, FormGroup, FormBuilder } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModWikiProvider } from '../../providers/wiki'; +import { AddonModWikiOfflineProvider } from '../../providers/wiki-offline'; +import { AddonModWikiSyncProvider } from '../../providers/wiki-sync'; + +/** + * Page that allows adding or editing a wiki page. + */ +@IonicPage({ segment: 'addon-mod-wiki-edit' }) +@Component({ + selector: 'page-addon-mod-wiki-edit', + templateUrl: 'edit.html', +}) +export class AddonModWikiEditPage implements OnInit, OnDestroy { + + title: string; // Title to display. + pageForm: FormGroup; // The form group. + contentControl: FormControl; // The FormControl for the page content. + canEditTitle: boolean; // Whether title can be edited. + loaded: boolean; // Whether the data has been loaded. + component = AddonModWikiProvider.COMPONENT; // Component to link the files to. + componentId: number; // Component ID to link the files to. + wrongVersionLock: boolean; // Whether the page lock doesn't match the initial one. + + protected module: any; // Wiki module instance. + protected courseId: number; // Course the wiki belongs to. + protected subwikiId: number; // Subwiki ID the page belongs to. + protected initialSubwikiId: number; // Same as subwikiId, but it won't be updated, it'll always be the value received. + protected wikiId: number; // Wiki ID the page belongs to. + protected pageId: number; // The page ID (if editing a page). + protected section: string; // The section being edited. + protected groupId: number; // The group the subwiki belongs to. + protected userId: number; // The user the subwiki belongs to. + protected blockId: string; // ID to block the subwiki. + protected editing: boolean; // Whether the user is editing a page (true) or creating a new one (false). + protected editOffline: boolean; // Whether the user is editing an offline page. + protected rteEnabled: boolean; // Whether rich text editor is enabled. + protected subwikiFiles: any[]; // List of files of the subwiki. + protected originalContent: string; // The original page content. + protected version: number; // Page version. + protected renewLockInterval: any; // An interval to renew the lock every certain time. + protected forceLeave = false; // To allow leaving the page without checking for changes. + protected isDestroyed = false; // Whether the page has been destroyed. + protected pageParamsToLoad: any; // Params of the page to load when this page is closed. + + constructor(navParams: NavParams, fb: FormBuilder, protected navCtrl: NavController, protected sitesProvider: CoreSitesProvider, + protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, + protected translate: TranslateService, protected courseProvider: CoreCourseProvider, + protected eventsProvider: CoreEventsProvider, protected wikiProvider: AddonModWikiProvider, + protected wikiOffline: AddonModWikiOfflineProvider, protected wikiSync: AddonModWikiSyncProvider, + protected textUtils: CoreTextUtilsProvider, protected courseHelper: CoreCourseHelperProvider) { + + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.subwikiId = navParams.get('subwikiId'); + this.wikiId = navParams.get('wikiId'); + this.pageId = navParams.get('pageId'); + this.section = navParams.get('section'); + this.groupId = navParams.get('groupId'); + this.userId = navParams.get('userId'); + + let pageTitle = navParams.get('pageTitle'); + pageTitle = pageTitle ? pageTitle.replace(/\+/g, ' ') : ''; + + this.initialSubwikiId = this.subwikiId; + this.componentId = this.module.id; + this.canEditTitle = !pageTitle; + this.title = pageTitle ? this.translate.instant('addon.mod_wiki.editingpage', {$a: pageTitle}) : + this.translate.instant('addon.mod_wiki.newpagehdr'); + this.blockId = this.wikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId); + + // Create the form group and its controls. + this.contentControl = fb.control(''); + this.pageForm = fb.group({ + title: pageTitle + }); + this.pageForm.addControl('text', this.contentControl); + + // Block the wiki so it cannot be synced. + this.syncProvider.blockOperation(this.component, this.blockId); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchWikiPageData().then((success) => { + if (success && this.blockId && !this.isDestroyed) { + // Block the subwiki now that we have blockId for sure. + const newBlockId = this.wikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId); + if (newBlockId != this.blockId) { + this.syncProvider.unblockOperation(this.component, this.blockId); + this.blockId = newBlockId; + this.syncProvider.blockOperation(this.component, this.blockId); + } + } + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Convenience function to get wiki page data. + * + * @return {Promise} Promise resolved with boolean: whether it was successful. + */ + protected fetchWikiPageData(): Promise { + let promise, + canEdit = false; + + if (this.pageId) { + // Editing a page that already exists. + this.canEditTitle = false; + this.editing = true; + this.editOffline = false; // Cannot edit pages in offline. + + // Get page contents to obtain title and editing permission + promise = this.wikiProvider.getPageContents(this.pageId).then((pageContents) => { + this.pageForm.controls.title.setValue(pageContents.title); // Set the title in the form group. + this.wikiId = pageContents.wikiid; + this.subwikiId = pageContents.subwikiid; + this.title = this.translate.instant('addon.mod_wiki.editingpage', {$a: pageContents.title}); + this.groupId = pageContents.groupid; + this.userId = pageContents.userid; + canEdit = pageContents.caneditpage; + + // Wait for sync to be over (if any). + return this.wikiSync.waitForSync(this.blockId); + }).then(() => { + // Check if rich text editor is enabled. + return this.domUtils.isRichTextEditorEnabled(); + }).then((enabled) => { + this.rteEnabled = enabled; + + if (enabled) { + // Get subwiki files, needed to replace URLs for rich text editor. + return this.wikiProvider.getSubwikiFiles(this.wikiId, this.groupId, this.userId); + } + }).then((files) => { + this.subwikiFiles = files; + + // Get editable text of the page/section. + return this.wikiProvider.getPageForEditing(this.pageId, this.section); + }).then((editContents) => { + // Get the original page contents, treating file URLs if needed. + const content = this.rteEnabled ? this.textUtils.replacePluginfileUrls(editContents.content, this.subwikiFiles) : + editContents.content; + + this.contentControl.setValue(content); + this.originalContent = content; + this.version = editContents.version; + + if (canEdit) { + // Renew the lock every certain time. + this.renewLockInterval = setInterval(() => { + this.renewLock(); + }, AddonModWikiProvider.RENEW_LOCK_TIME); + } + }); + } else { + // New page. Wait for sync to be over (if any). + promise = this.wikiSync.waitForSync(this.blockId); + + if (this.contentControl.value) { + // Check if there's already some offline data for this page. + promise = promise.then(() => { + return this.wikiOffline.getNewPage(this.pageForm.controls.title.value, this.subwikiId, this.wikiId, + this.userId, this.groupId); + }).then((page) => { + // Load offline content. + this.contentControl.setValue(page.cachedcontent); + this.originalContent = page.cachedcontent; + this.editOffline = true; + }).catch(() => { + // No offline data found. + this.editOffline = false; + }); + } else { + this.editOffline = false; + } + + promise.then(() => { + this.editing = false; + canEdit = !!this.blockId; // If no blockId, the user cannot edit the page. + }); + } + + return promise.then(() => { + return true; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting wiki data.'); + + // Go back. + this.forceLeavePage(); + + return false; + }).finally(() => { + if (!canEdit) { + // Cannot edit, show alert and go back. + this.domUtils.showAlert(this.translate.instant('core.notice'), + this.translate.instant('addon.mod_wiki.cannoteditpage')); + this.forceLeavePage(); + } + }); + } + + /** + * Force leaving the page, without checking for changes. + */ + protected forceLeavePage(): void { + this.forceLeave = true; + this.navCtrl.pop(); + } + + /** + * Navigate to a new offline page. + * + * @param {string} title Page title. + */ + protected goToNewOfflinePage(title: string): void { + if (this.courseId && (this.module.id || this.wikiId)) { + // We have enough data to navigate to the page. + if (!this.editOffline || this.previousViewPageIsDifferentOffline(title)) { + this.pageParamsToLoad = { + module: this.module, + courseId: this.courseId, + pageId: null, + pageTitle: title, + wikiId: this.wikiId, + subwikiId: this.subwikiId, + userId: this.userId, + groupId: this.groupId + }; + } + } else { + this.domUtils.showAlert(this.translate.instant('core.success'), this.translate.instant('core.datastoredoffline')); + } + + this.forceLeavePage(); + } + + /** + * Check if we need to navigate to a new state. + * + * @param {string} title Page title. + * @return {Promise} Promise resolved when done. + */ + protected gotoPage(title: string): Promise { + return this.retrieveModuleInfo(this.wikiId).then(() => { + let openPage = false; + + // Not the firstpage. + if (this.initialSubwikiId) { + if (!this.editing && this.editOffline && this.previousViewPageIsDifferentOffline(title)) { + // The user submitted an offline page that isn't loaded in the back view, open it. + openPage = true; + } else if (!this.editOffline && this.previousViewIsDifferentPageOnline()) { + // The user submitted an offline page that isn't loaded in the back view, open it. + openPage = true; + } + } + + if (openPage) { + // Setting that will do the app navigate to the page. + this.pageParamsToLoad = { + module: this.module, + courseId: this.courseId, + pageId: this.pageId, + pageTitle: title, + wikiId: this.wikiId, + subwikiId: this.subwikiId, + userId: this.userId, + groupId: this.groupId + }; + } + + this.forceLeavePage(); + }).catch(() => { + // Go back if it fails. + this.forceLeavePage(); + }); + } + + /** + * Check if data has changed. + * + * @return {boolean} Whether data has changed. + */ + protected hasDataChanged(): boolean { + const values = this.pageForm.value; + + return !(this.originalContent == values.text || (!this.editing && !values.text && !values.title)); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave) { + return true; + } + + // Check if data has changed. + if (this.hasDataChanged()) { + return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } + + return true; + } + + /** + * View left. + */ + ionViewDidLeave(): void { + if (this.pageParamsToLoad) { + // Go to the page we've just created/edited. + this.navCtrl.push('AddonModWikiIndexPage', this.pageParamsToLoad); + } + } + + /** + * In case we are NOT editing an offline page, check if the page loaded in previous view is different than this view. + * + * @return {boolean} Whether previous view wiki page is different than current page. + */ + protected previousViewIsDifferentPageOnline(): boolean { + // We cannot precisely detect when the state is the same but this is close to it. + const previousView = this.navCtrl.getPrevious(); + + return !this.editing || previousView.component.name != 'AddonModWikiIndexPage' || + previousView.data.module.id != this.module.id || previousView.data.pageId != this.pageId; + } + + /** + * In case we're editing an offline page, check if the page loaded in previous view is different than this view. + * + * @param {string} title The current page title. + * @return {boolean} Whether previous view wiki page is different than current page. + */ + protected previousViewPageIsDifferentOffline(title: string): boolean { + // We cannot precisely detect when the state is the same but this is close to it. + const previousView = this.navCtrl.getPrevious(); + + if (previousView.component.name != 'AddonModWikiIndexPage' || previousView.data.module.id != this.module.id || + previousView.data.wikiId != this.wikiId || previousView.data.pageTitle != title) { + return true; + } + + // Check subwiki using subwiki or user and group. + const previousSubwikiId = parseInt(previousView.data.subwikiId, 10) || 0; + if (previousSubwikiId > 0 && this.subwikiId > 0) { + return previousSubwikiId != this.subwikiId; + } + + const previousUserId = parseInt(previousView.data.userId, 10) || 0, + previousGroupId = parseInt(previousView.data.groupId, 10) || 0; + + return this.userId != previousUserId || this.groupId != previousGroupId; + } + + /** + * Save the data. + */ + save(): void { + const values = this.pageForm.value, + title = values.title, + modal = this.domUtils.showModalLoading('core.sending', true); + let promise, + text = values.text; + + if (this.rteEnabled) { + text = this.textUtils.restorePluginfileUrls(text, this.subwikiFiles); + } else { + text = this.textUtils.formatHtmlLines(text); + } + + if (this.editing) { + // Edit existing page. + promise = this.wikiProvider.editPage(this.pageId, text, this.section).then(() => { + // Invalidate page since it changed. + return this.wikiProvider.invalidatePage(this.pageId).then(() => { + return this.gotoPage(title); + }); + }); + } else { + // Creating a new page. + if (!title) { + // Title is mandatory, stop. + this.domUtils.showAlert(this.translate.instant('core.notice'), + this.translate.instant('addon.mod_wiki.titleshouldnotbeempty')); + modal.dismiss(); + + return; + } + + if (!this.editOffline) { + // Check if the user has an offline page with the same title. + promise = this.wikiOffline.getNewPage(title, this.subwikiId, this.wikiId, this.userId, this.groupId).then(() => { + // There's a page with same name, reject with error message. + return Promise.reject(this.translate.instant('addon.mod_wiki.pageexists')); + }, () => { + // Not found, page can be sent. + }); + } else { + promise = Promise.resolve(); + } + + promise = promise.then(() => { + // Try to send the page. + let wikiId = this.wikiId || (this.module && this.module.instance); + + return this.wikiProvider.newPage(title, text, this.subwikiId, wikiId, this.userId, this.groupId).then((id) => { + if (id > 0) { + // Page was created, get its data and go to the page. + this.pageId = id; + + return this.wikiProvider.getPageContents(this.pageId).then((pageContents) => { + const promises = []; + + wikiId = parseInt(pageContents.wikiid, 10); + if (!this.subwikiId) { + // Subwiki was not created, invalidate subwikis as well. + promises.push(this.wikiProvider.invalidateSubwikis(wikiId)); + } + + this.subwikiId = parseInt(pageContents.subwikiid, 10); + this.userId = parseInt(pageContents.userid, 10); + this.groupId = parseInt(pageContents.groupid, 10); + + // Invalidate subwiki pages since there are new. + promises.push(this.wikiProvider.invalidateSubwikiPages(wikiId)); + + return Promise.all(promises).then(() => { + return this.gotoPage(title); + }); + }).finally(() => { + // Notify page created. + this.eventsProvider.trigger(AddonModWikiProvider.PAGE_CREATED_EVENT, { + pageId: this.pageId, + subwikiId: this.subwikiId, + pageTitle: title, + siteId: this.sitesProvider.getCurrentSiteId() + }); + }); + } else { + // Page stored in offline. Go to see the offline page. + this.goToNewOfflinePage(title); + } + }); + }); + } + + return promise.catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error saving wiki data.'); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Renew lock and control versions. + */ + protected renewLock(): void { + this.wikiProvider.getPageForEditing(this.pageId, this.section, true).then((response) => { + if (response.version && this.version != response.version) { + this.wrongVersionLock = true; + } + }); + } + + /** + * Fetch module information to redirect when needed. + * + * @param {number} wikiId Wiki ID. + * @return {Promise} Promise resolved when done. + */ + protected retrieveModuleInfo(wikiId: number): Promise { + if (this.module.id && this.courseId) { + // We have enough data. + return Promise.resolve(); + } + + const promise = this.module.id ? Promise.resolve(this.module) : + this.courseProvider.getModuleBasicInfoByInstance(wikiId, 'wiki'); + + return promise.then((mod) => { + this.module = mod; + this.componentId = this.module.id; + + if (!this.courseId && this.module.course) { + this.courseId = this.module.course; + } else if (!this.courseId) { + return this.courseHelper.getModuleCourseIdByInstance(wikiId, 'wiki').then((course) => { + this.courseId = course; + }); + } + }); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + clearInterval(this.renewLockInterval); + + // Unblock the subwiki. + if (this.blockId) { + this.syncProvider.unblockOperation(this.component, this.blockId); + } + } +} diff --git a/src/addon/mod/wiki/providers/wiki.ts b/src/addon/mod/wiki/providers/wiki.ts index fd46d3474..511d245c3 100644 --- a/src/addon/mod/wiki/providers/wiki.ts +++ b/src/addon/mod/wiki/providers/wiki.ts @@ -61,6 +61,7 @@ export interface AddonModWikiSubwikiListData { export class AddonModWikiProvider { static COMPONENT = 'mmaModWiki'; static PAGE_CREATED_EVENT = 'addon_mod_wiki_page_created'; + static RENEW_LOCK_TIME = 30000; // Milliseconds. protected ROOT_CACHE_KEY = 'mmaModWiki:'; protected logger; diff --git a/src/components/rich-text-editor/rich-text-editor.ts b/src/components/rich-text-editor/rich-text-editor.ts index c0a9ed6c4..4eac21804 100644 --- a/src/components/rich-text-editor/rich-text-editor.ts +++ b/src/components/rich-text-editor/rich-text-editor.ts @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; +import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, AfterContentInit, OnDestroy } from '@angular/core'; import { TextInput } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { FormControl } from '@angular/forms'; import { Keyboard } from '@ionic-native/keyboard'; +import { Subscription } from 'rxjs'; /** * Directive to display a rich text editor if enabled. @@ -36,7 +37,7 @@ import { Keyboard } from '@ionic-native/keyboard'; selector: 'core-rich-text-editor', templateUrl: 'rich-text-editor.html' }) -export class CoreRichTextEditorComponent { +export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy { // Based on: https://github.com/judgewest2000/Ionic3RichText/ // @todo: Resize, images, anchor button, fullscreen... @@ -53,6 +54,8 @@ export class CoreRichTextEditorComponent { uniqueId = `rte{Math.floor(Math.random() * 1000000)}`; editorElement: HTMLDivElement; + protected valueChangeSubscription: Subscription; + constructor(private domUtils: CoreDomUtilsProvider, private keyboard: Keyboard) { this.contentChanged = new EventEmitter(); } @@ -69,13 +72,17 @@ export class CoreRichTextEditorComponent { this.editorElement = this.editor.nativeElement as HTMLDivElement; this.editorElement.innerHTML = this.control.value; this.textarea.value = this.control.value; - this.control.setValue(this.control.value); this.editorElement.onchange = this.onChange.bind(this); this.editorElement.onkeyup = this.onChange.bind(this); this.editorElement.onpaste = this.onChange.bind(this); this.editorElement.oninput = this.onChange.bind(this); + // Listen for changes on the control to update the editor (if it is updated from outside of this component). + this.valueChangeSubscription = this.control.valueChanges.subscribe((param) => { + this.editorElement.innerHTML = param; + }); + // Setup button actions. const buttons = (this.decorate.nativeElement as HTMLDivElement).getElementsByTagName('button'); for (let i = 0; i < buttons.length; i++) { @@ -109,14 +116,16 @@ export class CoreRichTextEditorComponent { if (this.isNullOrWhiteSpace(this.editorElement.innerText)) { this.clearText(); } else { - this.control.setValue(this.editorElement.innerHTML); + // Don't emit event so our valueChanges doesn't get notified by this change. + this.control.setValue(this.editorElement.innerHTML, {emitEvent: false}); this.textarea.value = this.editorElement.innerHTML; } } else { if (this.isNullOrWhiteSpace(this.textarea.value)) { this.clearText(); } else { - this.control.setValue(this.textarea.value); + // Don't emit event so our valueChanges doesn't get notified by this change. + this.control.setValue(this.textarea.value, {emitEvent: false}); } } @@ -183,7 +192,8 @@ export class CoreRichTextEditorComponent { clearText(): void { this.editorElement.innerHTML = '

'; this.textarea.value = ''; - this.control.setValue(null); + // Don't emit event so our valueChanges doesn't get notified by this change. + this.control.setValue(null, {emitEvent: false}); } /** @@ -199,4 +209,11 @@ export class CoreRichTextEditorComponent { $event.stopPropagation(); document.execCommand(command, false, parameters); } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.valueChangeSubscription && this.valueChangeSubscription.unsubscribe(); + } }