2

I am using CKEditor5 within my angular application. I have generated a custom build and saved the ckeditor.js file in my project dir. Everything works fine. I am able to render the editor and use it without issue in an angular component by adding this line to the top: import * as customBuild from 'src/ckCustomBuild/ckeditor';

Now I am going through the documentation for their sample "placeholder" plugin and I can't seem to get it to work in angular. I've added the same code from their site to a new file called placeholder.plugin.js in the same folder as ckeditor.js above.

I then import it into my component also, and structure the config as follows:

import * as customBuild from 'src/ckCustomBuild/ckeditor';
import * as Placeholder from 'src/ckCustomBuild/placeholder.plugin';

...component boilerplate code....
  htmlEditorConfig = {
    plugins: [Placeholder],
    toolbar: {
      items: ['bold', 'italic', 'underline', 'link', 'alignment', 'bulletedList', 'numberedList', '|', 'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor', '|', 'Paceholder'],
      shouldNotGroupWhenFull: false,
    },
    // placeholderConfig: {
    //   types: ['date', 'color', 'first name', 'surname'],
    // },
    fontSize: {
      options: [9, 11, 13, 'default', 17, 19, 21],
    },
    mediaEmbed: {
      previewsInData: true,
    },
  };

When I go to run the application, I get the following runtime error:

    core.mjs:6494 ERROR Error: Uncaught (in promise): CKEditorError: plugincollection-plugin-not-found {"plugin":{}}
    Read more: https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/error-codes.html#error-plugincollection-plugin-not-found
    CKEditorError: plugincollection-plugin-not-found {"plugin":{}}
    Read more: https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/error-codes.html#error-plugincollection-plugin-not-found
        at p (ckeditor.js:5:432588)
        at ckeditor.js:5:432433
        at Array.forEach (<anonymous>)
        at f (ckeditor.js:5:432420)
        at wa.init (ckeditor.js:5:431516)
        at bq.initPlugins (ckeditor.js:5:703694)
        at ckeditor.js:5:831531
        at new ZoneAwarePromise (zone.js:1351:1)
        at bq.create (ckeditor.js:5:831488)
        at CKEditorComponent.<anonymous> (ckeditor-ckeditor5-angular.js:235:1)
        at resolvePromise (zone.js:1262:1)
        at resolvePromise (zone.js:1216:1)
        at zone.js:1329:1
        at push.46026._ZoneDelegate.invokeTask (zone.js:443:1)
        at Object.onInvokeTask (core.mjs:25595:1)
        at push.46026._ZoneDelegate.invokeTask (zone.js:442:1)
        at push.46026.Zone.runTask (zone.js:214:1)
        at drainMicroTaskQueue (zone.js:632:1)
        at push.46026.ZoneTask.invokeTask [as invoke] (zone.js:529:1)
        at invokeTask (zone.js:1727:1)

Here is the contents of my 2 files: placeholder.plugin.js

import * as Plugin from 'src/ckCustomBuild/ckeditor';
import * as Widget from 'src/ckCustomBuild/ckeditor';
import * as Command from 'src/ckCustomBuild/ckeditor';
import * as Collection from 'src/ckCustomBuild/ckeditor';
import * as Model from 'src/ckCustomBuild/ckeditor';
import * as toWidget from 'src/ckCustomBuild/ckeditor';
import * as viewToModelPositionOutsideModelElement from 'src/ckCustomBuild/ckeditor';
import * as addListToDropdown from 'src/ckCustomBuild/ckeditor';
import * as createDropdown from 'src/ckCustomBuild/ckeditor';

 class Placeholder extends Plugin {
  static get requires() {
    return [PlaceholderEditing, PlaceholderUI];
  }
}

class PlaceholderCommand extends Command {
  execute({ value }) {
    const editor = this.editor;
    const selection = editor.model.document.selection;

    editor.model.change((writer) => {
      // Create a <placeholder> elment with the "name" attribute (and all the selection attributes)...
      const placeholder = writer.createElement('placeholder', {
        ...Object.fromEntries(selection.getAttributes()),
        name: value,
      });

      // ... and insert it into the document.
      editor.model.insertContent(placeholder);

      // Put the selection on the inserted element.
      writer.setSelection(placeholder, 'on');
    });
  }

  refresh() {
    const model = this.editor.model;
    const selection = model.document.selection;

    const isAllowed = model.schema.checkChild(selection.focus.parent, 'placeholder');

    this.isEnabled = isAllowed;
  }
}

