Files
Codiad/components/active/init.js
2012-12-22 08:59:54 +01:00

908 lines
33 KiB
JavaScript
Executable File

/*
* Copyright (c) Codiad & Kent Safranski (codiad.com), distributed
* as-is and without warranty under the MIT License. See
* [root]/license.txt for more. This information must remain intact.
*/
(function(global, $) {
var EditSession = require('ace/edit_session')
.EditSession;
var UndoManager = require('ace/undomanager')
.UndoManager;
var codiad = global.codiad;
$(function() {
codiad.active.init();
});
//////////////////////////////////////////////////////////////////
//
// Active Files Component for Codiad
// ---------------------------------
// Track and manage EditSession instaces of files being edited.
//
//////////////////////////////////////////////////////////////////
codiad.active = {
controller: 'components/active/controller.php',
// Path to EditSession instance mapping
sessions: {},
// History of opened files
history: [],
//////////////////////////////////////////////////////////////////
//
// Check if a file is open.
//
// Parameters:
// path - {String}
//
//////////////////////////////////////////////////////////////////
isOpen: function(path) {
return !!this.sessions[path];
},
open: function(path, content, mtime, inBackground) {
var _this = this;
if (this.isOpen(path)) {
this.focus(path);
return;
}
var ext = codiad.filemanager.getExtension(path);
var mode = codiad.editor.selectMode(ext);
var fn = function() {
//var Mode = require('ace/mode/' + mode)
// .Mode;
// TODO: Ask for user confirmation before recovering
// And maybe show a diff
var draft = _this.checkDraft(path);
if (draft) {
content = draft;
codiad.message.success('Recovered unsaved content for : ' + path);
}
//var session = new EditSession(content, new Mode());
var session = new EditSession(content);
session.setMode("ace/mode/" + mode);
session.setUndoManager(new UndoManager());
session.path = path;
session.serverMTime = mtime;
_this.sessions[path] = session;
session.untainted = content.slice(0);
if (!inBackground) {
codiad.editor.setSession(session);
}
_this.add(path, session);
};
// Assuming the mode file has no dependencies
$.loadScript('components/editor/ace-editor/mode-' + mode + '.js',
fn);
},
init: function() {
var _this = this;
_this.initTabDropdownMenu();
_this.updateTabDropdownVisibility();
// Focus from list.
$('#list-active-files a')
.live('click', function(e) {
e.stopPropagation();
_this.focus($(this).parent('li').attr('data-path'));
});
// Focus on left button click from dropdown.
$('#dropdown-list-active-files a')
.live('click', function(e) {
if(e.which == 1) {
/* Do not stop propagation of the event,
* it will be catch by the dropdown menu
* and close it. */
_this.focus($(this).parent('li').attr('data-path'));
}
});
// Focus on left button mousedown from tab.
$('#tab-list-active-files li.tab-item>a.label')
.live('mousedown', function(e) {
if(e.which == 1) {
e.stopPropagation();
_this.focus($(this).parent('li').attr('data-path'));
}
});
// Remove from list.
$('#list-active-files a>span')
.live('click', function(e) {
e.stopPropagation();
_this.remove($(this)
.parent('a')
.parent('li')
.attr('data-path'));
});
// Remove from dropdown.
$('#dropdown-list-active-files a>span')
.live('click', function(e) {
e.stopPropagation();
/* Get the active editor before removing anything. Remove the
* tab, then put back the focus on the previously active
* editor if it was not removed. */
var activePath = _this.getPath();
var pathToRemove = $(this).parents('li').attr('data-path');
_this.remove(pathToRemove);
if (activePath !== null && activePath !== pathToRemove) {
_this.focus(activePath);
}
_this.updateTabDropdownVisibility();
});
// Remove from tab.
$('#tab-list-active-files a.close')
.live('click', function(e) {
e.stopPropagation();
/* Get the active editor before removing anything. Remove the
* tab, then put back the focus on the previously active
* editor if it was not removed. */
var activePath = _this.getPath();
var pathToRemove = $(this).parent('li').attr('data-path');
_this.remove(pathToRemove);
if (activePath !== null && activePath !== pathToRemove) {
_this.focus(activePath);
}
_this.updateTabDropdownVisibility();
});
// Remove from middle button click on dropdown.
$('#dropdown-list-active-files li')
.live('mouseup', function(e) {
if (e.which == 2) {
e.stopPropagation();
/* Get the active editor before removing anything. Remove the
* tab, then put back the focus on the previously active
* editor if it was not removed. */
var activePath = _this.getPath();
var pathToRemove = $(this).attr('data-path');
_this.remove(pathToRemove);
if (activePath !== null && activePath !== pathToRemove) {
_this.focus(activePath);
}
_this.updateTabDropdownVisibility();
}
});
// Remove from middle button click on tab.
$('.tab-item')
.live('mouseup', function(e) {
if (e.which == 2) {
e.stopPropagation();
/* Get the active editor before removing anything. Remove the
* tab, then put back the focus on the previously active
* editor if it was not removed. */
var activePath = _this.getPath();
var pathToRemove = $(this).attr('data-path');
_this.remove(pathToRemove);
if (activePath !== null && activePath !== pathToRemove) {
_this.focus(activePath);
}
_this.updateTabDropdownVisibility();
}
});
// Make list sortable
$('#list-active-files')
.sortable({
placeholder: 'active-sort-placeholder',
tolerance: 'intersect',
start: function(e, ui) {
ui.placeholder.height(ui.item.height());
}
});
// Make dropdown sortable.
$('#dropdown-list-active-files')
.sortable({
axis: 'y',
tolerance: 'pointer',
start: function(e, ui) {
ui.placeholder.height(ui.item.height());
}
});
// Make tabs sortable.
$('#tab-list-active-files')
.sortable({
items: '> li',
axis: 'x',
tolerance: 'pointer',
containment: 'parent',
start: function(e, ui) {
ui.placeholder.css('background', 'transparent');
ui.helper.css('width', '200px');
},
stop: function(e, ui) {
// Reset css
ui.item.css('z-index', '')
ui.item.css('position', '')
}
});
/* Woaw, so tricky! At initialization, the tab-list is empty, so
* it is not marked as float so it is not detected as an horizontal
* list by the sortable plugin. Workaround is to mark it as
* floating at initialization time. See bug report
* http://bugs.jqueryui.com/ticket/6702. */
$('#tab-list-active-files').data('sortable').floating = true;
// Open saved-state active files on load
$.get(_this.controller + '?action=list', function(data) {
var listResponse = codiad.jsend.parse(data);
if (listResponse !== null) {
$.each(listResponse, function(index, value) {
codiad.filemanager.openFile(value);
});
// Run resize command to fix render issues
codiad.editor.resize();
}
});
// Run resize on window resize
$(window).resize(function() {
codiad.editor.resize();
_this.updateTabDropdownVisibility();
});
// Prompt if a user tries to close window without saving all filess
window.onbeforeunload = function(e) {
if ($('#list-active-files li.changed')
.length > 0) {
var e = e || window.event;
var errMsg = 'You have unsaved files.';
// For IE and Firefox prior to version 4
if (e) {
e.returnValue = errMsg;
}
// For rest
return errMsg;
}
};
},
//////////////////////////////////////////////////////////////////
// Drafts
//////////////////////////////////////////////////////////////////
checkDraft: function(path) {
var draft = localStorage.getItem(path);
if (draft !== null) {
return draft;
} else {
return false;
}
},
removeDraft: function(path) {
localStorage.removeItem(path);
},
//////////////////////////////////////////////////////////////////
// Get active editor path
//////////////////////////////////////////////////////////////////
getPath: function() {
try {
return codiad.editor.getActive()
.getSession()
.path;
} catch (e) {
return null;
}
},
//////////////////////////////////////////////////////////////////
// Check if opened by another user
//////////////////////////////////////////////////////////////////
check: function(path) {
$.get(this.controller + '?action=check&path=' + path,
function(data) {
var checkResponse = codiad.jsend.parse(data);
});
},
//////////////////////////////////////////////////////////////////
// Add newly opened file to list
//////////////////////////////////////////////////////////////////
add: function(path, session) {
var listThumb = this.createListThumb(path);
session.listThumb = listThumb;
$('#list-active-files').append(listThumb);
/* If the tab list would overflow with the new tab. Move the
* first tab to dropdown, then add a new tab. */
if (this.isTabListOverflowed(true)) {
var tab = $('#tab-list-active-files li:first-child');
this.moveTabToDropdownMenu(tab);
}
var tabThumb = this.createTabThumb(path);
$('#tab-list-active-files').append(tabThumb);
session.tabThumb = tabThumb;
this.updateTabDropdownVisibility();
$.get(this.controller + '?action=add&path=' + path);
this.focus(path);
// Mark draft as changed
if (this.checkDraft(path)) {
this.markChanged(path);
}
},
//////////////////////////////////////////////////////////////////
// Focus on opened file
//////////////////////////////////////////////////////////////////
focus: function(path, moveToTabList) {
if (typeof moveToTabList == 'undefined') {
moveToTabList = true;
}
this.highlightEntry(path, moveToTabList);
if(path != this.getPath()) {
codiad.editor.setSession(this.sessions[path]);
this.check(path);
this.activePath = path;
this.history.push(path);
}
},
highlightEntry: function(path, moveToTabList) {
if (typeof moveToTabList == 'undefined') {
moveToTabList = true;
}
$('#list-active-files li')
.removeClass('active');
$('#tab-list-active-files li')
.removeClass('active');
$('#dropdown-list-active-files li')
.removeClass('active');
var session = this.sessions[path];
if($('#dropdown-list-active-files').has(session.tabThumb).length > 0) {
if(moveToTabList) {
/* Get the menu item as a tab, and put the last tab in
* dropdown. */
var menuItem = session.tabThumb;
this.moveDropdownMenuItemToTab(menuItem, true);
var tab = $('#tab-list-active-files li:last-child');
this.moveTabToDropdownMenu(tab);
} else {
/* Show the dropdown menu if needed */
this.showTabDropdownMenu();
}
}
else if(this.history.length > 0) {
var prevPath = this.history[this.history.length-1];
var prevSession = this.sessions[prevPath];
if($('#dropdown-list-active-files').has(prevSession.tabThumb).length > 0) {
/* Hide the dropdown menu if needed */
this.hideTabDropdownMenu();
}
}
session.tabThumb.addClass('active');
session.listThumb.addClass('active');
},
//////////////////////////////////////////////////////////////////
// Mark changed
//////////////////////////////////////////////////////////////////
markChanged: function(path) {
this.sessions[path].listThumb.addClass('changed');
this.sessions[path].tabThumb.addClass('changed');
},
//////////////////////////////////////////////////////////////////
// Save active editor
//////////////////////////////////////////////////////////////////
save: function(path) {
var _this = this;
if ((path && !this.isOpen(path)) || (!path && !codiad.editor.getActive())) {
codiad.message.error('No Open Files to save');
return;
}
var session;
if (path) session = this.sessions[path];
else session = codiad.editor.getActive()
.getSession();
var content = session.getValue();
var path = session.path;
var handleSuccess = function(mtime){
var session = codiad.active.sessions[path];
session.untainted = newContent;
session.serverMTime = mtime;
if (session.listThumb) session.listThumb.removeClass('changed');
if (session.tabThumb) session.tabThumb.removeClass('changed');
_this.removeDraft(path);
}
// Replicate the current content so as to avoid
// discrepancies due to content changes during
// computation of diff
var newContent = content.slice(0);
if (session.serverMTime && session.untainted){
codiad.workerManager.addTask({
taskType: 'diff',
id: path,
original: session.untainted,
changed: newContent
}, function(success, patch){
if (success) {
codiad.filemanager.savePatch(path, patch, session.serverMTime, {
success: handleSuccess
});
} else {
condiad.filemanager.saveFile(path, newContent, {
success: handleSuccess
});
}
}, this);
} else {
codiad.filemanager.saveFile(path, newContent, {
success: handleSuccess
});
}
},
//////////////////////////////////////////////////////////////////
// Remove file
//////////////////////////////////////////////////////////////////
remove: function(path) {
if (!this.isOpen(path)) return;
var session = this.sessions[path];
var closeFile = true;
if (session.listThumb.hasClass('changed')) {
codiad.modal.load(450, 'components/active/dialog.php?action=confirm&path=' + path);
closeFile = false;
}
if (closeFile) {
this.close(path);
}
},
close: function(path) {
var _this = this;
var session = this.sessions[path];
/* Animate only if the tabThumb if a tab, not a dropdown item. */
if(session.tabThumb.hasClass('tab-item')) {
session.tabThumb.css({'z-index': 1});
session.tabThumb.animate({
top: $('#editor-top-bar').height() + 'px'
}, 300, function() {
session.tabThumb.remove();
_this.updateTabDropdownVisibility();
});
} else {
session.tabThumb.remove();
_this.updateTabDropdownVisibility();
}
session.listThumb.remove();
/* Remove closed path from history */
var history = [];
$.each(this.history, function(index) {
if(this != path) history.push(this);
})
this.history = history
/* Select all the tab tumbs except the one which is to be removed. */
var tabThumbs = $('#tab-list-active-files li[data-path!="' + path + '"]');
if (tabThumbs.length == 0) {
codiad.editor.exterminate();
} else {
var nextPath = '';
if(this.history.length > 0) {
nextPath = this.history[this.history.length - 1];
} else {
nextPath = $(tabThumbs[0]).attr('data-path');
}
var nextSession = this.sessions[nextPath];
codiad.editor.removeSession(session, nextSession);
nextSession.listThumb.addClass('active');
nextSession.tabThumb.addClass('active');
}
delete this.sessions[path];
$.get(this.controller + '?action=remove&path=' + path);
this.removeDraft(path);
},
//////////////////////////////////////////////////////////////////
// Process rename
//////////////////////////////////////////////////////////////////
rename: function(oldPath, newPath) {
var switchSessions = function(oldPath, newPath) {
var tabThumb = this.sessions[oldPath].tabThumb;
tabThumb.attr('data-path', newPath);
tabThumb.find('.label')
.text(newPath.substring(1));
this.sessions[newPath] = this.sessions[oldPath];
this.sessions[newPath].path = newPath;
this.sessions[oldPath] = undefined;
};
if (this.sessions[oldPath]) {
// A file was renamed
switchSessions.apply(this, [oldPath, newPath]);
// pass new sessions instance to setactive
for (var k = 0; k < codiad.editor.instances.length; k++) {
if (codiad.editor.instances[k].getSession().path === newPath) {
codiad.editor.setActive(codiad.editor.instances[k]);
}
}
var newSession = this.sessions[newPath];
// Change Editor Mode
var ext = codiad.filemanager.getExtension(newPath);
var mode = codiad.editor.selectMode(ext);
// handle async mode change
var fn = function() {
codiad.editor.setModeDisplay(newSession);
newSession.removeListener('changeMode', fn);
}
newSession.on("changeMode", fn);
newSession.setMode("ace/mode/" + mode);
} else {
// A folder was renamed
var newKey;
for (var key in this.sessions) {
newKey = key.replace(oldPath, newPath);
if (newKey !== key) {
switchSessions.apply(this, [key, newKey]);
}
}
}
$.get(this.controller + '?action=rename&old_path=' + oldPath + '&new_path=' + newPath);
},
//////////////////////////////////////////////////////////////////
// Open in Browser
//////////////////////////////////////////////////////////////////
openInBrowser: function() {
var path = this.getPath();
if (path) {
codiad.filemanager.openInBrowser(path);
} else {
codiad.message.error('No Open Files');
}
},
//////////////////////////////////////////////////////////////////
// Get Selected Text
//////////////////////////////////////////////////////////////////
getSelectedText: function() {
var path = this.getPath();
var session = this.sessions[path];
if (path && this.isOpen(path)) {
return session.getTextRange(
codiad.editor.getActive()
.getSelectionRange());
} else {
codiad.message.error('No Open Files or Selected Text');
}
},
//////////////////////////////////////////////////////////////////
// Insert Text
//////////////////////////////////////////////////////////////////
insertText: function(val) {
codiad.editor.getActive()
.insert(val);
},
//////////////////////////////////////////////////////////////////
// Goto Line
//////////////////////////////////////////////////////////////////
gotoLine: function(line) {
codiad.editor.getActive()
.gotoLine(line, 0, true);
},
//////////////////////////////////////////////////////////////////
// Move Up (Key Combo)
//////////////////////////////////////////////////////////////////
move: function(dir) {
var num = $('#tab-list-active-files li').length;
if (num === 0) return;
var newActive = null;
var active = null;
if (dir == 'up') {
// If active is in the tab list
active = $('#tab-list-active-files li.active');
if(active.length > 0) {
// Previous or rotate to the end
newActive = active.prev('li');
if (newActive.length === 0) {
newActive = $('#dropdown-list-active-files li:last-child')
if (newActive.length === 0) {
newActive = $('#tab-list-active-files li:last-child')
}
}
}
// If active is in the dropdown list
active = $('#dropdown-list-active-files li.active');
if(active.length > 0) {
// Previous
newActive = active.prev('li');
if (newActive.length === 0) {
newActive = $('#tab-list-active-files li:last-child')
}
}
} else {
// If active is in the tab list
active = $('#tab-list-active-files li.active');
if(active.length > 0) {
// Next or rotate to the beginning
newActive = active.next('li');
if (newActive.length === 0) {
newActive = $('#dropdown-list-active-files li:first-child');
if (newActive.length === 0) {
newActive = $('#tab-list-active-files li:first-child')
}
}
}
// If active is in the dropdown list
active = $('#dropdown-list-active-files li.active');
if(active.length > 0) {
// Next or rotate to the beginning
newActive = active.next('li');
if (newActive.length === 0) {
newActive = $('#tab-list-active-files li:first-child')
}
}
}
if(newActive) this.focus(newActive.attr('data-path'), false);
},
//////////////////////////////////////////////////////////////////
// Dropdown Menu
//////////////////////////////////////////////////////////////////
initTabDropdownMenu: function() {
var _this = this;
var menu = $('#dropdown-list-active-files');
var button = $('#tab-dropdown-button');
menu.appendTo($('body'));
button.click(function(e) {
e.stopPropagation();
_this.toggleTabDropdownMenu();
});
},
showTabDropdownMenu: function() {
var menu = $('#dropdown-list-active-files');
if(!menu.is(':visible')) this.toggleTabDropdownMenu();
},
hideTabDropdownMenu: function() {
var menu = $('#dropdown-list-active-files');
if(menu.is(':visible')) this.toggleTabDropdownMenu();
},
toggleTabDropdownMenu: function() {
var _this = this;
var menu = $('#dropdown-list-active-files');
menu.css({
top: $("#editor-top-bar").height() + 'px',
right: '20px',
width: '200px'
});
menu.slideToggle('fast');
if(menu.is(':visible')) {
// handle click-out autoclosing
var fn = function() {
menu.hide();
$(window).off('click', fn)
}
$(window).on('click', fn);
}
},
moveTabToDropdownMenu: function(tab, prepend) {
if (typeof prepend == 'undefined') {
prepend = false;
}
tab.remove();
path = tab.attr('data-path');
var tabThumb = this.createMenuItemThumb(path);
if(prepend) $('#dropdown-list-active-files').prepend(tabThumb);
else $('#dropdown-list-active-files').append(tabThumb);
if(tab.hasClass("changed")) {
tabThumb.addClass("changed");
}
if(tab.hasClass("active")) {
tabThumb.addClass("active");
}
this.sessions[path].tabThumb = tabThumb;
},
moveDropdownMenuItemToTab: function(menuItem, prepend) {
if (typeof prepend == 'undefined') {
prepend = false;
}
menuItem.remove();
path = menuItem.attr('data-path');
var tabThumb = this.createTabThumb(path);
if(prepend) $('#tab-list-active-files').prepend(tabThumb);
else $('#tab-list-active-files').append(tabThumb);
if(menuItem.hasClass("changed")) {
tabThumb.addClass("changed");
}
if(menuItem.hasClass("active")) {
tabThumb.addClass("active");
}
this.sessions[path].tabThumb = tabThumb;
},
isTabListOverflowed: function(includeFictiveTab) {
if (typeof includeFictiveTab == 'undefined') {
includeFictiveTab = false;
}
var tabs = $('#tab-list-active-files li');
var count = tabs.length
if (includeFictiveTab) count += 1;
if (count <= 1) return false;
var width = 0;
tabs.each(function(index) {
width += $(this).outerWidth(true);
})
if (includeFictiveTab) {
width += $(tabs[tabs.length-1]).outerWidth(true);
}
/* If we subtract the width of the left side bar, of the right side
* bar handle and of the tab dropdown handle to the window width,
* do we have enough room for the tab list? Its kind of complicated
* to handle all the offsets, so afterwards we add a fixed offset
* just t be sure. */
var lsbarWidth = $(".sidebar-handle").width();
if (codiad.sidebars.isLeftSidebarOpen) {
lsbarWidth = $("#sb-left").width();
}
var rsbarWidth = $(".sidebar-handle").width();
if (codiad.sidebars.isRightSidebarOpen) {
rsbarWidth = $("#sb-left").width();
}
var tabListWidth = $("#tab-list-active-files").width();
var dropdownWidth = $('#tab-dropdown').width();
var room = window.innerWidth - lsbarWidth - rsbarWidth - dropdownWidth - width - 30;
return (room < 0);
},
updateTabDropdownVisibility: function() {
while(this.isTabListOverflowed()) {
var tab = $('#tab-list-active-files li:last-child');
if (tab.length == 1) this.moveTabToDropdownMenu(tab, true);
else break;
}
while(!this.isTabListOverflowed(true)) {
var menuItem = $('#dropdown-list-active-files li:first-child');
if (menuItem.length == 1) this.moveDropdownMenuItemToTab(menuItem);
else break;
}
if ($('#dropdown-list-active-files li').length > 0) {
$('#tab-dropdown').show();
} else {
$('#tab-dropdown').hide();
// Be sure to hide the menu if it is opened.
$('#dropdown-list-active-files').hide();
}
},
//////////////////////////////////////////////////////////////////
// Factory
//////////////////////////////////////////////////////////////////
splitDirectoryAndFileName: function(path) {
var index = path.lastIndexOf('/');
return {
fileName: path.substring(index + 1),
directory: path.substring(0, index + 1)
}
},
createListThumb: function(path) {
return $('<li data-path="' + path + '"><a title="'+path+'"><span></span><div>' + path.substring(1) + '</div></a></li>');
},
createTabThumb: function(path) {
split = this.splitDirectoryAndFileName(path);
return $('<li class="tab-item" data-path="' + path + '"><a class="label" title="' + path + '">'
+ split.directory.substring(1) + '<span class="file-name">' + split.fileName + '</span>'
+ '</a><a class="close">x</a></li>');
},
createMenuItemThumb: function(path) {
split = this.splitDirectoryAndFileName(path);
return $('<li data-path="' + path + '"><a title="' + path + '"><span class="label"></span><div class="label">'
+ split.directory.substring(1) + '<span class="file-name">' + split.fileName + '</span>'
+ '</div></a></li>');
},
};
})(this, jQuery);