Skip to content
Merged
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"access": "public"
},
"dependencies": {
"@webscopeio/react-textarea-autocomplete": "2.1.1",
"@webscopeio/react-textarea-autocomplete": "2.2.0",
"classnames": "2.2.5",
"emojilib": "2.2.12",
"github-markdown-css": "2.10.0",
Expand Down
20 changes: 17 additions & 3 deletions src/BaseMarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import GithubMarkdown from 'github-markdown-css'
import AutocompleteItem from './AutocompleteItem'
import Toolbar from './Toolbar'

import type { Range } from './types'

const Loading = () => <div>Loading</div>

type MarkdownEditorProps = {
Expand Down Expand Up @@ -49,9 +51,20 @@ class MarkdownEditor extends React.Component<MarkdownEditorProps> {
this.props.onSetPreview(true)
}

setCaretPosition = (position: number) => this.rtaRef.setCaretPosition(position)
setSelectionRange = (range: Range) : void => {
this.rtaRef.textareaRef.setSelectionRange(range.start, range.end)
this.rtaRef.textareaRef.focus()
}

getCaretPosition = (): number => this.rtaRef.getCaretPosition()
getSelectedRange = () : Range => {
const { selectionStart, selectionEnd } = this.rtaRef.getSelectionPosition()
return {
start: selectionStart,
end: selectionEnd,
}
}

setCaretPosition = (position: number) => this.rtaRef.setCaretPosition(position)

// TODO - types
rtaRef: any = null
Expand Down Expand Up @@ -86,7 +99,8 @@ class MarkdownEditor extends React.Component<MarkdownEditorProps> {
</button>
</nav>
<Toolbar
getCaretPosition={this.getCaretPosition}
setSelectionRange={this.setSelectionRange}
getSelectedRange={this.getSelectedRange}
setCaretPosition={this.setCaretPosition}
value={value}
onChange={onChange}
Expand Down
59 changes: 38 additions & 21 deletions src/Toolbar.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,61 @@
// @flow
import React, { Component } from 'react'
import { insertSymbol } from './helpers'
import { insertSymbol, wrapSelectedRangeWithSymbols } from './helpers'
import styles from './Toolbar.module.css'

// get SVG icons https://zurb.com/playground/foundation-icon-fonts-3
import bold from './icons/fi-bold.svg'
import italics from './icons/fi-italic.svg'
import type { MarkdownAction, Range } from './types'

type ToolbarProps = {
getCaretPosition: () => number,
setSelectionRange: Range => void,
getSelectedRange: () => Range,
setCaretPosition: (number) => void,
onChange: any,
value: string,
}

class Toolbar extends Component<ToolbarProps> {
insertBold = () => {
insertSymbols = (action: MarkdownAction) => {
const { prefix, suffix } = action

const {
value, onChange, getCaretPosition, setCaretPosition,
value,
onChange,
setCaretPosition,
getSelectedRange,
setSelectionRange,
} = this.props
const caretPosition = getCaretPosition()
const [newValue, inserted] = insertSymbol(value, caretPosition, ['**', '**'])
onChange({ target: { value: newValue } })
setTimeout(() => {
const newPosition = inserted ? caretPosition + 2 : caretPosition - 2
setCaretPosition(newPosition)
})

const range = getSelectedRange()
const { start, end } = range

if (start === end) {
const caretPosition = start
const [newValue, inserted] = insertSymbol(value, caretPosition, [prefix, suffix])
onChange({ target: { value: newValue } })
setTimeout(() => {
const newPosition = inserted ? caretPosition + 2 : caretPosition - 2
setCaretPosition(newPosition)
})
} else {
const [newValue, inserted] = wrapSelectedRangeWithSymbols(value, range, { prefix, suffix })
onChange({ target: { value: newValue } })
setTimeout(() => {
const newStart = inserted ? start + 2 : start - 2
const newEnd = inserted ? end + 2 : end - 2
setSelectionRange({ start: newStart, end: newEnd })
})
}
}

insertBold = () => {
this.insertSymbols({ prefix: '**', suffix: '**' })
}

insertItalics = () => {
const {
value, onChange, getCaretPosition, setCaretPosition,
} = this.props
const caretPosition = getCaretPosition()
const [newValue, inserted] = insertSymbol(value, caretPosition, ['_', '_'])
onChange({ target: { value: newValue } })
setTimeout(() => {
const newPosition = inserted ? caretPosition + 1 : caretPosition - 1
setCaretPosition(newPosition)
})
this.insertSymbols({ prefix: '_', suffix: '_' })
}

render() {
Expand Down
23 changes: 23 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import type { MarkdownAction, Range } from './types'

const isEmptyAtPosition = (word: string, position: number) => word[position] === ' '

Expand All @@ -21,6 +22,7 @@ export const getWordStartAndEndLocation = (word: string, position: number) => {

const insertSymbolInWord = (word: string, position: number, [prefix, suffix]: Array<string>) => {
const [start, end] = getWordStartAndEndLocation(word, position)

const startWord = word.slice(0, start)
const actualWord = word.slice(start, end)
const endWord = word.slice(end, word.length)
Expand Down Expand Up @@ -52,3 +54,24 @@ export const insertSymbol = (text: string, position: number, [prefix, suffix]: A
return [lines.join('\n'), replaced]
}

/**
* Given a text and selected range, following function wraps this text with
* provided symbols.
*/
export const wrapSelectedRangeWithSymbols =
(text: string, range: Range, action: MarkdownAction): [string, boolean] => {
const { start, end } = range
const { prefix, suffix } = action

const startWord = text.slice(0, start)
const actualWord = text.slice(start, end)
const endWord = text.slice(end, text.length)

if (startWord.endsWith(prefix) && endWord.startsWith(suffix)) {
const strippedStartWord = startWord.slice(0, startWord.length - prefix.length)
const strippedEndWord = endWord.slice(2)
return [`${strippedStartWord}${actualWord}${strippedEndWord}`, false]
}

return [`${startWord}${prefix}${actualWord}${suffix}${endWord}`, true]
}
20 changes: 19 additions & 1 deletion src/helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { getWordStartAndEndLocation, insertSymbol } from './helpers'
// @flow
// TMP solution, install flow-typed - jest
declare var expect: any
declare var it: any
declare var describe: any

import { getWordStartAndEndLocation, insertSymbol, wrapSelectedRangeWithSymbols } from './helpers'

describe('Markdown helpers', () => {
it('get current world start & stop positions', () => {
Expand All @@ -17,6 +23,18 @@ describe('Markdown helpers', () => {
})

describe('Markdown replace functions', () => {
it('wraps selected text with symbols', () => {
const text = 'Hello world! I hope you like this plugin!'
expect(wrapSelectedRangeWithSymbols(text, { start: 2, end: 14 }, { prefix: '**', suffix: '**' }))
.toEqual(['He**llo world! I** hope you like this plugin!', true])
})

it('unwraps selected text with symbols', () => {
const text = 'He**llo world! I** hope you like this plugin!'
expect(wrapSelectedRangeWithSymbols(text, { start: 4, end: 16 }, { prefix: '**', suffix: '**' }))
.toEqual(['Hello world! I hope you like this plugin!', false])
})

it('inserts symbol helper', () => {
const text = 'Hello world'
expect(insertSymbol(text, 1, ['**', '**'])).toEqual(['**Hello** world', true])
Expand Down
11 changes: 11 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @flow

export type Range = {|
start: number,
end: number,
|}

export type MarkdownAction = {|
prefix: string,
suffix: string,
|}
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@
dependencies:
eslint-config-airbnb "^16.1.0"

"@webscopeio/react-textarea-autocomplete@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@webscopeio/react-textarea-autocomplete/-/react-textarea-autocomplete-2.1.1.tgz#8fa251827f4c50a1ddc28893b8eda706ca06b354"
"@webscopeio/react-textarea-autocomplete@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@webscopeio/react-textarea-autocomplete/-/react-textarea-autocomplete-2.2.0.tgz#793c5ffd659369d887f143547845ce40eedb0bee"
dependencies:
textarea-caret "3.0.2"

Expand Down