Skip to content

Commit

Permalink
[CORL-1029] Better HTML Support (#2956)
Browse files Browse the repository at this point in the history
* feat: Improve html handling, integrate new @coralproject/rte

* chore: refactor and add comments

* chore: remove obsolete line

* chore: rename `inputId` to `inputID`

* chore: upgrade @coralproject/rte

* fix: update snapshots

* chore: apply review suggestions

* fix: snapshot / tests

* fix: merge issues

* [CORL-1056] Configurable RTE (#2967)

* fix: merge issues

* feat: Configure RTE

* test: add tests

* chore: just a comment

* chore: remove unused translations
  • Loading branch information
cvle committed Jun 8, 2020
1 parent ba08447 commit 72e1b24
Show file tree
Hide file tree
Showing 94 changed files with 7,785 additions and 3,525 deletions.
8,252 changes: 5,536 additions & 2,716 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions package.json
Expand Up @@ -149,7 +149,7 @@
"@babel/preset-typescript": "^7.9.0",
"@babel/runtime-corejs3": "^7.9.2",
"@coralproject/npm-run-all": "^4.1.5",
"@coralproject/rte": "^0.11.1",
"@coralproject/rte": "^1.0.0",
"@fluent/react": "^0.11.1",
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
"@types/archiver": "^3.1.0",
Expand Down Expand Up @@ -185,8 +185,8 @@
"@types/html-to-text": "^1.4.31",
"@types/html-webpack-plugin": "^3.2.2",
"@types/ioredis": "^4.14.9",
"@types/jest": "^25.1.4",
"@types/jest-axe": "^3.2.1",
"@types/jest": "^25.2.1",
"@types/jest-axe": "^3.2.2",
"@types/jsdom": "^16.2.0",
"@types/jsonwebtoken": "^8.3.8",
"@types/linkifyjs": "^2.1.3",
Expand Down Expand Up @@ -292,10 +292,10 @@
"html-webpack-plugin": "^4.0.4",
"husky": "^4.2.3",
"intersection-observer": "^0.7.0",
"jest": "^25.2.4",
"jest": "^26.0.1",
"jest-axe": "^3.4.0",
"jest-junit": "^10.0.0",
"jest-localstorage-mock": "^2.4.0",
"jest-localstorage-mock": "^2.4.2",
"jest-mock-console": "^1.0.0",
"keymaster": "^1.6.2",
"lint-staged": "^10.1.1",
Expand Down Expand Up @@ -356,7 +356,7 @@
"terser-webpack-plugin": "^2.3.5",
"thread-loader": "^2.1.3",
"timekeeper": "^2.2.0",
"ts-jest": "^25.3.0",
"ts-jest": "25.4.0",
"ts-loader": "^6.2.2",
"ts-node": "^8.8.1",
"ts-node-dev": "^1.0.0-pre.44",
Expand All @@ -379,6 +379,11 @@
"whatwg-fetch": "^3.0.0"
},
"dependencies-pins-documentation": {
"ts-jest@25.4.0": [
"Newer versions require every definition file to be included",
"in tsconfig.json in the `files` field. This is cumbersome and",
"here is a discussion of it: https://github.com/kulshekhar/ts-jest/issues/1604"
],
"wait-for-expect@1.x.x": [
"Newer versions breaks the use of jest fake timers"
],
Expand Down
1 change: 1 addition & 0 deletions src/core/build/config.ts
Expand Up @@ -77,6 +77,7 @@ export type Config = typeof config;

export const createClientEnv = (c: Config) => ({
NODE_ENV: c.get("env"),
WEBPACK: "true",
});

// Setup the base configuration.
Expand Down
48 changes: 35 additions & 13 deletions src/core/client/admin/components/Comment/CommentContent.tsx
@@ -1,20 +1,50 @@
import cn from "classnames";
import React, { FunctionComponent, useMemo } from "react";
import striptags from "striptags";

import {
getPhrasesRegExp,
GetPhrasesRegExpOptions,
markHTMLNode,
} from "coral-admin/helpers";
import { createPurify } from "coral-common/utils/purify";
import {
ALL_FEATURES,
createSanitize,
Sanitize,
} from "coral-common/helpers/sanitize";

import styles from "./CommentContent.css";

/**
* Create a purify instance that will be used to handle HTML content.
* Return a purify instance that will be used to handle HTML content.
*/
const purify = createPurify(window, false);
const getSanitize: (highlight: boolean) => Sanitize = (() => {
let sanitizers: Record<"default" | "highlight", Sanitize> | null = null;
return (highlight: boolean) => {
if (!sanitizers) {
sanitizers = {
default: createSanitize(window, {
// Allow all RTE features to be displayed.
features: ALL_FEATURES,
}),
highlight: createSanitize(window, {
// We need normalized text nodes to mark nodes for suspect/banned words.
normalize: true,
// Allow all RTE features to be displayed.
features: ALL_FEATURES,
config: {
FORBID_TAGS: highlight
? ["b", "strong", "i", "em", "s", "span"]
: [],
},
}),
};
}
if (highlight) {
return sanitizers.highlight!;
}
return sanitizers.default!;
};
})();

interface Props {
className?: string;
Expand Down Expand Up @@ -51,15 +81,7 @@ const CommentContent: FunctionComponent<Props> = ({
}

// Sanitize the input for display.
let html = purify.sanitize(children);
if (highlight) {
html = striptags(html, ["a"]);
}

// We create a Shadow DOM Tree with the HTML body content and use it as a
// parser.
const node = document.createElement("div");
node.innerHTML = html;
const node = getSanitize(highlight)(children);

// If the expression is available, then mark the nodes.
if (expression) {
Expand Down
Expand Up @@ -110,7 +110,7 @@ const CommentLengthConfig: FunctionComponent<Props> = ({ disabled }) => (
<TextFieldAdornment>Characters</TextFieldAdornment>
</Localized>
}
placeholder={"No limit"}
placeholder="No limit"
textAlignCenter
/>
</Localized>
Expand Down
Expand Up @@ -18,6 +18,7 @@ import CommentLengthConfig from "./CommentLengthConfig";
import GuidelinesConfig from "./GuidelinesConfig";
import LocaleConfig from "./LocaleConfig";
import ReactionConfigContainer from "./ReactionConfigContainer";
import RTEConfig from "./RTEConfig";
import SitewideCommentingConfig from "./SitewideCommentingConfig";
import StaffConfig from "./StaffConfig";

Expand All @@ -44,6 +45,7 @@ const GeneralConfigContainer: React.FunctionComponent<Props> = ({
<SitewideCommentingConfig disabled={submitting} />
<AnnouncementConfigContainer disabled={submitting} settings={settings} />
<GuidelinesConfig disabled={submitting} />
<RTEConfig disabled={submitting} />
<CommentLengthConfig disabled={submitting} />
<CommentEditingConfig disabled={submitting} />
<ClosingCommentStreamsConfig disabled={submitting} />
Expand All @@ -67,6 +69,7 @@ const enhanced = withFragmentContainer<Props>({
...SitewideCommentingConfig_formValues @relay(mask: false)
...ReactionConfig_formValues @relay(mask: false)
...StaffConfig_formValues @relay(mask: false)
...RTEConfig_formValues @relay(mask: false)
...ReactionConfigContainer_settings
}
Expand Down
@@ -0,0 +1,10 @@
.disabledLabel {
color: var(--v2-colors-grey-400);
}
.spoilerDesc {
font-size: var(--v2-font-size-2);
font-weight: var(--v2-font-weight-primary-regular);
font-family: var(--v2-font-family-primary);
color: var(--v2-colors-grey-400);
padding-left: 26px;
}
125 changes: 125 additions & 0 deletions src/core/client/admin/routes/Configure/sections/General/RTEConfig.tsx
@@ -0,0 +1,125 @@
import { Localized } from "@fluent/react/compat";
import cn from "classnames";
import React, { FunctionComponent } from "react";
import { Field, FormSpy } from "react-final-form";
import { graphql } from "react-relay";

import { parseBool } from "coral-framework/lib/form";
import {
CheckBox,
FieldSet,
FormField,
FormFieldDescription,
Label,
} from "coral-ui/components/v2";

import ConfigBox from "../../ConfigBox";
import Header from "../../Header";
import OnOffField from "../../OnOffField";

import styles from "./RTEConfig.css";

// eslint-disable-next-line no-unused-expressions
graphql`
fragment RTEConfig_formValues on Settings {
rte {
enabled
strikethrough
spoiler
}
}
`;

interface Props {
disabled: boolean;
}

const RTEConfig: FunctionComponent<Props> = ({ disabled }) => (
<ConfigBox
title={
<Localized id="configure-general-rte-title">
<Header container={<legend />}>Rich-text comments</Header>
</Localized>
}
container={<FieldSet />}
>
<Localized id="configure-general-rte-express">
<FormFieldDescription>
Give your community more ways to express themselves beyond plain text
with rich-text formatting.
</FormFieldDescription>
</Localized>

<FormField container={<fieldset />}>
<Localized id="configure-general-rte-richTextComments">
<Label>Rich-Text comments</Label>
</Localized>
<OnOffField
name="rte.enabled"
disabled={disabled}
onLabel={
<Localized id="configure-general-rte-onBasicFeatures">
<span>On - bold, italics, block quotes, and bulletted lists</span>
</Localized>
}
/>
</FormField>
<FormSpy subscription={{ values: true }}>
{(props) => {
const rteDisabled = !props.values.rte.enabled;
return (
<>
<FormField container={<fieldset />}>
<Localized id="configure-general-rte-additional">
<Label
className={cn({
[styles.disabledLabel]: rteDisabled,
})}
>
Additional rich-text options
</Label>
</Localized>
<Field name="rte.strikethrough" type="checkbox" parse={parseBool}>
{({ input }) => (
<Localized id="configure-general-rte-strikethrough">
<CheckBox
{...input}
id={input.name}
disabled={rteDisabled || disabled}
>
Strikethrough
</CheckBox>
</Localized>
)}
</Field>
<Field name="rte.spoiler" type="checkbox" parse={parseBool}>
{({ input }) => (
<div>
<Localized id="configure-general-rte-spoiler">
<CheckBox
{...input}
id={input.name}
disabled={rteDisabled || disabled}
>
Spoiler
</CheckBox>
</Localized>
<Localized id="configure-general-rte-spoilerDesc">
<div className={styles.spoilerDesc}>
Words and phrases formatted as Spoiler are hidden behind
a dark background until the reader chooses to reveal the
text.
</div>
</Localized>
</div>
)}
</Field>
</FormField>
</>
);
}}
</FormSpy>
</ConfigBox>
);

export default RTEConfig;
Expand Up @@ -117,7 +117,7 @@ const SlackConfigContainer: FunctionComponent<Props> = ({ form, settings }) => {
<FieldArray name="slack.channels">
{({ fields }) =>
fields
.map((channel, index) => (
.map((channel: any, index: number) => (
<SlackChannel
key={index}
channel={channel}
Expand Down

0 comments on commit 72e1b24

Please sign in to comment.