Skip to content

Commit

Permalink
Improve rich text component (#13858)
Browse files Browse the repository at this point in the history
  • Loading branch information
swissspidy authored Nov 25, 2024
1 parent a5a0a71 commit 0366c37
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 64 deletions.
6 changes: 3 additions & 3 deletions packages/dom/src/stripHTML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
* limitations under the License.
*/

const buffer = document.createElement('div');
const parser = new DOMParser();

export default function stripHTML(string: string) {
// @todo: implement a cheaper way to strip markup.
buffer.innerHTML = string;
return buffer.textContent || '';
const doc = parser.parseFromString(string, 'text/html');
return doc.body.textContent || '';
}
12 changes: 8 additions & 4 deletions packages/element-library/src/text/display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import { useEffect, useRef, useMemo } from '@googleforcreators/react';
import { createSolid, type Solid } from '@googleforcreators/patterns';
import { useUnits } from '@googleforcreators/units';
import { useTransformHandler } from '@googleforcreators/transform';
import { getHTMLFormatters, getHTMLInfo } from '@googleforcreators/rich-text';
import {
getHTMLFormatters,
getHTMLInfo,
sanitizeEditorHtml,
} from '@googleforcreators/rich-text';
import { stripHTML } from '@googleforcreators/dom';
import {
getResponsiveBorder,
Expand Down Expand Up @@ -314,7 +318,7 @@ function TextDisplay({
borderRadius={borderRadius}
dataToEditorY={dataToEditorY}
dangerouslySetInnerHTML={{
__html: contentWithoutColor,
__html: sanitizeEditorHtml(contentWithoutColor),
}}
/>
</MarginedElement>
Expand All @@ -325,7 +329,7 @@ function TextDisplay({
ref={fgRef}
{...props}
dangerouslySetInnerHTML={{
__html: content,
__html: sanitizeEditorHtml(content),
}}
/>
</MarginedElement>
Expand All @@ -351,7 +355,7 @@ function TextDisplay({
<FillElement
ref={fgRef as RefObject<HTMLParagraphElement>}
dangerouslySetInnerHTML={{
__html: content,
__html: sanitizeEditorHtml(content),
}}
previewMode={previewMode}
{...props}
Expand Down
7 changes: 5 additions & 2 deletions packages/element-library/src/text/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import type {
TextElementFont,
FrameProps,
} from '@googleforcreators/elements';
import { getCaretCharacterOffsetWithin } from '@googleforcreators/rich-text';
import {
getCaretCharacterOffsetWithin,
sanitizeEditorHtml,
} from '@googleforcreators/rich-text';

/**
* Internal dependencies
Expand Down Expand Up @@ -166,7 +169,7 @@ function TextFrame({
// See https://github.com/googleforcreators/web-stories-wp/issues/7745.
data-fix-caret
className="syncMargin"
dangerouslySetInnerHTML={{ __html: content }}
dangerouslySetInnerHTML={{ __html: sanitizeEditorHtml(content) }}
{...props}
/>
);
Expand Down
14 changes: 1 addition & 13 deletions packages/rich-text/src/formatters/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
import {
createSolid,
generatePatternStyles,
getHexFromSolid,
getSolidFromHex,
isPatternEqual,
createSolidFromString,
} from '@googleforcreators/patterns';
Expand All @@ -37,17 +35,7 @@ import {
togglePrefixStyle,
getPrefixStylesInSelection,
} from '../styleManipulation';
import { isStyle, getVariable } from './util';

/*
* Color uses PREFIX-XXXXXXXX where XXXXXXXX is the 8 digit
* hex representation of the RGBA color.
*/
const styleToColor = (style: string): Pattern =>
getSolidFromHex(getVariable(style, COLOR));

const colorToStyle = (color: Solid): string =>
`${COLOR}-${getHexFromSolid(color)}`;
import { isStyle, styleToColor, colorToStyle } from './util';

function elementToStyle(element: HTMLElement): string | null {
const isSpan = element.tagName.toLowerCase() === 'span';
Expand Down
13 changes: 5 additions & 8 deletions packages/rich-text/src/formatters/gradientColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import {
createSolid,
generatePatternStyles,
getGradientStyleFromColor,
isPatternEqual,
getColorFromGradientStyle,
type Gradient,
Expand All @@ -38,13 +37,11 @@ import {
togglePrefixStyle,
getPrefixStylesInSelection,
} from '../styleManipulation';
import { isStyle, getVariable } from './util';

const styleToColor = (style: string): Gradient =>
getColorFromGradientStyle(getVariable(style, GRADIENT_COLOR));

const colorToStyle = (color: Gradient): string =>
`${GRADIENT_COLOR}-${getGradientStyleFromColor(color)}`;
import {
isStyle,
styleToGradientColor as styleToColor,
gradientColorToStyle as colorToStyle,
} from './util';

function elementToStyle(element: HTMLElement): string | null {
const isSpan = element.tagName.toLowerCase() === 'span';
Expand Down
32 changes: 31 additions & 1 deletion packages/rich-text/src/formatters/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,25 @@
* External dependencies
*/
import type { FontWeight, FontVariantStyle } from '@googleforcreators/elements';
import {
getColorFromGradientStyle,
getGradientStyleFromColor,
getHexFromSolid,
getSolidFromHex,
type Gradient,
type Pattern,
type Solid,
} from '@googleforcreators/patterns';

/**
* Internal dependencies
*/
import { type LETTERSPACING, WEIGHT } from '../customConstants';
import {
COLOR,
GRADIENT_COLOR,
type LETTERSPACING,
WEIGHT,
} from '../customConstants';

export const isStyle = (style: string | undefined, prefix: string) =>
Boolean(style?.startsWith(prefix));
Expand Down Expand Up @@ -60,3 +74,19 @@ export function styleToNumeric(
export function weightToStyle(weight: number) {
return numericToStyle(WEIGHT, weight);
}

/*
* Color uses PREFIX-XXXXXXXX where XXXXXXXX is the 8 digit
* hex representation of the RGBA color.
*/
export const styleToColor = (style: string): Pattern =>
getSolidFromHex(getVariable(style, COLOR));

export const colorToStyle = (color: Solid): string =>
`${COLOR}-${getHexFromSolid(color)}`;

export const styleToGradientColor = (style: string): Gradient =>
getColorFromGradientStyle(getVariable(style, GRADIENT_COLOR));

export const gradientColorToStyle = (color: Gradient): string =>
`${GRADIENT_COLOR}-${getGradientStyleFromColor(color)}`;
48 changes: 48 additions & 0 deletions packages/rich-text/src/getTextColors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import {
createSolid,
getHexFromSolid,
type Solid,
} from '@googleforcreators/patterns';

/**
* Internal dependencies
*/
import { COLOR, NONE } from './customConstants';
import { getSelectAllStateFromHTML } from './htmlManipulation';
import { getPrefixStylesInSelection } from './styleManipulation';
import { styleToColor } from './formatters/util';

export default function getTextColors(html: string): string[] {
const htmlState = getSelectAllStateFromHTML(html);
return getPrefixStylesInSelection(htmlState, COLOR)
.map((color) => {
if (color === NONE) {
return createSolid(0, 0, 0);
}

return styleToColor(color) as Solid;
})
.map(
// To remove the alpha channel.
(color) => '#' + getHexFromSolid(color).slice(0, 6)
);
}
43 changes: 41 additions & 2 deletions packages/rich-text/src/htmlManipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* External dependencies
*/
import { EditorState } from 'draft-js';
import { filterEditorState } from 'draftjs-filters';

/**
* Internal dependencies
Expand All @@ -28,6 +29,16 @@ import customImport from './customImport';
import customExport from './customExport';
import { getSelectionForAll } from './util';
import type { StyleSetter, AllowedSetterArgs } from './types';
import {
ITALIC,
UNDERLINE,
WEIGHT,
COLOR,
LETTERSPACING,
UPPERCASE,
GRADIENT_COLOR,
} from './customConstants';
import { getPrefixStylesInSelection } from './styleManipulation';

/**
* Return an editor state object with content set to parsed HTML
Expand Down Expand Up @@ -60,8 +71,7 @@ function updateAndReturnHTML(
...args: [AllowedSetterArgs]
) {
const stateWithUpdate = updater(getSelectAllStateFromHTML(html), ...args);
const renderedHTML = customExport(stateWithUpdate);
return renderedHTML;
return customExport(stateWithUpdate);
}

const getHTMLFormatter =
Expand Down Expand Up @@ -90,3 +100,32 @@ export function getHTMLInfo(html: string) {
const htmlStateInfo = getStateInfo(getSelectAllStateFromHTML(html));
return htmlStateInfo;
}

export function sanitizeEditorHtml(html: string) {
const editorState = getSelectAllStateFromHTML(html);

const styles: string[] = [
...getPrefixStylesInSelection(editorState, ITALIC),
...getPrefixStylesInSelection(editorState, UNDERLINE),
...getPrefixStylesInSelection(editorState, WEIGHT),
...getPrefixStylesInSelection(editorState, COLOR),
...getPrefixStylesInSelection(editorState, LETTERSPACING),
...getPrefixStylesInSelection(editorState, UPPERCASE),
...getPrefixStylesInSelection(editorState, GRADIENT_COLOR),
];

return (
customExport(
filterEditorState(
{
blocks: [],
styles,
entities: [],
maxNesting: 1,
whitespacedCharacters: [],
},
editorState
)
) || ''
);
}
1 change: 1 addition & 0 deletions packages/rich-text/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { default as RichTextContext } from './context';
export { default as useRichText } from './useRichText';
export { default as usePasteTextContent } from './usePasteTextContent';
export { default as getFontVariants } from './getFontVariants';
export { default as getTextColors } from './getTextColors';
export { default as getCaretCharacterOffsetWithin } from './utils/getCaretCharacterOffsetWithin';
export * from './htmlManipulation';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,22 +14,19 @@
* limitations under the License.
*/

let spansFromContentBuffer;
/**
*
* @param {string} content the buffer containing text element content
* @return {Array} list of individual span elements from the content
* Internal dependencies
*/
export function getSpansFromContent(content) {
// memoize buffer
if (!spansFromContentBuffer) {
spansFromContentBuffer = document.createElement('div');
}
import getTextColors from '../getTextColors';

describe('getTextColors', () => {
it('should return a list of text colors', () => {
const htmlContent =
'Fill in <span style="color: #eb0404">some</span> <span style="color: #026111">text</span>';
const expected = ['#000000', '#eb0404', '#026111'];

spansFromContentBuffer.innerHTML = content;
const actual = getTextColors(htmlContent);

// return Array instead of HtmlCollection
return Array.prototype.slice.call(
spansFromContentBuffer.getElementsByTagName('span')
);
}
expect(actual).toStrictEqual(expected);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
preloadImage,
} from '@googleforcreators/media';
import { createSolidFromString } from '@googleforcreators/patterns';
import { getTextColors } from '@googleforcreators/rich-text';

/**
* Internal dependencies
Expand All @@ -36,7 +37,6 @@ import {
calculateLuminanceFromStyleColor,
checkContrastFromLuminances,
} from '../../../../utils/contrastUtils';
import { getSpansFromContent } from '../../utils';
import getMediaBaseColor from '../../../../utils/getMediaBaseColor';
import { noop } from '../../../../utils/noop';

Expand Down Expand Up @@ -276,19 +276,7 @@ async function getOverlapBgColor({ bgImage, bgBox, overlapBox }) {
* @return {Array} the style colors from the span tags in text element content
*/
function getTextStyleColors(element) {
const spans = getSpansFromContent(element.content);
const textStyleColors = spans
.map((span) => span.style?.color)
.filter(Boolean);
// if no colors were retrieved but there are spans, there is a black default color
const noColorStyleOnSpans =
textStyleColors.length === 0 && spans.length !== 0;
// if no spans were retrieved but there is content, there is a black default color
const noSpans = element.content.length !== 0 && spans.length === 0;
if (noColorStyleOnSpans || noSpans) {
textStyleColors.push('rgb(0, 0, 0)');
}
return textStyleColors;
return getTextColors(element.content);
}

function getTextShapeBackgroundColor({ background }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ export { characterCountForPage } from './characterCountForPage';
export { filterStoryPages } from './filterStoryPages';
export { filterStoryElements } from './filterStoryElements';
export { getVisibleThumbnails } from './getVisibleThumbnails';
export { getSpansFromContent } from './getSpansFromContent';
export { ThumbnailPagePreview } from './thumbnailPagePreview';

0 comments on commit 0366c37

Please sign in to comment.