Доброго времени суток, уважаемыее.
Вот уже несколько месяцев я активно использую 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