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...
Related
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
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.
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.
I am building my first react site, using gatsby with prismic.io as the CMS for my news section.
Within prismic I am using slices for quotes and featured images in each of the news stories and am looking to try and pull this data into my page, however I am unsure how to target the specific fragment names that I have created within the relevant const that has been set up for each.
GraphQL Query
export const query = graphql`
query ($slug:String){
prismicNewsStory (uid:{eq: $slug}) {
data {
body {
__typename
... on PrismicNewsStoryBodyQuote {
primary {
quote {
text
}
}
}
... on PrismicNewsStoryBodyFeaturedImage {
primary {
featured_image {
url
}
}
}
}
}
}
}
`
Targetting consts
const quote = props.data.prismicNewsStory.data.body[0].primary.quote.text
const featured_image = props.data.prismicNewsStory.data.body[1].primary.featured_image.url
As the slices are optional within prismic, I am encountering issues on some of the news stories when a featured_image is added before a quote, making them swap order within the body.
Question
Is there a way within each const to target a particular fragment or is there a better way for me to do this?
//get the array
const body = props.data.prismicNewsStory.data.body;
const {feature_image : fi0, quote: q0} = body[0].primary;
// above line is equivalent to:
// const fi0 = body[0].primary.feature_image;
// const q0 = body[0].primary.quote;
// when order is reversed q0 will be undefined
const {feature_image : fi = fi0, quote : q = q0} = body[1].primary;
// above line is equivalent to:
// const fi = body[1].primary.feature_image || fi0;
// const q = body[1].primary.quote || q0;
// when order is reversed fi0 will be assigned to fi
const feature_image = fi.url;
const quote = q.text
or use a reduce
const reduceStory = (acc, item) => ({
feature_image: acc.feature_image|| item.primary.feature_image,
quote: acc.quote || item.primary.quote
})
const story = props.data.prismicNewsStory.data.body.reduce(reduceStory, {});
const feature_image = story.feature_image.url;
const quote = story.quote.text
>
After looking and learning a bit more learning with #paul-mcbride we came up with the following solution to target any __typename.
const body = props.data.prismicNewsStory.data.body.reduce((object, item) => ({
...object,
[item.__typename]: item.primary
}), {});
You can now use the targeted slice name.
<FeaturedImage src={body.PrismicNewsStoryBodyFeaturedImage.featured_image.url} />
or
<QuoteText>{body.PrismicNewsStoryBodyQuote.quote.text}</QuoteText>
I need to search a string, and if it has any values that match my array, I need to add <span></span> tags to them to add custom CSS. I am using reactJS.
How do I search the string for objects from my array?
Example:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'},...]
findAllOccurrances = () => {???}
Then systematically replace them '{{inputX}}' with <span className='bizarre-highlight'>{{inputX}}</span>
My intent is to add custom CSS to any text in the div which matches my array, so if you got any ideas please shoot! Again, using reactJS if that helps.
I created a component that will replace the elements that need to be highlighted with a span you can test it here
The component is:
import React from 'react';
export default ({ terms, children }) => {
const result = []
const regex = terms.map(escapeRegExp).join('|');
const re = new RegExp(regex);
let text = (' ' + children).slice(1); // copy
let match = re.exec(text);
while (match != null) {
const str = match.toString();
result.push(text.slice(0, match.index));
result.push(<span className="highlighted">{str}</span>);
text = text.slice(match.index + str.length);
match = re.exec(text);
}
result.push(text);
return result;
}
function escapeRegExp (str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
And you should use it like this:
import React from 'react';
import Highlighter from './Highlighter';
const terms = [ '{{input1}}', '{{input2}}' ]
const App = () => (
<div>
<Highlighter terms={terms}>
{'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}'}
</Highlighter>
</div>
);
Use String#replace with a RegExp to find all instances of '{{inputX}}', and wrap the matches with the span:
const string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input3}}'
const array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}]
const pattern = new RegExp(array.map(({ parameter }) => parameter).join('|'), 'g');
const result = string.replace(pattern, (match) =>
`<span className='bizarre-highlight'>${match}</span>`
)
console.log(result)
use Array#map to extract values for wrapping in <span> and then cycle on them for replacement:
let string = 'this is a message with many inputs, {{input1}}, {{input2}}, and again {{input1}}';
let array = [{parameter: '{{input1}}'},{parameter: '{{input2}}'}];
array.map(el => { return el.parameter }).forEach(str => {
string = string.split(str).join("<span className=\'bizarre-highlight\'>" + str + "</span>");
});
console.log(string);