I have a set of React components that represent a text document. When someone selects text, I want to display a toolbar. Buttons on the toolbar will eventually change styling on the selected text of the React components.
Each phrase in the document gets it's own component that looks like (simplified):
render: function() {
return (
<span handleSelection={this.handleSelection}>
<span className="pre"></span>
<span className="phrase">text</span>
<span className="post"></span>
{this.renderSelectionToolbar}
</span>
)
},
renderSelectionToolbar: function()
return this.state.selected ? <ToolbarHTML> : '';
},
handleSelection: function() {
this.setState({selected: true});
}
I've tried getting the selection with var selection = window.getSelection(). I can get the wrapping span (start of the selection) with selection.anchorNode. I tried calling selection.anchorNode.handleSelection(), but that function doesn't exist on the html element.
How can I call a method of my React component upon text selection? Is there a better way to display a toolbar after selected text? Thanks!
Thanks to Felipe T.'s comment, I've had an idea that solves the problem.
I was already using the onMouseUp event of a parent element to pull the list of phrases that have been changed. I've been rendering a phraseId attribute to each phrase so that I can update its entry in mongodb. Now, I'm updating the mongodb entry of the first phrase in the selection with an attribute {contextToolbar: true}. Then, in my render code I build a display off of the same attribute. If the user dismisses the toolbar or un-selects the text then the attribute gets set back to false. Though, I have to remember to remove the flag if the user goes straight into selecting a new set of text.
This is working for now, but I'd love to hear a more elegant solution if you have one. Especially one that doesn't force me to save temporary state information in my document database.
Related
I am working on a project that combines react-datasheet and Material UI Autocomplete to mimic a dropdown inside an Excel cell. The base requirement consists of allowing the user to type in or choose one of the options when the autocomplete gets focus. That is where my problem resides. Because the autocomplete is nested inside a cell, it does not get focus automatically when the parent cell is selected (for example, using the keyboard arrows).
So far, I tried using refs so that I would be able to call .current.focus() using something like the following:
const inputRef = useRef();
useEffect = (() => {
if (selected) {
inputRef.current.focus();
}
}, [selected]);
where selected is a boolean prop that I'm passing from the parent cell to the autocomplete. In other words, I'm trying to somehow make it get focus programatically when selected is true. If you have any tips or ideas to get this to work, or another potential approach I could investigate, I would appreciate it.
EDIT: Here's the component tree as shown in the React Dev Tools. Upon inspecting the autocomplete, it does not expose a focus() method. I see there are inputs down the tree but I am not clear on how I can call focus on them and if that would cause the autocomplete to get focus.
The parent (actually, ancestor) cell component. Here's where I have the selected prop.
The Material UI Autocomplete.
Inputs
#newdev I ran into the same issue and #Dekel's answer here helped solve the mystery: react material ui autocomplete element focus onclick.
TLDR: You need to add the ref to the TextField element in Autocomplete's renderInput prop.
I am trying to make component, Highlight, that can:
know what the user selected / highlighted (mousedown and drag over text)
show the highlighted text (color the aforementioned selected text in a color)
can handle multiple instances of this.
see images for idea of what I am aiming for:
From google, StackExchange, Medium posts, etc I have a component which can figure out the text the user highlighted:
codesandbox
This is mostly taken from this medium post by freecodecamp, which uses a slot based approach.
For the actually rendering of highlighted text, most examples I have found do so via regex and replacing the original text with a span or something (e.g. vue-text-highlight).
These two methods are naturally at odd. It might work for highlighting once, but if the original text is altered to now contain a span element, then subsequent or re-selection won't work.
So I was wondering if anyone had any ideas to surmount this.
In the above images, I show what I will be using this highlight component for (linking two different textual instances), but for the moment I think the images clarify what I mean by selecting and highlighting text.
Was pretty fun - and this isn't a great attempt, but a quick one to get you going.
https://codesandbox.io/s/zw2179y2yl
It supports the following:
Highlighting multiple bits of text independently with (relatively) random colours.
Support for selecting text in the right panel (then click on the corresponding already highlighted text in the left box) and it will inherit the same colour.
Pretty easy to extend to set some ids on the arrays, and then hold references between the right and left boxes.
How to use:
Action: Highlight some text on the left panel
Result: You will see the highlighted text under the panels
Action: Highlight some text on the right panel
Result: It will show up with a grey background in the right panel
Action: Click on one of the highlighted text rows under the panels
Result: The text you previously highlighted on the right will inherit the colour of the selected text you clicked.
Edit:
Just noticed a couple of bugs:
If you highlight a space, it totally screws up.
The highlighting is greedy, so if you highlight "it", it will highlight all instances of "it" whether they are in words or not with the same colour throughout the whole text.
I kinda did the same thing (about grabbing user's selection) in my single file component vue-selection-share when imitating medium's selection dropdown menu. This is what I did to grab the user's selection:
const selection = window.getSelection()
and then, I used startNode and endNode to grab the elements that the selection starts and ends in:
const startNode = selectionRange.startContainer.parentNode
const endNode = selectionRange.endContainer.parentNode
after I test whether the selection is valid, I transform it to a string
this.selectedText = selection.toString()
whereafter creating a handleAction method using this.selectedText.
the random color-generation part can be solved simply by this below:
data: {
myColour: '#'+(Math.random()*0xFFFFFF<<0).toString(16)
},
mounted() {
document.body.style.background = this.myColour;
},
methods: {
generator: function(){
// give the selected text an ID before doing this
let highlightedText = document.getElementById(highlightedText)
this.myColour = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
document.highlightedText.style.background = this.myColour;
}
}
you can, if I recall correctly, find more and getElementById and colour changing here on mozilla.
I did not have the time to check the other answer but do follow his when you think his solution is better as my answer is a quick one written on my phone. during lunch.
I want to make a very simple mix and match system, where the user chooses items from a select drop down menu which triggers things. I have buttons that are appended to the document in a rather off the cuff manner, that is to say, whenever the user chooses something from the select some text will appear as well as a button to remove that text (and corresponding button). I'm using D3 to manipulate selections, add classes and append things. I use classes to tell the button which text to remove. All that being said, I believe this still could simply be a native javascript problem I'm running into. The problem is as follows:
After you choose some things from the select drop down menu, and then proceed to click the x buttons in the order bottom to top, the behavior is as desired. However, if you click a button at the top or in the middle, the button will not remove the right text. I believe that is because the button is simply removing whatever the latest string value of the dynamic class I'm using. That makes me doubt that the button actually retains the initial properties of its .on('click', function() {}) (hence the post title).
If that's the case, I'm not really sure how to circumvent such an issue, as the buttons are dynamic in nature.
Very short and simple example here.
No need to retain memory kind of thing just make sure your element is accessible one such scenario would be to save the id reference of element as class of another element like this
d3.select('body').append('button')
.text('X')
.attr('id','b'+(intCount+1))
.attr('class',choice+'1') //class is the id of the text element
.on('click', function(d,i) {
var t = d3.select(this).attr('id')
var c = d3.select(this).attr('class')
var thisChoice = choice;
d3.selectAll('.' + t).remove(); //remove this element
d3.selectAll('.'+ c).remove(); //remove text element
intCount -= 1;
count -= .7;
});
working FIDDLE
I was creating a Dropdown component for React. Inside the dropdown, I have a form of radio group buttons.
<DropdownButton />
<DropdownForm />
In the DropdownButton, I have an state to know if it is open or not. Depends on that, DropdownForm it's hidden or not (using display: none).
The use case is: User selects a radio button, click apply and something happen. However, if user selects some radio button, and mouse out the dropdown (without clicking the apply button), the one that is selected should be the one that I get from the store.
Something like:
render: function () {
...
if(store.getSomeParam() != this.state.someParam && !this.props.isOpen){
someParam = store.getSomeParam()
}
Then the radio buttons are like:
<input checked={someParam == "something"} ... />
It doesn't really work. It re-renders but it doesn't change the button that is checked. I also tried with refs:
this.refs.myInput.getDOMNode().checked = true
But still nothing. Is this a correct behaviour?
The only solution I found so far is not using a css hiding class (display: none). So what I do is that the DropdownButton renders the DropdownForm depending on if it's open or not (so if you close it, you are forcing DropdownForm to unmount). Then when opening again, it is taking the values from the store (getInitialState) and it shows the correct radio button selected. But, I am not sure if this is the best solution and if there is any drawback in unmounting the component instead of just css hiding it.
This probably has nothing to do with React at all.
Most browsers don't validate the value of the checked attribute, but merely if it is there or not: http://jsfiddle.net/7jzm7gvw/
Just set the checked attribute to either true or null:
<input checked={someParam == "something" ? true: null} ... />
TL;DR: You must use the componentDidMount lifecycle method, not render, to work with the rendered dom nodes directly.
I was struggling with this as well, and after doing some online research I figured I might as well look into it for myself. Here's what I came up with:
Use the componentDidMount lifecycle method and update whatever you need to in there. Here's a Pen I used to prototype this, and I think it looks okay: http://codepen.io/gholts/pen/GpWzdb
You could drop this in pretty easily to what your'e working on by just putting a componentDidMount method on your object and doing it there. I used document.getElementById but you could definitely use jQuery or whatever else you wanted in there, as once the component has mounted it's available to DOM selectors.
I'm using this now to update 20 radio button groups (so it has to check a prop for three different states and update accordingly) and it loads instantly.
Hope it helps! I used the ES6 class syntax in my Pen, you should check it out if you have some time to refactor :) It's fun.
EDIT: So I figured it out, I'm a dummy. You don't need to do the whole document.getElementById business that I was doing. Just use your this.refs.whichever.getDOMNode().checked = true and it'll work, so long as you do it in componentDidMount. It works there because there is an actual DOM element on the page at that point.
Let me get straight to the point :). In my project I'm rendering an template, with jquery-tmpl, like this:
box = $.tmpl('<div> [....] <button></button> [....] </div>')
If I insert box in the DOM, nice JQuery buttons show up. According to the DOM (inspected with Chrome) the buttons have already been converted to jquery-ui buttons.
The question: I want to modify these buttons, but - after trying for two hours - I can't figure out how to. I figured
$('button', box).button({'icons' : {'primary' : 'icon name'}})
for example, would do the trick, but it doesn't. How do I modify my buttons?
jQueryUI generally follows a pattern of updating widgets after they've been initialized on DOM elements:
$("#foo").button("option", "optionname", value);
So to update the button's icon after init, you'd do this:
$("button", box).button("option", "icons", {primary:'icon-name'});