Skip to content

Commit

Permalink
Merge pull request #529 from Yoast/stories/fix-console-warning-when-o…
Browse files Browse the repository at this point in the history
…pening-replace-var-suggestions-in-react-snippet-editor

Update DraftJS Mention Plugin.
  • Loading branch information
boblinthorst authored May 16, 2018
2 parents 686732b + 4bf9ebb commit 6331ac5
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 77 deletions.
4 changes: 2 additions & 2 deletions app/SnippetEditorExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export default class SnippetEditorExample extends Component {

this.state = {
title: "Welcome to the Gutenberg Editor - Local WordPress Dev. Snippet Title Snippet" +
" Title Snippet Title Snippet Title Snippet Title Snippet Title Snippet Title Snippet" +
" Title Snippet Title Snippet Title Snippet Title Snippet Title",
" Snippet: %%snippet%% Title: %%title%% Manual: %%snippet_manual%% Type: %%post_type%%" +
" %%these%% %%are%% %%not%% %%tags%% and throw in some % here %%%%%%% and %%there too%%",
url: "https://local.wordpress.test/welcome-to-the-gutenberg-editor-2/",
slug: "welcome-to-the-gutenberg-editor-2",
description: "Merci, merçi, Of Mountains & Printing Presses The goal of this new editor is to make" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { serializeEditor, unserializeEditor } from "../serialization";
* @returns {EditorState} The editor state.
*/
const createEditorState = flow( [
unserializeEditor,
convertFromRaw,
EditorState.createWithContent,
] );
Expand Down Expand Up @@ -67,11 +66,12 @@ class ReplacementVariableEditor extends React.Component {
constructor( props ) {
super( props );

const { content: rawContent } = this.props;
const { content: rawContent, replacementVariables } = this.props;
const unserialized = unserializeEditor( rawContent, replacementVariables );

this.state = {
editorState: createEditorState( rawContent ),
replacementVariables: props.replacementVariables,
editorState: createEditorState( unserialized ),
replacementVariables,
};

/*
Expand Down
156 changes: 106 additions & 50 deletions composites/Plugin/SnippetEditor/serialization.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
const ENTITY_FORMAT = /%%([a-zA-Z_]+)%%/;
import forEach from "lodash/forEach";
import reduce from "lodash/reduce";
import sortBy from "lodash/sortBy";
import sortedIndexBy from "lodash/sortedIndexBy";
import trim from "lodash/trim";

const CIRCUMFIX = "%%";

/**
* Serializes an entity into a string.
* Serializes a tag into a string.
*
* @param {string} name The name of the entity.
* @param {string} name The name of the tag.
*
* @returns {string} Serialized entity.
* @returns {string} Serialized tag.
*/
export function serializeEntity( name ) {
return "%%" + name + "%%";
export function serializeTag( name ) {
return CIRCUMFIX + name + CIRCUMFIX;
}

/**
Expand All @@ -23,14 +29,17 @@ export function serializeBlock( entityMap, block ) {
const { text, entityRanges } = block;
let previousEntityEnd = 0;

let serialized = entityRanges.reduce( ( serialized, entityRange ) => {
// Ensure the entityRanges are in order from low to high offset.
const sortedEntityRanges = sortBy( entityRanges, "offset" );
let serialized = reduce( sortedEntityRanges, ( serialized, entityRange ) => {
const { key, length, offset } = entityRange;
const beforeEntityLength = offset - previousEntityEnd;

const beforeEntity = text.substr( previousEntityEnd, beforeEntityLength );
const serializedEntity = serializeEntity( entityMap[ key ].data.mention.get( "name" ) );
const serializedEntity = serializeTag( entityMap[ key ].data.mention.name );

previousEntityEnd = offset + length;

return serialized + beforeEntity + serializedEntity;
}, "" );

Expand All @@ -49,11 +58,22 @@ export function serializeBlock( entityMap, block ) {
export function serializeEditor( rawContent ) {
const { blocks, entityMap } = rawContent;

return blocks.reduce( ( serialized, block ) => {
return reduce( blocks, ( serialized, block ) => {
return serialized + serializeBlock( entityMap, block );
}, "" );
}

/**
* Unserializes a tag into a string.
*
* @param {string} serializedTag The serialized tag.
*
* @returns {string} Unserialized tag.
*/
export function unserializeTag( serializedTag ) {
return trim( serializedTag, CIRCUMFIX );
}

/**
* Unserializes an entity to DraftJS data.
*
Expand All @@ -64,69 +84,105 @@ export function serializeEditor( rawContent ) {
* @returns {Object} The serialized entity.
*/
export function unserializeEntity( key, name, offset ) {
const length = name.length;

const entityRange = {
key,
offset,
length,
length: name.length,
};

const mappedEntity = {
data: {
mention: new Map( [
[ "name", name ],
[ "description", "%%" + name + "%%" ],
] ),
mention: {
name,
},
},
mutability: "IMMUTABLE",
type: "%%mention",
type: "%mention",
};

return { entityRange, mappedEntity };
}

/**
* Find all indices of a search term in a string.
*
* @param {string} searchTerm The term to search for.
* @param {string} text The text to search in.
*
* @returns {Array} Array of found indices.
*/
const getIndicesOf = ( searchTerm, text ) => {
if ( searchTerm.length === 0 ) {
return [];
}

let startIndex = 0;
let index;
const indices = [];

while ( ( index = text.indexOf( searchTerm, startIndex ) ) > -1 ) {
indices.push( index );
startIndex = index + searchTerm.length;
}

return indices;
};

/**
* Unserializes a piece of content into DraftJS data.
*
* @param {string} content The content to unserialize.
* @param {Array} tags The tags for the DraftJS mention plugin.
*
* @returns {Object} The raw data ready for convertFromRaw.
*/
export function unserializeEditor( content ) {
export function unserializeEditor( content, tags ) {
const entityRanges = [];
const entityMap = {};
let entity, entityName, fullEntity;

do {
entity = ENTITY_FORMAT.exec( content );

if ( entity ) {
fullEntity = entity[ 0 ];
entityName = entity[ 1 ];

let offset = entity.index;

let key = entityRanges.length;

const { entityRange, mappedEntity } = unserializeEntity( key, entityName, offset );

entityRanges.push( entityRange );
entityMap[ key ] = mappedEntity;

const before = content.substr( 0, offset );
const between = content.substr( offset, fullEntity.length ).replace( /%%/g, "" );
const after = content.substr( offset + fullEntity.length );

content = before + between + after;
}
} while ( entity );

const blocks = [
{
entityRanges,
text: content,
},
];
const replaceIndices = [];

// Collect the replace indices for each tag.
forEach( tags, tag => {
const tagValue = serializeTag( tag.name );
const indices = getIndicesOf( tagValue, content );

forEach( indices, index => {
const replaceIndex = {
index,
tag,
tagValue,
};

// Add the replace index in order.
const insertAt = sortedIndexBy( replaceIndices, replaceIndex, "index" );
replaceIndices.splice( insertAt, 0, replaceIndex );
} );
} );

// Loop from high to low to ensure the index is still correct.
for( let i = replaceIndices.length - 1; i >= 0; i-- ) {
const { index, tag, tagValue } = replaceIndices[ i ];

// Replace the serialized tag with the unserialized tag.
const before = content.substr( 0, index );
const between = unserializeTag( content.substr( index, tagValue.length ) );
const after = content.substr( index + tagValue.length );
content = before + between + after;

// Decrease the offset by twice the length of the circumfix for every index we replace.
const offset = index - i * CIRCUMFIX.length * 2;
const key = entityRanges.length;

// Create the DraftJS data.
const { entityRange, mappedEntity } = unserializeEntity( key, tag.name, offset );
entityRanges.push( entityRange );
entityMap[ key ] = mappedEntity;
}

const blocks = [ {
entityRanges,
text: content,
} ];

return {
blocks,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ exports[`ReplacementVariableEditor wraps a DraftJS editor instance 1`] = `
}
stripPastedStyles={true}
/>
<Decorated(Component)
<Decorated(MentionSuggestions)
onSearchChange={[Function]}
/>
</React.Fragment>
Expand Down
42 changes: 28 additions & 14 deletions composites/Plugin/SnippetEditor/tests/serializationTest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { serializeEditor, unserializeEditor } from "../serialization";

const TAGS = [
{ name: "title", value: "Title" },
{ name: "post_type", value: "Gallery" },
];


describe( "editor serialization", () => {
it( "transforms the deep structure to a plain string", () => {
const structure = {
Expand All @@ -10,22 +16,26 @@ describe( "editor serialization", () => {
depth: 0,
inlineStyleRanges: [],
entityRanges: [ {
offset: 6,
length: 9,
key: 0,
}, {
offset: 0,
length: 5,
key: 0,
}, { offset: 6, length: 9, key: 1 } ],
key: 1,
} ],
data: {},
} ],
entityMap: {
0: {
type: "%%mention",
type: "%mention",
mutability: "IMMUTABLE",
data: { mention: new Map( [ [ "name", "title" ], [ "description", "%%title%%" ] ] ) },
data: { mention: { name: "post_type" } },
},
1: {
type: "%%mention",
type: "%mention",
mutability: "IMMUTABLE",
data: { mention: new Map( [ [ "name", "post_type" ], [ "description", "%%post_type%%" ] ] ) },
data: { mention: { name: "title" } },
},
},
};
Expand All @@ -44,26 +54,30 @@ describe( "editor unserialization", () => {
blocks: [ {
text: "title post_type test test123",
entityRanges: [ {
offset: 6,
length: 9,
key: 0,
}, {
offset: 0,
length: 5,
key: 0,
}, { offset: 6, length: 9, key: 1 } ],
key: 1,
} ],
} ],
entityMap: {
0: {
type: "%%mention",
type: "%mention",
mutability: "IMMUTABLE",
data: { mention: new Map( [ [ "name", "title" ], [ "description", "%%title%%" ] ] ) },
data: { mention: { name: "post_type" } },
},
1: {
type: "%%mention",
type: "%mention",
mutability: "IMMUTABLE",
data: { mention: new Map( [ [ "name", "post_type" ], [ "description", "%%post_type%%" ] ] ) },
data: { mention: { name: "title" } },
},
},
};

const actual = unserializeEditor( input );
const actual = unserializeEditor( input, TAGS );

expect( actual ).toEqual( expected );
} );
Expand All @@ -72,7 +86,7 @@ describe( "editor unserialization", () => {
const input = "The first thing, %%title%%, %%post_type%% type.";
const expected = input;

const actual = serializeEditor( unserializeEditor( input ) );
const actual = serializeEditor( unserializeEditor( input, TAGS ) );

expect( actual ).toBe( expected );
} );
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"algoliasearch": "^3.22.3",
"debug": "^3.0.0",
"draft-js": "^0.10.5",
"draft-js-mention-plugin": "^2.0.1",
"draft-js-mention-plugin": "^3.0.4",
"draft-js-plugins-editor": "^2.0.4",
"grunt-scss-to-json": "^1.0.1",
"interpolate-components": "^1.1.0",
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2442,12 +2442,12 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"

draft-js-mention-plugin@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/draft-js-mention-plugin/-/draft-js-mention-plugin-2.0.1.tgz#388e39861df25d61c3de577799a69c92374e206a"
draft-js-mention-plugin@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/draft-js-mention-plugin/-/draft-js-mention-plugin-3.0.4.tgz#e8530b7c16f23ce5b980515458418b5e24376f4a"
dependencies:
decorate-component-with-props "^1.0.2"
find-with-regex "^1.0.2"
find-with-regex "^1.1.3"
immutable "~3.7.4"
lodash.escaperegexp "^4.1.2"
prop-types "^15.5.8"
Expand Down Expand Up @@ -3169,7 +3169,7 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"

find-with-regex@^1.0.2, find-with-regex@^1.1.3:
find-with-regex@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/find-with-regex/-/find-with-regex-1.1.3.tgz#d6c6f2debee898d36b6a77e05709b13dd5dc8a26"

Expand Down

0 comments on commit 6331ac5

Please sign in to comment.