I have a problem with my custom WYSIWYG editor.
This is how it works:
Select a text and click on a button. The text will be formatted. Unselect the same text and select it again. Now click again on the same button to remove the format.
This is how it does NOT work:
Select a text and click on a button. The text will be formatted. Now click again on the same button to remove the format.
I assume, that it probably doesn't work, because I am inserting an element inside the parent element. So at this moment, this element is not selected. With selectedText?.selectNode(node) I have tried to select the correct node but this doesn't change anything.
So how can I remove the format, when the text stays selected?
document.getElementById('bold').addEventListener('click', () => edit('STRONG'));
document.getElementById('italic').addEventListener('click', () => edit('EM'));
function edit(format) {
const parentElementOfSelectedText = document.getSelection().getRangeAt(0).commonAncestorContainer.parentElement;
// If element is already formatted, undo the format
if (parentElementOfSelectedText.tagName === format) {
let grandParentOfSelectedText = parentElementOfSelectedText.parentElement;
if (parentElementOfSelectedText.textContent) {
const selectedText = document.createTextNode(parentElementOfSelectedText.textContent);
grandParentOfSelectedText.insertBefore(selectedText, parentElementOfSelectedText);
grandParentOfSelectedText.removeChild(parentElementOfSelectedText);
grandParentOfSelectedText.normalize();
}
} else {
const selectedText = document.getSelection().getRangeAt(0);
const node = document.createElement(format);
const fragment = selectedText.extractContents();
if (fragment) {
node.appendChild(fragment);
}
selectedText.insertNode(node);
}
}
<button id="bold">B</button>
<button id="italic">I</button>
<p>Lorem ipsum</p>
I assume, that it probably doesn't work, because I am inserting an element inside the parent element. So at this moment, this element is not selected.
correct. but you can just re-select it in js too.
document.getElementById('bold').addEventListener('click', () => edit('STRONG'));
document.getElementById('italic').addEventListener('click', () => edit('EM'));
function edit(format) {
let parentElementOfSelectedText = document.getSelection().getRangeAt(0).commonAncestorContainer;
// If element is already formatted, undo the format
if (parentElementOfSelectedText.tagName === format || parentElementOfSelectedText.parentElement.tagName === format) {
if(parentElementOfSelectedText.tagName !== format) parentElementOfSelectedText = parentElementOfSelectedText.parentElement;
let grandParentOfSelectedText = parentElementOfSelectedText.parentElement;
if (parentElementOfSelectedText.textContent) {
const selectedText = document.createTextNode(parentElementOfSelectedText.textContent);
//work with range of old element because
//text nodes are pass by value
//and we cant create a range after its a text node
const range = document.createRange();
range.selectNode(parentElementOfSelectedText);
//this replaces some of your code but uses a range
range.deleteContents();
range.insertNode(selectedText);
grandParentOfSelectedText.normalize();
//select the range again :)
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
} else {
let selectedText = document.getSelection().getRangeAt(0);
const node = document.createElement(format);
const fragment = selectedText.extractContents();
if (fragment) {
node.appendChild(fragment);
}
selectedText.insertNode(node);
//make only the inside of the node a range
//so [...].commonAncestorContainer is "STRONG" or "EM"
//and gets recognized
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges()
selection.addRange(range);
}
}
<body>
<button id="bold">B</button>
<button id="italic">I</button>
<p>Lorem ipsum</p>
</body>
edits: adding comments to code; debugging
Related
I'm trying to create a text editor and I'm using a contenteditable div and everytime someone changes the text inside it I want to wrrap all that new text with a strong element and change the div's innerHTML
This is what I tried (I'm using react/nextjs)
useEffect(() => {
if (!divRef.current) return;
let text = divRef.current.innerText;
const htmlArray = text.split(" ").map((word) => {
return `<strong style="color: red">${word} </strong>`;
});
text = htmlArray.join("");
divRef.current.innerHTML = text;
}, [text]);
everything here works as expected but everytime I type a character the cursor goes to the start and the text is rendered backwards. How can I fix the issue I want the cursor to stay an the end of the div when user type
This is due to the fact that you're updating the innerHTML of the contenteditable div every time text changes.
You need to capture the actual position, and set it back after state change with Selection
useEffect(() => {
if (!divRef.current) return;
// Get the current cursor position
let selection = window.getSelection();
if (!selection.rangeCount) return;
let range = selection.getRangeAt(0);
let cursorPosition = range.startOffset;
// Update the innerHTML
let text = divRef.current.innerText;
const htmlArray = text.split(" ").map((word) => {
return `<strong data-id="hey" style="color: red">${word} </strong>`;
});
text = htmlArray.join("");
divRef.current.innerHTML = text;
// Get the new cursor position
selection = window.getSelection();
range = selection.getRangeAt(0);
// Set the cursor position to the new position
range.setStart(range.startContainer, cursorPosition);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}, [text]);
My problem is when I want to remove the format of a selected text, the format of the whole text will be removed.
document.getElementById('bold').addEventListener('click', () => edit('STRONG'));
document.getElementById('italic').addEventListener('click', () => edit('EM'));
function edit(format) {
let parentElementOfSelectedText = document.getSelection().getRangeAt(0).commonAncestorContainer;
// If element is already formatted, undo the format
if (parentElementOfSelectedText.tagName === format || parentElementOfSelectedText.parentElement.tagName === format) {
if (parentElementOfSelectedText.tagName !== format) parentElementOfSelectedText = parentElementOfSelectedText.parentElement;
let grandParentOfSelectedText = parentElementOfSelectedText.parentElement;
if (parentElementOfSelectedText.textContent) {
const selectedText = document.createTextNode(parentElementOfSelectedText.textContent);
// const selectedText = document.createTextNode(document.getSelection().toString());
//work with range of old element because
//text nodes are pass by value
//and we cant create a range after its a text node
const range = document.createRange();
range.selectNode(parentElementOfSelectedText);
//this replaces some of your code but uses a range
range.deleteContents();
range.insertNode(selectedText);
grandParentOfSelectedText.normalize();
//select the range again :)
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
} else {
let selectedText = document.getSelection().getRangeAt(0);
const node = document.createElement(format);
const fragment = selectedText.extractContents();
if (fragment) {
node.appendChild(fragment);
}
selectedText.insertNode(node);
//make only the inside of the node a range
//so [...].commonAncestorContainer is "STRONG" or "EM"
//and gets recognized
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges()
selection.addRange(range);
}
}
<button id="bold">B</button>
<button id="italic">I</button>
<div contentEditable="true">Lorem</div>
Select Lorem and click on the B button. So Lorem will be bold. Now type ipsum after Lorem. Ipsum will also be bold. Select ipsum and click again on the B button. Now not only ipsum lost its format but also Lorem.
It is fine when the text that gets entered will have the same format as the text before. But the user should be able to remove the format of the selected text only.
So when they start to type, the value should be Lorem ipsum and when the remove the format of ipsum, it should be Lorem ipsum.
How can I do this?
I also tried it with
const selectedText = document.createTextNode(document.getSelection().toString());
but then the unselected text will be removed. See JSFiddle
Just get the text selection, and keep track of what style was applied previously (bold/italic). If the style has already been applied, remove the style only from the selected text.
document.getElementById('bold').addEventListener('click', () => edit('STRONG'));
document.getElementById('italic').addEventListener('click', () => edit('EM'));
// Variables to track what style has been applied
let textBold = false;
let textItalic = false;
function edit(format) {
// Getting selected text
let selectedText = getSelection();
// Create new element
let el = document.createElement('span');
// Applying style depending on the format
if (format === 'STRONG') {
// Assignation to retain previous style
el.style.fontStyle = (textItalic? 'italic' : 'normal');
if (textBold === false) {
el.style.fontWeight = 'bold';
textBold = true;
} else {
el.style.fontWeight = 'normal';
textBold = false;
}
}
else if (format === 'EM') {
// Assignation to retain previous style
el.style.fontWeight = (textBold ? 'bold' : 'normal');
if (textItalic === false) {
el.style.fontStyle = 'italic';
textItalic = true;
} else {
el.style.fontStyle = 'normal';
textItalic = false;
}
}
el.innerHTML = selectedText.toString();
let range = selectedText.getRangeAt(0);
range.deleteContents();
range.insertNode(el);
}
<button id="bold">B</button>
<button id="italic">I</button>
<div contentEditable="true">Lorem</div>
window.getSelection() returns an object that you are trying to then run functions that return undefined.
You should therefore get the actual string of the selected text to work with.
window.getSelection().toString();
i am trying to wrap the selected string between two characters
for eg: selecting 'test' and clicking on change button will change the selected text to 'atestb'
the problem is that, i am able to replace the selected text, but window.getSelection().toString() is coming empty.
This is the function that im using
replaceSelectedText(startTag, endTag) {
let sel, range;
console.log(window.getSelection().toString())
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
const selectedContent = sel.toString();
console.log("Selected Content ")
console.log(selectedContent)
let replaceDiv = startTag + selectedContent;
replaceDiv=endTag ? replaceDiv + endTag : replaceDiv;
range.insertNode(document.createTextNode(replaceDiv));
}
} else if ((document as any).selection && (document as any).selection.createRange) {
range = (document as any).selection.createRange();
range.text = startTag;
}
}
Link to Stackblitz
https://stackblitz.com/edit/angular-idyhj5?file=src%2Fapp%2Fapp.component.ts
You are deleting the value before retrieving it.
range.deleteContents();
const selectedContent = sel.toString();
If you flip those two lines and store the contents before deleting, it will work as you expect.
I want to create a CMS like wordpress. In my text editor I want the user to be able to create a hyperlink via a button click. But I don't want to show an alert so the user can input the url but a div shown under the selected word/sentence inside or over the text area with an text input. How do I get the location of the selected word?
I already tried to append a textnode to it like this:
window.getSelection().appendChild(document.createTextNode("testing"));
but I get an error, that .appendChild() is not a function.
$('#btnLink').click(function() {
window.getSelection().appendChild(document.createTextNode("testing"));
})
I expect the textnode is appended to the selected word, but it doesnt work
The getSelection() method will not return a node to append text to.
I've used some code from a different answer (added below the code) to achieve what you're asking.
$('#btnLink').click(function() {
var elm = getRange();
var div = document.createElement("div");
div.appendChild( document.createElement("input") );
elm.collapse(false);
elm.insertNode(div);
});
function getRange() {
var range, sel, container;
if (document.selection) {
range = document.selection.createRange();
range.collapse(isStart);
return range.parentElement();
} else {
sel = window.getSelection();
if (sel.getRangeAt) {
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0);
}
} else {
// Old WebKit
range = document.createRange();
range.setStart(sel.anchorNode, sel.anchorOffset);
range.setEnd(sel.focusNode, sel.focusOffset);
// Handle the case when the selection was selected backwards (from the end to the start in the document)
if (range.collapsed !== sel.isCollapsed) {
range.setStart(sel.focusNode, sel.focusOffset);
range.setEnd(sel.anchorNode, sel.anchorOffset);
}
}
if (range) {
return range;
}
}
}
This code is copied and altered from How can I get the DOM element which contains the current selection? to demonstrate the use for this specific question.
A JSFiddle: https://jsfiddle.net/zuvq9nyc/5/
try this:
$('#btnLink').click(function() {
window.getSelection.append(document.createTextNode('testing'));
})
.appendchild() is a javascript function, jquery can't use it. use .append() instead and use .createTextNode() inside it.
I have a contenteditable div (with id 'editor1') that allows users to input text. There is then a function that allows them to color any highlighted text. My js uses window.getSelection().getRangeAt(0), but the issue with this is that they can highlight words outside of the div and their color will change as well. So far; I've tried:
function red(){
{
var getText = document.getElementById("editor1").innerHTML;
var selection = getText.getSelection().getRangeAt(0);
var selectedText = selection.extractContents();
var span = document.createElement("span");
span.style.color = "red";
span.appendChild(selectedText);
selection.insertNode(span);
}
}
Fiddle: https://jsfiddle.net/xacqzhvq/
As you can see, if I highlight "this will become red as well", I can use the button to make that red too.
How can I only color the highlighted text only within the editor1 div?
You are able to get the node element from the selection using .baseNode. From there you can get the parent node and use that for comparison.
function red(){
// If it's not the element with an id of "foo" stop the function and return
if(window.getSelection().baseNode.parentNode.id != "foo") return;
...
// Highlight if it is our div.
}
In the example below I made the div have an id that you can check to make sure it's that element:
Demo
As #z0mBi3 noted, this will work the first time. But may not work for many highlights (if they happen to get cleared). The <span> elements inside the div create a hierarchy where the div is the parent elements of many span elements. The solution to this would be to take traverse up through the ancestors of the node until you find one with the id of "foo".
Luckily you can use jQuery to do that for you by using their .closest() method:
if($(window.getSelection().baseNode).closest("#foo").attr("id") != "foo") return;
Here is an answer with a native JS implemented method of .closest().
Are you looking for this,
//html
<body>
<p id='editor1'>asdf</p>
<button onclick='red()'>
RED
</button>
</body>
//JavaScript
window.red = function(){
//var getText = document.getElementById("editor1").innerHTML;
var selection = window.getSelection().getRangeAt(0);
var selectedText = selection.extractContents();
var span = document.createElement("span");
span.style.color = "red";
span.appendChild(selectedText);
selection.insertNode(span);
}
Plunker: https://plnkr.co/edit/FSFBADoh83Pp93z1JI3g?p=preview
Try This Code :
function addBold(){
if(window.getSelection().focusNode.parentElement.closest("#editor").id != "editor") return;
const selection = window.getSelection().getRangeAt(0);
let selectedParent = selection.commonAncestorContainer.parentElement;
let mainParent = selectedParent;
if(selectedParent.closest("b"))
{
//Unbold
var text = document.createTextNode(selectedParent.textContent);
mainParent = selectedParent.parentElement;
mainParent.insertBefore(text, selectedParent);
mainParent.removeChild(selectedParent);
mainParent.normalize();
}
else
{
const span = document.createElement("b");
span.appendChild(selection.extractContents());
selection.insertNode(span);
mainParent.normalize();
}
if (window.getSelection) {
if (window.getSelection().empty) { // Chrome
window.getSelection().empty();
} else if (window.getSelection().removeAllRanges) { // Firefox
window.getSelection().removeAllRanges();
}
} else if (document.selection) { // IE?
document.selection.empty();
}
};
<div id="editor" contenteditable="true">
You are the programmers of the future
</div>
<button onclick="addBold()">Bold</button>
I got the code and added my edits from those following answers :
Bold/unbold selected text using Window.getSelection()
getSelection().focusNode inside a specific id doesn't work