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 (
- -
); @@ -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; +}