Пишем плагин для CKeditor 4

от автора

Ckeditor — это WYSIWYG редактор HTML-кода для браузеров. Всякий раз, сталкиваясь с его документацией или же с его исходным кодом, с исходным кодом его плагинов я терялся. И это не мудрено, ведь CKeditor это очень большой продукт, имеющий довольно сложную инфраструктуру. Но, зачастую, стандартных возможностей не хватает и требуется добавить свою. В этой статье я хотел бы остановиться на плагине, который позволяет встраивать и оперировать в редакторе Yandex-картами.

Вот так это будет выглядеть по окончанию редактирования:

А вот так в режиме редактирования:

Принцип работы

Итак. Давайте определим каким мы хотим видеть будущий инструмент в работе. Нужно чтобы карту можно было встроить в документ, удалить из него и была возможность изменить какие-либо её параметры. Т.к. оперирование <script />-ми в режиме редактирования HTML — задача, как минимум, не тривиальная, то мы воспользуемся стандартным плагином «fakeobjects», который позволяет на время редактирования заменить наш HTML на что-нибудь более сподручное, точнее на &ltimg /> (именно их использует fakeobjects). Помимо прочего нам потребуется плагин «dialog» для редактирования настроек карты. Сразу отмечу, что плагин получится довольно примитивным, т.к. у меня не стояло задачи делать что-либо сложное. Плагин позволит разместить карту, используя значения 2-ух координат (широта и долгота), масштаба (1-17), сможет разместить на карте метку, а также текст с описанием под картой.

Т.к. Yandex-карты — объект динамически подключаемый, а встраивание лишних <script />-ов, по поводу и без, занятие неблагодарное, на выходе я буду получать следующий HTML-код: <em data-plugin="-json_data-">MAP</em>. И при помощи несложной javascript-функции превращать карету<em /> в карту, используя YandexMap API.

Хочу отметить, что во многом я опирался на работу стандартного плагина «Flash», поэтому часть используемых мною вещей мною до конца не понята. Во многом из-за не совершенства документации, частично из-за моей непонятливости. Большую часть выводов я сделал, опираясь именно на исходный код, и часть на базу ответов в StackOverflow.

Создание плагина

Для удобства разместим весь код плагина внутри самовыполняющейся анонимной функции:

function() { 	/* code */ } )();

Чтобы добавить плагин в систему воспользуемся:

