refactor(mail(js)): replace ckEditor directive by sgCkeditor component

This refactoring

 1. simplifies updating the CKEditor source code;
 2. allows many instances of the CKEditor on the same page;
 3. fixes the cursor positioning on focus.
pull/277/head
Francis Lachapelle 2020-05-15 11:03:39 -04:00
parent 9f861bd629
commit 07c06db69d
17 changed files with 461 additions and 165 deletions

View File

@ -203,11 +203,6 @@ static NSArray *infoKeys = nil;
return [[ud mailComposeMessageType] isEqualToString: @"html"];
}
- (NSString *) editorClass
{
return ([self isHTML]? @"ck-editor" : @"plain-text");
}
- (NSString *) itemPriorityText
{
return [self labelForKey: [NSString stringWithFormat: @"%@", [item lowercaseString]]];

View File

@ -8,7 +8,7 @@
xmlns:label="OGo:label"
className="UIxPageFrame"
title="title"
const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, Common.js, Preferences.services.js, Mailer.services.js, Contacts.js, Contacts.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js">
const:jsFiles="vendor/ckeditor/ckeditor.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Mailer.services.js, Contacts.js, Contacts.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js">
<script type="text/javascript">
var contactFolders = <var:string value="contactFolders.jsonRepresentation" const:escapeHTML="NO" />;
</script>

View File

@ -264,18 +264,22 @@
<!-- MESSAGE CONTENT -->
<md-input-container class="md-block sg-mail-editor-content">
<textarea name="content" var:class="editorClass"
<textarea name="content" class="plain-text"
ng-if="editor.composeType == 'text'"
rows="9"
ck-locale="editor.localeCode"
ck-margin="16px"
ck-focus="editor.onHTMLFocus($event)"
ng-model="editor.message.editable.text"
ng-focus="editor.onTextFocus($event)"
md-autofocus="::!editor.isNew()"
md-no-resize="md-no-resize"
md-no-autogrow="md-no-autogrow"
sg-autogrow="true"
md-detect-hidden="md-detect-hidden" />
md-detect-hidden="md-detect-hidden"><!-- plain text editor --></textarea>
<sg-ckeditor id="message-content"
ng-if="editor.composeType == 'html'"
config="editor.ckConfig"
on-instance-ready="editor.onHTMLReady($editor)"
on-focus="editor.onHTMLFocus($editor)"
ng-model="editor.message.editable.text"><!-- HTML editor --></sg-ckeditor>
</md-input-container>
</md-dialog-content>

View File

@ -8,7 +8,7 @@
xmlns:label="OGo:label"
className="UIxPageFrame"
title="title"
const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.js, Mailer.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js">
const:jsFiles="vendor/ckeditor/ckeditor.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.js, Mailer.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js">
<script type="text/javascript">
var mailAccounts = <var:string value="mailAccounts" const:escapeHTML="NO" />;
var userNames = <var:string value="userNames" const:escapeHTML="NO" />;

View File

@ -9,7 +9,7 @@
className="UIxPageFrame"
title="title"
const:popup="YES"
const:jsFiles="Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.app.popup.js, Mailer.services.js, vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js">
const:jsFiles="Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.app.popup.js, Mailer.services.js, vendor/ckeditor/ckeditor.js, Common/sgCkeditor.component.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js">
<main class="layout-fill" ng-controller="navController">
<div id="detailView" class="view-detail md-default-theme md-background md-bg md-hue-2"

View File

@ -122,19 +122,16 @@
<md-input-container class="md-block md-flex"
ng-if="$AccountDialogController.defaults.SOGoMailComposeMessageType == 'text'">
<label><var:string label:value="Signature"/></label>
<textarea ng-model="$AccountDialogController.account.identities[0].signature"><!-- signature --></textarea>
<textarea
ng-model="$AccountDialogController.account.identities[0].signature"><!-- plain text editor --></textarea>
</md-input-container>
<div class="pseudo-input-container"
ng-if="$AccountDialogController.defaults.SOGoMailComposeMessageType == 'html'">
<label class="pseudo-input-label"><var:string label:value="Signature"/></label>
<textarea class="ck-editor"
ck-locale="$AccountDialogController.defaults.LocaleCode"
ck-options="{ 'autoGrow_minHeight': 70,
'toolbar': [['Bold', 'Italic', '-', 'Link',
'Font','FontSize','-','TextColor',
'BGColor', 'Source']] }"
ck-margin="8px"
ng-model="$AccountDialogController.account.identities[0].signature"><!-- signature --></textarea>
<sg-ckeditor
config="$AccountDialogController.ckConfig"
ck-margin="8px"
ng-model="$AccountDialogController.account.identities[0].signature"><!-- HTML editor --></sg-ckeditor>
</div>
<md-input-container class="md-block md-input-has-value">

