diff options
author | Julius Härtl <jus@bitgrid.net> | 2019-07-19 15:18:52 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2019-08-26 12:21:21 +0300 |
commit | 5ffb9634079eb3ff829f1987aa08608598003914 (patch) | |
tree | 26e7358f5b229c2effd389b0b0f0083f0e2d63c0 /src | |
parent | 7ca3f97975322a4c7c6383e86a63790894d4d375 (diff) |
Implement post messages for across frame communication
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'src')
-rw-r--r-- | src/admin.js | 2 | ||||
-rw-r--r-- | src/document.js | 853 | ||||
-rw-r--r-- | src/helpers/guestName.js | 6 | ||||
-rw-r--r-- | src/helpers/index.js | 7 | ||||
-rw-r--r-- | src/helpers/mobile.js | 54 | ||||
-rw-r--r-- | src/helpers/types.js | 60 | ||||
-rw-r--r-- | src/helpers/url.js | 67 | ||||
-rw-r--r-- | src/services/config.tsx | 53 | ||||
-rw-r--r-- | src/services/postMessage.tsx | 107 | ||||
-rw-r--r-- | src/view/FilesAppIntegration.js | 447 | ||||
-rw-r--r-- | src/viewer.js | 326 |
11 files changed, 1129 insertions, 853 deletions
diff --git a/src/admin.js b/src/admin.js index 1b2fda5d..2d3344d4 100644 --- a/src/admin.js +++ b/src/admin.js @@ -202,7 +202,6 @@ var documentsSettings = { }) $(document).on('change', '#edit_group_select', function() { - var element = page.find('input.edit-groups-enable') var groups = $(this).val() documentsSettings.saveGroups({ edit_groups: groups }) }) @@ -223,7 +222,6 @@ var documentsSettings = { }) $(document).on('change', '#use_group_select', function() { - var element = page.find('input.use-groups-enable') var groups = $(this).val() documentsSettings.saveGroups({ use_groups: groups }) }) diff --git a/src/document.js b/src/document.js index 204415f8..70af94be 100644 --- a/src/document.js +++ b/src/document.js @@ -1,10 +1,20 @@ import { getRootUrl } from 'nextcloud-router' -import { getRequestToken, getCurrentUser } from 'nextcloud-auth' -import { languageToBCP47 } from './helpers' -import { getGuestNameCookie, setGuestNameCookie, shouldAskForGuestName } from './helpers/guestName' - -/* TODO: move to one object */ -/* global richdocuments_directEdit richdocuments_fileId richdocuments_urlsrc richdocuments_token richdocuments_path richdocuments_permissions richdocuments_title getURLParameter richdocuments_canonical_webroot */ +import { getRequestToken } from 'nextcloud-auth' +import Config from './services/config' +import { setGuestNameCookie, shouldAskForGuestName } from './helpers/guestName' + +import PostMessageService from './services/postMessage' +import { + callMobileMessage, + isDirectEditing, + isMobileInterfaceAvailable +} from './helpers/mobile' +import { getWopiUrl } from './helpers/url' + +const PostMessages = new PostMessageService({ + parent: window.parent, + loolframe: () => document.getElementById('loleafletframe').contentWindow +}) const showLoadingIndicator = () => document.getElementById('loadingContainer').classList.add('icon-loading') const hideLoadingIndicator = () => document.getElementById('loadingContainer').classList.remove('icon-loading') @@ -15,23 +25,20 @@ $.widget('oc.guestNamePicker', { _create: function() { hideLoadingIndicator() - var text = document.createElement('div') - $(text).attr('style', 'margin: 0 auto; margin-top: 100px; text-align: center;') - - var para = t('richdocuments', 'Please choose your nickname to continue as guest user.') - text.innerHTML = para + const text = document.createElement('div') + text.setAttribute('style', 'margin: 0 auto; margin-top: 100px; text-align: center;') + text.innerHTML = t('richdocuments', 'Please choose your nickname to continue as guest user.') - var div = document.createElement('div') - $(div).attr('style', 'margin: 0 auto; width: 250px; display: flex;') - var nick = '<input type="text" placeholder="' + t('richdocuments', 'Nickname') + '" id="nickname" style="flex-grow: 1; border-right:none; border-top-right-radius: 0; border-bottom-right-radius: 0">' - var btn = '<input style="border-left:none; border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -3px" type="button" id="btn" type="button" value="' + t('richdocuments', 'Set') + '">' + const div = document.createElement('div') + div.setAttribute('style', 'margin: 0 auto; width: 250px; display: flex;') + const nick = '<input type="text" placeholder="' + t('richdocuments', 'Nickname') + '" id="nickname" style="flex-grow: 1; border-right:none; border-top-right-radius: 0; border-bottom-right-radius: 0">' + const btn = '<input style="border-left:none; border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -3px" type="button" id="btn" type="button" value="' + t('richdocuments', 'Set') + '">' div.innerHTML = nick + btn $('#documents-content').prepend(div) $('#documents-content').prepend(text) - var that = this const setGuestNameSubmit = () => { - var username = $('#nickname').val() + const username = $('#nickname').val() setGuestNameCookie(username) window.location.reload(true) } @@ -66,40 +73,25 @@ const documentsMain = { baseName: null, canShare: false, canEdit: false, - loadError: false, - loadErrorMessage: '', - loadErrorHint: '', - fileModel: null, renderComplete: false, // false till page is rendered with all required data about the document(s) - toolbar: '<div id="ocToolbar"><div id="ocToolbarInside"></div><span id="toolbar" class="claro"></span></div>', $deferredVersionRestoreAck: null, wopiClientFeatures: null, // generates docKey for given fileId _generateDocKey: function(wopiFileId) { - var ocurl = getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + wopiFileId - if (richdocuments_canonical_webroot) { - if (!richdocuments_canonical_webroot.startsWith('/')) { richdocuments_canonical_webroot = '/' + richdocuments_canonical_webroot } - - ocurl = ocurl.replace(getRootUrl(), richdocuments_canonical_webroot) + let canonicalWebroot = Config.get('canonical_webroot') + let ocurl = getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + wopiFileId + if (canonicalWebroot) { + if (!canonicalWebroot.startsWith('/')) { + canonicalWebroot = '/' + canonicalWebroot + } + Config.update('canonical_webroot', canonicalWebroot) + ocurl = ocurl.replace(getRootUrl(), canonicalWebroot) } return ocurl }, - getFileList: function() { - if (window === parent) { - return null - } - if (parent.OCA.Files.App) { - return parent.OCA.Files.App.fileList - } - if (parent.OCA.Sharing.PublicApp) { - return parent.OCA.Sharing.PublicApp.fileList - } - return null - }, - UI: { /* Editor wrapper HTML */ container: '<div id="mainContainer" class="claro">' @@ -109,230 +101,24 @@ const documentsMain = { + '<div id="revViewer"></div>' + '</div>', - /* Previous window title */ - mainTitle: '', - /* Number of revisions already loaded */ - revisionsStart: 0, - - /* Views: people currently editing the file */ - views: {}, - - followingEditor: false, - - following: null, - - init: function() { - if (!richdocuments_directEdit && parent.$('#richdocuments-avatars').length === 0) { - documentsMain.UI.mainTitle = parent.document.title - - // Add the avatar toolbar if possible - var avatarList = $('<div id="richdocuments-avatars">') - avatarList.on('click', function(e) { - e.stopPropagation() - parent.$('#editors-menu').toggle() - }) - var headerRight = parent.$('#header .header-right') - headerRight.prepend(avatarList) - - this.addVersionSidebarEvents() - } - }, - - _addHeaderFileActions: function() { - parent.OC.unregisterMenu(parent.$('#richdocuments-actions .icon-more'), parent.$('#richdocuments-actions-menu')) - parent.$('#richdocuments-actions').remove() - var actionsContainer = $('<div id="richdocuments-actions"><div class="icon-more icon-white"></div><ul id="richdocuments-actions-menu" class="popovermenu"></ul></div>') - var actions = actionsContainer.find('#richdocuments-actions-menu').empty() - - var context = { - '$file': documentsMain.getFileList().$el.find('[data-id=' + documentsMain.originalFileId + ']').first(), - fileActions: documentsMain.getFileList().fileActions, - fileList: documentsMain.getFileList(), - fileInfoModel: documentsMain.getFileModel() - } - - var isFavorite = function(fileInfo) { - return fileInfo.get('tags') && fileInfo.get('tags').indexOf(parent.OC.TAG_FAVORITE) >= 0 - } - var $favorite = $('<li><a></a></li>').click(function(e) { - $favorite.find('a').removeClass('icon-starred').removeClass('icon-star-dark').addClass('icon-loading-small') - documentsMain.getFileList().fileActions.triggerAction('Favorite', documentsMain.getFileModel(), documentsMain.getFileList()) - documentsMain.getFileModel().trigger('change', documentsMain.getFileModel()) - }) - if (isFavorite(context.fileInfoModel)) { - $favorite.find('a').text(parent.t('files', 'Remove from favorites')) - $favorite.find('a').addClass('icon-starred') - } else { - $favorite.find('a').text(parent.t('files', 'Add to favorites')) - $favorite.find('a').addClass('icon-star-dark') - } - - var $info = $('<li><a class="icon-info"></a></li>').click(function() { - documentsMain.getFileList().fileActions.actions.all.Details.action(documentsMain.fileName, context) - parent.OC.hideMenus() - }) - $info.find('a').text(parent.t('files', 'Details')) - var $download = $('<li><a class="icon-download">Download</a></li>').click(function() { - documentsMain.getFileList().fileActions.actions.all.Download.action(documentsMain.fileName, context) - parent.OC.hideMenus() - }) - $download.find('a').text(parent.t('files', 'Download')) - actions.append($favorite).append($info).append($download) - actionsContainer.insertAfter(parent.$('#header .richdocuments-sharing')) - parent.OC.registerMenu(parent.$('#richdocuments-actions .icon-more'), parent.$('#richdocuments-actions-menu'), false, true) - }, - - /** - * @param {View} view - * @private - */ - _userEntry: function(view) { - var entry = $('<li></li>') - entry.append(this._avatarForView(view)) - - var label = $('<div class="label"></div>') - label.text(view.UserName) - if (view.ReadOnly === '1') { - label.text(view.UserName + ' ' + t('richdocuments', '(read only)')) - - } - label.click(function(event) { - event.stopPropagation() - documentsMain.UI.followView(view) - }) - if (this.following === view.ViewId) { - parent.$('#editors-menu').find('li').removeClass('active') - entry.addClass('active') - } - entry.append(label) - - var isFileOwner = documentsMain.getFileModel() && typeof documentsMain.getFileModel().get('shareOwner') === 'undefined' - if (documentsMain.canEdit && isFileOwner && !view.IsCurrentView) { - var removeButton = $('<div class="icon-close" title="Remove user"/>') - removeButton.click(function() { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Action_RemoveView', { ViewId: view.ViewId }) - }) - entry.append(removeButton) - } - return entry - }, - - /** - * @param {View} view - * @returns {$|HTMLElement} - * @private - */ - _avatarForView: function(view) { - var avatarContainer = $('<div class="richdocuments-avatar"><div class="avatar" title="' + view.UserName + '" data-user="' + view.UserId + '"></div></div>') - var avatar = avatarContainer.find('.avatar') - avatar.css({ 'border-color': view.Color, - 'border-width': '2px', - 'border-style': 'solid' }) - if (view.ReadOnly === '1') { - avatarContainer.addClass('read-only') - $(avatar).attr('title', view.UserName + ' ' + t('richdocuments', '(read only)')) - } else { - $(avatar).attr('title', view.UserName) - } - $(avatar).avatar(view.UserId, 32, undefined, true, undefined, view.UserName) - if (parent.OC.currentUser !== null && view.UserId !== '') { - // $(avatar).contactsMenu(view.UserId, 0, avatarContainer); - } - return avatarContainer - }, - - renderAvatars: function() { - var avatardiv = parent.$('#header .header-right #richdocuments-avatars') - avatardiv.empty() - var popover = $('<div id="editors-menu" class="popovermenu menu-center"><ul></ul></div>') - - var users = [] - // Add new avatars - var i = 0 - for (var viewId in this.views) { - /** - * @type {View} - */ - var view = this.views[viewId] - view.UserName = view.UserName !== '' ? view.UserName : t('richdocuments', 'Guest') - popover.find('ul').append(this._userEntry(view)) - - if (view.UserId === parent.OC.currentUser) { - continue - } - if (view.UserId !== '' && users.indexOf(view.UserId) > -1) { - continue - } - users.push(view.UserId) - if (i++ < 3) { - avatardiv.append(this._avatarForView(view)) - } - } - var followCurrentEditor = $('<li><input type="checkbox" class="checkbox" /><label class="label">' + t('richdocuments', 'Follow current editor') + '</label></li>') - followCurrentEditor.find('label').click(function(event) { - event.stopPropagation() - if (documentsMain.UI.followingEditor) { - documentsMain.UI.followReset() - } else { - documentsMain.UI.followCurrentEditor() - } - }) - followCurrentEditor.find('.checkbox').prop('checked', documentsMain.UI.followingEditor) - popover.find('ul').append(followCurrentEditor) - avatardiv.append(popover) - }, - followReset: function(event) { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Action_FollowUser', { Follow: false }) - this.following = null - this.followingEditor = false - this.renderAvatars() - }, - followCurrentEditor: function(event) { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Action_FollowUser', { Follow: true }) - this.following = null - this.followingEditor = true - this.renderAvatars() - }, - followView: function(view) { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Action_FollowUser', { ViewId: view.ViewId, Follow: true }) - documentsMain.UI.following = view.ViewId - documentsMain.UI.followingEditor = false - documentsMain.UI.renderAvatars() - }, - showViewer: function(fileId, title) { // remove previous viewer, if open, and set a new one if (documentsMain.isViewerMode) { $('#revViewer').remove() $('#revViewerContainer').prepend($('<div id="revViewer">')) - } else { - this.addCurrentVersion() } - // WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud) - // index.php is forced here to avoid different wopi srcs for the same document - var wopiurl = window.location.protocol + '//' + window.location.host + getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + fileId - var wopisrc = encodeURIComponent(wopiurl) - - // urlsrc - the URL from discovery xml that we access for the particular - // document; we add various parameters to that. - // The discovery is available at - // https://<loolwsd-server>:9980/hosting/discovery - var urlsrc = documentsMain.urlsrc - + 'WOPISrc=' + wopisrc - + '&title=' + encodeURIComponent(title) - + '&lang=' + languageToBCP47() - + '&permission=readonly' + const urlsrc = getWopiUrl({ fileId, title, readOnly: true }) // access_token - must be passed via a form post - var accessToken = encodeURIComponent(documentsMain.token) + const accessToken = encodeURIComponent(documentsMain.token) // form to post the access token for WOPISrc - var form = '<form id="loleafletform_viewer" name="loleafletform_viewer" target="loleafletframe_viewer" action="' + urlsrc + '" method="post">' + const form = '<form id="loleafletform_viewer" name="loleafletform_viewer" target="loleafletframe_viewer" action="' + urlsrc + '" method="post">' + '<input name="access_token" value="' + accessToken + '" type="hidden"/></form>' // iframe that contains the Collabora Online Viewer - var frame = '<iframe id="loleafletframe_viewer" name="loleafletframe_viewer" nonce="' + btoa(getRequestToken()) + '" style="width:100%;height:100%;position:absolute;"/>' + const frame = '<iframe id="loleafletframe_viewer" name="loleafletframe_viewer" nonce="' + btoa(getRequestToken()) + '" style="width:100%;height:100%;position:absolute;"/>' $('#revViewer').append(form) $('#revViewer').append(frame) @@ -340,7 +126,6 @@ const documentsMain = { // submit that $('#loleafletform_viewer').submit() documentsMain.isViewerMode = true - // for closing revision mode $('#revViewerContainer .closeButton').click(function(e) { e.preventDefault() @@ -351,134 +136,14 @@ const documentsMain = { loadRevViewerContainer: function() { if (!$('revViewerContainer').length) { $(document.body).prepend(documentsMain.UI.viewContainer) - var closeButton = $('<button class="icon-close closeButton" title="' + parent.t('richdocuments', 'Close version preview') + '"/>') + const closeButton = $('<button class="icon-close closeButton" title="' + t('richdocuments', 'Close version preview') + '"/>') $('#revViewerContainer').prepend(closeButton) } }, - showRevHistory: function(documentPath) { - // TODO: make sure this also works if using the sidebar with the share icon and navigating to versions then - parent.FileList.showDetailsView(documentsMain.fileName, 'versionsTabView') - this.loadRevViewerContainer() - // Load current revision - // TODO: add entry to versions - var fileId = documentsMain.fileId - var title = documentsMain.fileName - documentsMain.UI.showViewer( - fileId, title - ) - - }, - - addVersionSidebarEvents: function() { - $(parent.document.querySelector('#content')).on('click.revisions', '#app-sidebar .preview-container', this.showVersionPreview.bind(this)) - $(parent.document.querySelector('#content')).on('click.revisions', '#app-sidebar .downloadVersion', this.showVersionPreview.bind(this)) - // Use mousedown event to overwrite behavior of the versions app - $(parent.document.querySelector('#content')).on('mousedown.revisions', '#app-sidebar .revertVersion', this.restoreVersion.bind(this)) - }, - - removeVersionSidebarEvents: function() { - $(parent.document.querySelector('#content')).off('click.revisions') - $(parent.document.querySelector('#content')).off('click.revisions') - $(parent.document.querySelector('#content')).off('mousedown.revisions') - }, - - addCurrentVersion: function() { - if (documentsMain.fileModel) { - var preview = OC.MimeType.getIconUrl(documentsMain.fileModel.get('mimetype')) - parent.$('#versionsTabView').prepend('<ul id="lastSavedVersion"><li data-revision="0"><div><div class="preview-container"><img src="' + preview + '" width="44" /></div><div class="version-container">\n' - + '<div><a class="downloadVersion"><span class="versiondate has-tooltip live-relative-timestamp" data-timestamp="1551294326000"></span></div></div></li></ul>') - parent.$('#versionsTabView').prepend('<ul id="currentVersion"><li data-revision="" class="active"><div><div class="preview-container"><img src="' + preview + '" width="44" /></div><div class="version-container">\n' - + '<div><a class="downloadVersion">' + t('richdocuments', 'Current version') + '</a></div></div></li></ul>') - parent.$('.live-relative-timestamp').each(function() { - $(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10))) - }) - } - }, - - showVersionPreview: function(e) { - e.preventDefault() - documentsMain.UI.loadRevViewerContainer() - var element = e.currentTarget.parentElement.parentElement - if ($(e.currentTarget).hasClass('downloadVersion')) { - element = e.currentTarget.parentElement.parentElement.parentElement.parentElement - } - var version = element.dataset.revision - var fileId = documentsMain.fileId - var title = documentsMain.fileName - if (version !== '') { - fileId += '_' + version - title += '_' + version - } - documentsMain.UI.showViewer( - fileId, title - ) - - // mark only current <li> as active - $(element.parentElement.parentElement).find('li').removeClass('active') - $(element).addClass('active') - }, - - restoreVersion: function(e) { - var self = this - e.preventDefault() - e.stopPropagation() - - documentsMain.onCloseViewer() - - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Host_VersionRestore', { Status: 'Pre_Restore' }) - - var version = e.currentTarget.parentElement.parentElement.dataset.revision - - documentsMain.$deferredVersionRestoreAck = $.Deferred() - $.when(documentsMain.$deferredVersionRestoreAck).done(function(args) { - self._restoreDAV(version) - }) - - // resolve the deferred object immediately if client doesn't support version states - if (!documentsMain.wopiClientFeatures || !documentsMain.wopiClientFeatures.VersionStates) { - documentsMain.$deferredVersionRestoreAck.resolve() - } - - return false - }, - - _restoreSuccess: function(response) { - if (response.status === 'error') { - documentsMain.UI.notify(t('richdocuments', 'Failed to revert the document to older version')) - } - - // load the file again, it should get reverted now - window.location = $(parent.document.querySelector('#richdocumentsframe')).attr('src') - parent.OC.Apps.hideAppSidebar() - }, - - _restoreError: function() { - documentsMain.UI.notify(t('richdocuments', 'Failed to revert the document to older version')) - }, - - _restoreDAV: function(version) { - var restoreUrl = OC.linkToRemoteBase('dav') + '/versions/' + getCurrentUser().uid - + '/versions/' + documentsMain.originalFileId + '/' + version - $.ajax({ - type: 'MOVE', - url: restoreUrl, - headers: { - Destination: OC.linkToRemote('dav') + '/versions/' + getCurrentUser().uid + '/restore/target' - }, - success: this._restoreSuccess, - error: this._restoreError - }) - }, - showEditor: function(title, fileId, action) { - if (documentsMain.loadError) { - documentsMain.onEditorShutdown(documentsMain.loadErrorMessage + '\n' + documentsMain.loadErrorHint) - return - } - if (!documentsMain.renderComplete) { - setTimeout(function() { documentsMain.UI.showEditor(title, action) }, 10) + setTimeout(function() { documentsMain.UI.showEditor(title, fileId, action) }, 10) console.debug('Waiting for page to render…') return } @@ -486,29 +151,13 @@ const documentsMain = { OC.Util.History.addOnPopStateHandler(_.bind(documentsMain.onClose)) OC.Util.History.pushState() - parent.postMessage('loading', '*') + PostMessages.sendPostMessage('parent', 'loading') hideLoadingIndicator() $(document.body).addClass('claro') $(document.body).prepend(documentsMain.UI.container) - // WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud) - var wopiurl = window.location.protocol + '//' + window.location.host + getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + documentsMain.fileId - var wopisrc = encodeURIComponent(wopiurl) - - // urlsrc - the URL from discovery xml that we access for the particular - // document; we add various parameters to that. - // The discovery is available at - // https://<loolwsd-server>:9980/hosting/discovery - var urlsrc = documentsMain.urlsrc - + 'WOPISrc=' + wopisrc - + '&title=' + encodeURIComponent(title) - + '&lang=' + languageToBCP47() - + '&closebutton=1' - + '&revisionhistory=1' - if (!documentsMain.canEdit || action === 'view') { - urlsrc += '&permission=readonly' - } + const urlsrc = getWopiUrl({ fileId, title, readOnly: false, closeButton: true, revisionHistory: true }) // access_token - must be passed via a form post var accessToken = encodeURIComponent(documentsMain.token) @@ -525,55 +174,42 @@ const documentsMain = { // Listen for App_LoadingStatus as soon as possible $('#loleafletframe').ready(function() { - var editorInitListener = function(e) { - var msg = {} - try { - msg = JSON.parse(e.data) - } catch (e) { + const editorInitListener = ({ parsed, data }) => { + console.debug('[document] editorInitListener: Received post message ', parsed) + const { msgId, args } = parsed + + if (msgId !== 'App_LoadingStatus') { return } - if (msg.MessageId === 'App_LoadingStatus') { - if (msg.Values.Status === 'Frame_Ready') { - documentsMain.isFrameReady = true - documentsMain.wopiClientFeatures = msg.Values.Features - - // Forward to mobile handler - if (window.RichDocumentsMobileInterface) { - window.RichDocumentsMobileInterface.documentLoaded() - } - // iOS webkit fallback - if (window.webkit - && window.webkit.messageHandlers - && window.webkit.messageHandlers.RichDocumentsMobileInterface) { - window.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage('documentLoaded') - } - } else if (msg.Values.Status === 'Document_Loaded') { - window.removeEventListener('message', editorInitListener, false) - if (documentsMain.getFileList()) { - documentsMain.getFileModel() - } - // Hide buttons when using the mobile app integration - if ( - window.RichDocumentsMobileInterface - || (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface) - ) { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Hide_Button', { id: 'fullscreen' }) - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Hide_Menu_Item', { id: 'fullscreen' }) - } - } else if (msg.Values.Status === 'Failed') { - // Loading failed but editor shows the error - documentsMain.isFrameReady = true - } else if (msg.Values.Status === 'Timeout') { - // Timeout - no response from the editor - documentsMain.onClose() - parent.OC.Notification.showTemporary(t('richdocuments', 'Failed to connect to {productName}. Please try again later or contact your server administrator.', - { productName: OC.getCapabilities().richdocuments.productName } - )) + // Pass though all messages to viewer.js if not direct editing + if (!isDirectEditing()) { + PostMessages.sendPostMessage('parent', data) + } + + switch (args.Status) { + case 'Frame_Ready': + documentsMain.isFrameReady = true + documentsMain.wopiClientFeatures = args.Features + callMobileMessage('documentLoaded') + break + case 'Document_Loaded': + PostMessages.unregisterPostMessageHandler(editorInitListener) + + // Hide buttons when using the mobile app integration + if (isDirectEditing) { + PostMessages.sendWOPIPostMessage('loolframe', 'Hide_Button', { id: 'fullscreen' }) + PostMessages.sendWOPIPostMessage('loolframe', 'Hide_Menu_Item', { id: 'fullscreen' }) } + break + case 'Failed': + // Loading failed but editor shows the error + documentsMain.isFrameReady = true + break } } - window.addEventListener('message', editorInitListener, false) + + PostMessages.registerPostMessageHandler(editorInitListener) // In case of editor inactivity setTimeout(function() { @@ -584,100 +220,111 @@ const documentsMain = { }) $('#loleafletframe').load(function() { - // And start listening to incoming post messages - window.addEventListener('message', function(e) { - if (documentsMain.isViewerMode) { + const ViewerToLool = [ + 'Action_FollowUser', + 'Host_VersionRestore', + 'Action_RemoveView' + ] + PostMessages.registerPostMessageHandler(({ parsed, data }) => { + console.debug('[document] Received post message ', parsed) + const { msgId, args, deprecated } = parsed + + if (deprecated) { return } - try { - var msg = JSON.parse(e.data) - var msgId = msg.MessageId - var args = msg.Values - var deprecated = !!args.Deprecated - } catch (exc) { - msgId = e.data - } - - if (msgId === 'Download_As') { - console.debug('download for ' + args.Type + '. Use this url: ' + args.URL) - if ( - window.RichDocumentsMobileInterface - || (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface) - ) { - documentsMain.callMobileMessage('downloadAs', args) + if (documentsMain.isViewerMode) { + let { fileId, title, version } = args + switch (parsed.msgId) { + case 'Action_loadRevViewer': + documentsMain.UI.loadRevViewerContainer() + if (fileId) { + fileId += '_' + Config.get('instanceId') + if (version) { + fileId += `_${version}` + title += `_${version}` + } + documentsMain.UI.showViewer( + fileId, title + ) + } + break + case 'App_VersionRestore': + if (!documentsMain.$deferredVersionRestoreAck) { + console.warn('No version restore deferred object found.') + return + } + break + case 'Pre_Restore_Ack': + // user instructed to restore the version + documentsMain.$deferredVersionRestoreAck.resolve() + break + default: return } - } else if (msgId === 'File_Rename') { - documentsMain.fileModel = null - documentsMain.fileName = args.NewName - if ( - window.RichDocumentsMobileInterface - || (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface) - ) { - documentsMain.callMobileMessage('fileRename', args) - } else { - documentsMain.getFileList().reload() - parent.OC.Apps.hideAppSidebar() - } - return + } - // Check for webview handler - if (window.RichDocumentsMobileInterface) { - if (msgId === 'UI_Close') { - window.RichDocumentsMobileInterface.close() - } else if (msgId === 'UI_InsertGraphic') { - window.RichDocumentsMobileInterface.insertGraphic() - } else if (msgId === 'UI_Share') { - window.RichDocumentsMobileInterface.share() + // Pass all messages to viewer if not direct editing or + if (!isDirectEditing() && ViewerToLool.indexOf(msgId) === -1) { + PostMessages.sendPostMessage('parent', data) + } + // Pass messages from viewer to lool + if (ViewerToLool.indexOf(msgId) >= 0) { + return PostMessages.sendPostMessage('loolframe', data) + } + + if (isMobileInterfaceAvailable()) { + if (msgId === 'Download_As') { + return callMobileMessage('downloadAs', args) } - // Fallback to web UI for SaveAs - if (msgId !== 'UI_SaveAs') { + if (msgId === 'File_Rename') { + return callMobileMessage('fileRename', args) + } else if (msgId === 'UI_Paste') { + documentsMain.callMobileMessage('paste') return } - } - - // iOS webkit fallback - if (window.webkit - && window.webkit.messageHandlers - && window.webkit.messageHandlers.RichDocumentsMobileInterface) { if (msgId === 'UI_Close') { - window.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage('close') + callMobileMessage('close') } else if (msgId === 'UI_InsertGraphic') { - window.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage('insertGraphic') + callMobileMessage('insertGraphic') } else if (msgId === 'UI_Share') { - window.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage('share') + callMobileMessage('share') } - // Fallback to web UI for SaveAs + // Fallback to web UI for SaveAs, otherwise ignore other post messages if (msgId !== 'UI_SaveAs') { return } } - if (msgId === 'UI_Close' || msgId === 'close' /* deprecated */) { - // If a postmesage API is deprecated, we must ignore it and wait for the standard postmessage - // (or it might already have been fired) - if (deprecated) { return } - + switch (parsed.msgId) { + case 'UI_Close': + case 'close': documentsMain.onClose() - } else if (msgId === 'UI_FileVersions' || msgId === 'rev-history' /* deprecated */) { - if (deprecated) { return } - - documentsMain.UI.showRevHistory(documentsMain.fullPath) - } else if (msgId === 'UI_Share') { - if (documentsMain.getFileList()) { - documentsMain.getFileList().showDetailsView(documentsMain.fileName, 'shareTabView') - parent.OC.Apps.showAppSidebar() - } - } else if (msgId === 'UI_SaveAs') { + break + // Messages received from the viewer + case 'postAsset': + documentsMain.postAsset(args.FileName, args.Url) + break + case 'UI_FileVersions': + case 'rev-history': + documentsMain.UI.loadRevViewerContainer() + documentsMain.UI.showViewer( + documentsMain.fileId, documentsMain.title + ) + break + default: + console.debug('[document] Unhandled post message', parsed) + } + + if (msgId === 'UI_SaveAs') { // TODO Move to file picker dialog with input field OC.dialogs.prompt( t('richdocuments', 'Please enter the filename to store the document as.'), t('richdocuments', 'Save As'), function(result, value) { if (result === true && value) { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Action_SaveAs', { 'Filename': value }) + PostMessages.sendWOPIPostMessage('loolframe', 'Action_SaveAs', { 'Filename': value }) } }, true, @@ -689,221 +336,57 @@ const documentsMain = { $buttons.eq(0).text(t('richdocuments', 'Cancel')) $buttons.eq(1).text(t('richdocuments', 'Save')) }) - } else if (msgId === 'UI_CreateFile') { - documentsMain.UI.createNewFile(args.DocumentType) - - } else if (msgId === 'UI_InsertGraphic') { - parent.OC.dialogs.filepicker(t('richdocuments', 'Insert from {name}', { name: OC.theme.name }), function(path, type) { - if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) { - var filename = path.substring(path.lastIndexOf('/') + 1) - $.ajax({ - type: 'POST', - url: OC.generateUrl('apps/richdocuments/assets'), - data: { - path: path - } - }).done(function(resp) { - documentsMain.postAsset(filename, resp.url) - }) - } - }, false, ['image/png', 'image/gif', 'image/jpeg', 'image/svg'], true, OC.dialogs.FILEPICKER_TYPE_CHOOSE) - } else if (msgId === 'App_VersionRestore') { - if (!documentsMain.$deferredVersionRestoreAck) { - console.warn('No version restore deferred object found.') - return - } - - if (args.Status === 'Pre_Restore_Ack') { - // user instructed to restore the version - documentsMain.$deferredVersionRestoreAck.resolve() - } - } else if (msgId === 'View_Added') { - if (deprecated) { return } - - documentsMain.UI.views[args.ViewId] = args - documentsMain.UI.renderAvatars() - } else if (msgId === 'View_Removed') { - if (deprecated) { return } - - delete documentsMain.UI.views[args.ViewId] - documentsMain.UI.renderAvatars() - } else if (msgId === 'Get_Views_Resp' || msgId === 'Views_List') { - documentsMain.UI.views = {} - args.forEach(function(view) { - documentsMain.UI.views[view.ViewId] = view - }) - documentsMain.UI.renderAvatars() - } else if (msgId === 'FollowUser_Changed') { - if (args.IsFollowEditor) { - documentsMain.UI.followingEditor = true - } else { - documentsMain.UI.followingEditor = false - } - if (args.IsFollowUser) { - documentsMain.UI.following = args.FollowedViewId - } else { - documentsMain.UI.following = null - } - documentsMain.UI.renderAvatars() } }) // Tell the LOOL iframe that we are ready now - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Host_PostmessageReady', {}) + PostMessages.sendWOPIPostMessage('loolframe', 'Host_PostmessageReady', {}) }) // submit that $('#loleafletform').submit() }, - /* Ask for a new filename and open the files app in a new tab - * the parameters richdocuments_create and richdocuments_filename are - * parsed by viewer.js and open a template picker in the new tab - */ - createNewFile: function(type) { - parent.OC.dialogs.prompt( - t('richdocuments', 'Please enter the filename for the new document'), - t('richdocuments', 'Save As'), - function(result, value) { - if (result === true && value) { - if (type === 'text') { - type = 'document' - } - var dir = parent.$('#dir').val() - var url = OC.generateUrl('/apps/files/?dir=' + dir + '&richdocuments_create=' + type + '&richdocuments_filename=' + encodeURI(value)) - window.open(url, '_blank') - } - }, - true, - t('richdocuments', 'New filename'), - false - ).then(function() { - var $dialog = parent.$('.oc-dialog:visible') - var $buttons = $dialog.find('button') - $buttons.eq(0).text(t('richdocuments', 'Cancel')) - $buttons.eq(1).text(t('richdocuments', 'Create a new document')) - }) - }, - hideEditor: function() { // Fade out editor $('#mainContainer').fadeOut('fast', function() { $('#mainContainer').remove() $('#content-wrapper').fadeIn('fast') $(document.body).removeClass('claro') - parent.document.title = documentsMain.UI.mainTitle }) - }, - - notify: function(message) { - OC.Notification.show(message) - setTimeout(OC.Notification.hide, 10000) } }, onStartup: function() { var fileId - documentsMain.UI.init() // Does anything indicate that we need to autostart a session? - fileId = getURLParameter('fileId').replace(/^\W*/, '') + fileId = window.getURLParameter('fileId').replace(/^\W*/, '') if (fileId && Number.isInteger(Number(fileId)) && $('#nickname').length === 0) { - documentsMain.prepareSession() + documentsMain.isEditorMode = true documentsMain.originalFileId = fileId } documentsMain.ready = true }, - WOPIPostMessage: function(iframe, msgId, values) { - if (iframe) { - var msg = { - 'MessageId': msgId, - 'SendTime': Date.now(), - 'Values': values - } - - iframe.contentWindow.postMessage(JSON.stringify(msg), '*') - } - }, - - callMobileMessage: function(messageName, attributes) { - var message = messageName - if (typeof attributes !== 'undefined') { - message = { - MessageName: messageName, - Values: attributes - } - } - // Forward to mobile handler - if (window.RichDocumentsMobileInterface && typeof window.RichDocumentsMobileInterface[messageName] === 'function') { - window.RichDocumentsMobileInterface[messageName](JSON.stringify(attributes)) - } - - // iOS webkit fallback - if (window.webkit - && window.webkit.messageHandlers - && window.webkit.messageHandlers.RichDocumentsMobileInterface) { - window.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage(message) - } - }, - - prepareSession: function() { - documentsMain.isEditorMode = true - }, - initSession: function() { - documentsMain.urlsrc = richdocuments_urlsrc - documentsMain.fullPath = richdocuments_path - documentsMain.token = richdocuments_token + documentsMain.urlsrc = Config.get('urlsrc') + documentsMain.fullPath = Config.get('path') + documentsMain.token = Config.get('token') + documentsMain.fileId = Config.get('fileId') + documentsMain.fileName = Config.get('title') + documentsMain.canEdit = Boolean(Config.get('permissions') & OC.PERMISSION_UPDATE) + documentsMain.canShare = typeof OC.Share !== 'undefined' && Config.get('permissions') & OC.PERMISSION_SHARE $('footer,nav').hide() - $(documentsMain.toolbar).appendTo('#header') - - documentsMain.canShare = typeof OC.Share !== 'undefined' && richdocuments_permissions & OC.PERMISSION_SHARE - // fade out file list and show the document $('#content-wrapper').fadeOut('fast').promise().done(function() { - - documentsMain.fileId = richdocuments_fileId - documentsMain.fileName = richdocuments_title - - documentsMain.canEdit = Boolean(richdocuments_permissions & OC.PERMISSION_UPDATE) - documentsMain.loadDocument(documentsMain.fileName, documentsMain.fileId) }) }, - getFileModel: function() { - if (documentsMain.getFileList() && documentsMain.getFileList()._detailsView && documentsMain.getFileList()._detailsView.getFileInfo()) { - if (documentsMain.fileModel && documentsMain.fileModel !== documentsMain.getFileList()._detailsView.getFileInfo()) { - documentsMain.fileModel = documentsMain.getFileList()._detailsView.getFileInfo() - documentsMain.fileModel.on('change', function() { - documentsMain.UI._addHeaderFileActions() - }) - } - } - - if (documentsMain.fileModel) { - return documentsMain.fileModel - } - if (documentsMain.getFileList()) { - documentsMain.getFileList().scrollTo([documentsMain.fileName, '']) - var fileModel = documentsMain.getFileList().getModelForFile(documentsMain.fileName) - - if (fileModel) { - fileModel.on('change', function() { - documentsMain.UI._addHeaderFileActions() - }) - documentsMain.fileModel = fileModel - documentsMain.UI._addHeaderFileActions() - } else { - setTimeout(documentsMain.getFileModel, 500) - } - } - }, - loadDocument: function(title, fileId) { documentsMain.UI.showEditor(title, fileId, 'write') }, @@ -915,7 +398,6 @@ const documentsMain = { $(window).off('unload') if (documentsMain.isEditorMode) { documentsMain.isEditorMode = false - parent.location.hash = '' } else { setTimeout(OC.Notification.hide, 7000) } @@ -928,15 +410,11 @@ const documentsMain = { documentsMain.isEditorMode = false $(window).off('beforeunload') $(window).off('unload') - parent.location.hash = '' $('footer,nav').show() documentsMain.UI.hideEditor() - $('#ocToolbar').remove() - parent.document.title = documentsMain.UI.mainTitle - parent.postMessage('close', '*') - documentsMain.UI.removeVersionSidebarEvents() + PostMessages.sendPostMessage('parent', 'close', '*') }, onCloseViewer: function() { @@ -945,22 +423,19 @@ const documentsMain = { $('#revPanelContainer').remove() $('#revViewerContainer').remove() documentsMain.isViewerMode = false - documentsMain.UI.revisionsStart = 0 - parent.$('#versionsTabView .active').removeClass('active') - parent.$('#versionsTabView #currentVersion').remove() - parent.OC.Apps.hideAppSidebar() + $('#loleafletframe').focus() }, postAsset: function(filename, url) { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Action_InsertGraphic', { + PostMessages.sendWOPIPostMessage('loolframe', 'Action_InsertGraphic', { filename: filename, url: url }) }, postGrabFocus: function() { - documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Grab_Focus') + PostMessages.sendWOPIPostMessage('loolframe', 'Grab_Focus') } } @@ -987,4 +462,6 @@ $(document).ready(function() { viewport.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') documentsMain.onStartup() + + window.documentsMain = documentsMain }) diff --git a/src/helpers/guestName.js b/src/helpers/guestName.js index d7c6aae4..806d8039 100644 --- a/src/helpers/guestName.js +++ b/src/helpers/guestName.js @@ -20,8 +20,7 @@ * */ -/* global richdocuments_permissions */ - +import Config from './../services/config' import { getCurrentUser } from 'nextcloud-auth' import mobile from './mobile' @@ -50,8 +49,9 @@ const setGuestNameCookie = function(username) { const shouldAskForGuestName = () => { return !mobile.isDirectEditing() && getCurrentUser().uid === null + && Config.get('userId') === null && getGuestNameCookie() === '' - && (richdocuments_permissions & OC.PERMISSION_UPDATE) + && (Config.get('permissions') & OC.PERMISSION_UPDATE) } export { diff --git a/src/helpers/index.js b/src/helpers/index.js index 6f9ea750..177a935b 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -28,6 +28,11 @@ const languageToBCP47 = () => { .replace(/^([a-z]{2}).*_([A-Z]{2})$/, (match, p1, p2) => p1 + '-' + p2.toLowerCase()) } +const getNextcloudVersion = () => { + return parseInt(OC.config.version.split('.')[0]) +} + export { - languageToBCP47 + languageToBCP47, + getNextcloudVersion } diff --git a/src/helpers/mobile.js b/src/helpers/mobile.js index 34211883..66979009 100644 --- a/src/helpers/mobile.js +++ b/src/helpers/mobile.js @@ -20,10 +20,58 @@ * */ -/* global richdocuments_directEdit */ +import Config from './../services/config' -const isDirectEditing = () => richdocuments_directEdit +const isDirectEditing = () => Config.get('directEdit') + +const isMobileInterfaceAvailable = () => window.RichDocumentsMobileInterface + || (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface) + +const isMobileInterfaceOnIos = () => window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.RichDocumentsMobileInterface + +const isMobileInterfaceOnAndroid = () => window.RichDocumentsMobileInterface + +const callMobileMessage = (messageName, attributes) => { + console.debug('callMobileMessage', messageName, attributes) + let message = messageName + if (typeof attributes !== 'undefined') { + message = { + MessageName: messageName, + Values: attributes + } + } + let attributesString = null + try { + attributesString = JSON.stringify(attributes) + } catch (e) { + attributesString = null + } + // Forward to mobile handler + if (window.RichDocumentsMobileInterface && typeof window.RichDocumentsMobileInterface[messageName] === 'function') { + if (attributesString === null || typeof attributesString === 'undefined') { + window.RichDocumentsMobileInterface[messageName]() + } else { + window.RichDocumentsMobileInterface[messageName](attributesString) + } + } + + // iOS webkit fallback + if (window.webkit + && window.webkit.messageHandlers + && window.webkit.messageHandlers.RichDocumentsMobileInterface) { + window.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage(message) + } +} + +export default { + isDirectEditing, + callMobileMessage +} export { - isDirectEditing + isDirectEditing, + callMobileMessage, + isMobileInterfaceAvailable, + isMobileInterfaceOnAndroid, + isMobileInterfaceOnIos } diff --git a/src/helpers/types.js b/src/helpers/types.js new file mode 100644 index 00000000..597f7c06 --- /dev/null +++ b/src/helpers/types.js @@ -0,0 +1,60 @@ + +/* + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +const getFileType = (document, ooxml) => { + let documentTypes = { + document: { + extension: 'odt', + mime: 'application/vnd.oasis.opendocument.text' + }, + spreadsheet: { + extension: 'ods', + mime: 'application/vnd.oasis.opendocument.spreadsheet' + }, + presentation: { + extension: 'odp', + mime: 'application/vnd.oasis.opendocument.presentation' + } + } + if (ooxml) { + documentTypes = { + document: { + extension: 'docx', + mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }, + spreadsheet: { + extension: 'xlsx', + mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }, + presentation: { + extension: 'pptx', + mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + } + } + } + return documentTypes[document] +} + +export default { + getFileType +} diff --git a/src/helpers/url.js b/src/helpers/url.js index 49e9c006..777c0218 100644 --- a/src/helpers/url.js +++ b/src/helpers/url.js @@ -20,6 +20,10 @@ * */ +import { getRootUrl } from 'nextcloud-router' +import { languageToBCP47 } from './index' +import Config from './../services/config' + const getSearchParam = (name) => { var results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href) if (results === null) { @@ -28,4 +32,65 @@ const getSearchParam = (name) => { return decodeURI(results[1]) || 0 } -export { getSearchParam } +const getWopiUrl = ({ fileId, title, readOnly, closeButton, revisionHistory }) => { + // WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud) + // index.php is forced here to avoid different wopi srcs for the same document + const wopiurl = window.location.protocol + '//' + window.location.host + getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + fileId + console.debug('[getWopiUrl] ' + wopiurl) + const wopisrc = encodeURIComponent(wopiurl) + + // urlsrc - the URL from discovery xml that we access for the particular + // document; we add various parameters to that. + // The discovery is available at + // https://<loolwsd-server>:9980/hosting/discovery + return Config.get('urlsrc') + + 'WOPISrc=' + wopisrc + + '&title=' + encodeURIComponent(title) + + '&lang=' + languageToBCP47() + + (closeButton ? '&closebutton=1' : '') + + (revisionHistory ? '&revisionhistory=1' : '') + + (readOnly ? '&permission=readonly' : '') +} + +const getDocumentUrlFromTemplate = (templateId, fileName, fileDir, fillWithTemplate) => { + return OC.generateUrl( + 'apps/richdocuments/indexTemplate?templateId={templateId}&fileName={fileName}&dir={dir}&requesttoken={requesttoken}', + { + templateId: templateId, + fileName: fileName, + dir: fileDir, + requesttoken: OC.requestToken + } + ) +} + +const getDocumentUrlForPublicFile = (fileName, fileId) => { + return OC.generateUrl( + 'apps/richdocuments/public?shareToken={shareToken}&fileName={fileName}&requesttoken={requesttoken}&fileId={fileId}', + { + shareToken: document.getElementById('sharingToken').value, + fileName: fileName, + fileId: fileId, + requesttoken: OC.requestToken + } + ) +} + +const getDocumentUrlForFile = (fileDir, fileId) => { + return OC.generateUrl( + 'apps/richdocuments/index?fileId={fileId}&requesttoken={requesttoken}', + { + fileId: fileId, + dir: fileDir, + requesttoken: OC.requestToken + }) +} + +export { + getSearchParam, + getWopiUrl, + + getDocumentUrlFromTemplate, + getDocumentUrlForPublicFile, + getDocumentUrlForFile +} diff --git a/src/services/config.tsx b/src/services/config.tsx new file mode 100644 index 00000000..0789a97e --- /dev/null +++ b/src/services/config.tsx @@ -0,0 +1,53 @@ + +/* + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +class ConfigService { + private values: {[name: string]: string} + constructor () { + this.values = {} + this.loadFromGlobal('userId') + this.loadFromGlobal('urlsrc') + this.loadFromGlobal('directEdit') + this.loadFromGlobal('permissions') + this.loadFromGlobal('instanceId') + + } + loadFromGlobal(key: string) { + // @ts-ignore + this.values[key] = window['richdocuments_' + key] + } + update(key: string, value: string) { + // @ts-ignore + this.values[key] = value + } + get(key: string) { + if (typeof this.values[key] === 'undefined') { + this.loadFromGlobal(key) + } + return this.values[key] + } +} + +const Config = new ConfigService() + +export default Config diff --git a/src/services/postMessage.tsx b/src/services/postMessage.tsx new file mode 100644 index 00000000..ccdd6933 --- /dev/null +++ b/src/services/postMessage.tsx @@ -0,0 +1,107 @@ +/* + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +type MessageEventSource = Window | MessagePort | ServiceWorker; + +export interface WopiPost { + MessageId: string; + Values: WopiPostValues; +} + +export interface WopiPostValues { + Deprecated?: boolean; +} + +interface WindowCallbackHandler { (): Window} + +export default class PostMessageService { + private readonly targets: {[name: string]: (Window|WindowCallbackHandler)}; + private postMessageHandlers: Function[] = []; + + constructor(targets: {[name: string]: (Window|WindowCallbackHandler)}) { + this.targets = targets + window.addEventListener('message', (event: {source: MessageEventSource, data: any, origin: string}) => { + this.handlePostMessage(event.data) + }, false) + } + + sendPostMessage(target: string, message: any, targetOrigin: string = '*') { + let targetElement: Window; + if (typeof this.targets[target] === 'function') { + targetElement = (this.targets[target] as WindowCallbackHandler)() + } else { + targetElement = this.targets[target] as Window + } + targetElement.postMessage(message, targetOrigin) + console.debug('PostMessageService.sendPostMessage', target, message) + } + + + sendWOPIPostMessage(target: string, msgId: string, values: any = {}) { + const msg = { + MessageId: msgId, + SendTime: Date.now(), + Values: values + } + + this.sendPostMessage(target, JSON.stringify(msg)) + } + + private static parsePostMessage(data: any) { + let msgId: string, + args: WopiPostValues, + deprecated: boolean + + try { + const msg: WopiPost = JSON.parse(data) + msgId = msg.MessageId + args = msg.Values + deprecated = !!msg.Values.Deprecated + } catch (exc) { + msgId = data + } + return { msgId, args, deprecated } + } + + registerPostMessageHandler(callback: Function) { + this.postMessageHandlers.push(callback) + } + + unregisterPostMessageHandler(callback: Function) { + const handlerIndex = this.postMessageHandlers.findIndex(cb => cb === callback) + delete this.postMessageHandlers[handlerIndex] + } + + private handlePostMessage(data: any) { + this.postMessageHandlers.forEach((fn: Function): void => { + const parsed = PostMessageService.parsePostMessage(data); + if (parsed.deprecated) { + console.debug('PostMessageService.handlePostMessage', 'Ignoring deprecated post message', parsed.msgId) + return; + } + fn({ + data: data, + parsed + }) + }) + } + +} diff --git a/src/view/FilesAppIntegration.js b/src/view/FilesAppIntegration.js new file mode 100644 index 00000000..53bee506 --- /dev/null +++ b/src/view/FilesAppIntegration.js @@ -0,0 +1,447 @@ +/* + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Config from '../services/config' + +let documentsMain = null +const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1' + +export default { + + fileModel: null, + /* Views: people currently editing the file */ + views: {}, + + followingEditor: false, + + following: null, + + init({ fileName, fileId, sendPostMessage }) { + this.fileName = fileName + this.fileId = fileId + this.sendPostMessage = sendPostMessage + + if (typeof this.getFileList() !== 'undefined') { + this.getFileModel() + } + + const headerRight = document.querySelector('#header .header-right') + const richdocumentsHeader = document.createElement('div') + richdocumentsHeader.id = 'richdocuments-header' + headerRight.insertBefore(richdocumentsHeader, headerRight.firstChild) + + this._addAvatarList() + if (!isPublic) { + this._addHeaderShareButton() + this._addHeaderFileActions() + this.addVersionSidebarEvents() + } + }, + + initAfterReady() { + documentsMain = document.getElementById('richdocumentsframe').contentWindow.documentsMain + }, + + close() { + this.fileModel = null + if (!isPublic) { + this.removeVersionSidebarEvents() + } + $('#richdocuments-header').remove() + }, + + share() { + if (isPublic) { + console.error('[FilesAppIntegration] Sharing is not supported') + } + FileList.showDetailsView(this.fileName, 'shareTabView') + OC.Apps.showAppSidebar() + }, + + insertGraphic(callback) { + if (isPublic) { + console.error('[FilesAppIntegration] insertGraphic is not supported') + } + OC.dialogs.filepicker(t('richdocuments', 'Insert from {name}', { name: OC.theme.name }), function(path, type) { + if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) { + const filename = path.substring(path.lastIndexOf('/') + 1) + $.ajax({ + type: 'POST', + url: OC.generateUrl('apps/richdocuments/assets'), + data: { + path: path + } + }).done(function(resp) { + callback(filename, resp.url) + }) + } + }, false, ['image/png', 'image/gif', 'image/jpeg', 'image/svg'], true, OC.dialogs.FILEPICKER_TYPE_CHOOSE) + }, + + getFileList() { + if (OCA.Files.App) { + return OCA.Files.App.fileList + } + if (OCA.Sharing.PublicApp) { + return OCA.Sharing.PublicApp.fileList + } + return null + }, + + getFileModel() { + if (this.fileModel !== null) { + return this.fileModel + } + this.getFileList()._updateDetailsView(this.fileName, false) + this.fileModel = this.getFileList().getModelForFile(this.fileName) + + if (this.fileModel !== null) { + this.fileModel.on('change', () => { + this._addHeaderFileActions() + }) + } + + return this.fileModel + }, + + setViews(views) { + this.views = {} + views.forEach((view) => { + this.views[view.ViewId] = view + }) + this.renderAvatars() + }, + + followReset(event) { + this.sendPostMessage('Action_FollowUser', { Follow: false }) + this.following = null + this.followingEditor = false + this.renderAvatars() + }, + followCurrentEditor(event) { + this.sendPostMessage('Action_FollowUser', { Follow: true }) + this.following = null + this.followingEditor = true + this.renderAvatars() + }, + followView(view) { + this.sendPostMessage('Action_FollowUser', { ViewId: view.ViewId, Follow: true }) + this.following = view.ViewId + this.followingEditor = false + this.renderAvatars() + }, + + _addAvatarList() { + // Add the avatar toolbar if possible + const avatarList = $('<div id="richdocuments-avatars">') + avatarList.on('click', function(e) { + e.stopPropagation() + $('#editors-menu').toggle() + }) + $('#richdocuments-header').append(avatarList) + }, + + _addHeaderShareButton() { + if ($('header').length) { + var $button = $('<div id="richdocuments-sharing"><a class="icon-shared icon-white"></a></div>') + $('#richdocuments-header').append($button) + $button.on('click', () => { + if (!$('#app-sidebar').is(':visible')) { + return this.share() + } + OC.Apps.hideAppSidebar() + }) + $('.searchbox').hide() + } + }, + + _addHeaderFileActions() { + console.debug('[FilesAppIntegration] Adding header file actions') + OC.unregisterMenu($('#richdocuments-actions .icon-more'), $('#richdocuments-actions-menu')) + $('#richdocuments-actions').remove() + var actionsContainer = $('<div id="richdocuments-actions"><div class="icon-more icon-white"></div><ul id="richdocuments-actions-menu" class="popovermenu"></ul></div>') + var actions = actionsContainer.find('#richdocuments-actions-menu').empty() + + var context = { + '$file': this.getFileList().$el.find('[data-id=' + this.originalFileId + ']').first(), + fileActions: this.getFileList().fileActions, + fileList: this.getFileList(), + fileInfoModel: this.getFileModel() + } + + const isFavorite = function(fileInfo) { + return fileInfo.get('tags') && fileInfo.get('tags').indexOf(OC.TAG_FAVORITE) >= 0 + } + const $favorite = $('<li><a></a></li>').click((event) => { + $favorite.find('a').removeClass('icon-starred').removeClass('icon-star-dark').addClass('icon-loading-small') + this.getFileList().fileActions.triggerAction('Favorite', this.getFileModel(), this.getFileList()) + this.getFileModel().trigger('change', this.getFileModel()) + }) + if (isFavorite(context.fileInfoModel)) { + $favorite.find('a').text(t('files', 'Remove from favorites')) + $favorite.find('a').addClass('icon-starred') + } else { + $favorite.find('a').text(t('files', 'Add to favorites')) + $favorite.find('a').addClass('icon-star-dark') + } + + var $info = $('<li><a class="icon-info"></a></li>').click(() => { + this.getFileList().fileActions.actions.all.Details.action(this.fileName, context) + OC.hideMenus() + }) + $info.find('a').text(t('files', 'Details')) + var $download = $('<li><a class="icon-download">Download</a></li>').click(() => { + this.getFileList().fileActions.actions.all.Download.action(this.fileName, context) + OC.hideMenus() + }) + $download.find('a').text(t('files', 'Download')) + actions.append($favorite).append($info).append($download) + $('#richdocuments-header').append(actionsContainer) + OC.registerMenu($('#richdocuments-actions .icon-more'), $('#richdocuments-actions-menu'), false, true) + }, + + /** + * @param {View} view + * @private + */ + _userEntry: function(view) { + var entry = $('<li></li>') + entry.append(this._avatarForView(view)) + + var label = $('<div class="label"></div>') + label.text(view.UserName) + if (view.ReadOnly === '1') { + label.text(view.UserName + ' ' + t('richdocuments', '(read only)')) + + } + label.click((event) => { + event.stopPropagation() + this.followView(view) + }) + if (this.following === view.ViewId) { + $('#editors-menu').find('li').removeClass('active') + entry.addClass('active') + } + entry.append(label) + + var isFileOwner = !isPublic && this.getFileModel() && typeof this.getFileModel().get('shareOwner') === 'undefined' + if (documentsMain.canEdit && isFileOwner && !view.IsCurrentView) { + var removeButton = $('<div class="icon-close" title="Remove user"/>') + removeButton.click(() => { + this.sendPostMessage('Action_RemoveView', { ViewId: view.ViewId }) + }) + entry.append(removeButton) + } + return entry + }, + + /** + * @param {View} view + * @returns {$|HTMLElement} + * @private + */ + _avatarForView: function(view) { + const userId = (view.UserId === '') ? view.UserName : view.UserId + var avatarContainer = $('<div class="richdocuments-avatar"><div class="avatar" title="' + view.UserName + '" data-user="' + userId + '"></div></div>') + var avatar = avatarContainer.find('.avatar') + avatar.css({ 'border-color': view.Color, + 'border-width': '2px', + 'border-style': 'solid' }) + if (view.ReadOnly === '1') { + avatarContainer.addClass('read-only') + $(avatar).attr('title', view.UserName + ' ' + t('richdocuments', '(read only)')) + } else { + $(avatar).attr('title', view.UserName) + } + + $(avatar).avatar(userId, 32, undefined, true, undefined, view.UserName) + return avatarContainer + }, + + renderAvatars: function() { + var avatardiv = $('#header .header-right #richdocuments-avatars') + avatardiv.empty() + var popover = $('<div id="editors-menu" class="popovermenu menu-center"><ul></ul></div>') + + var users = [] + // Add new avatars + var i = 0 + for (var viewId in this.views) { + /** + * @type {View} + */ + var view = this.views[viewId] + view.UserName = view.UserName !== '' ? view.UserName : t('richdocuments', 'Guest') + popover.find('ul').append(this._userEntry(view)) + + if (view.UserId === OC.currentUser) { + continue + } + if (view.UserId !== '' && users.indexOf(view.UserId) > -1) { + continue + } + users.push(view.UserId) + if (i++ < 3) { + avatardiv.append(this._avatarForView(view)) + } + } + var followCurrentEditor = $('<li><input type="checkbox" class="checkbox" /><label class="label">' + t('richdocuments', 'Follow current editor') + '</label></li>') + followCurrentEditor.find('label').click((event) => { + event.stopPropagation() + if (this.followingEditor) { + this.followReset() + } else { + this.followCurrentEditor() + } + }) + followCurrentEditor.find('.checkbox').prop('checked', this.followingEditor) + popover.find('ul').append(followCurrentEditor) + avatardiv.append(popover) + }, + + addVersionSidebarEvents() { + $(document.querySelector('#content')).on('click.revisions', '#app-sidebar .preview-container', this.showVersionPreview.bind(this)) + $(document.querySelector('#content')).on('click.revisions', '#app-sidebar .downloadVersion', this.showVersionPreview.bind(this)) + $(document.querySelector('#content')).on('mousedown.revisions', '#app-sidebar .revertVersion', this.restoreVersion.bind(this)) + }, + + removeVersionSidebarEvents() { + $(document.querySelector('#content')).off('click.revisions') + $(document.querySelector('#content')).off('click.revisions') + $(document.querySelector('#content')).off('mousedown.revisions') + }, + + addCurrentVersion() { + if (this.getFileModel()) { + const preview = OC.MimeType.getIconUrl(this.getFileModel().get('mimetype')) + const mtime = this.getFileModel().get('mtime') + $('#versionsTabView').prepend('<ul id="lastSavedVersion"><li data-revision="0"><div><div class="preview-container"><img src="' + preview + '" width="44" /></div><div class="version-container">\n' + + '<div><a class="downloadVersion">' + t('richdocuments', 'Last saved version') + '<span class="versiondate has-tooltip live-relative-timestamp" data-timestamp="' + mtime + '"></span></div></div></li></ul>') + $('#versionsTabView').prepend('<ul id="currentVersion"><li data-revision="" class="active"><div><div class="preview-container"><img src="' + preview + '" width="44" /></div><div class="version-container">\n' + + '<div><a class="downloadVersion">' + t('richdocuments', 'Current version') + '</a></div></div></li></ul>') + $('.live-relative-timestamp').each(function() { + $(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10))) + }) + } + }, + + showRevHistory() { + FileList.showDetailsView(this.fileName, 'versionsTabView') + this.addCurrentVersion() + }, + + showVersionPreview: function(e) { + e.preventDefault() + var element = e.currentTarget.parentElement.parentElement + if ($(e.currentTarget).hasClass('downloadVersion')) { + element = e.currentTarget.parentElement.parentElement.parentElement.parentElement + } + var version = element.dataset.revision + var fileId = this.fileId + var title = this.fileName + console.debug('[FilesAppIntegration] showVersionPreview', version, fileId, title) + this.sendPostMessage('Action_loadRevViewer', { fileId, title, version }) + $(element.parentElement.parentElement).find('li').removeClass('active') + $(element).addClass('active') + }, + + restoreVersion: function(e) { + var self = this + e.preventDefault() + e.stopPropagation() + + documentsMain.onCloseViewer() + + this.sendPostMessage('Host_VersionRestore', { Status: 'Pre_Restore' }) + + var version = e.currentTarget.parentElement.parentElement.dataset.revision + + documentsMain.$deferredVersionRestoreAck = $.Deferred() + $.when(documentsMain.$deferredVersionRestoreAck).done(function(args) { + self._restoreDAV(version) + }) + + // resolve the deferred object immediately if client doesn't support version states + if (!documentsMain.wopiClientFeatures || !documentsMain.wopiClientFeatures.VersionStates) { + documentsMain.$deferredVersionRestoreAck.resolve() + } + + return false + }, + + _restoreSuccess: function(response) { + if (response.status === 'error') { + OC.Notification.showTemporary(t('richdocuments', 'Failed to revert the document to older version')) + } + + // load the file again, it should get reverted now + window.location = $(parent.document.querySelector('#richdocumentsframe')).attr('src') + parent.OC.Apps.hideAppSidebar() + }, + + _restoreError: function() { + OC.Notification.showTemporary(t('richdocuments', 'Failed to revert the document to older version')) + }, + + _restoreDAV: function(version) { + var restoreUrl = OC.linkToRemoteBase('dav') + '/versions/' + Config.get('userId') + + '/versions/' + this.fileId + '/' + version + $.ajax({ + type: 'MOVE', + url: restoreUrl, + headers: { + Destination: OC.linkToRemote('dav') + '/versions/' + Config.get('userId') + '/restore/target' + }, + success: this._restoreSuccess, + error: this._restoreError + }) + }, + + /* Ask for a new filename and open the files app in a new tab + * the parameters richdocuments_create and richdocuments_filename are + * parsed by viewer.js and open a template picker in the new tab + */ + createNewFile: function(type) { + OC.dialogs.prompt( + t('richdocuments', 'Please enter the filename for the new document'), + t('richdocuments', 'Save As'), + function(result, value) { + if (result === true && value) { + if (type === 'text') { + type = 'document' + } + var dir = parent.$('#dir').val() + var url = OC.generateUrl('/apps/files/?dir=' + dir + '&richdocuments_create=' + type + '&richdocuments_filename=' + encodeURI(value)) + window.open(url, '_blank') + } + }, + true, + t('richdocuments', 'New filename'), + false + ).then(function() { + var $dialog = parent.$('.oc-dialog:visible') + var $buttons = $dialog.find('button') + $buttons.eq(0).text(t('richdocuments', 'Cancel')) + $buttons.eq(1).text(t('richdocuments', 'Create a new document')) + }) + } +} diff --git a/src/viewer.js b/src/viewer.js index e70a6dca..1b4480a6 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1,112 +1,85 @@ -import { getSearchParam } from './helpers/url' +import { getDocumentUrlFromTemplate, getDocumentUrlForPublicFile, getDocumentUrlForFile, getSearchParam } from './helpers/url' +import PostMessageService from './services/postMessage' +import Config from './services/config' +import Types from './helpers/types' +import FilesAppIntegration from './view/FilesAppIntegration' + +const FRAME_DOCUMENT = 'FRAME_DOCUMENT' +const PostMessages = new PostMessageService({ + FRAME_DOCUMENT: () => document.getElementById('richdocumentsframe').contentWindow +}) -var preloadType = getSearchParam('richdocuments_create') -var preloadFilename = getSearchParam('richdocuments_filename') -var Preload = { +const Preload = { create: { - type: preloadType, - filename: preloadFilename + type: getSearchParam('richdocuments_create'), + filename: getSearchParam('richdocuments_filename') } } -var odfViewer = { - isDocuments: false, - nextcloudVersion: 0, +const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1' + +const odfViewer = { + + open: false, receivedLoading: false, supportedMimes: OC.getCapabilities().richdocuments.mimetypes.concat(OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen), excludeMimeFromDefaultOpen: OC.getCapabilities().richdocuments.mimetypesNoDefaultOpen, - register: function() { - odfViewer.nextcloudVersion = parseInt(OC.config.version.split('.')[0]) - var i, mime - var editActionName = 'Edit with ' + OC.getCapabilities().richdocuments.productName - for (i = 0; i < odfViewer.supportedMimes.length; ++i) { - mime = odfViewer.supportedMimes[i] + + register() { + const EDIT_ACTION_NAME = 'Edit with ' + OC.getCapabilities().richdocuments.productName + for (let mime of odfViewer.supportedMimes) { OCA.Files.fileActions.register( mime, - editActionName, + EDIT_ACTION_NAME, OC.PERMISSION_UPDATE | OC.PERMISSION_READ, OC.imagePath('core', 'actions/rename'), - odfViewer.onEdit, + this.onEdit, t('richdocuments', 'Edit with {productName}', { productName: OC.getCapabilities().richdocuments.productName }) ) if (odfViewer.excludeMimeFromDefaultOpen.indexOf(mime) === -1) { - OCA.Files.fileActions.setDefault(mime, editActionName) + OCA.Files.fileActions.setDefault(mime, EDIT_ACTION_NAME) } } }, - dispatch: function(filename) { - odfViewer.onEdit(filename) - }, - - getNewDocumentFromTemplateUrl: function(templateId, fileName, fileDir, fillWithTemplate) { - return OC.generateUrl( - 'apps/richdocuments/indexTemplate?templateId={templateId}&fileName={fileName}&dir={dir}&requesttoken={requesttoken}', - { - templateId: templateId, - fileName: fileName, - dir: fileDir, - requesttoken: OC.requestToken - } - ) - }, - onEdit: function(fileName, context) { + if (odfViewer.open === true) { + return + } + odfViewer.open = true if (context) { var fileDir = context.dir var fileId = context.fileId || context.$file.attr('data-id') var templateId = context.templateId + FileList.setViewerMode(true) + FileList.setPageTitle(fileName) + FileList.showMask() } odfViewer.receivedLoading = false - var viewer - if ($('#isPublic').val() === '1') { - viewer = OC.generateUrl( - 'apps/richdocuments/public?shareToken={shareToken}&fileName={fileName}&requesttoken={requesttoken}&fileId={fileId}', - { - shareToken: $('#sharingToken').val(), - fileName: fileName, - fileId: fileId, - requesttoken: OC.requestToken - } - ) - } else { - // We are dealing with a template - if (typeof (templateId) !== 'undefined') { - viewer = this.getNewDocumentFromTemplateUrl(templateId, fileName, fileDir) - } else { - viewer = OC.generateUrl( - 'apps/richdocuments/index?fileId={fileId}&requesttoken={requesttoken}', - { - fileId: fileId, - dir: fileDir, - requesttoken: OC.requestToken - } - ) - } + let documentUrl = getDocumentUrlForFile(fileDir, fileId) + if (isPublic) { + documentUrl = getDocumentUrlForPublicFile(fileName, fileId) } - - if (context) { - FileList.setViewerMode(true) - FileList.setPageTitle(fileName) - FileList.showMask() + if (typeof (templateId) !== 'undefined') { + documentUrl = getDocumentUrlFromTemplate(templateId, fileName, fileDir) } OC.addStyle('richdocuments', 'mobile') - var $iframe = $('<iframe id="richdocumentsframe" nonce="' + btoa(OC.requestToken) + '" scrolling="no" allowfullscreen src="' + viewer + '" />') + var $iframe = $('<iframe id="richdocumentsframe" nonce="' + btoa(OC.requestToken) + '" scrolling="no" allowfullscreen src="' + documentUrl + '" />') odfViewer.loadingTimeout = setTimeout(function() { if (!odfViewer.receivedLoading) { odfViewer.onClose() OC.Notification.showTemporary(t('richdocuments', 'Failed to load {productName} - please try again later', { productName: OC.getCapabilities().richdocuments.productName || 'Collabora Online' })) } }, 15000) - $iframe.src = viewer + $iframe.src = documentUrl $('body').css('overscroll-behavior-y', 'none') var viewport = document.querySelector('meta[name=viewport]') viewport.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') - if ($('#isPublic').val()) { + if (isPublic) { // force the preview to adjust its height $('#preview').append($iframe).css({ height: '100%' }) $('body').css({ height: '100%' }) @@ -121,42 +94,43 @@ var odfViewer = { $('body').css('overflow', 'hidden') $('#app-content').append($iframe) $iframe.hide() - if ($('header').length) { - var $button = $('<div class="richdocuments-sharing"><a class="icon-shared icon-white"></a></div>') - $('.header-right').prepend($button) - $button.on('click', function() { - if ($('#app-sidebar').is(':visible')) { - OC.Apps.hideAppSidebar() - return - } - var frameFilename = $('#richdocumentsframe')[0].contentWindow.documentsMain.fileName - FileList.showDetailsView(frameFilename || fileName, 'shareTabView') - OC.Apps.showAppSidebar() - }) - $('.searchbox').hide() - } } $('#app-content #controls').addClass('hidden') + FilesAppIntegration.init({ + fileName, + fileId, + sendPostMessage: (msgId, values) => PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, msgId, values) + }) + }, + + onReceiveLoading() { + odfViewer.receivedLoading = true + $('#richdocumentsframe').show() + $('html, body').scrollTop(0) + $('#content').removeClass('loading') + if (typeof FileList !== 'undefined') { + FileList.hideMask() + } + FilesAppIntegration.initAfterReady() }, onClose: function() { + odfViewer.open = false clearTimeout(odfViewer.loadingTimeout) if (typeof FileList !== 'undefined') { FileList.setViewerMode(false) FileList.reload() + // FileList.scrollTo() } odfViewer.receivedLoading = false $('link[href*="richdocuments/css/mobile"]').remove() $('#app-content #controls').removeClass('hidden') $('#richdocumentsframe').remove() - $('.richdocuments-sharing').remove() - $('#richdocuments-avatars').remove() - $('#richdocuments-actions').remove() $('.searchbox').show() $('body').css('overflow', 'auto') - if ($('#isPublic').val()) { + if (isPublic) { $('#content').removeClass('full-height') $('footer').removeClass('hidden') $('#imgframe').removeClass('hidden') @@ -165,75 +139,64 @@ var odfViewer = { } OC.Util.History.replaceState() + location.hash = '' + + FilesAppIntegration.close() }, registerFilesMenu: function(response) { - var ooxml = response.doc_format === 'ooxml' - - var docExt, spreadsheetExt, presentationExt - var docMime, spreadsheetMime, presentationMime - if (ooxml) { - docExt = 'docx' - spreadsheetExt = 'xlsx' - presentationExt = 'pptx' - docMime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - spreadsheetMime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - presentationMime = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' - } else { - docExt = 'odt' - spreadsheetExt = 'ods' - presentationExt = 'odp' - docMime = 'application/vnd.oasis.opendocument.text' - spreadsheetMime = 'application/vnd.oasis.opendocument.spreadsheet' - presentationMime = 'application/vnd.oasis.opendocument.presentation' - } + Config.update('ooxml', response.doc_format === 'ooxml') - (function(OCA) { + const registerFilesMenu = (OCA) => { OCA.FilesLOMenu = { attach: function(newFileMenu) { var self = this + const ooxml = Config.get('ooxml') + const document = Types.getFileType('document', ooxml) + const spreadsheet = Types.getFileType('spreadsheet', ooxml) + const presentation = Types.getFileType('presentation', ooxml) newFileMenu.addMenuEntry({ - id: 'add-' + docExt, + id: 'add-' + document.extension, displayName: t('richdocuments', 'New Document'), - templateName: t('richdocuments', 'New Document') + '.' + docExt, + templateName: t('richdocuments', 'New Document') + '.' + document.extension, iconClass: 'icon-filetype-document', fileType: 'x-office-document', actionHandler: function(filename) { if (OC.getCapabilities().richdocuments.templates) { - self._openTemplatePicker('document', docMime, filename) + self._openTemplatePicker('document', document.mime, filename) } else { - self._createDocument(docMime, filename) + self._createDocument(document.mime, filename) } } }) newFileMenu.addMenuEntry({ - id: 'add-' + spreadsheetExt, + id: 'add-' + spreadsheet.extension, displayName: t('richdocuments', 'New Spreadsheet'), - templateName: t('richdocuments', 'New Spreadsheet') + '.' + spreadsheetExt, + templateName: t('richdocuments', 'New Spreadsheet') + '.' + spreadsheet.extension, iconClass: 'icon-filetype-spreadsheet', fileType: 'x-office-spreadsheet', actionHandler: function(filename) { if (OC.getCapabilities().richdocuments.templates) { - self._openTemplatePicker('spreadsheet', spreadsheetMime, filename) + self._openTemplatePicker('spreadsheet', spreadsheet.mime, filename) } else { - self._createDocument(spreadsheetMime, filename) + self._createDocument(spreadsheet.mime, filename) } } }) newFileMenu.addMenuEntry({ - id: 'add-' + presentationExt, + id: 'add-' + presentation.extension, displayName: t('richdocuments', 'New Presentation'), - templateName: t('richdocuments', 'New Presentation') + '.' + presentationExt, + templateName: t('richdocuments', 'New Presentation') + '.' + presentation.extension, iconClass: 'icon-filetype-presentation', fileType: 'x-office-presentation', actionHandler: function(filename) { if (OC.getCapabilities().richdocuments.templates) { - self._openTemplatePicker('presentation', presentationMime, filename) + self._openTemplatePicker('presentation', presentation.mime, filename) } else { - self._createDocument(presentationMime, filename) + self._createDocument(presentation.mime, filename) } } }) @@ -259,11 +222,22 @@ var odfViewer = { _createDocumentFromTemplate: function(templateId, mimetype, filename) { OCA.Files.Files.isFileNameValid(filename) filename = FileList.getUniqueName(filename) - odfViewer.onEdit(filename, { - fileId: -1, - dir: $('#dir').val(), - templateId: templateId - }) + $.post( + OC.generateUrl('apps/richdocuments/ajax/documents/create'), + { mimetype: mimetype, filename: filename, dir: $('#dir').val() }, + function(response) { + if (response && response.status === 'success') { + FileList.add(response.data, { animate: false, scrollTo: false }) + odfViewer.onEdit(filename, { + fileId: -1, + dir: $('#dir').val(), + templateId: templateId + }) + } else { + OC.dialogs.alert(response.data.message, t('core', 'Could not create file')) + } + } + ) }, _openTemplatePicker: function(type, mimetype, filename) { @@ -334,29 +308,15 @@ var odfViewer = { dlg.querySelector('.template-container').appendChild(template) } } - })(OCA) + } + registerFilesMenu(OCA) OC.Plugins.register('OCA.Files.NewFileMenu', OCA.FilesLOMenu) // Open the template picker if there was a create parameter detected on load if (Preload.create && Preload.create.type && Preload.create.filename) { - var mimetype - var ext - switch (Preload.create.type) { - case 'document': - mimetype = docMime - ext = docExt - break - case 'spreadsheet': - mimetype = spreadsheetMime - ext = spreadsheetExt - break - case 'presentation': - mimetype = presentationMime - ext = presentationExt - break - } - OCA.FilesLOMenu._openTemplatePicker(Preload.create.type, mimetype, Preload.create.filename + '.' + ext) + const fileType = Types.getFileType(Preload.create.type, Config.get('ooxml')) + OCA.FilesLOMenu._openTemplatePicker(Preload.create.type, fileType.mime, Preload.create.filename + '.' + fileType.extension) } } @@ -369,34 +329,90 @@ $(document).ready(function() { && typeof OCA.Files.fileActions !== 'undefined' ) { // check if texteditor app is enabled and loaded... - if (_.isUndefined(OCA.Files_Texteditor)) { - // it is not, so we do open text files with this app too. + if (typeof OCA.Files_Texteditor === 'undefined' && typeof OCA.Text === 'undefined') { odfViewer.supportedMimes.push('text/plain') } - - // notice: when changing 'supportedMimes' interactively (e.g. dev console), - // register() needs to be re-run to re-register the fileActions. odfViewer.register() $.get(OC.filePath('richdocuments', 'ajax', 'settings.php')).done(function(settings) { + // TODO: move ooxml setting to capabilities so we don't need this request odfViewer.registerFilesMenu(settings) }) } // Open documents if a public page is opened for a supported mimetype - if ($('#isPublic').val() && odfViewer.supportedMimes.indexOf($('#mimetype').val()) !== -1) { - odfViewer.onEdit($('#filename').val()) + if (isPublic && odfViewer.supportedMimes.indexOf($('#mimetype').val()) !== -1) { + odfViewer.onEdit(document.getElementById('filename').value) } - // listen to message from the viewer for closing/loading actions - window.addEventListener('message', function(e) { - if (e.data === 'close') { + PostMessages.registerPostMessageHandler(({ parsed }) => { + console.debug('[viewer] Received post message', parsed) + const { msgId, args, deprecated } = parsed + if (deprecated) { return } + + switch (msgId) { + case 'loading': + odfViewer.onReceiveLoading() + break + case 'App_LoadingStatus': + if (args.Status === 'Timeout') { + odfViewer.onClose() + OC.Notification.showTemporary(t('richdocuments', 'Failed to connect to {productName}. Please try again later or contact your server administrator.', + { productName: OC.getCapabilities().richdocuments.productName } + )) + } + break + case 'UI_Share': + FilesAppIntegration.share() + break + case 'UI_CreateFile': + FilesAppIntegration.createNewFile(args.DocumentType) + break + case 'UI_InsertGraphic': + FilesAppIntegration.insertGraphic((filename, url) => { + PostMessages.sendWOPIPostMessage(FRAME_DOCUMENT, 'postAsset', { FileName: filename, Url: url }) + }) + break + case 'File_Rename': + FileList.reload() + OC.Apps.hideAppSidebar() + FilesAppIntegration.fileName = args.NewName + break + case 'close': odfViewer.onClose() - } else if (e.data === 'loading') { - odfViewer.receivedLoading = true - $('#content').removeClass('loading') - FileList.hideMask() + break + case 'Get_Views_Resp': + case 'Views_List': + FilesAppIntegration.setViews(args) + break + case 'UI_FileVersions': + case 'rev-history': + FilesAppIntegration.showRevHistory() + break } - }, false) + + // legacy view handling + if (msgId === 'View_Added') { + FilesAppIntegration.views[args.ViewId] = args + FilesAppIntegration.renderAvatars() + } else if (msgId === 'View_Removed') { + delete FilesAppIntegration.views[args.ViewId] + FilesAppIntegration.renderAvatars() + } else if (msgId === 'FollowUser_Changed') { + if (args.IsFollowEditor) { + FilesAppIntegration.followingEditor = true + } else { + FilesAppIntegration.followingEditor = false + } + if (args.IsFollowUser) { + FilesAppIntegration.following = args.FollowedViewId + } else { + FilesAppIntegration.following = null + } + FilesAppIntegration.renderAvatars() + } + + }) + window.FilesAppIntegration = FilesAppIntegration }) |