Refactor n active state

This commit is contained in:
Chi Vinh Le
2018-03-23 12:37:20 +01:00
parent 1fb1fc6ba9
commit de48880e01
15 changed files with 362 additions and 115 deletions
@@ -1,20 +0,0 @@
.button > i {
vertical-align: middle;
}
.button {
background-color: transparent;
padding: 3px;
border: none;
color: #4e4e4e;
margin-right: 3px;
}
.button:hover{
cursor: pointer;
border-radius: 3px;
background-color: #eae8e8;
}
.icon {
font-size: 20px;
}
@@ -1,15 +1,5 @@
.contentEditable {
.commentContent {
composes: content from "./CommentContent.css";
background: #fff;
border: solid 1px #bbb;
min-height: 120px;
box-sizing: border-box;
outline: 0;
overflow-y: auto;
width: 100%;
padding: 10px;
font-style: unset;
margin-bottom: 3px;
}
.placeholder {
@@ -17,3 +7,7 @@
margin: 12px 0 0 12px;
color: #bbb;
}
.icon {
font-size: 20px;
}
@@ -4,19 +4,18 @@ import styles from './Editor.css';
import cn from 'classnames';
import { PLUGIN_NAME } from '../constants';
import { htmlNormalizer } from '../utils';
import ContentEditable from 'react-contenteditable';
import Toolbar from './Toolbar';
import Button from './Button';
import bowser from 'bowser';
import RTE from './rte/RTE';
import { Icon } from 'plugin-api/beta/client/components/ui';
import { Bold, Italic, Blockquote } from './rte/buttons';
class Editor extends React.Component {
ref = null;
handleRef = ref => (this.ref = ref);
handleChange = evt => {
handleChange = c => {
this.props.onInputChange({
body: this.ref.htmlEl.innerText,
richTextBody: evt.target.value,
body: c.text,
richTextBody: c.html,
});
};
@@ -46,52 +45,10 @@ class Editor extends React.Component {
this.props.unregisterHook(this.normalizeHook);
}
hasAncestor(tag) {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
let cur = range.startContainer;
do {
if (cur.nodeName === tag) {
return true;
}
cur = cur.parentNode;
} while (cur);
return false;
}
formatBold = () => {
document.execCommand('bold');
this.ref.htmlEl.focus();
};
formatItalic = () => {
document.execCommand('italic');
this.ref.htmlEl.focus();
};
formatBlockquote = () => {
if (this.hasAncestor('BLOCKQUOTE')) {
document.execCommand('outdent');
} else {
if (bowser.msie) {
document.execCommand('indent');
} else {
document.execCommand('formatBlock', false, 'blockquote');
}
}
this.ref.htmlEl.focus();
};
outdentOnEnter = e => {
if (e.key === 'Enter' && !e.shiftKey) {
setTimeout(() => {
document.execCommand('outdent');
});
}
};
render() {
const inputId = `${this.props.id}-rte`;
const { id, placeholder, label, disabled } = this.props;
const inputId = `${id}-rte`;
return (
<div className={cn(styles.root, `${PLUGIN_NAME}-container`)}>
<label
@@ -99,32 +56,26 @@ class Editor extends React.Component {
className="screen-reader-text"
aria-hidden={true}
>
{this.props.label}
{label}
</label>
<Toolbar>
<Button icon="format_bold" title="bold" onClick={this.formatBold} />
<Button
icon="format_italic"
title="italic"
onClick={this.formatItalic}
/>
<Button
icon="format_quote"
title="quote"
onClick={this.formatBlockquote}
/>
</Toolbar>
{!this.props.input.body && (
<div className={styles.placeholder}>{this.props.placeholder}</div>
)}
<ContentEditable
id={inputId}
onKeyPress={this.outdentOnEnter}
className={styles.contentEditable}
ref={this.handleRef}
html={this.getHTML()}
disabled={false}
<RTE
inputId={inputId}
contentClassName={styles.commentContent}
onChange={this.handleChange}
value={this.getHTML()}
disabled={disabled}
placeholder={placeholder}
buttons={[
<Bold key="bold" title="bold">
<Icon className={styles.icon} name={'format_bold'} />
</Bold>,
<Italic key="italic" title="italic">
<Icon className={styles.icon} name={'format_italic'} />
</Italic>,
<Blockquote key="blockquote" title="blockquote">
<Icon className={styles.icon} name={'format_quote'} />
</Blockquote>,
]}
/>
</div>
);
@@ -137,7 +88,6 @@ Editor.propTypes = {
onInputChange: PropTypes.func,
disabled: PropTypes.bool,
comment: PropTypes.object,
classNames: PropTypes.object,
registerHook: PropTypes.func,
unregisterHook: PropTypes.func,
isReply: PropTypes.bool,
@@ -0,0 +1,37 @@
.buttonReset {
user-select: none;
outline: invert none medium;
border: none;
touch-action: manipulation;
padding: 0;
overflow: hidden;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
&::-moz-focus-inner: {
border: 0;
}
}
.button > i {
vertical-align: middle;
}
.button {
composes: buttonReset;
background-color: transparent;
padding: 3px;
border: none;
color: #4e4e4e;
margin-right: 3px;
}
.button:hover{
cursor: pointer;
border-radius: 3px;
background-color: #eae8e8;
}
.active {
border-radius: 3px;
background-color: #ddd;
}
@@ -1,29 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Button.css';
import { Icon, BareButton } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
class Button extends React.Component {
render() {
const { className, icon, title, onClick } = this.props;
const {
className,
title,
onClick,
children,
active,
activeClassName,
} = this.props;
return (
<BareButton
className={cn(className, styles.button)}
<button
className={cn(className, styles.button, {
[cn(styles.active, activeClassName)]: active,
})}
title={title}
onClick={onClick}
>
<Icon className={styles.icon} name={icon} />
</BareButton>
{children}
</button>
);
}
}
Button.propTypes = {
icon: PropTypes.string.isRequired,
className: PropTypes.string,
activeClassName: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
children: PropTypes.node,
active: PropTypes.bool,
};
export default Button;
@@ -0,0 +1,18 @@
.contentEditable {
background: #fff;
border: solid 1px #bbb;
min-height: 120px;
box-sizing: border-box;
outline: 0;
overflow-y: auto;
width: 100%;
padding: 10px;
font-style: unset;
margin-bottom: 3px;
}
.placeholder {
position: absolute;
margin: 12px 0 0 12px;
color: #bbb;
}
@@ -0,0 +1,135 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './RTE.css';
import cn from 'classnames';
import ContentEditable from 'react-contenteditable';
import Toolbar from './Toolbar';
class Editor extends React.Component {
ref = null;
handleRef = ref => (this.ref = ref);
buttonsRef = {};
get contentEditable() {
return this.ref.htmlEl;
}
createButtonRefHandler(key) {
return ref => {
if (ref) {
this.buttonsRef[key] = ref;
} else {
delete this.buttonsRef[key];
}
};
}
hasAncestor(tag) {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
let cur = range.startContainer;
do {
if (cur.nodeName === tag) {
return true;
}
cur = cur.parentNode;
} while (cur);
return false;
}
forEachButton(callback) {
Object.keys(this.buttonsRef).map(k => callback(this.buttonsRef[k]));
}
handleChange = evt => {
this.props.onChange({
text: this.ref.htmlEl.innerText,
html: evt.target.value,
});
};
handleSelectionChange = () => {
this.forEachButton(b => {
b.onSelectionChange && b.onSelectionChange();
});
};
handleClick = () => {
this.handleSelectionChange();
};
handleKeyDown = () => {
this.handleSelectionChange();
};
handleKeyUp = () => {
this.handleSelectionChange();
};
handleKeyPress = e => {
this.handleSelectionChange();
if (e.key === 'Enter' && !e.shiftKey) {
setTimeout(() => {
document.execCommand('outdent');
});
}
};
renderButtons() {
return this.props.buttons.map(b => {
return React.cloneElement(b, {
rte: this,
ref: this.createButtonRefHandler(b.key),
});
});
}
render() {
const {
className,
contentClassName,
toolbarClassName,
value,
placeholder,
inputId,
} = this.props;
return (
<div className={cn(styles.root, className)}>
<Toolbar className={toolbarClassName}>{this.renderButtons()}</Toolbar>
{!value && <div className={styles.placeholder}>{placeholder}</div>}
<ContentEditable
id={inputId}
onClick={this.handleClick}
onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyDown}
className={cn(contentClassName, styles.contentEditable)}
ref={this.handleRef}
html={value}
disabled={false}
onChange={this.handleChange}
/>
</div>
);
}
}
Editor.defaultProps = {
buttons: [],
};
Editor.propTypes = {
buttons: PropTypes.array,
inputId: PropTypes.string,
input: PropTypes.object,
onChange: PropTypes.func,
disabled: PropTypes.bool,
className: PropTypes.string,
contentClassName: PropTypes.string,
toolbarClassName: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.string,
};
export default Editor;
@@ -0,0 +1,27 @@
import createToggle from '../factories/createToggle';
import { hasAncestor } from '../utils';
import bowser from 'bowser';
const execCommand = () => {
if (hasAncestor('BLOCKQUOTE')) {
document.execCommand('outdent');
} else {
if (bowser.msie) {
document.execCommand('indent');
} else {
document.execCommand('formatBlock', false, 'blockquote');
}
}
};
const syncState = () => {
return hasAncestor('BLOCKQUOTE');
};
const Blockquote = createToggle(execCommand, syncState);
Blockquote.defaultProps = {
children: 'Blockquote',
};
export default Blockquote;
@@ -0,0 +1,12 @@
import createToggle from '../factories/createToggle';
const execCommand = () => document.execCommand('bold');
const syncState = () => document.queryCommandState('bold');
const Bold = createToggle(execCommand, syncState);
Bold.defaultProps = {
children: 'Bold',
};
export default Bold;
@@ -0,0 +1,12 @@
import createToggle from '../factories/createToggle';
const execCommand = () => document.execCommand('italic');
const syncState = () => document.queryCommandState('italic');
const Italic = createToggle(execCommand, syncState);
Italic.defaultProps = {
children: 'Italic',
};
export default Italic;
@@ -0,0 +1,3 @@
export { default as Bold } from './Bold';
export { default as Italic } from './Italic';
export { default as Blockquote } from './Blockquote';
@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../Button';
const createToggle = (execCommand, getCurrentState) => {
class Toggle extends React.Component {
state = {
active: false,
};
formatToggle = () => {
execCommand();
this.props.rte.contentEditable.focus();
};
handleClick = () => {
this.formatToggle();
this.syncState();
};
syncState = () => {
if (this.state.active !== getCurrentState()) {
this.setState(state => ({
active: !state.active,
}));
}
};
onSelectionChange() {
this.syncState();
}
render() {
const { className, title, children } = this.props;
return (
<Button
className={className}
title={title}
onClick={this.handleClick}
active={this.state.active}
>
{children}
</Button>
);
}
}
Toggle.propTypes = {
rte: PropTypes.object,
className: PropTypes.string,
title: PropTypes.string,
children: PropTypes.node,
};
return Toggle;
};
export default createToggle;
@@ -0,0 +1,12 @@
export function hasAncestor(tag) {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
let cur = range.startContainer;
do {
if (cur.nodeName === tag) {
return true;
}
cur = cur.parentNode;
} while (cur);
return false;
}