View File

@ -8,7 +8,7 @@
xmlns:label="OGo:label"
className="UIxPageFrame"
title="title"
const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, vendor/angular-file-upload.min.js, vendor/ng-sortable.min.js, vendor/qrcode.min.js, Common.js, Preferences.js, Preferences.services.js, Mailer.services.js, Contacts.services.js">
const:jsFiles="vendor/ckeditor/ckeditor.js, Common/sgCkeditor.component.js, vendor/angular-file-upload.min.js, vendor/ng-sortable.min.js, vendor/qrcode.min.js, Common.js, Preferences.js, Preferences.services.js, Mailer.services.js, Contacts.services.js">
<main layout-fill="layout-fill" ui-view="preferences"
ng-controller="navController"><!-- preferences --> </main>

View File

@ -8,7 +8,7 @@
xmlns:label="OGo:label"
className="UIxPageFrame"
title="title"
const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, vendor/ng-sortable.min.js, Common.js, Preferences.services.js, Contacts.services.js, Mailer.services.js, vendor/angular-file-upload.min.js, Scheduler.js, Scheduler.services.js, vendor/FileSaver.min.js">
const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ng-sortable.min.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Contacts.services.js, Mailer.services.js, vendor/angular-file-upload.min.js, Scheduler.js, Scheduler.services.js, vendor/FileSaver.min.js">
<script type="text/javascript">
var firstDayOfWeek = <var:string value="firstDayOfWeek"/>;
var dayStartHour = <var:string value="dayStartHour"/>;

View File

@ -0,0 +1,414 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
(function() {
'use strict';
/**
* sgCkeditor - A component for the CKEditor v4
* Based on https://github.com/jziggas/ng-ck/.
* @memberof SOGo.Common
* @example:
<sg-ckeditor
config="$ctrl.config"
on-instance-ready="$ctrl.onEditorReady($editor)"
on-focus="$ctrl.onEditorFocus($editor)"
ng-model="$ctrl.content"></sg-ckeditor>
*/
function sgCkeditorConfigProvider() {
// Default plugins that have successfully passed through Angular's $sanitize service
var defaultConfiguration = {
toolbarGroups: [
{ name: 'basicstyles', groups: [ 'basicstyles' ] },
{ name: 'colors' },
{ name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align' ] },
{ name: 'links' },
{ name: 'insert' },
{ name: 'editing', groups: [ 'spellchecker' ] },
{ name: 'styles' },
{ name: 'mode' }
],
// The default plugins included in the basic setup define some buttons that
// are not needed in a basic editor. They are removed here.
removeButtons: 'Strike,Subscript,Superscript,BGColor,Anchor,Format,Image',
// Dialog windows are also simplified.
removeDialogTabs: 'link:advanced',
enterMode: CKEDITOR.ENTER_BR,
tabSpaces: 4,
// fullPage: true, include header and body
allowedContent: true, // don't filter tags
entities: false,
// Configure autogrow
// https://ckeditor.com/docs/ckeditor4/latest/guide/dev_autogrow.html
autoGrow_onStartup: true,
autoGrow_minHeight: 300,
autoGrow_bottomSpace: 0,
language: 'en',
// The Upload Image plugin requires a remote URL to be defined even though we won't use it
imageUploadUrl: '/SOGo/'
};
var events = [
'activeEnterModeChange',
'activeFilterChange',
'afterCommandExec',
'afterInsertHtml',
'afterPaste',
'afterPasteFromWord',
'afterSetData',
'afterUndoImage',
'ariaEditorHelpLabel',
'autogrow',
'beforeCommandExec',
'beforeDestroy',
'beforeGetData',
'beforeModeUnload',
'beforeSetMode',
'beforeUndoImage',
'blur',
'change',
'configLoaded',
'contentDirLoaded',
'contentDom',
'contentDomInvalidated',
'contentDomUnload',
'customConfigLoaded',
'dataFiltered',
'dataReady',
'destroy',
'dialogHide',
'dialogShow',
'dirChanged',
'doubleclick',
'dragend',
'dragstart',
'drop',
'elementsPathUpdate',
'fileUploadRequest',
'fileUploadResponse',
'floatingSpaceLayout',
'focus',
'getData',
'getSnapshot',
'insertElement',
'insertHtml',
'insertText',
'instanceReady',
'key',
'langLoaded',
'loadSnapshot',
'loaded',
'lockSnapshot',
'maximize',
'menuShow',
'mode',
'notificationHide',
'notificationShow',
'notificationUpdate',
'paste',
'pasteFromWord',
'pluginsLoaded',
'readOnly',
'removeFormatCleanup',
'required',
'resize',
'save',
'saveSnapshot',
'selectionChange',
'setData',
'stylesSet',
'template',
'toDataFormat',
'toHtml',
'unlockSnapshot',
'updateSnapshot',
'widgetDefinition'
];
var config = angular.copy(defaultConfiguration);
this.$get = function () {
return {
config: config,
events: events
}
};
}
var sgCkeditorComponent = {
controllerAs: 'vm',
require: {
ngModelCtrl: 'ngModel'
},
bindings: {
checkTextLength: '<?',
config: '<?',
maxLength: '<?',
minLength: '<?',
ckMargin: '@?',
onActiveEnterModeChange: '&?',
onActiveFilterChange: '&?',
onAfterCommandExec: '&?',
onAfterInsertHtml: '&?',
onAfterPaste: '&?',
onAfterPasteFromWord: '&?',
onAfterSetData: '&?',
onAfterUndoImage: '&?',
onAriaEditorHelpLabel: '&?',
onAutogrow: '&?',
onBeforeCommandExec: '&?',
onBeforeDestroy: '&?',
onBeforeGetData: '&?',
onBeforeModeUnload: '&?',
onBeforeSetMode: '&?',
onBeforeUndoImage: '&?',
onBlur: '&?',
onChange: '&?',
onConfigLoaded: '&?',
onContentChanged: '&?', // Not CKEditor API
onContentDirLoaded: '&?',
onContentDom: '&?',
onContentDomInvalidated: '&?',
onContentDomUnload: '&?',
onCustomConfigLoaded: '&?',
onDataFiltered: '&?',
onDataReady: '&?',
onDestroy: '&?', // Not sure if this works because of the cleanup done in $onDestroy. Needs testing.
onDialogHide: '&?',
onDialogShow: '&?',
onDirChanged: '&?',
onDoubleclick: '&?',
onDragend: '&?',
onDragstart: '&?',
onDrop: '&?',
onElementsPathUpdate: '&?',
onFileUploadRequest: '&?',
onFileUploadResponse: '&?',
onFloatingSpaceLayout: '&?',
onFocus: '&?',
onGetData: '&?',
onGetSnapshot: '&?',
onInsertElement: '&?',
onInsertHtml: '&?',
onInsertText: '&?',
onInstanceReady: '&?',
onKey: '&?',
onLangLoaded: '&?',
onLoadSnapshot: '&?',
onLoaded: '&?',
onLockSnapshot: '&?',
onMaximize: '&?',
onMenuShow: '&?',
onMode: '&?',
onNotificationHide: '&?',
onNotificationShow: '&?',
onNotificationUpdate: '&?',
onPaste: '&?',
onPasteFromWord: '&?',
onPluginsLoaded: '&?',
onReadOnly: '&?',
onRemoveFormatCleanup: '&?',
onRequired: '&?',
onResize: '&?',
onSave: '&?',
onSaveSnapshot: '&?',
onSelectionChange: '&?',
onSetData: '&?',
onStylesSet: '&?',
onTemplate: '&?',
onToDataFormat: '&?',
onToHtml: '&?',
onUnlockSnapshot: '&?',
onUpdateSnapshot: '&?',
onWidgetDefinition: '&?',
placeholder: '<?',
readOnly: '<?',
required: '<?'
},
template: '<textarea ng-attr-placeholder="{{vm.placeholder}}"></textarea>',
controller: sgCkeditorController
};
sgCkeditorController.$inject = ['$element', '$scope', '$parse', '$timeout', 'sgCkeditorConfig'];
function sgCkeditorController ($element, $scope, $parse, $timeout, sgCkeditorConfig) {
var vm = this;
var config;
var content;
var editor;
var editorElement;
var editorChanged = false;
var modelChanged = false;
this.$onInit = function () {
vm.ngModelCtrl.$render = function () {
if (editor) {
editor.setData(vm.ngModelCtrl.$viewValue, {
noSnapshot: true,
callback: function () {
editor.fire('updateSnapshot')
}
})
}
};
config = vm.config ? angular.merge(sgCkeditorConfig.config, vm.config) : sgCkeditorConfig.config;
if (config.language) {
// Pickup the first matching language supported by SCAYT
// See http://docs.ckeditor.com/#!/guide/dev_howtos_scayt
config.scayt_sLang = _.find(['en_US', 'en_GB', 'pt_BR', 'da_DK', 'nl_NL', 'en_CA', 'fi_FI', 'fr_FR', 'fr_CA', 'de_DE', 'el_GR', 'it_IT', 'nb_NO', 'pt_PT', 'es_ES', 'sv_SE'], function(sLang) {
return sLang.lastIndexOf(config.language, 0) == 0;
}) || 'en_US';
// Disable caching of the language
// See https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/126
config.scayt_disableOptionsStorage = 'lang';
}
if (vm.ckMargin) {
// Set the margin of the iframe editable content
CKEDITOR.addCss('.cke_editable { margin-top: ' + vm.ckMargin +
'; margin-left: ' + vm.ckMargin +
'; margin-right: ' + vm.ckMargin + '; }');
}
};
this.$postLink = function () {
editorElement = $element[0].children[0];
editor = CKEDITOR.replace(editorElement, config);
editor.on('instanceReady', onInstanceReady);
editor.on('pasteState', onEditorChange);
editor.on('change', onEditorChange);
editor.on('paste', onEditorPaste);
editor.on('fileUploadRequest', onEditorFileUploadRequest);
if (content) {
modelChanged = true
editor.setData(content, {
noSnapshot: true,
callback: function () {
editor.fire('updateSnapshot')
}
});
}
};
this.$onChanges = function (changes) {
if (
changes.ngModel &&
changes.ngModel.currentValue !== changes.ngModel.previousValue
) {
content = changes.ngModel.currentValue;
if (editor && !editorChanged) {
if (content) {
editor.setData(content, {
noSnapshot: true,
callback: function () {
editor.fire('updateSnapshot')
}
});
modelChanged = true;
}
}
editorChanged = false;
}
if (editor && changes.readOnly) {
editor.setReadOnly(changes.readOnly.currentValue);
}
}
this.$onDestroy = function () {
editor.destroy();
}
function onInstanceReady (event) {
// Register binded callbacks for all available events
_.forEach(_.filter(sgCkeditorConfig.events, function (eventName) {
return eventName != 'instanceReady';
}), function (eventName) {
var callbackName = 'on' + eventName[0].toUpperCase() + eventName.slice(1);
if (vm[callbackName]) {
editor.on(eventName, function (event) {
vm[callbackName]({
'$event': event,
'$editor': editor
});
});
}
});
if (vm.onInstanceReady) {
vm.onInstanceReady({
'$event': event,
'$editor': editor
});
}
// vm.ngModelCtrl.$render();
}
function onEditorChange () {
var html = editor.getData();
var text = editor.document.getBody().getText();
if (text === '\n') {
text = '';
}
if (!modelChanged && html !== vm.ngModelCtrl.$viewValue) {
editorChanged = true;
vm.ngModelCtrl.$setViewValue(html);
validate(vm.checkTextLength ? text : html);
if (vm.onContentChanged) {
vm.onContentChanged({
'editor': editor,
'html': html,
'text': text
});
}
}
modelChanged = false;
}
function onEditorPaste (event) {
var html;
if (event.data.type == 'html') {
html = event.data.dataValue;
// Remove images to avoid ghost image in Firefox; images will be handled by the Image Upload plugin
event.data.dataValue = html.replace(/<img( [^>]*)?>/gi, '');
}
}
function onEditorFileUploadRequest (event) {
// Intercept the request when an image is pasted, keep an inline base64 version only.
var data, img;
data = event.data.fileLoader.data;
img = editor.document.createElement('img');
img.setAttribute('src', data);
editor.insertElement(img);
event.cancel();
}
function validate (body) {
if (vm.maxLength) {
vm.ngModelCtrl.$setValidity('maxlength', body.length > vm.maxLength + 1);
}
if (vm.minLength) {
vm.ngModelCtrl.$setValidity('minlength', body.length <= vm.minLength);
}
if (vm.required) {
vm.ngModelCtrl.$setValidity('required', body.length > 0);
}
}
}
angular
.module('sgCkeditor', [])
.provider('sgCkeditorConfig', sgCkeditorConfigProvider)
.component('sgCkeditor', sgCkeditorComponent);
})();

View File

@ -4,7 +4,7 @@
(function() {
'use strict';
angular.module('SOGo.ContactsUI', ['ngCookies', 'ui.router', 'angularFileUpload', 'ck', 'SOGo.Common', 'SOGo.PreferencesUI', 'SOGo.MailerUI'])
angular.module('SOGo.ContactsUI', ['ngCookies', 'ui.router', 'angularFileUpload', 'sgCkeditor', 'SOGo.Common', 'SOGo.PreferencesUI', 'SOGo.MailerUI'])
.config(configure)
.run(runBlock);

View File

@ -4,7 +4,7 @@
(function() {
'use strict';
angular.module('SOGo.MailerUI', ['ngCookies', 'ui.router', 'ck', 'angularFileUpload', 'SOGo.Common', 'SOGo.ContactsUI', 'SOGo.SchedulerUI', 'ngAnimate', 'SOGo.PreferencesUI'])
angular.module('SOGo.MailerUI', ['ngCookies', 'ui.router', 'sgCkeditor', 'angularFileUpload', 'SOGo.Common', 'SOGo.ContactsUI', 'SOGo.SchedulerUI', 'ngAnimate', 'SOGo.PreferencesUI'])
.config(configure)
.run(runBlock);

View File

@ -4,7 +4,7 @@
(function() {
'use strict';
angular.module('SOGo.MailerUI', ['ngCookies', 'ui.router', 'ck', 'angularFileUpload', 'SOGo.Common', 'SOGo.ContactsUI', 'SOGo.SchedulerUI', 'ngAnimate', 'SOGo.PreferencesUI'])
angular.module('SOGo.MailerUI', ['ngCookies', 'ui.router', 'sgCkeditor', 'angularFileUpload', 'SOGo.Common', 'SOGo.ContactsUI', 'SOGo.SchedulerUI', 'ngAnimate', 'SOGo.PreferencesUI'])
.config(configure)
.run(runBlock)
.controller('MessageEditorControllerPopup', MessageEditorControllerPopup);

View File

@ -40,8 +40,12 @@
if (Preferences.defaults.SOGoMailAutoSave)
// Enable auto-save of draft
this.autosave = $timeout(this.autosaveDrafts, Preferences.defaults.SOGoMailAutoSave*1000*60);
// Set the locale of CKEditor
this.localeCode = Preferences.defaults.LocaleCode;
this.ckConfig = { language: Preferences.defaults.LocaleCode };
this.composeType = Preferences.defaults.SOGoMailComposeMessageType;
this.replyPlacement = Preferences.defaults.SOGoMailReplyPlacement;
if (this.message.origin && this.message.origin.action == 'forward') {
@ -415,14 +419,21 @@
}
};
this.onHTMLFocus = function ($event) {
var caretAtTop = (this.replyPlacement == 'above');
this.onHTMLReady = function ($editor) {
if (!this.isNew()) {
onCompletePromise().then(function() {
$editor.focus();
});
}
};
this.onHTMLFocus = function (editor) {
if (this.firstFocus) {
onCompletePromise().then(function(element) {
var selected = $event.editor.getSelection(),
var caretAtTop = (vm.replyPlacement == 'above'),
selected = editor.getSelection(),
selected_ranges = selected.getRanges(),
children = $event.editor.document.getBody().getChildren(),
children = editor.document.getBody().getChildren(),
node;
if (caretAtTop) {

View File

@ -23,6 +23,13 @@
vm.cancel = cancel;
vm.save = save;
vm.hostnameRE = accountId > 0 ? /^(?!(127\.0\.0\.1|localhost(?:\.localdomain)?)$)/ : /./;
vm.ckConfig = {
'autoGrow_minHeight': 70,
'toolbar': [['Bold', 'Italic', '-', 'Link',
'Font','FontSize','-','TextColor',
'BGColor', 'Source']],
language: defaults.LocaleCode
};
if (!vm.account.encryption)
vm.account.encryption = "none";

View File

@ -4,7 +4,7 @@
(function() {
'use strict';
angular.module('SOGo.PreferencesUI', ['ui.router', 'ck', 'angularFileUpload', 'SOGo.Common', 'SOGo.MailerUI', 'SOGo.ContactsUI', 'SOGo.Authentication', 'as.sortable'])
angular.module('SOGo.PreferencesUI', ['ui.router', 'sgCkeditor', 'angularFileUpload', 'SOGo.Common', 'SOGo.MailerUI', 'SOGo.ContactsUI', 'SOGo.Authentication', 'as.sortable'])
.config(configure)
.run(runBlock);

View File

@ -4,7 +4,7 @@
(function() {
'use strict';
angular.module('SOGo.SchedulerUI', ['ngCookies', 'ui.router', 'angularFileUpload', 'ck', 'SOGo.Common', 'SOGo.PreferencesUI', 'SOGo.ContactsUI', 'SOGo.MailerUI', 'as.sortable'])
angular.module('SOGo.SchedulerUI', ['ngCookies', 'ui.router', 'angularFileUpload', 'sgCkeditor', 'SOGo.Common', 'SOGo.PreferencesUI', 'SOGo.ContactsUI', 'SOGo.MailerUI', 'as.sortable'])
.config(configure)
.run(runBlock);

View File

@ -1,132 +0,0 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* JavaScript for CKEditor module */
(function() {
'use strict';
ckEditor.$inject = ['$parse'];
function ckEditor($parse) {
var calledEarly, loaded;
loaded = false;
calledEarly = false;
return {
restrict: 'C',
require: '?ngModel',
compile: function(element, attributes, transclude) {
var loadIt, local;
local = this;
loadIt = function() {
return calledEarly = true;
};
element.ready(function() {
return loadIt();
});
return {
post: function($scope, element, attributes, controller) {
if (calledEarly) {
return local.link($scope, element, attributes, controller);
}
loadIt = (function($scope, element, attributes, controller) {
return function() {
local.link($scope, element, attributes, controller);
};
})($scope, element, attributes, controller);
}
};
},
link: function($scope, elm, attr, ngModel) {
var ck, options = {}, locale, margin;
if (!ngModel) {
return;
}
if (calledEarly && !loaded) {
return loaded = true;
}
loaded = false;
if (attr.ckOptions)
options = angular.fromJson(attr.ckOptions.replace(/'/g, "\""));
if (attr.ckLocale) {
locale = $parse(attr.ckLocale)($scope);
options.language = locale;
// Pickup the first matching language supported by SCAYT
// See http://docs.ckeditor.com/#!/guide/dev_howtos_scayt
options.scayt_sLang = _.find(['en_US', 'en_GB', 'pt_BR', 'da_DK', 'nl_NL', 'en_CA', 'fi_FI', 'fr_FR', 'fr_CA', 'de_DE', 'el_GR', 'it_IT', 'nb_NO', 'pt_PT', 'es_ES', 'sv_SE'], function(sLang) {
return sLang.lastIndexOf(locale, 0) == 0;
}) || 'en_US';
// Disable caching of the language
// See https://github.com/WebSpellChecker/ckeditor-plugin-scayt/issues/126
options.scayt_disableOptionsStorage = 'lang';
}
if (attr.ckMargin) {
// Set the margin of the iframe editable content
margin = attr.ckMargin;
CKEDITOR.addCss('.cke_editable { margin-top: ' + margin +
'; margin-left: ' + margin +
'; margin-right: ' + margin + '; }');
}
// The Upload Image plugin requires a remote URL to be defined even though we won't use it
options.imageUploadUrl = '/SOGo/';
ck = CKEDITOR.replace(elm[0], options);
if (attr.mdAutofocus && $parse(attr.mdAutofocus)($scope)) {
// Autofocus is enabled
ck.on('instanceReady', function(event) {
ck.focus();
});
ck.on('focus', function(event) {
if (attr.ckFocus) {
$parse(attr.ckFocus)($scope, {'$event': event});
}
});
}
// Update the model whenever the content changes
ck.on('change', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ck.on('paste', function(event) {
var html;
if (event.data.type == 'html') {
html = event.data.dataValue;
// Remove images to avoid ghost image in Firefox; images will be handled by the Image Upload plugin
event.data.dataValue = html.replace(/<img( [^>]*)?>/gi, '');
}
});
// Intercept the request when an image is pasted, keep an inline base64 version only.
ck.on('fileUploadRequest', function(event) {
var data, img;
data = event.data.fileLoader.data;
img = ck.document.createElement('img');
img.setAttribute('src', data);
ck.insertElement(img);
event.cancel();
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
}
};
}
angular
.module('ck', [])
.directive('ckEditor', ckEditor);
})();