medium-draft
Version:
A medium like rich text editor built upon draft-js with an emphasis on eliminating mouse usage by adding relevant keyboard shortcuts
447 lines (404 loc) • 16.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.INLINE_BUTTONS = exports.BLOCK_BUTTONS = undefined;
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 _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _blocktoolbar = require('./blocktoolbar');
var _blocktoolbar2 = _interopRequireDefault(_blocktoolbar);
var _inlinetoolbar = require('./inlinetoolbar');
var _inlinetoolbar2 = _interopRequireDefault(_inlinetoolbar);
var _index = require('../util/index');
var _index2 = require('../model/index');
var _constants = require('../util/constants');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: 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 './toolbar.scss';
var Toolbar = function (_React$Component) {
_inherits(Toolbar, _React$Component);
function Toolbar(props) {
_classCallCheck(this, Toolbar);
var _this = _possibleConstructorReturn(this, (Toolbar.__proto__ || Object.getPrototypeOf(Toolbar)).call(this, props));
_this.state = {
showURLInput: false,
urlInputValue: ''
};
_this.onKeyDown = _this.onKeyDown.bind(_this);
_this.onChange = _this.onChange.bind(_this);
_this.handleLinkInput = _this.handleLinkInput.bind(_this);
_this.hideLinkInput = _this.hideLinkInput.bind(_this);
return _this;
}
_createClass(Toolbar, [{
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(newProps) {
var editorState = newProps.editorState;
if (!newProps.editorEnabled) {
return;
}
var selectionState = editorState.getSelection();
if (selectionState.isCollapsed()) {
if (this.state.showURLInput) {
this.setState({
showURLInput: false,
urlInputValue: ''
});
}
return;
}
}
// shouldComponentUpdate(newProps, newState) {
// console.log(newState, this.state);
// if (newState.showURLInput !== this.state.showURLInput && newState.urlInputValue !== this.state.urlInputValue) {
// return true;
// }
// return false;
// }
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
if (!this.props.editorEnabled || this.state.showURLInput) {
return;
}
var selectionState = this.props.editorState.getSelection();
if (selectionState.isCollapsed()) {
return;
}
// eslint-disable-next-line no-undef
var nativeSelection = (0, _index.getSelection)(window);
if (!nativeSelection.rangeCount) {
return;
}
var selectionBoundary = (0, _index.getSelectionRect)(nativeSelection);
// eslint-disable-next-line react/no-find-dom-node
var toolbarNode = _reactDom2.default.findDOMNode(this);
var toolbarBoundary = toolbarNode.getBoundingClientRect();
// eslint-disable-next-line react/no-find-dom-node
var parent = _reactDom2.default.findDOMNode(this.props.editorNode);
var parentBoundary = parent.getBoundingClientRect();
/*
* Main logic for setting the toolbar position.
*/
toolbarNode.style.top = selectionBoundary.top - parentBoundary.top - toolbarBoundary.height - 5 + 'px';
toolbarNode.style.width = toolbarBoundary.width + 'px';
// The left side of the tooltip should be:
// center of selection relative to parent - half width of toolbar
var selectionCenter = selectionBoundary.left + selectionBoundary.width / 2 - parentBoundary.left;
var left = selectionCenter - toolbarBoundary.width / 2;
var screenLeft = parentBoundary.left + left;
if (screenLeft < 0) {
// If the toolbar would be off-screen
// move it as far left as it can without going off-screen
left = -parentBoundary.left;
}
toolbarNode.style.left = left + 'px';
}
}, {
key: 'onKeyDown',
value: function onKeyDown(e) {
if (e.which === 13) {
e.preventDefault();
e.stopPropagation();
this.props.setLink(this.state.urlInputValue);
this.hideLinkInput();
} else if (e.which === 27) {
this.hideLinkInput();
}
}
}, {
key: 'onChange',
value: function onChange(e) {
this.setState({
urlInputValue: e.target.value
});
}
}, {
key: 'handleLinkInput',
value: function handleLinkInput(e) {
var _this2 = this;
var direct = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (direct !== true) {
e.preventDefault();
e.stopPropagation();
}
var editorState = this.props.editorState;
var selection = editorState.getSelection();
if (selection.isCollapsed()) {
this.props.focus();
return;
}
var currentBlock = (0, _index2.getCurrentBlock)(editorState);
var selectedEntity = '';
var linkFound = false;
currentBlock.findEntityRanges(function (character) {
var entityKey = character.getEntity();
selectedEntity = entityKey;
return entityKey !== null && editorState.getCurrentContent().getEntity(entityKey).getType() === _constants.Entity.LINK;
}, function (start, end) {
var selStart = selection.getAnchorOffset();
var selEnd = selection.getFocusOffset();
if (selection.getIsBackward()) {
selStart = selection.getFocusOffset();
selEnd = selection.getAnchorOffset();
}
if (start === selStart && end === selEnd) {
linkFound = true;
var _editorState$getCurre = editorState.getCurrentContent().getEntity(selectedEntity).getData(),
url = _editorState$getCurre.url;
_this2.setState({
showURLInput: true,
urlInputValue: url
}, function () {
setTimeout(function () {
_this2.urlinput.focus();
_this2.urlinput.select();
}, 0);
});
}
});
if (!linkFound) {
this.setState({
showURLInput: true
}, function () {
setTimeout(function () {
_this2.urlinput.focus();
}, 0);
});
}
}
}, {
key: 'hideLinkInput',
value: function hideLinkInput() {
var e = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
if (e !== null) {
e.preventDefault();
e.stopPropagation();
}
this.setState({
showURLInput: false,
urlInputValue: ''
}, this.props.focus);
}
}, {
key: 'render',
value: function render() {
var _this3 = this;
var _props = this.props,
editorState = _props.editorState,
editorEnabled = _props.editorEnabled,
inlineButtons = _props.inlineButtons;
var _state = this.state,
showURLInput = _state.showURLInput,
urlInputValue = _state.urlInputValue;
var isOpen = true;
if (!editorEnabled || editorState.getSelection().isCollapsed()) {
isOpen = false;
}
if (showURLInput) {
var className = 'md-editor-toolbar' + (isOpen ? ' md-editor-toolbar--isopen' : '');
className += ' md-editor-toolbar--linkinput';
return _react2.default.createElement(
'div',
{
className: className
},
_react2.default.createElement(
'div',
{
className: 'md-RichEditor-controls md-RichEditor-show-link-input',
style: { display: 'block' }
},
_react2.default.createElement(
'span',
{ className: 'md-url-input-close', onClick: this.hideLinkInput },
'\xD7'
),
_react2.default.createElement('input', {
ref: function ref(node) {
_this3.urlinput = node;
},
type: 'text',
className: 'md-url-input',
onKeyDown: this.onKeyDown,
onChange: this.onChange,
placeholder: 'Press ENTER or ESC',
value: urlInputValue
})
)
);
}
var hasHyperLink = false;
var hyperlinkLabel = '#';
var hyperlinkDescription = 'Add a link';
for (var cnt = 0; cnt < inlineButtons.length; cnt++) {
if (inlineButtons[cnt].style === _constants.HYPERLINK) {
hasHyperLink = true;
if (inlineButtons[cnt].label) {
hyperlinkLabel = inlineButtons[cnt].label;
}
if (inlineButtons[cnt].description) {
hyperlinkDescription = inlineButtons[cnt].description;
}
break;
}
}
return _react2.default.createElement(
'div',
{
className: 'md-editor-toolbar' + (isOpen ? ' md-editor-toolbar--isopen' : '')
},
this.props.blockButtons.length > 0 ? _react2.default.createElement(_blocktoolbar2.default, {
editorState: editorState,
onToggle: this.props.toggleBlockType,
buttons: this.props.blockButtons
}) : null,
this.props.inlineButtons.length > 0 ? _react2.default.createElement(_inlinetoolbar2.default, {
editorState: editorState,
onToggle: this.props.toggleInlineStyle,
buttons: this.props.inlineButtons
}) : null,
hasHyperLink && _react2.default.createElement(
'div',
{ className: 'md-RichEditor-controls' },
_react2.default.createElement(
'span',
{
className: 'md-RichEditor-styleButton md-RichEditor-linkButton hint--top',
onClick: this.handleLinkInput,
'aria-label': hyperlinkDescription
},
hyperlinkLabel
)
)
);
}
}]);
return Toolbar;
}(_react2.default.Component);
Toolbar.propTypes = {
editorEnabled: _propTypes2.default.bool,
editorState: _propTypes2.default.object,
toggleBlockType: _propTypes2.default.func,
toggleInlineStyle: _propTypes2.default.func,
inlineButtons: _propTypes2.default.arrayOf(_propTypes2.default.object),
blockButtons: _propTypes2.default.arrayOf(_propTypes2.default.object),
editorNode: _propTypes2.default.object,
setLink: _propTypes2.default.func,
focus: _propTypes2.default.func
};
Toolbar.defaultProps = {
blockButtons: BLOCK_BUTTONS,
inlineButtons: INLINE_BUTTONS
};
exports.default = Toolbar;
var BLOCK_BUTTONS = exports.BLOCK_BUTTONS = [{
label: 'H3',
style: 'header-three',
icon: 'header',
description: 'Heading 3'
}, {
label: _react2.default.createElement(
'svg',
{ width: '10.83', height: '10', viewBox: '0 0 13 12' },
_react2.default.createElement(
'g',
{ stroke: 'none', strokeWidth: '1', fill: 'none', fillRule: 'evenodd' },
_react2.default.createElement(
'g',
{ transform: 'translate(-357.000000, -255.000000)', fill: '#FFFFFF' },
_react2.default.createElement(
'g',
{ transform: 'translate(260.000000, 165.000000)' },
_react2.default.createElement(
'g',
{ transform: 'translate(0.000000, 75.000000)' },
_react2.default.createElement(
'g',
{ transform: 'translate(19.000000, 0.000000)' },
_react2.default.createElement('path', { d: 'M90.500768,15 L91,15.56 C88.9631235,17.0533408 87.9447005,18.666658 87.9447005,20.4 C87.9447005,21.8800074 88.75012,23.1466614 90.3609831,24.2 L87.5453149,27 C85.9211388,25.7866606 85.109063,24.346675 85.109063,22.68 C85.109063,20.3199882 86.90628,17.7600138 90.500768,15 Z M83.3917051,15 L83.890937,15.56 C81.8540605,17.0533408 80.8356375,18.666658 80.8356375,20.4 C80.8356375,21.8800074 81.6344006,23.1466614 83.2319508,24.2 L80.4362519,27 C78.8120759,25.7866606 78,24.346675 78,22.68 C78,20.3199882 79.7972171,17.7600138 83.3917051,15 Z' })
)
)
)
)
)
),
style: 'blockquote',
icon: 'quote-right',
description: 'Blockquote'
}, {
label: 'UL',
style: 'unordered-list-item',
icon: 'list-ul',
description: 'Unordered List'
}, {
label: 'OL',
style: 'ordered-list-item',
icon: 'list-ol',
description: 'Ordered List'
}, {
label: '✓',
style: 'todo',
description: 'Todo List'
}];
var INLINE_BUTTONS = exports.INLINE_BUTTONS = [{
label: 'B',
style: 'BOLD',
icon: 'bold',
description: 'Bold'
}, {
label: 'I',
style: 'ITALIC',
icon: 'italic',
description: 'Italic'
}, {
label: 'U',
style: 'UNDERLINE',
icon: 'underline',
description: 'Underline'
}, {
label: 'Hi',
style: 'HIGHLIGHT',
description: 'Highlight selection'
}, {
label: _react2.default.createElement(
'svg',
{ width: '20', height: '15', viewBox: '0 0 14 14' },
_react2.default.createElement(
'g',
{ stroke: 'none', strokeWidth: '1', fill: 'none', fillRule: 'evenodd' },
_react2.default.createElement(
'g',
{ transform: 'translate(-468.000000, -254.000000)', stroke: '#FFFFFF' },
_react2.default.createElement(
'g',
{ transform: 'translate(260.000000, 165.000000)' },
_react2.default.createElement(
'g',
{ transform: 'translate(0.000000, 75.000000)' },
_react2.default.createElement(
'g',
{ transform: 'translate(19.000000, 0.000000)' },
_react2.default.createElement(
'g',
{ transform: 'translate(196.424621, 21.424621) rotate(45.000000) translate(-196.424621, -21.424621) translate(193.424621, 13.924621)' },
_react2.default.createElement('path', { d: 'M0.5,5.69098301 L0.5,2 C0.5,1.82069363 0.550664909,1.51670417 0.697213595,1.2236068 C0.927818928,0.762396132 1.32141313,0.5 2,0.5 L4,0.5 C4.67858687,0.5 5.07218107,0.762396132 5.3027864,1.2236068 C5.44933509,1.51670417 5.5,1.82069363 5.5,2 L5.5,6 C5.5,6.67858687 5.23760387,7.07218107 4.7763932,7.3027864 C4.53586606,7.42304998 4.28800365,7.47874077 4.1077327,7.49484936 L0.5,5.69098301 Z' }),
_react2.default.createElement('path', { d: 'M0.5,12.690983 L0.5,9 C0.5,8.82069363 0.550664909,8.51670417 0.697213595,8.2236068 C0.927818928,7.76239613 1.32141313,7.5 2,7.5 L4,7.5 C4.67858687,7.5 5.07218107,7.76239613 5.3027864,8.2236068 C5.44933509,8.51670417 5.5,8.82069363 5.5,9 L5.5,13 C5.5,13.6785869 5.23760387,14.0721811 4.7763932,14.3027864 C4.53586606,14.42305 4.28800365,14.4787408 4.1077327,14.4948494 L0.5,12.690983 Z', transform: 'translate(3.000000, 11.000000) scale(-1, -1) translate(-3.000000, -11.000000) ' })
)
)
)
)
)
)
),
style: _constants.HYPERLINK,
icon: 'link',
description: 'Add a link'
}];