// ==UserScript== // @name Editio // @name:zh-CN Editio // @namespace http://tampermonkey.net/ // @version 0.1.0 // @description Add some extra features to inputs and textareas // @description:zh-CN 给输入框和文本框添加一些额外功能 // @tag productivity // @author PRO-2684 // @match *://*/* // @run-at document-start // @icon https://github.com/PRO-2684/gadgets/raw/refs/heads/main/editio/editio.svg // @license gpl-3.0 // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_addValueChangeListener // @require https://update.greasyfork.icu/scripts/470224/1459364/Tampermonkey%20Config.js // @downloadURL none // ==/UserScript== (function () { const configDesc = { "$default": { autoClose: false }, pairing: { name: "🖇️ Pairing", title: "Pairing brackets and quotes", type: "folder", items: { autoClose: { name: "➕ Auto close", title: "Autoclose brackets and quotes (Similar to `editor.autoClosingBrackets` in VSCode)", type: "bool", value: true }, autoDelete: { name: "➖ Auto delete", title: "Remove adjacent closing quotes or brackets (Similar to `editor.autoClosingDelete` in VSCode)", type: "bool", value: true }, autoOvertype: { name: "🚫 Auto overtype", title: "Type over closing brackets - won't work for pairs with the same opening and closing characters (Similar to `editor.autoClosingOvertype` in VSCode)", type: "bool", value: false }, pairs: { name: "📜 Pairs", title: "A list of characters that should be paired", type: "str", value: "()[]{}<>\"\"''``", processor: (input) => { if (input.length % 2 !== 0) { throw new TypeError(`The length should be even, but got ${input.length}`); } return input; } } } } }; const config = new GM_config(configDesc); // Pairing /** * Pairs of characters we should consider. * @type {Record} */ let pairs = {}; /** * Reverse pairs of characters. * @type {Record} */ let reversePairs = {}; /** * Handle the InputEvent of type "insertText", so as to auto close and overtype on brackets and quotes * @param {InputEvent} e The InputEvent. */ function onInsertText(e) { /** * The input or textarea element that triggered the event. * @type {HTMLInputElement | HTMLTextAreaElement} */ const el = e.target; const { selectionStart: start, selectionEnd: end, value } = el; if ((e.data in pairs) && config.get("pairing.autoClose")) { // The input character is paired and autoClose feature is enabled e.preventDefault(); e.stopImmediatePropagation(); const wrapped = `${e.data}${value.substring(start, end)}${pairs[e.data]}`; document.execCommand("insertText", false, wrapped); // Wrap the selected text with the pair el.setSelectionRange(start + 1, end + 1); } else if ((e.data in reversePairs) && (start === end) && config.get("pairing.autoOvertype")) { // The input character is a closing one, nothing selected and autoOvertype feature is enabled const charBefore = value.charAt(start - 1); const charAfter = value.charAt(start); if (charBefore === reversePairs[e.data] && charAfter === e.data) { // The character before the cursor is the respective opening one and the character after the cursor is the same as the input character e.preventDefault(); e.stopImmediatePropagation(); el.setSelectionRange(start + 1, start + 1); // Move the cursor to the right } } } /** * Handle the InputEvent of type "deleteContentBackward", so as to auto delete the adjacent right bracket or quote * @param {InputEvent} e The InputEvent. */ function onBackspace(e) { const el = e.target; const { selectionStart: start, selectionEnd: end, value } = el; if (start === end && start > 0 && end < value.length) { const charBefore = value.charAt(start - 1); const charAfter = value.charAt(start); if (pairs[charBefore] === charAfter && config.get("pairing.autoDelete")) { e.preventDefault(); e.stopImmediatePropagation(); el.setSelectionRange(start - 1, start + 1); document.execCommand("delete"); } } } /** * Whether we should handle the InputEvent on the target. * @param {HTMLElement} target The target element. */ function validTarget(target) { // Only handle the InputEvent on input and textarea return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; } /** * Handlers for different types of InputEvent. * @type {Record void>} */ const inputHandlers = { "insertText": onInsertText, "deleteContentBackward": onBackspace, } /** * Handle the InputEvent. * @param {InputEvent} e The InputEvent. */ function onInput(e) { if (e.isComposing || e.defaultPrevented || !validTarget(e.target)) return; const handler = inputHandlers[e.inputType]; if (handler) handler(e); } // Set up document.addEventListener("beforeinput", onInput, { capture: false, passive: false }); /** * Prop-specific handlers for config changes. * @type {Record void>} */ const configChangeHandlers = { "pairing.pairs": (value) => { pairs = {}; reversePairs = {}; for (let i = 0; i < value.length; i += 2) { pairs[value.charAt(i)] = value.charAt(i + 1); reversePairs[value.charAt(i + 1)] = value.charAt(i); } } }; config.addEventListener("set", e => { const handler = configChangeHandlers[e.detail.prop]; if (handler) handler(e.detail.after); }); for (const [prop, handler] of Object.entries(configChangeHandlers)) { handler(config.get(prop)); } })();