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

feat(tracking): allow mention tracking to be turned off, turning the … #722

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/shaggy-cycles-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-mentions": minor
---

allow mention tracking to be turned off, turning the input into more of a macro expander
56 changes: 33 additions & 23 deletions src/MentionsInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ const propTypes = {
forceSuggestionsAboveCursor: PropTypes.bool,
ignoreAccents: PropTypes.bool,
a11ySuggestionsListLabel: PropTypes.string,
/**
* If set to false, all mentions will convert to plain text on
* add. This turns the input into more of a "macro expander."
* It is useful because now the text of the "mention" can be
* edited after insertion without great pains.
*/
trackMentions: PropTypes.bool,

value: PropTypes.string,
onKeyDown: PropTypes.func,
Expand Down Expand Up @@ -107,6 +114,7 @@ class MentionsInput extends React.Component {
static propTypes = propTypes

static defaultProps = {
trackMentions: true,
ignoreAccents: false,
singleLine: false,
allowSuggestionsAboveCursor: false,
Expand Down Expand Up @@ -183,7 +191,7 @@ class MentionsInput extends React.Component {
)
}

setContainerElement = (el) => {
setContainerElement = el => {
this.containerElement = el
}

Expand Down Expand Up @@ -242,15 +250,15 @@ class MentionsInput extends React.Component {
)
}

renderInput = (props) => {
renderInput = props => {
return <input type="text" ref={this.setInputRef} {...props} />
}

renderTextarea = (props) => {
renderTextarea = props => {
return <textarea ref={this.setInputRef} {...props} />
}

setInputRef = (el) => {
setInputRef = el => {
this.inputElement = el
const { inputRef } = this.props
if (typeof inputRef === 'function') {
Expand All @@ -260,7 +268,7 @@ class MentionsInput extends React.Component {
}
}

setSuggestionsElement = (el) => {
setSuggestionsElement = el => {
this.suggestionsElement = el
}

Expand Down Expand Up @@ -325,11 +333,11 @@ class MentionsInput extends React.Component {
)
}

setHighlighterElement = (el) => {
setHighlighterElement = el => {
this.highlighterElement = el
}

handleCaretPositionChange = (position) => {
handleCaretPositionChange = position => {
this.setState({ caretPosition: position })
}

Expand Down Expand Up @@ -499,7 +507,7 @@ class MentionsInput extends React.Component {
}

// Handle input element's change event
handleChange = (ev) => {
handleChange = ev => {
isComposing = false
if (isIE()) {
// if we are inside iframe, we need to find activeElement within its contentDocument
Expand All @@ -517,14 +525,14 @@ class MentionsInput extends React.Component {

let newPlainTextValue = ev.target.value

let selectionStartBefore = this.state.selectionStart;
if(selectionStartBefore == null) {
selectionStartBefore = ev.target.selectionStart;
let selectionStartBefore = this.state.selectionStart
if (selectionStartBefore == null) {
selectionStartBefore = ev.target.selectionStart
}

let selectionEndBefore = this.state.selectionEnd;
if(selectionEndBefore == null) {
selectionEndBefore = ev.target.selectionEnd;
let selectionEndBefore = this.state.selectionEnd
if (selectionEndBefore == null) {
selectionEndBefore = ev.target.selectionEnd
}

// Derive the new value to set by applying the local change in the textarea's plain text
Expand Down Expand Up @@ -586,7 +594,7 @@ class MentionsInput extends React.Component {
}

// Handle input element's select event
handleSelect = (ev) => {
handleSelect = ev => {
// keep track of selection range / caret position
this.setState({
selectionStart: ev.target.selectionStart,
Expand All @@ -610,7 +618,7 @@ class MentionsInput extends React.Component {
this.props.onSelect(ev)
}

handleKeyDown = (ev) => {
handleKeyDown = ev => {
// do not intercept key events if the suggestions overlay is not shown
const suggestionsCount = countSuggestions(this.state.suggestions)

Expand Down Expand Up @@ -652,7 +660,7 @@ class MentionsInput extends React.Component {
}
}

shiftFocus = (delta) => {
shiftFocus = delta => {
const suggestionsCount = countSuggestions(this.state.suggestions)

this.setState({
Expand All @@ -668,7 +676,7 @@ class MentionsInput extends React.Component {
const { result, queryInfo } = Object.values(suggestions).reduce(
(acc, { results, queryInfo }) => [
...acc,
...results.map((result) => ({ result, queryInfo })),
...results.map(result => ({ result, queryInfo })),
],
[]
)[focusIndex]
Expand All @@ -680,7 +688,7 @@ class MentionsInput extends React.Component {
})
}

handleBlur = (ev) => {
handleBlur = ev => {
const clickedSuggestion = this._suggestionsMouseDown
this._suggestionsMouseDown = false

Expand All @@ -700,11 +708,11 @@ class MentionsInput extends React.Component {
this.props.onBlur(ev, clickedSuggestion)
}

handleSuggestionsMouseDown = (ev) => {
handleSuggestionsMouseDown = ev => {
this._suggestionsMouseDown = true
}

handleSuggestionsMouseEnter = (focusIndex) => {
handleSuggestionsMouseEnter = focusIndex => {
this.setState({
focusIndex,
scrollFocusedIntoView: false,
Expand Down Expand Up @@ -1013,7 +1021,10 @@ class MentionsInput extends React.Component {
const start = mapPlainTextIndex(value, config, querySequenceStart, 'START')
const end = start + querySequenceEnd - querySequenceStart

let insert = makeMentionsMarkup(markup, id, display)
let displayValue = displayTransform(id, display)
let insert = this.props.trackMentions
? makeMentionsMarkup(markup, id, display)
: String(displayValue)

if (appendSpaceOnAdd) {
insert += ' '
Expand All @@ -1023,7 +1034,6 @@ class MentionsInput extends React.Component {
// Refocus input and set caret position to end of mention
this.inputElement.focus()

let displayValue = displayTransform(id, display)
if (appendSpaceOnAdd) {
displayValue += ' '
}
Expand Down
67 changes: 55 additions & 12 deletions src/MentionsInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ describe('MentionsInput', () => {
it.todo('should be possible to close the suggestions with esc.')

it('should be able to handle sync responses from multiple mentions sources', () => {
const extraData = [{ id: 'a', value: 'A' }, { id: 'b', value: 'B' }]
const extraData = [
{ id: 'a', value: 'A' },
{ id: 'b', value: 'B' },
]

const wrapper = mount(
<MentionsInput value="@">
Expand All @@ -64,7 +67,10 @@ describe('MentionsInput', () => {
wrapper.find('textarea').simulate('select', {
target: { selectionStart: 1, selectionEnd: 1 },
})
wrapper.find('textarea').getDOMNode().setSelectionRange(1, 1)
wrapper
.find('textarea')
.getDOMNode()
.setSelectionRange(1, 1)

expect(
wrapper.find('SuggestionsOverlay').find('Suggestion').length
Expand Down Expand Up @@ -104,7 +110,7 @@ describe('MentionsInput', () => {
<div id="root">
<div
id="portalDiv"
ref={(el) => {
ref={el => {
portalNode = el
}}
>
Expand Down Expand Up @@ -133,16 +139,19 @@ describe('MentionsInput', () => {
})

it('should accept a custom regex attribute', () => {
const data = [{ id: 'aaaa', display: '@A' }, { id: 'bbbb', display: '@B' }]
const data = [
{ id: 'aaaa', display: '@A' },
{ id: 'bbbb', display: '@B' },
]
const wrapper = mount(
<MentionsInput value=":aaaa and :bbbb and :invalidId">
<Mention
trigger="@"
data={data}
markup=":__id__"
regex={/:(\S+)/}
displayTransform={(id) => {
let mention = data.find((item) => item.id === id)
displayTransform={id => {
let mention = data.find(item => item.id === id)
return mention ? mention.display : `:${id}`
}}
/>
Expand Down Expand Up @@ -216,7 +225,7 @@ describe('MentionsInput', () => {

it.each(['cut', 'copy'])(
'should include the whole mention for a "%s" event when the selection starts in one.',
(eventType) => {
eventType => {
const textarea = component.find('textarea')

const selectionStart = plainTextValue.indexOf('First') + 2
Expand Down Expand Up @@ -251,7 +260,7 @@ describe('MentionsInput', () => {

it.each(['cut', 'copy'])(
'should include the whole mention for a "%s" event when the selection ends in one.',
(eventType) => {
eventType => {
const textarea = component.find('textarea')

const selectionStart = 0
Expand Down Expand Up @@ -286,7 +295,7 @@ describe('MentionsInput', () => {

it.each(['cut', 'copy'])(
'should fallback to the browsers behavior if the "%s" event does not support clipboardData',
(eventType) => {
eventType => {
// IE 11 has no clipboardData attached to the event and only supports mime type "text"
// therefore, the new mechanism should ignore those events and let the browser handle them
const textarea = component.find('textarea')
Expand Down Expand Up @@ -380,7 +389,7 @@ describe('MentionsInput', () => {

const event = new Event('paste', { bubbles: true })
event.clipboardData = {
getData: jest.fn((type) =>
getData: jest.fn(type =>
type === 'text/react-mentions' ? pastedText : ''
),
}
Expand Down Expand Up @@ -408,7 +417,7 @@ describe('MentionsInput', () => {

const event = new Event('paste', { bubbles: true })
event.clipboardData = {
getData: jest.fn((type) => (type === 'text/plain' ? pastedText : '')),
getData: jest.fn(type => (type === 'text/plain' ? pastedText : '')),
}

expect(onChange).not.toHaveBeenCalled()
Expand All @@ -430,7 +439,7 @@ describe('MentionsInput', () => {
const event = new Event('paste', { bubbles: true })

event.clipboardData = {
getData: jest.fn((type) => (type === 'text/plain' ? pastedText : '')),
getData: jest.fn(type => (type === 'text/plain' ? pastedText : '')),
}

const onChange = jest.fn()
Expand Down Expand Up @@ -476,4 +485,38 @@ describe('MentionsInput', () => {
expect(preventDefault).not.toHaveBeenCalled()
})
})

it('should not track mentions if `trackMentions` is false', () => {
const data = [{ id: 'first', display: 'First Entry' }]
const onChange = jest.fn()

const wrapper = mount(
<MentionsInput onChange={onChange} trackMentions={false} value="@">
<Mention trigger="@" data={data} />
</MentionsInput>
)

expect(onChange).not.toHaveBeenCalled()

wrapper.find('textarea').simulate('focus')
wrapper.find('textarea').simulate('select', {
target: { selectionStart: 1, selectionEnd: 1 },
})
wrapper
.find('textarea')
.getDOMNode()
.setSelectionRange(1, 1)

expect(
wrapper.find('SuggestionsOverlay').find('Suggestion').length
).toEqual(data.length)
wrapper
.find('SuggestionsOverlay')
.find('Suggestion')
.at(0)
.simulate('click')
const [[, newValue, newPlainTextValue]] = onChange.mock.calls
expect(newValue).toEqual('First Entry')
expect(newPlainTextValue).toEqual('First Entry')
})
})