Draft.js. How to get all entities data from the ContentState - javascript

From official docs I know about 2 methods: get entity by its key and get last created entity. In my case, I also need a method to access all entities from current ContentState.
Is there any method that could perform this? If not, is there a one that can provide all entities keys?

const getEntities = (editorState, entityType = null) => {
const content = editorState.getCurrentContent();
const entities = [];
content.getBlocksAsArray().forEach((block) => {
let selectedEntity = null;
block.findEntityRanges(
(character) => {
if (character.getEntity() !== null) {
const entity = content.getEntity(character.getEntity());
if (!entityType || (entityType && entity.getType() === entityType)) {
selectedEntity = {
entityKey: character.getEntity(),
blockKey: block.getKey(),
entity: content.getEntity(character.getEntity()),
};
return true;
}
}
return false;
},
(start, end) => {
entities.push({...selectedEntity, start, end});
});
});
return entities;
};

How I get the all entities keys:
const contentState = editorState.getCurrentContent()
const entityKeys = Object.keys(convertToRaw(contentState).entityMap)
result:
[0, 1]
then you can call the getEntity(key) method to get the responding entity.
this is how convertToRaw(contentState) looks:

Bao, You will find it inside key called 'blocks'.
convertToRaw(contentState).blocks.map(el=>el.text)
It will give you an array of raw text.

Unfortunatelly your suggested way using convertToRaw doesnt work because it reindexes all keys to ["0", .., "n"], but the real keys differ when you act with the editor. New ones > n will be added and unused will be omitted.

const rawState = convertToRaw(contentState)
const { entityMap } = rawState;
This entityMap will have list of all entities. But this is an expensive conversion. Because, it will convert whole thing to raw. A better way is loop through blocks and check for entity.

You'll have to look at every character:
const { editorState } = this.state; // assumes you store `editorState` on `state`
const contentState = editorState.getCurrentContent();
let entities = [];
contentState.getBlockMap().forEach(block => { // could also use .map() instead
block.findEntityRanges(character => {
const charEntity = character.getEntity();
if (charEntity) { // could be `null`
const contentEntity = contentState.getEntity(charEntity);
entities.push(contentEntity);
}
});
});
Then you could access it via:
entities.forEach((entity, i) => {
if (entity.get('type') === 'ANNOTATION') {
const data = entity.get('data');
// do something
}
})

Related

Problem with sessionStorage: I am not displaying the first item correctly

