Подсветка синтаксиса у TextArea
Сaмый простой способ - наслоить группу TextArea друг за другом и синхронно фильтровать в них текст соответственно синтаксической подкраске.
Однако, имеются некоторые нюансы. <html> <head> <title>Подсветка синтаксиса</title> <script> class FineTextArea extends HTMLTextAreaElement { constructor() { super(); } connectedCallback() { var update = this.update.bind(this, window.event, true); this.buddy = document.createElement("TextArea"); this.buddy.className = "FineTextArea-Buddy"; this.buddy.id = this.id + "_buddy"; this.buddy.disabled = true; this.parentElement.insertBefore(this.buddy, this); this.addEventListener("change", this.update.bind(this)); this.addEventListener("keyup", this.update.bind(this)); this.addEventListener("keydown", this.update.bind(this)); this.addEventListener("keypress", this.update.bind(this)); this.addEventListener("mousemove", this.update.bind(this)); this.addEventListener("mouseup", this.update.bind(this)); this.addEventListener("scroll", this.update.bind(this)); // Создаём элементы подсветки синтаксиса *!* const keywords = "dcr hlt inr inx lxi mov sphl xra jp".replace(/\s+/g, "|"); this.syntaxers = []; this.syntexps = [ new RegExp(`\\b(${keywords})\\b`, "gim"), new RegExp(`\\b(?!${keywords})\\b`, "gim"), new RegExp(`\\b(${keywords})\\b`, "gim"), /[^\x21-\x7F\n\r\s\t]+/gim, /[\x21-\x7F]+/gim ]; */!* for(var i in this.syntexps) { var syn = document.createElement("TextArea"); syn.className = "FineTextArea-Syntax" + i; syn.id = this.id + "_syntax" + i; syn.disabled = true; this.parentElement.insertBefore(syn, this); this.syntaxers.push(syn); } // Форсируем (альфа-отладка) this.lines = true; this.syntax = true; setTimeout(update, 0); setTimeout(update, 0); } set lines(value) { this._lines = value; this.buddy.style.visibility = value != false ? "visible" : "hidden"; this.style.paddingLeft = value != false ? this.buddy.offsetWidth + "px" : "0px"; if(value != false) this.update(window.event, true); } set syntax(value) { this._syntax = !!value; this.update(window.event, true); } get row() { return this.value.substr(0, this.selectionStart).split(/\r?\n/).length; } get col() { return this.value.substr(0, this.selectionEnd).split(/\r?\n/).pop().length + 1; } update(evt, force) { var buttons = evt && evt.buttons || 0; var rows = this.value.split(/\r?\n/), len = rows.length.toString().length; var syn, i, row = 1; this.buddy.scrollTop = this.scrollTop; for(syn of this.syntaxers) syn.scrollTop = this.scrollTop; var spc = ("string" == typeof this._lines && this._lines != "" ? this._lines.charAt(0) : "\xA0").repeat(len); var rex = new RegExp(`([ -])([^ -]{${len}})`, "gim"); // Выявление первых символов каждого слова // Распределяем текст по слоям var value = this.value, box = "\u2588"; for(i in this.syntexps) { this.syntaxers[i].value = value.replace(this.syntexps[i], s => box.repeat(s.length)); box = "\xA0"; if(i != 0) value = value.replace(this.syntexps[i], s => box.repeat(s.length)); } if((buttons == 0 && (Math.abs(this.buddy.scrollHeight - this.scrollHeight) > 30 || this.buddy.scrollWidth != this.scrollWidth)) || force == true) { var width; var height; var bg; // Здесь следуют некоторые манипуляции со стилями this.style.width = (parseInt(window.getComputedStyle(this, null).getPropertyValue("width")) & 0xFFE) + "px"; this.buddy.style.paddingRight = "0px"; this.buddy.style.width = "auto"; this.buddy.cols = len; // Маскируем задний план так, кроме области нумерации bg = `linear-gradient(90deg, rgba(0,0,0,0) ${this.buddy.clientWidth - 1}px, white ${this.buddy.clientWidth}px, white 100%)`; if(this._syntax == false || buttons) this.style.background = bg; else this.style.background = "transparent", this.syntaxers[0].style.background = bg; this.style.paddingLeft = `${this.buddy.clientWidth}px`; // Расчёт height = window.getComputedStyle(this, null).getPropertyValue("height"); width = window.getComputedStyle(this, null).getPropertyValue("width"); // Обновляем синтаксические помощники for(syn of this.syntaxers) { syn.cols = this.cols; syn.style.paddingLeft = `${this.buddy.clientWidth}px`; syn.style.height = height; syn.style.width = width; } // Копируем визуальные реквизиты элемента this.buddy.cols = this.cols; this.buddy.style.height = height; this.buddy.style.width = width; // Компенсируем визуальные отступы у текста и нумератора this.buddy.style.paddingLeft = window.getComputedStyle(this, null).getPropertyValue("padding-right"); this.buddy.style.paddingRight = window.getComputedStyle(this, null).getPropertyValue("padding-left"); if(this.scrollHeight != this.buddy.scrollHeight) force = !true; } // Затем всё совсем просто if(force == true || this._value != this.value) { // Переносим весь текст в поле нумератора, кроме первых символов каждого слова, заменяя их нумерацией console.time("Numbering"); for(var fine = 0; fine < 2; ++ fine) { this.buddy.value = this.value.replace(/^.*$/gm, function(str) { return Number(row ++).toString().padStart(len, spc) + str.substr(str.charAt(0) == "\t" ? 0 : len).replace(rex, `$1${spc}`); } ); if(this.scrollHeight == this.buddy.scrollHeight) fine = 1; else fine = 1; } console.timeEnd("Numbering"); this._value = this.value; } this.buddy.style.visibility = buttons || this._lines == false ? "hidden" : "visible"; this.buddy.scrollTop = this.scrollTop; for(syn of this.syntaxers) syn.scrollTop = this.scrollTop, syn.style.visibility = buttons || this._syntax == false ? "hidden" : "visible"; this.style.color = buttons || this._syntax == false ? "black" : "transparent"; } static get observedAttributes() { return "lines syntax".split(/\s+/); } attributeChangedCallback(name, oldValue, newValue) { switch(name) { case "lines": this._lines = newValue; break; case "syntax": this._syntax = !!newValue; //this.update(window.event, true); break; } } } customElements.define("fine-textarea", FineTextArea, {extends: "textarea"}); </script> <style> textarea[is='fine-textarea'] { position :relative; background-color:transparent; color :transparent; caret-color :black; } textarea.FineTextArea-Buddy { position :absolute; background-color:yellow; } textarea.FineTextArea-Syntax0, textarea.FineTextArea-Syntax1, textarea.FineTextArea-Syntax2, textarea.FineTextArea-Syntax3, textarea.FineTextArea-Syntax4, textarea.FineTextArea-Syntax5, textarea.FineTextArea-Syntax6, textarea.FineTextArea-Syntax7, textarea.FineTextArea-Syntax8, textarea.FineTextArea-Syntax9 { position :absolute; background-color:transparent; color :orange; animation-duration: 10s; animation-timing-function: linear; animation-name :Layer; animation-iteration-count: infinite; } textarea.FineTextArea-Syntax0 { color :lightgreen; --xy:60px;} textarea.FineTextArea-Syntax1 { color :darkgreen; --xy:110px;} textarea.FineTextArea-Syntax2 { color :magenta; --xy:160px;} textarea.FineTextArea-Syntax3 { color :blue; --xy:210px;} @keyframes Layer { from, 33% { transform:translate(0px, 0px); } 50%,100% { transform:translate(var(--xy), var(--xy)); } } </style> </head> <body> <input type=text placeholder='Атрибут = Значение' onchange='this.style.backgroundColor = "red"; eval(`hTextArea.${this.value}`); this.style.backgroundColor = "yellow"'> <input type=text placeholder='Цвет фона нумератора' onchange='hTextArea.buddy.style.backgroundColor = this.value'><br> <textarea rows=15 cols=40 id=Main spellcheck=false title='Текстовое поле для редактирования' is=fine-textarea lines=true syntax=true> ____________________________________ |Проверка цветовой подсветки V0.003| ==================================== lxi h,076D0h sphl xra a L1: mov m,a inx h dcr h inr h jp L1 hlt </textarea> <script> var hTextArea = document.querySelector("textarea#Main"); </script> </body>Как видно из анимации в примере, всё работает до примитивного просто в пользовательском элементе. Но, есть проблемы в строках #43-45 (без этого сбивается изначальный вид) и в строках #25-33 (нужно каким-то атрибутом унифицировать управление правилами синтаксической фильтрации). P.S.: Данная тема - логическое развитие предыдущей… |
Часовой пояс GMT +3, время: 05:56. |