Draft.js insert one contentState into another - javascript

I have a Draft.js editor with some HTML in it. How can I insert a new piece of HTML at a current selection position, preserving both stylings and entity/block mappings?
I know how to insert raw text via Modifier.insertText:
const contentState = Modifier.insertText(
editorState.getCurrentContent(),
editorState.getSelection(),
insertionText,
editorState.getCurrentInlineStyle(),
);
but it will strip all the HTML which is not ok.
// Original HTML piece
const { contentBlocks, entityMap } = htmlToDraft(originalHTML);
const contentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap,
);
const editorState = EditorState.createWithContent(contentState);
// Additional HTML piece, which I want to insert at the selection (cursor) position of
// previous editor state
const { contentBlocks, entityMap } = htmlToDraft(newHTML);
const newContentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap,
);

You should probably use Modifier.replaceWithFragment for this:
A "fragment" is a section of a block map, effectively just an OrderedMap much the same as the full block map of a ContentState object.
This method will replace the "target" range with the fragment.
This will work identically to insertText, except that you need to give it a block map as a parameter. It’s not entirely clear from your example what the return value of htmlToDraft is but it should be possible for you to either:
Use contentBlocks directly from the return value
Or create a contentState as in your example, then use its .getBlockMap() method to get the block map to insert into the editor’s content.
const newContentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap,
);
// Use newContentState.getBlockMap()

I could not post it in the comment section (too long of a snippet) but as mentioned earlier Modifier.replaceWithFragment is the way to go, so I made it work like in the snippet below.
*** Make sure you have all other infrastructure in place like blockRendererFn, blockRenderMap, compositeDecorator, etc to actually render all those blocks, inline styles for links, images, etc
import {
Modifier,
ContentState,
convertFromHTML,
} from 'draft-js'
onPaste = (text, html, state) => {
if (html) {
const blocks = convertFromHTML(html)
Modifier.replaceWithFragment(
this.state.editorState.getCurrentContent(),
this.state.editorState.getSelection(),
ContentState.createFromBlockArray(blocks, blocks.entityMap).getBlockMap(),
)
}
return false
}

Related

React DraftJS cursor auto jumps to beginning after typing in existing content and started typing backwards?

I'm using DraftJS to edit text contents, but when I tried to edit any existing contents that I've retrieved from DB and loaded into the Editor, the cursor auto jumps to the beginning of the texts and I started typing from backwards.
I've imported the Editor into Bulletin.js, so I have to get the content by passing getContent into Editor and retrieve back the raw html by using getContent in the handleEditorChange function of the Editor.
I've found out that if I've removed the getContent function to pass back the raw HTML in handleEditorChange, the editor works normally, but then I won't be able to get the html content back to Bulletin.js.
Here's the codesandbox that I've created for reference.
The issue is that you set on every change a new EditorState here:
useEffect(() => {
if (htmlContent) {
let convertedToHTML = decodeURIComponent(htmlContent);
const blocksFromHtml = htmlToDraft(convertedToHTML);
const { contentBlocks, entityMap } = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap
);
setEditorState(EditorState.createWithContent(contentState));
}
}, [htmlContent]);
htmlContent is always changing as you convert the EditorState to HTML on every change via the getContent function.
If you want to initialize your EditorState with the htmlContent you can do it in the useState function and remove the useEffect:
const [editorState, setEditorState] = useState(() => {
if (htmlContent) {
let convertedToHTML = decodeURIComponent(htmlContent);
const blocksFromHtml = htmlToDraft(convertedToHTML);
const { contentBlocks, entityMap } = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(
contentBlocks,
entityMap
);
return EditorState.createWithContent(contentState);
}
return EditorState.createEmpty()
});

how should i dynamically update a firebase document

i'm working on a simple note-taking app for my portfolio using JS and Firebase. Before i tell you what's happening i feel like i need to show you how my code works, if you have any tips and concerns please tell me as it would be GREATLY appreciated. That being said, let's have a look "together". I'm using this class to create the notes:
const htmlElements = [document.querySelector('.notes'), document.querySelector('.note')];
const [notesDiv, noteDiv] = htmlElements;
class CreateNote {
constructor(title, body) {
this.title = title;
this.body = body;
this.render = () => {
const div1 = document.createElement('div');
div1.className = 'notes-prev-container';
div1.addEventListener('click', () => { this.clickHandler(this) });
const div2 = document.createElement('div');
div2.className = 'notes-prev';
const hr = document.createElement('hr');
hr.className = 'notes__line';
// Nest 'div2' inside 'div1'
div1.appendChild(div2);
div1.appendChild(hr);
/*
Create Paragraph 1 & 2 and give them the same
class name and some text
*/
const p1 = document.createElement('p');
p1.className = 'notes-prev__title';
p1.innerText = this.title;
const p2 = document.createElement('p');
p2.className = 'notes-prev__body';
p2.innerText = this.body;
// Nest p 1 & 2 inside 'div2'
div2.appendChild(p1);
div2.appendChild(p2);
// Finally, render the div to its root tag
notesDiv.appendChild(div1);
}
}
/*
Every time this method is called, it creates 2 textareas,
one for the note title and the other for its body then it
appends it to the DOM.
*/
renderNoteContent () {
const title = document.createElement('textarea');
title.placeholder = 'Title';
title.value = this.title;
title.className = 'note__title';
const body = document.createElement('textarea');
body.placeholder = 'Body';
body.value = this.body;
body.className = 'note__body';
noteDiv.appendChild(title);
noteDiv.appendChild(body);
}
/*
When this method is called, it checks to see if there's a
note rendered already (childElementCount === 1 because there's a
button, so if there's only this button it means there's no
textareas rendered).
If yes, then merely call the renderNoteContent method. Else
get the tags with the classes 'note__title' and 'note__body'
and remove them from the DOM, then call renderNoteContent to
create the textareas with the clicked notes values.
This function gets mentioned at line 19.
*/
clickHandler(thisClass) {
if (noteDiv.childElementCount === 1) {
thisClass.renderNoteContent();
} else {
document.querySelector('.note__title').remove();
document.querySelector('.note__body').remove();
thisClass.renderNoteContent();
}
}
}
Now i need 2 buttons, createNotesButton and saveNotesButton respectively. These 2 buttons must be inside a function that will be called inside .onAuthStateChanged (why? because they will be needing access to the currentUser on firebase auth).
I want the createNotesButton to create a note prototype, render it to the DOM and create a new document on firestore, where this note contents will be stored. Here's how i did it:
PS: I feel like i'm not using this class correctly, so again if you have any tips i appreciate it.
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
/*
1. Creates a class.
2. Creates a new document on firebase with the class's empty value.
3. Renders the empty class to the DOM.
*/
createNotesButton.addEventListener('click', () => {
const note = new CreateNote('', '');
note.render();
// Each user has it's own note collection, said collection has their `uid` as name.
db.collection(`${user.uid}`).doc().set({
title: `${note.title}`,
body: `${note.body}`
})
})
}
Now i need a saveNotesButton, he's the one i'm having issues with. He needs to save the displayed note's content on firestore. Here's what i tried doing:
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
createNotesButton.addEventListener('click', () => {...})
/*
1. Creates 2 variables, `title` and `body, if there's not a note being displayed
their values will be null, which is why the rest of the code is inside an if
statement
2. If statement to check if there's a note being displayed, if yes then:
1. Call the user's note collection. Any document who has the title field equal to the
displayed note's value gets returned as a promise.
2. Then call an specific user document and update the fields `title` and `body` with
the displayed note's values.
3. If no then do nothing.
*/
saveNotesButton.addEventListener('click', () => {
const title = document.querySelector('.note__title');
const body = document.querySelector('.note__body');
db.collection(`${user.uid}`).where('title', '==', `${title.value}`)
.get()
.then(userCollection => {
db.collection(`${user.uid}`).doc(`${userCollection.docs[0].id}`).update({
title: `${title.value}`,
body: `${body.value}`
})
})
.catch(error => {
console.log('Error getting documents: ', error);
});
});
}
This didn't work because i'm using title.value as a query, so if i change it's value it will also change the queries direction to a path that doesn't exist.
So here's the question: how can i make it so the saveNotesButton does its job? I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note. Again, if there's something in my code that you think can or should be formatted please let me know, i'm using this project as a way to solidify my native JS knowledge so please be patient. I feel like if i had used React i would've finished this sometime ago but definitely wouldn't have learned as much, anyway thanks for your help in advance.
I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note.
Yes, you absolutely need an immutable identifier for each note document in the firestore so you can unambiguously reference it. You almost always want this whenever you're storing a data object, in any application with any database.
But, the firestore already does this for you: after calling db.collection(user.uid).doc() you should get a doc with an ID. That's the ID you want to use when updating the note.
The part of your code that interacts with the DOM will need to keep track of this. I suggest moving the code the creates the firestore document into the constructor of CreateNote and storing it on this. You'll need the user id there as well.
constructor(title, body, userId) {
this.title = title;
this.body = body;
const docRef = db.collection(userId).doc();
this.docId = docRef.id;
/* etc. */
Then any time you have an instance of CreateNote, you'll know the right user and document to reference.
Other suggestions (since you asked)
Use JsPrettier. It's worth the setup, you'll never go back.
Use HTML semantics correctly. Divs shouldn't be appended as children of hrs, because they're for "a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section." MDN
For your next project, use a framework. Essentially no one hand-codes event listeners and appends children to get things done. I see the value for basic understanding, but there's a rich and beautiful world of frameworks out there; don't limit yourself by avoiding them :-)

Draft.js - convert current block to Atomic Block

I'm working on a custom draft.js plugin that inserts an Atomic Block with a GIF in it. I started by copying the Draft.js Image Plugin as it's almost identical. I've got my plugin working but there's one issue I'm not sure best how to solve.
To insert a GIF I'm following the example addImage Modifier in the Image Plugin. However, this always creates a new Atomic Block after the current selection. If the current block is empty, I want to place the GIF there instead.
Here's what my modifier function looks like:
const addGiphy = (editorState, giphyId) => {
const contentState = editorState.getCurrentContent();
// This creates the new Giphy entity
const contentStateWithEntity = contentState.createEntity("GIPHY", "IMMUTABLE", {
giphyId
});
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
// Work out where the selection is
const currentSelection = editorState.getSelection();
const currentKey = currentSelection.getStartKey();
const currentBlock = editorState.getCurrentContent().getBlockForKey(currentKey);
let newEditorState;
if (currentBlock.getType() === "unstyled" && currentBlock.getText() === "") {
// Current line is empty, we should convert to a gif atomic block
// <INSERT MAGIC HERE>
} else {
// There's stuff here, lets create a new block
newEditorState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, " ");
}
return EditorState.forceSelection(
newEditorState,
newEditorState.getCurrentContent().getSelectionAfter()
);
};
I'm not sure how to handle the condition of converting the current block to an Atomic Block. Is this Draft.js best practice? Alternatively, am I better to always insert a new block and then remove the empty block?
For clarity, this same issue also exists in the Image Plugin, it's not something I've introduced.
you can use https://draftjs.org/docs/api-reference-modifier/#setblocktype
const content = Modifier.setBlockType(content, editorState.getSelection(), 'atomic');
const newState = EditorState.push(editorState, content, 'change-block-type');

Draft js Editor gets slower as the content increases due to having many decorators

