` element inside a `` element as a parameter
+ * @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.
+ * @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]'.
+ * @param {boolean} includeCodeInputInHighlightFunc - Setting this to true passes the `` element as a second argument to the highlight function.
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.Plugin`
+ * @returns {codeInput.Template} template object
+ */
+ constructor(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
+ this.highlight = highlight;
+ this.preElementStyled = preElementStyled;
+ this.isCode = isCode;
+ this.includeCodeInputInHighlightFunc = includeCodeInputInHighlightFunc;
+ this.plugins = plugins;
+ }
+
+ /**
+ * A callback to highlight the code, that takes an HTML `` element
+ * inside a `` element as a parameter, and an optional second
+ * `` element parameter if `this.includeCodeInputInHighlightFunc` is
+ * `true`.
+ */
+ highlight = function(codeElement) {};
+
+ /**
+ * Is the element CSS-styled as well as the `` element?
+ * If `true`, `` element's scrolling is synchronised; if false,
+ * element's scrolling is synchronised.
+ */
+ preElementStyled = true;
+
+ /**
+ * 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]'.
+ */
+ isCode = true;
+
+ /**
+ * Setting this to true passes the `` element as a
+ * second argument to the highlight function.
+ */
+ includeCodeInputInHighlightFunc = false;
+
+ /**
+ * An array of plugin objects to add extra features -
+ * see `codeInput.Plugin`.
+ */
+ plugins = [];
+ },
+
+ /**
+ * For creating a custom template from scratch, please
+ * use `new codeInput.Template(...)`
+ *
+ * Shortcut functions for creating templates.
+ * Each code-input element has a template attribute that
+ * tells it which template to use.
+ * Each template contains functions and preferences that
+ * run the syntax-highlighting and let code-input control
+ * the highlighting.
+ * For adding small pieces of functionality, please see `codeInput.plugins`.
+ */
+ templates: {
+ /**
+ * Constructor to create a template that uses Prism.js syntax highlighting (https://prismjs.com/)
+ * @param {Object} prism Import Prism.js, then after that import pass the `Prism` object as this parameter.
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
+ * @returns {codeInput.Template} template object
+ */
+ prism(prism, plugins = []) { // Dependency: Prism.js (https://prismjs.com/)
+ return new codeInput.Template(
+ prism.highlightElement, // highlight
+ true, // preElementStyled
+ true, // isCode
+ false, // includeCodeInputInHighlightFunc
+ plugins
+ );
+ },
+ /**
+ * Constructor to create a template that uses highlight.js syntax highlighting (https://highlightjs.org/)
+ * @param {Object} hljs Import highlight.js, then after that import pass the `hljs` object as this parameter.
+ * @param {codeInput.Plugin[]} plugins - An array of plugin objects to add extra features - see `codeInput.plugins`
+ * @returns {codeInput.Template} template object
+ */
+ hljs(hljs, plugins = []) { // Dependency: Highlight.js (https://highlightjs.org/)
+ return new codeInput.Template(
+ function(codeElement) {
+ codeElement.removeAttribute("data-highlighted");
+ hljs.highlightElement(codeElement);
+ }, // highlight
+ false, // preElementStyled
+ true, // isCode
+ false, // includeCodeInputInHighlightFunc
+ plugins
+ );
+ },
+
+ /**
+ * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input.
+ */
+ characterLimit(plugins) {
+ return {
+ highlight: function (resultElement, codeInput, plugins = []) {
+
+ let characterLimit = Number(codeInput.getAttribute("data-character-limit"));
+
+ let normalCharacters = codeInput.escapeHtml(codeInput.value.slice(0, characterLimit));
+ let overflowCharacters = codeInput.escapeHtml(codeInput.value.slice(characterLimit));
+
+ resultElement.innerHTML = `${normalCharacters}${overflowCharacters}`;
+ if (overflowCharacters.length > 0) {
+ resultElement.innerHTML += ` ${codeInput.getAttribute("data-overflow-msg") || "(Character limit reached)"}`;
+ }
+ },
+ includeCodeInputInHighlightFunc: true,
+ preElementStyled: true,
+ isCode: false,
+ plugins: plugins,
+ }
+ },
+
+ /**
+ * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input.
+ */
+ rainbowText(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) {
+ return {
+ highlight: function (resultElement, codeInput) {
+ let htmlResult = [];
+ let sections = codeInput.value.split(codeInput.template.delimiter);
+ for (let i = 0; i < sections.length; i++) {
+ htmlResult.push(`${codeInput.escapeHtml(sections[i])}`);
+ }
+ resultElement.innerHTML = htmlResult.join(codeInput.template.delimiter);
+ },
+ includeCodeInputInHighlightFunc: true,
+ preElementStyled: true,
+ isCode: false,
+
+ rainbowColors: rainbowColors,
+ delimiter: delimiter,
+
+ plugins: plugins,
+ }
+ },
+
+ /**
+ * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input.
+ */
+ character_limit() {
+ return this.characterLimit([]);
+ },
+ /**
+ * @deprecated Make your own version of this template if you need it - we think it isn't widely used so will remove it from the next version of code-input.
+ */
+ rainbow_text(rainbowColors = ["red", "orangered", "orange", "goldenrod", "gold", "green", "darkgreen", "navy", "blue", "magenta"], delimiter = "", plugins = []) {
+ return this.rainbowText(rainbowColors, delimiter, plugins);
+ },
+
+ /**
+ * @deprecated Please use `new codeInput.Template()`
+ */
+ custom(highlight = function () { }, preElementStyled = true, isCode = true, includeCodeInputInHighlightFunc = false, plugins = []) {
+ return {
+ highlight: highlight,
+ includeCodeInputInHighlightFunc: includeCodeInputInHighlightFunc,
+ preElementStyled: preElementStyled,
+ isCode: isCode,
+ plugins: plugins,
+ };
+ },
+ },
+
+ /* ------------------------------------
+ * ------------Plugins-----------------
+ * ------------------------------------ */
+
+ /**
+ * Before using any plugin in this namespace, please ensure you import its JavaScript
+ * files (in the plugins folder), or continue to get a more detailed error in the developer
+ * console.
+ *
+ * Where plugins are stored, after they are imported. The plugin
+ * file assigns them a space in this object.
+ * For adding completely new syntax-highlighting algorithms, please see `codeInput.templates`.
+ *
+ * Key - plugin name
+ *
+ * Value - plugin object
+ * @type {Object}
+ */
+ plugins: new Proxy({}, {
+ get(plugins, name) {
+ if(plugins[name] == undefined) {
+ throw ReferenceError(`code-input: Plugin '${name}' 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 plugins[name];
+ }
+ }),
+
+ /**
+ * Plugins are imported from the plugins folder. They will then
+ * provide custom extra functionality to code-input elements.
+ */
+ Plugin: class {
+ /**
+ * Create a Plugin
+ *
+ * @param {Array} observedAttributes - The HTML attributes to watch for this plugin, and report any
+ * modifications to the `codeInput.Plugin.attributeChanged` method.
+ */
+ constructor(observedAttributes) {
+ console.log("code-input: plugin: Created plugin");
+
+ observedAttributes.forEach((attribute) => {
+ codeInput.observedAttributes.push(attribute);
+ });
+ }
+
+ /**
+ * Runs before code is highlighted.
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
+ */
+ beforeHighlight(codeInput) { }
+ /**
+ * Runs after code is highlighted.
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
+ */
+ afterHighlight(codeInput) { }
+ /**
+ * Runs before elements are added into a code-input element.
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
+ */
+ beforeElementsAdded(codeInput) { }
+ /**
+ * Runs after elements are added into a code-input element (useful for adding events to the textarea).
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
+ */
+ afterElementsAdded(codeInput) { }
+ /**
+ * Runs when an attribute of a code-input element is changed (you must add the attribute name to `codeInput.Plugin.observedAttributes` first).
+ * @param {codeInput.CodeInput} codeInput - The codeInput element
+ * @param {string} name - The name of the attribute
+ * @param {string} oldValue - The value of the attribute before it was changed
+ * @param {string} newValue - The value of the attribute after it is changed
+ */
+ attributeChanged(codeInput, name, oldValue, newValue) { }
+ },
+
+ /* ------------------------------------
+ * -------------Main-------------------
+ * ------------------------------------ */
+
+ /**
+ * A code-input element.
+ */
+ CodeInput: class extends HTMLElement {
+ constructor() {
+ super(); // Element
+ }
+
+ /**
+ * Exposed child textarea element for user to input code in
+ */
+ textareaElement = null;
+ /**
+ * Exposed child pre element where syntax-highlighted code is outputted.
+ * Contains this.codeElement as its only child.
+ */
+ preElement = null
+ /**
+ * Exposed child pre element's child code element where syntax-highlighted code is outputted.
+ * Has this.preElement as its parent.
+ */
+ codeElement = null;
+
+ /**
+ * Exposed non-scrolling element designed to contain dialog boxes etc. that shouldn't scroll
+ * with the code-input element.
+ */
+ dialogContainerElement = null;
+
+ /**
+ * Form-Associated Custom Element Callbacks
+ * https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example
+ */
+ static formAssociated = true;
+
+ /**
+ * When events are transferred to the textarea element, callbacks
+ * are bound to set the this variable to the code-input element
+ * rather than the textarea. This allows the callback to be converted
+ * to a bound one:
+ * Key - Callback not bound
+ * Value - Callback that is bound, with this equalling the code-input element in the callback
+ */
+ boundEventCallbacks = {};
+
+ /** Trigger this event in all plugins with a optional list of arguments
+ * @param {string} eventName - the name of the event to trigger
+ * @param {Array} args - the arguments to pass into the event callback in the template after the code-input element. Normally left empty
+ */
+ pluginEvt(eventName, args) {
+ for (let i in this.template.plugins) {
+ let plugin = this.template.plugins[i];
+ if (eventName in plugin) {
+ if (args === undefined) {
+ plugin[eventName](this);
+ } else {
+ plugin[eventName](this, ...args);
+ }
+ }
+ }
+ }
+
+ /* ------------------------------------
+ * ----------Main Functionality--------
+ * ------------------------------------
+ * The main function of a code-input element is to take
+ * code written in its textarea element, copy this code into
+ * the result (pre code) element, then use the template object
+ * to syntax-highlight it. */
+
+ needsHighlight = false; // Just inputted
+ handleEventsFromTextarea = true; // Turn to false when unusual internal events are called on the textarea
+ originalAriaDescription;
+
+ /**
+ * Highlight the code as soon as possible
+ */
+ scheduleHighlight() {
+ this.needsHighlight = true;
+ }
+
+ /**
+ * Call an animation frame
+ */
+ animateFrame() {
+ // Synchronise the contents of the pre/code and textarea elements
+ if(this.needsHighlight) {
+ this.update();
+ this.needsHighlight = false;
+ }
+
+ window.requestAnimationFrame(this.animateFrame.bind(this));
+ }
+
+ /**
+ * Update the text value to the result element, after the textarea contents have changed.
+ */
+ update() {
+ let resultElement = this.codeElement;
+ let value = this.value;
+ value += "\n"; // Placeholder for next line
+
+ // Update code
+ resultElement.innerHTML = this.escapeHtml(value);
+ this.pluginEvt("beforeHighlight");
+
+ // Syntax Highlight
+ if (this.template.includeCodeInputInHighlightFunc) this.template.highlight(resultElement, this);
+ else this.template.highlight(resultElement);
+
+ this.syncSize();
+
+ // If editing here, scroll to the caret by focusing, though this shouldn't count as a focus event
+ if(this.textareaElement === document.activeElement) {
+ this.handleEventsFromTextarea = false;
+ this.textareaElement.blur();
+ this.textareaElement.focus();
+ this.handleEventsFromTextarea = true;
+ }
+
+ this.pluginEvt("afterHighlight");
+ }
+
+ /**
+ * Set the size of the textarea element to the size of the pre/code element.
+ */
+ syncSize() {
+ // Synchronise the size of the pre/code and textarea elements
+ if(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;
+ } else {
+ this.style.backgroundColor = getComputedStyle(this.codeElement).backgroundColor;
+ this.textareaElement.style.height = getComputedStyle(this.codeElement).height;
+ this.textareaElement.style.width = getComputedStyle(this.codeElement).width;
+ }
+ }
+
+ /**
+ * Show some instructions to the user only if they are using keyboard navigation - for example, a prompt on how to navigate with the keyboard if Tab is repurposed.
+ * @param {string} instructions The instructions to display only if keyboard navigation is being used. If it's blank, no instructions will be shown.
+ * @param {boolean} includeAriaDescriptionFirst Whether to include the aria-description of the code-input element before the keyboard navigation instructions for a screenreader. Keep this as true when the textarea is first focused.
+ */
+ setKeyboardNavInstructions(instructions, includeAriaDescriptionFirst) {
+ this.dialogContainerElement.querySelector(".code-input_keyboard-navigation-instructions").innerText = instructions;
+ if(includeAriaDescriptionFirst) {
+ this.textareaElement.setAttribute("aria-description", this.originalAriaDescription + ". " + instructions);
+ } else {
+ this.textareaElement.setAttribute("aria-description", instructions);
+ }
+ }
+
+ /**
+ * HTML-escape an arbitrary string.
+ * @param {string} text - The original, unescaped text
+ * @returns {string} - The new, HTML-escaped text
+ */
+ escapeHtml(text) {
+ return text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
+ }
+
+ /**
+ * HTML-unescape an arbitrary string.
+ * @param {string} text - The original, HTML-escaped text
+ * @returns {string} - The new, unescaped text
+ */
+ unescapeHtml(text) {
+ return text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<").replace(new RegExp(">", "g"), ">"); /* Global RegExp */
+ }
+
+ /**
+ * Get the template object this code-input element is using.
+ * @returns {Object} - Template object
+ */
+ getTemplate() {
+ let templateName;
+ if (this.getAttribute("template") == undefined) {
+ // Default
+ templateName = codeInput.defaultTemplate;
+ } else {
+ templateName = this.getAttribute("template");
+ }
+ if (templateName in codeInput.usedTemplates) {
+ return codeInput.usedTemplates[templateName];
+ } else {
+ // Doesn't exist - add to queue
+ if (!(templateName in codeInput.templateNotYetRegisteredQueue)) {
+ codeInput.templateNotYetRegisteredQueue[templateName] = [];
+ }
+ codeInput.templateNotYetRegisteredQueue[templateName].push(this);
+ return undefined;
+ }
+ }
+
+ /**
+ * Set up and initialise the textarea.
+ * This will be called once the template has been added.
+ */
+ setup() {
+ if(this.textareaElement != null) return; // Already set up
+
+ this.classList.add("code-input_registered"); // Remove register message
+ if (this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
+
+ this.pluginEvt("beforeElementsAdded");
+
+ // First-time attribute sync
+ let lang = this.getAttribute("language") || this.getAttribute("lang");
+ let placeholder = this.getAttribute("placeholder") || this.getAttribute("language") || this.getAttribute("lang") || "";
+ let value = this.unescapeHtml(this.innerHTML) || this.getAttribute("value") || "";
+ // Value attribute deprecated, but included for compatibility
+
+ this.initialValue = value; // For form reset
+
+ // Create textarea
+ let textarea = document.createElement("textarea");
+ textarea.placeholder = placeholder;
+ if(value != "") {
+ textarea.value = value;
+ }
+ textarea.innerHTML = this.innerHTML;
+ textarea.setAttribute("spellcheck", "false");
+
+ // Disable focusing on the code-input element - only allow the textarea to be focusable
+ textarea.setAttribute("tabindex", this.getAttribute("tabindex") || 0);
+ this.setAttribute("tabindex", -1);
+ // Save aria-description so keyboard navigation guidance can be added.
+ this.originalAriaDescription = this.getAttribute("aria-description") || "Code input field";
+
+ // Accessibility - detect when mouse focus to remove focus outline + keyboard navigation guidance that could irritate users.
+ this.addEventListener("mousedown", () => {
+ this.classList.add("code-input_mouse-focused");
+ });
+ textarea.addEventListener("blur", () => {
+ if(this.handleEventsFromTextarea) {
+ this.classList.remove("code-input_mouse-focused");
+ }
+ });
+
+ this.innerHTML = ""; // Clear Content
+
+ // Synchronise attributes to textarea
+ for(let i = 0; i < this.attributes.length; i++) {
+ let attribute = this.attributes[i].name;
+ if (codeInput.textareaSyncAttributes.includes(attribute) || attribute.substring(0, 5) == "aria-") {
+ textarea.setAttribute(attribute, this.getAttribute(attribute));
+ }
+ }
+
+ textarea.addEventListener('input', (evt) => { this.value = this.textareaElement.value; });
+
+ // Save element internally
+ this.textareaElement = textarea;
+ this.append(textarea);
+
+ // Create result element
+ let code = document.createElement("code");
+ let pre = document.createElement("pre");
+ pre.setAttribute("aria-hidden", "true"); // Hide for screen readers
+ pre.setAttribute("tabindex", "-1"); // Hide for keyboard navigation
+ pre.setAttribute("inert", true); // Hide for keyboard navigation
+
+ // Save elements internally
+ this.preElement = pre;
+ this.codeElement = code;
+ pre.append(code);
+ this.append(pre);
+
+ if (this.template.isCode) {
+ if (lang != undefined && lang != "") {
+ code.classList.add("language-" + lang.toLowerCase());
+ }
+ }
+
+ // dialogContainerElement used to store non-scrolling dialog boxes, etc.
+ let dialogContainerElement = document.createElement("div");
+ dialogContainerElement.classList.add("code-input_dialog-container");
+ this.append(dialogContainerElement);
+ this.dialogContainerElement = dialogContainerElement;
+
+ let keyboardNavigationInstructions = document.createElement("div");
+ keyboardNavigationInstructions.classList.add("code-input_keyboard-navigation-instructions");
+ dialogContainerElement.append(keyboardNavigationInstructions);
+
+ this.pluginEvt("afterElementsAdded");
+
+ this.dispatchEvent(new CustomEvent("code-input_load"));
+
+ this.value = value;
+ this.animateFrame();
+
+ const resizeObserver = new ResizeObserver((elements) => {
+ // The only element that could be resized is this code-input element.
+ this.syncSize();
+ });
+ resizeObserver.observe(this);
+ }
+
+ /**
+ * @deprecated Please use `codeInput.CodeInput.escapeHtml`
+ */
+ escape_html(text) {
+ return this.escapeHtml(text);
+ }
+
+ /**
+ * @deprecated Please use `codeInput.CodeInput.getTemplate`
+ */
+ get_template() {
+ return this.getTemplate();
+ }
+
+
+ /* ------------------------------------
+ * -----------Callbacks----------------
+ * ------------------------------------
+ * Implement the `HTMLElement` callbacks
+ * to trigger the main functionality properly. */
+
+ /**
+ * When the code-input element has been added to the document,
+ * find its template and set up the element.
+ */
+ connectedCallback() {
+ this.template = this.getTemplate();
+ if (this.template != undefined) {
+ 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: true,
+ attributeOldValue: true
+ });
+ }
+
+ mutationObserverCallback(mutationList, observer) {
+ for (const mutation of mutationList) {
+ if (mutation.type !== 'attributes')
+ continue;
+
+ /* Check regular attributes */
+ for(let i = 0; i < codeInput.observedAttributes.length; i++) {
+ if (mutation.attributeName == codeInput.observedAttributes[i]) {
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
+ }
+ }
+ if (mutation.attributeName.substring(0, 5) == "aria-") {
+ return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
+ }
+ }
+ }
+
+ disconnectedCallback() {
+ this.mutationObserver.disconnect();
+ }
+
+ /**
+ * Triggered when an observed HTML attribute
+ * has been modified (called from `mutationObserverCallback`).
+ * @param {string} name - The name of the attribute
+ * @param {string} oldValue - The value of the attribute before it was changed
+ * @param {string} newValue - The value of the attribute after it is changed
+ */
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (this.isConnected) {
+ this.pluginEvt("attributeChanged", [name, oldValue, newValue]);
+ switch (name) {
+
+ case "value":
+ this.value = newValue;
+ break;
+ case "template":
+ this.template = codeInput.usedTemplates[newValue || codeInput.defaultTemplate];
+ if (this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
+ else this.classList.remove("code-input_pre-element-styled");
+ // Syntax Highlight
+ this.scheduleHighlight();
+
+ break;
+
+ case "lang":
+ case "language":
+ let code = this.codeElement;
+ let mainTextarea = this.textareaElement;
+
+ // Check not already updated
+ if (newValue != null) {
+ newValue = newValue.toLowerCase();
+
+ if (code.classList.contains(`language-${newValue}`)) break; // Already updated
+ }
+
+ if(oldValue !== null) {
+ // Case insensitive
+ oldValue = oldValue.toLowerCase();
+
+ // Remove old language class and add new
+ code.classList.remove("language-" + oldValue); // From codeElement
+ code.parentElement.classList.remove("language-" + oldValue); // From preElement
+ }
+ // Add new language class
+ code.classList.remove("language-none"); // Prism
+ code.parentElement.classList.remove("language-none"); // Prism
+
+ if (newValue != undefined && newValue != "") {
+ code.classList.add("language-" + newValue);
+ }
+
+ if (mainTextarea.placeholder == oldValue) mainTextarea.placeholder = newValue;
+
+ this.scheduleHighlight();
+
+ break;
+ default:
+ if (codeInput.textareaSyncAttributes.includes(name) || name.substring(0, 5) == "aria-") {
+ if(newValue == null || newValue == undefined) {
+ this.textareaElement.removeAttribute(name);
+ } else {
+ this.textareaElement.setAttribute(name, newValue);
+ }
+ } else {
+ codeInput.textareaSyncAttributes.regexp.forEach((attribute) => {
+ if (name.match(attribute)) {
+ if(newValue == null) {
+ this.textareaElement.removeAttribute(name);
+ } else {
+ this.textareaElement.setAttribute(name, newValue);
+ }
+ }
+ });
+ }
+ break;
+ }
+ }
+
+ }
+
+ /* ------------------------------------
+ * -----------Overrides----------------
+ * ------------------------------------
+ * Override/Implement ordinary HTML textarea functionality so that the
+ * element acts just like a