Remove the format of selected text only - javascript

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();

Related

How to remove the format of a selected text?

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

Changing color of a certain word in a paragraph using js

$('#txtInput').keyup(function(){
var txtInput = $(this).val();
localStorage.setItem('inputData', txtInput);
var returnData = localStorage.getItem('inputData');
$('#txtInput').val(returnData);
var hasTest = returnData.includes("<h1>");
if(hasTest == true){
}
});
I'm creating a text editor using js.Here I'm using localStorage to store data and retrieve data.I need to add highlighting syntax feature.
For example : If 'h1' found from the text, color the 'h1' to red.I used ".includes() and it finds the word but I have no idea how to change the color of the found text.I really appreciate your help
Try this solution:
1. Use a contenteditable element instead of input or textarea.
<div id="txtInput" contenteditable></div>
The reason is we need to display HTML with CSS inside of this input area.
2. Filter the result with highlight text.
// get highlight text from string
const highlighten = (string, highlight) => {
string = stripHtml(string);
// add highlight
if (string.includes(highlight)) {
string = string.replaceAll(highlight, `<span style="color:red">${highlight}</span>`);
}
return string;
};
Use replaceAll to set the highlight CSS. You can see the stripHTML() which is for cleaning the string before doing the filter.
3. Handle keyup event
// on keyup event
$('#txtInput').on('keyup', function(e) {
const $input = $(e.currentTarget);
// you can manage your localStorage data here
// ...
// filter the input
$input.html(highlighten($input.html(), 'h1'));
// set caret
setCaretAtTheEnd($input.get());
});
As we replace the #txtInput with the new result, we will lose the caret position, that's why we need setCaretAtTheEnd() to set the caret back to the end of input.
// https://stackoverflow.com/questions/822452/strip-html-from-text-javascript
function stripHtml(html) {
let tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
}
// https://stackoverflow.com/questions/6249095/how-to-set-the-caret-cursor-position-in-a-contenteditable-element-div
function setCaretAtTheEnd(el) {
var range = document.createRange()
var sel = window.getSelection()
const nodes = el[0].childNodes;
let offset = 0;
nodes.forEach((node, i) => {
offset = node.length || 1;
});
range.setStart(nodes[nodes.length - 1], offset)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
// get highlight text from string
const highlighten = (string, highlight) => {
string = stripHtml(string);
// add highlight
if (string.includes(highlight)) {
string = string.replaceAll(highlight, `<span style="color:red">${highlight}</span>`);
}
return string;
};
// on keyup event
$('#txtInput').on('keyup', function(e) {
const $input = $(e.currentTarget);
// you can manage your localStorage data here
// ...
// filter the input
$input.html(highlighten($input.html(), 'h1'));
// set caret
setCaretAtTheEnd($input.get());
});
#txtInput {
width: 400px;
height: 200px;
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="txtInput" contenteditable>
</div>
This should do it for you:
if (hasTest == true) {
returnData = returnData.replace('<h1>', '<h1 style="color:red">')
$('#txtInput').val(returnData);
}

Bold/unbold selected text using Window.getSelection()

I'm trying to make a simple text editor so users can be able to bold/unbold selected text. I want to use Window.getSelection() not Document.execCommand(). It does exactly what I want but when you bold any text, you can't unbold it. I want it in a way that I can bold and unbold any selected text. I tried several things but no success.
function addBold(){
const selection = window.getSelection().getRangeAt(0);
const selectedText = selection.extractContents();
const span = document.createElement("span");
span.classList.toggle("bold-span");
span.appendChild(selectedText);
selection.insertNode(span);
};
.bold-span {font-weight: bold;}
<p contentEditable>Bold anything here and unbold it</p>
<button onclick="addBold()">Bold</button>
This is close to what you want but groups words together so an unselect will remove from whole word. I have not been able to complete this as I have to go, but should be a good starting point.
function addBold(){
const selection = window.getSelection().getRangeAt(0);
let selectedParent = selection.commonAncestorContainer.parentElement;
//console.log(parent.classList.contains("bold-span"))
//console.log(parent)
let mainParent = selectedParent;
if(selectedParent.classList.contains("bold-span"))
{
var text = document.createTextNode(selectedParent.textContent);
mainParent = selectedParent.parentElement;
mainParent.insertBefore(text, selectedParent);
mainParent.removeChild(selectedParent);
mainParent.normalize();
}
else
{
const span = document.createElement("span");
span.classList.toggle("bold-span");
span.appendChild(selection.extractContents());
//selection.surroundContents(span);
selection.insertNode(span);
mainParent.normalize();
}
//selection is set to body after clicking button for some reason
//https://stackoverflow.com/questions/3169786/clear-text-selection-with-javascript
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();
}
};
.bold-span {font-weight: bold;}
<p contentEditable>Bold anything here and unbold it</p>
<button onclick="addBold()">Bold</button>
var span = '';
jQuery(function($) {
$('.embolden').click(function(){
var highlight = window.getSelection();
if(highlight != ""){
span = '<span class="bold">' + highlight + '</span>';
}else{
highlight = span;
span = $('span.bold').html();
}
var text = $('.textEditor').html();
$('.textEditor').html(text.replace(highlight, span));
});
});
You could define a function like this where the name of your class is "embolden"

Selected text from window is coming as empty

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.

How to change only the *selected* text to uppercase, not the entire paragraph in which the selection exists

