From d42e51ee6df1428b5f06c3ef96244326622337fe Mon Sep 17 00:00:00 2001 From: Gordon Dent <2034973+aforismesen@users.noreply.github.com> Date: Thu, 11 Jan 2018 09:40:26 -0800 Subject: [PATCH] Preserve list indentation from one DraftEditor to another on paste Summary: **Summary** When pasting from one editor to another we lose list indentation. By inspecting the classList of each list item contained in HTML pasted we can preserve this state. **Test Plan** I validated behavior manually: - Rebuild - Open two tabs to rich text example - In one create indented list - Select all - Paste into other - Styling is preserved I also added some snapshot testing for regression testing. Closes https://github.com/facebook/draft-js/pull/1605 Reviewed By: flarnie Differential Revision: D6702583 Pulled By: flarnie fbshipit-source-id: 071cecc786e2aaef11af9bd21cc03f9b2544a2b4 --- ...onvertFromHTMLToContentBlocks-test.js.snap | 360 ++++++++++++++++++ .../convertFromHTMLToContentBlocks-test.js | 30 ++ .../convertFromHTMLToContentBlocks.js | 30 ++ 3 files changed, 420 insertions(+) diff --git a/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap b/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap index 02fa448..7b2bafa 100644 --- a/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap +++ b/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap @@ -1,5 +1,365 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Should import recognised draft li depths 1`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key0", + "text": "depth0", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 1, + "key": "key2", + "text": "depth1", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 2, + "key": "key4", + "text": "depth2", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 3, + "key": "key6", + "text": "depth3", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 4, + "key": "key8", + "text": "depth4", + "type": "unordered-list-item", + }, +] +`; + +exports[`Should import recognised draft li depths when nesting enabled 1`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key0", + "nextSibling": "key1", + "parent": null, + "prevSibling": null, + "text": "depth0", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key1", + "nextSibling": "key2", + "parent": null, + "prevSibling": "key0", + "text": "depth1", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key2", + "nextSibling": "key3", + "parent": null, + "prevSibling": "key1", + "text": "depth2", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key3", + "nextSibling": "key4", + "parent": null, + "prevSibling": "key2", + "text": "depth3", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key4", + "nextSibling": null, + "parent": null, + "prevSibling": "key3", + "text": "depth4", + "type": "unordered-list-item", + }, +] +`; + exports[`Should not create empty container blocks around ol and their list items 1`] = ` Array [ Object { diff --git a/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js b/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js index df81e74..9fd122a 100644 --- a/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js +++ b/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js @@ -279,3 +279,33 @@ test('Should preserve entities for whitespace-only content', () => { experimentalTreeDataSupport: false, }); }); + +test('Should import recognised draft li depths', () => { + const html_string = ` + + `; + assertConvertFromHTMLToContentBlocks(html_string, { + experimentalTreeDataSupport: false, + }); +}); + +test('Should import recognised draft li depths when nesting enabled', () => { + const html_string = ` + + `; + assertConvertFromHTMLToContentBlocks(html_string, { + experimentalTreeDataSupport: true, + }); +}); diff --git a/src/model/encoding/convertFromHTMLToContentBlocks.js b/src/model/encoding/convertFromHTMLToContentBlocks.js index 0b32f32..460de8f 100644 --- a/src/model/encoding/convertFromHTMLToContentBlocks.js +++ b/src/model/encoding/convertFromHTMLToContentBlocks.js @@ -30,6 +30,7 @@ const Immutable = require('immutable'); const {Set} = require('immutable'); const URI = require('URI'); +const cx = require('cx'); const generateRandomKey = require('generateRandomKey'); const getSafeBodyFromHTML = require('getSafeBodyFromHTML'); const invariant = require('invariant'); @@ -84,6 +85,14 @@ const inlineTags = { u: 'UNDERLINE', }; +const knownListItemDepthClasses = { + [cx('public/DraftStyleDefault/depth0')]: 0, + [cx('public/DraftStyleDefault/depth1')]: 1, + [cx('public/DraftStyleDefault/depth2')]: 2, + [cx('public/DraftStyleDefault/depth3')]: 3, + [cx('public/DraftStyleDefault/depth4')]: 4, +}; + const anchorAttr = ['className', 'href', 'rel', 'target', 'title']; const imgAttr = ['alt', 'className', 'height', 'src', 'width']; @@ -333,6 +342,19 @@ const getBlockDividerChunk = ( }; }; +/** + * If we're pasting from one DraftEditor to another we can check to see if + * existing list item depth classes are being used and preserve this style + */ +const getListItemDepth = (node: HTMLElement, depth: number = 0): number => { + Object.keys(knownListItemDepthClasses).some(depthClass => { + if (node.classList.contains(depthClass)) { + depth = knownListItemDepthClasses[depthClass]; + } + }); + return depth; +}; + const genFragment = ( entityMap: EntityMap, node: Node, @@ -444,6 +466,14 @@ const genFragment = ( lastList = nodeName; } + if ( + !experimentalTreeDataSupport && + nodeName === 'li' && + node instanceof HTMLElement + ) { + depth = getListItemDepth(node, depth); + } + const blockType = getBlockTypeForTag(nodeName, lastList, blockRenderMap); const inListBlock = lastList && inBlock === 'li' && nodeName === 'li'; const inBlockOrHasNestedBlocks =