Как интегрировать ckEditor в AngularJS

от автора

Доброго времени суток, уважаемые хабровчане.

Вот уже несколько месяцев я активно использую AngularJS в одном из рабочих проектов. Петь “похвальные песни” или возносить этот фреймверк я не буду, потому что идеальных вещей нет (да и наверно было бы очень скучно жить в мире с такими вещами, которые не оставляют возможности побороть их недостатки своим “творчеством”). Скажу только пару слов относительно результатов: идеология AngularJS очень хорошо справляется с организацией кода в моем лице и дает волшебный инструмент Directives. Кстати, недавно уже была заметка о CornerJS, в котором директивы выведены в центр технологии, а на Google I/O в этом году проскакивала новость о возможной поддержки custom-elements(не просто тегов, а комплексных html компонентов, встраиваемых в страницу).

На очередном этапе разработки встал вопрос о интеграции с продвинутым WYSIWYG редактором и мой взор сразу же пал на ckEditor, так как я его уже неоднократно использовал в рамках проектов на базе DotNetNuke и впечатления остались весьма положительные (ну или скажем по другому: сильных огрех в компоненте найдено не было а интеграция заняла считанные часы).

Взывая к Google мольбы о помощи в интеграционной магии, я получил несколько ссылок на Stackoverflow и другие частные блоги, где решением всех проблем выступает событие pasteState и директива в целом выглядит просто и доступно:

app.directive('ckEditor', [function () {     return {         require: '?ngModel',         link: function ($scope, elm, attr, ngModel) {             var ck = CKEDITOR.replace(elm[0]);              ck.on('pasteState', function () {                 $scope.$apply(function () {                     ngModel.$setViewValue(ck.getData());                 });             });              ngModel.$render = function (value) {                 ck.setData(ngModel.$modelValue);             };         }     }; }]); 

Но после очередного мейлстоуна и деплоя, заказчик начал замечать, что иногда отредактированный текст не сохраняется в полной мере или компонент вообще вылетает с Access Violation, в случае ранних версий InternetExplorer (< 9). Проблема была воспроизведена, идеальные условия получены и я отправился искать решение проблемы.

Перечитав еще раз документацию и потыкав страничку на предмет общей картины, я пришел к выводу, что данное событие отлично работает с клавиатурой, но совершенно игнорирует вставки элементов (будь то текст, картинки и т.п.) из плагинов, идущих в комплекте. Сразу же было решено искать новое событие, которое носит более глобальный характер. Но результаты поисков привели к вот такому “неправославному” методу, который в довесок еще и плагин (а это чревато при миграции или обновлениях самого ckEditor). Поэтому было решено поднять версию редактора до последней доступной (CKEditor 4.2.1 на тот момент), рискнув стабильностью и получить волшебное событие change на уровне нативного api (возможно они просто интегрировали вышеупомянутый плагин в ядро, за историей я, честно, не следил). Замена события на change помогла решить вопрос с сохранением измененного содержимого, но не решила проблемы с Access Violation.

Наученный горьким опытом popup окон для IE < 9 (IE разносит выполнение разных окон, на разные потоки с полным сбросом cookies и т.п.) я пришел к выводу, что проблема в iframe компоненте и последовательности создания и обращения к нему внутри самого ckEditor, а также цикла жизни scope в рамках AngularJS. Проблема, связана с тем, что метод CKEDITOR.replace(…) срабатывает несинхронно в старых IE и “отпустив” контекст исполнения AngularJS пытается сделать биндинг модели и вызвать setData, который пытается обратиться к еще неготовому iframe, что и вызывает Access Violation. Ничего “лучше” и “надежнее” очереди я не придумал, поэтому результатом стал следующий код директивы:

app.directive('ckEditor', [function () {         return {             require: '?ngModel',             restrict: 'C',             link: function (scope, elm, attr, model) {                 var isReady = false;                 var data = [];                 var ck = CKEDITOR.replace(elm[0]);                  function setData() {                     if (!data.length) { return; }                      var d = data.splice(0, 1);                     ck.setData(d[0] || '<span></span>', function () {                         setData();                         isReady = true;                     });                 }                  ck.on('instanceReady', function (e) {                     if (model) { setData(); }                 });                  elm.bind('$destroy', function () {                     ck.destroy(false);                 });                  if (model) {                     ck.on('change', function () {                         scope.$apply(function () {                             var data = ck.getData();                             if (data == '<span></span>') {                                 data = null;                             }                             model.$setViewValue(data);                         });                     });                      model.$render = function (value) {                         if (model.$viewValue === undefined) {                             model.$setViewValue(null);                             model.$viewValue = null;                         }                          data.push(model.$viewValue);                          if (isReady) {                             isReady = false;                             setData();                         }                     };                 }              }         };     }]); 

В коде есть неоднозначный моменты, связанные с очередью и значениями model.$viewValue (в частности, это были попытки справиться с замиранием компонента в модальных диалогах, которая была решена патчем twitter bootstrap modal компонета, но это уже другая история).

Так же я не в полной мере раскрыл моменты, связанные с setData(…, callback), которая по сути является одним из синхронизирующих механизмов, но, как мне кажется, код выглядит информативно в данном контексте и заменит слова.

Буду раз выслушать предложения и критику по поводу данного подхода.

P.S.
Рабочий пример того, что получилось у меня http://jsfiddle.net/jWANb/2/
Рабочий пример того, что советуют в интернетах http://jsfiddle.net/fvApg/1/

Попробуйте вставить картинку во втором примере и посмотреть на “result html”. Будет видно, что контекст не изменился.

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


Комментарии

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

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