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
Related
I have rjsf version ^5.0.0-beta.10 installed in package.json and am able to render a proper Form using react-jsonschema-form. The problem is that I'm using ObjectFieldTemplate and every time I enter a character in one of the string input boxes, the box goes out of focus and I have to click on the box again to be able to type anything.
I have read https://github.com/rjsf-team/react-jsonschema-form/issues/2106, which suggested me to move the ObjectFieldTemplate outside of the custom Form definition. I did that and it does not work. I have also read Custom widget with input loses focus in react-jsonschema-form when formData is passed as a prop to the form, which is an advice about setting state, but I'm using functional components rather than class components, so I'm not sure if it's applicable.
The code looks like:
import validator from "#rjsf/validator-ajv6";
import Form from "#rjsf/mui";
const ObjectFieldTemplate = (props) => {
// some logic to be computed
return (
<div>
<h3>{props.title}</h3>
<p>{props.description}</p>
{props.properties.map(function (field) {
// logic to determine the style
return (<fieldset style={style} key={uuidv4()}>{field.content}</fieldset>);
})}
</div>
);
}
const JsonSchemaForm = (props) => {
// define schema and uiSchema
const onSubmit = ({formData}, e) => {
// some logic
}
const onError = (errors) => {console.log(errors);}
return (<Form
schema={schema}
validator={validator}
formData={data}
uiSchema={uiSchema}
onSubmit={onSubmit}
onError={onError}
templates={{ ObjectFieldTemplate }}
/>);
}
Solved. I'm not sure why, but it appears that setting key={uuidv4()} is an expensive computation step that forces the input box to be out of focus.
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 need to add a value (from the dropdown), this will be added in the input field 'at the position of the cursor':
import { useState } from "react";
import "./styles.css";
export default function App() {
const [cur, setCur] = useState("");
const [state, setState] = useState("");
const [dropVal, setDropVal] = useState("");
const handleChange = (e) => {
setState(e.target.value);
// gives cursor index
// this only shows cursor position if user types
// I need to track the position of the cursor and add dropVal there
setCur(e.target.selectionStart);
};
return (
<div className="App">
<input onChange={(e) => handleChange(e)} value={state} />
<select onChange={(e) => setDropVal(e.target.value)} >
<option>ONE</option>
<option>TWO</option>
</select>
</div>
);
}
I tried this, which is incomplete, couldn't find a way to implement it anywhere.
Would appreciate the help, thanks in advance!!
What you are looking for is the selectionStart and selectionEnd properties on an input field.
Basically, you can attach an onBlur listener on your input field and inside it you can access the selectionStart property and save it in state.
Blur means that the input field has lost its focus (meaning you have clicked somewhere outside - like on the dropdown in our case). So once the onBlur is triggered, the selectionStart refers to where your cursor was while the input was still in focus.
Later you can use this value to break the string and add whatever you want (option value in this case) at the position of the cursor.
const onBlur = (e) => {
setCur(e.target.selectionStart);
};
Have a look at this code sandbox
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"
/>
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