Javascript-форум (https://javascript.ru/forum/)
-   Элементы интерфейса (https://javascript.ru/forum/dom-window/)
-   -   Отображение строк у TextArea (https://javascript.ru/forum/dom-window/84421-otobrazhenie-strok-u-textarea.html)

Alikberov 05.09.2022 01:00

Отображение строк у TextArea
 
Сaмый простой способ - поставить два TextArea рядом и в левом отображать все номера строк, имеющихся в правом, синхронизируя по скроллингу.
Однако, такой подход, хоть визуально практически не имеет глюков, крайне медленный, если довольно активно число строк растёт.

Самый быстрый способ - через OL-список, так как требуется попросту создать несколько пустых пунктов, число которых эквивалентно количеству отображаемых строк текста.
Но за месяц экспериментов я так и не смог выйти из тупика глюков.
<html>
<head>
<title>Построчная нумерация</title>
<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>
<div>
<textarea rows=15 cols=105 spellcheck=false title='Текстовое поле для редактирования'>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est, omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
</textarea>
</div>

<script>
// Обновляем колонку нумерации строк
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	lineHeight = this.dataset.lineHeight;
	// Смещение по вертикали для синхронности со скролл-баром
	var	marginTop = Math.abs(lineHeight - ((this.scrollTop - (this.offsetHeight - this.clientHeight)) % (lineHeight)));
	var	i;
	var	buddy = this.previousSibling;
	// Убеждаемся в наличии колонки
	if(buddy && buddy.localName == "ol") {
		// Сверяемся количеством отображаемых строк
		if(this.rows != buddy.children.length) {
			buddy.replaceChildren();
			// Обновляем всю колонку нумерации строк
			for(i = 0; i < this.getAttribute("rows"); ++ i)
				buddy.appendChild(document.createElement("li"));
		}
		// Резервируем в текстовом поле область под "товарищескую" колонку с нумерацией строк
		this.style.paddingLeft = buddy.offsetWidth + "px";
		// Вычисляем номер самой верхней отображаемой строки текстового поля
		//buddy.start = 1 + Math.floor(this.scrollTop / (this.scrollHeight / this.value.split(/\r?\n/).length));
		buddy.start = 1 + this.scrollTop / this.dataset.lineHeight;
		buddy.style.marginTop = marginTop;
	}
	// Дальше следует чуток лишнего отладочного кода
	var	status = {
		Row			:row,
		Column		:col,
		margin		:marginTop,
		lineHeight	:lineHeight,
		scrollTop	:this.scrollTop
	}
	this.parentElement.querySelector("footer").innerHTML = Object.keys(status).map(key => `${key}:${status[key]}`).join("<br>");
}
</script>
<script>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
	var	buddy = document.createElement("ol");
	var	header  = document.createElement("header");
	var	footer  = document.createElement("footer");
	var	rows = this.rows;
	// Узнаём точную высоту одной строчки в текстовом поле
	this.rows = 1;
	buddy.style.position = "fixed";
	// Приводим нумератор строк к общему стилю
	buddy.style.fontFamily = document.defaultView.getComputedStyle(this, null).getPropertyValue("font-family");
	buddy.style.marginTop = document.defaultView.getComputedStyle(this, null).getPropertyValue("border-top");
	// Запоминаем точное значение высоты одной строки
	// \/ - это не даёт желаемого результата - \/
	this.dataset.lineHeight = this.clientHeight;
	// \/ - это тоже не даёт желаемого результата - \/
	this.dataset.lineHeight = parseFloat(document.defaultView.getComputedStyle(this, null).getPropertyValue("font-size"));
	// Восстанавливаем предустановленное значение отображаемых строк
	this.rows = rows;
	this.style.display = "inline-block";
	// Вставляем "товарища" перед основным элементом
	this.parentElement.insertBefore(buddy, this);
	// Вставляем заголовок над основным элементом
	this.parentElement.insertBefore(header, buddy);
	header.textContent = this.title;
	// Вставляем строку статуса под основным элементом
	this.parentElement.appendChild(footer);
	// Первая инициация "товарища"
	buddy.setAttribute("name", "buddy");
	this.updateBuddy();
	this.addEventListener("mouseup",
		function(evt) {
			var	src = evt.srcElement;
			var	lineHeight = src.scrollHeight / src.value.split(/\r?\n/).length;
			var	rows = Math.floor(src.offsetHeight / lineHeight);
			// Проверяем на изменение размера мышкой
			if(src.rows != rows) {
				src.rows = rows;
				src.updateBuddy();
			}
		}
	);
	this.addEventListener("keydown",
		function(evt) {
			var	src = evt.srcElement;
			src.updateBuddy();
		}
	);
	this.addEventListener("scroll",
		function(evt) {
			var	src = evt.srcElement;
			src.updateBuddy();
		}
	);
}
</script>

<script>
document.querySelector("textarea").addBuddy();
</script>
</body>
Потому, прошу помощи в этом математическом лабиринте свойств.:lol:

voraa 05.09.2022 08:06

То, что не нумеруется первая строка - это бага или фича?

По любому строка 129 должна быть так

buddy.style.marginTop = document.defaultView.getComputedStyle(this, null).getPropertyValue("border-top-width");

(Хотя проще
buddy.style.marginTop = getComputedStyle(this, null).borderTopWidth;
)

а строка 104

buddy.style.marginTop = marginTop + 'px';

(а нужна ли она вообще?)

voraa 05.09.2022 08:19

По поводу стр 129.
Там не только в отступе (marginTop) borderTopWidth надо учитывать, но и paddingTop.

как то так должно быть

const {borderTopWidth, paddingTop} = getComputedStyle(this, null)
buddy.style.marginTop = (parseFloat(borderTopWidth) + parseFloat(paddingTop)) + 'px'

Alikberov 05.09.2022 12:42

Цитата:

Сообщение от voraa (Сообщение 547773)
То, что не нумеруется первая строка - это бага или фича?

Вoт именно, что баг!:lol:
Цитата:

Сообщение от voraa (Сообщение 547773)
По любому строка 129 должна быть так
а строка 104

(а нужна ли она вообще?)

Ничего не помогает!:lol:

На самом деле на Си в WinAPI я тоже как-то неделю боролся с подобной задачей (кажется, решил).

А в HTML/CSS никак не получается.
Вот тут в редакторе я просто (временно) пристыковал два TextArea к друг друг - выглядит вполне нормально и сколлер нормально отрабатывается (хотя и с задержкой).

И вот решил это «временное» переписать нормально…
Но, никак не получается!

Всё должно выглядить как в стандартном редакторе. Без фич и исключений.

А то у меня сейчас и последняя строка текста слева индексируется как #49, хотя внизу же указывается «Row:46»: На три единицы ошибка…:-?

voraa 05.09.2022 16:48

<html>
<head>
<title>Построчная нумерация</title>
<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>
<div>
<textarea rows=15 cols=105 spellcheck=false title='Текстовое поле для редактирования'>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est, omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
</textarea>
</div>
 
<script>
// Обновляем колонку нумерации строк
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 lineHeight = this.dataset.lineHeight;
    // Смещение по вертикали для синхронности со скролл-баром
    var marginTop = Math.abs(lineHeight - ((this.scrollTop - (this.offsetHeight - this.clientHeight)) % (lineHeight)));
    var i;
    var buddy = this.previousSibling;
    // Убеждаемся в наличии колонки
    if(buddy && buddy.localName == "ol") {
        // Сверяемся количеством отображаемых строк
        if(this.rows != buddy.children.length) {
            buddy.replaceChildren();
            // Обновляем всю колонку нумерации строк
            for(i = 0; i < this.getAttribute("rows"); ++ i)
                buddy.appendChild(document.createElement("li"));
        }
        // Резервируем в текстовом поле область под "товарищескую" колонку с нумерацией строк
        this.style.paddingLeft = buddy.offsetWidth + "px";
        // Вычисляем номер самой верхней отображаемой строки текстового поля
        //buddy.start = 1 + Math.floor(this.scrollTop / (this.scrollHeight / this.value.split(/\r?\n/).length));
        buddy.start = 1 + this.scrollTop / this.dataset.lineHeight;
        //buddy.style.marginTop = marginTop + 'px';
    }
    // Дальше следует чуток лишнего отладочного кода
    var status = {
        Row         :row,
        Column      :col,
        margin      :marginTop,
        lineHeight  :lineHeight,
        scrollTop   :this.scrollTop
    }
    this.parentElement.querySelector("footer").innerHTML = Object.keys(status).map(key => `${key}:${status[key]}`).join("<br>");
}
</script>
<script>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
    var buddy = document.createElement("ol");
    var header  = document.createElement("header");
    var footer  = document.createElement("footer");
    var rows = this.rows;
    // Узнаём точную высоту одной строчки в текстовом поле
    this.rows = 1;
    buddy.style.position = "absolute";
    // Приводим нумератор строк к общему стилю
    buddy.style.fontFamily = document.defaultView.getComputedStyle(this, null).getPropertyValue("font-family");
    const mt = getComputedStyle(this, null).borderTopWidth;
    const pb = getComputedStyle(this, null).paddingBottom;
    //document.defaultView.getComputedStyle(this, null).getPropertyValue("border-top-width");
    
    buddy.style.marginTop = (parseFloat(mt) + parseFloat(pb)) +'px';
    // Запоминаем точное значение высоты одной строки
    // \/ - это не даёт желаемого результата - \/
    this.dataset.lineHeight = this.clientHeight;
    // \/ - это тоже не даёт желаемого результата - \/
    this.dataset.lineHeight = parseFloat(document.defaultView.getComputedStyle(this, null).getPropertyValue("font-size"));
    // Восстанавливаем предустановленное значение отображаемых строк
    this.rows = rows;
    this.style.display = "inline-block";
    // Вставляем "товарища" перед основным элементом
    this.parentElement.insertBefore(buddy, this);
    // Вставляем заголовок над основным элементом
    this.parentElement.insertBefore(header, buddy);
    header.textContent = this.title;
    // Вставляем строку статуса под основным элементом
    this.parentElement.appendChild(footer);
    // Первая инициация "товарища"
    buddy.setAttribute("name", "buddy");
    this.updateBuddy();
    this.addEventListener("mouseup",
        function(evt) {
            var src = evt.srcElement;
            var lineHeight = src.scrollHeight / src.value.split(/\r?\n/).length;
            var rows = Math.floor(src.offsetHeight / lineHeight);
            // Проверяем на изменение размера мышкой
            if(src.rows != rows) {
                src.rows = rows;
                src.updateBuddy();
            }
        }
    );
    this.addEventListener("keydown",
        function(evt) {
            var src = evt.srcElement;
            src.updateBuddy();
        }
    );
    this.addEventListener("scroll",
        function(evt) {
            var src = evt.srcElement;
            src.updateBuddy();
        }
    );
}
</script>
 
<script>
document.querySelector("textarea").addBuddy();
</script>
</body>

Вроде так лучше.
Что не так?

Alikberov 05.09.2022 17:00

Идеально должно быть так:
<html>
<head>
<title>Построчная нумерация</title>
<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>
<div>
<textarea rows=15 cols=90 spellcheck=false title='Текстовое поле для редактирования'>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est, omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
repellat.
</textarea>
</div>

<script>
// Обновляем колонку нумерации строк
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	i = 0;
	var	buddy = this.previousSibling;
	// Убеждаемся в наличии колонки
	if(buddy && buddy.localName == "textarea") {
		// Сверяемся количеством отображаемых строк
		if(this.rows != buddy.rows || this.scrollHeight != buddy.scrollHeight || this.offsetHeight != buddy.offsetHeight) {
			buddy.rows = this.rows;
			buddy.cols = this.value.split(/\r?\n/).length.toString().length;
			// Обновляем всю колонку нумерации строк
			buddy.value = this.value.replace(/^.*$/gm, () => ++ i);
			// Выравниваем поле слева под нумерацию строк
			this.style.paddingLeft = buddy.offsetWidth;
			buddy.style.height = this.offsetHeight + "px";
		}
		buddy.scrollTop = this.scrollTop;
	}
	// Дальше следует информирование о позиции каретки
	var	status = {
		Row			:row,
		Column		:col
	}
	this.parentElement.querySelector("footer").innerHTML = Object.keys(status).map(key => `${key}:${status[key]}`).join("|");
}
</script>
<script>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
	var	buddy = document.createElement("textarea");
	var	header  = document.createElement("header");
	var	footer  = document.createElement("footer");
	buddy.style.position = "fixed";
	buddy.style.overflow = "hidden";
	buddy.style.textAlign = "right";
	buddy.style.resize = "none";
	buddy.disabled = true;
	this.style.display = "inline-block";
	// Вставляем "товарища" перед основным элементом
	this.parentElement.insertBefore(buddy, this);
	// Вставляем заголовок над основным элементом
	this.parentElement.insertBefore(header, buddy);
	header.textContent = this.title;
	// Вставляем строку статуса под основным элементом
	this.parentElement.appendChild(footer);
	// Первая инициация "товарища"
	buddy.setAttribute("name", "buddy");
	this.updateBuddy();
	this.addEventListener("mousemove",
		function(evt) {
			var	src = evt.srcElement;
			src.updateBuddy();
		}
	);
	this.addEventListener("keydown",
		function(evt) {
			var	src = evt.srcElement;
			src.updateBuddy();
		}
	);
	this.addEventListener("keyup",
		function(evt) {
			var	src = evt.srcElement;
			src.updateBuddy();
		}
	);
	this.addEventListener("scroll",
		function(evt) {
			var	src = evt.srcElement;
			src.updateBuddy();
		}
	);
}
</script>

<script>
document.querySelector("textarea").addBuddy();
</script>
</body>

Aetae 05.09.2022 18:51

У вас всё дрожит дёргается и скачет. Возьми любой готовый редактор на textarea и посмотри как там.

Alikberov 05.09.2022 19:30

Цитата:

Сообщение от Aetae (Сообщение 547795)
У вас всё дрожит дёргается и скачет. Возьми любой готовый редактор на textarea и посмотри как там.

Готовoго решения я как-то не нашёл.

Adding Line Numbers To HTML Textarea

Почему вообще за четверть века не ввели в спецификацию стиля с флагом отображения этих номеров?
Всюду нужно подключать сторонние библиотеки с кучей кода…

P.S.: Несчастный Math.sign появился совсем недавно.
И что мешало его давно ввести?

voraa 05.09.2022 20:06

Цитата:

Сообщение от Alikberov
Несчастный Math.sign появился совсем недавно.
И что мешало его давно ввести?

Ну вроде в 2013-2014г.
А может он просто не нужен особо был?

Вообще понятие строки довольно размытое.
Что является строкой - то, что выглядит как строка, или это текст от \n до \n?
Во всех редакторах, которые показывают номера строк - это второе. У вас - первое.
Если очень длинная строка занимает в визуальном узком окне несколько строк, то номер имеет только первая часть строки, а те, что перенесены идут без своих номеров, т.к это часть той же строки. А следующий номер имеет следующая строка, которая идет после \n.
А какой смысл в ваших номерах, если эти номера зависят не от реального количества строк, а от ширины окна? На такой номер нельзя сослаться.

Alikberov 05.09.2022 20:25

Цитата:

Сообщение от voraa (Сообщение 547800)
Ну вроде в 2013-2014г.

Относительнo недавно. Гораздо позднее, чем появились контексты Canvas, Audio, Video.
Цитата:

Сообщение от voraa (Сообщение 547800)
А может он просто не нужен особо был?

Ну, в Бейсике оно было всегда. А в JS иногда бывали моменты, где с этим было бы проще.
Цитата:

Сообщение от voraa (Сообщение 547800)
Вообще понятие строки довольно размытое.
Что является строкой - то, что выглядит как строка, или это текст от \n до \n?
Во всех редакторах, которые показывают номера строк - это второе.

Для активной разработки - очень важны.
Цитата:

Сообщение от voraa (Сообщение 547800)
Если очень длинная строка занимает в визуальном узком окне несколько строк, то номер имеет только первая часть строки, а те, что перенесены идут без своих номеров, т.к это часть той же строки. А следующий номер имеет следующая строка, которая идет после \n.
А какой смысл в ваших номерах, если эти номера зависят не от реального количества строк, а от ширины окна? На такой номер нельзя сослаться.

Вот это - большая проблема. И каждый решает её сам, так как до сих пор стандартного алгоритма (встроенного) не предоставили.

voraa 05.09.2022 21:01

Ну это немного разные понятия - функция в языке, и какой то новый стиль в элементе HTML.
Js к стилям и html имеет очень маленькое отношение (точнее никакого).
Js он и в браузере и в Node один и тот же. А API разные.

Alikberov 05.09.2022 21:24

Цитата:

Сообщение от voraa (Сообщение 547803)
Ну это немного разные понятия - функция в языке, и какой то новый стиль в элементе HTML.
Js к стилям и html имеет очень маленькое отношение (точнее никакого).
Js он и в браузере и в Node один и тот же. А API разные.

Согласeн, понятия разные и аналогия не совсем корректная.
Просто, хотел обратить внимание на масштаб изменений.

Описание функции Math.sign - не более сотни байтов определённого машинного кода.
Описание тегов canvas/audio/video - больше сотни различных библиотек.

Вопрос лишь в формальностях, что развивать…:yes:

В том же jsfiddle код подсветки синтаксиса и строк - значимый процент на фоне общего кода.
И уважаемый себя ресурс с интерактивной демонстрацией примеров кода всегда предоставляет нумерацию к строкам листинга.

P.S.: Так или иначе, рано или поздно, всё равно поддержку введут на базе известной библиотеки.
Вопрос вот только когда и на какой библиотеке?:)

Alikberov 06.09.2022 23:03

Цитата:

Сообщение от Aetae (Сообщение 547795)
У вас всё дрожит дёргается и скачет.

Именнo на #6?
Можно поинтересоваться, в каком браузере это наблюдается и как можно побороть?:thanks:

Aetae 06.09.2022 23:14

Alikberov, левый нижний угол:

Alikberov 07.09.2022 12:52

Вложений: 1
Цитата:

Сообщение от Aetae (Сообщение 547820)
Alikberov, левый нижний угол:

Дa, в FF такая проблема наблюдается (в Chrome её практически не видно, если на видео не записать).
А можно ли как-то побороть?
Ну, preventDefault поставить, например, со своим кодом скроллинга (что усложняет задачу).

P.S.: Спасибо за анимацию!:thanks:

voraa 07.09.2022 14:21

Alikberov,
Попробуйте вставлять длинные строки (длиннее ширины области), что бы они переносились на 2-3 строки.
Там все еще хуже будет.

Alikberov 07.09.2022 18:00

Цитата:

Сообщение от voraa (Сообщение 547827)
Попробуйте вставлять длинные строки (длиннее ширины области), что бы они переносились на 2-3 строки.
Там все еще хуже будет.

Эту ситуацию я пока в расчёт совсем не брал, так как там просто не известно, как в двух строчках всё вычислить…

Здесь нужно подключать готовую библиотеку или писать своё.

Думал, что как-то можно реализовать такую простую элементарную функцию одним блоком кода.

micscr 08.09.2022 06:34

Цитата:

Сообщение от Alikberov (Сообщение 547798)
Готовoго решения я как-то не нашёл.

jQuery-вский вроде норм.

voraa 08.09.2022 08:36

Цитата:

Сообщение от micscr
jQuery-вский вроде норм.

Там даже на картинке видно, что номера и строки не совпадают. Номера как бы между строк получаются.

Alikberov 08.09.2022 16:08

Цитата:

Сообщение от voraa (Сообщение 547827)
Alikberov,
Попробуйте вставлять длинные строки (длиннее ширины области), что бы они переносились на 2-3 строки.
Там все еще хуже будет.

Кажeтся, я нашёл промежуточный костыльный вариант.

Вроде бы работает, но крайне медленный.
<html>
<head>
<title>Построчная нумерация</title>
<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>
<div>
<textarea rows=15 cols=40 spellcheck=false title='Текстовое поле для редактирования'>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est,
omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
repellat.
</textarea>
</div>

<script>
// Обновляем колонку нумерации строк
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 = [];
			// Создаём текстовое поле для построчной проверки признака переноса строки
			var	dummy = document.createElement("textarea");
			// Делаем его невидимым
			dummy.style.position='absolute';
			dummy.style.top='800px';
			dummy.style.visibility='hidden';
			// Вставляем в документ, иначе высота прокрутки будет нулевой
			document.body.appendChild(dummy);
			dummy.rows = 1;
			dummy.cols = this.cols;
			dummy.style.width = this.offsetWidth + "px";
			// Выравниваем поле слева под нумерацию строк
			var	lineHeight = dummy.scrollHeight;
			buddy.cols = text.length.toString().length;
			this.style.paddingLeft = buddy.offsetWidth + "px";
			dummy.style.paddingLeft = buddy.offsetWidth + "px";
			status.lineHeight = lineHeight;
			status.scrollHeight = dummy.scrollHeight;
			// Прочёсываем все строки
			for(line of text) {
				lines.push(i);
				dummy.value = line;
				// Не перенеслась ли строчка?
				if(dummy.scrollHeight > lineHeight) {
					var	separating = Math.floor(dummy.scrollHeight / (lineHeight - 0));
					// Разбиваем нумерацию в этом месте
					for(j = 0; j < separating; ++ j)
						lines.push("");
				}
				i ++;
			}
			// Обновляем всю колонку нумерации строк
			buddy.value = lines.join("\r\n");
			buddy.scrollTop = this.scrollTop;
			// Удаляем поле коррекции
			document.body.removeChild(dummy);
			this.dataset.value = this.value;
		}
	}
	this.parentElement.querySelector("footer").innerHTML = Object.keys(status).map(key => `${key}:${status[key]}`).join("|");
}
</script>
<script>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
	var	buddy = document.createElement("textarea");	// "Товарищеская" колонка строк
	var	header  = document.createElement("header");	// Заголовок текстового поля
	var	footer  = document.createElement("footer");	// Статус текстового поля
	// Наш "товарищ" должен располагаться слева от текста
	buddy.style.position = "fixed";
	buddy.style.overflow = "hidden";
	buddy.style.textAlign = "right";
	buddy.style.resize = "none";
	buddy.disabled = true;
	this.style.display = "inline-block";
	// Вставляем "товарища" перед основным элементом
	this.parentElement.insertBefore(buddy, this);
	// Вставляем заголовок над основным элементом
	this.parentElement.insertBefore(header, buddy);
	header.textContent = this.title;
	// Вставляем строку статуса под основным элементом
	this.parentElement.appendChild(footer);
	// Первая инициация "товарища"
	buddy.setAttribute("name", "buddy");
	this.dataset.value = this.value;
	this.updateBuddy();
	// Назначаем события для оперативной синхронизации "товарища"
	this.addEventListener("mousemove",
		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>

<script>
document.querySelector("textarea").addBuddy();
</script>
</body>
Эрзац собственной разработки хоть более-менее работает, но глючит местами довольно конкретно.:blink:
(Если не менять размера поля, то скроллинг до последней строки работает почти нормально, так как невыровненный размер по вертикали сильно влияет на расчёты.)

Alikberov 09.09.2022 19:00

Имеются ли жалобы (исключая производительность)
 
В общем, пришлось повозиться со стилями и устранить ненужные помехи в расчётах высоты текста.

Судя по значению «Ratio» в статусе, теперь высота прокрутки текста совпадает с высотой прокрутки нумерации, что было проблемой и вызывало расхождения на последней строке…
Сейчас учитывается ширина скролл-бара при построчном анализе высоты текста и сбоев не наблюдалось.
<html>
<head>
<title>Построчная нумерация</title>
<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>
<div>
<textarea rows=15 cols=40 spellcheck=false title='Текстовое поле для редактирования'>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est,
omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
repellat.
</textarea>
</div>

<script>
HTMLTextAreaElement.prototype.getLineBreaks = function(text) {
	const	cleaner = "0px 0px 0px 0px";
	// Без "новой строки" бывает сбой,
	// так как вся строка умещается на одной линии и не учитывается ширина скролл-бара
	this.value = text + "\r\n";
	this.rows = 1;
	this.style.paddingTop = "0px";
	this.style.marginTop = "0px";
	this.style.borderTop = "none";
	this.style.paddingBottom = "0px";
	this.style.marginBottom = "0px";
	this.style.borderBottom = "none";
	return this.scrollHeight / this.offsetHeight - 1;
}
</script>

<script>
// Обновляем колонку нумерации строк
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;
			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 = "-800px";
			// Делаем его невидимым
			dummy.style.visibility='hidden';
			// Вставляем в документ, иначе высота прокрутки будет нулевой
			document.body.appendChild(dummy);
			// Прочёсываем все строки
			for(line of text) {
				lines.push(i);
				dummy.value = line;
				// Не перенеслась ли строчка?
				var	separating = dummy.getLineBreaks(line);
				// Разбиваем нумерацию в этом месте
				for(j = 1; j < separating; ++ j)
					lines.push("");
				i ++;
			}
			// Обновляем всю колонку нумерации строк
			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>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
	var	buddy = document.createElement("textarea");	// "Товарищеская" колонка строк
	var	header  = document.createElement("header");	// Заголовок текстового поля
	var	footer  = document.createElement("footer");	// Статус текстового поля
	// Наш "товарищ" должен располагаться слева от текста
	buddy.style.position = "fixed";
	buddy.style.overflow = "hidden";
	buddy.style.textAlign = "right";
	buddy.style.resize = "none";
	buddy.disabled = true;
	this.style.display = "inline-block";
	// Вставляем "товарища" перед основным элементом
	this.parentElement.insertBefore(buddy, this);
	// Вставляем заголовок над основным элементом
	this.parentElement.insertBefore(header, buddy);
	header.textContent = this.title;
	// Вставляем строку статуса под основным элементом
	this.parentElement.appendChild(footer);
	// Первая инициация "товарища"
	buddy.setAttribute("name", "buddy");
	this.dataset.value = this.value;
	this.updateBuddy();
	// Назначаем события для оперативной синхронизации "товарища"
	this.addEventListener("mousemove",
		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>

<script>
document.querySelector("textarea").addBuddy();
</script>
</body>
P.S: Буду рад выслушать замечания о выявленных сбоях в этом скрипте класса «дёшего и сердито»…:thanks:

рони 09.09.2022 21:05

Alikberov,
скролл над нумерацией отсутствует, это так и задумано?

Alikberov 09.09.2022 22:22

Предыдущий вариант не дружит с FireFox и пришлось повозиться, так как всё было ужасно!
(Оставлю вариант выше для сравнения…)

Этот вариант корректно работает и в FF:
<html>
<head>
<title>Построчная нумерация</title>
<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>
<div>
<textarea rows=15 cols=40 spellcheck=false title='Текстовое поле для редактирования'>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est,
omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
repellat.
</textarea>
</div>

<script>
// Возвращает, на какое количество подстрок разбилась строка
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>
// Обновляем колонку нумерации строк
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;
			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.visibility='hidden';
			// Вставляем в документ, иначе высота прокрутки будет нулевой
			document.body.appendChild(dummy);
			dummy.style.overflowX = "hidden";	// Обязательно для FF
			dummy.style.overflowY = "scroll";	// Обязательно для точности
			// Прочёсываем все строки
			for(line of text) {
				lines.push(i);
				dummy.value = line;
				// Не перенеслась ли строчка?
				var	separating = dummy.getLineBreaks(line);
				// Разбиваем нумерацию в этом месте
				for(j = 1; j < separating; ++ j)
					lines.push("");
				i ++;
			}
			// Обновляем всю колонку нумерации строк
			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>
// Добавляем колонку нумерации строк
HTMLTextAreaElement.prototype.addBuddy = function() {
	var	buddy = document.createElement("textarea");	// "Товарищеская" колонка строк
	var	header  = document.createElement("header");	// Заголовок текстового поля
	var	footer  = document.createElement("footer");	// Статус текстового поля
	// Наш "товарищ" должен располагаться слева от текста
	buddy.style.position = "fixed";
	buddy.style.overflow = "hidden";
	buddy.style.textAlign = "right";
	buddy.style.resize = "none";
	buddy.disabled = true;
	this.style.display = "inline-block";
	// Вставляем "товарища" перед основным элементом
	this.parentElement.insertBefore(buddy, this);
	// Вставляем заголовок над основным элементом
	this.parentElement.insertBefore(header, buddy);
	header.textContent = this.title;
	// Вставляем строку статуса под основным элементом
	this.parentElement.appendChild(footer);
	// Первая инициация "товарища"
	buddy.setAttribute("name", "buddy");
	this.dataset.value = this.value;
	this.updateBuddy();
	// Назначаем события для оперативной синхронизации "товарища"
	this.addEventListener("mousemove",
		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>

<script>
document.querySelector("textarea").addBuddy();
</script>
</body>
Цитата:

Сообщение от рони (Сообщение 547893)
Alikberov,
скролл над нумерацией отсутствует, это так и задумано?

Что Вы имеете в виду?
Нумерация строк не прокручивается вместе с текстом?

рони 09.09.2022 22:27

Цитата:

Сообщение от Alikberov
Что Вы имеете в виду?

курсор на цифры, колесо мышки круть, нефига не круть)))

Alikberov 09.09.2022 22:54

Цитата:

Сообщение от рони (Сообщение 547895)
курсор на цифры, колесо мышки круть, нефига не круть)))

A на сколько важна такая функция?:D

Ведь основные усилия я прикладывал к известной задаче (адекватное отображение нумерации без подключения сторонних тяжёлых библиотек).

В DOM столбик нумератора имеет флаг «disabled» и «fixed», а попытки сейчас поправить положение вещей чуть не поломало всё…:agree:

Сейчас скрипт функционирует на принципе «работает - не трожь!» и я уже опасаюсь что-то менять…
Нужно потом в копии скрипта опыты проводить, если потребуется…
А так, в контексте вдруг предложенной задачи #16 вопрос исчерпан.
Нужно только какую-то обвёртку написать, чтобы не вызывать функцию явно, а лишь добавлять к любому TextArea атрибут, например, "numbering".

P.S.: Зашёл на страницу Война и Мир и вставил сюда всю страницу целиком…
  • Chrome: Скрипт задумался на целых 2 минуты, а после выдал 7751 строку без глюка.
  • FireFox: Скрипт задумался примерно на 50 секунд и выдал 7751 строку.

Alikberov 11.09.2022 22:00

Идeально было бы использовать фоном element нумератора, как я понял. Который, к сожалению, в FF только и работает.

На данный момент я попытался использовать фоновый наблюдатель мутаций:
// Create an observer instance linked to the callback function
window.user_observer = new MutationObserver(WatchMutations);
// Options for the observer (which mutations to observe)
const config = { attributes: false, childList: true, subtree: true };
// Start observing the target node for configured mutations
window.user_observer.observe(document, config);

function WatchMutations(mutations, observer) {
	for(var mutation of mutations) {
		// examine new nodes, is there anything to highlight?
		for(var node of mutation.addedNodes) {
			// we track only elements, skip other nodes (e.g. text nodes)
			if(node instanceof HTMLElement) {
				// check the inserted element for being a code snippet
				if(node.matches("textarea[data-lines]")) {
					node.addBuddy();
				}
			}
		}
	}
}
Который выполняет свою функцию и сам добавляет нумератор строк к каждому текстовому полю с флагом «data-lines». Не знаю, как сильно это скажется на производительности и просто экспериментирую…

Также, теперь прокрутка мышью работает и над нумерацией, но имеются нюансы, так как пришлось само текстовое поле поставить поверх нумератора, сделав фон главного элемента прозрачным. Из-за чего вся страница позади стала фоном под текстом. А сам нумератор пришлось правымм бордюром растянуть так, чтобы он и стал фоном под текстом. В следствии чего имеются приличные глюки с просветами при ресайзинге мышью.

Да, эти два "товарища" помещены внутрь div вместе с header и footer.
Но, как Вы понимаете, это для визуального акцента на демострацию того, что я хочу. А в идеале должен быть собственно сам textarea без каких-либо сущностей, чтобы засорять вёрстку лишним матрёшиством (чем грешит абсолютное большинство сайтов).

P.S.: Есть ещё вариант с canvas-фоном…

Alikberov 12.09.2022 02:01

Обновлённый вариант (редуцирован до 10 тысяч символов)
 
Хотел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'ов для непосредственного управления "заголовком" и "статусом", а также цветом самого нумератора, чтобы продемонстрировать простоту доступа ко всем сопутствующим элементам через основной.

voraa 12.09.2022 09:14

Цитата:

Сообщение от Alikberov
В DOM столбик нумератора имеет флаг «disabled» и «fixed»,

