Nickolay.info. Javascript. Пишем простой редактор HTML-кода на 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>',
 '&lt;','&gt;',
 '<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(
 "Абзац: &lt;p&gt;текст&lt;/p&gt;",
 "Перевод строки: &lt;br&gt;текст",
 "Жирный текст: &lt;b&gt;текст&lt;/b&gt;",
 "Наклонный текст: &lt;i&gt;текст&lt;/i&gt;",
 "Подчёркнутый текст: &lt;u&gt;текст&lt;/u&gt;",
 "Перечёркнутый текст: &lt;s&gt;текст&lt;/s&gt;",
 "Нижний индекс: &lt;sub&gt;текст&lt;/sub&gt;",
 "Верхний индекс: &lt;sup&gt;текст&lt;/sup&gt;",
 "Центрировать: &lt;div align=center&gt;текст&lt;/div&gt;",
 "Цвет шрифта: &lt;font color=red&gt;текст&lt;/font&gt;  Подсказка: или color=#FF0000",
 "Код: &lt;code&gt;текст&lt;/code&gt;",
 "Листинг (программа): &lt;pre&gt;код&lt;/pre&gt;",
 "Цитата: &lt;blockquote&gt;текст&lt;/blockquote&gt;",
 "Знаки &lt; и &gt; в тексте страницы",
 "Маркированный список: &lt;ul&gt;текст&lt;/ul&gt;",
 "Нумерованный список: &lt;ol&gt;текст&lt;/ol&gt;",
 "Номер или маркер в списке: &lt;li&gt;текст",
 "Вставить картинку: &lt;img src=http://image_url&gt;",
 "Вставить ссылку: &lt;a href=http://url&gt;текст ссылки&lt;/a&gt;",
 "Адрес E-mail: &lt;a href=mailto:E-mail&gt;E-mail&lt;/a&gt;"
);

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 и написать метод, который всё это рисует. Между группами кнопок добавлены дополнительные жёсткие пробелы &nbsp; в тех же местах, в которых они есть на рисунке выше.

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 ('&nbsp;&nbsp;&nbsp;');
 }
}

Функция по-хитрому получает имена файлов с картинками из самих названий тегов, считая, что имя картинки к тегу - это та подстрока из строки его открывающей части в массиве 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 (для удаления лишних пробелов из начала и конца строки) и 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 Кб)

Рейтинг@Mail.ru

вверх гостевая; E-mail