@@ -6,6 +6,15 @@ type Options = {
6
6
moveToNewLine : RegExp
7
7
spellcheck : boolean
8
8
catchTab : boolean
9
+ /**
10
+ * Enabling multilineIndentation allows users to select blocks of
11
+ * text and indent (or dedent) them with the tab (or shift-tab) key.
12
+ * This setting is currently disabled by default.
13
+ *
14
+ * Note that this feature does not currently work with Firefox, and
15
+ * will be disabled automatically if the browser does not support it.
16
+ */
17
+ multilineIndentation : boolean
9
18
preserveIdent : boolean
10
19
addClosing : boolean
11
20
history : boolean
@@ -32,6 +41,7 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
32
41
moveToNewLine : / ^ [ ) } \] ] / ,
33
42
spellcheck : false ,
34
43
catchTab : true ,
44
+ multilineIndentation : false ,
35
45
preserveIdent : true ,
36
46
addClosing : true ,
37
47
history : true ,
@@ -65,7 +75,11 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
65
75
66
76
highlight ( editor )
67
77
if ( editor . contentEditable !== 'plaintext-only' ) isLegacy = true
68
- if ( isLegacy ) editor . setAttribute ( 'contenteditable' , 'true' )
78
+ if ( isLegacy ) {
79
+ editor . setAttribute ( 'contenteditable' , 'true' )
80
+ // Disable multiline indentation if plaintext-only is not supported.
81
+ options . multilineIndentation = false
82
+ }
69
83
70
84
const debounceHighlight = debounce ( ( ) => {
71
85
const pos = save ( )
@@ -353,9 +367,39 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
353
367
}
354
368
}
355
369
370
+ /**
371
+ * Expands (or shrinks) a range by a given number of characters by adding
372
+ * (or removing) a given number of characters from the tail of the range.
373
+ * @param range The range to expand.
374
+ * @param additionalCharacters The number of characters to expand by.
375
+ * @returns A new range representing the original range expanded by the given
376
+ * number of characters (in the tail direction.)
377
+ * @description Passing a negative value for the `additionalCharacters`
378
+ * parameter will shrink the range instead of expanding it. This is by
379
+ * design.
380
+ */
381
+ function inflateRange ( range : Position , additionalCharacters : number ) {
382
+ if ( range . dir === '->' ) {
383
+ return { start : range . start , end : range . end + additionalCharacters , dir : range . dir }
384
+ } else {
385
+ return { start : range . start + additionalCharacters , end : range . end , dir : range . dir }
386
+ }
387
+ }
388
+
356
389
function handleTabCharacters ( event : KeyboardEvent ) {
357
- if ( event . key === 'Tab' ) {
358
- preventDefault ( event )
390
+ if ( event . key !== 'Tab' ) {
391
+ return ;
392
+ }
393
+
394
+ preventDefault ( event ) ;
395
+
396
+ // For standard tab behavior, simply allow the tab to be inserted (or
397
+ // removed.) This behavior could probably be combined with the multi-line
398
+ // behavior below but this is being left as-is for now to de-risk the
399
+ // multiline behavior change and maintain the previous behavior of the
400
+ // library by default.
401
+ const selection = getSelection ( ) ;
402
+ if ( ! options . multilineIndentation || selection . getRangeAt ( 0 ) . collapsed ) {
359
403
if ( event . shiftKey ) {
360
404
const before = beforeCursor ( )
361
405
let [ padding , start ] = findPadding ( before )
@@ -372,7 +416,52 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
372
416
} else {
373
417
insert ( options . tab )
374
418
}
419
+ return ;
375
420
}
421
+
422
+ // For multi-line tab behavior, we indent or dedent the selected lines.
423
+ // Since this operation effects the entire line, we extend the selection
424
+ // to cover the entire line before proceeding.
425
+ // Firefox's support for calling .modify() on a selection is limited.
426
+ // It will only modify the user's original selection, and will not
427
+ // modify any selection that was created programmatically. So we have
428
+ // disabled this feature for Firefox until we can find a workaround.
429
+ selection . modify ( 'extend' , 'backward' , 'lineboundary' )
430
+ selection . modify ( 'extend' , 'forward' , 'lineboundary' )
431
+ const selectedText = selection . getRangeAt ( 0 ) . toString ( )
432
+ const selectedLines = selectedText . split ( '\n' )
433
+ const lineCount = selectedLines . length
434
+
435
+ const initialSelection = save ( )
436
+
437
+ let insertedCharacters = 0
438
+ // If the shift key is being held, it's a dedent request.
439
+ if ( event . shiftKey ) {
440
+ for ( let i = 0 ; i < lineCount ; i ++ ) {
441
+ // We can only dedent lines that begin with some sort of whitespace.
442
+ // So we check for that first, and never consume more characters than
443
+ // there is whitespace.
444
+ const match = selectedLines [ i ] . match ( / ^ \s + / )
445
+ if ( match !== null ) {
446
+ const leadingSpace = match [ 0 ]
447
+ const originalLength = selectedLines [ i ] . length
448
+ if ( leadingSpace . length >= options . tab . length ) {
449
+ selectedLines [ i ] = selectedLines [ i ] . slice ( options . tab . length )
450
+ } else if ( leadingSpace . length > 0 ) {
451
+ selectedLines [ i ] = selectedLines [ i ] . slice ( leadingSpace . length )
452
+ }
453
+ insertedCharacters = insertedCharacters + ( selectedLines [ i ] . length - originalLength )
454
+ }
455
+ }
456
+ } else {
457
+ insertedCharacters = lineCount * options . tab . length
458
+ for ( let i = 0 ; i < lineCount ; i ++ ) {
459
+ selectedLines [ i ] = options . tab + selectedLines [ i ]
460
+ }
461
+ }
462
+
463
+ insert ( selectedLines . join ( '\n' ) )
464
+ restore ( inflateRange ( initialSelection , insertedCharacters ) )
376
465
}
377
466
378
467
function handleUndoRedo ( event : KeyboardEvent ) {
0 commit comments