ckeditor4.xgrapejs

Grapesjs Ckeditor not working for mentions


I am trying to use mentions in the editor but somehow it is not working and throwing error.

Uncaught Error: [CKEDITOR.resourceManager.load] Resource name "{class:'mention'" was not found at "https://cdn.ckeditor.com/4.21.0/standard-all/plugins/{class:'mention'/plugin.js?t=N2M9".
    at window.CKEDITOR.window.CKEDITOR.dom.CKEDITOR.resourceManager.<anonymous> (cke

Here is my code

'grapesjs-plugin-ckeditor': {
                    options: {
                        versionCheck: false,
                        extraPlugins: [ MyCustomUploadAdapterPlugin, MentionLinks, 'WordCount'],
            toolbar: [
                    'heading',
                    '|',
                    'bold',
                    'italic',
                    'underline',
                    'link',
                    'bulletedList',
                    'numberedList',
                    '|',
                    'findAndReplace',
                    'outdent',
                    'indent',
                    'alignment',
                    '|',
                    'imageUpload',
                    'imageInsert',
                    'blockQuote',
                    'insertTable',
                    'mediaEmbed',
                    'undo',
                    'redo',
                    '-',
                    'fontFamily',
                    'fontBackgroundColor',
                    'fontColor',
                    'fontSize',
                    'removeFormat',
                    'horizontalLine',
                    '|',
                    'sourceEditing'
                ],
            language: 'en',
            wordCount: {
                showParagraphs: false,
                showWordCount: false,
                displayWords: false,
                showCharCount: true,
                countSpacesAsChars: true,
                countHTML: false,
                maxWordCount: -1,
                maxCharCount: 10,
                onUpdate: stats => {
                    var maxCharacters = 750;
                    var isLimitExceeded = stats.characters > maxCharacters;
                    var charCount = '';
                    var exc
                    if ( isLimitExceeded ) {
                        charCount = `<span class="text-danger"> <b>${maxCharacters}</b> characters allowed. Content exceeded by <b>${ stats.characters - maxCharacters }</b> characters.</span>
                                    <br><span class="text-danger charCountFlag" data-charcount=false></span>`;

                    } else {
                        charCount = `<span class="charCountFlag" data-charcount=true>Characters: <b>${ stats.characters }</b> of <b>${ maxCharacters }</b></span>`;
                        $("#mandatoryError").remove();
                    }
                    $( `${this.jQuerySelector}-word-count` ).html(`${ charCount }`);
                }
            },
            image: {
                toolbar: [
                    'imageTextAlternative',
                    'imageStyle:inline',
                    'imageStyle:block',
                    'imageStyle:full',
                    'imageStyle:alignLeft',
                    'imageStyle:alignRight',
                ],
                resizeUnit: 'px'
            },
            mediaEmbed: {
                extraProviders: [
                    {
                        name: 'xyz',
                        url: /^edms\.xyz\.(\w+)\.com\/documents\/documents\/(\d+)\/download/,
                        html: match => {
                            var rawUrl = match[0];
                            return (`
                                <video class="" controls="" src="https://${ rawUrl }" preload="metadata" style="height: 100%; width: 100%;" title="">
                                </video>
                            `);
                        }
                    }
                ],
                previewsInData: true
            },
            alignment: {
                options: ['left', 'right', 'center', 'justify']
            },
            table: {
                contentToolbar: [
                    'tableColumn',
                    'tableRow',
                    'mergeTableCells',
                    'tableCellProperties',
                    'tableProperties'
                ]
            },
            licenseKey: '',
            mention: {
                dropdownLimit: 500,
                feeds: [
//                    {
//                        marker: '@',
//                        feed: getFeedItems,
//                        minimumCharacters: 1
//                    },

                    {
                        marker: '@',
                        feed: getNodeItems,
                        minimumCharacters: 1,
                        itemRenderer: customItemRenderer,

                    }
                ]
            }
                    },
                    },
          'grapesjs-plugin-dynamic-columns': {}
        },
    });

    // COPIED from https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html
class MyUploadAdapter {
    constructor( loader ) {
        // The file loader instance to use during the upload.
        this.loader = loader;
    }

    // Starts the upload process.
    upload() {
        return this.loader.file
            .then( file => new Promise( ( resolve, reject ) => {
                this._initRequest();
                this._initListeners( resolve, reject, file );
                this._sendRequest( file );
            } ) );
    }

    // Aborts the upload process.
    abort() {
        if ( this.xhr ) {
            this.xhr.abort();
        }
    }

    // Initializes the XMLHttpRequest object using the URL passed to the constructor.
    _initRequest() {
        const xhr = this.xhr = new XMLHttpRequest();

        // Note that your request may look different. It is up to you and your editor
        // integration to choose the right communication channel. This example uses
        // a POST request with JSON as a data structure but your configuration
        // could be different.
        xhr.open( 'POST', '/file_upload/', true );
        xhr.responseType = 'json';
    }

