Reselecting text in content editable div after re-rendering-React - javascript

I am trying to implement a simple text editor in React. I have a content editable div, and a button. What I want to achieve is to
Select text in the content editable div
When I press the button , which is outside the content editable div, to make the text bold
Re-render the component to capture the inner html of the content editable div to display the content as preview under the editor
Preserve the selection after the re-render, for any possible further edits on the selected text, for example, make it italic in addition for being bold.
The problem I am facing is that, when I make the selected text bold, by inserting a span with class text-bold with the selected text as its content in place of the selected text, I need to re-render the component to capture the new html code inside the content editable div, to display the content of the editor as preview under the editor (the same as it happens here on StackOverflow). However, when the component re-renders, I cannot restore the selection. I tried to use useEffect to implement this, but it didn't work.
So, my question is: how can restore the selection after I re-render the component?
This is the full code (here is the codesandbox code)
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [htmlContent, setHtmlContent] = useState("<p>Hello World!</p>");
const [selectionRange, setSelectionRange] = useState(null);
// useEffect(() => {
// if (selectionRange) {
// // Restore selection
// let sel = window.getSelection();
// sel.removeAllRanges();
// sel.addRange(selectionRange);
// console.log(sel.toString());
// }
// }, [htmlContent, selectionRange]);
const storeSelectionRange = () => {
if (window.getSelection().toString().length) {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
setSelectionRange(range);
}
};
const onClick = () => {
if (selectionRange) {
// Restore selection
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(selectionRange);
// Create a new html element with the selected text as its content
const newNode = document.createElement("span");
newNode.className = "text-bold";
const newContent = document.createTextNode(sel.toString());
newNode.appendChild(newContent);
// Re-select the text to be replace by the newly created element
let range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(newNode);
// setSelectionRange(range);
// Update the html inside the content editable div with class "editor__content"
const newHtml = document.querySelector(".content").innerHTML;
setHtmlContent(newHtml);
}
};
return (
<div className="container">
<div className="editor">
<div
className="content"
contentEditable="true"
dangerouslySetInnerHTML={{ __html: htmlContent }}
onBlur={storeSelectionRange}
></div>
<button className="btn" onClick={onClick}>
Bold
</button>
</div>
<div className="selected-text">{htmlContent}</div>
</div>
);
}

Related

element.focus() doesn't work on rerendered contentEditable div element

import React, { useEffect, useState } from "react";
import "./styles.css";
const RichText = () => {
const [value, setValue] = useState("");
useEffect(() => {
const div = document.getElementById("textarea");
if (div) {
setTimeout(() => {
div.focus();
}, 0);
}
});
return (
<div
className="rich-text"
onInput={(e) => {
setValue(e.target.innerText);
}}
contentEditable
id="textarea"
dangerouslySetInnerHTML={{
__html: value
}}
/>
);
};
export default RichText;
I want to implement rich text component, the idea , is that you can type text inside some field , you can style this text ( make it bold, italic, underlined , etc) . I want to same text value inside on value state variable , and then wrap it somehow inside of html tags <p>Hello <b> Andrew</b></p> , and show it in real time inside the same field , styled. For showing html tags inside of div , contentEditable field, I need to use dangerouslySetInnerHTML , and this is the main problem. That on each button press , I update value, component then rerendered , focus goes at the start of field , but I want it to be at the end in time you typing new text . I've tried to make it by ref => ref.current.focus() , it doesn't work, in code above you can see that I also try to make it by vanilla js with using timeout , it also doesn't work , autoFocus - can be used only on input, textarea, etc , div can be used with this property . I have save it to ref , but then I can't show wrapped html inside of div . Tried a lot of cases , but it is seemles . Any ideas how to make it ?
The issue is when using useState hook with contentEditable and dangerouslySetInnerHTML to sync value. When you type something in the div it rerenders again and brings the cursor back to the start.
You can use instance variables in the function component (useRef to update value) to get rid of the issue.
And you should use innerHTML instead of innerText to get the HTML string saved
Try like below
import React, { useRef } from "react";
import "./styles.css";
const RichText = () => {
const editableRef = useRef(null);
const { current: value } = useRef(
'<div style="color:green">my initial content</div>'
);
const setValue = (e) => {
value.current = e.target.innerHTML;
};
const keepFocus = () => {
const { current } = editableRef;
if (current) {
current.focus();
}
};
return (
<div
className="rich-text"
onInput={setValue}
contentEditable
id="textarea"
ref={editableRef}
onBlur={keepFocus}
dangerouslySetInnerHTML={{
__html: value
}}
/>
);
};
export default RichText;
Code Sandbox
import React from "react";
import "./styles.css";
class TextInput extends React.Component {
constructor(props) {
super();
this.state = { value: "" };
}
shouldComponentUpdate() {
return true;
}
componentDidUpdate() {
const el = document.getElementById("textarea");
if (el) {
console.log(el.selectionStart, el.selectionEnd);
var range, selection;
if (document.createRange) {
//Firefox, Chrome, Opera, Safari, IE 9+
range = document.createRange(); //Create a range (a range is a like the selection but invisible)
range.selectNodeContents(el); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection(); //get the selection object (allows you to change selection)
selection.removeAllRanges(); //remove any selections already made
selection.addRange(range); //make the range you have just created the visible selection
} else if (document.selection) {
//IE 8 and lower
range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible)
range.moveToElementText(el); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
range.select(); //Select the range (make it the visible selection
}
}
}
update(value) {
this.setState({ value });
}
render() {
console.log(this.state.value);
return (
<div
className="rich-text"
onInput={(e) => {
this.update(e.target.innerText);
}}
contentEditable
id="textarea"
dangerouslySetInnerHTML={{
__html: `<b>${this.state.value}</b>`
}}
/>
);
}
}
export default TextInput;

