diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 7d3a83264..c7365d722 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -11,8 +11,8 @@ slide: #e57614; poll: #2c9e98; form: #2c9e98; - whiteboard: #a72ba7; - diagram: #f447fa; // XXX placeholder + whiteboard: #8f40f5; // XXX TBC + diagram: #ce3ad3; // XXX TBC kanban: #8C4; sheet: #40865c; doc: #5170B5; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index f0b14cda7..e826b4cb5 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -11,8 +11,8 @@ slide: #e57614; poll: #2c9e98; form: #2c9e98; - whiteboard: #a72ba7; - diagram: #f447fa; // XXX placeholder + whiteboard: #8f40f5; // XXX TBC + diagram: #ce3ad3; // XXX TBC kanban: #8C4; sheet: #40865c; doc: #5170B5; diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 2a89713bf..87880a12e 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -13,6 +13,7 @@ define(function() { */ AppConfig.availablePadTypes = ['drive', 'teams', 'sheet', 'doc', 'presentation', 'pad', 'kanban', 'code', 'form', 'poll', 'whiteboard', 'file', 'contacts', 'slide', 'convert', 'diagram']; + /* The registered only types are apps restricted to registered users. * You should never remove apps from this list unless you know what you're doing. The apps * listed here by default can't work without a user account. @@ -192,7 +193,12 @@ define(function() { poll: 'cptools-poll', form: 'cptools-poll', whiteboard: 'cptools-whiteboard', +<<<<<<< HEAD diagram: 'cptools-diagram', +||||||| c94666efb +======= + drawio: 'cptools-diagram', +>>>>>>> origin/drawio-bower todo: 'cptools-todo', contacts: 'fa-address-book', calendar: 'fa-calendar', diff --git a/www/drawio/app.less b/www/drawio/app.less new file mode 100644 index 000000000..93e92c4b4 --- /dev/null +++ b/www/drawio/app.less @@ -0,0 +1,42 @@ +body.cp-app-generic { + +@import (once) "../../customize.dist/src/less2/include/browser.less"; +@import (once) "../../customize.dist/src/less2/include/framework.less"; + + .framework_main( + @bg-color: @colortheme_apps[drawio], + ); + +// body +&.cp-app-generic { + display: flex; + flex-flow: column; + max-height: 100%; + min-height: auto; + + .cp-app-generic-container { + display: inline-flex; + flex-flow: column; + height: 100%; + min-height: 100%; + width: 100%; + resize: horizontal; + overflow: hidden; + } + .cp-app-generic-editor { + flex: 1; + display: flex; + flex-flow: row; + height: 100%; + overflow: hidden; + } + + @media (max-width: @browser_media-medium-screen) { + .cp-app-drawio-container { + flex: 1; + max-width: 100%; + resize: none; + } + } +} +} diff --git a/www/drawio/drawio.css b/www/drawio/drawio.css new file mode 100644 index 000000000..f9ed9bcd2 --- /dev/null +++ b/www/drawio/drawio.css @@ -0,0 +1,92 @@ +/* The diagram editor styles should be loaded after the skin and before our overwrites. */ +/* +@import url("mxgraph-editor/3.7.2/styles/grapheditor.css"); +*/ + +body { + background-color: #ebebeb; +} + +.diagram-editor { + height: 110%; + min-height: 20px; + background-color: #ebebeb; +} + +.diagram-editor .geFooterContainer { + display: none; +} + +.diagram-editor input[type="checkbox"], .diagram-editor input[type="radio"], +.mxPopupMenu input[type="checkbox"], .mxPopupMenu input[type="radio"], +.mxWindow input[type="checkbox"], .mxWindow input[type="radio"], +.geDialog input[type="checkbox"], .geDialog input[type="radio"] { + vertical-align: text-bottom; +} + +/** + * Overwrite XWiki skin styles + */ +.diagram-editor *, +.mxPopupMenu *, +.mxWindow *, +.geDialog, +.geDialog * { + box-sizing: content-box; +} + +.mxPopupMenu, +.mxWindow, +.geDialog { + /* We need the same font size as on draw.io because the dialog height is hard-coded. */ + font-size: 10pt; +} + +.diagram-editor button, .diagram-editor select, +.mxPopupMenu button, .mxPopupMenu select, +.mxWindow button, .mxWindow select, +.geDialog button, .geDialog select { + box-sizing: border-box; +} + +.diagram-editor input[type="text"], +.mxPopupMenu input[type="text"], +.mxWindow input[type="text"], +.geDialog input[type="text"] { + font-size: inherit; + height: auto; + padding: 1px; +} + +.diagram-editor img, +.mxPopupMenu img, +.mxWindow img, +.geDialog img { + vertical-align: baseline; +} + +.diagram-editor hr, +.mxPopupMenu hr, +.mxWindow hr, +.geDialog hr { + margin: 0; +} + +.mxPopupMenu table, +.mxWindow table, +.geDialog table { + margin-bottom: 0; + width: auto; +} + +.diagram-editor table > tbody > tr > td, +.mxPopupMenu table > tbody > tr > td, +.mxWindow table > tbody > tr > td, +.geDialog table > tbody > tr > td { + border-top: 0 none; +} + +.geDialog table > tbody > tr > td { + padding: 0; + vertical-align: baseline; +} diff --git a/www/drawio/index.html b/www/drawio/index.html new file mode 100644 index 000000000..20a9de476 --- /dev/null +++ b/www/drawio/index.html @@ -0,0 +1,38 @@ + + + + + CryptPad + + + + + + + + + + + + diff --git a/www/drawio/inner.html b/www/drawio/inner.html new file mode 100644 index 000000000..b265909ae --- /dev/null +++ b/www/drawio/inner.html @@ -0,0 +1,50 @@ + + + + + + + + + +
+
+
+
+
+ +
+
+
+ + diff --git a/www/drawio/inner.js b/www/drawio/inner.js new file mode 100644 index 000000000..c509ca36b --- /dev/null +++ b/www/drawio/inner.js @@ -0,0 +1,200 @@ +// This is the initialization loading the CryptPad libraries +define([ + '/common/sframe-app-framework.js', + '/customize/messages.js', // translation keys + '/bower_components/pako/dist/pako.min.js', + '/bower_components/js-base64/base64.js', + '/bower_components/x2js/xml2json.min.js', + 'less!/drawio/app.less', + 'css!/drawio/drawio.css', +], function ( + Framework, + Messages, + pako, + base64, + X2JS) { + + // As described here: https://drawio-app.com/extracting-the-xml-from-mxfiles/ + const decompressDrawioXml = function(xmlDocStr) { + var TEXT_NODE = 3; + + var parser = new DOMParser(); + var doc = parser.parseFromString(xmlDocStr, "application/xml"); + + var errorNode = doc.querySelector("parsererror"); + if (errorNode) { + console.error("error while parsing", errorNode); + return xmlDocStr; + } + + doc.firstChild.removeAttribute('modified'); + doc.firstChild.removeAttribute('agent'); + doc.firstChild.removeAttribute('etag'); + + var diagrams = doc.querySelectorAll('diagram'); + + diagrams.forEach(function(diagram) { + if (diagram.firstChild && diagram.firstChild.nodeType === TEXT_NODE) { + const innerText = diagram.firstChild.nodeValue; + const bin = base64.toUint8Array(innerText); + const xmlUrlStr = pako.inflateRaw(bin, {to: 'string'}); + const xmlStr = decodeURIComponent(xmlUrlStr); + const diagramDoc = parser.parseFromString(xmlStr, "application/xml"); + diagram.replaceChild(diagramDoc.firstChild, diagram.firstChild); + } + }); + + + var result = new XMLSerializer().serializeToString(doc); + return result; + }; + + const deepEqual = function(o1, o2) { + return JSON.stringify(o1) === JSON.stringify(o2); + }; + + // This is the main initialization loop + var onFrameworkReady = function (framework) { + var EMPTY_DRAWIO = ""; + var drawioFrame = document.querySelector('#cp-app-drawio-content'); + var x2js = new X2JS(); + var lastContent = x2js.xml_str2json(EMPTY_DRAWIO); + var drawIoInitalized = false; + + var postMessageToDrawio = function(msg) { + if (!drawIoInitalized) { + console.log('draw.io postMessageToDrawio blocked', msg); + return; + } + + console.log('draw.io postMessageToDrawio', msg); + drawioFrame.contentWindow.postMessage(JSON.stringify(msg), '*'); + }; + + const jsonContentAsXML = (content) => x2js.json2xml_str(content); + + var onDrawioInit = function() { + drawIoInitalized = true; + var xmlStr = jsonContentAsXML(lastContent); + postMessageToDrawio({ + action: 'load', + xml: xmlStr, + autosave: 1 + }); + }; + + const xmlAsJsonContent = (xml) => { + var decompressedXml = decompressDrawioXml(xml); + return x2js.xml_str2json(decompressedXml); + }; + + var onDrawioChange = function(newXml) { + var newJson = xmlAsJsonContent(newXml); + if (!deepEqual(lastContent, newJson)) { + lastContent = newJson; + framework.localChange(); + } + }; + + var onDrawioAutosave = function(data) { + onDrawioChange(data.xml); + + // Tell draw.io to hide "Unsaved changes" message + postMessageToDrawio({action: 'status', message: '', modified: false}); + }; + + var drawioHandlers = { + init: onDrawioInit, + autosave: onDrawioAutosave, + }; + + // This is the function from which you will receive updates from CryptPad + framework.onContentUpdate(function (newContent) { + lastContent = newContent; + var xmlStr = jsonContentAsXML(lastContent); + postMessageToDrawio({ + action: 'merge', + xml: xmlStr, + }); + + framework.localChange(); + }); + + // This is the function called to get the current state of the data in your app + framework.setContentGetter(function () { + return lastContent; + }); + + framework.setFileImporter( + {accept: ['.drawio', 'application/x-drawio']}, + (content) => { + return xmlAsJsonContent(content); + } + ); + + framework.setFileExporter( + '.drawio', + () => { + return new Blob([jsonContentAsXML(lastContent)], {type: 'application/x-drawio'}); + } + ); + + framework.onEditableChange(function () { + const editable = !framework.isLocked() && !framework.isReadOnly(); + postMessageToDrawio({ + action: 'spinner', + message: Messages.reconnecting, + show: !editable + }); + + document.getElementById('overlay').className = editable + ? "" + : "show"; + }); + + // starting the CryptPad framework + framework.start(); + + drawioFrame.src = '/bower_components/drawio/src/main/webapp/index.html?' + + new URLSearchParams({ + // pages: 0, + // dev: 1, + test: 1, + stealth: 1, + embed: 1, + drafts: 0, + + chrome: framework.isReadOnly() ? 0 : 1, + dark: window.CryptPad_theme === "dark" ? 1 : 0, + + // Hide save and exit buttons + noSaveBtn: 1, + saveAndExit: 0, + noExitBtn: 1, + + modified: 'unsavedChanges', + proto: 'json', + }); + + window.addEventListener("message", (event) => { + if (event.source === drawioFrame.contentWindow) { + var data = JSON.parse(event.data); + console.log('draw.io got message', data); + var eventType = data.event; + var handler = drawioHandlers[eventType]; + if (handler) { + handler(data); + } + } + }, false); + }; + + // Framework initialization + Framework.create({ + toolbarContainer: '#cme_toolbox', + contentContainer: '#cp-app-drawio-editor', + // validateContent: validateXml, + }, function (framework) { + onFrameworkReady(framework); + }); +});