Open MickaelBZH opened 5 years ago
@MickaelBZH : you need to do something like this for image: https://github.com/jpuri/react-draft-wysiwyg/blob/master/src/controls/Image/index.js#L55
Hello.
I'm also trying to add HTML directly to the Editor. @MickaelBZH, did you came with a solution? I added a button like this answer: https://github.com/jpuri/react-draft-wysiwyg/issues/315#issuecomment-439028260
But when I add html in the 'code' view, it gets lost. div's are converted to p's, etc.
My final goal is to add html code to not be converted, just returned to the editor the same way (and maybe replaced with some 'there is custom html here' indicator).
Any advises @jpuri will be appreciated :D
Thank you all.
Hey @Huespal , Are you trying to embed some HTML section in editor, may be a custom block can help you there.
Or are you trying to set text of editor using the HTML ? For that it is required to use draftjs api to generate editor content.
Hello.
Maybe second one. I already have an embed button to add videos, for example. But what I'm trying to do is to give the user the possibility to append html, wrote by him/her, without being converted to br's or p', etc. For example adding div's or script's. I'm playing with draftJS API :), with no results :(, at the moment.
Thanks
Hey @Huespal , users will not be able to append html block / inline styles not supported by the editor :( Only option is a custom block type, that is also not so straightforward.
Ok, I get a solution that suits my problem :D
I added a fantastic 'code view' button:
render() {
const { editorCode } = this.state;
return (<Editor
[...]
blockRendererFn={this.blockRenderer}
onEditorStateChange={this.onEditorStateChange}
toolbarCustomButtons={[
[...],
<CodeOption toggleEditorCode={this.toggleEditorCode} /> // See more info to know how this should work.
]}
/>
{
showEditorCode
&& (
<textarea
id="code-view"
name="codeView"
value={editorCode}
onChange={this.onEditorCodeStateChange}
/>
)
});
}
More info: https://github.com/jpuri/react-draft-wysiwyg/issues/315#issuecomment-439028260
Played with blockRendererFn
:
blockRenderer(contentBlock) {
const { editorState } = this.state;
const type = contentBlock.getType();
if (type === 'atomic') {
const contentState = editorState.getCurrentContent();
const entityKey = contentBlock.getEntityAt(0);
if (entityKey) {
const entity = contentState.getEntity(entityKey);
if (entity
&& (entity.type === 'SCRIPT'
|| entity.type === 'DIV')
) {
return {
component: () => '</>', // Or whatever you like.
editable: false
};
}
}
}
return null;
}
}
And made some magic with customEntityTransform
parameter in draftToHtml, and customChunkRenderer
parameter in htmlToDraft() function;
customChunkRenderer(nodeName, node) {
if (nodeName === 'div') {
return {
type: 'DIV',
mutability: 'MUTABLE',
data: { // Pass whatever you want here (like id, or classList, etc.)
innerText: node.innerText,
innerHTML: node.innerHTML
}
};
}
if (nodeName === 'script') {
return {
type: 'SCRIPT',
mutability: 'MUTABLE',
data: { // Pass whatever you want here (like id, or keyEvents, etc.)
innerText: node.innerText,
innerHTML: node.innerHTML
}
};
}
return null;
}
onEditorCodeStateChange(editorCode) {
let editorState = EditorState.createEmpty();
const blocksFromHtml = htmlToDraft(
editorCode,
customChunkRenderer
);
if (blocksFromHtml) {
const { contentBlocks, entityMap } = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
editorState = EditorState.createWithContent(contentState);
}
this.setState({
editorState,
editorCode
});
}
onEditorStateChange(editorState) {
const editorCode = draftToHtml(
convertToRaw(editorState.getCurrentContent()),
null,
null,
(entity) => {
if (entity.type === 'DIV') { // Receive what you passed before, here (like id, or classList, etc.)
return `<div>${entity.data.innerHTML}</div>`;
}
if (entity.type === 'SCRIPT') { // Receive what you passed before, here (like id, or keyEvents, etc.)
return `<script>${entity.data.innerHTML}</script>`;
}
return '';
}
);
this.setState({
editorState,
editorCode
});
}
It's so cool to allow people to add html to WYSWYG :D
@Huespal I'm bit confused! would you mind providing the whole component code?
@sachinkammar You can see all code in the fork on my profile. It is not totally working :/ I'm struggling with uploaded images not being renderer. Any help is appreciated.
Where did you land on this @Huespal ? I could not find a fork in your profile.
@Huespal Your approach is fine but it doesn't work for nested tags. e.g for a table. I made some changes to make it work for divs and tables. Check my solution. (It was written on TypeScript but the same applies to JS)
import React from 'react'
import { Editor as Draft } from 'react-draft-wysiwyg'
import { ColorPalette } from '@allthings/colors'
import htmlToDraft from 'html-to-draftjs'
import { EditorState, ContentState, convertToRaw } from 'draft-js'
import draftToHtml from 'draftjs-to-html'
import { css } from 'glamor'
const styles = {
editorStyle: invalid => ({
backgroundColor: ColorPalette.white,
border: `1px solid ${ColorPalette[invalid ? 'red' : 'lightGreyIntense']}`,
borderRadius: '2px',
maxHeight: '30vh',
minHeight: '300px',
overflowY: 'auto',
padding: '5px',
width: 'inherit',
}),
toolbarStyle: {
backgroundColor: ColorPalette.lightGrey,
border: `1px solid ${ColorPalette.lightGreyIntense}`,
borderBottom: '0px none',
marginBottom: '0px',
marginTop: '5px',
width: 'inherit',
},
wrapperStyle: {},
}
interface IProps {
editorState: any
invalid?: boolean
onEditorStateChange: (state) => void
toolbar?: object
onChange?: (editorState) => void
}
interface IState {
showEditorCode: boolean
editor: any
editorHTML: any
textareaEditor: any
showCode: boolean
}
function customChunkRenderer(nodeName, node) {
const allowedNodes = [
'div',
'table',
'tbody',
'tr',
'th',
'td',
'thead',
'style',
]
if (allowedNodes.includes(nodeName)) {
return {
type: nodeName.toString().toUpperCase(),
mutability: 'MUTABLE',
data: {
// Pass whatever you want here (like id, or classList, etc.)
innerText: node.innerText,
innerHTML: node.innerHTML,
},
}
}
return null
}
function entityMapper(entity) {
if (entity.type === 'DIV') {
return `<div>${entity.data.innerHTML}</div>`
}
if (entity.type === 'TABLE') {
return `<table>${entity.data.innerHTML}</table>`
}
if (entity.type === 'TBODY') {
return `<tbody>${entity.data.innerHTML}</tbody>`
}
if (entity.type === 'TR') {
return `<tr>${entity.data.innerHTML}</tr>`
}
if (entity.type === 'TH') {
return `<th>${entity.data.innerHTML}</th>`
}
if (entity.type === 'TD') {
return `<td>${entity.data.innerHTML}</td>`
}
if (entity.type === 'STYLE') {
return `<style>${entity.data.innerHTML}</style>`
}
return ''
}
function entityMapperToComponent(entity) {
if (entity.type === 'DIV') {
return () => (
<div dangerouslySetInnerHTML={{ __html: entity.data.innerHTML }} />
)
}
if (entity.type === 'TABLE') {
return () => (
<table dangerouslySetInnerHTML={{ __html: entity.data.innerHTML }} />
)
}
if (entity.type === 'TBODY') {
return <tbody dangerouslySetInnerHTML={{ __html: entity.data.innerHTML }} />
}
if (entity.type === 'TR') {
return () => (
<tr dangerouslySetInnerHTML={{ __html: entity.data.innerHTML }} />
)
}
if (entity.type === 'TH') {
return () => (
<th dangerouslySetInnerHTML={{ __html: entity.data.innerHTML }} />
)
}
if (entity.type === 'TD') {
return () => (
<td dangerouslySetInnerHTML={{ __html: entity.data.innerHTML }} />
)
}
if (entity.type === 'STYLE') {
return () => <style>{entity.data.innerHTML}</style>
}
return ''
}
function customBlockRenderFunc(block, config) {
if (block.getType() === 'atomic') {
const contentState = config.getEditorState().getCurrentContent()
const entity = contentState.getEntity(block.getEntityAt(0))
return {
component: entityMapperToComponent(entity),
editable: false,
props: {
children: () => entity.innerHTML,
},
}
}
return undefined
}
export default class Editor extends React.Component<IProps, IState> {
constructor(props) {
super(props)
this.state = {
showEditorCode: false,
editor: EditorState.createEmpty(),
editorHTML: '',
textareaEditor: '',
showCode: false,
}
}
onEditorStateChange = editor => {
const editorHTML = draftToHtml(
convertToRaw(editor.getCurrentContent()),
null,
false,
entityMapper,
)
this.setState({ editor, editorHTML })
}
onEditEditorHTML = e => {
const editorHTML = e.target.value
this.setState({ editorHTML })
}
toggleEditorCode = () => {
const { showEditorCode } = this.state
const { editorState } = this.props
if (!showEditorCode) {
this.onEditorStateChange(editorState)
}
this.setState({ showEditorCode: !showEditorCode })
}
addHtmlToEditor = () => {
const { editorHTML } = this.state
const contentBlock = htmlToDraft(editorHTML, customChunkRenderer)
let editor
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(
contentBlock.contentBlocks,
)
editor = EditorState.createWithContent(contentState)
} else {
editor = EditorState.createEmpty()
}
this.props.onChange(editor)
}
render() {
const {
editorState,
invalid = false,
onEditorStateChange,
toolbar,
intl,
} = this.props
const { showEditorCode, editorHTML } = this.state
const ShowEditorCode = () => (
<div className="rdw-option-wrapper" onClick={this.toggleEditorCode}>
{showEditorCode
? intl.formatMessage(MESSAGES.hideCode)
: intl.formatMessage(MESSAGES.showCode)}
</div>
)
return (
<div>
<Draft
editorState={editorState}
editorStyle={styles.editorStyle(invalid)}
name="content"
onEditorStateChange={onEditorStateChange}
toolbar={toolbar}
toolbarStyle={styles.toolbarStyle}
wrapperStyle={styles.wrapperStyle}
toolbarCustomButtons={[<ShowEditorCode />]}
customBlockRenderFunc={customBlockRenderFunc}
/>
{showEditorCode && (
<div {...css({ width: '100%' })}>
<textarea
rows={10}
{...css({
width: '100%',
padding: '0',
})}
value={editorHTML}
onChange={this.onEditEditorHTML}
/>
<div>
<button type="button" onClick={this.addHtmlToEditor}>
Submit
</button>
</div>
</div>
)}
</div>
)
}
}
I improve the code and separete the helper functions as shown below:
draft-js-helpers.tsx
import { Parser as HtmlToReactParser } from 'html-to-react'
import DOMPurify from 'dompurify'
const allowedNodes = ['div', 'table', 'style', 'img']
const styleObjToCSS = styleObj =>
Object.keys(styleObj).reduce((acum, style) => {
return (style && styleObj[style]
? `${style}:${styleObj[style]}; ${acum}`
: ''
).trim()
}, '')
const nodeAttributesToObj = attrs => {
const objAttrs = { style: null }
for (let i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name !== 'style') {
if (attrs[i].name && attrs[i].value) {
objAttrs[attrs[i].name] = attrs[i].value
}
} else {
const stylesInText = attrs[i].value.split(';')
const styles = stylesInText.reduce((acum, style) => {
const components = style.split(':')
if (components[0] && components[1]) {
acum[components[0]] = `${components[1]}`
}
return acum
}, {})
objAttrs.style = styles
}
}
return objAttrs
}
export function entityMapper(entity) {
let type = entity.type
let data = { ...entity.data }
if (type === 'IMAGE') {
// added to support the existing image option in the editor
type = 'IMG'
data = { attributes: data, innerHTML: '' }
}
data.attributes = data.attributes ? data.attributes : {}
let styleAsAttribute
if (data.attributes.style) {
styleAsAttribute = styleObjToCSS(data.attributes.style)
}
const attributes = Object.keys(data.attributes).reduce(
(acum, key) =>
(key === 'style'
? `${key}="${styleAsAttribute}" ${acum}`
: `${key}="${data.attributes[key]}" ${acum}`
).trim(),
'',
)
const node = type.toLowerCase()
if (allowedNodes.includes(node)) {
return `<${node} ${attributes}>${data.innerHTML}</${node}>`
}
return ''
}
export function entityMapperToComponent(entity) {
const htmlToReactParser = new HtmlToReactParser()
return () => htmlToReactParser.parse(DOMPurify.sanitize(entityMapper(entity)))
}
export function customChunkRenderer(nodeName, node) {
if (allowedNodes.includes(nodeName)) {
let objAttrs = {}
if (node.hasAttributes()) {
objAttrs = nodeAttributesToObj(node.attributes)
}
return {
type: nodeName.toString().toUpperCase(),
mutability: 'MUTABLE',
data: {
// Pass whatever you want here (like id, or classList, etc.)
innerText: node.innerText,
innerHTML: node.innerHTML,
attributes: objAttrs,
},
}
}
return null
}
Editor.tsx
import React from 'react'
import { Editor as Draft } from 'react-draft-wysiwyg'
import { ColorPalette } from '@allthings/colors'
import htmlToDraft from 'html-to-draftjs'
import { EditorState, ContentState, convertToRaw } from 'draft-js'
import draftToHtml from 'draftjs-to-html'
import { css } from 'glamor'
import {
entityMapperToComponent,
customChunkRenderer,
entityMapper,
} from 'utils/draft-js-helpers'
const styles = {
....
}
interface IProps {
color?: string
editorState: any
invalid?: boolean
onEditorStateChange: (state) => void
toolbar?: object
onChange?: (editorState) => void
}
interface IState {
showEditorCode: boolean
editorHTML: string
showCode: boolean
}
function customBlockRenderFunc(block, config) {
if (block.getType() === 'atomic') {
const contentState = config.getEditorState().getCurrentContent()
const entity = contentState.getEntity(block.getEntityAt(0))
return {
component: entityMapperToComponent(entity),
editable: false,
props: {
children: () => entity.innerHTML,
},
}
}
return undefined
}
class Editor extends React.Component<IProps & InjectedIntlProps, IState> {
constructor(props) {
super(props)
this.state = {
showEditorCode: false,
editorHTML: '',
showCode: false,
}
}
onEditorStateChange = editor => {
const editorHTML = draftToHtml(
convertToRaw(editor.getCurrentContent()),
null,
false,
entityMapper,
)
this.setState({ editorHTML })
}
onEditEditorHTML = ({ target: { value: editorHTML } }) =>
this.setState({ editorHTML })
toggleEditorCode = () => {
const { showEditorCode } = this.state
const { editorState } = this.props
if (!showEditorCode) {
this.onEditorStateChange(editorState)
}
this.setState({ showEditorCode: !showEditorCode })
}
addHtmlToEditor = () => {
const { editorHTML } = this.state
const contentBlock = htmlToDraft(editorHTML, customChunkRenderer)
let editor
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(
contentBlock.contentBlocks,
)
editor = EditorState.createWithContent(contentState)
} else {
editor = EditorState.createEmpty()
}
this.props.onChange(editor)
}
render() {
const {
editorState,
invalid = false,
onEditorStateChange,
toolbar,
intl,
color,
} = this.props
const { showEditorCode, editorHTML } = this.state
const ShowEditorCode = () => (
<div className="rdw-option-wrapper" onClick={this.toggleEditorCode}>
{showEditorCode
? 'Hide Code'
: 'Show Code'
</div>
)
return (
<>
<Draft
editorState={editorState}
editorStyle={styles.editorStyle(invalid)}
name="content"
onEditorStateChange={onEditorStateChange}
toolbar={toolbar}
toolbarStyle={styles.toolbarStyle}
wrapperStyle={styles.wrapperStyle}
toolbarCustomButtons={[<ShowEditorCode />]}
customBlockRenderFunc={customBlockRenderFunc}
/>
{showEditorCode && (
<div {...css({ width: '100%' })}>
<textarea
rows={10}
{...css({
width: '100%',
padding: '0',
})}
value={editorHTML}
onChange={this.onEditEditorHTML}
/>
<div>
<button
type="button"
onClick={this.addHtmlToEditor}
>
Submit
</Button>
</div>
</div>
)}
</>
)
}
}
I improve the code and separete the helper functions as shown below:
draft-js-helpers.tsx
import { Parser as HtmlToReactParser } from 'html-to-react' import DOMPurify from 'dompurify' const allowedNodes = ['div', 'table', 'style', 'img'] const styleObjToCSS = styleObj => Object.keys(styleObj).reduce((acum, style) => { return (style && styleObj[style] ? `${style}:${styleObj[style]}; ${acum}` : '' ).trim() }, '') const nodeAttributesToObj = attrs => { const objAttrs = { style: null } for (let i = attrs.length - 1; i >= 0; i--) { if (attrs[i].name !== 'style') { if (attrs[i].name && attrs[i].value) { objAttrs[attrs[i].name] = attrs[i].value } } else { const stylesInText = attrs[i].value.split(';') const styles = stylesInText.reduce((acum, style) => { const components = style.split(':') if (components[0] && components[1]) { acum[components[0]] = `${components[1]}` } return acum }, {}) objAttrs.style = styles } } return objAttrs } export function entityMapper(entity) { let type = entity.type let data = { ...entity.data } if (type === 'IMAGE') { // added to support the existing image option in the editor type = 'IMG' data = { attributes: data, innerHTML: '' } } data.attributes = data.attributes ? data.attributes : {} let styleAsAttribute if (data.attributes.style) { styleAsAttribute = styleObjToCSS(data.attributes.style) } const attributes = Object.keys(data.attributes).reduce( (acum, key) => (key === 'style' ? `${key}="${styleAsAttribute}" ${acum}` : `${key}="${data.attributes[key]}" ${acum}` ).trim(), '', ) const node = type.toLowerCase() if (allowedNodes.includes(node)) { return `<${node} ${attributes}>${data.innerHTML}</${node}>` } return '' } export function entityMapperToComponent(entity) { const htmlToReactParser = new HtmlToReactParser() return () => htmlToReactParser.parse(DOMPurify.sanitize(entityMapper(entity))) } export function customChunkRenderer(nodeName, node) { if (allowedNodes.includes(nodeName)) { let objAttrs = {} if (node.hasAttributes()) { objAttrs = nodeAttributesToObj(node.attributes) } return { type: nodeName.toString().toUpperCase(), mutability: 'MUTABLE', data: { // Pass whatever you want here (like id, or classList, etc.) innerText: node.innerText, innerHTML: node.innerHTML, attributes: objAttrs, }, } } return null }
Editor.tsx
import React from 'react' import { Editor as Draft } from 'react-draft-wysiwyg' import { ColorPalette } from '@allthings/colors' import htmlToDraft from 'html-to-draftjs' import { EditorState, ContentState, convertToRaw } from 'draft-js' import draftToHtml from 'draftjs-to-html' import { css } from 'glamor' import { entityMapperToComponent, customChunkRenderer, entityMapper, } from 'utils/draft-js-helpers' const styles = { .... } interface IProps { color?: string editorState: any invalid?: boolean onEditorStateChange: (state) => void toolbar?: object onChange?: (editorState) => void } interface IState { showEditorCode: boolean editorHTML: string showCode: boolean } function customBlockRenderFunc(block, config) { if (block.getType() === 'atomic') { const contentState = config.getEditorState().getCurrentContent() const entity = contentState.getEntity(block.getEntityAt(0)) return { component: entityMapperToComponent(entity), editable: false, props: { children: () => entity.innerHTML, }, } } return undefined } class Editor extends React.Component<IProps & InjectedIntlProps, IState> { constructor(props) { super(props) this.state = { showEditorCode: false, editorHTML: '', showCode: false, } } onEditorStateChange = editor => { const editorHTML = draftToHtml( convertToRaw(editor.getCurrentContent()), null, false, entityMapper, ) this.setState({ editorHTML }) } onEditEditorHTML = ({ target: { value: editorHTML } }) => this.setState({ editorHTML }) toggleEditorCode = () => { const { showEditorCode } = this.state const { editorState } = this.props if (!showEditorCode) { this.onEditorStateChange(editorState) } this.setState({ showEditorCode: !showEditorCode }) } addHtmlToEditor = () => { const { editorHTML } = this.state const contentBlock = htmlToDraft(editorHTML, customChunkRenderer) let editor if (contentBlock) { const contentState = ContentState.createFromBlockArray( contentBlock.contentBlocks, ) editor = EditorState.createWithContent(contentState) } else { editor = EditorState.createEmpty() } this.props.onChange(editor) } render() { const { editorState, invalid = false, onEditorStateChange, toolbar, intl, color, } = this.props const { showEditorCode, editorHTML } = this.state const ShowEditorCode = () => ( <div className="rdw-option-wrapper" onClick={this.toggleEditorCode}> {showEditorCode ? 'Hide Code' : 'Show Code' </div> ) return ( <> <Draft editorState={editorState} editorStyle={styles.editorStyle(invalid)} name="content" onEditorStateChange={onEditorStateChange} toolbar={toolbar} toolbarStyle={styles.toolbarStyle} wrapperStyle={styles.wrapperStyle} toolbarCustomButtons={[<ShowEditorCode />]} customBlockRenderFunc={customBlockRenderFunc} /> {showEditorCode && ( <div {...css({ width: '100%' })}> <textarea rows={10} {...css({ width: '100%', padding: '0', })} value={editorHTML} onChange={this.onEditEditorHTML} /> <div> <button type="button" onClick={this.addHtmlToEditor} > Submit </Button> </div> </div> )} </> ) } }
it is working wonderfully but now i am not getting the LCR alignment option on hover of image. I want to align image Left right center, could you please help resolve this issue. Thanks for the above snippet.
i am using above code but style tag is not supporting, it is automatically removing
Hello,
I'm trying to get a custom option in the toolbar to add HTML (here a specific image).
Any idea to get this work? There is an image option in the toolbar, so I guess it should also be possible to add an image programmatically.