Using an anchor element to remove a label in a contentEditable div

I have a React app that has a component using react-contentEditable.
in the image I provided, I need to click on the X and remove that label from the html prop in contentEditable component. contentEditable needs a string for its html prop. I need to somehow update the html prop string to a new string that discards the selected label but keeps the others. my first thought was a button, but I couldnt seem to pass an onClick when the html requires a string... so I defaulted to trying a tag. can an tag somehow remove or change the class of the clicked element in contentEditable?
In my component I have a "add Contacts" button that when clicked updates the contentEditable html to a string with a <label> <span> <a> </a> </span> </label>. I need to figure out how to use an anchor element or maybe a button with some type of onclick to remove the "clicked" element.
in the image I provided, I need to click on the X and remove that label from the html prop in contentEditable component. contentEditable needs a string for its html prop. I need to somehow update the html prop string to a new string that discards the selected label but keeps the others. my first thought was a button, but I couldnt seem to pass an onClick when the html requires a string... so I defaulted to trying a tag. can an tag somehow remove or change the class of the clicked element in contentEditable?
import React, { createRef, Fragment, useState, useEffect} from 'react';
const AddContactTo = ({ toFieldRef, recipients, setRecipients, contactsModal, setContactsModal, toField, setToField }) => {
const addContactsToDiv = () => {
let selectedContactsArray = [] // create and empty array
const Matches = selectedRows.filter((row) => {
const isMatched = contacts.some(contact => { if(contact._id == row) {
selectedContactsArray.push({firstName: contact.firstName, lastName: contact.lastName, phone_number:contact.phone_number, _id: contact._id})
}})
return isMatched
})
const matchedContactsArray = []
selectedContactsArray.map(contact => { // mapping over selected to add html
matchedContactsArray.push((`
<label contentEditable="false" class="p-1 font-weight-bold bg-primary ml-2 text-white rounded-capsule shadow-none fs--3">${contact.firstName + " " + contact.lastName + " "}
<span class="badge fs--1 badge-soft-success badge-pill ml-2">${contact.phone_number}</span>
X // when this <a> is clicked, I need to remove only this element from the html
<span name="indy-contacts" class="d-none">${contact._id}</span>
</label>`))
})
matchedContactsArray.map(contact => { return contact})
const stringifiedRows = matchedContactsArray.toString()
setToField({...toField, html: stringifiedRows})
selectedContactsArray = []
}
export default AddContactTo
Here is the contentEditable div. This is in a parent component
MessageCreateForm.js
import React, { useState, useRef, Fragment } from 'react';
import ContentEditable from 'react-contenteditable';
const MessageCreateForm = () => {
const toFieldRef = useRef()
const handleChange = evt => {
console.log(evt.target.value)
console.log(toField.value)
console.log(toFieldRef)
const indyContacts = document.getElementsByName('indy-contacts')
const indyGroups = document.getElementsByName('indy-groups')
setToField({html: evt.target.value})
};
const handleBlur = (evt) => {
console.log(toFieldRef.current);
console.log(evt.target.value)
};
const handleClick = () => {
console.log(toFieldRef)
}
<ContentEditable
name="to"
innerRef={toFieldRef} // passing our ref instead of state
html={toField.html} // the html = our ref.current property
//value={toField}
onBlur={handleBlur}
onClick={() => {handleClick()}}
onChange={handleChange} // this sets our refs.current = event.target.value
style={{minHeight: "7em", maxHeight: "10em", overflow: "auto"}}
className="border border-2x border-300 bg-light rounded-soft fs-1"
>
</ContentEditable>
export default MessageCreateForm;

Select specific text in the editorState

I'm creating a rich text editor using draftjs. Here is the minimal codesandbox so you have an idea of what the issue is.
So I have an helper function getCurrentTextSelection that return me the text that I'm selecting:
const getCurrentTextSelection = (editorState: EditorState): string => {
const selectionState = editorState.getSelection();
const anchorKey = selectionState.getAnchorKey();
const currentContent = editorState.getCurrentContent();
const currentContentBlock = currentContent.getBlockForKey(anchorKey);
const start = selectionState.getStartOffset();
const end = selectionState.getEndOffset();
const selectedText = currentContentBlock.getText().slice(start, end);
return selectedText;
};
When I click outside the TextEditor, the focus is lost so the text isn't selected (but stay the selected one for the editorState).
Is there a programmatic way to reselect this text using the editorState? So that when you click the Select text again button, the text in the TextEditor is selected.
I believe what you're looking to do is restore focus to the editor. If all you do is click outside the editor, the selection state doesn't change (which is why your selected text remains the same). If you then restore focus the same selection becomes visible again, without any changes to editorState.
Draft.js has some documentation about how to do this: https://draftjs.org/docs/advanced-topics-managing-focus/
The Editor component itself has a focus() method which, as you might expect, restores focus to the editor. You can gain access to the component instance with a ref:
const editorRef = React.useRef<Editor>(null)
const selectAgain = () => {
editorRef.current.focus()
};
Then connect the ref to the component and add the click handler to the button:
<div>
<Editor
editorState={editorState}
onChange={onEditorStateChange}
placeholder={placeholder}
ref={editorRef} // added ref
/>
<h2>Selected text:</h2>
<p>{getCurrentTextSelection(editorState)}</p>
// added click handler
<button onClick={selectAgain}>Select text again</button>
</div>
Complete example: https://codesandbox.io/s/flamboyant-hill-l31bn
Maybe you can store the selectedText into EditorState
Using
EditorState.push( editorState, contentState, changeType)
More Info

ContentEditable on nextjs, update not showing in state

i got a component for a message. I got there a ContentEditable component https://www.npmjs.com/package/react-contenteditable i use there because i would need to add contacts in this "textarea" but i needed to implement html code inside for separate every tag, give them a color, etc.
The problem is that i want to prevent characters, user will not be able to add letters, just numbers, comma, and space. I created a function for this for use "onChange", it shows me the right data in the console. But in the frame it stills show the ilegal characters that the user has typed in. The correct data is in the state, but it does not update on the ContentEditable frame.
const contentEditable = React.createRef();
let state = { html: "0424" };
const handleChange = evt => {
let htmlf = evt.target.value.replace(/\D/g,''); ;
console.log(htmlf);
state = { html: htmlf };
console.log(state);
};
<ContentEditable
innerRef={contentEditable}
html={state.html} // innerHTML of the editable div
disabled={false} // use true to disable editing
onChange={handleChange} // handle innerHTML change
tagName="numero" // Use a custom HTML tag (uses a div by default)
id="contacts"
/>
SOLUTION
Just declare the component state in a different way.
constructor(props) {
super(props);
this.state = {
html: "0424"
};
}
contentEditable = React.createRef();
handleChange = evt => {
let htmlf = evt.target.value.replace(/\D/g, "");
console.log(htmlf);
this.setState({ html: htmlf })
console.log(this.state);
};
<ContentEditable
innerRef={this.contentEditable}
html={this.state.html} // innerHTML of the editable div
disabled={false} // use true to disable editing
onChange={this.handleChange} // handle innerHTML change
tagName="numero" // Use a custom HTML tag (uses a div by default)
id="contacts"
/>

document.execCommand ('copy') don't work in React

I have the function below that is called on click of a button . Everything works well, but the document.execCommand ('copy') simply does not work.
If I create another button and call only the contents of if in a separate function, it works well.
I have already tried calling a second function inside the first one, but it also does not work. the copy is only working if it is alone in the function.
Does anyone know what's going on?
copyNshort = () => {
const bitly = new BitlyClient('...') // Generic Access Token bit.ly
let txt = document.getElementById('link-result')
bitly.shorten(txt.value)
.then((res) => {
this.setState({ shortedLink: res.url })
if (this.state.shortedLink !== undefined) {
document.getElementById('link-result-shorted').select() // get textarea value and select
document.execCommand('copy') // copy selected
console.log('The link has been shortened and copied to clipboard!')
ReactDOM.render(<i className="fas fa-clipboard-check"></i>, document.getElementById('copied'))
}
console.log('Shortened link 👉🏼', res.url) // Shorted url
})
}
The problem is that the copy-to-clipboard functionality will only work as a direct result of a user's click event listener... This event cannot be virtualised and the execCommand will not work anywhere else than the immediate callback assigned to the event listener...
Because react virtualises and abstracts 'events' then that's very possibly where the problem lies and as suggested you should be using React's react-copy-to-clipboard.
You can use lib react-copy-to-clipboard to copy text.
import {CopyToClipboard} from 'react-copy-to-clipboard';`
function(props) {
return (
<CopyToClipboard text={'Text will be copied'}>
<button>Copy button</button>
</CopyToClipboard>
);
}
if you click button Copy button, it will copy the text Text will be copied
The lib react-copy-to-clipboard based on copy-to-clipboard does work for me, but if you want to copy the source into your own file, Some places need attention.
The code below works fine.
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<div className="App">
<h1
onClick={e => {
const range = document.createRange()
const selection = document.getSelection()
const mark = document.createElement('span')
mark.textContent = 'text to copy'
// reset user styles for span element
mark.style.all = 'unset'
// prevents scrolling to the end of the page
mark.style.position = 'fixed'
mark.style.top = 0
mark.style.clip = 'rect(0, 0, 0, 0)'
// used to preserve spaces and line breaks
mark.style.whiteSpace = 'pre'
// do not inherit user-select (it may be `none`)
mark.style.webkitUserSelect = 'text'
mark.style.MozUserSelect = 'text'
mark.style.msUserSelect = 'text'
mark.style.userSelect = 'text'
mark.addEventListener('copy', function(e) {
e.stopPropagation()
})
document.body.appendChild(mark)
// The following line is very important
if (selection.rangeCount > 0) {
selection.removeAllRanges()
}
range.selectNodeContents(mark)
selection.addRange(range)
document.execCommand('copy')
document.body.removeChild(mark)
}}
>
Click to Copy Text
</h1>
</div>
)
}
}
export default App
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<div className="App">
<h1
onClick={e => {
const mark = document.createElement('textarea')
mark.setAttribute('readonly', 'readonly')
mark.value = 'copy me'
mark.style.position = 'fixed'
mark.style.top = 0
mark.style.clip = 'rect(0, 0, 0, 0)'
document.body.appendChild(mark)
mark.select()
document.execCommand('copy')
document.body.removeChild(mark)
}}
>
Click to Copy Text
</h1>
</div>
)
}
}
export default App

Categories

Resources