@@ -4,14 +4,23 @@ export function triggerEditorContentChanged(target: HTMLElement) {
44 target . dispatchEvent ( new CustomEvent ( EventEditorContentChanged , { bubbles : true } ) ) ;
55}
66
7- export function textareaInsertText ( textarea : HTMLTextAreaElement , value : string ) {
8- const startPos = textarea . selectionStart ;
9- const endPos = textarea . selectionEnd ;
10- textarea . value = textarea . value . substring ( 0 , startPos ) + value + textarea . value . substring ( endPos ) ;
11- textarea . selectionStart = startPos ;
12- textarea . selectionEnd = startPos + value . length ;
7+ /** replace selected text or insert text by creating a new edit history entry,
8+ * e.g. CTRL-Z works after this */
9+ export function replaceTextareaSelection ( textarea : HTMLTextAreaElement , text : string ) {
10+ const before = textarea . value . slice ( 0 , textarea . selectionStart ) ;
11+ const after = textarea . value . slice ( textarea . selectionEnd ) ;
12+
1313 textarea . focus ( ) ;
14- triggerEditorContentChanged ( textarea ) ;
14+ let success = false ;
15+ try {
16+ success = document . execCommand ( 'insertText' , false , text ) ; // eslint-disable-line @typescript-eslint/no-deprecated
17+ } catch { }
18+
19+ // fall back to regular replacement
20+ if ( ! success ) {
21+ textarea . value = `${ before } ${ text } ${ after } ` ;
22+ triggerEditorContentChanged ( textarea ) ;
23+ }
1524}
1625
1726type TextareaValueSelection = {
@@ -176,7 +185,7 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa
176185 return { handled : true , valueSelection : { value : linesBuf . lines . join ( '\n' ) , selStart : newPos , selEnd : newPos } } ;
177186}
178187
179- function handleNewline ( textarea : HTMLTextAreaElement , e : Event ) {
188+ function handleNewline ( textarea : HTMLTextAreaElement , e : KeyboardEvent ) {
180189 const ret = markdownHandleIndention ( { value : textarea . value , selStart : textarea . selectionStart , selEnd : textarea . selectionEnd } ) ;
181190 if ( ! ret . handled || ! ret . valueSelection ) return ; // FIXME: the "handled" seems redundant, only valueSelection is enough (null for unhandled)
182191 e . preventDefault ( ) ;
@@ -185,6 +194,28 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
185194 triggerEditorContentChanged ( textarea ) ;
186195}
187196
197+ // Keys that act as dead keys will not work because the spec dictates that such keys are
198+ // emitted as `Dead` in e.key instead of the actual key.
199+ const pairs = new Map < string , string > ( [
200+ [ "'" , "'" ] ,
201+ [ '"' , '"' ] ,
202+ [ '`' , '`' ] ,
203+ [ '(' , ')' ] ,
204+ [ '[' , ']' ] ,
205+ [ '{' , '}' ] ,
206+ [ '<' , '>' ] ,
207+ ] ) ;
208+
209+ function handlePairCharacter ( textarea : HTMLTextAreaElement , e : KeyboardEvent ) : void {
210+ const selStart = textarea . selectionStart ;
211+ const selEnd = textarea . selectionEnd ;
212+ if ( selEnd === selStart ) return ; // do not process when no selection
213+ e . preventDefault ( ) ;
214+ const inner = textarea . value . substring ( selStart , selEnd ) ;
215+ replaceTextareaSelection ( textarea , `${ e . key } ${ inner } ${ pairs . get ( e . key ) } ` ) ;
216+ textarea . setSelectionRange ( selStart + 1 , selEnd + 1 ) ;
217+ }
218+
188219function isTextExpanderShown ( textarea : HTMLElement ) : boolean {
189220 return Boolean ( textarea . closest ( 'text-expander' ) ?. querySelector ( '.suggestions' ) ) ;
190221}
@@ -198,6 +229,8 @@ export function initTextareaMarkdown(textarea: HTMLTextAreaElement) {
198229 } else if ( e . key === 'Enter' && ! e . shiftKey && ! e . ctrlKey && ! e . metaKey && ! e . altKey ) {
199230 // use Enter to insert a new line with the same indention and prefix
200231 handleNewline ( textarea , e ) ;
232+ } else if ( pairs . has ( e . key ) ) {
233+ handlePairCharacter ( textarea , e ) ;
201234 }
202235 } ) ;
203236}
0 commit comments