Skip to main content

rich-content-preview

Introduction#

The Preview project comes to meet the requirement for a brief content preview; similar concept could be found in social media applications.

Architecture#

The core idea is to transform the content state and to display it within RichContentViewer. This transformation is a product of content state metadata and a set of rules defined by the consumer. This section discusses the main logic elements required to perform such a transformation.

Content State Analyzer#

The content state analyzer retrieves content metadata containing the info regarding the data rather draft block and entity structure.

For example, the metadata could provide the following details:

  • number of images and videos
  • length of text (in total and per block)
  • lists and code-blocks consisting of multiple sequential blocks are retrieved as a whole

Content State Builder#

The content state builder provides a way to create a preview content state using the retrieved metadata. The preview state then can be rendered by RichContentViewer.

Rule Engine#

The rule engine provides a way to define transformation rules and to apply them on a given ContentState according to the metadata.

Transformation Rule#

The transformation rule consists of condition and transform action.

Example:

if: metadata => metadata.images.length >= 4
then: (metadata, preview) => preview.gallery({
items: metadata.images.slice(0, 4),
config: { layout: 'grid', imageWidth: 40 }
})

Preview Interactions#

The preview interactions allow to expand hidden content. The rich-content repo introduces 3 predefined interactions. The Content State Builder exposes methods to add interactions to the preview state, and the RichContentViewer renders the interaction components.

'Read More'#

The Read More interacton relies on react-lines-ellipsis package. It displays required number of lines appended by an ellipsis. Although the package is capable to handle the HTML content, this feature is experimental. The current interacton is limited to work with plain text only. Mouse click expands the full post.

'See Full Post'#

The See Full Post displays a text label over an overlay. Mouse click expands the full post.

Image Counter#

The Image Counter renders hidden image counter. By default, the counter label placed above the last visible image element. The image elements are selected by role='img' attribute.

APIs#

getContentStateMetadata#

The metadata is derived from ContentState and exposes its details:

const metadata = getContentStateMetadata(contentState);

These details are categorized by content type rather actual ContentState structure.

ContentStateMetadata.allText#

The ContentStateMetadata.allText exposes all the textual content a string array.

ContentStateMetadata.plain#

The ContentStateMetadata.plain exposes the textual content extracted from the unstyled blocks as a string array.

ContentStateMetadata.ol#

The ContentStateMetadata.ol exposes the textual content extracted from the ordered list blocks as a string array.

ContentStateMetadata.ul#

The ContentStateMetadata.ul exposes the textual content extracted from the unordered list blocks as a string array.

ContentStateMetadata.quotes#

The ContentStateMetadata.quotes exposes the textual content extracted from the blockquote blocks as a string array.

ContentStateMetadata.code#

The ContentStateMetadata.code exposes the textual content extracted from the code-block blocks as a string array.

ContentStateMetadata.h1/h2/h3/h4/h5/h6#

The ContentStateMetadata.h1/h2/h3/h4/h5/h6 exposes the textual content extracted from the heading blocks as a string array.

ContentStateMetadata.images#

The ContentStateMetadata.images exposes the data extracted from image / gallery / giphy entities. The image data object structure is:

{
url: string,
width: integer,
height: integer,
thumbnail?: string,
link?: {
url: string,
target: string,
rel: string,
},
metadata?: {
alt: string,
caption: string,
}
}

The properties marked with ? are optional and relevant only for some of the image entities.

ContentStateMetadata.videos#

The ContentStateMetadata.videos exposes the data extracted from video / sound-cloud / youtube entities.

The video data object structure is:

{
url: string,
width: integer,
height: integer,
}

ContentStateMetadata.files#

The ContentStateMetadata.files exposes the data extracted from file-upload entities.

The file object structure:

{
name: string,
fileType: string,
url: string,
}

ContentStateMetadata.maps#

The ContentStateMetadata.maps exposes the data extracted from google-map entities. The map data structure is similar to mapSettings object structure (found in the map entity data).

ContentStateBuilder#

The ContentStateBuilder exposes API for ContentState generation, based on the Builder design pattern.

Content generation methods#

The basic ContentStateBuilder methods allow to add content elements to the constructed ContentState. The methods named after the content type being added, e.g. is image method allows to add an image, and plain method adds plain text. The methods could be chained:

const previewState = new ContentStateBuilder(initialState)
.image(imageData, config)
.gallery(items, config)
.plain(text);
ContentStateBuilder.plain(text, config)#

The ContentStateBuilder.plain method appends an unstyled block with given text. If the text param is a string array, then it will append block for each string. The optional config is merged to the block data.

ContentStateBuilder.ul(text, config)#

The ContentStateBuilder.ul method appends an unordered-list-item block with given text. If the text param is a string array, then it will append block for each string. The optional config is merged to the block data.

ContentStateBuilder.ol(text, config)#

The ContentStateBuilder.ol method appends an ordered-list-item block with given text. If the text param is a string array, then it will append block for each string. The optional config is merged to the block data.

ContentStateBuilder.code(text, config)#

The ContentStateBuilder.code method appends a code-block block with given text. If the text param is a string array, then it will append block for each string. The optional config is merged to the block data.

ContentStateBuilder.quote(text, config)#

The ContentStateBuilder.quote method appends a blockquote block with given text. If the text param is a string array, then it will append block for each string. The optional config is merged to the block data.

ContentStateBuilder.h1/h2/h3/h4/h5/h6(text, config)#

The ContentStateBuilder.h1/h2/h3/h4/h5/h6 methods append a header block with given text. If the text param is a string array, then it will append block for each string. The optional config is merged to the block data.

ContentStateBuilder.image({ mediaInfo, config, overrides })#

The ContentStateBuilder.image method appends an atomic block to the blocks and a wix-draft-plugin-image entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The mediaInfo param is expected to be an image data object returned by ContentStateMetadata.images method.

ContentStateBuilder.giphy({ mediaInfo, config, overrides })#

The ContentStateBuilder.giphy method appends an atomic block to the blocks and a wix-draft-plugin-giphy entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The mediaInfo param is expected to be an image data object returned by ContentStateMetadata.images method.

ContentStateBuilder.gallery({ mediaInfo, config, overrides })#

The ContentStateBuilder.gallery method appends an atomic block to the blocks and a wix-draft-plugin-gallery entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The items param is expected to be an array of image data objects returned by ContentStateMetadata.images method.

ContentStateBuilder.video({ mediaInfo, config, overrides })#

The ContentStateBuilder.video method appends an atomic block to the blocks and a wix-draft-plugin-video entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The mediaInfo param is expected to be a video data object returned by ContentStateMetadata.videos method.

ContentStateBuilder.soundCloud({ mediaInfo, config, overrides })#

The ContentStateBuilder.soundCloud method appends an atomic block to the blocks and a wix-draft-plugin-sound-cloud entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The mediaInfo param is expected to be a video data object returned by ContentStateMetadata.videos method.

ContentStateBuilder.file({ mediaInfo, config, overrides })#

The ContentStateBuilder.file method appends an atomic block to the blocks and a wix-draft-plugin-file-upload entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The mediaInfo param is expected to be a file data object returned by ContentStateMetadata.files method.

ContentStateBuilder.map({ mediaInfo, config, overrides })#

The ContentStateBuilder.map method appends an atomic block to the blocks and a wix-draft-plugin-map entity to the entityMap. The optional params config and overrides are merged with the entity.data.config and entity.data, respectively. The mediaInfo parameter is expected to be a map data object returned by ContentStateMetadata.maps method.

ContentStateTransformation#

The ContentStateTransformation represents a rule to be applied on content state in order to achieve the desired preview state:

const transformation = new ContentStateTransformation({ _if, _then, initialPreviewState });
const previewState = transformation.apply(contentState);

In the code fragment above, the _if and _then arguments are functions following the convention:

_if = ContentStateMetadata => boolean;
_then = (ContentStateMetadata, ContentStateBuilder) => ContentStateBuilder;

The optional argument initialPreviewState allows to pass an initial preview state.

