6

I've read this excellent answer to pretty much the same question. However, I have tried every technique that @Reinmar recommended, and none of them seem to work.

The situation is that I am taking the current HTML from the editor and wrapping certain pieces in span tags. I then set the now modified HTML back and try to restore the user's cursor location. No technique works.

Here is a very simple example to reproduce the issue:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="//cdn.ckeditor.com/4.4.7/standard/ckeditor.js"></script>

</head>
<body>
    <textarea id="cktest"><p>Sometimes Lorem. Sometime Ipsum. Always dolor.</p></textarea>

    <script type="text/javascript">

        (function () {
            var checkTimeout;
            var bookmark;

            var storeCursorLocation = function(editor) {
                bookmark = editor.getSelection().createBookmarks();
            };

            var restoreCursorLocation = function(editor) {
                editor.getSelection().selectBookmarks(bookmark);
            };

            var validateText = function(editor) {
                storeCursorLocation(editor);
                var data = editor.document.getBody().getHtml();
                data = data.replace("Lorem", "<span class='err-item'>Lorem</span>");
                editor.document.getBody().setHtml(data);
                restoreCursorLocation(editor);
            };


            CKEDITOR.replace('cktest', {
                on: {
                    'instanceReady': function(evt) {

                    },
                    'key' : function(evt) {
                        clearTimeout(checkTimeout);
                        checkTimeout = setTimeout(function () {
                            validateText(evt.editor);
                        }, 1000);
                    }
                }
            });
        })();

    </script>
</body>
</html>

This code starts a timer when a user presses a key, and then waits for 1 second after they stop pressing keys to do the check.

Copy this to a new .html file and run it in your favorite browser (I am using Chrome).

When the CKEditor loads, use the mouse to place your cursor somewhere in the middle of the text. Then press the CTRL key and wait 1 second. You will see your cursor jump back to the start of the text.

This code example uses

editor.getSelection().createBookmarks();

to create the bookmark. But I have also tried:

editor.getSelection().createBookmarks(true);

and

editor.getSelection().createBookmarks2();

I have also tried just saving the range using

var ranges = editor.getSelection().getRanges();

and

editor.getSelection().selectRanges(ranges);

in the restoreCursorLocation function.

Community
  • 1
  • 1
CleverPatrick
  • 9,261
  • 5
  • 63
  • 86

2 Answers2

6
        (function () {
            var checkTimeout;
            var bookmark;

            var storeCursorLocation = function( editor ) {
               bookmark = editor.getSelection().createBookmarks( true );
            };

            var restoreCursorLocation = function( editor ) {
                //editor.focus();
                editor.getSelection().selectBookmarks( bookmark );
            };

            var validateText = function( editor ) {
                storeCursorLocation( editor );

                var data = editor.document.getBody().getHtml();
                data = data.replace( "spaceflight", "<span class='err-item'>spaceflight</span>" );
                editor.document.getBody().setHtml( data );
                restoreCursorLocation( editor );
                //fire this event after DOM changes if working with widgets
                //editor.fire( 'contentDomInvalidated' ); 
            };


           var editor = CKEDITOR.replace( 'editor1', {
                extraAllowedContent : 'span(err-item)',             
                on: {
                    "pluginsLoaded" : function( event ){
                        editor.on( 'contentDom', function() {
                            var editable = editor.editable();                   
                            editable.attachListener( editable, 'keyup', function( e ) { 
                                clearTimeout( checkTimeout );
                                checkTimeout = setTimeout(function () {
                                    validateText( editor );
                                }, 100 );
                            });
                        });
                    }
                }
            });
        })();

I have checked your code, made some corrections and the above seems to work fine. I know you said you have tried it but for me createBookmarks(true) has done the trick.

Explanations and Notes:

  1. You needed to use createBookmarks(true) which inserts unique span into HTML. Such bookmark is not affected by changes you are doing inside the DOM (there are limits of course e.g. your custom changes remove bookmark).
  2. It was clever to use getBody().getHtml() and getBody().setHTML(). If you have used editor.getData() this would have removed empty spans that represent bookmarks. Please note however that such approach may break widgets so it is required to fire contentDomInvalidated event after such changes.
  3. I was also focusing editor before restoring selection but this is “just in case” solution, as I have noticed that editor selects bookmark without it. If however, for some reason, you are losing the selection, this would be another thing to use.

Here you have working example: http://jsfiddle.net/j_swiderski/nwbsywnn/1/

Reinmar
  • 21,729
  • 4
  • 67
  • 78
j.swiderski
  • 2,405
  • 2
  • 12
  • 20
  • Just to add to this answer - a much safer solution would be to use [`CKEDITOR.style`](http://docs.ckeditor.com/#!/api/CKEDITOR.style), because it would touch only these parts of the tree that must be changed. However, to use it you would need to implement a function looking for a text in the DOM, which is a pretty complex thing. – Reinmar Apr 29 '15 at 13:18
  • Thanks for the excellent answer, Reinmar. In the real code, I am using CKEDITOR.htmlParser to go through the document and make modifications. Not just doing a simple .replace(). I'll look at using styles and see if that works any better. Not sure why createBookmark(true) wasn't working for me before. But it is now. – CleverPatrick Apr 29 '15 at 13:55
1

Check the default behaviour when you set innerHtml in https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML

Removes all of element's children, parses the content string and assigns the resulting nodes as children of the element

The bookmarks in CKEDITOR are hidden span elements and setting innerHtml will remove all those elements.

Anyway the solution is very simple.

Change your storeCursorLocation function to this

var storeCursorLocation = function(editor) {
    bookmark = editor.getSelection().createBookmarks(true);
};

When you pass true as parameters it will use the ids as the reference instead of storing the DOM elements so you can restore then after an innerHtml change.

{Edit}

Reading Solution 2 from @Reinmar he says

If you can avoid uncontrolled innerHTML changes and instead append/remove/move some nodes, then just remember that you have to preserve these elements and this method will work perfectly. You can also move bookmarks' elements if your modifications should change the selection as well.

This is how you do it if you can't replace the contents of the element innerHtml.

This solution is less efficient but might work in some scenarios

Change the validateText function to this.

var validateText = function(editor) {
    storeCursorLocation(editor);
    var parent = editor.document.getBody().$.firstChild,
        nodes = parent.childNodes,
        nodeText,
        words,
        index = 0,
        current,
        newElement;
    while (index < nodes.length) {
        current = nodes[index];
        nodeText = current.nodeValue;
        if (current.nodeType === Node.TEXT_NODE && nodeText.indexOf('Lorem') !== -1) {
            words = nodeText.split('Lorem');
            newElement = document.createTextNode(words[0]);
            parent.insertBefore(newElement, current);
            newElement = document.createTextNode(words[1]);
            parent.insertBefore(newElement, current.nextSibling);
            newElement = document.createElement('span')
            newElement.className = 'err-item';
            newElement.innerHTML = 'Lorem';
            parent.replaceChild(newElement, current);
            break;
        }
        index++;

    }
    restoreCursorLocation(editor);
};

Basically I'm transversing the nodes of the first p in the chkeditor body and replacing only the node of type text that contains Lorem with a span and add the remaining text before and after as text elements. If you replace the whole text like you were doing it will remove from the DOM the bookmarks so when you tried to restore they don't exist.

Community
  • 1
  • 1
devconcept
  • 3,665
  • 1
  • 26
  • 40
  • In the real code I am using the CKEDITOR.htmlParser to do something similar to your second solution. It is, indeed, better than just using .replace(). But .replace() was far less complex for the example, and how the replacement happened wasn't the issue. – CleverPatrick Apr 29 '15 at 13:57
  • 1
    With the first solution you can use replace. The bookmarks are saved using ids and restored. I added the second solution because there are some scenarios in wich using replace is harder than replace a single text element. It's an example like you said and I personally preffer to give more data to be more helpful with the real problem. – devconcept Apr 29 '15 at 14:08