Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors and initial fact-bank redux setup #663

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/distributions/editor/TextForm/TextForm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {Component, PropTypes} from 'react'

import {GuesstimateTypeIcon} from './GuesstimateTypeIcon'
import TextInput from './TextInput'
import {TextInput} from './TextInput'
import DistributionSelector from './DistributionSelector'

export class TextForm extends Component{
Expand Down
164 changes: 116 additions & 48 deletions src/components/distributions/editor/TextForm/TextInput.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'

import $ from 'jquery'
import {EditorState, Editor, ContentState, Modifier, CompositeDecorator} from 'draft-js'

import {clearSuggestion, globalsSearch} from 'gModules/factBank/actions'

import {isData, formatData} from 'lib/guesstimator/formatter/formatters/Data'
import {getFactParams, addText, addSuggestionToEditorState, findWithRegex, FACT_DECORATOR_LIST} from 'lib/factParser'

const ValidInput = props => <span {...props} className='valid input'>{props.children}</span>
const ErrorInput = props => <span {...props} className='error input'>{props.children}</span>
const NOUN_REGEX = /(\@[\w]+)/g
const PROPERTY_REGEX = /[a-zA-Z_](\.[\w]+)/g
function findWithRegex(regex, contentBlock, callback) {
const text = contentBlock.getText()
let matchArr, start
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index + matchArr[0].indexOf(matchArr[1])
callback(start, start + matchArr[1].length)
}
}

