wix-style-react
Version:
wix-style-react
491 lines (419 loc) • 16.8 kB
JavaScript
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;