Skip to content

Commit aeb208d

Browse files
committed
Big Refactor and multiline support
1 parent 70eb905 commit aeb208d

File tree

1 file changed

+152
-63
lines changed

1 file changed

+152
-63
lines changed

components/MarkdownToolbar.tsx

Lines changed: 152 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -25,94 +25,179 @@ type StyleArgs = {
2525
multiline?: boolean
2626
surroundWithNewLines?: boolean
2727
}
28-
type ApplyMarkdown = (
29-
inputRef: React.RefObject<HTMLTextAreaElement>,
28+
29+
const applyMarkdown = (
30+
textarea: HTMLTextAreaElement,
3031
styleArgs: StyleArgs
31-
) => void
32-
const applyMarkdown: ApplyMarkdown = (
33-
inputRef,
34-
{
32+
): void => {
33+
if (!textarea) return
34+
35+
const {
3536
prefix = '',
3637
suffix = '',
3738
blockPrefix = '',
3839
blockSuffix = '',
3940
multiline = false,
4041
surroundWithNewLines = false
41-
} = {}
42-
) => {
43-
if (!inputRef.current) return
44-
const { value, selectionStart, selectionEnd } = inputRef.current
42+
} = styleArgs
4543

46-
const leftOfSelection = value.slice(0, selectionStart)
47-
const selection = value.slice(selectionStart, selectionEnd)
48-
const rightOfSelection = value.slice(selectionEnd)
49-
inputRef.current.focus()
44+
const { value, selectionStart, selectionEnd } = textarea
45+
46+
const left = value.slice(0, selectionStart)
47+
const selectionLines = value.slice(selectionStart, selectionEnd).split('\n')
48+
const right = value.slice(selectionEnd)
49+
textarea.focus()
50+
51+
// If multiline selection use blockPrefix/blockSuffix if provided
52+
const [selectedPrefix, selectedSuffix] =
53+
selectionLines.length > 1
54+
? [blockPrefix || prefix, blockSuffix || suffix]
55+
: [prefix, suffix]
5056

5157
// If Style is already applied remove it
52-
if (leftOfSelection.endsWith(prefix) && rightOfSelection.startsWith(suffix)) {
53-
// expand selection to include prefix and suffix
54-
inputRef.current.setSelectionRange(
55-
selectionStart - prefix.length,
56-
selectionEnd + suffix.length
57-
)
5858

59-
insertText(inputRef.current, selection)
59+
const action = detectActionType({
60+
left,
61+
selectionLines,
62+
right,
63+
selectedPrefix,
64+
selectedSuffix,
65+
multiline
66+
})
6067

61-
// update selection with removed prefix
62-
inputRef.current.setSelectionRange(
63-
selectionStart - prefix.length,
64-
selectionEnd - prefix.length
65-
)
66-
} else {
67-
// apply style updates
68+
console.log({ action })
69+
70+
switch (action) {
71+
case 'removeSingle':
72+
textarea.setSelectionRange(
73+
selectionStart - selectedPrefix.length,
74+
selectionEnd + selectedSuffix.length
75+
)
76+
77+
insertText(textarea, selectionLines.join('\n'))
6878

69-
let newText = prefix + selection + suffix
79+
// update selection with removed prefix
80+
textarea.setSelectionRange(
81+
selectionStart - selectedPrefix.length,
82+
selectionEnd - selectedPrefix.length
83+
)
84+
break
85+
case 'removeMulti':
86+
insertText(
87+
textarea,
88+
selectionLines
89+
.map(line =>
90+
line.slice(
91+
selectedPrefix.length,
92+
line.length - selectedSuffix.length
93+
)
94+
)
95+
.join('\n')
96+
)
97+
textarea.selectionStart = selectionStart
7098

71-
if (surroundWithNewLines)
72-
newText = newLineSurround(leftOfSelection, rightOfSelection, newText)
99+
break
100+
default:
101+
// Handle both add cases 'addSingle' & 'addMulti'
102+
let newText =
103+
action === 'addMulti'
104+
? styleEachLine(prefix, suffix, selectionLines)
105+
: selectedPrefix + selectionLines.join('\n') + selectedSuffix
73106

74-
insertText(inputRef.current, newText)
107+
let newPrefixLineCount = 0
108+
if (surroundWithNewLines) {
109+
newPrefixLineCount = !left ? 0 : 2 - left.match(/\n*$/)![0].length
110+
newText = newLineSurround(left, right, newText)
111+
}
75112

76-
/// update selection with added prefix and added newlines
113+
// Insert Selection Text
114+
insertText(textarea, newText)
77115

78-
const addedNewLines =
79-
!surroundWithNewLines || !leftOfSelection
80-
? 0
81-
: 2 - leftOfSelection.match(/\n*$/)![0].length
116+
// Update Selection Cursor
117+
let startOffset = newPrefixLineCount
118+
let endOffset = newPrefixLineCount
119+
if (action === 'addMulti') {
120+
//In multiline update we want the selection to include the added prefix so we dont update the start postion
121+
endOffset +=
122+
selectionLines.length *
123+
(selectedPrefix.length + selectedSuffix.length)
124+
} else {
125+
startOffset += selectedPrefix.length
126+
endOffset += selectedPrefix.length
127+
}
82128

83-
inputRef.current.setSelectionRange(
84-
selectionStart + prefix.length + addedNewLines,
85-
selectionEnd + prefix.length + addedNewLines
129+
textarea.setSelectionRange(
130+
selectionStart + startOffset,
131+
selectionEnd + endOffset
132+
)
133+
break
134+
}
135+
}
136+
137+
type StyleDetectionInput = {
138+
left: string
139+
selectionLines: string[]
140+
right: string
141+
selectedPrefix: string
142+
selectedSuffix: string
143+
multiline: boolean
144+
}
145+
type ActionTypes = 'addMulti' | 'addSingle' | 'removeSingle' | 'removeMulti'
146+
147+
export const detectActionType = ({
148+
left,
149+
selectionLines,
150+
right,
151+
selectedPrefix,
152+
selectedSuffix,
153+
multiline
154+
}: StyleDetectionInput): ActionTypes => {
155+
if (
156+
(!multiline || selectionLines.length === 1) &&
157+
left.endsWith(selectedPrefix) &&
158+
right.startsWith(selectedSuffix)
159+
)
160+
return 'removeSingle'
161+
162+
if (multiline && selectionLines.length > 1) {
163+
const linesAreStyled = selectionLines.every(
164+
line => line.startsWith(selectedPrefix) && line.endsWith(selectedSuffix)
86165
)
166+
167+
return linesAreStyled ? 'removeMulti' : 'addMulti'
87168
}
169+
170+
return 'addSingle'
88171
}
89172

90-
export function newLineSurround(
91-
leftOfSelection: string,
92-
rightOfSelection: string,
173+
export const styleEachLine = (
174+
prefix: string,
175+
suffix: string,
176+
lines: string[]
177+
): string => lines.map(line => prefix + line + suffix).join('\n')
178+
179+
// Do not add new lines if text is at the start or end of the input
180+
export const newLineSurround = (
181+
left: string,
182+
right: string,
93183
text: string
94-
): string {
95-
// If left or right are empty strings meaning the text is at the beginning or end
96-
// of the textarea so don't add new lines in that case.
97-
// Else make sure there are at least 2 new lines
184+
): string => {
98185
return (
99-
'\n'.repeat(
100-
!leftOfSelection ? 0 : 2 - leftOfSelection.match(/\n*$/)![0].length
101-
) +
186+
'\n'.repeat(!left ? 0 : 2 - left.match(/\n*$/)![0].length) +
102187
text +
103-
'\n'.repeat(
104-
!rightOfSelection ? 0 : 2 - rightOfSelection.match(/^\n*/)![0].length
105-
)
188+
'\n'.repeat(!right ? 0 : 2 - right.match(/^\n*/)![0].length)
106189
)
107190
}
108191

109-
export function insertText(textArea: HTMLTextAreaElement, text: string) {
192+
export const insertText = (
193+
textArea: HTMLTextAreaElement,
194+
text: string
195+
): void => {
110196
// Detect bug case with chrome automatically removing linefeed character
111197
// when replace all the text on last line with empty string
112-
const leftOfSelection = textArea.value[textArea.selectionStart - 1]
113-
const rightOfSelection = textArea.value[textArea.selectionEnd + 1]
114-
const isChromeBugCase =
115-
text === '' && leftOfSelection === '\n' && rightOfSelection === undefined
198+
const left = textArea.value[textArea.selectionStart - 1]
199+
const right = textArea.value[textArea.selectionEnd + 1]
200+
const isChromeBugCase = text === '' && left === '\n' && right === undefined
116201

117202
// Expand selection to include line feed in bug case
118203
if (isChromeBugCase) textArea.selectionStart = textArea.selectionStart - 1
@@ -138,7 +223,7 @@ const buttons = [
138223
},
139224
{
140225
tooltipTitle: 'Add italic text',
141-
styleArgs: { prefix: '_', suffix: '_' },
226+
styleArgs: { prefix: ' _', suffix: '_ ' }, // Must include space for style to work
142227
Icon: ItalicIcon
143228
},
144229
{
@@ -151,8 +236,8 @@ const buttons = [
151236
styleArgs: {
152237
prefix: '`',
153238
suffix: '`',
154-
blockPrefix: '```',
155-
blockSuffix: '```'
239+
blockPrefix: '```\n',
240+
blockSuffix: '\n```'
156241
},
157242
Icon: CodeIcon
158243
},
@@ -173,7 +258,11 @@ const buttons = [
173258
},
174259
{
175260
tooltipTitle: 'Add a task list',
176-
styleArgs: { prefix: '- [ ] ', surroundWithNewLines: true },
261+
styleArgs: {
262+
prefix: '- [ ] ',
263+
surroundWithNewLines: true,
264+
multiline: true
265+
},
177266
Icon: TasklistIcon
178267
}
179268
]
@@ -202,7 +291,7 @@ const MarkdownToolbar: React.FC<Props & ButtonToolbarProps> = ({
202291
{...triggerHandler}
203292
ref={ref}
204293
aria-label={tooltipTitle}
205-
onClick={() => applyMarkdown(inputRef, styleArgs)}
294+
onClick={() => applyMarkdown(inputRef.current, styleArgs)}
206295
>
207296
<Icon size={'small'} />
208297
</button>

0 commit comments

Comments
 (0)