export default class TextInput extends Component{
const stylizedSpan = className => props => <span {...props} className={className}>{props.children}</span>
const Noun = stylizedSpan('noun')
const Property = stylizedSpan('property')
const Suggestion = stylizedSpan('suggestion')
const ValidInput = stylizedSpan('valid input')
const ErrorInput = stylizedSpan('error input')

const FACT_DECORATOR_LIST = [
{
strategy: (contentBlock, callback) => { findWithRegex(NOUN_REGEX, contentBlock, callback) },
component: Noun,
},
{
strategy: (contentBlock, callback) => { findWithRegex(PROPERTY_REGEX, contentBlock, callback) },
component: Property,
},
]

const positionDecorator = (start, end, component) => ({
strategy: (contentBlock, callback) => {if (end <= contentBlock.text.length) {callback(start, end)}},
component,
})

@connect(state => ({suggestion: state.factBank.currentSuggestion}))
export class TextInput extends Component{
displayName: 'Guesstimate-TextInput'

state = {
editorState: EditorState.createWithContent(
ContentState.createFromText(this.props.value || ''),
new CompositeDecorator(this.decoratorList()),
),
suggestion: {
text: '',
suffix: '',
},
extraDecorators: [],
decoratorsUpToDate: false,
}

static propTypes = {
value: PropTypes.string,
}

decoratorList() {
decoratorList(extraDecorators=[]) {
const {validInputs, errorInputs} = this.props

let decorators = [...(_.get(this, 'state.extraDecorators') || []), ...FACT_DECORATOR_LIST]
let decorators = [...extraDecorators, ...FACT_DECORATOR_LIST]

if (!_.isEmpty(validInputs)) {
const validInputsRegex = new RegExp(`(${validInputs.join('|')})`, 'g')
Expand All @@ -53,20 +81,57 @@ export default class TextInput extends Component{

focus() { this.refs.editor.focus() }

insertAtCaret(text) {
this.onChange(addText(this.state.editorState, text, false))
addText(text, maintainCursorPosition = true, replaceLength = 0) {
const selection = this.state.editorState.getSelection()
const content = this.state.editorState.getCurrentContent()

let baseEditorState
if (replaceLength > 0) {
const replaceSelection = selection.merge({anchorOffset: this.cursorPosition(), focusOffset: this.cursorPosition() + replaceLength})
baseEditorState = EditorState.push(this.state.editorState, Modifier.replaceText(content, replaceSelection, text), 'paste')
} else {
baseEditorState = EditorState.push(this.state.editorState, Modifier.insertText(content, selection, text), 'paste')
}

if (!maintainCursorPosition) { return baseEditorState }

const cursorPosition = selection.getFocusOffset()
const newSelectionState = selection.merge({focusOffset: cursorPosition})
return EditorState.forceSelection(baseEditorState, newSelectionState)
}

stripExtraDecorators(editorState) { return this.withExtraDecorators(editorState, []) }
withExtraDecorators(editorState, extraDecorators) {
return EditorState.set(editorState, {decorator: new CompositeDecorator(this.decoratorList(extraDecorators))})
}

replaceAtCaret(text, start, end) {
this.onChange(addText(this.state.editorState, text, false, start, end))
deleteOldSuggestion(oldSuggestion) {
const freshEditorState = this.addText('', true, oldSuggestion.length)
this.setState({editorState: this.stripExtraDecorators(freshEditorState)})
}

addSuggestion() {
const partial = this.prevWord().slice(1).split('.').pop()
const inProperty = this.prevWord().includes('.')

const partialComponent = inProperty ? Property : Noun
const extraDecorators = [
positionDecorator(this.cursorPosition() - partial.length - 1, this.cursorPosition(), partialComponent),
positionDecorator(this.cursorPosition(), this.cursorPosition() + this.props.suggestion.length, Suggestion),
]

const addedEditorState = this.addText(this.props.suggestion, true, this.nextWord().length)

this.setState({editorState: this.withExtraDecorators(addedEditorState, extraDecorators)})
}

componentDidUpdate(prevProps, prevState) {
if (
!this.state.decoratorsUpToDate ||
!_.isEqual(prevState.extraDecorators, this.state.extraDecorators)
) {
this.updateDecorators()
componentDidUpdate(prevProps) {
if (this.props.suggestion !== prevProps.suggestion && this.nextWord() === prevProps.suggestion) {
if (_.isEmpty(this.props.suggestion)) {
this.deleteOldSuggestion(prevProps.suggestion)
} else {
this.addSuggestion()
}
}
}

Expand All @@ -77,39 +142,49 @@ export default class TextInput extends Component{
}
}

onChange(editorState) {
const newState = {
editorState,
...addSuggestionToEditorState(editorState, this.state.suggestion.text)
}
this.setState(newState)
cursorPosition(editorState = this.state.editorState) { return editorState.getSelection().getFocusOffset() }
text(editorState = this.state.editorState) { return editorState.getCurrentContent().getPlainText('') }
nextWord(editorState = this.state.editorState) {
return this.text(editorState).slice(this.cursorPosition(editorState)).split(/[^\w]/)[0]
}
prevWord(editorState = this.state.editorState) {
return this.text(editorState).slice(0, this.cursorPosition(editorState)).split(/[^\w@\.]/).pop()
}

const text = newState.editorState.getCurrentContent().getPlainText('')
if (text === this.props.value) { return }
if (isData(text)) {
this.props.onChangeData(formatData(text))
fetchSuggestion(editorState) {
const prevWord = this.prevWord(editorState)
if (!(prevWord.startsWith('@') && editorState.getSelection().isCollapsed())) {
if (!_.isEmpty(this.props.suggestion)) { this.props.dispatch(clearSuggestion()) }
} else {
this.props.onChange(text)
this.props.dispatch(globalsSearch(prevWord.slice(1).split('.')))
}
}

onChange(editorState) {
this.fetchSuggestion(editorState)
this.setState({editorState})

const text = this.text(editorState)
if (text !== this.props.value) {
isData(text) ? this.props.onChangeData(formatData(text)) : this.props.onChange(text)
}
}

handleTab(e){
if (!_.isEmpty(this.state.suggestion.text)) { this.acceptSuggestion() }
if (!_.isEmpty(this.props.suggestion) && this.nextWord() === this.props.suggestion) { this.acceptSuggestion() }
else { this.props.onTab(e.shiftKey) }
e.preventDefault()
}

acceptSuggestion(){
const {text, suffix} = this.state.suggestion
const inProperty = this.prevWord().includes('.')
const cursorPosition = this.cursorPosition()
this.replaceAtCaret(`${text}${suffix}`, cursorPosition, cursorPosition + text.length - 1)
this.setState({suggestion: {text: '', suffix: ''}, extraDecorators: [], decoratorsUpToDate: false})
const addedEditorState = this.addText(`${this.props.suggestion}${inProperty ? '' : '.'}`, false, this.props.suggestion.length)
this.onChange(this.stripExtraDecorators(addedEditorState))
}

cursorPosition(editorState = this.state.editorState) { return editorState.getSelection().getFocusOffset() }

handleFocus() {
$(window).on('functionMetricClicked', (_, {readableId}) => {this.insertAtCaret(readableId)})
$(window).on('functionMetricClicked', (_, {readableId}) => {this.onChange(this.addText(readableId, false))})
this.props.onFocus()
}

Expand All @@ -118,21 +193,14 @@ export default class TextInput extends Component{
this.props.onBlur()
}

updateDecorators() {
this.setState({
editorState: EditorState.set(this.state.editorState, {decorator: new CompositeDecorator(this.decoratorList())}),
decoratorsUpToDate: true,
})
}

render() {
const [{hasErrors, width, value, validInputs, errorInputs}, {editorState}] = [this.props, this.state]
const className = `TextInput ${width}` + (_.isEmpty(value) && hasErrors ? ' hasErrors' : '')
return (
<span
className={className}
onClick={this.focus.bind(this)}
onKeyDown={e => {this.setState({decoratorsUpToDate: false}); e.stopPropagation()}}
onKeyDown={e => {e.stopPropagation()}}
onFocus={this.handleFocus.bind(this)}
>
<Editor
Expand Down
22 changes: 17 additions & 5 deletions src/components/spaces/denormalized-space-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,22 @@ function checkpointMetadata(id, checkpoints) {
return attributes
}

const SPACE_GRAPH_PARTS = [
'spaces',
'calculators',
'metrics',
'guesstimates',
'simulations',
'users',
'organizations',
'userOrganizationMemberships',
'me',
'checkpoints',
]

const spaceGraphSelector = state => {
window.recorder.recordSelectorStart(NAME)
return _.pick(state, 'spaces', 'calculators', 'metrics', 'guesstimates', 'simulations', 'users', 'organizations', 'userOrganizationMemberships', 'me', 'checkpoints')
return _.pick(state, SPACE_GRAPH_PARTS)
}
const spaceIdSelector = (_, {spaceId}) => spaceId
const canvasStateSelector = state => state.canvasState
Expand All @@ -36,8 +49,7 @@ export const denormalizedSpaceSelector = createSelector(
}

window.recorder.recordSelectorStop(NAME, {denormalizedSpace: dSpace})
return {
denormalizedSpace: dSpace
};

return { denormalizedSpace: dSpace }
}
);
)
Loading