grapesjs-plugin-ckeditor
grapesjs-plugin-ckeditor copied to clipboard
Mentions not working
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;
}