contentEditable - Firefox <br /> tag - javascript

Firefox inserts a <br /> tag on press enter whereas the other browsers are adding either a <p> or <div>. I know that chrome and safari are inserting the same tag of the firstchild of the contentEditable div. So do Firefox, however, if the first tag's innerHTML is empty firefox is just ignoring the tag and creating a new line by pushing the default node in the second line and writes directly inside the editor instead of inside a child node. So basically, I want Firefox to write inside the given tag and continue to insert that kind of node on each press on enter. How can it be done? Any suggestions?

I've found the solution :) In order to make this work, you've to give an id to the caret's parent element. Then you can use the following code. Please note that I get the browsernName from c# and put it into a hidden field. That's why I equaled it to "firefox". The following code is tested with FF 3.6 and it works perfectly. The only thing is that you'll have to check the caret's parent element and if it is not equal to the current row's id, then you'll have to place the caret inside the current row by using a selection function. Also, perform this code on the keyup event and make sure that if you perform some other codes on the keyup event, put this code at the end of it! Anways, enjoy :)
// The code works good in the following cases:
// 1) If there is text inside the editor and the user selects the whole text
// and replace it with a single character, the <p> tag will be placed and the
// text will place inside the p tag
// 2) If the user selects the whole code and deletes it and begins to type again
// 3) If the user types normally and press enter
// NB: Please note me if you find any bug
if (browserName == "firefox") {
//remove all br tags
var brs = txteditor.getElementsByTagName("br");
for (var i = 0; i < brs.length; i++) { brs[i].parentNode.removeChild(brs[i]); }
//check whether there is a p tag inside
var para = txteditor.getElementsByTagName("p");
if (para.length == 0) {
var inner = txteditor.innerHTML.replace(/^\s+|\s+$/g, '');
var str = (inner == "") ? "​" : txteditor.innerHTML;
var nText = "<p id=\"" + cRow + "\">" + str + "</p>";
// in order to prevent a dublicate row clear inside the text editor
txteditor.innerHTML = "";
document.execCommand('insertHTML', false, nText);
} else {
// always make sure that the current row's innerHTML is never empty
if (document.getElementById(cRow).innerHTML == "")
document.getElementById(cRow).innerHTML = "​";
}
}

Try inserting a <p></p> inside your element. Then almost certainly every newline will be a new paragraph.

If you change the contenteditable element to be a <span> instead of a <div> the new line will be made with a <br>. I haven't tested this with other elements, but it would be interesting to see how different elements would behave.
A <span> element can be styled with display: block to make it look like a <div> element.

Related

Reset content editable caret position

I am currently trying to create a syntax highlighter for Javascript and I currently facing the issue which I have found out is common with creating something like this which is setting the caret position to the end while the user types or edit contentEditable text.
I researched and found this and many other solutions here on SO but none works. It gets the position of the caret but never resets it so I am trying to find a workaround for this problem.
Below is the code I came up with.
html
<div id="editor" contentEditable="true" onkeyup="resetPosition(this)"></div>
<input type="text" onkeyup="resetPosition(this)" />
js
function getPos(e) {
// for contentedit field
if (e.isContentEditable) {
e.focus()
let _range = document.getSelection().getRangeAt(0)
let range = _range.cloneRange()
range.selectNodeContents(e)
range.setEnd(_range.endContainer, _range.endOffset)
return range.toString().length;
}
// for texterea/input element
return e.target.selectionStart
}
function setPos(pos, e) {
// for contentedit field
if (e.isContentEditable) {
e.focus()
document.getSelection().collapse(e, pos);
return
}
e.setSelectionRange(pos, pos)
}
function resetPosition(e) {
if(e.isContentEditable) {
let currentPosition = getPos(e);
e.innerHTML=e.innerHTML.replace(/[0-9]/g, "a");
setPos(currentPosition, e);
return;
}
e.value = e.value.replace(/[0-9]/g, "a");
setPos(currentPosition, e);
}
This works fine for text input but not for contentEditable divs.
When I type something like function, I get otincfun.
UPDATE: I was able to fix the setPos function by changing this line from document.getSelection().collapse(e, pos); to document.getSelection().collapse(e.firstChild, pos); but a new bug arose.
When I press ENTER Key, the caret goes back to the first line and first character. Please how do I fix?
Below is the fiddle link
https://jsfiddle.net/oketega/bfeh9nm5/35/
Thanks.
The Problems
document.getSelection().collapse(element, index) collapses the cursor to the child node that index points to, not the character index.
I was able to fix the setPos function by changing this line from document.getSelection().collapse(e, pos); to document.getSelection().collapse(e.firstChild, pos);
That will work if you are only replacing characters, but if you are creating a syntax highlighter, you will want to encase characters in span elements to style them. e.firstChild would then only set the position to an index within e's first child, excluding latter span's
Another thing to consider is that you may want to autocomplete the certain chars. The caret position before you manipulate the text may not be the same as after you do so.
The Solution
I recommend creating a <span id="caret-position"></span> element to track where the caret is.
It would work like this:
function textChanged(element) {
// 1
const text = setCursorMarker(element.innerText, element);
// 2
const html = manipulate(text);
element.innerHTML = html;
// 3
const index = findCursorIndex(element);
document.getSelection().collapse(element, index)
}
Every time the user types, you can get the current caret position and slip in the #caret-position element in there.
Overwrite the existing html with the syntax highlighted text
Find out where #caret-position is and put the caret there.
Note: The recommended way to listen for when the user types in the content-editable element is with the oninput listener, not onkeyup. It is possible to insert many characters by holding down a key.
Example
There is a working js fiddle here: https://jsfiddle.net/Vehmloewff/0j8hzevm/132/
Known Issue: After you hit Enter twice, it looses track of where the caret is supposed to be. I am not quite sure why it does that.

Cannot delete HTML element in contentEditable parent if said element is the first child

The problem
I am building a custom boolean search input that should accept the boolean tags under the form of some "boolean tag bubbles" that can be added to the query via a click. Basically, instead of typing {AND} the user can click on the "AND" boolean tag and it is added to the input. Please see the picture attached to understand the layout - https://i.imgur.com/Fwa00zA.png
LE:This behaviour appears to happen only on Chrome.
Regarding the problem: if a tag is added first in the input, (like in the layout example picture) and after adding the tag the types characters only to return before the tag and press backspace, the tag is not going to get deleted.
The tag can be removed via backspace only if the user deletes all the characters added - basically moving the caret at the end and then via backspace deleting everything.
What I have already tried
Initially I made a connection between the problem and all the spans that were generated when I was moving between with my "left/right" arrow keys between the characters toward the boolean tag. Therefore I wrote some code that every time I press a key, I scans the contentEditable parent and clears all the spans and brs created. This cleared some strange cases but I am still stuck with not being able to delete the tag if it's the first element and if there are characters or other elements after the boolean tag.
Several hours ago I found this - contenteditable div backspace and deleting text node problems.
The function that inserts my boolean tag creates the tag as an element. I tried creating the node element as a as suggested in that post. Even as a button, if my element is the first element and it has characters after, it cannot be deleted.
Some of my code
For my current version with the boolean tags as elements, this is the method that creates, on click, my boolean tags and adds them to the parent element.
addBooleanTag($event){
this.$refs.divInput.focus();
if(this.typed == false & this.input_length == 0){
this.$refs.divInput.innerHTML = ''
var space = '';
this.typed = true
this.saveCursorLocation();
}
rangy.restoreSelection(this.saved_sel);
var node = document.createElement('img');
node.src = $event.img;
node.className = "boolean-button--img boolean-button--no-margin";
node.addEventListener('click', () => {
this.$refs.divInput.removeChild(node);
})
this.insertNode(node);
this.saveCursorLocation();
},
This is how the contentEditable parent element looks like
<div
#keydown.enter.prevent
#blur="addPlaceholder"
#keyup="saveCursorLocation(); clearHtmlElem($event)"
#input="updateBooleanInput($event); clearHtmlElem($event)"
#paste="pasted"
v-on:click="clearPlaceholder(); saveCursorLocation(); deleteBooleanTag();"
class="input__boolean input__boolean--no-focus"
ref="divInput"
contenteditable="true">Boolean search..</div>
This is the method that clears my contentEditable parent of breaks and spans
clearHtmlElem($event){
var i = 0;
var temp = $event.target.querySelectorAll("span, br");
if(temp.length > 0){
for(i = 0; i < temp.length; i++){
if(!temp[i].classList.contains('rangySelectionBoundary')){
if (temp[i].tagName == "br"){
temp[i].parentNode.removeChild(temp[i]);
} else {
temp[i].outerHTML = temp[i].innerHTML;
}
}
}
}
},
Expected behaviour vs actual results
I expect that when I press backspace with the caret being after the boolean tag (img element) to delete the element. A normal behaviour.
Instead, it does nothing. The only way to delete that added element/boolean tag is if the tag is the last thing to be deleted.
Please see this gif that I made while reproducing the problem. https://imgur.com/a/vAXop2s - when the console "freaks out" it's basically me pressing backspace against that img element/boolean tag and the element refusing to be deleted.
With some help, I managed to figure this out. I am posting this here for anyone who has problems with contentEditable.
Do not style the contentEditable element. I had some style on the contentEditable div and that was messing a lot of stuff like caret positioning, backspace not working properly etc. Make a wrap around it and move the styling on that wrapper.
LE: One (and probably the main) reason why the contentEditable element was misbehaving was due to having display: flex attached to it.

jquery & doc.createElement throws error for <