    // Initializes XMLHttpRequest listeners.
    _initListeners( resolve, reject, file ) {
        const xhr = this.xhr;
        const loader = this.loader;
        const genericErrorText = `Couldn't upload file: ${ file.name }.`;

        xhr.addEventListener( 'error', () => reject( genericErrorText ) );
        xhr.addEventListener( 'abort', () => reject() );
        xhr.addEventListener( 'load', () => {
            const response = xhr.response;

            // This example assumes the XHR server's "response" object will come with
            // an "error" which has its own "message" that can be passed to reject()
            // in the upload promise.
            //
            // Your integration may handle upload errors in a different way so make sure
            // it is done properly. The reject() function must be called when the upload fails.
            if ( !response || response.error ) {
                return reject( response && response.error ? response.error.message : genericErrorText );
            }

            // If the upload is successful, resolve the upload promise with an object containing
            // at least the "default" URL, pointing to the image on the server.
            // This URL will be used to display the image in the content. Learn more in the
            // UploadAdapter#upload documentation.
            resolve( {
                default: response.file_path.replace("/home", "")
            } );
        } );

        // Upload progress when it is supported. The file loader has the #uploadTotal and #uploaded
        // properties which are used e.g. to display the upload progress bar in the editor
        // user interface.
        if ( xhr.upload ) {
            xhr.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
            xhr.upload.addEventListener('progress', evt => {
                if ( evt.lengthComputable ) {
                    loader.uploadTotal = evt.total;
                    loader.uploaded = evt.loaded;
                }
            } );
        }
    }

    // Prepares the data and sends the request.
    _sendRequest( file ) {
        // Prepare the form data.
        const data = new FormData();
        data.append( 'file', file );

        // Important note: This is the right place to implement security mechanisms
        // like authentication and CSRF protection. For instance, you can use
        // XMLHttpRequest.setRequestHeader() to set the request headers containing
        // the CSRF token generated earlier by your application.

        // Send the request.
        this.xhr.send( data );
    }
}

function MyCustomUploadAdapterPlugin( editor ) {
    editor.plugins.get('FileRepository').createUploadAdapter = ( loader ) => {
        // Configure the URL to the upload script in your back-end here!
        return new MyUploadAdapter( loader );
    };
}

// TODO Commented code for future reference
//function getFeedItems( queryText ) {
//    return new Promise( resolve => {
//        setTimeout( () => {
//            $.ajax({
//                url : "/get_user_list/?queryText="+queryText,
//                type : "GET",
//                success : function(data){
//                    var data = JSON.parse(data);
//                    resolve( data );
//                },
//                error: handleAJAXError
//            });
//        }, 100 );
//    } );
//}

function getNodeItems( queryText ){
    return new Promise( resolve =>{
        setTimeout(()=>{
            $.ajax({
                url : "/typeahead/?q="+queryText,
                type : "GET",
                success : function(data){
                    var data = JSON.parse(data);
                    var result = []
                    last_type_added = null
                    for(node of data) {
                        var _type = node['labels'].split("_").join(" ")
                        var _title = node['title']
                        var _display = node['display']?node['display']:""
                        var _hint = node['hint']?node['hint']:null
                        if (last_type_added != _type) {
                            last_type_added = _type;
                        }
                        result.push({id: "@" + _type + "." + _title, type: _type, title: _title})
                    }
                    resolve( result );
                }
            });

        }, 100);
    });
}

/*
 * This plugin customizes the way mentions are handled in the editor model and data.
 * Instead of a classic <span class="mention"></span>,
 */
function MentionLinks( editor ) {
    // The upcast converter will convert a view
    //
    //      <a href="..." class="mention" data-mention="...">...</a>
    //
    // element to the model "mention" text attribute.
    editor.conversion.for( 'upcast' ).elementToAttribute( {
        view: {
            name: 'a',
            key: 'data-mention',
            classes: 'mention',
            attributes: {
                href: true,
                target: true,
            }
        },
        model: {
            key: 'mention',
            value: viewItem => editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem )
        },
        converterPriority: 'high'
    } );

    // Downcast the model "mention" text attribute to a view
    //
    //      <a href="..." class="mention" data-mention="...">...</a>
    //
    // element.
    editor.conversion.for( 'downcast' ).attributeToElement( {
        model: 'mention',
        view: ( modelAttributeValue, { writer } ) => {
            // Do not convert empty attributes (lack of value means no mention).
            if ( !modelAttributeValue ) {
                return;
            }

            const nt_title = modelAttributeValue.id.slice(1).split(/\.(.+)/);
            const nt = encodeURIComponent(nt_title[0]);
            const title = encodeURIComponent(nt_title[1]);

            return writer.createAttributeElement( 'a', {
                class: 'mention',
                'data-mention': modelAttributeValue.id,
                target: "_blank",
                href: `https://${window.location.hostname}/draw_graph/?ntype1=${nt}&title1=${title}&focusWithPropWin=true`
            }, {
                // Make mention attribute to be wrapped by other attribute elements.
                priority: 20,
                // Prevent merging mentions together.
                id: modelAttributeValue.id
            } );
        },
        converterPriority: 'high'
    } );
}

function customItemRenderer( item ) {
    const itemElement = document.createElement('div');
    itemElement.innerHTML = `<span style='font-weight: bold'>@${ item.type }.</span>`;

    const usernameElement = document.createElement('span');
    usernameElement.innerHTML = synthesize_string(item.title);
    itemElement.appendChild(usernameElement);
    $(".ck-balloon-panel").css("z-index", 9999);
    return itemElement;
}

I have created the class for MentionLinks and have also provided methods to fetch the mention feed from the backend. The same code is working in django cms but inside grapesjs editor it is throwing error. Any of the extra plugins in my case are not working. Like word count or anything. But Mention is very important for me.


Solution

  • I have figured out the problem. The ckeditor i was referring to was not having mentions feature. I had to build it from scratch to generate the ckeditor with all my desired features. And it started working.