diff --git a/build/build.js b/build/build.js index bee846a5..1f348e09 100644 --- a/build/build.js +++ b/build/build.js @@ -90,6 +90,13 @@ override: { skipModuleInsertion: true } + }, + { + name: 'sandboxes/webodf', + dir: './out/sandboxes', + override: { + skipModuleInsertion: true + } } ] }) diff --git a/src/styles/components/_presentation.scss b/src/styles/components/_presentation.scss index 70bd697f..835a5c68 100644 --- a/src/styles/components/_presentation.scss +++ b/src/styles/components/_presentation.scss @@ -205,30 +205,6 @@ width: 100%; } } - - .odfcanvas { - cursor: default; - user-select: none; - - body { - background-color: transparent; - } - - document { - display: block; - } - } - - .odfcontainer { - display: none; - margin: 0; - padding: 0; - - &.showonepage { - overflow: hidden; - text-align: center; - } - } } .pageinfo input { diff --git a/static/js/directives/odfcanvas.js b/static/js/directives/odfcanvas.js index aa29c796..a5a035bc 100644 --- a/static/js/directives/odfcanvas.js +++ b/static/js/directives/odfcanvas.js @@ -20,109 +20,101 @@ */ "use strict"; -define(['require', 'underscore', 'jquery'], function(require, _, $) { +define(['require', 'underscore', 'jquery', 'text!partials/odfcanvas_sandbox.html'], function(require, _, $, sandboxTemplate) { - return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) { - - var webodf = null; + return ["$window", "$compile", "$http", "translation", "safeApply", "restURL", "sandbox", function($window, $compile, $http, translation, safeApply, restURL, sandbox) { var DOCUMENT_TYPE_PRESENTATION = "presentation"; var DOCUMENT_TYPE_SPREADSHEET = "spreadsheet"; var DOCUMENT_TYPE_TEXT = "text"; - var nsResolver = function(prefix) { - var ns = { - 'draw': "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", - 'presentation': "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0", - 'text': "urn:oasis:names:tc:opendocument:xmlns:text:1.0", - 'office': "urn:oasis:names:tc:opendocument:xmlns:office:1.0" - }; - return ns[prefix] || console.log('prefix [' + prefix + '] unknown.'); - } - - var ODFCanvas_readFile = function(path, encoding, callback) { - if (typeof path === "string") { - webodf.runtime.orig_readFile.call(webodf.runtime, path, encoding, callback); - return; - } - - var fp = path.file || path; - if (typeof URL !== "undefined" && URL.createObjectURL) { - var url = URL.createObjectURL(fp); - webodf.runtime.orig_readFile.call(webodf.runtime, url, encoding, function() { - URL.revokeObjectURL(url); - callback.apply(callback, arguments); - }); - return; - } - - console.error("TODO(fancycode): implement readFile for", path); - }; - - var ODFCanvas_loadXML = function(path, callback) { - if (typeof path === "string") { - webodf.runtime.orig_loadXML.call(webodf.runtime, path, callback); - return; - } - - var fp = path.file || path; - if (typeof URL !== "undefined" && URL.createObjectURL) { - var url = URL.createObjectURL(fp); - webodf.runtime.orig_loadXML.call(webodf.runtime, url, function() { - URL.revokeObjectURL(url); - callback.apply(callback, arguments); - }); - return; - } + var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { - console.error("TODO(fancycode): implement loadXML for", path); - }; + var container = $($element); - var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { + var odfCanvas; + + var template = sandboxTemplate; + template = template.replace(/__PARENT_ORIGIN__/g, $window.location.protocol + "//" + $window.location.host); + template = template.replace(/__WEBODF_SANDBOX_JS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('sandboxes/webodf') + ".js")); + template = template.replace(/__WEBODF_URL__/g, restURL.createAbsoluteUrl(require.toUrl('webodf') + ".js")); + var sandboxApi = sandbox.createSandbox($("iframe", container)[0], template); + + sandboxApi.e.on("message", function(event, message) { + var msg = message.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "ready": + break; + case "webodf.loading": + $scope.$apply(function(scope) { + scope.$emit("presentationLoading", data.source); + container.hide(); + }); + break; + case "webodf.loaded": + odfCanvas._odfLoaded(data.url, data.type, data.numPages); + break; + case "webodf.keyUp": + $scope.$apply(function(scope) { + scope.$emit("keyUp", data.key); + }); + break; + default: + console.log("Unknown message received", message); + break; + } + }); - var ODFCanvas = function(scope, container, canvasDom) { + var ODFCanvas = function(scope, container) { this.scope = scope; this.container = container; - this.canvasDom = canvasDom; - this.canvas = null; + this.doc = null; this.maxPageNumber = -1; this.currentPageNumber = -1; this.pendingPageNumber = null; }; - ODFCanvas.prototype._close = function() { - if (this.canvas) { - this.canvas.destroy(function() { - // ignore callback - }); - this.canvas = null; - } + ODFCanvas.prototype.close = function() { + sandboxApi.postMessage("closeFile", {"close": true}); this.maxPageNumber = -1; this.currentPageNumber = -1; this.pendingPageNumber = null; - }; - - ODFCanvas.prototype.close = function() { - this._close(); + this.doc = null; }; ODFCanvas.prototype.open = function(presentation) { this.scope.$emit("presentationOpening", presentation); presentation.open(_.bind(function(source) { console.log("Loading ODF from", source); - this._openFile(source); + this.close(); + if (typeof source === "string") { + // got a url + this._openFile(source); + return; + } + + var fp = source.file || source; + if (typeof URL !== "undefined" && URL.createObjectURL) { + this.url = URL.createObjectURL(fp); + this._openFile(this.url); + } else { + var fileReader = new FileReader(); + fileReader.onload = _.bind(function(evt) { + var buffer = evt.target.result; + var uint8Array = new Uint8Array(buffer); + this._openFile(uint8Array); + }, this); + fileReader.readAsArrayBuffer(fp); + } }, this)); }; - ODFCanvas.prototype._odfLoaded = function() { + ODFCanvas.prototype._odfLoaded = function(url, document_type, numPages) { this.scope.$apply(_.bind(function(scope) { - var odfcontainer = this.canvas.odfContainer(); - this.document_type = odfcontainer.getDocumentType(); - // pages only supported for presentations - var pages = []; - switch (this.document_type) { + this.document_type = document_type; + switch (document_type) { case DOCUMENT_TYPE_PRESENTATION: - pages = odfcontainer.rootElement.getElementsByTagNameNS(nsResolver('draw'), 'page'); this.container.addClass("showonepage"); break; @@ -131,13 +123,12 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { break; } - this.maxPageNumber = Math.max(1, pages.length); + this.maxPageNumber = numPages; this.currentPageNumber = -1; - console.log("ODF loaded", odfcontainer); - var odfDoc = { + this.doc = { numPages: this.maxPageNumber }; - scope.$emit("presentationLoaded", odfcontainer.getUrl(), odfDoc); + scope.$emit("presentationLoaded", url, this.doc); if (this.pendingPageNumber !== null) { this._showPage(this.pendingPageNumber); this.pendingPageNumber = null; @@ -145,41 +136,19 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { }, this)); }; - ODFCanvas.prototype._doOpenFile = function(source) { - this.scope.$emit("presentationLoading", source); - this.container.hide(); - if (!this.canvas) { - this.canvas = new webodf.odf.OdfCanvas(this.canvasDom[0]); - this.canvas.addListener("statereadychange", _.bind(function() { - this._odfLoaded(); - }, this)); - } - - this.canvas.setZoomLevel(1); - this.canvas.load(source); - }; - ODFCanvas.prototype._openFile = function(source) { - if (webodf === null) { - // load webodf.js lazily - require(['webodf'], _.bind(function(webodf_) { - console.log("Using webodf.js " + webodf_.webodf.Version); - - webodf = webodf_; - - // monkey-patch IO functions - webodf.runtime.orig_readFile = webodf.runtime.readFile; - webodf.runtime.readFile = ODFCanvas_readFile; - webodf.runtime.orig_loadXML = webodf.runtime.loadXML; - webodf.runtime.loadXML = ODFCanvas_loadXML; - - this.scope.$apply(_.bind(function(scope) { - this._doOpenFile(source); - }, this)); + if (typeof(source) === "string") { + // we can't load urls from inside the sandbox, do so here and transmit the contents + $http.get(source, { + responseType: "arraybuffer" + }).then(_.bind(function(response) { + this._openFile(response.data); }, this)); - } else { - this._doOpenFile(source); + return; } + + console.log("Opening file", source); + sandboxApi.postMessage("openFile", {"source": source}); }; ODFCanvas.prototype._showPage = function(page) { @@ -200,7 +169,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { this.redrawPage(); } this.currentPageNumber = page; - this.canvas.showPage(page); + sandboxApi.postMessage("showPage", {"page": page}); this.scope.$emit("presentationPageRendering", page); this.scope.$emit("presentationPageRendered", page, this.maxPageNumber); }, this)); @@ -208,22 +177,12 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { }; ODFCanvas.prototype.redrawPage = function() { - if (this.canvas) { - switch (this.document_type) { - case DOCUMENT_TYPE_PRESENTATION: - this.canvas.fitToContainingElement(this.container.width(), this.container.height()); - break; - - default: - this.canvas.fitToWidth(this.container.width()); - break; - } - } + sandboxApi.postMessage("redrawPage", {"redraw": true}); }; ODFCanvas.prototype.showPage = function(page) { if (page >= 1 && page <= this.maxPageNumber) { - if (!this.canvas) { + if (!this.doc) { this.pendingPageNumber = page; } else { this._showPage(page); @@ -231,9 +190,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { } }; - var container = $($element); - var canvas = $($element).find(".odfcanvas"); - var odfCanvas = new ODFCanvas($scope, container, canvas); + odfCanvas = new ODFCanvas($scope, container); $scope.$watch("currentPresentation", function(presentation, previousPresentation) { if (presentation) { @@ -251,6 +208,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { $scope.$on("$destroy", function() { odfCanvas.close(); odfCanvas = null; + sandboxApi.destroy(); }); $scope.$watch("currentPageNumber", function(page, oldValue) { @@ -273,7 +231,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { return { restrict: 'E', replace: true, - template: '
', + template: '
', controller: controller }; diff --git a/static/js/sandboxes/webodf.js b/static/js/sandboxes/webodf.js new file mode 100644 index 00000000..0dd7af7a --- /dev/null +++ b/static/js/sandboxes/webodf.js @@ -0,0 +1,251 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * 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 . + * + */ + +"use strict"; +(function () { + + var DOCUMENT_TYPE_PRESENTATION = "presentation"; + var DOCUMENT_TYPE_SPREADSHEET = "spreadsheet"; + var DOCUMENT_TYPE_TEXT = "text"; + + var nsResolver = function(prefix) { + var ns = { + 'draw': "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", + 'presentation': "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0", + 'text': "urn:oasis:names:tc:opendocument:xmlns:text:1.0", + 'office': "urn:oasis:names:tc:opendocument:xmlns:office:1.0" + }; + return ns[prefix] || console.log('prefix [' + prefix + '] unknown.'); + } + + var body = document.getElementsByTagName("body")[0]; + var script = document.getElementsByTagName("script")[0]; + var PARENT_ORIGIN = script.getAttribute("data-parent-origin"); + var WEBODF_URL = script.getAttribute("data-webodf-url"); + var container = document.getElementById("container"); + + var webodfScript = null; + var webodf = null; + var runtime = null; + + var ODFCanvas_readFile = function(path, encoding, callback) { + if (typeof path === "string") { + runtime.orig_readFile.call(runtime, path, encoding, callback); + return; + } + + // we're loading typed arrays in the sandbox + callback(null, new Uint8Array(path)); + }; + + var ODFCanvas_loadXML = function(path, callback) { + if (typeof path === "string") { + runtime.orig_loadXML.call(runtime, path, callback); + return; + } + + // we're loading typed arrays in the sandbox + console.log("LoadXML", path); + var bb = new Blob([new Uint8Array(path)]); + var f = new FileReader(); + f.onload = function(e) { + var parser = new DOMParser(); + var doc = parser.parseFromString(e.target.result, "text/xml"); + callback(null, doc); + }; + f.readAsText(bb); + }; + + var WebODFSandbox = function(window) { + this.head = document.getElementsByTagName('head')[0]; + this.canvasDom = document.getElementById("odfcanvas"); + this.window = window; + this.canvas = null; + this.document_type = null; + }; + + WebODFSandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type}; + msg[type] = message; + this.window.parent.postMessage(msg, PARENT_ORIGIN); + }; + + WebODFSandbox.prototype.openFile = function(source) { + if (!webodfScript) { + var that = this; + webodfScript = document.createElement('script'); + webodfScript.type = "text/javascript"; + webodfScript.src = WEBODF_URL; + webodfScript.onerror = function(evt) { + that.postMessage("webodf.error", {"msgid": "loadScriptFailed"}); + that.head.removeChild(webodfScript); + webodfScript = null; + }; + webodfScript.onload = function(evt) { + console.log("Using webodf.js " + that.window.webodf.Version); + webodf = that.window.odf; + + // monkey-patch IO functions + runtime = that.window.runtime; + runtime.orig_readFile = runtime.readFile; + runtime.readFile = ODFCanvas_readFile; + runtime.orig_loadXML = runtime.loadXML; + runtime.loadXML = ODFCanvas_loadXML; + + that._doOpenFile(source); + }; + this.head.appendChild(webodfScript); + } else { + this._doOpenFile(source); + } + }; + + WebODFSandbox.prototype.closeFile = function() { + if (this.canvas) { + this.canvas.destroy(function() { + // ignore callback + }); + this.canvas = null; + } + }; + + WebODFSandbox.prototype._doOpenFile = function(source) { + this.postMessage("webodf.loading", {"source": source}); + if (!this.canvas) { + var that = this; + this.canvas = new webodf.OdfCanvas(this.canvasDom); + this.canvas.addListener("statereadychange", function() { + that._odfLoaded(); + }); + } + + this.canvas.setZoomLevel(1); + this.canvas.load(source); + }; + + WebODFSandbox.prototype._odfLoaded = function() { + var odfcontainer = this.canvas.odfContainer(); + console.log("ODF loaded", odfcontainer); + this.document_type = odfcontainer.getDocumentType(); + var pages = []; + switch (this.document_type) { + case DOCUMENT_TYPE_PRESENTATION: + container.className += " showonepage"; + pages = odfcontainer.rootElement.getElementsByTagNameNS(nsResolver('draw'), 'page'); + break; + default: + container.className = this.canvasDom.className.replace(/(?:^|\s)showonepage(?!\S)/g, ""); + break; + } + + var numPages = Math.max(1, pages.length); + this.postMessage("webodf.loaded", {"url": odfcontainer.getUrl(), "type": this.document_type, "numPages": numPages}); + }; + + WebODFSandbox.prototype.showPage = function(page) { + this.canvas.showPage(page); + this.redrawPage(); + }; + + WebODFSandbox.prototype.redrawPage = function() { + if (this.canvas) { + switch (this.document_type) { + case DOCUMENT_TYPE_PRESENTATION: + this.canvas.fitToContainingElement(container.offsetWidth, container.offsetHeight); + break; + + default: + this.canvas.fitToWidth(container.offsetWidth); + break; + } + } + }; + + var sandbox = new WebODFSandbox(window); + + window.addEventListener("message", function(event) { + if (event.origin !== PARENT_ORIGIN) { + // only accept messages from spreed-webrtc + return; + } + var msg = event.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "openFile": + sandbox.openFile(data.source); + break; + case "closeFile": + sandbox.closeFile(); + break; + case "showPage": + sandbox.showPage(data.page); + break; + case "redrawPage": + sandbox.redrawPage(); + break; + default: + console.log("Unknown message received", event); + break; + } + }, false); + + document.addEventListener("keyup", function(event) { + sandbox.postMessage("webodf.keyUp", {"key": event.keyCode}); + event.preventDefault(); + }); + + window.addEventListener("resize", function() { + sandbox.redrawPage(); + }); + + var toggleFullscreen = function(elem) { + var fullScreenElement = document.fullscreenElement || document.msFullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement; + if (fullScreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + elem.webkitRequestFullscreen(); + } else if (elem.mozRequestFullScreen) { + elem.mozRequestFullScreen(); + } else if (elem.msRequestFullscreen) { + elem.msRequestFullscreen(); + } + } + }; + + container.addEventListener("dblclick", function(event) { + toggleFullscreen(container); + }); + + console.log("WebODF sandbox ready."); + sandbox.postMessage("ready", {"ready": true}); + +})(); diff --git a/static/partials/odfcanvas_sandbox.html b/static/partials/odfcanvas_sandbox.html new file mode 100644 index 00000000..6421930b --- /dev/null +++ b/static/partials/odfcanvas_sandbox.html @@ -0,0 +1,39 @@ + + + + WebODF Sandbox + + + +
+
+
+ + +