Fix cursor position when replacing innerHTML of <div contenteditable="true"> - javascript

How can I keep the cursor in the right place when typing inside of a <div id="richTextBox" contenteditable="true"></div> whose innerHTML changes on each keystroke? The act of replacing the innerHTML messes up the cursor position.
The reason I change the innerHTML is because I am adding <span> tags. It's part of a code highlighting program. The span tags allow me to place the correct color highlights.
I am using the below code from a StackOverflow answer as a band aid for the moment, but it has a significant bug. If you hit enter, the cursor stays at the old spot, or goes to a random spot. That's because the algorithm counts how many characters from the beginning the cursor is. But it doesn't count HTML tags or line breaks as characters. And the richTextBox inserts <br> to make enters.
Ideas for fixing:
Fix the below code? See Fiddle
Replace with simpler code? I tried a bunch of simpler stuff involving window.getSelection() and document.createRange(), but I could not get that to work.
Replace with a richTextBox library or module that doesn't have this bug?
Screenshot
// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
static getCurrentCursorPosition(parentElement) {
var selection = window.getSelection(),
charCount = -1,
node;
if (selection.focusNode) {
if (Cursor._isChildOf(selection.focusNode, parentElement)) {
node = selection.focusNode;
charCount = selection.focusOffset;
while (node) {
if (node === parentElement) {
break;
}
if (node.previousSibling) {
node = node.previousSibling;
charCount += node.textContent.length;
} else {
node = node.parentNode;
if (node === null) {
break;
}
}
}
}
}
return charCount;
}
static setCurrentCursorPosition(chars, element) {
if (chars >= 0) {
var selection = window.getSelection();
let range = Cursor._createRange(element, { count: chars });
if (range) {
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
static _createRange(node, chars, range) {
if (!range) {
range = document.createRange()
range.selectNode(node);
range.setStart(node, 0);
}
if (chars.count === 0) {
range.setEnd(node, chars.count);
} else if (node && chars.count >0) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length < chars.count) {
chars.count -= node.textContent.length;
} else {
range.setEnd(node, chars.count);
chars.count = 0;
}
} else {
for (var lp = 0; lp < node.childNodes.length; lp++) {
range = Cursor._createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) {
break;
}
}
}
}
return range;
}
static _isChildOf(node, parentElement) {
while (node !== null) {
if (node === parentElement) {
return true;
}
node = node.parentNode;
}
return false;
}
}
window.addEventListener('DOMContentLoaded', (e) => {
let richText = document.getElementById('rich-text');
richText.addEventListener('input', function(e) {
let offset = Cursor.getCurrentCursorPosition(richText);
// Pretend we do stuff with innerHTML here. The innerHTML will end up getting replaced with slightly changed code.
let s = richText.innerHTML;
richText.innerHTML = "";
richText.innerHTML = s;
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus(); // blinks the cursor
});
});
body {
margin: 1em;
}
#rich-text {
width: 100%;
height: 450px;
border: 1px solid black;
cursor: text;
overflow: scroll;
resize: both;
/* in Chrome, must have display: inline-block for contenteditable=true to prevent it from adding <div> <p> and <span> when you type. */
display: inline-block;
}
<p>
Click somewhere in the middle of line 1. Hit enter. Start typing. Cursor is in the wrong place.
</p>
<p>
Reset. Click somewhere in the middle of line 1. Hit enter. Hit enter again. Cursor goes to some random place.
</p>
<div id="rich-text" contenteditable="true">Testing 123<br />Testing 456</div>
Browser
Google Chrome v83, Windows 7

The issue seems to be that adding a new line adds a <br>, but as you are still in the parent element, previous DOM children are not taken into account, and the selection.focusOffset only gives the value of 4.
It may help to add a newline to the end of the innerHtml, as it is being stripped when you remove and re-add it. + "\n" to the end of line 100 on the Fiddle would do.
Your main problem though is that getCurrentCursorPosition you copied from that other StackOverflow question doesn't actually work.
I'd suggest you go through some of the other answers to this question:
Get contentEditable caret index position, and console.log what they output and see which one works best for your edge-cases.
If you don't want to write it yourself, then Caret.js (part of the At.js editor library) would be useful.

Related

show a div when certain word is typed not working as desired

Hey all so I'll keep it short. I know there've been some questions asked similar to this but none of them are able to answer what I'm trying to achieve. This is what I want the code to do:
Show a div when forward slash "/" is typed in textarea
Not show a div when forward slash is not typed
hide the pop up when the forward slash is deleted/backspaced
I can achieve the first two with the following code I'm working with right now: https://jsfiddle.net/jtk37vs8/1/. However the problem is, whenever I type forward slash and then delete it, the pop up still stays there. I'm new to JS and the code is kinda unorganized but it's quite easy to understand. So basically I'd appreciate if any of you could at least tell me if there is any straightforward way to achieve this? Thank you for your patience and reading my query.
function getCaretCoordinates() {
let x = 0,
y = 0;
const isSupported = typeof window.getSelection !== "undefined";
if (isSupported) {
const selection = window.getSelection();
// Check if there is a selection (i.e. cursor in place)
if (selection.rangeCount !== 0) {
// Clone the range
const range = selection.getRangeAt(0).cloneRange();
// Collapse the range to the start, so there are not multiple chars selected
range.collapse(true);
// getCientRects returns all the positioning information we need
const rect = range.getClientRects()[0];
if (rect) {
x = rect.left; // since the caret is only 1px wide, left == right
y = rect.top; // top edge of the caret
}
}
}
return { x, y };
}
function getCaretIndex(element) {
let position = 0;
const isSupported = typeof window.getSelection !== "undefined";
if (isSupported) {
const selection = window.getSelection();
// Check if there is a selection (i.e. cursor in place)
if (selection.rangeCount !== 0) {
// Store the original range
const range = window.getSelection().getRangeAt(0);
// Clone the range
const preCaretRange = range.cloneRange();
// Select all textual contents from the contenteditable element
preCaretRange.selectNodeContents(element);
// And set the range end to the original clicked position
preCaretRange.setEnd(range.endContainer, range.endOffset);
// Return the text length from contenteditable start to the range end
position = preCaretRange.toString().length;
}
}
return position;
}
$("#contenteditable").bind("keypress", function toggleTooltip(e) {
const tooltip = document.getElementById("tooltip");
if(String.fromCharCode(e.keyCode) == '/') {
const { x, y } = getCaretCoordinates();
$(".tooltip").show();
// tooltip.setAttribute("aria-hidden", "false");
tooltip.setAttribute( "style", `display: inline-block; left: ${x - -10}px; top: ${y - 160}px`
);
}
else if (document.getElementById('contenteditable').innerHTML.indexOf("/") != -1) {
// $(".tooltip").hide();
// tooltip.setAttribute("aria-hidden", "true");
tooltip.setAttribute("style", "display: none;");
}
// else if (document.getElementById('contenteditable').innerHTML.indexOf("/") >=0) {
// tooltip.setAttribute("aria-hidden", "true");
//tooltip.setAttribute("style", "display: none;");
// }
else {
// $(".tooltip").hide();
// tooltip.setAttribute("aria-hidden", "true");
//tooltip.setAttribute("style", "display: none;");
}
} )
You're close. BTW, doing it this way is definitely not the most performant way to be handling this, but it works.
First, you need to change your event listener to keydown. Keypress doesn't work for backspace / delete, if I remember correctly. Once you do that, it's as easy as adding what you already have.
if (e.keyCode == 46) {
// ... check if '/' is in the text area value, you already have this code
// OR
// ... check if the last index of document.getElementById('textarea-id').value is a '/'
}
There's a lot wrong with the conditionals here though, but using indexOf (like you have already) will be the most user friendly, I guess. That way, nothing changes in the case of multiple '/', and the tooltip is deleted when all the '/' are deleted.
Change the value == instead of != as you want to hide if the slash is missing
else if (document.getElementById('contenteditable').innerHTML.indexOf("/") == -1) {
when the typed character is not a slash hide it
tooltip.setAttribute("style", "display: none;");
as the backspace is not handled by the keypress event, we will a use a different handler to handle it in a keyup event as follow
$("#contenteditable").on("keyup", function toggleTooltip(e) {
if (document.getElementById('contenteditable').innerHTML.indexOf("/") == -1) {
// $(".tooltip").hide();
// tooltip.setAttribute("aria-hidden", "true");
tooltip.setAttribute("style", "display: none;");
}
});
Working fiddle code https://jsfiddle.net/BETOMBO_Mariot/a5ntzjg4/
This should solve the problems you commented under the other answers. This just looks for the last character.
$("#contenteditable").on("keyup", function(e) {
const tooltip = document.getElementById("tooltip");
let content = document.getElementById('contenteditable').innerHTML;
if(content[content.length - 1] == "/") {
const {x, y} = getCaretCoordinates();
tooltip.setAttribute("style", `display: inline-block; left: ${x + 10}px; top: ${y - 160}px`
);
}
else {
tooltip.setAttribute("style", "display: none");
}
})
Fiddle: https://jsfiddle.net/sra7ufLy/76/

HTML contenteditable: Keep Caret Position When Inner HTML Changes

I have a div that acts as a WYSIWYG editor. This acts as a text box but renders markdown syntax within it, to show live changes.
Problem: When a letter is typed, the caret position is reset to the start of the div.
const editor = document.querySelector('div');
editor.innerHTML = parse('**dlob** *cilati*');
editor.addEventListener('input', () => {
editor.innerHTML = parse(editor.innerText);
});
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}
div {
height: 100vh;
width: 100vw;
}
<div contenteditable />
Codepen: https://codepen.io/ADAMJR/pen/MWvPebK
Markdown editors like QuillJS seem to edit child elements without editing the parent element. This avoids the problem but I'm now sure how to recreate that logic with this setup.
Question: How would I get the caret position to not reset when typing?
Update:
I have managed to send the caret position to the end of the div, on each input. However, this still essentially resets the position. https://codepen.io/ADAMJR/pen/KKvGNbY
You need to get position of the cursor first then process and set the content. Then restore the cursor position.
Restoring cursor position is a tricky part when there are nested elements. Also you are creating new <strong> and <em> elements every time, old ones are being discarded.
const editor = document.querySelector(".editor");
editor.innerHTML = parse(
"For **bold** two stars.\nFor *italic* one star. Some more **bold**."
);
editor.addEventListener("input", () => {
//get current cursor position
const sel = window.getSelection();
const node = sel.focusNode;
const offset = sel.focusOffset;
const pos = getCursorPosition(editor, node, offset, { pos: 0, done: false });
if (offset === 0) pos.pos += 0.5;
editor.innerHTML = parse(editor.innerText);
// restore the position
sel.removeAllRanges();
const range = setCursorPosition(editor, document.createRange(), {
pos: pos.pos,
done: false,
});
range.collapse(true);
sel.addRange(range);
});
function parse(text) {
//use (.*?) lazy quantifiers to match content inside
return (
text
.replace(/\*{2}(.*?)\*{2}/gm, "**<strong>$1</strong>**") // bold
.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/gm, "*<em>$1</em>*") // italic
// handle special characters
.replace(/\n/gm, "<br>")
.replace(/\t/gm, " ")
);
}
// get the cursor position from .editor start
function getCursorPosition(parent, node, offset, stat) {
if (stat.done) return stat;
let currentNode = null;
if (parent.childNodes.length == 0) {
stat.pos += parent.textContent.length;
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
if (currentNode === node) {
stat.pos += offset;
stat.done = true;
return stat;
} else getCursorPosition(currentNode, node, offset, stat);
}
}
return stat;
}
//find the child node and relative position and set it on range
function setCursorPosition(parent, range, stat) {
if (stat.done) return range;
if (parent.childNodes.length == 0) {
if (parent.textContent.length >= stat.pos) {
range.setStart(parent, stat.pos);
stat.done = true;
} else {
stat.pos = stat.pos - parent.textContent.length;
}
} else {
for (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
currentNode = parent.childNodes[i];
setCursorPosition(currentNode, range, stat);
}
}
return range;
}
.editor {
height: 100px;
width: 400px;
border: 1px solid #888;
padding: 0.5rem;
white-space: pre;
}
em, strong{
font-size: 1.3rem;
}
<div class="editor" contenteditable ></div>
The API window.getSelection returns Node and position relative to it. Every time you are creating brand new elements so we can't restore position using old node objects. So to keep it simple and have more control, we are getting position relative to the .editor using getCursorPosition function. And, after we set innerHTML content we restore the cursor position using setCursorPosition.
Both functions work with nested elements.
Also, improved the regular expressions: used (.*?) lazy quantifiers and lookahead and behind for better matching. You can find better expressions.
Note:
I've tested the code on Chrome 97 on Windows 10.
Used recursive solution in getCursorPosition and setCursorPosition for the demo and to keep it simple.
Special characters like newline require conversion to their equivalent HTML form, e.g. <br>. Tab characters require white-space: pre set on the editable element. I've tried to handled \n, \t in the demo.
The way most rich text editors does it is by keeping their own internal state, updating it on key down events and rendering a custom visual layer. For example like this:
const $editor = document.querySelector('.editor');
const state = {
cursorPosition: 0,
contents: 'hello world'.split(''),
isFocused: false,
};
const $cursor = document.createElement('span');
$cursor.classList.add('cursor');
$cursor.innerText = '᠎'; // Mongolian vowel separator
const renderEditor = () => {
const $contents = state.contents
.map(char => {
const $span = document.createElement('span');
$span.innerText = char;
return $span;
});
$contents.splice(state.cursorPosition, 0, $cursor);
$editor.innerHTML = '';
$contents.forEach(el => $editor.append(el));
}
document.addEventListener('click', (ev) => {
if (ev.target === $editor) {
$editor.classList.add('focus');
state.isFocused = true;
} else {
$editor.classList.remove('focus');
state.isFocused = false;
}
});
document.addEventListener('keydown', (ev) => {
if (!state.isFocused) return;
switch(ev.key) {
case 'ArrowRight':
state.cursorPosition = Math.min(
state.contents.length,
state.cursorPosition + 1
);
renderEditor();
return;
case 'ArrowLeft':
state.cursorPosition = Math.max(
0,
state.cursorPosition - 1
);
renderEditor();
return;
case 'Backspace':
if (state.cursorPosition === 0) return;
delete state.contents[state.cursorPosition-1];
state.contents = state.contents.filter(Boolean);
state.cursorPosition = Math.max(
0,
state.cursorPosition - 1
);
renderEditor();
return;
default:
// This is very naive
if (ev.key.length > 1) return;
state.contents.splice(state.cursorPosition, 0, ev.key);
state.cursorPosition += 1;
renderEditor();
return;
}
});
renderEditor();
.editor {
position: relative;
min-height: 100px;
max-height: max-content;
width: 100%;
border: black 1px solid;
}
.editor.focus {
border-color: blue;
}
.editor.focus .cursor {
position: absolute;
border: black solid 1px;
border-top: 0;
border-bottom: 0;
animation-name: blink;
animation-duration: 1s;
animation-iteration-count: infinite;
}
#keyframes blink {
from {opacity: 0;}
50% {opacity: 1;}
to {opacity: 0;}
}
<div class="editor"></div>
You need to keep the state of the position and restore it on each input. There is no other way. You can look at how content editable is handled in my project jQuery Terminal (the links point to specific lines in source code and use commit hash, current master when I've written this, so they will always point to those lines).
insert method that is used when user type something (or on copy-paste).
fix_textarea - the function didn't changed after I've added content editable. The function makes sure that textarea or contenteditable (that are hidden) have the same state as the visible cursor.
clip object (that is textarea or content editable - another not refactored name that in beginning was only for clipboard).
For position I use jQuery Caret that is the core of moving the cursor. You can easily modify this code and make it work as you want. jQuery plugin can be easily refactored into a function move_cursor.
This should give you an idea how to implement this on your own in your project.
You can use window.getSelection to get the current position and, after parsing, move the cursor to again this position with sel.modify.
const editor = document.querySelector('div')
editor.innerHTML = parse('**dlob** *cilati*')
sel = window.getSelection()
editor.addEventListener('input', () => {
sel.extend(editor, 0)
pos = sel.toString().length
editor.innerHTML = parse(editor.innerText)
while (pos-->0)
sel.modify('move', 'forward', "character")
})
function parse(text) {
return text
.replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**') // bold
.replace(/\*(.*)\*/gm, '*<em>$1</em>*'); // italic
}
div {
height: 100vh;
width: 100vw;
}
<div contenteditable />
That said, note the edit history is gone (i.e. no undo), when using editor.innerHTML = ....
As other indicated, it seems better to separate editing and rendering.
I call this pseudo-contenteditable. I asked a question related to this
Pseudo contenteditable: how does codemirror works?. Still waiting for an answer.
But the basic idea might look this https://jsfiddle.net/Lfbt4c7p.

How to insert a HTML tag into the same <div> tag in a single keypress event? (in JQuery or Javascript)

I want to insert a HTML tag in a keypress event. I am new to jQuery and JavaScript, and I've also searched for this thing, but got no answer for this thing
I know about the append and selectionStart methods but i want to insert at the position of text cursor
For E.g., if i type the letter 'a', it should send <img src='a.png'> to the div, at the cursor position
$("#textcontent").contentEditable = "true";
$(document).ready(function(){
$("#textcontent").keydown(function(event){
$("#textcontent").append("<img src='" + event.key +".png'>");
But this (JS append code at the top) is not I want, but I want to insert a HTML tag at the current text cursor position
My HTML:
<div class="textcontent" contenteditable="true">
<img src='h.png'>
... // more image tags goes here
</div>
Can anybody pls give some answers for this problem?
Check this out:
$("#textcontent").keydown(function(event) {
if (event.key === "Backspace") return; // allow backspace to clear image
if (!/^[a-zA-Z0-9]{1,1}$/.test(event.key)) return false; // just allow a-z and 0-9
const position = document.getSelection().anchorOffset; // get position
const newElement = $(`<img alt="${event.key}" src="https://eu.ui-avatars.com/api/?name=${event.key}">`); // create new element
const element = [...$("#textcontent img")][position]; // get previous element on index "position"
if (element)
textcontent.insertBefore(newElement.get(0), element); // prepend element
else $("#textcontent").append(newElement); // append element
/* OPTIONAL! place caret at the end */
textcontent.focus();
const range = document.createRange();
range.selectNodeContents(textcontent);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
/**/
return false; // cancel event to avoid text inside div
});
#textcontent {
border: 2px solid gray;
padding: 10px;
border-radius: 10px;
}
img {
margin-left: 7px;
width: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="textcontent" contenteditable="true"></div>
I've made some changes on #jns's code and this is what i've came up with...
Added the function to use lowercase characters, space and also for some other symbols...
Thank you #jns!
$("#textcontent").keydown(function(event) {
if ((event.key === "Backspace")||(event.ctrlKey)||(event.altKey)||(event.keyCode == 46)||(event.which>=33 && event.which <=40)) return; // Allow other keys to do their normal functions (not for Enter key)
if (!/^[a-zA-Z0-9\/`~=\[\]'\\; ]$/.test(event.key)) return false; // Just allows A-Z, a-z, 0-9, and also characters like `~=[];'\/
var letter_name = event.key;
var uppercase = "false";
if (event.key === ' ' || event.key === 'Spacebar') // To add spacebar input feature ('Spacebar' for IE9 and Firefox < 37)
letter_name = '';
else
letter_name = event.key;
if (/^[A-Z]$/.test(letter_name)) // To chech whether its an uppercase letter
uppercase = "true";
else
uppercase = "false";
const position = document.getSelection().anchorOffset; // get position
const newElement = $(`<img alt="${letter_name}" src="https://eu.ui-avatars.com/api/?uppercase=${uppercase}&name=${letter_name}">`); // create new element
const element = [...$("#textcontent img")][position]; // get previous element on index "position"
if (element)
textcontent.insertBefore(newElement.get(0), element); // prepend element
else $("#textcontent").append(newElement); // append element
/* OPTIONAL! place caret at the end */
textcontent.focus();
const range = document.createRange();
range.selectNodeContents(textcontent);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
/**/
return false; // cancel event to avoid text inside div
});
#textcontent {
border: 2px solid gray;
padding: 10px;
border-radius: 10px;
}
img {
margin-left: 7px;
width: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="textcontent" contenteditable="true"></div>

How to build a Smart Compose like Gmail? Possible in a textarea?

The new predictive type feature Smart Compose of Gmail is quite interesting.
Let's say we want to implement such a functionality ourselves:
User enters beginning of text, e.g. How and in gray behind it appears are you?.
User hits TAB and the word tomorrow is set.
Example:
Can a textarea with Javascript be used to achieve this?
And if not, how could this be implemented otherwise?
My previous answer got deleted, so here's a better attempt at explaining how I've somewhat replicated Smart Compose. My answer only focuses on the pertinent aspects. See https://github.com/jkhaui/predictable for the code.
We are using vanilla js and contenteditable in our solution (just like Gmail does). I bootstrap my example with create-react-app and Medium-Editor, but neither React nor Medium-Editor are necessary.
We have a database of "suggestions" which can be an array of words or phrases. For our purposes, in my example, I use a static array containing 50,000+ common English phrases. But you can easily see how this could be substituted for a dynamic data-source - such as how Gmail uses its neural network API to offer suggestions based on the current context of users' emails: https://ai.googleblog.com/2018/05/smart-compose-using-neural-networks-to.html
Smart Compose uses JavaScript to insert a <span></span> element immediately after the word you are writing when it detects a phrase to suggest. The span element contains only the characters of the suggestion that have not been typed.
E.g. Say you've written "Hi, how a" and a suggestion appears. Let's say the entire suggestion is "how are you going today". In this case, the suggestion is rendered as "re you going today" within the span. If you continue typing the characters in the placeholder - such as "Hi, how are you goi" - then the text content of the span changes dynamically - such that "ng today" is now the text within the span.
My solution works slightly differently but achieves the same visual effect. The difference is I can't figure out how to insert an inline span adjacent to the user's current text and dynamically mutate the span's content in response to the user's input.
So, Instead, I've opted for an overlay element containing the suggestion. The trick is now to position the overlay container exactly over the last word being typed (where the suggestion will be rendered). This provides the same visual effect of an inline typeahead suggestion.
We achieve correct positioning of the overlay by calculating the top + left coordinates for the last word being typed. Then, using JavaScript, we couple the top + left CSS attributes of the overlay container so that they always match the coordinates of the last word. The tricky part is getting these coordinates in the first place. The general steps are:
Call window.getSelection().anchorNode.data.length which retrieves the current text node the user is writing in and returns its length, which is necessary to calculate the offset of the last word within its parent element (explained in the following steps).
For simplicity's sake, only continue if the caret is at the end of the text.
Get the parent node of the current text node we're in. Then get the length of the parent node's text content.
The parent node's text length - the current text node's (i.e the last word's) text length = the offset position of the last text node within its contenteditable parent.
Now we have the offset of the last word, we can use the various range methods to insert a span element immediately preceding the last word: https://developer.mozilla.org/en-US/docs/Web/API/Range
Let's call this span element a shadowNode. Mentally, you can now picture the DOM as follows: we have the user's text content, and we have a shadowNode placed at the position of the last word.
Finally, we call getBoundingClientRect on the shadowNode which returns specific metadata, including the top + left coordinates we're after.
Apply the top + left coordinates to the suggestions overlay container and add the appropriate event handlers/listeners to render the suggestion when Tab is pressed.
Visit this link for documentation https://linkkaro.com/autocomplete.html .
May be you need to make few adjustment in CSS ( padding and width ).
I hope it will help.[![
$(document).ready(function(){
//dummy random output. You can use api
var example = {
1:"dummy text 1",
2:"dummy text 2"
};
function randomobj(obj) {
var objkeys = Object.keys(obj)
return objkeys[Math.floor(Math.random() * objkeys.length)]
}
var autocomplete = document.querySelectorAll("#autocomplete");
var mainInput = document.querySelectorAll("#mainInput");
var foundName = '';
var predicted = '';
var apibusy= false;
var mlresponsebusy = false;
$('#mainInput').keyup(function(e) {
//check if null value send
if (mainInput[0].value == '') {
autocomplete[0].textContent = '';
return;
}
//check if space key press
if (e.keyCode == 32) {
CallMLDataSetAPI(e);
scrolltobototm();
return;
}
//check if Backspace key press
if (e.key == 'Backspace'){
autocomplete[0].textContent = '';
predicted = '';
apibusy = true;
return;
}
//check if ArrowRight or Tab key press
if(e.key != 'ArrowRight'){
if (autocomplete[0].textContent != '' && predicted){
var first_character = predicted.charAt(0);
if(e.key == first_character){
var s1 = predicted;
var s2 = s1.substr(1);
predicted = s2;
apibusy = true;
}else{
autocomplete[0].textContent = '';
apibusy= false;
}
}else{
autocomplete[0].textContent = '';
apibusy= false;
}
return;
}else{
if(predicted){
if (apibusy == true){
apibusy= false;
}
if (apibusy== false){
mainInput[0].value = foundName;
autocomplete[0].textContent = '';
}
}else{
return;
}
}
function CallMLDataSetAPI(event) {
//call api and get response
var response = {
"predicted": example[randomobj(example)]
};
if(response.predicted != ''){
predicted = response.predicted;
var new_text = event.target.value + response.predicted;
autocomplete[0].textContent = new_text;
foundName = new_text
}else{
predicted = '';
var new_text1 = event.target.value + predicted;
autocomplete[0].textContent = new_text1;
foundName = new_text1
}
};
});
$('#mainInput').keypress(function(e) {
var sc = 0;
$('#mainInput').each(function () {
this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
this.setAttribute('style', 'height:' + (this.scrollHeight+3) + 'px;overflow-y:hidden;');
sc = this.scrollHeight;
});
$('#autocomplete').each(function () {
if (sc <=400){
this.setAttribute('style', 'height:' + (0) + 'px;overflow-y:hidden;');
this.setAttribute('style', 'height:' + (sc+2) + 'px;overflow-y:hidden;');
}
}).on('input', function () {
this.style.height = 0;
this.style.height = (sc+2) + 'px';
});
});
function scrolltobototm() {
var target = document.getElementById('autocomplete');
var target1 = document.getElementById('mainInput');
setInterval(function(){
target.scrollTop = target1.scrollHeight;
}, 1000);
};
$( "#mainInput" ).keydown(function(e) {
if (e.keyCode === 9) {
e.preventDefault();
presstabkey();
}
});
function presstabkey() {
if(predicted){
if (apibusy == true){
apibusy= false;
}
if (apibusy== false){
mainInput[0].value = foundName;
autocomplete[0].textContent = '';
}
}else{
return;
}
};
});
#autocomplete { opacity: 0.6; background: transparent; position: absolute; box-sizing: border-box; cursor: text; pointer-events: none; color: black; width: 421px;border:none;} .vc_textarea{ padding: 10px; min-height: 100px; resize: none; } #mainInput{ background: transparent; color: black; opacity: 1; width: 400px; } #autocomplete{ opacity: 0.6; background: transparent;padding: 11px 11px 11px 11px; }
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<textarea id="autocomplete" type="text" class="vc_textarea"></textarea>
<textarea id="mainInput" type="text" name="comments" placeholder="Write some text" class="vc_textarea"></textarea>
]1]1

How to remove H1 formatting within ContentEditable (wysiwyg)

With the exception of using Undo, I don't think there's a way to remove h1 and h2 tags in content editable. The expected behavior is clicking the H1 button again should toggle it off, but it does not. There's also a "remove formatting" button, but it only works on items that are bold, italic, etc. Is there a way to do this through javascript?
Edit: Result must remove the opening and closing H1 tag, and not replace it with anything else.
Please see the simplified test case here:
http://jsfiddle.net/kthornbloom/GSnbb/1/
<div id="editor" contenteditable="true">
<h1>This is a heading one</h1>
How can I remove the header styling if I want to?
</div>
I decided to implement the approach I outlined in my comment to my other answer: traversing nodes within the selected range and removing particular nodes (in this case, based on tag name).
Here's the full demo. It won't work in IE <= 8 (which lacks DOM Range and Selection support) but will in everything other major current browser. One problem is that the selection isn't always preserved, but that isn't too hard to achieve.
http://jsfiddle.net/gF3sa/1/
This example includes modified range traversal code from elsewhere on SO.
function nextNode(node) {
if (node.hasChildNodes()) {
return node.firstChild;
} else {
while (node && !node.nextSibling) {
node = node.parentNode;
}
if (!node) {
return null;
}
return node.nextSibling;
}
}
function getRangeSelectedNodes(range, includePartiallySelectedContainers) {
var node = range.startContainer;
var endNode = range.endContainer;
var rangeNodes = [];
// Special case for a range that is contained within a single node
if (node == endNode) {
rangeNodes = [node];
} else {
// Iterate nodes until we hit the end container
while (node && node != endNode) {
rangeNodes.push( node = nextNode(node) );
}
// Add partially selected nodes at the start of the range
node = range.startContainer;
while (node && node != range.commonAncestorContainer) {
rangeNodes.unshift(node);
node = node.parentNode;
}
}
// Add ancestors of the range container, if required
if (includePartiallySelectedContainers) {
node = range.commonAncestorContainer;
while (node) {
rangeNodes.push(node);
node = node.parentNode;
}
}
return rangeNodes;
}
function getSelectedNodes() {
var nodes = [];
if (window.getSelection) {
var sel = window.getSelection();
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
nodes.push.apply(nodes, getRangeSelectedNodes(sel.getRangeAt(i), true));
}
}
return nodes;
}
function replaceWithOwnChildren(el) {
var parent = el.parentNode;
while (el.hasChildNodes()) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
}
function removeSelectedElements(tagNames) {
var tagNamesArray = tagNames.toLowerCase().split(",");
getSelectedNodes().forEach(function(node) {
if (node.nodeType == 1 &&
tagNamesArray.indexOf(node.tagName.toLowerCase()) > -1) {
// Remove the node and replace it with its children
replaceWithOwnChildren(node);
}
});
}
removeSelectedElements("h1,h2,h3,h4,h5,h6");
This may not exactly meet your needs, but you could do it by using the FormatBlock command and passing in "div" or "pre" as the final parameter:
document.execCommand('formatBlock', false, 'p');
Demo: http://jsfiddle.net/GSnbb/2/ [jsFiddle has been deleted]
EDIT: Yes, this doesn't answer the question as it is now. However, it pre-dates the edit to the question about not replacing the <h1> element and was a reasonable answer to the original question.
It is feasible with javascript, logic is the following:
get the selected text and its position (cf.
Get the Highlighted/Selected text
and
javascript - Getting selected text position)
remove all the <h1> and </h1> from the selected text
s = s.replace(/<h1>/g, '');
s = s.replace(/<\/h1>/g,'');
Insert the corrected text in place of the original one
I have drafted a solution based on your JSFiddle, but it requires some tweaking.
works: removing <h1> and </h1> from selected text on Gecko and webKit based browsers
not developed: IE support - cf. links in the jsfiddle, should not be difficult
broken:
replacement of incomplete selections (containing only one of <h1> and </h1>) - easy to fix
removal of <h1> when it is right at the beginning of the selected text - you will need to play around a bit more with selections and ranges to sort that out.
P.S. Have you considered using an existing text editor plugin instead of creating it by yourself ?

Categories