I have a React component with parsed text:
The html structure is like:
<div>
<span>It's a </span>
<span className="highlight-text">cat</span>
</div>
How I can have a event listener which enable I pass all selected text within this div? For example, if I select "a ca", the event listener can get e.target.value = "a ca".
It is possible the highlight part will be repeating within the full text, for example:
<div>
<span>It's a slim cat, not a fat </span>
<span className="highlight-text">cat</span>
</div>
In this case, the selection listener will get 2nd part string, start position of whole text.
I got one answer myself, in order to get the selected text, I can just use window.getSelection().
But not sure if there's a risk
This is the first thing that comes to mind.
// Example impl
<Component text="This is a cat" highlight="is a" />
// Component render
render() {
const {
text,
highlight,
} = this.props;
// Returns the start index for your high light string
// in our example 'is a' starts at index 5
const startHighlightTextIdx = text.search(highlight);
// Create a substring for the first part of the string
// ie: 'This is '
const startText = text.substring(0, startHighlightTextIdx);
// Create a substring for the last part of the string
// For this substr we want to start where our startHighlightTextIdx starts
// and we want to add the length of the highlighted string
// in order to ignore the highlighted string
// ie: start at index 5 + 4 (highlight text length) so substr(9) to end
const endText = text.substring(
startHighlightTextIdx + highlight.length,
);
<div>
<span>{startText}</span>
<span className="highlight-text">{highlight}</span>
<span>{endText}</span>
</div>
}
v2:
// This might be a cleaner solution
// explode by the hightlight string and grab index 0 and last
// this outputs ["This ", " cat"]
const textParts = text.split(highlight);
const startText = textParts[0];
const endText = textParts[1];
Related
I am making a game with react, it's a memory game where you match images.so what I want to archive is, when I click on the div it selects the child of the div(the image) and then add class to the parent and then add class to the child itself. Based on the game logic I have to select the the child element first then if pass some conditions I then add a class to it and it's parent element. look at the code I currently have but it's not working, please help me out.
`
let currentElement;
const imageclick=(e)=>{
//select current image
currentElement=e.target.firstElementChild;
// some game logic the add class to child and parent
//add class to parent
currentElement.closest("div").classList.add("show");
//add class to child
currentElement.classList.remove("hidden")
}
const app=()=>{
return(
<div className="imgDiv transition" key={i} onClick={imageclick}>
<img src={img} alt="" className="tileImage hidden" />
</div>
)
}
`
There are multiple approaches.
One way is to store your images in an array of objects. This array is used to render all of your images. You could even shuffle the array to make the order random.
Inside your component you have a state. This state tracks the index of the currently selected image of the array. The initial state can be null to indicate that there is no current selected image.
While looping over your images, check for each image if the selectedImageIndex is equal to the index of the current image. If so, pass some extra classes.
(You don't need to toggle hidden class on the image. Use the show class on the div to either show or hide the child image).
Pass the index of the current image to the imageClick function in the onClick handler of the div. Whenever we click an image, we want to set the index of the image as our selectedImageIndex.
The component will now rerender and add the class to the clicked div.
Edit
I've modified the answer according to your comment. This example allows for 2 indexes to be stored into the state that tracks the selected images.
Whenever you click the same, the image is deselected. Whenever you click another image it will add its index to the state.
In the useEffect hook you can asses if the images corresponding to the indexes have a similar src or other property that matches.
(I would recommend creating a more robust system in which YOU say which images are the same instead of depending on the URL to be the same. E.g. two images can be the same while having different URL's)
const images = [
{
id: 'A',
src: 'your-image.jpg',
alt: 'Something about your image'
},
{
id: 'B'
src: 'your-other-image.jpg',
alt: 'Something about your image'
},
{
id: 'A' // The match to the other A
src: 'the-other-image-that-matches-the-first.jpg',
alt: 'Something about your image'
}
];
const App = () => {
const [selectedImageIndexes, setSelectedImageIndexes] = useState([null, null]);
const imageClick = index => {
setSelectedImageIndex(currentIndexes => {
// If the same index is clicked, deselect all.
if (currentIndexes.includes(index)) {
return [null, null];
}
// If no indexes have been set.
if (currentIndexes.every(index => index === null)) {
return [index, null];
}
// Set the second clicked image.
return [currentIndexes[0], index];
});
};
useEffect(() => {
// If both indexes are set.
if (selectedImageIndexes.every(index => index !== null) {
/**
* With the indexes in the selectedImageIndexes state,
* check if the images corresponding to the indexes are matches.
*/
if (selectedImageIndexes[0].id === selectedImageIndexes[1].id) {
// Match!
}
}
}, [selectedImageIndexes]);
return (
<div className="images">
{images.map((image, index) => {
const className = selectedImageIndex.includes(index) ? 'show' : '';
return (
<div className="imgDiv transition" key={`img-div-${index}`} onClick={() => imageClick(index)}>
<img src={image.src} className="tileImage" alt={image.alt} />
</div>
);
})}
</div>
)
};
I am new to ReactJS and building an app that highlights to the user whether a letter in a string is a consonant or a vowel by changing the letter's colour and adding a small 'c' or 'v' beneath the relevant letter.
I am struggling with implementing this and wondering how I add css styling and the 'c' or 'v' to a particular letter (grapheme) as the user types depending on whether it is a consonant or vowel.
Any advice would be very welcome, thanks!
Here is what I have so far:
const TextArea = () => {
const registerKeyPresses = (e) => {
let consonants = [
"b",
"d",
[...]//consonant list
];
let grapheme = e.key;
for (let i = 0; i < consonants.length; i++) {
if (grapheme === consonants[i]) {
console.log("consonant");
}
}
};
return (
<form className="textinputframe">
<div className="textinputframe">
<textarea
className="textinput"
type="text"
onKeyDown={registerKeyPresses}
/>
</div>
</form>
);
};
export default TextArea;
I think what you're trying to build is a text-rich based editor, something like what draft.js can achieve (note that this library is a HUGE one)
What you're trying to do can't be achieved with a normal text area. I can see 2 options:
Use a library like draft.js to achieve what you want.
"Fake" the display, so you'll have an element that where it tracks your key input (mimics a text area/input field), then style it on display.
So you can have a textarea like one above, but style it so the output doesn't show color: rgba and make the alpha 0 or any style that suits your need. Then on top of that is a div that display whatever you typed and you can style it line by line or even by letters.
I'm trying to create a text editor with tiptap using reactjs. I would like to create a button next to each "block" of the editor (paragraph block, blockquote block, codeblock block, ...) that allows the user to add a new empty block just before the selected block. It would look like this (Notion editor) :
So the way I tried to do this is to set the cursor's position at the end of the current node :
src/components/blocks/Paragraph.js
const Paragraph = (props) => {
// ...
return {
// ...
<button onMouseDown={() => {
// ...
// props.getPos() gives the position at the beginning of the block
// props.node.nodeSize gives the "length" of the node
const endPos = props.getPos() + props.node.nodeSize;
props.editor.commands.focus(endPos);
// ...
}}>
Add block below
</button>
}
}
So at this point, it works. But then, when I try to insert a new node at this position...
// It will insert a paragraph node containing text "New block added"
props.editor.commands.insertContent({
"type":"paragraph",
"content":[
{
"type":"text",
"text":"New block added"
}
]
})
... I get an error : TypeError: Cannot read property 'nodeType' of null
So, to let you see this error in details, I've made a sandbox on codesandbox.io. To reproduce the error, you just have to focus on the editor, write something random and then click on + button. You should see the error.
Thanks in advance for your help !
Current solution
Currently the best solution I've found :
const endPos = props.getPos() + props.node.nodeSize
// I focus the start of the editor because
// when the cursor is at the end of the node below which
// we want to add a block, it doesn't focus the next block
props.editor.commands.focus('start')
props.editor
.chain()
.insertContentAt(endPos, {type: "paragraph"})
.focus(endPos)
.run()
I am currently creating a search box in VueJS.
Now my question is how to make letters that are searched bold in the results.
This is the part of code I have now:
<div v-for="result in searchResults.merk" :key="result.uid">
<inertia-link :href="result.url">
<strong v-if="result.name.toLowerCase().includes(searchInput.toLowerCase())">{{result.name}}</strong>
<span v-else>{{result.name}}</span>
</inertia-link>
</div>
This sort of does what I want, but not really, now it makes the whole word bold if it contains these letters. I would like to only give those specific letters bold, and the rest of the word normal styling.
See example below. It contains 2 solutions
Very simple but with the BIG warning as it is using v-html. v-html should not be used with user input for safety reasons (see the link). Luckily in this case if user enters some html into search box, it will be rendered only if it is also contained in searched text. So if searched text (source in example) is safe (produced by trusted source), it is perfectly save
Second solution is little bit involved - simply split the text into multiple segments, mark segments which should be highlighted and then render it using v-for and v-if. Advantage of this solution is you can also render Vue components (for example Chips) and use other Vue features (wanna bind a click handler on highlighted text?) which is not possible with v-html solution above...
const vm = new Vue({
el: '#app',
data() {
return {
source: 'foo bar baz baba',
search: 'ba',
}
},
computed: {
formatedHTML() {
const regexp = new RegExp(this.search, "ig")
const highlights = this.source.replace(regexp, '<strong>$&</strong>')
return `<span>${highlights}</span>`
},
highlights() {
const results = []
if (this.search && this.search.length > 0) {
const regexp = new RegExp(this.search, "ig")
let start = 0
for (let match of this.source.matchAll(regexp)) {
results.push({
text: this.source.substring(start, match.index),
match: false
})
start = match.index
results.push({
text: this.source.substr(start, this.search.length),
match: true
})
start += this.search.length
}
if(start < this.source.length)
results.push({ text: this.source.substring(start), match: false})
}
if (results.length === 0) {
results.push({
text: this.source,
match: false
})
}
return results
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="search" />
<div>As HTML:
<span v-html="formatedHTML"></span>
</div>
<div>Safe:
<span>
<template v-for="result in highlights">
<template v-if="result.match"><strong>{{result.text}}</strong></template>
<template v-else>{{result.text}}</template>
</template>
</span>
</div>
</div>
You can try splitting the word into a collection of in-line spans.
So you'd first iterate over the results. Then you'd iterate over each letter of the result.name, putting each individual letter in its own span.
Lastly, you'd toggle a bold class on the spam using the object class syntax.
Consider the following example:
<span
v-for="letter in result.name.toLowerCase()"
:key="letter"
:class={ isBold: searchInput.toLowerCase().includes(letter) }
>
{{letter}}
</span>
The object syntax for classes will toggle the class on or off if the condition to the right of the class name is true.
This isn't an ideal solution as it contains a reasonable amount of computational complexity (2x lowercase, and searching the search string for each letter). However, the use case is very simple so modern devices shouldn't struggle.
If you find it worth optimising, you can debounce the input and generate a map of the different letters lowercase search letters then search that map as map access is an O(1) operation.
Really simple problem where when I hover over the navigation bar of my page (in a unordered list), the tab is supposed to update with a new value. The value updates, but the attributing HTML markup does not render along with it (such as when I tried to make the 'why' value bold. I tried even storing both of the values in two variables and concat together, but still no luck. Any help would be appreciated.
Screenshot of Nav Bar (Vertical):
My Default Props:
getDefaultProps: function(){
var text = '<i className="fa fa-laptop"></i>';
var text2 = '<b>why<b>';
return{why: text + text2};
},
Line needing to be changed:
<li value = '3' className = "nav-content" style = {ulTop} onMouseEnter = {this.change} onMouseLeave = {this.out} ref = 'whyTab'><Link to = '/Why'><i className="fa fa-code"></i>{this.props.why}</Link></li>