angular-froala
Version:
Angular.js bindings for Froala WYSIWYG HTML rich text editor
242 lines (208 loc) • 8.07 kB
JavaScript
(function(window, angular, undefined) {
'use strict';
angular.module('froala', [])
.value('froalaConfig', {})
.directive('froala', ['froalaConfig', function(froalaConfig) {
"use strict"; //Scope strict mode to only this directive
var generatedIds = 0;
var defaultConfig = {
immediateAngularModelUpdate: false,
angularIgnoreAttrs: null
};
var innerHtmlAttr = 'innerHTML';
var scope = {
froalaOptions: '=froala',
initFunction: '&froalaInit'
};
froalaConfig = froalaConfig || {};
// Constants
var MANUAL = "manual";
var AUTOMATIC = "automatic";
var SPECIAL_TAGS = ['img', 'button', 'input', 'a'];
return {
restrict: 'A',
require: 'ngModel',
scope: scope,
link: function(scope, element, attrs, ngModel) {
// Create a blur event to update the data of ngModel
element.on('blur', function () {
ngModel.$setViewValue(element[0].innerHTML)
});
var specialTag = false;
if (SPECIAL_TAGS.indexOf(element.prop("tagName").toLowerCase()) != -1) {
specialTag = true;
}
var ctrl = {
editorInitialized: false
};
var firstTime = false;
scope.initMode = attrs.froalaInit ? MANUAL : AUTOMATIC;
ctrl.init = function() {
if (!attrs.id) {
// generate an ID if not present
attrs.$set('id', 'froala-' + generatedIds++);
}
//init the editor
if (scope.initMode === AUTOMATIC) {
ctrl.createEditor();
}
//Instruct ngModel how to update the froala editor
ngModel.$render = function () {
// Update special tags.
if (specialTag) {
var tags = ngModel.$modelValue;
// add tags on element
if (tags) {
for (var attr in tags) {
if (tags.hasOwnProperty(attr) && attr != innerHtmlAttr) {
element.attr(attr, tags[attr]);
}
}
if (tags.hasOwnProperty(innerHtmlAttr)) {
element[0].innerHTML = tags[innerHtmlAttr];
}
}
}
else {
if (ctrl.editorInitialized) {
// Set HTML.
ctrl.froalaEditor.html.set(ngModel.$viewValue || '')
//This will reset the undo stack everytime the model changes externally. Can we fix this?
ctrl.froalaEditor.undo.reset();
ctrl.froalaEditor.undo.saveStep();
}
}
};
ngModel.$isEmpty = function(value) {
if (!value) {
return true;
}
return true;
};
};
ctrl.createEditor = function(froalaInitOptions) {
if (!ctrl.editorInitialized) {
froalaInitOptions = (froalaInitOptions || {});
ctrl.options = angular.extend({}, defaultConfig, froalaConfig, scope.froalaOptions, froalaInitOptions);
ctrl.registerEventsWithCallbacks('initializationDelayed', function() {
ngModel.$render()
});
ctrl.registerEventsWithCallbacks('initialized', ctrl.options.events && ctrl.options.events.initialized)
var initEvent = ctrl.options.events.initialized;
if (!ctrl.options.events) ctrl.options.events = {};
ctrl.options.events.initialized = function () {
initEvent && initEvent();
ctrl.initListeners();
ctrl.editorInitialized = true;
ngModel.$render()
}
ctrl.froalaEditor = new FroalaEditor(element[0], ctrl.options);
//assign the froala instance to the options object to make methods available in parent scope
if (scope.froalaOptions) {
scope.froalaOptions.froalaEditor = ctrl.froalaEditor;
}
}
};
ctrl.initListeners = function() {
if (ctrl.options.immediateAngularModelUpdate) {
ctrl.froalaEditor.events.on('keyup', function() {
scope.$evalAsync(ctrl.updateModelView);
});
}
ctrl.froalaEditor.events.on('contentChanged', function() {
scope.$evalAsync(ctrl.updateModelView);
});
if (ctrl.initEvents) {
for (var i = 0; i < ctrl.initEvents; i++) {
ctrl.initEvents[i].call(ctrl.froalaEditor);
}
}
element.bind('$destroy', function() {
if (element) {
ctrl.froalaEditor.destroy();
element = null;
}
});
};
ctrl.updateModelView = function() {
if (!element) {
return;
}
var modelContent = null;
if (specialTag) {
var attributeNodes = element[0].attributes;
var attrs = {};
for (var i = 0; i < attributeNodes.length; i++) {
var attrName = attributeNodes[i].name;
if (ctrl.options.angularIgnoreAttrs && ctrl.options.angularIgnoreAttrs.indexOf(attrName) != -1) {
continue;
}
attrs[attrName] = attributeNodes[i].value;
}
if (element[0].innerHTML) {
attrs[innerHtmlAttr] = element[0].innerHTML;
}
modelContent = attrs;
} else {
var returnedHtml = ctrl.froalaEditor.html.get();
if (angular.isString(returnedHtml)) {
modelContent = returnedHtml;
}
}
ngModel.$setViewValue(modelContent);
if (!scope.$root.$$phase) {
scope.$apply();
}
};
ctrl.registerEventsWithCallbacks = function(eventName, callback) {
if (eventName && callback) {
if(!ctrl.options.events){
ctrl.options.events = {};
}
if (eventName == 'initialized') {
if (!ctrl.initEvents) ctrl.initEvents = [];
ctrl.initEvents.push(callback);
}
else {
ctrl.options.events[eventName] = callback;
}
}
};
if (scope.initMode === MANUAL) {
var _ctrl = ctrl;
var controls = {
initialize: ctrl.createEditor,
destroy: function() {
if (_ctrl.froalaEditor) {
_ctrl.froalaEditor.destroy();
_ctrl.editorInitialized = false;
}
},
getEditor: function() {
return _ctrl.froalaEditor ? _ctrl.froalaEditor : null;
}
};
scope.initFunction({
initControls: controls
});
}
ctrl.init();
}
};
}])
.directive('froalaView', ['$sce', function($sce) {
return {
restrict: 'ACM',
scope: false,
link: function(scope, element, attrs) {
element.addClass('fr-view');
scope.$watch(attrs.froalaView, function(nv) {
if (nv || nv === '') {
var explicitlyTrustedValue = $sce.trustAsHtml(nv);
element.html(explicitlyTrustedValue.toString());
}
});
}
};
}]);
})(window, window.angular);