Track changes to multiple Ckeditor 5 instances in Javascript - javascript

I'm accessing an iframe thru fancybox 3 with multiple ckeditor. It's working well but I want to track if a user has inputted anything on the textboxes so that I can change the button of the fancybox and give them an alert once they hit close. This is my JS code on the iframe page:
window.editors = {};
document.querySelectorAll( '.editor' ).forEach( ( node, index ) => {
ClassicEditor
.create( node, {
removePlugins: ['Heading', 'BlockQuote', 'CKFinderUploadAdapter', 'CKFinder', 'EasyImage', 'Image', 'ImageCaption', 'ImageStyle', 'ImageToolbar', 'ImageUpload', 'MediaEmbed'],
} )
.then( newEditor => {
window.editors[ index ] = newEditor;
} );
editors[ index ].document.on( 'change', () => {
console.log( 'Alert: The document has changed!, are you sure you want to close without saving?' );
} );
} );
But it's giving me an error:
ifram.php:1070 Uncaught TypeError: Cannot read properties of undefined (reading 'document')
at ifram.php:1070
at NodeList.forEach (<anonymous>)
at ifram.php:1060
I also tried taking this out off document.querySelectorAll() function
editors.document.on( 'change', () => {
console.log( 'Alert: The document has changed!, are you sure you want to close without saving?' );
} );
But it gave me another error: Uncaught TypeError: Cannot read properties of undefined (reading 'on')

You could use newEditor.model.document.on('change:data') document
<script src="https://cdn.ckeditor.com/ckeditor5/30.0.0/classic/ckeditor.js"></script>
<textarea class="editor"></textarea>
<script>
window.editors = {};
document.querySelectorAll( '.editor' ).forEach( ( node, index ) => {
ClassicEditor
.create( node, {
removePlugins: ['Heading', 'BlockQuote', 'CKFinderUploadAdapter', 'CKFinder', 'EasyImage', 'Image', 'ImageCaption', 'ImageStyle', 'ImageToolbar', 'ImageUpload', 'MediaEmbed'],
} )
.then( newEditor => {
window.editors[ index ] = newEditor;
newEditor.model.document.on('change:data', ()=> {
console.log( 'Alert: The document has changed!, are you sure you want to close without saving?' );
});
} );
});
</script>

Related

Insert custom element in CKEditor5

In CKEditor5 I am creating a plugin to insert a span element to show a tooltip. The idea is to show a tooltip with a (foot)note inside of it while the element itself will display an incremental number. In CKEditor4 I made something like this with:
CKEDITOR.dialog.add( 'footnoteDialog', function( editor ) {
return {
title: 'Footnote Properties',
minWidth: 400,
minHeight: 100,
contents: [
{
id: 'tab-basic',
label: 'Basic Settings',
elements: [
{
type: 'text',
id: 'content',
label: 'Content of footnote',
validate: CKEDITOR.dialog.validate.notEmpty( "Footnote field cannot be empty." )
}
]
}
],
onOk: function() {
var dialog = this;
var footnote = editor.document.createElement( 'span' );
footnote.setAttribute('class', 'footnote');
footnote.setAttribute('data-toggle', 'tooltip');
footnote.setAttribute( 'title', dialog.getValueOf( 'tab-basic', 'content' ) );
footnote.setText('[FN]');
editor.insertElement( footnote );
}
};
});
[FN] would be transformed in an incremental number.
Now I try to make this plugin with in CKEditor5, but with no success. There are two issues I run in to. Fist, I can't manage to insert the element inside the text. Second, when I want to use the attribute data-toggle this doesn't work because of the - syntax. This is my current code:
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import pilcrowIcon from '#ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
import ButtonView from '#ckeditor/ckeditor5-ui/src/button/buttonview';
export default class Footnote extends Plugin {
init() {
const editor = this.editor;
editor.ui.componentFactory.add( 'footnote', locale => {
const view = new ButtonView( locale );
view.set( {
label: 'Insert footnote',
icon: pilcrowIcon,
tooltip: true
} );
view.on( 'execute', () => {
const source = prompt( 'Footnote' );
editor.model.schema.register( 'span', { allowAttributes: ['class', 'data-toggle', 'title'] } );
editor.model.change( writer => {
const footnoteElement = writer.createElement( 'span', {
class: 'footnote',
// data-toggle: 'tooltip',
title: source
});
editor.model.insertContent( footnoteElement, editor.model.document.selection );
} );
} );
return view;
} );
}
}
How can I make sure my span element is inserted and also contains data-toggle="tooltip"?
For anyone who comes across this, there is a good description of how to set up inline elements in the model and view and then map between them here - How to add "target" attribute to `a` tag in ckeditor5?
Based on my experience, you will also need to set up Javascript code for a command that is run when a button is pressed. The command will insert the new information into the model, then this mapping code will convert it to the view (HTML) for display.

