grapesjs-plugin-ckeditor icon indicating copy to clipboard operation
grapesjs-plugin-ckeditor copied to clipboard

Mentions not working

Open snehlata08 opened this issue 7 months ago • 0 comments

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;
}

snehlata08 avatar Jul 03 '24 18:07 snehlata08