design-comuni-plone-theme
Version:
Volto Theme for Italia design guidelines
416 lines (390 loc) • 11.4 kB
JSX
/**
* Edit html block.
* @module components/manage/Blocks/HTML/Edit
*
* Customizations:
* - added bgColor
* - added showFullWidths
*/
import { compose } from 'redux';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Popup } from 'semantic-ui-react';
import { defineMessages, injectIntl } from 'react-intl';
import loadable from '@loadable/component';
import { isEqual } from 'lodash';
import cx from 'classnames';
import { Icon, SidebarPortal } from '@plone/volto/components';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import showSVG from '@plone/volto/icons/show.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
import codeSVG from '@plone/volto/icons/code.svg';
import indentSVG from '@plone/volto/icons/indent.svg';
import Sidebar from 'design-comuni-plone-theme/components/ItaliaTheme/Blocks/HTML/Sidebar';
import { Toast } from '@plone/volto/components';
const Editor = loadable(() => import('react-simple-code-editor'));
const messages = defineMessages({
source: {
id: 'Source',
defaultMessage: 'Source',
},
preview: {
id: 'Preview',
defaultMessage: 'Preview',
},
placeholder: {
id: '<p>Add some HTML here</p>',
defaultMessage: '<p>Add some HTML here</p>',
},
prettier: {
id: 'Prettify your code',
defaultMessage: 'Prettify your code',
},
clear: {
id: 'Clear',
defaultMessage: 'Clear',
},
code: {
id: 'Code',
defaultMessage: 'Code',
},
});
/**
* Edit html block class.
* @class Edit
* @extends Component
*/
class Edit extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
selected: PropTypes.bool.isRequired,
block: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
data: PropTypes.objectOf(PropTypes.any).isRequired,
onChangeBlock: PropTypes.func.isRequired,
onSelectBlock: PropTypes.func.isRequired,
onDeleteBlock: PropTypes.func.isRequired,
handleKeyDown: PropTypes.func.isRequired,
editable: PropTypes.bool,
};
/**
* Default properties
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
editable: true,
};
/**
* Constructor
* @method constructor
* @param {Object} props Component properties
*/
constructor(props) {
super(props);
this.state = {
isPreview: false,
};
this.onChangeCode = this.onChangeCode.bind(this);
this.onPreview = this.onPreview.bind(this);
this.onCodeEditor = this.onCodeEditor.bind(this);
}
codeEditorRef = React.createRef();
savedSelection = {};
componentDidUpdate(prevProps, prevState, snapshot) {
// The selection is saved in the snapshot.
this.savedSelection = snapshot;
this.restoreSelectionAndFocus(this.codeEditorRef.current);
}
/**
* @param {*} nextProps
* @param {*} nextState
* @returns {boolean}
* @memberof Edit
*/
shouldComponentUpdate(nextProps) {
// Always rerender when the DOM node is not created for the Editor (the
// first call to shouldComponentUpdate).
if (!this._input) {
return true;
}
// Rerender the entire component when the Editor in it changes its selection
// because this way we get a call to getSnapshotBeforeUpdate where we can
// save the selection.
return (
this.props.selected ||
!isEqual(this.props.data, nextProps.data) ||
this._input.selectionStart !== this.savedSelection.selectionStart ||
this._input.selectionEnd !== this.savedSelection.selectionEnd
);
}
/**
* Change html handler
* @method onChangeCode
* @param {string} code New value html
* @returns {undefined}
*/
onChangeCode(code) {
this.props.onChangeBlock(this.props.block, {
...this.props.data,
html: code,
});
}
getValue() {
return this.props.data.html || '';
}
/**
* Preview mode handler
* @method onPreview
* @returns {undefined}
*/
onPreview() {
try {
this.props.prettierStandalone
.format(this.getValue(), {
parser: 'html',
plugins: [this.props.prettierParserHtml],
})
.then((value) => {
this.onChangeCode(value.trim());
});
this.setState({
isPreview: !this.state.isPreview,
});
} catch (ex) {
// error while parsing the user-typed HTML
this.props.toastify.toast.error(
<Toast
error
title="Errore"
content="C'è stato un errore nel formattare il codice html inserito"
/>,
);
}
}
/**
* Prettify handler
* @method onPrettify
* @returns {undefined}
*/
onPrettify = () => {
try {
this.props.prettierStandalone
.format(this.getValue(), {
parser: 'html',
plugins: [this.props.prettierParserHtml],
})
.then((value) => {
this.onChangeCode(value.trim());
});
} catch (ex) {
// error while parsing the user-typed HTML
this.props.toastify.toast.error(
<Toast
error
title="Errore"
content="C'è stato un errore nel formattare il codice html inserito"
/>,
);
}
};
/**
* Code Editor mode handler
* @method onPreview
* @returns {undefined}
*/
onCodeEditor() {
this.setState({ isPreview: !this.state.isPreview });
}
getSelection = (editor) => {
if (!editor || !editor._input) {
return {};
}
const o = {};
if (editor._input.selectionStart) {
o.selectionStart = editor._input.selectionStart;
}
if (editor._input.selectionEnd) {
o.selectionEnd = editor._input.selectionEnd;
}
return o;
};
getSnapshotBeforeUpdate(prevProps, prevState) {
return this.getSelection(this.codeEditorRef.current);
}
restoreSelectionAndFocus = (editor) => {
// Don't restore selection when the block is not selected.
if (
this.props.selected &&
editor._input &&
typeof this.savedSelection?.selectionStart === 'number' &&
typeof this.savedSelection?.selectionEnd === 'number'
) {
editor._input.selectionStart = this.savedSelection?.selectionStart;
editor._input.selectionEnd = this.savedSelection?.selectionEnd;
editor._input.focus();
}
};
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
const placeholder =
this.props.data.placeholder ||
this.props.intl.formatMessage(messages.placeholder);
const value = this.getValue();
return (
<div className="block html">
{this.props.selected && value && (
<div className="toolbar">
<Popup
trigger={
<Button
icon
basic
aria-label={this.props.intl.formatMessage(messages.source)}
active={!this.state.isPreview}
onClick={this.onCodeEditor}
type="button"
>
<Icon name={codeSVG} size="24px" />
</Button>
}
position="top center"
content={this.props.intl.formatMessage(messages.code)}
size="mini"
/>
<Popup
trigger={
<Button
icon
basic
aria-label={this.props.intl.formatMessage(messages.preview)}
active={this.state.isPreview}
onClick={this.onPreview}
type="button"
>
<Icon name={showSVG} size="24px" />
</Button>
}
position="top center"
content={this.props.intl.formatMessage(messages.preview)}
size="mini"
/>
<Popup
trigger={
<Button
icon
basic
aria-label={this.props.intl.formatMessage(messages.prettier)}
onClick={this.onPrettify}
type="button"
>
<Icon name={indentSVG} size="24px" />
</Button>
}
position="top center"
content={this.props.intl.formatMessage(messages.prettier)}
size="mini"
/>
<div className="separator" />
<Popup
trigger={
<Button.Group>
<Button
icon
basic
type="button"
onClick={() => this.onChangeCode('')}
>
<Icon name={clearSVG} size="24px" color="#e40166" />
</Button>
</Button.Group>
}
position="top center"
content={this.props.intl.formatMessage(messages.clear)}
size="mini"
/>
</div>
)}
<div className="public-ui">
<div
className={cx('py-5', {
'full-width': this.props.data.showFullWidth,
'bg-light': this.props.data.bgColor,
})}
>
{this.state.isPreview ? (
<div
className="container px-4"
dangerouslySetInnerHTML={{ __html: value }}
/>
) : (
<Editor
value={this.getValue()}
readOnly={!this.props.editable}
placeholder={placeholder}
onValueChange={(code) => this.onChangeCode(code)}
highlight={
this.props.prismCore?.highlight &&
this.props.prismCore?.languages?.html
? (code) =>
this.props.prismCore.highlight(
code,
this.props.prismCore.languages.html,
'html',
)
: () => {}
}
padding={8}
className="html-editor"
ref={(node) => {
if (node) {
this.codeEditorRef.current = node;
}
}}
ignoreTabKey={true}
/>
)}
</div>
<SidebarPortal selected={this.props.selected || false}>
<Sidebar {...this.props} />
</SidebarPortal>
</div>
</div>
);
}
}
const withPrismMarkup = (WrappedComponent) => (props) => {
const [loaded, setLoaded] = React.useState();
const promise = React.useRef(null);
const cancelled = React.useRef(false);
React.useEffect(() => {
promise.current = import('prismjs/components/prism-markup');
promise.current.then(() => {
if (!cancelled.current) {
setLoaded(true);
}
});
return () => {
cancelled.current = true;
};
}, []);
return loaded ? <WrappedComponent {...props} /> : null;
};
export default compose(
injectLazyLibs([
'prettierStandalone',
'prettierParserHtml',
'prismCore',
'toastify',
]),
withPrismMarkup,
injectIntl,
)(Edit);