diff --git a/assets/css/browser-solidity.css b/assets/css/browser-solidity.css index a7f5839845e..b09eb240b6a 100644 --- a/assets/css/browser-solidity.css +++ b/assets/css/browser-solidity.css @@ -1,3 +1,6 @@ +html { box-sizing: border-box; } +*, *:before, *:after { box-sizing: inherit; } + body { padding: 0; font-size: 12px; @@ -39,11 +42,11 @@ body { left: 0; } -.files-wrapper { +#tabs-bar { position: absolute; overflow: hidden; top: 0; - left: 5em; + left: 200px; right: 3em; } @@ -75,8 +78,6 @@ body { color: #999; } -.newFile, -.uploadFile, .toggleRHP { display: block; float: left; @@ -124,18 +125,6 @@ body { display: inline-block; } -#input { - border-top: 3px solid #F4F6FF; - padding-top: 0.5em; - font-size: 15px; - position: absolute; - top: 2.5em; - left: 0; - right: 0; - bottom: 0; - min-width: 20vw; -} - #righthand-panel { position: absolute; top: 0; @@ -161,6 +150,7 @@ body { float: right; height: 90%; background-color: white; + padding-right: 1%; } #header #menu { @@ -475,7 +465,7 @@ body { bottom: 0; cursor: col-resize; z-index: 999; - border-right: 3px solid #F4F6FF; + border-right: 2px solid #C6CFF7; } #editor .ace-tm .ace_gutter, diff --git a/index.html b/index.html index d1cca62607e..e882ab0847e 100644 --- a/index.html +++ b/index.html @@ -41,15 +41,16 @@
- - -
+
-
+
+
+
+
diff --git a/src/app.js b/src/app.js index 4da7b9c1fab..2fac32971e6 100644 --- a/src/app.js +++ b/src/app.js @@ -1,10 +1,11 @@ -/* global alert, confirm, prompt, FileReader, Option, Worker, chrome */ +/* global alert, confirm, prompt, Option, Worker, chrome */ 'use strict' var async = require('async') var $ = require('jquery') var base64 = require('js-base64').Base64 var swarmgw = require('swarmgw') +var csjs = require('csjs-inject') var QueryParams = require('./app/query-params') var queryParams = new QueryParams() @@ -24,6 +25,7 @@ var FormalVerification = require('./app/formalVerification') var EventManager = require('./lib/eventManager') var StaticAnalysis = require('./app/staticanalysis/staticAnalysisView') var OffsetToLineColumnConverter = require('./lib/offsetToLineColumnConverter') +var FilePanel = require('./app/file-panel') var examples = require('./app/example-contracts') @@ -84,12 +86,6 @@ var run = function () { loadFiles(filesToLoad) } - // -------- check file upload capabilities ------- - - if (!(window.File || window.FileReader || window.FileList || window.Blob)) { - $('.uploadFile').remove() - } - // ------------------ gist load ---------------- var loadingFromGist = gistHandler.handleLoad(queryParams.get(), function (gistId) { @@ -159,26 +155,83 @@ var run = function () { chromeCloudSync() // ----------------- editor ---------------------- - var editor = new Editor(document.getElementById('input')) - // ----------------- tabbed menu ------------------- - $('#options li').click(function (ev) { - var $el = $(this) - selectTab($el) - }) - - var selectTab = function (el) { - var match = /[a-z]+View/.exec(el.get(0).className) - if (!match) return - var cls = match[0] - if (!el.hasClass('active')) { - el.parent().find('li').removeClass('active') - $('#optionViews').attr('class', '').addClass(cls) - el.addClass('active') + // ---------------- FilePanel -------------------- + /**************************************************************************** + @TODO's + 1. I would put a virtual file called Summary as the root entry of the treeview, which displays the list of the files with the size in bytes of each + + 2. drag'n'drop to enable to rename files&folders in the file explorer into different sub folders + + 3. I would put a virtual file called `Summary` as the root entry of the treeview, which displays the list of the files with the size in bytes of each. + + 4. add maybe more tape tests + + 5. gist imports + copy to the browser => phase of writing + + 6. add filemanagement from righthand panel to filepanel compoennt (editing/imports/exports, public gist, load from github, create new project, ... setup load and modify files) + */ + // var sources = { + // 'test/client/credit.sol': '', + // 'src/voting.sol': '', + // 'src/leasing.sol': '', + // 'src/gmbh/contract.sol': false, + // 'src/gmbh/test.sol': false, + // 'src/gmbh/company.sol': false, + // 'src/gmbh/node_modules/ballot.sol': false, + // 'src/ug/finance.sol': false, + // 'app/solidity/mode.sol': true, + // 'app/ethereum/constitution.sol': true + // } + // Object.keys(sources).forEach(function (key) { files.set(key, sources[key]) }) + /****************************************************************************/ + var css = csjs` + .filepanel { + display : flex; + width : 200px; + } + ` + var filepanel = document.querySelector('#filepanel') + filepanel.className = css.filepanel + var FilePanelAPI = { + createName: createNonClashingName, + switchToFile: switchToFile + } + var el = new FilePanel(FilePanelAPI, files) + filepanel.appendChild(el) + var api = el.api + + api.register('ui', function changeLayout (data) { + var value + if (data.type === 'minimize') { + value = -parseInt(window['filepanel'].style.width) + value = (isNaN(value) ? -window['filepanel'].getBoundingClientRect().width : value) + window['filepanel'].style.position = 'absolute' + window['filepanel'].style.left = (value - 5) + 'px' + window['filepanel'].style.width = -value + 'px' + window['tabs-bar'].style.left = '45px' + } else if (data.type === 'maximize') { + value = -parseInt(window['filepanel'].style.left) + 'px' + window['filepanel'].style.position = 'static' + window['filepanel'].style.width = value + window['filepanel'].style.left = '' + window['tabs-bar'].style.left = value + } else { + window['filepanel'].style.width = data.width + 'px' + window['tabs-bar'].style.left = data.width + 'px' } - self.event.trigger('tabChanged', [cls]) - } + }) + api.register('focus', function (path) { + [...window.files.querySelectorAll('.file .name')].forEach(function (span) { + if (span.innerText === path) switchToFile(path) // @TODO: scroll into view + }) + }) + files.event.register('fileRenamed', function (oldName, newName) { + [...window.files.querySelectorAll('.file .name')].forEach(function (span) { + if (span.innerText === oldName) span.innerText = newName + }) + }) // ------------------ gist publish -------------- @@ -221,39 +274,26 @@ var run = function () { }).appendTo('body') }) - // ----------------- file selector------------- - - var $filesEl = $('#files') - var FILE_SCROLL_DELTA = 300 - - $('.newFile').on('click', function () { - var newName = createNonClashingName('Untitled') - if (!files.set(newName, '')) { - alert('Failed to create file ' + newName) - } else { - switchToFile(newName) - } + // ---------------- tabbed menu ------------------ + $('#options li').click(function (ev) { + var $el = $(this) + selectTab($el) }) - // ----------------- file upload ------------- - - $('.inputFile').on('change', function () { - var fileList = $('input.inputFile')[0].files - for (var i = 0; i < fileList.length; i++) { - var name = fileList[i].name - if (!files.exists(name) || confirm('The file ' + name + ' already exists! Would you like to overwrite it?')) { - var fileReader = new FileReader() - fileReader.onload = function (ev) { - if (!files.set(name, ev.target.result)) { - alert('Failed to create file ' + name) - } else { - switchToFile(name) - } - } - fileReader.readAsText(fileList[i]) - } + var selectTab = function (el) { + var match = /[a-z]+View/.exec(el.get(0).className) + if (!match) return + var cls = match[0] + if (!el.hasClass('active')) { + el.parent().find('li').removeClass('active') + $('#optionViews').attr('class', '').addClass(cls) + el.addClass('active') } - }) + self.event.trigger('tabChanged', [cls]) + } + + var $filesEl = $('#files') + var FILE_SCROLL_DELTA = 300 // Switch tab $filesEl.on('click', '.file:not(.active)', function (ev) { @@ -325,6 +365,8 @@ var run = function () { currentFile = file + files.event.trigger('fileFocus', [file]) + if (files.isReadOnly(file)) { editor.openReadOnly(file, files.get(file)) } else { @@ -368,7 +410,6 @@ var run = function () { }) } - var $filesWrapper = $('.files-wrapper') var $scrollerRight = $('.scroller-right') var $scrollerLeft = $('.scroller-left') @@ -381,12 +422,8 @@ var run = function () { return itemsWidth } - // function widthOfHidden () { - // return ($filesWrapper.outerWidth() - widthOfList() - getLeftPosi()) - // } - function widthOfVisible () { - return $filesWrapper.outerWidth() + return document.querySelector('#editor-container').offsetWidth } function getLeftPosi () { diff --git a/src/app/editor.js b/src/app/editor.js index e5f4e59e0a0..19475be4d79 100644 --- a/src/app/editor.js +++ b/src/app/editor.js @@ -2,13 +2,34 @@ var EventManager = require('../lib/eventManager') +var csjs = require('csjs-inject') var ace = require('brace') var Range = ace.acequire('ace/range').Range require('../mode-solidity.js') +var css = csjs` + .editor-container { + display : flex; + position : absolute; + top : 2.5em; + left : 0; + right : 0; + bottom : 0; + min-width : 20vw; + } + .ace-editor { + top : 4px; + border-top : 3px solid transparent; + font-size : 15px; + width : 100%; + } +` +document.querySelector('#editor-container').className = css['editor-container'] + function Editor (editorElement) { var editor = ace.edit(editorElement) editorElement.editor = editor // required to access the editor during tests + editorElement.className += ' ' + css['ace-editor'] var event = new EventManager() this.event = event var sessions = {} diff --git a/src/app/file-explorer.js b/src/app/file-explorer.js new file mode 100755 index 00000000000..800afde4009 --- /dev/null +++ b/src/app/file-explorer.js @@ -0,0 +1,318 @@ +/* global FileReader, confirm, alert */ +var yo = require('yo-yo') +var csjs = require('csjs-inject') +var Treeview = require('ethereum-remix').ui.TreeView + +var EventManager = require('../lib/eventManager') + +var css = csjs` + .fileexplorer { + box-sizing : border-box; + } + .folder, + .file { + font-size : 14px; + } + .hasFocus { + background-color : #F4F6FF; + } + .rename { + background-color : white; + } + .remove { + align-self : center; + padding-left : 10px; + } + .activeMode { + display : flex; + justify-content : space-between; + margin-right : 10px; + padding-right : 19px; + } + ul { + padding : 0; + } +` +module.exports = fileExplorer + +function fileExplorer (appAPI, files) { + var fileEvents = files.event + var tv = new Treeview({ + extractData: function (value, tree, key) { + var newValue = {} + // var isReadOnly = false + var isFile = false + Object.keys(value).filter(function keep (x) { + if (x === '/content') isFile = true + // if (x === '/readOnly') isReadOnly = true + if (x[0] !== '/') return true + }).forEach(function (x) { newValue[x] = value[x] }) + return { + path: (tree || {}).path ? tree.path + '/' + key : key, + children: isFile ? undefined + : value instanceof Array ? value.map((item, index) => ({ + key: index, value: item + })) : value instanceof Object ? Object.keys(value).map(subkey => ({ + key: subkey, value: value[subkey] + })) : undefined + } + }, + formatSelf: function (key, data) { + return yo`` + } + }) + + var deleteButton = yo` + + + + ` + + fileEvents.register('fileFocus', fileFocus) + fileEvents.register('fileRemoved', fileRemoved) + fileEvents.register('fileRenamed', fileRenamed) + fileEvents.register('fileAdded', fileAdded) + fileEvents.register('fileChanged', fileChanged) + + var filepath = null + var focusElement = null + var textUnderEdit = null + + var element = tv.render(files.listAsTree()) + element.className = css.fileexplorer + + var api = new EventManager() + api.addFile = function addFile (file) { + var name = file.name + if (!files.exists(name) || confirm('The file ' + name + ' already exists! Would you like to overwrite it?')) { + var fileReader = new FileReader() + fileReader.onload = function (event) { + var success = files.set(name, event.target.result) + if (!success) alert('Failed to create file ' + name) + else api.trigger('focus', [name]) + } + fileReader.readAsText(file) + } + } + + function focus (event) { + event.cancelBubble = true + var li = this + if (focusElement === li) return + if (focusElement) focusElement.classList.toggle(css.hasFocus) + focusElement = li + focusElement.classList.toggle(css.hasFocus) + var label = getLabelFrom(li) + var filepath = label.dataset.path + var isFile = label.className.indexOf('file') === 0 + if (isFile) api.trigger('focus', [filepath]) + } + + function hover (event) { + if (event.type === 'mouseout') { + var exitedTo = event.toElement || event.relatedTarget + if (this.contains(exitedTo)) return + this.style.backgroundColor = '' + this.style.paddingRight = '19px' + return this.removeChild(deleteButton) + } + this.style.backgroundColor = '#F4F6FF' + this.style.paddingRight = '0px' + this.appendChild(deleteButton) + } + + function getElement (path) { + var label = element.querySelector(`label[data-path="${path}"]`) + if (label) return getLiFrom(label) + } + + function deletePath (event) { + event.cancelBubble = true + var span = this + var li = span.parentElement.parentElement + var label = getLabelFrom(li) + var path = label.dataset.path + var isFolder = !!~label.className.indexOf('folder') + if (confirm(` + Do you really want to delete "${path}" ? + ${isFolder ? '(and all contained files and folders)' : ''} + `)) { + li.parentElement.removeChild(li) + removeSubtree(files, path) + } + } + + function editModeOn (event) { + var label = this + var li = getLiFrom(label) + var classes = li.className + if (~classes.indexOf('hasFocus') && !label.getAttribute('contenteditable')) { + textUnderEdit = label.innerText + label.setAttribute('contenteditable', true) + label.classList.add(css.rename) + label.focus() + } + } + + function editModeOff (event) { + var label = this + if (event.type === 'blur' || event.which === 27 || event.which === 13) { + var save = textUnderEdit !== label.innerText + if (event.which === 13) event.preventDefault() + if (save && event.which !== 13) save = confirm('Do you want to rename?') + if (save) renameSubtree(label) + else label.innerText = textUnderEdit + label.removeAttribute('contenteditable') + label.classList.remove(css.rename) + } + } + + function renameSubtree (label, dontcheck) { + var oldPath = label.dataset.path + var newPath = oldPath + newPath = newPath.split('/') + newPath[newPath.length - 1] = label.innerText + newPath = newPath.join('/') + if (!dontcheck) { + var allPaths = Object.keys(files.list()) + for (var i = 0, len = allPaths.length, path, err; i < len; i++) { + path = allPaths[i] + if (files.IsReadOnly(path)) { + err = 'path contains readonly elements' + break + } else if (path.indexOf(newPath) === 0) { + err = 'new path is conflicting with another existing path' + break + } + } + } + if (err) { + alert(`couldn't rename - ${err}`) + label.innerText = textUnderEdit + } else { + textUnderEdit = label.innerText + updateAllLabels([getElement(oldPath)], oldPath, newPath) + } + } + + function updateAllLabels (lis, oldPath, newPath) { + lis.forEach(function (li) { + var label = getLabelFrom(li) + var path = label.dataset.path + var newName = path.replace(oldPath, newPath) + label.dataset.path = newName + var isFile = label.className.indexOf('file') === 0 + if (isFile) files.rename(path, newName) + var ul = li.lastChild + if (ul.tagName === 'UL') { + updateAllLabels([...ul.children], oldPath, newPath) + } + }) + } + + function fileChanged (filepath) { } + + function fileFocus (path) { + if (filepath === path) return + filepath = path + var el = getElement(filepath) + expandPathTo(el) + setTimeout(function focusNode () { el.click() }, 0) + } + + function fileRemoved (filepath) { + var li = getElement(filepath) + if (li) li.parentElement.removeChild(li) + } + + function fileRenamed (oldName, newName) { + var li = getElement(oldName) + if (li) { + oldName = oldName.split('/') + newName = newName.split('/') + var index = oldName.reduce(function (idx, key, i) { + return oldName[i] !== newName[i] ? i : idx + }, undefined) + var newKey = newName[index] + var oldPath = oldName.slice(0, index + 1).join('/') + li = getElement(oldPath) + var label = getLabelFrom(li) + label.innerText = newKey + renameSubtree(label, true) + } + } + + function fileAdded (filepath) { + var el = tv.render(files.listAsTree()) + el.className = css.fileexplorer + element.parentElement.replaceChild(el, element) + element = el + fileFocus(filepath) + appAPI.switchToFile(filepath) + } + + element.api = api + return element +} +/****************************************************************************** + HELPER FUNCTIONS +******************************************************************************/ +function adaptEnvironment (label, focus, hover) { + var li = getLiFrom(label) + li.style.position = 'relative' + var span = li.firstChild + // add focus + li.addEventListener('click', focus) + // add hover + span.classList.add(css.activeMode) + span.addEventListener('mouseover', hover) + span.addEventListener('mouseout', hover) +} + +function unadaptEnvironment (label, focus, hover) { + var li = getLiFrom(label) + var span = li.firstChild + li.style.position = undefined + // remove focus + li.removeEventListener('click', focus) + // remove hover + span.classList.remove(css.activeMode) + span.removeEventListener('mouseover', hover) + span.removeEventListener('mouseout', hover) +} + +function getLiFrom (label) { + return label.parentElement.parentElement.parentElement +} + +function getLabelFrom (li) { + return li.children[0].children[1].children[0] +} + +function removeSubtree (files, path) { + var allPaths = Object.keys(files.list()) // @TODO: change `files` + var removePaths = allPaths.filter(function (p) { return ~p.indexOf(path) }) + removePaths.forEach(function (path) { + [...window.files.querySelectorAll('.file .name')].forEach(function (span) { + if (span.innerText === path) { + var li = span.parentElement + li.parentElement.removeChild(li) // delete tab + } + }) + files.remove(path) + }) +} + +function expandPathTo (li) { + while ((li = li.parentElement.parentElement) && li.tagName === 'LI') { + var caret = li.firstChild.firstChild + if (caret.classList.contains('fa-caret-right')) caret.click() // expand + } +} diff --git a/src/app/file-panel.js b/src/app/file-panel.js new file mode 100644 index 00000000000..a296e258045 --- /dev/null +++ b/src/app/file-panel.js @@ -0,0 +1,173 @@ +/* global alert */ +var csjs = require('csjs-inject') +var yo = require('yo-yo') + +var EventManager = require('../lib/eventManager') +var FileExplorer = require('./file-explorer') + +module.exports = filepanel + +var css = csjs` + .container { + display : flex; + flex-direction : row; + width : 100%; + box-sizing : border-box; + } + .fileexplorer { + display : flex; + flex-direction : column; + position : relative; + top : -33px; + width : 100%; + } + .menu { + display : flex; + flex-direction : row; + } + .newFile { + padding : 10px; + } + .uploadFile { + padding : 10px; + } + .toggleLHP { + display : flex; + justify-content : flex-end; + padding : 10px; + width : 100%; + font-weight : bold; + cursor : pointer; + color : black; + } + .isVisible { + position : absolute; + left : 35px; + } + .isHidden { + position : absolute; + height : 99% + left : -101%; + } + .treeview { + height : 100%; + background-color : white; + } + .dragbar { + position : relative; + top : 6px; + cursor : col-resize; + z-index : 999; + border-right : 2px solid #C6CFF7; + } + .ghostbar { + width : 3px; + background-color : #C6CFF7; + opacity : 0.5; + position : absolute; + cursor : col-resize; + z-index : 9999; + top : 0; + bottom : 0; + } +` + +var limit = 60 +var canUpload = window.File || window.FileReader || window.FileList || window.Blob +var ghostbar = yo`
` + +function filepanel (appAPI, files) { + var fileExplorer = new FileExplorer(appAPI, files) + var dragbar = yo`
` + + function template () { + return yo` +
+
+
+ + + + ${canUpload ? yo` + + + + ` : ''} + + + +
+
${fileExplorer}
+
+ ${dragbar} +
+ ` + } + + var api = new EventManager() + var element = template() + element.api = api + fileExplorer.api.register('focus', function (path) { + api.trigger('focus', [path]) + }) + + return element + + function toggle (event) { + var isHidden = element.classList.toggle(css.isHidden) + this.classList.toggle(css.isVisible) + this.children[0].classList.toggle('fa-angle-double-right') + this.children[0].classList.toggle('fa-angle-double-left') + api.trigger('ui', [{ type: isHidden ? 'minimize' : 'maximize' }]) + } + + function uploadFile (event) { + ;[...this.files].forEach(fileExplorer.api.addFile) + } + + function mousedown (event) { + event.preventDefault() + if (event.which === 1) { + moveGhostbar(event) + document.body.appendChild(ghostbar) + document.addEventListener('mousemove', moveGhostbar) + document.addEventListener('mouseup', removeGhostbar) + document.addEventListener('keydown', cancelGhostbar) + } + } + function cancelGhostbar (event) { + if (event.keyCode === 27) { + document.body.removeChild(ghostbar) + document.removeEventListener('mousemove', moveGhostbar) + document.removeEventListener('mouseup', removeGhostbar) + document.removeEventListener('keydown', cancelGhostbar) + } + } + function moveGhostbar (event) { + var rhp = window['righthand-panel'].offsetLeft + var newpos = (event.pageX < limit) ? limit : event.pageX + newpos = (newpos < (rhp - limit)) ? newpos : (rhp - limit) + ghostbar.style.left = newpos + 'px' + } + + function removeGhostbar (event) { + document.body.removeChild(ghostbar) + document.removeEventListener('mousemove', moveGhostbar) + document.removeEventListener('mouseup', removeGhostbar) + document.removeEventListener('keydown', cancelGhostbar) + var width = (event.pageX < limit) ? limit : event.pageX + element.style.width = width + 'px' + api.trigger('ui', [{ type: 'resize', width: width }]) + } + + function createNewFile () { + var newName = appAPI.createName('Untitled') + if (!files.set(newName, '')) { + alert('Failed to create file ' + newName) + } else { + appAPI.switchToFile(newName) + } + } +} diff --git a/test-browser/tests/fileExplorer.js b/test-browser/tests/fileExplorer.js new file mode 100644 index 00000000000..c530b9a4226 --- /dev/null +++ b/test-browser/tests/fileExplorer.js @@ -0,0 +1,40 @@ +'use strict' + +var examples = require('../../src/app/example-contracts') +var init = require('../helpers/init') +var sauce = require('./sauce') + +var sources = { + 'sources': { + 'ballot.sol': examples.ballot.content, + 'test/client/credit.sol': '', + 'src/voting.sol': '', + 'src/leasing.sol': '', + 'src/gmbh/contract.sol': false, + 'src/gmbh/test.sol': false, + 'src/gmbh/company.sol': false, + 'src/gmbh/node_modules/ballot.sol': false, + 'src/ug/finance.sol': false, + 'app/solidity/mode.sol': true, + 'app/ethereum/constitution.sol': true + } +} + +module.exports = { + before: function (browser, done) { + init(browser, done) + }, + '@sources': function () { + return sources + }, + 'FileExplorer': function (browser) { + runTests(browser) + }, + tearDown: sauce +} + +function runTests (browser, testData) { + browser + .waitForElementPresent('#filepanel ul li', 10000, true, function () {}) + .end() +}