Skip to content

Instantly share code, notes, and snippets.

@joshdover
Last active July 30, 2021 12:58
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshdover/7c5e61ed68cc5552dc8a25463e357960 to your computer and use it in GitHub Desktop.
Save joshdover/7c5e61ed68cc5552dc8a25463e357960 to your computer and use it in GitHub Desktop.
Example of how to add metadata to blocks for handling text alignment correctly
import { Modifier, EditorState, RichUtils } from 'draft-js';
import getCurrentlySelectedBlock from './getCurrentlySelectedBlock';
export const ALIGNMENTS = {
CENTER: 'center',
JUSTIFY: 'justify',
LEFT: 'left',
RIGHT: 'right'
};
export const ALIGNMENT_DATA_KEY = 'textAlignment';
const ExtendedRichUtils = Object.assign({}, RichUtils, {
// Largely copied from RichUtils' `toggleBlockType`
toggleAlignment(editorState, alignment) {
const { content, currentBlock, hasAtomicBlock, target } = getCurrentlySelectedBlock(editorState);
if (hasAtomicBlock) {
return editorState;
}
const blockData = currentBlock.getData();
const alignmentToSet = blockData && blockData.get(ALIGNMENT_DATA_KEY) === alignment ?
undefined :
alignment;
return EditorState.push(
editorState,
Modifier.mergeBlockData(content, target, {
[ALIGNMENT_DATA_KEY]: alignmentToSet
}),
'change-block-data'
);
},
/*
* An extension of the default split block functionality, originally pulled from
* https://github.com/facebook/draft-js/blob/master/src/component/handlers/edit/commands/keyCommandInsertNewline.js
*
* This version ensures that the text alignment is copied from the previously selected block.
*/
splitBlock(editorState) {
// Original split logic
const contentState = Modifier.splitBlock(
editorState.getCurrentContent(),
editorState.getSelection()
);
const splitState = EditorState.push(editorState, contentState, 'split-block');
// Assign alignment if previous block has alignment. Note that `currentBlock` is the block that was selected
// before the split.
const { currentBlock } = getCurrentlySelectedBlock(editorState);
const alignment = currentBlock.getData().get(ALIGNMENT_DATA_KEY);
if (alignment) {
return ExtendedRichUtils.toggleAlignment(splitState, alignment);
} else {
return splitState;
}
}
});
export default ExtendedRichUtils;
const getCurrentlySelectedBlock = (editorState) => {
const selection = editorState.getSelection();
const startKey = selection.getStartKey();
let endKey = selection.getEndKey();
const content = editorState.getCurrentContent();
let target = selection;
// Triple-click can lead to a selection that includes offset 0 of the
// following block. The `SelectionState` for this case is accurate, but
// we should avoid toggling block type for the trailing block because it
// is a confusing interaction.
if (startKey !== endKey && selection.getEndOffset() === 0) {
const blockBefore = content.getBlockBefore(endKey);
if (!blockBefore) {
throw new Error('Got unexpected null or undefined');
}
endKey = blockBefore.getKey();
target = target.merge({
anchorKey: startKey,
anchorOffset: selection.getStartOffset(),
focusKey: endKey,
focusOffset: blockBefore.getLength(),
isBackward: false
});
}
const hasAtomicBlock = content.getBlockMap()
.skipWhile((_, k) => k !== startKey)
.takeWhile((_, k) => k !== endKey)
.some(v => v.getType() === 'atomic');
const currentBlock = content.getBlockForKey(startKey);
return {
content,
currentBlock,
hasAtomicBlock,
target
};
};
export default getCurrentlySelectedBlock;
@jaredtmartin
Copy link

jsfiddle/codepen would be great for this!

@michorvath
Copy link

michorvath commented Jul 30, 2021

I tried to set up a codepen to share a fully working example but I used Material UI and it was a nightmare to try and get that running in codepen. So instead I will just briefly explain that to use the splitBlock method you want to add a prop (handleReturn) to the Editor component that comes from Draft, like so:

import React, { useState } from 'react';
import { Editor, EditorState } from 'draft-js';
import ExtendedRichUtils from './utils/ExtendedRichUtils.js';

function RichTextEditor() {
  const [editorState, setEditorState] = useState(EditorState.createEmpty());
  
  return (
    <Editor
      editorState={editorState}
      onChange={setEditorState}
      handleReturn={() => {
        setEditorState(ExtendedRichUtils.splitBlock(editorState));
        return 'handled';
      }}
    }
  );
/>

The handleReturn method is briefly mentioned here. Returning the string "handled" prevents the default behavior and lets us supplement our own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment