add file explorer

pull/1/head
serapath 8 years ago
parent a51ff50181
commit 40f8abe827
  1. 24
      assets/css/browser-solidity.css
  2. 9
      index.html
  3. 157
      src/app.js
  4. 21
      src/app/editor.js
  5. 318
      src/app/file-explorer.js
  6. 173
      src/app/file-panel.js
  7. 40
      test-browser/tests/fileExplorer.js

@ -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,

@ -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>

@ -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 () {

@ -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 = {}

@ -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`<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>
`
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
}
}

@ -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`<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)
}
}
}

@ -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()
}
Loading…
Cancel
Save