diff --git a/plugins/talk-plugin-rich-text/client/components/Button.css b/plugins/talk-plugin-rich-text/client/components/Button.css
deleted file mode 100644
index f9f1ba186..000000000
--- a/plugins/talk-plugin-rich-text/client/components/Button.css
+++ /dev/null
@@ -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;
-}
diff --git a/plugins/talk-plugin-rich-text/client/components/Editor.css b/plugins/talk-plugin-rich-text/client/components/Editor.css
index 4e6a072a9..2259d01a3 100644
--- a/plugins/talk-plugin-rich-text/client/components/Editor.css
+++ b/plugins/talk-plugin-rich-text/client/components/Editor.css
@@ -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;
+}
diff --git a/plugins/talk-plugin-rich-text/client/components/Editor.js b/plugins/talk-plugin-rich-text/client/components/Editor.js
index eefb978a8..5962630b5 100644
--- a/plugins/talk-plugin-rich-text/client/components/Editor.js
+++ b/plugins/talk-plugin-rich-text/client/components/Editor.js
@@ -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 (
-
-
-
-
-
- {!this.props.input.body && (
-
{this.props.placeholder}
- )}
-
+
+ ,
+
+
+ ,
+
+
+
,
+ ]}
/>
);
@@ -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,
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/Button.css b/plugins/talk-plugin-rich-text/client/components/rte/Button.css
new file mode 100644
index 000000000..daedd54ec
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/Button.css
@@ -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;
+}
diff --git a/plugins/talk-plugin-rich-text/client/components/Button.js b/plugins/talk-plugin-rich-text/client/components/rte/Button.js
similarity index 50%
rename from plugins/talk-plugin-rich-text/client/components/Button.js
rename to plugins/talk-plugin-rich-text/client/components/rte/Button.js
index 330e3d993..f7bc75941 100644
--- a/plugins/talk-plugin-rich-text/client/components/Button.js
+++ b/plugins/talk-plugin-rich-text/client/components/rte/Button.js
@@ -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 (
-
-
-
+ {children}
+
);
}
}
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;
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/RTE.css b/plugins/talk-plugin-rich-text/client/components/rte/RTE.css
new file mode 100644
index 000000000..ab24828bd
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/RTE.css
@@ -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;
+}
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/RTE.js b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js
new file mode 100644
index 000000000..55ce25333
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js
@@ -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 (
+
+
{this.renderButtons()}
+ {!value &&
{placeholder}
}
+
+
+ );
+ }
+}
+
+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;
diff --git a/plugins/talk-plugin-rich-text/client/components/Toolbar.css b/plugins/talk-plugin-rich-text/client/components/rte/Toolbar.css
similarity index 100%
rename from plugins/talk-plugin-rich-text/client/components/Toolbar.css
rename to plugins/talk-plugin-rich-text/client/components/rte/Toolbar.css
diff --git a/plugins/talk-plugin-rich-text/client/components/Toolbar.js b/plugins/talk-plugin-rich-text/client/components/rte/Toolbar.js
similarity index 100%
rename from plugins/talk-plugin-rich-text/client/components/Toolbar.js
rename to plugins/talk-plugin-rich-text/client/components/rte/Toolbar.js
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js
new file mode 100644
index 000000000..1ba9d933f
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js
@@ -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;
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js
new file mode 100644
index 000000000..4988b0679
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js
@@ -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;
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js
new file mode 100644
index 000000000..6387e4237
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js
@@ -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;
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/index.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/index.js
new file mode 100644
index 000000000..5e985fe6a
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/index.js
@@ -0,0 +1,3 @@
+export { default as Bold } from './Bold';
+export { default as Italic } from './Italic';
+export { default as Blockquote } from './Blockquote';
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js
new file mode 100644
index 000000000..61c7263a3
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js
@@ -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 (
+
+ );
+ }
+ }
+
+ Toggle.propTypes = {
+ rte: PropTypes.object,
+ className: PropTypes.string,
+ title: PropTypes.string,
+ children: PropTypes.node,
+ };
+ return Toggle;
+};
+
+export default createToggle;
diff --git a/plugins/talk-plugin-rich-text/client/components/rte/utils.js b/plugins/talk-plugin-rich-text/client/components/rte/utils.js
new file mode 100644
index 000000000..0d613df8a
--- /dev/null
+++ b/plugins/talk-plugin-rich-text/client/components/rte/utils.js
@@ -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;
+}