Pull to refresh

Suit Up! Простой и легкий WYSIWYG

Reading time10 min
Views58K


Статья делится на три части:
UPD Критика


Вступление


Не так давно, а, точнее, года два назад, в кругу разработчиков, с которыми я имел честь общаться (почти все — новички), каждый, кому поступила задача поставить WYSIWYG, ставили монстрообразный TinyMCE. Этот редактор считался почему-то стандартом у многих веб разработчиков, хотя, мало кому нужны были то большое количество функций, которые предлагались программистам. Тут тебе и то и это. Наверно, таким образом, новички пытались сказать клиенту «смотри, мы тебе на сайт запилили Ворд».

Однажды (не помню при каких условиях), мне захотелось или понадобилось разобраться в том, как работают браузерные «рич эдиторы». Моему удивлению не было предела, когда я сам, не имея каких-либо глубоких познаний в веб разработке, сделал две кнопочки: Bold и Italic, что оказалось очень простой задачей. Мне захотелось больше узнать о том, что же делать дальше. Так я познакомился с серией статей «WYSIWYG HTML редактор в браузере» (по ссылке первая статья, советую прочесть). Но информация на тот момент мне показалась несколько сложноватой. Поэтому я решил методом тыка, наступая на уже растоптанные кем-то грабли, написать свой простой редактор.

Сделал я его в виде jQuery плагина, и, думаю, не стоит отвечать «почему». Получилось кое-как заставить работать его в разных браузерах. Тут мне пришла в голову идея написать статью на хабр, после некоторых доработок. Время шло, допиливание я откладывал, откладывал… Два года, черт, целых два года. Но я постараюсь исправиться.


Простейший редактор


Для того, чтоб дать возможность пользователю менять содержимое блока (в данном случае, обычного дива) просто задаём ему (блоку) атрибут contenteditable:
<div contenteditable></div>

Редактор готов!

Шучу :)

Для того, чтоб проделать некие изменения над текстом (например, задать цвет шрифта), существует метод document.execCommand, который применяется к текущему выделению или позиции каретки. Метод принимает три аргумента:
  1. Имя команды (например, italic).
  2. Показывать ли стандартный диалог для получения значения (поправьте, если определение неверно). Как правило, никто этим не пользуется, поэтому значение всегда будет false.
  3. Значение команды.


Добавим на страницу кнопку Bold, делающую текст жирным:
<button class="bold"></button>

Пишем простейший обработчик клика:
$( '.bold' ).on( 'click', function() {
   document.execCommand( 'bold', null, null ); 
});

Готово. Выделяем текст в диве, нажимает на кнопку: оп, текст жирный. Нажимаем еще раз: хоба, текст стал прежним.

Добавляем еще две кнопки
<button class="italic">italic</button>
<button class="red">Red</button>

Первая будет делать текст наклонным (italic, как и bold не требует значения, так как он и в Африке italic), вторая — менять цвет текста на красный.

$( '.italic' ).on( 'click', function() {
   document.execCommand( 'italic', false, null ); 
});

$( '.red' ).on( 'click', function() {
   document.execCommand( 'forecolor', false, 'red' ); 
});


Команда forecolor, очевидно, не может обойтись без значения, иначе браузер не поймет, какой именно цвет надо задать тексту.

Результат: jsfiddle.net/6BkPu

Вот и всё, теперь дело за малым:
1. Взять .innerHTML дива и делать с ним что угодно: отправить на сервер, вставить в textarea и пр.
2. По аналогии добавить еще команд, которые можно глянуть по этой ссылке.
3. Добавить иконки и поиграться со шрифтами.

Для подсветки кнопки (для проверки текущего значения команд) используются методы document.queryCommandValue и document.queryCommandState. Описывать их поведение не буду, так как оно зависит от браузера. Только производитель браузера решает, бросать исключение или нет, только производитель браузера решает, что вернуть, поэтому предлагаю самим, при желании, разобраться, что к чему. Придется, в основном, разбираться методом тыка.

