Показать сообщение отдельно
  #27 (permalink)  
Старый 12.09.2022, 02:01
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 112

Обновлённый вариант (редуцирован до 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. Причина: Уточнил высоту фрейма просмотра кода
Ответить с цитированием