mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Enhance: Added 'Set Value' functionality and support for HTML with hyperlinks in Rich Text Editor component (#11406)
* Added HTML support for Placeholder and Default Value property. Added CSA for loading state. * Exposed isDisabled, isVisible and isLoading variables. Also exposed setValue, setDisable, setVisibility and setLoading functions. * Fixed issue where loading state functionality was not visible in the properties section on the initial render of rich text editor.
This commit is contained in:
parent
5e9e9381e9
commit
3ebc1933cc
6 changed files with 190 additions and 12 deletions
|
|
@ -21,6 +21,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
|
||||||
'DropdownV2',
|
'DropdownV2',
|
||||||
'MultiselectV2',
|
'MultiselectV2',
|
||||||
'Button',
|
'Button',
|
||||||
|
'RichTextEditor',
|
||||||
];
|
];
|
||||||
const PROPERTIES_VS_ACCORDION_TITLE = {
|
const PROPERTIES_VS_ACCORDION_TITLE = {
|
||||||
Text: 'Data',
|
Text: 'Data',
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,15 @@ export const richtextareaConfig = {
|
||||||
defaultValue: 'Default text',
|
defaultValue: 'Default text',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
loadingState: {
|
||||||
|
type: 'toggle',
|
||||||
|
displayName: 'Show loading state',
|
||||||
|
validation: {
|
||||||
|
schema: { type: 'boolean' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
section: 'additionalActions',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
events: {},
|
events: {},
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -55,6 +64,28 @@ export const richtextareaConfig = {
|
||||||
exposedVariables: {
|
exposedVariables: {
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
handle: 'setValue',
|
||||||
|
displayName: 'Set value',
|
||||||
|
params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setDisable',
|
||||||
|
displayName: 'Set disable',
|
||||||
|
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setVisibility',
|
||||||
|
displayName: 'Set visibility',
|
||||||
|
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setLoading',
|
||||||
|
displayName: 'Set loading',
|
||||||
|
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
definition: {
|
definition: {
|
||||||
others: {
|
others: {
|
||||||
showOnDesktop: { value: '{{true}}' },
|
showOnDesktop: { value: '{{true}}' },
|
||||||
|
|
@ -63,6 +94,7 @@ export const richtextareaConfig = {
|
||||||
properties: {
|
properties: {
|
||||||
placeholder: { value: 'Placeholder text' },
|
placeholder: { value: 'Placeholder text' },
|
||||||
defaultValue: { value: '' },
|
defaultValue: { value: '' },
|
||||||
|
loadingState: { value: `{{false}}` },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
/* eslint-disable react/no-string-refs */
|
/* eslint-disable react/no-string-refs */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState } from 'draft-js';
|
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState, convertFromHTML } from 'draft-js';
|
||||||
import 'draft-js/dist/Draft.css';
|
import 'draft-js/dist/Draft.css';
|
||||||
import { stateToHTML } from 'draft-js-export-html';
|
import { stateToHTML } from 'draft-js-export-html';
|
||||||
|
import Loader from '@/ToolJetUI/Loader/Loader';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
// Custom overrides for "code" style.
|
// Custom overrides for "code" style.
|
||||||
const styleMap = {
|
const styleMap = {
|
||||||
|
|
@ -148,8 +150,11 @@ const InlineStyleControls = (props) => {
|
||||||
class DraftEditor extends React.Component {
|
class DraftEditor extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||||
this.state = {
|
this.state = {
|
||||||
editorState: EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue)),
|
editorState: EditorState.createWithContent(
|
||||||
|
ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.editorContainerRef = React.createRef();
|
this.editorContainerRef = React.createRef();
|
||||||
|
|
@ -181,6 +186,38 @@ class DraftEditor extends React.Component {
|
||||||
if (this.controlsRef.current) {
|
if (this.controlsRef.current) {
|
||||||
this.resizeObserver.observe(this.controlsRef.current);
|
this.resizeObserver.observe(this.controlsRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exposedVariables = {
|
||||||
|
value: this.props.defaultValue,
|
||||||
|
isDisabled: this.props.isDisabled,
|
||||||
|
isVisible: this.props.isVisible,
|
||||||
|
isLoading: this.props.isLoading,
|
||||||
|
setValue: async (text) => {
|
||||||
|
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(text));
|
||||||
|
const newContentState = ContentState.createFromBlockArray(
|
||||||
|
blocksFromHTML.contentBlocks,
|
||||||
|
blocksFromHTML.entityMap
|
||||||
|
);
|
||||||
|
const newEditorState = EditorState.createWithContent(newContentState);
|
||||||
|
const html = stateToHTML(newContentState);
|
||||||
|
this.props.handleChange(html);
|
||||||
|
this.setState({ editorState: newEditorState });
|
||||||
|
},
|
||||||
|
setDisable: async (value) => {
|
||||||
|
this.props.setExposedVariable('isDisabled', value);
|
||||||
|
this.props.setIsDisabled(value);
|
||||||
|
},
|
||||||
|
setVisibility: async (value) => {
|
||||||
|
this.props.setExposedVariable('isVisible', value);
|
||||||
|
this.props.setIsVisible(value);
|
||||||
|
},
|
||||||
|
setLoading: async (value) => {
|
||||||
|
this.props.setExposedVariable('isLoading', value);
|
||||||
|
this.props.setIsLoading(value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.props.setExposedVariables(exposedVariables);
|
||||||
|
this.props.isInitialRender.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
@ -191,7 +228,8 @@ class DraftEditor extends React.Component {
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||||
const newContentState = ContentState.createFromText(this.props.defaultValue);
|
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||||
|
const newContentState = ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap);
|
||||||
const newEditorState = EditorState.createWithContent(newContentState);
|
const newEditorState = EditorState.createWithContent(newContentState);
|
||||||
const html = stateToHTML(newContentState);
|
const html = stateToHTML(newContentState);
|
||||||
|
|
||||||
|
|
@ -242,7 +280,13 @@ class DraftEditor extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return this.props.isLoading ? (
|
||||||
|
<div style={{ height: '100%', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<center>
|
||||||
|
<Loader width="16" absolute={false} />
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="RichEditor-root" style={{ overflowY: 'scroll', scrollbarWidth: 'none' }}>
|
<div className="RichEditor-root" style={{ overflowY: 'scroll', scrollbarWidth: 'none' }}>
|
||||||
<div className="RichEditor-controls" ref={this.controlsRef}>
|
<div className="RichEditor-controls" ref={this.controlsRef}>
|
||||||
<BlockStyleControls editorState={editorState} onToggle={this.toggleBlockType} />
|
<BlockStyleControls editorState={editorState} onToggle={this.toggleBlockType} />
|
||||||
|
|
@ -256,7 +300,13 @@ class DraftEditor extends React.Component {
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
keyBindingFn={this.mapKeyToEditorCommand}
|
keyBindingFn={this.mapKeyToEditorCommand}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(this.props.placeholder || ''),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
ref="editor"
|
ref="editor"
|
||||||
spellCheck={true}
|
spellCheck={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect } from 'react';
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import 'draft-js/dist/Draft.css';
|
import 'draft-js/dist/Draft.css';
|
||||||
import { DraftEditor } from './DraftEditor';
|
import { DraftEditor } from './DraftEditor';
|
||||||
|
|
||||||
|
|
@ -8,17 +9,38 @@ export const RichTextEditor = function RichTextEditor({
|
||||||
properties,
|
properties,
|
||||||
styles,
|
styles,
|
||||||
setExposedVariable,
|
setExposedVariable,
|
||||||
|
setExposedVariables,
|
||||||
dataCy,
|
dataCy,
|
||||||
}) {
|
}) {
|
||||||
|
const isInitialRender = useRef(true);
|
||||||
const { visibility, disabledState, boxShadow } = styles;
|
const { visibility, disabledState, boxShadow } = styles;
|
||||||
const placeholder = properties.placeholder;
|
const placeholder = properties.placeholder;
|
||||||
const defaultValue = properties?.defaultValue ?? '';
|
const defaultValue = properties?.defaultValue ?? '';
|
||||||
|
|
||||||
// exposing the default value at first
|
const [isDisabled, setIsDisabled] = useState(disabledState);
|
||||||
|
const [isVisible, setIsVisible] = useState(visibility);
|
||||||
|
const [isLoading, setIsLoading] = useState(properties?.loadingState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExposedVariable('value', defaultValue);
|
if (isDisabled !== disabledState) setIsDisabled(disabledState);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
if (isVisible !== visibility) setIsVisible(visibility);
|
||||||
}, []);
|
if (isLoading !== properties.loadingState) setIsLoading(properties.loadingState);
|
||||||
|
}, [properties.loadingState, styles.visibility, styles.disabledState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialRender.current) return;
|
||||||
|
setExposedVariable('isDisabled', disabledState);
|
||||||
|
}, [disabledState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialRender.current) return;
|
||||||
|
setExposedVariable('isVisible', visibility);
|
||||||
|
}, [visibility]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialRender.current) return;
|
||||||
|
setExposedVariable('isLoading', isLoading);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
function handleChange(html) {
|
function handleChange(html) {
|
||||||
setExposedVariable('value', html);
|
setExposedVariable('value', html);
|
||||||
|
|
@ -26,16 +48,25 @@ export const RichTextEditor = function RichTextEditor({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-disabled={disabledState}
|
data-disabled={isDisabled}
|
||||||
style={{ height: `${height}px`, display: visibility ? '' : 'none', boxShadow }}
|
style={{ height: `${height}px`, display: isVisible ? '' : 'none', boxShadow }}
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
<DraftEditor
|
<DraftEditor
|
||||||
|
isInitialRender={isInitialRender}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
height={height}
|
height={height}
|
||||||
width={width}
|
width={width}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isVisible={visibility}
|
||||||
|
isDisabled={disabledState}
|
||||||
|
setExposedVariable={setExposedVariable}
|
||||||
|
setExposedVariables={setExposedVariables}
|
||||||
|
setIsDisabled={setIsDisabled}
|
||||||
|
setIsVisible={setIsVisible}
|
||||||
|
setIsLoading={setIsLoading}
|
||||||
></DraftEditor>
|
></DraftEditor>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,15 @@ export const richtextareaConfig = {
|
||||||
defaultValue: 'Default text',
|
defaultValue: 'Default text',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
loadingState: {
|
||||||
|
type: 'toggle',
|
||||||
|
displayName: 'Show loading state',
|
||||||
|
validation: {
|
||||||
|
schema: { type: 'boolean' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
section: 'additionalActions',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
events: {},
|
events: {},
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -55,6 +64,28 @@ export const richtextareaConfig = {
|
||||||
exposedVariables: {
|
exposedVariables: {
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
handle: 'setValue',
|
||||||
|
displayName: 'Set value',
|
||||||
|
params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setDisable',
|
||||||
|
displayName: 'Set disable',
|
||||||
|
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setVisibility',
|
||||||
|
displayName: 'Set visibility',
|
||||||
|
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setLoading',
|
||||||
|
displayName: 'Set loading',
|
||||||
|
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
definition: {
|
definition: {
|
||||||
others: {
|
others: {
|
||||||
showOnDesktop: { value: '{{true}}' },
|
showOnDesktop: { value: '{{true}}' },
|
||||||
|
|
@ -63,6 +94,7 @@ export const richtextareaConfig = {
|
||||||
properties: {
|
properties: {
|
||||||
placeholder: { value: 'Placeholder text' },
|
placeholder: { value: 'Placeholder text' },
|
||||||
defaultValue: { value: '' },
|
defaultValue: { value: '' },
|
||||||
|
loadingState: { value: `{{false}}` },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,15 @@ export const richtextareaConfig = {
|
||||||
defaultValue: 'Default text',
|
defaultValue: 'Default text',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
loadingState: {
|
||||||
|
type: 'toggle',
|
||||||
|
displayName: 'Show loading state',
|
||||||
|
validation: {
|
||||||
|
schema: { type: 'boolean' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
section: 'additionalActions',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
events: {},
|
events: {},
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -55,6 +64,28 @@ export const richtextareaConfig = {
|
||||||
exposedVariables: {
|
exposedVariables: {
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
handle: 'setValue',
|
||||||
|
displayName: 'Set value',
|
||||||
|
params: [{ handle: 'value', displayName: 'Value', defaultValue: 'New text' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setDisable',
|
||||||
|
displayName: 'Set disable',
|
||||||
|
params: [{ handle: 'setDisable', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setVisibility',
|
||||||
|
displayName: 'Set visibility',
|
||||||
|
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'setLoading',
|
||||||
|
displayName: 'Set loading',
|
||||||
|
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
definition: {
|
definition: {
|
||||||
others: {
|
others: {
|
||||||
showOnDesktop: { value: '{{true}}' },
|
showOnDesktop: { value: '{{true}}' },
|
||||||
|
|
@ -63,6 +94,7 @@ export const richtextareaConfig = {
|
||||||
properties: {
|
properties: {
|
||||||
placeholder: { value: 'Placeholder text' },
|
placeholder: { value: 'Placeholder text' },
|
||||||
defaultValue: { value: '' },
|
defaultValue: { value: '' },
|
||||||
|
loadingState: { value: `{{false}}` },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue