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>
);
}
Related
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;
i'm trying to add a class after returning a map, basically im trying to highlight the text after translating each word im doing it by adding a CSS class to it. I'm able to add the class but just to the whole box instead of the words i changed, i've tried many methods but i guess im not understanding well the functions, i've tried matchAll, also tried replacing them as the previous method, and tried styling them directly but im still unable to achieve it, i've also tried adding the function right after the "return" but javascript ends the execution of the function when return is called.... :/
Thanks for your time!
export default function Text() {
var inputText;
var mapObj = { blue: "azul", green: "verde", yellow: "amarillo" };
function changeText() {
inputText = document.querySelector("#inputF");
document.getElementById("outputF").innerHTML = inputText.value.replaceAll(/\b(blue|green|yellow)\b(|,.$)/gi,
function(matched){
return mapObj[matched];
})
outputF.classList.add("mark");
}
return (
<div>
<textarea id="inputF"></textarea>
<button onClick={changeText}>Change</button>
<textarea id="outputF"></textarea>
</div>
);
};
///CSS
.mark{
background-color:blue;}
I would recommend using more React here instead of native JS especially for the state management of this component I may have gone a bit over board, but I updated your component to the following:
const Text = () => {
const [input, setInput] = React.useState('');
const [output, setOutput] = React.useState('');
const mapObj = {
blue: "azul",
green: "verde",
yellow: "amarillo"
};
const changeText = (e) => {
e.preventDefault();
const changedText = input.replaceAll(
/\b(blue|green|yellow)\b(|,.$)/gi,
(matched) => `<span class="mark">${mapObj[matched]}</span>`
)
setOutput(changedText);
}
return (
<form>
<textarea
id="inputF"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" onClick={changeText}>Change</button>
<div
id="outputF"
dangerouslySetInnerHTML={{__html: output}}
/>
</form>
);
};
Fiddle: https://jsfiddle.net/3tc2xzms/
For starters, I added 2 useState hooks to manage the input and output text. And then for the highlight portion, each matched word that was changed gets wrapped in a span.mark. Also the output textarea was changed to a div.
Related Docs:
Forms: https://reactjs.org/docs/forms.html
State Hook: https://reactjs.org/docs/hooks-state.html
Setting string as HTML: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
The style you applied will change the background color of whole textarea, what you can do instead is use setSelectionRange() method of textarea, that will give you the result near to what you are expecting.
function changeText() {
inputText = document.querySelector("#inputF");
document.getElementById("outputF").innerHTML = inputText.value.replaceAll(/\b(blue|green|yellow)\b(|,.$)/gi,
function(matched){
return mapObj[matched];
})
// something like this
// here you need to set start and end index for the highlights part, you can use the logic as per need
outputF.setSelectionRange(0, inputText.value.length )
}
and to change the highlight color you can use the following css:
::selection {
color: red;
background: yellow;
}
::-moz-selection { /* Code for Firefox */
color: red;
background: yellow;
}
With draft-js and a styled component, I made an inline input intended for a calculator. When I type into it with a keyboard, it works as expected:
When I press the plus button, a "+" is added to the text, but the view doesn't scroll:
Here's the behavior and the code in a codesandbox:
https://codesandbox.io/s/charming-brattain-mvkj3?from-embed=&file=/src/index.js
How can I get the view to scroll when text is added programmatically like that?
At long last, I figured out a solution. I added this to the calculator input component:
const shell = React.useRef(null);
const [scrollToggle, setScrollToggle] = React.useState(
{ value: false }
);
React.useEffect(() => {
const scrollMax = shell.current.scrollWidth - shell.current.clientWidth;
shell.current.scrollLeft += scrollMax;
}, [scrollToggle]);
const scrollToEnd = () => {
setScrollToggle({ value: !scrollToggle.value });
};
Then at the end of the insertChars function I added scrollToEnd();.
And I set ref={shell} on <InputShell>
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
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