In Google docs, this function changes the selected text to black
function selectedFontColorBlack() {
// DocumentApp.getUi().alert('selectedFontColorBlack');
var sel = DocumentApp.getActiveDocument().getSelection();
var elements = sel.getRangeElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
// Only modify elements that can be edited as text; skip images and other non-text elements.
if(element.getElement().editAsText) {
var text = element.getElement().editAsText();
// Bold the selected part of the element, or the full element if it's completely selected.
if (element.isPartial()) {
text.setForegroundColor(element.getStartOffset(), element.getEndOffsetInclusive(), "#000000");
} else {
text.setForegroundColor("#000000");
}
}
}
}
This function changes the entire paragraph in which the cursor (or selection) exists to uppercase:
function uppercaseSelected() {
// DocumentApp.getUi().alert('uppercaseSelected');
var sel = DocumentApp.getActiveDocument().getSelection();
var elements = sel.getRangeElements();
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
// Only modify elements that can be edited as text; skip images and other non-text elements.
if(element.getElement().editAsText) {
var text = element.getElement().editAsText();
text.setText(text.getText().toUpperCase());
}
}
}
I don't see any corresponding setText function that works on the selection's "offset", as does the setForegroundColor(Integer,Integer,String). (Both of these functions are in class Text.)
How can I change the actually selected text to uppercase, and not the entire paragraph in which the selection exists?
Thank you.
Try using the setAttributes(startOffset, endOffsetInclusive, attributes) method. Check out the documentation
[EDIT: my bad, i don't think that'll do it. I'll look a bit longer tho]
The gem hidden in the post that #Mogsdad is referring to is this: var selectedText = elementText.substring(startOffset,endOffset+1);. to be little more verbose on how this is used: you can use the string method substring on objects such as DocumentApp.getActiveDocument().getSelection().getSelectedElements()[i].getElement().editAsText().getText()
so, essentially, grab that substring, convert it to uppercase, delete the text in the range (selectedElement.getstartOffset,selectedElement.endOffsetInclusive) and insert the bolded text at selectedElement.getstartOffset
Tada! check it out:
function uppercaseSelected() {
// Try to get the current selection in the document. If this fails (e.g.,
// because nothing is selected), show an alert and exit the function.
var selection = DocumentApp.getActiveDocument().getSelection();
if (!selection) {
DocumentApp.getUi().alert('Cannot find a selection in the document.');
return;
}
var selectedElements = selection.getSelectedElements();
for (var i = 0; i < selectedElements.length; ++i) {
var selectedElement = selectedElements[i];
// Only modify elements that can be edited as text; skip images and other
// non-text elements.
var text = selectedElement.getElement().editAsText();
// Change the background color of the selected part of the element, or the
// full element if it's completely selected.
if (selectedElement.isPartial()) {
var bitoftext = text.getText().substring(selectedElement.getStartOffset(), selectedElement.getEndOffsetInclusive() + 1);
text.deleteText(selectedElement.getStartOffset(), selectedElement.getEndOffsetInclusive());
text.insertText(selectedElement.getStartOffset(), bitoftext.toUpperCase());
} else {
text.setText(text.getText().toUpperCase());
}
}
}
Started with the code from Google App script Document App get selected lines or words?, and made this almost a year ago. I'm happy if it helps you.
The "trick" is that you need to delete the original text and insert the converted text.
This script produces a menu with options for UPPER, lower and Title Case. Because of the delete / insert, handling more than one paragraph needs special attention. I've left that to you!
function onOpen() {
DocumentApp.getUi().createMenu('Change Case')
.addItem("UPPER CASE", 'toUpperCase' )
.addItem("lower case", 'toLowerCase' )
.addItem("Title Case", 'toTitleCase' )
.addToUi();
}
function toUpperCase() {
_changeCase(_toUpperCase);
}
function toLowerCase() {
_changeCase(_toLowerCase);
}
function toTitleCase() {
_changeCase(_toTitleCase);
}
function _changeCase(newCase) {
var doc = DocumentApp.getActiveDocument();
var selection = doc.getSelection();
var ui = DocumentApp.getUi();
var report = ""; // Assume success
if (!selection) {
report = "Select text to be modified.";
}
else {
var elements = selection.getSelectedElements();
if (elements.length > 1) {
report = "Select text in one paragraph only.";
}
else {
var element = elements[0].getElement();
var startOffset = elements[0].getStartOffset(); // -1 if whole element
var endOffset = elements[0].getEndOffsetInclusive(); // -1 if whole element
var elementText = element.asText().getText(); // All text from element
// Is only part of the element selected?
if (elements[0].isPartial())
var selectedText = elementText.substring(startOffset,endOffset+1);
else
selectedText = elementText;
// Google Doc UI "word selection" (double click)
// selects trailing spaces - trim them
selectedText = selectedText.trim();
endOffset = startOffset + selectedText.length - 1;
// Convert case of selected text.
var convertedText = newCase(selectedText);
element.deleteText(startOffset, endOffset);
element.insertText(startOffset, convertedText);
}
}
if (report !== '') ui.alert( report );
}
function _toUpperCase(str) {
return str.toUpperCase();
}
function _toLowerCase(str) {
return str.toLowerCase();
}
// https://stackoverflow.com/a/196991/1677912
function _toTitleCase(str)
{
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}

Categories