Чтоб не быть голословным, приведу пример.
document.queryCommandValue( 'formatblock' ) в ФФ и Хроме возвращает следующие значения: h1, h2, p..., несмотря на то, что значения в document.execCommand( 'formatblock', ... ) были обрамлены в треугольные скобки:

,

, для корректной работы в IE, который не принимает в качестве третьего аргумента execCommand( 'formatblock' ... ) значения без скобок. Это понять можно: мы всегда можем определить "имя блока", которое будет всегда одним и тем же. А вот IE... что бы вы думалиии?.. в ответ на вызов queryCommandValue( 'formatblock' ) возвращает "Заголовок 1", "Заголовок 2", "Обычный", соответственно. Отловить имя блока просто невозможно, учитывая, что, очевидно, для каждого языка существует свой индивидуальный набор возвращаемых значений. И это всё в инновационном, быстром и славном IE10. Черт, в инновационном, быстром и славном IE10! Мазилла и Гугл крайне медленно, но уверенно работают над приведением поведения этих методов к одному виду, но Мелкософт вообще не чешется.


Редактор SuitUp


Демо
Итак, хочу показать свою версию легковесного редактора, который, конечно, не так крут, как многофункциональные фено-морозилко-будильнико-утюги, но неплохо справляется с задачами нетребовательного разработчика. Хотя, количество поддерживаемых методов велико.

Задачей было предоставить удобную прослойку между командами и программистом :)

Запускается это дело, как и все плагины таким образом:
$( '.my-textarea' ).suitUp();

В качестве аргументов можно передать список команд:
$( '.my-textarea' ).suitUp( 'italic', 'bold', '|', 'formatblock#<h1>' );

Или один аргумент в виде массива
$( '.my-textarea' ).suitUp([ 'italic', 'bold', '|', 'formatblock#<h1>' ]);


«|» в списке означает обычный разделитель, как элемент дизайна. Элементы, в которых присутствует решетка, говорят нам о том, что перед решеткой находится команда, после — значение. Просто пишете название_команды#значение, получив при этом обычную кнопку. Остальные элементы — это команды, которые следует определить, иначе не известно, как мы должны получать значение. Если поведение не определено, то, по умолчанию, значением будет считаться null. Ниже я попробую привести примеры для большей наглядности.

Определение команд (как должны вести себя команды)

Для определения команд был создан объект jQuery.suitUp.commands, содержащий пары ключ-значение (ваш кэп), где ключём является имя команды, например, 'forecolor', а значение может иметь три типа:

1. Собственно, само значение: для forecolor это может быть red:
$.suitUp.commands = {
  forecolor: 'red'
}

или
$.extend( jQuery.suitUp.commands, {
  forecolor: 'red'
})

или
$.suitUp.commands.forecolor = 'red' // (просто помните, что commands - обычный объект)

Подключаем:
$( '.suitup-textarea' ).suitUp( 'forecolor' );

2. Словарь значений (объект)
$.suitUp.commands.forecolor = {
   'Make Red': 'red',
   'Make Green': 'green'
}

Он преобразуется в обычный тег select, опции которого выглядят как ключ словаря, при выборе которых задействуется значение словаря.

Подключаем аналогично:
$( '.suitup-textarea' ).suitUp( 'forecolor' );



3. Функция, которая отвечает за то, как мы должны получить значение команды. Она асинхронна, то есть, в функции можно создать модальное окно с запросом значения. Для примера приведу обычный propmt:

jQuery.suitUp.commands.forecolor = function( callback ) {
   var color = prompt( 'Введите цвет', '' ); // произвольным образом получаем значение
  callback( color ); //передаём в коллбек
}

И снова:
$( '.suitup-textarea' ).suitUp( 'forecolor' );


Часть команд определена в самом скрипте, часть — в отдельном файле, список команд достаточно велик для того, чтоб не заморачиваться с определением. То есть, можно просто передать аргументы в метод suitUp и это будет просто работать:
$( 'textarea' ).suitUp( 'italic', 'bold', '|', 'link', 'formatblock#<h1>' );

