Skip to content

Commit

Permalink
Implement editor focus / blur support
Browse files Browse the repository at this point in the history
  • Loading branch information
rocketraman committed Jul 21, 2015
1 parent 9dbbc9e commit 62c39a7
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 24 deletions.
3 changes: 3 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ keep the cursor visible.
* Cut/copy/paste of clipboard data, including conversion between rich text and
HTML.

* Focus/blur support with appropriate styling for cursors (invisible) and
selections (gray).

* Cursor user identification via color and tag (TODO).

* Figures and tables (TODO).
Expand Down
3 changes: 2 additions & 1 deletion src/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export default React.createClass({
minFontSize: T.number.isRequired,
unitsPerEm: T.number.isRequired,
width: T.number.isRequired,
margin: T.number.isRequired
margin: T.number.isRequired,
initialFocus: T.bool
},

//mixins: [React.addons.PureRenderMixin],
Expand Down
56 changes: 42 additions & 14 deletions src/components/EditorContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,18 @@ export default React.createClass({
minFontSize: T.number.isRequired,
unitsPerEm: T.number.isRequired,
width: T.number.isRequired,
margin: T.number.isRequired
margin: T.number.isRequired,
initialFocus: T.bool
},

mixins: [TextReplicaMixin],

getDefaultProps() {
return {
initialFocus: true
}
},

getInitialState() {
return EditorStore.getState()
},
Expand All @@ -58,7 +65,6 @@ export default React.createClass({
componentDidMount() {
this.clickCount = 0
EditorStore.listen(this.onStateChange)
this.refs.input.focus()
},

componentDidUpdate() {
Expand Down Expand Up @@ -115,7 +121,7 @@ export default React.createClass({
},

_doOnSingleClick(e) {
this.refs.input.focus()
EditorActions.focusInput()

let coordinates = this._mouseEventToCoordinates(e)
if(!coordinates) {
Expand Down Expand Up @@ -169,12 +175,18 @@ export default React.createClass({

// DEBUGGING ---------------------------------------------------------------------------------------------------------

_dumpState() {
console.debug('Current state contents (NOTE: state.focus always false due to debug button click):')
console.dir(this.state)
EditorActions.focusInput()
},

_dumpReplica() {
let text = this.replica.getTextRange(BASE_CHAR)
console.debug('Current replica text: [' + text.map(c => c.char).join('') + ']')
console.debug('Current replica contents:')
console.dir(text)
this.refs.input.focus()
EditorActions.focusInput()
},

_dumpPosition() {
Expand All @@ -183,7 +195,7 @@ export default React.createClass({
} else {
console.debug('No active position')
}
this.refs.input.focus()
EditorActions.focusInput()
},

_dumpCurrentLine() {
Expand Down Expand Up @@ -214,7 +226,7 @@ export default React.createClass({
console.debug('No lines')
}
})
this.refs.input.focus()
EditorActions.focusInput()
},

_dumpLines() {
Expand All @@ -223,7 +235,7 @@ export default React.createClass({
} else {
console.debug('No lines')
}
this.refs.input.focus()
EditorActions.focusInput()
},

_dumpSelection() {
Expand All @@ -237,17 +249,17 @@ export default React.createClass({
} else {
console.debug('No active selection')
}
this.refs.input.focus()
EditorActions.focusInput()
},

_forceFlow() {
EditorActions.replicaUpdated()
this.refs.input.focus()
EditorActions.focusInput()
},

_forceRender() {
this.forceUpdate(() => console.debug('Render done.'))
this.refs.input.focus()
EditorActions.focusInput()
},

_togglePositionEolStart() {
Expand All @@ -257,7 +269,7 @@ export default React.createClass({
console.debug('Toggling positionEolStart from ' + previous + ' to ' + !previous)
return { positionEolStart: !previous }
})
this.refs.input.focus()
EditorActions.focusInput()
},

// RENDERING ---------------------------------------------------------------------------------------------------------
Expand All @@ -283,9 +295,24 @@ export default React.createClass({

let selectionDiv = (leftX, widthX) => {
let height = Math.round(lineHeight * 10) / 10
let selectionStyle = {
top: 0,
left: leftX,
width: widthX,
height: height
}

if(!this.state.focus) {
selectionStyle.borderTopColor = 'rgb(0, 0, 0)'
selectionStyle.borderBottomColor = 'rgb(0, 0, 0)'
selectionStyle.backgroundColor = 'rgb(0, 0, 0)'
selectionStyle.opacity = 0.15
selectionStyle.color = 'black'
}

return (
<div className="text-selection-overlay text-htmloverlay ui-unprintable text-htmloverlay-under-text"
style={{top: 0, left: leftX, width: widthX, height: height}}></div>
style={selectionStyle}></div>
)
}

Expand Down Expand Up @@ -457,7 +484,7 @@ export default React.createClass({
let position = cursorPosition ? cursorPosition.top : 0

return (
<TextInput id={this.props.id} ref="input" position={position}/>
<TextInput id={this.props.id} ref="input" position={position} focused={this.state.focus}/>
)
},

Expand All @@ -484,7 +511,7 @@ export default React.createClass({
top: cursorPosition.top
}

if (this.state.selectionActive) {
if (this.state.selectionActive || !this.state.focus) {
cursorStyle.opacity = 0
cursorStyle.visibility = 'hidden'
} else {
Expand Down Expand Up @@ -528,6 +555,7 @@ export default React.createClass({
{/*
<div style={{position: 'relative', zIndex: 100, paddingTop: 30}}>
<span>Dump:&nbsp;</span>
<button onClick={this._dumpState}>State</button>&nbsp;
<button onClick={this._dumpReplica}>Replica</button>&nbsp;
<button onClick={this._dumpPosition}>Position</button>&nbsp;
<button onClick={this._dumpCurrentLine}>Line</button>&nbsp;
Expand Down
21 changes: 15 additions & 6 deletions src/components/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const ALL_CHARS = [
export default React.createClass({
propTypes: {
id: T.number.isRequired,
position: T.number.isRequired
position: T.number.isRequired,
focused: T.bool.isRequired
},

mixins: [React.addons.PureRenderMixin],
Expand Down Expand Up @@ -86,11 +87,15 @@ export default React.createClass({
keyBindings.bind('alt+shift+5', this._handleKeyStrikethrough)
keyBindings.bind('ctrl+.', this._handleKeySuperscript)
keyBindings.bind('ctrl+,', this._handleKeySubscript)
},

// TODO figure out tab, enter, return, pageup, pagedown, end, home, ins
componentDidUpdate() {
if(this.props.focused) {
this._focus()
}
},

focus() {
_focus() {
this._checkEmptyValue()
this.input.focus()

Expand Down Expand Up @@ -291,6 +296,10 @@ export default React.createClass({
return false
},

_onInputFocusLost() {
EditorActions.inputFocusLost()
},

_onCopy(e) {
let selectionChunks = EditorActions.getSelection()

Expand Down Expand Up @@ -327,7 +336,7 @@ export default React.createClass({
this._selectNodeContents(this.ieClipboardDiv)
setTimeout(() => {
emptyNode(this.ieClipboardDiv)
this.focus()
this._focus()
}, 0)
},

Expand Down Expand Up @@ -376,7 +385,7 @@ export default React.createClass({
EditorActions.insertCharsBatch(pastedChunks)
} finally {
emptyNode(this.ieClipboardDiv)
this.focus()
this._focus()
}
}, 0)
},
Expand Down Expand Up @@ -406,7 +415,7 @@ export default React.createClass({
return (
<div style={divStyle}>
<textarea key="input" ref="input" onInput={this._onInput}
onCopy={this._onCopy} onCut={this._onCut} onPaste={this._onPaste}/>
onCopy={this._onCopy} onCut={this._onCut} onPaste={this._onPaste} onBlur={this._onInputFocusLost}/>
<div style={{display: 'none'}} ref="hiddenContainer"></div>
<div contentEditable="true" ref="ieClipboardDiv" onPaste={this._onPaste}></div>
</div>
Expand Down
8 changes: 8 additions & 0 deletions src/flux/EditorActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ class EditorActions {
this.dispatch()
}

focusInput() {
this.dispatch()
}

inputFocusLost() {
this.dispatch()
}

// navigation actions
navigateLeft() {
this.dispatch()
Expand Down
20 changes: 17 additions & 3 deletions src/flux/EditorStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,32 @@ class EditorStore {
position: BASE_CHAR,
positionEolStart: true,
cursorMotion: false,
selectionActive: false
selectionActive: false,
focus: false
}
}

initialize({config, replica}) {
TextFontMetrics.setConfig(config)
this.config = config
this.replica = replica

this.setState({focus: config.initialFocus})
}

replicaUpdated() {
this._flow()
}

focusInput() {
this._delayedCursorBlink(0)
this.setState({focus: true})
}

inputFocusLost() {
this.setState({focus: false})
}

navigateLeft() {
this._navigateLeftRight(-1)
}
Expand Down Expand Up @@ -736,7 +748,9 @@ class EditorStore {
this._setPosition(BASE_CHAR)
}

_delayedCursorBlink() {
_delayedCursorBlink(timeout) {
if(_.isUndefined(timeout)) timeout = 1000

this.setState({cursorMotion: true})

// in a second, reset the cursor blink, clear any previous resets to avoid unnecessary state changes
Expand All @@ -746,7 +760,7 @@ class EditorStore {
this.cursorMotionTimeout = setTimeout(() => {
this.setState({cursorMotion: false})
this.cursorMotionTimeout = null
}, 1000)
}, timeout)
}

_lastLine() {
Expand Down

0 comments on commit 62c39a7

Please sign in to comment.