Skip to content

Instantly share code, notes, and snippets.

@francislavoie
Last active July 5, 2021 19:29
Show Gist options
  • Save francislavoie/fe50da4c00ba4b843b28be983e804b9a to your computer and use it in GitHub Desktop.
Save francislavoie/fe50da4c00ba4b843b28be983e804b9a to your computer and use it in GitHub Desktop.
Violentmonkey: Tab key support for caddy.community
// ==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