Попробуем добавить несколько кнопочек. Смотрим команды на сайте мозиллы. Скажем, нам хочется добавить:
1. Опять Bold.
2. Выбор шрифта.
3. Диалог, в котором пользователю нужно ввести значение цвета в кастомное окошко и нажать "ок".

И представим, что ни одна из этих команд не определена.

$.extend( $.suitUp.commands, {
	bold: null, // строка для наглядности, не определенные команды по умолчанию имеют значение null
	fontname: {
		Arial: 'arial',
		Times: 'times',
		Verdana: 'verdana'
	},
	forecolor: function( callback ){
		var blackBackground = $( '<div/>' ).css({
				background: 'black',
				position: 'absolute',
				top: 0,
				left:0,
				opacity: .5,
				width: '100%',
				height: '100%'
			}).on( 'click', function(){
				popup.add( this ).remove();
			}).appendTo('body'),
			
			popup = $( '<div/>' ).css({
				padding: '10px 20px',
				width: 200,
				position: 'absolute',
				background: 'white',
				top: 200,
				left: $( window ).width()/2 - 110,
				zIndex: 100
			}).appendTo('body');
		
		$( '<input/>' ).appendTo( popup );
		
		$( '<button/>' ).appendTo( popup ).text( 'Go!' ).on( 'click', function() {
			var val = popup.children( 'input' ).val();
			blackBackground.add( popup ).remove()
			callback( val );
		});
	}
});


Запускаем
$( '.suitup-textarea' ).suitUp( 'bold', 'fontname', 'forecolor' );








С первой и второй командой, думаю, всё понятно. В третьей большую часть кода занимает создание элементов, стоит только обратить внимание на обработчик клика на button. В нем, после получения значения цвета и удаления попапа, вызывается callback, в который и передаётся полученный цвет. Callback возвращает редактору фокус, восстанавливает выделение, сделанное до нажатия кнопки и применяет команду forecolor к выделению.

document.execCommand( 'forecolor', false, 'введенное значение' );

Внимание! Несмотря не то, что команды из разных источников обозначены в виде camelCase, для корректной работы плагина, все символы должны быть в нижнем регистре. Это касается и значений команд.

Кастомные команды

Кроме возможности использования стандартных команд, плагин поддерживает создание собственных элементов. Дабы не мудрить, я решил сделать добавление кастомных контролов обычными функциями, возвращающими элемент.

Как и в случае с командами, для кастомных элементов используется обычный объект:
$.suitUp.custom = {};

Добавим элемент, при нажатии на который вызывается обычный алерт с приветствием.

$.extend( jQuery.suitUp.custom, {
	helloWorld: function() {
		return $( '<span/>', {
			'class': 'suitup-control' // задаёт размеры кнопки
		}).css( 'backgroundColor', 'red' ).on( 'click', function() {
			alert( 'Hello World!' );
		});
	}
});


Теперь вызываем:
$( '.suitup-textarea' ).suitUp( 'bold', 'fontname', 'forecolor', 'helloWorld' );



Обратите внимание, что в кастомных командах регистр не имеет значения.

По дефолту, в списке кастомных элементов наличествует элемент "link", который кроме добавления ссылки (команда "createlink"), убирает ссылку в текущем выделении (команда "unlink").

... {
	link: function() {
		return jQuery._createElement( 'a', {
			className: 'suitup-control',
			href: '#'
		}).attr({
			'data-command': 'createlink' // добавляет такой же стиль как и у команды createlink
		}).on( 'click', function() {
			if( !$.suitUp.hasSelectedNodeParent( 'a' ) ) {
				document.execCommand( 'createlink', false, window.prompt( 'URL:', '' ) );
			} else {
				document.execCommand( 'unlink', false, null );
			}
		});			
	}
}

Как видно, кастомные элементы нужны не только для создания необычных контролов (как в случае с helloWorld), но и позволяет решить проблемы со стандартными командами.

В итоге имеем следующие способы создания кнопок:
  • В перечислении аргументов suitUp указать не определенную команду (которая не определена в commands), при этом значение по умолчанию будет null.
  • Указать не определенную команду задав значение через решетку (forecolor#red)
  • Определить команду тремя способами:
    1. Задать значение.
    2. Задать список значений.
    3. Задать функцию, "добывающую" значение (например, какой-нибудь colorpicker).

    И указать её в качестве одного из аргументов suitUp.
  • Добавить кастомный контрол.


Список команд по умолчанию

Вместо постоянной передачи аргументов в метод suitUp можно объявить набор кнопок лишь однажды:
$.suitUp.controls = [ 'bold', 'italic' ];
$( '.suitup-textarea' ).suitUp();
// почти то же самое, что и
$( '.suitup-textarea' ).suitUp( 'bold', 'italic' );

В массиве controls содержится список кнопок, которые подключаются по умолчанию. Это самый обычный массив. Например, можно добавить к списку по умолчанию еще один элемент:

$.suitUp.controls.push( 'forecolor' );
$( '.suitup-textarea' ).suitUp();

Или сделать так:
$( '.suitup-textarea' ).suitUp( $.suitUp.controls.concat([ 'forecolor' ]) );
для того, чтоб добавить кнопки для конкретного редактора (если их несколько на странице).

Несколько дополнительных функций

Для упрощения некоторых действий, в том числе, создания альтернативных контролов, в объект $.suitUp добавлено несколько функций. Сразу предупреждаю, они писались для использования внутри плагина и в этих рамках работают неплохо. Если вы собираетесь их юзать отдельно, они могут повести себя не так, как расчитывалось.

$.suitUp.getSelection
Возвращает текущее выделение. Для нормальных браузеров и для ослов возвращаемые значения будут разными.

$.suitUp.restoreSelection
Восстанавливает выделение. В качестве единственного аргумента передается значение, полученное в getSelection.

var sel = $.suitUp.getSelection();
// делаем что-то, при этом теряя фокус

$( '.suitup-editor' ).focus(); // возвращаем фокус
jQuery.suitUp.restoreSelection( sel ); // восстанавливаем выделение


$.suitUp.getSelectedNode
Возвращает ноду текущего выделения (это может быть как тег так и текстовая нода)

$.suitUp.hasSelectedNodeParent
Малозначимая функция, используется только в кастомном элементе "link". Проверяет, есть ли у ноды текущего выделения родитель с тегом, определенным в единственном аргументе.
jQuery.suitUp.hasSelectedNodeParent( 'a' ); // true/false


Скрипт тестировался в Chrome, Firefox, IE10 (+IE7 Mode)
На скорую руку создал репозиторий, пользуйтесь: github.com/finom/Suitup

В файле extended-commands.suitup.jquery.js содержится несколько команд для тестов. Раскомментируйте код с 50 строки и оцените изобилие далеко не всех команд, которые поддерживаются плагином.



С опечатками и не точностями, пожалуйте в личку.

Лучей бобра.

UPD

Критика из комментариев

Наверное, стоит уведомить потенциальных пользователей, с какими проблемами они могут столкнуться при работе с редактором. Вся критика из комментариев сводится к особенностям браузеров, которые могут быть неподобающими, если проект требует унифицированного форматирования.
1. Для разных браузеров некоторые команды обрамляют текст в разные теги. Например, команда bold в Chrome обрамляет текст в тег b, а в IE в тег strong.
2. Обработка пользовательских действий может быть разной в разных браузерах (примеров пока что не приходит в голову).
3. Юзер может использовать форматирование, которое не подразумевалось. Например, может скопировать текст из ворда и вставить в редактор.

Всё это — дань компактности плагина. Если желаете использовать редактор для серьезного публичного проекта, я, как автор, советую воспользоваться альтернативными решениями. Редактор подойдет для небольших проектов и внутреннего использования, например, в админках. Впечатление от использования в таком ключе — исключительно положительные.

Еще раз добра.
Tags:
Hubs:
Total votes 95: ↑90 and ↓5+85
Comments88

Articles