Nickolay.info. Javascript. Пишем простой редактор HTML-кода на JavaScript |
В статье описан и доступен для скачивания очень компактный (6-8 Кб) кросс-браузерный редактор кода HTML c кнопками ввода тегов. Вот как одна из его разновидностей выглядит сейчас, когда я набираю в нём эту статью.
Разумеется, существует немало готовых решений, например, TinyMCE (полтора мегабайта), FCKeditor (более полутора мегабайт), BUEditor (входит в движок Drupal, но если помучить, можно заставить работать отдельно... размера не помню, но много) и т.п. Во-первых, все такие визуальные редакторы HTML громоздки, во-вторых, не всегда очевидны в установке, в-третьих, они используют весьма "навороченные" возможности AJAX, "знакомые" далеко не каждому браузеру, несмотря на бурный прогресс в этой сфере. Очень часто ставить такой Javascript-движок на пользовательскую форму ввода или в собственный блог - непозволительная роскошь и напоминает расстрел ни в чём не повинных воробьёв из крупнокалиберных пушек.
Я думаю, мы напишем такой редактор, как показан на картинке выше, уложившись в 6-8 килобайт кода. Особенно если сделаем невероятное допущение, что пользователь, увидев тег вида <a href="" target="_blank">сюда</a>, который он ввёл одним нажатием кнопки при выделенном слове "сюда", в состоянии понять, куда нужно вставить адрес ссылки - между двойными кавычками. К сожалению, неспособность большинства людей к элементарным логически обоснованным действиям и есть основная причина массовой визуализации всего и вся в современной культуре, но мы рассчитываем на остаток человечества, уверенный, что дважды два - таки четыре, а не "примерно пять-шесть" ... Но довольно отступлений.
Мы не обязаны вводить именно HTML, куда безопаснее BB-коды, в таком случае в скрипте изменится только описание вот этого массива:
bbtags = new Array( '<p>','', '<br>','', '<b>','</b>', '<i>','</i>', '<u>','</u>', '<s>','</s>', '<sub>','</sub>', '<sup>','</sup>', '<div align="center">','</div>', '<font color="red">','</font>', '<code>','</code>', '<pre>','</pre>', '<blockquote>','</blockquote>', '<','>', '<ul>','</ul>', '<ol>','</ol>', '<li>','', '<img src="" hspace="2" vspace="2" title="">','', '<a href="" target="_blank">','</a>', '<a href="mailto:">','</a>' );
...ну, может, ещё где-то пара функций чуть поменяется, посмотрим в процессе.
Как видите, здесь просто перечислены открывающие и закрывающие части допустимых тегов, наверняка будут теги, которые закрывать не нужно или нельзя, в таком случае закрывающая часть представляет собой пустую строку. Нетрудно их идентифицировать по этой строке, но мы для простоты перечислим номера незакрываемых тегов в отдельном методе:
function not_closed_tags(n) { var r=false; if (n==0 || n==2 || n==32 || n==34) r=true; return r; }
Здесь "незакрываемыми" показаны теги <p>, <br>, <li> и <img>. Для самих знаков < и > стоило бы сделать 2 отдельных незакрываемых тега, они ведь не обязаны вводиться вместе, однако стереть отдельно стоящий > нетрудно, так что не будем и заморачиваться.
Непосредственно над полем ввода сделаем информационную строку с именем helpline
, куда будет выводиться длина уже напечатанного текста и подсказки о назначении кнопок. Для неё понадобится массив подсказок к тегам и функция-обработчик:
helplines = new Array( "Абзац: <p>текст</p>", "Перевод строки: <br>текст", "Жирный текст: <b>текст</b>", "Наклонный текст: <i>текст</i>", "Подчёркнутый текст: <u>текст</u>", "Перечёркнутый текст: <s>текст</s>", "Нижний индекс: <sub>текст</sub>", "Верхний индекс: <sup>текст</sup>", "Центрировать: <div align=center>текст</div>", "Цвет шрифта: <font color=red>текст</font> Подсказка: или color=#FF0000", "Код: <code>текст</code>", "Листинг (программа): <pre>код</pre>", "Цитата: <blockquote>текст</blockquote>", "Знаки < и > в тексте страницы", "Маркированный список: <ul>текст</ul>", "Нумерованный список: <ol>текст</ol>", "Номер или маркер в списке: <li>текст", "Вставить картинку: <img src=http://image_url>", "Вставить ссылку: <a href=http://url>текст ссылки</a>", "Адрес E-mail: <a href=mailto:E-mail>E-mail</a>" ); function helpline (i) { if (i<0) document.getElementById('helpbox').innerHTML = 'Можно быстро применить стили к выделенному тексту'; else document.getElementById('helpbox').innerHTML = helplines[i]; }
Информация об используемых тегах будет сохраняться в массиве-стеке bbcode
, соответственно, кроме самого массива, понадобятся методы получения размера массива getarraysize
, добавления элемента arraypush
и извлечения элемента arraypop
:
bbcode = new Array(); function getarraysize(thearray) { for (i = 0; i < thearray.length; i++) { if ((thearray[i] == "undefined") || (thearray[i] == "") || (thearray[i] == null)) return i; } return thearray.length; } function arraypush(thearray,value) { thearray[ getarraysize(thearray) ] = value; } function arraypop(thearray) { thearraysize = getarraysize(thearray); retval = thearray[thearraysize - 1]; delete thearray[thearraysize - 1]; return retval; }
Главное из того, что осталось - программно отличать ситуацию, когда в поле ввода <textarea> есть выделенный текст от ситуации, когда выделенного текста нет, и в зависимости от этого либо окружать открывающей и закрывающей частью тега выделенный кусочек поля, либо просто писать теги в конец поля. Здесь надо, кроме всего прочего, отличать Internet Explorer от остальных браузеров, работающих с выделением иначе.
Итак, метод bbplace
у нас будет выполнять работу с выделенными фрагментами в <textarea>
, метод bbstyle
с параметром bbnumber
будет управлять тегами с переданными параметром номерами, а остальное в следующем кусочке листинга - просто браузерные "патчики" и служебные переменные.
Здесь и далее предположим, что форма у нас будет называться f1
, а поле ввода в ней - text
. Элемент подсказки, как уже написано выше в коде, имеет id="helpbox"
.
var theSelection = false; var clientPC = navigator.userAgent.toLowerCase(); var clientVer = parseInt(navigator.appVersion); var is_ie = ((clientPC.indexOf("msie") != -1) && (clientPC.indexOf("opera") == -1)); var is_win = ((clientPC.indexOf("win")!=-1) || (clientPC.indexOf("16bit") != -1)); function bbplace(text) { var txtarea = document.f1.text; var scrollTop = (typeof(txtarea.scrollTop) == 'number' ? txtarea.scrollTop : -1); if (txtarea.createTextRange && txtarea.caretPos) { var caretPos = txtarea.caretPos; caretPos.text = caretPos.text.charAt(caretPos.text.length - 1) == ' ' ? caretPos.text + text + ' ' : caretPos.text + text; txtarea.focus(); } else if (txtarea.selectionStart || txtarea.selectionStart == '0') { var startPos = txtarea.selectionStart; var endPos = txtarea.selectionEnd; txtarea.value = txtarea.value.substring(0, startPos) + text + txtarea.value.substring(endPos, txtarea.value.length); txtarea.focus(); txtarea.selectionStart = startPos + text.length; txtarea.selectionEnd = startPos + text.length; } else { txtarea.value += text; txtarea.focus(); } if (scrollTop >= 0 ) { txtarea.scrollTop = scrollTop; } } function bbstyle(bbnumber) { var txtarea = document.f1.text; txtarea.focus(); donotinsert = false; theSelection = false; bblast = 0; if (bbnumber == -1) { //Закрыть все теи while (bbcode[0]) { butnumber = arraypop(bbcode) - 1; txtarea.value += bbtags[butnumber + 1]; } txtarea.focus(); return; } if ((clientVer >= 4) && is_ie && is_win) { theSelection = document.selection.createRange().text; //Получить выделение для IE if (theSelection) { //Добавить теги вокруг непустого выделения document.selection.createRange().text = bbtags[bbnumber] + theSelection + bbtags[bbnumber+1]; txtarea.focus(); theSelection = ''; return; } } else if (txtarea.selectionEnd && (txtarea.selectionEnd - txtarea.selectionStart > 0)) { //Получить выделение для Mozilla mozWrap(txtarea, bbtags[bbnumber], bbtags[bbnumber+1]); return; } for (i = 0; i < bbcode.length; i++) { if (bbcode[i] == bbnumber+1 && !not_closed_tags(bbnumber)) { bblast = i; donotinsert = true; } } if (donotinsert) { while (bbcode[bblast]) { butnumber = arraypop(bbcode) - 1; if (!not_closed_tags(butnumber)) bbplace(bbtags[butnumber + 1]); } txtarea.focus(); return; } else { //Открыть тег bbplace(bbtags[bbnumber]); arraypush(bbcode,bbnumber+1); txtarea.focus(); return; } storeCaret(txtarea); } function mozWrap(txtarea, open, close) { if (txtarea.selectionEnd > txtarea.value.length) { txtarea.selectionEnd = txtarea.value.length; } var oldPos = txtarea.scrollTop; var oldHght = txtarea.scrollHeight; var selStart = txtarea.selectionStart; var selEnd = txtarea.selectionEnd+open.length; txtarea.value = txtarea.value.slice(0,selStart)+open+ txtarea.value.slice(selStart); txtarea.value = txtarea.value.slice(0,selEnd)+close+ txtarea.value.slice(selEnd); txtarea.selectionStart = selStart+open.length; txtarea.selectionEnd = selEnd; var newHght = txtarea.scrollHeight - oldHght; txtarea.scrollTop = oldPos + newHght; txtarea.focus(); } function storeCaret(textEl) { //Вставка в позицию каретки - патч для IE if (textEl.createTextRange) textEl.caretPos = document.selection.createRange().duplicate(); document.getElementById('helpbox').innerHTML = "Всего: "+ document.f1.text.value.length; }
Остаётся нарисовать для тегов кнопки, положить их во вложенную папку tags
и написать метод, который всё это рисует. Между группами кнопок добавлены дополнительные жёсткие пробелы
в тех же местах, в которых они есть на рисунке выше.
function showIcons () { var l=bbtags.length; for (i=0; i<l; i+=2) { var p = bbtags[i].indexOf(' '); if (p<0) p = bbtags[i].indexOf('>'); if (p<0) p = bbtags[i].indexOf(';'); var tagname = bbtags[i].substring (1,p); if (i==38) tagname='am'; var i2= i/2; var alter= helplines[i2]; document.writeln ('<img src="tags/'+tagname+ '.gif" width="16" height="16" hspace="0" vspace="0" alt="'+ alter+'" title="'+alter+ '" onClick="bbstyle('+i+')" onMouseOver="helpline('+ i2+')" onMouseOut="helpline(-1)">'); if (i==14 || i==26 || i==32) document.writeln (' '); } }
Функция по-хитрому получает имена файлов с картинками из самих названий тегов, считая, что имя картинки к тегу - это та подстрока из строки его открывающей части в массиве bbtags
, которая находится после открывающего знака < и до первого пробела, либо знака >, либо точки с запятой (для <div align="center">
получится имя файла div
, а путь и расширение добавятся программным кодом).
Поскольку у тега <a>
2 разновидности - обычная ссылка и почтовая - его отслеживаем отдельно. Для BB-кодов функция поменяется и станет проще.
Сами кнопочки с теми же именами, что нужны в коде, будут в архиве. Но сначала немного о том, как всё это вызвать.
Разумеется, скрипт, который мы поместим в файл blockeditor.js
, будет вызываться откуда-то из .php или .html-файла, а введённые данные будут передаваться на сервер. Мы для простоты напишем только файл index.html
с формой, а код страницы никуда передавать не будем, оставив атрибут action
тега <form>
пустым. Фактически, в этом случае кнопка "Отправить" будет просто обновлять страницу.
<html><head> <link rel="stylesheet" type="text/css" href="style.css"> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> <title>Простой текстовый редактор на JS</title> <script type="text/javascript" src="blockeditor.js"></script> </head> <body bgcolor="#EEEEEE" text="#111111"> <form name="f1" action="" method="post" enctype="multipart/form-data"> <table border=0 align="center"> <tr><td> <script type="text/javascript"> showIcons (); </script> <br> <span id="helpbox" style="width:450px; font-size:10px" class="small">Можно быстро применить стили к выделенному тексту</span> </td></tr> <tr><td> <textarea name="text" class="button" rows="10" cols="72" onselect="storeCaret(this);" onclick="storeCaret(this);" onkeyup="storeCaret(this);"></textarea> </td></tr> <tr><td> <input type="submit" class="button" value="Отправить" onclick="bbstyle(-1); return checkblock();"> </td></tr> </form> </body></html>
Обратите внимание на события onselect
, onclick
, onkeyup
в теге поля ввода <textarea>
, если их не указать, в Internet Explorer теги не будут применяться к выделенному в поле ввода тексту (как и сделано на половине форумов инета).
Также посмотрим на обработчик нажатия кнопки "Отправить", имеющий вид onclick="bbstyle(-1); return checkblock();
. Здесь сначала закрываются все теги, которые нужно закрыть (вызов метода bbstyle
с параметром, равным -1
), а затем предполагается, что вызван метод с именем checkblock
, который определяет, корректны ли данные и возвращает true
, если их имеет смысл пересылать методом post
.
Напишем этот недостающий метод, предположив, что проверка корректности сводится к тому, чтоб текст был не пуст, не превышал по общей длине значения maxLen
и не содержал слишком длинных слов, длиннее значения maxWordLen
. В JavaScript недостаёт методов trim
(для удаления лишних пробелов из начала и конца строки - в более новом стандарте JS метод появился!) и strip_tags
(для удаления из строки тегов HTML), так что их тоже придётся написать.
var maxLen=1024; var maxWordLen=80; function strip_tags (string) { return string.replace(/<\/?[^>]+>/gi, ''); } function trim(string) { return string.replace (/(^\s+)|(\s+$)/g, ""); } function goodWordsLength (v) { var s=v.split(/\s/); for (var i=0; i<s.length; i++) if (s[i].length>maxWordLen) { return false; } return true; } function checkblock() { var text=strip_tags(trim(document.f1.text.value)); if (text=='') { window.alert ( 'Текст блока не может быть пустым, пожалуйста, заполните его'); return false; } if (text.length > maxLen) { window.alert ( 'Текст слишком длинный. Допустимая длина: '+ maxLen); return false; } if (goodWordsLength(text)==false) { window.alert ( 'В тексте есть слишком длинные слова. Допустимая длина: '+ maxWordLen); return false; } return true; }
Вот и всё, размер файла со скриптом - чуть более 7 Кб. Добавим до куче стиль, раз в HTML-файле упоминался style.css
и что-то оттуда использовалось:
body { background-color: #EEEEEE; overflow: scroll; } th,td,p { font-family: Verdana, Arial, Helvetica, sans-serif; font-size : 12px; line-height: 18px; } small { font-family: Verdana, Arial, Helvetica, sans-serif; font-size : 9px; line-height: 12px; } form { margin: 3px 2px 3px 3px; } input,textarea,select { color : #000000; background-color : #FFFFFF; font: normal 12px Verdana, Arial, Helvetica, sans-serif; border-color : #000000; border-width: 1px; } input { text-indent : 2px; } .button { background-color : #EFEFEF; color : #000000; font-size: 11px; font-family: Verdana, Arial, Helvetica, sans-serif; border-style: solid; border-width: 1px; }
Скачать этот пример в архиве ZIP (9 Кб)
гостевая; E-mail |