CKEditor: How to show a block of code differently in preview - javascript

I want to create an element that users can insert it in the editor.
The hard part is that it needs to be shown differently from its source.
For example, I want to users insert this in the preview part:
23
And in the source part it should be shown like this:
<span class="tagSpecialClass">{{birthYear}}</span>
The main purpose is to show the mustache tags in a way that users can avoid writing them and just insert them from toolbar that automatically inserts the correct html and preview to it.
My issue is my understanding over CKEditor and the terms I need to read about them in order to implement such a plugin.
How can I force CKEditor to parse/compile/preview a specific tag differently?
Please tell me if my question is too generic!

This sounds like a job for CKEditor widgets (demos)!
Please take a look at the following example (JSFiddle). It will give you some basic idea how to use widgets to solve your problem. If you follow this official tutorial, you'll know how to implement editable parts in your widgets and enable editing with dialogs, which also might be helpful.
// Some CSS for the widget to make it more visible.
CKEDITOR.addCss( '.tagSpecialClass { background: green; padding: 3px; color: #fff } ' );
CKEDITOR.replace( 'editor', {
// A clean-up in the toolbar to focus on essentials.
toolbarGroups: [
{ name: 'document', groups: [ 'mode' ] },
{ name: 'basicstyles' },
{ name: 'insert' }
],
removeButtons: 'Image,Table,HorizontalRule,SpecialChar',
extraPlugins: 'widget',
on: {
pluginsLoaded: function() {
var editor = this;
// Define a new widget. This should be done in a separate plugin
// to keep things clear and maintainable.
editor.widgets.add( 'sampleWidget', {
// This will be inserted into the editor if the button is clicked.
template: '<span class="tagSpecialClass">23</span>',
// A rule for ACF, which permits span.tagSpecialClass in this editor.
allowedContent: 'span(tagSpecialClass)',
// When editor is initialized, this function will be called
// for every single element. If element matches, it will be
// upcasted as a "sampleWidget".
upcast: function( el ) {
return el.name == 'span' && el.hasClass( 'tagSpecialClass' );
},
// This is what happens with existing widget, when
// editor data is returned (called editor.getData() or viewing source).
downcast: function( el ) {
el.setHtml( '{{birthYear}}' );
},
// This could be done in upcast. But let's do it here
// to show that there's init function, which, unlike
// upcast, works on real DOM elements.
init: function() {
this.element.setHtml( '23' );
}
} );
// Just a button to show that "sampleWidget"
// becomes a command.
editor.ui.addButton && editor.ui.addButton( 'SampleWidget', {
label: 'Some label',
command: 'sampleWidget',
toolbar: 'insert,0'
} );
}
}
} );
HTML
<textarea id="editor">
<p>Some text.</p>
<p>And there's the widget <span class="tagSpecialClass">{{birthYear}}</span></p>
<p>Some text <span class="tagSpecialClass">{{birthYear}}</span>.</p>
</textarea>
Happy coding!

Related

Open Ace.js code editor and select content on click

I have a page containing multiple instances of ace.js editors, all on read-only, displaying code.
I wish to open the editor like so and highlight its content to allow my user to easily copy it, like so:
My current approach does not show the wanted result, it does open an editor with the code inside of it BUT its height is not at the size of its content:
Heres's how I do it:
function openCodeEditor(event, domElement, language) {
event.preventDefault();
getInstance().then((ace) => {
ace.edit(domElement, {
mode: `ace/mode/${language}`,
theme: 'ace/theme/monokai',
startLineNumber: 1,
trim: true,
});
});
}
// STARTS HERE
// Called on page load to highlight each block of code
function syntaxHighlight(domElement, language, showGutter) {
return getInstance().then((ace) => {
ace.require('ace/ext/static_highlight')(domElement, {
mode: `ace/mode/${language}`,
theme: 'ace/theme/monokai',
startLineNumber: 1,
showGutter,
trim: true,
});
// Catch clicks here, and open the editor for selection
domElement.addEventListener('click', (event) =>
openCodeEditor(event, domElement, language)
);
});
}
Does anyone of you have recommendations or perhaps my approach is faulty?
Thank you !
You can set the height of editor to be same as the height of replaced element.
var editor = ace.edit(null, {
value: domElement.textContent,
theme: 'ace/theme/monokai',
mode: `ace/mode/${language}`,
});
editor.container.style.height = domElement.clientHeight + "px"
domElement.replaceWith(editor.container)
but if the goal is simply to allow copying, selecting contents of domElement should work too.

In Quilljs Editor, How to insert an undeletable block-level element?

