Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular, ng-ckeditor, and the editor-destroy-iframe bug

This posting is regards [email protected] loaded with [email protected].

Although there are a couple of articles on SO with this question I believe this is still an open question. Please do not mark this posting as a duplicate.

To replicate problem: 1) instantiate a DOM element with the ckeditor directive. 2) navigate away from the view in such a way that the Angular Component that is hosting the ckeditor is destroyed. You get this in the Javascript console:

ckeditor.js:19 [CKEDITOR] Error code: editor-destroy-iframe.

There is a closed bug on github over this with no apparent or acceptable solution, unless I missed it. If there is a solution there we should document it on this post because it is incomplete or obscure.

The problem is apparently that Angular framework destroys the iframe element before the ckeditor itself is destroyed. This causes the diagnostic message.

One proposed solution involves the use of a plugin called divarea. From what I understand this replaces the iframe with a div element. The reason this is problematic is that the CSS elements from the hosting page will be mixed in with the CSS that CKEDITOR uses.

Another proposed solution involves the following snippet of code:

for (name in CKEDITOR.instances) {
    CKEDITOR.instances[name].destroy(true);
}

This SO posting mentions this and asks for where this code can be inserted. There is no Angular lifecycle event that seems to work. Even if it did this would not be an acceptable fix because it destroys all CKEDITOR instances and in my application there might be more than one on the page that might survive a router event.

Along those lines I discovered that I can find "my" CKEDITOR instance in my Angular Component with these steps. In the template:

  <ckeditor id="content"
            #editor
            [(ngModel)]="editorContent"
            [config]="{}"
            debounce="500"
            name="content"
  ></ckeditor>

Which is standard except for the #editor directive which establishes a local variable. Then in the component you can build a method like this:

declare const CKEDITOR: any;

@Component() ....

  @ViewChild('editor') ckeditor: CKEditorComponent;

  destroyEditor() {
     var name = this.ckeditor.instance.name;
     CKEDITOR.instances[name].destroy(true);
  }

So the question is: where to call the destroyEditor function? (Other question is whether the destroy(true) method is the right one to call.)

Calling it from ngOnDestroy does not work. Catching an event from the router hasn't worked either. Any other suggestions?

like image 738
AlanObject Avatar asked Jan 30 '18 16:01

AlanObject


2 Answers

I am using Angular 6 and here is what I did.

I programmatically destroy the iFrame using this

Please Note: I did not want any editor present so I destroyed them all. Its import to note, that if a user is navigating away from, and back to a CKEDITOR you have destroyed a new instance will be created. (editor2...3...4...5 etc) I did not care about the instance int so I destroyed them all and let angular create new instances as it needed based on user interaction.

destroyEditor(){
      let editor = window['CKEDITOR'];
      if(editor.instances){
        for(var editorInstance in editor.instances){
         editor.instances[editorInstance].destroy();
        }
       }
      }

This will allow you to destroy the iframe on any exit condition you have. When a user navigates away from the editor, I call this right before I set my value that changes the UI view and completes the navigation away from the editor.

  • It will leave a text area.
  • If you time it correctly the user will not see this.

You will notice at this point you are still getting an error in the console from ckeditor.component.js

  • This is where you are going to have to make a modification to the library files.
  • This is because the if condition in the CKEditorComponent.prototype.ngOnDestroy function is only checking for the instance and not if the instance name exists.
  • The instance appears to always evaluate to true for me even though the instance has been destroyed. (example:editor1,editor2,editor3 etc)
  • The instance name does not exist when ngDestroy occurs... because... you already destroyed it.

Make this change in the following file.

\node_modules\ng2-ckeditor\lib\src\ckeditor.component.js

This will check for the instance name specifically and not attempt a destroy... because.. you already destroyed it.

CKEditorComponent.prototype.ngOnDestroy = function () {
        var _this = this;
        //if (this.instance) {
        if(CKEDITOR.instances[_this.instance.name]) {
            setTimeout(function () {
                _this.instance.removeAllListeners();
                CKEDITOR.instances[_this.instance.name].destroy();
                _this.instance.destroy();
                _this.instance = null;
            });
        }
    };
like image 188
Marshal Avatar answered Nov 20 '22 08:11

Marshal


If modifying JS-files under node_module is not desired, as in my case, call the following function from ngOnDestroy():

public destroyEditor(): void {
  const editor = window['CKEDITOR'];
  if (editor.instances) {
      for (const editorInstance in editor.instances) {
          if (editor.instances.hasOwnProperty(editorInstance) && 
              editor.instances[editorInstance]) {
              editor.instances[editorInstance].destroy();
              editor.instances[editorInstance] = {
                  destroy: () => true,
              };
          }
      }
}

Note, that after destroying, a dummy object is assigned to the collection item to prevent a JS error, when the component is to be destroyed from the native function. The component must implement OnDestroy interface and the destroyEditor() function called like this:

export class RichTextEditorComponent implements OnDestroy {
    public ngOnDestroy(): void {
        this.destroyEditor();
    }
}
like image 3
Semyon Krotkih Avatar answered Nov 20 '22 08:11

Semyon Krotkih