class PlaceholderUI extends Plugin {
  init() {
    const editor = this.editor;
    const t = editor.t;
    const placeholderNames = editor.config.get('placeholderConfig.types');

    // The "placeholder" dropdown must be registered among the UI components of the editor
    // to be displayed in the toolbar.
    editor.ui.componentFactory.add('placeholder', (locale) => {
      const dropdownView = createDropdown(locale);

      // Populate the list in the dropdown with items.
      addListToDropdown(dropdownView, getDropdownItemsDefinitions(placeholderNames));

      dropdownView.buttonView.set({
        // The t() function helps localize the editor. All strings enclosed in t() can be
        // translated and change when the language of the editor changes.
        label: t('Placeholder'),
        tooltip: true,
        withText: true,
      });

      // Disable the placeholder button when the command is disabled.
      const command = editor.commands.get('placeholder');
      dropdownView.bind('isEnabled').to(command);

      // Execute the command when the dropdown item is clicked (executed).
      this.listenTo(dropdownView, 'execute', (evt) => {
        editor.execute('placeholder', { value: evt.source.commandParam });
        editor.editing.view.focus();
      });

      return dropdownView;
    });
  }
}

function getDropdownItemsDefinitions(placeholderNames) {
  const itemDefinitions = new Collection();

  for (const name of placeholderNames) {
    const definition = {
      type: 'button',
      model: new Model({
        commandParam: name,
        label: name,
        withText: true,
      }),
    };

    // Add the item definition to the collection.
    itemDefinitions.add(definition);
  }

  return itemDefinitions;
}

class PlaceholderEditing extends Plugin {
  static get requires() {
    return [Widget];
  }

  init() {
    console.log('PlaceholderEditing#init() got called');

    this._defineSchema();
    this._defineConverters();

    this.editor.commands.add('placeholder', new PlaceholderCommand(this.editor));

    this.editor.editing.mapper.on(
      'viewToModelPosition',
      viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) => viewElement.hasClass('placeholder'))
    );
    this.editor.config.define('placeholderConfig', {
      types: ['date', 'first name', 'surname'],
    });
  }

  _defineSchema() {
    const schema = this.editor.model.schema;

    schema.register('placeholder', {
      // Allow wherever text is allowed:
      allowWhere: '$text',

      // The placeholder will act as an inline node:
      isInline: true,

      // The inline widget is self-contained so it cannot be split by the caret and it can be selected:
      isObject: true,

      // The inline widget can have the same attributes as text (for example linkHref, bold).
      allowAttributesOf: '$text',

      // The placeholder can have many types, like date, name, surname, etc:
      allowAttributes: ['name'],
    });
  }

  _defineConverters() {
    const conversion = this.editor.conversion;

    conversion.for('upcast').elementToElement({
      view: {
        name: 'span',
        classes: ['placeholder'],
      },
      model: (viewElement, { writer: modelWriter }) => {
        // Extract the "name" from "{name}".
        const name = viewElement.getChild(0).data.slice(1, -1);

        return modelWriter.createElement('placeholder', { name });
      },
    });

    conversion.for('editingDowncast').elementToElement({
      model: 'placeholder',
      view: (modelItem, { writer: viewWriter }) => {
        const widgetElement = createPlaceholderView(modelItem, viewWriter);

        // Enable widget handling on a placeholder element inside the editing view.
        return toWidget(widgetElement, viewWriter);
      },
    });

    conversion.for('dataDowncast').elementToElement({
      model: 'placeholder',
      view: (modelItem, { writer: viewWriter }) => createPlaceholderView(modelItem, viewWriter),
    });

    // Helper method for both downcast converters.
    function createPlaceholderView(modelItem, viewWriter) {
      const name = modelItem.getAttribute('name');

      const placeholderView = viewWriter.createContainerElement('span', {
        class: 'placeholder',
      });

      // Insert the placeholder name (as a text).
      const innerText = viewWriter.createText('{' + name + '}');
      viewWriter.insert(viewWriter.createPositionAt(placeholderView, 0), innerText);

      return placeholderView;
    }
  }
}

Component.ts

import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { CKEditorUploadAdapter } from 'src/app/_helpers/ckeditor-upload-adapter';
import { ConferenceViewModelWithDetails, PaperAcknowledgementViewModel, PaperConfigurationViewModel } from 'src/app/_models/generatedModels';
import { AuthenticationService } from 'src/app/_services/authentication.service';
import { AdminCallForPapersService, OrganizationService } from 'src/app/_services/generatedServices';
import { ToasterService } from 'src/app/_services/toaster.service';
import * as customBuild from 'src/ckCustomBuild/ckeditor';
//import * as Placeholder from 'src/ckCustomBuild/ckeditor';
import * as Placeholder from 'src/ckCustomBuild/placeholder.plugin';