Почему fixed, а не absolute?

Зачем при каждой обработке события заново создавать dummy? И каждый раз присваивать фиксированные значения (типа 'none', '0px') в style. Не эффективнее ли будет создать один dummy один раз, заполнить все фиксированные значения. А в обработчике только задавать значения зависящие от конкретного элемента textarea и присоединять это dummy в DOM, а потом удалять из DOM.

Alikberov 12.09.2022 14:31

Сравнение двух экземпляров одного текста
 
Цитата:

Сообщение от voraa (Сообщение 547924)
Почему fixed, а не absolute?

Кажeтся там успели побывать все значения. Что было последним, то и осталось (fixed короче absolute в рамках лимита размера поста на данном форуме в 10 тысяч символов), так как особой разницы не заметил.:no:
Цитата:

Сообщение от voraa (Сообщение 547924)
Зачем при каждой обработке события заново создавать dummy? И каждый раз присваивать фиксированные значения (типа 'none', '0px') в style.

В выходные попытался оптимизировать со статическим dummy - всё слетело!
Цитата:

Не эффективнее ли будет создать один dummy один раз, заполнить все фиксированные значения. А в обработчике только задавать значения зависящие от конкретного элемента textarea и присоединять это dummy в DOM, а потом удалять из DOM.
Всё слетело и потребовалось бы писать кругом
Цитата:

Сообщение от voraa (Сообщение 547924)
И каждый раз присваивать фиксированные значения (типа 'none', '0px') в style.

как Вы и предлагаете. А это - излишнее захламление кода в рамках поста на данном форуме.:D

То есть, я не подчеркнул главного: Здесь представлена усечённая форумная реализация.
Естественно, производительность страдает.
(В полной реализации контейнер div перетаскивается мышью, как полноценное окно, в отладчике ассемблера (тема) и жалкой сотней строчек листинга без переносов.)

Для оптимизации мало dummy сделать постоянным.
Необходимо создать отдельный массив оценки переносов строк, чтобы сравнивать два текста и перевычислять только изменённый фрагмент (не только по onkeypress и selectionStart/End, но и изменении value). Тогда при редактировании текста «Война и Мир» не должно быть зависаний на каждом новом символе…
И расчёт переносов нужно сделать не одним циклом, а генератором для работы в фоновом режиме.
Работы - много!:lol:

Здесь возник вопрос, на который поисковики не выдают готовых решений.
Как можно сравнить два текста (аналогично github) и локализовать именно область изменений?
(С такой задачей я ещё не сталкивался…)

P.S.: А что на счёт моих экспериментов с MutationObserver и Object.defineProperty для buddy, caption и status?
На сколько легально так делать в рамках совместимостей?:yes:

voraa 12.09.2022 18:46

Цитата:

Сообщение от Alikberov
И расчёт переносов нужно сделать не одним циклом, а генератором для работы в фоновом режиме.

Наверно worker имеется в виду.
Не думаю, что это что-нибудь даст. Там большая часть времени тратится на вставку одной строки в dummy, вычисление ее высоты и проч. Но worker не может работать с DOM.
Цитата:

Сообщение от Alikberov
Как можно сравнить два текста (аналогично github) и локализовать именно область изменений?

Ну для поиска первого несовпадения напрашивается что то вроде поиска методом деления пополам (берем половину первой и такое же кол-во символов второй строки, если не равны делим опять их на половины и т.д., если равны, значит идем к задним половинам)
Цитата:

Сообщение от Alikberov
А что на счёт моих экспериментов с MutationObserver и Object.defineProperty для buddy, caption и status?
На сколько легально так делать в рамках совместимостей?

Про MutationObserver я думаю не можно ли, а нужно ли.
А вот про модификацию существующих системных объектов, я - категорический противник. Если так делать во всех библиотеках, то потом не разобраться, что откуда в них взялось, и не будет ли коллизий сейчас или впоследствии.
Создавайте свои объекты.
Можно хоть рассмотреть возможность создания своего пользовательского элемента, наследующего от textarea.

Alikberov 12.09.2022 19:00

Цитата:

Сообщение от voraa (Сообщение 547929)
Наверно worker имеется в виду.

Нет.
Цитата:

Сообщение от voraa (Сообщение 547929)
Ну для поиска первого несовпадения напрашивается что то вроде поиска методом деления пополам (берем половину первой и такое же кол-во символов второй строки, если не равны делим опять их на половины и т.д., если равны, значит идем к задним половинам)

Значит, готового метода нет.:-?
Ясно: Снова костыльничать…
Цитата:

Сообщение от voraa (Сообщение 547929)
Про MutationObserver я думаю не можно ли, а нужно ли.
А вот про модификацию существующих системных объектов, я - категорический противник. Если так делать во всех библиотеках, то потом не разобраться, что откуда в них взялось, и не будет ли коллизий сейчас или впоследствии.

Буду учитывать…
Цитата:

Сообщение от voraa (Сообщение 547929)
Создавайте свои объекты.
Можно хоть рассмотреть возможность создания своего пользовательского элемента, наследующего от textarea.

Уже создавал, но всё сломалось с новыми ревизиями.
(К 25-летию Befunge использовал в своём скрипте свой элемент, который потом перестал работать во всех браузерах.)

voraa 12.09.2022 19:16

Цитата:

Сообщение от Alikberov
К 25-летию Befunge использовал

В РФ jsfiddle.net заблочен. ничего не могу посмотреть.
Но сейчас все API пользовательских элементов стабилизировались.

Цитата:

Сообщение от Alikberov
Наверно worker имеется в виду.
Нет.

А что имеется в виду под "генератором для работы в фоновом режиме."?

voraa 12.09.2022 20:12

Цитата:

Сообщение от Alikberov
Ну для поиска первого несовпадения напрашивается что то вроде поиска методом деления пополам (берем половину первой и такое же кол-во символов второй строки, если не равны делим опять их на половины и т.д., если равны, значит идем к задним половинам)

Хотя может быть простое линейное сравнение символов (с помощью charCodeAt()) будет эффективнее.

Alikberov 12.09.2022 21:01

Цитата:

Сообщение от voraa (Сообщение 547931)
В РФ jsfiddle.net заблочен.

Очeнь жаль…
Цитата:

Сообщение от voraa (Сообщение 547931)
ничего не могу посмотреть.

А там и нечего смотреть, так как ничего не работает из-за того, что я накастомничал свой элемент (как в Windows: Заголовок с тремя кнопками и меню под ним - классика!) в качестве эксперимента.
теме ссылка на gist не заблокирована? Там я простенько заголовок с меню сделал такой же - без кастома, можно посмотреть.)
Цитата:

Сообщение от voraa (Сообщение 547931)
Но сейчас все API пользовательских элементов стабилизировались.