The following fragment demonstrates the "if content images > 3 => add a gallery with 3 items" rule definition:

const transformation = new ContentStateTransformation({
_if: metadata => metadata.images.length > 3,
_then: (metadata, preview) =>
preview.gallery({
mediaInfo: metadata.images.slice(0, 3),
}),
});
const preview = transformation.apply(contentState);

The rule method allows to chain multiple rules. They will be applied one after another:

const transformation = new ContentStateTransformation({ _if: ..., _then: ... })
.rule({ _if: ..., _then: ... })
.rule({ _if: ..., _then: ... });

Content Interactions#

The content interaction indicate that the preview content is collapsed, and provide a way to expand it. Content interactions work at the block level. Every interaction consists of the following elements:

  • ContentStateBuilder API allows a consumer to add an interaction to the preview state and configure it. These APIs can be chained just like any other ContentStateBuilder method:

    builder.plain('some looong text').readMore({ lines: 5 }).image({ mediaInfo: {...} })

    In this example, the readMore interaction is being applied to the previous plain block.

  • block data interactions array contains configuration data for all the interactions applied to the block

  • UI component defines the appearance of the interaction element

Predefined Interactions#

ReadMore#

The ReadMore component comes to display a portion of a long text, appending it an ellipsis symbol (…) and label (by default, those are '… read more'). The ContentStateBuilder exposes readMore API that accepts configuration object:

{
lines: number,
label: string,
onClick: function,
text: string,
}

The text field allows to specify the text to be displayed. By default, the text is taken from the wrapped children. The onClick field is common for all the interactions. It defines the interaction click behavior, by intercepting the expansion click.

SeeFullPost#

The SeeFullPost interaction adds an overlay containing a link-like label (by default, it says See Full Post). The click on label expands the full content. The settings object:

{
label: string,
overlayStyle: object,
labelStyle: object,
onClick: function,
}

The ContentStateBuilder exposes seeFullPost method that applies the interaction on the previous block data.

ImageCounter#

The ImageCounter serves as a counter of media entities in collapsed content. The configuration object structure:

{
counter: number,
formatLabel: number => string,
imageSelector: Array<image> => Array<image>,
style: object,
onClick: function,
}

The counter field is self-explanatory, the formatLabel defines the format of the label. The default implementation is:

counter => `+ ${ counter }`

The imageSelector function selects images that should be decorated by the counter label, among the images found within ImageCounter children. By default, last image is selected.

The ContentStateBuilder exposes imageCounter method that applies the interaction on previous block data.

RichContentViewer Integration#

RichContentViewer config prop now allows to pass the preview related configuration, by adding the following entry:

config: {
PREVIEW: {
contentInteractionMappers: [ () => Component, ... ],
onPreviewExpand: function,
}
}

The contentInteractionMappers field allows to pass the interactions as a function array, similar to the typeMappers and inlineStyleMappers. The preview package exports the interactionMap API containing three predefined interactions mentioned in previous section.

The onPreviewExpand handler determines the behavior on content expansion. The RichContentPreview component wraps the RichContentViewer while providing the default PREVIEW configuration, interaction map, and default transformation rule:

const defaultTransformation = new ContentStateTransformation({
_if: metadata => metadata.plain.length > 0,
_then: (metadata, preview) =>
preview.plain(metadata.plain[0].join('')).readMore({ lines: 3 }),
})
.rule({
_if: metadata => metadata.images.length > 0 && metadata.images.length < 5,
_then: (metadata, preview) =>
preview.image({ mediaInfo: metadata.images[0] }).seeFullPost(),
})
.rule({
_if: metadata => metadata.images.length >= 5,
_then: (metadata, preview) =>
preview
.gallery({
mediaInfo: metadata.images.slice(0, 4),
overrides: {
styles: {
galleryLayout: 2,
...
},
},
})
.imageCounter({ counter: metadata.images.length - 4 }),
});

The default rule displays 3 lines of plain text, and a single image appended by 'See Full Post' label, if the ContentState contains less than 5 images; otherwise, it displays a gallery grid of 4 images with image counter.

Custom Interactions#

TBD