From 572e4acfb246b3d5aa5296306d7e492bc8e9f889 Mon Sep 17 00:00:00 2001
From: Marius Dumitru Florea <marius@xwiki.com>
Date: Mon, 18 Oct 2021 15:10:13 +0300
Subject: [PATCH] XWIKI-18862: Cleanup and integrate the real-time WYSIWYG
 editor (rtWysiwyg) * More cleanup (fixing JSHint reported issues)

---
 .../src/main/webjar/loader.js                 |  27 +-
 .../src/main/webjar/wysiwygEditor.js          | 726 +++++++++---------
 2 files changed, 365 insertions(+), 388 deletions(-)

diff --git a/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/loader.js b/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/loader.js
index 21c5cce1e66..530b3200355 100644
--- a/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/loader.js
+++ b/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/loader.js
@@ -19,9 +19,8 @@
  */
 define('xwiki-realtime-wysiwygEditor-loader', [
   'jquery',
-  'xwiki-realtime-loader',
-  'deferred!ckeditor'
-], function($, Loader, ckeditorPromise) {
+  'xwiki-realtime-loader'
+], function($, Loader) {
   'use strict';
 
   // TODO: Check if this is really needed.
@@ -42,33 +41,13 @@ define('xwiki-realtime-wysiwygEditor-loader', [
     compatible: ['wysiwyg', 'wiki']
   };
 
-  var waitForEditorInstance = function(name, ckeditor) {
-    var deferred = $.Deferred();
-    var editor = ckeditor.instances[name];
-    if (editor) {
-      if (editor.status === 'ready') {
-        deferred.resolve(editor);
-      } else {
-        editor.on('instanceReady', $.proxy(deferred, 'resolve', editor));
-      }
-    } else {
-      ckeditor.on('instanceReady', function(event) {
-        if (event.editor.name === name) {
-          deferred.resolve(editor);
-        }
-      });
-    }
-    return deferred.promise();
-  };
-
   Loader.bootstrap(info).done(function(keys) {
     require(['xwiki-realtime-wysiwygEditor'], function(RealtimeWysiwygEditor) {
       if (RealtimeWysiwygEditor && RealtimeWysiwygEditor.main) {
         keys._update = $.proxy(Loader, 'updateKeys', editorId);
         var config = Loader.getConfig();
         config.rtURL = Loader.getEditorURL(window.location.href, info);
-        RealtimeWysiwygEditor.main(config, keys, Loader.isRt);
-        ckeditorPromise.then($.proxy(waitForEditorInstance, null, 'content')).done(function(editor) {
+        RealtimeWysiwygEditor.main(config, keys, Loader.isRt).done(function(editor) {
           RealtimeWysiwygEditor.currentMode = editor.mode;
           if (Loader.isRt) {
             $('.cke_button__source').remove();
diff --git a/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/wysiwygEditor.js b/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/wysiwygEditor.js
index e06b908e73c..67b7da490c7 100644
--- a/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/wysiwygEditor.js
+++ b/xwiki-platform-core/xwiki-platform-realtime/xwiki-platform-realtime-wysiwyg/xwiki-platform-realtime-wysiwyg-webjar/src/main/webjar/wysiwygEditor.js
@@ -32,11 +32,12 @@ define('xwiki-realtime-wysiwygEditor', [
   'xwiki-realtime-saver',
   'chainpad',
   'xwiki-realtime-crypto',
-  'diff-dom'
+  'diff-dom',
+  'deferred!ckeditor'
 ], function (
   /* jshint maxparams:false */
   $, realtimeConfig, ErrorBox, Toolbar, ChainPadNetflux, Hyperjson, Cursor, UserData, TypingTest, JSONSortify,
-  Interface, Saver, Chainpad, Crypto, DiffDom
+  Interface, Saver, Chainpad, Crypto, DiffDom, ckeditorPromise
 ) {
   'use strict';
 
@@ -111,31 +112,46 @@ define('xwiki-realtime-wysiwygEditor', [
     return hj;
   };
 
-  var stringifyDOM = window.stringifyDOM = function (dom) {
+  var stringifyDOM = window.stringifyDOM = function(dom) {
     return JSONSortify(Hyperjson.fromDOM(dom, shouldSerialize, hjFilter));
   };
 
-  var main = module.main = function (editorConfig, docKeys, useRt) {
+  var waitForEditorInstance = function(name, ckeditor) {
+    var deferred = $.Deferred();
+    var editor = ckeditor.instances[name];
+    if (editor) {
+      if (editor.status === 'ready') {
+        deferred.resolve(editor);
+      } else {
+        editor.on('instanceReady', $.proxy(deferred, 'resolve', editor));
+      }
+    } else {
+      ckeditor.on('instanceReady', function(event) {
+        if (event.editor.name === name) {
+          deferred.resolve(editor);
+        }
+      });
+    }
+    return deferred.promise();
+  };
+
+  module.main = function(editorConfig, docKeys, useRt) {
     var WebsocketURL = editorConfig.WebsocketURL;
     var htmlConverterUrl = editorConfig.htmlConverterUrl;
     var userName = editorConfig.userName;
-    var DEMO_MODE = editorConfig.DEMO_MODE;
     var language = editorConfig.language;
     var userAvatar = editorConfig.userAvatarURL;
     var network = editorConfig.network;
     var saverConfig = editorConfig.saverConfig || {};
     saverConfig.chainpad = Chainpad;
     saverConfig.editorType = editorId;
-    saverConfig.editorName = 'Wysiwyg';
+    saverConfig.editorName = 'WYSIWYG';
     saverConfig.isHTML = true;
     saverConfig.mergeContent = true;
     var Messages = saverConfig.messages || {};
 
     saverConfig.mergeContent = realtimeConfig.enableMerge !== 0;
 
-    /** Key in the localStore which indicates realtime activity should be disallowed. */
-    var LOCALSTORAGE_DISALLOW = editorConfig.LOCALSTORAGE_DISALLOW;
-
     var channel = docKeys[editorId];
     var eventsChannel = docKeys.events;
     var userdataChannel = docKeys.userdata;
@@ -168,15 +184,13 @@ define('xwiki-realtime-wysiwygEditor', [
       Interface.createAllowRealtimeCheckbox(allowRealtimeCbId, Interface.realtimeAllowed(), Messages.allowRealtime);
 
       var $disallowButton = $('#' + allowRealtimeCbId);
-      var disallowClick = function () {
-        var checked = $disallowButton[0].checked;
-        //console.log("Value of 'allow realtime collaboration' is %s", checked);
-        if (checked || DEMO_MODE) {
+      $disallowButton.change(function() {
+        if ($disallowButton.prop('checked')) {
           Interface.realtimeAllowed(true);
-          // TODO : join the RT session without reloading the page?
+          // TODO: Join the RT session without reloading the page?
           window.location.href = editorConfig.rtURL;
         } else {
-          editorConfig.displayDisableModal(function (state) {
+          editorConfig.displayDisableModal(function(state) {
             if (!state) {
               $disallowButton.prop('checked', true);
               return;
@@ -185,8 +199,7 @@ define('xwiki-realtime-wysiwygEditor', [
             module.onAbort();
           });
         }
-      };
-      $disallowButton.on('change', disallowClick);
+      });
     }
 
     if (!useRt) {
@@ -228,33 +241,29 @@ define('xwiki-realtime-wysiwygEditor', [
     // configure Saver with the merge URL and language settings
     Saver.configure(saverConfig);
 
-    var whenReady = function (editor, iframe) {
+    var whenReady = function(editor, iframe) {
 
       var inner = window.inner = iframe.contentWindow.body;
       var innerDoc = window.innerDoc = iframe.contentWindow.document;
       var cursor = window.cursor = Cursor(inner);
       var initializing = true;
 
-      var ml = editor.plugins.magicline.backdoor ? editor.plugins.magicline.backdoor.that.line.$ :
-        editor._.magiclineBackdoor.that.line.$;
-      [ml, ml.parentElement].forEach(function (el) {
-        el.setAttribute('class', 'rt-non-realtime');
-      }); 
-      // Fix the magic line issue
-      var fixMagicLine = function () {
+      
+      // Fix the magic line issue.
+      var fixMagicLine = function() {
         if (editor.plugins.magicline) {
           var ml = editor.plugins.magicline.backdoor ? editor.plugins.magicline.backdoor.that.line.$ :
             editor._.magiclineBackdoor.that.line.$;
-          [ml, ml.parentElement].forEach(function (el) {
+          [ml, ml.parentElement].forEach(function(el) {
             el.setAttribute('class', 'rt-non-realtime');
           });
         } else {
           setTimeout(fixMagicLine, 100);
         }
-
       };
-      var afterRefresh = [];
-      // User position indicator style
+      fixMagicLine();
+
+      // User position indicator style.
       var userIconStyle = [
         '<style>',
         '.rt-user-position {',
@@ -271,7 +280,8 @@ define('xwiki-realtime-wysiwygEditor', [
           'color: #3333FF;',
           'user-select: none;',
         '}',
-        '</style>'].join('\n');
+        '</style>'
+      ].join('\n');
       var addStyle = function() {
         var iframe = jQuery('iframe')[0];
         inner = iframe.contentWindow.body;
@@ -281,28 +291,23 @@ define('xwiki-realtime-wysiwygEditor', [
       };
       addStyle();
 
-      editor.on('afterCommandExec', function(evt) {
-        if (evt && evt.data && evt.data.name && evt.data.name === "xwiki-refresh") {
+      var afterRefresh = [];
+      editor.on('afterCommandExec', function(event) {
+        if (event?.data?.name === 'xwiki-refresh') {
           initializing = false;
-          if (onLocal) { onLocal(); }
-          afterRefresh.forEach(function (el) {
-            el();
-          });
+          realtimeOptions.onLocal();
+          afterRefresh.forEach(item => item());
           afterRefresh = [];
           fixMagicLine();
         }
       });
+
       // Add the style again when modifying a macro (which reloads the iframe).
       iframe.onload = addStyle;
 
-      var setEditable = module.setEditable = function (bool) {
-        console.log('SETEDITABLE');
+      var setEditable = module.setEditable = function(bool) {
         window.inner.setAttribute('contenteditable', bool);
-        if (bool) {
-          $('.buttons [name^="action_save"], .buttons [name^="action_preview"]').removeAttr('disabled');
-        } else {
-          $('.buttons [name^="action_save"], .buttons [name^="action_preview"]').attr('disabled', 'disabled');
-        }
+        $('.buttons [name^="action_save"], .buttons [name^="action_preview"]').prop('disabled', bool);
       };
 
       // Don't let the user edit until the real-time framework is ready.
@@ -355,7 +360,7 @@ define('xwiki-realtime-wysiwygEditor', [
           (info.diff.action === 'removeAttribute' && info.diff.name === 'style'))
       ];
 
-      var diffOptions = {
+      var DD = new DiffDom({
         preDiffApply: function(info) {
           // Apply our filters.
           if (preDiffFilters.some(filter => {
@@ -407,9 +412,15 @@ define('xwiki-realtime-wysiwygEditor', [
           var cursor = window.cursor;
           if (info.frame) {
             if (info.node) {
-              if (info.frame & 1) { cursor.fixStart(info.node); }
-              if (info.frame & 2) { cursor.fixEnd(info.node); }
-            } else { console.error("info.node did not exist"); }
+              if (info.frame & 1) {
+                cursor.fixStart(info.node);
+              }
+              if (info.frame & 2) {
+                cursor.fixEnd(info.node);
+              }
+            } else {
+              console.error("info.node did not exist");
+            }
 
             var sel = cursor.makeSelection();
             var range = cursor.makeRange();
@@ -417,7 +428,7 @@ define('xwiki-realtime-wysiwygEditor', [
             cursor.fixSelection(sel, range);
           }
         }
-      };
+      });
 
       // List of pretty name of all users (mapped with their server ID).
       var userData = {};
@@ -425,60 +436,37 @@ define('xwiki-realtime-wysiwygEditor', [
       var userList;
       var myId;
 
-      var DD = new DiffDom(diffOptions);
-
-      var fixMacros = function () {
-        if ($(window.inner).find('.macro[data-cke-widget-data]')) {
-          var dataValues = {};
-          var $elements = $(window.innerDoc).find('[data-cke-widget-data]');
-          $elements.each(function (idx, el) {
-            dataValues[idx] = $(el).attr('data-cke-widget-data');
-          });
-          editor.widgets.instances = {};
-          editor.widgets.checkWidgets();
-          $elements.each(function (idx, el) {
-            $(el).attr('data-cke-widget-data', dataValues[idx]);
-          });
-        }
+      var fixMacros = function() {
+        var dataValues = {};
+        var $elements = $(window.innerDoc).find('[data-cke-widget-data]');
+        $elements.each(function(idx, element) {
+          dataValues[idx] = $(element).attr('data-cke-widget-data');
+        });
+        editor.widgets.instances = {};
+        editor.widgets.checkWidgets();
+        $elements.each(function(idx, element) {
+          $(element).attr('data-cke-widget-data', dataValues[idx]);
+        });
       };
 
-      // apply patches, and try not to lose the cursor in the process!
-      var applyHjson = function (shjson) {
+      // Apply patches and try not to lose the cursor in the process!
+      var applyHjson = function(shjson) {
         var userDocStateDom = Hyperjson.toDOM(JSON.parse(shjson));
-        userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
-        var patch = (DD).diff(window.inner, userDocStateDom);
-        (DD).apply(window.inner, patch);
-        try { fixMacros(); } catch (e) { console.log("Unable to fix the macros", e); }
-      };
-
-      var realtimeOptions = {
-        // provide initialstate...
-        initialState: stringifyDOM(inner) || '{}',
-
-        // the websocket URL
-        websocketURL: WebsocketURL,
-
-        // our username
-        userName: userName,
-
-        // the channel we will communicate over
-        channel: channel,
-
-        // Crypto object to avoid loading it twice in Cryptpad
-        crypto: Crypto,
-
-        // Network loaded in realtime-frontend
-        network: network,
-
-        // OT
-        //patchTransformer: Chainpad.NaiveJSONTransformer
+        userDocStateDom.setAttribute('contenteditable', 'true');
+        var patch = DD.diff(window.inner, userDocStateDom);
+        DD.apply(window.inner, patch);
+        try {
+          fixMacros();
+        } catch (e) {
+          console.log("Unable to fix the macros.", e);
+        }
       };
 
       var findMacroComments = function(el) {
         var arr = [];
-        for(var i = 0; i < el.childNodes.length; i++) {
+        for (var i = 0; i < el.childNodes.length; i++) {
           var node = el.childNodes[i];
-          if(node.nodeType === 8 && node.data && /startmacro/.test(node.data)) {
+          if (node.nodeType === 8 && node.data && /startmacro/.test(node.data)) {
             arr.push(node);
           } else {
             arr.push.apply(arr, findMacroComments(node));
@@ -487,138 +475,83 @@ define('xwiki-realtime-wysiwygEditor', [
         return arr;
       };
 
-      var createSaver = function (info) {
-        if(!DEMO_MODE) {
-          Saver.lastSaved.mergeMessage = Interface.createMergeMessageElement(toolbar.toolbar
-            .find('.rt-toolbar-rightside'),
-            saverConfig.messages);
-          Saver.setLastSavedContent(editor._.previousModeData);
-          var saverCreateConfig = {
-            formId: window.XWiki.editor === "wysiwyg" ? "edit" : "inline", // Id of the wiki page form
-            setTextValue: function(newText, toConvert, callback) {
-              var andThen = function (data) {
-                var doc = window.DOMDoc = (new DOMParser()).parseFromString(data,"text/html");
-                window.cursor.update();
-                doc.body.setAttribute("contenteditable", "true");
-                var patch = (DD).diff(window.inner, doc.body);
-                (DD).apply(window.inner, patch);
-
-                // If available, transform the HTML comments for XWiki macros into macros before saving
-                // (<!--startmacro:{...}-->). We can do that by using the "xwiki-refresh" command provided the by
-                // CKEditor Integration application.
-                if (editor.plugins['xwiki-macro'] && findMacroComments(window.inner).length > 0) {
-                  initializing = true;
-                  editor.execCommand('xwiki-refresh');
-                  afterRefresh.push(callback);
-                } else {
-                  callback();
-                  onLocal();
-                }
-              };
-              if (toConvert) {
-                var object = {
-                  wiki: XWiki.currentWiki,
-                  space: XWiki.currentSpace,
-                  page: XWiki.currentPage,
-                  convert: true,
-                  text: newText
-                };
-                $.post(htmlConverterUrl+'?xpage=plain&outputSyntax=plain', object).done(function(data) {
-                  andThen(data);
-                }).fail(function(err){
-                  var debugLog = {
-                    state: editorId + '/convertHTML',
-                    postData: object
-                  };
-                  module.onAbort(null, 'converthtml', JSON.stringify(debugLog));
-                });
+      var createSaver = function(info) {
+        Saver.lastSaved.mergeMessage = Interface.createMergeMessageElement(
+          toolbar.toolbar.find('.rt-toolbar-rightside'));
+        Saver.setLastSavedContent(editor._.previousModeData);
+        var saverCreateConfig = {
+          // Id of the wiki page form.
+          formId: window.XWiki.editor === "wysiwyg" ? "edit" : "inline",
+          setTextValue: function(newText, toConvert, callback) {
+            var andThen = function(data) {
+              var doc = window.DOMDoc = (new DOMParser()).parseFromString(data,"text/html");
+              window.cursor.update();
+              doc.body.setAttribute("contenteditable", "true");
+              var patch = (DD).diff(window.inner, doc.body);
+              (DD).apply(window.inner, patch);
+
+              // If available, transform the HTML comments for XWiki macros into macros before saving
+              // (<!--startmacro:{...}-->). We can do that by using the "xwiki-refresh" command provided the by
+              // CKEditor Integration application.
+              if (editor.plugins['xwiki-macro'] && findMacroComments(window.inner).length > 0) {
+                initializing = true;
+                editor.execCommand('xwiki-refresh');
+                afterRefresh.push(callback);
               } else {
-                andThen(newText);
+                callback();
+                realtimeOptions.onLocal();
               }
-            },
-            getSaveValue: function() {
-              return {
-                content: editor.getData(),
-                RequiresHTMLConversion: 'content',
-                'content_syntax': 'xwiki/2.1'
+            };
+            if (toConvert) {
+              var object = {
+                wiki: XWiki.currentWiki,
+                space: XWiki.currentSpace,
+                page: XWiki.currentPage,
+                convert: true,
+                text: newText
               };
-            },
-            getTextValue: function() {
-              try {
-                return editor.getData();
-              } catch (e) {
-                // ckError: "The content cannot be saved because of a CKEditor internal error. You should try to copy
-                //   your important changes and reload the editor.",
-                // ckError: "Le contenu n'a pas pu être sauvé à cause d'une erreur interne de CKEditor. Vous devriez
-                //   essayer de copier vos modifications importantes et de recharger la page.",
-                editor.showNotification(Messages.ckError, 'warning');
-                return null;
-              }
-            },
-            realtime: info.realtime,
-            userList: info.userList,
-            userName: userName,
-            network: info.network,
-            channel: eventsChannel,
-            demoMode: DEMO_MODE,
-            safeCrash: function(reason, debugLog) { module.onAbort(null, reason, debugLog); }
-          };
-          Saver.create(saverCreateConfig);
-        }
-      };
-
-      var onRemote = realtimeOptions.onRemote = function (info) {
-        if (initializing) { return; }
-
-        var shjson = info.realtime.getUserDoc();
-
-        // remember where the cursor is
-        window.cursor.update();
-
-        // build a dom from HJSON, diff, and patch the editor
-        applyHjson(shjson);
-
-        var shjson2 = stringifyDOM(window.inner);
-        if (shjson2 !== shjson) {
-          console.error("shjson2 !== shjson");
-          var diff = Chainpad.Diff.diff(shjson, shjson2);
-          console.log(shjson, diff);
-          module.chainpad.contentUpdate(shjson2);
-        }
-      };
-
-      var onInit = realtimeOptions.onInit = function (info) {
-        var $bar = $('#cke_1_toolbox');
-        userList = info.userList;
-        var config = {
-          userData: userData,
-          onUsernameClick: function (id) {
-            var basehref = iframe.contentWindow.location.href.split('#')[0] || "";
-            iframe.contentWindow.location.href = basehref + "#rt-user-" + id;
-          }
-        };
-        toolbar = Toolbar.create({
-          '$container': $bar,
-          myUserName: info.myID,
+              $.post(htmlConverterUrl + '?xpage=plain&outputSyntax=plain', object).done(function(data) {
+                andThen(data);
+              }).fail(function(err) {
+                var debugLog = {
+                  state: editorId + '/convertHTML',
+                  postData: object
+                };
+                module.onAbort(null, 'converthtml', JSON.stringify(debugLog));
+              });
+            } else {
+              andThen(newText);
+            }
+          },
+          getSaveValue: function() {
+            return {
+              content: editor.getData(),
+              RequiresHTMLConversion: 'content',
+              'content_syntax': 'xwiki/2.1'
+            };
+          },
+          getTextValue: function() {
+            try {
+              return editor.getData();
+            } catch (e) {
+              // ckError: "The content cannot be saved because of a CKEditor internal error. You should try to copy
+              //   your important changes and reload the editor.",
+              // ckError: "Le contenu n'a pas pu être sauvé à cause d'une erreur interne de CKEditor. Vous devriez
+              //   essayer de copier vos modifications importantes et de recharger la page.",
+              editor.showNotification(Messages.ckError, 'warning');
+              return null;
+            }
+          },
           realtime: info.realtime,
-          getLag: info.getLag,
           userList: info.userList,
-          config
-        });
-        // When someone leaves, if they used Save&View, it removes the locks from the document.
-        // We're going to add it again to be sure new users will see the lock page and be able to join.
-        var oldUsers = JSON.parse(JSON.stringify(userList.users || []));
-        userList.change.push(function () {
-          if (userList.length === 0) { return; }
-          // If someone has left, try to get the lock
-          if (oldUsers.some(function (u) {
-            return userList.users.indexOf(u) === -1;
-          })) {
-            XWiki.EditLock = new XWiki.DocumentLock();
-            XWiki.EditLock.lock();
+          userName,
+          network: info.network,
+          channel: eventsChannel,
+          safeCrash: function(reason, debugLog) {
+            module.onAbort(null, reason, debugLog);
           }
-          oldUsers = JSON.parse(JSON.stringify(userList.users || []));
-        });
+        };
+        Saver.create(saverCreateConfig);
       };
 
       var getXPath = function(element) {
@@ -632,7 +565,7 @@ define('xwiki-realtime-wysiwygEditor', [
       };
 
       var getPrettyName = function(userName) {
-        return (userName) ? userName.replace(/^.*-([^-]*)%2d[0-9]*$/, function(all, one) { 
+        return userName ? userName.replace(/^.*-([^-]*)%2d[0-9]*$/, function(all, one) { 
           return decodeURIComponent(one);
         }) : userName;
       };
@@ -732,145 +665,227 @@ define('xwiki-realtime-wysiwygEditor', [
         $(window.inner).css('padding-left', requiredPadding === 0 ? '' : ((requiredPadding + 15) + 'px'));
       };
 
-      var first = true;
-      var onReady = realtimeOptions.onReady = function (info) {
-        if (!initializing) {
-          return;
-        }
+      var isFirstOnReadyCall = true;
 
-        $.extend(module, {
-          chainpad: info.realtime,
-          leaveChannel: info.leave,
-          realtimeOptions
-        });
-        var shjson = module.chainpad.getUserDoc();
-
-        myId = info.myId;
-
-        if (first === true) {
-          first = false;
-          // Update the user list to link the wiki name to the user id.
-          var userdataConfig = {
-            myId: info.myId,
-            userName,
-            userAvatar,
-            onChange: userList.onChange,
-            crypto: Crypto,
-            editor: editorId,
-            getCursor: function() {
-              var selection = editor.getSelection();
-              if (!selection) {
-                return '';
-              }
-              var ranges = selection.getRanges();
-              if (!ranges || !ranges[0] || !ranges[0].startContainer || !ranges[0].startContainer.$) {
-                return '';
-              }
-              var node = ranges[0].startContainer.$;
-              node = (node.nodeName === '#text') ? node.parentNode : node;
-              var xpath = getXPath(node);
-              return xpath;
+      var realtimeOptions = {
+        // provide initialstate...
+        initialState: stringifyDOM(inner) || '{}',
+
+        // the websocket URL
+        websocketURL: WebsocketURL,
+
+        // our username
+        userName: userName,
+
+        // the channel we will communicate over
+        channel: channel,
+
+        // Crypto object to avoid loading it twice in Cryptpad
+        crypto: Crypto,
+
+        // Network loaded in realtime-frontend
+        network: network,
+
+        // OT
+        //patchTransformer: Chainpad.NaiveJSONTransformer
+
+        onRemote: function(info) {
+          if (initializing) {
+            return;
+          }
+
+          var shjson = info.realtime.getUserDoc();
+
+          // Remember where the cursor is.
+          window.cursor.update();
+
+          // Build a DOM from HJSON, diff, and patch the editor.
+          applyHjson(shjson);
+
+          var shjson2 = stringifyDOM(window.inner);
+          if (shjson2 !== shjson) {
+            console.error('shjson2 !== shjson');
+            var diff = Chainpad.Diff.diff(shjson, shjson2);
+            console.log(shjson, diff);
+            module.chainpad.contentUpdate(shjson2);
+          }
+        },
+
+        onInit: function(info) {
+          var $bar = $('#cke_1_toolbox');
+          userList = info.userList;
+          var config = {
+            userData,
+            onUsernameClick: function(id) {
+              var basehref = iframe.contentWindow.location.href.split('#')[0] || '';
+              iframe.contentWindow.location.href = basehref + '#rt-user-' + id;
             }
           };
-          if (!realtimeConfig.marginAvatar) {
-            delete userdataConfig.getCursor;
+          toolbar = Toolbar.create({
+            '$container': $bar,
+            myUserName: info.myID,
+            realtime: info.realtime,
+            getLag: info.getLag,
+            userList: info.userList,
+            config
+          });
+          // When someone leaves, if they used Save&View, it removes the locks from the document. We're going to add it
+          // again to be sure new users will see the lock page and be able to join.
+          var oldUsers = JSON.parse(JSON.stringify(userList.users || []));
+          userList.change.push(function() {
+            if (userList.length) {
+              // If someone has left, try to get the lock.
+              if (oldUsers.some(user => userList.users.indexOf(user) === -1)) {
+                XWiki.EditLock = new XWiki.DocumentLock();
+                XWiki.EditLock.lock();
+              }
+              oldUsers = JSON.parse(JSON.stringify(userList.users || []));
+            }
+          });
+        },
+
+        onReady: function(info) {
+          if (!initializing) {
+            return;
           }
 
-          userData = UserData.start(info.network, userdataChannel, userdataConfig);
-          userList.change.push(changeUserIcons);
-        }
+          $.extend(module, {
+            chainpad: info.realtime,
+            leaveChannel: info.leave,
+            realtimeOptions
+          });
+          var shjson = module.chainpad.getUserDoc();
+
+          myId = info.myId;
+
+          if (isFirstOnReadyCall) {
+            isFirstOnReadyCall = false;
+            // Update the user list to link the wiki name to the user id.
+            var userdataConfig = {
+              myId: info.myId,
+              userName,
+              userAvatar,
+              onChange: userList.onChange,
+              crypto: Crypto,
+              editor: editorId,
+              getCursor: function() {
+                var selection = editor.getSelection();
+                if (!selection) {
+                  return '';
+                }
+                var ranges = selection.getRanges();
+                if (!ranges || !ranges[0] || !ranges[0].startContainer || !ranges[0].startContainer.$) {
+                  return '';
+                }
+                var node = ranges[0].startContainer.$;
+                node = (node.nodeName === '#text') ? node.parentNode : node;
+                var xpath = getXPath(node);
+                return xpath;
+              }
+            };
+            if (!realtimeConfig.marginAvatar) {
+              delete userdataConfig.getCursor;
+            }
 
-        applyHjson(shjson);
+            userData = UserData.start(info.network, userdataChannel, userdataConfig);
+            userList.change.push(changeUserIcons);
+          }
 
-        console.log('Unlocking editor');
-        initializing = false;
-        setEditable(true);
-        module.chainpad.start();
+          applyHjson(shjson);
 
-        onLocal();
-        createSaver(info);
-      };
+          console.log('Unlocking editor');
+          initializing = false;
+          setEditable(true);
+          module.chainpad.start();
 
-      var onAbort = module.onAbort = realtimeOptions.onAbort = function (info, reason, debug) {
-        console.log("Aborting the session!");
-        var msg = reason || 'disconnected';
-        module.chainpad.abort();
-        try {
-          // Don't break if the channel doesn't exist anymore
-          module.leaveChannel();
-        } catch (e) {}
-        module.aborted = true;
-        editorConfig.abort();
-        Saver.stop();
-        toolbar.failed();
-        toolbar.toolbar.remove();
-        if (userData.leave && typeof userData.leave === "function") { userData.leave(); }
-        changeUserIcons({});
-        if($disallowButton[0].checked && !module.aborted) {
-          ErrorBox.show(msg, debug);
-        }
-      };
+          realtimeOptions.onLocal();
+          createSaver(info);
+        },
 
-      var onConnectionChange = realtimeOptions.onConnectionChange = function (info) {
-        if (module.aborted) { return; }
-        console.log("Connection status : "+info.state);
-        toolbar.failed();
-        if (info.state) {
-          //ErrorBox.hide();
-          initializing = true;
-          toolbar.reconnecting(info.myId);
-        } else {
+        onAbort: function(info, reason, debug) {
+          console.log("Aborting the session!");
+          var msg = reason || 'disconnected';
           module.chainpad.abort();
-          setEditable(false);
-          //ErrorBox.show('disconnected');
-        }
-      };
+          try {
+            // Don't break if the channel doesn't exist anymore.
+            module.leaveChannel();
+          } catch (e) {}
+          module.aborted = true;
+          editorConfig.abort();
+          Saver.stop();
+          toolbar.failed();
+          toolbar.toolbar.remove();
+          if (typeof userData.leave === 'function') {
+            userData.leave();
+          }
+          changeUserIcons({});
+          if ($disallowButton[0].checked && !module.aborted) {
+            ErrorBox.show(msg, debug);
+          }
+        },
 
-      var beforeReconnecting = realtimeOptions.beforeReconnecting = function (callback) {
-        var oldChannel = channel;
-        updateKeys().done(function() {
-          if (channel !== oldChannel) {
-            editorConfig.onKeysChanged();
-            setEditable(false);
-            $disallowButton.prop('checked', false);
-            onAbort();
+        onConnectionChange: function(info) {
+          if (module.aborted) {
+            return;
+          }
+          console.log('Connection status: ' + info.state);
+          toolbar.failed();
+          if (info.state) {
+            initializing = true;
+            toolbar.reconnecting(info.myId);
           } else {
-            callback(channel, stringifyDOM(window.inner));
+            module.chainpad.abort();
+            setEditable(false);
           }
-        });
-      };
+        },
 
-      // This function resets the realtime fields after coming back from source mode.
-      var onLocalFromSource = realtimeOptions.onLocalFromSource = function () {
-        var iframe = jQuery('iframe')[0]; 
-        window.inner = iframe.contentWindow.body;
-        window.innerDoc = iframe.contentWindow.document;
-        window.cursor = Cursor(window.inner);
-        iframe.onload = addStyle;
-        addStyle();
-        onLocal();
-      };
+        beforeReconnecting: function(callback) {
+          var oldChannel = channel;
+          updateKeys().done(function() {
+            if (channel !== oldChannel) {
+              editorConfig.onKeysChanged();
+              setEditable(false);
+              $disallowButton.prop('checked', false);
+              module.onAbort();
+            } else {
+              callback(channel, stringifyDOM(window.inner));
+            }
+          });
+        },
 
-      var onLocal = realtimeOptions.onLocal = function () {
-        if (initializing) { return; }
-        // stringify the json and send it into chainpad
-        var shjson = stringifyDOM(window.inner);
-        module.chainpad.contentUpdate(shjson);
+        // This function resets the realtime fields after coming back from source mode.
+        onLocalFromSource: function() {
+          var iframe = jQuery('iframe')[0]; 
+          window.inner = iframe.contentWindow.body;
+          window.innerDoc = iframe.contentWindow.document;
+          window.cursor = Cursor(window.inner);
+          iframe.onload = addStyle;
+          addStyle();
+          this.onLocal();
+        },
 
-        if (module.chainpad.getUserDoc() !== shjson) {
-          console.error("realtime.getUserDoc() !== shjson");
+        onLocal: function() {
+          if (initializing) {
+            return;
+          }
+          // Stringify the JSON and send it into ChainPad.
+          var shjson = stringifyDOM(window.inner);
+          module.chainpad.contentUpdate(shjson);
+
+          if (module.chainpad.getUserDoc() !== shjson) {
+            console.error("realtime.getUserDoc() !== shjson");
+          }
         }
       };
 
+      module.onAbort = realtimeOptions.onAbort;
+
       module.realtimeInput = ChainPadNetflux.start(realtimeOptions);
 
-      /* hitting enter makes a new line, but places the cursor inside
-        of the <br> instead of the <p>. This makes it such that you
-        cannot type until you click, which is rather unnacceptable.
-        If the cursor is ever inside such a <br>, you probably want
-        to push it out to the parent element, which ought to be a
-        paragraph tag. This needs to be done on keydown, otherwise
-        the first such keypress will not be inserted into the P. */
+      // Hitting enter makes a new line, but places the cursor inside of the <br> instead of the <p>. This makes it such
+      // that you cannot type until you click, which is rather unnacceptable. If the cursor is ever inside such a <br>,
+      // you probably want to push it out to the parent element, which ought to be a paragraph tag. This needs to be
+      // done on keydown, otherwise the first such keypress will not be inserted into the P.
       window.inner.addEventListener('keydown', window.cursor.brFix);
 
       editor.on('change', function() {
@@ -878,42 +893,25 @@ define('xwiki-realtime-wysiwygEditor', [
         if (!initializing) {
           Saver.setLocalEditFlag(true);
         }
-        onLocal();
+        realtimeOptions.onLocal();
       });
 
-      // export the typing tests to the window.
+      // Export the typing tests to the window.
       // call like `test = easyTest()`
       // terminate the test like `test.cancel()`
-      var easyTest = window.easyTest = function () {
+      window.easyTest = function () {
         window.cursor.update();
         var start = window.cursor.Range.start;
-        var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
-        onLocal();
+        var test = TypingTest.testInput(inner, start.el, start.offset, realtimeOptions.onLocal);
+        realtimeOptions.onLocal();
         return test;
       };
     };
 
-    var untilThen = function () {
-      var $iframe = $('iframe');
-      if (window.CKEDITOR &&
-        window.CKEDITOR.instances &&
-        window.CKEDITOR.instances.content &&
-        $iframe.length &&
-        $iframe[0].contentWindow &&
-        $iframe[0].contentWindow.body) {
-        var editor = window.CKEDITOR.instances.content;
-        if (editor.status === "ready") {
-          whenReady(editor, $iframe[0]);
-        } else {
-          editor.on('instanceReady', function() { whenReady(editor, $iframe[0]); });
-        }
-        return;
-        //return whenReady(window.CKEDITOR.instances.content, $iframe[0]);
-      }
-      setTimeout(untilThen, 100);
-    };
-    /* wait for the existence of CKEDITOR before doing things...  */
-    untilThen();
+    return ckeditorPromise.then($.proxy(waitForEditorInstance, null, 'content')).done(function(editor) {
+      // FIXME: This works only with the stand-alone (classic) editor.
+      whenReady(editor, $(editor.container.$).find('iframe')[0]);
+    });
   };
 
   return module;
-- 
GitLab