/** * EGroupware - Filemanager - Collab editor application object * * @link http://www.egroupware.org * @package filemanager * @author Hadi Nategh * @copyright (c) 2016 Stylite AG * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ /*egw:uses /filemanager/js/collab_config.js; /api/js/webodf/collab/dojo-amalgamation.js; /api/js/webodf/collab/webodf.js; /api/js/webodf/collab/wodocollabtexteditor.js; /filemanager/js/app.js; */ /** * UI for filemanager collab * * @augments AppJS */ app.classes.filemanager = app.classes.filemanager.extend({ /* * @var editor odf editor object */ editor: {}, /** * @var regexp for acceptable mime types */ editor_mime: RegExp(/application\/vnd\.oasis\.opendocument\.text/), /** * @var collab_server server object */ collab_server: {}, /** * Destructor */ destroy: function() { delete this.editor; delete editor_mime; delete collab_server; // call parent this._super.apply(this, arguments); }, /** * This function is called when the etemplate2 object is loaded * and ready. If you must store a reference to the et2 object, * make sure to clean it up in destroy(). * * @param et2 etemplate2 Newly ready object * @param {string} name template name */ et2_ready: function(et2,name) { // call parent this._super.apply(this, arguments); if (name == "filemanager.editor") { // need to make body rock solid to avoid extra scrollbars jQuery('body').css({overflow:'hidden'}); // Remove app header to be able to see the toolbar in compact template jQuery('#divAppboxHeader').remove(); var self = this; jQuery(window).on('unload', function(){self.editor_leaveSession();}); jQuery(window).on('beforeunload', function(){ if (!self.collab_server.close) { return true; } else { return ; } }); this._init_odf_collab_editor (); } }, /** * Initiate odf collab editor popup & load given file_path as active session * editors options: * directParagraphStylingEnabled * paragraphStyleSelectingEnabled * paragraphStyleEditingEnabled * zoomingEnabled * directTextStylingEnabled * imageEditingEnabled * hyperlinkEditingEnabled * annotationsEnabled * unstableFeaturesEnabled * reviewModeEnabled */ _init_odf_collab_editor: function () { var self = this, isNew = window.location.href.search(/&path=/) == -1?true:false; egw.json('filemanager.filemanager_collab.ajax_getGenesisUrl',[this.editor_getFilePath(), isNew], function (_data){ var serverOptions = { serverParams: { url:egw.link('/index.php?', { menuaction: 'filemanager.filemanager_collab.poll' }), genesisUrl:_data.genesis_url }, sessionId: _data.es_id, editorOptions: { directParagraphStylingEnabled:true, paragraphStyleSelectingEnabled:true, paragraphStyleEditingEnabled:true, zoomingEnabled: true, directTextStylingEnabled:true, imageEditingEnabled:true, hyperlinkEditingEnabled:true, annotationsEnabled:true, unstableFeaturesEnabled:true, // review has to be explicitly disabled to be able to edit the document reviewModeEnabled:false, viewOptions:{ editInfoMarkersInitiallyVisible: true, caretAvatarsInitiallyVisible: false, caretBlinksOnRangeSelect: true } } }; var editor = self.et2.getWidgetById('odfEditor'); if (editor) { self.create_collab_editor(serverOptions); } }).sendRequest(); }, /** * Function to leave the current editing session * and as result it will call client-side and server leave session. * * @param {function} _successCallback function to gets called after leave session is successful * @param {boolean} _checkLastActive flag to set for checking if the user is the last * one in the open sessions. */ editor_leaveSession: function (_successCallback,_checkLastActive) { var self = this; var successCallback = _successCallback || function(){window.close();}; var leave = function () { self.editor.leaveSession(function(){}); self.collab_server.server.leaveSession(self.collab_server.es_id, self.collab_server.memberid, successCallback); self.collab_server.close = true; }; if (!_checkLastActive) { leave(); } egw.json('filemanager.filemanager_collab.ajax_actions',[{'es_id':this.collab_server.es_id, 'member_id':this.collab_server.memberid},'checkLastMember'], function(_isLastMember){ if (_isLastMember) { var buttons = [ {"button_id": 2,"text": 'save and close', id: 'save', image: 'check' }, {"button_id": 1,"text": 'discard unsaved changes', id: 'discard', image: 'discard' }, {"button_id": 0,"text": 'cancel', id: 'cancel', image: 'cancel', "default":true} ]; et2_dialog.show_dialog( function(_btn) { switch (_btn) { case 'save': self.editor_save({id:'save'}, function(){ leave(); }); break; case 'discard': self.editor_discard(); break; default: } }, egw.lang('You are the last one on this session. What would you like to do with all unsaved changes in this document?'), 'Closing session', null, buttons, et2_dialog.WARNING_MESSAGE ); } else { leave(); } }).sendRequest(); }, /** * Method to close an opened document */ editor_close: function () { var self = this, file_path = this.et2.getWidgetById('file_path'); if (this.editor) { var closeFn = function (_checkLastActive) { self.editor_leaveSession(null, _checkLastActive); }; // it's an unsaved new file try to warn user about unsaved changes if (file_path.value == '') { et2_dialog.show_dialog( function(_btn) { if (_btn == 2) { closeFn(false); } }, 'There are unsaved changes. Are you sure you want to close this document without saving them?', 'unsaved changes', null, et2_dialog.BUTTONS_YES_NO, et2_dialog.WARNING_MESSAGE ); } else { closeFn(true); } } }, /** * Method call for saving edited document * * @param {object} _egwAction egw action object * @param {function} _afterSaveCallback callback function to be called after * save action happens. * */ editor_save: function (_egwAction, _afterSaveCallback) { var self = this, widgetFilePath = this.et2.getWidgetById('file_path'), file_path = widgetFilePath.value, afterSaveCallback = _afterSaveCallback || function(){}; if (this.editor) { function saveByteArrayLocally(err, data) { if (err) { alert(err); return; } var blob = new Blob([data.buffer], {type: self.editor_mime}); self.editor_file_operation({ url: egw.webserverUrl+'/webdav.php'+file_path, method: 'PUT', processData: false, success: function(data) { egw(window).message(egw.lang('Document %1 has been saved successfully.', file_path)); self.editor.setDocumentModified(false); if (_egwAction.id == 'saveas') return; egw.json('filemanager.filemanager_collab.ajax_actions',[{'es_id':self.collab_server.es_id, 'file_path': egw.webserverUrl+'/webdav.php'+file_path}, 'save'], function(){ afterSaveCallback.call(self,{}); }).sendRequest(); }, error: function () {}, data: blob, mimeType: self.editor_mime }); } //existed file if (file_path != '' && _egwAction.id != 'saveas') { this.editor.getDocumentAsByteArray(saveByteArrayLocally); } // new file else { var vfs_attrs = { id:'savefile', mode: 'saveas', button_caption:"", button_label:_egwAction.id == 'saveas'?"save as":"save", value: "doc.odt", onchange: function (){ file_path = vfs_select.get_value(); if (vfs_select.get_value()) { // Add odt extension if not exist if (!file_path.match(/\.odt$/,'ig')) file_path += '.odt'; widgetFilePath.set_value(file_path); self.editor.getDocumentAsByteArray(saveByteArrayLocally); self.editor_leaveSession(function(){ var path = window.location.href.split('&path='); window.location.href = path[0]+'&path='+self.editor_getFilePath(); }); egw.refresh('','filemanager'); } } }; // create file selector var vfs_select = et2_createWidget('vfs-select', vfs_attrs, this.et2); // start the file selector dialog vfs_select.click(); } } }, /** * Method to delete loaded file in editor * @param {type} _egwAction */ editor_delete: function (_egwAction) { var fullpath = this.et2.getWidgetById('file_path').value; var selected = fullpath.split('/'); selected.pop(); var path = selected.join('/'); var self =this; et2_dialog.show_dialog( function(_btn) { if (_btn == 2) { self._do_action('delete', [fullpath,path+'/.'+self.collab_server.es_id+'.webodf.odt'], false, path); self.editor_close(_egwAction); } }, egw.lang('Delete file %1?', path), 'Delete file', null, et2_dialog.BUTTONS_YES_NO, et2_dialog.WARNING_MESSAGE ); }, /** * Function to handle file operations (PGD) for editor * * @param {object} _params jquery ajax parameters */ editor_file_operation: function (_params) { var ajaxObj = { url: egw.webserverUrl+'/webdav.php?/home/'+egw.user('account_lid')+'/default.odt' }; jQuery.extend(ajaxObj, _params); switch (ajaxObj && ajaxObj.cmd) { case 'PUT': jQuery.extend({},ajaxObj, { data: JSON.stringify(ajaxObj.data), contentType: 'application/json' }); break; case 'GET': jQuery.extend({},ajaxObj, { dataType: 'json' }); break; case 'DELETE': break; } jQuery.ajax(ajaxObj); }, /** * Function to get full file path * * @returns {String} retruns file path */ editor_getFilePath: function () { var widgetFilePath = this.et2.getWidgetById('file_path'), file_path = widgetFilePath.value, path = egw.webserverUrl+'/webdav.php'+file_path; return path; }, /** * This function gets called after discard action to * notify particioant to join to the new session or * save as the document to not lose changes. * */ editor_discarded: function () { var self = this; var buttons = [ {"button_id": 1,"text": 'reload', id: 'reload', image: 'check' }, {"button_id": 0,"text": 'save as', id: 'save', image: 'cancel', "default":true} ]; et2_dialog.show_dialog( function(_btn) { if (_btn == 'save') { self.editor_save({id:'saveas'}); } else if (_btn == 'reload') { self.collab_server.close = true; window.location.reload(); } }, egw.lang('This session is not valid anymore! Save as your local changes if you need them or reload to join new session.'), 'Delete file', null, buttons, et2_dialog.WARNING_MESSAGE ); }, /** * Function to create collab editor * * @param {type} _args parameteres to be set for server factory and texteditor */ create_collab_editor: function (_args) { var serverFactory, server, serverParams = _args.serverParams, sessionId = _args.sessionId, editorOptions = jQuery.extend(_args.editorOptions,{}), userId = egw.user('account_id'), memberId, self = this; /** * Editor error handler function * * this function also been used in order to notify * participant about session changes. * * @param {string} e */ function handleEditingError (e) { switch (e) { // This type of error happens when the session is discarded or // the document has been deleted and all records in database's been removed. case 'sessionDoesNotExist': this.editor_discarded(); break; default: console.log(e); } }; function onEditing () { }; /** * Callback function which gets called after the collab editor is created * * @param {string} _err * @param {object} _editor webodf collabtexteditor object * * @return {undefined} return undefined if something goes wrong */ function onEditorCreated (_err, _editor) { if (_err) { console.log('Something went wrong whilst loading editor.'+ _err); return; } self.editor = _editor; self.editor.addEventListener(Wodo.EVENT_UNKNOWNERROR, jQuery.proxy(handleEditingError, self)); self.editor.joinSession(serverFactory.createSessionBackend(sessionId, memberId, server), onEditing); }; /** * Function to join a doc session * * @param {type} _sessionId session id of the opened document */ function joinSession(_sessionId) { var sid = _sessionId; server.joinSession(userId, sid, function (_memberId) { memberId = _memberId; // Set server object for current session self.collab_server = {server:server, memberid: memberId, es_id: sid}; if (Object.keys(self.editor).length == 0) { Wodo.createCollabTextEditor('filemanager-editor_odfEditor', editorOptions, onEditorCreated); } else { self.editor.joinSession(serverFactory.createSessionBackend(sid, _memberId, server), onEditing); } }, function(err) { console.log(err); }); }; webodfModule.require(["egwCollab/ServerFactory"], function (ServerFactory) { serverFactory = new ServerFactory(); server = serverFactory.createServer(serverParams); server.connect(8000, function (state) { switch (state) { case "ready": joinSession(sessionId); break; case "timeout": console.log('did not connect to server because of timeout.'); break; default: console.log('server is not available.'); } }); }); }, _do_action_callback:function(_data) { this._super.apply(this,arguments); switch(_data.action) { case 'delete': if (!_data.errs) egw.json('filemanager.filemanager_collab.ajax_actions', [{'es_id':this.collab_server.es_id}, 'delete'], function(){window.close();}).sendRequest(); } }, /** * Discard stacked modification in session from all participants * it will warn user about the consequences which would be removing * all stored OP modfifications in DB. Then as result it will notify * other participants about the action and prompt them to reload the * session or save as the current session if they want to keep their * changes. * */ editor_discard: function () { var self = this; var buttons = [ {"button_id": 1,"text": 'discard', id: 'discard', image: 'discard' }, {"button_id": 0,"text": 'cancel', id: 'cancel', image: 'cancel', "default":true} ]; et2_dialog.show_dialog( function(_btn) { if (_btn == 'discard') { egw.json('filemanager.filemanager_collab.ajax_actions',[{'es_id': self.collab_server.es_id}, 'discard'], function(){ self.collab_server.close = true; window.location.reload(); }).sendRequest(); } }, egw.lang('Are you sure that you want to discard all modifications applied to this document by you and other participants?'), 'Discard all changes', null, buttons, et2_dialog.WARNING_MESSAGE ); } });