Both the $("<test") & document.createElement(",test") throws error due to < character associated to the text. I do not want to replace the character & wanted to see if there is option to create dom or jquery object using such text. I know replace will work but since the code is pre-existing & also since code is written such that it assume it can either have the simple text (textnode) or html tag (like span) hence this error is occuring as it fails to check if it is proper self closing html tag.
I am thinking of creating it to xml node & then check if the childnode is textNode or not before trying to create jquery object,however I am looking for suggestion & best approach to tackle such issue. I know replace of < will work & also there is no need to check for attributes of plain text but since the code is dynamic it sometimes retrieves plain text & some time it gives valid html tag that why this issue appears
I am not sure what your exact end goal is, but basically you need to do something like this:
function makeElemHack( str ) {
var div = $("<div>").html(str); //create a div and add the html
var html = div.html(); //read the html
if (!html.length) { //if the html has no length the str was invalid
div.html(str.replace(/</g,"<")); //escape the < like text should be
//div.text(str); //or you can just add it as plain text
}
return div; //with the div wraper
//return div.contents(); //without the div wrapper
}
var bd = $("body");
bd.append( makeElemHack("<p>Hello</p>") );
bd.append( makeElemHack("1<0") );
bd.append( makeElemHack("<booo") );

TinyMce editor not returning tags

H7i guys, I am having a weird problem with the TinyMce editor. What I am trying to do is to select some text, click a button and append a tag at the start and at the end.
For example, if the original text is <p>hello</p>, the end text would be <myTag><p>hello</p></myTag>.
It works fine but when selecting a single line of text the existing tags are not returned. So in the previous example I would get hello only and not <p>hello</p>.
When I select multiple lines it returns the tags.
Here is what I have tried so far:
var se = ed.selection.getContent(); //Doesn't return tags on single line
var be = ed.selection.getNode().outerHtml; //Doesn't work with multiline
var ke = ed.selection.getContent({ format: 'raw' }); //Same as the first option
Any help?
You will need to employ different functions to get the content, depending on the content the user selected
var node = ed.selection.getNode();
if (node.nodeName != 'P' )
{
content = ed.selection.getContent();
}
else content = node.outerHtml;
I use this, and works well:
var textt= tinyMCE.activeEditor.selection.getContent({format : 'text'});
alert(textt);
BUT NOTE: You should not select text from the start of a paragraph to the end of a paragraph,
because in that case(maybe bug of TinyMce), it cant get content .

workaround for ckeditor bug nesting divs bug?

Any suggestions or workarounds for this bug?
http://dev.ckeditor.com/ticket/6436
i really need to get around this bug as i need to delete whole divs with one backspace in the editor area in CKEditor. but on insertion the divs get nested due to the bug. So, deletion of individual divs become impossible.
Here is the workaround i finally designed--
http://thecamelcase.com/2011/06/reining-in-the-cursor-in-ckeditor/
I encountered this same issue and unfortunately the link to the fix made by ghostCoder isn't available anymore.
In addition to nested divs issue, we have forced paste as plain text (so data is pasted as plain text unless "paste from word" is used) and we are using div as our enterMode. In the pasted data line breaks were replaced with <br /> tags whereas we wanted each line to be wrapper inside element.
This our how I eventually solved the issue. You should know that we have not changed autoParagraph config option so it defaults to true and I don't recommend disabling it in config level. I'll try to code comment this solution well so that you get a good idea what is actually done here.
config.on.instanceReady = function(e) {
var enterMode = e.editor.config.enterMode, elem = 'div';
if(enterMode === CKEDITOR.ENTER_P) {
elem = 'p';
}
// We didn't encounter any issues when using br as enterMode so
// continue only if enterMode is div or p element
if(enterMode === CKEDITOR.ENTER_DIV || enterMode === CKEDITOR.ENTER_P) {
// Disable autoParagraph in initialization to avoid nested div issue.
// When autoParagraph is manipulated this way it will still be active
// later on so if you write some inline content in source mode it
// will be properly wrapped inside <p> or <div> element.
e.editor.config.autoParagraph = false;
// Handle paste event to get rid of <br /> line breaks issue
e.editor.on('paste', function(evt) {
var data = '';
// Stop event, handle everything here manually
evt.stop();
// Get data either from event's text or html property
if(evt.data.text) {
// Remove HTML markup from pasted data
data = evt.data.text.replace(/(<([^>]+)>)/ig, '');
// Replace all line breaks with </div><div> or </p><p> markup
// And wrap the whole content inside <div> or <p> element
data = '<' + elem + '>'
+ data.replace(/\r\n|\r|\n/g, '</' + elem + '><' + elem + '>')
+ '</' + elem + '>';
} else {
// If data was not pasted as plain text just
// get data as is from event's html property
data = evt.data.html;
}
// Insert HTML data to editor
evt.editor.insertHtml(data);
// Fire afterPaste manually as we have stopped the event
// and afterPaste wouldn't get triggered otherwise
evt.editor.fire( 'afterPaste' );
}, e.editor.element.$);
}
};

Categories