So my draft-js editor becomes really slow(hacky) the more content I insert (after about 20 decorator replacements). I am guessing this behavior is due to the decorator which checks the entire editor content using regex and replaces the matches with emoji component every time the state changes. I am also creating entities for each of the matches the regex find, I do this by decorating the component with editor state as a prop. Is there a way to make it faster?
Here is my decorator :
{
strategy: emojiStrategy,
component: decorateComponentWithProps(RenderEmoji, {
getEditorState: this.getEditorState,
setEditorState: this.onChange
})
}
here is my emojiStrategy :
function emojiRegexF(regex, contentBlock, callback, contentState) {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
}
function emojiStrategy(contentBlock, callback, contentState) {
emojiRegexF(EMOJI_REGEX, contentBlock, callback, contentState);
}
here is my RenderEmoji component:
const RenderEmoji = props => {
const contentBlock = props.children[0].props.block;
const emojiKey = contentBlock.getEntityAt(props.children[0].props.start);
const emojiShortName = props.decoratedText;
if (!emojiKey) {
setEntity(props, emojiShortName);
}
return (
<Emoji emoji={emojiShortName} set="emojione" size={24}>
{props.children}
</Emoji>
);
};
and here is my setEntity function that sets the entity for the match:
function setEntity(props, emojiShortName) {
const editorState = props.getEditorState();
const contentstate = editorState.getCurrentContent();
const contentStateWithEntity = contentstate.createEntity(
"emoji",
"IMMUTABLE",
{
emojiUnicode: emojiShortName
}
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const oldSelectionState = editorState.getSelection();
const selectionState = oldSelectionState.merge({
focusOffset: props.children[0].props.start + props.decoratedText.length,
anchorOffset: props.children[0].props.start
});
const newContentState = Modifier.applyEntity(
contentstate,
selectionState,
entityKey
);
const withBlank = Modifier.replaceText(
newContentState,
selectionState,
emojiShortName + " ",
null,
entityKey
);
const newEditorState = EditorState.push(
editorState,
withBlank,
"apply-entity"
);
props.setEditorState(newEditorState);
}
Any way I can optimize this? Thanks
I'm not sure if this would be the source of any real performance problem but there are two things that seem funny:
Matching the emoji decorator by regex, even though you're creating entities for them.
Changing the editor state (via setEntity) during the rendering of the decorator. Render functions should be pure.
I imagine you do this type of processing because emojis might be inserted via copy-paste, or via some kind of native emoji picker. A better way would be to:
Insert entities for emojis with the setEntity logic as part of onChange – before the content is saved and ultimately rendered.
Use a decorator strategy based on entities only, e.g.:
const emojiStrategy = (contentBlock, callback, contentState) => {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'emoji'
);
}, callback);
};
Then your decorator component won't need to update the editor state during the rendering. You also might not need to use decorateComponentWithProps anymore.
Now back to performance – the best way for you to know for sure how to improve it is to profile your app. You'll be able to tell exactly what takes time to render during keystrokes, and then track down the issue.

react-draft-wysiwyg update editorstate from dynamically created editor

I'm currently working on a project which involves using multiple wysiwyg editors. I have previously used react-draft in the same project but has always been used with static elements eg, each editor is fixed.
In my case, my editors are created on the fly, (min 1, max 15) editors. I'm rendering these into my containers using map() with constructed object each time. Allowing the user to click + or - buttons to create / remove a editor.
for example to create a new editor into, i push to then map over the components array which looks something like the below:
components: [
{
id:1,
type: 'default',
contentValue: [
title: 'content-block',
value: null,
editorState: EditorState.CreateEmpty(),
]
}
]
I am able to render multiple editors just fine and createEmpty ediorstates. My issue is when i try to update the contents editor state.
Usually to update a single editor id use:
onEditorStateChange = editorState => {
this.setState({
editorstate,
})
}
However, given the fact my editors are dynamically rendered, i have the editor state isolated within the "Components" array. So i've tried the following which did not work:
In Render
this.state.components.map((obj) => {
return (
<Editor
editorState={obj.contentValue.editorState}
onEditorStateChange={(e) => this.onEditorStateChange(e, obj.id)}
/>
);
}
onEditorStateChange
onEditorStateChange(e, id){
const { components } = this.state;
const x = { components };
for (const i in x){
if(x[i].id ==== id){
x[i].contentValue.editorState = e;
}
}
this.setState({components: x})
}
Upon debugging, the "setState" does get called in the above snippet, and it does enter the if statement, but no values are set in my editorState.
I'm happy for alternative ways to be suggested, as long as this will work with dynamically rendered components.
I require this value to be set, as i will be converting to HTML / string and using the content to save to a database.
I hope i have explained this well, if not please let me know and ill be happy to provide further information / snippets.
Thank you in advance.
Okay, i figured out a solution.
inside my onEditorStateChange() i update the value with the parameter (e) which was initally passed in. Using draftToHtml i convert it to raw and pass it to set state as shown below:
onEditorStateChange(e, id) {
const { components } = this.state;
console.log(e);
const x = components;
for (const i in x) {
if (x[i].id === id) {
x[i].contentValue.editorState = e;
x[i].value = draftToHtml(convertToRaw(e.getCurrentContent()));
//console.log(x[i]);
}
}
this.setState({
components: x,
});
}
This gives me the a HTML value which i can now convert to string and save to the database.
Hope this helps someone else with the same issue :)

Categories

Resources