Better dom handling, disable state

This commit is contained in:
Chi Vinh Le
2018-03-24 15:36:49 +01:00
parent cc66be033a
commit a154ccfc60
9 changed files with 100 additions and 39 deletions
@@ -16,3 +16,14 @@
margin: 12px 0 0 12px;
color: #bbb;
}
.toolbarDisabled {
background: #f8f8f8;
cursor: default;
}
.contentEditableDisabled {
background: #fafafa;
color: #888;
cursor: default;
}
@@ -27,6 +27,7 @@ class RTE extends React.Component {
}
handleChange = () => {
this.handleSelectionChange();
this.props.onChange({
text: this.ref.htmlEl.innerText,
html: this.ref.htmlEl.innerHTML,
@@ -60,18 +61,10 @@ class RTE extends React.Component {
return handled;
};
handleClick = () => {
handleMouseUp = () => {
setTimeout(() => this.handleSelectionChange());
};
handleKeyDown = () => {
this.handleSelectionChange();
};
handleKeyUp = () => {
this.handleSelectionChange();
};
handleKeyPress = e => {
this.handleSelectionChange();
if (e.key === 'Enter') {
@@ -92,36 +85,51 @@ class RTE extends React.Component {
renderButtons() {
return this.props.buttons.map(b => {
return React.cloneElement(b, {
disabled: this.props.disabled,
api: this.api,
ref: this.createButtonRefHandler(b.key),
});
});
}
getClassNames() {
const { disabled } = this.props;
return {
toolbar: cn(this.props.toolbarClassName, {
[this.props.toolbarClassNameDisabled]: disabled,
[styles.toolbarDisabled]: disabled,
}),
content: cn(styles.contentEditable, this.props.contentClassName, {
[this.props.contentClassNameDisabled]: disabled,
[styles.contentEditableDisabled]: disabled,
}),
root: cn(this.props.className, {
[this.props.classNameDisabled]: disabled,
}),
placeholder: cn(styles.placeholder, this.props.placeholderClassName, {
[this.props.placeholderClassNameDisabled]: disabled,
}),
};
}
render() {
const {
className,
contentClassName,
toolbarClassName,
value,
placeholder,
inputId,
} = this.props;
const { value, placeholder, inputId, disabled } = this.props;
const classNames = this.getClassNames();
return (
<div className={cn(styles.root, className)}>
<Toolbar className={toolbarClassName}>{this.renderButtons()}</Toolbar>
{!value && <div className={styles.placeholder}>{placeholder}</div>}
<div className={classNames.root}>
<Toolbar className={classNames.toolbar}>{this.renderButtons()}</Toolbar>
{!value && <div className={classNames.placeholder}>{placeholder}</div>}
<ContentEditable
id={inputId}
onClick={this.handleClick}
onMouseUp={this.handleMouseUp}
onKeyUp={this.handleSelectionChange}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyDown}
className={cn(contentClassName, styles.contentEditable)}
className={classNames.content}
ref={this.handleRef}
html={value}
disabled={false}
disabled={disabled}
onChange={this.handleChange}
/>
</div>
@@ -140,8 +148,13 @@ RTE.propTypes = {
onChange: PropTypes.func,
disabled: PropTypes.bool,
className: PropTypes.string,
classNameDisabled: PropTypes.string,
contentClassName: PropTypes.string,
contentClassNameDisabled: PropTypes.string,
toolbarClassName: PropTypes.string,
toolbarClassNameDisabled: PropTypes.string,
placeholderClassName: PropTypes.string,
placeholderClassNameDisabled: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.string,
};
@@ -36,11 +36,11 @@ function execCommand() {
insertNodes(node);
selectNode(node);
}
this.broadcastChange();
}
this.broadcastChange();
}
function syncState() {
function isActive() {
return !!findIntersectingTag('BLOCKQUOTE');
}
@@ -52,7 +52,7 @@ const onEnter = node => {
return true;
};
const Blockquote = createToggle(execCommand, syncState, { onEnter });
const Blockquote = createToggle(execCommand, { onEnter, isActive });
Blockquote.defaultProps = {
children: 'Blockquote',
@@ -1,9 +1,9 @@
import createToggle from '../factories/createToggle';
const execCommand = () => document.execCommand('bold');
const syncState = () => document.queryCommandState('bold');
const isActive = () => document.queryCommandState('bold');
const Bold = createToggle(execCommand, syncState);
const Bold = createToggle(execCommand, { isActive });
Bold.defaultProps = {
children: 'Bold',
@@ -1,9 +1,9 @@
import createToggle from '../factories/createToggle';
const execCommand = () => document.execCommand('italic');
const syncState = () => document.queryCommandState('italic');
const isActive = () => document.queryCommandState('italic');
const Italic = createToggle(execCommand, syncState);
const Italic = createToggle(execCommand, { isActive });
Italic.defaultProps = {
children: 'Italic',
@@ -35,3 +35,9 @@
border-radius: 3px;
background-color: #ddd;
}
.button:disabled{
color: #bbb;
cursor: default;
background-color: inherit;
}
@@ -12,6 +12,7 @@ class Button extends React.Component {
children,
active,
activeClassName,
disabled,
} = this.props;
return (
<button
@@ -20,6 +21,7 @@ class Button extends React.Component {
})}
title={title}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
@@ -34,6 +36,7 @@ Button.propTypes = {
onClick: PropTypes.func,
children: PropTypes.node,
active: PropTypes.bool,
disabled: PropTypes.bool,
};
export default Button;
@@ -2,14 +2,19 @@ import React from 'react';
import PropTypes from 'prop-types';
import Button from '../components/Button';
const createToggle = (execCommand, getCurrentState, { onEnter } = {}) => {
const createToggle = (
execCommand,
{ onEnter, isActive = () => false, isDisabled = () => false } = {}
) => {
class Toggle extends React.Component {
state = {
active: false,
disabled: false,
};
execCommand = () => execCommand.apply(this.props.api);
getCurrentState = () => getCurrentState.apply(this.props.api);
isActive = () => isActive.apply(this.props.api);
isDisabled = () => isDisabled.apply(this.props.api);
onEnter = (...args) => onEnter && onEnter.apply(this.props.api, args);
formatToggle = () => {
@@ -23,11 +28,16 @@ const createToggle = (execCommand, getCurrentState, { onEnter } = {}) => {
};
syncState = () => {
if (this.state.active !== this.getCurrentState()) {
if (this.state.active !== this.isActive()) {
this.setState(state => ({
active: !state.active,
}));
}
if (this.state.disabled !== this.isDisabled()) {
this.setState(state => ({
disabled: !state.disabled,
}));
}
};
onSelectionChange() {
@@ -35,13 +45,14 @@ const createToggle = (execCommand, getCurrentState, { onEnter } = {}) => {
}
render() {
const { className, title, children } = this.props;
const { className, title, children, disabled } = this.props;
return (
<Button
className={className}
title={title}
onClick={this.handleClick}
active={this.state.active}
disabled={disabled || this.state.disabled}
>
{children}
</Button>
@@ -54,6 +65,7 @@ const createToggle = (execCommand, getCurrentState, { onEnter } = {}) => {
className: PropTypes.string,
title: PropTypes.string,
children: PropTypes.node,
disabled: PropTypes.bool,
};
return Toggle;
};
@@ -118,11 +118,24 @@ function ensureEndMarker(node) {
}
}
export function nodeContains(node, lookFor) {
if (node.isSameNode(lookFor)) {
return true;
}
for (let i = 0; i < node.childNodes.length; i++) {
if (nodeContains(node.childNodes[i], lookFor)) {
return true;
}
}
return false;
}
export function selectionIsInside(node) {
const range = getSelectionRange();
return (
range &&
(node.contains(range.startContainer) || node.contains(range.endContainer))
(nodeContains(node, range.startContainer) ||
nodeContains(node, range.endContainer))
);
}
@@ -251,6 +264,9 @@ export function getWholeLine(node) {
return [node];
}
const child = lastParentBeforeBlock(node);
if (child.tagName === 'BR') {
return [...getLeftOfNode(child), child];
}
return [...getLeftOfNode(child), child, ...getRightOfNode(child)];
}
@@ -302,13 +318,13 @@ export function getSelectedChildren(ancestor) {
for (let i = 0; i < ancestor.childNodes.length; i++) {
const node = ancestor.childNodes[i];
if (!foundStart) {
if (node.contains(start)) {
if (nodeContains(node, start)) {
foundStart = true;
}
}
if (foundStart) {
result.push(node);
if (node.contains(end)) {
if (nodeContains(node, end)) {
break;
}
}