diff --git a/.arcconfig b/.arcconfig new file mode 100644 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,6 @@ +{ + "phabricator.uri": "https://we.phorge.it/", + "load": [ + "src/" + ] +} diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ -/data/drawio - -/src/__phutil_library_init__.php -/src/__phutil_library_map__.php +/drawio /src/.phutil_module_cache diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -3,12 +3,19 @@ Installation ============ -1) Extract the content of this repository into /src/extensions -2) /bin/arc liberate -3) /bin/storage upgrade -4) CD to /src/extensions/drawio/data -5) git clone https://github.com/jgraph/drawio.git -6) Diagrams application is available under "More Applications" in Phorge. +1) `git clone` this repository somewhere safe: `` +2) CD to `` and `git clone https://github.com/jgraph/drawio.git && cd drawio && git switch v24.2.5` +3) `/bin/storage upgrade` +4) In Phorge's `conf/local/local.json` add the path to the `src/` dir to the entry `load-libraries`, + something like this: +``` + ... + "load-libraries": [ + "/somewhere/safe/diagrams/src/" + ], + ... +``` +5) Diagrams application is available under "More Applications" in Phorge. You may add it to your navigator menu via "Edit Menu" Usage diff --git a/data/iframe.css b/data/iframe.css deleted file mode 100644 --- a/data/iframe.css +++ /dev/null @@ -1,97 +0,0 @@ -body { overflow:hidden; } -div.picker { z-index: 10007; } -.geSidebarContainer .geTitle input { - font-size:8pt; - color:#606060; -} -.geBlock { - z-index:-3; - margin:100px; - margin-top:40px; - margin-bottom:30px; - padding:20px; - text-align:center; - min-width:50%; -} -.geBlock h1, .geBlock h2 { - margin-top:0px; - padding-top:0px; -} -.geEditor *:not(.geScrollable)::-webkit-scrollbar { - width:10px; - height:10px; -} -.geEditor ::-webkit-scrollbar-track { - background-clip:padding-box; - border:solid transparent; - border-width:1px; -} -.geEditor ::-webkit-scrollbar-corner { - background-color:transparent; -} -.geEditor ::-webkit-scrollbar-thumb { - background-color:rgba(0,0,0,.1); - background-clip:padding-box; - border:solid transparent; - border-radius:10px; -} -.geEditor ::-webkit-scrollbar-thumb:hover { - background-color:rgba(0,0,0,.4); -} -.geTemplate { - border:1px solid transparent; - display:inline-block; - _display:inline; - vertical-align:top; - border-radius:3px; - overflow:hidden; - font-size:14pt; - cursor:pointer; - margin:5px; -} - -@keyframes init-spinner-animation { - 0%, 39%, 100% { - opacity: 0.33; - } - 40% { - opacity: .83; - } -} -.init-spinner { - position: relative; - height: 80px; - width: 80px; - left: calc(50% - 50px); -} -.init-spinner .content { - position: absolute; - width: 0px; - left: 50%; - top: 50%; -} - -.init-spinner .content .spike { - position: absolute; - top: -2.5px; - width: 15px; - height: 5px; - background: #000c; - border-radius: 2.5px; - transform-origin: left center 0px; -} - -.init-spinner .content .spike .animator { - width: 100%; - height: 100%; - background: rgb(255, 255, 255); - border-radius: 2.5px; - box-shadow: transparent 0px 0px 1px; - animation-name: init-spinner-animation; - animation-duration: 1s; - animation-timing-function: linear; - animation-iteration-count: infinite; - animation-direction: normal; - animation-fill-mode: none; - animation-play-state: running; -} \ No newline at end of file diff --git a/data/iframe1.js b/data/iframe1.js deleted file mode 100644 --- a/data/iframe1.js +++ /dev/null @@ -1,144 +0,0 @@ -var urlParams = (function() { - var result = {}; - var params = window.location.search.slice(1).split('&'); - - for (var i = 0; i < params.length; i++) { - var idx = params[i].indexOf('='); - - if (idx > 0) { - result[params[i].substring(0, idx)] = params[i].substring(idx + 1); - } - } - - return result; -})(); - -if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P') { - try { - urlParams = JSON.parse(decodeURIComponent(window.location.hash.substring(2))); - - if (urlParams.hash != null) { - window.location.hash = urlParams.hash; - } - } - catch (e) { - } -} - -(function() { - var proto = window.location.protocol; - var host = window.location.host; - var href = proto + '//' + host + window.location.href.substring( - window.location.protocol.length + - window.location.host.length + 2); - - if (href != window.location.href) { - window.location.href = href; - } -})(); - -function mxmeta(name, content, httpEquiv) { - try { - var s = document.createElement('meta'); - - if (name != null) { - s.setAttribute('name', name); - } - - s.setAttribute('content', content); - - if (httpEquiv != null) { - s.setAttribute('http-equiv', httpEquiv); - } - - var t = document.getElementsByTagName('meta')[0]; - t.parentNode.insertBefore(s, t); - } - catch (e) - { - } -} - -function mxscript(src, onLoad, id, dataAppKey, noWrite, onError) { - var s = document.createElement('script'); - s.setAttribute('type', 'text/javascript'); - s.setAttribute('defer', 'true'); - s.setAttribute('src', src); - - if (id != null) { - s.setAttribute('id', id); - } - - if (dataAppKey != null) { - s.setAttribute('data-app-key', dataAppKey); - } - - if (onLoad != null) { - var r = false; - - s.onload = s.onreadystatechange = function() { - if (!r && (!this.readyState || this.readyState == 'complete')) { - r = true; - onLoad(); - } - }; - } - - if (onError != null) { - s.onerror = function(e) { - onError('Failed to load ' + src, e); - }; - } - - var t = document.getElementsByTagName('script')[0]; - - if (t != null) { - t.parentNode.insertBefore(s, t); - } -} - -function mxinclude(src) { - var g = document.createElement('script'); - g.type = 'text/javascript'; - g.async = true; - g.src = src; - - var s = document.getElementsByTagName('script')[0]; - s.parentNode.insertBefore(g, s); -} - -(function() { - var name = 'diagrams.net'; - mxmeta('apple-mobile-web-app-title', name); - mxmeta('application-name', name); -})(); - -var isLocalStorage = true; -var mxScriptsLoaded = false, mxWinLoaded = false; - -function checkAllLoaded() { - if (mxScriptsLoaded && mxWinLoaded) { - App.main(); - } -} - -var t0 = new Date(); - (function() { - function loadAppJS() { - mxscript('js/app.min.js', function() { - mxScriptsLoaded = true; - checkAllLoaded(); - mxscript('js/PostConfig.js'); - }); - } - - mxscript('js/PreConfig.js', loadAppJS); -})(); - -window.onerror = function() { - var status = document.getElementById('geStatus'); - - if (status != null) { - status.innerHTML = 'Page could not be loaded. Please try refreshing.'; - } -}; diff --git a/data/iframe2.js b/data/iframe2.js deleted file mode 100644 --- a/data/iframe2.js +++ /dev/null @@ -1,7 +0,0 @@ -window.addEventListener('load', function() -{ - mxWinLoaded = true; - checkAllLoaded(); -}); - -phorge_extension = parent.phorge_extension; \ No newline at end of file diff --git a/data/phorge_extension.js b/data/phorge_extension.js deleted file mode 100644 --- a/data/phorge_extension.js +++ /dev/null @@ -1,246 +0,0 @@ -var phorge_extension = {}; - -function edit(image) { - var iframe = document.createElement('iframe'); - var iframeInitializedEvent = new CustomEvent('initReceived'); - iframe.setAttribute('title', 'diagrams.net editor'); - iframe.setAttribute('frameborder', '0'); - iframe.style.width = '100%'; - iframe.style.height = 'calc(100vh - 91px)'; - iframe.style.marginTop = '30px'; - iframe.style.marginBottom = '-16px'; - image.style.display = 'none'; - - var receive = function (evt) { - if (evt.data.length > 0) { - var msg = JSON.parse(evt.data); - if (msg.event == 'init') { - if (phorge_extension.diagramBase64) { - image.src = 'data:image/png;base64,' + phorge_extension.diagramBase64; - - var diagramName = document.querySelector('.diagramName'); - diagramName.style.display = 'inline-block'; - diagramName.querySelector('a').innerText = phorge_extension.diagramName; - if (phorge_extension.diagramVersion != '') { - diagramName.querySelector('.version').innerText = '(#' - + phorge_extension.diagramVersion - + ')'; - } - } - - iframe.contentWindow.postMessage(JSON.stringify({ - action: 'load', - autosave: 1, - xmlpng: image.getAttribute('src') - }), '*'); - } - else if (msg.event == 'load') { - // enable Mathematical Typesettings by default - iframe.contentWindow.sb.editorUi.setMathEnabled(true); - - setupButtonsInMenuToolbar(); - } - else if (msg.event == 'export') { - saveFlowchart(name, msg.data, iframe); - } - else if (msg.event == 'save') { - iframe.contentWindow.postMessage(JSON.stringify({ - action: 'export', - format: 'xmlpng', - xml: msg.xml, - spin: 'Updating page' - }), '*'); - } - } - }; - - window.addEventListener('message', receive); - iframe.setAttribute('src', phorge_extension.editor); - document.querySelector('#mainScreen').appendChild(iframe); - iframe.dispatchEvent(iframeInitializedEvent); - - iframe.contentWindow.RESOURCES_PATH = document.baseURI + 'iframe/resources'; - iframe.contentWindow.STENCIL_PATH = document.baseURI + 'iframe/stencils'; - iframe.contentWindow.IMAGE_PATH = document.baseURI + 'iframe/images'; - iframe.contentWindow.STYLE_PATH = document.baseURI + 'iframe/styles'; - iframe.contentWindow.CSS_PATH = document.baseURI + 'iframe/styles'; -} - -function loadJsExtension(diagramName, diagramPHID, diagramVersion, diagramBase64) { - var baseURI = document.baseURI; - if (diagramName != '' && diagramBase64 != '') { - baseURI = baseURI.substr(0, baseURI.length - diagramName.length - 1); - - if (diagramVersion != '') { - baseURI = baseURI.substr(0, baseURI.length - diagramVersion.length - 1); - } - } - phorge_extension.baseURI = baseURI; - phorge_extension.csrf = document.querySelector('input[name="__csrf__"]')?.value; - phorge_extension.editor = baseURI + '/iframe/?embed=1&spin=0&proto=json&noExitBtn=1'; - phorge_extension.name = null; - phorge_extension.editor += '&lang=en'; - phorge_extension.editor += '&ui=min'; - phorge_extension.diagramPHID = diagramPHID; - phorge_extension.diagramName = diagramName; - phorge_extension.diagramVersion = diagramVersion; - phorge_extension.diagramBase64 = diagramBase64; - - document.addEventListener('DOMContentLoaded', function () { - edit(document.querySelector('img.drawio')); - }, false); -} - -function saveFlowchart(name, flowchartData, iframe) { - var diagramID = document.querySelector('.diagramName a').innerText.replace(/^DIAG/, ''); - var csrf = document.querySelector('input[name="__csrf__"]')?.value; - var data = new URLSearchParams(); - data.append('data', flowchartData); - data.append('diagramID', diagramID); - data.append('__csrf__', csrf); - data.append('__form__', '1'); - data.append('__ajax__', 'true'); - - var xmlhttp = new XMLHttpRequest(); - xmlhttp.overrideMimeType('application/json'); - xmlhttp.open('POST', 'save/', true); - xmlhttp.onload = function () { - if (xmlhttp.readyState == 4) { - var errorMessage = null; - try { - var result = JSON.parse(xmlhttp.responseText); - if (result.Status != 'OK') { - errorMessage = result.Error; - } else { - // make sure we don't show messagebox about redirection in browser - iframe.parentNode.removeChild(iframe); - - if (!phorge_extension.diagramVersion || !phorge_extension.diagramVersion.trim()) { - if (!phorge_extension.diagramName || !phorge_extension.diagramName.trim()) { - // load new diagram - window.location = window.location + '/DIAG' + result.DiagramID; - } else { - // reload actual page (so versioned diagrams info is also updated) - window.location.reload(); - } - } else { - // cut off version id from url - var url = document.baseURI - .substring(0, - document.baseURI - .length - - phorge_extension.diagramVersion - .length - - 1 - ); - // redirect to latest version of diagram - window.location = url; - } - } - } catch (exc) { - errorMessage = exc.message; - } - } - }; - - xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); - xmlhttp.send(data); -} - -function setupButtonsInMenuToolbar() { - var iframe = document.querySelector('iframe'); - var btnSave = Array.prototype.slice.call( - iframe.contentDocument - .querySelector('.geMenubarContainer') - .querySelectorAll('button'), 0 - ).reverse()[0]; - - // identify Exit Button - btnSave.classList.add('btnSave'); - - // change layout settings of area where btnSave belongs to so - // that the dropdown menu is not hidden under the drawing area - btnSave.parentNode.style.position = 'fixed'; - - // create extra controls - var subscribeUnsubscribe = setupSubscriptionButtonInMenuToolbar(iframe, btnSave); - var dropdown = setupVersionDropDownInMenuToolbar(iframe, subscribeUnsubscribe); - - // initialize toolbuttons - for (var tb in phorge_extension.toolbtn) { - if (phorge_extension.toolbtn.hasOwnProperty(tb)) { - phorge_extension.toolbtn[tb].instance.initialize(); - } - } -} - -function setupVersionDropDownInMenuToolbar(iframe, btnLeft) { - // generate 'Select Version' button - // create the dropdown element - const dropdown = document.createElement('div'); - dropdown.classList.add('toolbtn-version-dropdown'); - - // create the dropdown toggle button - const toggle = document.createElement('button'); - toggle.classList.add('toolbtn-version-dropdown-toggle'); - toggle.textContent = 'Select Version'; - toggle.title = 'View previous versions'; - dropdown.appendChild(toggle); - - const iframeContent = document.querySelector('iframe').contentDocument; - const menubarContainer = iframeContent.querySelector('.geMenubarContainer'); - - // create the dropdown menu - const menu = document.createElement('div'); - menu.classList.add('toolbtn-version-dropdown-menu'); - menubarContainer.parentNode.insertBefore(menu, menubarContainer.nextSibling); - - // create the version list - const versionList = document.createElement('ul'); - versionList.classList.add('toolbtn-version-list'); - - menu.appendChild(versionList); - - // create the prev and next buttons - const prevBtn = document.createElement('button'); - prevBtn.classList.add('toolbtn-version-prev-btn'); - prevBtn.textContent = '<'; - - const nextBtn = document.createElement('button'); - nextBtn.classList.add('toolbtn-version-next-btn'); - nextBtn.textContent = '>'; - - menu.appendChild(prevBtn); - menu.appendChild(nextBtn); - - // place dropdown next to button on the left - btnLeft.parentNode.insertBefore(dropdown, btnLeft.nextSibling); - - return dropdown; -} - -function setupSubscriptionButtonInMenuToolbar(iframe, btnLeft) { - // generate 'Subscribe/Unsubscribe' button - // create grouping div element - const div = document.createElement('div'); - div.classList.add('toolbtn-diagram-subscription'); - - // create subscribe button - const btnSubscribe = document.createElement('button'); - btnSubscribe.classList.add('subscribe'); - btnSubscribe.classList.add('eye'); - btnSubscribe.title = 'Subscribe'; - div.appendChild(btnSubscribe); - - // create unsubscribe button - const btnUnsubscribe = document.createElement('button'); - btnUnsubscribe.classList.add('unsubscribe'); - btnUnsubscribe.classList.add('eye'); - btnUnsubscribe.title = 'Unsubscribe'; - div.appendChild(btnUnsubscribe); - - // place grouping next to button on the left - btnLeft.parentNode.insertBefore(div, btnLeft.nextSibling); - - return div; -} \ No newline at end of file diff --git a/data/iframe-toolbtn.css b/rsrc/iframe-toolbtn.css rename from data/iframe-toolbtn.css rename to rsrc/iframe-toolbtn.css --- a/data/iframe-toolbtn.css +++ b/rsrc/iframe-toolbtn.css @@ -1,4 +1,8 @@ -/* begin stylesheets for version toolbutton */ +/** + * @provides diagram-css-iframe-toolbtn + */ + +/* begin stylesheets for version toolbutton */ .toolbtn-version-dropdown { right: 0; white-space: nowrap; diff --git a/data/iframe-toolbtn.js b/rsrc/iframe-toolbtn.js rename from data/iframe-toolbtn.js rename to rsrc/iframe-toolbtn.js --- a/data/iframe-toolbtn.js +++ b/rsrc/iframe-toolbtn.js @@ -1,8 +1,14 @@ +/** + * @provides diagram-js-iframe-toolbtn + */ + /** * global dicttionary containing all configuration of all installed toolbuttons */ const toolbtn = {}; +const phorge_extension = parent.__diagram; + /** * Abstract parent class for creating ToolButtons */ @@ -15,7 +21,7 @@ toolbtn[name] = {}; toolbtn[name].instance = this; - parent.phorge_extension.toolbtn = toolbtn; + phorge_extension.toolbtn = toolbtn; } initialize() { @@ -248,4 +254,4 @@ * Initialization of all ToolButton classes */ new ToolButtonVersion('version'); -new ToolButtonSubscription('subscription'); \ No newline at end of file +new ToolButtonSubscription('subscription'); diff --git a/rsrc/phorge_extension.js b/rsrc/phorge_extension.js new file mode 100644 --- /dev/null +++ b/rsrc/phorge_extension.js @@ -0,0 +1,280 @@ +/** + * @provides javelin-behavior-diagram-extension + */ + +JX.behavior('diagram-extension', function(config) { + + var phorge_extension = {}; + console.log('diagram-extension behavior called with config:', config); + + function edit(image) { + var iframe = document.createElement('iframe'); + var iframeInitializedEvent = new CustomEvent('initReceived'); + iframe.setAttribute('title', 'diagrams.net editor'); + iframe.setAttribute('frameborder', '0'); + iframe.style.width = '100%'; + iframe.style.height = 'calc(100vh - 91px)'; + iframe.style.marginTop = '30px'; + iframe.style.marginBottom = '-16px'; + image.style.display = 'none'; + + var receive = function (evt) { + if (evt.data.length > 0) { + var msg = JSON.parse(evt.data); + if (msg.event == 'init') { + if (phorge_extension.diagramBase64) { + image.src = 'data:image/png;base64,' + phorge_extension.diagramBase64; + + var diagramName = document.querySelector('.diagramName'); + diagramName.style.display = 'inline-block'; + diagramName.querySelector('a').innerText = phorge_extension.diagramName; + if (phorge_extension.diagramVersion != '') { + diagramName.querySelector('.version').innerText = '(#' + + phorge_extension.diagramVersion + + ')'; + } + } + + iframe.contentWindow.postMessage(JSON.stringify({ + action: 'load', + autosave: 1, + xmlpng: image.getAttribute('src') + }), '*'); + } + else if (msg.event == 'load') { + // enable Mathematical Typesettings by default + iframe.contentWindow.sb.editorUi.setMathEnabled(true); + + setupButtonsInMenuToolbar(); + } + else if (msg.event == 'export') { + saveFlowchart(name, msg.data, iframe); + } + else if (msg.event == 'save') { + iframe.contentWindow.postMessage(JSON.stringify({ + action: 'export', + format: 'xmlpng', + xml: msg.xml, + spin: 'Updating page' + }), '*'); + } + } + }; + + window.addEventListener('message', receive); + iframe.setAttribute('src', phorge_extension.editor); + document.querySelector('#mainScreen').appendChild(iframe); + console.log('Iframe appended:', iframe); + iframe.contentWindow.addEventListener('DOMContentLoaded', addToolbarResourcesToIframe); + iframe.dispatchEvent(iframeInitializedEvent); + + iframe.contentWindow.RESOURCES_PATH = document.baseURI + 'iframe/resources'; + iframe.contentWindow.STENCIL_PATH = document.baseURI + 'iframe/stencils'; + iframe.contentWindow.IMAGE_PATH = document.baseURI + 'iframe/images'; + iframe.contentWindow.STYLE_PATH = document.baseURI + 'iframe/styles'; + iframe.contentWindow.CSS_PATH = document.baseURI + 'iframe/styles'; + } + + function addToolbarResourcesToIframe() { + var head = this.document.getElementsByTagName('head')[0]; + + var script = this.document.createElement('script'); + script.src = config.toolbarJs; + head.appendChild(script); + + var styles = this.document.createElement('link'); + styles.setAttribute('type', 'text/css'); + styles.setAttribute('rel', 'stylesheet'); + styles.setAttribute('href', config.toolbarCss); + head.appendChild(styles); + + console.log('Iframe DOM loaded:', this.document.body.innerHTML); + } + + function loadJsExtension(diagramName, diagramPHID, diagramVersion, diagramBase64) { + console.log('loadJsExtension called with args:', diagramName, diagramPHID, diagramVersion, diagramBase64); + var baseURI = document.baseURI; + if (diagramName != '' && diagramBase64 != '') { + baseURI = baseURI.substr(0, baseURI.length - diagramName.length - 1); + + if (diagramVersion != '') { + baseURI = baseURI.substr(0, baseURI.length - diagramVersion.length - 1); + } + } + phorge_extension.baseURI = baseURI; + phorge_extension.csrf = document.querySelector('input[name="__csrf__"]')?.value; + phorge_extension.editor = baseURI + '/iframe/?embed=1&spin=0&proto=json&noExitBtn=1'; + phorge_extension.name = null; + phorge_extension.editor += '&lang=en'; + phorge_extension.editor += '&ui=min'; + phorge_extension.diagramPHID = diagramPHID; + phorge_extension.diagramName = diagramName; + phorge_extension.diagramVersion = diagramVersion; + phorge_extension.diagramBase64 = diagramBase64; + + document.addEventListener('DOMContentLoaded', function () { + edit(document.querySelector('img.drawio')); + }, false); + } + + function saveFlowchart(name, flowchartData, iframe) { + var diagramID = document.querySelector('.diagramName a').innerText.replace(/^DIAG/, ''); + var csrf = document.querySelector('input[name="__csrf__"]')?.value; + var data = new URLSearchParams(); + data.append('data', flowchartData); + data.append('diagramID', diagramID); + data.append('__csrf__', csrf); + data.append('__form__', '1'); + data.append('__ajax__', 'true'); + + var xmlhttp = new XMLHttpRequest(); + xmlhttp.overrideMimeType('application/json'); + xmlhttp.open('POST', 'save/', true); + xmlhttp.onload = function () { + if (xmlhttp.readyState == 4) { + var errorMessage = null; + try { + var result = JSON.parse(xmlhttp.responseText); + if (result.Status != 'OK') { + errorMessage = result.Error; + } else { + // make sure we don't show messagebox about redirection in browser + iframe.parentNode.removeChild(iframe); + + if (!phorge_extension.diagramVersion || !phorge_extension.diagramVersion.trim()) { + if (!phorge_extension.diagramName || !phorge_extension.diagramName.trim()) { + // load new diagram + window.location = window.location + '/DIAG' + result.DiagramID; + } else { + // reload actual page (so versioned diagrams info is also updated) + window.location.reload(); + } + } else { + // cut off version id from url + var url = document.baseURI + .substring(0, + document.baseURI + .length + - phorge_extension.diagramVersion + .length + - 1 + ); + // redirect to latest version of diagram + window.location = url; + } + } + } catch (exc) { + errorMessage = exc.message; + } + } + }; + + xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;'); + xmlhttp.send(data); + } + + function setupButtonsInMenuToolbar() { + var iframe = document.querySelector('iframe'); + var btnSave = Array.prototype.slice.call( + iframe.contentDocument + .querySelector('.geMenubarContainer') + .querySelectorAll('button'), 0 + ).reverse()[0]; + + // identify Exit Button + btnSave.classList.add('btnSave'); + + // change layout settings of area where btnSave belongs to so + // that the dropdown menu is not hidden under the drawing area + btnSave.parentNode.style.position = 'fixed'; + + // create extra controls + var subscribeUnsubscribe = setupSubscriptionButtonInMenuToolbar(iframe, btnSave); + var dropdown = setupVersionDropDownInMenuToolbar(iframe, subscribeUnsubscribe); + + // initialize toolbuttons + for (var tb in phorge_extension.toolbtn) { + if (phorge_extension.toolbtn.hasOwnProperty(tb)) { + phorge_extension.toolbtn[tb].instance.initialize(); + } + } + } + + function setupVersionDropDownInMenuToolbar(iframe, btnLeft) { + // generate 'Select Version' button + // create the dropdown element + const dropdown = document.createElement('div'); + dropdown.classList.add('toolbtn-version-dropdown'); + + // create the dropdown toggle button + const toggle = document.createElement('button'); + toggle.classList.add('toolbtn-version-dropdown-toggle'); + toggle.textContent = 'Select Version'; + toggle.title = 'View previous versions'; + dropdown.appendChild(toggle); + + const iframeContent = document.querySelector('iframe').contentDocument; + const menubarContainer = iframeContent.querySelector('.geMenubarContainer'); + + // create the dropdown menu + const menu = document.createElement('div'); + menu.classList.add('toolbtn-version-dropdown-menu'); + menubarContainer.parentNode.insertBefore(menu, menubarContainer.nextSibling); + + // create the version list + const versionList = document.createElement('ul'); + versionList.classList.add('toolbtn-version-list'); + + menu.appendChild(versionList); + + // create the prev and next buttons + const prevBtn = document.createElement('button'); + prevBtn.classList.add('toolbtn-version-prev-btn'); + prevBtn.textContent = '<'; + + const nextBtn = document.createElement('button'); + nextBtn.classList.add('toolbtn-version-next-btn'); + nextBtn.textContent = '>'; + + menu.appendChild(prevBtn); + menu.appendChild(nextBtn); + + // place dropdown next to button on the left + btnLeft.parentNode.insertBefore(dropdown, btnLeft.nextSibling); + + return dropdown; + } + + function setupSubscriptionButtonInMenuToolbar(iframe, btnLeft) { + // generate 'Subscribe/Unsubscribe' button + // create grouping div element + const div = document.createElement('div'); + div.classList.add('toolbtn-diagram-subscription'); + + // create subscribe button + const btnSubscribe = document.createElement('button'); + btnSubscribe.classList.add('subscribe'); + btnSubscribe.classList.add('eye'); + btnSubscribe.title = 'Subscribe'; + div.appendChild(btnSubscribe); + + // create unsubscribe button + const btnUnsubscribe = document.createElement('button'); + btnUnsubscribe.classList.add('unsubscribe'); + btnUnsubscribe.classList.add('eye'); + btnUnsubscribe.title = 'Unsubscribe'; + div.appendChild(btnUnsubscribe); + + // place grouping next to button on the left + btnLeft.parentNode.insertBefore(div, btnLeft.nextSibling); + + return div; + } + + if (config.loadJsExtensionArgs) { + loadJsExtension.apply(undefined, config.loadJsExtensionArgs); + } + + window.__diagram = phorge_extension; + +}); diff --git a/rsrc/remarkup-image.css b/rsrc/remarkup-image.css new file mode 100644 --- /dev/null +++ b/rsrc/remarkup-image.css @@ -0,0 +1,8 @@ +/** + * @provides diagram-remarkup-image-css + */ + +.diagram-container > .diagram-content { + max-width: 100%; + cursor: pointer; +} diff --git a/rsrc/remarkup-image.js b/rsrc/remarkup-image.js new file mode 100644 --- /dev/null +++ b/rsrc/remarkup-image.js @@ -0,0 +1,27 @@ +/** + * @provides diagram-remarkup-image-js + */ + +JX.onload(function() { + + var singleClickTimeout = null; + + JX.Stratcom.listen( + 'click', + ['diagram-remarkup-image'], + function(evt) { + var detail = evt.getRawEvent().detail; + + if (detail === 1) { + singleClickTimeout = window.setTimeout(function() { + window.open('/diagram/data/' + evt.getTarget().dataset.diagramVersion); + singleClickTimeout = null; + }, 300); + } else if (detail === 2) { + window.clearTimeout(singleClickTimeout); + window.open('/diagram/DIAG' + evt.getTarget().dataset.diagramId); + } + } + ); + +}); diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php new file mode 100644 --- /dev/null +++ b/src/__phutil_library_init__.php @@ -0,0 +1,3 @@ + 2, + 'class' => array( + 'Diagram' => 'storage/Diagram.php', + 'DiagramApplication' => 'application/DiagramApplication.php', + 'DiagramCelerityResources' => 'celerity/DiagramCelerityResources.php', + 'DiagramContentTransaction' => 'xaction/DiagramContentTransaction.php', + 'DiagramController' => 'controller/DiagramController.php', + 'DiagramDAO' => 'storage/DiagramDAO.php', + 'DiagramPHIDType' => 'phid/DiagramPHIDType.php', + 'DiagramPatchList' => 'storage/patch/DiagramPatchList.php', + 'DiagramReplyHandler' => 'mail/DiagramReplyHandler.php', + 'DiagramSchemaSpec' => 'storage/DiagramSchemaSpec.php', + 'DiagramSearchConduitAPIMethod' => 'conduit/DiagramSearchConduitAPIMethod.php', + 'DiagramTransaction' => 'storage/DiagramTransaction.php', + 'DiagramTransactionEditor' => 'editor/DiagramTransactionEditor.php', + 'DiagramTransactionType' => 'xaction/DiagramTransactionType.php', + 'DiagramUploadConduitAPIMethod' => 'conduit/DiagramUploadConduitAPIMethod.php', + 'DiagramVersion' => 'storage/DiagramVersion.php', + 'PhabricatorDiagramQuery' => 'query/PhabricatorDiagramQuery.php', + 'PhabricatorDiagramTransactionQuery' => 'query/PhabricatorDiagramTransactionQuery.php', + 'PhabricatorDiagramVersionQuery' => 'query/PhabricatorDiagramVersionQuery.php', + 'PhabricatorRemarkupDiagramRule' => 'remarkup/PhabricatorRemarkupDiagramRule.php', + 'PlainHtmlWebpageResponse' => 'response/PlainHtmlWebpageResponse.php', + ), + 'function' => array(), + 'xmap' => array( + 'Diagram' => array( + 'DiagramDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorSubscribableInterface', + ), + 'DiagramApplication' => 'PhabricatorApplication', + 'DiagramCelerityResources' => 'CelerityResourcesOnDisk', + 'DiagramContentTransaction' => 'DiagramTransactionType', + 'DiagramController' => 'PhabricatorController', + 'DiagramDAO' => 'PhabricatorLiskDAO', + 'DiagramPHIDType' => 'PhabricatorPHIDType', + 'DiagramPatchList' => 'PhabricatorSQLPatchList', + 'DiagramReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'DiagramSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'DiagramSearchConduitAPIMethod' => 'ConduitAPIMethod', + 'DiagramTransaction' => 'PhabricatorModularTransaction', + 'DiagramTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'DiagramTransactionType' => 'PhabricatorModularTransactionType', + 'DiagramUploadConduitAPIMethod' => 'ConduitAPIMethod', + 'DiagramVersion' => array( + 'DiagramDAO', + 'PhabricatorDestructibleInterface', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorDiagramQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDiagramTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorDiagramVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorRemarkupDiagramRule' => 'PhabricatorObjectRemarkupRule', + 'PlainHtmlWebpageResponse' => 'AphrontHTMLResponse', + ), +)); diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php --- a/src/application/DiagramApplication.php +++ b/src/application/DiagramApplication.php @@ -35,33 +35,33 @@ // url to image data "data/(?PPHID-DGVN-[a-z0-9]{20})" . "(?:/(?P[^/]+))?" - . "(?:/|\?|$).*" + . "(?:/|\?|$).*" => "DiagramController", - + // version info requests "version/DIAG(?P(\d+))" . "/(?P(\d+))" - . "(?:/|\?|$).*" + . "(?:/|\?|$).*" => "DiagramController", - + // draw.io iframe urls "(?:(?PDIAG(\d+))/(?P\d+)/?)" . "(?Piframe)" - . "(?:/|\?|$).*" + . "(?:/|\?|$).*" => "DiagramController", - + // draw.io iframe urls "(?:(?PDIAG(\d+))/?)?" . "(?Piframe)" - . "(?:/|\?|$).*" + . "(?:/|\?|$).*" => "DiagramController", - + // url with diagram id and version in it "(?PDIAG(\d+))" . "/(?P\d+)" - . "(?:/|\?|$).*" + . "(?:/|\?|$).*" => "DiagramController", - + // url with diagram id in it "(?PDIAG(\d+))" . "(?:/|\?|$).*" @@ -72,18 +72,10 @@ . "(?P[^/]+)/" => 'DiagramController', - // url for loading/initializing diagram content - "(?PloadJsExtension)/" - . "(/?(?P[^/]*))?" - . "(/?(?P[^/]*))?" - . "(/?(?P[^/]*))?" - . ".*" - => 'DiagramController', - // other urls ".*" => "DiagramController", ) ); } -} \ No newline at end of file +} diff --git a/src/celerity/DiagramCelerityResources.php b/src/celerity/DiagramCelerityResources.php new file mode 100644 --- /dev/null +++ b/src/celerity/DiagramCelerityResources.php @@ -0,0 +1,15 @@ + array( + 'iframe-toolbtn.css' => '35ad6f49', + 'iframe-toolbtn.js' => '26d75a35', + 'phorge_extension.js' => 'd9f07830', + 'remarkup-image.css' => '42b46bf1', + 'remarkup-image.js' => '64e7e9e1', + ), + 'symbols' => array( + 'diagram-css-iframe-toolbtn' => '35ad6f49', + 'diagram-js-iframe-toolbtn' => '26d75a35', + 'diagram-remarkup-image-css' => '42b46bf1', + 'diagram-remarkup-image-js' => '64e7e9e1', + 'javelin-behavior-diagram-extension' => 'd9f07830', + ), + 'requires' => array(), + 'packages' => array(), +); diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -21,9 +21,6 @@ $route = $request->getURIData('route'); $versioninfoDiagramID = $request->getURIData('versioninfodiagram'); $versioninfoPage = $request->getURIData('versioninfopage'); - $loadDiagramName = $request->getURIData('loadDiagramName'); - $loadDiagramPHID = $request->getURIData('loadDiagramPHID'); - $loadDiagramVersion = $request->getURIData('loadDiagramVersion'); if (isset($diagramphid) && !empty(trim($diagramphid))) { // return PNG image data @@ -103,39 +100,6 @@ } } - if ($route == 'loadJsExtension') { - $response = new AphrontFileResponse(); - $response->setMimeType('application/javascript'); - - $base64_data = ""; - if (isset($loadDiagramName) && !empty(trim($loadDiagramName))) { - $diagram_id = (int) substr($loadDiagramName, strlen("DIAG")); - if (isset($loadDiagramVersion) && !empty(trim($loadDiagramVersion))) { - $diagramVersion = id(new DiagramVersion())->loadByVersionedDiagramID( - $diagram_id, - $loadDiagramVersion); - } else { - $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( - $diagram_id); - } - if ($diagramVersion) { - $data = $diagramVersion->getData(); - $base64_data = base64_encode($data); - } - } - - $response->setContent('loadJsExtension("' - . $loadDiagramName - . '", "' - . $loadDiagramPHID - . '", "' - . $loadDiagramVersion - . '", "' - . $base64_data - . '");'); - return $response; - } - $root = ''; $file = rtrim($request->getPath(), '/'); $root = dirname(phutil_get_library_root('diagram')); @@ -150,11 +114,11 @@ if ($versioneddiagramid != null && $version != null) { $file = preg_replace( "/^\/diagram\/$versioneddiagramid\/$version" ."iframe\//", - "data/drawio/src/main/webapp/", + "drawio/src/main/webapp/", $file); } else { $file = preg_replace("/^\/diagram\/($diagramid\/?)?iframe\//", - "data/drawio/src/main/webapp/", + "drawio/src/main/webapp/", $file); } } else { @@ -179,20 +143,6 @@ } } - // check if we are trying to load "iframe loader" files, - // if so, correct the path accordingly - if ($file == "data/drawio/src/main/webapp/index.html") { - return $this->showIframe($request); - } - else - if ($file == "data/drawio/src/main/webapp/iframe.css" - || $file == "data/drawio/src/main/webapp/iframe-toolbtn.css" - || $file == "data/drawio/src/main/webapp/iframe1.js" - || $file == "data/drawio/src/main/webapp/iframe2.js" - || $file == "data/drawio/src/main/webapp/iframe-toolbtn.js") { - $file = str_replace("drawio/src/main/webapp/", "", $file); - } - // determine full path $path = $root . '/' . $file; @@ -230,6 +180,7 @@ switch (pathinfo($file, PATHINFO_EXTENSION)) { case 'html': $response = id(new PlainHtmlWebpageResponse()) + ->setDisableContentSecurityPolicy(true) ->setFrameable(true) ->setContent(file_get_contents($path)); break; @@ -265,6 +216,9 @@ } try { $response->setContent(file_get_contents($path)); + $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); + $response->setLastModified(time()); + $response->setCanCDN(true); } catch (Exception $e) { $response->setContent($route); } @@ -464,13 +418,26 @@ */ private function showApplication( AphrontRequest $request, - string $diagramName = null, - string $diagramPHID = null, - string $diagramVersion = null, - string $diagramBase64 = null + string $diagramName = '', + string $diagramPHID = '', + string $diagramVersion = '', + string $base64_data = '' ) { $applicationUrl = "/" . explode("/", $request->getPath())[1]; + $behaviorConfig = array(); + $behaviorConfig['loadJsExtensionArgs'] = array( + $diagramName, + $diagramPHID, + $diagramVersion, + $base64_data + ); + $behaviorConfig['toolbarCss'] = celerity_get_resource_uri('/iframe-toolbtn.css', 'diagram-resources'); + $behaviorConfig['toolbarJs'] = celerity_get_resource_uri('/iframe-toolbtn.js', 'diagram-resources'); + + require_celerity_resource('javelin-behavior'); + Javelin::initBehavior('diagram-extension', $behaviorConfig, 'diagram-resources'); + $content = phutil_tag( 'div', array(), @@ -489,28 +456,6 @@ 'class' => 'drawio', )), )), - phutil_tag( - 'script', - array( - 'src' => 'phorge_extension.js', - 'defer' => true - ), - '' - ), - phutil_tag( - 'script', - array( - 'src' => $applicationUrl . '/loadJsExtension/' - . $diagramName - . '/' - . $diagramPHID - . '/' - . $diagramVersion - . '/', - 'defer' => true - ), - '' - ), phutil_tag( 'div', array( @@ -585,363 +530,4 @@ return $response; } - /** - * Shows the internal draw.io application - */ - private function showIframe( - AphrontRequest $request - ) { - $content = phutil_tag( - 'html', - array(), - array( - phutil_tag( - 'head', - array(), - array( - phutil_tag( - 'title', - array(), - 'Flowchart Maker & Online Diagram Software' - ), - phutil_tag( - 'meta', - array( - 'charset' => 'utf-8' - ) - ), - phutil_tag( - 'meta', - array( - 'http-equiv' => 'content-type', - 'content' => 'text/html; charset=utf-8' - ) - ), - phutil_tag( - 'meta', - array( - 'name' => 'viewport', - 'content' => 'width=device-width, initial-scale=1.0, ' - . 'maximum-scale=1.0, user-scalable=no' - ) - ), - phutil_tag( - 'meta', - array( - 'name' => 'mobile-web-app-capable', - 'content' => 'yes' - ) - ), - phutil_tag( - 'link', - array( - 'rel' => 'stylesheet', - 'type' => 'text/css', - 'href' => 'iframe.css' - ) - ), - phutil_tag( - 'link', - array( - 'rel' => 'stylesheet', - 'type' => 'text/css', - 'href' => 'iframe-toolbtn.css' - ) - ), - phutil_tag( - 'link', - array( - 'rel' => 'stylesheet', - 'type' => 'text/css', - 'href' => 'styles/grapheditor.css' - ) - ), - phutil_tag( - 'script', - array( - 'type' => 'text/javascript', - 'src' => 'iframe1.js', - 'defer' => true - ) - ) - ) - ), - phutil_tag( - 'body', - array( - 'class' => 'geEditor' - ), - array( - phutil_tag( - 'div', - array( - 'id' => 'geinfo' - ), - phutil_tag( - 'div', - array( - 'class' => 'geBlock' - ), - array( - phutil_tag( - 'h1', - array(), - 'Flowchart Maker and Online Diagram Software' - ), - phutil_tag( - 'p', - array(), - 'draw.io is free online diagram software. You can use ' - . 'it as a flowchart maker, network diagram software, ' - . 'to create UML online, as an ER diagram tool, to ' - . 'design database schema, to build BPMN online, as a ' - . 'circuit diagram maker, and more. draw.io can import ' - . '.vsdx, Gliffy™ and Lucidchart™ files.' - ), - phutil_tag( - 'h2', - array( - 'id' => 'gestatus' - ), - 'Loading...' - ), - phutil_tag( - 'div', - array( - 'class' => 'init-spinner' - ), - phutil_tag( - 'div', - array( - 'class' => 'content' - ), - array( - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(0deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -1s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(27deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.923077ss;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(55deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.846154s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(83deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.769231s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(110deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.692308s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(138deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.615385s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(166deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.538462s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(193deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.461538s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(221deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.384615s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(249deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.307692s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(276deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.230769s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(304deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.153846s;' - ) - ) - ), - phutil_tag( - 'div', - array( - 'class' => 'spike', - 'style' => 'transform: rotate(332deg)' - . ' translatex(13px);' - ), - phutil_tag( - 'div', - array( - 'class' => 'animator', - 'style' => 'animation-delay: -0.0769231s;' - ) - ) - ) - ) - ) - ) - ) - ) - ), - phutil_tag( - 'script', - array( - 'type' => 'text/javascript', - 'src' => 'iframe2.js', - 'defer' => true - ) - ), - phutil_tag( - 'script', - array( - 'type' => 'text/javascript', - 'src' => 'iframe-toolbtn.js', - 'defer' => true - ) - ) - ) - ) - ) - ); - - $response = id(new PlainHtmlWebpageResponse()) - ->setFrameable(true) - ->setContent($content); - - return $response; - } } diff --git a/src/remarkup/PhabricatorRemarkupDiagramRule.php b/src/remarkup/PhabricatorRemarkupDiagramRule.php --- a/src/remarkup/PhabricatorRemarkupDiagramRule.php +++ b/src/remarkup/PhabricatorRemarkupDiagramRule.php @@ -1,5 +1,8 @@ getPHID(); - // Generate the appropriate HTML using the data from the Diagram and // file objects. $style = ''; @@ -88,11 +88,12 @@ array( 'style' => $style, 'class' => $class, - 'src' => $diagram->getViewURI(), + 'src' => 'data:image/png;base64,' . $diagram->getBase64Data(), 'alt' => $alt, - 'ondblclick' => 'window.open("/diagram/DIAG' - . $diagram->getDiagramID() - . '", "_blank")', + 'data-sigil' => 'diagram-remarkup-image', + 'data-diagram-version' => $diagram->getPHID(), + 'data-diagram-id' => $diagram->getDiagramID(), + 'title' => 'Double click to edit...' ) ) );