00030ba2fa
It is useful on small screens to be able to collapse the toolbar.
419 lines
12 KiB
JavaScript
419 lines
12 KiB
JavaScript
/* -*- 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', 'colors', 'list', 'indent', 'blocks', 'align', 'links', 'insert', 'spellchecker', 'styles', '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',
|
|
|
|
// Allow the toolbar to be collapsed (useful for small screens)
|
|
toolbarCanCollapse: true,
|
|
|
|
// 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 () {
|
|
editorElement.classList.add('ng-cloak');
|
|
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
|
|
});
|
|
}
|
|
|
|
editorElement.classList.remove('ng-cloak');
|
|
vm.ngModelCtrl.$render();
|
|
}
|
|
|
|
function onEditorChange () {
|
|
var html = editor.getData();
|
|
var body = editor.document.getBody();
|
|
var text;
|
|
|
|
if (_.isEmpty(body))
|
|
return;
|
|
else
|
|
text = body.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);
|
|
})();
|