Мне всегда нравились очень простые, но достаточно умненькие редакторы. Именно поэтому я отдаю предпочтение не тяжеловесным IDE, а легковесным мини-IDE (или вовсе обычным редакторам). Особенно меня привлекает в них одна небольшая особенность: в них много совершенно разных на вкус и цвет особенностей. Про одну из них мы сегодня и поговорим.
Очень часто мне приходится иметь дело с набором текстов (даже записей в данный блог), которые включают в себя различную разметку: элементы (X)HTML, их атрибуты и тому подобное. Иногда довольно быстро устают пальцы, набирая очередную порцию сортированных списков, содержащих около 10, 15 записей. В некоторых из вышеупомянутых редакторов существует возможность автодополнения закрывающего тега. Казалось бы, такая маленькая мелочь, а «на ладошке не умещается». Посмотрим, как это можно применить на практике.
Как это?
Начнём с краткого самоконтроля: нам необходимо такое текстовое поле, которое распознавало бы XML-элементы, а именно их закрывающие и открывающие теги. При изменении содержимого текстового контейнера, просчитываются имеющиеся в нём теги. Когда логика определяет, что пользователь решил всё-таки закрыть тег, она сама за него подставляет его имя, освобождая разработчика от ненужной беготни уставшими глазками вверх по тексту. Берегите зрение, товарищи разработчики!
Ещё одно маленькое пожелание (требование — примечание авт.) — поддержка вложенности тегов и вставки последних в любое место текстового поля.
Где применять?
Я уже использую при написании данной записи эту разработку. Для меня это оказалось очень удобной возможностью. Так сказать, «удобное удобство». Я планирую оптимизировать данный прототип для последующей ассоциации с формой отправки комментариев в нашем с вами блоге.
Помимо всего прочего, такой инструмент можно удобно расширить, добавив к нему автодополнение (всплывающие листы-подсказки), как это реализовано в большинстве популярных (и не очень) средах разработки. Окромя этого, можно реализовать любые другие полезные улучшения, однако я лишь хочу показать базис.
Ну что? Приступим?
Структура
Структуры у нас не так уж и много: текстовое поле типа textarea, одна шт.
<textarea id="autotags"> < /textarea>
Репрезентация
Дело вкуса, но я, всё-таки, задам представление нашего текстового поля:
#autotags
{
margin: 1em;
padding: 0.3em;
width: 500px;
height: 300px;
border: dotted 1px #777;
}
Реализация
Итак, начнём колоть ядрышко. Для начала нам понадобятся наши замечательные функции-помошницы:
/* Быстрая работа с DOM-элементами */
function $(elid)
{
return document.getElementById(elid);
}
/* Связывание событий и класс-метод-обработчиков */
function bind(toObject, methodName)
{
return function(e){toObject[methodName](e)}
}
function listen(object, hevent, hfunc)
{
if (object.addEventListener)
object.addEventListener(hevent,hfunc,false);
else if (object.attachEvent)
object.attachEvent('on'+hevent,hfunc);
}
//Связка для двух вышеописанных функций
function listenex(object, hevent, lobject, lfunc)
{
listen(object, hevent, bind(lobject, lfunc));
}
Мы должны обеспечить возможность создания нескольких подобных полей в документе. Сделаем это путём внедрения антисинглтона:
//Антисинглтон
//Принимает единственный аргумент: id текстовой области
function AutoTagsField(fieldid)
{
//Получаем текущее поле (внимание, отсутствует проверка на существование поля)
this.field = $(fieldid);
//Временное хранилище текстовых данных
this.buffer = "";
//Обработчик нажатия клавиши
listenex(this.field, "keyup", this, "key_up_handler");
//Наш обработчик собственной персоной
this.key_up_handler = function()
{
//Необходима реализация! План горит!
}
//Метод для фильтрации тегов
this.filter_tags = function(tagarray)
{
//Кто последний — тот редиска
}
}
Теперь реализуем метод-обработчик нажатия клавиши. Комментарии излишни, код ими обильно насыщен:
this.key_up_handler = function()
{
//Получаем параметры выделения для обычных браузеров
var selectionStart = this.field.selectionStart;
var selectionEnd = this.field.selectionEnd;
//Они же, но для необычных браузеров
if (document.all)
{
//Создаём выделение
var range = document.selection.createRange();
//Дубль
var crange = range.duplicate();
//Переносим туда наше текстовое поле
crange.moveToElementText(this.field);
crange.setEndPoint("EndToEnd", range);
//Переписываем значения
selectionStart = crange.text.length - range.text.length;
selectionEnd = crange.text.length - selectionStart;
}
//Добавляем текущий символ в буффер
this.buffer += this.field.value.substring(selectionStart - 1, selectionStart);
//Обрабатываем теги
var matches = this.filter_tags(this.field.value.match(/<.*?[^\/]>/gm));
//Окончание тега? Да? И существуют незакрытые теги?
if (this.buffer.match(/<\//g) && matches.length != 0)
{
//Создаём имя тега
var tagname = "/" + matches.pop().match(/<(.*)>/)[1] + ">";
//Сохраняем положение курсора текстовой области
var safe_position = selectionStart + tagname.length -1;
//В нужном месте и в нужное время
this.field.value = this.field.value.substring(0, selectionStart-1) + tagname +
this.field.value.substring(selectionStart);
//Устанавливаем нормальную позицию курсора
this.field.selectionStart = safe_position;
this.field.selectionEnd = safe_position;
//Для необычных браузеров
if (document.all)
{
//Схлапываем (скукоживаем)
crange.collapse(true);
//Переносим на наше любимое место
crange.moveStart("character", safe_position);
//И конец выделения туда же
crange.moveEnd("character", selectionEnd);
//Вперёд!
crange.select();
}
//Буффер должен быть пуст! Смерть неверным!
this.buffer = " ";
//Для наших друзей, необычных браузеров
if (window.event)
window.event.returnValue = false;
//Ой? Что такое? Вы не получали посылку?
return false;
}
}
И реализуем обработку тегов:
//Фильтруй теги!
this.filter_tags = function(tagarray)
{
//А есть ли они вообще, или нас надули?!
if (tagarray == null)
return;
//Создаём дополнительные массивы
var clean = new Array(), result = new Array();
//Путешествуем по нашему массиву
for (i = 0, ilength = tagarray.length; i < ilength; i++)
{
//Ох-ох, сверка
var name = tagarray[i].match(/<(?:\/|)(.*?)(?:\s+[^>]+|)>/)[1];
//Считаем всё, что нужно
if (clean[name] == null)
clean[name] = 1;
else
clean[name]++;
}
//Ещё раз путешествуем (да, да, знаю, про O(n), ну а что делать?)
for (elem in clean)
{
//Пациент жив?
//Если количество тегов чётно, то всё впорядке! Иначе — есть незакрытый тег
//Вся логика в одной строке. Живи и дай жить, JavaScript!
if (clean[elem] mod 2 != 0 && elem != "")
result.push("<" + elem + ">");
}
//Возвращаем обратно, а то покалечат
return result;
}
Вот и всё! Применить вышезапрограммированное также не составляет большого труда:
//На безымянной высоте
new AutoTagsField("autotags");
Контрольная сверка
Итак, сегодня мы попытались облегчить себе жизнь на уровне редактора записей. По крайней мере, я себе это уже сделал, а вы?
И, да, кстати, маленькая подсказка в блоге: нажмите на дату комментария для подсветки всех комментариев данного пользователя к данной записи.
Удачных вам посевов, господа!