I am having a problem with sessionStorage; in particular, I want the id of the ads to be saved in the session where the user puts the like on that particular favorite article.
However, I note that the array of objects that is returned contains the ids starting with single quotes, as shown below:
['', '1', '7']
but I want '1' to be shown to me directly.
While if I go into the sessionStorage I notice that like is shown as:
,1,7
ie with the leading comma, but I want it to start with the number directly.
How can I fix this?
function likeAnnunci(){
let likeBtn = document.querySelectorAll('.like');
likeBtn.forEach(btn => {
btn.addEventListener('click', function(){
let id = btn.getAttribute('ann-id');
//sessionStorage.setItem('like', [])
let storage = sessionStorage.getItem('like').split(',');
//console.log(storage);
if(storage.includes(id)){
storage = storage.filter(id_a => id_a != id);
} else {
storage.push(id);
}
sessionStorage.setItem('like', storage)
console.log(sessionStorage.getItem('like').split(','));
btn.classList.toggle('fas');
btn.classList.toggle('far');
btn.classList.toggle('tx-main');
})
})
};
function setLike(id){
if(sessionStorage.getItem('like')){
let storage = sessionStorage.getItem('like').split(',');
if(storage.includes(id.toString())){
return `fas`
} else {
return `far`
}
} else {
sessionStorage.setItem('like', '');
return`far`;
}
}
The main issue you're having is that you're splitting on a , instead of using JSON.parse().
Also, you've got some other code issues and logical errors.
Solution:
function likeAnnunci() {
const likeBtn = document.querySelectorAll('.like');
likeBtn.forEach((btn) => {
btn.addEventListener('click', function () {
let id = btn.getAttribute('ann-id');
//sessionStorage.setItem('like', [])
let storage = JSON.parse(sessionStorage.getItem('like') || '[]');
//console.log(storage);
if (!storage.includes(id)) {
storage.push(id);
}
sessionStorage.setItem('like', JSON.stringify(storage));
console.log(JSON.parse(sessionStorage.getItem('like')));
btn.classList.toggle('fas');
btn.classList.toggle('far');
btn.classList.toggle('tx-main');
});
});
}
More modular and optimal solution:
const likeBtns = document.querySelectorAll('.like');
// If there is no previous array stored, initialize it as an empty array
const initLikesStore = () => {
if (!sessionStorage.getItem('likes')) sessionStorage.setItem('likes', JSON.stringify([]));
};
// Get the item from sessionStorage and parse it into an array
const grabLikesStore = () => JSON.parse(sessionStorage.getItem('likes'));
// Set a new value for the likesStore, automatically serializing the value into a string
const setLikesStore = (array) => sessionStorage.setItem('likes', JSON.stringify(array));
// Pass in a value.
const addToLikesStore = (value) => {
// Grab the current likes state
const pulled = grabStorage();
// If the value is already there, do nothing
if (pulled.includes(value)) return;
// Otherwise, add the value and set the new array
// of the likesStore
storage.push(value);
setLikesStore(pulled);
};
const likeAnnunci = (e) => {
// Grab the ID from the button clicked
const id = e.target.getAttribute('ann-id');
// Pass the ID to be handled by the logic in the
// function above.
addToLikesStore(id);
console.log(grabLikesStore());
btn.classList.toggle('fas');
btn.classList.toggle('far');
btn.classList.toggle('tx-main');
};
// When the dom content loads, initialize the likesStore and
// add all the button event listeners
window.addEventListener('DOMContentLoaded', () => {
initLikesStore();
likeBtns.forEach((btn) => btn.addEventListener('click', likeAnnunci));
});

Async JS validation issues for html textarea

