How to dynamically change text of ContentEditable on React? - javascript

I have string ^aUsername ^bLastName ^cAge. And I need create one editable element where I can edit that string. But the biggest problem of this input is that this element should color every single letter after marc ^. So it's should look like this
I'm currently using library react-contenteditable to manage that in React.
function getParsedValue(value) {
let vals = value.split('^').slice(1);
let text = "";
if (vals) {
vals.map((val, i) => {
text = text + `<span style="color:red; font-weight:bold">${val[0]}</span>${val.slice(1)}`;
})
} else {
text = value;
}
return text;
}
function MarcInput({ value }) {
const [text, setText] = useState(marc);
const handleChanges = (event) => {
setText(event.target.value);
};
return (
<>
<ContentEditable
onChange={handleChanges}
html={getParsedValue(text)}
/>
</>
);
}
But now I can't add more string to this input. How to achieve that every word starting from ^ has color and it gonna work for new words by adding text to input

Related

How to type unique letters only in input text box in html or react?

I want to create an input box that allows to type only a distinct alphabet letter in the input box
( No duplicate alphabet value, ONLY ONE)
I looked up all the attributes for input box but I couldn't find one and there were no example.
Do I have to handle it within JavaScript functions?
(I am using React)
<input
className="App-Contain-Input"
name="containedLetter"
type={"text"}
onChange={containedChange}
/>
Here is how this can be done, note that we have to use onkeydown which fires when a key is being pressed, but not before it is being released (this way we can intercept and prevent the key stroke from being taken into account):
function MyInput() {
const containedChange = (event) => {
if (event.key.length === 1 && event.code.startsWith('Key') && event.code.length === 4 && event.target.value.indexOf(event.key) !== -1) {
event.preventDefault()
return false;
}
return true
}
return (
<input
id="input"
className="App-Contain-Input"
name="containedLetter"
type="text"
onkeydown={containedChange}
/>
}
A way to do it with a cheeky two-liner is to get rid of any non-letters with regex, then let Set do the de-duping by converting to Array => Set => Array => String:
As a side note, many of the other solutions posted here make one or both of two assumptions:
The user is only ever going to edit the last character... but what if they edit from the beginning or middle? Any last character solution will then fail.
The user will never copy/paste a complete value... again, if they do, most of these solutions will fail.
In general, it's best to be completely agnostic as to how the value is going to arrive to the input, and simply deal with the value after it has arrived.
import { useState } from 'react';
export default function Home() {
const [value, setValue] = useState('');
return (
<div>
<input
type='text'
value={value}
onChange={(e) => {
const onlyLetters = e.target.value.replace(/[^a-zA-Z]/g, '');
setValue(Array.from(new Set(onlyLetters.split(''))).join(''));
}}
/>
</div>
);
}
Your containedChange function can be like this:
const containedChange = (e) => {
const value = e.target.value;
const lastChar = value[value.length - 1];
if (value.slice(value.length - 1, value.length).indexOf(lastChar) != -1) {
// show error message
}
The function checks whether the last entered character already exists in the previous value or not.
An isogram is a word with no repeating letters. You can check this using regex
function isIsogram (str) {
return !/(.).*\1/.test(str);
}
Or using array.some
function isIsogram(str) {
var strArray = str.split("");
return !strArray.some(function(el,idx,arr){
return arr.lastIndexOf(el)!=idx;
});
}
In react - you need to use state to reject inputs which contain repeated letters (not an Isogram). Complete example on codesandbox.
import { useState } from 'react';
export default function App() {
const [ text, setText ] = useState('');
function isIsogram(str) {
var strArray = str.split("");
return !strArray.some(function(el,idx,arr){
return arr.lastIndexOf(el)!==idx;
});
}
function handleChange(e) {
const { value } = e.target;
console.log(isIsogram(value));
if (value.length === 1 || isIsogram(value)) {
setText(value);
}
}
return (
<input value={text} onChange={handleChange} />
);
}
Use a state to maintain the current value of the input. Then when the value changes get the last letter of the input, check that it's a letter, and if it is a letter and it isn't already in the state, update the state with the new input value.
const { useState } = React;
function Example() {
const [ input, setInput ] = useState('');
function handleChange(e) {
const { value } = e.target;
const lastChar = value.slice(-1);
const isLetter = /[a-zA-Z]/.test(lastChar);
if (isLetter && !input.includes(lastChar)) {
setInput(value);
}
}
function handleKeyDown(e) {
if (e.code === 'Backspace') {
setInput(input.slice(0, input.length - 1));
}
}
return (
<input
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
One pure js solution would be to handle the input value in onkeyup event where we have access to the latest key as well as updated input target value.
If the latest key pressed is duplicate then we will found it on 2 location in the target value, first on some where middle of the target value and second at the end of the target value. So we can remove the value at the end (which is the duplicate one).
The below code snippet shows the implementation of above in React
const Input = () => {
const handleKeyUp = (e) => {
const { key, target: { value }} = e;
const len = value.length;
if (value.indexOf(key) < len - 2) {
e.target.value = value.slice(0, len - 1);
}
}
return (
<div>
<label>Unique Keywords</label>
<input type="text" placeholder="my-input" onKeyUp={handleKeyUp} />
</div>
);
}

Draft js replaceText remove inlineStyles

I implement WYSIWYG editor with draftjs and I have a set of rules for typography fixing. I use Modifier.replaceText to replace what I want, but the problem is, when I call it, it removes inlineStyles in replaced text.
Here is a block of code that I use for typography. Inputs are rules (array with rules) and editorState.
rules.forEach(({ toReplace, replace }) => {
const blockToReplace = [];
let contentState = editorState.getCurrentContent();
const blockMap = contentState.getBlockMap();
blockMap.forEach(contentBlock => {
const text = contentBlock.getText();
let matchArr;
while ((matchArr = toReplace.exec(text)) !== null) {
const start = matchArr.index;
const end = start + matchArr[0].length;
const blockKey = contentBlock.getKey();
const blockSelection = SelectionState.createEmpty(blockKey).merge({
anchorOffset: start,
focusOffset: end,
});
blockToReplace.push(blockSelection);
}
});
blockToReplace.reverse().forEach((selectionState) => {
contentState = Modifier.replaceText(
contentState,
selectionState,
text.replace(search, replace)
);
});
editorState = EditorState.push(editorState, contentState);
});
So, my input is: *bold...*
The wrong output is: *bold*…
Should be: *bold…*
Note: asterisks are for bold designation, change is three dots to horizontal ellipsis (U+2026)
Anybody any idea? I google it for two days and nothing...

How to replace text with substitute that contains JSX code?

interface ICard {
content: string,
blanks: Array<{word: string, hidden: boolean}>
}
function processCards():Array<any>{
if (cards !==null ){
const text = cards.map((card,cardIndex)=>{
var content = card.content
card.blanks.map((blank,blankIndex)=>{
// replace content
const visibility = (blank.hidden)?'hidden':'visible'
const click_blank = <span className={visibility} onClick={()=>toggleBlank(cardIndex,blankIndex)}>{blank.word}</span>
content = content.replace(blank.word,click_blank)
})
return content
})
return text
} else {
return []
}
}
I have an array of objects of type ICard.
Whenever card.blanks.word appears in card.content, I want to wrap that word in tags that contain a className style AND an onClick parameter.
It seems like I can't just replace the string using content.replace like I've tried, as replace() does not like the fact I have JSX in the code.
Is there another way to approach this problem?
You need to construct a new ReactElement from the parts of string preceding and following each blank.word, with the new span stuck in the middle. You can do this by iteratively building an array and then returning it wrapped in <> (<React.Fragment>). Here's a (javascript) example:
export default function App() {
const toggleBlankPlaceholder = (cardIndex, blankIndex) => {};
const cardIndexPlaceholder = 0;
const blanks = [
{ word: "foo", hidden: true },
{ word: "bar", hidden: false },
];
const content = "hello foo from bar!";
const res = [content];
for (const [blankIndex, { word, hidden }] of blanks.entries()) {
const re = new RegExp(`(.*?)${word}(.*)`);
const match = res[res.length - 1].match(re);
if (match) {
const [, prefix, suffix] = match;
res[res.length - 1] = prefix;
const visibility = hidden ? "hidden" : "visible";
res.push(
<span
className={visibility}
onClick={() =>
toggleBlankPlaceholder(cardIndexPlaceholder, blankIndex)
}
>
{word}
</span>
);
res.push(suffix);
}
}
return <>{res}</>;
}
The returned value will be hello <span class="hidden">foo</span> from <span class="visible">bar</span>!
A couple of things:
In your example, you used map over card.blanks without consuming the value. Please don't do that! If you don't intend to use the new array that map creates, use forEach instead.
In my example, I assumed for simplicity that each entry in blanks occurs 0 or 1 times in order in content. Your usage of replace in your example code would only have replaced the first occurrence of blank.word (see the docs), though I'm not sure that's what you intended. Your code did not make an ordering assumption, so you'll need to rework my example code a little depending on the desired behavior.

What are reliable approaches for finding words/text within an HTML-markup's text-content and replacing the matches with highlighting markup?

I have some text. And I have a function that receives a word or phrase and I have to return the same text but with a span with a class around this keyword or phrase.
Example:
If I have this
text = https://www.website.com
I want
text = https://www.<span class="bold">website</span>.com
but what I'm getting is
text = website </span>.com&context=post" target="_blank" rel="noopener noreferrer">https://www.<span class="bold"> website </span>.com
What I'm doing is
...
const escapedPhrases = ["\\bwebsite\\b"]
const regex = new RegExp(`(${escapedPhrases.join('|')})`, 'gi');
text = text.replace(
regex,
'<span class="bold"> $1 </span>'
);
How can I improve my regex?
Also I have tried to "clean" the text after the replacement of <span class="bold"> $1 </span> to try of remove it if it's inside the href but with no success.
UPDATE for clarification:
I have this text:
text = `Follow me on
https://www.twitter.com
Thanks!`
Example 1:
I want to highlight the word twitter:
For this I want to add a span with class bold for example around twitter:
text = `Follow me on
https://www.<span class="bold">twitter</span>.com
Thanks!`
Example 2:
I want to highlight the word twitter.com:
For this I want to add a span with class bold for example around twitter.com:
text = `Follow me on
https://www.<span class="bold">twitter.com</span>
Thanks!`
Example 3:
I want to highlight the word https://twitter.com/:
For this I want to add a span with class bold for example around https://twitter.com/:
text = `Follow me on
<span class="bold">https://www.twitter.com</span>
Thanks!`
Example 4:
I have this text and want to highlight twitter:
text = `Follow me on
https://www.twitter.com
Thanks for follow my twitter!`
Then I have to return
text = `Follow me on
https://www.<span class="bold">twitter</span>.com
Thanks for follow my <span class="bold">twitter</span>!`
Regex is not a solution to everything, in that case, to only modifying the textContent and not the attribute maybe this following code will fit your needs:
let text = `Follow me on
https://www.twitter.com
Thanks for follow my twitter!`;
const replaceKeyword = (keyword, text) => {
let template = document.createElement('template');
template.innerHTML = text;
let children = template.content.childNodes;
let str = '';
let substitute = `<span style='color:red;font-weight:bold;'>${keyword}</span>`;
for (let child of children){
if (child.nodeType === 3){
// #text
str += child.textContent.replace(keyword, substitute);
} else if (child.nodeType === 1) {
// element
let nodeStr = child.textContent.replace(keyword, substitute);
child.innerHTML = nodeStr;
str += child.outerHTML;
}
}
return str;
}
let result = replaceKeyword('twitter', text);
console.log(result);
document.body.innerHTML = result;
With the latest features which got added to the requirements, the OP entirely changed the game. One now is talking about a full-text-search within the text-contents of html-markup.
Something similar to ...
How to highlight the search-result of a text-query within an html document ignoring the html tags?
Markdown-like functionality for tooltips ... or ... How to query text-nodes from DOM, find markdown-patterns, replace matches with HTML-markup and replace the original text-node with the new content?
What is a good enough approach for writing real-time text search and highlight functionality which does not break the order of text- and element-nodes
... with the last two one providing different but generic DOM-node/text-node based approaches.
As for the OP's problem. With requirements like finding a text-query within the text-content of html-code, one can not stick to a simple solution. One now has to assume nested markup.
Providing/adding a special markup around each search result has to start with firstly collecting every single text-node from the very DOM-fragment which had to be parsed before from the passed html-code.
Having such a base, one can not anymore just fire around with a regex based String.replace. One now has to replace/reassamble each text-node that partially matches the search-query with the text-contents which did not match and the part that now changes into an element-node due to the additional markup which gets wrapped around the matching text.
Thus just from the OP's last requirement change, one has to provide a generic full text search and highlight approach which of cause in addition has to take into account and to sanitize/handle white-space sequences and regex-specific characters within the provided search query ...
// node detection helpers.
function isElementNode(node) {
return (node && (node.nodeType === 1));
}
function isNonEmptyTextNode(node) {
return (
node
&& (node.nodeType === 3)
&& (node.nodeValue.trim() !== '')
&& (node.parentNode.tagName.toLowerCase() !== 'script')
);
}
// dom node render helper.
function insertNodeAfter(node, referenceNode) {
const { parentNode, nextSibling } = referenceNode;
if (nextSibling !== null) {
node = parentNode.insertBefore(node, nextSibling);
} else {
node = parentNode.appendChild(node);
}
return node;
}
// text node reducer functionality.
function collectNonEmptyTextNode(list, node) {
if (isNonEmptyTextNode(node)) {
list.push(node);
}
return list;
}
function collectTextNodeList(list, elmNode) {
return Array.from(
elmNode.childNodes
).reduce(
collectNonEmptyTextNode,
list
);
}
function getTextNodeList(rootNode) {
rootNode = (isElementNode(rootNode) && rootNode) || document.body;
const elementNodeList = Array.from(
rootNode.getElementsByTagName('*')
);
elementNodeList.unshift(rootNode);
return elementNodeList.reduce(collectTextNodeList, []);
}
// search result emphasizing functinality.
function createSearchMatch(text) {
const elmMatch = document.createElement('strong');
// elmMatch.classList.add("bold");
elmMatch.textContent = text;
return elmMatch;
}
function aggregateSearchResult(collector, text, idx) {
const { previousNode, regXSearch } = collector;
const currentNode = regXSearch.test(text)
? createSearchMatch(text)
: document.createTextNode(text);
if (idx === 0) {
previousNode.parentNode.replaceChild(currentNode, previousNode);
} else {
insertNodeAfter(currentNode, previousNode);
}
collector.previousNode = currentNode;
return collector;
}
function emphasizeTextContentMatch(textNode, regXSearch) {
// console.log(regXSearch);
textNode.textContent
.split(regXSearch)
.filter(text => text !== '')
.reduce(aggregateSearchResult, {
previousNode: textNode,
regXSearch,
})
}
function emphasizeEveryTextContentMatch(htmlCode, searchValue, isIgnoreCase) {
searchValue = searchValue.trim();
if (searchValue !== '') {
const replacementNode = document.createElement('div');
replacementNode.innerHTML = htmlCode;
const regXSearchString = searchValue
// escaping of regex specific characters.
.replace((/[.*+?^${}()|[\]\\]/g), '\\$&')
// additional escaping of whitespace (sequences).
.replace((/\s+/g), '\\s+');
const regXFlags = `g${ !!isIgnoreCase ? 'i' : '' }`;
const regXSearch = RegExp(`(${ regXSearchString })`, regXFlags);
getTextNodeList(replacementNode).forEach(textNode =>
emphasizeTextContentMatch(textNode, regXSearch)
);
htmlCode = replacementNode.innerHTML
}
return htmlCode;
}
const htmlLinkList = [
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks!',
'twitter'
),
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks!',
'twitter.com'
),
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks!',
'https://www.twitter.com/'
),
emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks for follow my Twitter!',
'TWITTER',
true
),
emphasizeEveryTextContentMatch(
`Follow me on https://www.twitter.com/
Thanks
for follow
my Twitter!`,
'follow my twitter',
true
),
];
document.body.innerHTML = htmlLinkList.join('<br/>');
const container = document.createElement('code');
container.textContent = emphasizeEveryTextContentMatch(
'Follow me on https://www.twitter.com/ Thanks for follow my Twitter!',
'TWITTER',
true
);
document.body.appendChild(container.cloneNode(true));
container.textContent = emphasizeEveryTextContentMatch(
`Follow me on https://www.twitter.com/
Thanks
for follow
my Twitter!`,
'follow my twitter',
true
);
document.body.appendChild(container.cloneNode(true));
code {
display: block;
margin: 10px 0;
padding: 0
}
a strong {
font-weight: bold;
}
.as-console-wrapper { min-height: 100%!important; top: 0; }

How to get object properties on element click?

I am building a weather app for practice. I get to that point that I have to make an autocomplete input field with data from JSON object. When someone makes an input, it displays the matched data, but on click I want to get two properties from the object. I need to get the longitude and latitude properties from JSON object to make an API request to return the object with the weather data. The content displays properly but I can't make that onClick event listener work. I tried very different things and failed, either was a scope problem or something else. It is one of my first projects and I am in a downfall right now. Please help me. :)
P.S. You can find it on this link: https://objective-edison-1d6da6.netlify.com/
// Testing
const search = document.querySelector('#search');
const matchList = document.querySelector('#match-list');
let states;
// Get states
const getStates = async () => {
const res = await fetch('../data/bg.json');
states = await res.json();
};
// Filter states
const searchStates = searchText => {
// Get matches to current text input
let matches = states.filter(state => {
const regex = new RegExp(`^${searchText}`, 'gi');
return state.city.match(regex);
});
// Clear when input or matches are empty
if (searchText.length === 0) {
matches = [];
matchList.innerHTML = '';
}
outputHtml(matches);
};
// Show results in HTML
const outputHtml = matches => {
if (matches.length > 0) {
const html = matches
.map(
match => `<div class="card match card-body mb-1">
<h4>${match.city}
<span class="text-primary">${match.country}</span></h4>
<small>Lat: ${match.lat} / Long: ${match.lng}</small>
</div>`
)
.join('');
matchList.innerHTML = html;
document.querySelector('.match').addEventListener('click', function() {});
//Wconsole.log(matches);
//let test = document.querySelectorAll('#match-list .card');
//const values = matches.values(city);
}
};
window.addEventListener('DOMContentLoaded', getStates);
search.addEventListener('input', () => searchStates(search.value));
If I understand correctly, you're trying to access the lat and lng values of the clicked match, if that is the case, here is one way of doing it:
const outputHtml = matches => {
if (matches.length > 0) {
const html = matches
.map(
match => `<div class="card match card-body mb-1" data-lat="`${match.lat}" data-lng="`${match.lng}">
<h4>${match.city}
<span class="text-primary">${match.country}</span></h4>
<small>Lat: ${match.lat} / Long: ${match.lng}</small>
</div>`
)
.join('');
matchList.innerHTML = html;
document.querySelectorAll('.match').forEach(matchElm => {
matchElm.addEventListener('click', function(event) {
const { currentTarget } = event;
const { lat, lng } = currentTarget.dataset;
});
});
}
};
I've used the data-lat and data-lng attributes to store the required values in the element's dataset and I've used document.querySelectorAll('.match') to get all the elements that have the class match not just the first one.

Categories