Очень приятно это слышать.
(Хотя, может я ошибаюсь, но FF сейчас стоит в той же позиции, что и некогда IE: Некоторые уникальные вещи работают только в FF, хотя некоторые банальные вещи в FF так и не работают.)

Пытался пару лет назад снова кастомный элемент сделать. Типа, получилось, но настроения уже не было до такой степени мозгами скрипеть.
Цитата:

Сообщение от voraa (Сообщение 547931)
А что имеется в виду под "генератором для работы в фоновом режиме."?

Я говорил про функцию-генератор с циклом прочёсывания всего текста на признак переноса строк, чтобы buddy заполнялся на лету, не подвешивая всё кругом.
Или вообще, точно определять область видимости текста и прочёсывать только конкретный фрагмент, заполняя buddy по мере прокрутки текста.

Вариантов несколько, но у всех свои недостатки.

Сейчас нужно с dummy как-то разобраться, сделав его глобальным и статичным, добиться отсутствия глюков в переносах. А потом взяться за кастомный TextArea.

Но меня больше расстроило отсутствие метода сравнения строк, так как это тоже не самая последняя функция из востребованных.
Цитата:

Сообщение от voraa (Сообщение 547932)
Хотя может быть простое линейное сравнение символов (с помощью charCodeAt()) будет эффективнее.

Может массив хэш-сумм строк делать и потом их сравнивать на первом этапе?

Alikberov 13.09.2022 03:18

Набросок класса кастомного текстового поля
 
Цитата:

Сообщение от voraa (Сообщение 547929)
Создавайте свои объекты.
Можно хоть рассмотреть возможность создания своего пользовательского элемента, наследующего от textarea.

Xм, как-то так:
<script>
class FineTextArea extends HTMLTextAreaElement {
	constructor() {
		super();
		this.dummy = document.createElement("textarea");
		this.dummy.style.position = "absolute";
		this.dummy.style.top = "-400px";
		this.dummy.style.paddingTop = "0px";
		this.dummy.style.marginTop = "0px";
		this.dummy.style.borderTop = "none";
		this.dummy.style.borderTopWidth = "0px"
		this.dummy.style.paddingBottom = "0px";
		this.dummy.style.marginBottom = "0px";
		this.dummy.style.borderBottom = "none";
		this.dummy.style.borderBottomWidth = "0px";
		this.dummy.style.visibility = "hidden";
		this.dummy.style.overflowX = "hidden";
*!*
		this.dummy.style.overflowY = "scroll";	// Важно для FF
*/!*
		document.body.appendChild(this.dummy);
	}
	connectedCallback() {
		this.buddy = document.createElement("TextArea");
		this.buddy.className = "FineTextArea-Buddy";
		this.buddy.id = this.id + "_buddy";
		this.parentElement.insertBefore(this.buddy, this);
		this.addEventListener("mousemove", this.update.bind(this));
		this.addEventListener("mouseup", this.update.bind(this));
		this.addEventListener("scroll", this.update.bind(this));
		setTimeout(this.update.bind(this), 0);
	}
	set lines(value) {
		this.buddy.style.visibility = value == true ? "visible" : "hidden";
		this.style.paddingLeft = value == true ? this.buddy.offsetWidth + "px" : "0px";
		if(value)
			this.update();
	}
	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() {
		this.buddy.cols = this.value.split(/\r?\n/).length.toString().length;
		this.buddy.rows = this.rows;
		this.buddy.style.height = this.offsetHeight + "px";
		this.buddy.scrollTop = this.scrollTop;
		this.style.paddingLeft = this.buddy.offsetWidth + "px";
		this.dummy.rows = 1;
		this.dummy.cols = this.cols;
		this.dummy.style.width = this.offsetWidth + "px";
		this.dummy.style.paddingLeft = this.buddy.offsetWidth + "px";
		// Прочёсываем все строки
		var	lines = [];
		var	i = 1;
		console.time("Calculating line-breaks");
		var	fromTime = window.performance.now();
		for(var line of this.value.split(/\r?\n/)) {
			lines.push(i);
			this.dummy.value = line;
			// Не перенеслась ли строчка?
			var	separating = this.dummy.scrollHeight / this.dummy.offsetHeight;
			// Разбиваем нумерацию в этом месте
			while(-- separating > 0)
				lines.push("");
			i ++;
		}
		console.timeEnd("Calculating line-breaks");
		var lastTime = window.performance.now();
		// Обновляем всю колонку нумерации строк
		this.buddy.value = lines.join("\r\n");
		this.buddy.scrollTop = this.scrollTop;
	}
	static get observedAttributes() {
		return "lines value".split(/\s+/);
	}
	attributeChangedCallback(name, oldValue, newValue) {
		switch(name) {
		case "lines":
			//
			break;
		}
	}
}

customElements.define("fine-textarea", FineTextArea, {extends: "textarea"});
</script>

<style>
textarea[is='fine-textarea']
{
	position		:relative;
	background-color:transparent;
}
textarea.FineTextArea-Buddy
{
	position		:absolute;
	background-color:red;
	overflow		:hidden;
	text-align		:right;
	resize			:none;
}
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='this.style.backgroundColor = "red"; eval(`hTextArea.${this.value}`); this.style.backgroundColor = "yellow"'><br>
<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=15 cols=40 id=Main spellcheck=false title='Текстовое поле для редактирования' is=fine-textarea caption='Опциональный текст заголовка' status lines=true>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est,
omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
repellat.
</textarea>
<script>
var	hTextArea = document.querySelector("textarea#Main");
</script>
</body>
Здесь возникла ещё проблема в том, что невозможно отследить изменение value.:nono:
Или я не до конца разобрался во невсех тонкостях?:no:

Проблем больше, чем я предполагал.
Одна из очевидных: Сломался калькулятор переносов, так как при ресайзинге нумерация строк сбивается на последней строке, что говорит о необходимости более основательной зачистки атрибутов dummy, о чём выше речь и шла.

P.S.: А так, в целом, как видно, класс получился не таким уж большим - менее сотни строк. Что радует в определённой степени.:yes:

micscr 13.09.2022 07:28

Вложений: 1
В Firefox не совсем хорошо

Alikberov 13.09.2022 14:17

Вложений: 2
Цитата:

Сообщение от micscr (Сообщение 547938)
В Firefox не совсем хорошо

Упустил строку одну (исправил и пометил её в листинге).:thanks:

Заметил другую особенность:
FireFox отображает колонку нумерации как я и задумывал: Шириной достаточной, чтобы вместить номер строки самой последней строки.
Chrome добавляет какой-то отступ, подавить который я не могу: Весь перечень вычисленных свойств просмотрел - нету намёка. Подозреваю, что ширина бара вертикального скролла учитывается, хотя он и скрыт.
(Скриншоты прикрепил.)

