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

Support Multiline Indentation on Chrome and Safari #3

Merged
merged 1 commit into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 92 additions & 3 deletions codejar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ type Options = {
moveToNewLine: RegExp
spellcheck: boolean
catchTab: boolean
/**
* Enabling multilineIndentation allows users to select blocks of
* text and indent (or dedent) them with the tab (or shift-tab) key.
* This setting is currently disabled by default.
*
* Note that this feature does not currently work with Firefox, and
* will be disabled automatically if the browser does not support it.
*/
multilineIndentation: boolean
preserveIdent: boolean
addClosing: boolean
history: boolean
Expand All @@ -32,6 +41,7 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
moveToNewLine: /^[)}\]]/,
spellcheck: false,
catchTab: true,
multilineIndentation: false,
preserveIdent: true,
addClosing: true,
history: true,
Expand Down Expand Up @@ -65,7 +75,11 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P

highlight(editor)
if (editor.contentEditable !== 'plaintext-only') isLegacy = true
if (isLegacy) editor.setAttribute('contenteditable', 'true')
if (isLegacy) {
editor.setAttribute('contenteditable', 'true')
// Disable multiline indentation if plaintext-only is not supported.
options.multilineIndentation = false
}

const debounceHighlight = debounce(() => {
const pos = save()
Expand Down Expand Up @@ -353,9 +367,39 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
}
}

/**
* Expands (or shrinks) a range by a given number of characters by adding
* (or removing) a given number of characters from the tail of the range.
* @param range The range to expand.
* @param additionalCharacters The number of characters to expand by.
* @returns A new range representing the original range expanded by the given
* number of characters (in the tail direction.)
* @description Passing a negative value for the `additionalCharacters`
* parameter will shrink the range instead of expanding it. This is by
* design.
*/
function inflateRange(range: Position, additionalCharacters: number) {
if (range.dir === '->') {
return { start: range.start, end: range.end + additionalCharacters, dir: range.dir }
} else {
return { start: range.start + additionalCharacters, end: range.end, dir: range.dir }
}
}

function handleTabCharacters(event: KeyboardEvent) {
if (event.key === 'Tab') {
preventDefault(event)
if (event.key !== 'Tab') {
return;
}

preventDefault(event);

// For standard tab behavior, simply allow the tab to be inserted (or
// removed.) This behavior could probably be combined with the multi-line
// behavior below but this is being left as-is for now to de-risk the
// multiline behavior change and maintain the previous behavior of the
// library by default.
const selection = getSelection();
if (!options.multilineIndentation || selection.getRangeAt(0).collapsed) {
if (event.shiftKey) {
const before = beforeCursor()
let [padding, start] = findPadding(before)
Expand All @@ -372,7 +416,52 @@ export function CodeJar(editor: HTMLElement, highlight: (e: HTMLElement, pos?: P
} else {
insert(options.tab)
}
return;
}

// For multi-line tab behavior, we indent or dedent the selected lines.
// Since this operation effects the entire line, we extend the selection
// to cover the entire line before proceeding.
// Firefox's support for calling .modify() on a selection is limited.
// It will only modify the user's original selection, and will not
// modify any selection that was created programmatically. So we have
// disabled this feature for Firefox until we can find a workaround.
selection.modify('extend', 'backward', 'lineboundary')
selection.modify('extend', 'forward', 'lineboundary')
const selectedText = selection.getRangeAt(0).toString()
const selectedLines = selectedText.split('\n')
const lineCount = selectedLines.length

const initialSelection = save()

let insertedCharacters = 0
// If the shift key is being held, it's a dedent request.
if (event.shiftKey) {
for (let i = 0; i < lineCount; i++) {
// We can only dedent lines that begin with some sort of whitespace.
// So we check for that first, and never consume more characters than
// there is whitespace.
const match = selectedLines[i].match(/^\s+/)
if (match !== null) {
const leadingSpace = match[0]
const originalLength = selectedLines[i].length
if (leadingSpace.length >= options.tab.length) {
selectedLines[i] = selectedLines[i].slice(options.tab.length)
} else if (leadingSpace.length > 0) {
selectedLines[i] = selectedLines[i].slice(leadingSpace.length)
}
insertedCharacters = insertedCharacters + (selectedLines[i].length - originalLength)
}
}
} else {
insertedCharacters = lineCount * options.tab.length
for (let i = 0; i < lineCount; i++) {
selectedLines[i] = options.tab + selectedLines[i]
}
}

insert(selectedLines.join('\n'))
restore(inflateRange(initialSelection, insertedCharacters))
}

function handleUndoRedo(event: KeyboardEvent) {
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
hljs.highlightBlock(editor)
}

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

jar.updateCode(localStorage.getItem('code'))
jar.onUpdate(code => {
Expand Down