Draft-js: ConvertFromHTML images are converted to unstyled rather than Atomic blocks

Created on 28 Nov 2016  路  12Comments  路  Source: facebook/draft-js

Do you want to request a feature or report a bug?
bug

What is the current behavior?

img converted with convertFromHTML are converted to unstyled div.

<div class="" data-block="true" data-editor="48f1g" data-offset-key="2g03k-0-0">
    <div data-offset-key="2g03k-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr">
        <img src="image.png" height="112" width="200">
    </div>
</div>

*If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. You can use this jsfiddle to get started: *

This is the current behaviour on the master branch convertFromHTML example.

What is the expected behavior?

Should convert to atomic and use customRenderFn

Which versions of Draft.js, and which browser / OS are affected by this issue? Did this work in previous versions of Draft.js?

Master branch of Draft.js

Most helpful comment

Hi is still have the same problem like @apuntovanini. I get the following back if I run above code:
My HTML:
<p><img src="https://octodex.github.com/images/stormtroopocat.jpg" alt="Stormtroopocat" /></p>

return from convertFromHTML:
{"contentBlocks":[{"key":"adbln","type":"unstyled","text":"馃摲","characterList":[{"style":[],"entity":"2"},{"style":[],"entity":"2"}],"depth":0,"data":{}}],"entityMap":{}}
I'm running the latest version of draft-js 0.10.4

All 12 comments

I've also run into this problem, and resolved via a _dirty hack_: https://github.com/facebook/draft-js/issues/685 .

@RaoHai I fixed my issue my editing the contentBlock after the conversion. You can see the code below. I want to edit the mutability of the entity of the images to "IMMUTABLE" but don't think that is possible.

const sampleMarkup =
      '<p><img src="https://hackpacific.s3.amazonaws.com/website-images/homepage-hero.png" /></p>' +
      '<p><a href="http://www.facebook.com">Example link</a><br /><br/ ><p>' +
      '<b>Bold text</b>, <i>Italic text</i><br/ ><br />';

      const blocksFromHTML = CustomConvertFromHTML(sampleMarkup);

      const defaultConvertedContentState = ContentState.createFromBlockArray(
        blocksFromHTML.contentBlocks,
        blocksFromHTML.entityMap,
      );

      const customConvertedContentState = CustomContentStateConverter(defaultConvertedContentState);

      const initialEditorState = EditorState.createWithContent(
        customConvertedContentState,
        decorator,
      );

CustomConvertFromHTML: This solves the issue of adjacent paragraphs being combined into one contentBlock.

export const CustomConvertFromHTML = (html) => {
  // Correctly seperates paragraphs into their own blocks
  const blockRenderMap = DefaultDraftBlockRenderMap.set('p', { element: 'p' });
  const blocksFromHTML = convertFromHTML(html, getSafeBodyFromHTML, blockRenderMap);
  blocksFromHTML.contentBlocks = blocksFromHTML.contentBlocks.map(block => (block.get('type') === 'p' ? block.set('type', 'unstyled') : block));

  return blocksFromHTML;
};

CustomContentStateConverter: This looks for contentBlocks with "IMAGE" entities and convert the Block type to 'atomic'. I want to change the entity to "IMMUTABLE" but right now we can only edit entity data.

export const CustomContentStateConverter = (contentState) => {
  // Correctly assign properties to images and links
  const newBlockMap = contentState.getBlockMap().map((block) => {
    const entityKey = block.getEntityAt(0);
    if (entityKey !== null) {
      const entityBlock = contentState.getEntity(entityKey);
      const entityType = entityBlock.getType();
      switch (entityType) {
        case 'IMAGE': {
          const newBlock = block.merge({
            type: 'atomic',
            text: 'img',
          });
          // const newContentState = contentState.mergeEntityData(entityKey, { mutability: 'IMMUTABLE' });
          return newBlock;
        }
        default:
          return block;
      }
    }
    return block;
  });
  const newContentState = contentState.set('blockMap', newBlockMap);

  return newContentState;
};

I am probably not following and this might be a more confusing issue than I realise, but if you are trying to use ConvertFromHTML for images the following example is working well for me. https://github.com/facebook/draft-js/tree/master/examples/draft-0-10-0/convertFromHTML It is using the master branch though.

Looks like this has been resolved so I'm closing it out. Feel free to reopen if that's not the case.

Hi, tried with master branch and it doesn't seem to work. I'm using the same example @StevenIseki posted as a guide, but images are apparently not considered as atomic. @xiaopow can you confirm whether the issue is still open?

Hi is still have the same problem like @apuntovanini. I get the following back if I run above code:
My HTML:
<p><img src="https://octodex.github.com/images/stormtroopocat.jpg" alt="Stormtroopocat" /></p>

return from convertFromHTML:
{"contentBlocks":[{"key":"adbln","type":"unstyled","text":"馃摲","characterList":[{"style":[],"entity":"2"},{"style":[],"entity":"2"}],"depth":0,"data":{}}],"entityMap":{}}
I'm running the latest version of draft-js 0.10.4

Hi is still have the same problem like @apuntovanini. I get the following back if I run above code:
My HTML:
<p><img src="https://octodex.github.com/images/stormtroopocat.jpg" alt="Stormtroopocat" /></p>

return from convertFromHTML:
{"contentBlocks":[{"key":"adbln","type":"unstyled","text":"馃摲","characterList":[{"style":[],"entity":"2"},{"style":[],"entity":"2"}],"depth":0,"data":{}}],"entityMap":{}}
I'm running the latest version of draft-js 0.10.4

