Javascript.RU

Создать новую тему Ответ
 
Опции темы Искать в теме
  #21 (permalink)  
Старый 09.09.2022, 19:00
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 109

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

Судя по значению «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: Буду рад выслушать замечания о выявленных сбоях в этом скрипте класса «дёшего и сердито»…
Ответить с цитированием
  #22 (permalink)  
Старый 09.09.2022, 21:05
Аватар для рони
Профессор
Отправить личное сообщение для рони Посмотреть профиль Найти все сообщения от рони
 
Регистрация: 27.05.2010
Сообщений: 33,072

Alikberov,
скролл над нумерацией отсутствует, это так и задумано?
Ответить с цитированием
  #23 (permalink)  
Старый 09.09.2022, 22:22
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 109

Предыдущий вариант не дружит с 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>
Сообщение от рони Посмотреть сообщение
Alikberov,
скролл над нумерацией отсутствует, это так и задумано?
Что Вы имеете в виду?
Нумерация строк не прокручивается вместе с текстом?
Ответить с цитированием
  #24 (permalink)  
Старый 09.09.2022, 22:27
Аватар для рони
Профессор
Отправить личное сообщение для рони Посмотреть профиль Найти все сообщения от рони
 
Регистрация: 27.05.2010
Сообщений: 33,072

Сообщение от Alikberov
Что Вы имеете в виду?
курсор на цифры, колесо мышки круть, нефига не круть)))
Ответить с цитированием
  #25 (permalink)  
Старый 09.09.2022, 22:54
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 109

Сообщение от рони Посмотреть сообщение
курсор на цифры, колесо мышки круть, нефига не круть)))
A на сколько важна такая функция?

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

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

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

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

Последний раз редактировалось Alikberov, 10.09.2022 в 00:22. Причина: Проверял Толстым! :-)
Ответить с цитированием
  #26 (permalink)  
Старый 11.09.2022, 22:00
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 109

Ид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-фоном…
Ответить с цитированием
  #27 (permalink)  
Старый 12.09.2022, 02:01
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 109

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

Последний раз редактировалось Alikberov, 12.09.2022 в 02:11. Причина: Уточнил высоту фрейма просмотра кода
Ответить с цитированием
  #28 (permalink)  
Старый 12.09.2022, 09:14
Аватар для voraa
Профессор
Отправить личное сообщение для voraa Посмотреть профиль Найти все сообщения от voraa
 
Регистрация: 03.02.2020
Сообщений: 2,707

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

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

Последний раз редактировалось voraa, 12.09.2022 в 10:05.
Ответить с цитированием
  #29 (permalink)  
Старый 12.09.2022, 14:31
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 109

Сравнение двух экземпляров одного текста
Сообщение от voraa Посмотреть сообщение
Почему fixed, а не absolute?
Кажeтся там успели побывать все значения. Что было последним, то и осталось (fixed короче absolute в рамках лимита размера поста на данном форуме в 10 тысяч символов), так как особой разницы не заметил.
Сообщение от voraa Посмотреть сообщение
Зачем при каждой обработке события заново создавать dummy? И каждый раз присваивать фиксированные значения (типа 'none', '0px') в style.
В выходные попытался оптимизировать со статическим dummy - всё слетело!
Цитата:
Не эффективнее ли будет создать один dummy один раз, заполнить все фиксированные значения. А в обработчике только задавать значения зависящие от конкретного элемента textarea и присоединять это dummy в DOM, а потом удалять из DOM.
Всё слетело и потребовалось бы писать кругом
Сообщение от voraa Посмотреть сообщение
И каждый раз присваивать фиксированные значения (типа 'none', '0px') в style.
как Вы и предлагаете. А это - излишнее захламление кода в рамках поста на данном форуме.

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

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

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

P.S.: А что на счёт моих экспериментов с MutationObserver и Object.defineProperty для buddy, caption и status?
На сколько легально так делать в рамках совместимостей?
Ответить с цитированием
  #30 (permalink)  
Старый 12.09.2022, 18:46
Аватар для voraa
Профессор
Отправить личное сообщение для voraa Посмотреть профиль Найти все сообщения от voraa
 
Регистрация: 03.02.2020
Сообщений: 2,707

Сообщение от Alikberov
И расчёт переносов нужно сделать не одним циклом, а генератором для работы в фоновом режиме.
Наверно worker имеется в виду.
Не думаю, что это что-нибудь даст. Там большая часть времени тратится на вставку одной строки в dummy, вычисление ее высоты и проч. Но worker не может работать с DOM.
Сообщение от Alikberov
Как можно сравнить два текста (аналогично github) и локализовать именно область изменений?
Ну для поиска первого несовпадения напрашивается что то вроде поиска методом деления пополам (берем половину первой и такое же кол-во символов второй строки, если не равны делим опять их на половины и т.д., если равны, значит идем к задним половинам)
Сообщение от Alikberov
А что на счёт моих экспериментов с MutationObserver и Object.defineProperty для buddy, caption и status?
На сколько легально так делать в рамках совместимостей?
Про MutationObserver я думаю не можно ли, а нужно ли.
А вот про модификацию существующих системных объектов, я - категорический противник. Если так делать во всех библиотеках, то потом не разобраться, что откуда в них взялось, и не будет ли коллизий сейчас или впоследствии.
Создавайте свои объекты.
Можно хоть рассмотреть возможность создания своего пользовательского элемента, наследующего от textarea.
Ответить с цитированием
Ответ



Опции темы Искать в теме
Искать в теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Вопрос по textarea stivins Элементы интерфейса 3 05.11.2015 16:59
Перевод строк в textarea optron Общие вопросы Javascript 6 16.12.2013 11:57
Переводы строк в textarea на разных платформах GRIG Элементы интерфейса 2 06.10.2011 12:41
Отображение форматирования текста в textarea vahrusha Элементы интерфейса 2 18.09.2010 20:06
Динамическое отображение строк таблицы JukiPuki Общие вопросы Javascript 2 18.06.2010 12:55