Skip to content
This repository was archived by the owner on Aug 4, 2023. It is now read-only.

Commit 33ee715

Browse files
committed
Support Multiline Indentation on Chrome and Safari
1 parent e7cfb65 commit 33ee715

File tree

2 files changed

+93
-4
lines changed

2 files changed

+93
-4
lines changed

codejar.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ type Options = {
66
moveToNewLine: RegExp
77
spellcheck: boolean
88
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
918
preserveIdent: boolean
1019
addClosing: boolean
1120
history: boolean
@@ -32,6 +41,7 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
3241
moveToNewLine: /^[)}\]]/,
3342
spellcheck: false,
3443
catchTab: true,
44+
multilineIndentation: false,
3545
preserveIdent: true,
3646
addClosing: true,
3747
history: true,
@@ -65,7 +75,11 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
6575

6676
highlight(editor)
6777
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+
}
6983

7084
const debounceHighlight = debounce(() => {
7185
const pos = save()
@@ -353,9 +367,39 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
353367
}
354368
}
355369

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+
356389
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) {
359403
if (event.shiftKey) {
360404
const before = beforeCursor()
361405
let [padding, start] = findPadding(before)
@@ -372,7 +416,52 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
372416
} else {
373417
insert(options.tab)
374418
}
419+
return;
375420
}
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))
376465
}
377466

378467
function handleUndoRedo(event: KeyboardEvent) {

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
hljs.highlightBlock(editor)
5656
}
5757

58-
const jar = CodeJar(editor, withLineNumbers(highlight))
58+
const jar = CodeJar(editor, withLineNumbers(highlight), {multilineIndentation: true, tab: ' '})
5959

6060
jar.updateCode(localStorage.getItem('code'))
6161
jar.onUpdate(code => {

0 commit comments

Comments
 (0)