Отображение строк у 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: |
То, что не нумеруется первая строка - это бага или фича?
По любому строка 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'; (а нужна ли она вообще?) |
По поводу стр 129.
Там не только в отступе (marginTop) borderTopWidth надо учитывать, но и paddingTop. как то так должно быть const {borderTopWidth, paddingTop} = getComputedStyle(this, null) buddy.style.marginTop = (parseFloat(borderTopWidth) + parseFloat(paddingTop)) + 'px' |
Цитата:
Цитата:
На самом деле на Си в WinAPI я тоже как-то неделю боролся с подобной задачей (кажется, решил). А в HTML/CSS никак не получается. Вот тут в редакторе я просто (временно) пристыковал два TextArea к друг друг - выглядит вполне нормально и сколлер нормально отрабатывается (хотя и с задержкой). И вот решил это «временное» переписать нормально… Но, никак не получается! Всё должно выглядить как в стандартном редакторе. Без фич и исключений. А то у меня сейчас и последняя строка текста слева индексируется как #49, хотя внизу же указывается «Row:46»: На три единицы ошибка…:-? |
<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> Вроде так лучше. Что не так? |
Идеально должно быть так:
<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> |
У вас всё дрожит дёргается и скачет. Возьми любой готовый редактор на textarea и посмотри как там.
|
Цитата:
Adding Line Numbers To HTML Textarea Почему вообще за четверть века не ввели в спецификацию стиля с флагом отображения этих номеров? Всюду нужно подключать сторонние библиотеки с кучей кода… P.S.: Несчастный Math.sign появился совсем недавно. И что мешало его давно ввести? |
Цитата:
А может он просто не нужен особо был? Вообще понятие строки довольно размытое. Что является строкой - то, что выглядит как строка, или это текст от \n до \n? Во всех редакторах, которые показывают номера строк - это второе. У вас - первое. Если очень длинная строка занимает в визуальном узком окне несколько строк, то номер имеет только первая часть строки, а те, что перенесены идут без своих номеров, т.к это часть той же строки. А следующий номер имеет следующая строка, которая идет после \n. А какой смысл в ваших номерах, если эти номера зависят не от реального количества строк, а от ширины окна? На такой номер нельзя сослаться. |
Цитата:
Цитата:
Цитата:
Цитата:
|
Ну это немного разные понятия - функция в языке, и какой то новый стиль в элементе HTML.
Js к стилям и html имеет очень маленькое отношение (точнее никакого). Js он и в браузере и в Node один и тот же. А API разные. |
Цитата:
Просто, хотел обратить внимание на масштаб изменений. Описание функции Math.sign - не более сотни байтов определённого машинного кода. Описание тегов canvas/audio/video - больше сотни различных библиотек. Вопрос лишь в формальностях, что развивать…:yes: В том же jsfiddle код подсветки синтаксиса и строк - значимый процент на фоне общего кода. И уважаемый себя ресурс с интерактивной демонстрацией примеров кода всегда предоставляет нумерацию к строкам листинга. P.S.: Так или иначе, рано или поздно, всё равно поддержку введут на базе известной библиотеки. Вопрос вот только когда и на какой библиотеке?:) |
Цитата:
Можно поинтересоваться, в каком браузере это наблюдается и как можно побороть?:thanks: |
Alikberov, левый нижний угол:
![]() |
Вложений: 1
Цитата:
А можно ли как-то побороть? Ну, preventDefault поставить, например, со своим кодом скроллинга (что усложняет задачу). P.S.: Спасибо за анимацию!:thanks: |
Alikberov,
Попробуйте вставлять длинные строки (длиннее ширины области), что бы они переносились на 2-3 строки. Там все еще хуже будет. |
Цитата:
Здесь нужно подключать готовую библиотеку или писать своё. Думал, что как-то можно реализовать такую простую элементарную функцию одним блоком кода. |
Цитата:
|
Цитата:
|
Цитата:
Вроде бы работает, но крайне медленный. <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: (Если не менять размера поля, то скроллинг до последней строки работает почти нормально, так как невыровненный размер по вертикали сильно влияет на расчёты.) |
Имеются ли жалобы (исключая производительность)
В общем, пришлось повозиться со стилями и устранить ненужные помехи в расчётах высоты текста.
Судя по значению «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: |
Alikberov,
скролл над нумерацией отсутствует, это так и задумано? |
Предыдущий вариант не дружит с 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> Цитата:
Нумерация строк не прокручивается вместе с текстом? |
Цитата:
|
Цитата:
Ведь основные усилия я прикладывал к известной задаче (адекватное отображение нумерации без подключения сторонних тяжёлых библиотек). В DOM столбик нумератора имеет флаг «disabled» и «fixed», а попытки сейчас поправить положение вещей чуть не поломало всё…:agree: Сейчас скрипт функционирует на принципе «работает - не трожь!» и я уже опасаюсь что-то менять… Нужно потом в копии скрипта опыты проводить, если потребуется… А так, в контексте вдруг предложенной задачи #16 вопрос исчерпан. Нужно только какую-то обвёртку написать, чтобы не вызывать функцию явно, а лишь добавлять к любому TextArea атрибут, например, "numbering". P.S.: Зашёл на страницу Война и Мир и вставил сюда всю страницу целиком…
|
Ид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-фоном… |
Обновлённый вариант (редуцирован до 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'ов для непосредственного управления "заголовком" и "статусом", а также цветом самого нумератора, чтобы продемонстрировать простоту доступа ко всем сопутствующим элементам через основной. |
Цитата:
Зачем при каждой обработке события заново создавать dummy? И каждый раз присваивать фиксированные значения (типа 'none', '0px') в style. Не эффективнее ли будет создать один dummy один раз, заполнить все фиксированные значения. А в обработчике только задавать значения зависящие от конкретного элемента textarea и присоединять это dummy в DOM, а потом удалять из DOM. |
Сравнение двух экземпляров одного текста
Цитата:
Цитата:
Цитата:
Цитата:
То есть, я не подчеркнул главного: Здесь представлена усечённая форумная реализация. Естественно, производительность страдает. (В полной реализации контейнер div перетаскивается мышью, как полноценное окно, в отладчике ассемблера (тема) и жалкой сотней строчек листинга без переносов.) Для оптимизации мало dummy сделать постоянным. Необходимо создать отдельный массив оценки переносов строк, чтобы сравнивать два текста и перевычислять только изменённый фрагмент (не только по onkeypress и selectionStart/End, но и изменении value). Тогда при редактировании текста «Война и Мир» не должно быть зависаний на каждом новом символе… И расчёт переносов нужно сделать не одним циклом, а генератором для работы в фоновом режиме. Работы - много!:lol: Здесь возник вопрос, на который поисковики не выдают готовых решений. Как можно сравнить два текста (аналогично github) и локализовать именно область изменений? (С такой задачей я ещё не сталкивался…) P.S.: А что на счёт моих экспериментов с MutationObserver и Object.defineProperty для buddy, caption и status? На сколько легально так делать в рамках совместимостей?:yes: |
Цитата:
Не думаю, что это что-нибудь даст. Там большая часть времени тратится на вставку одной строки в dummy, вычисление ее высоты и проч. Но worker не может работать с DOM. Цитата:
Цитата:
А вот про модификацию существующих системных объектов, я - категорический противник. Если так делать во всех библиотеках, то потом не разобраться, что откуда в них взялось, и не будет ли коллизий сейчас или впоследствии. Создавайте свои объекты. Можно хоть рассмотреть возможность создания своего пользовательского элемента, наследующего от textarea. |
Цитата:
Цитата:
Ясно: Снова костыльничать… Цитата:
Цитата:
(К 25-летию Befunge использовал в своём скрипте свой элемент, который потом перестал работать во всех браузерах.) |
Цитата:
Но сейчас все API пользовательских элементов стабилизировались. Цитата:
|
Цитата:
|
Цитата:
Цитата:
(В теме ссылка на gist не заблокирована? Там я простенько заголовок с меню сделал такой же - без кастома, можно посмотреть.) Цитата:
(Хотя, может я ошибаюсь, но FF сейчас стоит в той же позиции, что и некогда IE: Некоторые уникальные вещи работают только в FF, хотя некоторые банальные вещи в FF так и не работают.) Пытался пару лет назад снова кастомный элемент сделать. Типа, получилось, но настроения уже не было до такой степени мозгами скрипеть. Цитата:
Или вообще, точно определять область видимости текста и прочёсывать только конкретный фрагмент, заполняя buddy по мере прокрутки текста. Вариантов несколько, но у всех свои недостатки. Сейчас нужно с dummy как-то разобраться, сделав его глобальным и статичным, добиться отсутствия глюков в переносах. А потом взяться за кастомный TextArea. Но меня больше расстроило отсутствие метода сравнения строк, так как это тоже не самая последняя функция из востребованных. Цитата:
|
Набросок класса кастомного текстового поля
Цитата:
<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: Или я не до конца разобрался во Проблем больше, чем я предполагал. Одна из очевидных: Сломался калькулятор переносов, так как при ресайзинге нумерация строк сбивается на последней строке, что говорит о необходимости более основательной зачистки атрибутов dummy, о чём выше речь и шла. P.S.: А так, в целом, как видно, класс получился не таким уж большим - менее сотни строк. Что радует в определённой степени.:yes: |
Вложений: 1
В Firefox не совсем хорошо
|
Вложений: 2
Цитата:
Заметил другую особенность: FireFox отображает колонку нумерации как я и задумывал: Шириной достаточной, чтобы вместить номер строки самой последней строки. Chrome добавляет какой-то отступ, подавить который я не могу: Весь перечень вычисленных свойств просмотрел - нету намёка. Подозреваю, что ширина бара вертикального скролла учитывается, хотя он и скрыт. (Скриншоты прикрепил.) Внезапно обнаружил, что встроенных функций вычисления хэш-сумм текста нет, что негативно скажется на производительности (привет, полифил-костыли!) в очередной раз… |
Супер быстрый и простой вариант
<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.: К сожалению, вариант иногда сбоит… |
Цитата:
|
Цитата:
Ну, смoтря по какому алгоритму вычислять так называемую условную хэш-сумму (если не для банка, можно учитывать только коды первого и последнего символов, а также длину самой строки и количество пробелов в ней - четыре параметра), как подсчитывали чек-суммы ещё на ЭВМ типа ДВК/РК.:D А так - вариант #38 мне больше нравится своей простотой и производительностью. Нужно лишь со стабильностью разобраться… Вариант №38 - лучшее компромиссное решение, так как в поле нумератора строк копируется исходный текст, что гарантирует эквивалентый разрыв строк. Остаётся лишь начало каждой строки промаркировать индексом и замаскировать остальные символы, так как графически текст смещён и нельзя допустить визуального смешивания контентов. И вот тут нас ждёт подвох, так как не все символы можно равноценно подменить под неразрывной пробел - пиксельная длина строки местами отличается, что ломает всё! Но именно в этом направлении следует исследовать вопрос, так как код варианта №38 самый простой, компактный (практически 10 строк цикла) и не требует всяких скрытых dummy-элементов. |
Часовой пояс GMT +3, время: 15:13. |