mirror of
https://github.com/wassname/talk.git
synced 2026-08-07 11:29:44 +08:00
[next] Implement configuration panes (#2173)
* feat: moderation config * feat: configure banned and suspect words * chore: upgrade react and test libs to the newest version <3 * chore: upgrade typescript + some refactor * feat: general, organization and advanced configuration panes * fix: translation * feat: speedup fetching markdown editor * feat: localize markdown editor * chore: refactor container names * chore: rename infobox to communityGuidelines * feat: closing comment streams duration config * test: add feature tests for configurations * fix: mock only console.error * chore: upgrade node * chore: require node >= 10 * fix: better validation and default values * feat: Make DurationField a general purpose component and reuse for Edit Comment Timeframe * test: add unit test for duration field * fix: patch for bug when built in production * chore: bump npm version to latest * fix: adapted Dockerfile to new version of node * refactor: harmonized seconds/milliseconds to seconds * fix: resolve bug from merge conflict
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
.value {
|
||||
width: calc(6 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
.unit {
|
||||
height: 100%;
|
||||
min-width: calc(17 * var(--spacing-unit));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import DurationField, { DURATION_UNIT } from "./DurationField";
|
||||
|
||||
it("renders correctly with default units", () => {
|
||||
const props: PropTypesOf<typeof DurationField> = {
|
||||
name: "duration",
|
||||
value: "",
|
||||
disabled: false,
|
||||
onChange: noop,
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<DurationField {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly with specified units", () => {
|
||||
const props: PropTypesOf<typeof DurationField> = {
|
||||
name: "duration",
|
||||
value: "",
|
||||
disabled: false,
|
||||
onChange: noop,
|
||||
units: [DURATION_UNIT.SECONDS, DURATION_UNIT.HOURS],
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<DurationField {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("use best matching unit", () => {
|
||||
const props: PropTypesOf<typeof DurationField> = {
|
||||
name: "duration",
|
||||
value: "3600",
|
||||
disabled: false,
|
||||
onChange: noop,
|
||||
units: [DURATION_UNIT.SECONDS, DURATION_UNIT.MINUTES, DURATION_UNIT.HOURS],
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<DurationField {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("use initial unit if 0", () => {
|
||||
const props: PropTypesOf<typeof DurationField> = {
|
||||
name: "duration",
|
||||
value: "0",
|
||||
disabled: false,
|
||||
onChange: noop,
|
||||
units: [DURATION_UNIT.SECONDS, DURATION_UNIT.MINUTES, DURATION_UNIT.HOURS],
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<DurationField {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("accepts invalid input", () => {
|
||||
const props: PropTypesOf<typeof DurationField> = {
|
||||
name: "duration",
|
||||
value: "this is so invalid",
|
||||
disabled: false,
|
||||
onChange: noop,
|
||||
units: [DURATION_UNIT.SECONDS, DURATION_UNIT.MINUTES, DURATION_UNIT.HOURS],
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<DurationField {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { ChangeEvent, Component } from "react";
|
||||
|
||||
import { Flex, Option, SelectField, TextField } from "talk-ui/components";
|
||||
|
||||
import styles from "./DurationField.css";
|
||||
|
||||
/**
|
||||
* DURATION_UNIT are units that can be used in the
|
||||
* DurationField components.
|
||||
*/
|
||||
export enum DURATION_UNIT {
|
||||
SECONDS = 1,
|
||||
MINUTES = 60,
|
||||
HOURS = 3600,
|
||||
DAYS = 86400,
|
||||
WEEKS = 604800,
|
||||
}
|
||||
|
||||
type UnitElementCallback = (
|
||||
currentValue: DURATION_UNIT,
|
||||
unitValue: string
|
||||
) => React.ReactElement<any>;
|
||||
|
||||
// This is used to render the Option elements to inlcude in the select field.
|
||||
const unitElementMap: Record<DURATION_UNIT, UnitElementCallback> = {
|
||||
[DURATION_UNIT.SECONDS]: (currentValue, unitValue) => (
|
||||
<Localized
|
||||
id="framework-durationField-seconds"
|
||||
$value={currentValue}
|
||||
key={unitValue}
|
||||
>
|
||||
<Option value={unitValue}>Seconds</Option>
|
||||
</Localized>
|
||||
),
|
||||
[DURATION_UNIT.MINUTES]: (currentValue, unitValue) => (
|
||||
<Localized
|
||||
id="framework-durationField-minutes"
|
||||
$value={currentValue}
|
||||
key={unitValue}
|
||||
>
|
||||
<Option value={unitValue}>Minutes</Option>
|
||||
</Localized>
|
||||
),
|
||||
[DURATION_UNIT.HOURS]: (currentValue, unitValue) => (
|
||||
<Localized
|
||||
id="framework-durationField-hours"
|
||||
$value={currentValue}
|
||||
key={unitValue}
|
||||
>
|
||||
<Option value={unitValue}>Hours</Option>
|
||||
</Localized>
|
||||
),
|
||||
[DURATION_UNIT.DAYS]: (currentValue, unitValue) => (
|
||||
<Localized
|
||||
id="framework-durationField-days"
|
||||
$value={currentValue}
|
||||
key={unitValue}
|
||||
>
|
||||
<Option value={unitValue}>Days</Option>
|
||||
</Localized>
|
||||
),
|
||||
[DURATION_UNIT.WEEKS]: (currentValue, unitValue) => (
|
||||
<Localized
|
||||
id="framework-durationField-weeks"
|
||||
$value={currentValue}
|
||||
key={unitValue}
|
||||
>
|
||||
<Option value={unitValue}>Weeks</Option>
|
||||
</Localized>
|
||||
),
|
||||
};
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onChange: (v: string) => void;
|
||||
/** Specifiy units to include */
|
||||
units?: ReadonlyArray<DURATION_UNIT>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
/** Current value */
|
||||
value: string;
|
||||
/** Current unit */
|
||||
unit?: DURATION_UNIT;
|
||||
/** All available units */
|
||||
units: ReadonlyArray<DURATION_UNIT>;
|
||||
/**
|
||||
* Element callbacks to generate the rendered
|
||||
* Option element for the select field
|
||||
*/
|
||||
elementCallbacks: ReadonlyArray<UnitElementCallback>;
|
||||
}
|
||||
|
||||
/**
|
||||
* valueToState converts the value we receive from props to a new state.
|
||||
* @param value The value that was passed through props.
|
||||
* @param units The units that we use.
|
||||
* @param unit The current value if any otherwise the best matching unit will be used.
|
||||
*/
|
||||
function valueToState(
|
||||
value: string,
|
||||
units: ReadonlyArray<DURATION_UNIT>,
|
||||
unit?: DURATION_UNIT
|
||||
) {
|
||||
const parsed = parseInt(value, 10);
|
||||
|
||||
// If value was a valid number..
|
||||
if (!isNaN(parsed)) {
|
||||
// If unit is not set, we'll find the best matching unit.
|
||||
if (!unit) {
|
||||
// Start from the first unit,
|
||||
// keep first unit if value is set to 0,
|
||||
// otherwise use better matching unit if the value is fully dividable by the unit.
|
||||
unit = units.reduce(
|
||||
(x, cur) => (parsed % cur === 0 && parsed !== 0 ? cur : x)
|
||||
);
|
||||
}
|
||||
// Compute new value relative to the selected unit.
|
||||
value = (parsed / unit).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
unit,
|
||||
value,
|
||||
units,
|
||||
elementCallbacks: units.map(k => unitElementMap[k]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* stateToValue converts current state to the value we pass to onChange.
|
||||
* @param state
|
||||
*/
|
||||
function stateToValue(state: State) {
|
||||
const parsed = parseInt(state.value, 10);
|
||||
// If state.value was a number, return computed result, otherwise return the string.
|
||||
return (isNaN(parsed) ? state.value : parsed * state.unit!).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration Field renders a TextField that accepts a number and a SelectField with a unit.
|
||||
* If the entered value is a valid number, it'll propagate the computed value in seconds via
|
||||
* onChange otherwise it'll just propogate whatever was entered as the value TextField.
|
||||
*/
|
||||
class DurationField extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
units: [DURATION_UNIT.HOURS, DURATION_UNIT.DAYS, DURATION_UNIT.WEEKS],
|
||||
};
|
||||
|
||||
public state: State = valueToState(this.props.value, this.props.units!);
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.setState(
|
||||
valueToState(nextProps.value, this.props.units!, this.state.unit)
|
||||
);
|
||||
}
|
||||
|
||||
private handleValueChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (this.props.onChange) {
|
||||
const newState: State = {
|
||||
...this.state,
|
||||
value: e.target.value,
|
||||
};
|
||||
// Assume we have a controlled component and propage the value up,
|
||||
// it then should come back in the props.
|
||||
this.props.onChange(stateToValue(newState));
|
||||
}
|
||||
};
|
||||
|
||||
private handleUnitChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
// First set new unit before propagting new value.
|
||||
this.setState(
|
||||
{
|
||||
unit: parseInt(e.target.value, 10),
|
||||
},
|
||||
() => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(stateToValue(this.state));
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { disabled, name } = this.props;
|
||||
return (
|
||||
<Flex itemGutter>
|
||||
<TextField
|
||||
className={styles.value}
|
||||
name={`${name}-value`}
|
||||
onChange={this.handleValueChange}
|
||||
value={this.state.value}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck={false}
|
||||
textAlignCenter
|
||||
aria-label="value"
|
||||
/>
|
||||
<SelectField
|
||||
name={`${name}-unit`}
|
||||
onChange={this.handleUnitChange}
|
||||
disabled={disabled}
|
||||
aria-label="unit"
|
||||
classes={{
|
||||
select: styles.unit,
|
||||
}}
|
||||
value={(this.state.unit || this.state.units[0]).toString()}
|
||||
>
|
||||
{this.state.elementCallbacks!.map((cb, i) =>
|
||||
cb(parseInt(this.state.value, 10), this.state.units[i].toString())
|
||||
)}
|
||||
</SelectField>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DurationField;
|
||||
@@ -0,0 +1,306 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`accepts invalid input 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
itemGutter={true}
|
||||
>
|
||||
<withPropsOnChange(TextField)
|
||||
aria-label="value"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="DurationField-value"
|
||||
disabled={false}
|
||||
name="duration-value"
|
||||
onChange={[Function]}
|
||||
spellCheck={false}
|
||||
textAlignCenter={true}
|
||||
value="this is so invalid"
|
||||
/>
|
||||
<withPropsOnChange(WithKeyboardFocus)
|
||||
aria-label="unit"
|
||||
classes={
|
||||
Object {
|
||||
"select": "DurationField-unit",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
name="duration-unit"
|
||||
onChange={[Function]}
|
||||
value="1"
|
||||
>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-seconds"
|
||||
>
|
||||
<Option
|
||||
value="1"
|
||||
>
|
||||
Seconds
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-minutes"
|
||||
>
|
||||
<Option
|
||||
value="60"
|
||||
>
|
||||
Minutes
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-hours"
|
||||
>
|
||||
<Option
|
||||
value="3600"
|
||||
>
|
||||
Hours
|
||||
</Option>
|
||||
</Localized>
|
||||
</withPropsOnChange(WithKeyboardFocus)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders correctly with default units 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
itemGutter={true}
|
||||
>
|
||||
<withPropsOnChange(TextField)
|
||||
aria-label="value"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="DurationField-value"
|
||||
disabled={false}
|
||||
name="duration-value"
|
||||
onChange={[Function]}
|
||||
spellCheck={false}
|
||||
textAlignCenter={true}
|
||||
value=""
|
||||
/>
|
||||
<withPropsOnChange(WithKeyboardFocus)
|
||||
aria-label="unit"
|
||||
classes={
|
||||
Object {
|
||||
"select": "DurationField-unit",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
name="duration-unit"
|
||||
onChange={[Function]}
|
||||
value="3600"
|
||||
>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-hours"
|
||||
>
|
||||
<Option
|
||||
value="3600"
|
||||
>
|
||||
Hours
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-days"
|
||||
>
|
||||
<Option
|
||||
value="86400"
|
||||
>
|
||||
Days
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-weeks"
|
||||
>
|
||||
<Option
|
||||
value="604800"
|
||||
>
|
||||
Weeks
|
||||
</Option>
|
||||
</Localized>
|
||||
</withPropsOnChange(WithKeyboardFocus)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders correctly with specified units 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
itemGutter={true}
|
||||
>
|
||||
<withPropsOnChange(TextField)
|
||||
aria-label="value"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="DurationField-value"
|
||||
disabled={false}
|
||||
name="duration-value"
|
||||
onChange={[Function]}
|
||||
spellCheck={false}
|
||||
textAlignCenter={true}
|
||||
value=""
|
||||
/>
|
||||
<withPropsOnChange(WithKeyboardFocus)
|
||||
aria-label="unit"
|
||||
classes={
|
||||
Object {
|
||||
"select": "DurationField-unit",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
name="duration-unit"
|
||||
onChange={[Function]}
|
||||
value="1"
|
||||
>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-seconds"
|
||||
>
|
||||
<Option
|
||||
value="1"
|
||||
>
|
||||
Seconds
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={NaN}
|
||||
id="framework-durationField-hours"
|
||||
>
|
||||
<Option
|
||||
value="3600"
|
||||
>
|
||||
Hours
|
||||
</Option>
|
||||
</Localized>
|
||||
</withPropsOnChange(WithKeyboardFocus)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`use best matching unit 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
itemGutter={true}
|
||||
>
|
||||
<withPropsOnChange(TextField)
|
||||
aria-label="value"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="DurationField-value"
|
||||
disabled={false}
|
||||
name="duration-value"
|
||||
onChange={[Function]}
|
||||
spellCheck={false}
|
||||
textAlignCenter={true}
|
||||
value="1"
|
||||
/>
|
||||
<withPropsOnChange(WithKeyboardFocus)
|
||||
aria-label="unit"
|
||||
classes={
|
||||
Object {
|
||||
"select": "DurationField-unit",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
name="duration-unit"
|
||||
onChange={[Function]}
|
||||
value="3600"
|
||||
>
|
||||
<Localized
|
||||
$value={1}
|
||||
id="framework-durationField-seconds"
|
||||
>
|
||||
<Option
|
||||
value="1"
|
||||
>
|
||||
Seconds
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={1}
|
||||
id="framework-durationField-minutes"
|
||||
>
|
||||
<Option
|
||||
value="60"
|
||||
>
|
||||
Minutes
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={1}
|
||||
id="framework-durationField-hours"
|
||||
>
|
||||
<Option
|
||||
value="3600"
|
||||
>
|
||||
Hours
|
||||
</Option>
|
||||
</Localized>
|
||||
</withPropsOnChange(WithKeyboardFocus)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`use initial unit if 0 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
itemGutter={true}
|
||||
>
|
||||
<withPropsOnChange(TextField)
|
||||
aria-label="value"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="DurationField-value"
|
||||
disabled={false}
|
||||
name="duration-value"
|
||||
onChange={[Function]}
|
||||
spellCheck={false}
|
||||
textAlignCenter={true}
|
||||
value="0"
|
||||
/>
|
||||
<withPropsOnChange(WithKeyboardFocus)
|
||||
aria-label="unit"
|
||||
classes={
|
||||
Object {
|
||||
"select": "DurationField-unit",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
name="duration-unit"
|
||||
onChange={[Function]}
|
||||
value="1"
|
||||
>
|
||||
<Localized
|
||||
$value={0}
|
||||
id="framework-durationField-seconds"
|
||||
>
|
||||
<Option
|
||||
value="1"
|
||||
>
|
||||
Seconds
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={0}
|
||||
id="framework-durationField-minutes"
|
||||
>
|
||||
<Option
|
||||
value="60"
|
||||
>
|
||||
Minutes
|
||||
</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
$value={0}
|
||||
id="framework-durationField-hours"
|
||||
>
|
||||
<Option
|
||||
value="3600"
|
||||
>
|
||||
Hours
|
||||
</Option>
|
||||
</Localized>
|
||||
</withPropsOnChange(WithKeyboardFocus)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Button)
|
||||
<ForwardRef(forwardRef)
|
||||
classes={
|
||||
Object {
|
||||
"active": "FacebookButton-active",
|
||||
@@ -31,5 +31,5 @@ exports[`renders correctly 1`] = `
|
||||
<span>
|
||||
Login with Facebook
|
||||
</span>
|
||||
</withPropsOnChange(Button)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Button)
|
||||
<ForwardRef(forwardRef)
|
||||
classes={
|
||||
Object {
|
||||
"active": "GoogleButton-active",
|
||||
@@ -31,5 +31,5 @@ exports[`renders correctly 1`] = `
|
||||
<span>
|
||||
Login with Google
|
||||
</span>
|
||||
</withPropsOnChange(Button)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Button)
|
||||
<ForwardRef(forwardRef)
|
||||
classes={
|
||||
Object {
|
||||
"active": "OIDCButton-active",
|
||||
@@ -19,5 +19,5 @@ exports[`renders correctly 1`] = `
|
||||
<span>
|
||||
Login with OIDC
|
||||
</span>
|
||||
</withPropsOnChange(Button)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
@@ -3,3 +3,4 @@ export { default as PasswordField } from "./PasswordField";
|
||||
export { default as FacebookButton } from "./FacebookButton";
|
||||
export { default as GoogleButton } from "./GoogleButton";
|
||||
export { default as OIDCButton } from "./OIDCButton";
|
||||
export { default as DurationField, DURATION_UNIT } from "./DurationField";
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
$minHeight: 200px;
|
||||
$fullscreenZIndex: 10;
|
||||
|
||||
.wrapper {
|
||||
/* Fix blockquote styling of MDL: https://github.com/google/material-design-lite/issues/2037 */
|
||||
blockquote {
|
||||
> p:first-child {
|
||||
padding-top: 16px;
|
||||
}
|
||||
> p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&::after {
|
||||
margin-left: -0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconBold {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "format_bold";
|
||||
}
|
||||
}
|
||||
|
||||
.iconItalic {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "format_italic";
|
||||
}
|
||||
}
|
||||
|
||||
.iconTitle {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "title";
|
||||
}
|
||||
}
|
||||
|
||||
.iconQuote {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "format_quote";
|
||||
}
|
||||
}
|
||||
|
||||
.iconUnorderedList {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "format_list_bulleted";
|
||||
}
|
||||
}
|
||||
|
||||
.iconOrderedList {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "format_list_numbered";
|
||||
}
|
||||
}
|
||||
|
||||
.iconLink {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "link";
|
||||
}
|
||||
}
|
||||
|
||||
.iconImage {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "insert_photo";
|
||||
}
|
||||
}
|
||||
|
||||
.iconPreview {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "remove_red_eye";
|
||||
}
|
||||
}
|
||||
|
||||
.iconSideBySide {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "chrome_reader_mode";
|
||||
}
|
||||
}
|
||||
|
||||
.iconFullscreen {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "fullscreen";
|
||||
}
|
||||
}
|
||||
|
||||
.iconGuide {
|
||||
composes: icon from "talk-ui/shared/icon.css";
|
||||
&::before {
|
||||
content: "help_outline";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* These are modified styles taken from https://github.com/NextStepWebs/simplemde-markdown-editor and
|
||||
* put through http://sebastianpontow.de/css2compass/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* simplemde v1.11.2
|
||||
* Copyright Next Step Webs, Inc.
|
||||
* @link https://github.com/NextStepWebs/simplemde-markdown-editor
|
||||
* @license MIT
|
||||
*/
|
||||
:global {
|
||||
.CodeMirror {
|
||||
font-family: monospace;
|
||||
color: black;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
height: auto;
|
||||
min-height: $minHeight;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 10px;
|
||||
font: inherit;
|
||||
z-index: 1;
|
||||
pre {
|
||||
padding: 0 4px;
|
||||
border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
span {
|
||||
/*TODO: vertical-align: text-bottom;*/
|
||||
}
|
||||
.CodeMirror-code {
|
||||
.cm-tag {
|
||||
color: #63a35c;
|
||||
}
|
||||
.cm-attribute {
|
||||
color: #795da3;
|
||||
}
|
||||
.cm-string {
|
||||
color: #183691;
|
||||
}
|
||||
.cm-header-1 {
|
||||
font-size: 200%;
|
||||
line-height: 200%;
|
||||
}
|
||||
.cm-header-2 {
|
||||
font-size: 160%;
|
||||
line-height: 160%;
|
||||
}
|
||||
.cm-header-3 {
|
||||
font-size: 125%;
|
||||
line-height: 125%;
|
||||
}
|
||||
.cm-header-4 {
|
||||
font-size: 110%;
|
||||
line-height: 110%;
|
||||
}
|
||||
.cm-comment {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.cm-link {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
.cm-url {
|
||||
color: #aab2b3;
|
||||
}
|
||||
.cm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-tab {
|
||||
display: inline-block;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
position: absolute;
|
||||
}
|
||||
.cm-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.cm-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.cm-em {
|
||||
font-style: italic;
|
||||
}
|
||||
.cm-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-invalidchar {
|
||||
color: #f00;
|
||||
}
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
.CodeMirror-placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) {
|
||||
background: rgba(255, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0;
|
||||
cursor: text;
|
||||
min-height: 1px;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 100%;
|
||||
z-index: 3;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-guttermarker {
|
||||
color: black;
|
||||
}
|
||||
.CodeMirror-guttermarker-subtle {
|
||||
color: #999;
|
||||
}
|
||||
.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {
|
||||
}
|
||||
50% {
|
||||
background-color: transparent;
|
||||
}
|
||||
100% {
|
||||
}
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important;
|
||||
margin-bottom: -30px;
|
||||
margin-right: -30px;
|
||||
padding-bottom: 30px;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
position: relative;
|
||||
min-height: $minHeight;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 30px solid transparent;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -30px;
|
||||
*zoom: 1;
|
||||
*display: inline;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
user-select: none;
|
||||
}
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
pre {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
.CodeMirror-focused {
|
||||
.CodeMirror-selected {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
.CodeMirror-line::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
.CodeMirror-line {
|
||||
> span::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
> span {
|
||||
> span::selection {
|
||||
background: #d7d4f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.CodeMirror-fullscreen {
|
||||
background: #fff;
|
||||
position: fixed !important;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
z-index: $fullscreenZIndex;
|
||||
}
|
||||
.CodeMirror-sided {
|
||||
width: 50% !important;
|
||||
}
|
||||
.editor-toolbar {
|
||||
position: relative;
|
||||
opacity: 0.6;
|
||||
user-select: none;
|
||||
padding: 0 10px;
|
||||
border-top: 1px solid #bbb;
|
||||
border-left: 1px solid #bbb;
|
||||
border-right: 1px solid #bbb;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
&:after {
|
||||
display: block;
|
||||
content: " ";
|
||||
height: 1px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
&:before {
|
||||
display: block;
|
||||
content: " ";
|
||||
height: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.fullscreen {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
z-index: $fullscreenZIndex;
|
||||
}
|
||||
&.fullscreen::before {
|
||||
width: 20px;
|
||||
height: 50px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 1) 0,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
&.fullscreen::after {
|
||||
width: 20px;
|
||||
height: 50px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0) 0,
|
||||
rgba(255, 255, 255, 1) 100%
|
||||
);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none !important;
|
||||
color: #2c3e50 !important;
|
||||
height: 30px;
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
margin-right: 2px;
|
||||
font-size: 1.5em;
|
||||
width: 25px;
|
||||
&.active {
|
||||
background: #fcfcfc;
|
||||
border-color: #95a5a6;
|
||||
}
|
||||
&:hover {
|
||||
background: #fcfcfc;
|
||||
border-color: #95a5a6;
|
||||
}
|
||||
&:active {
|
||||
background: #eee;
|
||||
}
|
||||
&:before {
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
i.separator {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
border-right: 1px solid #fff;
|
||||
color: transparent;
|
||||
text-indent: -10px;
|
||||
margin: 0 6px;
|
||||
}
|
||||
&.disabled-for-preview a:not(.no-disable) {
|
||||
pointer-events: none;
|
||||
background: #fff;
|
||||
border-color: transparent;
|
||||
text-shadow: inherit;
|
||||
}
|
||||
}
|
||||
@media only screen and(max-width: 700px) {
|
||||
.editor-toolbar a.no-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.editor-statusbar {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
color: #959694;
|
||||
text-align: right;
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 4em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.lines:before {
|
||||
content: "lines: ";
|
||||
}
|
||||
.words:before {
|
||||
content: "words: ";
|
||||
}
|
||||
.characters:before {
|
||||
content: "characters: ";
|
||||
}
|
||||
}
|
||||
.editor-preview {
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #fafafa;
|
||||
z-index: 7;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
> p {
|
||||
margin-top: 0;
|
||||
}
|
||||
pre {
|
||||
background: #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
table {
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.editor-preview-side {
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
background: #fafafa;
|
||||
z-index: $fullscreenZIndex;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ddd;
|
||||
> p {
|
||||
margin-top: 0;
|
||||
}
|
||||
pre {
|
||||
background: #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
table {
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.editor-preview-active-side {
|
||||
display: block;
|
||||
}
|
||||
.editor-preview-active {
|
||||
display: block;
|
||||
}
|
||||
.CodeMirror-overwrite .CodeMirror-cursor {
|
||||
}
|
||||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
.cm-tab-wrap-hack:after {
|
||||
content: "";
|
||||
}
|
||||
span.CodeMirror-selectedtext {
|
||||
background: none;
|
||||
}
|
||||
.editor-wrapper input.title {
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import cn from "classnames";
|
||||
import React, { ChangeEvent, Component, Ref } from "react";
|
||||
import SimpleMDE from "simplemde";
|
||||
|
||||
import { GetMessage, withGetMessage } from "talk-framework/lib/i18n";
|
||||
|
||||
import styles from "./MarkdownEditor.css";
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
name?: string;
|
||||
getMessage: GetMessage;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
class MarkdownEditor extends Component<Props> {
|
||||
private config = {
|
||||
status: false,
|
||||
|
||||
// Do not download fontAwesome icons as we replace them with
|
||||
// material icons.
|
||||
autoDownloadFontAwesome: false,
|
||||
|
||||
// Disable built-in spell checker as it is very rudimentary.
|
||||
spellChecker: false,
|
||||
|
||||
toolbar: [
|
||||
{
|
||||
name: "bold",
|
||||
action: SimpleMDE.toggleBold,
|
||||
className: styles.iconBold,
|
||||
title: this.props.getMessage("framework-markdownEditor-bold", "Bold"),
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
action: SimpleMDE.toggleItalic,
|
||||
className: styles.iconItalic,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-italic",
|
||||
"Italic"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
action: SimpleMDE.toggleHeadingSmaller,
|
||||
className: styles.iconTitle,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-titleSubtitleHeading",
|
||||
"Title, Subtitle, Heading"
|
||||
),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "quote",
|
||||
action: SimpleMDE.toggleBlockquote,
|
||||
className: styles.iconQuote,
|
||||
title: this.props.getMessage("framework-markdownEditor-quote", "Quote"),
|
||||
},
|
||||
{
|
||||
name: "unordered-list",
|
||||
action: SimpleMDE.toggleUnorderedList,
|
||||
className: styles.iconUnorderedList,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-genericList",
|
||||
"Generic List"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ordered-list",
|
||||
action: SimpleMDE.toggleOrderedList,
|
||||
className: styles.iconOrderedList,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-numberedList",
|
||||
"Numbered List"
|
||||
),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "link",
|
||||
action: SimpleMDE.drawLink,
|
||||
className: styles.iconLink,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-createLink",
|
||||
"Create Link"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
action: SimpleMDE.drawImage,
|
||||
className: styles.iconImage,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-insertImage",
|
||||
"Insert Image"
|
||||
),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "preview",
|
||||
action: SimpleMDE.togglePreview,
|
||||
className: cn(styles.iconPreview, "no-disable"),
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-togglePreview",
|
||||
"Toggle Preview"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "side-by-side",
|
||||
action: SimpleMDE.toggleSideBySide,
|
||||
className: cn(styles.iconSideBySide, "no-disable"),
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-toggleSideBySide",
|
||||
"Toggle Side by Side"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "fullscreen",
|
||||
action: SimpleMDE.toggleFullScreen,
|
||||
className: cn(styles.iconFullscreen, "no-disable"),
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-toggleFullscreen",
|
||||
"Toggle Fullscreen"
|
||||
),
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "guide",
|
||||
action: "https://simplemde.com/markdown-guide",
|
||||
className: styles.iconGuide,
|
||||
title: this.props.getMessage(
|
||||
"framework-markdownEditor-markdownGuide",
|
||||
"Markdown Guide"
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
public textarea: HTMLTextAreaElement | null = null;
|
||||
public editor: SimpleMDE | null = null;
|
||||
|
||||
public onRef: Ref<HTMLTextAreaElement> = ref => (this.textarea = ref);
|
||||
|
||||
public componentDidMount() {
|
||||
this.editor = new SimpleMDE({
|
||||
...this.config,
|
||||
element: this.textarea!,
|
||||
});
|
||||
|
||||
// Don't trap the key, to stay accessible.
|
||||
this.editor.codemirror.options.extraKeys.Tab = false;
|
||||
this.editor.codemirror.options.extraKeys["Shift-Tab"] = false;
|
||||
|
||||
this.editor.codemirror.on("change", this.onChange);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (
|
||||
this.props.value !== nextProps.value &&
|
||||
nextProps.value !== this.editor!.value()
|
||||
) {
|
||||
this.editor!.value(nextProps.value);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
// Workaround empty render issue.
|
||||
// https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
|
||||
this.editor!.codemirror.refresh();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.editor!.toTextArea();
|
||||
}
|
||||
|
||||
private onChange = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.editor!.value());
|
||||
}
|
||||
};
|
||||
|
||||
// This is for accessibility purposes.
|
||||
private onTextAreaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { getMessage: _g, ...rest } = this.props;
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea ref={this.onRef} {...rest} onChange={this.onTextAreaChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let enhanced = withGetMessage(MarkdownEditor);
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
// Replace with simple texteditor because it won't work in a jsdom environment.
|
||||
enhanced = ({ onChange, ...rest }) => (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea
|
||||
{...rest}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement> | string) => {
|
||||
if (onChange) {
|
||||
onChange(typeof e === "string" ? e : e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default enhanced;
|
||||
Reference in New Issue
Block a user