nikki
Version:
A browser-based IDE written in NodeJS. For Real.
274 lines (237 loc) • 6.56 kB
JavaScript
var _ = require('lodash');
var p = require('path');
var keyboard = require('./keyboard');
var socket = require('./socket');
var state = require('./state');
/**
* Hovering on the filesystem will
* automatically switch the focus
* there.
*/
$('#fs').mouseenter(function(){
state.switchFocus('fs');
$(this).focus();
});
/**
* Builds the title of the page.
*
* @param path
*/
var buildTitle = function(path) {
var title = $('<h2>')
title.attr('id', 'navigation');
var segments = [];
_.each(path.split('/').splice(1), function(directory){
var segment = $('<a>');
segment.addClass('segment');
segment.text('/' + directory);
segments.push(directory);
segment.attr('href', '/' + segments.join('/'));
title.append(segment);
});
$('#subject').html('').append(title);
$('#navigation .segment').click(function(){
socket.emit('resource.open', {path: $(this).attr('href')});
return false;
});
};
/**
* Rules that assign some CSS classes to resources
* based on their (guessed) type.
*/
var iconRules = {
read: function(resource) {
if (_.contains(['.md', '.markdown'], p.extname(resource.name))) {
return "glyphicon fa fa-book fa-1x";
}
},
lock: function(resource) {
if (resource.type === 'file' && p.extname(resource.name) === '.lock') {
return "glyphicon fa fa-lock fa-1x";
}
},
html: function(resource) {
if (resource.type === 'file' && p.extname(resource.name) === '.html') {
return "glyphicon fa fa-code fa-1x";
}
},
sql: function(resource) {
if (resource.type === 'file' && p.extname(resource.name) === '.sql') {
return "glyphicon fa fa-database fa-1x";
}
},
log: function(resource) {
if (resource.type === 'file' && p.extname(resource.name) === '.log') {
return "glyphicon fa fa-exclamation-triangle fa-1x";
}
},
coffeescript: function(resource) {
if (resource.type === 'file' && p.extname(resource.name) === '.coffee') {
return "glyphicon fa fa-coffee fa-1x";
}
},
nikki: function(resource) {
if (resource.name === '.nikki.yml') {
return "glyphicon fa fa-heart fa-1x";
}
},
git: function(resource) {
if (resource.name.match(/^\.git/)) {
return "glyphicon fa fa-git fa-1x";
}
},
executable: function(resource) {
if (resource.type === 'file' && !p.extname(resource.name)) {
return "glyphicon fa fa-terminal fa-1x";
}
},
dir: function(resource) {
if (resource.type === 'directory') {
return "glyphicon fa fa-folder fa-1x";
}
},
file: function(resource) {
if (resource.type === 'file') {
return "glyphicon fa fa-file-text-o fa-1x";
}
}
};
/**
* Adds an icon based on the resource.
*/
var iconize = function(icon, resource) {
var match = false;
_.forEach(iconRules, function(rule){
if (!match && rule(resource)) {
icon.addClass(rule(resource));
match = true;
}
});
};
/**
* Adds a resource to the filesystem.
*
* @param resource
* @param index
*/
var addResource = function(resource) {
var res = $('<li>')
res.addClass(resource.type);
var icon = $('<span>')
iconize(icon, resource);
res.addClass('resource');
if (!$('.resource.active').length) {
res.addClass('active');
}
res.attr('id', resource.path);
res.text(resource.name);
if (state.focus === 'bar') {
var path = $('<p>');
path.addClass('path')
path.text(resource.parent);
res.append(path);
if (resource.misc) {
resource.misc.forEach(function(misc){
var m = $('<p>');
m.addClass('misc')
m.text("> " + misc + "...");
res.append(m);
});
}
}
res.hover(function(){
$('.resource.active').removeClass('active');
res.addClass('active');
});
res.attr('data-resource', JSON.stringify(resource))
res.click(function(){
socket.emit('resource.open', resource)
});
res.prepend(icon);
$('#fs ul').append(res);
}
/**
* Opens a directory on the filesystem.
*
* @param fs
*/
var openDir = function(fs) {
buildTitle(fs.root.path)
var filesystem = $('<ul>')
$('#fs').html('').append(filesystem);
$('#fs').attr('filesystem', JSON.stringify(fs));
/**
* Super-ghetto trick explained!
*
* If we are coming from a popstate we prevent
* logging the openDir in the history,
* else we will never be able to really go back
* as everytime we go back we'd trigger a fs.open
* event and adding an entry in the history...
*
* Basically we'd go back in history and add a new
* entry in it, so you really don't go back...
*/
if (!fs.root.from || fs.root.from !== 'window.popstate') {
history.pushState(fs, null, fs.root.path);
}
_.each(fs.resources, function(resource, index){
addResource(resource)
})
$('[id="' + fs.root.path + '"]').addClass('active');
};
/**
* Returns the structure of the current
* filesystem as it was received by the
* server.
*
* @returns {*}
*/
var getStructure = function() {
return JSON.parse($('#fs').attr('filesystem'))
}
/**
* Resets the current filesystem.
*/
var reset = function() {
openDir(getStructure());
}
/**
* Super-ghetto trick!
*
* Adding the 'from' state to the FS
* object.
*
* Look for 'window.popstate' to check
* why we need this.
*/
window.onpopstate = function(event) {
if (event.state && event.state.root) {
root = event.state.root;
root.from = 'window.popstate';
socket.emit('resource.open', root);
}
}
var moveUp = function() {
try {
var path = JSON.parse($('#fs').attr('filesystem')).root.path;
var parts = path.split('/');
parts.pop();
path = parts.join('/');
socket.emit('resource.open', {path: path});
} catch (err) {}
};
/**
* Let's open a new filesystem once the
* server sends the related event.
*/
socket.on('fs.root', function (fs) {
openDir(fs);
});
module.exports = {
buildTitle: buildTitle,
addResource: addResource,
reset: reset,
moveUp: moveUp,
getStructure: getStructure
}