From 66d250b152aaeeb846f2416ed8dfe8e9d7e104b0 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 26 Jul 2016 12:27:16 -0700 Subject: [PATCH 1/6] Refactored text input to work via prop-based suggestion. --- .../distributions/editor/TextForm/TextForm.js | 2 +- .../editor/TextForm/TextInput.js | 113 +++++++++++------- .../spaces/denormalized-space-selector.js | 23 +++- src/lib/factParser.js | 19 +-- src/modules/canvas_state/reducer.js | 10 +- src/modules/factBank/actions.js | 26 ++-- src/modules/factBank/reducer.js | 33 +++++ src/modules/factBank/reducers.js | 0 src/modules/organizations/actions.js | 29 +++-- src/modules/reducers.js | 4 +- src/modules/spaces/actions.js | 2 +- 11 files changed, 178 insertions(+), 83 deletions(-) create mode 100644 src/modules/factBank/reducer.js delete mode 100644 src/modules/factBank/reducers.js diff --git a/src/components/distributions/editor/TextForm/TextForm.js b/src/components/distributions/editor/TextForm/TextForm.js index 73bb6a50b..576e7a294 100644 --- a/src/components/distributions/editor/TextForm/TextForm.js +++ b/src/components/distributions/editor/TextForm/TextForm.js @@ -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{ diff --git a/src/components/distributions/editor/TextForm/TextInput.js b/src/components/distributions/editor/TextForm/TextInput.js index d1843be64..0b9d2e6cf 100644 --- a/src/components/distributions/editor/TextForm/TextInput.js +++ b/src/components/distributions/editor/TextForm/TextInput.js @@ -1,15 +1,23 @@ 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' +import {getFactParams, addText, addSuggestionToEditorState, findWithRegex, FACT_DECORATOR_LIST, positionDecorator, NounSpan, PropertySpan, SuggestionSpan} from 'lib/factParser' const ValidInput = props => {props.children} const ErrorInput = props => {props.children} -export default class TextInput extends Component{ +function mapStateToProps(state) { + return { suggestion: state.factBank.currentSuggestion } +} + +@connect(mapStateToProps) +export class TextInput extends Component{ displayName: 'Guesstimate-TextInput' state = { @@ -17,22 +25,16 @@ export default class TextInput extends Component{ 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') @@ -57,16 +59,38 @@ export default class TextInput extends Component{ this.onChange(addText(this.state.editorState, text, false)) } - replaceAtCaret(text, start, end) { - this.onChange(addText(this.state.editorState, text, false, start, end)) + stripExtraDecorators(editorState) { return this.withExtraDecorators(editorState, []) } + withExtraDecorators(editorState, extraDecorators) { + return EditorState.set(editorState, {decorator: new CompositeDecorator(this.decoratorList(extraDecorators))}) + } + + deleteOldSuggestion(oldSuggestion) { + const freshEditorState = addText(this.state.editorState, '', true, this.cursorPosition(), this.cursorPosition() + oldSuggestion.length) + this.setState({editorState: this.stripExtraDecorators(freshEditorState)}) + } + + addSuggestion() { + const partial = this.prevWord().slice(1).split('.').pop() + const inProperty = this.prevWord().includes('.') + + const decoratorComponent = inProperty ? PropertySpan : NounSpan + const extraDecorators = [ + positionDecorator(this.cursorPosition() - partial.length - 1, this.cursorPosition(), decoratorComponent), + positionDecorator(this.cursorPosition(), this.cursorPosition() + this.props.suggestion.length, SuggestionSpan), + ] + + const addedEditorState = addText(this.state.editorState, this.props.suggestion, true, this.cursorPosition(), this.cursorPosition()+this.nextWord().length-1) + + 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() + } } } @@ -77,37 +101,47 @@ 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.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 = addText(this.state.editorState, `${this.props.suggestion}${inProperty ? '' : '.'}`, false, cursorPosition, cursorPosition + this.props.suggestion.length - 1) + this.onChange(this.stripExtraDecorators(addedEditorState)) } - cursorPosition(editorState = this.state.editorState) { return editorState.getSelection().getFocusOffset() } - handleFocus() { $(window).on('functionMetricClicked', (_, {readableId}) => {this.insertAtCaret(readableId)}) this.props.onFocus() @@ -118,13 +152,6 @@ 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' : '') @@ -132,7 +159,7 @@ export default class TextInput extends Component{ {this.setState({decoratorsUpToDate: false}); e.stopPropagation()}} + onKeyDown={e => {e.stopPropagation()}} onFocus={this.handleFocus.bind(this)} > { 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 @@ -36,8 +50,7 @@ export const denormalizedSpaceSelector = createSelector( } window.recorder.recordSelectorStop(NAME, {denormalizedSpace: dSpace}) - return { - denormalizedSpace: dSpace - }; + + return { denormalizedSpace: dSpace } } -); +) diff --git a/src/lib/factParser.js b/src/lib/factParser.js index 0dce8b5fe..6c71593ff 100644 --- a/src/lib/factParser.js +++ b/src/lib/factParser.js @@ -15,9 +15,9 @@ export function findWithRegex(regex, contentBlock, callback) { } } -const NounSpan = props => {props.children} -const PropertySpan = props => {props.children} -const SuggestionSpan = props => {props.children} +export const NounSpan = props => {props.children} +export const PropertySpan = props => {props.children} +export const SuggestionSpan = props => {props.children} export const FACT_DECORATOR_LIST = [ { @@ -30,7 +30,10 @@ export const FACT_DECORATOR_LIST = [ }, ] -const positionDecorator = (start, end, component) => ({strategy: (contentBlock, callback) => {callback(start, end)}, component}) +export const positionDecorator = (start, end, component) => ({ + strategy: (contentBlock, callback) => {if (end <= contentBlock.text.length) {callback(start, end)}}, + component, +}) export function addText(editorState, text, maintainCursorPosition = true, anchorOffset = null, focusOffset = null) { const selection = editorState.getSelection() @@ -52,10 +55,12 @@ export function addText(editorState, text, maintainCursorPosition = true, anchor } class Suggestor { - constructor(editorState, currentSuggestion) { + constructor(editorState, currentSuggestion, dispatch) { this.editorState = editorState this.currentSuggestion = currentSuggestion + this.dispatch = dispatch + this.cursorPosition = editorState.getSelection().getFocusOffset() this.text = editorState.getCurrentContent().getPlainText('') @@ -114,6 +119,6 @@ class Suggestor { } } -export function addSuggestionToEditorState(editorState, currentSuggestion){ - return (new Suggestor(editorState, currentSuggestion)).run() +export function addSuggestionToEditorState(editorState, currentSuggestion, dispatch){ + return (new Suggestor(editorState, currentSuggestion, dispatch)).run() } diff --git a/src/modules/canvas_state/reducer.js b/src/modules/canvas_state/reducer.js index a99279e86..ec4476a20 100644 --- a/src/modules/canvas_state/reducer.js +++ b/src/modules/canvas_state/reducer.js @@ -6,11 +6,11 @@ const initialState = { editsAllowed: true, } -export default function canvasState(state = initialState, action) { +export function canvasStateR(state = initialState, action) { switch (action.type) { - case 'CHANGE_CANVAS_STATE': - return Object.assign({}, state, action.values) - default: - return state + case 'CHANGE_CANVAS_STATE': + return Object.assign({}, state, action.values) + default: + return state } } diff --git a/src/modules/factBank/actions.js b/src/modules/factBank/actions.js index 716dfaed9..59fdebad5 100644 --- a/src/modules/factBank/actions.js +++ b/src/modules/factBank/actions.js @@ -1,10 +1,6 @@ -import FACTS from './cities.json' - -const findFactByName = name => FACTS.find(f => f.name === name) - const search = (partial, list) => _.isEmpty(partial) ? '' : [...list.filter(e => e.startsWith(partial)), ''][0] -const nounSearch = partial => search(partial, FACTS.map(f => f.name)) -const propertySearch = (noun, partial) => search(partial, Object.keys(findFactByName(noun))) +const nounSearch = (partial, facts) => search(partial, facts.map(f => f.name)) +const propertySearch = (noun, partial, facts) => search(partial, Object.keys(facts.find(f => f.name === noun))) export function getSuggestion([noun, property]) { const suggestion = typeof property !== 'undefined' ? propertySearch(noun, property) : nounSearch(noun) @@ -12,5 +8,21 @@ export function getSuggestion([noun, property]) { } export function resolveProperty([noun, property]) { - return findFactByName(noun)[property] + return 5//findFactByName(noun)[property] +} + +export function globalsSearch([noun, property]) { + return (dispatch, getState) => { + const facts = getState().factBank.globals + const suggestion = typeof property !== 'undefined' ? propertySearch(noun, property, facts) : nounSearch(noun, facts) + dispatch({type: 'SUGGEST_FACT', suggestion: suggestion.replace(property || noun, '')}) + } +} + +export function clearSuggestion() { + return {type: 'CLEAR_SUGGESTION'} +} + +export function loadByOrg(facts) { + return {type: 'LOAD_FACTS_BY_ORG', facts} } diff --git a/src/modules/factBank/reducer.js b/src/modules/factBank/reducer.js new file mode 100644 index 000000000..bea2e5a36 --- /dev/null +++ b/src/modules/factBank/reducer.js @@ -0,0 +1,33 @@ +import FACTS from './cities.json' + +const INITIAL_STATE = { + currentSuggestion: '', + globals: FACTS, + byOrg: [], +} + +export function factBankR(state = INITIAL_STATE, {type, facts, suggestion}) { + const by = property => e => !_.some(facts, f => f[property] === e[property]) + switch (type) { + case 'LOAD_FACTS_BY_ORG': + return { + ...state, + byOrg: [ + ...facts, + ...state.byOrg.filter(by('organization_id')), + ], + } + case 'SUGGEST_FACT': + return { + ...state, + currentSuggestion: suggestion, + } + case 'CLEAR_SUGGESTION': + return { + ...state, + currentSuggestion: INITIAL_STATE.currentSuggestion, + } + default: + return state + } +} diff --git a/src/modules/factBank/reducers.js b/src/modules/factBank/reducers.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/modules/organizations/actions.js b/src/modules/organizations/actions.js index 338a01fa3..6ef838abf 100644 --- a/src/modules/organizations/actions.js +++ b/src/modules/organizations/actions.js @@ -2,14 +2,15 @@ import cuid from 'cuid' import app from 'ampersand-app' import {actionCreatorsFor} from 'redux-crud' -import * as displayErrorsActions from 'gModules/displayErrors/actions.js' -import * as membershipActions from 'gModules/userOrganizationMemberships/actions.js' -import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions.js' -import * as userOrganizationInvitationActions from 'gModules/userOrganizationInvitations/actions.js' +import * as displayErrorsActions from 'gModules/displayErrors/actions' +import * as membershipActions from 'gModules/userOrganizationMemberships/actions' +import * as userOrganizationMembershipActions from 'gModules/userOrganizationMemberships/actions' +import * as userOrganizationInvitationActions from 'gModules/userOrganizationInvitations/actions' +import * as factActions from 'gModules/factBank/actions' -import {captureApiError} from 'lib/errors/index.js' +import {captureApiError} from 'lib/errors/index' -import {setupGuesstimateApi} from 'servers/guesstimate-api/constants.js' +import {setupGuesstimateApi} from 'servers/guesstimate-api/constants' let oActions = actionCreatorsFor('organizations') @@ -27,13 +28,7 @@ export function fetchById(organizationId) { dispatch(displayErrorsActions.newError()) captureApiError('OrganizationsFetch', err.jqXHR, err.textStatus, err, {url: 'fetch'}) } else if (organization) { - dispatch(oActions.fetchSuccess([organization])) - - const memberships = !!organization.memberships ? organization.memberships : [] - const invitations = !!organization.invitations ? organization.invitations : [] - - dispatch(userOrganizationMembershipActions.fetchSuccess(memberships)) - dispatch(userOrganizationInvitationActions.fetchSuccess(invitations)) + dispatch(fetchSuccess([organization])) } }) } @@ -43,6 +38,14 @@ export function fetchSuccess(organizations) { return (dispatch) => { const formatted = organizations.map(o => _.pick(o, ['id', 'name', 'picture', 'admin_id', 'account', 'plan'])) dispatch(oActions.fetchSuccess(formatted)) + + const memberships = _.flatten(organizations.map(o => o.memberships || [])) + const invitations = _.flatten(organizations.map(o => o.invitations || [])) + const factsByOrg = organizations.map(o => ({organization_id: o.id, facts: o.facts || []})) + + if (!_.isEmpty(memberships)) { dispatch(userOrganizationMembershipActions.fetchSuccess(memberships)) } + if (!_.isEmpty(invitations)) { dispatch(userOrganizationInvitationActions.fetchSuccess(invitations)) } + if (!_.isEmpty(factsByOrg)) { dispatch(factActions.loadByOrg(factsByOrg)) } } } diff --git a/src/modules/reducers.js b/src/modules/reducers.js index 25f67e982..5cbe3ee69 100644 --- a/src/modules/reducers.js +++ b/src/modules/reducers.js @@ -9,7 +9,7 @@ import {metricsR} from './metrics/reducer' import {guesstimatesR} from './guesstimates/reducer' import simulationsR from './simulations/reducer' import meR from './me/reducer' -import canvasStateR from './canvas_state/reducer' +import {canvasStateR} from './canvas_state/reducer' import searchSpacesR from './search_spaces/reducer' import firstSubscriptionsR from './first_subscription/reducer' import modalR from './modal/reducer' @@ -21,6 +21,7 @@ import {copiedR} from './copied/reducer' import {checkpointsR} from './checkpoints/reducer' import {httpRequestsR} from './httpRequests/reducer' import {newOrganizationR} from './newOrganization/reducer' +import {factBankR} from './factBank/reducer' export function changeSelect(location) { return { type: 'CHANGE_SELECT', location }; @@ -50,6 +51,7 @@ const rootReducer = function app(state = {}, action){ checkpoints: SI(checkpointsR(state.checkpoints, action)), httpRequests: SI(httpRequestsR(state.httpRequests, action)), calculators: SI(reduxCrud.reducersFor('calculators')(state.calculators, action)), + factBank: SI(factBankR(state.factBank, action)), } } diff --git a/src/modules/spaces/actions.js b/src/modules/spaces/actions.js index 9375f6a1b..d0a0b9d5c 100644 --- a/src/modules/spaces/actions.js +++ b/src/modules/spaces/actions.js @@ -68,7 +68,7 @@ export function fetchById(spaceId) { const calculators = (_.get(value, '_embedded.calculators') || []).filter(c => !!c) if (!_.isEmpty(calculators)) {dispatch(calculatorActions.sActions.fetchSuccess(calculators))} - if (!!organization) {dispatch(organizationActions.fetchSuccess([organization]))} + if (!!organization) { dispatch(organizationActions.fetchSuccess([organization])) } if (!!user) { dispatch(userActions.fetchSuccess([user])) } else { From 9ac62916a893d6c4eb284d1ab77ce98319e7e136 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 26 Jul 2016 14:19:47 -0700 Subject: [PATCH 2/6] Cleanup and refactors to cement store-based change. --- .../editor/TextForm/TextInput.js | 72 +++++++++-- src/lib/factParser.js | 122 ------------------ 2 files changed, 59 insertions(+), 135 deletions(-) diff --git a/src/components/distributions/editor/TextForm/TextInput.js b/src/components/distributions/editor/TextForm/TextInput.js index 0b9d2e6cf..380c037c0 100644 --- a/src/components/distributions/editor/TextForm/TextInput.js +++ b/src/components/distributions/editor/TextForm/TextInput.js @@ -7,15 +7,43 @@ import {EditorState, Editor, ContentState, Modifier, CompositeDecorator} from 'd import {clearSuggestion, globalsSearch} from 'gModules/factBank/actions' import {isData, formatData} from 'lib/guesstimator/formatter/formatters/Data' -import {getFactParams, addText, addSuggestionToEditorState, findWithRegex, FACT_DECORATOR_LIST, positionDecorator, NounSpan, PropertySpan, SuggestionSpan} from 'lib/factParser' -const ValidInput = props => {props.children} -const ErrorInput = props => {props.children} - -function mapStateToProps(state) { - return { suggestion: state.factBank.currentSuggestion } +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) + } } +const stylizedSpan = className => props => {props.children} +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, +}) + +const mapStateToProps = state => ({suggestion: state.factBank.currentSuggestion}) + @connect(mapStateToProps) export class TextInput extends Component{ displayName: 'Guesstimate-TextInput' @@ -55,8 +83,26 @@ export class TextInput extends Component{ focus() { this.refs.editor.focus() } + addText(text, maintainCursorPosition = true, replaceLength = 0) { + const selection = this.state.editorState.getSelection() + const content = this.state.editorState.getCurrentContent() + + let baseEditorState + if (replaceLength === 0) { + baseEditorState = EditorState.push(this.state.editorState, Modifier.insertText(content, selection, text), 'paste') + } else { + const replaceSelection = selection.merge({anchorOffset: this.cursorPosition(), focusOffset: this.cursorPosition() + replaceLength + 1}) + baseEditorState = EditorState.push(this.state.editorState, Modifier.replaceText(content, replaceSelection, text), 'paste') + } + + if (!maintainCursorPosition) { return baseEditorState } + + const cursorPosition = selection.getFocusOffset() + const newSelectionState = selection.merge({focusOffset: cursorPosition}) + return EditorState.forceSelection(baseEditorState, newSelectionState) + } + insertAtCaret(text) { - this.onChange(addText(this.state.editorState, text, false)) } stripExtraDecorators(editorState) { return this.withExtraDecorators(editorState, []) } @@ -65,7 +111,7 @@ export class TextInput extends Component{ } deleteOldSuggestion(oldSuggestion) { - const freshEditorState = addText(this.state.editorState, '', true, this.cursorPosition(), this.cursorPosition() + oldSuggestion.length) + const freshEditorState = this.addText('', true, oldSuggestion.length) this.setState({editorState: this.stripExtraDecorators(freshEditorState)}) } @@ -73,13 +119,13 @@ export class TextInput extends Component{ const partial = this.prevWord().slice(1).split('.').pop() const inProperty = this.prevWord().includes('.') - const decoratorComponent = inProperty ? PropertySpan : NounSpan + const decoratorComponent = inProperty ? Property : Noun const extraDecorators = [ positionDecorator(this.cursorPosition() - partial.length - 1, this.cursorPosition(), decoratorComponent), - positionDecorator(this.cursorPosition(), this.cursorPosition() + this.props.suggestion.length, SuggestionSpan), + positionDecorator(this.cursorPosition(), this.cursorPosition() + this.props.suggestion.length, Suggestion), ] - const addedEditorState = addText(this.state.editorState, this.props.suggestion, true, this.cursorPosition(), this.cursorPosition()+this.nextWord().length-1) + const addedEditorState = this.addText(this.props.suggestion, true, this.nextWord().length - 1) this.setState({editorState: this.withExtraDecorators(addedEditorState, extraDecorators)}) } @@ -138,12 +184,12 @@ export class TextInput extends Component{ acceptSuggestion(){ const inProperty = this.prevWord().includes('.') const cursorPosition = this.cursorPosition() - const addedEditorState = addText(this.state.editorState, `${this.props.suggestion}${inProperty ? '' : '.'}`, false, cursorPosition, cursorPosition + this.props.suggestion.length - 1) + const addedEditorState = this.addText(`${this.props.suggestion}${inProperty ? '' : '.'}`, false, this.props.suggestion.length - 1) this.onChange(this.stripExtraDecorators(addedEditorState)) } handleFocus() { - $(window).on('functionMetricClicked', (_, {readableId}) => {this.insertAtCaret(readableId)}) + $(window).on('functionMetricClicked', (_, {readableId}) => {this.onChange(this.addText(readableId, false))}) this.props.onFocus() } diff --git a/src/lib/factParser.js b/src/lib/factParser.js index 6c71593ff..c5728e9de 100644 --- a/src/lib/factParser.js +++ b/src/lib/factParser.js @@ -1,124 +1,2 @@ import React from 'react' -import {EditorState, ContentState, Modifier, CompositeDecorator} from 'draft-js' - -import {getSuggestion} from 'gModules/factBank/actions' - -const NOUN_REGEX = /(\@[\w]+)/g -const PROPERTY_REGEX = /[a-zA-Z_](\.[\w]+)/g -export 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 const NounSpan = props => {props.children} -export const PropertySpan = props => {props.children} -export const SuggestionSpan = props => {props.children} - -export const FACT_DECORATOR_LIST = [ - { - strategy: (contentBlock, callback) => { findWithRegex(NOUN_REGEX, contentBlock, callback) }, - component: NounSpan, - }, - { - strategy: (contentBlock, callback) => { findWithRegex(PROPERTY_REGEX, contentBlock, callback) }, - component: PropertySpan, - }, -] - -export const positionDecorator = (start, end, component) => ({ - strategy: (contentBlock, callback) => {if (end <= contentBlock.text.length) {callback(start, end)}}, - component, -}) - -export function addText(editorState, text, maintainCursorPosition = true, anchorOffset = null, focusOffset = null) { - const selection = editorState.getSelection() - const content = editorState.getCurrentContent() - - let baseEditorState - if (!anchorOffset || !focusOffset) { - baseEditorState = EditorState.push(editorState, Modifier.insertText(content, selection, text), 'paste') - } else { - const replaceSelection = selection.merge({anchorOffset, focusOffset: focusOffset + 1}) - baseEditorState = EditorState.push(editorState, Modifier.replaceText(content, replaceSelection, text), 'paste') - } - - if (!maintainCursorPosition) { return baseEditorState } - - const cursorPosition = selection.getFocusOffset() - const newSelectionState = selection.merge({focusOffset: cursorPosition}) - return EditorState.forceSelection(baseEditorState, newSelectionState) -} - -class Suggestor { - constructor(editorState, currentSuggestion, dispatch) { - this.editorState = editorState - this.currentSuggestion = currentSuggestion - - this.dispatch = dispatch - - this.cursorPosition = editorState.getSelection().getFocusOffset() - this.text = editorState.getCurrentContent().getPlainText('') - - this.prevWord = this.text.slice(0, this.cursorPosition).split(/[^\w@\.]/).pop() - - this.nextWord = this.text.slice(this.cursorPosition).split(/[^\w]/)[0] - this.inProperty = this.prevWord.includes('.') - } - - run(){ - if (!this._shouldSuggest()) { return {suggestion: {text: '', suffix: ''}, extraDecorators: []} } - - const partials = this.prevWord.slice(1).split('.') - const newSuggestion = getSuggestion(partials) - - if (this._shouldRemoveSuggestion(newSuggestion)) { - const noSuggestion = addText(this.editorState, '', true, this.cursorPosition, this.cursorPosition + this.currentSuggestion.length) - return { - editorState: noSuggestion, - suggestion: { text: '', suffix: '' }, - extraDecorators: [], - } - } else { - return this._suggestionEditorState(partials.pop(), newSuggestion) - } - } - - _shouldRemoveSuggestion(newSuggestion){ - const hadSuggestion = !_.isEmpty(this.currentSuggestion) - const hasSuggestion = !_.isEmpty(newSuggestion) - const samePlace = this.nextWord === this.currentSuggestion - return (hadSuggestion && !hasSuggestion && samePlace) - } - - _shouldSuggest() { - return this.prevWord.startsWith('@') && this.editorState.getSelection().isCollapsed() - } - - _suggestionEditorState(partial, newSuggestion) { - const {inProperty, editorState, currentSuggestion, cursorPosition, nextWord} = this - - const nextWordSuitable = [currentSuggestion, newSuggestion].includes(nextWord) - if (_.isEmpty(newSuggestion) || !nextWordSuitable) { return {suggestion: {text: '', suffix: ''}} } - - const decoratorComponent = inProperty ? PropertySpan : NounSpan - const extraDecorators = [ - positionDecorator(cursorPosition - partial.length - 1, cursorPosition, decoratorComponent), - positionDecorator(cursorPosition, cursorPosition + newSuggestion.length, SuggestionSpan), - ] - - return { - editorState: addText(editorState, newSuggestion, true, cursorPosition, cursorPosition+nextWord.length-1), - suggestion: {text: newSuggestion, suffix: inProperty ? '' : '.'}, - extraDecorators, - } - } -} - -export function addSuggestionToEditorState(editorState, currentSuggestion, dispatch){ - return (new Suggestor(editorState, currentSuggestion, dispatch)).run() -} From 3e34dfeb575b1914e7fd6759631052a56c4becf5 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 26 Jul 2016 14:35:33 -0700 Subject: [PATCH 3/6] Minor pre-PR touch-ups. --- src/components/distributions/editor/TextForm/TextInput.js | 7 ++----- src/components/spaces/denormalized-space-selector.js | 1 - src/lib/factParser.js | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 src/lib/factParser.js diff --git a/src/components/distributions/editor/TextForm/TextInput.js b/src/components/distributions/editor/TextForm/TextInput.js index 380c037c0..a9e3af308 100644 --- a/src/components/distributions/editor/TextForm/TextInput.js +++ b/src/components/distributions/editor/TextForm/TextInput.js @@ -102,9 +102,6 @@ export class TextInput extends Component{ return EditorState.forceSelection(baseEditorState, newSelectionState) } - insertAtCaret(text) { - } - stripExtraDecorators(editorState) { return this.withExtraDecorators(editorState, []) } withExtraDecorators(editorState, extraDecorators) { return EditorState.set(editorState, {decorator: new CompositeDecorator(this.decoratorList(extraDecorators))}) @@ -119,9 +116,9 @@ export class TextInput extends Component{ const partial = this.prevWord().slice(1).split('.').pop() const inProperty = this.prevWord().includes('.') - const decoratorComponent = inProperty ? Property : Noun + const partialComponent = inProperty ? Property : Noun const extraDecorators = [ - positionDecorator(this.cursorPosition() - partial.length - 1, this.cursorPosition(), decoratorComponent), + positionDecorator(this.cursorPosition() - partial.length - 1, this.cursorPosition(), partialComponent), positionDecorator(this.cursorPosition(), this.cursorPosition() + this.props.suggestion.length, Suggestion), ] diff --git a/src/components/spaces/denormalized-space-selector.js b/src/components/spaces/denormalized-space-selector.js index 5992514ac..a7bc8f690 100644 --- a/src/components/spaces/denormalized-space-selector.js +++ b/src/components/spaces/denormalized-space-selector.js @@ -22,7 +22,6 @@ const SPACE_GRAPH_PARTS = [ 'metrics', 'guesstimates', 'simulations', - 'factBank', 'users', 'organizations', 'userOrganizationMemberships', diff --git a/src/lib/factParser.js b/src/lib/factParser.js deleted file mode 100644 index c5728e9de..000000000 --- a/src/lib/factParser.js +++ /dev/null @@ -1,2 +0,0 @@ -import React from 'react' - From 8f2e74656fe5f5bd6baa40c59e0ec95f1d7c62b5 Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 26 Jul 2016 15:14:27 -0700 Subject: [PATCH 4/6] Fixed backspace issue. --- src/components/distributions/editor/TextForm/TextInput.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/distributions/editor/TextForm/TextInput.js b/src/components/distributions/editor/TextForm/TextInput.js index a9e3af308..250d1f5bc 100644 --- a/src/components/distributions/editor/TextForm/TextInput.js +++ b/src/components/distributions/editor/TextForm/TextInput.js @@ -42,9 +42,7 @@ const positionDecorator = (start, end, component) => ({ component, }) -const mapStateToProps = state => ({suggestion: state.factBank.currentSuggestion}) - -@connect(mapStateToProps) +@connect(state => ({suggestion: state.factBank.currentSuggestion})) export class TextInput extends Component{ displayName: 'Guesstimate-TextInput' @@ -83,12 +81,12 @@ export class TextInput extends Component{ focus() { this.refs.editor.focus() } - addText(text, maintainCursorPosition = true, replaceLength = 0) { + addText(text, maintainCursorPosition = true, replaceLength = null) { const selection = this.state.editorState.getSelection() const content = this.state.editorState.getCurrentContent() let baseEditorState - if (replaceLength === 0) { + if (replaceLength === null) { baseEditorState = EditorState.push(this.state.editorState, Modifier.insertText(content, selection, text), 'paste') } else { const replaceSelection = selection.merge({anchorOffset: this.cursorPosition(), focusOffset: this.cursorPosition() + replaceLength + 1}) From 9913cb57ffe2d3942a8146db7a5b2dd4dd28e74c Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 26 Jul 2016 15:18:42 -0700 Subject: [PATCH 5/6] Further minor refactor. --- .../distributions/editor/TextForm/TextInput.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/distributions/editor/TextForm/TextInput.js b/src/components/distributions/editor/TextForm/TextInput.js index 250d1f5bc..964192ea8 100644 --- a/src/components/distributions/editor/TextForm/TextInput.js +++ b/src/components/distributions/editor/TextForm/TextInput.js @@ -81,16 +81,16 @@ export class TextInput extends Component{ focus() { this.refs.editor.focus() } - addText(text, maintainCursorPosition = true, replaceLength = null) { + addText(text, maintainCursorPosition = true, replaceLength = 0) { const selection = this.state.editorState.getSelection() const content = this.state.editorState.getCurrentContent() let baseEditorState - if (replaceLength === null) { - baseEditorState = EditorState.push(this.state.editorState, Modifier.insertText(content, selection, text), 'paste') - } else { - const replaceSelection = selection.merge({anchorOffset: this.cursorPosition(), focusOffset: this.cursorPosition() + replaceLength + 1}) + 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 } @@ -120,7 +120,7 @@ export class TextInput extends Component{ positionDecorator(this.cursorPosition(), this.cursorPosition() + this.props.suggestion.length, Suggestion), ] - const addedEditorState = this.addText(this.props.suggestion, true, this.nextWord().length - 1) + const addedEditorState = this.addText(this.props.suggestion, true, this.nextWord().length) this.setState({editorState: this.withExtraDecorators(addedEditorState, extraDecorators)}) } @@ -179,7 +179,7 @@ export class TextInput extends Component{ acceptSuggestion(){ const inProperty = this.prevWord().includes('.') const cursorPosition = this.cursorPosition() - const addedEditorState = this.addText(`${this.props.suggestion}${inProperty ? '' : '.'}`, false, this.props.suggestion.length - 1) + const addedEditorState = this.addText(`${this.props.suggestion}${inProperty ? '' : '.'}`, false, this.props.suggestion.length) this.onChange(this.stripExtraDecorators(addedEditorState)) } From 00fd34a9a6bd80d93f6d6af931bf376840c73d9d Mon Sep 17 00:00:00 2001 From: Matthew McDermott Date: Tue, 26 Jul 2016 15:23:01 -0700 Subject: [PATCH 6/6] One more tiny thing. --- src/components/distributions/editor/TextForm/TextInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/distributions/editor/TextForm/TextInput.js b/src/components/distributions/editor/TextForm/TextInput.js index 964192ea8..8653b637e 100644 --- a/src/components/distributions/editor/TextForm/TextInput.js +++ b/src/components/distributions/editor/TextForm/TextInput.js @@ -171,7 +171,7 @@ export class TextInput extends Component{ } handleTab(e){ - if (!_.isEmpty(this.props.suggestion)) { this.acceptSuggestion() } + if (!_.isEmpty(this.props.suggestion) && this.nextWord() === this.props.suggestion) { this.acceptSuggestion() } else { this.props.onTab(e.shiftKey) } e.preventDefault() }