I'm trying to replicate the code in this article:
https://depth-first.com/articles/2020/08/24/smiles-validation-in-the-browser/
What I'm trying to do different is that I'm using a textarea instead of input to take multi-line input. In addition to displaying an error message, I also want to display the entry which doesn't pass the validation.
The original validation script is this:
const path = '/target/wasm32-unknown-unknown/release/smival.wasm';
const read_smiles = instance => {
return smiles => {
const encoder = new TextEncoder();
const encoded = encoder.encode(`${smiles}\0`);
const length = encoded.length;
const pString = instance.exports.alloc(length);
const view = new Uint8Array(
instance.exports.memory.buffer, pString, length
);
view.set(encoded);
return instance.exports.read_smiles(pString);
};
};
const watch = instance => {
const read = read_smiles(instance);
document.querySelector('input').addEventListener('input', e => {
const { target } = e;
if (read(target.value) === 0) {
target.classList.remove('invalid');
} else {
target.classList.add('invalid');
}
});
}
(async () => {
const response = await fetch(path);
const bytes = await response.arrayBuffer();
const wasm = await WebAssembly.instantiate(bytes, { });
watch(wasm.instance);
})();
For working with a textarea, I've changed the watch function to this and added a <p id="indicator"> element to the html to display an error:
const watch = instance => {
const read = read_smiles(instance);
document.querySelector("textarea").addEventListener('input', e => {
const { target } = e;
var lines_array = target.value.split('/n');
var p = document.getElementById("indicator");
p.style.display = "block";
p.innerHTML = "The size of the input is : " + lines_array.length;
if (read(target.value) === 0) {
target.classList.remove('invalid');
} else {
target.classList.add('invalid');
}
});
}
I'm not even able to get a count of entries that fail the validation. I believe this is async js and I'm just a beginner in JavaScript so it's hard to follow what is happening here, especially the part where the function e is referencing itself.
document.querySelector("textarea").addEventListener('input', e => {
const { target } = e;
Can someone please help me in understanding this complicated code and figuring out how to get a count of entries that fail the validation and also printing the string/index of the same for helping the user?
There is a mistake in you code to count entries in the textarea:
var lines_array = target.value.split('\n'); // replace /n with \n
You are asking about the function e is referencing itself:
The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables. You can find more informations Mdn web docs - Destructuring object

How to filter an array by two indvidual strings from an object?

I'm working on a project where I need to filter 13 items by two different select box values, and I'm getting stuck on persisting the filter.
I have two select boxes that I've selected like so:
let pickupLocation = document.querySelector("#pa_location"); //values are 'complete-set', 'neck', 'bridge'.
let pickupType = document.querySelector("#pa_type1"); // Values are 'soapbar', 'dogear', 'short'.
What's Working:
I'm initializing an object like so:
const activeFilters = {};
To populate the values like so:
//Persist the Complete Set / Single
pickupLocation.addEventListener("change", function () {
if (pickupLocation.value === "complete-set") {
activeFilters.location = "set";
} else {
activeFilters.location = "single";
}
});
pickupType.addEventListener("change", function () {
if (pickupType.value === "soapbar") {
activeFilters.type = "soapbar";
} else if (pickupType.value === "dogear") {
activeFilters.type = "dogear";
} else {
activeFilters.type = "short";
}
});
// Returns something like
// {location: single, type: dogear}
I'm trying to filter an array of input elements by their value. I have 13 inputs each with a value containing words like set, single, dogear, soapbar etc.
Where I'm stuck:
I have a filter function that I'm trying to filter the values of these inputs by two values of the activeFilters object:
const performFilter = (covers) => {
let results;
let filteredValues = Object.values(activeFilters);
filteredValues.forEach((value) => {
results = covers.filter((cover) => cover.value.indexOf(value) !== -1);
});
return results;
};
The problem is my function is returning only one of the two words. For instance, if the my activeFilters object is {location: set, type: dogear} the filtered results array contains only one of them. Where am I going wrong?
Edit:
This function returns all inputs that match one of the activeFilters, and I apologize if I wasn't clear above, but I'd like it to match ALL of the Active Filters. Is this possible with the function below?
const performFilter = (covers) => {
let results = []; // initialise the array
let filteredValues = Object.values(activeFilters);
filteredValues.forEach((value) => {
let res = covers.filter((cover) => cover.value.indexOf(value) !== -1);
results.push(...res);
});
console.log(results);
};
CODEPEN:
Codepen!
const performFilter = (covers) => {
let results = []; // initialise the array
let filteredValues = Object.values(activeFilters);
filteredValues.forEach((value) => {
let res = covers.filter((cover) => cover.value.indexOf(value) !== -1);
// push the value it find individually
// you were overriding the previous value with result = filter()
results.push(...res);
});
return results;
};
// according to Edited question
const performFilter = (covers) => {
let results = []; // initialise the array
let filteredValues = Object.values(activeFilters);
return covers.filter((cover) => filteredValues.every(value => cover.value.indexOf(value) !== -1));
};
I'm not sure if I understood clearly your question, so feel free to comment it.
First, I suggest you to filter your covers array and inside the filtering function iterate through your selected filters. This is because the filter function returns the array already filtered and so you don't need to assign it to a result variable or things like that. So based on that, try this:
const performFilter = (covers) => {
let results;
let filteredValues = Object.values(activeFilters);
const filteredCovers = covers.filter((cover) => {
return cover.value.split("-").some((tag) => filteredValues.includes(tag))
});
console.log(filteredCovers)
};

Updating data in an Object using Firebase arrayUnion

I am looking to update data in an object without changing the index of the object within the array it is contained. As it currently stands, the code removes the current object from the array and then applies array Union to update the array but pushes the component to the end of the array but I am looking to just update the data without the component losing its index. This is the code I am currently working with, I looked through the Firebase docs to see if there was a way to just update the component but couldn't find anything if anyone could point me in the right direction, please.
await firestore.update(project, {
pages: firestore.FieldValue.arrayRemove(page),
});
await firestore.update(project, {
pages: firestore.FieldValue.arrayUnion(newPage),
});
Unfortunately there is no field transform to replace a value like this:
firestore.FieldValue.arrayReplace(page, newPage);
Storing arrays and making changes by index in remote databases is generally discouraged. This older Firebase blog post covers some of the reasons why even though it was written with the Firebase Realtime Database in mind.
If the order of that array is important, you have two options:
fetch the array, mutate it, and then write it back. (simple)
fetch the array, find the relevant index, update that index only. (difficult)
To achieve the first result, you would make use of a transaction to find the previous value and replace it:
const db = firebase.firestore();
const projectDocRef = db.doc("projects/projectId");
function replacePage(oldPage, newPage) {
return db.runTransaction(async (t) => {
const snapshot = await t.get(projectDocRef);
if (!snapshot.exists) {
// no previous data, abort.
return "aborted";
}
const pagesArray = snapshot.get("pages");
const index = pagesArray.findIndex((page) => page === oldPage);
if (index === -1)
return "not-found";
pagesArray[index] = newPage;
await t.set(projectDocRef, { pages: pagesArray }, { merge: true });
return "replaced";
});
}
replacePage("index", "shop")
.then((result) => console.log("Page replacement was " + (result === "replaced" ? "" : " not") + " successful"))
.catch((err) => console.error('failed: ', err));
Note: Anything beyond this point is educational. There are many issues with this approach at scale.
Because Firestore doesn't support array entry replacement by index, you'll need to implement a way to update an index using something Firestore understands - maps. Using some FirestoreDataConverter trickery, you can use the converter to serialize your array as a map when you write it to Cloud Firestore and deserialize it back to an array when you read it. The major trade-off here is in how you will be able to query your data. You will be able to perform queries by index (such as where('pages.0', '==', 'shop')) but you'll lose the ability to use array-contains queries (such as where('pages', 'array-contains', 'shop')).
First, you need to define the converter:
// const obj = {};
// setNestedProperty(obj, ["a", "b", "c"], true)
// obj is now { "a": { "b": { "c": true } } }
function setNestedProperty(originalObj, pathPropsArray, val) {
const props = pathPropsArray.slice(0, -1);
const lastProp = pathPropsArray[pathPropsArray.length-1];
const parent = props.reduce((obj, p) => obj[p] ? obj[p] : (obj[p] = {}), originalObj);
parent[lastProp] = val;
}
const pagesArrayConverter = {
toFirestore(data) {
if (data.pages !== undefined) {
// step 1) convert array to map
const pagesAsMap = {};
data.pages.forEach((page, index) => {
if (page !== undefined) {
pagesAsMap[index] = page;
}
});
data.pages = pagesAsMap;
// step 2) if there are any mutations to "pages"
// while you are changing it, make the
// changes now before uploading to Firestore
Object.keys(data)
.filter(k => k.startsWith("pages."))
.forEach(k => {
const nestedValue = data[k];
data[k] = undefined;
delete data[k];
setNestedProperty(pagesAsMap, k.slice(6).split("."), nestedValue);
});
}
return data;
},
fromFirestore(snapshot, options) {
const data = snapshot.data(options);
if (data.pages !== undefined) {
const pagesAsArray = [];
Object.entries(data.pages)
.map(([index, page]) => pagesAsArray[index] = page);
// `pagesAsArray` may have empty elements, so we need
// to fill in the gaps with `undefined`:
data.pages = Array.from(pagesAsArray);
}
return data;
}
};
Which you would then attach to a query/reference like this:
const db = firebase.firestore();
const projectDocRef = db.doc("projects/projectId")
.withConverter(pagesArrayConverter)
If you already know that the previous value has an index of 2, you can just use:
await projectDocRef.set({ "pages.2": newPage }, { merge: true });
If you need to find it like before, you can use a transaction:
function replacePage(oldPage, newPage) {
return db.runTransaction(aysnc (t) => {
const snapshot = await t.get(projectDocRef);
if (!snapshot.exists) {
// no previous data, abort.
return "missing";
}
const data = snapshot.data();
// data is a { pages: Page[] }
const index = data.pages.findIndex((page) => page === oldPage);
if (index === -1)
return "not-found";
await t.set(projectDocRef, { ["pages." + oldIndex]: newPage }, { merge: true });
return "replaced";
});
}
replacePage("index", "shop")
.then((result) => console.log("Page replacement was " + (result === "replaced" ? "" : " not") + " successful"))
.catch((err) => console.error('failed: ', err));
arrayUnion adds new items to the array and arrayRemove removes items from an array. There isn't any way to update an existing item in array directly.
You would have to fetch the document, manually add/update the item at relevant index and then update the whole array back to the document.

Value is not changing in real time -- VueJS

I am using a JS class, I have following code:
class Field {
public Value = null;
public Items = [];
public UniqueKey = null;
public getItems() {
let items = [...this.Items];
items = items.filter((item) => {
if (item.VisibleIf) {
const matched = item.VisibleIf.match(/\$\[input:(.*?)\]/g);
if (matched?.length) {
const srv = Service.getInstance();
for (let match of matched) {
match = match.slice(8, -1);
if (srv.Fields?.length) {
let found = srv.Fields.find((x) => x.UniqueKey === match);
if (found) {
item.VisibleIf = item.VisibleIf.replace(
`$[input:${match}]`,
found.Value ?? ''
);
return JSON.parse('' + eval(item.VisibleIf));
}
}
}
}
}
return true;
});
return items;
}
public getInputTitle() {
let title = this.Title;
const matched = title.match(/\$\[input:(.*?)\]/g);
if (matched?.length && title) {
const srv = Service.getInstance();
for (let match of matched) {
match = match.slice(8, -1);
if (srv.Fields?.length) {
let found = srv.Fields.find((x) => x.UniqueKey === match);
if (found) {
title = title.replace(`$[input:${match}]`, found.Value ?? '');
}
}
}
}
return title;
}
}
Now I have a Vue component:
<div v-for="Field in Fields" :key="Field.UniqueKey">
<v-select
v-if="Field.Type == 'Select'"
:label="Field.getInputTitle()"
v-model="Field.Value"
:items="Field.getItems()"
item-text="Value"
item-value="Id"
/>
<v-input
v-else-if="Field.Type == 'Input'"
v-model="Field.Value"
:label="Field.getInputTitle()"
/>
</div>
// JS
const srv = Service.getInstance();
Fields = srv.getFields(); // <- API call will be there.
So basically, data comes from an API, having Title as Input $[input:uniqueKey], in a component I am looping over the data and generating the fields. See getInputTitle function in Field class, it works very well. All the fields which are dependent on the $[input:uniqueKey] are changing when I start typing into that field on which other fields are dependent.
Now I have pretty much same concept in the getItems function, so basically, what I want to do is whenever I type into a field and that field exists in the VisibleIf on the Items, the VisibleIf will be like '$[input:uniqueKey] < 1', or any other valid JavaScript expression which can be solved by eval function. But the getItems function is called only 1st time when page gets loaded, on the other hand the getInputTitle function which is pretty much same, gets called every time when I type into the field.
I tried to explain at my best, I will provide any necessary information if needed.
Any solution will be appreciated. Thanks.
You are updating the Object itself in here:
item.VisibleIf = item.VisibleIf.replace( `$[input:${match}]`, found.Value ?? '' );
Even though you tried to copy the array, but you have done shallow copy of the object in here: let items = [...this.Config.Items];
I suggest the following solution:
const visibleIf = item.VisibleIf.replace(
`$[input:${match}]`,
found.Value ?? ''
);
const val = '' + helpers.evalExp('' + visibleIf);
if (helpers.isJSON(val)) {
return JSON.parse(val);
}
Means instead of changing the VisibleIf object, just store it into the variable and just use that.
I hope that it will fix your issue. Let me know if it works.

Categories

Resources