Ability to select what inline styles to retain/strip in paste
remko opened this issue ยท 16 comments
Right now, you can choose whether your editor accepts rich text or not in paste using stripPastedStyles
. It's not always all-or-nothing, though: in case your editor only supports a subset of the supported styles, pasting can insert things the user can't remove anymore. For example, the Facebook Notes editor doesn't support underline or strikethrough, and yet you can paste underlined/striked text, and you can't get rid of that style anymore without deleting the entire piece of text.
It would be handy to have a prop on the editor component to specify which inline styles should be retained on paste. (opt-in)
You're right, we could handle this better. One idea would be to replace stripPastedStyles
with a configuration object, and pass that through to the paste processor:
// Flow type
type DraftPasteSupport = {
inlineStyles: List<string>,
blockTypes: List<string>,
links: boolean,
};
// in DraftEditorProps
pasteSupport: DraftPasteSupport,
// defaultProps
pasteSupport: {
inlineStyles: List('BOLD', 'ITALIC', 'UNDERLINE', ...),
blockTypes: List('header-one', ...),
links: true,
}
Then the equivalent of stripPastedStyles={true}
would be:
const PLAINTEXT_PASTE = {
inlineStyles: List(),
blockTypes: List(),
links: false,
};
What do you think?
cc @CLowbrow
Sounds great to me! ๐๐ป
I'm assuming this will play nice with internal custom blocks pasting, if it should ever be implemented #125?
Yeah, internal pastes should never be a problem here, since we just use the extracted fragment and this will contain any copied custom styles.
I like this. Would it make sense to pass blocks an inlines as a map of tag -> Draft editor style?
If we are making this customizable we might as well go all the way.
Interesting, instead of doing the mapping ourselves within the paste processor?
That could work for blocks, but for inlines, what happens to these: https://github.com/facebook/draft-js/blob/master/src/model/paste/DraftPasteProcessor.js#L148?
That's a good point. Those styles are more baked in to the editor and aren't generated based on tag name. Would probably still be a list.
For pasting, I myself am happy with the configuration object mentioned above.
However, per the change in #174 : I'm interested in having the ability to customize what happens when the html2block convertor encounters tags it doesn't know (img
, figure
); I was thinking of the ability to provide a function that takes an HTMLElement and returns a block (with a block entity). As I said, I don't really need this for paste, I wanted this more in a use case to one-off populate the editor from another (HTML-based) format, but I guess such a function would be useful for customizing paste support as well (e.g. to support image paste). This would only be block-level though, so maybe this is orthogonal to the original request.
That makes sense, but we are going to hit some weird edge cases.
Paste handler takes a tree structure and reduces it to a flat array of blocks, so we throw away nested block elements (a list inside a blockquote will get ignored as we don't have a way of storing that). The simplest thing to do would be to only call that function on block elements we care about that aren't inside other block elements we care about. I'm worried that will be confusing and not useful enough.
My other concern is that we convert to blocks at the very end of going through the DOM tree. Until we are done walking the tree, the document state is just a long string with some associated arrays for block types/entities.
I think I'm going to work on making the list of tags we care about customizable first, and will save passing in custom handlers later.
@CLowbrow @hellendag Has there been any progress on this? I just now ran into a similar issue where it is desirable for internal text to just be copy/pasted as is, but to prevent certain or all styles from external pastes.
Would love to see this implemented. It has a lot of potential use cases.
I have implemented @hellendag's suggestion in #1057
Great to see this is being worked on.
Just to check would you envisage this functionality eventually allowing:
- Calling back interested observers when styles are stripped. A single call back post-paste might be ideal, allowing us to let the user know what has been stripped.
- Clients overriding the stripping behavior for each type of element, for example in some cases you might want to replace an image with custom text where as in others you'd want to completely remove it.
Is it planned to release this feature?
same concern. The idea with
// in DraftEditorProps
pasteSupport: DraftPasteSupport,
is very valuable. Is it already done?
I've made a library that does this, among other types of filtering to fix other issues with Draft.js copy/paste: https://github.com/thibaudcolas/draftjs-filters, (demo).
It works by filtering all of the editor's content in the onChange
handler whenever the change is a paste (insert-fragment
), here's what it looks like:
import { filterEditorState } from "draftjs-filters"
function onChange(nextState) {
const { editorState } = this.state
let filteredState = editorState
const shouldFilterPaste =
nextState.getCurrentContent() !== editorState.getCurrentContent() &&
filteredState.getLastChangeType() === "insert-fragment"
if (shouldFilterPaste) {
filteredState = filterEditorState(
{
blocks: ["header-two", "header-three", "unordered-list-item"],
styles: ["BOLD"],
entities: [
{
type: "IMAGE",
// Whitelist of attributes to keep.
attributes: ["src"],
// Whitelist rules to define which entities to keep.
whitelist: {
src: "^http",
},
},
{
type: "LINK",
attributes: ["url"],
},
],
maxNesting: 1,
whitespacedCharacters: ["\n", "\t", "๐ท"],
},
filteredState,
)
}
this.setState({ editorState: filteredState })
}
If you only want to filter styles, the package also exposes a filterInlineStyles
function that does just that.
It's more limited than what is suggested in this issue because you can only filter the whole content (rather than just what got pasted), but works well in my experience. Feedback welcome!
Here is stand-alone version of mine:
const removeInlineStyles = (
editorState: EditorState,
retainInlineStyles: string[] = []
) => {
let blocks = editorState
.getCurrentContent()
.getBlocksAsArray()
.map(singleBlock =>
singleBlock.set(
"characterList",
singleBlock.getCharacterList().map(charMetaData => {
if (!charMetaData) {
return charMetaData;
}
let entity = charMetaData.getEntity();
let style = charMetaData.getStyle();
return CharacterMetadata.create({
entity: entity,
style: style.intersect(retainInlineStyles)
});
})
)
) as ContentBlock[];
return EditorState.createWithContent(
ContentState.createFromBlockArray(blocks)
);
};
// Usage on the editor onChange, e.g. retain only bold styling:
onChange = (editorState: EditorState) => {
if (editorState.getLastChangeType() == "insert-fragment") {
editorState = removeInlineStyles(editorState, ["BOLD"]); // This retains only BOLD styling
}
this.setState({
editorState
});
};
If you don't want to retain any inline styles just omit the second argument.