Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve rich text component #13858

Merged
merged 5 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Loading