@Component({
  selector: 'bxl-abstract-management-configuration',
  templateUrl: 'abstract-management-configuration.component.html',
})
export class AbstractManagementConfigurationComponent implements OnInit {
  initialized = false;
  public Editor = customBuild;
  htmlEditorConfig = {
    plugins: [Placeholder],
    toolbar: {
      items: ['bold', 'italic', 'underline', 'link', 'alignment', 'bulletedList', 'numberedList', '|', 'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor', '|', 'Paceholder'],
      shouldNotGroupWhenFull: false,
    },
    // placeholderConfig: {
    //   types: ['date', 'color', 'first name', 'surname'],
    // },
    fontSize: {
      options: [9, 11, 13, 'default', 17, 19, 21],
    },
    mediaEmbed: {
      previewsInData: true,
    },
  };
  organizationId: string;
  conferenceId: any;
  configuration: PaperConfigurationViewModel;

  constructor(private auth: AuthenticationService, private orgService: OrganizationService, private router: Router, private route: ActivatedRoute, private cfpService: AdminCallForPapersService, private toaster: ToasterService) {}

  ngOnInit(): void {
    this.route.paramMap.subscribe((params) => {
      this.organizationId = params.get('organizationId');
      this.conferenceId = params.get('conferenceId');

      this.cfpService.getPaperConfiguration(this.organizationId, this.conferenceId).subscribe((result) => {
        this.configuration = result;
        this.initialized = true;
      });
    });
  }
7
  • @Vega I tried that, it says its not a module Commented Jun 27, 2022 at 20:10
  • @Vega Yea, I just tired again. It wouldn't compile until I marked the class as export class Placeholder in the plugin file. Now that it compiles, I get a rutime error with this line: const schema = this.editor.model.schema; TypeError: Cannot read properties of undefined (reading 'model') Commented Jun 27, 2022 at 20:16
  • @Vega no that won't work. Its not getting a reference to this.editor which is needed all over this file to properly initialize the plugin Commented Jun 27, 2022 at 20:20
  • Ok, this needs a bit more time for debugging :) Commented Jun 27, 2022 at 20:21
  • 1
    I'v set it up exactly the way I have it, but stackblitz just spins and spins: stackblitz.com/edit/angular-ivy-4ccgk4 Commented Jun 29, 2022 at 14:04

1 Answer 1

0

In your placeholder.plugin.js put the below content,

import * as Plugin from 'src/app/ckeditor/ckeditor';
import * as Widget from 'src/app/ckeditor/ckeditor';
import * as Command from 'src/app/ckeditor/ckeditor';
import * as Collection from 'src/app/ckeditor/ckeditor';
import * as Model from 'src/app/ckeditor/ckeditor';
import * as toWidget from 'src/app/ckeditor/ckeditor';
import * as viewToModelPositionOutsideModelElement from 'src/app/ckeditor/ckeditor';
import * as addListToDropdown from 'src/app/ckeditor/ckeditor';
import * as createDropdown from 'src/app/ckeditor/ckeditor';

Instead of these lines,

import * as Plugin from 'src/ckCustomBuild/ckeditor';
import * as Widget from 'src/ckCustomBuild/ckeditor';
import * as Command from 'src/ckCustomBuild/ckeditor';
import * as Collection from 'src/ckCustomBuild/ckeditor';
import * as Model from 'src/ckCustomBuild/ckeditor';
import * as toWidget from 'src/ckCustomBuild/ckeditor';
import * as viewToModelPositionOutsideModelElement from 'src/ckCustomBuild/ckeditor';
import * as addListToDropdown from 'src/ckCustomBuild/ckeditor';
import * as createDropdown from 'src/ckCustomBuild/ckeditor';

It was referring to a wrong path and it must be the path to your ckeditor.js. In this way you can eliminate the module load error in that file.

Sign up to request clarification or add additional context in comments.

3 Comments

It would be much shorter and clearer if you have said that the import paths are wrong and /app should be added
This is just an issue with the StackBlitz when I was transcribing from my real code. In my real code, the paths are correct. I've updated the stackblitz and its still not working.
Please add your working example without placeholder plugin. I am not sure how you can use a ckeditor component without a definition of that element.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.