Минные разметочные поля

20 августа 2008 года, 04:36
К данной статье привязаны следующие примеры:

Мне всегда нравились очень простые, но достаточно умненькие редакторы. Именно поэтому я отдаю предпочтение не тяжеловесным 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");

Контрольная сверка

Итак, сегодня мы попытались облегчить себе жизнь на уровне редактора записей. По крайней мере, я себе это уже сделал, а вы?

И, да, кстати, маленькая подсказка в блоге: нажмите на дату комментария для подсветки всех комментариев данного пользователя к данной записи.

Удачных вам посевов, господа!

Мнения (20) прямой порядок обратный порядок

  • Miscђka

    20 августа 2008
    10:55

    вместо new AutoTagsField("autotags"); удобно использовать маленькую функцию при старте подключаемого скрипта (лучше внедриться в body.onload()), которая будет проверять класс всех полей textarea, и при наличии в них жестко определенного класса (e.g. class="din_autotags, my_textarea_class"), будет вызывать сама эту функцию new.

    Это очень удобно для разработчика структуры — достаточно указать доп. класс, и не нужно возиться с созданием элементов в жабаскрипте.

  • Miscђka

    20 августа 2008
    10:57

    посмотрел пример — это просто чудо!

  • pepelsbey

    20 августа 2008
    13:26
    <ul> <li>hhh <li>ggg </ul>

    Если после ggg набрать закрывающий тег, то выскочит вот такая конструкция:

    <ul> <li>hhh <li>ggg</li/li> </ul>

    ps: а ещё очень раздражает пляшущая кнопка предпросмотра.

    Зачем там вообще таймер? Очень отвлекает от написания.

  • Rakovets` Oleksandr

    20 августа 2008
    13:59

    Я обычно пишу сначала <>, а потом уже тег внутри. Такой вариант не работает, вставляет дополнительную >. Или и не должен?

  • Rakovets` Oleksandr

    20 августа 2008
    14:02

    А так вообще отлично! Я понял, как буду учить JS — по твоим скриптам!

  • Miscђka

    20 августа 2008
    14:12
    <ul> <li>hhh <li>ggg </ul>

    Если после ggg набрать закрывающий тег, то выскочит вот такая конструкция:

    ...

    а потому что такая разметка li невалидна с точки зрения XHTML и XML. Реализация такой разметки намного сложнее, и, думаю, Дин ее не будет делать из-за ненужности.

  • Николай

    20 августа 2008
    14:27

    Хм что-то я не понял, у меня там просто textfield, никаких кнопок, ничего нет.

  • platun

    20 августа 2008
    15:02

    Та же привычка что и у Rakovets` Oleksandr. В Эклипсе аналогично сделано, напрягает.

  • Дин

    20 августа 2008
    18:29

    вместо new AutoTagsField("autotags"); удобно использовать маленькую функцию при старте подключаемого скрипта (лучше внедриться в body.onload()), которая будет проверять класс всех полей textarea, и при наличии в них жестко определенного класса (e.g. class="din_autotags, my_textarea_class"), будет вызывать сама эту функцию new.

    Unobtrusive JavaScript? Конечно, можно и так, но, я считаю, что суть примера не в этом, однако на заметку возьму обязательно. ;-)

    ps: а ещё очень раздражает пляшущая кнопка предпросмотра.

    Зачем там вообще таймер? Очень отвлекает от написания.

    @pepelsbey, можно убрать, не проблема. Но тогда возникнет вопрос: как оповещать пользователя о том, что предпросмотр будет обновлён? Подумаю над этим.

    @pepelsbey, да, это однозначно баг со списками. Вся проблема в том, что механизм поиска тегов максимально упрощён, то есть не предусматривает каких-либо странных ситуаций. Но данную ситуацию, я думаю, стоит предусмотреть, изменив логику.

    @Николай, просто пишите туда код с какой либо (X)HTML-разметкой. :-)

    А вообще, это дело вкуса: как набирать теги. можно предусмотреть и вариант, указанный Александром, чтобы никого не обижать.

  • Miscђka

    20 августа 2008
    19:10

    суть примера не в этом

    само собой, вся статья вообще не об этом. Я просто для заметки напомнил о таком удобном механизме. Если кто будет по твоей статье делать реализацию, чтобы тоже можно было сразу предусмотреть.

  • Дин

    20 августа 2008
    19:26

    @pepelsbey и компания! Я исправил, чтобы всё работало как вы просили.

  • Sam

    20 августа 2008
    19:26

    Понравилось. Почувствовал себя почти в Eclipse без подсветки.

  • pepelsbey

    21 августа 2008
    03:23

    > как оповещать пользователя о том, что предпросмотр будет обновлён?

    А может просто делать это раз в секунду? Ну или подобрать комфортное значение таймаута. Так, в общем, везде и сделано.

  • Дин

    21 августа 2008
    03:28

    @pepelsbey, раз в секунду делать AJAX-запрос на сайт? Если бы там отображался непреформатированный ввод, то куда ни шло, а там же отправляется запрос на сайт для обработки типографом, поэтому, я думаю, что это будет затратно (и для сервера, и для клиента).

    Два выхода: переносить логику типографа на клиент-сайд или просто спрятать глупый таймер.

  • miripiruni

    22 августа 2008
    01:19

    Касательно предпросмотра:

    Не понимаю для чего такое решение с таймером: если я захочу увидеть как будет выглядеть мой комментарий я нажму кнопку предпросмотра когда пожелаю. На мой взгляд здесь крайне неудобное решение. Мне не ясно зачем автоматизировать то, что не нужно, пораждая при этом массу вопросов и спорных моментов.

    По теме поста:

    Интересная задумка. Только вот я за собой всегда теги закрываю сам. Не доверяю никому) Мало ли как оно пойдет… Да еще и на не знакомом сайте.

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

    Хотя раскрашивать можно что угодно. Это уже вопросы дизайна.

  • Дин

    22 августа 2008
    01:44

    @miripiruni, очень спорно с предпросмотром. Я, например, не люблю постоянно отвлекаться при написании комментариев. Пусть само обновляет, пока я пишу; одновременно, я смогу следить за тем, что я пишу, лишь переводя взгляд вниз.

    Альтернативное решение: добавить accesskey к этой кнопке и использовать горячую клавишу (не утилизируя при этом мышку).

    Таймер сам по себе уже убран, изменения станут доступны при очередном моём обновлении блога.

  • Дин

    22 августа 2008
    01:51

    @miripiruni, идея с цветом тоже нравится. Можно попробовать посидеть подумать, как оно. :-) Пока в голову пришла лишь генерация на стороне сервера подложки для обозначенной иконки, а саму её сделать прозрачной. Хм. Хм!

  • Miscђka

    22 августа 2008
    16:50

    насчет таймера — да. Лучше сделать предпросмотр по требованию.

    А вот что делать, если мне по религиозным соображениям не подойдет цвет, которым меня обозначит автомат?

  • Максим

    14 сентября 2008
    16:02

    Дин, почему статистика стала закрытой?

  • Дин

    14 сентября 2008
    19:11

    @Максим, такие вопросы стоит задавать в личной переписке, либо писать на почту. И почему вас интересует статистика?

Добавить мнение

Публичное сообщение
Моё имя

Отправить Включить предпросмотр Подписаться на оповещения