CKEDITOR.plugins.add( 'ymap' /* наименование плагина */, // объект с настройками и функциями { 	// зависимости 	requires: [ 'dialog', 'fakeobjects' ],  	// функция вызываемая при инициалзации плагина 	init: function( editor /* этот объект - экземпляр редактора */ ) 	{ 	},  	// эта функция вызывается чуть позднее, и почему то отсутствует в API 	afterInit: function( editor ) 	{ 	} } );

Подробнее о init:

this._initTranslations( editor ); // локализация, об этом ниже  var req = 'em' /* tag */ + '[!data-plugin,!width,!height]' /* attrs */ + 	'(' + ymap_class_name + ')' /* classes */ + 	'{width,height}' /* styles */; // добавляем команду для вызова диалога editor.addCommand( 'ymap', new CKEDITOR.dialogCommand( 'ymap', { 	allowedContent: req } ) );  // добавляем в систему новую кнопку editor.ui.addButton( 'YMap', { 	label: lang.button_label, 	command: 'ymap' } ); // регистрируем сам диалог CKEDITOR.dialog.add( 'ymap', add_dialog /* функция, о ней ниже */ );  // вешаем свой обработчик на событие двойного-клика // если объектом является наш fakeobject - указываем диалог настройки editor.on( 'doubleclick', function( evt ) { 	var element = evt.data.element;  	if( element.is('img') && element.data('cke-real-element-type') == 'ymap' ) 	{ 		evt.data.dialog = 'ymap'; 	} } );	 

Наиболее важным пунктом является определение allowedContent при создании новой команды. Дело в том, что в Ckeditor 4 добавили новую систему — «Allowed Content Rules». Эта система сражается с некорректным HTML кодом (например, при вставке извне). Она удаляет лишние теги, атрибуты, стили и классы из HTML-кода. А для того, чтобы системе дать понять, что лишнее, а что нет, при регистрации команд мы указываем объект, который может содержать поля allowedContent и requiredContent. Задача первого запросить возможности, задача второго отключить команду, если возможностей не хватает. Мне хватило использования только allowedContent-а. Принципы его работы можно посмотреть здесь. Я остановлюсь на самых основных:

  • Значением является строка, которая может состять из 4 частей: теги, атрибуты, стили и классы. Пример: «p[data-role]{text-align}(tip)»
  • Аттрибуты, которые являются обязательными для поддержки нужно указывать через «!». Пример: «[!width]»
  • Принцип работы несколько не очевиден, мне так и не удалось в нём разобраться. Периодически у меня отваливалась поддержка атрибутов и классов. Лечение было нетривиальным. Да прибудет с вами сила!

Отдельный момент с локализацией. Её вы вольны организовать как вам удобнее, я остановился на следующем подходе:

var translations = { 	ru: 	{ 		fake_object_label: 'Yandex Карта', 		title: 'Yandex Карта', 		// ... 	}, 	en: 	{ 		fake_object_label: 'Yandex Map', 		title: 'Yandex Map', 		// ... 	}, 	def: 'ru' }; var lang; // shotrcut  // ...  CKEDITOR.plugins.add( 'ymap', { 	// ...  	_initTranslations: function( editor ) 	{ 		var current_lang = CKEDITOR.lang.detect(); 		CKEDITOR.lang[ current_lang ]['ymap'] = translations[ current_lang ] 			? translations[ current_lang ] 			: translations[ translations.def_lang ]; 		lang = editor.lang.ymap; 		// подсказка при наведении мыши на fakeobject 		editor.lang.fakeobjects.ymap = lang.fake_object_label; 	},  	// ... } );

afterInit

В коде этой функции мы добавим в фильтры редактора свой собственный, который при переключении из режима исходного кода (или просто при инициализации редактора) в режим редактирования позволит заменить нам реальный код на fakeobject:

afterInit: function( editor ) { 	// получаем фильтр текущего редактора 	var dataProcessor = editor.dataProcessor, 		dataFilter = dataProcessor && dataProcessor.dataFilter;  	if( dataFilter ) 	{ 		// и создаём своё правило 		dataFilter.addRules 		( 			{ 				// для тегов 				elements: 				{ 					// в качестве конечного тега я выбрал em. Причина такого выбора 					// заключется в том, что мне потребовался inline-тег, чтобы  					// усмирить повадки CKeditor-а по любому поводу плодить 					// пустые P-ки 					'em': function( el ) 					{ 						// если эта em - не Ymap, значит ищем глубже 						if( ! is_plugin_node( el ) ) 						{ 							for( var i = 0 ; i < el.children.length; ++ i ) 							{ 								if( el.children[ i ].name === 'cke:ymap' ) 								{ 									if( ! is_plugin_node( el.children[ i ] ) ) 									{ 										return null; // не наш случай 									}  									// создаём новый fakeobject 									return create_fake_element( editor, el ); 								} 							}  							return null; // не наш случай 						}  						// иначе - создаём новый fakeobject 						return create_fake_element( editor, el ); 					} 				} 			}, 1 /* приоритет фильтрации */ 		) 	} // if dataFilter }

Функция для проверки — является ли элемент «картой»

var is_plugin_node = function( el ) { 	return el.attributes['class'] === ymap_class_name; };

Фукнция для превращения em в fakeobject:

var create_fake_element = function( editor, real_el ) { 	// заменяемый элемент, класс для IMG, тип объекта, растягиваемость 	return editor.createFakeParserElement( real_el, 'cke_ymap', 'ymap', true ); };

Под растягиваемостью следует понимать возможность наличия у объекта ширины и высоты. Если указать false, то fakeobject не унаследует width и height у исходного элемента.

Диалог

Вот мы и подошли к самому главному — к диалогу. В нём заключается почти всё. Описывается он следующим образом:

var add_dialog = function( editor ) { 	var dialog = 	{ 		title: lang.title, // заголовок диалога 		// и его размеры 		width: 300,  		height: 100, 		// в этом массиве — все кнопки, табы, поля ввода… 		contents: 		[ 		],  		// методы 		onShow: function(){}, 		onOk: function(){} 	}; 	return dialog; };

Ну а теперь по порядку. Начнём с компонентов:

{ // таб, обязателен, если он 1, то отображается только содержимое 	id: 'tab_info', 	expand: true, 	padding: 0, 	elements: 	[ 		{ // название 			id: 'name', 			type: 'text', 			label: lang.f_name, 			commit: commit, 			setup: load 		}, 		{ // метка 			id: 'label', 			type: 'text', 			label: lang.f_label, 			commit: commit, 			setup: load 		}, 		{ // гор.панелька 			type: 'hbox', 			align: 'left', 			children: 			[ 				{ // latitude 					id: 'lon', 					type: 'text', 					label: lang.f_lat, 					commit: commit, 					validate: CKEDITOR.dialog.validate 						.regex( /^[\d\,\.]+$/, lang.inc_coord ), 					setup: load, 					'default': '43.2503' 				}, 				// … 

Компонентам можно задать следующие обработчики:

  • «commit» — вызывается при сборе значений
  • «setup» — вызывается при установке значений
  • «validate» — используется для проверки значения

Поле id используется в качестве идентификатора компонента, а не в качестве атрибута. Все типы готовых валидаторов вы можете посмотреть в объекте CKEDITOR.dialog.validate.

onShow

Как нетрудно догадаться, этот метод будет вызван при отображении диалога. Наша задача в нём выполнить все приготовления и заполнить значения полей, если перед вызовом был выделен наш fakeobject.

// забываем параметры предыдущих вызовов this.fake_image = this.ymap_node = null;  // проверяем был ли выбран какой-нибудь элемент перед вызовом диалога var fake_image = this.getSelectedElement(); // если этот объект наш... if( fake_image && fake_image.data( 'cke-real-element-type' ) === 'ymap' ) { 	this.fake_image = fake_image; 	// этот метод возвращает нашу EM-ку (не отображает в редакторе, а просто 	// возвращает объект) 	this.ymap_node = editor.restoreRealElement( fake_image ); 	// т.к. все настройки мы будем хранить как JSON в атрибуте "data-plugin", 	// то получаем их назад в переменную cfg 	var cfg = JSON.parse( this.ymap_node.getAttribute('data-plugin') ) 	// эта функция инициирует setup у каждого из компонентов. 	// в setup-функцию будут переданы все аргументы, которые мы здесь 	// зададим. В данном случае хватает cfg 	this.setupContent( cfg ); }

Функция setup (для компонентов):

var load = function( cfg ) { 	// просто задаём в качестве значения - данные из cfg 	// которые в свою очередь взяты из data-plugin атрибута 	// куда мы поместили их ранее (об этом позднее :) ) 	this.setValue( cfg[ '_' + this.id ] ); };

onOk

Эта функция вызывается тогда, когда пользователь нажал в диалоге кнопку «Ок», и при этом все поля не содержали ошибок. Наиболее важная из всех наших функций 🙂 В ней мы создаём и манипулируем fakeobject-ом, а также создаём итоговую EM-ку.

// this.fake_image мы задаём в onShow, если // был выбран ранее созданный fakeobject // если его нет, то пользователь нажал на кнопку вызова диалога как раз  // с намерением создать НОВУЮ карту if( ! this.fake_image ) { 	// создаём новый EM-элемент 	var node = CKEDITOR.dom.element 		.createFromHtml( '<cke:em>MAP</cke:em>', editor.document ); 	// и задаём ему нужный класс 	node.addClass( ymap_class_name ); } else { 	// если такой объект уже есть 	// то в качестве EM-ки воспользуемся старой, 	// которую мы восстановили в onShow 	// методом restoreRealElement 	var node = this.ymap_node; }  // определим все стили и атрибуты var extra_styles = {}, extra_attributes = {}; // эта функция вызывает commit у каждого из компонентов и // работает точно так же как и load. // мы передаём в неё объекты для того, чтобы собрать все // нужные стили и атрибуты this.commitContent( node, extra_styles, extra_attributes );  // чтобы задать их EM-ке node.setStyles( extra_styles ); // важный момент - сохраняем все настройки в атрибут node.$.setAttribute( 'data-plugin', JSON.stringify( extra_attributes ) );  // и новому fakeobject-у // первый аргумент - тег, который мы хотим спрятать // второй аргумент - класс, который будет у тега IMG  // третий аргумент - тип нашего объекта // четвёртый - есть ли у нашего объекта такое понятие как размер var new_fake_image = editor.createFakeElement( node, 'cke_ymap', 'ymap', true ); new_fake_image.setAttributes( extra_attributes ); new_fake_image.setStyles( extra_styles );  // если у нас уже был fakeobject if( this.fake_image ) { 	// то заменим его 	new_fake_image.replace( this.fake_image ); 	// и выделим 	editor.getSelection().selectElement( new_fake_image ); } else { 	// иначе вставим в документ новый объект 	// если пользователь перед этим что-либо выделил 	// наш объект это уничтожит 	editor.insertElement( new_fake_image ); }

Функция commit у компонентов (вызывается нами в принудительном порядке при помощи commitContent):

var commit = function( ymap, styles, attrs ) { 	var value = this.getValue();  	if( this.id === 'width' || this.id === 'height' ) 	{ 		// чтобы объекты действительно изменили свой размер 		// им нужно его задать в стилях 		styles[ this.id ] = value; 	} 	else if( this.id === 'lat' || this.id === 'lon' ) 	{ 		// если в координатах указаны запятые - заменим их на точки 		value = value.replace( ',', '.' ); 	}  	// сохраняем все поля в атрибутах, 	// которые мы в onOk запишем ещё и в data-plugin 	attrs[ '_' + this.id ] = value; };

Отмечу, что если предполагается наличие у объекта полей width и height, то их необходимо использовать, в противном случае может отвалиться allowedContents 🙁

CSS

В CSS нам нужно задать внешний вид для EM-ки и для IMG-fakeobject-а. Внешний вид для EM-ки задаётся в стилях вашего сайта, а внешний вид IMG в стилях, которые подключатся к редактору. Их можно задать и через JS, при помощи:

CKEDITOR.addCss( 'img.cke_ymap { /* css */ }' );

Итог

Плагин в целом оказался не сложным. Причиной тому наличие плагина fakeobject и то, что мы работаем с одним блочным объектом. Я полагаю, что работа со строчными тегами намного сложнее, из-за того, что пользователь может выделить текст так, что в него попадут разные части содержимого разных тегов. Впрочем, я не пробовал 🙂

При желании мы можем подключить к нашему плагину контекстное меню, добавить поля (к примеру, позволить задавать множество меток, а не только одну), панель предпросмотра и многое другое.

Я подготовил тестовую страничку, в которой можно посмотреть на плагин в деле. Скачать архив (~ 900 KiB, zip).

Ссылки

Нужен ли этот плагин на github-e? (или в репозитории плагинов для CKeditor 4)

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/204176/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *