Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Matt Brailsford 4125 posts 22223 karma points MVP 9x c-trib
    Apr 17, 2014 @ 15:23
    Matt Brailsford
    0

    tinyMceService weirdness

    Hey Guys,

    I'm working on a package that can instantiate multiple RTE's at once. I want to be able to style / control what the RTE can do, so I thought I'd use the tinyMceService to instantiate them.

    So, I've created a custom directive that has a textarea as it's template with a unique ID set, then have the following code in my "link" function for the directive:

    tinyMceService.configuration().then(function (tinyMceConfig) {
    
    //config value from general tinymce.config file
    var validElements = tinyMceConfig.validElements;
    
    //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]";
    
    var invalidElements = tinyMceConfig.inValidElements;
    var plugins = _.map(tinyMceConfig.plugins, function (plugin) {
        if (plugin.useOnFrontend) {
            return plugin.name;
        }
    }).join(" ");
    
    //var editorConfig = $scope.model.config.editor;
    //if (!editorConfig || angular.isString(editorConfig)) {
        editorConfig = tinyMceService.defaultPrevalues();
    //}
    
    //config value on the data type
    var toolbar = editorConfig.toolbar.join(" | ");
    var stylesheets = [];
    var styleFormats = [];
    var await = [];
    
    //queue file loading
    await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope));
    
    //queue rules loading
    angular.forEach(editorConfig.stylesheets, function (val, key) {
        stylesheets.push("../css/" + val + ".css?" + new Date().getTime());
    
        await.push(stylesheetResource.getRulesByName(val).then(function (rules) {
            angular.forEach(rules, function (rule) {
                var r = {};
                r.title = rule.name;
                if (rule.selector[0] == ".") {
                    r.inline = "span";
                    r.classes = rule.selector.substring(1);
                }
                else if (rule.selector[0] == "#") {
                    r.inline = "span";
                    r.attributes = { id: rule.selector.substring(1) };
                }
                else {
                    r.block = rule.selector;
                }
    
                styleFormats.push(r);
            });
        }));
    });
    
    
    //stores a reference to the editor
    var tinyMceEditor = null;
    
    //wait for queue to end
    $q.all(await).then(function () {
    
    
        //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,
            height: 200,
            width: "100%",
            toolbar: toolbar,
            content_css: stylesheets.join(','),
            relative_urls: false,
            style_formats: styleFormats,
        };
    
    
        if (tinyMceConfig.customConfig) {
            angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
        }
    
        //console.log(baseLineConfigObj);
    
        //set all the things that user configs should not be able to override
        baseLineConfigObj.elements = "rte_" + $scope.guid;
        baseLineConfigObj.setup = function (editor) {
    
            //set the reference
            tinyMceEditor = editor;
    
            //enable browser based spell checking
            editor.on('init', function (e) {
                editor.getBody().setAttribute('spellcheck', true);
            });
    
            //We need to listen on multiple things here because of the nature of tinymce, it doesn't 
            //fire events when you think!
            //The change event doesn't fire when content changes, only when cursor points are changed and undo points
            //are created. the blur event doesn't fire if you insert content into the editor with a button and then 
            //press save. 
            //We have a couple of options, one is to do a set timeout and check for isDirty on the editor, or we can 
            //listen to both change and blur and also on our own 'saving' event. I think this will be best because a 
            //timer might end up using unwanted cpu and we'd still have to listen to our saving event in case they clicked
            //save before the timeout elapsed.
            editor.on('change', function (e) {
                angularHelper.safeApply($scope, function () {
                    $scope.model.value = editor.getContent();
                });
            });
    
            editor.on('blur', function (e) {
                angularHelper.safeApply($scope, function () {
                    $scope.model.value = editor.getContent();
                });
            });
    
            //Create the insert media plugin
            tinyMceService.createMediaPicker(editor, $scope);
    
            //Create the embedded plugin
            tinyMceService.createInsertEmbeddedMedia(editor, $scope);
    
            //Create the insert link plugin
            tinyMceService.createLinkPicker(editor, $scope);
    
            //Create the insert macro plugin
            tinyMceService.createInsertMacro(editor, $scope);
        };
    
        /** 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);
            }, 200, 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.model.value = tinyMceEditor.getContent();
        });
    
        //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();
        });
    });
    });
    

    As you can see, this is pretty much a straight copy from the RTEController at the moment.

    When I use this code, it sort of works. The RTE is created, but there is a weird bug happening.

    On occasions, when I create the second RTE, the toolbar is attached to the correct container, but the iframe containing the editor is being attached to the iframe container of the first rte. Am I missing something as to why this would be happening? like I say, sometimes it works, and sometimes it doesn't and I can't figure out why.

    The textareas definitely have unique id's so that can't be it, but I've poked around a bit and just can't find why it's not working.

    Any suggestions greatly appreciated.

    Matt

  • Antoine 176 posts 1494 karma points
    Apr 18, 2014 @ 15:20
    Antoine
    0

    Hi Matt,

    It is quite strange, I remember having the same issue and we had to create unique Ids for each textarea as well. Are you sure that $scope.guid refer to the textarea and not to the property editor himself?

  • Matt Brailsford 4125 posts 22223 karma points MVP 9x c-trib
    Apr 18, 2014 @ 16:14
    Matt Brailsford
    101

    Hi Antoine,

    After poking around for a while, I think I've found the culprit. The issue is where it loads in the tinymce scripts, basically there are no checks performed to see if the tinymce script has already been loaded (likewise for the css etc) so when you queue up the scripts to load, it just outright reloads them. The problem is, tinymce maintains an internal count for issuing unique ids so my reloading the scripts you are resetting the counter and you get into a situation where it starts to reuse ids and therefor starts attaching things to other containers.

    Really, I think we need (my package, and the core) to have the await registration do some checks first, ie:

    if (typeof tinymce === "undefined") {
        // Add tinymce to await list
    }
    

    Or, modify the assetService or such to only loads scripts once (maybe with an override to force it if that's really what you want).

    I'm off to raise this as an issue :)

    Matt

  • Matt Brailsford 4125 posts 22223 karma points MVP 9x c-trib
    Apr 18, 2014 @ 16:19
  • Comment author was deleted

    Apr 18, 2014 @ 16:24

    This is great, it's giving ideas for Archetype's issues.

  • Antoine 176 posts 1494 karma points
    Apr 30, 2014 @ 10:11
    Antoine
    0

    I have had a look around this issue, you solution seems to be ok, but sometime the bug appears again in chrome. For now, I added a hack in this case which injects tinyMCE through yepnope directly. Not very clean but works fine. Waiting for more info from the core...

Please Sign in or register to post replies

Write your reply to:

Draft