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;
Related
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;
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>
);
}
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
Please, view my js fiddle where I've illustrated the problem
Here's my js fiddle:
https://jsfiddle.net/jajabya/fb93f7b0/
My goad is to get an input tag where special words (like dates or users' names could be highlighted by wrapping in span tags)
There must be a problem with div, because when I use input field instead everything works fine.
My problem is that I can't make the caret appear in the right place
Every time when the state updates in onInput
onInput(event) {
this.setState({
html: event.target.innerText.toUpperCase()
});
}
the caret rolls back to the beginning
My idea is to save the current caret position in the state, and set it back via a ref in componentDidUpdate() (since ref doesn't rerender the component).
Note: This is a prototype idea, that I've never battle tested, so use with caution.
The caret position code was take from this answers:
Code for getting the caret position
Code for setting caret position
class Editable extends React.Component {
componentDidUpdate(prev) {
const { position } = this.props;
if(position !== prev.position && this.ce.childNodes.length) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(this.ce.childNodes[0], position);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
render() {
return (
<div
contentEditable
className={this.props.className}
onInput={this.props.onInput}
ref={ce => this.ce = ce}
suppressContentEditableWarning>
{this.props.html}
</div>
);
}
}
class App extends React.Component {
state = {
html: 'Text',
caret: 0
};
handleInput = (event) => this.setState({
html: event.target.innerText.toUpperCase(),
position: window.getSelection().getRangeAt(0).startOffset
});
render() {
return (
<Editable
{...this.state}
className="Editable"
onInput={this.handleInput} />
);
}
}
ReactDOM.render(
<App />,
demo
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="demo"></div>
I have a React application which displays some spans:
<span>Hello</span> <span>my</span> <span>name</span> <span> is </span> ...
I would like the user to select the text with the mouse like so
..and then get the selected value, or highlight the text etc.
How would I do this in React? I am not sure what event handlers to use and how to get hold of the current selection! A minimal example or a hint would be appreciated!
Use onMouseUp and onDoubleClick events to detect to call method, that will determine selection using JavaScript Selection API.
Use window.getSelection() to get selection object.
To get selected text, use window.getSelection.toString().
To get coordinates of selected text area for rendering popup menu, use selection.getRangeAt(0).getBoundingClientRect().
As an example implementation, take a look at react-highlight library.
There is no React-specific solution for this. Just use window.getSelection API.
To output highlighted text run window.getSelection().toString()
Here's an example in React using a functional component:
const Post = () => {
const handleMouseUp() {
console.log(`Selected text: ${window.getSelection().toString()}`);
}
return (
<div onMouseUp={handleMouseUp}>Text</div>
);
}
As Lyubomir pointed out the window.getSelection() API does the trick!
I think that it's the right way...
document.onmouseup = () => {
console.log(window.getSelection().toString());
};
Step 1: - Initially Mouse will capture every time you enter something
<textarea type="text"
className="contentarea__form__input"
onMouseUpCapture={this.selectedText}>
</textarea>
Step 2: To make sure it only catches your selected text I used if-else condition
selectedText = () => {
window.getSelection().toString() ? console.log(window.getSelection().toString()) : null;
}
Due to this bug in Firefox, I was unable to get it working with
window.getSelection() as suggested by other answers.
So I had to do the following (in React):
constructor(props) {
this.textAreaRef = React.createRef();
}
getSelection() {
const textArea = (this.textAreaRef.current as HTMLTextAreaElement);
console.log(textArea.value.substring(
textArea.selectionStart,
textArea.selectionEnd
)
);
}
render() {
<textarea onMouseUp={() => this.getSelection()}
ref={this.textAreaRef}>
</textarea>
}
I just made a custom hook for this (in typescript if needed):
export const useSelectedText = () => {
const [text, setText] = useState('')
const select = () => {
const selected = window.getSelection() as Selection
setText(selected.toString())
}
return [select, text] as const
}
This can be done using react-text-annotate
library: https://www.npmjs.com/package/react-text-annotate
We can do using state simply to select and highlight them.
import styled from 'styled-components';
const ClipBoardLink = styled.span<{ selection: boolean }>`
background: ${(p) => (p.selection ? '#1597E5' : '')};
margin-left: 0.5rem;
color: ${(p) => (p.selection ? '#fff' : '')};
`;
const GroupOrderModel = () => {
const [isCopied, setIsCopied] = useState(false);
const handleIsClipboardCopy = async (text: string) => {
setIsCopied(true);
navigator.clipboard.writeText(text);
setTimeout(() => {
setIsCopied(false);
}, 5000);
};
return (
<ClipBoardLink selection={isCopied} onClick={async () => await handleIsClipboardCopy("Click me!")}>Click me!</ClipBoardLink>
);
}