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

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

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

Я тоже знаю!

Для обращения к человеку используйте символ @, после которого следует имя того, к кому обращаетесь (пробелы заменяются на знак подчёркивания). Если вам интересно, можете подписаться на комментарии по RSS или по эл. почте. Ведите себя достойно, вы же не роботы, правда?

Вы можете использовать следующие XHTML-элементы в разметке комментария: strong, em, span[class=crossline], a[href=uri], code[type=язык], blockquote, ul и ol. В качестве языка кода может быть указан, например, javascript или css.