Commit 62cd8797 authored by chriseth's avatar chriseth Committed by GitHub

Merge pull request #449 from serapath-contribution/file-explorer

File explorer (WIP)
parents b0234ae5 f09f5a38
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,
......
......@@ -41,15 +41,16 @@
<body>
<div id="editor">
<span class="newFile" title="New File"><i class="fa fa-plus-circle" aria-hidden="true"></i></span>
<span class="uploadFile" title="Open local file"><label class="fa fa-folder-open"><input type="file" class="inputFile" multiple /></label></span>
<div class="files-wrapper">
<div id="tabs-bar">
<div class="scroller scroller-left"><i class="fa fa-chevron-left "></i></div>
<div class="scroller scroller-right"><i class="fa fa-chevron-right "></i></div>
<ul id="files" class="nav nav-tabs"></ul>
</div>
<span class="toggleRHP" title="Toggle right hand panel"><i class="fa fa-angle-double-right"></i></span>
<div id="input"></div>
<div id="editor-container">
<div id="filepanel"></div>
<div id="input"></div>
</div>
<div id="dragbar"></div>
</div>
......
......@@ -63,7 +63,6 @@ module.exports = {
'browserName': 'internet explorer',
'javascriptEnabled': true,
'acceptSslCerts': true,
'platform': 'WIN8.1',
'version': '11',
'build': 'build-' + TRAVIS_JOB_NUMBER,
'tunnel-identifier': 'browsersolidity_tests_' + TRAVIS_JOB_NUMBER
......
......@@ -5,39 +5,14 @@
"description": "Minimalistic browser-based Solidity IDE",
"devDependencies": {
"async": "^2.1.2",
"babel-cli": "^6.16.0",
"babel-eslint": "^7.1.1",
"babel-plugin-check-es2015-constants": "^6.8.0",
"babel-plugin-transform-es2015-arrow-functions": "^6.8.0",
"babel-plugin-transform-es2015-block-scoped-functions": "^6.8.0",
"babel-plugin-transform-es2015-block-scoping": "^6.18.0",
"babel-plugin-transform-es2015-classes": "^6.18.0",
"babel-plugin-transform-es2015-computed-properties": "^6.8.0",
"babel-plugin-transform-es2015-destructuring": "^6.18.0",
"babel-plugin-transform-es2015-duplicate-keys": "^6.8.0",
"babel-plugin-transform-es2015-for-of": "^6.18.0",
"babel-plugin-transform-es2015-function-name": "^6.9.0",
"babel-plugin-transform-es2015-literals": "^6.8.0",
"babel-plugin-transform-es2015-object-super": "^6.8.0",
"babel-plugin-transform-es2015-parameters": "^6.18.0",
"babel-plugin-transform-es2015-shorthand-properties": "^6.18.0",
"babel-plugin-transform-es2015-spread": "^6.8.0",
"babel-plugin-transform-es2015-sticky-regex": "^6.8.0",
"babel-plugin-transform-es2015-template-literals": "^6.8.0",
"babel-plugin-transform-es2015-unicode-regex": "^6.11.0",
"babel-plugin-transform-regenerator": "^6.16.1",
"babel-polyfill": "^6.22.0",
"babel-plugin-yo-yoify": "^0.3.3",
"babel-polyfill": "^6.22.0",
"babel-preset-es2015": "^6.24.0",
"babelify": "^7.3.0",
"brace": "^0.8.0",
"browserify": "^13.0.0",
"browserify-reload": "^1.0.3",
"csjs-inject": "^1.0.1",
"csslint": "^1.0.2",
"ethereum-remix": "https://github.com/ethereum/remix",
"ethereumjs-abi": "https://github.com/ethereumjs/ethereumjs-abi",
......@@ -160,6 +135,6 @@
"start": "npm-run-all -lpr serve watch onchange",
"test": "standard; npm run csslint; node test/index.js",
"test-browser": "npm-run-all -lpr selenium downloadsolc make-mock-compiler serve browsertest",
"watch": "watchify src/index.js -dv --delay 0 -p browserify-reload -o '| npm run sourcemap'"
"watch": "watchify src/index.js --transform-key=development -dv -p browserify-reload -o build/app.js"
}
}
This diff is collapsed.
......@@ -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 = {}
......
......@@ -147,7 +147,7 @@ function ExecutionContext () {
selectExEnv.value = executionContext
}
})
selectExEnv.value = executionContext
}
......
/* 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 appUI = appAPI.ui
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`<label class=${data.children ? css.folder : css.file}
data-path="${data.path}"
onload=${function (el) { adaptEnvironment(el, focus, hover) }}
onunload=${function (el) { unadaptEnvironment(el, focus, hover) }}
onclick=${editModeOn}
onkeydown=${editModeOff}
onblur=${editModeOff}
>${key}</label>`
}
})
var deleteButton = yo`
<span class=${css.remove} onclick=${deletePath}>
<i class="fa fa-trash" aria-hidden="true"></i>
</span>
`
appUI.register('currentFile', 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 (isFolder) path += '/'
if (confirm(`Do you really want to delete "${path}" ?`)) {
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 parts = path.split('/')
var isFile = parts[parts.length - 1].length
var removePaths = isFile ? [path] : Object.keys(files.list()).filter(keep)
function keep (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
}
}
/* 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`<div class=${css.ghostbar}></div>`
function filepanel (appAPI, files) {
var fileExplorer = new FileExplorer(appAPI, files)
var dragbar = yo`<div onmousedown=${mousedown} class=${css.dragbar}></div>`
function template () {
return yo`
<div class=${css.container}>
<div class=${css.fileexplorer}>
<div class=${css.menu}>
<span onclick=${createNewFile} class="newFile ${css.newFile}" title="New File">
<i class="fa fa-plus-circle"></i>
</span>
${canUpload ? yo`
<span class=${css.uploadFile} title="Open local file">
<label class="fa fa-folder-open">
<input type="file" onchange=${uploadFile} multiple />
</label>
</span>
` : ''}
<span class=${css.toggleLHP} onclick=${toggle} title="Toggle left hand panel">
<i class="fa fa-angle-double-left"></i>
</span>
</div>
<div class=${css.treeview}>${fileExplorer}</div>
</div>
${dragbar}
</div>
`
}
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)
}
}
}
'use strict'
function Storage () {
function Storage (prefix) {
this.exists = function (name) {
return this.get(name) !== null
}
this.get = function (name) {
return window.localStorage.getItem('sol:' + name)
return window.localStorage.getItem(prefix + name)
}
this.set = function (name, content) {
try {
window.localStorage.setItem('sol:' + name, content)
window.localStorage.setItem(prefix + name, content)
} catch (exception) {
return false
}
......@@ -19,7 +19,7 @@ function Storage () {
}
this.remove = function (name) {
window.localStorage.removeItem('sol:' + name)
window.localStorage.removeItem(prefix + name)
return true
}
......@@ -40,7 +40,7 @@ function Storage () {
this.keys = function () {
return safeKeys()
// filter any names not including sol:
.filter(function (item) { return item.indexOf('sol:', 0) === 0 })
.filter(function (item) { return item.indexOf(prefix, 0) === 0 })
// remove sol: from filename
.map(function (item) { return item.replace(/^sol:/, '') })
}
......@@ -49,7 +49,7 @@ function Storage () {
safeKeys().forEach(function (name) {
if (name.indexOf('sol-cache-file-', 0) === 0) {
var content = window.localStorage.getItem(name)
window.localStorage.setItem(name.replace(/^sol-cache-file-/, 'sol:'), content)
window.localStorage.setItem(name.replace(/^sol-cache-file-/, prefix), content)
window.localStorage.removeItem(name)
}
})
......
'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()
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment