Commit 2aed8798 authored by yann300's avatar yann300

system files

parent d3537b31
...@@ -461,3 +461,69 @@ input[type="file"] { ...@@ -461,3 +461,69 @@ input[type="file"] {
.ace_gutter-cell.ace_breakpoint{ .ace_gutter-cell.ace_breakpoint{
background-color: #F77E79; background-color: #F77E79;
} }
/* The Modal (background) */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 6; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Header */
.modal-header {
padding: 2px 16px;
background-color: orange;
color: white;
}
/* Modal Body */
.modal-body {padding: 2px 16px;}
/* Modal Footer */
.modal-footer {
padding: 2px 16px;
background-color: orange;
color: white;
}
/* Modal Content */
.modal-content {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 0;
border: 1px solid #888;
width: 50%;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s
}
#modal-footer-cancel {
margin-left: 1em;
cursor: pointer;
}
#modal-footer-ok {
cursor: pointer;
}
/* Add Animation */
@-webkit-keyframes animatetop {
from {top: -300px; opacity: 0}
to {top: 0; opacity: 1}
}
@keyframes animatetop {
from {top: -300px; opacity: 0}
to {top: 0; opacity: 1}
}
...@@ -54,5 +54,17 @@ ...@@ -54,5 +54,17 @@
<script src="build/app.js"></script> <script src="build/app.js"></script>
<div id="modaldialog" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Localhost connection</h2>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<span id="modal-footer-ok">OK</span><span id="modal-footer-cancel">Cancel</span>
</div>
</div>
</div>
</body> </body>
</html> </html>
...@@ -12,8 +12,10 @@ var queryParams = new QueryParams() ...@@ -12,8 +12,10 @@ var queryParams = new QueryParams()
var GistHandler = require('./app/gist-handler') var GistHandler = require('./app/gist-handler')
var gistHandler = new GistHandler() var gistHandler = new GistHandler()
var Storage = require('./app/storage') var Remixd = require('./lib/remixd')
var Files = require('./app/files') var Storage = require('./app/files/storage')
var Browserfiles = require('./app/files/browser-files')
var Systemfiles = require('./app/files/system-files')
var Config = require('./app/config') var Config = require('./app/config')
var Editor = require('./app/editor') var Editor = require('./app/editor')
var Renderer = require('./app/renderer') var Renderer = require('./app/renderer')
...@@ -42,22 +44,31 @@ var run = function () { ...@@ -42,22 +44,31 @@ var run = function () {
var self = this var self = this
this.event = new EventManager() this.event = new EventManager()
var fileStorage = new Storage('sol:') var fileStorage = new Storage('sol:')
var files = new Files(fileStorage)
var config = new Config(fileStorage) var config = new Config(fileStorage)
var remixd = new Remixd()
var filesProviders = {}
filesProviders['browser'] = new Browserfiles(fileStorage)
filesProviders['localhost'] = new Systemfiles(remixd)
// return all the files, except the temporary/readonly ones var tabbedFiles = {} // list of files displayed in the tabs bar
function packageFiles () {
// return all the files, except the temporary/readonly ones.. package only files from the browser storage.
function packageFiles (cb) {
var ret = {} var ret = {}
Object.keys(files.list()) var files = filesProviders['browser']
.filter(function (path) { if (!files.isReadOnly(path)) { return path } }) var filtered = Object.keys(files.list()).filter(function (path) { if (!files.isReadOnly(path)) { return path } })
.map(function (path) { ret[path] = { content: files.get(path) } }) async.eachSeries(filtered, function (path, cb) {
return ret ret[path] = { content: files.get(path) }
cb()
}, () => {
cb(ret)
})
} }
function createNonClashingName (path) { function createNonClashingName (path) {
var counter = '' var counter = ''
if (path.endsWith('.sol')) path = path.substring(0, path.lastIndexOf('.sol')) if (path.endsWith('.sol')) path = path.substring(0, path.lastIndexOf('.sol'))
while (files.exists(path + counter + '.sol')) { while (filesProviders['browser'].exists(path + counter + '.sol')) {
counter = (counter | 0) + 1 counter = (counter | 0) + 1
} }
return path + counter + '.sol' return path + counter + '.sol'
...@@ -66,7 +77,7 @@ var run = function () { ...@@ -66,7 +77,7 @@ var run = function () {
// Add files received from remote instance (i.e. another browser-solidity) // Add files received from remote instance (i.e. another browser-solidity)
function loadFiles (filesSet) { function loadFiles (filesSet) {
for (var f in filesSet) { for (var f in filesSet) {
files.set(createNonClashingName(f), filesSet[f].content) filesProviders['browser'].set(createNonClashingName(f), filesSet[f].content)
} }
switchToNextFile() switchToNextFile()
} }
...@@ -101,8 +112,8 @@ var run = function () { ...@@ -101,8 +112,8 @@ var run = function () {
}) })
// insert ballot contract if there are no files available // insert ballot contract if there are no files available
if (!loadingFromGist && Object.keys(files.list()).length === 0) { if (!loadingFromGist && Object.keys(filesProviders['browser'].list()).length === 0) {
if (!files.set(examples.ballot.name, examples.ballot.content)) { if (!filesProviders['browser'].set(examples.ballot.name, examples.ballot.content)) {
alert('Failed to store example contract in browser. Remix will not work properly. Please ensure Remix has access to LocalStorage. Safari in Private mode is known not to work.') alert('Failed to store example contract in browser. Remix will not work properly. Please ensure Remix has access to LocalStorage. Safari in Private mode is known not to work.')
} }
} }
...@@ -123,11 +134,16 @@ var run = function () { ...@@ -123,11 +134,16 @@ var run = function () {
console.log('comparing to cloud', key, resp) console.log('comparing to cloud', key, resp)
if (typeof resp[key] !== 'undefined' && obj[key] !== resp[key] && confirm('Overwrite "' + key + '"? Click Ok to overwrite local file with file from cloud. Cancel will push your local file to the cloud.')) { if (typeof resp[key] !== 'undefined' && obj[key] !== resp[key] && confirm('Overwrite "' + key + '"? Click Ok to overwrite local file with file from cloud. Cancel will push your local file to the cloud.')) {
console.log('Overwriting', key) console.log('Overwriting', key)
files.set(key, resp[key]) filesProviders['browser'].set(key, resp[key])
refreshTabs()
} else { } else {
console.log('add to obj', obj, key) console.log('add to obj', obj, key)
obj[key] = files.get(key) filesProviders['browser'].get(key, (error, content) => {
if (error) {
console.log(error)
} else {
obj[key] = content
}
})
} }
done++ done++
if (done >= count) { if (done >= count) {
...@@ -138,12 +154,18 @@ var run = function () { ...@@ -138,12 +154,18 @@ var run = function () {
}) })
} }
for (var y in files.list()) { for (var y in filesProviders['browser'].list()) {
console.log('checking', y) console.log('checking', y)
obj[y] = files.get(y) filesProviders['browser'].get(y, (error, content) => {
if (error) {
console.log(error)
} else {
obj[y] = content
count++ count++
check(y) check(y)
} }
})
}
} }
window.syncStorage = chromeCloudSync window.syncStorage = chromeCloudSync
...@@ -167,9 +189,18 @@ var run = function () { ...@@ -167,9 +189,18 @@ var run = function () {
event: this.event, event: this.event,
editorFontSize: function (incr) { editorFontSize: function (incr) {
editor.editorFontSize(incr) editor.editorFontSize(incr)
},
currentFile: function () {
return config.get('currentFile')
},
currentContent: function () {
return editor.get(config.get('currentFile'))
},
setText: function (text) {
editor.setText(text)
} }
} }
var filePanel = new FilePanel(FilePanelAPI, files) var filePanel = new FilePanel(FilePanelAPI, filesProviders)
// TODO this should happen inside file-panel.js // TODO this should happen inside file-panel.js
filepanelContainer.appendChild(filePanel) filepanelContainer.appendChild(filePanel)
...@@ -194,29 +225,63 @@ var run = function () { ...@@ -194,29 +225,63 @@ var run = function () {
window['filepanel'].style.width = width + 'px' window['filepanel'].style.width = width + 'px'
window['tabs-bar'].style.left = width + 'px' window['tabs-bar'].style.left = width + 'px'
}) })
files.event.register('fileRenamed', function (oldName, newName) {
function fileRenamedEvent (oldName, newName, isFolder) {
// TODO please never use 'window' when it is possible to use a variable // TODO please never use 'window' when it is possible to use a variable
// that references the DOM node // that references the DOM node
[...window.files.querySelectorAll('.file .name')].forEach(function (span) { [...window.files.querySelectorAll('.file .name')].forEach(function (span) {
if (span.innerText === oldName) span.innerText = newName if (span.innerText === oldName) span.innerText = newName
}) })
}) if (!isFolder) {
files.event.register('fileRemoved', function (path) { config.set('currentFile', '')
editor.discard(oldName)
if (tabbedFiles[oldName]) {
delete tabbedFiles[oldName]
tabbedFiles[newName] = newName
}
switchToFile(newName)
} else {
var newFocus
for (var k in tabbedFiles) {
if (k.indexOf(oldName + '/') === 0) {
var newAbsolutePath = k.replace(oldName, newName)
tabbedFiles[newAbsolutePath] = newAbsolutePath
delete tabbedFiles[k]
if (config.get('currentFile') === k) {
newFocus = newAbsolutePath
}
}
}
if (newFocus) {
switchToFile(newFocus)
}
}
refreshTabs()
}
filesProviders['browser'].event.register('fileRenamed', fileRenamedEvent)
filesProviders['localhost'].event.register('fileRenamed', fileRenamedEvent)
function fileRemovedEvent (path) {
if (path === config.get('currentFile')) { if (path === config.get('currentFile')) {
config.set('currentFile', '') config.set('currentFile', '')
switchToNextFile() switchToNextFile()
} }
editor.discard(path) editor.discard(path)
delete tabbedFiles[path]
refreshTabs() refreshTabs()
}) }
files.event.register('fileAdded', function (path) { filesProviders['browser'].event.register('fileRemoved', fileRemovedEvent)
refreshTabs() filesProviders['localhost'].event.register('fileRemoved', fileRemovedEvent)
})
// ------------------ gist publish -------------- // ------------------ gist publish --------------
$('#gist').click(function () { $('#gist').click(function () {
if (confirm('Are you sure you want to publish all your files anonymously as a public gist on github.com?')) { if (confirm('Are you sure you want to publish all your files anonymously as a public gist on github.com?')) {
var files = packageFiles() packageFiles((error, packaged) => {
if (error) {
console.log(error)
} else {
var description = 'Created using browser-solidity: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://ethereum.github.io/browser-solidity/#version=' + queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&gist=' var description = 'Created using browser-solidity: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://ethereum.github.io/browser-solidity/#version=' + queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&gist='
$.ajax({ $.ajax({
...@@ -225,7 +290,7 @@ var run = function () { ...@@ -225,7 +290,7 @@ var run = function () {
data: JSON.stringify({ data: JSON.stringify({
description: description, description: description,
public: true, public: true,
files: files files: packaged
}) })
}).done(function (response) { }).done(function (response) {
if (response.html_url && confirm('Created a gist at ' + response.html_url + ' Would you like to open it in a new window?')) { if (response.html_url && confirm('Created a gist at ' + response.html_url + ' Would you like to open it in a new window?')) {
...@@ -236,6 +301,8 @@ var run = function () { ...@@ -236,6 +301,8 @@ var run = function () {
}) })
} }
}) })
}
})
$('#copyOver').click(function () { $('#copyOver').click(function () {
var target = prompt( var target = prompt(
...@@ -245,12 +312,17 @@ var run = function () { ...@@ -245,12 +312,17 @@ var run = function () {
if (target === null) { if (target === null) {
return return
} }
var files = packageFiles() packageFiles((error, packaged) => {
if (error) {
console.log(error)
} else {
$('<iframe/>', { $('<iframe/>', {
src: target, src: target,
style: 'display:none;', style: 'display:none;',
load: function () { this.contentWindow.postMessage(['loadFiles', files], '*') } load: function () { this.contentWindow.postMessage(['loadFiles', packaged], '*') }
}).appendTo('body') }).appendTo('body')
}
})
}) })
// ----------------- editor ---------------------- // ----------------- editor ----------------------
...@@ -267,97 +339,77 @@ var run = function () { ...@@ -267,97 +339,77 @@ var run = function () {
return false return false
}) })
// Edit name of current tab
$filesEl.on('click', '.file.active', function (ev) {
var $fileTabEl = $(this)
var originalName = $fileTabEl.find('.name').text()
ev.preventDefault()
if ($(this).find('input').length > 0) return false
var $fileNameInputEl = $('<input value="' + originalName + '"/>')
$fileTabEl.html($fileNameInputEl)
$fileNameInputEl.focus()
$fileNameInputEl.select()
$fileNameInputEl.on('blur', handleRename)
$fileNameInputEl.keyup(handleRename)
function handleRename (ev) {
ev.preventDefault()
if (ev.which && ev.which !== 13) return false
var newName = ev.target.value
$fileNameInputEl.off('blur')
$fileNameInputEl.off('keyup')
if (newName !== originalName && confirm(
files.exists(newName)
? 'Are you sure you want to overwrite: ' + newName + ' with ' + originalName + '?'
: 'Are you sure you want to rename: ' + originalName + ' to ' + newName + '?')) {
if (!files.rename(originalName, newName)) {
alert('Error while renaming file')
} else {
config.set('currentFile', '')
switchToFile(newName)
editor.discard(originalName)
}
}
return false
}
return false
})
// Remove current tab // Remove current tab
$filesEl.on('click', '.file .remove', function (ev) { $filesEl.on('click', '.file .remove', function (ev) {
ev.preventDefault() ev.preventDefault()
var name = $(this).parent().find('.name').text() var name = $(this).parent().find('.name').text()
delete tabbedFiles[name]
if (confirm('Are you sure you want to remove: ' + name + ' from local storage?')) { refreshTabs()
if (!files.remove(name)) { if (Object.keys(tabbedFiles).length) {
alert('Error while removing file') switchToFile(Object.keys(tabbedFiles)[0])
} } else {
editor.displayEmptyReadOnlySession()
} }
return false return false
}) })
editor.event.register('sessionSwitched', refreshTabs)
function switchToFile (file) { function switchToFile (file) {
editorSyncFile() editorSyncFile()
config.set('currentFile', file) config.set('currentFile', file)
refreshTabs(file)
if (files.isReadOnly(file)) { fileProviderOf(file).get(file, (error, content) => {
editor.openReadOnly(file, files.get(file)) if (error) {
console.log(error)
} else { } else {
editor.open(file, files.get(file)) if (fileProviderOf(file).isReadOnly(file)) {
editor.openReadOnly(file, content)
} else {
editor.open(file, content)
}
self.event.trigger('currentFileChanged', [file, fileProviderOf(file)])
} }
self.event.trigger('currentFileChanged', [file]) })
} }
function switchToNextFile () { function switchToNextFile () {
var fileList = Object.keys(files.list()) var fileList = Object.keys(filesProviders['browser'].list())
if (fileList.length) { if (fileList.length) {
switchToFile(fileList[0]) switchToFile(fileList[0])
} }
} }
var previouslyOpenedFile = config.get('currentFile') var previouslyOpenedFile = config.get('currentFile')
if (previouslyOpenedFile && files.get(previouslyOpenedFile)) { if (previouslyOpenedFile) {
filesProviders['browser'].get(previouslyOpenedFile, (error, content) => {
if (!error && content) {
switchToFile(previouslyOpenedFile) switchToFile(previouslyOpenedFile)
} else { } else {
switchToNextFile() switchToNextFile()
} }
})
} else {
switchToNextFile()
}
// Synchronise tab list with file names known to the editor function fileProviderOf (file) {
function refreshTabs () { var provider = file.match(/[^/]*/)
var $filesEl = $('#files') if (provider !== null) {
var fileNames = Object.keys(files.list()) return filesProviders[provider[0]]
}
return null
}
// Display files that have already been selected
function refreshTabs (newfile) {
if (newfile) {
tabbedFiles[newfile] = newfile
}
var $filesEl = $('#files')
$filesEl.find('.file').remove() $filesEl.find('.file').remove()
for (var f in fileNames) { for (var file in tabbedFiles) {
var name = fileNames[f] $filesEl.append($('<li class="file"><span class="name">' + file + '</span><span class="remove"><i class="fa fa-close"></i></span></li>'))
$filesEl.append($('<li class="file"><span class="name">' + name + '</span><span class="remove"><i class="fa fa-close"></i></span></li>'))
} }
var currentFileOpen = !!config.get('currentFile') var currentFileOpen = !!config.get('currentFile')
...@@ -447,7 +499,7 @@ var run = function () { ...@@ -447,7 +499,7 @@ var run = function () {
} }
}, },
errorClick: (errFile, errLine, errCol) => { errorClick: (errFile, errLine, errCol) => {
if (errFile !== config.get('currentFile') && files.exists(errFile)) { if (errFile !== config.get('currentFile') && (filesProviders['browser'].exists(errFile) || filesProviders['localhost'].exists(errFile))) {
switchToFile(errFile) switchToFile(errFile)
} }
editor.gotoLine(errLine, errCol) editor.gotoLine(errLine, errCol)
...@@ -524,7 +576,7 @@ var run = function () { ...@@ -524,7 +576,7 @@ var run = function () {
return cb('No metadata') return cb('No metadata')
} }
Object.keys(metadata.sources).forEach(function (fileName) { async.eachSeries(Object.keys(metadata.sources), function (fileName, cb) {
// find hash // find hash
var hash var hash
try { try {
...@@ -533,16 +585,23 @@ var run = function () { ...@@ -533,16 +585,23 @@ var run = function () {
return cb('Metadata inconsistency') return cb('Metadata inconsistency')
} }
fileProviderOf(fileName).get(fileName, (error, content) => {
if (error) {
console.log(error)
} else {
sources.push({ sources.push({
content: files.get(fileName), content: content,
hash: hash hash: hash
}) })
}
cb()
}) })
}, function () {
// publish the list of sources in order, fail if any failed // publish the list of sources in order, fail if any failed
async.eachSeries(sources, function (item, cb) { async.eachSeries(sources, function (item, cb) {
swarmVerifiedPublish(item.content, item.hash, cb) swarmVerifiedPublish(item.content, item.hash, cb)
}, cb) }, cb)
})
} }
udapp.event.register('publishContract', this, function (contract) { udapp.event.register('publishContract', this, function (contract) {
...@@ -635,9 +694,9 @@ var run = function () { ...@@ -635,9 +694,9 @@ var run = function () {
} }
function handleImportCall (url, cb) { function handleImportCall (url, cb) {
if (files.exists(url)) { var provider = fileProviderOf(url)
cb(null, files.get(url)) if (provider && provider.exists(url)) {
return return provider.get(url, cb)
} }
var handlers = [ var handlers = [
...@@ -664,7 +723,7 @@ var run = function () { ...@@ -664,7 +723,7 @@ var run = function () {
} }
// FIXME: at some point we should invalidate the cache // FIXME: at some point we should invalidate the cache
files.addReadOnly(url, content) filesProviders['browser'].addReadOnly(url, content)
cb(null, content) cb(null, content)
}) })
} }
...@@ -754,16 +813,32 @@ var run = function () { ...@@ -754,16 +813,32 @@ var run = function () {
if (currentFile) { if (currentFile) {
var target = currentFile var target = currentFile
var sources = {} var sources = {}
sources[target] = files.get(target) var provider = fileProviderOf(currentFile)
if (provider) {
provider.get(target, (error, content) => {
if (error) {
console.log(error)
} else {
sources[target] = content
compiler.compile(sources, target) compiler.compile(sources, target)
} }
})
} else {
console.log('cannot compile ' + currentFile + '. Does not belong to any explorer')
}
}
} }
function editorSyncFile () { function editorSyncFile () {
var currentFile = config.get('currentFile') var currentFile = config.get('currentFile')
if (currentFile && editor.current()) { if (currentFile && editor.current()) {
var input = editor.get(currentFile) var input = editor.get(currentFile)
files.set(currentFile, input) var provider = fileProviderOf(currentFile)
if (provider) {
provider.set(currentFile, input)
} else {
console.log('cannot save ' + currentFile + '. Does not belong to any explorer')
}
} }
} }
......
...@@ -59,6 +59,12 @@ function Editor (editorElement) { ...@@ -59,6 +59,12 @@ function Editor (editorElement) {
e.stop() e.stop()
}) })
this.displayEmptyReadOnlySession = function () {
currentSession = null
editor.setSession(emptySession)
editor.setReadOnly(true)
}
this.setBreakpoint = function (row, css) { this.setBreakpoint = function (row, css) {
editor.session.setBreakpoint(row, css) editor.session.setBreakpoint(row, css)
} }
...@@ -67,6 +73,12 @@ function Editor (editorElement) { ...@@ -67,6 +73,12 @@ function Editor (editorElement) {
editor.setFontSize(editor.getFontSize() + incr) editor.setFontSize(editor.getFontSize() + incr)
} }
this.setText = function (text) {
if (currentSession && sessions[currentSession]) {
sessions[currentSession].setValue(text)
}
}
function createSession (content) { function createSession (content) {
var s = new ace.EditSession(content, 'ace/mode/javascript') var s = new ace.EditSession(content, 'ace/mode/javascript')
s.setUndoManager(new ace.UndoManager()) s.setUndoManager(new ace.UndoManager())
...@@ -87,6 +99,8 @@ function Editor (editorElement) { ...@@ -87,6 +99,8 @@ function Editor (editorElement) {
var session = createSession(content) var session = createSession(content)
sessions[path] = session sessions[path] = session
readOnlySessions[path] = false readOnlySessions[path] = false
} else if (sessions[path].getValue() !== content) {
sessions[path].setValue(content)
} }
switchSession(path) switchSession(path)
} }
......
...@@ -33,6 +33,17 @@ var css = csjs` ...@@ -33,6 +33,17 @@ var css = csjs`
module.exports = fileExplorer module.exports = fileExplorer
function fileExplorer (appAPI, files) { function fileExplorer (appAPI, files) {
this.files = files
this.files.event.register('fileExternallyChanged', (path, content) => {
if (appAPI.currentFile() === path && appAPI.currentContent() !== content) {
if (confirm('This file has been changed outside of the remix.\nDo you want to replace the content displayed in remix by the new one?')) {
appAPI.setText(content)
}
}
})
var self = this
var fileEvents = files.event var fileEvents = files.event
var treeView = new Treeview({ var treeView = new Treeview({
extractData: function (value, tree, key) { extractData: function (value, tree, key) {
...@@ -55,8 +66,11 @@ function fileExplorer (appAPI, files) { ...@@ -55,8 +66,11 @@ function fileExplorer (appAPI, files) {
} }
}, },
formatSelf: function (key, data) { formatSelf: function (key, data) {
return yo`<label class=${data.children ? css.folder : css.file} var isRoot = data.path.indexOf('/') === -1
return yo`<label class="${data.children ? css.folder : css.file}"
data-path="${data.path}" data-path="${data.path}"
style="${isRoot ? 'font-weight:bold;' : ''}"
isfolder=${data.children !== undefined}
onload=${function (el) { adaptEnvironment(el, focus, hover) }} onload=${function (el) { adaptEnvironment(el, focus, hover) }}
onunload=${function (el) { unadaptEnvironment(el, focus, hover) }} onunload=${function (el) { unadaptEnvironment(el, focus, hover) }}
onclick=${editModeOn} onclick=${editModeOn}
...@@ -66,30 +80,35 @@ function fileExplorer (appAPI, files) { ...@@ -66,30 +80,35 @@ function fileExplorer (appAPI, files) {
} }
}) })
this.treeView = treeView
var deleteButton = yo` var deleteButton = yo`
<span class=${css.remove} onclick=${deletePath}> <span class=${css.remove} onclick=${deletePath}>
<i class="fa fa-trash" aria-hidden="true"></i> <i class="fa fa-trash" aria-hidden="true"></i>
</span> </span>
` `
appAPI.event.register('currentFileChanged', (newFile) => { appAPI.event.register('currentFileChanged', (newFile, explorer) => {
if (explorer === files) {
fileFocus(newFile) fileFocus(newFile)
} else {
unfocus(focusElement)
}
}) })
fileEvents.register('fileRemoved', fileRemoved) fileEvents.register('fileRemoved', fileRemoved)
fileEvents.register('fileRenamed', fileRenamed) fileEvents.register('fileRenamed', fileRenamed)
fileEvents.register('fileRenamedError', fileRenamedError)
fileEvents.register('fileAdded', fileAdded) fileEvents.register('fileAdded', fileAdded)
var filepath = null var filepath = null
var focusElement = null var focusElement = null
var textUnderEdit = null var textUnderEdit = null
var element = treeView.render(files.listAsTree())
element.className = css.fileexplorer
var events = new EventManager() var events = new EventManager()
this.events = events
var api = {} var api = {}
api.addFile = function addFile (file) { api.addFile = function addFile (file) {
var name = file.name var name = files.type + '/' + file.name
if (!files.exists(name) || confirm('The file ' + name + ' already exists! Would you like to overwrite it?')) { if (!files.exists(name) || confirm('The file ' + name + ' already exists! Would you like to overwrite it?')) {
var fileReader = new FileReader() var fileReader = new FileReader()
fileReader.onload = function (event) { fileReader.onload = function (event) {
...@@ -100,12 +119,13 @@ function fileExplorer (appAPI, files) { ...@@ -100,12 +119,13 @@ function fileExplorer (appAPI, files) {
fileReader.readAsText(file) fileReader.readAsText(file)
} }
} }
this.api = api
function focus (event) { function focus (event) {
event.cancelBubble = true event.cancelBubble = true
var li = this var li = this
if (focusElement === li) return if (focusElement === li) return
if (focusElement) focusElement.classList.toggle(css.hasFocus) unfocus(focusElement)
focusElement = li focusElement = li
focusElement.classList.toggle(css.hasFocus) focusElement.classList.toggle(css.hasFocus)
var label = getLabelFrom(li) var label = getLabelFrom(li)
...@@ -114,6 +134,11 @@ function fileExplorer (appAPI, files) { ...@@ -114,6 +134,11 @@ function fileExplorer (appAPI, files) {
if (isFile) events.trigger('focus', [filepath]) if (isFile) events.trigger('focus', [filepath])
} }
function unfocus (el) {
if (focusElement) focusElement.classList.toggle(css.hasFocus)
focusElement = null
}
function hover (event) { function hover (event) {
if (event.type === 'mouseout') { if (event.type === 'mouseout') {
var exitedTo = event.toElement || event.relatedTarget var exitedTo = event.toElement || event.relatedTarget
...@@ -128,7 +153,7 @@ function fileExplorer (appAPI, files) { ...@@ -128,7 +153,7 @@ function fileExplorer (appAPI, files) {
} }
function getElement (path) { function getElement (path) {
var label = element.querySelector(`label[data-path="${path}"]`) var label = self.element.querySelector(`label[data-path="${path}"]`)
if (label) return getLiFrom(label) if (label) return getLiFrom(label)
} }
...@@ -160,12 +185,26 @@ function fileExplorer (appAPI, files) { ...@@ -160,12 +185,26 @@ function fileExplorer (appAPI, files) {
function editModeOff (event) { function editModeOff (event) {
var label = this 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 (event.which === 13) event.preventDefault()
if ((event.type === 'blur' || event.which === 27 || event.which === 13) && label.getAttribute('contenteditable')) {
var isFolder = label.getAttribute('isfolder') === 'true'
var save = textUnderEdit !== label.innerText
if (save && event.which !== 13) save = confirm('Do you want to rename?') if (save && event.which !== 13) save = confirm('Do you want to rename?')
if (save) renameSubtree(label) if (save) {
else label.innerText = textUnderEdit var newPath = label.dataset.path
newPath = newPath.split('/')
newPath[newPath.length - 1] = label.innerText
newPath = newPath.join('/')
if (label.innerText.match(/(\/|:|\*|\?|"|<|>|\\|\||')/) !== null) {
alert('special characters are not allowsed')
label.innerText = textUnderEdit
} else if (!files.exists(newPath)) {
files.rename(label.dataset.path, newPath, isFolder)
} else {
alert('File already exists.')
label.innerText = textUnderEdit
}
} else label.innerText = textUnderEdit
label.removeAttribute('contenteditable') label.removeAttribute('contenteditable')
label.classList.remove(css.rename) label.classList.remove(css.rename)
} }
...@@ -205,8 +244,6 @@ function fileExplorer (appAPI, files) { ...@@ -205,8 +244,6 @@ function fileExplorer (appAPI, files) {
var path = label.dataset.path var path = label.dataset.path
var newName = path.replace(oldPath, newPath) var newName = path.replace(oldPath, newPath)
label.dataset.path = newName label.dataset.path = newName
var isFile = label.className.indexOf('file') === 0
if (isFile) files.rename(path, newName)
var ul = li.lastChild var ul = li.lastChild
if (ul.tagName === 'UL') { if (ul.tagName === 'UL') {
updateAllLabels([...ul.children], oldPath, newPath) updateAllLabels([...ul.children], oldPath, newPath)
...@@ -227,7 +264,7 @@ function fileExplorer (appAPI, files) { ...@@ -227,7 +264,7 @@ function fileExplorer (appAPI, files) {
if (li) li.parentElement.removeChild(li) if (li) li.parentElement.removeChild(li)
} }
function fileRenamed (oldName, newName) { function fileRenamed (oldName, newName, isFolder) {
var li = getElement(oldName) var li = getElement(oldName)
if (li) { if (li) {
oldName = oldName.split('/') oldName = oldName.split('/')
...@@ -244,16 +281,16 @@ function fileExplorer (appAPI, files) { ...@@ -244,16 +281,16 @@ function fileExplorer (appAPI, files) {
} }
} }
function fileRenamedError (error) {
alert(error)
}
function fileAdded (filepath) { function fileAdded (filepath) {
var el = treeView.render(files.listAsTree()) var el = treeView.render(files.listAsTree())
el.className = css.fileexplorer el.className = css.fileexplorer
element.parentElement.replaceChild(el, element) self.element.parentElement.replaceChild(el, self.element)
element = el self.element = el
} }
element.events = events
element.api = api
return element
} }
/****************************************************************************** /******************************************************************************
HELPER FUNCTIONS HELPER FUNCTIONS
...@@ -312,3 +349,16 @@ function expandPathTo (li) { ...@@ -312,3 +349,16 @@ function expandPathTo (li) {
if (caret.classList.contains('fa-caret-right')) caret.click() // expand if (caret.classList.contains('fa-caret-right')) caret.click() // expand
} }
} }
fileExplorer.prototype.init = function () {
var files = this.files.listAsTree()
if (!Object.keys(files).length) {
files[this.files.type] = {} // default
}
var element = this.treeView.render(files)
element.className = css.fileexplorer
element.events = this.events
element.api = this.api
this.element = element
return element
}
...@@ -4,6 +4,7 @@ var yo = require('yo-yo') ...@@ -4,6 +4,7 @@ var yo = require('yo-yo')
var EventManager = require('ethereum-remix').lib.EventManager var EventManager = require('ethereum-remix').lib.EventManager
var FileExplorer = require('./file-explorer') var FileExplorer = require('./file-explorer')
var modalDialog = require('./modaldialog')
module.exports = filepanel module.exports = filepanel
...@@ -28,6 +29,9 @@ var css = csjs` ...@@ -28,6 +29,9 @@ var css = csjs`
.newFile { .newFile {
padding : 10px; padding : 10px;
} }
.connectToLocalhost {
padding : 10px;
}
.uploadFile { .uploadFile {
padding : 10px; padding : 10px;
} }
...@@ -46,11 +50,10 @@ var css = csjs` ...@@ -46,11 +50,10 @@ var css = csjs`
} }
.isHidden { .isHidden {
position : absolute; position : absolute;
height : 99% height : 99%;
left : -101%; left : -101%;
} }
.treeview { .treeview {
height : 100%;
background-color : white; background-color : white;
} }
.dragbar { .dragbar {
...@@ -83,25 +86,39 @@ var limit = 60 ...@@ -83,25 +86,39 @@ var limit = 60
var canUpload = window.File || window.FileReader || window.FileList || window.Blob var canUpload = window.File || window.FileReader || window.FileList || window.Blob
var ghostbar = yo`<div class=${css.ghostbar}></div>` var ghostbar = yo`<div class=${css.ghostbar}></div>`
function filepanel (appAPI, files) { function filepanel (appAPI, filesProvider) {
var fileExplorer = new FileExplorer(appAPI, files) var fileExplorer = new FileExplorer(appAPI, filesProvider['browser'])
var fileSystemExplorer = new FileExplorer(appAPI, filesProvider['localhost'])
var dragbar = yo`<div onmousedown=${mousedown} class=${css.dragbar}></div>` var dragbar = yo`<div onmousedown=${mousedown} class=${css.dragbar}></div>`
function remixdDialog () {
return yo`<div><div>This feature allows to interact with your file system from Remix. Once the connection is made the shared folder will be available in the file explorer under <i>localhost</i></div>
<div><i>Remixd</i> first has to be run in your local computer. See <a href="http://remix.readthedocs.io/en/latest/tutorial_mist.html">http://remix.readthedocs.io/en/latest/remixd.html</a> for more details.</div>
<div>Accepting this dialog will start a session between <i>${window.location.href}</i> and your local file system <i>ws://127.0.0.1:65520</i></div>
<div>Please be sure your system is secured enough (port 65520 neither opened nor forwarded).</div>
<div><i class="fa fa-link"></i> will update the connection status.</div>
<div>This feature is still alpha, we recommend to keep a copy of the shared folder.</div>
</div>`
}
function template () { function template () {
return yo` return yo`
<div class=${css.container}> <div class=${css.container}>
<div class=${css.fileexplorer}> <div class=${css.fileexplorer}>
<div class=${css.menu}> <div class=${css.menu}>
<span onclick=${createNewFile} class="newFile ${css.newFile}" title="New File"> <span onclick=${createNewFile} class="newFile ${css.newFile}" title="Create New File in the Browser Storage Explorer">
<i class="fa fa-plus-circle"></i> <i class="fa fa-plus-circle"></i>
</span> </span>
${canUpload ? yo` ${canUpload ? yo`
<span class=${css.uploadFile} title="Open local file"> <span class=${css.uploadFile} title="Add Local file to the Browser Storage Explorer">
<label class="fa fa-folder-open"> <label class="fa fa-folder-open">
<input type="file" onchange=${uploadFile} multiple /> <input type="file" onchange=${uploadFile} multiple />
</label> </label>
</span> </span>
` : ''} ` : ''}
<span onclick=${connectToLocalhost} class="${css.connectToLocalhost}">
<i class="websocketconn fa fa-link" title="Connect to Localhost"></i>
</span>
<span class=${css.changeeditorfontsize} > <span class=${css.changeeditorfontsize} >
<i class="increditorsize fa fa-plus" aria-hidden="true" title="increase editor font size"></i> <i class="increditorsize fa fa-plus" aria-hidden="true" title="increase editor font size"></i>
<i class="decreditorsize fa fa-minus" aria-hidden="true" title="decrease editor font size"></i> <i class="decreditorsize fa fa-minus" aria-hidden="true" title="decrease editor font size"></i>
...@@ -110,7 +127,8 @@ function filepanel (appAPI, files) { ...@@ -110,7 +127,8 @@ function filepanel (appAPI, files) {
<i class="fa fa-angle-double-left"></i> <i class="fa fa-angle-double-left"></i>
</span> </span>
</div> </div>
<div class=${css.treeview}>${fileExplorer}</div> <div class=${css.treeview}>${fileExplorer.init()}</div>
<div class="filesystemexplorer ${css.treeview}"></div>
</div> </div>
${dragbar} ${dragbar}
</div> </div>
...@@ -121,6 +139,33 @@ function filepanel (appAPI, files) { ...@@ -121,6 +139,33 @@ function filepanel (appAPI, files) {
var element = template() var element = template()
element.querySelector('.increditorsize').addEventListener('click', () => { appAPI.editorFontSize(1) }) element.querySelector('.increditorsize').addEventListener('click', () => { appAPI.editorFontSize(1) })
element.querySelector('.decreditorsize').addEventListener('click', () => { appAPI.editorFontSize(-1) }) element.querySelector('.decreditorsize').addEventListener('click', () => { appAPI.editorFontSize(-1) })
var containerFileSystem = element.querySelector('.filesystemexplorer')
var websocketconn = element.querySelector('.websocketconn')
filesProvider['localhost'].remixd.event.register('connecting', (event) => {
websocketconn.style.color = 'orange'
websocketconn.setAttribute('title', 'Connecting to localhost. ' + JSON.stringify(event))
})
filesProvider['localhost'].remixd.event.register('connected', (event) => {
websocketconn.style.color = 'green'
websocketconn.setAttribute('title', 'Connected to localhost. ' + JSON.stringify(event))
})
filesProvider['localhost'].remixd.event.register('errored', (event) => {
websocketconn.style.color = 'red'
websocketconn.setAttribute('title', 'localhost connection errored. ' + JSON.stringify(event))
if (fileSystemExplorer.element && containerFileSystem.children.length > 0) {
containerFileSystem.removeChild(fileSystemExplorer.element)
}
})
filesProvider['localhost'].remixd.event.register('closed', (event) => {
websocketconn.style.color = '#111111'
websocketconn.setAttribute('title', 'localhost connection closed. ' + JSON.stringify(event))
if (fileSystemExplorer.element && containerFileSystem.children.length > 0) {
containerFileSystem.removeChild(fileSystemExplorer.element)
}
})
// TODO please do not add custom javascript objects, which have no // TODO please do not add custom javascript objects, which have no
// relation to the DOM to DOM nodes // relation to the DOM to DOM nodes
element.events = events element.events = events
...@@ -128,6 +173,10 @@ function filepanel (appAPI, files) { ...@@ -128,6 +173,10 @@ function filepanel (appAPI, files) {
appAPI.switchToFile(path) appAPI.switchToFile(path)
}) })
fileSystemExplorer.events.register('focus', function (path) {
appAPI.switchToFile(path)
})
return element return element
function toggle (event) { function toggle (event) {
...@@ -182,11 +231,39 @@ function filepanel (appAPI, files) { ...@@ -182,11 +231,39 @@ function filepanel (appAPI, files) {
} }
function createNewFile () { function createNewFile () {
var newName = appAPI.createName('Untitled') var newName = filesProvider['browser'].type + '/' + appAPI.createName('Untitled.sol')
if (!files.set(newName, '')) { if (!filesProvider['browser'].set(newName, '')) {
alert('Failed to create file ' + newName) alert('Failed to create file ' + newName)
} else { } else {
appAPI.switchToFile(newName) appAPI.switchToFile(newName)
} }
} }
/**
* connect to localhost if no connection and render the explorer
* disconnect from localhost if connected and remove the explorer
*
* @param {String} txHash - hash of the transaction
*/
function connectToLocalhost () {
var container = document.querySelector('.filesystemexplorer')
if (filesProvider['localhost'].files !== null) {
filesProvider['localhost'].close((error) => {
if (error) console.log(error)
})
} else {
modalDialog('Connection to Localhost', remixdDialog(), () => {
filesProvider['localhost'].init((error) => {
if (error) {
console.log(error)
} else {
if (fileSystemExplorer.element && container.children.length > 0) {
container.removeChild(fileSystemExplorer.element)
}
container.appendChild(fileSystemExplorer.init())
}
})
})
}
}
} }
...@@ -6,40 +6,52 @@ function Files (storage) { ...@@ -6,40 +6,52 @@ function Files (storage) {
var event = new EventManager() var event = new EventManager()
this.event = event this.event = event
var readonly = {} var readonly = {}
this.type = 'browser'
this.exists = function (path) { this.exists = function (path) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file // NOTE: ignore the config file
if (path === '.remix.config') { if (path === '.remix.config') {
return false return false
} }
return this.isReadOnly(path) || storage.exists(path) return this.isReadOnly(unprefixedpath) || storage.exists(unprefixedpath)
} }
this.get = function (path) { this.init = function (cb) {
cb()
}
this.get = function (path, cb) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file // NOTE: ignore the config file
if (path === '.remix.config') { if (path === '.remix.config') {
return null return null
} }
return readonly[path] || storage.get(path) var content = readonly[unprefixedpath] || storage.get(unprefixedpath)
if (cb) {
cb(null, content)
}
return content
} }
this.set = function (path, content) { this.set = function (path, content) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file // NOTE: ignore the config file
if (path === '.remix.config') { if (path === '.remix.config') {
return false return false
} }
if (!this.isReadOnly(path)) { if (!this.isReadOnly(unprefixedpath)) {
var exists = storage.exists(path) var exists = storage.exists(unprefixedpath)
if (!storage.set(path, content)) { if (!storage.set(unprefixedpath, content)) {
return false return false
} }
if (!exists) { if (!exists) {
event.trigger('fileAdded', [path, false]) event.trigger('fileAdded', [this.type + '/' + unprefixedpath, false])
} else { } else {
event.trigger('fileChanged', [path]) event.trigger('fileChanged', [this.type + '/' + unprefixedpath])
} }
return true return true
} }
...@@ -48,9 +60,10 @@ function Files (storage) { ...@@ -48,9 +60,10 @@ function Files (storage) {
} }
this.addReadOnly = function (path, content) { this.addReadOnly = function (path, content) {
if (!storage.exists(path)) { var unprefixedpath = this.removePrefix(path)
readonly[path] = content if (!storage.exists(unprefixedpath)) {
event.trigger('fileAdded', [path, true]) readonly[unprefixedpath] = content
event.trigger('fileAdded', [this.type + '/' + unprefixedpath, true])
return true return true
} }
...@@ -58,31 +71,35 @@ function Files (storage) { ...@@ -58,31 +71,35 @@ function Files (storage) {
} }
this.isReadOnly = function (path) { this.isReadOnly = function (path) {
path = this.removePrefix(path)
return readonly[path] !== undefined return readonly[path] !== undefined
} }
this.remove = function (path) { this.remove = function (path) {
if (!this.exists(path)) { var unprefixedpath = this.removePrefix(path)
if (!this.exists(unprefixedpath)) {
return false return false
} }
if (this.isReadOnly(path)) { if (this.isReadOnly(unprefixedpath)) {
readonly[path] = undefined readonly[unprefixedpath] = undefined
} else { } else {
if (!storage.remove(path)) { if (!storage.remove(unprefixedpath)) {
return false return false
} }
} }
event.trigger('fileRemoved', [path]) event.trigger('fileRemoved', [this.type + '/' + unprefixedpath])
return true return true
} }
this.rename = function (oldPath, newPath) { this.rename = function (oldPath, newPath, isFolder) {
if (!this.isReadOnly(oldPath) && storage.exists(oldPath)) { var unprefixedoldPath = this.removePrefix(oldPath)
if (!storage.rename(oldPath, newPath)) { var unprefixednewPath = this.removePrefix(newPath)
if (!this.isReadOnly(unprefixedoldPath) && storage.exists(unprefixedoldPath)) {
if (!storage.rename(unprefixedoldPath, unprefixednewPath)) {
return false return false
} }
event.trigger('fileRenamed', [oldPath, newPath]) event.trigger('fileRenamed', [this.type + '/' + unprefixedoldPath, this.type + '/' + unprefixednewPath, isFolder])
return true return true
} }
return false return false
...@@ -92,21 +109,25 @@ function Files (storage) { ...@@ -92,21 +109,25 @@ function Files (storage) {
var files = {} var files = {}
// add r/w files to the list // add r/w files to the list
storage.keys().forEach(function (path) { storage.keys().forEach((path) => {
// NOTE: as a temporary measure do not show the config file // NOTE: as a temporary measure do not show the config file
if (path !== '.remix.config') { if (path !== '.remix.config') {
files[path] = false files[this.type + '/' + path] = false
} }
}) })
// add r/o files to the list // add r/o files to the list
Object.keys(readonly).forEach(function (path) { Object.keys(readonly).forEach((path) => {
files[path] = true files[this.type + '/' + path] = true
}) })
return files return files
} }
this.removePrefix = function (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
}
// //
// Tree model for files // Tree model for files
// { // {
...@@ -146,7 +167,6 @@ function Files (storage) { ...@@ -146,7 +167,6 @@ function Files (storage) {
'/content': self.get(path) '/content': self.get(path)
}) })
}) })
return tree return tree
} }
......
...@@ -41,7 +41,7 @@ function Storage (prefix) { ...@@ -41,7 +41,7 @@ function Storage (prefix) {
return safeKeys() return safeKeys()
// filter any names not including the prefix // filter any names not including the prefix
.filter(function (item) { return item.indexOf(prefix, 0) === 0 }) .filter(function (item) { return item.indexOf(prefix, 0) === 0 })
// remove prefix from filename // remove prefix from filename and add the 'browser' path
.map(function (item) { return item.substr(prefix.length) }) .map(function (item) { return item.substr(prefix.length) })
} }
......
'use strict'
var async = require('async')
var EventManager = require('ethereum-remix').lib.EventManager
class SystemFiles {
constructor (remixd) {
this.event = new EventManager()
this.remixd = remixd
this.files = null
this.filesContent = {}
this.filesTree = null
this.type = 'localhost'
this.error = {
'EEXIST': 'File already exists'
}
this.remixd.event.register('notified', (data) => {
if (data.scope === 'systemfiles') {
if (data.name === 'created') {
this.init(() => {
this.event.trigger('fileAdded', [this.type + '/' + data.value.path, data.value.isReadOnly, data.value.isFolder])
})
} else if (data.name === 'removed') {
this.init(() => {
this.event.trigger('fileRemoved', [this.type + '/' + data.value.path])
})
} else if (data.name === 'changed') {
this.remixd.call('systemfiles', 'get', {path: data.value}, (error, content) => {
if (error) {
console.log(error)
} else {
var path = this.type + '/' + data.value
this.filesContent[path] = content
this.event.trigger('fileExternallyChanged', [path, content])
}
})
}
}
})
}
close (cb) {
this.remixd.close()
this.files = null
this.filesTree = null
cb()
}
init (cb) {
this.remixd.call('systemfiles', 'list', {}, (error, filesList) => {
if (error) {
cb(error)
} else {
this.files = {}
for (var k in filesList) {
this.files[this.type + '/' + k] = filesList[k]
}
listAsTree(this, this.files, (error, tree) => {
this.filesTree = tree
cb(error)
})
}
})
}
exists (path) {
if (!this.files) return false
return this.files[path] !== undefined
}
get (path, cb) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('systemfiles', 'get', {path: unprefixedpath}, (error, content) => {
if (!error) {
this.filesContent[path] = content
cb(error, content)
} else {
// display the last known content.
// TODO should perhaps better warn the user that the file is not synced.
cb(null, this.filesContent[path])
}
})
}
set (path, content, cb) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('systemfiles', 'set', {path: unprefixedpath, content: content}, (error, result) => {
if (cb) cb(error, result)
var path = this.type + '/' + unprefixedpath
this.filesContent[path]
this.event.trigger('fileChanged', [path])
})
return true
}
addReadOnly (path, content) {
return false
}
isReadOnly (path) {
if (this.files) return this.files[path]
return true
}
remove (path) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('systemfiles', 'remove', {path: unprefixedpath}, (error, result) => {
if (error) console.log(error)
var path = this.type + '/' + unprefixedpath
delete this.filesContent[path]
this.init(() => {
this.event.trigger('fileRemoved', [path])
})
})
}
rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath)
this.remixd.call('systemfiles', 'rename', {oldPath: unprefixedoldPath, newPath: unprefixednewPath}, (error, result) => {
if (error) {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
this.event.trigger('fileRenamedError', [this.error[error.code]])
} else {
var newPath = this.type + '/' + unprefixednewPath
var oldPath = this.type + '/' + unprefixedoldPath
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.trigger('fileRenamed', [oldPath, newPath, isFolder])
})
}
})
return true
}
list () {
return this.files
}
listAsTree () {
return this.filesTree
}
removePrefix (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
}
}
//
// Tree model for files
// {
// 'a': { }, // empty directory 'a'
// 'b': {
// 'c': {}, // empty directory 'b/c'
// 'd': { '/readonly': true, '/content': 'Hello World' } // files 'b/c/d'
// 'e': { '/readonly': false, '/path': 'b/c/d' } // symlink to 'b/c/d'
// 'f': { '/readonly': false, '/content': '<executable>', '/mode': 0755 }
// }
// }
//
function listAsTree (self, filesList, callback) {
function hashmapize (obj, path, val) {
var nodes = path.split('/')
var i = 0
for (; i < nodes.length - 1; i++) {
var node = nodes[i]
if (obj[node] === undefined) {
obj[node] = {}
}
obj = obj[node]
}
obj[nodes[i]] = val
}
var tree = {}
// This does not include '.remix.config', because it is filtered
// inside list().
async.eachSeries(Object.keys(filesList), function (path, cb) {
self.get(path, (error, content) => {
if (error) {
console.log(error)
cb(error)
} else {
self.filesContent[path] = content
hashmapize(tree, path, {
'/readonly': filesList[path],
'/content': content
})
cb()
}
})
}, (error) => {
callback(error, tree)
})
}
module.exports = SystemFiles
module.exports = (title, content, okFn, cancelFn) => {
var modal = document.querySelector('.modal-body')
var modaltitle = document.querySelector('.modal-header h2')
modaltitle.innerHTML = ' - '
if (title) modaltitle.innerHTML = title
modal.innerHTML = ''
if (content) modal.appendChild(content)
var container = document.querySelector('.modal')
container.style.display = container.style.display === 'block' ? hide() : show()
function ok () {
hide()
if (okFn) okFn()
removeEventListener()
}
function cancel () {
hide()
if (cancelFn) cancelFn()
removeEventListener()
}
function blur (event) {
if (event.target === container) {
cancel()
}
}
window.onclick = (event) => {
console.log('clicj windo')
blur(event)
}
function hide () {
container.style.display = 'none'
}
function show () {
container.style.display = 'block'
}
function removeEventListener () {
document.getElementById('modal-footer-ok').removeEventListener('click', ok)
document.getElementById('modal-footer-cancel').removeEventListener('click', cancel)
}
document.getElementById('modal-footer-ok').addEventListener('click', ok)
document.getElementById('modal-footer-cancel').addEventListener('click', cancel)
}
'use strict'
var EventManager = require('ethereum-remix').lib.EventManager
class Remixd {
constructor () {
this.event = new EventManager()
this.callbacks = {}
this.callid = 0
this.socket = null
this.connected = false
}
online () {
return this.socket !== null
}
close () {
if (this.socket) {
this.socket.close()
this.socket = null
}
}
start (cb) {
if (this.socket) {
try {
this.socket.close()
} catch (e) {}
}
this.event.trigger('connecting', [])
this.socket = new WebSocket('ws://localhost:65520', 'echo-protocol') // eslint-disable-line
this.socket.addEventListener('open', (event) => {
this.connected = true
this.event.trigger('connected', [event])
cb()
})
this.socket.addEventListener('message', (event) => {
var data = JSON.parse(event.data)
if (data.type === 'reply') {
if (this.callbacks[data.id]) {
this.callbacks[data.id](data.error, data.result)
delete this.callbacks[data.id]
}
this.event.trigger('replied', [data])
} else if (data.type === 'notification') {
this.event.trigger('notified', [data])
}
})
this.socket.addEventListener('error', (event) => {
this.errored(event)
cb(event)
})
this.socket.addEventListener('close', (event) => {
if (event.wasClean) {
this.connected = false
this.event.trigger('closed', [event])
} else {
this.errored(event)
}
this.socket = null
})
}
errored (event) {
if (this.connected) {
alert('connection to Remixd lost!.') // eslint-disable-line
}
this.connected = false
this.socket = null
this.event.trigger('errored', [event])
}
call (service, fn, args, callback) {
this.ensureSocket((error) => {
if (error) return callback(error)
if (this.socket && this.socket.readyState === this.socket.OPEN) {
var data = this.format(service, fn, args)
this.callbacks[data.id] = callback
this.socket.send(JSON.stringify(data))
} else {
callback('Socket not ready. state:' + this.socket.readyState)
}
})
}
ensureSocket (cb) {
if (this.socket) return cb(null, this.socket)
this.start((error) => {
if (error) {
cb(error)
} else {
cb(null, this.socket)
}
})
}
format (service, fn, args) {
var data = {
id: this.callid,
service: service,
fn: fn,
args: args
}
this.callid++
return data
}
}
module.exports = Remixd
...@@ -280,13 +280,13 @@ UniversalDApp.prototype.getContractByName = function (contractName) { ...@@ -280,13 +280,13 @@ UniversalDApp.prototype.getContractByName = function (contractName) {
} }
UniversalDApp.prototype.getCreateInterface = function ($container, contract) { UniversalDApp.prototype.getCreateInterface = function ($container, contract) {
function remove () {
self.$el.remove()
}
var self = this var self = this
var createInterface = yo`<div class="create"></div>` var createInterface = yo`<div class="create"></div>`
if (self.options.removable) { if (self.options.removable) {
var close = yo`<div class="udapp-close" onclick=${remove}></div>` var close = yo`<div class="udapp-close" onclick=${remove}></div>`
function remove () {
self.$el.remove()
}
createInterface.appendChild(close) createInterface.appendChild(close)
} }
...@@ -348,10 +348,10 @@ UniversalDApp.prototype.getInstanceInterface = function (contract, address, $tar ...@@ -348,10 +348,10 @@ UniversalDApp.prototype.getInstanceInterface = function (contract, address, $tar
var appendFunctions = function (address, $el) { var appendFunctions = function (address, $el) {
if ($el) $el = $el.get(0) if ($el) $el = $el.get(0)
function remove () { $instance.remove() }
var $instance = $(`<div class="instance ${cssInstance.instance}"/>`) var $instance = $(`<div class="instance ${cssInstance.instance}"/>`)
if (self.options.removable_instances) { if (self.options.removable_instances) {
var close = yo`<div class="udapp-close" onclick=${remove}></div>` var close = yo`<div class="udapp-close" onclick=${remove}></div>`
function remove () { $instance.remove() }
$instance.get(0).appendChild(close) $instance.get(0).appendChild(close)
} }
var context = self.executionContext.isVM() ? 'memory' : 'blockchain' var context = self.executionContext.isVM() ? 'memory' : 'blockchain'
......
...@@ -6,7 +6,7 @@ var sauce = require('./sauce') ...@@ -6,7 +6,7 @@ var sauce = require('./sauce')
var sources = { var sources = {
'sources': { 'sources': {
'Untitled.sol': examples.ballot.content 'browser/Untitled.sol': examples.ballot.content
} }
} }
...@@ -27,7 +27,7 @@ function runTests (browser, testData) { ...@@ -27,7 +27,7 @@ function runTests (browser, testData) {
browser browser
.waitForElementVisible('.newFile', 10000) .waitForElementVisible('.newFile', 10000)
.click('.envView') .click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:Ballot'], function () { contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:Ballot'], function () {
browser.end() browser.end()
}) })
} }
...@@ -5,7 +5,7 @@ var sauce = require('./sauce') ...@@ -5,7 +5,7 @@ var sauce = require('./sauce')
var sources = { var sources = {
'sources': { 'sources': {
'Untitled.sol': `pragma solidity ^0.4.0; 'browser/Untitled.sol': `pragma solidity ^0.4.0;
contract TestContract { function f() returns (uint) { return 8; } }` contract TestContract { function f() returns (uint) { return 8; } }`
} }
} }
...@@ -27,7 +27,7 @@ function runTests (browser) { ...@@ -27,7 +27,7 @@ function runTests (browser) {
browser browser
.waitForElementVisible('.newFile', 10000) .waitForElementVisible('.newFile', 10000)
.click('.envView') .click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:TestContract'], function () { contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:TestContract'], function () {
browser.click('.create .constructor .call') browser.click('.create .constructor .call')
.waitForElementPresent('.instance .call[title="f"]') .waitForElementPresent('.instance .call[title="f"]')
.click('.instance .call[title="f"]') .click('.instance .call[title="f"]')
......
...@@ -6,17 +6,17 @@ var sauce = require('./sauce') ...@@ -6,17 +6,17 @@ var sauce = require('./sauce')
var sources = { var sources = {
'sources': { 'sources': {
'ballot.sol': examples.ballot.content, 'browser/ballot.sol': examples.ballot.content,
'test/client/credit.sol': '', 'browser/test/client/credit.sol': '',
'src/voting.sol': '', 'browser/src/voting.sol': '',
'src/leasing.sol': '', 'browser/src/leasing.sol': '',
'src/gmbh/contract.sol': false, 'browser/src/gmbh/contract.sol': false,
'src/gmbh/test.sol': false, 'browser/src/gmbh/test.sol': false,
'src/gmbh/company.sol': false, 'browser/src/gmbh/company.sol': false,
'src/gmbh/node_modules/ballot.sol': false, 'browser/src/gmbh/node_modules/ballot.sol': false,
'src/ug/finance.sol': false, 'browser/src/ug/finance.sol': false,
'app/solidity/mode.sol': true, 'browser/app/solidity/mode.sol': true,
'app/ethereum/constitution.sol': true 'browser/app/ethereum/constitution.sol': true
} }
} }
......
...@@ -5,7 +5,7 @@ var sauce = require('./sauce') ...@@ -5,7 +5,7 @@ var sauce = require('./sauce')
var sources = { var sources = {
'sources': { 'sources': {
'Untitled.sol': 'contract test1 {} contract test2 {}' 'browser/Untitled.sol': 'contract test1 {} contract test2 {}'
} }
} }
...@@ -26,7 +26,7 @@ function runTests (browser) { ...@@ -26,7 +26,7 @@ function runTests (browser) {
browser browser
.waitForElementVisible('.newFile', 10000) .waitForElementVisible('.newFile', 10000)
.click('.envView') .click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:test1', 'Untitled.sol:test2'], function () { contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:test1', 'browser/Untitled.sol:test2'], function () {
browser.end() browser.end()
}) })
} }
...@@ -6,7 +6,7 @@ var dom = require('../helpers/dom') ...@@ -6,7 +6,7 @@ var dom = require('../helpers/dom')
var sources = { var sources = {
'sources': { 'sources': {
'Untitled.sol': ` 'browser/Untitled.sol': `
contract test1 { address test = tx.origin; } contract test1 { address test = tx.origin; }
contract test2 {} contract test2 {}
contract TooMuchGas { contract TooMuchGas {
...@@ -33,13 +33,13 @@ function runTests (browser) { ...@@ -33,13 +33,13 @@ function runTests (browser) {
browser browser
.waitForElementVisible('.newFile', 10000) .waitForElementVisible('.newFile', 10000)
.click('.envView') .click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:TooMuchGas', 'Untitled.sol:test1', 'Untitled.sol:test2'], function () { contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:TooMuchGas', 'browser/Untitled.sol:test1', 'browser/Untitled.sol:test2'], function () {
browser browser
.click('.staticanalysisView') .click('.staticanalysisView')
.click('#staticanalysisView button') .click('#staticanalysisView button')
.waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () { .waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () {
dom.listSelectorContains(['Untitled.sol:2:33: Use of tx.origin', dom.listSelectorContains(['browser/Untitled.sol:2:33: Use of tx.origin',
'Fallback function of contract Untitled.sol:TooMuchGas requires too much gas'], 'Fallback function of contract browser/Untitled.sol:TooMuchGas requires too much gas'],
'#staticanalysisresult .warning span', '#staticanalysisresult .warning span',
browser, function () { browser, function () {
browser.end() browser.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