@@ -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