@ehs035 Hello, I am also facing the same issues instead of getting inline styles i am getting characterList . Did you ever get this working or find a solution ?

I'm still facing the issue, any solution guys ?

@aslamanver Good to see that I'm not the only one in 2020 ;)
here's my workaround for that problem
my dependencies

import MUIRichTextEditor from 'mui-rte';
import { convertFromHTML, ContentState, convertToRaw } from 'draft-js';
import draftToHtml from 'draftjs-to-html';

and converting html to editors state (mui-rte requires stringfied object) part:

let editorDefaultValue = null;
if (initialData?.text) {
    const contentHTML = convertFromHTML(initialData.text);
    const state = ContentState.createFromBlockArray(contentHTML.contentBlocks, contentHTML.entityMap)
    const raw = convertToRaw(state);
    const { blocks, entityMap } = raw;

    const fixedBlocks = blocks.map((block) => {

      return {
        ...block,
        type: block.text === '馃摲' ? 'atomic' : block.type
      }
    })

   const fixedEntityMap = {}
    for (const [key, value] of Object.entries(entityMap)) {
      if (value.type === 'IMAGE') {
        value.data.url = value.data.src
      }

      fixedEntityMap[key] = value;
    }

    editorDefaultValue = JSON.stringify({
      blocks: fixedBlocks,
      entityMap:fixedEntityMap
    });
  }

I know that might be discussable to rely on emoji comparison, but at least it works for now ;)
And fixing entities is because somehow editor displays images src from 'url' property.

hi I seem to be havinng a similar problem - I im currently using draftjs and sending the html to a 'note' object that displays it as a note [its a sticky note app] but when i try editing the html by sending it back to the draftjs editor everything is except for the fact that images are being returned a an image emoji 馃摲 instead of an actual img

This is still an issue. The convertFromHTML does not translate images into an atomic block but an unstyled block. I am trying to use that in conjunction with the draft-js-image-plugin which expects images to have an atomic block type.

 blockRendererFn: (block, { getEditorState }) => {
      if (block.getType() === 'atomic') {
        const contentState = getEditorState().getCurrentContent();
        const entity = block.getEntityAt(0);
        if (!entity) return null;
        const type = contentState.getEntity(entity).getType();
        if (type === 'IMAGE' || type === 'image') {
          return {
            component: ThemedImage,
            editable: false,
          };
        }
        return null;
      }

      return null;
    }

https://github.com/draft-js-plugins/draft-js-plugins/blob/master/packages/image/src/index.tsx

I guess the question is what is the proper type for an image and perhaps if it isn't of type atomic why is that? @KubaZachacz has a solution but it is very much a workaround, it would be nice to have an actual solution @tylercraft, could this be reopen?

Also a bit of a caveat with @KubaZachacz work around is that it assumes that the camera icon was in a block by itself, which if there wasn't a space entered between the text and the image you may end up in a case where the camera icon and text are in the same block which you be problematic for that work around.

This is the work around I came up with, which somewhat ugly but work great for now:

const {
  EditorState,
  convertToRaw,
  convertFromRaw,
  DefaultDraftBlockRenderMap,
  ContentState,
  convertFromHTML,
  getSafeBodyFromHTML
} = require('draft-js');

const Immutable = require('immutable');
const clone = require('rfdc')();

module.exports.editorStateFromHTML = htmlBody => {
  console.log('HTML ---> EDITOR ::: RAW BODY', htmlBody);
  const blockRenderMap = Immutable.Map({
   //making a new type of block called image that will later substitute to `atomic`
    image: {
      element: 'img'
    }
  });

  const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(
    blockRenderMap
  );

  const blocksFromHTML = convertFromHTML(
    htmlBody,
    getSafeBodyFromHTML,
    extendedBlockRenderMap
  );

  const state = ContentState.createFromBlockArray(
    blocksFromHTML.contentBlocks,
    blocksFromHTML.entityMap
  );
  const { blocks, entityMap } = convertToRaw(state);
  const imgCount = blocks.filter(b => b.type === 'image').length;

  // if img tags have been detected do the below
  if (imgCount > 0) {
    const fixedEntityMap = clone(entityMap);
    let arrKeys = Object.keys(fixedEntityMap);
    arrKeys = arrKeys.map(k => parseInt(k, 10));
    const lastKey = Math.max(...arrKeys);
    let blockCounter = lastKey;

    const fixedContentBlocks = blocks.map(blck => {
      if (blck.type === 'image') {
        blockCounter += 1;
        return {
          ...blck,
          text: ' ',
          type: 'atomic', //overriding the image block for atomic
          entityRanges: [{ offset: 0, length: 1, key: blockCounter }]
        };
      }
      return blck;
    });

    const blocksFromHTML2 = convertFromHTML(htmlBody);
    const state2 = ContentState.createFromBlockArray(
      blocksFromHTML2.contentBlocks,
      blocksFromHTML2.entityMap
    );
    const { entityMap: imgEntities } = convertToRaw(state2);

    let entityCounter = lastKey;
    // eslint-disable-next-line no-restricted-syntax
    for (const [key, value] of Object.entries(imgEntities)) {
      if (value.type === 'IMAGE') {
        entityCounter += 1;
        fixedEntityMap[entityCounter] = value;
      }
    }

    const editorDefaultValue = {
      blocks: fixedContentBlocks,
      entityMap: fixedEntityMap
    };
    return EditorState.createWithContent(convertFromRaw(editorDefaultValue));
  }

  return EditorState.createWithContent(state);
};

Was this page helpful?
0 / 5 - 0 ratings