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

в 15:43, , рубрики: AngularJS, ckeditor, javascript, wysiwyg, метки: , ,

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

Вот уже несколько месяцев я активно использую 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”. Будет видно, что контекст не изменился.

Автор: leximus

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js