4 июля 2013 в 14:47

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

Добрый день.

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


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


Так как не существует способа добавить поддержку тегов в textarea, то следующим вариантом является использование «многослойности».
С помощью z-index и абсолютного позиционирования поместим блок pre за необходимым нам textarea.
В элементе pre настроим шрифт аналогичный textarea и также зададим css свойства, которые нам позволят зеркально повторять текст блока 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 строку не растягивают элемент, а переносятся на другую строку.

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


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

Расширения



В данном примере представлен способ работы для встроенной функции.
В тех или иных ситуациях возможны случаи, когда результат (выделять\не выделять) необходимо получить от сервера. В таком случае возможно воспользоваться паттерном команда, чтобы отправлять серверу только список изменений. А, непосредственно, выделение организовать в отдельной callback функции.
Никита Мостовой @xnim
карма
27,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (14)

  • +1
    > Для браузеров, не поддерживающих функцию indexOf, определим метод прямого поиска — findText (в нем реализуем прямой проход по массивам)

    Таких давно нет. Есть которые неподдерживают indexOf для массивов, но это другое.
    • 0
      Ещё добавлю, что метод «substr» устарел и в спецификации помечен как deprecated — es5.github.io/#B.2
      Так, что лучше всё таки slice.
      • 0
        А где он там помечен, как deprecated?
        • 0
          > Some implementations of ECMAScript have included additional properties for some of the standard native objects. This non-normative annex suggests uniform semantics for such properties without making the properties or their semantics part of this standard.

          Что то типо пересказа: В некоторых движках по тем или иным причинам присутствуют устаревшии свойства, которые не входят в специкацию. Если разработчики современных реализаций хотят, они могут реализовать эти свойства/методы для обратной совместимости. Для это общие алгоритмы реализации.

          В общем это однозначно можно читать как deprecated. Если хотите, можно назвать эти свойства — нестандартные, что будет более правильно. Однако, по моему личному мнение, deprecated сюда подходит гораздо больше.
          • 0
            Если что-то поддерживается всеми распространёнными реализациями, то это стандарт де-факто, который с большой вероятностью будет поддерживаться всегда.

            Что касается метода substr(), то он может быть удобен, когда известна не позиция окончания диапазона, а его длина).
            • +1
              substr поддерживается только по тому, что он поддерживался раньше, так делаются многие вещи в вебе, совместимость очень важна. В живом HTML стандарте даже сказано, что в некоторых случаях не реализации подстраиваются под спецификацию, а на оборот. Однако, если следить за тем, что происходит со стандартом, можно заметить, что устаревшие вещи выкидываются и перестают быть частью спецификации и со временем, удаляются из реализаций браузеров.

              Стандарты де-факто это круто конечно, всякие __proto__, которые вроде как и здорово иметь, но опять же, они бажат и ведут себя в каждом браузере по разному, ибо нет стандарта, каждый делает как хочет/как знает.

              Ваш последний аргумент решается так: str.slice(start, start + count), не так уж и сложно. Чем меньше людей пишут с устаревшими методами, тем быстрее эти методы испаряться.
              • 0
                Испарятся :( Не успел…
  • 0
    Событие «input» в нормальных браузерах появилось ой как давно. Попробуйте мышкой вставить текст и посмотрите результат. А сие событие как раз решило бы эту проблему.
    • 0
      И «propertychange» для IE, который либо не понимает либо хреново понимает «input».
  • +3
    Всё бы хорошо, но
    элемент textarea не поддерживает html\bb теги
    причем тут bb-тэги? они же вроде браузерами ни в каком виде никогда нигде не поддерживались. Или я чего-то не знаю?
  • 0
    Да, наверное, я поторопился так писать. И тут необходимо было бы описать, что работу c bb-тегами поддерживается многими «текстовыми» редакторами использующими фреймы\div.

    Также, я думаю, все знают, что, сделав тот же contenteditable, мы получаем возможность использовать хоткеи -> следствие использование тегов b, i…

    Я допустил ошибку, описав это как bb. Вы правы. Было бы верно описать, что тот же textarea не поддерживает те же хоткеи (такие как ctrl+b)
  • 0
    > И тут необходимо было бы описать, что работу c bb-тегами поддерживается многими «текстовыми» редакторами использующими фреймы\div

    По моему опять торопитесь. BB-тэги можно напрямую организовать в textarea, c горячими клавишами и без. Всё в действии можно посмотреть, например, в форумном движке — phpBB.
    • 0
      Я ищу поиск отображения «не отходя от кассы». Так то вставить HTML-теги в textarea нам тоже никто не мешает. Но они останутся текстом.
      • 0
        Да, здесь спорить не буду.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.