Here I customize a block element by Quill.import('blots/block/embed') which I insert into the editor content. I would like to know that if there is any way to make it undeletable, therefore the user could not delete it or edit it? Thanks a lot.
I had a similar issue and the solution I came up with was to intercept the keyboard binding for backspace. In the example here I have a custom 'video' blot. So, if backspace is entered and the cursor is on or directly after a video, it does nothing. Here is the documentation for the Keyboard Module for reference: https://quilljs.com/docs/modules/keyboard/
let _this = this;
this.quill = new Quill(this.contentElement, {
modules: {
keyboard: {
bindings: {
video: {
key: 'backspace',
handler: function(range, keycontext) {
let format = _this.quill.getFormat(range.index - 1);
if (!format.video && !keycontext.format.video) {
// propogate to Quill's default
return true;
} // else do nothing to prevent deleting video
}
}
}
}
},
theme: 'snow'
});
Also, another thing to keep in mind, the editor has contenteditable="true", which your custom blot will inherit. So you'll probably want to set contenteditable="false" on the node in your custom blot.

TinyMCE Enable button while in read only mode

I have a TinyMCE 4.x instance where the text should be in read only mode. But I still have some buttons that I want to have enabled. For example, one button could provide a character count for the part of the text I've selected.
But when I turn on read only mode for TinyMCE all buttons are disabled. Can I enable just my buttons while still retaining read only mode?
It's probably too late for you but other people may pass by here.
I came up by writing this function
function enableTinyMceEditorPlugin(editorId, pluginName, commandName) {
var htmlEditorDiv = document.getElementById(editorId).previousSibling;
var editor = tinymce.get(editorId);
var buttonDiv = htmlEditorDiv.querySelectorAll('.mce-i-' + pluginName.toLowerCase())[0].parentElement.parentElement;
buttonDiv.className = buttonDiv.className.replace(' mce-disabled', '');
buttonDiv.removeAttribute('aria-disabled');
buttonDiv.firstChild.onclick = function () {
editor.execCommand(commandName);
};
}
It does the trick in 2 steps:
make the button clickable (remove mce-disabled CSS class and remove the aria-disabled property)
assign the good command to the click event
And in my editor init event I call the function.
editor.on('init', function () {
if (readOnly) {
editor.setMode('readonly');
enableTinyMceEditorPlugin(htmlEditorId, 'preview', 'mcePreview');
enableTinyMceEditorPlugin(htmlEditorId, 'code', 'mceCodeEditor');
}
});
Current version of TinyMCE for which I wrote this code is 4.4.3. It may break in a future version, specifically about the selectors to get and modify the good HTML elements.
Command identifiers can be found at this page otherwise you can also find them under tinymce\plugins\PluginName\plugin(.min).js
Here is a simple way to enable your custom toolbar button and attach a click event handler inside a read only TinyMCE editor using JQUERY:
//Initialize read only Tinymce editor so that Lock button is also disabled
function initReadOnlyTinyMCE() {
tinymce.init({
selector: '#main'
, toolbar: 'myLockButton'
, body_class: 'main-div'
, content_css: 'stylesheets/index.css'
, readonly: true
, setup: function (readOnlyMain) {
readOnlyMain.addButton('myLockButton', { //Lock button is disabled because readonly is set to true
image: 'images/lock.png'
, tooltip: 'Lock Editor'
});
}
});
}
function displayReadOnlyTinyMCEwithLockButtonEnabled() {
var edContent = $('main').html();
$("#main").empty();
initReadOnlyTinyMCE(true);
tinyMCE.activeEditor.setContent(edContent);
//enable the lock button and attach a click event handler
$('[aria-label="Lock Editor"]').removeClass("mce-disabled");
$('[aria-label="Lock Editor"]').removeAttr("aria-disabled");
$('[aria-label="Lock Editor"]').attr("onclick", "LockEditor()");
}
function LockEditor() {
alert("Tiny mce editor is locked by the current user!!");
//Write your logic to lock the editor...
}
I couldn't find an easy way to do this. The simplest way is to remove the contenteditable attribute from the iframe body instead and substitute a read only toolbar set. It also means that people will still be able to copy content from the editor.
$("iframe").contents().find("body").removeAttr("contenteditable");
How about this :
editor.addButton('yourButton', {
title: 'One can Enable/disable TinyMCE',
text: "Disable",
onclick: function (ee) {
editor.setMode('readonly');
if($(ee.target).text() == "Disable"){
var theEle = $(ee.target).toggle();
var edit = editor;
var newBut = "<input type='button' style='opacity:1;color:white; background-color:orange;' value='Enable'/>";
$(newBut).prependTo($(theEle).closest("div")).click(function(e){
edit.setMode('design');
$(e.target).remove();
$(theEle).toggle();
});
}
}
});
You can try to run the code below:
$("#tinymce").contentEditable="false";
if you have more than one editors, you can use their id like below
$("#tinymce[data-id='idOfTheEditor']").contentEditable="false";

How to set up CKEditor for multiple instances with different heights?

I'd like to have multiple instances of CKEditor based on the same config settings, but with different heights. I tried setting up config with the default height, setting up the 1st instance, then overriding the height & setting up the 2nd instance:
var config = {
.....
height:'400'
};
$('#editor1').ckeditor(config);
config.height = '100';
$('#editor2').ckeditor(config);
...but I get two CKEditor instances that both have 100px height.
I also tried this:
CKEDITOR.replace('editor2',{
height: '100'
});
.. I got error messages that the instance already existed. I searched around a bit & found someone in a similar situation got advice that you have to destroy() the instance before replace(), but that seems too complicated for just setting a different initial height.
In the end I set up two different configs & copied over the toolbar_Full property:
var config1 = {
height:'400',
startupOutlineBlocks:true,
scayt_autoStartup:true,
toolbar_Full:[
{ name: 'clipboard', items : [ 'Cut','Copy','Paste','PasteText','PasteFromWord','-','Undo','Redo' ] },
{ name: 'editing', items : [ 'Find','Replace','-' ] },
{ name: 'basicstyles', items : [ 'Bold','Italic','Underline','Strike','Subscript','Superscript','-','RemoveFormat' ] },
{ name: 'paragraph', items : [ 'NumberedList','BulletedList','-','Outdent','Indent','-','Blockquote','-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock','-','BidiLtr','BidiRtl' ] },
'/',
{ name: 'links', items : [ 'Link','Unlink','Anchor' ] },
{ name: 'insert', items : [ 'Image','HorizontalRule' ] },
{ name: 'styles', items : [ 'Styles','Format','Font','FontSize' ] },
{ name: 'colors', items : [ 'TextColor','BGColor' ] },
{ name: 'tools', items : [ 'Maximize', 'ShowBlocks' ] },
{ name: 'document', items : [ 'Source' ] }
]
}
var config2 = {
height:'100',
startupOutlineBlocks:true,
scayt_autoStartup:true
};
config2.toolbar_Full = config1.toolbar_Full;
$('#editor1').ckeditor(config1);
$('#editor2').ckeditor(config2);
Is there a better way? Anything I'm missing? There's this question but they didn't post quite enough to be useful, & this very similar question hasn't been answered. Thanks!
Update:
This seems to be a timing/config handling quirk of CKEditor -- the config is read & applied later (I'm guessing after the editor's DOM framework has been set up) rather than when the editor is first instantiated.
So, any changes to the config settings made immediately after the 1st editor is instantiated with .ckeditor() are actually applied by the editor at some point in the following several milliseconds. I'd argue this isn't normal behavior, or logical.
For instance, you can get the expected behavior in my first example (overriding the config.height property after the first editor has been instantiated) to work by delaying the 2nd CKEditor instance with setTimeout(). Firefox needed ~100ms, IE needed 1ms. Wacky & wrong.
CKEditor should read the config settings when each editor is first instantiated. For now, everyone has to work around that quirk.
The easiest way to initialize two editors with custom heights is:
$('#editor1').ckeditor({ height: 100 });
$('#editor2').ckeditor({ height: 200 });
or without jQuery:
CKEDITOR.replace('editor1', { height: 100 });
CKEDITOR.replace('editor2', { height: 200 });
AFAIK it isn't possible to change editor's height on the fly.
If these methods weren't working for you, then you were doing sth else wrong.
Update:
Answering to your comment - your question in fact wasn't about CKEditor, but rather about sharing one object with only two different properties. So you can try like this:
var configShared = {
startupOutlineBlocks:true,
scayt_autoStartup:true,
// etc.
},
config1 = CKEDITOR.tools.prototypedCopy(configShared),
config2 = CKEDITOR.tools.prototypedCopy(configShared);
config1.height = 100;
config2.height = 200;
CKEDITOR.replace('editor1', config1);
CKEDITOR.replace('editor2', config2);
CKEDITOR.tools.prototypedCopy is a tool that creates new object with prototype set to the passed one. So they share all properties except of these you override later.
Update 2:
This is the update for the "Update" section in the question :).
There's no quirk in CKEditor's timing or bug or whatsoever - it's pure JavaScript and how BOM/DOM and browsers work plus some practical approach.
First thing - 90% of BOM/DOM is synchronous, but there are a couple of things that aren't. Because of this entire editor has to have asynchronous nature. That's why it provides so many events.
Second thing - in JS object are passed by reference and as we want CKEditor to initialize very quickly we should avoid unnecessary tasks. One of these is copying config object (without good reason). So to save some msecs (and because of async plugins loading too) CKEditor extends passed config object only by setting its prototype to object containing default options.
Summarizing - I know that this may look like a bug, but it's how JS/BOM/DOM libs work. I'm pretty sure that many other libs' async methods are affected by the same issue.
Add this you will get the different toolbar for both CKeditor in single page
<script>
CKEDITOR.on('instanceCreated', function (event) {
var editor = event.editor,
element = editor.element;
if (element.is('h1', 'h2', 'h3') || element.getAttribute('id') == 'editorTitle') {
editor.on('configLoaded', function () {
// Remove unnecessary plugins to make the editor simpler.
editor.config.removePlugins = 'find,flash,' +
'forms,iframe,image,newpage,removeformat,' +
'smiley,specialchar,stylescombo,templates';
// Rearrange the layout of the toolbar.
editor.config.toolbarGroups = [
{ name: 'editing', groups: ['basicstyles'] },
{ name: 'undo' },
//{ name: 'clipboard', groups: ['selection', 'clipboard'] },
{ name: 'styles' },
{ name: 'colors' },
{ name: 'tools' }
// { name: 'about' }
];
});
}
});
</script>
Solution above from Reinmar is working for me, however I decided to give 1 more solution that i used before this one.
It's really simple, all you need to know is that ckeditor create content div element for every instance with almost the same id, only difference is incremental value. So if you have 2,3,4.. instances only difference will be ordinal number. Code is here:
CKEDITOR.on('instanceReady', function(){
$('#cke_1_contents').css('height','200px');
});
This event will be activated for every instance you have, so if you want to set height for all instances you could create global variable and use it like x in #cke_"+x+"contents, every time event is activated increase x for 1, check which instance in row is with simple if and then set height.
var x=1;
CKEDITOR.on('instanceReady', function(){
if(x==1) h='200px';
else if(x==2)h='400px';
else if(x==3)h='700px';
$('#cke_'+x+'_contents').css('height',h);
x++;
});
This is not best solution but it is working, problem is you actually see content div resizing.
Update 25 Jun 2019.
Please Use this code to add multiple CKEditor instances with custom height for each one. Easiest way ever.
<textarea name="editor1" style="height:30px;" class="ckeditor"></textarea>
<script type="text/javascript">
CKEDITOR.replace( 'editor1' );
CKEDITOR.add
</script>
<textarea name="editor2" style="height:40px;" class="ckeditor"></textarea>
<script type="text/javascript">
CKEDITOR.replace( 'editor2' );
CKEDITOR.add
</script>
<textarea name="editor3" style="height:50px;" class="ckeditor"></textarea>
<script type="text/javascript">
CKEDITOR.replace( 'editor3' );
CKEDITOR.add
</script>
<textarea name="editor4" style="height:60px;" class="ckeditor"></textarea>
<script type="text/javascript">
CKEDITOR.replace( 'editor4' );
CKEDITOR.add
</script>
<textarea name="editor5" style="height:70px;" class="ckeditor"></textarea>
<script type="text/javascript">
CKEDITOR.replace( 'editor5' );
CKEDITOR.add
</script>
Ref: Here
If you add the ckeditor.js to page more than once too, it may cause that problem.
The script code must be defined once in every page.
<script type="text/javascript" src="Fck342/ckeditor.js"></script>
just use CKEDITOR.replaceAll();

Customising CKEditors Link Plugin

I am editing the link plugin to allow staff to select links to internal content.
I have managed to add another tab to the link plugin dialog with a text input with an onKeyup event. The idea is, when they type it will list the results below where they can select the link they want. Once selected I was just going to update the info tab with the url and protocol.
Here is my code sections from the existing link plugin:
....
....
//Should update info tab with value
function AddLink(txtLink)
{
var dialog = this.getDialog();
dialog.setValueOf('info', 'url', txtLink);
dialog.setValueOf('info', 'protocol', '');
}
//called when the user types in the search box. currently just uses text for basic testing
var searchBoxChanged = function ()
{
var dialog = this.getDialog();
var href = $(this).attr('href');
var txt = dialog.getValueOf('article', 'searchWords');
$('#searchResults').html("Test Title");
}
....
....
{
//Adds extra tab to the link plugin for custom link searching
id: 'article',
label: linkLang.article,
title: linkLang.article,
elements:
[
{
type: 'text',
id: 'searchWords',
label: linkLang.articleSearch,
style: 'height:40px',
size: 29,
onKeyUp: searchBoxChanged
},
{
type: 'html',
html: '<div id="searchResults">Please start tying to get results</div>'
}
]
}
....
....
At the moment I am just using some basic static data from the textbox. The link in creating on the page ok, but when it is clicked I get the error:
CRIPT5009: 'AddLink' is undefined
Can anyone shed some light on where I am going wrong?
In my experience, ["x" is undefined] errors quite often mean there's a syntax error or, often, something in the function does not evaluate to what you think it does.
Possibly, this.getDialog() is out of context so it doesn't return anything. Then, dialog.setValueOf() won't work.

Categories

Resources