element.nextElementSibling is not returning the right element [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 3 years ago.
I'm using CKEditor 5 to create a text-rich editor on each textarea that has the class .section-dynamique-fr.
The editor is indeed appended to my html page.
Now, when I try to access the editor via javascript using e.nextElementSibling, it does not return its nextElementSibling as seen in the node tree from the console inspector, instead it returns e.nextElementSibling.nextElementSibling.
Any idea why I can't access e.nextElementSibling? (the div with the class .ck-editor) ?
See attached image for html node structure
document.querySelectorAll('.section-dynamique-fr').forEach( (e) => {
ClassicEditor
.create( document.querySelector('#' + e.id), {
// toolbar: [ 'heading', '|', 'bold', 'italic', 'link' ]
} )
.then( editor => {
window.editor = editor;
thisEditor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
console.log(e); //This gives me the textarea
console.log(e.nextElementSibling); //This should gives me the div with class ck-editor but instead gives me the div with class .textarea-saved-content-fr
This is because editor did not create after create execution. you need replace you code to thencallback. callback is a Promise and it will be resolve after editor fully created. See documentaion
document.querySelectorAll('.section-dynamique-fr').forEach( (e) => {
ClassicEditor
.create( document.querySelector('#' + e.id), {
// toolbar: [ 'heading', '|', 'bold', 'italic', 'link' ]
} )
.then( editor => {
window.editor = editor;
thisEditor = editor;
console.log(e.nextElementSibling);
} )
.catch( err => {
console.error( err.stack );
} );

How do I save global inspector controls in Gutenberg editor?

I'm new to WP's gutenberg and React(but not WP/PHP). I'm trying to add a series of custom controls that show up on all core blocks. Using the WP documentation, I was able to add a new inspector section with a simple toggle:
var el = wp.element.createElement;
const { ToggleControl, PanelBody, PanelHeader, BaseControl } = wp.components;
var withInspectorControls = wp.compose.createHigherOrderComponent( function( BlockEdit ) {
return function( props ) {
return el(
wp.element.Fragment,
{},
el(
BlockEdit,
props
),
el(
wp.editor.InspectorControls,
{},
el(
PanelBody,
{
title: 'Section Controls'
},
el(
ToggleControl,
{
label: 'Full Width Section',
checked: props.attributes.full_width_section,
onChange: function(value){ props.setAttributes( { full_width_section: value } ); }
}
),
)
)
);
};
}, 'withInspectorControls' );
wp.hooks.addFilter( 'editor.BlockEdit', 'brink/with-inspector-controls', withInspectorControls );
What I can't do is figure out the proper way to utilize blocks.getSaveContent.extraProps to save the new full_width_section toggle.
I know I'll then need to figure out how to manipulate the block output after this, but one problem at a time!
I finally figured this out by dissecting a few Gutenberg plugins. In this case, before adding controls to the inspector I had to create attributes for all blocks:
// Attributes
const backgroundSettings = {
fullWidthSection: {
type: "boolean",
},
};
function addAttributes(settings) {
const { assign } = lodash;
settings.attributes = assign(settings.attributes, backgroundSettings);
return settings;
}
wp.hooks.addFilter( 'blocks.registerBlockType', 'brink/add-attributes', addAttributes ); // Add Attributes
From here all you have to do is add any attributes you want and their settings, default, etc. in backgroundSettings.

CKEditor 5 paste as plain text

Is there an option to paste always from the clipboard as plain text?
I tried it that way, but that does not work:
$(document).ready(function () {
ClassicEditor.create(document.querySelector('#text'), {
toolbar: [
'heading',
'bold',
'italic',
'link',
'bulletedList',
'numberedList',
'blockQuote',
'undo',
'redo'
]
}).then(function (editor) {
this.listenTo(editor.editing.view, 'clipboardInput', function (event, data) {
// No log.
console.log('hello');
});
}).catch(function (error) {
});
});
https://docs.ckeditor.com/ckeditor5/latest/api/module_clipboard_clipboard-Clipboard.html
https://docs.ckeditor.com/ckeditor5/latest/api/clipboard.html
https://docs.ckeditor.com/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:paste
The clipboardInput event is fired on the Document, not the View. So the first thing will be to listen on the right object.
The second thing is ensuring that the content inserted into the editor is a plain text. This can be done in two ways:
HTML taken from the clipboard can be "plain-textified". But this is hard.
We can take plain-text from the clipboard and insert that into the editor. However, the editor expects HTML to be pasted, so you need to "HTMLize" this plain-text. CKEditor 5 offers a function for that – plainTextToHtml().
To override the editor's default behaviour we'll need to override this callback: https://github.com/ckeditor/ckeditor5-clipboard/blob/a7819b9e6e2bfd64cc27f65d8e56b0d26423d156/src/clipboard.js#L137-L158
To do that, we'll listen to the same event (with a higher priority), do all the same things, but ignore text/html flavour of the clipboard data. Finally, we 'll call evt.stop() to block the default listener from being executed and ruining our job:
import plainTextToHtml from '#ckeditor/ckeditor5-clipboard/src/utils/plaintexttohtml';
// ...
const clipboardPlugin = editor.plugins.get( 'Clipboard' );
const editingView = editor.editing.view;
editingView.document.on( 'clipboardInput', ( evt, data ) => {
if ( editor.isReadOnly ) {
return;
}
const dataTransfer = data.dataTransfer;
let content = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
content = clipboardPlugin._htmlDataProcessor.toView( content );
clipboardPlugin.fire( 'inputTransformation', { content, dataTransfer } );
editingView.scrollToTheSelection();
evt.stop();
} );
EDIT:
Starting from CKEditor 27.0.0 the code has changed (you can read more about it here https://ckeditor.com/docs/ckeditor5/latest/builds/guides/migration/migration-to-27.html#clipboard-input-pipeline-integration)
import plainTextToHtml from '#ckeditor/ckeditor5-clipboard/src/utils/plaintexttohtml';
//...
const clipboardPlugin = editor.plugins.get( 'ClipboardPipeline' );
const editingView = editor.editing.view;
editingView.document.on( 'clipboardInput', ( evt, data ) => {
if ( editor.isReadOnly ) {
return;
}
const dataTransfer = data.dataTransfer;
let content = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
data.content = editor.data.htmlProcessor.toView( content );
editingView.scrollToTheSelection();
}, { priority: 'high' } );
Without any imports:
.then(editor => {
editor.editing.view.document.on('clipboardInput', (evt, data) => {
data.content = editor.data.htmlProcessor.toView(data.dataTransfer.getData('text/plain'));
});
})
You have complete methods in documentation of ckeditor clipboard events

Ember multiple sendAction failing

Update below
Here's an interesting one. I have a template which contains a form component, and that form component's template also includes a component:
NewObj.hbs -> obj-form.js / obj-form.hbs -> file-uploader.js
The file uploader uses ember-uploader and it works great. I used its events to make sendAction calls to tell the obj-form controller that things are happening, IE pass on the upload progress percentage and indicate when the upload is complete.
If however I add one more sendAction, for some reason obj-form never receives the event for onComplete. No matter how I try it, it just never happens - no errors anywhere.
The file-uploader component is invoked in the obj-form template like so:
{{file-uploader
url="/uploads"
onProgress="fileUploadProgress"
onComplete="fileUploadComplete"
value=newProjectDownloadFile}}
file-uploader.js itself looks like this:
App.FileUploaderComponent = EmberUploader.FileField.extend({
url: '',
onProgress: 'onProgress',
onComplete: 'onComplete',
filesDidChange: function(files) {
var uploadUrl = this.get('url');
var that = this;
var uploader = EmberUploader.Uploader.create({
url: uploadUrl
});
uploader.on('progress', function (e) {
that.sendAction('onProgress', e);
});
uploader.on('didUpload', function (response) {
that.sendAction('onComplete', response);
});
if (!Ember.isEmpty(files)) {
uploader.upload(files[0]);
}
}
});
And obj-form.js has these methods in the actions hash:
fileUploadProgress: function (e) {
this.set('uploadPercentage', e.percent.toFixed(2));
},
fileUploadComplete: function (response) {
this.set('newProjectDownloadFileUrl', response.url);
this.set('newProjectDownloadFileName', response.fileName);
this.send('addDownload');
},
It works great. But I wanted to make a progress bar only appear while uploading and disappear when upload was complete. I added the property uploadInProgress: false to the obj-form controller, and this:
isUploading: function () {
return this.uploadInProgress;
}.property('this.uploadInProgress')
I use {{#if isUploading}} to show/hide the progress bar.
I added:
this.set('uploadInProgress', false);
to the obj-form fileUploadComplete() method, and added this new method:
fileUploadStart: function () {
this.set('uploadInProgress', true);
},
I modified the file-uploader component call to this:
{{file-uploader
url="/connect/projectDownloads/file"
onStart="fileUploadStart"
onProgress="fileUploadProgress"
onComplete="fileUploadComplete"
value=newProjectDownloadFile}}
And changed file-uploader.js to look like this:
App.FileUploaderComponent = EmberUploader.FileField.extend({
url: '',
onStart: 'onStart',
onProgress: 'onProgress',
onComplete: 'onComplete',
filesDidChange: function(files) {
var uploadUrl = this.get('url');
var that = this;
var uploader = EmberUploader.Uploader.create({
url: uploadUrl
});
uploader.on('progress', function (e) {
that.sendAction('onProgress', e);
});
uploader.on('didUpload', function (response) {
that.sendAction('onComplete', response);
});
if (!Ember.isEmpty(files)) {
that.sendAction('onStart', true);
uploader.upload(files[0]);
}
}
});
I've tried putting the that.sendAction('onStart', true); bit in all kinds of places in file-uploader.js but if that line exists, then the obj-form controller never receives the onComplete action. I have absolutely no idea why. I take that line out, it works again.
UPDATE ok I've found something new. It's not the sendAction that breaks it, it's this line in obj-form.js:
this.set('uploadInProgress', true);
For some reason, when I set that to true, it all falls apart. I'm wondering if I'm doing something wrong with the way I'm trying to make that work?
Could it be something to do with didInsertElement? I noticed that's getting triggered when I set uploadInProgress to true - because that property is being used to determine whether the progress bar appears on the page or not. So maybe I'm using didInsertElement incorrectly? Here's my didInsertElement and willDestroyElement:
didInsertElement: function() {
// console.log('didInsertElement');
this.set( 'postType', this.get( 'content' ).get( 'postType' ) );
this.set( 'projectDownloads', this.get( 'content' ).get( 'projectDownloads' ) );
this.set( 'projectLinks', this.get( 'content' ).get( 'projectLinks' ) );
this.set( 'published', this.get( 'content' ).get( 'published' ) );
Ember.addObserver( this.get( 'content' ), 'postType', this, this.postTypeDidChange );
Ember.addObserver( this.get( 'content' ), 'projectDownloads', this, this.projectDownloadsDidChange );
Ember.addObserver( this.get( 'content' ), 'projectLinks', this, this.projectLinksDidChange );
Ember.addObserver( this.get( 'content' ), 'published', this, this.publishedDidChange );
},
willDestroyElement: function() {
console.log('willDestroyElement');
Ember.removeObserver( this.get( 'content' ), 'postType', this.postTypeDidChange );
Ember.removeObserver( this.get( 'content' ), 'projectDownloads', this.projectDownloadsDidChange );
Ember.removeObserver( this.get( 'content' ), 'projectLinks', this.projectLinksDidChange );
Ember.removeObserver( this.get( 'content' ), 'published', this.publishedDidChange );
}
I've tried modifying isUploading to the alias style you recommended, but that hasn't helped anything. The idea is when that gets set to true, it makes the progress bar appear and the fileupload form disappear. Then when it gets set to false the reverse happens. I'm open to other ways to make this happen in Ember, I just can't figure out what I'm doing that's breaking things.
Not the answer yet, but won't fit in comment correctly... this isn't necessary in the dependency chain, even easier is to change this to an alias.
From
isUploading: function () {
return this.uploadInProgress;
}.property('this.uploadInProgress')
To
isUploading: Ember.computed.alias('uploadInProgress')

Categories