define([ //'jquery', //'RTWiki_WebHome_sharejs_textarea', //'RTWiki_ErrorBox', //'RTWiki_WebHome_chainpad' '/common/crypto.js', '/code/errorbox.js', '/common/messages.js', '/common/toolbar.js', '/common/chainpad.js', '/common/otaml.js', '/bower_components/jquery/dist/jquery.min.js' ], function(Crypto, ErrorBox, Messages, Toolbar) { var $ = window.jQuery; var ChainPad = window.ChainPad; var Otaml = window.Otaml; var module = { exports: {} }; var LOCALSTORAGE_DISALLOW = 'rtwiki-disallow'; // Number for a message type which will not interfere with chainpad. var MESSAGE_TYPE_ISAVED = 5000; // how often to check if the document has been saved recently var SAVE_DOC_CHECK_CYCLE = 20000; // how often to save the document var SAVE_DOC_TIME = 60000; // How long to wait before determining that the connection is lost. var MAX_LAG_BEFORE_DISCONNECT = 30000; var MAX_RECOVERABLE_ERRORS = 15; var warn = function (x) { }; var debug = function (x) { }; //debug = function (x) { console.log(x) }; warn = function (x) { console.log(x); }; var setStyle = function () { $('head').append([ '' ].join('')); }; var uid = function () { return 'rtwiki-uid-' + String(Math.random()).substring(2); }; // only used within updateUserList so far var decodeUser = function (all, one) { return decodeURIComponent(one); }; var updateUserList = function (myUserName, listElement, userList, messages) { var meIdx = userList.indexOf(myUserName); if (meIdx === -1) { listElement.text(messages.disconnected); return; } var userMap = {}; userMap[messages.myself] = 1; userList.splice(meIdx, 1); for (var i = 0; i < userList.length; i++) { var user; if (userList[i].indexOf('xwiki:XWiki.XWikiGuest') === 0) { if (userMap.Guests) { user = messages.guests; } else { user = messages.guest; } } else { user = userList[i].replace(/^.*-([^-]*)%2d[0-9]*$/, decodeUser); } userMap[user] = userMap[user] || 0; if (user === messages.guest && userMap[user] > 0) { userMap.Guests = userMap[user]; delete userMap[user]; user = messages.guests; } userMap[user]++; } var userListOut = []; for (var name in userMap) { if (userMap[name] > 1) { userListOut.push(userMap[name] + " " + name); } else { userListOut.push(name); } } if (userListOut.length > 1) { userListOut[userListOut.length-1] = messages.and + ' ' + userListOut[userListOut.length-1]; } listElement.text(messages.editingWith + ' ' + userListOut.join(', ')); }; var createUserList = function (realtime, myUserName, container, messages) { var id = uid(); $(container).prepend('
'); var listElement = $('#'+id); return listElement; }; var checkLag = function (realtime, lagElement, messages) { var lag = realtime.getLag(); var lagSec = lag.lag/1000; var lagMsg = messages.lag + ' '; if (lag.waiting && lagSec > 1) { lagMsg += "?? " + Math.floor(lagSec); } else { lagMsg += lagSec; } lagElement.text(lagMsg); }; var createLagElement = function (socket, realtime, container, messages) { var id = uid(); $(container).append(''); var lagElement = $('#'+id); var intr = setInterval(function () { checkLag(realtime, lagElement, messages); }, 3000); socket.onClose.push(function () { clearTimeout(intr); }); return lagElement; }; var createRealtimeToolbar = function (container) { var id = uid(); $(container).prepend( ' ' ); return $('#'+id); }; var now = function () { return (new Date()).getTime(); }; var getFormToken = function () { return $('meta[name="form_token"]').attr('content'); }; var getDocumentSection = function (sectionNum, andThen) { debug("getting document section..."); $.ajax({ url: window.docediturl, type: "POST", async: true, dataType: 'text', data: { xpage: 'editwiki', section: ''+sectionNum }, success: function (jqxhr) { var content = $(jqxhr).find('#content'); if (!content || !content.length) { andThen(new Error("could not find content")); } else { andThen(undefined, content.text()); } }, error: function (jqxhr, err, cause) { andThen(new Error(err)); } }); }; var getIndexOfDocumentSection = function (documentContent, sectionNum, andThen) { getDocumentSection(sectionNum, function (err, content) { if (err) { andThen(err); return; } // This is screwed up, XWiki generates the section by rendering the XDOM back to // XWiki2.0 syntax so it's not possible to find the actual location of a section. // See: http://jira.xwiki.org/browse/XWIKI-10430 var idx = documentContent.indexOf(content); if (idx === -1) { content = content.split('\n')[0]; idx = documentContent.indexOf(content); } if (idx === -1) { warn("Could not find section content.."); } else if (idx !== documentContent.lastIndexOf(content)) { warn("Duplicate section content.."); } else { andThen(undefined, idx); return; } andThen(undefined, 0); }); }; var seekToSection = function (textArea, andThen) { var sect = window.location.hash.match(/^#!([\W\w]*&)?section=([0-9]+)/); if (!sect || !sect[2]) { andThen(); return; } var text = $(textArea).text(); getIndexOfDocumentSection(text, Number(sect[2]), function (err, idx) { if (err) { andThen(err); return; } if (idx === 0) { warn("Attempted to seek to a section which could not be found"); } else { var heightOne = $(textArea)[0].scrollHeight; $(textArea).text(text.substring(idx)); var heightTwo = $(textArea)[0].scrollHeight; $(textArea).text(text); $(textArea).scrollTop(heightOne - heightTwo); } andThen(); }); }; var saveDocument = function (textArea, language, andThen) { debug("saving document..."); $.ajax({ url: window.docsaveurl, type: "POST", async: true, dataType: 'text', data: { xredirect: '', content: $(textArea).val(), xeditaction: 'edit', comment: 'Auto-Saved by Realtime Session', action_saveandcontinue: 'Save & Continue', minorEdit: 1, ajax: true, form_token: getFormToken(), language: language }, success: function () { andThen(); }, error: function (jqxhr, err, cause) { warn(err); // Don't callback, this way in case of error we will keep trying. //andThen(); } }); }; /** * If we are editing a page which does not exist and creating it from a template * then we should not auto-save the document otherwise it will cause RTWIKI-16 */ var createPageMode = function () { return (window.location.href.indexOf('template=') !== -1); }; var createSaver = function (socket, channel, myUserName, textArea, demoMode, language) { var timeOfLastSave = now(); socket.onMessage.unshift(function (evt) { // get the content... var chanIdx = evt.data.indexOf(channel); var content = evt.data.substring(evt.data.indexOf(':[', chanIdx + channel.length)+1); // parse var json = JSON.parse(content); // not an isaved message if (json[0] !== MESSAGE_TYPE_ISAVED) { return; } timeOfLastSave = now(); return false; }); var lastSavedState = ''; var to; var check = function () { if (to) { clearTimeout(to); } debug("createSaver.check"); to = setTimeout(check, Math.random() * SAVE_DOC_CHECK_CYCLE); if (now() - timeOfLastSave < SAVE_DOC_TIME) { return; } var toSave = $(textArea).val(); if (lastSavedState === toSave) { return; } if (demoMode) { return; } saveDocument(textArea, language, function () { debug("saved document"); timeOfLastSave = now(); lastSavedState = toSave; var saved = JSON.stringify([MESSAGE_TYPE_ISAVED, 0]); socket.send('1:x' + myUserName.length + ':' + myUserName + channel.length + ':' + channel + saved.length + ':' + saved ); }); }; check(); socket.onClose.push(function () { clearTimeout(to); }); }; var isSocketDisconnected = function (socket, realtime) { return socket.readyState === socket.CLOSING || socket.readyState === socket.CLOSED || (realtime.getLag().waiting && realtime.getLag().lag > MAX_LAG_BEFORE_DISCONNECT); }; var setAutosaveHiddenState = function (hidden) { var elem = $('#autosaveControl'); if (hidden) { elem.hide(); } else { elem.show(); } }; var startWebSocket = function (textArea, toolbarContainer, websocketUrl, userName, channel, messages, demoMode, language) { debug("Opening websocket"); localStorage.removeItem(LOCALSTORAGE_DISALLOW); var toolbar = createRealtimeToolbar(toolbarContainer); var socket = new WebSocket(websocketUrl); socket.onClose = []; socket.onMessage = []; var initState = $(textArea).val(); var realtime = socket.realtime = ChainPad.create(userName, 'x', channel, initState); // for debugging window.rtwiki_chainpad = realtime; // http://jira.xwiki.org/browse/RTWIKI-21 var onbeforeunload = window.onbeforeunload || function () { }; window.onbeforeunload = function (ev) { socket.intentionallyClosing = true; return onbeforeunload(ev); }; var isErrorState = false; var checkSocket = function () { if (socket.intentionallyClosing || isErrorState) { return false; } if (isSocketDisconnected(socket, realtime)) { realtime.abort(); socket.close(); ErrorBox.show('disconnected'); isErrorState = true; return true; } return false; }; socket.onopen = function (evt) { var initializing = true; var userListElement = createUserList(realtime, userName, toolbar.find('.rtwiki-toolbar-leftside'), messages); userListElement.text(messages.initializing); createLagElement(socket, realtime, toolbar.find('.rtwiki-toolbar-rightside'), messages); setAutosaveHiddenState(true); createSaver(socket, channel, userName, textArea, demoMode, language); socket.onMessage.push(function (evt) { debug(evt.data); realtime.message(evt.data); }); realtime.onMessage(function (message) { socket.send(message); }); $(textArea).attr("disabled", "disabled"); realtime.onUserListChange(function (userList) { if (initializing && userList.indexOf(userName) > -1) { initializing = false; $(textArea).val(realtime.getUserDoc()); textArea.attach($(textArea)[0], realtime); $(textArea).removeAttr("disabled"); } if (!initializing) { updateUserList(userName, userListElement, userList, messages); } }); debug("Bound websocket"); realtime.start(); }; socket.onclose = function (evt) { for (var i = 0; i < socket.onClose.length; i++) { if (socket.onClose[i](evt) === false) { return; } } }; socket.onmessage = function (evt) { for (var i = 0; i < socket.onMessage.length; i++) { if (socket.onMessage[i](evt) === false) { return; } } }; socket.onerror = function (err) { warn(err); checkSocket(realtime); }; var to = setInterval(function () { checkSocket(realtime); }, 500); socket.onClose.push(function () { clearTimeout(to); if (toolbar && typeof toolbar.remove === 'function') { toolbar.remove(); } else { warn("toolbar.remove is not a function"); //why not? } setAutosaveHiddenState(false); }); return socket; }; var stopWebSocket = function (socket) { debug("Stopping websocket"); socket.intentionallyClosing = true; if (!socket) { return; } if (socket.realtime) { socket.realtime.abort(); } socket.close(); }; var checkSectionEdit = function () { var href = window.location.href; if (href.indexOf('#') === -1) { href += '#!'; } var si = href.indexOf('section='); if (si === -1 || si > href.indexOf('#')) { return false; } var m = href.match(/([&]*section=[0-9]+)/)[1]; href = href.replace(m, ''); if (m[0] === '&') { m = m.substring(1); } href = href + '&' + m; window.location.href = href; return true; }; var editor = function (websocketUrl, userName, messages, channel, demoMode, language) { var contentInner = $('#xwikieditcontentinner'); var textArea = contentInner.find('#content'); if (!textArea.length) { warn("WARNING: Could not find textarea to bind to"); return; } if (createPageMode()) { return; } if (checkSectionEdit()) { return; } setStyle(); var checked = (localStorage.getItem(LOCALSTORAGE_DISALLOW)) ? "" : 'checked="checked"'; var allowRealtimeCbId = uid(); $('#mainEditArea .buttons').append( '