|
Pers.narod.ru. 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 (для удаления лишних пробелов из начала и конца строки) и 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 Кб)
|
|