Хотелoсь бы выслушать критику профессионалов по поводу такого варианта:
<html>
<head><title>Построчная нумерация</title>
<script title='Пробная заготовка "наблюдателя" для автоматического оформления TextArea'>
window.my_observer = new MutationObserver(WatchMutations);
const config = { attributes: false, childList: true, subtree: true };
window.my_observer.observe(document, config);
function WatchMutations(mutations, observer) {
for(var mutation of mutations) {
for(var node of mutation.addedNodes) {
if(node instanceof HTMLElement) {
if(node.matches("textarea[data-lines]")) {
// Условие, иначе всё повиснет!!!
if(!("value" in node.dataset))
node.addBuddy();
}
}
}
}
}
</script>
<script title='"Запасной" метод вычисления числа разрывов/переноса строки у конкретного TextArea'>
HTMLTextAreaElement.prototype.getLineBreaks = function(text) {
this.value = text;
// Здесь проводим принудительный ритуал для повышения точности
this.rows = 1;
this.style.paddingTop = "0px";
this.style.marginTop = "0px";
this.style.borderTop = "none";
this.style.borderTopWidth = "0px"; // Обязательно для FF
this.style.paddingBottom = "0px";
this.style.marginBottom = "0px";
this.style.borderBottom = "none";
this.style.borderBottomWidth = "0px"; // Для FF
// Сколько подстрок получилось?
return this.scrollHeight / this.offsetHeight;
}
</script>
<script title='Описание метода обновления колонки строк у TextArea'>
// Обновляем колонку нумерации строк
HTMLTextAreaElement.prototype.updateBuddy = function() {
var row = this.value.substr(0, this.selectionStart).split(/\r?\n/).length;
var col = this.value.substr(0, this.selectionEnd).split(/\r?\n/).pop().length + 1;
// ищем своего "товарища"
var buddy = this.parentElement.querySelector("[name='buddy']") || this.previousSibling;
var status = {
Row :row,
Column :col
}
// Убеждаемся в наличии колонки
if(buddy && buddy.localName == "textarea") {
buddy.rows = this.rows;
buddy.style.height = this.offsetHeight + "px";
buddy.scrollTop = this.scrollTop;
// Сверяемся количеством отображаемых строк
if(this.dataset.value != this.value || this.scrollHeight != buddy.scrollHeight) {
var i = 1;
// Разбиваем текст на строки
var text = this.value.split(/\r?\n/);
var lines = [];
// Выравниваем поле слева под нумерацию строк
buddy.cols = text.length.toString().length;
buddy.style.borderRightWidth = "0px";
this.style.paddingLeft = buddy.offsetWidth + "px";
// Создаём текстовое поле для построчной проверки признака переноса строки
var dummy = document.createElement("textarea");
// Настраиваем его на отображение одной строки при известном количестве колонок
dummy.rows = 1;
dummy.cols = this.cols;
dummy.style.width = this.offsetWidth + "px";
dummy.style.paddingLeft = buddy.offsetWidth + "px";
dummy.style.position = "absolute";
dummy.style.top = "400px";
dummy.style.paddingTop = "0px";
dummy.style.marginTop = "0px";
dummy.style.borderTop = "none";
dummy.style.borderTopWidth = "0px"; // Обязательно для FF
dummy.style.paddingBottom = "0px";
dummy.style.marginBottom = "0px";
dummy.style.borderBottom = "none";
dummy.style.borderBottomWidth = "0px"; // Обязательно для FF
// Делаем его невидимым
dummy.style.visibility='hidden';
// Вставляем в документ, иначе высота прокрутки будет нулевой
document.body.appendChild(dummy);
dummy.style.overflowX = "hidden"; // Обязательно для FF
dummy.style.overflowY = "scroll"; // Обязательно для точности
//buddy.style.borderRightWidth = (this.offsetWidth - buddy.offsetWidth) + "px";
this.style.background = `linear-gradient(90deg, rgba(0,0,0,0) ${buddy.clientWidth - 1}px, white ${buddy.clientWidth}px, white 100%)`;
// Прочёсываем все строки
console.time("Calculating line-breaks");
var fromTime = window.performance.now();
for(line of text) {
lines.push(i);
dummy.value = line;
// Не перенеслась ли строчка?
var separating = dummy.scrollHeight / dummy.offsetHeight;
// Разбиваем нумерацию в этом месте
for(j = 1; j < separating; ++ j)
lines.push("");
i ++;
}
console.timeEnd("Calculating line-breaks");
var lastTime = window.performance.now();
this.parentElement.querySelector("footer").title = `Last performance: ${(lastTime - fromTime).toPrecision(10)}ms`;
status["Scan in"] = `${((lastTime - fromTime) / 1000).toPrecision(5)} secs`;
// Обновляем всю колонку нумерации строк
buddy.value = lines.join("\r\n");
buddy.scrollTop = this.scrollTop;
// Удаляем поле коррекции
document.body.removeChild(dummy);
this.dataset.value = this.value;
}
}
// Добавляем необязательную отладочную информацию
status.Ratio = `${this.scrollHeight}/${buddy.scrollHeight}`;
this.parentElement.querySelector("footer").innerHTML = Object.keys(status).map(key => `${key}:${status[key]}`).join("|");
}
</script>
<script title='Описание метода добавления колонки нумерации строк у TextArea'>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
var buddy = document.createElement("textarea"); // "Товарищеская" колонка строк
var keeper = document.createElement("div"); // "Хранитель" связки элементов
// Чтобы всё не повисло при очередной "мутации", присваиваем значение прямо сейчас
this.dataset.value = this.value;
// Вставляем "хранителя" перед основным элементом
this.parentElement.insertBefore(keeper, this);
// Передаём основной элемент "хранителю"
keeper.appendChild(this);
// Текстовое поле поверх нумератора строк должно быть прозрачным, чтобы колёсико мышки действовало
this.style.backgroundColor = "transparent";
this.style.position = "relative";
// Наш "товарищ" должен располагаться слева от текста и отображать номера строк
buddy.style.position = "absolute";
buddy.style.overflow = "hidden";
buddy.style.textAlign = "right";
buddy.style.resize = "none";
// Нумерация строк "чёрным по серебристому"
buddy.style.backgroundColor = "silver";
// Доступ к цвету "товарищу" через основной элемент
Object.defineProperty(this, "buddy", {
get: (function() {
return this.buddy;
}).bind({buddy: buddy})
});
// Вставляем "товарища" перед основным элементом
this.parentElement.insertBefore(buddy, this);
// Вставляем заголовок над основным элементом?
if("caption" in this.dataset) {
var header = document.createElement("header"); // Заголовок текстового поля
this.parentElement.insertBefore(header, buddy); // Вставляем над "товарищем" и основным элементом
header.textContent = this.dataset["caption"] != "" ? this.dataset["caption"] : this.title;
// Предоставляем доступ к строке заголовка через основной элемент
Object.defineProperty(this, "caption", {
get: (function() {
return this.caption;
}).bind({caption: header})
});
}
if("status" in this.dataset) {
var footer = document.createElement("footer"); // Статус текстового поля
this.parentElement.appendChild(footer); // Вставляем строку статуса под основным элементом
// Предоставляем доступ к статусной строке через основной элемент
Object.defineProperty(this, "status", {
get: (function() {
return this.status;
}).bind({status: footer})
});
}
// Первая инициация "товарища"
buddy.setAttribute("name", "buddy");
this.updateBuddy();
// Примечание: Данная череда назначения обработчиков не обязательна,
// но необходима для демонстрации вызова в пользовательских функциях
this.addEventListener("mousemove",
function(evt) {
evt.srcElement.updateBuddy();
}
);
this.addEventListener("mouseup",
function(evt) {
evt.srcElement.updateBuddy();
}
);
this.addEventListener("keydown",
function(evt) {
evt.srcElement.updateBuddy();
}
);
this.addEventListener("keyup",
function(evt) {
evt.srcElement.updateBuddy();
}
);
this.addEventListener("scroll",
function(evt) {
evt.srcElement.updateBuddy();
}
);
}
</script>
<style>
div
{
position :absolute;
width :auto;
height :auto;
}
header
{
background-color:blue;
color :yellow;
}
footer
{
background-color:silver;
color :black;
border :medium silver sunken;
}
</style>
</head>
<body>
<input type=text placeholder='Цвет фона заголовка' onchange='hTextArea.caption.style.backgroundColor = this.value'>
<input type=text placeholder='Текст заголовка' onchange='hTextArea.caption.textContent = this.value'><br>
<input type=text placeholder='Цвет фона статуса' onchange='hTextArea.status.style.backgroundColor = this.value'>
<input type=text placeholder='Текст статуса' onchange='hTextArea.status.textContent = this.value'><br>
<input type=text placeholder='Цвет фона нумератора' onchange='hTextArea.buddy.style.backgroundColor = this.value'><br>
<textarea rows=5 cols=40 id=Main spellcheck=false title='Текстовое поле для редактирования' data-lines=show data-caption='Опциональный текст заголовка' data-status>
lorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.</textarea>
<script>
var hTextArea = document.querySelector("textarea#Main");
</script></body>
Так как здесь используются специфические
запрещённые приёмы, которые не всем браузерам придутся по вкусу.
Код разбит на отдельные законченные фрагменты и достаточно прокомментирован, чтобы легче воспринимался.
Теперь строка статуса имеет всплывающую подсказку с точным периодом времени, затраченного на расчёт строк.
В качестве эксперимента добавил несколько input'ов для непосредственного управления "заголовком" и "статусом", а также цветом самого нумератора, чтобы продемонстрировать простоту доступа ко всем сопутствующим элементам через основной.