@webcoder49/code-input 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ code-input{position:relative;top:0;left:0;display:block;overflow:hidden;padding:8px;margin:0!important;width:calc(100% - 16px);height:250px;font-size:normal;font-family:monospace;line-height:1.5;tab-size:2;caret-color:#a9a9a9;white-space:pre}code-input textarea,code-input.code-input_pre-element-styled pre,code-input:not(.code-input_pre-element-styled) pre code{margin:0!important;padding:var(--padding,16px)!important;border:0;width:calc(100% - var(--padding,16px) * 2);height:calc(100% - var(--padding,16px) * 2)}code-input.code-input_pre-element-styled pre code,code-input:not(.code-input_pre-element-styled) pre{margin:0!important;padding:0!important;width:100%;height:100%}code-input pre,code-input pre *,code-input textarea{font-size:inherit!important;font-family:inherit!important;line-height:inherit!important;tab-size:inherit!important}code-input pre,code-input textarea{position:absolute;top:0;left:0}code-input textarea{z-index:1}code-input pre{z-index:0}code-input textarea{color:transparent;background:0 0;caret-color:inherit!important}code-input pre,code-input textarea{overflow:auto!important;white-space:inherit;word-spacing:normal;word-break:normal;word-wrap:normal}code-input textarea{resize:none;outline:0!important}code-input:not(.code-input_registered)::before{content:"Use codeInput.registerTemplate to set up.";display:block;color:grey}
@@ -0,0 +1 @@
1
+ var codeInput={observedAttributes:["value","name","placeholder","lang","template"],usedTemplates:{},defaultTemplate:void 0,templateQueue:{},plugins:{},Plugin:class{constructor(){console.log("code-input: plugin: Created plugin!"),codeInput.observedAttributes=codeInput.observedAttributes.concat(self.observedAttributes)}beforeHighlight(){}afterHighlight(){}beforeElementsAdded(){}afterElementsAdded(){}attributeChanged(){}observedAttributes=[]},CodeInput:class extends HTMLElement{constructor(){super()}bound_callbacks={};plugin_evt(a,b){for(let c in this.template.plugins){let d=this.template.plugins[c];a in d&&(b===void 0?d[a](this):d[a](this,...b))}}update(a){if(!this.ignoreValueUpdate){this.ignoreValueUpdate=!0,this.value=a,this.ignoreValueUpdate=!1,this.querySelector("textarea").value!=a&&(this.querySelector("textarea").value=a);let b=this.querySelector("pre code");"\n"==a[a.length-1]&&(a+=" "),b.innerHTML=this.escape_html(a),this.plugin_evt("beforeHighlight"),this.template.includeCodeInputInHighlightFunc?this.template.highlight(b,this):this.template.highlight(b),this.plugin_evt("afterHighlight")}}sync_scroll(){let a=this.querySelector("textarea"),b=this.template.preElementStyled?this.querySelector("pre"):this.querySelector("pre code");b.scrollTop=a.scrollTop,b.scrollLeft=a.scrollLeft}escape_html(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;")}get_template(){let a;return a=null==this.getAttribute("template")?codeInput.defaultTemplate:this.getAttribute("template"),a in codeInput.usedTemplates?codeInput.usedTemplates[a]:(a in codeInput.templateQueue||(codeInput.templateQueue[a]=[]),void codeInput.templateQueue[a].push(this))}setup(){this.classList.add("code-input_registered"),this.template.preElementStyled&&this.classList.add("code-input_pre-element-styled"),this.plugin_evt("beforeElementsAdded");let a=this.getAttribute("lang"),b=this.getAttribute("placeholder")||this.getAttribute("lang")||"",c=this.value||this.innerHTML||"";this.innerHTML="";let d=document.createElement("textarea");d.placeholder=b,d.value=c,d.setAttribute("spellcheck","false"),this.getAttribute("name")&&d.setAttribute("name",this.getAttribute("name")),d.addEventListener("input",()=>{d.parentElement.update(d.value),d.parentElement.sync_scroll()}),d.addEventListener("scroll",()=>d.parentElement.sync_scroll()),this.append(d);let e=document.createElement("code"),f=document.createElement("pre");f.setAttribute("aria-hidden","true"),f.append(e),this.append(f),this.template.isCode&&a!=null&&""!=a&&e.classList.add("language-"+a),this.plugin_evt("afterElementsAdded"),this.update(c,this)}connectedCallback(){this.template=this.get_template(),this.template!=null&&this.setup()}static get observedAttributes(){return codeInput.observedAttributes}attributeChangedCallback(a,b,c){if(this.isConnected)switch(this.plugin_evt("attributeChanged",[a,b,c]),a){case"value":this.update(c);break;case"name":null!==this.querySelector("textarea")&&this.querySelector("textarea").setAttribute("name",c);break;case"placeholder":this.querySelector("textarea").placeholder=c;break;case"template":this.template=codeInput.usedTemplates[c||codeInput.defaultTemplate],this.template.preElementStyled?this.classList.add("code-input_pre-element-styled"):this.classList.remove("code-input_pre-element-styled"),this.update(this.value);break;case"lang":let a=this.querySelector("pre code"),d=this.querySelector("textarea");if(null!=c&&(c=c.toLowerCase(),a.classList.contains(`language-${c}`)))break;b=b.toLowerCase(),console.log("code-input: Language: REMOVE","language-"+b),a.classList.remove("language-"+b),a.parentElement.classList.remove("language-"+b),a.classList.remove("language-none"),a.parentElement.classList.remove("language-none"),null!=c&&""!=c&&(a.classList.add("language-"+c),console.log("code-input: Language:ADD","language-"+c)),d.placeholder==b&&(d.placeholder=c),this.update(this.value)}}addEventListener(a,b,c=null){let d=b.bind(this);this.bound_callbacks[b]=d,"change"==a?null===c?this.querySelector("textarea").addEventListener("change",d):this.querySelector("textarea").addEventListener("change",d,c):"selectionchange"==a&&(null===c?this.querySelector("textarea").addEventListener("selectionchange",d):this.querySelector("textarea").addEventListener("selectionchange",d,c))}removeEventListener(a,b,c=null){let d=this.bound_callbacks[b];"change"==a?null===c?this.querySelector("textarea").removeEventListener("change",d):this.querySelector("textarea").removeEventListener("change",d,c):"selectionchange"==a&&(null===c?this.querySelector("textarea").removeEventListener("selectionchange",d):this.querySelector("textarea").removeEventListener("selectionchange",d,c))}get value(){return this.getAttribute("value")}set value(a){return this.setAttribute("value",a)}get placeholder(){return this.getAttribute("placeholder")}set placeholder(a){return this.setAttribute("placeholder",a)}pluginData={}},registerTemplate:function(a,b){if(codeInput.usedTemplates[a]=b,a in codeInput.templateQueue){for(let c in codeInput.templateQueue[a])elem=codeInput.templateQueue[a][c],elem.template=b,elem.setup();console.log(`code-input: template: Added existing elements with template ${a}`)}if(null==codeInput.defaultTemplate){if(codeInput.defaultTemplate=a,void 0 in codeInput.templateQueue)for(let a in codeInput.templateQueue[void 0])elem=codeInput.templateQueue[void 0][a],elem.template=b,elem.setup();console.log(`code-input: template: Set template ${a} as default`)}console.log(`code-input: template: Created template ${a}`)},templates:{custom(a=function(){},b=!0,c=!0,d=!1,e=[]){return{highlight:a,includeCodeInputInHighlightFunc:d,preElementStyled:b,isCode:c,plugins:e}},prism(a,b=[]){return{includeCodeInputInHighlightFunc:!1,highlight:a.highlightElement,preElementStyled:!0,isCode:!0,plugins:b}},hljs(a,b=[]){return{includeCodeInputInHighlightFunc:!1,highlight:a.highlightElement,preElementStyled:!1,isCode:!0,plugins:b}},characterLimit(){return{highlight:function(a,b,c=[]){let d=+b.getAttribute("data-character-limit"),e=b.escape_html(b.value.slice(0,d)),f=b.escape_html(b.value.slice(d));a.innerHTML=`${e}<mark class="overflow">${f}</mark>`,0<f.length&&(a.innerHTML+=` <mark class="overflow-msg">${b.getAttribute("data-overflow-msg")||"(Character limit reached)"}</mark>`)},includeCodeInputInHighlightFunc:!0,preElementStyled:!0,isCode:!1,plugins:plugins}},rainbowText(a=["red","orangered","orange","goldenrod","gold","green","darkgreen","navy","blue","magenta"],b="",c=[]){return{highlight:function(a,b){let c=[],d=b.value.split(b.template.delimiter);for(let e=0;e<d.length;e++)c.push(`<span style="color: ${b.template.rainbow_colors[e%b.template.rainbow_colors.length]}">${b.escape_html(d[e])}</span>`);a.innerHTML=c.join(b.template.delimiter)},includeCodeInputInHighlightFunc:!0,preElementStyled:!0,isCode:!1,rainbow_colors:a,delimiter:b,plugins:c}}}};customElements.define("code-input",codeInput.CodeInput);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@webcoder49/code-input",
3
+ "version": "1.5.0",
4
+ "description": "Fully customisable, editable syntax-highlighted textareas.",
5
+ "browser": "code-input.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/WebCoder49/code-input.git"
12
+ },
13
+ "keywords": [
14
+ "front-end",
15
+ "syntax",
16
+ "highlight",
17
+ "textarea",
18
+ "editable",
19
+ "web-components"
20
+ ],
21
+ "author": {
22
+ "name": "WebCoder49",
23
+ "email": "webcoder49.tutorials@gmail.com",
24
+ "url": "https://webcoder49.github.io/"
25
+ },
26
+ "license": "MIT",
27
+ "bugs": {
28
+ "url": "https://github.com/WebCoder49/code-input/issues"
29
+ },
30
+ "homepage": "https://github.com/WebCoder49/code-input#readme"
31
+ }
@@ -0,0 +1,68 @@
1
+ # Code-input: Plugins
2
+ ## List Of Plugins
3
+
4
+ ### Autocomplete
5
+ Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions.
6
+
7
+ Files: [autocomplete.js](./autocomplete.js) / [autocomplete.css](./autocomplete.css)
8
+
9
+ [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/xxapjXB)
10
+
11
+ ### Autodetect
12
+ Autodetect the language live and change the `lang` attribute using the syntax highlighter's autodetect capabilities. Works with highlight.js.
13
+
14
+ Files: [autodetect.js](./autodetect.js)
15
+
16
+ [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/eYLyMae)
17
+
18
+ ### Debounce Update
19
+ Debounce the update and highlighting function ([What is Debouncing?](https://medium.com/@jamischarles/what-is-debouncing-2505c0648ff1))
20
+
21
+ Files: [debounce-update.js](./debounce-update.js)
22
+
23
+ [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/GRXyxzV)
24
+
25
+ ### Indent
26
+ 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
27
+
28
+ Files: [indent.js](./indent.js)
29
+
30
+ [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/WNgdzar)
31
+
32
+ ### Prism Line Numbers
33
+ Allows code-input elements to be used with the Prism.js line-numbers plugin, as long as the code-input element or a parent element of it has the CSS class `line-numbers`. [Prism.js Plugin Docs](https://prismjs.com/plugins/line-numbers/)
34
+
35
+ Files: [prism-line-numbers.css](./prism-line-numbers.css) (NO JS FILE)
36
+
37
+ [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/XWPVrWv)
38
+
39
+ ### Special Chars
40
+ Render special characters and control characters as a symbol
41
+ with their hex code.
42
+
43
+ Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-chars.css)
44
+
45
+ [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm)
46
+
47
+ ## Using Plugins
48
+ Plugins allow you to add extra features to a template, like [automatic indentation](./indent.js) or [support for highlight.js's language autodetection](./autodetect.js). To use them, just:
49
+ - Import the plugins' JS/CSS files (there may only be one of these; import all of the files that exist) after you have imported `code-input` and before registering the template.
50
+ - If a JavaScript file is present, Place an instance of each plugin in the array of plugins argument when registering, like this:
51
+ ```html
52
+ <script src="code-input.js"></script>
53
+ <!--...-->
54
+ <script src="plugins/autodetect.js"></script>
55
+ <script src="plugins/indent.js"></script>
56
+ <!--...-->
57
+ <script>
58
+ codeInput.registerTemplate("syntax-highlighted",
59
+ codeInput.templates.hljs(
60
+ hljs,
61
+ [
62
+ new codeInput.plugins.Autodetect(),
63
+ new codeInput.plugins.Indent()
64
+ ]
65
+ )
66
+ );
67
+ </script>
68
+ ```
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions.
3
+ * Files: autocomplete.js / autocomplete.css
4
+ */
5
+ code-input .code-input_autocomplete_popup {
6
+ display: block;
7
+ position: absolute;
8
+ margin-top: 1em; /* Popup shows under the caret */
9
+ z-index: 100;
10
+ }
11
+
12
+
13
+ code-input .code-input_autocomplete_testpos {
14
+ opacity: 0;
15
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions.
3
+ * Files: autocomplete.js / autocomplete.css
4
+ */
5
+ codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
6
+ /**
7
+ * Pass in a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
8
+ * @param {function} updatePopupCallback
9
+ */
10
+ constructor(updatePopupCallback) {
11
+ super();
12
+ this.updatePopupCallback = updatePopupCallback;
13
+ }
14
+ /* When a key is pressed, or scrolling occurs, update the autocomplete */
15
+ updatePopup(codeInput, onlyScrolled) {
16
+ let textarea = codeInput.querySelector("textarea");
17
+ let caretCoords = this.getCaretCoordinates(codeInput, textarea, textarea.selectionEnd, onlyScrolled);
18
+ let popupElem = codeInput.querySelector(".code-input_autocomplete_popup");
19
+ popupElem.style.top = caretCoords.top + "px";
20
+ popupElem.style.left = caretCoords.left + "px";
21
+
22
+ if(!onlyScrolled) {
23
+ this.updatePopupCallback(popupElem, textarea, textarea.selectionEnd);
24
+ }
25
+ }
26
+ /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
27
+ afterElementsAdded(codeInput) {
28
+ let popupElem = document.createElement("div");
29
+ popupElem.classList.add("code-input_autocomplete_popup");
30
+ codeInput.appendChild(popupElem);
31
+
32
+ let testPosElem = document.createElement("pre");
33
+ testPosElem.classList.add("code-input_autocomplete_testpos");
34
+ codeInput.appendChild(testPosElem); // Styled like first pre, but first pre found to update
35
+
36
+ let textarea = codeInput.querySelector("textarea");
37
+ textarea.addEventListener("keyup", this.updatePopup.bind(this, codeInput, false)); // Override this+args in bind - not just scrolling
38
+ textarea.addEventListener("click", this.updatePopup.bind(this, codeInput, false)); // Override this+args in bind - not just scrolling
39
+ textarea.addEventListener("scroll", this.updatePopup.bind(this, codeInput, true)); // Override this+args in bind - just scrolling
40
+ }
41
+ /**
42
+ * Return the coordinates of the caret in a code-input
43
+ * @param {codeInput.CodeInput} codeInput
44
+ * @param {HTMLElement} textarea
45
+ * @param {Number} charIndex
46
+ * @param {boolean} onlyScrolled True if no edits have been made to the text and the caret hasn't been repositioned
47
+ * @returns
48
+ */
49
+ getCaretCoordinates(codeInput, textarea, charIndex, onlyScrolled) {
50
+ let afterSpan;
51
+ if(onlyScrolled) {
52
+ // No edits to text; don't update element - span at index 1 is after span
53
+ let spans = codeInput.querySelector(".code-input_autocomplete_testpos").querySelectorAll("span");
54
+ if(spans.length < 2) {
55
+ // Hasn't saved text in test pre to find pos
56
+ // Need to regenerate text in test pre
57
+ return this.getCaretCoordinates(codeInput, textarea, charIndex, false);
58
+ }
59
+ afterSpan = spans[1];
60
+ } else {
61
+ /* Inspired by https://github.com/component/textarea-caret-position */
62
+ let testPosElem = codeInput.querySelector(".code-input_autocomplete_testpos");
63
+
64
+ let beforeSpan = document.createElement("span");
65
+ beforeSpan.textContent = textarea.value.substring(0, charIndex);
66
+ afterSpan = document.createElement("span");
67
+ afterSpan.textContent = "."; // Placeholder
68
+
69
+ // Clear test pre - https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
70
+ while (testPosElem.firstChild) {
71
+ testPosElem.removeChild(testPosElem.firstChild);
72
+ }
73
+ testPosElem.appendChild(beforeSpan);
74
+ testPosElem.appendChild(afterSpan);
75
+ }
76
+ return {"top": afterSpan.offsetTop - textarea.scrollTop, "left": afterSpan.offsetLeft - textarea.scrollLeft};
77
+ }
78
+ observedAttributes = [];
79
+ updatePopupCallback = function() {};
80
+ }
@@ -0,0 +1 @@
1
+ code-input .code-input_autocomplete_popup{display:block;position:absolute;margin-top:1em;z-index:100}code-input .code-input_autocomplete_testpos{opacity:0}
@@ -0,0 +1 @@
1
+ codeInput.plugins.Autocomplete=class extends codeInput.Plugin{constructor(a){super(),this.updatePopupCallback=a}updatePopup(a,b){let c=a.querySelector("textarea"),d=this.getCaretCoordinates(a,c,c.selectionEnd,b),e=a.querySelector(".code-input_autocomplete_popup");e.style.top=d.top+"px",e.style.left=d.left+"px",b||this.updatePopupCallback(e,c,c.selectionEnd)}afterElementsAdded(a){let b=document.createElement("div");b.classList.add("code-input_autocomplete_popup"),a.appendChild(b);let c=document.createElement("pre");c.classList.add("code-input_autocomplete_testpos"),a.appendChild(c);let d=a.querySelector("textarea");d.addEventListener("keyup",this.updatePopup.bind(this,a,!1)),d.addEventListener("click",this.updatePopup.bind(this,a,!1)),d.addEventListener("scroll",this.updatePopup.bind(this,a,!0))}getCaretCoordinates(a,b,c,d){let e;if(d){let d=a.querySelector(".code-input_autocomplete_testpos").querySelectorAll("span");if(2>d.length)return this.getCaretCoordinates(a,b,c,!1);e=d[1]}else{let d=a.querySelector(".code-input_autocomplete_testpos"),f=document.createElement("span");for(f.textContent=b.value.substring(0,c),e=document.createElement("span"),e.textContent=".";d.firstChild;)d.removeChild(d.firstChild);d.appendChild(f),d.appendChild(e)}return{top:e.offsetTop-b.scrollTop,left:e.offsetLeft-b.scrollLeft}}observedAttributes=[];updatePopupCallback=function(){}};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Autodetect the language live and change the `lang` attribute using the syntax highlighter's
3
+ * autodetect capabilities. Works with highlight.js.
4
+ * Files: autodetect.js
5
+ */
6
+ codeInput.plugins.Autodetect = class extends codeInput.Plugin {
7
+ constructor() {
8
+ super();
9
+ }
10
+ /* Remove previous language class */
11
+ beforeHighlight(codeInput) {
12
+ let result_element = codeInput.querySelector("pre code");
13
+ result_element.className = ""; // CODE
14
+ result_element.parentElement.className = ""; // PRE
15
+ }
16
+ /* Get new language class and set `lang` attribute */
17
+ afterHighlight(codeInput) {
18
+ let result_element = codeInput.querySelector("pre code");
19
+ let lang_class = result_element.className || result_element.parentElement.className;
20
+ let lang = lang_class.match(/lang(\w|-)*/i)[0]; // Get word starting with lang...; Get outer bracket
21
+ lang = lang.split("-")[1];
22
+ if(lang == "undefined") {
23
+ codeInput.removeAttribute("lang");
24
+ } else {
25
+ codeInput.setAttribute("lang", lang);
26
+ }
27
+ }
28
+ observedAttributes = []
29
+ }
@@ -0,0 +1 @@
1
+ codeInput.plugins.Autodetect=class extends codeInput.Plugin{constructor(){super()}beforeHighlight(a){let b=a.querySelector("pre code");b.className="",b.parentElement.className=""}afterHighlight(a){let b=a.querySelector("pre code"),c=b.className||b.parentElement.className,d=c.match(/lang(\w|-)*/i)[0];d=d.split("-")[1],"undefined"==d?a.removeAttribute("lang"):a.setAttribute("lang",d)}observedAttributes=[]};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Debounce the update and highlighting function
3
+ * https://medium.com/@jamischarles/what-is-debouncing-2505c0648ff1
4
+ * Files: debounce-update.js
5
+ */
6
+ codeInput.plugins.DebounceUpdate = class extends codeInput.Plugin {
7
+ /**
8
+ * Create a debounced update plugin to pass into a template
9
+ * @param {Number} delayMs Delay, in ms, to wait until updating the syntax highlighting
10
+ */
11
+ constructor(delayMs) {
12
+ super();
13
+ this.delayMs = delayMs;
14
+ }
15
+ /* Runs before elements are added into a `code-input`; Params: codeInput element) */
16
+ beforeElementsAdded(codeInput) {
17
+ console.log(codeInput, "before elements added");
18
+ this.update = codeInput.update.bind(codeInput); // Save previous update func
19
+ codeInput.update = this.updateDebounced.bind(this, codeInput);
20
+ }
21
+
22
+ /**
23
+ * Debounce the `update` function
24
+ */
25
+ updateDebounced(codeInput, text) {
26
+ // Editing - cancel prev. timeout
27
+ if(this.debounceTimeout != null) {
28
+ window.clearTimeout(this.debounceTimeout);
29
+ }
30
+
31
+ this.debounceTimeout = window.setTimeout(() => {
32
+ // Closure arrow function can take in variables like `text`
33
+ this.update(text);
34
+ }, this.delayMs);
35
+ }
36
+
37
+ // this.`update` function is original function
38
+
39
+ debounceTimeout = null; // Timeout until update
40
+ delayMs = 0; // Time until update
41
+ }
@@ -0,0 +1 @@
1
+ codeInput.plugins.DebounceUpdate=class extends codeInput.Plugin{constructor(a){super(),this.delayMs=a}beforeElementsAdded(a){console.log(a,"before elements added"),this.update=a.update.bind(a),a.update=this.updateDebounced.bind(this,a)}updateDebounced(a,b){null!=this.debounceTimeout&&window.clearTimeout(this.debounceTimeout),this.debounceTimeout=window.setTimeout(()=>{this.update(b)},this.delayMs)}debounceTimeout=null;delayMs=0};
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it
3
+ * possible to indent/unindent multiple lines using Tab/Shift+Tab
4
+ * Files: indent.js
5
+ */
6
+ codeInput.plugins.Indent = class extends codeInput.Plugin {
7
+ constructor() {
8
+ super();
9
+ }
10
+
11
+ /* Add keystroke events */
12
+ afterElementsAdded(codeInput) {
13
+ let textarea = codeInput.querySelector("textarea");
14
+ textarea.addEventListener('keydown', (event) => { this.check_tab(codeInput, event); this.check_enter(codeInput, event); });
15
+ }
16
+
17
+ /* Event handlers */
18
+ check_tab(codeInput, event) {
19
+ if(event.key != "Tab") {
20
+ return;
21
+ }
22
+ let input_element = codeInput.querySelector("textarea");
23
+ let code = input_element.value;
24
+ event.preventDefault(); // stop normal
25
+
26
+ if(!event.shiftKey && input_element.selectionStart == input_element.selectionEnd) {
27
+ // Just place a tab here.
28
+ document.execCommand("insertText", false, "\t");
29
+
30
+ } else {
31
+ let lines = input_element.value.split("\n");
32
+ let letter_i = 0;
33
+
34
+ let selection_start = input_element.selectionStart; // where cursor moves after tab - moving forward by 1 indent
35
+ let selection_end = input_element.selectionEnd; // where cursor moves after tab - moving forward by 1 indent
36
+
37
+ let number_indents = 0;
38
+ let first_line_indents = 0;
39
+
40
+ for (let i = 0; i < lines.length; i++) {
41
+ // console.log(lines[i], ": start", selection_start, letter_i + lines[i].length + 1, "&& end", selection_end , letter_i + 1)
42
+ if((selection_start <= letter_i+lines[i].length && selection_end >= letter_i + 1)
43
+ || (selection_start == selection_end && selection_start <= letter_i+lines[i].length+1 && selection_end >= letter_i)) { // + 1 so newlines counted
44
+ // Starts before or at last char and ends after or at first char
45
+ if(event.shiftKey) {
46
+ if(lines[i][0] == "\t") {
47
+ // Remove first tab
48
+ input_element.selectionStart = letter_i;
49
+ input_element.selectionEnd = letter_i+1;
50
+ document.execCommand("delete", false, "");
51
+
52
+ // Change selection
53
+ if(selection_start > letter_i) { // Indented outside selection
54
+ selection_start--;
55
+ }
56
+ selection_end--;
57
+ letter_i--;
58
+ }
59
+ } else {
60
+ // Add tab at start
61
+ input_element.selectionStart = letter_i;
62
+ input_element.selectionEnd = letter_i;
63
+ document.execCommand("insertText", false, "\t");
64
+
65
+ // Change selection
66
+ if(selection_start > letter_i) { // Indented outside selection
67
+ selection_start++;
68
+ }
69
+ selection_end++;
70
+ letter_i++;
71
+ }
72
+ }
73
+
74
+ letter_i += lines[i].length+1; // newline counted
75
+ }
76
+ // input_element.value = lines.join("\n");
77
+
78
+ // move cursor
79
+ input_element.selectionStart = selection_start;
80
+ input_element.selectionEnd = selection_end;
81
+ }
82
+
83
+ codeInput.update(input_element.value);
84
+ }
85
+
86
+ check_enter(codeInput, event) {
87
+ if(event.key != "Enter") {
88
+ return;
89
+ }
90
+ event.preventDefault(); // stop normal
91
+
92
+ let input_element = codeInput.querySelector("textarea");
93
+ let lines = input_element.value.split("\n");
94
+ let letter_i = 0;
95
+ let current_line = lines.length - 1;
96
+ let new_line = "";
97
+ let number_indents = 0;
98
+
99
+ // find the index of the line our cursor is currently on
100
+ for (let i = 0; i < lines.length; i++) {
101
+ letter_i += lines[i].length + 1;
102
+ if(input_element.selectionEnd <= letter_i) {
103
+ current_line = i;
104
+ break;
105
+ }
106
+ }
107
+
108
+ // count the number of indents the current line starts with (up to our cursor position in the line)
109
+ let cursor_pos_in_line = lines[current_line].length - (letter_i - input_element.selectionEnd) + 1;
110
+ for (let i = 0; i < cursor_pos_in_line; i++) {
111
+ if (lines[current_line][i] == "\t") {
112
+ number_indents++;
113
+ } else {
114
+ break;
115
+ }
116
+ }
117
+
118
+ // determine the text before and after the cursor and chop the current line at the new line break
119
+ let text_after_cursor = "";
120
+ if (cursor_pos_in_line != lines[current_line].length) {
121
+ text_after_cursor = lines[current_line].substring(cursor_pos_in_line);
122
+ lines[current_line] = lines[current_line].substring(0, cursor_pos_in_line);
123
+ }
124
+
125
+ // insert our indents and any text from the previous line that might have been after the line break
126
+ for (let i = 0; i < number_indents; i++) {
127
+ new_line += "\t";
128
+ }
129
+
130
+ // save the current cursor position
131
+ let selection_start = input_element.selectionStart;
132
+ let selection_end = input_element.selectionEnd;
133
+
134
+ document.execCommand("insertText", false, "\n" + new_line); // Write new line, including auto-indentation
135
+
136
+ // move cursor to new position
137
+ input_element.selectionStart = selection_start + number_indents + 1; // count the indent level and the newline character
138
+ input_element.selectionEnd = selection_start + number_indents + 1;
139
+
140
+ codeInput.update(input_element.value);
141
+
142
+
143
+ // Update scrolls
144
+ input_element.scrollLeft = 0;
145
+ // Move down 1 line
146
+ let lineHeight = Number(getComputedStyle(input_element).lineHeight.split(0, -2));
147
+ // console.log(getComputedStyle(input_element).lineHeight);
148
+ if(lineHeight == NaN && getComputedStyle(input_element).lineHeight.split(-2) == "px") {
149
+ input_element.scrollTop += lineHeight;
150
+ } else {
151
+ input_element.scrollTop += 20; // px
152
+ }
153
+ }
154
+ }
@@ -0,0 +1 @@
1
+ codeInput.plugins.Indent=class extends codeInput.Plugin{constructor(){super()}afterElementsAdded(a){let b=a.querySelector("textarea");b.addEventListener("keydown",b=>{this.check_tab(a,b),this.check_enter(a,b)})}check_tab(a,b){if("Tab"!=b.key)return;let c=a.querySelector("textarea"),d=c.value;if(b.preventDefault(),!b.shiftKey&&c.selectionStart==c.selectionEnd)document.execCommand("insertText",!1,"\t");else{let a=c.value.split("\n"),d=0,e=c.selectionStart,f=c.selectionEnd;for(let g=0;g<a.length;g++)(e<=d+a[g].length&&f>=d+1||e==f&&e<=d+a[g].length+1&&f>=d)&&(b.shiftKey?"\t"==a[g][0]&&(c.selectionStart=d,c.selectionEnd=d+1,document.execCommand("delete",!1,""),e>d&&e--,f--,d--):(c.selectionStart=d,c.selectionEnd=d,document.execCommand("insertText",!1,"\t"),e>d&&e++,f++,d++)),d+=a[g].length+1;c.selectionStart=e,c.selectionEnd=f}a.update(c.value)}check_enter(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<d.length;g++)if(e+=d[g].length+1,c.selectionEnd<=e){f=g;break}let j=d[f].length-(e-c.selectionEnd)+1;for(let c=0;c<j&&"\t"==d[f][c];c++)h++;let k="";j!=d[f].length&&(k=d[f].substring(j),d[f]=d[f].substring(0,j));for(let c=0;c<h;c++)g+="\t";let l=c.selectionStart;document.execCommand("insertText",!1,"\n"+g),c.selectionStart=l+h+1,c.selectionEnd=l+h+1,a.update(c.value),c.scrollLeft=0;let m=+getComputedStyle(c).lineHeight.split(0,-2);c.scrollTop+=m==NaN&&"px"==getComputedStyle(c).lineHeight.split(-2)?m:20}};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Allows code-input elements to be used with the Prism.js line-numbers plugin, as long as the code-input element
3
+ * or a parent element of it has the CSS class `line-numbers`.
4
+ * https://prismjs.com/plugins/line-numbers/
5
+ * Files: prism-line-numbers.css
6
+ */
7
+ /* Update padding to match line-numbers plugin */
8
+ code-input.line-numbers textarea, code-input.line-numbers.code-input_pre-element-styled pre,
9
+ .line-numbers code-input textarea, .line-numbers code-input.code-input_pre-element-styled pre {
10
+ padding-top: 1em!important;
11
+ padding-bottom: 1em!important;
12
+ padding-right: 1em!important;
13
+ padding-left: 3.8em!important;
14
+ }
15
+
16
+ /* Remove unnecessary, interfering padding values */
17
+ code-input.line-numbers, code-input.line-numbers.code-input_pre-element-styled code,
18
+ .line-numbers code-input, .line-numbers code-input.code-input_pre-element-styled code{
19
+ padding: 0;
20
+ }
@@ -0,0 +1 @@
1
+ .line-numbers code-input textarea,.line-numbers code-input.code-input_pre-element-styled pre,code-input.line-numbers textarea,code-input.line-numbers.code-input_pre-element-styled pre{padding-top:1em!important;padding-bottom:1em!important;padding-right:1em!important;padding-left:3.8em!important}.line-numbers code-input,.line-numbers code-input.code-input_pre-element-styled code,code-input.line-numbers,code-input.line-numbers.code-input_pre-element-styled code{padding:0}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Render special characters and control characters as a symbol with their hex code.
3
+ * Files: special-chars.js, special-chars.css
4
+ */
5
+
6
+ /* Main styling */
7
+
8
+ :root, body { /* Font for hex chars */
9
+ --code-input_special-chars_0: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdjZGBgYPj///9/RhCAMcA0bg6yHgAPmh/6BoxTcQAAAABJRU5ErkJgggAA');
10
+ --code-input_special-chars_1: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdjZGBgYPj///9/RhAggwMAitIUBr9U6sYAAAAASUVORK5CYII=');
11
+ --code-input_special-chars_2: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGCEMUCCjCgyYBFGRrAKFBkAuLYT9kYcIu0AAAAASUVORK5CYII=');
12
+ --code-input_special-chars_3: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABhJREFUGFdj/P///38GKGCEMUCCjMTJAACYiBPyG8sfAgAAAABJRU5ErkJggg==');
13
+ --code-input_special-chars_4: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///39GRkZGMI3BYYACRhgDrAKZAwAYxhvyz0DRIQAAAABJRU5ErkJggg==');
14
+ --code-input_special-chars_5: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACJJREFUGFdj/P///38GKGAEcRgZGRlBfDAHLgNjgFUgywAAuR4T9hxJl2YAAAAASUVORK5CYII=');
15
+ --code-input_special-chars_6: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdj/P///38GKGAEcRgZGRlBfDAHQwasAlkGABcdF/Y4yco2AAAAAElFTkSuQmCC');
16
+ --code-input_special-chars_7: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdj/P///38GKGCEMUCCRHIAWMgT8kue3bQAAAAASUVORK5CYII=');
17
+ --code-input_special-chars_8: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GKGAEcRgZGSE0cTIAvHcb8v+mIfAAAAAASUVORK5CYII=');
18
+ --code-input_special-chars_9: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGAEcRgZGSE0igxMCVgGmQMAPqcX8hWL1K0AAAAASUVORK5CYII=');
19
+ --code-input_special-chars_A: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdjZGBgYPj///9/RhCAMcA0iADJggCmDEw5ALdxH/aGuYHqAAAAAElFTkSuQmCC');
20
+ --code-input_special-chars_B: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GBgYGRhAAceA0cTIAvc0b/vRDnVoAAAAASUVORK5CYII=');
21
+ --code-input_special-chars_C: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdjZGBgYPj///9/EM0IYjAyMjIS4CDrAQC57hP+uLwvFQAAAABJRU5ErkJggg==');
22
+ --code-input_special-chars_D: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdj/P///38GBgYGRhAAceA0fg5MDwAveh/6ToN9VwAAAABJRU5ErkJggg==');
23
+ --code-input_special-chars_E: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABxJREFUGFdj/P///38GKGAEcRgZGRlBfDCHsAwA2UwT+mVIH1MAAAAASUVORK5CYII=');
24
+ --code-input_special-chars_F: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==');
25
+ }
26
+
27
+ .code-input_special-char_container { /* pre element */
28
+ font-size: 20px;
29
+ }
30
+
31
+ .code-input_special-char {
32
+ display: inline-block;
33
+ position: relative;
34
+ top: 0;
35
+ left: 0;
36
+ height: 1em;
37
+ /* width: set by JS */
38
+ overflow: hidden;
39
+ text-decoration: none;
40
+ text-shadow: none;
41
+ vertical-align: middle;
42
+ outline: 0.1px solid currentColor;
43
+
44
+ --hex-0: var(
45
+ --code-input_special-chars_0);
46
+ --hex-1: var(
47
+ --code-input_special-chars_0);
48
+ --hex-2: var(
49
+ --code-input_special-chars_0);
50
+ --hex-3: var(
51
+ --code-input_special-chars_0);
52
+ }
53
+
54
+ /* Default - Two bytes - 4 hex chars */
55
+
56
+ .code-input_special-char::before {
57
+ margin-left: 50%;
58
+ transform: translate(-50%, 0);
59
+ content: " ";
60
+
61
+ background-color: var(--code-input_special-char_color, currentColor);
62
+ image-rendering: pixelated;
63
+ display: inline-block;
64
+ width: calc(100%-2px);
65
+ height: 100%;
66
+
67
+ mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
68
+ mask-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
69
+ mask-size: 40%, 40%, 40%, 40%;
70
+ mask-position: 10% 10%, 90% 10%, 10% 90%, 90% 90%;
71
+
72
+ -webkit-mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
73
+ -webkit-mask-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
74
+ -webkit-mask-size: min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em);
75
+ -webkit-mask-position: 10% 10%, min(90%, 0.5em) 10%, 10% 90%, min(90%, 0.5em) 90%;
76
+ }
77
+
78
+ .code-input_special-char_zero-width {
79
+ z-index: 1;
80
+ width: 1em;
81
+ margin-left: -0.5em;
82
+ margin-right: -0.5em;
83
+ position: relative;
84
+
85
+ opacity: 0.75;
86
+ }
87
+
88
+ /* One byte - 2 hex chars */
89
+ .code-input_special-char_one-byte::before {
90
+ height: 1.5em;
91
+ top: -1em;
92
+ content: attr(data-hex2);
93
+ }
94
+ .code-input_special-char_one-byte::after {
95
+ height: 1.5em;
96
+ bottom: -1em;
97
+ content: attr(data-hex3);
98
+ }