Context:
TinyMce has a noneditable plugin that allows to make an element non editable. If an element has the mceNonEditable
class, then TinyMce will make the element non editable.
Problem:
I want to be able to wrap this non editable element with basic styling tags.
For example if I have :
Hello <span class="mceNonEditable">user_name</span> how are you today ?
If I click on user_name
to select the non editable span and click on the TinyMce Blod button.
I would like the result to be :
Hello <b><span class="mceNonEditable">user_name</span></b> how are you today ?
But instead of this, nothing happens. When I click the TinyMce Blod button, the code doesn't change.
I created a small jsFiddle to demonstrate this : /
What I tried:
- Configure the noneditable plugin differently (/)
- Use the
data-mce-contenteditable
attribute to override the non editable behavior when a button is clicked. (See usage in TinyMce source code in DOMUtils.js line 1739) - Go around the content editable detection. (See in TinyMce source code in Formatter.js line 609)
- Build my own plugin (looks like it's not possible to solve the issue with a plugin)
I really hope you can help!
Context:
TinyMce has a noneditable plugin that allows to make an element non editable. If an element has the mceNonEditable
class, then TinyMce will make the element non editable.
Problem:
I want to be able to wrap this non editable element with basic styling tags.
For example if I have :
Hello <span class="mceNonEditable">user_name</span> how are you today ?
If I click on user_name
to select the non editable span and click on the TinyMce Blod button.
I would like the result to be :
Hello <b><span class="mceNonEditable">user_name</span></b> how are you today ?
But instead of this, nothing happens. When I click the TinyMce Blod button, the code doesn't change.
I created a small jsFiddle to demonstrate this : https://jsfiddle/timotheejeannin/2hhpenm5/
What I tried:
- Configure the noneditable plugin differently (https://www.tinymce./docs/plugins/noneditable/)
- Use the
data-mce-contenteditable
attribute to override the non editable behavior when a button is clicked. (See usage in TinyMce source code in DOMUtils.js line 1739) - Go around the content editable detection. (See in TinyMce source code in Formatter.js line 609)
- Build my own plugin (looks like it's not possible to solve the issue with a plugin)
I really hope you can help!
Share Improve this question asked Nov 30, 2016 at 10:27 Timothée JeanninTimothée Jeannin 9,9123 gold badges58 silver badges66 bronze badges 1- I know this is an old question, but this answer seems to work for me for stackoverflow./a/4886911/2088345 – ahong Commented Feb 25, 2019 at 10:27
5 Answers
Reset to default 9I figured out a "slightly" less hacky way to do this. Essentially, I register a "BeforeExecCommand" event that, for certain events, removes the contenteditable attribute, allows the mand to run, and readds the contenteditable false attribute in the "ExecCommand" event. Doing so allowed me to avoid having to custom handle the various possible events that I have seen in other proposed solutions. I have forked your original example to demonstrate the solution (and added a couple of additional formatting options and a "Variables" feature) which can be found here: https://jsfiddle/hunterae/8fsnv3h6/40/
Here are the most relevant pieces of the solution:
tinymce.init({
// Additional options here
setup: function (editor) {
var $ = tinymce.dom.DomQuery;
var nonEditableClass = editor.getParam('noneditable_noneditable_class', 'mceNonEditable');
// Register a event before certain mands run that will turn contenteditable off temporarilly on noneditable fields
editor.on('BeforeExecCommand', function (e) {
// The mands we want to permit formatting noneditable items for
var textFormatCommands = [
'mceToggleFormat',
'mceApplyTextcolor',
'mceRemoveTextcolor'
];
if (textFormatCommands.indexOf(e.mand) !== -1) {
// Find all elements in the editor body that have the noneditable class on them
// and turn contenteditable off
$(editor.getBody()).find('.' + nonEditableClass).attr('contenteditable', null);
}
});
// Turn the contenteditable attribute back to false after the mand has executed
editor.on('ExecCommand', function (e) {
// Find all elements in the editor body that have the noneditable class on them
// and turn contenteditable back to false
$(editor.getBody()).find('.' + nonEditableClass).attr('contenteditable', false);
});
},
init_instance_callback: function (editor) {
/*
The following two hacks fix some weirdness with the way the textcolor
plugin works - namely, it was attemping to apply color and background-color
directly on the element that had the noneditable css class on it instead of putting
a span around it as underline does.
*/
editor.formatter.get('forecolor')[0].exact = true;
editor.formatter.get('hilitecolor')[0].exact = true;
}
});
That is my workaround for that. Might be glitchy though.
tinyMCE.init({
/*your initializer settings*/
setup: function (ed) {
ed.on('ExecCommand', function(e) {
var selection = tinyMCE.activeEditor.selection.getContent();
var el = document.createElement( 'html' );
el.innerHTML = "<head></head><body>"+selection+"</body>";
var datapoints = Array.from(el.getElementsByClassName('mceNonEditable'));
if (datapoints.length>0) {
var styleToggle = function(key, value) {
var criteria = (datapoints.map(function(datapoint){
return (datapoint.style[key] == value);
}).reduce(function(a,b) {
return a&&b;
}));
if (criteria) {
datapoints.forEach(function(datapoint){
datapoint.style[key] = "";
datapoint.contentEditable = false;
});
} else {
datapoints.forEach(function(datapoint){
datapoint.style[key] = value;
datapoint.contentEditable = false;
});
};
}
switch (e.mand) {
case 'mceToggleFormat':
switch (e.value) {
case 'bold':
styleToggle("font-weight", "bold");
break;
case 'italic':
styleToggle ("font-style", "italic");
break;
case 'strikethrough':
styleToggle ("text-decoration", "line-through");
break;
case 'underline':
styleToggle ("text-decoration", "underline");
};
tinyMCE.activeEditor.selection.setContent(el.children[1].innerHTML);
break;
case ("mceApplyTextcolor"):
styleToggle ("color", e.value);
tinyMCE.activeEditor.selection.setContent(el.children[1].innerHTML);
break;
case ("FontName"):
styleToggle ("font-family", e.value);
tinyMCE.activeEditor.selection.setContent(el.children[1].innerHTML);
break;
case ("FontSize"):
styleToggle ("font-size", e.value);
tinyMCE.activeEditor.selection.setContent(el.children[1].innerHTML);
break;
case ("RemoveFormat"):
datapoints.forEach(function(datapoint){
["font-weight", "font-style", "text-decoration",
"text-decoration", "color", "font-family", "font-size"].forEach(function(key){
datapoint.style[key]="";
})
datapoint.contentEditable = false;
});
tinyMCE.activeEditor.selection.setContent(el.children[1].innerHTML);
break;
};
}
});
/*more stuff*/
}
});
I have also used the answer from lestrade as a starting point to my implementation. My implementation changes the original DOM in the editor and does not overwrite existing nodes with setContent.
editor.on('ExecCommand', function(e) {
//list of selected nonEditable elements
const nonEditableElements = [];
//get the list of nonEditable elements if the selection is a Range
if(editor.selection.getSel().type === "Range"){
//get the range ancestor container
editor.selection.getRng().monAncestorContainer
//get the ancestor container children
.querySelectorAll('*')
//if the child is in the selection and has a mceNonEditable class add it to the nonEditableElements list
.forEach(function(element){
if(editor.selection.getRng().intersectsNode(element) && element.classList.contains('mceNonEditable')) {
nonEditableElements.push(element);
}
});
}
//check if the selection contains nonEditableElements
if (nonEditableElements.length > 0) {
//function toggles the style on the selected nonEditable elements
function styleToggle(key, value){
nonEditableElements.forEach(function(element){
if(element.style[key] === value){
element.style[key] = "";
}else{
element.style[key] = value;
}
});
}
switch (e.mand) {
case 'mceToggleFormat':
switch (e.value) {
case 'bold':
styleToggle("font-weight", "bold");
break;
case 'italic':
styleToggle ("font-style", "italic");
break;
case 'strikethrough':
styleToggle ("text-decoration", "line-through");
break;
case 'underline':
styleToggle ("text-decoration", "underline");
}
break;
case ("mceApplyTextcolor"):
if(e.ui === "forecolor"){
styleToggle ("color", e.value);
}else if(e.ui === "hilitecolor"){
styleToggle ("background-color", e.value);
}
break;
case ("FontName"):
styleToggle ("font-family", e.value);
break;
case ("FontSize"):
styleToggle ("font-size", e.value);
break;
case ("RemoveFormat"):
nonEditableElements.forEach(function(element){
["font-weight", "font-style", "text-decoration",
"text-decoration", "color", "font-family", "font-size"].forEach(function(key){
element.style[key]="";
});
});
break;
}
}
});
I believe you can do this with a simple custom toolbar button that you can add to your TinyMCE configuration.
When you click on a non-editable element you effectively get a DOM node that is the entire non-edtible element. You can absolutely then use DOM manipulation to add styles to that element or wrap the element in another tag. For example see this TinyMCE Fiddle:
http://fiddle.tinymce./sDfaab
When you click on the non-editable element and click the Add Style to Node
button you will note that the entire non-editable element gets a new inline style that bolds the text.
This works for basic formatting.
UPDATE: it's not reliable at all to change the selection like suggested, because TinyMCE handles this event before and after, and will mess it up... the best way is to assign IDs to your noneditable objects and change them directly as DOM on ExecCommand!! So, in the selection get the object IDs and then change their styling in the DOM... not in the string HTML selection.
This is old code based on answer above (use just for inspiration):
var ed = tinymce.activeEditor;
ed.on('ExecCommand', function(e)
{
// Note: this runs after the execution
var selection = ed.selection.getContent();
var dom = $('<body></body>').html(selection);
var changed = 0;
dom.find('.mceNonEditable').each(function(){
// we apply the styles... like switches -> it would be better to check sibling elements and check their styling
var o = $(this);
// We have to store to attribute, because TinyMCE replaces stuff in the selection string!!!
// It erases defined styles from our nonEditable object, but never adds!!
var styles = o.attr('data-style');
if (typeof styles != 'undefined') styles = styles.replace(/::/g, ':'); // back to correct formatting
else styles = o.attr('style');
styles = (typeof styles == 'undefined' ? '' : styles).split(';');
var toggleStyle = function(k, v) {
var found=0;
for(var i=0; i<styles.length; i++){
var pair = styles[i].split(':', 2);
if (pair[0] == k) {
if (v == '' || pair[1] == v)
delete styles[i];
else
styles[i] = pair[0]+':'+v; // new value
found=1;
}
}
styles = styles.join(';');
if (!found) styles+= (styles == '' ? '' : ';')+k+':'+v;
o.attr('style', styles)
.attr('data-style', styles.replace(/:/g, '::')); // so tinymce doesn't remove this
changed = 1;
};
var x = e.mand.toLowerCase();
if (x == 'mcetoggleformat') {
x = e.value.toLowerCase(); // bold, italic etc.
}
if(x=='bold') toggleStyle("font-weight", "bold");
if(x=='italic') toggleStyle("font-style", "italic");
if(x=='linethrough' || x=='strikethrough') toggleStyle("text-decoration", "line-through");
if(x=='underline') toggleStyle("text-decoration", "underline");
if(x=='mceapplytextcolor') toggleStyle("color", e.value);
if(x=='fontname') toggleStyle("font-family", e.value);
if(x=='fontsize') toggleStyle("font-size", e.value);
if (x=='removeformat'){
["font-weight", "font-style", "text-decoration", "color", "font-family", "font-size"].forEach(function(key){
toggleStyle(key, '');
});
}
});
// change the selection string
if (changed) {
ed.selection.setContent( dom.html() );
}
});