Организуем выделение текста в textarea

в 10:47, , рубрики: css, javascript, метки:

Добрый день.

В разработке интерфейсов иногда можно встретиться с задачей выделения вводимого пользователя текста в зависимости от определенных условий. (Например, была реализована серверная проверка грамматики, либо необходимо выделять определенным цветом те или иные словаучастки и т.д.)
Однако, элемент textarea не поддерживает htmlbb теги. Как один из способов решения — использование contenteditable в элементах div.
В данной небольшой статье я предлагаю более-менее подробно посмотреть способ выделения текста, используя textarea.

Общая идея решения

Так как не существует способа добавить поддержку тегов в textarea, то следующим вариантом является использование «многослойности».
С помощью z-index и абсолютного позиционирования поместим блок pre за необходимым нам textarea.
В элементе pre настроим шрифт аналогичный textarea и также зададим css свойства, которые нам позволяет зеркально повторять текст блока textarea (о них чуть ниже)
Также создадим класс, который будет после каждого изменения содержимого textarea синхронизировать данные, осуществлять поиск данных, необходимых для выделения.
Иллюстрация данного решения:
Организуем выделение текста в textarea

Разбираем решение

За основную задачу возьмем — создание класса, который на вход получает значения целевой ноды (textarea), функции — проверки на выделение и значение шрифта (css свойства font)
Конструктор класса должен добавить в DOM-модель документа необходимый элемент pre, позиционировать его и повесить события. Сюда же можно добавить установку css свойств.

/**
  * Создает экземпляр "интерактивного" textarea
  * @name TextareaExtension
  * @param target - целевой нода textarea
  * @param processor - функция для проверки слова на выделение
  * @param font - шрифт 
  */
function TextareaExtension(target , processor, font)
{
	var setStyleOptions = function()
	{
		//Добавляем класс (чтобы не прописывать все свойства), добавляем в DOM, устанавливаем font
		preItem.className = "text-area-selection";
		target.parentNode.appendChild(preItem);
		target.style.font = preItem.style.font = font || "14px Ariel";
		
		//Определяем позиционирование, прозрачность, сразу же устанавливаем скроллы
		target.style.width = preItem.style.width = target.offsetWidth + "px";
		target.style.height = preItem.style.height = target.offsetHeight + "px";
		preItem.style.top = target.offsetTop + "px";
		preItem.style.left = target.offsetLeft + "px";
		target.style.background = "transparent";
		target.style.overflow = "scroll";
		
		//Для тега pre свойство margin по умолчанию = 1em 0px. Поставим нулевые значения. 
		//(при использовании, например span вместо pre такая проблема отпадает)
		preItem.style.margin = "0px 0px";
	}
	
	setStyleOptions();
	
	//Добавляем события
    if (target.addEventListener) {
		//При изменении анализируем новое состояние textarea
        target.addEventListener("change", this.analyse, false);
        target.addEventListener("keyup", this.analyse, false);
		//Если текста было введено много - необходимо синхронизировать скролы textarea и pre
        target.addEventListener("scroll", this.scrollSync, false);
		//Также ставим обработчик на resize
        target.addEventListener("mousemove", this.resize, false);
    }
    else
        if (target.attachEvent) {
            target.attachEvent("onchange", this.analyse);
            target.attachEvent("onkeyup", this.analyse);
            target.attachEvent("onscroll", this.scrollSync);
            target.attachEvent("mousemove", this.resize);
        }
}

Итак, каркас класса создан. Определим оставшиеся методы класса:

    this.scrollSync = function () {
        preItem.scrollTop = target.scrollTop;
    };

    this.resize = function () {
        preItem.style.width = target.style.width;
        preItem.style.height = target.style.height;
        preItem.style.top = target.offsetTop  + "px";
        preItem.style.left = target.offsetLeft + "px";
    };
	
	 this.analyse = function (){

        var text = target.value;
        var words = text.split(/[s]/);
        var textPosition = 0;
        var result = "";
       
	   for (var i in words) {
            if (processor(words[i])) {
                var textIndex;
                if (text.indexOf) {
                    textIndex = text.indexOf(words[i]);
                }
                else textIndex = findText(text, words[i]);
				
				
                result += text.substr(0, textIndex) + "<span class='text-color-bordered text-checker'>" + words[i] + "</span>";
				
                text = text.substr(textIndex + words[i].length, text.length);
            }
        }
        result += text;
      
        
        preItem.innerHTML = result;
    };

Метод analyse перебирает каждое слово, отправляя его в определенную заранее функцию. Если слово должно выделяться — метод копирует предыдущее содержимое в pre и «оборачивает» необходимое слово в span с классом, определяющий способ выделения (в данном примере — нижнее точечное подчеркивание)
Для браузеров, не поддерживающих функцию indexOf, определим метод прямого поиска — findText (в нем реализуем прямой проход по массивам)

CSS-свойства

Приведем список определенных свойств, а затем разберем их:

.text-area-selection {
    position:absolute;  
    padding:2px; 
    z-index:-1;
    display:block;
    word-wrap:break-word;  
    white-space:pre-wrap;
    color:white;
    overflow:scroll;
}

.text-color-bordered {
    border-bottom:1px dotted red;
}

Как уже было сказано, элемент pre должен позиционироваться под textarea, поэтому позиционируем его абсолютно и устанавливаем z-index в -1. Добавляем отступы, скроллы.
Теперь перейдем к определениям word-wrap и white-space. В данной задаче эти свойства играют очень важную роль.
white-space: pre-wrap позволяет учитывать все пробелы в строках и в то же время он не позволяет продолжать текст горизонтально (переносит его), если он не помещается в 1 строку
word-wrap:break-word — определяет поведение текста, при котором слова, не помещающие на 1 страницу не растягивают элемент, а переносятся на другую страницу.

В результате мы получаем выделение текста по результату работы нашей функции:
Организуем выделение текста в textarea

Исходники:
Ссылка на GitHub
CodePen

Расширения

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

Автор: xnim

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js