Skip to content

Instantly share code, notes, and snippets.

@norayr93
Last active December 9, 2022 03:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save norayr93/ba84a3b4b2e2bb5de88c8f8c5e5f01c1 to your computer and use it in GitHub Desktop.
Save norayr93/ba84a3b4b2e2bb5de88c8f8c5e5f01c1 to your computer and use it in GitHub Desktop.
Editor
// This component is for creating and editing ctaBoxes. It openes a modal.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import linkifyIt from 'linkify-it';
import { stopPropagation } from '../../../utils/common';
import { getFirstIcon } from '../../../utils/toolbar';
import Option from '../../../components/Option';
import { Dropdown, DropdownOption } from '../../../components/Dropdown';
import './styles.css';
const linkify = linkifyIt();
const MAX_DESCRIPTION = 90;
const MAX_BUTTON_TEXT = 25;
class LayoutComponent extends Component {
static propTypes = {
expanded: PropTypes.bool,
doExpand: PropTypes.func,
doCollapse: PropTypes.func,
onExpandEvent: PropTypes.func,
config: PropTypes.object,
onChange: PropTypes.func,
currentState: PropTypes.object,
translations: PropTypes.object,
showModalforEdit: PropTypes.bool,
};
state: Object = {
showModal: false,
boxTitle: '',
boxText: '',
ctaTargetLink: '',
buttonText: '',
ctaTargetLinkOption: this.props.config.defaultTargetOption,
linkError: false,
};
componentWillReceiveProps(props) {
if (this.props.expanded && !props.expanded) {
this.setState({
showModal: false,
boxTitle: '',
boxText: '',
ctaTargetLink: '',
buttonText: '',
ctaTargetLinkOption: this.props.config.defaultTargetOption,
});
}
}
charactersLengthForDiscription = () => this.state.boxText.length || 0;
charactersLengthForButtonText= () => this.state.buttonText.length || 0;
charactersLengthForBoxTitle= () => this.state.boxTitle.length || 0;
addCTABox: Function = (): void => {
const { onChange } = this.props;
const {
boxTitle, boxText, buttonText, ctaTargetLink, ctaTargetLinkOption,
} = this.state;
onChange('ctaBox', boxTitle, boxText, buttonText, ctaTargetLink, ctaTargetLinkOption);
};
updateValue: Function = (event: Object): void => {
let { linkError } = this.state;
const links = linkify.match(event.target.value);
const linkifiedTarget = links && links[0] ? links[0].url : '';
let { value } = event.target;
if (linkifiedTarget === '' && event.target.name === 'ctaTargetLink') {
linkError = true;
} else if (event.target.name === 'ctaTargetLink') {
linkError = false;
}
if (event.target.name === 'boxTitle' && event.target.value.length > 25) {
value = this.state.boxTitle;
}
if (event.target.name === 'boxText' && event.target.value.length > 90) {
value = this.state.boxText;
}
if (event.target.name === 'buttonText' && event.target.value.length > 25) {
value = this.state.buttonText;
}
this.setState({
[`${event.target.name}`]: value,
linkError,
});
};
updateTargetOption: Function = (event: Object): void => {
this.setState({
ctaTargetLinkOption: event.target.checked ? '_blank' : '_self',
});
};
hideModal: Function = (): void => {
this.setState({
showModal: false,
});
};
signalExpandShowModal = () => {
const { onExpandEvent, currentState: { ctaBox } } = this.props;
const { ctaTargetLinkOption } = this.state;
onExpandEvent();
this.setState({
showModal: true,
boxTitle: (ctaBox && ctaBox.boxTitle) || '',
boxText: (ctaBox && ctaBox.boxText) || '',
ctaTargetLink: (ctaBox && ctaBox.ctaTargetLink) || '',
ctaTargetLinkOption: (ctaBox && ctaBox.targetOption) || ctaTargetLinkOption,
buttonText: (ctaBox && ctaBox.buttonText) || '',
});
}
forceExpandAndShowModal: Function = (): void => {
const { doExpand, currentState: { ctaBox } } = this.props;
const { ctaTargetLinkOption } = this.state;
doExpand();
this.setState({
showModal: true,
boxTitle: (ctaBox && ctaBox.boxTitle) || '',
boxText: (ctaBox && ctaBox.boxText) || '',
ctaTargetLink: ctaBox && ctaBox.ctaTargetLink,
ctaTargetLinkOption: (ctaBox && ctaBox.targetOption) || ctaTargetLinkOption,
buttonText: (ctaBox && ctaBox.buttonText) || '',
});
}
renderAddCTABoxModal() {
const {
config: { popupClassName }, doCollapse, translations, currentState: { ctaBox },
} = this.props;
const {
buttonText, ctaTargetLink, linkError, boxTitle, boxText,
} = this.state;
const isLinkError = !(!linkError && ctaTargetLink && ctaTargetLink.length > 0);
return (
<React.Fragment>
{this.state.showModal && <div className="ctabox-overlay" onClick={this.hideModal} />}
<div
className={classNames('rdw-modal', popupClassName)}
onClick={stopPropagation}
>
<h5 className="rdw-modal-header">
{ctaBox ? translations['components.controls.ctaBox.headlineEdit'] : translations['components.controls.ctaBox.headlineAdd']}
</h5>
<label className="rdw-modal-label" htmlFor="boxTitle">
{translations['components.controls.ctaBox.boxTitle']}
</label>
<div className="form-input-wrapper">
<input
id="boxTitle"
className="form-control-box"
onChange={this.updateValue}
onBlur={this.updateValue}
name="boxTitle"
value={boxTitle}
/>
<i>{MAX_BUTTON_TEXT - this.charactersLengthForBoxTitle()}</i>
</div>
<label className="rdw-modal-label" htmlFor="boxText">
{translations['components.controls.ctaBox.boxText']}
</label>
<div className="form-input-wrapper">
<input
id="boxText"
className="form-control-box"
onChange={this.updateValue}
onBlur={this.updateValue}
name="boxText"
value={boxText}
/>
<i>{MAX_DESCRIPTION - this.charactersLengthForDiscription()}</i>
</div>
<label className="rdw-modal-label" htmlFor="buttonText">
{translations['components.controls.ctaBox.buttonText']}
</label>
<div className="form-input-wrapper">
<input
id="buttonText"
className="form-control-box"
onChange={this.updateValue}
onBlur={this.updateValue}
name="buttonText"
value={buttonText}
/>
<i>{MAX_BUTTON_TEXT - this.charactersLengthForButtonText()}</i>
</div>
<label className="rdw-modal-label" htmlFor="ctaTargetLink">
{translations['components.controls.ctaBox.ctaTargetLink']}
</label>
<div className="form-input-wrapper-with-icon-link" style={linkError && ctaTargetLink && ctaTargetLink.length > 0 ? { borderColor: '#FC5457' } : { borderColor: '#DEDEDE' }}>
<i className="mi-link" />
<input
id="ctaTargetLink"
className="form-control-box"
onChange={this.updateValue}
onBlur={this.updateValue}
name="ctaTargetLink"
value={ctaTargetLink}
placeholder="https://example.com"
/>
{ !isLinkError && <i className="mi-check-circle" style={{ color: '#4CAF50' }} />}
{ linkError && ctaTargetLink && ctaTargetLink.length > 0 && <i className="mi-highlight-off" onClick={() => this.setState({ ctaTargetLink: '' })} style={{ color: '#FC5457' }} />}
</div>
{linkError && ctaTargetLink && ctaTargetLink.length > 0 && <i className="rwd-link-error-message" >URL adress is not valid</i>}
<span className="rdw-modal-buttonsection">
<button
className="rdw-modal-btn"
onClick={doCollapse}
>
{translations['generic.cancel']}
</button>
<button
className={!ctaTargetLink || !buttonText || isLinkError ? 'rdw-modal-btn-disabled' : 'rdw-modal-btn'}
onClick={this.addCTABox}
disabled={!ctaTargetLink || !buttonText || isLinkError}
>
{translations['components.controls.ctaBox.AddCta']}
</button>
</span>
</div>
</React.Fragment>
);
}
renderInFlatList(): Object {
const {
config: { options, link, className },
expanded,
showModalforEdit,
translations,
} = this.props;
const { showModal } = this.state;
if (showModalforEdit && !showModal) {
this.forceExpandAndShowModal();
}
return (
<div className={classNames('rdw-ctabox-wrapper', className)} aria-label="rdw-ctabox-control">
{options.indexOf('link') >= 0 && <Option
value="unordered-list-item"
className={classNames(link.className)}
onClick={this.signalExpandShowModal}
aria-haspopup="true"
aria-expanded={showModal}
title={link.title || translations['components.controls.ctaBox.link']}
>
<img
src={link.icon}
alt=""
/>
</Option>}
{expanded && showModal ? this.renderAddCTABoxModal() : undefined}
</div>
);
}
renderInDropDown(): Object {
const {
expanded,
onExpandEvent,
doCollapse,
doExpand,
onChange,
config,
translations,
} = this.props;
const {
options, link, className, dropdownClassName, title,
} = config;
const { showModal } = this.state;
return (
<div
className="rdw-ctabox-wrapper"
aria-haspopup="true"
aria-label="rdw-ctabox-control"
aria-expanded={expanded}
title={title}
>
<Dropdown
className={classNames('rdw-ctabox-dropdown', className)}
optionWrapperClassName={classNames(dropdownClassName)}
onChange={onChange}
expanded={expanded && !showModal}
doExpand={doExpand}
doCollapse={doCollapse}
onExpandEvent={onExpandEvent}
>
<img
src={getFirstIcon(config)}
alt=""
/>
{options.indexOf('link') >= 0 &&
<DropdownOption
onClick={this.forceExpandAndShowModal}
className={classNames('rdw-ctabox-dropdownoption', link.className)}
title={link.title || translations['components.controls.ctaBox.link']}
>
<img
src={link.icon}
alt=""
/>
</DropdownOption>}
</Dropdown>
{expanded && showModal ? this.renderAddCTABoxModal() : undefined}
</div>
);
}
render(): Object {
const { config: { inDropdown } } = this.props;
if (inDropdown) {
return this.renderInDropDown();
}
return this.renderInFlatList();
}
}
export default LayoutComponent;
// CTABox/index.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { EditorState, Modifier } from 'draft-js';
import {
getSelectionText,
getEntityRange,
getSelectionEntity,
} from 'draftjs-utils';
import linkifyIt from 'linkify-it';
import LayoutComponent from './Component';
const linkify = linkifyIt();
class CTABox extends Component {
static propTypes = {
editorState: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
modalHandler: PropTypes.object,
config: PropTypes.object,
translations: PropTypes.object,
modalCtaBoxOpen: PropTypes.bool,
};
static getDerivedStateFromProps(nextProps, state) {
if (nextProps.modalCtaBoxOpen.entityKey !== state.currentEntity) {
return { currentEntity: nextProps.modalCtaBoxOpen.entityKey };
}
return state;
}
state = {
expanded: false,
link: undefined,
selectionText: undefined,
};
componentDidMount() {
const { editorState, modalHandler, modalCtaBoxOpen: { entityKey } } = this.props;
if (editorState) {
this.setState({
currentEntity: getSelectionEntity(editorState) || entityKey,
});
}
modalHandler.registerCallBack(this.expandCollapse);
}
componentWillUnmount() {
const { modalHandler } = this.props;
modalHandler.deregisterCallBack(this.expandCollapse);
}
onExpandEvent: Function = () => {
this.signalExpanded = !this.state.expanded;
};
onChange = (action, boxTitle, boxText, buttonText, target, targetOption) => {
if (action === 'ctaBox') {
const links = linkify.match(target);
const linkifiedTarget = links && links[0] ? links[0].url : '';
this.addCTABox(boxTitle, boxText, buttonText, linkifiedTarget, targetOption);
}
}
getCurrentValues = () => {
const { editorState, modalCtaBoxOpen: { isOpen } } = this.props;
const { currentEntity } = this.state;
const contentState = editorState.getCurrentContent();
const currentValues = {};
if (currentEntity && (contentState.getEntity(currentEntity).get('type') === 'CTA_BOX')) {
currentValues.ctaBox = {};
const entityRange = currentEntity && getEntityRange(editorState, currentEntity);
currentValues.ctaBox.ctaTargetLink = currentEntity && contentState.getEntity(currentEntity).get('data').url;
currentValues.ctaBox.ctaTargetLinkOption = currentEntity && contentState.getEntity(currentEntity).get('data').targetOption;
currentValues.ctaBox.buttonText = (entityRange && entityRange.text) || contentState.getEntity(currentEntity).get('data').ctaButtonText;
currentValues.ctaBox.boxTitle = currentEntity && contentState.getEntity(currentEntity).get('data').ctaTitle;
currentValues.ctaBox.boxText = currentEntity && contentState.getEntity(currentEntity).get('data').ctaText;
currentValues.showModalforEdit = isOpen;
}
currentValues.selectionText = getSelectionText(editorState);
return currentValues;
}
doExpand: Function = () => {
this.setState({
expanded: true,
});
};
expandCollapse: Function = () => {
this.setState({
expanded: this.signalExpanded,
});
this.signalExpanded = false;
}
doCollapse: Function = () => {
const { clearStateEntities } = this.props;
clearStateEntities();
this.setState({
expanded: false,
});
};
addCTABox = (boxTitle, boxText, buttonText, url, ctaTargetLinkOption) => {
const { editorState, onChange } = this.props;
const { currentEntity } = this.state;
let selection = editorState.getSelection();
if (currentEntity) {
const entityRange = getEntityRange(editorState, currentEntity);
selection = selection.merge({
anchorOffset: entityRange.start,
focusOffset: entityRange.end,
});
}
const entityKey = editorState
.getCurrentContent()
.createEntity('CTA_BOX', 'MUTABLE', {
ctaTitle: boxTitle,
ctaText: boxText,
ctaButtonText: buttonText,
url,
targetOption: ctaTargetLinkOption,
})
.getLastCreatedEntityKey();
let contentState = Modifier.replaceText(
editorState.getCurrentContent(),
selection,
`${buttonText}`,
editorState.getCurrentInlineStyle(),
entityKey,
);
let newEditorState = EditorState.push(editorState, contentState, 'insert-characters');
// insert a blank space after ctaBox
if (!currentEntity) {
selection = newEditorState.getSelection().merge({
anchorOffset: selection.get('anchorOffset') + buttonText.length,
focusOffset: selection.get('focusOffset') + buttonText.length,
});
newEditorState = EditorState.acceptSelection(newEditorState, selection);
contentState = Modifier.insertText(
newEditorState.getCurrentContent(),
selection,
' ',
newEditorState.getCurrentInlineStyle(),
undefined,
);
}
onChange(EditorState.push(newEditorState, contentState, 'insert-characters'));
this.doCollapse();
};
render() {
const { config, translations, clearStateEntities } = this.props;
const { expanded } = this.state;
const { ctaBox, selectionText, showModalforEdit } = this.getCurrentValues();
const CTABoxComponent = config.component || LayoutComponent;
return (
<CTABoxComponent
config={config}
translations={translations}
expanded={expanded}
showModalforEdit={showModalforEdit}
onExpandEvent={this.onExpandEvent}
clearStateEntities={clearStateEntities}
doExpand={this.doExpand}
doCollapse={this.doCollapse}
currentState={{
ctaBox,
selectionText,
}}
onChange={this.onChange}
/>
);
}
}
export default CTABox;
// Here is the actual Editor component which will be imported in UI . Also want to note , this is a fork of react-draft-wysivgg
Editor/index.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Editor,
EditorState,
RichUtils,
convertToRaw,
convertFromRaw,
CompositeDecorator,
Modifier,
SelectionState,
CharacterMetadata,
} from 'draft-js';
import {
changeDepth,
handleNewLine,
blockRenderMap,
getCustomStyleMap,
extractInlineStyle,
getSelectedBlocksType,
getSelectionText,
getEntityRange,
getSelectionEntity,
} from 'draftjs-utils';
import classNames from 'classnames';
import ModalHandler from '../event-handler/modals';
import FocusHandler from '../event-handler/focus';
import KeyDownHandler from '../event-handler/keyDown';
import SuggestionHandler from '../event-handler/suggestions';
import blockStyleFn from '../utils/BlockStyle';
import { mergeRecursive } from '../utils/toolbar';
import { hasProperty, filter } from '../utils/common';
import { handlePastedText } from '../utils/handlePaste';
import Controls from '../controls';
import getLinkDecorator from '../decorators/Link';
import getCTABoxDecorator from '../decorators/CTABox';
import getMentionDecorators from '../decorators/Mention';
import getHashtagDecorator from '../decorators/HashTag';
import getBlockRenderFunc from '../renderer';
import defaultToolbar from '../config/defaultToolbar';
import localeTranslations from '../i18n';
import './styles.css';
import '../../css/Draft.css';
export default class WysiwygEditor extends Component {
static propTypes = {
onChange: PropTypes.func,
onEditorStateChange: PropTypes.func,
onContentStateChange: PropTypes.func,
// initialContentState is deprecated
initialContentState: PropTypes.object,
defaultContentState: PropTypes.object,
contentState: PropTypes.object,
editorState: PropTypes.object,
defaultEditorState: PropTypes.object,
toolbarOnFocus: PropTypes.bool,
spellCheck: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
stripPastedStyles: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
toolbar: PropTypes.object,
toolbarCustomButtons: PropTypes.array,
toolbarClassName: PropTypes.string,
toolbarHidden: PropTypes.bool,
locale: PropTypes.string,
localization: PropTypes.object,
editorClassName: PropTypes.string,
wrapperClassName: PropTypes.string,
toolbarStyle: PropTypes.object,
editorStyle: PropTypes.object,
wrapperStyle: PropTypes.object,
uploadCallback: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onTab: PropTypes.func,
mention: PropTypes.object,
hashtag: PropTypes.object,
textAlignment: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
readOnly: PropTypes.bool,
tabIndex: PropTypes.number, // eslint-disable-line react/no-unused-prop-types
placeholder: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
ariaLabel: PropTypes.string,
ariaOwneeID: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
ariaActiveDescendantID: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
ariaAutoComplete: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
ariaDescribedBy: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
ariaExpanded: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
ariaHasPopup: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
customBlockRenderFunc: PropTypes.func,
wrapperId: PropTypes.number,
customDecorators: PropTypes.array,
editorRef: PropTypes.func,
};
static defaultProps = {
toolbarOnFocus: false,
toolbarHidden: false,
stripPastedStyles: false,
localization: { locale: 'en', translations: {} },
customDecorators: [],
};
constructor(props) {
super(props);
const toolbar = mergeRecursive(defaultToolbar, props.toolbar);
this.state = {
editorState: props.editorState,
editorFocused: false,
toolbar,
modalCtaBoxOpen: {
isOpen: false,
entityKey: null,
},
modalCtaBoxDeleted: false,
};
const wrapperId = props.wrapperId
? props.wrapperId
: Math.floor(Math.random() * 10000);
this.wrapperId = `rdw-wrapper-${wrapperId}`;
this.modalHandler = new ModalHandler();
this.focusHandler = new FocusHandler();
this.blockRendererFn = getBlockRenderFunc(
{
isReadOnly: this.isReadOnly,
isImageAlignmentEnabled: this.isImageAlignmentEnabled,
isVideoAlignmentEnabled: this.isVideoAlignmentEnabled,
isCtaImageAlignmentEnabled: this.isCtaImageAlignmentEnabled,
getEditorState: this.getEditorState,
onChange: this.onChange,
deleteEntity: this.onDeleteClicked,
},
props.customBlockRenderFunc,
);
this.editorProps = this.filterEditorProps(props);
this.customStyleMap = getCustomStyleMap();
}
componentWillMount() {
this.compositeDecorator = this.getCompositeDecorator();
console.log(this.props, 'will mounttt');
const editorState = this.createEditorState(this.compositeDecorator);
extractInlineStyle(editorState);
this.setState({
editorState,
});
}
componentDidMount() {
this.modalHandler.init(this.wrapperId);
}
componentWillReceiveProps(props) {
const newState = {};
if (this.props.toolbar !== props.toolbar) {
const toolbar = mergeRecursive(defaultToolbar, props.toolbar);
newState.toolbar = toolbar;
}
console.log(this.props.editorState, 'this.props.editorState');
console.log(props.editorState, 'props.editorState');
if (
hasProperty(props, 'editorState') &&
this.props.editorState !== props.editorState
) {
if (props.editorState) {
newState.editorState = EditorState.set(props.editorState, {
decorator: this.compositeDecorator,
});
} else {
newState.editorState = EditorState.createEmpty(this.compositeDecorator);
}
} else if (
hasProperty(props, 'contentState') &&
this.props.contentState !== props.contentState
) {
if (props.contentState) {
const newEditorState = this.changeEditorState(props.contentState);
if (newEditorState) {
newState.editorState = newEditorState;
}
} else {
newState.editorState = EditorState.createEmpty(this.compositeDecorator);
}
}
if (
props.editorState !== this.props.editorState ||
props.contentState !== this.props.contentState
) {
extractInlineStyle(newState.editorState);
}
this.setState(newState);
this.editorProps = this.filterEditorProps(props);
this.customStyleMap = getCustomStyleMap();
}
onEditorBlur = () => {
this.setState({
editorFocused: false,
});
};
onEditCTABoxClicked = (entityKey) => {
const modalCtaBoxOpen = { entityKey, isOpen: !this.state.modalCtaBoxOpen.isOpen };
this.setState({
modalCtaBoxOpen,
});
};
onDeleteCTABoxClicked = (entityKey) => {
const { editorState } = this.state;
let selection = editorState.getSelection();
if (entityKey) {
const entityRange = getEntityRange(editorState, entityKey);
if (entityRange) {
selection = selection.merge({
anchorOffset: entityRange.start,
focusOffset: entityRange.end,
});
this.setState({
editorState: RichUtils.toggleLink(editorState, selection, null),
});
} else {
let chars;
let content = editorState.getCurrentContent();
const blocks = content.getBlockMap().map((block) => {
if (block.getType() === 'atomic') {
chars = block.getCharacterList().map((char) => {
const entity = char.getEntity();
if (entityKey === entity) {
return CharacterMetadata.applyEntity(char, null);
}
return char;
});
}
return block.set('characterList', chars);
});
content = content.replaceEntityData(entityKey, {});
content.merge({ blockMap: content.getBlockMap().merge(blocks) });
this.setState({ editorState: EditorState.createWithContent(content) });
}
}
};
onDeleteClicked = (blockKey) => {
const { editorState } = this.state;
const contentState = editorState.getCurrentContent();
const afterKey = contentState.getKeyAfter(blockKey);
const targetRange = new SelectionState({
anchorKey: blockKey,
anchorOffset: 0,
focusKey: afterKey,
focusOffset: 0,
});
let newContentState = Modifier.setBlockType(
contentState,
targetRange,
'unstyled',
);
newContentState = Modifier.removeRange(newContentState, targetRange, 'backward');
const newEditorState = EditorState.push(editorState, newContentState, 'remove-range');
this.setState({
editorState: newEditorState,
});
}
onEditorFocus = (event) => {
const { onFocus } = this.props;
this.setState({
editorFocused: true,
});
const editFocused = this.focusHandler.isEditorFocused();
if (onFocus && editFocused) {
onFocus(event);
}
};
onEditorMouseDown = () => {
this.focusHandler.onEditorMouseDown();
};
onTab = (event) => {
const { onTab } = this.props;
if (!onTab || !onTab(event)) {
const editorState = changeDepth(
this.state.editorState,
event.shiftKey ? -1 : 1,
4,
);
if (editorState && editorState !== this.state.editorState) {
this.onChange(editorState);
event.preventDefault();
}
}
};
onUpDownArrow = (event) => {
if (SuggestionHandler.isOpen()) {
event.preventDefault();
}
};
onToolbarFocus = (event) => {
const { onFocus } = this.props;
if (onFocus && this.focusHandler.isToolbarFocused()) {
onFocus(event);
}
};
onWrapperBlur = (event) => {
const { onBlur } = this.props;
if (onBlur && this.focusHandler.isEditorBlur(event)) {
onBlur(event, this.getEditorState());
}
};
onChange = (editorState) => {
const { readOnly, onEditorStateChange } = this.props;
if (
!readOnly &&
!(
getSelectedBlocksType(editorState) === 'atomic' &&
editorState.getSelection().isCollapsed
)
) {
if (onEditorStateChange) {
onEditorStateChange(editorState, this.props.wrapperId);
}
if (!hasProperty(this.props, 'editorState')) {
this.setState({ editorState }, this.afterChange(editorState));
} else {
this.afterChange(editorState);
}
}
};
setWrapperReference = (ref) => {
this.wrapper = ref;
};
setEditorReference = (ref) => {
if (this.props.editorRef) {
this.props.editorRef(ref);
}
this.editor = ref;
};
getCompositeDecorator = () => {
const decorators = [
...this.props.customDecorators,
getLinkDecorator({
showOpenOptionOnHover: this.state.toolbar.link.showOpenOptionOnHover,
}),
getCTABoxDecorator({
showOpenOptionOnHover: this.state.toolbar.ctaBox.showOpenOptionOnHover,
onEditCTABoxClicked: this.onEditCTABoxClicked,
onDeleteCTABoxClicked: this.onDeleteCTABoxClicked,
}),
];
if (this.props.mention) {
decorators.push(...getMentionDecorators({
...this.props.mention,
onChange: this.onChange,
getEditorState: this.getEditorState,
getSuggestions: this.getSuggestions,
getWrapperRef: this.getWrapperRef,
modalHandler: this.modalHandler,
}));
}
if (this.props.hashtag) {
decorators.push(getHashtagDecorator(this.props.hashtag));
}
return new CompositeDecorator(decorators);
};
getWrapperRef = () => this.wrapper;
getEditorState = () => this.state.editorState;
getSuggestions = () => this.props.mention && this.props.mention.suggestions;
clearStateEntities =() => {
this.setState({
modalCtaBoxOpen: {
isOpen: false,
},
});
}
afterChange = (editorState) => {
setTimeout(() => {
const { onChange, onContentStateChange } = this.props;
if (onChange) {
onChange(convertToRaw(editorState.getCurrentContent()));
}
if (onContentStateChange) {
onContentStateChange(convertToRaw(editorState.getCurrentContent()));
}
});
};
isReadOnly = () => this.props.readOnly;
isImageAlignmentEnabled = () => this.state.toolbar.image.alignmentEnabled;
isVideoAlignmentEnabled = () => this.state.toolbar.video.alignmentEnabled;
isCtaImageAlignmentEnabled = () => this.state.toolbar.ctaImage.alignmentEnabled;
createEditorState = (compositeDecorator) => {
let editorState;
if (hasProperty(this.props, 'editorState')) {
if (this.props.editorState) {
editorState = EditorState.set(this.props.editorState, {
decorator: compositeDecorator,
});
}
} else if (hasProperty(this.props, 'defaultEditorState')) {
if (this.props.defaultEditorState) {
editorState = EditorState.set(this.props.defaultEditorState, {
decorator: compositeDecorator,
});
}
} else if (hasProperty(this.props, 'contentState')) {
if (this.props.contentState) {
const contentState = convertFromRaw(this.props.contentState);
editorState = EditorState.createWithContent(
contentState,
compositeDecorator,
);
editorState = EditorState.moveSelectionToEnd(editorState);
}
} else if (
hasProperty(this.props, 'defaultContentState') ||
hasProperty(this.props, 'initialContentState')
) {
let contentState =
this.props.defaultContentState || this.props.initialContentState;
if (contentState) {
contentState = convertFromRaw(contentState);
editorState = EditorState.createWithContent(
contentState,
compositeDecorator,
);
editorState = EditorState.moveSelectionToEnd(editorState);
}
}
if (!editorState) {
editorState = EditorState.createEmpty(compositeDecorator);
}
return editorState;
};
filterEditorProps = props =>
filter(props, [
'onChange',
'onEditorStateChange',
'onContentStateChange',
'initialContentState',
'defaultContentState',
'contentState',
'editorState',
'defaultEditorState',
'locale',
'localization',
'toolbarOnFocus',
'toolbar',
'toolbarCustomButtons',
'toolbarClassName',
'editorClassName',
'toolbarHidden',
'wrapperClassName',
'toolbarStyle',
'editorStyle',
'wrapperStyle',
'uploadCallback',
'onFocus',
'onBlur',
'onTab',
'mention',
'hashtag',
'ariaLabel',
'customBlockRenderFunc',
'customDecorators',
'handlePastedText',
]);
changeEditorState = (contentState) => {
const newContentState = convertFromRaw(contentState);
let { editorState } = this.state;
editorState = EditorState.push(
editorState,
newContentState,
'insert-characters',
);
editorState = EditorState.moveSelectionToEnd(editorState);
return editorState;
};
focusEditor = () => {
setTimeout(() => {
this.editor.focus();
});
};
handleKeyCommand = (command) => {
const { editorState, toolbar: { inline } } = this.state;
if (inline && inline.options.indexOf(command) >= 0) {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
this.onChange(newState);
return true;
}
}
return false;
};
handleReturn = (event) => {
if (SuggestionHandler.isOpen()) {
return true;
}
const editorState = handleNewLine(this.state.editorState, event);
if (editorState) {
this.onChange(editorState);
return true;
}
return false;
};
handlePastedText = (text, html) => {
const { editorState } = this.state;
if (this.props.handlePastedText) {
return this.props.handlePastedText(
text,
html,
editorState,
this.onChange,
);
}
if (!this.props.stripPastedStyles) {
return handlePastedText(text, html, editorState, this.onChange);
}
return false;
};
preventDefault = (event) => {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'LABEL') {
this.focusHandler.onInputMouseDown();
} else {
event.preventDefault();
}
};
render() {
const {
editorState, editorFocused, toolbar, modalCtaBoxOpen,
} = this.state;
const {
locale,
localization: { locale: newLocale, translations },
toolbarCustomButtons,
toolbarOnFocus,
toolbarClassName,
toolbarHidden,
editorClassName,
wrapperClassName,
toolbarStyle,
editorStyle,
wrapperStyle,
uploadCallback,
ariaLabel,
} = this.props;
const controlProps = {
modalHandler: this.modalHandler,
editorState,
onChange: this.onChange,
translations: {
...localeTranslations[locale || newLocale],
...translations,
},
};
const toolbarShow =
editorFocused || this.focusHandler.isInputFocused() || !toolbarOnFocus;
return (
<div
id={this.wrapperId}
className={classNames(wrapperClassName, 'rdw-editor-wrapper')}
style={wrapperStyle}
onClick={this.modalHandler.onEditorClick}
onBlur={this.onWrapperBlur}
aria-label="rdw-wrapper"
>
{!toolbarHidden && (
<div
className={classNames('rdw-editor-toolbar', toolbarClassName)}
style={{
visibility: toolbarShow ? 'visible' : 'hidden',
...toolbarStyle,
}}
onMouseDown={this.preventDefault}
aria-label="rdw-toolbar"
aria-hidden={(!editorFocused && toolbarOnFocus).toString()}
onFocus={this.onToolbarFocus}
>
{toolbar.options.map((opt, index) => {
const Control = Controls[opt];
const config = toolbar[opt];
if (['image', 'video', 'ctaImage'].includes(opt) && uploadCallback) {
config.uploadCallback = uploadCallback;
}
return (<Control
key={index}
clearStateEntities={this.clearStateEntities}
modalCtaBoxOpen={modalCtaBoxOpen}
{...controlProps}
config={config}
/>);
})}
{toolbarCustomButtons &&
toolbarCustomButtons.map((button, index) =>
React.cloneElement(button, { key: index, ...controlProps }))}
</div>
)}
<div
ref={this.setWrapperReference}
className={classNames(editorClassName, 'rdw-editor-main')}
style={editorStyle}
onClick={this.focusEditor}
onFocus={this.onEditorFocus}
onBlur={this.onEditorBlur}
onKeyDown={KeyDownHandler.onKeyDown}
onMouseDown={this.onEditorMouseDown}
>
<Editor
ref={this.setEditorReference}
onTab={this.onTab}
onUpArrow={this.onUpDownArrow}
onDownArrow={this.onUpDownArrow}
editorState={editorState}
onChange={this.onChange}
blockStyleFn={blockStyleFn}
customStyleMap={getCustomStyleMap()}
handleReturn={this.handleReturn}
handlePastedText={this.handlePastedText}
blockRendererFn={this.blockRendererFn}
handleKeyCommand={this.handleKeyCommand}
ariaLabel={ariaLabel || 'rdw-editor'}
blockRenderMap={blockRenderMap}
{...this.editorProps}
/>
</div>
</div>
);
}
}
// todo: evaluate draftjs-utils to move some methods here
// todo: move color near font-family
// Here it gets the html content from UI , and creates Editor components
// fork of html-to-draftjs
import {CharacterMetadata, ContentBlock, genKey, Entity} from 'draft-js';
import {Map, List, OrderedMap, OrderedSet} from 'immutable';
import getSafeBodyFromHTML from './getSafeBodyFromHTML';
import {
createTextChunk,
getSoftNewlineChunk,
getEmptyChunk,
getBlockDividerChunk,
getFirstBlockChunk,
getAtomicBlockChunk,
joinChunks,
} from './chunkBuilder';
import getBlockTypeForTag from './getBlockTypeForTag';
import processInlineTag from './processInlineTag';
import getBlockData from './getBlockData';
import getEntityId from './getEntityId';
const SPACE = ' ';
const REGEX_NBSP = new RegExp('&nbsp;', 'g');
let firstBlock = true;
type CustomChunkGenerator = (nodeName: string, node: HTMLElement) => ?{type: string, mutability: string, data: {}};
function genFragment(
node: Object,
inlineStyle: OrderedSet,
depth: number,
lastList: string,
inEntity: number,
customChunkGenerator: ?CustomChunkGenerator,
): Object {
const nodeName = node.nodeName.toLowerCase();
if (customChunkGenerator) {
const value = customChunkGenerator(nodeName, node);
if (value) {
const entityId = Entity.__create(
value.type,
value.mutability,
value.data || {},
);
return {chunk: getAtomicBlockChunk(entityId)};
}
}
if (nodeName === 'div' &&
node instanceof HTMLDivElement
) {
const entityConfig = {};
entityConfig.ctaTitle = node.getElementsByTagName('H3')[0].innerHTML;
entityConfig.ctaText = node.getElementsByTagName('P')[0].innerHTML;
entityConfig.ctaButtonText = node.getElementsByTagName('A')[0].innerHTML;
entityConfig.url = node.getElementsByTagName('A')[0].getAttribute('href');
entityConfig.targetOption = node.getElementsByTagName('A')[0].getAttribute('target');
const entityId = Entity.__create(
'CTA_BOX',
'MUTABLE',
entityConfig,
);
return {chunk: getAtomicBlockChunk(entityId)};
}
if (nodeName === 'a' &&
node instanceof HTMLAnchorElement &&
node.id === 'ctaimage-root'
) {
const image = node.getElementsByTagName('img')[0];
const entityConfig = {};
entityConfig.src = image.getAttribute ? image.getAttribute('src') || image.src : image.src;
entityConfig.alt = image.alt;
entityConfig.height = image.style.height;
entityConfig.width = image.style.width;
if (image.style.float) {
entityConfig.alignment = image.style.float;
}
entityConfig.linkUrl = node.href;
const entityId = Entity.__create(
'CTA_IMAGE',
'MUTABLE',
entityConfig,
);
return {chunk: getAtomicBlockChunk(entityId)};
}
if (nodeName === '#text' && node.textContent !== '\n') {
return createTextChunk(node, inlineStyle, inEntity);
}
if (nodeName === 'br') {
return {chunk: getSoftNewlineChunk()};
}
if (
nodeName === 'img' &&
node instanceof HTMLImageElement
) {
const entityConfig = {};
entityConfig.src = node.getAttribute ? node.getAttribute('src') || node.src : node.src;
entityConfig.alt = node.alt;
entityConfig.height = node.style.height;
entityConfig.width = node.style.width;
if (node.style.float) {
entityConfig.alignment = node.style.float;
}
const entityId = Entity.__create(
'IMAGE',
'MUTABLE',
entityConfig,
);
return {chunk: getAtomicBlockChunk(entityId)};
}
if (
nodeName === 'video' &&
node instanceof HTMLVideoElement
) {
const entityConfig = {};
entityConfig.controls = true;
entityConfig.src = node.getAttribute ? node.getAttribute('src') || node.src : node.src;
entityConfig.alt = node.alt;
entityConfig.height = node.style.height;
entityConfig.width = node.style.width;
if (node.style.float) {
entityConfig.alignment = node.style.float;
}
const entityId = Entity.__create(
'VIDEO',
'MUTABLE',
entityConfig,
);
return {chunk: getAtomicBlockChunk(entityId)};
}
if (
nodeName === 'iframe' &&
node instanceof HTMLIFrameElement
) {
const entityConfig = {};
entityConfig.src = node.getAttribute ? node.getAttribute('src') || node.src : node.src;
entityConfig.height = node.height;
entityConfig.width = node.width;
const entityId = Entity.__create(
'EMBEDDED_LINK',
'MUTABLE',
entityConfig,
);
return {chunk: getAtomicBlockChunk(entityId)};
}
const blockType = getBlockTypeForTag(nodeName, lastList, node.className);
let chunk;
if (blockType) {
if (nodeName === 'ul' || nodeName === 'ol') {
lastList = nodeName;
depth += 1;
} else {
if (
blockType !== 'unordered-list-item' &&
blockType !== 'ordered-list-item'
) {
lastList = '';
depth = -1;
}
if (!firstBlock) {
chunk = getBlockDividerChunk(
blockType,
depth,
getBlockData(node)
);
} else {
chunk = getFirstBlockChunk(
blockType,
getBlockData(node)
);
firstBlock = false;
}
}
}
if (!chunk) {
chunk = getEmptyChunk();
}
inlineStyle = processInlineTag(nodeName, node, inlineStyle);
let child = node.firstChild;
while (child) {
const entityId = getEntityId(child);
const {chunk: generatedChunk} = genFragment(child, inlineStyle, depth, lastList, (entityId || inEntity), customChunkGenerator);
chunk = joinChunks(chunk, generatedChunk);
const sibling = child.nextSibling;
child = sibling;
}
return {chunk};
}
function getChunkForHTML(html: string, customChunkGenerator: ?CustomChunkGenerator): Object {
const sanitizedHtml = html.trim().replace(REGEX_NBSP, SPACE);
const safeBody = getSafeBodyFromHTML(sanitizedHtml);
if (!safeBody) {
return null;
}
firstBlock = true;
const {chunk} = genFragment(safeBody, new OrderedSet(), -1, '', undefined, customChunkGenerator);
return {chunk};
}
export default function htmlToDraft(html: string, customChunkGenerator: ?CustomChunkGenerator): Object {
const chunkData = getChunkForHTML(html, customChunkGenerator);
if (chunkData) {
const {chunk} = chunkData;
let entityMap = new OrderedMap({});
chunk.entities && chunk.entities.forEach(entity => {
if (entity) {
entityMap = entityMap.set(entity, Entity.__get(entity));
}
});
let start = 0;
return {
contentBlocks: chunk.text.split('\r')
.map(
(textBlock, ii) => {
const end = start + textBlock.length;
const inlines = chunk && chunk.inlines.slice(start, end);
const entities = chunk && chunk.entities.slice(start, end);
const characterList = new List(
inlines.map((style, index) => {
const data = {style, entity: null};
if (entities[index]) {
data.entity = entities[index];
}
return CharacterMetadata.create(data);
}),
);
start = end;
return new ContentBlock({
key: genKey(),
type: (chunk && chunk.blocks[ii] && chunk.blocks[ii].type) || 'unstyled',
depth: chunk && chunk.blocks[ii] && chunk.blocks[ii].depth,
data: (chunk && chunk.blocks[ii] && chunk.blocks[ii].data) || new Map({}),
text: textBlock,
characterList,
});
},
),
entityMap,
};
}
return null;
}
import ctaBox from './CTABox';
module.exports = {
ctaBox,
};
// UI , which fetches the editor content from database and rendes the Editor
import PropTypes from 'prop-types';
import React from 'react';
import {EditorState, convertToRaw, ContentState} from 'draft-js';
import {Editor} from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import PureBase from 'services/pure-base';
import {uploadEmbeddedFile} from 'modules/article-edit/operations.js';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
class RichEditor extends PureBase {
constructor(props) {
super(props);
const state = this.convertContentToEditorState(this.props.content);
this.state = {editorState: state};
}
static propTypes = {
onChange: PropTypes.func,
content: PropTypes.string,
};
onEditorStateChange = editorState => {
let content = draftToHtml(convertToRaw(editorState.getCurrentContent()));
if(content.indexOf('drop-cap')> -1 ){
if(content.indexOf('drop-cap', content.indexOf('drop-cap') + 8) !== -1){
while(content.indexOf('drop-cap', content.indexOf('drop-cap')+8) !== -1){
const index = content.indexOf('drop-cap', content.indexOf('drop-cap')+8);
content = content.substring(0, index - 1) + content.substring(index+8, content.length);
}
const state = this.convertContentToEditorState(content);
this.setState({editorState:state});
} else {
this.setState({editorState});
}
} else {
this.setState({editorState});
}
this.props.onChange(content);
};
convertContentToEditorState(content){
const blocksFromHtml = htmlToDraft(content || ''); // htmlToDraft is a fork from last file (fork html-to-draft-js/index.js)
const {contentBlocks, entityMap} = blocksFromHtml;
const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
return EditorState.createWithContent(contentState);
}
uploadImageCallBack(file) {
return uploadEmbeddedFile('image', file);
}
uploadVideoCallBack(file) {
return uploadEmbeddedFile('video', file);
}
render() {
const {editorState} = this.state;
return (
<div>
<Editor
editorState={editorState}
wrapperClassName="RichEditor-wrapper"
editorClassName="RichEditor-root"
onEditorStateChange={this.onEditorStateChange}
placeholder="Enter the text here…"
toolbar={{
options: ['blockType', 'inline', 'list', 'link', 'ctaBox', 'ctaImage', 'video', 'remove'],
inline: {
options: ['bold', 'italic', 'underline', 'superscript', 'subscript'],
},
image: {
uploadCallback: this.uploadImageCallBack,
alt: {present: false, mandatory: false},
previewImage: true,
},
video: {
uploadCallback: this.uploadVideoCallBack,
alt: {present: false, mandatory: false},
previewVideo: true,
sound: {
src: '/static/img/icon-volume-on.png',
text: 'Turn sound on',
},
},
ctaImage: {
uploadCallback: this.uploadImageCallBack,
alt: {present: false, mandatory: false},
previewImage: true,
},
blockType: {
inDropdown: true,
options: ['Normal', 'Header', 'Intro', 'Blockquote', 'Drop Cap'],
className: undefined,
component: undefined,
dropdownClassName: undefined,
},
}}
/>
</div>
);
}
}
export default RichEditor;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment