Files
Umbraco-CMS/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
2017-11-29 12:16:12 +01:00

385 lines
21 KiB
JavaScript

angular.module("umbraco.directives")
.directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) {
return {
scope: {
uniqueId: '=',
value: '=',
onClick: '&',
onFocus: '&',
onBlur: '&',
configuration:"=",
onMediaPickerClick: "=",
onEmbedClick: "=",
onMacroPickerClick: "=",
onLinkPickerClick: "="
},
template: "<textarea ng-model=\"value\" rows=\"10\" class=\"mceNoEditor\" style=\"overflow:hidden\" id=\"{{uniqueId}}\"></textarea>",
replace: true,
link: function (scope, element, attrs) {
var initTiny = function () {
//we always fetch the default one, and then override parts with our own
tinyMceService.configuration().then(function (tinyMceConfig) {
//config value from general tinymce.config file
var validElements = tinyMceConfig.validElements;
var fallbackStyles = [{title: "Page header", block: "h2"}, {title: "Section header", block: "h3"}, {title: "Paragraph header", block: "h4"}, {title: "Normal", block: "p"}, {title: "Quote", block: "blockquote"}, {title: "Code", block: "code"}];
//These are absolutely required in order for the macros to render inline
//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]";
var invalidElements = tinyMceConfig.inValidElements;
var plugins = _.map(tinyMceConfig.plugins, function (plugin) {
if (plugin.useOnFrontend) {
return plugin.name;
}
}).join(" ") + " autoresize";
//config value on the data type
var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | ");
var stylesheets = [];
var styleFormats = [];
var await = [];
//queue file loading
if (typeof (tinymce) === "undefined") {
await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope));
}
if(scope.configuration && scope.configuration.toolbar){
toolbar = scope.configuration.toolbar.join(' | ');
}
if(scope.configuration && scope.configuration.stylesheets){
angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){
stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css");
await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) {
angular.forEach(rules, function (rule) {
var r = {};
var split = "";
r.title = rule.name;
if (rule.selector[0] === ".") {
r.inline = "span";
r.classes = rule.selector.substring(1);
}else if (rule.selector[0] === "#") {
//Even though this will render in the style drop down, it will not actually be applied
// to the elements, don't think TinyMCE even supports this and it doesn't really make much sense
// since only one element can have one id.
r.inline = "span";
r.attributes = { id: rule.selector.substring(1) };
}else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) {
split = rule.selector.split(".");
r.block = split[0];
r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " ");
}else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) {
split = rule.selector.split("#");
r.block = split[0];
r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1);
}else {
r.block = rule.selector;
}
styleFormats.push(r);
});
}));
});
}else{
stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css");
styleFormats = fallbackStyles;
}
//stores a reference to the editor
var tinyMceEditor = null;
$q.all(await).then(function () {
var uniqueId = scope.uniqueId;
//create a baseline Config to exten upon
var baseLineConfigObj = {
mode: "exact",
skin: "umbraco",
plugins: plugins,
valid_elements: validElements,
invalid_elements: invalidElements,
extended_valid_elements: extendedValidElements,
menubar: false,
statusbar: false,
relative_urls: false,
toolbar: toolbar,
content_css: stylesheets,
style_formats: styleFormats,
autoresize_bottom_margin: 0
};
if (tinyMceConfig.customConfig) {
//if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to
// convert it to json instead of having it as a string since this is what tinymce requires
for (var i in tinyMceConfig.customConfig) {
var val = tinyMceConfig.customConfig[i];
if (val) {
val = val.toString().trim();
if (val.detectIsJson()) {
try {
tinyMceConfig.customConfig[i] = JSON.parse(val);
//now we need to check if this custom config key is defined in our baseline, if it is we don't want to
//overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise
//if it's an object it will overwrite the baseline
if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) {
//concat it and below this concat'd array will overwrite the baseline in angular.extend
tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]);
}
}
catch (e) {
//cannot parse, we'll just leave it
}
}
}
}
angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
}
//set all the things that user configs should not be able to override
baseLineConfigObj.elements = uniqueId;
baseLineConfigObj.setup = function (editor) {
//set the reference
tinyMceEditor = editor;
//enable browser based spell checking
editor.on('init', function (e) {
editor.getBody().setAttribute('spellcheck', true);
//force overflow to hidden to prevent no needed scroll
editor.getBody().style.overflow = "hidden";
$timeout(function(){
if(scope.value === null){
editor.focus();
}
}, 400);
});
// pin toolbar to top of screen if we have focus and it scrolls off the screen
var pinToolbar = function () {
var _toolbar = $(editor.editorContainer).find(".mce-toolbar");
var toolbarHeight = _toolbar.height();
var _tinyMce = $(editor.editorContainer);
var tinyMceRect = _tinyMce[0].getBoundingClientRect();
var tinyMceTop = tinyMceRect.top;
var tinyMceBottom = tinyMceRect.bottom;
var tinyMceWidth = tinyMceRect.width;
var _tinyMceEditArea = _tinyMce.find(".mce-edit-area");
// set padding in top of mce so the content does not "jump" up
_tinyMceEditArea.css("padding-top", toolbarHeight);
if (tinyMceTop < 160 && ((160 + toolbarHeight) < tinyMceBottom)) {
_toolbar
.css("visibility", "visible")
.css("position", "fixed")
.css("top", "160px")
.css("margin-top", "0")
.css("width", tinyMceWidth);
} else {
_toolbar
.css("visibility", "visible")
.css("position", "absolute")
.css("top", "auto")
.css("margin-top", "0")
.css("width", tinyMceWidth);
}
};
// unpin toolbar to top of screen
var unpinToolbar = function() {
var _toolbar = $(editor.editorContainer).find(".mce-toolbar");
var _tinyMce = $(editor.editorContainer);
var _tinyMceEditArea = _tinyMce.find(".mce-edit-area");
// reset padding in top of mce so the content does not "jump" up
_tinyMceEditArea.css("padding-top", "0");
_toolbar.css("position", "static");
};
//when we leave the editor (maybe)
editor.on('blur', function (e) {
editor.save();
angularHelper.safeApply(scope, function () {
scope.value = editor.getContent();
var _toolbar = $(editor.editorContainer)
.find(".mce-toolbar");
if(scope.onBlur){
scope.onBlur();
}
unpinToolbar();
$('.umb-panel-body').off('scroll', pinToolbar);
});
});
// Focus on editor
editor.on('focus', function (e) {
angularHelper.safeApply(scope, function () {
if(scope.onFocus){
scope.onFocus();
}
pinToolbar();
$('.umb-panel-body').on('scroll', pinToolbar);
});
});
// Click on editor
editor.on('click', function (e) {
angularHelper.safeApply(scope, function () {
if(scope.onClick){
scope.onClick();
}
pinToolbar();
$('.umb-panel-body').on('scroll', pinToolbar);
});
});
//when buttons modify content
editor.on('ExecCommand', function (e) {
editor.save();
angularHelper.safeApply(scope, function () {
scope.value = editor.getContent();
});
});
// Update model on keypress
editor.on('KeyUp', function (e) {
editor.save();
angularHelper.safeApply(scope, function () {
scope.value = editor.getContent();
});
});
// Update model on change, i.e. copy/pasted text, plugins altering content
editor.on('SetContent', function (e) {
if (!e.initial) {
editor.save();
angularHelper.safeApply(scope, function () {
scope.value = editor.getContent();
});
}
});
editor.on('ObjectResized', function (e) {
var qs = "?width=" + e.width + "&height=" + e.height;
var srcAttr = $(e.target).attr("src");
var path = srcAttr.split("?")[0];
$(e.target).attr("data-mce-src", path + qs);
});
//Create the insert link plugin
tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){
if(scope.onLinkPickerClick) {
scope.onLinkPickerClick(editor, currentTarget, anchorElement);
}
});
//Create the insert media plugin
tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){
if(scope.onMediaPickerClick) {
scope.onMediaPickerClick(editor, currentTarget, userData);
}
});
//Create the embedded plugin
tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){
if(scope.onEmbedClick) {
scope.onEmbedClick(editor);
}
});
//Create the insert macro plugin
tinyMceService.createInsertMacro(editor, scope, function(dialogData){
if(scope.onMacroPickerClick) {
scope.onMacroPickerClick(editor, dialogData);
}
});
};
/** Loads in the editor */
function loadTinyMce() {
//we need to add a timeout here, to force a redraw so TinyMCE can find
//the elements needed
$timeout(function () {
tinymce.DOM.events.domLoaded = true;
tinymce.init(baseLineConfigObj);
}, 150, false);
}
loadTinyMce();
//here we declare a special method which will be called whenever the value has changed from the server
//this is instead of doing a watch on the model.value = faster
//scope.model.onValueChanged = function (newVal, oldVal) {
// //update the display val again if it has changed from the server;
// tinyMceEditor.setContent(newVal, { format: 'raw' });
// //we need to manually fire this event since it is only ever fired based on loading from the DOM, this
// // is required for our plugins listening to this event to execute
// tinyMceEditor.fire('LoadContent', null);
//};
//listen for formSubmitting event (the result is callback used to remove the event subscription)
var unsubscribe = scope.$on("formSubmitting", function () {
//TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer
// we do parse it out on the server side but would be nice to do that on the client side before as well.
scope.value = tinyMceEditor ? tinyMceEditor.getContent() : null;
});
//when the element is disposed we need to unsubscribe!
// NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
// element might still be there even after the modal has been hidden.
scope.$on('$destroy', function () {
unsubscribe();
if (tinyMceEditor !== undefined && tinyMceEditor != null) {
tinyMceEditor.destroy()
}
});
});
});
};
initTiny();
}
};
});