Внезапно обнаружил, что встроенных функций вычисления хэш-сумм текста нет, что негативно скажется на производительности (привет, полифил-костыли!) в очередной раз…

Alikberov 13.09.2022 23:23

Супер быстрый и простой вариант
 
<html>
<head>
<title>Построчная нумерация</title>

<script>
class FineTextArea extends HTMLTextAreaElement {
	constructor() {
		super();
	}
	connectedCallback() {
		this._lines = 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("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));
		setTimeout(this.update.bind(this), 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(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(force) {
		this.buddy.scrollTop = this.scrollTop;
		var	rows = this.value.split(/\r?\n/);
		var	row = 1;
		var	len = rows.length.toString().length;
		var	spc = "string" == typeof this._lines && this._lines != "" ? this._lines.charAt(0) : "\xA0";
		// Здесь следуют некоторые манипуляции со стилями
		this.style.paddingLeft = `${len}em`;
		this.buddy.style.paddingRight = window.getComputedStyle(this, null).getPropertyValue("padding-left");
		this.buddy.style.height = window.getComputedStyle(this, null).getPropertyValue("height");
		this.buddy.style.width = window.getComputedStyle(this, null).getPropertyValue("width");
		// Далее следует довольно важная строчка, которая влияет на накапливание ошибок
		this.buddy.style.paddingLeft = window.getComputedStyle(this, null).getPropertyValue("padding-right");
		// Затем всё совсем просто
		if(force || this._value != this.value) {
			// Переносим весь текст в поле нумератора, заменяя всё (почти: дефисы - нельзя),
			// кроме Пробела, на символ неразрывного пробела,
			// попутно вставляя нумерацию
			this.buddy.value = this.value.replace(/^.*$/gm,
				function(str) {
					return Number(row ++).toString().padStart(len, spc) + str.substr(len).replace(/-/gm, " ").replace(/\S/gm, spc);
				}
			);
			this._value = this.value;
		}
		this.buddy.scrollTop = this.scrollTop;
	}
	static get observedAttributes() {
		return "lines".split(/\s+/);
	}
	attributeChangedCallback(name, oldValue, newValue) {
		switch(name) {
		case "lines":
			this._lines = newValue;
			break;
		}
	}
}

customElements.define("fine-textarea", FineTextArea, {extends: "textarea"});
</script>

<style>
textarea[is='fine-textarea']
{
	position		:relative;
	background-color:transparent;
}
textarea.FineTextArea-Buddy
{
	--border-right	:none;
	position		:absolute;
	--top:400px;
	background-color:yellow;
	--overflow		:hidden;
	--text-align		:right;
	--resize			:none;
}
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='this.style.backgroundColor = "red"; eval(`hTextArea.${this.value}`); this.style.backgroundColor = "yellow"'><br>
<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 caption='Опциональный текст заголовка' status lines=true>
Sed ut perspiciatis,
unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam eaque ipsa,
quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt,
explicabo.
Nemo enim ipsam voluptatem,
quia voluptas sit,
aspernatur aut odit aut fugit,
sed quia consequuntur magni dolores eos,
qui ratione voluptatem sequi nesciunt,
neque porro quisquam est,
qui dolorem ipsum,
quia dolor sit,
amet,
consectetur,
adipisci velit,
sed quia non numquam eius modi tempora incidunt,
ut labore et dolore magnam aliquam quaerat voluptatem.
Ut enim ad minima veniam,
quis nostrum exercitationem ullam corporis suscipit laboriosam,
nisi ut aliquid ex ea commodi consequatur?
Quis autem vel eum iure reprehenderit,
qui in ea voluptate velit esse,
quam nihil molestiae consequatur,
vel illum,
qui dolorem eum fugiat,
quo voluptas nulla pariatur?
At vero eos et accusamus et iusto odio dignissimos ducimus,
qui blanditiis praesentium voluptatum deleniti atque corrupti,
quos dolores et quas molestias excepturi sint,
obcaecati cupiditate non provident,
similique sunt in culpa, qui officia deserunt mollitia animi,
id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore,
cum soluta nobis est eligendi optio,
cumque nihil impedit,
quo minus id,
quod maxime placeat,
facere possimus,
omnis voluptas assumenda est,
omnis dolor repellendus.
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet,
ut et voluptates repudiandae sint et molestiae non recusandae.
Itaque earum rerum hic tenetur a sapiente delectus,
ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
repellat.
</textarea>
<script>
var	hTextArea = document.querySelector("textarea#Main");
</script>
</body>
Сутью метода является то, что два элемента TextArea одинаково себя ведут на одном и том же тексте, даже если во вспомогательном TextArea все зримые символы заменить на неразрывной пробел, чтобы всё стало прозрачным, кроме первых символов каждой строки, которые заменяем на порядковый номер строки.

P.S.: К сожалению, вариант иногда сбоит…

voraa 14.09.2022 08:12

Цитата:

Сообщение от Alikberov
Внезапно обнаружил, что встроенных функций вычисления хэш-сумм текста нет, что негативно скажется на производительности (привет, полифил-костыли!) в очередной раз…

Вы думаете, что считать хеш будет быстрее, чем тупо сравнивать строки?

Alikberov 14.09.2022 13:42

Цитата:

Сообщение от voraa (Сообщение 547963)
Вы думаете, что считать хеш будет быстрее, чем тупо сравнивать строки?

Алгоритмы сравнения строк тоже разные бывают (Бойера-Мура).
Ну, смoтря по какому алгоритму вычислять так называемую условную хэш-сумму (если не для банка, можно учитывать только коды первого и последнего символов, а также длину самой строки и количество пробелов в ней - четыре параметра), как подсчитывали чек-суммы ещё на ЭВМ типа ДВК/РК.:D

А так - вариант #38 мне больше нравится своей простотой и производительностью. Нужно лишь со стабильностью разобраться…

Вариант №38 - лучшее компромиссное решение, так как в поле нумератора строк копируется исходный текст, что гарантирует эквивалентый разрыв строк. Остаётся лишь начало каждой строки промаркировать индексом и замаскировать остальные символы, так как графически текст смещён и нельзя допустить визуального смешивания контентов.
И вот тут нас ждёт подвох, так как не все символы можно равноценно подменить под неразрывной пробел - пиксельная длина строки местами отличается, что ломает всё!

Но именно в этом направлении следует исследовать вопрос, так как код варианта №38 самый простой, компактный (практически 10 строк цикла) и не требует всяких скрытых dummy-элементов.


Часовой пояс GMT +3, время: 15:13.