Last active
July 5, 2021 19:29
-
-
Save francislavoie/fe50da4c00ba4b843b28be983e804b9a to your computer and use it in GitHub Desktop.
Violentmonkey: Tab key support for caddy.community
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Tab key - caddy.community | |
// @namespace Violentmonkey Scripts | |
// @match https://caddy.community/* | |
// @grant none | |
// @version 1.0 | |
// @author - | |
// @description 5/21/2020, 8:04:50 AM | |
// ==/UserScript== | |
// Set up the observer on DOM load | |
(function(f){ | |
if(document.readyState != "loading") { | |
f(); | |
} else { | |
document.addEventListener("DOMContentLoaded", f); | |
} | |
})(function(event) { | |
function indent(element) { | |
var selectionStart = element.selectionStart, | |
selectionEnd = element.selectionEnd, | |
value = element.value; | |
var selectedText = value.slice(selectionStart, selectionEnd); | |
// If there's no selection or no newlines in the selection, then just insert one tab | |
if (selectedText == "" || (/\n/g.exec(selectedText).length <= 0)) { | |
insertText(element, '\t'); | |
return; | |
} | |
// Select full first line to replace everything at once | |
var firstLineStart = value.lastIndexOf('\n', selectionStart - 1) + 1; | |
var newSelection = element.value.slice(firstLineStart, selectionEnd - 1); | |
var indentedText = newSelection.replace( | |
/^|\n/g, // Match all line starts | |
'$&\t' | |
); | |
var replacementsCount = indentedText.length - newSelection.length; | |
// Replace newSelection with indentedText | |
element.setSelectionRange(firstLineStart, selectionEnd - 1); | |
insertText(element, indentedText); | |
// Restore selection position, including the indentation | |
element.setSelectionRange(selectionStart + 1, selectionEnd + replacementsCount); | |
} | |
function findLineEnd(value, currentEnd) { | |
// Go to the beginning of the last line | |
var lastLineStart = value.lastIndexOf('\n', currentEnd - 1) + 1; | |
// There's nothing to unindent after the last cursor, so leave it as is | |
if (value.charAt(lastLineStart) !== '\t') { | |
return currentEnd; | |
} | |
return lastLineStart + 1; // Include the first character, which will be a tab | |
} | |
// The first line should always be unindented | |
// The last line should only be unindented if the selection includes any characters after `\n` | |
function unindent(element) { | |
var selectionStart = element.selectionStart, | |
selectionEnd = element.selectionEnd, | |
value = element.value; | |
// Select the whole first line because it might contain \t | |
var firstLineStart = value.lastIndexOf('\n', selectionStart - 1) + 1; | |
var minimumSelectionEnd = findLineEnd(value, selectionEnd); | |
var newSelection = element.value.slice(firstLineStart, minimumSelectionEnd); | |
var indentedText = newSelection.replace(/(^|\n)\t/g, '$1'); | |
var replacementsCount = newSelection.length - indentedText.length; | |
// Replace newSelection with indentedText | |
element.setSelectionRange(firstLineStart, minimumSelectionEnd); | |
insertText(element, indentedText); | |
// Restore selection position, including the indentation | |
var wasTheFirstLineUnindented = value.slice(firstLineStart, selectionStart).includes('\t'); | |
var newSelectionStart = selectionStart - Number(wasTheFirstLineUnindented); | |
element.setSelectionRange( | |
selectionStart - Number(wasTheFirstLineUnindented), | |
Math.max(newSelectionStart, selectionEnd - replacementsCount) | |
); | |
} | |
function insertTextFirefox(field, text) { | |
// Found on https://www.everythingfrontend.com/posts/insert-text-into-textarea-at-cursor-position.html :balloon: | |
field.setRangeText( | |
text, | |
field.selectionStart || 0, | |
field.selectionEnd || 0, | |
'end' // Without this, the cursor is either at the beginning or `text` remains selected | |
); | |
field.dispatchEvent(new InputEvent('input', { | |
data: text, | |
inputType: 'insertText', | |
isComposing: false // TODO: fix @types/jsdom, this shouldn't be required | |
})); | |
} | |
/** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */ | |
function insertText(field, text) { | |
var document = field.ownerDocument; | |
var initialFocus = document.activeElement; | |
if (initialFocus !== field) { | |
field.focus(); | |
} | |
if (!document.execCommand('insertText', false, text)) { | |
insertTextFirefox(field, text); | |
} | |
if (initialFocus === document.body) { | |
field.blur(); | |
} else if (initialFocus instanceof HTMLElement && initialFocus !== field) { | |
initialFocus.focus(); | |
} | |
} | |
/** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */ | |
function set(field, text) { | |
field.select(); | |
insertText(field, text); | |
} | |
/** Adds the `wrappingText` before and after field’s selection (or cursor). If `endWrappingText` is provided, it will be used instead of `wrappingText` at on the right. */ | |
function wrapSelection(field, wrap, wrapEnd) { | |
var selectionStart = field.selectionStart, | |
selectionEnd = field.selectionEnd; | |
var selection = field.value.slice(field.selectionStart, field.selectionEnd); | |
insertText(field, wrap + selection + (wrapEnd ? wrapEnd : wrap)); | |
// Restore the selection around the previously-selected text | |
field.selectionStart = selectionStart + wrap.length; | |
field.selectionEnd = selectionEnd + wrap.length; | |
} | |
/** Finds and replaces strings and regex in the field’s value, like `field.value = field.value.replace()` but better */ | |
function replace(field, searchValue, replacer) { | |
/** Remembers how much each match offset should be adjusted */ | |
var drift = 0; | |
field.value.replace(searchValue, function () { | |
var args = []; | |
for (var _i = 0; _i < arguments.length; _i++) { | |
args[_i - 0] = arguments[_i]; | |
} | |
// Select current match to replace it later | |
var matchStart = drift + (args[args.length - 2]); | |
var matchLength = args[0].length; | |
field.selectionStart = matchStart; | |
field.selectionEnd = matchStart + matchLength; | |
var replacement = typeof replacer === 'string' ? replacer : replacer.apply(void 0, args); | |
insertText(field, replacement); | |
// Select replacement. Without this, the cursor would be after the replacement | |
field.selectionStart = matchStart; | |
drift += replacement.length - matchLength; | |
return replacement; | |
}); | |
} | |
function keydownListener (e) { | |
if (e.defaultPrevented) { | |
return; | |
} | |
if (e.key === 'Tab') { | |
if (e.shiftKey) { | |
unindent(e.target); | |
} else { | |
indent(e.target); | |
} | |
e.preventDefault(); | |
return false; | |
} | |
} | |
const callback = function (mutationsList, observer) { | |
for (let mutation of mutationsList) { | |
// Only events where a node is added | |
if (mutation.type !== 'childList') continue; | |
// Only stuff in reply-control | |
if (mutation.target.id !== 'reply-control') continue; | |
// Get current text areas | |
for (let textarea of document.getElementsByTagName('textarea')) { | |
// Remove any existing events to not double up | |
textarea.removeEventListener('keydown', keydownListener); | |
// Add a listener for the tab key | |
textarea.addEventListener('keydown', keydownListener); | |
} | |
} | |
} | |
// Create an observer instance linked to the callback function | |
const observer = new MutationObserver(callback); | |
// Start observing the target node for configured mutations | |
observer.observe( | |
document.querySelector('body'), | |
{ childList: true, subtree: true } | |
); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment