` element.
158 | */
159 | showPrompt(codeInput: CodeInput): void;
160 | }
161 |
162 | /**
163 | * Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it
164 | * possible to indent/unindent multiple lines using Tab/Shift+Tab
165 | * Files: indent.js
166 | */
167 | class Indent extends Plugin {
168 | /**
169 | * Create an indentation plugin to pass into a template
170 | * @param {boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false.
171 | * @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4.
172 | * @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.
173 | * @param {boolean} escTabToChangeFocus Whether pressing the Escape key before (Shift+)Tab should make this keypress focus on a different element (Tab's default behaviour). You should always either enable this or use this plugin's disableTabIndentation and enableTabIndentation methods linked to other keyboard shortcuts, for accessibility.
174 | */
175 | constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object, escTabToChangeFocus?: boolean);
176 | }
177 |
178 | /**
179 | * Make tokens in the element that are included within the selected text of the
180 | * gain a CSS class while selected, or trigger JavaScript callbacks.
181 | * Files: select-token-callbacks.js
182 | */
183 | class SelectTokenCallbacks extends Plugin {
184 | /**
185 | * Set up the behaviour of tokens text-selected in the `` element, and the exact definition of a token being text-selected.
186 | *
187 | * All parameters are optional. If you provide no arguments to the constructor, this will dynamically apply the "code-input_select-token-callbacks_selected" class to selected tokens only, for you to style via CSS.
188 | *
189 | * @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
190 | * @param {boolean} onlyCaretNotSelection If true, tokens will only be marked as selected when no text is selected but rather the caret is inside them (start of selection == end of selection). Default false.
191 | * @param {boolean} caretAtStartIsSelected Whether the caret or text selection's end being just before the first character of a token means said token is selected. Default true.
192 | * @param {boolean} caretAtEndIsSelected Whether the caret or text selection's start being just after the last character of a token means said token is selected. Default true.
193 | * @param {boolean} createSubTokens Whether temporary `` elements should be created inside partially-selected tokens containing just the selected text and given the selected class. Default false.
194 | * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
195 | * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
196 | */
197 | constructor(tokenSelectorCallbacks?: codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks, onlyCaretNotSelection?: boolean, caretAtStartIsSelected?: boolean, caretAtEndIsSelected?: boolean, createSubTokens?: boolean, partiallySelectedTokensAreSelected?: boolean, parentTokensAreSelected?: boolean);
198 | }
199 |
200 | namespace SelectTokenCallbacks {
201 | /**
202 | * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
203 | * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
204 | */
205 | class TokenSelectorCallbacks {
206 | /**
207 | * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
208 | *
209 | * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
210 | * @param {(token: HTMLElement) => void} tokenSelectedCallback Runs multiple times when the text selection inside the code-input changes, each time inputting a single (part of the highlighted ``) token element that is selected in the new text selection.
211 | * @param {(tokenContainer: HTMLElement) => void} selectChangedCallback Each time the text selection inside the code-input changes, runs once before any tokenSelectedCallback calls, inputting the highlighted ``'s `` element that contains all token elements.
212 | */
213 | constructor(tokenSelectedCallback: (token: HTMLElement) => void, selectChangedCallback: (tokenContainer: HTMLElement) => void);
214 |
215 | /**
216 | * Use preset callbacks which ensure all tokens in the selected text range in the ``, and only such tokens, are given a certain CSS class.
217 | *
218 | * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
219 | *
220 | * @param {string} selectedClass The CSS class that will be present on tokens only when they are part of the selected text in the `` element. Defaults to "code-input_select-token-callbacks_selected".
221 | * @returns {TokenSelectorCallbacks} A new TokenSelectorCallbacks instance that encodes this behaviour.
222 | */
223 | static createClassSynchronisation(selectedClass: string): codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks;
224 | }
225 | }
226 |
227 | /**
228 | * Render special characters and control characters as a symbol with their hex code.
229 | * Files: special-chars.js, special-chars.css
230 | */
231 | class SpecialChars extends Plugin {
232 | /**
233 | * Create a special characters plugin instance.
234 | * Default = covers many non-renderable ASCII characters.
235 | * @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code
236 | * @param {Boolean} inheritTextColor If `inheritTextColor` is false, forces the color of the hex code to inherit from syntax highlighting. Otherwise, the base color of the `pre code` element is used to give contrast to the small characters.
237 | * @param {RegExp} specialCharRegExp The regular expression which matches special characters
238 | */
239 | constructor(colorInSpecialChars?: boolean, inheritTextColor?: boolean, specialCharRegExp?: RegExp);
240 | }
241 | }
242 |
243 | /**
244 | * Register a plugin class under `codeInput.plugins`.
245 | * @param {string} pluginName The identifier of the plugin: if it is `"foo"`, `new codeInput.plugins.foo(`...`)` will instantiate it, etc.
246 | * @param {Object} pluginClass The class of the plugin, created with `class extends codeInput.plugin {`...`}`
247 | */
248 | export function registerPluginClass(pluginName: string, pluginClass: Object): void;
249 |
250 | /**
251 | * Please see `codeInput.templates.prism` or `codeInput.templates.hljs`.
252 | * Templates are used in `` elements and once registered with
253 | * `codeInput.registerTemplate` will be in charge of the highlighting
254 | * algorithm and settings for all code-inputs with a `template` attribute
255 | * matching the registered name.
256 | */
257 | export class Template {
258 | /**
259 | * **When `includeCodeInputInHighlightFunc` is `false`, `highlight` takes only the `` element as a parameter.**
260 | *
261 | * Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
262 | * I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
263 | * @param {(codeElement: HTMLElement) => void} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
264 | * @param {boolean} preElementStyled - is the `` element CSS-styled as well as the `` element? If true, `` element's scrolling is synchronised; if false, `` element's scrolling is synchronised.
265 | * @param {boolean} isCode - is this for writing code? If true, the code-input's lang HTML attribute can be used, and the `` element will be given the class name 'language-[lang attribute's value]'.
266 | * @param {false} includeCodeInputInHighlightFunc - Setting this to true passes the `` element as a second argument to the highlight function.
267 | * @param {boolean} autoDisableDuplicateSearching - Leaving this as true uses code-input's default fix for preventing duplicate results in Ctrl+F searching from the input and result elements, and setting this to false indicates your highlighting function implements its own fix. The default fix works by moving text content from elements to CSS `::before` pseudo-elements after highlighting.
268 | * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
269 | * @returns template object
270 | */
271 | constructor(highlight?: (codeElement: HTMLElement) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: false, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
272 | /**
273 | * **When `includeCodeInputInHighlightFunc` is `true`, `highlight` takes two parameters: the `` element, and the `` element.**
274 | *
275 | * Constructor to create a custom template instance. Pass this into `codeInput.registerTemplate` to use it.
276 | * I would strongly recommend using the built-in simpler template `codeInput.templates.prism` or `codeInput.templates.hljs`.
277 | * @param {(codeElement: HTMLElement, codeInput: CodeInput) => void} highlight - a callback to highlight the code, that takes an HTML `` element inside a `` element as a parameter
278 | * @param {boolean} preElementStyled - is the `` element CSS-styled as well as the `` element? If true, `` element's scrolling is synchronised; if false, `` element's scrolling is synchronised.
279 | * @param {boolean} isCode - is this for writing code? If true, the code-input's lang HTML attribute can be used, and the `` element will be given the class name 'language-[lang attribute's value]'.
280 | * @param {true} includeCodeInputInHighlightFunc - Setting this to true passes the `` element as a second argument to the highlight function.
281 | * @param {boolean} autoDisableDuplicateSearching - Leaving this as true uses code-input's default fix for preventing duplicate results in Ctrl+F searching from the input and result elements, and setting this to false indicates your highlighting function implements its own fix. The default fix works by moving text content from elements to CSS `::before` pseudo-elements after highlighting.
282 | * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
283 | * @returns template object
284 | */
285 | constructor(highlight?: (codeElement: HTMLElement, codeInput: CodeInput) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: true, autoDisableDuplicateSearching?: boolean, plugins?: Plugin[])
286 | highlight: Function
287 | preElementStyled: boolean
288 | isCode: boolean
289 | includeCodeInputInHighlightFunc: boolean
290 | autoDisableDuplicateSearching: boolean
291 | plugins: Plugin[]
292 | /**
293 | * @deprecated Please give a value for the `autoDisableDuplicateSearching` parameter.
294 | */
295 | constructor(highlight?: (code: HTMLElement) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: false, plugins?: Plugin[])
296 | /**
297 | * @deprecated Please give a value for the `autoDisableDuplicateSearching` parameter.
298 | */
299 | constructor(highlight?: (code: HTMLElement, codeInput: CodeInput) => void, preElementStyled?: boolean, isCode?: boolean, includeCodeInputInHighlightFunc?: true, plugins?: Plugin[])
300 | }
301 |
302 | /**
303 | * Shortcut functions for creating templates.
304 | * Each code-input element has a template attribute that
305 | * tells it which template to use.
306 | * Each template contains functions and preferences that
307 | * run the syntax-highlighting and let code-input control
308 | * the highlighting.
309 | *
310 | * For creating a custom template from scratch, please
311 | * use `new codeInput.Template(...)`.
312 | *
313 | * For adding small pieces of functionality, please see `codeInput.plugins`.
314 | */
315 | export namespace templates {
316 | /**
317 | * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/)
318 | * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter.
319 | * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
320 | * @returns template object
321 | */
322 | function prism(prism: Object, plugins?: Plugin[]): Template
323 | /**
324 | * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/)
325 | * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter.
326 | * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
327 | * @returns template object
328 | */
329 | function hljs(hljs: Object, plugins?: Plugin[]): Template
330 | /**
331 | * Constructor to create a proof-of-concept template that gives a message if too many characters are typed.
332 | * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
333 | * @returns template object
334 | */
335 | function characterLimit(plugins?: Plugin[]): Template
336 | /**
337 | * Constructor to create a proof-of-concept template that shows text in a repeating series of colors.
338 | * @param {string[]} rainbowColors - An array of CSS colors, in the order each color will be shown
339 | * @param {string} delimiter - The character used to split up parts of text where each part is a different color (e.g. "" = characters, " " = words)
340 | * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
341 | * @returns template object
342 | */
343 | function rainbowText(rainbowColors?: string[], delimiter?: string, plugins?: Plugin[]): Template
344 | }
345 |
346 | /**
347 | * A `` element, an instance of an `HTMLElement`, and the result
348 | * of `document.createElement("code-input")`.
349 | */
350 | export class CodeInput extends HTMLElement { }
351 |
352 | /**
353 | * Register a template so code-input elements with a template attribute that equals the templateName will use the template.
354 | * See `codeInput.templates` for constructors to create templates.
355 | * @param {string} templateName - the name to register the template under
356 | * @param {Object} template - a Template object instance - see `codeInput.templates`
357 | */
358 | export function registerTemplate(templateName: string, template: Template): void;
--------------------------------------------------------------------------------
/code-input.min.css:
--------------------------------------------------------------------------------
1 | code-input{display:block;overflow-y:auto;overflow-x:auto;position:relative;top:0;left:0;margin:8px;--padding:16px;height:250px;font-size:inherit;font-family:monospace;line-height:1.5;tab-size:2;caret-color:#a9a9a9;white-space:pre;padding:0!important;display:grid;grid-template-columns:100%;grid-template-rows:100%}code-input:not(.code-input_loaded){padding:var(--padding,16px)!important}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;min-width:calc(100% - var(--padding) * 2);min-height:calc(100% - var(--padding) * 2);overflow:hidden;resize:none;grid-row:1;grid-column:1;display:block}code-input.code-input_pre-element-styled pre,code-input:not(.code-input_pre-element-styled) pre code{height:max-content;width:max-content}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{grid-column:1;grid-row:1}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 textarea::placeholder{color:#d3d3d3}code-input pre,code-input textarea{white-space:inherit;word-spacing:normal;word-break:normal;word-wrap:normal}code-input textarea{resize:none;outline:0!important}code-input:has(textarea:focus):not(.code-input_mouse-focused){outline:2px solid #000}code-input:not(.code-input_registered){overflow:hidden;display:block;box-sizing:border-box}code-input:not(.code-input_registered)::after{content:"Use codeInput.registerTemplate to set up.";display:block;position:absolute;bottom:var(--padding);left:var(--padding);width:calc(100% - 2 * var(--padding));border-top:1px solid grey;outline:var(--padding) solid #fff;background-color:#fff}code-input:not(.code-input_loaded) pre,code-input:not(.code-input_loaded) textarea{opacity:0}code-input .code-input_dialog-container{z-index:2;position:sticky;grid-row:1;grid-column:1;top:0;left:0;width:100%;height:0;text-align:left}code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions{top:0;right:0;display:block;position:absolute;background-color:#000;color:#fff;padding:2px;padding-left:10px;text-wrap:pretty;overflow:hidden;text-overflow:ellipsis;width:calc(100% - 12px);max-height:3em}code-input .code-input_dialog-container .code-input_keyboard-navigation-instructions:empty,code-input.code-input_mouse-focused .code-input_dialog-container .code-input_keyboard-navigation-instructions,code-input:not(:has(textarea:focus)) .code-input_dialog-container .code-input_keyboard-navigation-instructions{display:none}code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:focus):not(.code-input_mouse-focused) textarea,code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:focus):not(.code-input_mouse-focused).code-input_pre-element-styled pre,code-input:not(:has(.code-input_keyboard-navigation-instructions:empty)):has(textarea:focus):not(.code-input_mouse-focused):not(.code-input_pre-element-styled) pre code{padding-top:calc(var(--padding) + 3em)!important}
--------------------------------------------------------------------------------
/code-input.min.js:
--------------------------------------------------------------------------------
1 | var codeInput={observedAttributes:["value","placeholder","language","lang","template"],textareaSyncAttributes:["value","min","max","type","pattern","autocomplete","autocorrect","autofocus","cols","dirname","disabled","form","maxlength","minlength","name","placeholder","readonly","required","rows","spellcheck","wrap"],textareaSyncEvents:["change","selectionchange","invalid","input"],usedTemplates:{},defaultTemplate:void 0,templateNotYetRegisteredQueue:{},registerTemplate:function(a,b){if(!("string"==typeof a||a instanceof String))throw TypeError(`code-input: Name of template "${a}" must be a string.`);if(!("function"==typeof b.highlight||b.highlight instanceof Function))throw TypeError(`code-input: Template for "${a}" invalid, because the highlight function provided is not a function; it is "${b.highlight}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);if(!("boolean"==typeof b.includeCodeInputInHighlightFunc||b.includeCodeInputInHighlightFunc instanceof Boolean))throw TypeError(`code-input: Template for "${a}" invalid, because the includeCodeInputInHighlightFunc value provided is not a true or false; it is "${b.includeCodeInputInHighlightFunc}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);if(!("boolean"==typeof b.preElementStyled||b.preElementStyled instanceof Boolean))throw TypeError(`code-input: Template for "${a}" invalid, because the preElementStyled value provided is not a true or false; it is "${b.preElementStyled}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);if(!("boolean"==typeof b.isCode||b.isCode instanceof Boolean))throw TypeError(`code-input: Template for "${a}" invalid, because the isCode value provided is not a true or false; it is "${b.isCode}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);if(!Array.isArray(b.plugins))throw TypeError(`code-input: Template for "${a}" invalid, because the plugin array provided is not an array; it is "${b.plugins}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`);if(b.plugins.forEach((c,d)=>{if(!(c instanceof codeInput.Plugin))throw TypeError(`code-input: Template for "${a}" invalid, because the plugin provided at index ${d} is not valid; it is "${b.plugins[d]}". Please make sure you use one of the constructors in codeInput.templates, and that you provide the correct arguments.`)}),codeInput.usedTemplates[a]=b,a in codeInput.templateNotYetRegisteredQueue){for(let c in codeInput.templateNotYetRegisteredQueue[a])elem=codeInput.templateNotYetRegisteredQueue[a][c],elem.template=b,codeInput.runOnceWindowLoaded(function(a){a.connectedCallback()}.bind(null,elem),elem);console.log(`code-input: template: Added existing elements with template ${a}`)}if(null==codeInput.defaultTemplate){if(codeInput.defaultTemplate=a,void 0 in codeInput.templateNotYetRegisteredQueue)for(let a in codeInput.templateNotYetRegisteredQueue[void 0])elem=codeInput.templateNotYetRegisteredQueue[void 0][a],elem.template=b,codeInput.runOnceWindowLoaded(function(a){a.connectedCallback()}.bind(null,elem),elem);console.log(`code-input: template: Set template ${a} as default`)}console.log(`code-input: template: Created template ${a}`)},Template:class{constructor(a=function(){},b=!0,c=!0,d=!1,e=[]){this.highlight=a,this.preElementStyled=b,this.isCode=c,this.includeCodeInputInHighlightFunc=d,this.plugins=e}highlight=function(){};preElementStyled=!0;isCode=!0;includeCodeInputInHighlightFunc=!1;plugins=[]},templates:{prism(a,b=[]){return new codeInput.Template(a.highlightElement,!0,!0,!1,b)},hljs(a,b=[]){return new codeInput.Template(function(b){b.removeAttribute("data-highlighted"),a.highlightElement(b)},!1,!0,!1,b)},characterLimit(a){return{highlight:function(a,b,c=[]){let d=+b.getAttribute("data-character-limit"),e=b.escapeHtml(b.value.slice(0,d)),f=b.escapeHtml(b.value.slice(d));a.innerHTML=`${e}${f} `,0${b.getAttribute("data-overflow-msg")||"(Character limit reached)"}`)},includeCodeInputInHighlightFunc:!0,preElementStyled:!0,isCode:!1,plugins:a}},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${b.escapeHtml(d[e])}
`);a.innerHTML=c.join(b.template.delimiter)},includeCodeInputInHighlightFunc:!0,preElementStyled:!0,isCode:!1,rainbowColors:a,delimiter:b,plugins:c}},character_limit(){return this.characterLimit([])},rainbow_text(a=["red","orangered","orange","goldenrod","gold","green","darkgreen","navy","blue","magenta"],b="",c=[]){return this.rainbowText(a,b,c)},custom(a=function(){},b=!0,c=!0,d=!1,e=[]){return{highlight:a,includeCodeInputInHighlightFunc:d,preElementStyled:b,isCode:c,plugins:e}}},plugins:new Proxy({},{get(a,b){if(a[b]==null)throw ReferenceError(`code-input: Plugin '${b}' is not defined. Please ensure you import the necessary files from the plugins folder in the WebCoder49/code-input repository, in the of your HTML, before the plugin is instatiated.`);return a[b]}}),Plugin:class{constructor(a){console.log("code-input: plugin: Created plugin"),a.forEach(a=>{codeInput.observedAttributes.push(a)})}beforeHighlight(){}afterHighlight(){}beforeElementsAdded(){}afterElementsAdded(){}attributeChanged(){}},CodeInput:class extends HTMLElement{constructor(){super()}textareaElement=null;preElement=null;codeElement=null;dialogContainerElement=null;static formAssociated=!0;boundEventCallbacks={};pluginEvt(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))}}needsHighlight=!1;handleEventsFromTextarea=!0;originalAriaDescription;scheduleHighlight(){this.needsHighlight=!0}animateFrame(){this.needsHighlight&&(this.update(),this.needsHighlight=!1),window.requestAnimationFrame(this.animateFrame.bind(this))}update(){let a=this.codeElement,b=this.value;b+="\n",a.innerHTML=this.escapeHtml(b),this.pluginEvt("beforeHighlight"),this.template.includeCodeInputInHighlightFunc?this.template.highlight(a,this):this.template.highlight(a),this.syncSize(),this.textareaElement===document.activeElement&&(this.handleEventsFromTextarea=!1,this.textareaElement.blur(),this.textareaElement.focus(),this.handleEventsFromTextarea=!0),this.pluginEvt("afterHighlight")}syncSize(){this.template.preElementStyled?(this.style.backgroundColor=getComputedStyle(this.preElement).backgroundColor,this.textareaElement.style.height=getComputedStyle(this.preElement).height,this.textareaElement.style.width=getComputedStyle(this.preElement).width):(this.style.backgroundColor=getComputedStyle(this.codeElement).backgroundColor,this.textareaElement.style.height=getComputedStyle(this.codeElement).height,this.textareaElement.style.width=getComputedStyle(this.codeElement).width)}setKeyboardNavInstructions(a,b){this.dialogContainerElement.querySelector(".code-input_keyboard-navigation-instructions").innerText=a,b?this.textareaElement.setAttribute("aria-description",this.originalAriaDescription+". "+a):this.textareaElement.setAttribute("aria-description",a)}escapeHtml(a){return a.replace(/&/g,"&").replace(/")}getTemplate(){let a;return a=null==this.getAttribute("template")?codeInput.defaultTemplate:this.getAttribute("template"),a in codeInput.usedTemplates?codeInput.usedTemplates[a]:(a in codeInput.templateNotYetRegisteredQueue||(codeInput.templateNotYetRegisteredQueue[a]=[]),void codeInput.templateNotYetRegisteredQueue[a].push(this))}setup(){if(null!=this.textareaElement)return;this.classList.add("code-input_registered"),this.template.preElementStyled&&this.classList.add("code-input_pre-element-styled"),this.pluginEvt("beforeElementsAdded");let a=this.getAttribute("language")||this.getAttribute("lang"),b=this.getAttribute("placeholder")||this.getAttribute("language")||this.getAttribute("lang")||"",c=this.unescapeHtml(this.innerHTML)||this.getAttribute("value")||"";this.initialValue=c;let d=document.createElement("textarea");d.placeholder=b,""!=c&&(d.value=c),d.innerHTML=this.innerHTML,d.setAttribute("spellcheck","false"),d.setAttribute("tabindex",this.getAttribute("tabindex")||0),this.setAttribute("tabindex",-1),this.originalAriaDescription=this.getAttribute("aria-description")||"Code input field",this.addEventListener("mousedown",()=>{this.classList.add("code-input_mouse-focused")}),d.addEventListener("blur",()=>{this.handleEventsFromTextarea&&this.classList.remove("code-input_mouse-focused")}),this.innerHTML="";for(let a,b=0;b{this.value=this.textareaElement.value}),this.textareaElement=d,this.append(d);let e=document.createElement("code"),f=document.createElement("pre");f.setAttribute("aria-hidden","true"),f.setAttribute("tabindex","-1"),f.setAttribute("inert",!0),this.preElement=f,this.codeElement=e,f.append(e),this.append(f),this.template.isCode&&a!=null&&""!=a&&e.classList.add("language-"+a.toLowerCase());let g=document.createElement("div");g.classList.add("code-input_dialog-container"),this.append(g),this.dialogContainerElement=g;let h=document.createElement("div");h.classList.add("code-input_keyboard-navigation-instructions"),g.append(h),this.pluginEvt("afterElementsAdded"),this.dispatchEvent(new CustomEvent("code-input_load")),this.value=c,this.animateFrame();const i=new ResizeObserver(()=>{this.syncSize()});i.observe(this)}escape_html(a){return this.escapeHtml(a)}get_template(){return this.getTemplate()}connectedCallback(){this.template=this.getTemplate(),this.template!=null&&(this.classList.add("code-input_registered"),codeInput.runOnceWindowLoaded(()=>{this.setup(),this.classList.add("code-input_loaded")},this)),this.mutationObserver=new MutationObserver(this.mutationObserverCallback.bind(this)),this.mutationObserver.observe(this,{attributes:!0,attributeOldValue:!0})}mutationObserverCallback(a){for(const b of a)if("attributes"===b.type){for(let a=0;a{a.match(b)&&(null==c?this.textareaElement.removeAttribute(a):this.textareaElement.setAttribute(a,c))})}}addEventListener(a,b,c=void 0){let d=function(a){"function"==typeof b?b(a):b&&b.handleEvent&&b.handleEvent(a)}.bind(this);if(this.boundEventCallbacks[b]=d,codeInput.textareaSyncEvents.includes(a)){let e=function(a){this.handleEventsFromTextarea&&d(a)}.bind(this);this.boundEventCallbacks[b]=e,void 0===c?null==this.textareaElement?this.addEventListener("code-input_load",()=>{this.textareaElement.addEventListener(a,d)}):this.textareaElement.addEventListener(a,e):null==this.textareaElement?this.addEventListener("code-input_load",()=>{this.textareaElement.addEventListener(a,d,c)}):this.textareaElement.addEventListener(a,e,c)}else void 0===c?super.addEventListener(a,d):super.addEventListener(a,d,c)}removeEventListener(a,b,c=void 0){let d=this.boundEventCallbacks[b];codeInput.textareaSyncEvents.includes(a)?c===void 0?null==this.textareaElement?this.addEventListener("code-input_load",()=>{this.textareaElement.removeEventListener(a,d)}):this.textareaElement.removeEventListener(a,d):null==this.textareaElement?this.addEventListener("code-input_load",()=>{this.textareaElement.removeEventListener(a,d,c)}):this.textareaElement.removeEventListener(a,d,c):c===void 0?super.removeEventListener(a,d):super.removeEventListener(a,d,c)}get value(){return this.textareaElement.value}set value(a){return(null===a||void 0===a)&&(a=""),this.textareaElement.value=a,this.scheduleHighlight(),a}get placeholder(){return this.getAttribute("placeholder")}set placeholder(a){return this.setAttribute("placeholder",a)}get validity(){return this.textareaElement.validity}get validationMessage(){return this.textareaElement.validationMessage}setCustomValidity(a){return this.textareaElement.setCustomValidity(a)}checkValidity(){return this.textareaElement.checkValidity()}reportValidity(){return this.textareaElement.reportValidity()}pluginData={};formResetCallback(){this.value=this.initialValue}},runOnceWindowLoaded(a){"complete"==document.readyState?a():window.addEventListener("load",a)}};customElements.define("code-input",codeInput.CodeInput);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webcoder49/code-input",
3 | "version": "2.4.0",
4 | "description": "Fully customisable, editable syntax-highlighted textareas.",
5 | "browser": "code-input.js",
6 | "scripts": {
7 | "test": "echo \"This is a front-end library, not a Node library. Please see the README for how to use.\" && 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": "hi@webcoder49.dev",
24 | "url": "https://webcoder49.dev/"
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 | }
32 |
--------------------------------------------------------------------------------
/plugins/README.md:
--------------------------------------------------------------------------------
1 | # Code-input: Plugins
2 | ## List Of Plugins
3 |
4 | 💡 Do you just want to get a quick editor working? We suggest the [Indent](#indent) and [Prism Line Numbers](#prism-line-numbers) plugins.
5 |
6 | **Lots of plugins are very customisable - please see the JavaScript files for parameters and if you want more features let us know via GitHub Issues.**
7 |
8 | ---
9 |
10 | ### Auto-Close Brackets
11 | Automatically close pairs of brackets/quotes/other syntaxes in code, but also optionally choose the brackets this
12 | is activated for.
13 |
14 | Files: [auto-close-brackets.js](./auto-close-brackets.js)
15 |
16 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/qBgGGKR)
17 |
18 | ### Autocomplete
19 | Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions.
20 |
21 | Files: [autocomplete.js](./autocomplete.js) / [autocomplete.css](./autocomplete.css)
22 |
23 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/xxapjXB)
24 |
25 | ### Autodetect
26 | Autodetect the language live and change the `lang` attribute using the syntax highlighter's autodetect capabilities. Works with highlight.js.
27 |
28 | Files: [autodetect.js](./autodetect.js)
29 |
30 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/eYLyMae)
31 |
32 | ### Find and Replace
33 | Add Find-and-Replace (Ctrl+F for find, Ctrl+H for replace by default, or when JavaScript triggers it) functionality to the code editor.
34 |
35 | Files: [find-and-replace.js](./find-and-replace.js) / [find-and-replace.css](./find-and-replace.css)
36 |
37 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/oNVVBBz)
38 |
39 | ### Go To Line
40 | Add a feature to go to a specific line when a line number is given (or column as well, in the format line no:column no) that appears when (optionally) Ctrl+G is pressed or when JavaScript triggers it.
41 |
42 | Files: [go-to-line.js](./go-to-line.js) / [go-to-line.css](./go-to-line.css)
43 |
44 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/YzBMOXP)
45 |
46 | ### Indent
47 | 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.**
48 |
49 | Files: [indent.js](./indent.js)
50 |
51 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/WNgdzar)
52 |
53 | ### Prism Line Numbers
54 | Allow 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/)
55 |
56 | Files: [prism-line-numbers.css](./prism-line-numbers.css) (NO JS FILE)
57 |
58 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/XWPVrWv)
59 |
60 | ### Special Chars
61 | Render special characters and control characters as a symbol
62 | with their hex code.
63 |
64 | Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-chars.css)
65 |
66 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm)
67 |
68 | ### Select Token Callbacks
69 | Make tokens in the `` element that are included within the selected text of the `` gain a CSS class while selected, or trigger JavaScript callbacks.
70 |
71 | Files: select-token-callbacks.js
72 |
73 | [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/WNVZXxM)
74 |
75 | ## Using Plugins
76 | 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:
77 | - 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.
78 | - If a JavaScript file is present, Place an instance of each plugin in the array of plugins argument when registering, like this:
79 | ```html
80 |
81 |
82 |
83 |
84 |
85 |
96 | ```
97 |
--------------------------------------------------------------------------------
/plugins/auto-close-brackets.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Automatically close pairs of brackets/quotes/other syntaxes in code, but also optionally choose the brackets this
3 | * is activated for.
4 | * Files: auto-close-brackets.js
5 | */
6 | codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin {
7 | bracketPairs = [];
8 | bracketsOpenedStack = []; // Each item [closing bracket string, opening bracket location] Innermost at right so can know which brackets should be ignored when retyped
9 |
10 | /**
11 | * Create an auto-close brackets plugin to pass into a template
12 | * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}", '"': '"'}. All brackets must only be one character.
13 | */
14 | constructor(bracketPairs={"(": ")", "[": "]", "{": "}", '"': '"'}) {
15 | super([]); // No observed attributes
16 |
17 | this.bracketPairs = bracketPairs;
18 | }
19 |
20 | /* Add keystroke events */
21 | afterElementsAdded(codeInput) {
22 | codeInput.textareaElement.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) });
23 | codeInput.textareaElement.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); });
24 | }
25 |
26 | /* Deal with the automatic creation of closing bracket when opening brackets are typed, and the ability to "retype" a closing
27 | bracket where one has already been placed. */
28 | checkBrackets(codeInput, event) {
29 | if(event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) {
30 | // Check if a closing bracket is typed
31 | for(let openingBracket in this.bracketPairs) {
32 | let closingBracket = this.bracketPairs[openingBracket];
33 | if(event.data == closingBracket) {
34 | // "Retype" a closing bracket, i.e. just move caret
35 | codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd += 1;
36 | event.preventDefault();
37 | break;
38 | }
39 | }
40 | } else if(event.data in this.bracketPairs) {
41 | // Opening bracket typed; Create bracket pair
42 | let closingBracket = this.bracketPairs[event.data];
43 | // Insert the closing bracket
44 | document.execCommand("insertText", false, closingBracket);
45 | // Move caret before the inserted closing bracket
46 | codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd -= 1;
47 | }
48 | }
49 |
50 | /* Deal with cases where a backspace deleting an opening bracket deletes the closing bracket straight after it as well */
51 | checkBackspace(codeInput, event) {
52 | if(event.key == "Backspace" && codeInput.textareaElement.selectionStart == codeInput.textareaElement.selectionEnd) {
53 | let closingBracket = this.bracketPairs[codeInput.textareaElement.value[codeInput.textareaElement.selectionStart-1]];
54 | if(closingBracket != undefined && codeInput.textareaElement.value[codeInput.textareaElement.selectionStart] == closingBracket) {
55 | // Opening bracket being deleted so delete closing bracket as well
56 | codeInput.textareaElement.selectionEnd = codeInput.textareaElement.selectionStart + 1;
57 | codeInput.textareaElement.selectionStart -= 1;
58 | }
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/plugins/auto-close-brackets.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.AutoCloseBrackets=class extends codeInput.Plugin{bracketPairs=[];bracketsOpenedStack=[];constructor(a={"(":")","[":"]","{":"}",'"':"\""}){super([]),this.bracketPairs=a}afterElementsAdded(a){a.textareaElement.addEventListener("keydown",b=>{this.checkBackspace(a,b)}),a.textareaElement.addEventListener("beforeinput",b=>{this.checkBrackets(a,b)})}checkBrackets(a,b){if(b.data==a.textareaElement.value[a.textareaElement.selectionStart])for(let c in this.bracketPairs){let d=this.bracketPairs[c];if(b.data==d){a.textareaElement.selectionStart=a.textareaElement.selectionEnd+=1,b.preventDefault();break}}else if(b.data in this.bracketPairs){let c=this.bracketPairs[b.data];document.execCommand("insertText",!1,c),a.textareaElement.selectionStart=a.textareaElement.selectionEnd-=1}}checkBackspace(a,b){if("Backspace"==b.key&&a.textareaElement.selectionStart==a.textareaElement.selectionEnd){let b=this.bracketPairs[a.textareaElement.value[a.textareaElement.selectionStart-1]];b!=null&&a.textareaElement.value[a.textareaElement.selectionStart]==b&&(a.textareaElement.selectionEnd=a.textareaElement.selectionStart+1,a.textareaElement.selectionStart-=1)}}};
--------------------------------------------------------------------------------
/plugins/autocomplete.css:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/plugins/autocomplete.js:
--------------------------------------------------------------------------------
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 create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd).
8 | * @param {(popupElement: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
9 | */
10 | constructor(updatePopupCallback) {
11 | super([]); // No observed attributes
12 | this.updatePopupCallback = updatePopupCallback;
13 | }
14 | /* When a key is pressed, or scrolling occurs, update the popup position */
15 | updatePopup(codeInput, onlyScrolled) {
16 | let textarea = codeInput.textareaElement;
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 | /* Create the popup 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 testPosPre = document.createElement("pre");
33 | popupElem.setAttribute("inert", true); // Invisible to keyboard navigation
34 | popupElem.setAttribute("tabindex", -1); // Invisible to keyboard navigation
35 | testPosPre.setAttribute("aria-hidden", true); // Hide for screen readers
36 | if(codeInput.template.preElementStyled) {
37 | testPosPre.classList.add("code-input_autocomplete_testpos");
38 | codeInput.appendChild(testPosPre); // Styled like first pre, but first pre found to update
39 | } else {
40 | let testPosCode = document.createElement("code");
41 | testPosCode.classList.add("code-input_autocomplete_testpos");
42 | testPosPre.appendChild(testPosCode);
43 | codeInput.appendChild(testPosPre); // Styled like first pre, but first pre found to update
44 | }
45 |
46 | let textarea = codeInput.textareaElement;
47 | textarea.addEventListener("input", () => { this.updatePopup(codeInput, false)});
48 | textarea.addEventListener("click", () => { this.updatePopup(codeInput, false)});
49 | }
50 | /**
51 | * Return the coordinates of the caret in a code-input
52 | * @param {codeInput.CodeInput} codeInput
53 | * @param {HTMLElement} textarea
54 | * @param {Number} charIndex
55 | * @param {boolean} onlyScrolled True if no edits have been made to the text and the caret hasn't been repositioned
56 | * @returns {Object} {"top": CSS top value in pixels, "left": CSS left value in pixels}
57 | */
58 | getCaretCoordinates(codeInput, textarea, charIndex, onlyScrolled) {
59 | let afterSpan;
60 | if(onlyScrolled) {
61 | // No edits to text; don't update element - span at index 1 is after span
62 | let spans = codeInput.querySelector(".code-input_autocomplete_testpos").querySelectorAll("span");
63 | if(spans.length < 2) {
64 | // Hasn't saved text in test pre to find pos
65 | // Need to regenerate text in test pre
66 | return this.getCaretCoordinates(codeInput, textarea, charIndex, false);
67 | }
68 | afterSpan = spans[1];
69 | } else {
70 | /* Inspired by https://github.com/component/textarea-caret-position */
71 | let testPosElem = codeInput.querySelector(".code-input_autocomplete_testpos");
72 |
73 | let beforeSpan = document.createElement("span");
74 | beforeSpan.textContent = textarea.value.substring(0, charIndex);
75 | afterSpan = document.createElement("span");
76 | afterSpan.textContent = "."; // Placeholder
77 |
78 | // Clear test pre - https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
79 | while (testPosElem.firstChild) {
80 | testPosElem.removeChild(testPosElem.firstChild);
81 | }
82 | testPosElem.appendChild(beforeSpan);
83 | testPosElem.appendChild(afterSpan);
84 | }
85 | return {"top": afterSpan.offsetTop - textarea.scrollTop, "left": afterSpan.offsetLeft - textarea.scrollLeft};
86 | }
87 | updatePopupCallback = function() {};
88 | }
--------------------------------------------------------------------------------
/plugins/autocomplete.min.css:
--------------------------------------------------------------------------------
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}
--------------------------------------------------------------------------------
/plugins/autocomplete.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.Autocomplete=class extends codeInput.Plugin{constructor(a){super([]),this.updatePopupCallback=a}updatePopup(a,b){let c=a.textareaElement,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");if(b.setAttribute("inert",!0),b.setAttribute("tabindex",-1),c.setAttribute("aria-hidden",!0),a.template.preElementStyled)c.classList.add("code-input_autocomplete_testpos"),a.appendChild(c);else{let b=document.createElement("code");b.classList.add("code-input_autocomplete_testpos"),c.appendChild(b),a.appendChild(c)}let d=a.textareaElement;d.addEventListener("input",()=>{this.updatePopup(a,!1)}),d.addEventListener("click",()=>{this.updatePopup(a,!1)})}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}}updatePopupCallback=function(){}};
--------------------------------------------------------------------------------
/plugins/autodetect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Autodetect the language live and change the `lang` attribute using the syntax highlighter's
3 | * autodetect capabilities. Works with highlight.js only.
4 | * Files: autodetect.js
5 | */
6 | codeInput.plugins.Autodetect = class extends codeInput.Plugin {
7 | constructor() {
8 | super([]); // No observed attributes
9 | }
10 | /* Remove previous language class */
11 | beforeHighlight(codeInput) {
12 | let resultElement = codeInput.codeElement;
13 | resultElement.className = ""; // CODE
14 | resultElement.parentElement.className = ""; // PRE
15 | }
16 | /* Get new language class and set `language` attribute */
17 | afterHighlight(codeInput) {
18 | let langClass = codeInput.codeElement.className || codeInput.preElement.className;
19 | let lang = langClass.match(/lang(\w|-)*/i)[0]; // Get word starting with lang...; Get outer bracket
20 | lang = lang.split("-")[1];
21 | if(lang == "undefined") {
22 | codeInput.removeAttribute("language");
23 | codeInput.removeAttribute("lang");
24 | } else {
25 | codeInput.setAttribute("language", lang);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/plugins/autodetect.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.Autodetect=class extends codeInput.Plugin{constructor(){super([])}beforeHighlight(a){let b=a.codeElement;b.className="",b.parentElement.className=""}afterHighlight(a){let b=a.codeElement.className||a.preElement.className,c=b.match(/lang(\w|-)*/i)[0];c=c.split("-")[1],"undefined"==c?(a.removeAttribute("language"),a.removeAttribute("lang")):a.setAttribute("language",c)}};
--------------------------------------------------------------------------------
/plugins/find-and-replace.css:
--------------------------------------------------------------------------------
1 | /* Find functionality matches */
2 | .code-input_find-and-replace_find-match {
3 | color: inherit;
4 | text-shadow: none!important;
5 | background-color: #ffff00!important;
6 | }
7 | .code-input_find-and-replace_find-match-focused, .code-input_find-and-replace_find-match-focused * {
8 | background-color: #ff8800!important;
9 | color: black!important;
10 | }
11 | .code-input_find-and-replace_start-newline::before {
12 | content: "⤶";
13 | }
14 |
15 | /* Find-and-replace dialog */
16 |
17 | @keyframes code-input_find-and-replace_roll-in {
18 | 0% {opacity: 0; transform: translateY(-34px);}
19 | 100% {opacity: 1; transform: translateY(0px);}
20 | }
21 |
22 | @keyframes code-input_find-and-replace_roll-out {
23 | 0% {opacity: 1;top: 0;}
24 | 100% {opacity: 0;top: -34px;}
25 | }
26 |
27 | .code-input_find-and-replace_dialog {
28 | position: absolute;
29 | top: 0;
30 | right: 14px;
31 | padding: 6px;
32 | padding-top: 8px;
33 | border: solid 1px #00000044;
34 | background-color: white;
35 | border-radius: 6px;
36 | box-shadow: 0 .2em 1em .2em rgba(0, 0, 0, 0.16);
37 | }
38 |
39 | .code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog) {
40 | animation: code-input_find-and-replace_roll-in .2s;
41 | opacity: 1;
42 | pointer-events: all;
43 | }
44 |
45 | .code-input_find-and-replace_dialog.code-input_find-and-replace_hidden-dialog {
46 | animation: code-input_find-and-replace_roll-out .2s;
47 | opacity: 0;
48 | pointer-events: none;
49 | }
50 |
51 | .code-input_find-and-replace_dialog input::placeholder {
52 | font-size: 80%;
53 | }
54 |
55 | .code-input_find-and-replace_dialog input {
56 | position: relative;
57 | width: 240px; height: 32px; top: -3px;
58 | font-size: large;
59 | color: #000000aa;
60 | border: 0;
61 | }
62 |
63 | .code-input_find-and-replace_dialog input:hover {
64 | outline: none;
65 | }
66 |
67 | .code-input_find-and-replace_dialog input.code-input_find-and-replace_error {
68 | color: #ff0000aa;
69 | }
70 |
71 | .code-input_find-and-replace_dialog button, .code-input_find-and-replace_dialog input[type="checkbox"] {
72 | display: inline-block;
73 | line-height: 24px;
74 | font-size: 22px;
75 | cursor: pointer;
76 | appearance: none;
77 | width: min-content;
78 |
79 | margin: 5px;
80 | padding: 5px;
81 | border: 0;
82 | background-color: #dddddd;
83 |
84 | text-align: center;
85 | color: black;
86 | vertical-align: top;
87 | }
88 |
89 | .code-input_find-and-replace_dialog input[type="checkbox"].code-input_find-and-replace_case-sensitive-checkbox::before {
90 | content: "Aa";
91 | }
92 | .code-input_find-and-replace_dialog input[type="checkbox"].code-input_find-and-replace_reg-exp-checkbox::before {
93 | content: ".*";
94 | }
95 |
96 | .code-input_find-and-replace_dialog button:hover, .code-input_find-and-replace_dialog input[type="checkbox"]:hover {
97 | background-color: #bbbbbb;
98 | }
99 |
100 | .code-input_find-and-replace_dialog input[type="checkbox"]:checked {
101 | background-color: #222222;
102 | color: white;
103 | }
104 |
105 | .code-input_find-and-replace_match-description {
106 | display: block; /* So not on same line as other */
107 | color: #444444;
108 | }
109 |
110 | .code-input_find-and-replace_dialog details summary, .code-input_find-and-replace_dialog button {
111 | cursor: pointer;
112 | }
113 |
114 |
115 | .code-input_find-and-replace_dialog button.code-input_find-and-replace_button-hidden {
116 | opacity: 0;
117 | pointer-events: none;
118 | }
119 |
120 | /* Cancel icon */
121 | .code-input_find-and-replace_dialog span {
122 | display: block;
123 | float: right;
124 | margin: 5px;
125 | padding: 5px;
126 |
127 | width: 24px;
128 | line-height: 24px;
129 | font-family: system-ui;
130 | font-size: 22px;
131 | font-weight: 500;
132 | text-align: center;
133 | border-radius: 50%;
134 | color: black;
135 | opacity: 0.6;
136 | }
137 |
138 | .code-input_find-and-replace_dialog span:before {
139 | content: "\00d7";
140 | }
141 |
142 | .code-input_find-and-replace_dialog span:hover {
143 | opacity: .8;
144 | background-color: #00000018;
145 | }
146 |
--------------------------------------------------------------------------------
/plugins/find-and-replace.min.css:
--------------------------------------------------------------------------------
1 | .code-input_find-and-replace_find-match{color:inherit;text-shadow:none!important;background-color:#ff0!important}.code-input_find-and-replace_find-match-focused,.code-input_find-and-replace_find-match-focused *{background-color:#f80!important;color:#000!important}.code-input_find-and-replace_start-newline::before{content:"⤶"}@keyframes code-input_find-and-replace_roll-in{0%{opacity:0;transform:translateY(-34px)}100%{opacity:1;transform:translateY(0)}}@keyframes code-input_find-and-replace_roll-out{0%{opacity:1;top:0}100%{opacity:0;top:-34px}}.code-input_find-and-replace_dialog{position:absolute;top:0;right:14px;padding:6px;padding-top:8px;border:solid 1px #00000044;background-color:#fff;border-radius:6px;box-shadow:0 .2em 1em .2em rgba(0,0,0,.16)}.code-input_find-and-replace_dialog:not(.code-input_find-and-replace_hidden-dialog){animation:code-input_find-and-replace_roll-in .2s;opacity:1;pointer-events:all}.code-input_find-and-replace_dialog.code-input_find-and-replace_hidden-dialog{animation:code-input_find-and-replace_roll-out .2s;opacity:0;pointer-events:none}.code-input_find-and-replace_dialog input::placeholder{font-size:80%}.code-input_find-and-replace_dialog input{position:relative;width:240px;height:32px;top:-3px;font-size:large;color:#000000aa;border:0}.code-input_find-and-replace_dialog input:hover{outline:0}.code-input_find-and-replace_dialog input.code-input_find-and-replace_error{color:#ff0000aa}.code-input_find-and-replace_dialog button,.code-input_find-and-replace_dialog input[type=checkbox]{display:inline-block;line-height:24px;font-size:22px;cursor:pointer;appearance:none;width:min-content;margin:5px;padding:5px;border:0;background-color:#ddd;text-align:center;color:#000;vertical-align:top}.code-input_find-and-replace_dialog input[type=checkbox].code-input_find-and-replace_case-sensitive-checkbox::before{content:"Aa"}.code-input_find-and-replace_dialog input[type=checkbox].code-input_find-and-replace_reg-exp-checkbox::before{content:".*"}.code-input_find-and-replace_dialog button:hover,.code-input_find-and-replace_dialog input[type=checkbox]:hover{background-color:#bbb}.code-input_find-and-replace_dialog input[type=checkbox]:checked{background-color:#222;color:#fff}.code-input_find-and-replace_match-description{display:block;color:#444}.code-input_find-and-replace_dialog button,.code-input_find-and-replace_dialog details summary{cursor:pointer}.code-input_find-and-replace_dialog button.code-input_find-and-replace_button-hidden{opacity:0;pointer-events:none}.code-input_find-and-replace_dialog span{display:block;float:right;margin:5px;padding:5px;width:24px;line-height:24px;font-family:system-ui;font-size:22px;font-weight:500;text-align:center;border-radius:50%;color:#000;opacity:.6}.code-input_find-and-replace_dialog span:before{content:"\00d7"}.code-input_find-and-replace_dialog span:hover{opacity:.8;background-color:#00000018}
--------------------------------------------------------------------------------
/plugins/find-and-replace.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.FindAndReplace=class extends codeInput.Plugin{useCtrlF=!1;useCtrlH=!1;findMatchesOnValueChange=!0;constructor(a=!0,b=!0){super([]),this.useCtrlF=a,this.useCtrlH=b}afterElementsAdded(a){const b=a.textareaElement;this.useCtrlF&&b.addEventListener("keydown",b=>{this.checkCtrlF(a,b)}),this.useCtrlH&&b.addEventListener("keydown",b=>{this.checkCtrlH(a,b)})}afterHighlight(a){a.pluginData.findAndReplace==null||a.pluginData.findAndReplace.dialog==null||a.pluginData.findAndReplace.dialog.classList.contains("code-input_find-and-replace_hidden-dialog")||(a.pluginData.findAndReplace.dialog.findMatchState.rehighlightMatches(),this.updateMatchDescription(a.pluginData.findAndReplace.dialog),0==a.pluginData.findAndReplace.dialog.findMatchState.numMatches&&a.pluginData.findAndReplace.dialog.findInput.classList.add("code-input_find-and-replace_error"))}text2RegExp(a,b,c){return new RegExp(c?a:a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),b?"g":"gi")}updateMatchDescription(a){a.matchDescription.textContent=0==a.findInput.value.length?"Search for matches in your code.":0>=a.findMatchState.numMatches?"No matches.":1==a.findMatchState.numMatches?"1 match found.":`${a.findMatchState.focusedMatchID+1} of ${a.findMatchState.numMatches} matches.`}updateFindMatches(a){let b=a.findInput.value;setTimeout(()=>{if(b==a.findInput.value){if(a.findMatchState.clearMatches(),0{j.setAttribute("open",!0)}),p.className="code-input_find-and-replace_button-hidden",p.innerText="Replace All",p.title="Replace All Occurences",p.addEventListener("focus",()=>{j.setAttribute("open",!0)}),m.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.nextMatch(),this.updateMatchDescription(c)}),n.addEventListener("click",()=>{event.preventDefault(),c.findMatchState.previousMatch(),this.updateMatchDescription(c)}),o.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.replaceOnce(i.value),c.focus()}),p.addEventListener("click",a=>{a.preventDefault(),c.findMatchState.replaceAll(i.value),p.focus()}),j.addEventListener("toggle",()=>{o.classList.toggle("code-input_find-and-replace_button-hidden"),p.classList.toggle("code-input_find-and-replace_button-hidden")}),c.findMatchState=new codeInput.plugins.FindAndReplace.FindMatchState(a),c.codeInput=a,c.textarea=d,c.findInput=e,c.findCaseSensitiveCheckbox=f,c.findRegExpCheckbox=g,c.matchDescription=h,c.replaceInput=i,c.replaceDropdown=j,this.checkCtrlH&&e.addEventListener("keydown",a=>{a.ctrlKey&&"h"==a.key&&(a.preventDefault(),j.setAttribute("open",!0))}),e.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),i.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),i.addEventListener("input",()=>{c.classList.contains("code-input_find-and-replace_hidden-dialog")?this.showPrompt(c.codeInput,!0):!c.replaceDropdown.hasAttribute("open")&&c.replaceDropdown.setAttribute("open",!0)}),c.addEventListener("keyup",b=>{"Escape"==b.key&&this.cancelPrompt(c,a,b)}),e.addEventListener("keyup",b=>{this.checkFindPrompt(c,a,b)}),e.addEventListener("input",()=>{this.findMatchesOnValueChange&&this.updateFindMatches(c),c.classList.contains("code-input_find-and-replace_hidden-dialog")&&this.showPrompt(c.codeInput,!1)}),f.addEventListener("click",()=>{this.updateFindMatches(c)}),g.addEventListener("click",()=>{this.updateFindMatches(c)}),i.addEventListener("keyup",b=>{this.checkReplacePrompt(c,a,b),i.focus()}),q.addEventListener("click",b=>{this.cancelPrompt(c,a,b)}),q.addEventListener("keypress",b=>{("Space"==b.key||"Enter"==b.key)&&this.cancelPrompt(c,a,b)}),a.dialogContainerElement.appendChild(c),a.pluginData.findAndReplace={dialog:c},e.focus(),b&&j.setAttribute("open",!0),c.selectionStart=a.textareaElement.selectionStart,c.selectionEnd=a.textareaElement.selectionEnd,c.selectionStart=this.matchStartIndexes.length&&(a=0)}this.focusedMatchStartIndex=this.matchStartIndexes[a],this.focusedMatchID=a;let b=this.codeInput.codeElement.querySelectorAll(".code-input_find-and-replace_find-match-focused");for(let c=0;c=c){if(g.length>=d){if(h){let b=document.createElement("span");b.classList.add("code-input_find-and-replace_find-match"),b.setAttribute("data-code-input_find-and-replace_match-id",a),b.classList.add("code-input_find-and-replace_temporary-span"),b.textContent=g.substring(0,d),"\n"==b.textContent[0]&&b.classList.add("code-input_find-and-replace_start-newline");let c=g.substring(d);return f.textContent=c,f.insertAdjacentElement("beforebegin",b),void e++}return void this.highlightMatch(a,f,0,d)}f.classList.add("code-input_find-and-replace_find-match"),f.setAttribute("data-code-input_find-and-replace_match-id",a),"\n"==f.textContent[0]&&f.classList.add("code-input_find-and-replace_start-newline")}else if(g.length>c){if(!h)this.highlightMatch(a,f,c,d);else if(g.length>d){let b=document.createElement("span");b.classList.add("code-input_find-and-replace_temporary-span"),b.textContent=g.substring(0,c);let h=g.substring(c,d);f.textContent=h,f.classList.add("code-input_find-and-replace_find-match"),f.setAttribute("data-code-input_find-and-replace_match-id",a),"\n"==f.textContent[0]&&f.classList.add("code-input_find-and-replace_start-newline");let i=document.createElement("span");i.classList.add("code-input_find-and-replace_temporary-span"),i.textContent=g.substring(d),f.insertAdjacentElement("beforebegin",b),f.insertAdjacentElement("afterend",i),e++}else{let b=g.substring(0,c);f.textContent=b;let d=document.createElement("span");d.classList.add("code-input_find-and-replace_find-match"),d.setAttribute("data-code-input_find-and-replace_match-id",a),d.classList.add("code-input_find-and-replace_temporary-span"),d.textContent=g.substring(c),"\n"==d.textContent[0]&&d.classList.add("code-input_find-and-replace_start-newline"),f.insertAdjacentElement("afterend",d),e++}if(g.length>d)return}c-=g.length,d-=g.length}}};
--------------------------------------------------------------------------------
/plugins/go-to-line.css:
--------------------------------------------------------------------------------
1 | @keyframes code-input_go-to-line_roll-in {
2 | 0% {opacity: 0; transform: translateY(-34px);}
3 | 100% {opacity: 1; transform: translateY(0px);}
4 | }
5 |
6 | @keyframes code-input_go-to-line_roll-out {
7 | 0% {opacity: 1; transform: translateY(0px);}
8 | 100% {opacity: 0; transform: translateY(-34px);}
9 | }
10 |
11 | .code-input_go-to-line_dialog {
12 | position: absolute;
13 | top: 0; right: 14px;
14 | height: 28px;
15 | padding: 6px;
16 | padding-top: 8px;
17 | border: solid 1px #00000044;
18 | background-color: white;
19 | border-radius: 6px;
20 | box-shadow: 0 .2em 1em .2em rgba(0, 0, 0, 0.16);
21 | }
22 |
23 | .code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog) {
24 | animation: code-input_go-to-line_roll-in .2s;
25 | opacity: 1;
26 | pointer-events: all;
27 | }
28 |
29 | .code-input_go-to-line_dialog.code-input_go-to-line_hidden-dialog {
30 | animation: code-input_go-to-line_roll-out .2s;
31 | opacity: 0;
32 | pointer-events: none;
33 | }
34 |
35 | .code-input_go-to-line_dialog input::placeholder {
36 | font-size: 80%;
37 | }
38 |
39 | .code-input_go-to-line_dialog input {
40 | position: relative;
41 | width: 240px; height: 32px; top: -3px;
42 | font-size: large;
43 | color: #000000aa;
44 | border: 0;
45 | }
46 |
47 | .code-input_go-to-line_dialog input.code-input_go-to-line_error {
48 | color: #ff0000aa;
49 | }
50 |
51 | .code-input_go-to-line_dialog input:focus {
52 | outline: none;
53 | }
54 |
55 | /* Cancel icon */
56 | .code-input_go-to-line_dialog span {
57 | display: inline-block;
58 | width: 24px;
59 | line-height: 24px;
60 | font-family: system-ui;
61 | font-size: 22px;
62 | font-weight: 500;
63 | text-align: center;
64 | border-radius: 50%;
65 | color: black;
66 | opacity: 0.6;
67 | vertical-align: top;
68 | }
69 |
70 | .code-input_go-to-line_dialog span:before {
71 | content: "\00d7";
72 | }
73 |
74 | .code-input_go-to-line_dialog span:hover {
75 | opacity: .8;
76 | background-color: #00000018;
77 | }
78 |
--------------------------------------------------------------------------------
/plugins/go-to-line.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Add basic Go-To-Line (Ctrl+G by default) functionality to the code editor.
3 | * Files: go-to-line.js / go-to-line.css
4 | */
5 | codeInput.plugins.GoToLine = class extends codeInput.Plugin {
6 | useCtrlG = false;
7 |
8 | /**
9 | * Create a go-to-line command plugin to pass into a template
10 | * @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? If not, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
11 | */
12 | constructor(useCtrlG = true) {
13 | super([]); // No observed attributes
14 | this.useCtrlG = useCtrlG;
15 | }
16 |
17 | /* Add keystroke events */
18 | afterElementsAdded(codeInput) {
19 | const textarea = codeInput.textareaElement;
20 | if(this.useCtrlG) {
21 | textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); });
22 | }
23 | }
24 |
25 | /* Called with a dialog box keyup event to check the validity of the line number entered and submit the dialog if Enter is pressed */
26 | checkPrompt(dialog, event) {
27 | // Line number(:column number)
28 | const lines = dialog.textarea.value.split('\n');
29 | const maxLineNo = lines.length;
30 | const lineNo = Number(dialog.input.value.split(':')[0]);
31 | let columnNo = 0; // Means go to start of indented line
32 | let maxColumnNo = 1;
33 | const querySplitByColons = dialog.input.value.split(':');
34 | if(querySplitByColons.length > 2) return dialog.input.classList.add('code-input_go-to-line_error');
35 |
36 | if (event.key == 'Escape') return this.cancelPrompt(dialog, event);
37 |
38 | if (dialog.input.value) {
39 | if (!/^[0-9:]*$/.test(dialog.input.value) || lineNo < 1 || lineNo > maxLineNo) {
40 | return dialog.input.classList.add('code-input_go-to-line_error');
41 | } else {
42 | // Check if line:column
43 | if(querySplitByColons.length >= 2) {
44 | columnNo = Number(querySplitByColons[1]);
45 | maxColumnNo = lines[lineNo-1].length;
46 | }
47 | if(columnNo < 0 || columnNo > maxColumnNo) {
48 | return dialog.input.classList.add('code-input_go-to-line_error');
49 | } else {
50 | dialog.input.classList.remove('code-input_go-to-line_error');
51 | }
52 | }
53 | }
54 |
55 | if (event.key == 'Enter') {
56 | this.goTo(dialog.textarea, lineNo, columnNo);
57 | this.cancelPrompt(dialog, event);
58 | }
59 | }
60 |
61 | /* Called with a dialog box keyup event to close and clear the dialog box */
62 | cancelPrompt(dialog, event) {
63 | event.preventDefault();
64 | dialog.codeInput.handleEventsFromTextarea = false;
65 | dialog.textarea.focus();
66 | dialog.codeInput.handleEventsFromTextarea = true;
67 | dialog.setAttribute("inert", true); // Hide from keyboard navigation when closed.
68 | dialog.setAttribute("tabindex", -1); // Hide from keyboard navigation when closed.
69 | dialog.setAttribute("aria-hidden", true); // Hide from screen reader when closed.
70 |
71 | // Remove dialog after animation
72 | dialog.classList.add('code-input_go-to-line_hidden-dialog');
73 | dialog.input.value = "";
74 | }
75 |
76 | /**
77 | * Show a search-like dialog prompting line number.
78 | * @param {codeInput.CodeInput} codeInput the `` element.
79 | */
80 | showPrompt(codeInput) {
81 | if(codeInput.pluginData.goToLine == undefined || codeInput.pluginData.goToLine.dialog == undefined) {
82 | const textarea = codeInput.textareaElement;
83 |
84 | const dialog = document.createElement('div');
85 | const input = document.createElement('input');
86 | const cancel = document.createElement('span');
87 | cancel.setAttribute("tabindex", 0); // Visible to keyboard navigation
88 | cancel.setAttribute("title", "Close Dialog and Return to Editor");
89 |
90 | dialog.appendChild(input);
91 | dialog.appendChild(cancel);
92 |
93 | dialog.className = 'code-input_go-to-line_dialog';
94 | input.spellcheck = false;
95 | input.placeholder = "Line:Column / Line no. then Enter";
96 | dialog.codeInput = codeInput;
97 | dialog.textarea = textarea;
98 | dialog.input = input;
99 |
100 | input.addEventListener('keypress', (event) => {
101 | /* Stop enter from submitting form */
102 | if (event.key == 'Enter') event.preventDefault();
103 | });
104 |
105 | input.addEventListener('keyup', (event) => { return this.checkPrompt(dialog, event); });
106 | cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, event); });
107 | cancel.addEventListener('keypress', (event) => { if (event.key == "Space" || event.key == "Enter") this.cancelPrompt(dialog, event); });
108 |
109 | codeInput.dialogContainerElement.appendChild(dialog);
110 | codeInput.pluginData.goToLine = {dialog: dialog};
111 | input.focus();
112 | } else {
113 | codeInput.pluginData.goToLine.dialog.classList.remove("code-input_go-to-line_hidden-dialog");
114 | codeInput.pluginData.goToLine.dialog.removeAttribute("inert"); // Show to keyboard navigation when open.
115 | codeInput.pluginData.goToLine.dialog.setAttribute("tabindex", 0); // Show to keyboard navigation when open.
116 | codeInput.pluginData.goToLine.dialog.removeAttribute("aria-hidden"); // Show to screen reader when open.
117 | codeInput.pluginData.goToLine.dialog.input.focus();
118 | }
119 | }
120 |
121 | /* Set the cursor on the first non-space char of textarea's nth line, or to the columnNo-numbered character in the line if it's not 0; and scroll it into view */
122 | goTo(textarea, lineNo, columnNo = 0) {
123 | let fontSize;
124 | let lineHeight;
125 | let scrollAmount;
126 | let topPadding;
127 | let cursorPos = -1;
128 | let lines = textarea.value.split('\n');
129 |
130 | if (lineNo > 0 && lineNo <= lines.length) {
131 | if (textarea.computedStyleMap) {
132 | fontSize = textarea.computedStyleMap().get('font-size').value;
133 | lineHeight = fontSize * textarea.computedStyleMap().get('line-height').value;
134 | } else {
135 | fontSize = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('font-size').split('px')[0];
136 | lineHeight = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('line-height').split('px')[0];
137 | }
138 |
139 | // scroll amount and initial top padding (3 lines above, if possible)
140 | scrollAmount = (lineNo > 3 ? lineNo - 3 : 1) * lineHeight;
141 | topPadding = (lineHeight - fontSize) / 2;
142 |
143 | if (lineNo > 1) {
144 | // cursor positon just after n - 1 full lines
145 | cursorPos = lines.slice(0, lineNo - 1).join('\n').length;
146 | }
147 |
148 | // scan first non-space char in nth line
149 | if (columnNo == 0) {
150 | do cursorPos++; while (textarea.value[cursorPos] != '\n' && /\s/.test(textarea.value[cursorPos]));
151 | } else {
152 | cursorPos += 1 + columnNo - 1;
153 | }
154 |
155 | textarea.scrollTop = scrollAmount - topPadding;
156 | textarea.setSelectionRange(cursorPos, cursorPos);
157 | textarea.click();
158 | }
159 | }
160 |
161 | /* Event handler for keydown event that makes Ctrl+G open go to line dialog */
162 | checkCtrlG(codeInput, event) {
163 | if (event.ctrlKey && event.key == 'g') {
164 | event.preventDefault();
165 | this.showPrompt(codeInput);
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/plugins/go-to-line.min.css:
--------------------------------------------------------------------------------
1 | @keyframes code-input_go-to-line_roll-in{0%{opacity:0;transform:translateY(-34px)}100%{opacity:1;transform:translateY(0)}}@keyframes code-input_go-to-line_roll-out{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-34px)}}.code-input_go-to-line_dialog{position:absolute;top:0;right:14px;height:28px;padding:6px;padding-top:8px;border:solid 1px #00000044;background-color:#fff;border-radius:6px;box-shadow:0 .2em 1em .2em rgba(0,0,0,.16)}.code-input_go-to-line_dialog:not(.code-input_go-to-line_hidden-dialog){animation:code-input_go-to-line_roll-in .2s;opacity:1;pointer-events:all}.code-input_go-to-line_dialog.code-input_go-to-line_hidden-dialog{animation:code-input_go-to-line_roll-out .2s;opacity:0;pointer-events:none}.code-input_go-to-line_dialog input::placeholder{font-size:80%}.code-input_go-to-line_dialog input{position:relative;width:240px;height:32px;top:-3px;font-size:large;color:#000000aa;border:0}.code-input_go-to-line_dialog input.code-input_go-to-line_error{color:#ff0000aa}.code-input_go-to-line_dialog input:focus{outline:0}.code-input_go-to-line_dialog span{display:inline-block;width:24px;line-height:24px;font-family:system-ui;font-size:22px;font-weight:500;text-align:center;border-radius:50%;color:#000;opacity:.6;vertical-align:top}.code-input_go-to-line_dialog span:before{content:"\00d7"}.code-input_go-to-line_dialog span:hover{opacity:.8;background-color:#00000018}
--------------------------------------------------------------------------------
/plugins/go-to-line.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.GoToLine=class extends codeInput.Plugin{useCtrlG=!1;constructor(a=!0){super([]),this.useCtrlG=a}afterElementsAdded(a){const b=a.textareaElement;this.useCtrlG&&b.addEventListener("keydown",b=>{this.checkCtrlG(a,b)})}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||e>d)return a.input.classList.add("code-input_go-to-line_error");if(2<=h.length&&(f=+h[1],g=c[e-1].length),0>f||f>g)return a.input.classList.add("code-input_go-to-line_error");a.input.classList.remove("code-input_go-to-line_error")}"Enter"==b.key&&(this.goTo(a.textarea,e,f),this.cancelPrompt(a,b))}cancelPrompt(a,b){b.preventDefault(),a.codeInput.handleEventsFromTextarea=!1,a.textarea.focus(),a.codeInput.handleEventsFromTextarea=!0,a.setAttribute("inert",!0),a.setAttribute("tabindex",-1),a.setAttribute("aria-hidden",!0),a.classList.add("code-input_go-to-line_hidden-dialog"),a.input.value=""}showPrompt(a){if(a.pluginData.goToLine==null||a.pluginData.goToLine.dialog==null){const b=a.textareaElement,c=document.createElement("div"),d=document.createElement("input"),e=document.createElement("span");e.setAttribute("tabindex",0),e.setAttribute("title","Close Dialog and Return to Editor"),c.appendChild(d),c.appendChild(e),c.className="code-input_go-to-line_dialog",d.spellcheck=!1,d.placeholder="Line:Column / Line no. then Enter",c.codeInput=a,c.textarea=b,c.input=d,d.addEventListener("keypress",a=>{"Enter"==a.key&&a.preventDefault()}),d.addEventListener("keyup",a=>this.checkPrompt(c,a)),e.addEventListener("click",a=>{this.cancelPrompt(c,a)}),e.addEventListener("keypress",a=>{("Space"==a.key||"Enter"==a.key)&&this.cancelPrompt(c,a)}),a.dialogContainerElement.appendChild(c),a.pluginData.goToLine={dialog:c},d.focus()}else a.pluginData.goToLine.dialog.classList.remove("code-input_go-to-line_hidden-dialog"),a.pluginData.goToLine.dialog.removeAttribute("inert"),a.pluginData.goToLine.dialog.setAttribute("tabindex",0),a.pluginData.goToLine.dialog.removeAttribute("aria-hidden"),a.pluginData.goToLine.dialog.input.focus()}goTo(a,b,c=0){let d,e,f,g,h=-1,i=a.value.split("\n");if(0 { if(this.escTabToChangeFocus) codeInput.setKeyboardNavInstructions("Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.", true); })
53 | textarea.addEventListener('keydown', (event) => { this.checkTab(codeInput, event); this.checkEnter(codeInput, event); this.checkBackspace(codeInput, event); });
54 | textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); });
55 |
56 | // Get the width of the indentation in pixels
57 | let testIndentationWidthPre = document.createElement("pre");
58 | testIndentationWidthPre.setAttribute("aria-hidden", "true"); // Hide for screen readers
59 | let testIndentationWidthSpan = document.createElement("span");
60 | if(codeInput.template.preElementStyled) {
61 | testIndentationWidthPre.appendChild(testIndentationWidthSpan);
62 | testIndentationWidthPre.classList.add("code-input_autocomplete_test-indentation-width");
63 | codeInput.appendChild(testIndentationWidthPre); // Styled like first pre, but first pre found to update
64 | } else {
65 | let testIndentationWidthCode = document.createElement("code");
66 | testIndentationWidthCode.appendChild(testIndentationWidthSpan);
67 | testIndentationWidthCode.classList.add("code-input_autocomplete_test-indentation-width");
68 | testIndentationWidthPre.appendChild(testIndentationWidthCode);
69 | codeInput.appendChild(testIndentationWidthPre); // Styled like first pre, but first pre found to update
70 | }
71 |
72 | testIndentationWidthSpan.innerHTML = codeInput.escapeHtml(this.indentation);
73 | let indentationWidthPx = testIndentationWidthSpan.offsetWidth;
74 | codeInput.removeChild(testIndentationWidthPre);
75 |
76 | codeInput.pluginData.indent = {indentationWidthPx: indentationWidthPx};
77 | }
78 |
79 | /* Deal with the Tab key causing indentation, and Tab+Selection indenting / Shift+Tab+Selection unindenting lines, and the mechanism through which Tab can be used to switch focus instead (accessibility). */
80 | checkTab(codeInput, event) {
81 | if(!this.tabIndentationEnabled) return;
82 | if(this.escTabToChangeFocus) {
83 | // Accessibility - allow Tab for keyboard navigation when Esc pressed right before it.
84 | if(event.key == "Escape") {
85 | this.escJustPressed = true;
86 | codeInput.setKeyboardNavInstructions("Tab and Shift-Tab currently for keyboard navigation. Type to return to indentation.", false);
87 | return;
88 | } else if(event.key != "Tab") {
89 | if(event.key == "Shift") {
90 | return; // Shift+Tab after Esc should still be keyboard navigation
91 | }
92 | codeInput.setKeyboardNavInstructions("Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.", false);
93 | this.escJustPressed = false;
94 | return;
95 | }
96 |
97 | if(!this.enableTabIndentation || this.escJustPressed) {
98 | codeInput.setKeyboardNavInstructions("", false);
99 | this.escJustPressed = false;
100 | return;
101 | }
102 | } else if(event.key != "Tab") {
103 | return;
104 | }
105 |
106 | let inputElement = codeInput.textareaElement;
107 | event.preventDefault(); // stop normal
108 |
109 | if(!event.shiftKey && inputElement.selectionStart == inputElement.selectionEnd) {
110 | // Just place a tab/spaces here.
111 | document.execCommand("insertText", false, this.indentation);
112 |
113 | } else {
114 | let lines = inputElement.value.split("\n");
115 | let letterI = 0;
116 |
117 | let selectionStartI = inputElement.selectionStart; // where cursor moves after tab - moving forward by 1 indent
118 | let selectionEndI = inputElement.selectionEnd; // where cursor moves after tab - moving forward by 1 indent
119 |
120 | for (let i = 0; i < lines.length; i++) {
121 | if((selectionStartI <= letterI+lines[i].length && selectionEndI >= letterI + 1)
122 | || (selectionStartI == selectionEndI && selectionStartI <= letterI+lines[i].length+1 && selectionEndI >= letterI)) { // + 1 so newlines counted
123 | // Starts before or at last char and ends after or at first char
124 | if(event.shiftKey) {
125 | if(lines[i].substring(0, this.indentationNumChars) == this.indentation) {
126 | // Remove first indent
127 | inputElement.selectionStart = letterI;
128 | inputElement.selectionEnd = letterI+this.indentationNumChars;
129 | document.execCommand("delete", false, "");
130 |
131 | // Change selection
132 | if(selectionStartI > letterI) { // Indented outside selection
133 | selectionStartI = Math.max(selectionStartI - this.indentationNumChars, letterI); // Don't move to before indent
134 | }
135 | selectionEndI -= this.indentationNumChars;
136 | letterI -= this.indentationNumChars;
137 | }
138 | } else {
139 | // Add tab at start
140 | inputElement.selectionStart = letterI;
141 | inputElement.selectionEnd = letterI;
142 | document.execCommand("insertText", false, this.indentation);
143 |
144 | // Change selection
145 | if(selectionStartI > letterI) { // Indented outside selection
146 | selectionStartI += this.indentationNumChars;
147 | }
148 | selectionEndI += this.indentationNumChars;
149 | letterI += this.indentationNumChars;
150 | }
151 | }
152 |
153 | letterI += lines[i].length+1; // newline counted
154 | }
155 |
156 | // move cursor
157 | inputElement.selectionStart = selectionStartI;
158 | inputElement.selectionEnd = selectionEndI;
159 |
160 | // move scroll position to follow code
161 | if(event.shiftKey) {
162 | codeInput.scrollBy(-codeInput.pluginData.indent.indentationWidthPx, 0);
163 | } else {
164 | codeInput.scrollBy(codeInput.pluginData.indent.indentationWidthPx, 0);
165 | }
166 | }
167 |
168 | codeInput.value = inputElement.value;
169 | }
170 |
171 | /* Deal with new lines retaining indentation */
172 | checkEnter(codeInput, event) {
173 | if(event.key != "Enter") {
174 | return;
175 | }
176 | event.preventDefault(); // Stop normal \n only
177 |
178 | let inputElement = codeInput.textareaElement;
179 | let lines = inputElement.value.split("\n");
180 | let letterI = 0;
181 | let currentLineI = lines.length - 1;
182 | let newLine = "";
183 | let numberIndents = 0;
184 |
185 | // find the index of the line our cursor is currently on
186 | for (let i = 0; i < lines.length; i++) {
187 | letterI += lines[i].length + 1;
188 | if(inputElement.selectionEnd <= letterI) {
189 | currentLineI = i;
190 | break;
191 | }
192 | }
193 |
194 | // count the number of indents the current line starts with (up to our cursor position in the line)
195 | let cursorPosInLine = lines[currentLineI].length - (letterI - inputElement.selectionEnd) + 1;
196 | for (let i = 0; i < cursorPosInLine; i += this.indentationNumChars) {
197 | if (lines[currentLineI].substring(i, i+this.indentationNumChars) == this.indentation) {
198 | numberIndents++;
199 | } else {
200 | break;
201 | }
202 | }
203 |
204 | // determine the text before and after the cursor and chop the current line at the new line break
205 | let textAfterCursor = "";
206 | if (cursorPosInLine != lines[currentLineI].length) {
207 | textAfterCursor = lines[currentLineI].substring(cursorPosInLine);
208 | lines[currentLineI] = lines[currentLineI].substring(0, cursorPosInLine);
209 | }
210 |
211 | let bracketThreeLinesTriggered = false;
212 | let furtherIndentation = "";
213 | if(this.bracketPairs != null) {
214 | for(let openingBracket in this.bracketPairs) {
215 | if(lines[currentLineI][lines[currentLineI].length-1] == openingBracket) {
216 | let closingBracket = this.bracketPairs[openingBracket];
217 | if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) {
218 | // Create new line and then put textAfterCursor on yet another line:
219 | // {
220 | // |CARET|
221 | // }
222 | bracketThreeLinesTriggered = true;
223 | for (let i = 0; i < numberIndents+1; i++) {
224 | furtherIndentation += this.indentation;
225 | }
226 | } else {
227 | // Just create new line:
228 | // {
229 | // |CARET|
230 | numberIndents++;
231 | }
232 | break;
233 | } else {
234 | // Check whether brackets cause unindent
235 | let closingBracket = this.bracketPairs[openingBracket];
236 | if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) {
237 | numberIndents--;
238 | break;
239 | }
240 | }
241 | }
242 | }
243 |
244 | // insert our indents and any text from the previous line that might have been after the line break
245 | for (let i = 0; i < numberIndents; i++) {
246 | newLine += this.indentation;
247 | }
248 |
249 | // save the current cursor position
250 | let selectionStartI = inputElement.selectionStart;
251 |
252 | if(bracketThreeLinesTriggered) {
253 | document.execCommand("insertText", false, "\n" + furtherIndentation); // Write indented line
254 | numberIndents += 1; // Reflects the new indent
255 | }
256 | document.execCommand("insertText", false, "\n" + newLine); // Write new line, including auto-indentation
257 |
258 | // move cursor to new position
259 | inputElement.selectionStart = selectionStartI + numberIndents*this.indentationNumChars + 1; // count the indent level and the newline character
260 | inputElement.selectionEnd = inputElement.selectionStart;
261 |
262 |
263 | // Scroll down to cursor if necessary
264 | let paddingTop = Number(getComputedStyle(inputElement).paddingTop.replace("px", ""));
265 | let lineHeight = Number(getComputedStyle(inputElement).lineHeight.replace("px", ""));
266 | let inputHeight = Number(getComputedStyle(codeInput).height.replace("px", ""));
267 | if(currentLineI*lineHeight + lineHeight*2 + paddingTop >= inputElement.scrollTop + inputHeight) { // Cursor too far down
268 | codeInput.scrollBy(0, Number(getComputedStyle(inputElement).lineHeight.replace("px", "")));
269 | }
270 |
271 | codeInput.value = inputElement.value;
272 | }
273 |
274 | /* Deal with one 'tab' of spaces-based-indentation being deleted by each backspace, rather than one space */
275 | checkBackspace(codeInput, event) {
276 | if(event.key != "Backspace" || this.indentationNumChars == 1) {
277 | return; // Normal backspace when indentation of 1
278 | }
279 |
280 | let inputElement = codeInput.textareaElement;
281 |
282 | if(inputElement.selectionStart == inputElement.selectionEnd && codeInput.value.substring(inputElement.selectionStart - this.indentationNumChars, inputElement.selectionStart) == this.indentation) {
283 | // Indentation before cursor = delete it
284 | inputElement.selectionStart -= this.indentationNumChars;
285 | event.preventDefault();
286 | document.execCommand("delete", false, "");
287 | }
288 | }
289 |
290 | /* Deal with the typing of closing brackets causing a decrease in indentation */
291 | checkCloseBracket(codeInput, event) {
292 | if(codeInput.textareaElement.selectionStart != codeInput.textareaElement.selectionEnd) {
293 | return;
294 | }
295 |
296 | for(let openingBracket in this.bracketPairs) {
297 | let closingBracket = this.bracketPairs[openingBracket];
298 | if(event.data == closingBracket) {
299 | // Closing bracket unindents line
300 | if(codeInput.value.substring(codeInput.textareaElement.selectionStart - this.indentationNumChars, codeInput.textareaElement.selectionStart) == this.indentation) {
301 | // Indentation before cursor = delete it
302 | codeInput.textareaElement.selectionStart -= this.indentationNumChars;
303 | document.execCommand("delete", false, "");
304 | }
305 | }
306 | }
307 | }
308 | }
--------------------------------------------------------------------------------
/plugins/indent.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.Indent=class extends codeInput.Plugin{bracketPairs={};indentation="\t";indentationNumChars=1;tabIndentationEnabled=!0;escTabToChangeFocus=!0;escJustPressed=!1;constructor(a=!1,b=4,c={"(":")","[":"]","{":"}"},d=!0){if(super([]),this.bracketPairs=c,a){this.indentation="";for(let a=0;a{this.escTabToChangeFocus&&a.setKeyboardNavInstructions("Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.",!0)}),b.addEventListener("keydown",b=>{this.checkTab(a,b),this.checkEnter(a,b),this.checkBackspace(a,b)}),b.addEventListener("beforeinput",b=>{this.checkCloseBracket(a,b)});let c=document.createElement("pre");c.setAttribute("aria-hidden","true");let d=document.createElement("span");if(a.template.preElementStyled)c.appendChild(d),c.classList.add("code-input_autocomplete_test-indentation-width"),a.appendChild(c);else{let b=document.createElement("code");b.appendChild(d),b.classList.add("code-input_autocomplete_test-indentation-width"),c.appendChild(b),a.appendChild(c)}d.innerHTML=a.escapeHtml(this.indentation);let e=d.offsetWidth;a.removeChild(c),a.pluginData.indent={indentationWidthPx:e}}checkTab(a,b){var c=Math.max;if(this.tabIndentationEnabled){if(this.escTabToChangeFocus){if("Escape"==b.key)return this.escJustPressed=!0,void a.setKeyboardNavInstructions("Tab and Shift-Tab currently for keyboard navigation. Type to return to indentation.",!1);if("Tab"!=b.key)return"Shift"==b.key?void 0:(a.setKeyboardNavInstructions("Tab and Shift-Tab currently for indentation. Press Esc to enable keyboard navigation.",!1),void(this.escJustPressed=!1));if(!this.enableTabIndentation||this.escJustPressed)return a.setKeyboardNavInstructions("",!1),void(this.escJustPressed=!1)}else if("Tab"!=b.key)return;let d=a.textareaElement;if(b.preventDefault(),!b.shiftKey&&d.selectionStart==d.selectionEnd)document.execCommand("insertText",!1,this.indentation);else{let e=d.value.split("\n"),f=0,g=d.selectionStart,h=d.selectionEnd;for(let a=0;a=f+1||g==h&&g<=f+e[a].length+1&&h>=f)&&(b.shiftKey?e[a].substring(0,this.indentationNumChars)==this.indentation&&(d.selectionStart=f,d.selectionEnd=f+this.indentationNumChars,document.execCommand("delete",!1,""),g>f&&(g=c(g-this.indentationNumChars,f)),h-=this.indentationNumChars,f-=this.indentationNumChars):(d.selectionStart=f,d.selectionEnd=f,document.execCommand("insertText",!1,this.indentation),g>f&&(g+=this.indentationNumChars),h+=this.indentationNumChars,f+=this.indentationNumChars)),f+=e[a].length+1;d.selectionStart=g,d.selectionEnd=h,b.shiftKey?a.scrollBy(-a.pluginData.indent.indentationWidthPx,0):a.scrollBy(a.pluginData.indent.indentationWidthPx,0)}a.value=d.value}}checkEnter(a,b){if("Enter"!=b.key)return;b.preventDefault();let c=a.textareaElement,d=c.value.split("\n"),e=0,f=d.length-1,g="",h=0;for(let g=0;g=c.scrollTop+q&&a.scrollBy(0,+getComputedStyle(c).lineHeight.replace("px","")),a.value=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,""))}}};
--------------------------------------------------------------------------------
/plugins/prism-line-numbers.css:
--------------------------------------------------------------------------------
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-left: max(3.8em, var(--padding, 16px))!important;
11 | }
12 |
13 | /* Ensure pre code/textarea just wide enough to give 100% width with line numbers */
14 | code-input.line-numbers, .line-numbers code-input {
15 | grid-template-columns: calc(100% - max(0em, calc(3.8em - var(--padding, 16px))));
16 | }
--------------------------------------------------------------------------------
/plugins/prism-line-numbers.min.css:
--------------------------------------------------------------------------------
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-left:max(3.8em,var(--padding,16px))!important}.line-numbers code-input,code-input.line-numbers{grid-template-columns:calc(100% - max(0em,calc(3.8em - var(--padding,16px))))}
--------------------------------------------------------------------------------
/plugins/select-token-callbacks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Make tokens in the element that are included within the selected text of the
3 | * gain a CSS class while selected, or trigger JavaScript callbacks.
4 | * Files: select-token-callbacks.js
5 | */
6 | codeInput.plugins.SelectTokenCallbacks = class extends codeInput.Plugin {
7 | /**
8 | * Set up the behaviour of tokens text-selected in the `` element, and the exact definition of a token being text-selected.
9 | *
10 | * All parameters are optional. If you provide no arguments to the constructor, this will dynamically apply the "code-input_select-token-callbacks_selected" class to selected tokens only, for you to style via CSS.
11 | *
12 | * @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
13 | * @param {boolean} onlyCaretNotSelection If true, tokens will only be marked as selected when no text is selected but rather the caret is inside them (start of selection == end of selection). Default false.
14 | * @param {boolean} caretAtStartIsSelected Whether the caret or text selection's end being just before the first character of a token means said token is selected. Default true.
15 | * @param {boolean} caretAtEndIsSelected Whether the caret or text selection's start being just after the last character of a token means said token is selected. Default true.
16 | * @param {boolean} createSubTokens Whether temporary `` elements should be created inside partially-selected tokens containing just the selected text and given the selected class. Default false.
17 | * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
18 | * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
19 | */
20 | constructor(tokenSelectorCallbacks = codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(), onlyCaretNotSelection = false, caretAtStartIsSelected = true, caretAtEndIsSelected = true, createSubTokens = false, partiallySelectedTokensAreSelected = true, parentTokensAreSelected = true) {
21 | super([]); // No observed attributes
22 |
23 | this.tokenSelectorCallbacks = tokenSelectorCallbacks;
24 | this.onlyCaretNotSelection = onlyCaretNotSelection;
25 | this.caretAtStartIsSelected = caretAtStartIsSelected;
26 | this.caretAtEndIsSelected = caretAtEndIsSelected;
27 | this.createSubTokens = createSubTokens;
28 | this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
29 | this.parentTokensAreSelected = parentTokensAreSelected;
30 | }
31 | /* Runs after code is highlighted; Params: codeInput element) */
32 | afterHighlight(codeInputElement) {
33 | this.syncSelection(codeInputElement);
34 | }
35 | /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
36 | afterElementsAdded(codeInputElement) {
37 | codeInputElement.pluginData.selectTokenCallbacks = {};
38 | codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
39 | codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
40 | codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState = new codeInput.plugins.SelectTokenCallbacks.SelectedTokenState(codeInputElement.codeElement, this.tokenSelectorCallbacks, this.onlyCaretNotSelection, this.caretAtStartIsSelected, this.caretAtEndIsSelected, this.createSubTokens, this.partiallySelectedTokensAreSelected, this.parentTokensAreSelected);
41 | this.syncSelection(codeInputElement);
42 |
43 | // As of 2024-08, the selectionchange event is only supported on Firefox.
44 | codeInputElement.textareaElement.addEventListener("selectionchange", () => {
45 | this.checkSelectionChanged(codeInputElement)
46 | });
47 | // When selectionchange has complete support, the listeners below can be deleted.
48 | codeInputElement.textareaElement.addEventListener("select", () => {
49 | this.checkSelectionChanged(codeInputElement)
50 | });
51 | codeInputElement.textareaElement.addEventListener("keypress", () => {
52 | this.checkSelectionChanged(codeInputElement)
53 | });
54 | codeInputElement.textareaElement.addEventListener("mousedown", () => {
55 | this.checkSelectionChanged(codeInputElement)
56 | });
57 | }
58 | /* If the text selection has changed, run syncSelection. */
59 | checkSelectionChanged(codeInputElement) {
60 | if(
61 | codeInputElement.textareaElement.selectionStart != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart
62 | || codeInputElement.textareaElement.selectionEnd != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd
63 | ) {
64 | this.syncSelection(codeInputElement);
65 | codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
66 | codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
67 | }
68 | }
69 | /* Update which elements have the code-input_selected class. */
70 | syncSelection(codeInputElement) {
71 | codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(codeInputElement.textareaElement.selectionStart, codeInputElement.textareaElement.selectionEnd)
72 | }
73 | }
74 |
75 | /**
76 | * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
77 | * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
78 | */
79 | codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks = class {
80 | /**
81 | * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
82 | *
83 | * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
84 | * @param {(token: HTMLElement) => void} tokenSelectedCallback Runs multiple times when the text selection inside the code-input changes, each time inputting a single (part of the highlighted ``) token element that is selected in the new text selection.
85 | * @param {(tokenContainer: HTMLElement) => void} selectChangedCallback Each time the text selection inside the code-input changes, runs once before any tokenSelectedCallback calls, inputting the highlighted ``'s `` element that contains all token elements.
86 | */
87 | constructor(tokenSelectedCallback, selectChangedCallback) {
88 | this.tokenSelectedCallback = tokenSelectedCallback;
89 | this.selectChangedCallback = selectChangedCallback;
90 | }
91 |
92 | /**
93 | * Use preset callbacks which ensure all tokens in the selected text range in the ``, and only such tokens, are given a certain CSS class.
94 | *
95 | * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
96 | *
97 | * @param {string} selectedClass The CSS class that will be present on tokens only when they are part of the selected text in the `` element. Defaults to "code-input_select-token-callbacks_selected".
98 | * @returns A new TokenSelectorCallbacks instance that encodes this behaviour.
99 | */
100 | static createClassSynchronisation(selectedClass = "code-input_select-token-callbacks_selected") {
101 | return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(
102 | (token) => {
103 | token.classList.add(selectedClass);
104 | },
105 | (tokenContainer) => {
106 | // Remove selected class
107 | let selectedClassTokens = tokenContainer.getElementsByClassName(selectedClass);
108 | // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
109 | while(selectedClassTokens.length > 0) {
110 | selectedClassTokens[0].classList.remove(selectedClass);
111 | }
112 | }
113 | );
114 | }
115 | }
116 |
117 | /* Manages a single element's selected tokens, and calling the correct functions on the selected tokens */
118 | codeInput.plugins.SelectTokenCallbacks.SelectedTokenState = class {
119 | constructor(codeElement, tokenSelectorCallbacks, onlyCaretNotSelection, caretAtStartIsSelected, caretAtEndIsSelected, createSubTokens, partiallySelectedTokensAreSelected, parentTokensAreSelected) {
120 | this.tokenContainer = codeElement;
121 | this.tokenSelectorCallbacks = tokenSelectorCallbacks;
122 | this.onlyCaretNotSelection = onlyCaretNotSelection;
123 | this.caretAtStartIsSelected = caretAtStartIsSelected;
124 | this.caretAtEndIsSelected = caretAtEndIsSelected;
125 | this.createSubTokens = createSubTokens;
126 | this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
127 | this.parentTokensAreSelected = parentTokensAreSelected;
128 | }
129 |
130 | /* Change the selected region to a new range from selectionStart to selectionEnd and run
131 | the callbacks. */
132 | updateSelection(selectionStart, selectionEnd) {
133 | this.selectChanged()
134 | if(!this.onlyCaretNotSelection || selectionStart == selectionEnd) { // Only deal with selected text if onlyCaretNotSelection is false.
135 | this.updateSelectedTokens(this.tokenContainer, selectionStart, selectionEnd)
136 | }
137 | }
138 | /* Runs when the text selection has changed, before any updateSelectedTokens call. */
139 | selectChanged() {
140 | if(this.createSubTokens) {
141 | // Remove generated spans to hold selected partial tokens
142 | let tempSpans = this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");
143 | while(tempSpans.length > 0) {
144 | // Replace with textContent as Text node
145 | // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
146 | tempSpans[0].parentElement.replaceChild(new Text(tempSpans[0].textContent), tempSpans[0]);
147 | }
148 | }
149 |
150 | this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer);
151 | }
152 |
153 | /* Do the desired behaviour for selection to all tokens (elements in the currentElement)
154 | from startIndex to endIndex in the text. Start from the currentElement as this function is recursive.
155 | This code is similar to codeInput.plugins.FindAndReplace.FindMatchState.highlightMatch*/
156 | updateSelectedTokens(currentElement, startIndex, endIndex) {
157 | if(endIndex < 0 || endIndex == 0 && !this.caretAtStartIsSelected) {
158 | return; // Nothing selected
159 | }
160 | if(this.parentTokensAreSelected && currentElement !== this.tokenContainer) {
161 | this.tokenSelectorCallbacks.tokenSelectedCallback(currentElement); // Parent elements also marked with class / have callback called
162 | }
163 | for(let i = 0; i < currentElement.childNodes.length; i++) {
164 | let childElement = currentElement.childNodes[i];
165 | let childText = childElement.textContent;
166 |
167 | let noInnerElements = false;
168 | if(childElement.nodeType == 3) {
169 | // Text node
170 | if(this.createSubTokens) {
171 | // Replace with token
172 | if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
173 | // Can merge with next text node
174 | currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
175 | currentElement.removeChild(childElement); // Delete this node
176 | i--; // As an element removed
177 | continue; // Move to next node
178 | }
179 | noInnerElements = true;
180 |
181 | let replacementElement = document.createElement("span");
182 | replacementElement.textContent = childText;
183 | replacementElement.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
184 |
185 | currentElement.replaceChild(replacementElement, childElement);
186 | childElement = replacementElement;
187 | } else {
188 | // Skip text node
189 | // Make indexes skip the element
190 | startIndex -= childText.length;
191 | endIndex -= childText.length;
192 | continue;
193 | }
194 | }
195 |
196 | if(startIndex <= 0) {
197 | // Started selection
198 | if(childText.length > endIndex) {
199 | // Selection ends in childElement
200 | if(this.partiallySelectedTokensAreSelected) {
201 | if(noInnerElements) {
202 | if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
203 | // Text node - add selection class to first part
204 | let startSpan = document.createElement("span");
205 | this.tokenSelectorCallbacks.tokenSelectedCallback(startSpan); // Selected
206 | startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
207 | startSpan.textContent = childText.substring(0, endIndex);
208 |
209 | let endText = childText.substring(endIndex);
210 | childElement.textContent = endText;
211 |
212 | childElement.insertAdjacentElement('beforebegin', startSpan);
213 | i++; // An extra element has been added
214 | }
215 | if(this.parentTokensAreSelected || !this.createSubTokens) {
216 | this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
217 | }
218 | } else {
219 | this.updateSelectedTokens(childElement, 0, endIndex);
220 | }
221 | }
222 |
223 | // Match ended - nothing to do after backtracking
224 | return;
225 | } else {
226 | // Match goes through child element
227 | this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
228 | }
229 | } else if(this.caretAtEndIsSelected && childText.length >= startIndex || childText.length > startIndex) {
230 | // Match starts in childElement
231 | if(this.partiallySelectedTokensAreSelected) {
232 | if(noInnerElements) {
233 | if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
234 | if(childText.length > endIndex) {
235 | // Match starts and ends in childElement - selection middle part
236 | let startSpan = document.createElement("span");
237 | startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
238 | startSpan.textContent = childText.substring(0, startIndex);
239 |
240 | let middleText = childText.substring(startIndex, endIndex);
241 | childElement.textContent = middleText;
242 | this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selection
243 |
244 | let endSpan = document.createElement("span");
245 | endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
246 | endSpan.textContent = childText.substring(endIndex);
247 |
248 | childElement.insertAdjacentElement('beforebegin', startSpan);
249 | childElement.insertAdjacentElement('afterend', endSpan);
250 | i++; // 2 extra elements have been added
251 | } else {
252 | // Match starts in element - highlight last part
253 | let startText = childText.substring(0, startIndex);
254 | childElement.textContent = startText;
255 |
256 | let endSpan = document.createElement("span");
257 | this.tokenSelectorCallbacks.tokenSelectedCallback(endSpan); // Selected
258 | endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
259 | endSpan.textContent = childText.substring(startIndex);
260 |
261 | childElement.insertAdjacentElement('afterend', endSpan);
262 | i++; // An extra element has been added
263 | }
264 | }
265 | if(this.parentTokensAreSelected || !this.createSubTokens) {
266 | this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
267 | }
268 | } else {
269 | this.updateSelectedTokens(childElement, startIndex, endIndex);
270 | }
271 | }
272 |
273 | if(this.caretAtStartIsSelected) {
274 | if(childText.length > endIndex) {
275 | // Match completely in childElement - nothing to do after backtracking
276 | return;
277 | }
278 | } else if(childText.length >= endIndex) {
279 | // Match completely in childElement - nothing to do after backtracking
280 | return;
281 | }
282 | }
283 |
284 | // Make indexes skip the element
285 | startIndex -= childText.length;
286 | endIndex -= childText.length;
287 | }
288 | }
289 | }
--------------------------------------------------------------------------------
/plugins/select-token-callbacks.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.SelectTokenCallbacks=class extends codeInput.Plugin{constructor(a=codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(),b=!1,c=!0,d=!0,e=!1,f=!0,g=!0){super([]),this.tokenSelectorCallbacks=a,this.onlyCaretNotSelection=b,this.caretAtStartIsSelected=c,this.caretAtEndIsSelected=d,this.createSubTokens=e,this.partiallySelectedTokensAreSelected=f,this.parentTokensAreSelected=g}afterHighlight(a){this.syncSelection(a)}afterElementsAdded(a){a.pluginData.selectTokenCallbacks={},a.pluginData.selectTokenCallbacks.lastSelectionStart=a.textareaElement.selectionStart,a.pluginData.selectTokenCallbacks.lastSelectionEnd=a.textareaElement.selectionEnd,a.pluginData.selectTokenCallbacks.selectedTokenState=new codeInput.plugins.SelectTokenCallbacks.SelectedTokenState(a.codeElement,this.tokenSelectorCallbacks,this.onlyCaretNotSelection,this.caretAtStartIsSelected,this.caretAtEndIsSelected,this.createSubTokens,this.partiallySelectedTokensAreSelected,this.parentTokensAreSelected),this.syncSelection(a),a.textareaElement.addEventListener("selectionchange",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("select",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("keypress",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("mousedown",()=>{this.checkSelectionChanged(a)})}checkSelectionChanged(a){(a.textareaElement.selectionStart!=a.pluginData.selectTokenCallbacks.lastSelectionStart||a.textareaElement.selectionEnd!=a.pluginData.selectTokenCallbacks.lastSelectionEnd)&&(this.syncSelection(a),a.pluginData.selectTokenCallbacks.lastSelectionStart=a.textareaElement.selectionStart,a.pluginData.selectTokenCallbacks.lastSelectionEnd=a.textareaElement.selectionEnd)}syncSelection(a){a.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(a.textareaElement.selectionStart,a.textareaElement.selectionEnd)}},codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks=class{constructor(a,b){this.tokenSelectedCallback=a,this.selectChangedCallback=b}static createClassSynchronisation(a="code-input_select-token-callbacks_selected"){return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(b=>{b.classList.add(a)},b=>{for(let c=b.getElementsByClassName(a);0c)&&(0!=c||this.caretAtStartIsSelected)){this.parentTokensAreSelected&&a!==this.tokenContainer&&this.tokenSelectorCallbacks.tokenSelectedCallback(a);for(let d=0;d=b){if(f.length>c){if(this.partiallySelectedTokensAreSelected)if(g){if(this.createSubTokens&&b!=c){let a=document.createElement("span");this.tokenSelectorCallbacks.tokenSelectedCallback(a),a.classList.add("code-input_select-token-callbacks_temporary-span"),a.textContent=f.substring(0,c);let b=f.substring(c);e.textContent=b,e.insertAdjacentElement("beforebegin",a),d++}(this.parentTokensAreSelected||!this.createSubTokens)&&this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else this.updateSelectedTokens(e,0,c);return}this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else if(this.caretAtEndIsSelected&&f.length>=b||f.length>b){if(this.partiallySelectedTokensAreSelected)if(g){if(this.createSubTokens&&b!=c)if(f.length>c){let a=document.createElement("span");a.classList.add("code-input_select-token-callbacks_temporary-span"),a.textContent=f.substring(0,b);let g=f.substring(b,c);e.textContent=g,this.tokenSelectorCallbacks.tokenSelectedCallback(e);let h=document.createElement("span");h.classList.add("code-input_select-token-callbacks_temporary-span"),h.textContent=f.substring(c),e.insertAdjacentElement("beforebegin",a),e.insertAdjacentElement("afterend",h),d++}else{let a=f.substring(0,b);e.textContent=a;let c=document.createElement("span");this.tokenSelectorCallbacks.tokenSelectedCallback(c),c.classList.add("code-input_select-token-callbacks_temporary-span"),c.textContent=f.substring(b),e.insertAdjacentElement("afterend",c),d++}(this.parentTokensAreSelected||!this.createSubTokens)&&this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else this.updateSelectedTokens(e,b,c);if(this.caretAtStartIsSelected){if(f.length>c)return;}else if(f.length>=c)return}b-=f.length,c-=f.length}}}};
--------------------------------------------------------------------------------
/plugins/special-chars.css:
--------------------------------------------------------------------------------
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 {
28 | display: inline-block;
29 | position: relative;
30 | top: 0;
31 | left: 0;
32 | height: 1em;
33 | /* width: set by JS */
34 | overflow: hidden;
35 | text-decoration: none;
36 | text-shadow: none;
37 | vertical-align: middle;
38 | outline: 0.1px solid currentColor;
39 |
40 | --hex-0: var(
41 | --code-input_special-chars_0);
42 | --hex-1: var(
43 | --code-input_special-chars_0);
44 | --hex-2: var(
45 | --code-input_special-chars_0);
46 | --hex-3: var(
47 | --code-input_special-chars_0);
48 | }
49 |
50 | /* Default - Two bytes - 4 hex chars */
51 |
52 | .code-input_special-char::before {
53 | margin-left: 50%;
54 | transform: translate(-50%, 0);
55 | content: " ";
56 |
57 | background-color: var(--code-input_special-char_color, currentColor);
58 | image-rendering: pixelated;
59 | display: inline-block;
60 | width: calc(100%-2px);
61 | height: 100%;
62 |
63 | mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
64 | mask-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
65 | mask-size: min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em);
66 | mask-position: 10% 10%, 90% 10%, 10% 90%, 90% 90%;
67 |
68 | -webkit-mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
69 | -webkit-mask-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
70 | -webkit-mask-size: min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em);
71 | -webkit-mask-position: 10% 10%, min(90%, 0.5em) 10%, 10% 90%, min(90%, 0.5em) 90%;
72 | }
73 |
74 | .code-input_special-char_zero-width {
75 | z-index: 1;
76 | width: 1em;
77 | margin-left: -0.5em;
78 | margin-right: -0.5em;
79 | position: relative;
80 |
81 | opacity: 0.75;
82 | }
83 |
84 | /* One byte - 2 hex chars */
85 | .code-input_special-char_one-byte::before {
86 | height: 1.5em;
87 | top: -1em;
88 | content: attr(data-hex2);
89 | }
90 | .code-input_special-char_one-byte::after {
91 | height: 1.5em;
92 | bottom: -1em;
93 | content: attr(data-hex3);
94 | }
--------------------------------------------------------------------------------
/plugins/special-chars.js:
--------------------------------------------------------------------------------
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 | codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
7 | specialCharRegExp;
8 |
9 | cachedColors; // ascii number > [background color, text color]
10 | cachedWidths; // character > character width
11 | canvasContext;
12 |
13 | /**
14 | * Create a special characters plugin instance.
15 | * Default = covers many non-renderable ASCII characters.
16 | * @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code
17 | * @param {Boolean} inheritTextColor If `inheritTextColor` is false, forces the color of the hex code to inherit from syntax highlighting. Otherwise, the base color of the `pre code` element is used to give contrast to the small characters.
18 | * @param {RegExp} specialCharRegExp The regular expression which matches special characters
19 | */
20 | constructor(colorInSpecialChars = false, inheritTextColor = false, specialCharRegExp = /(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug) { // By default, covers many non-renderable ASCII characters
21 | super([]); // No observed attributes
22 |
23 | this.specialCharRegExp = specialCharRegExp;
24 | this.colorInSpecialChars = colorInSpecialChars;
25 | this.inheritTextColor = inheritTextColor;
26 |
27 | this.cachedColors = {};
28 | this.cachedWidths = {};
29 |
30 | let canvas = document.createElement("canvas");
31 | this.canvasContext = canvas.getContext("2d");
32 | }
33 |
34 | /* Initially render special characters as the highlighting algorithm may automatically highlight and remove them */
35 | afterElementsAdded(codeInput) {
36 | setTimeout(() => { codeInput.value = codeInput.value; }, 100);
37 | }
38 |
39 | /* After highlighting, render special characters as their stylised hexadecimal equivalents */
40 | afterHighlight(codeInput) {
41 | let resultElement = codeInput.codeElement;
42 |
43 | // Reset data each highlight so can change if font size, etc. changes
44 | codeInput.pluginData.specialChars = {};
45 | codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(resultElement).color;
46 |
47 | this.recursivelyReplaceText(codeInput, resultElement);
48 |
49 | this.lastFont = window.getComputedStyle(codeInput.textareaElement).font;
50 | }
51 |
52 | /* Search for special characters in an element and replace them with their stylised hexadecimal equivalents */
53 | recursivelyReplaceText(codeInput, element) {
54 | for(let i = 0; i < element.childNodes.length; i++) {
55 |
56 | let nextNode = element.childNodes[i];
57 | if(nextNode.nodeType == 3) {
58 | // Text node - Replace in here
59 | let oldValue = nextNode.nodeValue;
60 |
61 | this.specialCharRegExp.lastIndex = 0;
62 | let searchResult = this.specialCharRegExp.exec(oldValue);
63 | if(searchResult != null) {
64 | let charIndex = searchResult.index; // Start as returns end
65 |
66 | nextNode = nextNode.splitText(charIndex+1).previousSibling;
67 |
68 | if(charIndex > 0) {
69 | nextNode = nextNode.splitText(charIndex); // Keep characters before the special character in a different span
70 | }
71 |
72 | if(nextNode.textContent != "") {
73 | let replacementElement = this.getStylisedSpecialChar(codeInput, nextNode.textContent);
74 | // This next node will become the i+1th node so automatically iterated to
75 | nextNode.parentNode.insertBefore(replacementElement, nextNode);
76 | nextNode.textContent = "";
77 | }
78 | }
79 | } else if(nextNode.nodeType == 1) {
80 | if(nextNode.className != "code-input_special-char" && nextNode.nodeValue != "") {
81 | // Element - recurse
82 | this.recursivelyReplaceText(codeInput, nextNode);
83 | }
84 | }
85 | }
86 | }
87 |
88 | /* Get the stylised hexadecimal representation HTML element for a given special character */
89 | getStylisedSpecialChar(codeInput, matchChar) {
90 | let hexCode = matchChar.codePointAt(0);
91 |
92 | let colors;
93 | if(this.colorInSpecialChars) colors = this.getCharacterColors(hexCode);
94 |
95 | hexCode = hexCode.toString(16);
96 | hexCode = ("0000" + hexCode).substring(hexCode.length); // So 2 chars with leading 0
97 | hexCode = hexCode.toUpperCase();
98 |
99 | let charWidth = this.getCharacterWidthEm(codeInput, matchChar);
100 |
101 | // Create element with hex code
102 | let result = document.createElement("span");
103 | result.classList.add("code-input_special-char");
104 | result.style.setProperty("--hex-0", "var(--code-input_special-chars_" + hexCode[0] + ")");
105 | result.style.setProperty("--hex-1", "var(--code-input_special-chars_" + hexCode[1] + ")");
106 | result.style.setProperty("--hex-2", "var(--code-input_special-chars_" + hexCode[2] + ")");
107 | result.style.setProperty("--hex-3", "var(--code-input_special-chars_" + hexCode[3] + ")");
108 |
109 | // Handle zero-width chars
110 | if(charWidth == 0) result.classList.add("code-input_special-char_zero-width");
111 | else result.style.width = charWidth + "em";
112 |
113 | if(this.colorInSpecialChars) {
114 | result.style.backgroundColor = "#" + colors[0];
115 | result.style.setProperty("--code-input_special-char_color", colors[1]);
116 | } else if(!this.inheritTextColor) {
117 | result.style.setProperty("--code-input_special-char_color", codeInput.pluginData.specialChars.contrastColor);
118 | }
119 | return result;
120 | }
121 |
122 | /* Get the colors a stylised representation of a given character must be shown in; lazy load and return [background color, text color] */
123 | getCharacterColors(asciiCode) {
124 | let textColor;
125 | if(!(asciiCode in this.cachedColors)) {
126 | // Get background color
127 | let asciiHex = asciiCode.toString(16);
128 | let backgroundColor = "";
129 | for(let i = 0; i < asciiHex.length; i++) {
130 | backgroundColor += asciiHex[i] + asciiHex[i];
131 | }
132 | backgroundColor = ("000000" + backgroundColor).substring(backgroundColor.length); // So valid HEX color with 6 characters
133 |
134 | // Get most suitable text color - white or black depending on background brightness
135 | let colorBrightness = 0;
136 | const luminanceCoefficients = [0.299, 0.587, 0.114];
137 | for(let i = 0; i < 6; i += 2) {
138 | colorBrightness += parseInt(backgroundColor.substring(i, i+2), 16) * luminanceCoefficients[i/2];
139 | }
140 | // Calculate darkness
141 | textColor = colorBrightness < 128 ? "white" : "black";
142 |
143 | this.cachedColors[asciiCode] = [backgroundColor, textColor];
144 | return [backgroundColor, textColor];
145 | } else {
146 | return this.cachedColors[asciiCode];
147 | }
148 | }
149 |
150 | /* Get the width of a character in em (relative to font size), for use in creation of the stylised hexadecimal representation with the same width */
151 | getCharacterWidthEm(codeInput, char) {
152 | // Force zero-width characters
153 | if(new RegExp("\u00AD|\u02DE|[\u0300-\u036F]|[\u0483-\u0489]|[\u200B-\u200D]|\uFEFF").test(char) ) { return 0 }
154 | // Non-renderable ASCII characters should all be rendered at same size
155 | if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
156 | let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
157 | return fallbackWidth;
158 | }
159 |
160 | let font = getComputedStyle(codeInput.textareaElement).fontFamily + " " + getComputedStyle(codeInput.textareaElement).fontStretch + " " + getComputedStyle(codeInput.textareaElement).fontStyle + " " + getComputedStyle(codeInput.textareaElement).fontVariant + " " + getComputedStyle(codeInput.textareaElement).fontWeight + " " + getComputedStyle(codeInput.textareaElement).lineHeight; // Font without size
161 |
162 | // Lazy-load width of each character
163 | if(this.cachedWidths[font] == undefined) {
164 | this.cachedWidths[font] = {};
165 | }
166 | if(this.cachedWidths[font][char] != undefined) { // Use cached width
167 | return this.cachedWidths[font][char];
168 | }
169 |
170 | // Ensure font the same - 20px font size is where this algorithm works
171 | this.canvasContext.font = getComputedStyle(codeInput.textareaElement).font.replace(getComputedStyle(codeInput.textareaElement).fontSize, "20px");
172 |
173 | // Try to get width from canvas
174 | let width = this.canvasContext.measureText(char).width/20; // From px to em (=proportion of font-size)
175 | if(width > 1) {
176 | width /= 2; // Fix double-width-in-canvas Firefox bug
177 | } else if(width == 0 && char != "\u0096") {
178 | let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
179 | return fallbackWidth; // In Firefox some control chars don't render, but all control chars are the same width
180 | }
181 |
182 | // Firefox will never make smaller than size at 20px
183 | if(navigator.userAgent.includes("Mozilla") && !navigator.userAgent.includes("Chrome") && !navigator.userAgent.includes("Safari")) {
184 | let fontSize = Number(getComputedStyle(codeInput.textareaElement).fontSize.substring(0, getComputedStyle(codeInput.textareaElement).fontSize.length-2)); // Remove 20, make px
185 | if(fontSize < 20) width *= 20 / fontSize;
186 | }
187 |
188 | this.cachedWidths[font][char] = width;
189 |
190 | return width;
191 | }
192 | }
--------------------------------------------------------------------------------
/plugins/special-chars.min.css:
--------------------------------------------------------------------------------
1 | :root,body{--code-input_special-chars_0:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdjZGBgYPj///9/RhCAMcA0bg6yHgAPmh/6BoxTcQAAAABJRU5ErkJgggAA');--code-input_special-chars_1:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdjZGBgYPj///9/RhAggwMAitIUBr9U6sYAAAAASUVORK5CYII=');--code-input_special-chars_2:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGCEMUCCjCgyYBFGRrAKFBkAuLYT9kYcIu0AAAAASUVORK5CYII=');--code-input_special-chars_3:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABhJREFUGFdj/P///38GKGCEMUCCjMTJAACYiBPyG8sfAgAAAABJRU5ErkJggg==');--code-input_special-chars_4:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///39GRkZGMI3BYYACRhgDrAKZAwAYxhvyz0DRIQAAAABJRU5ErkJggg==');--code-input_special-chars_5:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACJJREFUGFdj/P///38GKGAEcRgZGRlBfDAHLgNjgFUgywAAuR4T9hxJl2YAAAAASUVORK5CYII=');--code-input_special-chars_6:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdj/P///38GKGAEcRgZGRlBfDAHQwasAlkGABcdF/Y4yco2AAAAAElFTkSuQmCC');--code-input_special-chars_7:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdj/P///38GKGCEMUCCRHIAWMgT8kue3bQAAAAASUVORK5CYII=');--code-input_special-chars_8:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GKGAEcRgZGSE0cTIAvHcb8v+mIfAAAAAASUVORK5CYII=');--code-input_special-chars_9:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGAEcRgZGSE0igxMCVgGmQMAPqcX8hWL1K0AAAAASUVORK5CYII=');--code-input_special-chars_A:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdjZGBgYPj///9/RhCAMcA0iADJggCmDEw5ALdxH/aGuYHqAAAAAElFTkSuQmCC');--code-input_special-chars_B:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GBgYGRhAAceA0cTIAvc0b/vRDnVoAAAAASUVORK5CYII=');--code-input_special-chars_C:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdjZGBgYPj///9/EM0IYjAyMjIS4CDrAQC57hP+uLwvFQAAAABJRU5ErkJggg==');--code-input_special-chars_D:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdj/P///38GBgYGRhAAceA0fg5MDwAveh/6ToN9VwAAAABJRU5ErkJggg==');--code-input_special-chars_E:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABxJREFUGFdj/P///38GKGAEcRgZGRlBfDCHsAwA2UwT+mVIH1MAAAAASUVORK5CYII=');--code-input_special-chars_F:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==')}.code-input_special-char{display:inline-block;position:relative;top:0;left:0;height:1em;overflow:hidden;text-decoration:none;text-shadow:none;vertical-align:middle;outline:.1px solid currentColor;--hex-0:var(
2 | --code-input_special-chars_0);--hex-1:var(
3 | --code-input_special-chars_0);--hex-2:var(
4 | --code-input_special-chars_0);--hex-3:var(
5 | --code-input_special-chars_0)}.code-input_special-char::before{margin-left:50%;transform:translate(-50%,0);content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;display:inline-block;width:calc(100%-2px);height:100%;mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);mask-position:10% 10%,90% 10%,10% 90%,90% 90%;-webkit-mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);-webkit-mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;-webkit-mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);-webkit-mask-position:10% 10%,min(90%,.5em) 10%,10% 90%,min(90%,.5em) 90%}.code-input_special-char_zero-width{z-index:1;width:1em;margin-left:-.5em;margin-right:-.5em;position:relative;opacity:.75}.code-input_special-char_one-byte::before{height:1.5em;top:-1em;content:attr(data-hex2)}.code-input_special-char_one-byte::after{height:1.5em;bottom:-1em;content:attr(data-hex3)}
--------------------------------------------------------------------------------
/plugins/special-chars.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.SpecialChars=class extends codeInput.Plugin{specialCharRegExp;cachedColors;cachedWidths;canvasContext;constructor(a=!1,b=!1,c=/(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug){super([]),this.specialCharRegExp=c,this.colorInSpecialChars=a,this.inheritTextColor=b,this.cachedColors={},this.cachedWidths={};let d=document.createElement("canvas");this.canvasContext=d.getContext("2d")}afterElementsAdded(a){setTimeout(()=>{a.value=a.value},100)}afterHighlight(a){let b=a.codeElement;a.pluginData.specialChars={},a.pluginData.specialChars.contrastColor=window.getComputedStyle(b).color,this.recursivelyReplaceText(a,b),this.lastFont=window.getComputedStyle(a.textareaElement).font}recursivelyReplaceText(a,b){for(let c,d=0;da;a+=2)e+=parseInt(d.substring(a,a+2),16)*f[a/2];return b=128>e?"white":"black",this.cachedColors[a]=[d,b],[d,b]}return this.cachedColors[a]}getCharacterWidthEm(a,b){if(/|˞|[̀-ͯ]|[҃-҉]|[-]|/.test(b))return 0;if("\x96"!=b&&/[\0-]|[-]/g.test(b)){let b=this.getCharacterWidthEm(a,"\x96");return b}let c=getComputedStyle(a.textareaElement).fontFamily+" "+getComputedStyle(a.textareaElement).fontStretch+" "+getComputedStyle(a.textareaElement).fontStyle+" "+getComputedStyle(a.textareaElement).fontVariant+" "+getComputedStyle(a.textareaElement).fontWeight+" "+getComputedStyle(a.textareaElement).lineHeight;if(null==this.cachedWidths[c]&&(this.cachedWidths[c]={}),null!=this.cachedWidths[c][b])return this.cachedWidths[c][b];this.canvasContext.font=getComputedStyle(a.textareaElement).font.replace(getComputedStyle(a.textareaElement).fontSize,"20px");let d=this.canvasContext.measureText(b).width/20;if(1b&&(d*=20/b)}return this.cachedWidths[c][b]=d,d}};
--------------------------------------------------------------------------------
/plugins/test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copy this to create a plugin, which brings extra,
3 | * non-central optional functionality to code-input.
4 | * Instances of plugins can be passed in in an array
5 | * to the `plugins` argument when registering a template,
6 | * for example like this:
7 | * ```javascript
8 | * codeInput.registerTemplate("syntax-highlighted", codeInput.templates.hljs(hljs, [new codeInput.plugins.Test()]));
9 | * ```
10 | */
11 | codeInput.plugins.Test = class extends codeInput.Plugin {
12 | constructor() {
13 | super(["testattr"]);
14 | // Array of observed attributes as parameter
15 | }
16 | /* Runs before code is highlighted; Params: codeInput element) */
17 | beforeHighlight(codeInput) {
18 | console.log(codeInput, "before highlight");
19 | }
20 | /* Runs after code is highlighted; Params: codeInput element) */
21 | afterHighlight(codeInput) {
22 | console.log(codeInput, "after highlight");
23 | }
24 | /* Runs before elements are added into a `code-input`; Params: codeInput element) */
25 | beforeElementsAdded(codeInput) {
26 | console.log(codeInput, "before elements added");
27 | }
28 | /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
29 | afterElementsAdded(codeInput) {
30 | console.log(codeInput, "after elements added");
31 | }
32 | /* Runs when an observed attribute of a `code-input` is changed (you must add the attribute name in the constructor); Params: codeInput element, name attribute name, oldValue previous value of attribute, newValue changed value of attribute) */
33 | attributeChanged(codeInput, name, oldValue, newValue) {
34 | console.log(codeInput, name, ":", oldValue, ">", newValue);
35 | }
36 | }
--------------------------------------------------------------------------------
/plugins/test.min.js:
--------------------------------------------------------------------------------
1 | codeInput.plugins.Test=class extends codeInput.Plugin{constructor(){super(["testattr"])}beforeHighlight(a){console.log(a,"before highlight")}afterHighlight(a){console.log(a,"after highlight")}beforeElementsAdded(a){console.log(a,"before elements added")}afterElementsAdded(a){console.log(a,"after elements added")}attributeChanged(a,b,c,d){console.log(a,b,":",c,">",d)}};
--------------------------------------------------------------------------------
/tests/hljs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | code-input Tester
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | code-input Tester (highlight.js)
39 | If the page doesn't load, please reload it, and answer the questions in alert boxes.
40 |
41 | This page carries out automated tests for the code-input library to check that both the core components and the plugins work in some ways. It doesn't fully cover every scenario so you should test any code you change by hand, but it's good for quickly checking a wide range of functionality works.
42 |
43 | Test Results (Click to Open)
44 |
50 |
51 |
54 |
55 |
--------------------------------------------------------------------------------
/tests/prism-match-braces-compatibility.js:
--------------------------------------------------------------------------------
1 | /* Modified from https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/match-braces/prism-match-braces.js
2 | to enable codeInput SelectTokenCallbacks compatibility. Use:
3 | new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace, deselectAllBraces), true) */
4 | // Additions on lines 6-10, and lines 84-98.
5 |
6 | // code-input modification: ADD
7 | // Callbacks
8 | let selectBrace;
9 | let deselectAllBraces;
10 | // END code-input modification
11 | (function () {
12 |
13 | if (typeof Prism === 'undefined' || typeof document === 'undefined') {
14 | return;
15 | }
16 |
17 | function mapClassName(name) {
18 | var customClass = Prism.plugins.customClass;
19 | if (customClass) {
20 | return customClass.apply(name, 'none');
21 | } else {
22 | return name;
23 | }
24 | }
25 |
26 | var PARTNER = {
27 | '(': ')',
28 | '[': ']',
29 | '{': '}',
30 | };
31 |
32 | // The names for brace types.
33 | // These names have two purposes: 1) they can be used for styling and 2) they are used to pair braces. Only braces
34 | // of the same type are paired.
35 | var NAMES = {
36 | '(': 'brace-round',
37 | '[': 'brace-square',
38 | '{': 'brace-curly',
39 | };
40 |
41 | // A map for brace aliases.
42 | // This is useful for when some braces have a prefix/suffix as part of the punctuation token.
43 | var BRACE_ALIAS_MAP = {
44 | '${': '{', // JS template punctuation (e.g. `foo ${bar + 1}`)
45 | };
46 |
47 | var LEVEL_WARP = 12;
48 |
49 | var pairIdCounter = 0;
50 |
51 | var BRACE_ID_PATTERN = /^(pair-\d+-)(close|open)$/;
52 |
53 | /**
54 | * Returns the brace partner given one brace of a brace pair.
55 | *
56 | * @param {HTMLElement} brace
57 | * @returns {HTMLElement}
58 | */
59 | function getPartnerBrace(brace) {
60 | var match = BRACE_ID_PATTERN.exec(brace.id);
61 | return document.querySelector('#' + match[1] + (match[2] == 'open' ? 'close' : 'open'));
62 | }
63 |
64 | /**
65 | * @this {HTMLElement}
66 | */
67 | function hoverBrace() {
68 | if (!Prism.util.isActive(this, 'brace-hover', true)) {
69 | return;
70 | }
71 |
72 | [this, getPartnerBrace(this)].forEach(function (e) {
73 | e.classList.add(mapClassName('brace-hover'));
74 | });
75 | }
76 | /**
77 | * @this {HTMLElement}
78 | */
79 | function leaveBrace() {
80 | [this, getPartnerBrace(this)].forEach(function (e) {
81 | e.classList.remove(mapClassName('brace-hover'));
82 | });
83 | }
84 | // code-input modification: ADD
85 | selectBrace = (token) => {
86 | if(BRACE_ID_PATTERN.test(token.id)) { // Check it's a brace
87 | hoverBrace.apply(token); // Move the brace from a this to a parameter
88 | }
89 | };
90 | deselectAllBraces = (tokenContainer) => {
91 | // Remove selected class
92 | let selectedClassTokens = tokenContainer.getElementsByClassName(mapClassName('brace-hover'));
93 | // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
94 | while(selectedClassTokens.length > 0) {
95 | selectedClassTokens[0].classList.remove(mapClassName('brace-hover'));
96 | }
97 | }; // Moves the brace from a this to a parameter
98 | // end code-input modification
99 | /**
100 | * @this {HTMLElement}
101 | */
102 | function clickBrace() {
103 | if (!Prism.util.isActive(this, 'brace-select', true)) {
104 | return;
105 | }
106 |
107 | [this, getPartnerBrace(this)].forEach(function (e) {
108 | e.classList.add(mapClassName('brace-selected'));
109 | });
110 | }
111 |
112 | Prism.hooks.add('complete', function (env) {
113 |
114 | /** @type {HTMLElement} */
115 | var code = env.element;
116 | var pre = code.parentElement;
117 |
118 | if (!pre || pre.tagName != 'PRE') {
119 | return;
120 | }
121 |
122 | // find the braces to match
123 | /** @type {string[]} */
124 | var toMatch = [];
125 | if (Prism.util.isActive(code, 'match-braces')) {
126 | toMatch.push('(', '[', '{');
127 | }
128 |
129 | if (toMatch.length == 0) {
130 | // nothing to match
131 | return;
132 | }
133 |
134 | if (!pre.__listenerAdded) {
135 | // code blocks might be highlighted more than once
136 | pre.addEventListener('mousedown', function removeBraceSelected() {
137 | // the code element might have been replaced
138 | var code = pre.querySelector('code');
139 | var className = mapClassName('brace-selected');
140 | Array.prototype.slice.call(code.querySelectorAll('.' + className)).forEach(function (e) {
141 | e.classList.remove(className);
142 | });
143 | });
144 | Object.defineProperty(pre, '__listenerAdded', { value: true });
145 | }
146 |
147 | /** @type {HTMLSpanElement[]} */
148 | var punctuation = Array.prototype.slice.call(
149 | code.querySelectorAll('span.' + mapClassName('token') + '.' + mapClassName('punctuation'))
150 | );
151 |
152 | /** @type {{ index: number, open: boolean, element: HTMLElement }[]} */
153 | var allBraces = [];
154 |
155 | toMatch.forEach(function (open) {
156 | var close = PARTNER[open];
157 | var name = mapClassName(NAMES[open]);
158 |
159 | /** @type {[number, number][]} */
160 | var pairs = [];
161 | /** @type {number[]} */
162 | var openStack = [];
163 |
164 | for (var i = 0; i < punctuation.length; i++) {
165 | var element = punctuation[i];
166 | if (element.childElementCount == 0) {
167 | var text = element.textContent;
168 | text = BRACE_ALIAS_MAP[text] || text;
169 | if (text === open) {
170 | allBraces.push({ index: i, open: true, element: element });
171 | element.classList.add(name);
172 | element.classList.add(mapClassName('brace-open'));
173 | openStack.push(i);
174 | } else if (text === close) {
175 | allBraces.push({ index: i, open: false, element: element });
176 | element.classList.add(name);
177 | element.classList.add(mapClassName('brace-close'));
178 | if (openStack.length) {
179 | pairs.push([i, openStack.pop()]);
180 | }
181 | }
182 | }
183 | }
184 |
185 | pairs.forEach(function (pair) {
186 | var pairId = 'pair-' + (pairIdCounter++) + '-';
187 |
188 | var opening = punctuation[pair[0]];
189 | var closing = punctuation[pair[1]];
190 |
191 | opening.id = pairId + 'open';
192 | closing.id = pairId + 'close';
193 |
194 | [opening, closing].forEach(function (e) {
195 | e.addEventListener('mouseenter', hoverBrace);
196 | e.addEventListener('mouseleave', leaveBrace);
197 | e.addEventListener('click', clickBrace);
198 | });
199 | });
200 | });
201 |
202 | var level = 0;
203 | allBraces.sort(function (a, b) { return a.index - b.index; });
204 | allBraces.forEach(function (brace) {
205 | if (brace.open) {
206 | brace.element.classList.add(mapClassName('brace-level-' + (level % LEVEL_WARP + 1)));
207 | level++;
208 | } else {
209 | level = Math.max(0, level - 1);
210 | brace.element.classList.add(mapClassName('brace-level-' + (level % LEVEL_WARP + 1)));
211 | }
212 | });
213 | });
214 |
215 | }());
--------------------------------------------------------------------------------
/tests/prism-match-braces-compatibility.min.js:
--------------------------------------------------------------------------------
1 | let selectBrace,deselectAllBraces;(function(){function a(a){var b=Prism.plugins.customClass;return b?b.apply(a,"none"):a}function b(a){var b=m.exec(a.id);return document.querySelector("#"+b[1]+("open"==b[2]?"close":"open"))}function c(){Prism.util.isActive(this,"brace-hover",!0)&&[this,b(this)].forEach(function(b){b.classList.add(a("brace-hover"))})}function d(){[this,b(this)].forEach(function(b){b.classList.remove(a("brace-hover"))})}function f(){Prism.util.isActive(this,"brace-select",!0)&&[this,b(this)].forEach(function(b){b.classList.add(a("brace-selected"))})}if("undefined"!=typeof Prism&&"undefined"!=typeof document){var g={"(":")","[":"]","{":"}"},h={"(":"brace-round","[":"brace-square","{":"brace-curly"},j={"${":"{"},k=12,l=0,m=/^(pair-\d+-)(close|open)$/;selectBrace=a=>{m.test(a.id)&&c.apply(a)},deselectAllBraces=b=>{for(let c=b.getElementsByClassName(a("brace-hover"));0
2 |
3 |
4 |
5 |
6 | code-input Tester
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | code-input Tester (Prism.js)
38 | If the page doesn't load, please reload it, and answer the questions in alert boxes.
39 |
40 | This page carries out automated tests for the code-input library to check that both the core components and the plugins work in some ways. It doesn't fully cover every scenario so you should test any code you change by hand, but it's good for quickly checking a wide range of functionality works.
41 |
42 | Test Results (Click to Open)
43 |
49 |
50 |
53 |
54 |
--------------------------------------------------------------------------------
/tests/tester.min.js:
--------------------------------------------------------------------------------
1 | var testsFailed=!1;function testData(a,b,c){let d=document.getElementById("test-results"),e=d.querySelector("#test-"+a);e==null&&(e=document.createElement("span"),e.innerHTML=`Group ${a} :\n`,e.id="test-"+a,d.append(e)),e.innerHTML+=`\t${b}: ${c}\n`}function testAssertion(a,b,c,d){let e=document.getElementById("test-results"),f=e.querySelector("#test-"+a);f==null&&(f=document.createElement("span"),f.innerHTML=`Group ${a} :\n`,f.id="test-"+a,e.append(f)),f.innerHTML+=`\t${b}: ${c?"passed ":"failed ("+d+")"}\n`,c||(testsFailed=!0)}function assertEqual(a,b,c,d){let e=c==d;testAssertion(a,b,e,"see console output"),e||console.error(a,b,c,"should be",d)}function testAddingText(a,b,c,d,e,f){let g=b.selectionStart,h=b.value.substring(0,b.selectionStart),i=b.value.substring(b.selectionEnd);c(b);let j=h+d+i;assertEqual(a,"Text Output",b.value,j),assertEqual(a,"Code-Input Value JS Property Output",b.parentElement.value,j),assertEqual(a,"Selection Start",b.selectionStart,g+e),assertEqual(a,"Selection End",b.selectionEnd,g+f)}function addText(a,b,c=!1){for(let d=0;d{setTimeout(()=>{b()},a)})}function beginTest(a){let b=document.querySelector("code-input");a?codeInput.registerTemplate("code-editor",codeInput.templates.hljs(hljs,[new codeInput.plugins.AutoCloseBrackets,new codeInput.plugins.Autocomplete(function(a,b,c){"popup"==b.value.substring(c-5,c)?(a.style.display="block",a.innerHTML="Here's your popup!"):a.style.display="none"}),new codeInput.plugins.Autodetect,new codeInput.plugins.FindAndReplace,new codeInput.plugins.GoToLine,new codeInput.plugins.Indent(!0,2),new codeInput.plugins.SelectTokenCallbacks(codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation("in-selection"),!1,!0,!0,!0,!0,!1),new codeInput.plugins.SpecialChars(!0)])):codeInput.registerTemplate("code-editor",codeInput.templates.prism(Prism,[new codeInput.plugins.AutoCloseBrackets,new codeInput.plugins.Autocomplete(function(a,b,c){"popup"==b.value.substring(c-5,c)?(a.style.display="block",a.innerHTML="Here's your popup!"):a.style.display="none"}),new codeInput.plugins.FindAndReplace,new codeInput.plugins.GoToLine,new codeInput.plugins.Indent(!0,2),new codeInput.plugins.SelectTokenCallbacks(new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(selectBrace,deselectAllBraces),!0),new codeInput.plugins.SpecialChars(!0)])),startLoad(b,a)}function startLoad(a,b){let c,d=0,e=window.setInterval(()=>{c=a.querySelector("textarea"),null!=c&&window.clearInterval(e),d+=10,testData("TimeTaken","Textarea Appears",d+"ms (nearest 10)"),startTests(c,b)},10)}function allowInputEvents(a){a.addEventListener("input",function(a){a.isTrusted||(a.preventDefault(),document.execCommand("insertText",!1,a.data))},!1)}async function startTests(a,b){a.focus(),allowInputEvents(a),codeInputElement=a.parentElement,assertEqual("Core","Initial Textarea Value",a.value,`console.log("Hello, World!");
2 | // A second line
3 | // A third line with tags`);let c=codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g,"");assertEqual("Core","Initial Rendered Value",c,`console.log("Hello, World!");
4 | // A second line
5 | // A third line with <html> tags
6 | `),codeInputElement.value+=`
7 | console.log("I've got another line!", 2 < 3, "should be true.");`,await waitAsync(50),assertEqual("Core","JS-updated Textarea Value",a.value,`console.log("Hello, World!");
8 | // A second line
9 | // A third line with tags
10 | console.log("I've got another line!", 2 < 3, "should be true.");`),c=codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g,""),assertEqual("Core","JS-updated Rendered Value",c,`console.log("Hello, World!");
11 | // A second line
12 | // A third line with <html> tags
13 | console.log("I've got another line!", 2 < 3, "should be true.");
14 | `);let d=0,e=0,f=a=>{a.isTrusted||d++};codeInputElement.addEventListener("input",f);let g=()=>{e++};codeInputElement.addEventListener("change",g);let h=!1,i=()=>{h=!0};codeInputElement.addEventListener("input",i),codeInputElement.removeEventListener("input",i),a.focus(),addText(a," // Hi"),a.blur(),a.focus(),assertEqual("Core","Function Event Listeners: Input Called Right Number of Times",d,6),assertEqual("Core","Function Event Listeners: Change Called Right Number of Times",e,1),testAssertion("Core","Function Event Listeners: Input Removed Listener Not Called",!h,"(code-input element).removeEventListener did not work."),codeInputElement.removeEventListener("input",f),codeInputElement.removeEventListener("change",g),d=0,e=0,codeInputElement.addEventListener("input",{handleEvent:a=>{a.isTrusted||d++}}),codeInputElement.addEventListener("change",{handleEvent:()=>{e++}}),h=!1,i={handleEvent:()=>{h=!0}},codeInputElement.addEventListener("input",i),codeInputElement.removeEventListener("input",i),a.focus(),addText(a," // Hi"),a.blur(),a.focus(),assertEqual("Core","Object Event Listeners: Input Called Right Number of Times",d,6),assertEqual("Core","Object Event Listeners: Change Called Right Number of Times",e,1),testAssertion("Core","Object Event Listeners: Input Removed Listener Not Called",!h,"(code-input element).removeEventListener did not work."),b||(testAssertion("Core","Language attribute Initial value",!codeInputElement.codeElement.classList.contains("language-javascript")&&!codeInputElement.codeElement.classList.contains("language-html"),`Language unset but code element's class name is ${codeInputElement.codeElement.className}.`),codeInputElement.setAttribute("language","HTML"),await waitAsync(50),testAssertion("Core","Language attribute Changed value 1",codeInputElement.codeElement.classList.contains("language-html")&&!codeInputElement.codeElement.classList.contains("language-javascript"),`Language set to HTML but code element's class name is ${codeInputElement.codeElement.className}.`),codeInputElement.setAttribute("language","JavaScript"),await waitAsync(50),testAssertion("Core","Language attribute Changed value 2",codeInputElement.codeElement.classList.contains("language-javascript")&&!codeInputElement.codeElement.classList.contains("language-html"),`Language set to JavaScript but code element's class name is ${codeInputElement.codeElement.className}.`));let j=codeInputElement.parentElement;j.reset(),await waitAsync(50),assertEqual("Core","Form Reset resets Code-Input Value",codeInputElement.value,`console.log("Hello, World!");
15 | // A second line
16 | // A third line with tags`),assertEqual("Core","Form Reset resets Textarea Value",a.value,`console.log("Hello, World!");
17 | // A second line
18 | // A third line with tags`),c=codeInputElement.codeElement.innerHTML.replace(/<[^>]+>/g,""),assertEqual("Core","Form Reset resets Rendered Value",c,`console.log("Hello, World!");
19 | // A second line
20 | // A third line with <html> tags
21 | `),testAddingText("AutoCloseBrackets",a,function(a){addText(a,`\nconsole.log("A test message`),move(a,2),addText(a,`;\nconsole.log("Another test message");\n{[{[]}(([[`),backspace(a),backspace(a),backspace(a),addText(a,`)`)},"\nconsole.log(\"A test message\");\nconsole.log(\"Another test message\");\n{[{[]}()]}",77,77),addText(a,"popup"),await waitAsync(50),testAssertion("Autocomplete","Popup Shows",confirm("Does the autocomplete popup display correctly? (OK=Yes)"),"user-judged"),backspace(a),await waitAsync(50),testAssertion("Autocomplete","Popup Disappears",confirm("Has the popup disappeared? (OK=Yes)"),"user-judged"),backspace(a),backspace(a),backspace(a),backspace(a),b&&(a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"console.log(\"Hello, World!\");\nfunction sayHello(name) {\n console.log(\"Hello, \" + name + \"!\");\n}\nsayHello(\"code-input\");"),await waitAsync(50),assertEqual("Autodetect","Detects JavaScript",codeInputElement.getAttribute("language"),"javascript"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"#!/usr/bin/python\nprint(\"Hello, World!\")\nfor i in range(5):\n print(i)"),await waitAsync(50),assertEqual("Autodetect","Detects Python",codeInputElement.getAttribute("language"),"python"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"body, html {\n height: 100%;\n background-color: blue;\n color: red;\n}"),await waitAsync(50),assertEqual("Autodetect","Detects CSS",codeInputElement.getAttribute("language"),"css")),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"// hello /\\S/g\nhe('llo', /\\s/g);\nhello"),a.selectionStart=a.selectionEnd=0,await waitAsync(50),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"f",ctrlKey:!0}));let k=codeInputElement.querySelectorAll(".code-input_find-and-replace_dialog input"),l=k[0],m=k[1],n=k[2],o=k[3],p=codeInputElement.querySelectorAll(".code-input_find-and-replace_dialog button"),q=p[0],r=p[1],s=p[2],t=p[3],u=codeInputElement.querySelector(".code-input_find-and-replace_dialog details summary");l.value="/\\s/g",n.click(),await waitAsync(150),testAssertion("FindAndReplace","Finds Case-Sensitive Matches Correctly",confirm("Is there a match on only the lowercase '/\\s/g'?"),"user-judged"),l.value="he[^l]*llo",m.click(),n.click(),await waitAsync(150),testAssertion("FindAndReplace","Finds RegExp Matches Correctly",confirm("Are there matches on all 'he...llo's?"),"user-judged"),u.click(),r.click(),o.value="do('hello",s.click(),await waitAsync(50),assertEqual("FindAndReplace","Replaces Once Correctly",a.value,"// hello /\\S/g\ndo('hello', /\\s/g);\nhello"),q.click(),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"})),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keyup",{key:"Escape"})),assertEqual("FindAndReplace","Selection Start on Focused Match when Dialog Exited",a.selectionStart,3),assertEqual("FindAndReplace","Selection End on Focused Match when Dialog Exited",a.selectionEnd,8),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"h",ctrlKey:!0})),l.value="",l.focus(),allowInputEvents(l),addText(l,"hello"),await waitAsync(150),o.value="hi",t.click(),assertEqual("FindAndReplace","Replaces All Correctly",a.value,"// hi /\\S/g\ndo('hi', /\\s/g);\nhi"),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"})),codeInputElement.querySelector(".code-input_find-and-replace_dialog").dispatchEvent(new KeyboardEvent("keyup",{key:"Escape"})),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line"),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0}));let v=codeInputElement.querySelector(".code-input_go-to-line_dialog input");v.value="1",v.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),v.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Line Only",a.selectionStart,0),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),v.value="3:18",v.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),v.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Line and Column",a.selectionStart,45),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),v.value="10",v.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),v.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Rejects Out-of-range Line",v.classList.contains("code-input_go-to-line_error"),!0),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),v.value="2:12",v.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),v.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Rejects Out-of-range Column",v.classList.contains("code-input_go-to-line_error"),!0),a.dispatchEvent(new KeyboardEvent("keydown",{cancelable:!0,key:"g",ctrlKey:!0})),v.value="sausages",v.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter"})),v.dispatchEvent(new KeyboardEvent("keyup",{key:"Enter"})),assertEqual("GoToLine","Rejects Invalid Input",v.classList.contains("code-input_go-to-line_error"),!0),assertEqual("GoToLine","Stays open when Rejects Input",v.parentElement.classList.contains("code-input_go-to-line_hidden-dialog"),!1),v.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape"})),v.dispatchEvent(new KeyboardEvent("keyup",{key:"Escape"})),assertEqual("GoToLine","Exits when Esc pressed",v.parentElement.classList.contains("code-input_go-to-line_hidden-dialog"),!0),a.selectionStart=a.selectionEnd=a.value.length,addText(a,"\nfor(let i = 0; i < 100; i++) {\n for(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n }\n}\n{\n // This is indented\n}"),a.selectionStart=0,a.selectionEnd=a.value.length,a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!1})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!1})),assertEqual("Indent","Indents Lines",a.value," // 7 times table\n let i = 1;\n while(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n // That's my code.\n // This is another comment\n // Another\n // Line\n for(let i = 0; i < 100; i++) {\n for(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n }\n }\n {\n // This is indented\n }"),a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!0})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!0})),assertEqual("Indent","Unindents Lines",a.value,"// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\n for(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n }\n}\n{\n // This is indented\n}"),a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!0})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!0})),assertEqual("Indent","Unindents Lines where some are already fully unindented",a.value,"// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\nfor(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n}\n}\n{\n// This is indented\n}"),a.selectionStart=255,a.selectionEnd=274,a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!1})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!1})),assertEqual("Indent","Indents Lines by Selection",a.value,"// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\nfor(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n}\n}\n{\n // This is indented\n}"),a.selectionStart=265,a.selectionEnd=265,a.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:!0})),a.dispatchEvent(new KeyboardEvent("keyup",{key:"Tab",shiftKey:!0})),assertEqual("Indent","Unindents Lines by Selection",a.value,"// 7 times table\nlet i = 1;\nwhile(i <= 12) { console.log(`7 x ${i} = ${7*i}`) }\n// That's my code.\n// This is another comment\n// Another\n// Line\nfor(let i = 0; i < 100; i++) {\nfor(let j = i; j < 100; j++) {\n // Here's some code\n console.log(i,j);\n}\n}\n{\n// This is indented\n}"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),testAddingText("Indent-AutoCloseBrackets",a,function(a){addText(a,`function printTriples(max) {\nfor(let i = 0; i < max-2; i++) {\nfor(let j = 0; j < max-1; j++) {\nfor(let k = 0; k < max; k++) {\nconsole.log(i,j,k);\n}\n//Hmmm...`,!0)},"function printTriples(max) {\n for(let i = 0; i < max-2; i++) {\n for(let j = 0; j < max-1; j++) {\n for(let k = 0; k < max; k++) {\n console.log(i,j,k);\n }\n //Hmmm...\n }\n }\n }\n}",189,189),b?(addText(a,"\nlet x = 1;\nlet y = 2;\nconsole.log(`${x} + ${y} = ${x+y}`);"),move(a,-4),a.selectionStart-=35,await waitAsync(50),assertEqual("SelectTokenCallbacks","Number of Selected Tokens",codeInputElement.querySelectorAll(".in-selection").length,13),assertEqual("SelectTokenCallbacks","Number of Selected .hljs-string Tokens",codeInputElement.querySelectorAll(".hljs-string.in-selection").length,0),assertEqual("SelectTokenCallbacks","Number of Selected .hljs-subst Tokens",codeInputElement.querySelectorAll(".hljs-subst.in-selection").length,2)):(addText(a,"\n[(),((),'Hi')]"),await waitAsync(50),move(a,-2),await waitAsync(50),assertEqual("SelectTokenCallbacks","Number of Selected Braces 1",codeInputElement.getElementsByClassName("brace-hover").length,2),move(a,1),await waitAsync(50),assertEqual("SelectTokenCallbacks","Number of Selected Braces 2",codeInputElement.getElementsByClassName("brace-hover").length,4)),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),addText(a,"\"Some special characters: \x96,\x01\x03,\x02...\""),a.selectionStart=a.value.length-4,a.selectionEnd=a.value.length,await waitAsync(50),testAssertion("SpecialChars","Displays Correctly",confirm("Do the special characters read (0096),(0001)(0003),(0002) and align with the ellipsis? (OK=Yes)"),"user-judged"),a.selectionStart=0,a.selectionEnd=a.value.length,backspace(a),fetch(new Request("https://cdn.jsdelivr.net/gh/webcoder49/code-input@2.1/code-input.js")).then(a=>a.text()).then(b=>{a.value="// code-input v2.1: A large code file (not the latest version!)\n// Editing this here should give little latency.\n\n"+b,a.selectionStart=112,a.selectionEnd=112,addText(a,"\n",!0),document.getElementById("collapse-results").setAttribute("open",!0)}),testsFailed?(document.querySelector("h2").style.backgroundColor="red",document.querySelector("h2").textContent="Some Tests have Failed."):(document.querySelector("h2").style.backgroundColor="lightgreen",document.querySelector("h2").textContent="All Tests have Passed.")}
--------------------------------------------------------------------------------