UNPKG

wix-style-react

Version:
491 lines (419 loc) • 16.8 kB
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _class, _temp, _initialiseProps; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import FormFieldError from 'wix-ui-icons-common/system/FormFieldError'; import WixComponent from '../BaseComponents/WixComponent'; import { Editor, Block } from 'slate'; import Tooltip from '../Tooltip'; import RichTextEditorToolbar from './RichTextAreaToolbar'; import htmlSerializer from './htmlSerializer'; import styles from './RichTextArea.scss'; import isImage from 'is-image'; import isUrl from 'is-url'; var DEFAULT_NODE = 'paragraph'; var defaultBlock = { type: 'paragraph', isVoid: false, data: {}, key: 'defaultBlock' }; /* here we are checking is link absolute(if it contain 'https' or http or '//') and if it not absolute, then we add '//' at the beginning of it, to make link absolute */ export var makeHrefAbsolute = function makeHrefAbsolute(href) { return (/^(https?:)?\/\//.test(href) ? href : '//' + href ); }; var RichTextArea = (_temp = _class = function (_WixComponent) { _inherits(RichTextArea, _WixComponent); /* eslint-disable */ function RichTextArea(props) { _classCallCheck(this, RichTextArea); var _this = _possibleConstructorReturn(this, (RichTextArea.__proto__ || Object.getPrototypeOf(RichTextArea)).call(this, props)); _initialiseProps.call(_this); var editorState = htmlSerializer.deserialize(props.value); _this.state = { editorState: editorState }; _this.lastValue = props.value; return _this; } /* eslint-disable react/prop-types */ _createClass(RichTextArea, [{ key: 'componentWillReceiveProps', value: function componentWillReceiveProps(props) { var isPlaceholderChanged = props.placeholder !== this.props.placeholder; var isValueChanged = props.value && props.value !== this.props.value && props.value !== this.lastValue; if (isPlaceholderChanged || isValueChanged) { if (props.isAppend) { var newEditorState = this.state.editorState.transform().insertText(props.value).apply(); this.setEditorState(newEditorState); } else { var editorState = htmlSerializer.deserialize(props.value); this.setEditorState(editorState); } } } }, { key: 'triggerChange', value: function triggerChange() { var isTextChanged = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; var serialized = htmlSerializer.serialize(this.state.editorState); this.lastValue = serialized; if (isTextChanged) { var onChange = this.props.onChange; onChange && onChange(serialized); } } }]); return RichTextArea; }(WixComponent), _class.displayName = 'RichTextArea', _class.propTypes = { /** Is the rich text area automatically transforming relative links to absolute after user insert */ absoluteLinks: PropTypes.bool, buttons: PropTypes.arrayOf(PropTypes.string), // TODO: use PropTypes.oneOf(), dataHook: PropTypes.string, /** Is the rich text area disabled */ disabled: PropTypes.bool, /** Is input value erroneous */ error: PropTypes.bool, /** The error message to display when hovering the error icon, if not given or empty there will be no tooltip */ errorMessage: PropTypes.string, /** Placeholder text */ placeholder: PropTypes.string, /** Max height of the text editor */ maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), resizable: PropTypes.bool, /** Content HTML. Supported tags: `p`, `strong`, `em`, `u`, `ul`, `ol`, `li` */ value: PropTypes.string, /** Change callback */ onChange: PropTypes.func, /** Image icon click callback. * It is a function which recieves a callback. * The callback function should be called * when we obtain the url text (callback(text)), causing the image to reflect in the editor */ onImageRequest: PropTypes.func }, _class.defaultProps = { absoluteLinks: false, errorMessage: '', value: '<p></p>' }, _initialiseProps = function _initialiseProps() { var _this2 = this; this.schema = { nodes: { 'unordered-list': function unorderedList(props) { return React.createElement( 'ul', props.attributes, props.children ); }, 'list-item': function listItem(props) { return React.createElement( 'li', props.attributes, props.children ); }, 'ordered-list': function orderedList(props) { return React.createElement( 'ol', props.attributes, props.children ); }, link: function link(props) { var data = props.node.data; var href = data.get('href'); return React.createElement( 'a', _extends({ className: styles.link }, props.attributes, { rel: 'noopener noreferrer', target: '_blank', href: href }), props.children ); }, image: function image(props) { var node = props.node, state = props.state; var isFocused = state.selection.hasEdgeIn(node); var src = node.data.get('src'); return React.createElement('img', { 'data-hook': 'editor-image', src: src, className: classNames(styles.editorImage, _defineProperty({}, styles.activeEditorImage, isFocused)) }); } }, marks: { bold: { fontWeight: 'bold' }, italic: { fontStyle: 'italic' }, underline: { textDecoration: 'underline' } }, rules: [ // Rule to insert a paragraph block if the document is empty. { match: function match(node) { return node.kind === 'document'; }, validate: function validate(document) { return document.nodes.size ? null : true; }, normalize: function normalize(transform, document) { var block = Block.create(defaultBlock); transform.insertNodeByKey(document.key, 0, block); } }, // Rule to insert a paragraph below a void node (the image) if that node is // the last one in the document. { match: function match(node) { return node.kind === 'document'; }, validate: function validate(document) { var lastNode = document.nodes.last(); return lastNode && lastNode.isVoid ? true : null; }, normalize: function normalize(transform, document) { var block = Block.create(defaultBlock); transform.insertNodeByKey(document.key, document.nodes.size, block); } }] }; this.setEditorState = function (editorState) { var isTextChanged = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; _this2.setState({ editorState: editorState }, function () { return _this2.triggerChange(isTextChanged); }); }; this.hasBlock = function (type) { return _this2.state.editorState.blocks.some(function (node) { return node.type == type; }); }; this.hasListBlock = function (type) { var editorState = _this2.state.editorState; return editorState.blocks.some(function (node) { var parent = editorState.document.getParent(node.key); return parent && parent.type === type; }); }; this.hasMark = function (type) { return _this2.state.editorState.marks.some(function (mark) { return mark.type == type; }); }; this.hasLink = function () { return _this2.state.editorState.inlines.some(function (inline) { return inline.type === "link"; }); }; this.handleButtonClick = function (action, type) { _this2.setState({ activeToolbarButton: type }); switch (action) { case "mark": return _this2.handleMarkButtonClick(type); case "block": return _this2.handleBlockButtonClick(type); case "link": return _this2.handleLinkButtonClick(type); case "image": return _this2.handleImageButtonClick(type); } }; this.handleMarkButtonClick = function (type) { var editorState = _this2.state.editorState.transform().toggleMark(type).apply(); _this2.setEditorState(editorState); }; this.handleImageButtonClick = function (type) { _this2.props.onImageRequest(_this2.handleImageInput.bind(_this2)); }; this.handleImageInput = function (text) { if (_this2.isValidImage(text)) { var editorState = _this2.insertImage(_this2.state.editorState, text); _this2.setEditorState(editorState); } }; this.onPaste = function (e, data, state, editor) { switch (data.type) { case "text": return _this2.onPasteText(data.text, state); } }; this.onPasteText = function (text, state) { if (_this2.isValidImage(text)) { return _this2.insertImage(state, text); } return; }; this.isValidImage = function (text) { return isUrl(text) && isImage(text); }; this.insertImage = function (state, src) { return state.transform().insertBlock({ type: "image", isVoid: true, data: { src: src } }).apply(); }; this.handleBlockButtonClick = function (type) { var editorState = _this2.state.editorState; var transform = editorState.transform(); var _editorState = editorState, document = _editorState.document; // Handle everything but list buttons. if (type !== "unordered-list" && type !== "ordered-list") { var isActive = _this2.hasBlock(type); var isList = _this2.hasBlock("list-item"); if (isList) { transform.setBlock(isActive ? "" : type).unwrapBlock("unordered-list").unwrapBlock("ordered-list"); } else { transform.setBlock(isActive ? "" : type); } } // Handle the extra wrapping required for list buttons. else { var _isList = _this2.hasBlock("list-item"); var isType = editorState.blocks.some(function (block) { return !!document.getClosest(block.key, function (parent) { return parent.type == type; }); }); if (_isList && isType) { transform.setBlock(DEFAULT_NODE).unwrapBlock("unordered-list").unwrapBlock("ordered-list"); } else if (_isList) { transform.unwrapBlock(type == "unordered-list" ? "ordered-list" : "unordered-list").wrapBlock(type); } else { transform.setBlock("list-item").wrapBlock(type); } } editorState = transform.apply(); _this2.setState({ editorState: editorState }); }; this.handleLinkButtonClick = function () { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, href = _ref.href, text = _ref.text; var editorState = _this2.state.editorState; var transform = editorState.transform(); var decoratedHref = _this2.props.absoluteLinks ? makeHrefAbsolute(href) : href; if (_this2.hasLink()) { transform.unwrapInline("link"); } else { var linkContent = text || decoratedHref; var startPos = editorState.anchorOffset; transform.insertText(linkContent).select({ anchorOffset: startPos, focusOffset: startPos + linkContent.length, isFocused: true, isBackward: false }).wrapInline({ type: "link", data: { href: decoratedHref } }).focus().collapseToEnd(); } _this2.setEditorState(transform.apply()); }; this.render = function () { var _classNames2, _classNames4; var editorState = _this2.state.editorState; var _props = _this2.props, error = _props.error, placeholder = _props.placeholder, disabled = _props.disabled, resizable = _props.resizable, onImageRequest = _props.onImageRequest, dataHook = _props.dataHook; var className = classNames(styles.container, (_classNames2 = {}, _defineProperty(_classNames2, styles.withError, error), _defineProperty(_classNames2, styles.isEditorFocused, editorState.isFocused), _classNames2)); var isScrollable = resizable || _this2.props.maxHeight; return React.createElement( 'div', { className: className, 'data-hook': dataHook }, React.createElement( 'div', { className: classNames(styles.toolbar, _defineProperty({}, styles.disabled, disabled)), 'data-hook': 'toolbar' }, React.createElement(RichTextEditorToolbar /* activeToolbarButton prop required to trigger RichTextEditorToolbar re-render after toolbar button click */ , { activeToolbarButton: _this2.state.activeToolbarButton, selection: editorState.fragment.text, disabled: disabled, onClick: _this2.handleButtonClick, onLinkButtonClick: _this2.handleLinkButtonClick, onImageButtonClick: onImageRequest ? _this2.handleImageButtonClick : null, hasMark: _this2.hasMark, hasListBlock: _this2.hasListBlock, hasLink: _this2.hasLink, isSelectionExpanded: editorState.isExpanded }) ), React.createElement( 'div', { className: classNames(styles.editorWrapper, (_classNames4 = {}, _defineProperty(_classNames4, styles.resizable, resizable), _defineProperty(_classNames4, styles.scrollable, isScrollable), _defineProperty(_classNames4, styles.disabled, disabled), _classNames4)), 'data-hook': 'editor-wrapper', style: { maxHeight: _this2.props.maxHeight } }, React.createElement(Editor, { readOnly: disabled, placeholder: placeholder, placeholderClassName: styles.placeholder, className: classNames(styles.editor, _defineProperty({}, styles.disabled, disabled)), schema: _this2.schema, state: editorState, onPaste: _this2.onPaste, onChange: function onChange(e) { var serialized = htmlSerializer.serialize(e); var isValueChanged = serialized !== _this2.lastValue; _this2.lastValue = serialized; _this2.setEditorState(e, isValueChanged); } }), _this2.renderError() ) ); }; this.renderError = function () { var errorMessage = _this2.props.errorMessage; return React.createElement( Tooltip, { disabled: !errorMessage, placement: 'top', moveBy: { x: 2, y: 0 }, alignment: 'center', content: errorMessage, theme: 'dark' }, React.createElement( 'div', { className: styles.exclamation }, React.createElement(FormFieldError, null) ) ); }; }, _temp); export default RichTextArea;