Skip to content

Commit

Permalink
Close widget on lost focus, render widget into portal (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
estambakio-sc authored Oct 29, 2018
1 parent 0bc0368 commit abba48d
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 154 deletions.
7 changes: 1 addition & 6 deletions src/client/components/MarkdownInput/MarkdownInput.react.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import Types from 'prop-types';
import PlainMarkdownInput from '../PlainMarkdownInput';
import ProvideBlur from './ProvideBlur.react';

class MarkdownInput extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -42,12 +41,8 @@ class MarkdownInput extends React.Component {
}

render() {
const { onBlur, ...props } = this.props;

return (
<ProvideBlur onBlur={onBlur}>
<PlainMarkdownInput {...props} ref={this.plainInputRef}/>
</ProvideBlur>
<PlainMarkdownInput {...this.props} ref={this.plainInputRef}/>
);
}
}
Expand Down
62 changes: 0 additions & 62 deletions src/client/components/MarkdownInput/ProvideBlur.react.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class MarkdownInputModalScope extends React.Component {
state = {
markdownExample: text,
show: true,
showConfirm: false,
fullScreen: false
};

Expand All @@ -26,7 +27,7 @@ class MarkdownInputModalScope extends React.Component {
};

handleHideModal = () => {
this.setState({ show: false });
this.setState({ showConfirm: true });
};

handleFullScreen = (fullScreen) => {
Expand All @@ -38,6 +39,8 @@ class MarkdownInputModalScope extends React.Component {
}

render() {
const { showConfirm } = this.state;

const modalClasses = classNames({
'markdown-input_fullscreen': this.state.fullScreen,
...this.props.className
Expand All @@ -59,9 +62,27 @@ class MarkdownInputModalScope extends React.Component {
Modal Test
</Modal.Header>
<Modal.Body>
<Modal
show={showConfirm}
style={{ top: '30%' }}
>
<Modal.Header>Confirmation</Modal.Header>
<Modal.Body>You what?</Modal.Body>
<Modal.Footer>
<button
className='btn btn-primary'
onClick={_ => this.setState({ show: false, showConfirm: false })}
>Confirm</button>
<button
className='btn btn-default'
onClick={_ => this.setState({ showConfirm: false })}
>Cancel</button>
</Modal.Footer>
</Modal>
{this._renderChildren()}
</Modal.Body>
</Modal>

</div>
);
}
Expand Down
78 changes: 78 additions & 0 deletions src/client/components/PlainMarkdownInput/FocusBlur.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { PureComponent } from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';

/**
* This setup is needed to create proper `blur` handler.
* Slate-react Editor has `onBlur` prop, but if we just supply it to Editor component,
* `blur` will fire when a user clicks buttons B, I etc. But we need `blur` to fire only when
* component loses focus, and not when we operate with internal elements of editor.
*/
export default class ProvideBlur extends PureComponent {
static propTypes = {
onBlur: PropTypes.func,
onFocus: PropTypes.func
}

static defaultProps = {
onBlur: () => {},
onFocus: () => {}
}

state = {
isFocused: false
}

componentDidMount() {
this.el = findDOMNode(this);
this.el.addEventListener('focusin', this.handleFocusIn);
this.el.addEventListener('focusout', this.handleFocusOut);
}

componentDidUpdate(prevProps, prevState) {
const prevFocused = prevState.isFocused;
const { isFocused } = this.state;

if (isFocused && prevFocused !== isFocused) {
this.props.onFocus()
}

if (!isFocused && prevFocused !== isFocused) {
this.props.onBlur()
}
}

componentWillUnmount() {
this.el.removeEventListener('focusin', this.handleFocusIn)
this.el.removeEventListener('focusout', this.handleFocusOut)
}

handleFocusIn = event => {
// IE11 & firefox trigger focusout when you click on PlainMarkdownInput.
// In order to eliminate it we can stop listening for focusout when focusin occurs.
this.el.removeEventListener('focusout', this.handleFocusOut);
setTimeout(_ => this.el && this.el.addEventListener('focusout', this.handleFocusOut));
return this.setState(prevState => prevState.isFocused ? {} : { isFocused: true })
};

timeout = null;

abortFocusOut = _ => {
clearTimeout(this.timeout);
}

handleFocusOut = event => {
this.el.addEventListener('focusin', this.abortFocusOut);
this.timeout = setTimeout(_ => {
if (this.el) {
this.el.removeEventListener('focusin', this.abortFocusOut);
}
this.setState({ isFocused: false });
});
}

render() {
const { children } = this.props;
return children instanceof Function ? children() : children;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { PureComponent } from 'react';
import { findDOMNode } from 'react-dom';
import Types from 'prop-types';
import classnames from 'classnames';
Expand Down Expand Up @@ -36,6 +36,8 @@ import {
setSelectionToState,
} from './slate/transforms';

import FocusBlur from './FocusBlur.react';

const ACCENTS = {
b: 'bold',
i: 'italic',
Expand Down Expand Up @@ -67,12 +69,13 @@ function copySelectionToClipboard(event, { state }) {
}
}

class PlainMarkdownInput extends React.Component {
class PlainMarkdownInput extends PureComponent {
static propTypes = {
extensions: Types.array,
additionalButtons: Types.array,
value: Types.string,
onChange: Types.func,
onBlur: Types.func,
onFullScreen: Types.func,
readOnly: Types.bool,
autoFocus: Types.bool,
Expand All @@ -87,6 +90,7 @@ class PlainMarkdownInput extends React.Component {
value: '',
onFullScreen: () => {},
onChange: () => {},
onBlur: () => {},
readOnly: false,
autoFocus: true,
showFullScreenButton: false,
Expand All @@ -95,14 +99,19 @@ class PlainMarkdownInput extends React.Component {

state = {
editorState: '',
fullScreen: false
fullScreen: false,
isActive: true // for fix about widget position: hide widget if editor is not focused currently
}

componentWillMount() {
this.initialBodyOverflowStyle = document.body.style.overflow;
this.handleNewValue(this.props.value);
}

componentDidMount() {
this.setState({ containerRef: this.container }) // eslint-disable-line react/no-did-mount-set-state
}

componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value) {
this.handleNewValue(nextProps.value);
Expand Down Expand Up @@ -457,7 +466,30 @@ class PlainMarkdownInput extends React.Component {
<div className="btn-group">
{children}
</div>
)
);

handleBlur = event => {
if (event && event.persist) {
event.persist();
}
this.setState(prevState => {
const change = prevState.editorState.change();
change.blur();
return ({
editorState: change.state,
...(prevState.isActive && { isActive: false })
})
}, _ => this.props.onBlur(event));
}

handleFocus = _ => this.setState(prevState => {
const change = prevState.editorState.change();
change.focus();
return ({
editorState: change.state,
...(!prevState.isActive && { isActive: true })
})
});

render() {
const { editorState, fullScreen } = this.state;
Expand Down Expand Up @@ -494,7 +526,9 @@ class PlainMarkdownInput extends React.Component {
onChange: this.handleChange,
onScroll: this.handleScroll,
onMouseUp: this.handleMouseUp,
onToggle: this.handleAutocompleteToggle
onToggle: this.handleAutocompleteToggle,
editorIsActive: this.state.isActive,
containerRef: this.state.containerRef
})
]}
readOnly={readOnly}
Expand All @@ -506,31 +540,34 @@ class PlainMarkdownInput extends React.Component {
);

return (
<div
className={classnames(
'react-markdown--slate-editor',
{ 'react-markdown--slate-editor--fullscreen': fullScreen }
)}
>
{
!hideToolbar && (
<div className="react-markdown--toolbar">
{emphasisButtons}
{linkButton}
{headerButtons}
{listButtons}
{additionalButtons}
{fullScreenButton}
</div>
)
}
<FocusBlur onBlur={this.handleBlur} onFocus={this.handleFocus}>
<div
className={'react-markdown--slate-content'}
{...(hideToolbar && { style: { borderTop: '0' } })}
className={classnames(
'react-markdown--slate-editor',
{ 'react-markdown--slate-editor--fullscreen': fullScreen }
)}
ref={el => (this.container = el)}
>
{editor}
{
!hideToolbar && (
<div className="react-markdown--toolbar">
{emphasisButtons}
{linkButton}
{headerButtons}
{listButtons}
{additionalButtons}
{fullScreenButton}
</div>
)
}
<div
className={'react-markdown--slate-content'}
{...(hideToolbar && { style: { borderTop: '0' } })}
>
{editor}
</div>
</div>
</div>
</FocusBlur>
);
}
}
Expand Down
Loading

0 comments on commit abba48d

Please sign in to comment.