diff --git a/code-input.d.ts b/code-input.d.ts index c31bd85..22b8eb9 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -76,6 +76,19 @@ export namespace plugins { constructor(); } + /** + * Automatically closes pairs of brackets/quotes/other syntaxes in code, but also lets you choose the brackets this + * is activated for. + * Files: auto-close-brackets.js + */ + class AutoCloseBrackets extends Plugin { + /** + * Create an auto-close brackets plugin to pass into a template + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}", '"': '"'}. All brackets must only be one character. + */ + constructor(bracketPairs: Object); + } + /** * Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions. * Files: autocomplete.js / autocomplete.css @@ -137,8 +150,9 @@ export namespace plugins { * Create an indentation plugin to pass into a template * @param {Boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false. * @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4. + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}"}. All brackets must only be one character, and this can be left as null to remove bracket-based indentation behaviour. */ - constructor(defaultSpaces?: boolean, numSpaces?: Number); + constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object); } /** diff --git a/plugins/README.md b/plugins/README.md index b841b2a..9c27dd8 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -7,6 +7,14 @@ --- +### Auto-Close Brackets +Automatically close pairs of brackets/quotes/other syntaxes in code, but also optionally choose the brackets this +is activated for. + +Files: [auto-close-brackets.js](./auto-close-brackets.js) + +[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/qBgGGKR) + ### Autocomplete Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions. @@ -36,7 +44,7 @@ Files: [go-to-line.js](./go-to-line.js) / [go-to-line.css](./go-to-line.css) [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/YzBMOXP) ### Indent -Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it possible to indent/unindent multiple lines using Tab/Shift+Tab. **Supports tab characters and custom numbers of spaces as indentation.** +Add indentation using the `Tab` key, and auto-indents after a newline, as well as making it possible to indent/unindent multiple lines using Tab/Shift+Tab. **Supports tab characters and custom numbers of spaces as indentation, as well as (optionally) brackets typed affecting indentation.** Files: [indent.js](./indent.js) diff --git a/plugins/auto-close-brackets.js b/plugins/auto-close-brackets.js new file mode 100644 index 0000000..54e20f5 --- /dev/null +++ b/plugins/auto-close-brackets.js @@ -0,0 +1,54 @@ +/** + * Automatically close pairs of brackets/quotes/other syntaxes in code, but also optionally choose the brackets this + * is activated for. + * Files: auto-close-brackets.js + */ +codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { + bracketPairs = []; + bracketsOpenedStack = []; // Each item [closing bracket string, opening bracket location] Innermost at right so can know which brackets should be ignored when retyped + + /** + * Create an auto-close brackets plugin to pass into a template + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}", '"': '"'}. All brackets must only be one character. + */ + constructor(bracketPairs={"(": ")", "[": "]", "{": "}", '"': '"'}) { + super([]); // No observed attributes + + this.bracketPairs = bracketPairs; + } + + /* Add keystroke events */ + afterElementsAdded(codeInput) { + let textarea = codeInput.textareaElement; + textarea.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) }); + textarea.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); }); + + } + + /* Event handlers */ + checkBrackets(codeInput, event) { + if(this.bracketsOpenedStack.length > 0 && event.data == this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][0] && event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) { + // "Retype" bracket, i.e. just move caret + codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd += 1; + this.bracketsOpenedStack.pop(); + event.preventDefault(); + } else if(event.data in this.bracketPairs) { + // Create bracket pair + let closingBracket = this.bracketPairs[event.data]; + this.bracketsOpenedStack.push([closingBracket, codeInput.textareaElement.selectionStart]); + document.execCommand("insertText", false, closingBracket); + codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd -= 1; + } + } + + checkBackspace(codeInput, event) { + if(event.key == "Backspace" && codeInput.textareaElement.selectionStart == codeInput.textareaElement.selectionEnd) { + if(this.bracketsOpenedStack.length > 0 && this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][1]+1 == codeInput.textareaElement.selectionStart && codeInput.textareaElement.value[codeInput.textareaElement.selectionStart] == this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][0]) { + // Delete closing bracket as well + codeInput.textareaElement.selectionEnd = codeInput.textareaElement.selectionStart + 1; + codeInput.textareaElement.selectionStart -= 1; + this.bracketsOpenedStack.pop(); + } + } + } +} \ No newline at end of file diff --git a/plugins/auto-close-brackets.min.js b/plugins/auto-close-brackets.min.js new file mode 100644 index 0000000..a8bf438 --- /dev/null +++ b/plugins/auto-close-brackets.min.js @@ -0,0 +1 @@ +codeInput.plugins.AutoCloseBrackets=class extends codeInput.Plugin{bracketPairs=[];bracketsOpenedStack=[];constructor(a={"(":")","[":"]","{":"}",'"':"\""}){super([]),this.bracketPairs=a}afterElementsAdded(a){let b=a.textareaElement;b.addEventListener("keydown",b=>{this.checkBackspace(a,b)}),b.addEventListener("beforeinput",b=>{this.checkBrackets(a,b)})}checkBrackets(a,b){if(0 { this.checkCtrlG(codeInput, event); }); + if(this.useCtrlG) { + textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); }); + } } blockSearch(dialog, event) { diff --git a/plugins/go-to-line.min.js b/plugins/go-to-line.min.js index 402979e..b7bc374 100644 --- a/plugins/go-to-line.min.js +++ b/plugins/go-to-line.min.js @@ -1 +1 @@ -codeInput.plugins.GoToLine=class extends codeInput.Plugin{constructor(){super([])}afterElementsAdded(a){const b=a.textareaElement;b.addEventListener("keydown",b=>{this.checkCtrlG(a,b)})}blockSearch(a,b){if(b.ctrlKey&&"g"==b.key)return b.preventDefault()}checkPrompt(a,b){const c=a.textarea.value.split("\n"),d=c.length,e=+a.input.value.split(":")[0];let f=0,g=1;const h=a.input.value.split(":");if(2e||0>f||e>d||f>g)return a.input.classList.add("error");a.input.classList.remove("error")}"Enter"==b.key&&(this.goTo(a.textarea,e,f),this.cancelPrompt(a,b))}cancelPrompt(a,b){let c;b.preventDefault(),a.textarea.focus(),a.classList.add("bye"),c=a.computedStyleMap?1e3*a.computedStyleMap().get("animation").toString().split("s")[0]:1e3*document.defaultView.getComputedStyle(a,null).getPropertyValue("animation").split("s")[0],setTimeout(()=>{a.codeInput.removeChild(a)},.9*c)}showPrompt(a){const b=a.textareaElement,c=document.createElement("div"),d=document.createElement("input"),e=document.createElement("span");c.appendChild(d),c.appendChild(e),c.className="code-input_go-to_dialog",d.spellcheck=!1,d.placeholder="Line:Column / Line no. then Enter",c.codeInput=a,c.textarea=b,c.input=d,d.addEventListener("keydown",a=>{this.blockSearch(c,a)}),d.addEventListener("keyup",a=>{this.checkPrompt(c,a)}),e.addEventListener("click",a=>{this.cancelPrompt(c,a)}),a.appendChild(c),d.focus()}goTo(a,b,c=0){let d,e,f,g,h=-1,i=a.value.split("\n");if(0{this.checkCtrlG(a,b)})}blockSearch(a,b){if(b.ctrlKey&&"g"==b.key)return b.preventDefault()}checkPrompt(a,b){const c=a.textarea.value.split("\n"),d=c.length,e=+a.input.value.split(":")[0];let f=0,g=1;const h=a.input.value.split(":");if(2e||0>f||e>d||f>g)return a.input.classList.add("error");a.input.classList.remove("error")}"Enter"==b.key&&(this.goTo(a.textarea,e,f),this.cancelPrompt(a,b))}cancelPrompt(a,b){let c;b.preventDefault(),a.textarea.focus(),a.classList.add("bye"),c=a.computedStyleMap?1e3*a.computedStyleMap().get("animation").toString().split("s")[0]:1e3*document.defaultView.getComputedStyle(a,null).getPropertyValue("animation").split("s")[0],setTimeout(()=>{a.codeInput.removeChild(a)},.9*c)}showPrompt(a){const b=a.textareaElement,c=document.createElement("div"),d=document.createElement("input"),e=document.createElement("span");c.appendChild(d),c.appendChild(e),c.className="code-input_go-to_dialog",d.spellcheck=!1,d.placeholder="Line:Column / Line no. then Enter",c.codeInput=a,c.textarea=b,c.input=d,d.addEventListener("keydown",a=>{this.blockSearch(c,a)}),d.addEventListener("keyup",a=>{this.checkPrompt(c,a)}),e.addEventListener("click",a=>{this.cancelPrompt(c,a)}),a.appendChild(c),d.focus()}goTo(a,b,c=0){let d,e,f,g,h=-1,i=a.value.split("\n");if(0 { this.checkTab(codeInput, event); this.checkEnter(codeInput, event); this.checkBackspace(codeInput, event); }); + textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); }); } /* Event handlers */ @@ -135,6 +139,39 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { lines[currentLineI] = lines[currentLineI].substring(0, cursorPosInLine); } + let bracketThreeLinesTriggered = false; + let furtherIndentation = ""; + if(this.bracketPairs != null) { + for(let openingBracket in this.bracketPairs) { + if(lines[currentLineI][lines[currentLineI].length-1] == openingBracket) { + let closingBracket = this.bracketPairs[openingBracket]; + if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) { + // Create new line and then put textAfterCursor on yet another line: + // { + // |CARET| + // } + bracketThreeLinesTriggered = true; + for (let i = 0; i < numberIndents+1; i++) { + furtherIndentation += this.indentation; + } + } else { + // Just create new line: + // { + // |CARET| + numberIndents++; + } + break; + } else { + // Check whether brackets cause unindent + let closingBracket = this.bracketPairs[openingBracket]; + if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) { + numberIndents--; + break; + } + } + } + } + // insert our indents and any text from the previous line that might have been after the line break for (let i = 0; i < numberIndents; i++) { newLine += this.indentation; @@ -143,6 +180,10 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { // save the current cursor position let selectionStartI = inputElement.selectionStart; + if(bracketThreeLinesTriggered) { + document.execCommand("insertText", false, "\n" + furtherIndentation); // Write indented line + numberIndents += 1; // Reflects the new indent + } document.execCommand("insertText", false, "\n" + newLine); // Write new line, including auto-indentation // move cursor to new position @@ -175,4 +216,22 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { document.execCommand("delete", false, ""); } } + + checkCloseBracket(codeInput, event) { + if(codeInput.textareaElement.selectionStart != codeInput.textareaElement.selectionEnd) { + return; + } + + for(let openingBracket in this.bracketPairs) { + let closingBracket = this.bracketPairs[openingBracket]; + if(event.data == closingBracket) { + // Closing bracket unindents line + if(codeInput.value.substring(codeInput.textareaElement.selectionStart - this.indentationNumChars, codeInput.textareaElement.selectionStart) == this.indentation) { + // Indentation before cursor = delete it + codeInput.textareaElement.selectionStart -= this.indentationNumChars; + document.execCommand("delete", false, ""); + } + } + } + } } \ No newline at end of file diff --git a/plugins/indent.min.js b/plugins/indent.min.js index a37ddaa..26de329 100644 --- a/plugins/indent.min.js +++ b/plugins/indent.min.js @@ -1 +1 @@ -codeInput.plugins.Indent=class extends codeInput.Plugin{numSpaces;indentation="\t";indentationNumChars=1;constructor(a=!1,b=4){if(super([]),this.numSpaces=b,a){this.indentation="";for(let a=0;a{this.checkTab(a,b),this.checkEnter(a,b),this.checkBackspace(a,b)})}checkTab(a,b){var c=Math.max;if("Tab"==b.key){let d=a.textareaElement;if(b.preventDefault(),!b.shiftKey&&d.selectionStart==d.selectionEnd)document.execCommand("insertText",!1,this.indentation);else{let a=d.value.split("\n"),e=0,f=d.selectionStart,g=d.selectionEnd;for(let h=0;h=e+1||f==g&&f<=e+a[h].length+1&&g>=e)&&(b.shiftKey?a[h].substring(0,this.indentationNumChars)==this.indentation&&(d.selectionStart=e,d.selectionEnd=e+this.indentationNumChars,document.execCommand("delete",!1,""),f>e&&(f=c(f-this.indentationNumChars,e)),g-=this.indentationNumChars,e-=this.indentationNumChars):(d.selectionStart=e,d.selectionEnd=e,document.execCommand("insertText",!1,this.indentation),f>e&&(f+=this.indentationNumChars),g+=this.indentationNumChars,e+=this.indentationNumChars)),e+=a[h].length+1;d.selectionStart=f,d.selectionEnd=g}a.update(d.value)}}checkEnter(a,b){if("Enter"!=b.key)return;b.preventDefault();let c=a.querySelector("textarea"),d=c.value.split("\n"),e=0,f=d.length-1,g="",h=0;for(let g=0;g=c.scrollTop+o&&c.scrollBy(0,+getComputedStyle(c).lineHeight.replace("px","")),a.update(c.value)}checkBackspace(a,b){if("Backspace"==b.key&&1!=this.indentationNumChars){let c=a.textareaElement;c.selectionStart==c.selectionEnd&&a.value.substring(c.selectionStart-this.indentationNumChars,c.selectionStart)==this.indentation&&(c.selectionStart-=this.indentationNumChars,b.preventDefault(),document.execCommand("delete",!1,""))}}}; \ No newline at end of file +codeInput.plugins.Indent=class extends codeInput.Plugin{numSpaces;bracketPairs=null;indentation="\t";indentationNumChars=1;constructor(a=!1,b=4,c={"(":")","[":"]","{":"}"}){if(super([]),this.numSpaces=b,this.bracketPairs=c,a){this.indentation="";for(let a=0;a{this.checkTab(a,b),this.checkEnter(a,b),this.checkBackspace(a,b)}),b.addEventListener("beforeinput",b=>{this.checkCloseBracket(a,b)})}checkTab(a,b){var c=Math.max;if("Tab"==b.key){let d=a.textareaElement;if(b.preventDefault(),!b.shiftKey&&d.selectionStart==d.selectionEnd)document.execCommand("insertText",!1,this.indentation);else{let a=d.value.split("\n"),e=0,f=d.selectionStart,g=d.selectionEnd;for(let h=0;h=e+1||f==g&&f<=e+a[h].length+1&&g>=e)&&(b.shiftKey?a[h].substring(0,this.indentationNumChars)==this.indentation&&(d.selectionStart=e,d.selectionEnd=e+this.indentationNumChars,document.execCommand("delete",!1,""),f>e&&(f=c(f-this.indentationNumChars,e)),g-=this.indentationNumChars,e-=this.indentationNumChars):(d.selectionStart=e,d.selectionEnd=e,document.execCommand("insertText",!1,this.indentation),f>e&&(f+=this.indentationNumChars),g+=this.indentationNumChars,e+=this.indentationNumChars)),e+=a[h].length+1;d.selectionStart=f,d.selectionEnd=g}a.update(d.value)}}checkEnter(a,b){if("Enter"!=b.key)return;b.preventDefault();let c=a.querySelector("textarea"),d=c.value.split("\n"),e=0,f=d.length-1,g="",h=0;for(let g=0;g=c.scrollTop+q&&c.scrollBy(0,+getComputedStyle(c).lineHeight.replace("px","")),a.update(c.value)}checkBackspace(a,b){if("Backspace"==b.key&&1!=this.indentationNumChars){let c=a.textareaElement;c.selectionStart==c.selectionEnd&&a.value.substring(c.selectionStart-this.indentationNumChars,c.selectionStart)==this.indentation&&(c.selectionStart-=this.indentationNumChars,b.preventDefault(),document.execCommand("delete",!1,""))}}checkCloseBracket(a,b){if(a.textareaElement.selectionStart==a.textareaElement.selectionEnd)for(let c in this.bracketPairs){let d=this.bracketPairs[c];b.data==d&&a.value.substring(a.textareaElement.selectionStart-this.indentationNumChars,a.textareaElement.selectionStart)==this.indentation&&(a.textareaElement.selectionStart-=this.indentationNumChars,document.execCommand("delete",!1,""))}}}; \ No newline at end of file