kopia lustrzana https://github.com/c9/core
1986 wiersze
75 KiB
JavaScript
1986 wiersze
75 KiB
JavaScript
define(function(require, module, exports) {
|
|
main.consumes = [
|
|
"Plugin", "menus", "settings", "layout", "ui", "commands", "fs",
|
|
"Tab", "editors", "Pane", "watcher", "c9", "dialog.alert",
|
|
"focusManager", "util"
|
|
];
|
|
main.provides = ["tabManager"];
|
|
return main;
|
|
|
|
function main(options, imports, register) {
|
|
var Plugin = imports.Plugin;
|
|
var Tab = imports.Tab;
|
|
var Pane = imports.Pane;
|
|
var ui = imports.ui;
|
|
var fs = imports.fs;
|
|
var c9 = imports.c9;
|
|
var settings = imports.settings;
|
|
var util = imports.util;
|
|
var menus = imports.menus;
|
|
var editors = imports.editors;
|
|
var commands = imports.commands;
|
|
var layout = imports.layout;
|
|
var watcher = imports.watcher;
|
|
var focusManager = imports.focusManager;
|
|
var alert = imports["dialog.alert"].show;
|
|
|
|
var basename = require("path").basename;
|
|
|
|
/***** Initialization *****/
|
|
|
|
var plugin = new Plugin("Ajax.org", main.consumes);
|
|
var emit = plugin.getEmitter();
|
|
emit.setMaxListeners(100);
|
|
|
|
var loadFilesAtInit = options.loadFilesAtInit;
|
|
var ideProviderName = options.ideProviderName || "Cloud9";
|
|
|
|
var PREFIX = "/////";
|
|
var XPREVIEW = /\.(gz|tar|tgz|zip|rar|jar|exe|pyc|pdf)$/;
|
|
|
|
var unfocussed = true;
|
|
var showTabs = true;
|
|
var panes = []; // aml pane elements
|
|
var tabs = {}; // Tab objects (non-aml)
|
|
var containers = [];
|
|
var counter = 1;
|
|
|
|
var focussedTab, previewTab, previewTimeout;
|
|
var container, mnuEditors, collapsedMenu, isReady;
|
|
|
|
// Ref to focusManager - this will be changed later
|
|
focusManager.tabManager = plugin;
|
|
|
|
var loaded = false, changed = false;
|
|
function load(){
|
|
if (loaded) return false;
|
|
loaded = true;
|
|
|
|
draw();
|
|
|
|
// Commands & Menus
|
|
|
|
commands.addCommand({
|
|
name: "toggleButtons",
|
|
group: "Tabs",
|
|
exec: function(e) {
|
|
toggleButtons();
|
|
}
|
|
}, plugin);
|
|
|
|
// commands.addCommand({
|
|
// name : "newfile",
|
|
// group : "Tabs",
|
|
// exec : function(e) {
|
|
// plusNewFile({}, true);
|
|
// }
|
|
// }, plugin);
|
|
|
|
function getEditor(){
|
|
var editor = focussedTab && focussedTab.editor;
|
|
var subeditor = editor && editor.focussedWidget;
|
|
return subeditor && subeditor.editor || editor;
|
|
}
|
|
commands.on("getEditor", getEditor, plugin);
|
|
menus.on("getEditor", getEditor, plugin);
|
|
|
|
menus.addItemByPath("View/Tab Buttons", new apf.item({
|
|
type: "check",
|
|
checked: "user/tabs/@show",
|
|
command: "toggleButtons"
|
|
}), 300, plugin);
|
|
|
|
menus.on("focusEditor", function(){
|
|
focussedTab && focussedTab.editor.focus();
|
|
}, plugin);
|
|
|
|
// Filesystem hooks
|
|
|
|
fs.on("beforeRename", function(e) {
|
|
var oldPath = e.args[0];
|
|
var newPath = e.args[1];
|
|
|
|
var changed = [];
|
|
Object.keys(tabs).forEach(function(id) {
|
|
var tab = tabs[id];
|
|
if (tab.path && tab.path.indexOf(oldPath) === 0) {
|
|
var char = tab.path.charAt(oldPath.length);
|
|
// Make sure that a path like /Untitled1 is not matched
|
|
// by a path like /Untitled, which are clearly different
|
|
// files with no relation to each other
|
|
if (!char || char == "/") {
|
|
tab.path = tab.path.replace(oldPath, newPath);
|
|
changed.push(tab);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (changed.length) {
|
|
e.revert = function(){
|
|
changed.forEach(function(tab) {
|
|
// TODO store id instead of tab
|
|
// var tab = pages[id];
|
|
tab.path = tab.path.replace(newPath, oldPath);
|
|
});
|
|
};
|
|
}
|
|
});
|
|
fs.on("afterRename", function(e) {
|
|
if (e.error && e.revert)
|
|
e.revert();
|
|
});
|
|
function removeTab(e) {
|
|
if (!e.error) {
|
|
var tab = findTab(e.path);
|
|
if (tab)
|
|
tab.unload();
|
|
}
|
|
}
|
|
fs.on("afterUnlink", removeTab);
|
|
fs.on("afterRmfile", removeTab);
|
|
fs.on("afterRmdir", function(e) {
|
|
var path = e.path;
|
|
Object.keys(tabs).forEach(function(id) {
|
|
var tab = tabs[id];
|
|
if (tab.path && tab.path.indexOf(path) === 0)
|
|
tab.unload();
|
|
});
|
|
});
|
|
// Close a pane when it doesn't open
|
|
// @todo this should probably be interactive (unless during init)
|
|
// @todo move this to tabbehaviours?
|
|
fs.on("error", function(e) {
|
|
if (e.name == "readFile") {
|
|
var tab = findTab(e.path);
|
|
if (tab)
|
|
tab.unload();
|
|
}
|
|
}, plugin);
|
|
|
|
// Disconnect handler
|
|
var disconnected;
|
|
c9.on("disconnect", function(e) {
|
|
disconnected = true;
|
|
});
|
|
c9.on("connect", function(e) {
|
|
if (disconnected)
|
|
checkAllTabs();
|
|
disconnected = false;
|
|
});
|
|
|
|
// Editors menu handling
|
|
|
|
editors.on("menuClick", function(e) {
|
|
if (!editors.findEditor(e.value).fileExtensions.length)
|
|
openEditor(e.value, true, function(){});
|
|
else if (focussedTab)
|
|
switchEditor(focussedTab, e.value, function(){});
|
|
});
|
|
editors.on("menuShow", function(e) {
|
|
var group, editor = focussedTab && focussedTab.editor;
|
|
e.menu.childNodes.forEach(function(node) {
|
|
group = node.group;
|
|
|
|
var path = focussedTab && focussedTab.path || "";
|
|
var type = node.value;
|
|
var isAvailable = editors.editorSupportsFile(type, path);
|
|
|
|
node.setAttribute("disabled", !isAvailable);
|
|
});
|
|
|
|
if (editor && group)
|
|
group.setValue(editor.type);
|
|
});
|
|
|
|
// Settings
|
|
settings.on("read", function(e) {
|
|
// Defaults
|
|
settings.setDefaults("user/tabs", [
|
|
["show", "true"],
|
|
["title", "false"],
|
|
["asterisk", "false"]
|
|
]);
|
|
settings.setDefaults("state/tabs", []);
|
|
|
|
// Corner Handling
|
|
collapsedMenu = settings.getBool("state/menus/@minimized");
|
|
|
|
if (e.reset) {
|
|
setCornerPadding();
|
|
return;
|
|
}
|
|
|
|
// Create new tabs
|
|
var state = settings.getJson("state/tabs");
|
|
if (!state) {
|
|
state = {
|
|
type: "pane",
|
|
nodes: []
|
|
};
|
|
}
|
|
|
|
setTimeout(function() {
|
|
// Only set the state if we're not testing something else
|
|
if (options.testing != 2 && !isReady) {
|
|
setState(state, !isReady, function(){
|
|
emit.sticky("ready");
|
|
});
|
|
}
|
|
isReady = true;
|
|
|
|
showTabs = settings.getBool("user/tabs/@show");
|
|
toggleButtons(showTabs);
|
|
}, 0);
|
|
|
|
if (settings.getBool("user/tabs/@asterisk"))
|
|
ui.setStyleClass(container, "asterisk");
|
|
|
|
changed = false;
|
|
|
|
}, plugin);
|
|
|
|
settings.on("write", function(e) {
|
|
if (!changed && !e.unload)
|
|
return;
|
|
|
|
// When testing another component, don't save state
|
|
if (options.testing != 2) {
|
|
var state = getState(null, true);
|
|
settings.setJson("state/tabs", state);
|
|
}
|
|
|
|
changed = false;
|
|
}, plugin);
|
|
|
|
settings.on("user/tabs/@asterisk", function(value) {
|
|
containers.forEach(function(container) {
|
|
if (value)
|
|
ui.setStyleClass(container, "asterisk");
|
|
else
|
|
ui.setStyleClass(container, "", ["asterisk"]);
|
|
});
|
|
});
|
|
|
|
// Top Right Corner Handling
|
|
var CSS1 = ".editor_tab.morepadding .btnsesssioncontainer";
|
|
var CSS2 = ".editor_tab .btnsesssioncontainer";
|
|
var clearAnim = function(prop){
|
|
setTimeout(function(){
|
|
ui.setStyleRule(prop, "transition-duration", "0");
|
|
}, 200);
|
|
};
|
|
|
|
menus.on("minimize", function(){
|
|
collapsedMenu = true;
|
|
|
|
ui.setStyleRule(CSS1, "transition-duration", ".2s");
|
|
clearAnim(CSS1);
|
|
|
|
setCornerPadding();
|
|
});
|
|
menus.on("restore", function(){
|
|
collapsedMenu = false;
|
|
if (lastCorner) {
|
|
ui.setStyleRule(CSS2, "transition-duration", ".2s");
|
|
clearAnim(CSS2);
|
|
|
|
ui.setStyleClass(lastCorner.$ext, "", ["morepadding"]);
|
|
lastCorner = null;
|
|
}
|
|
});
|
|
|
|
// Focus Handling
|
|
apf.addEventListener("movefocus", function(e) {
|
|
var iter = e.toElement, list = [], c = 0;
|
|
|
|
if (iter && iter.localName == "menu")
|
|
return;
|
|
|
|
while (iter && containers.indexOf(iter) == -1)
|
|
iter = (list[c++] = iter).parentNode;
|
|
if (!iter) {
|
|
// Editor can be unset during unload of tabs
|
|
if (!unfocussed && focussedTab && focussedTab.editor) {
|
|
focussedTab.editor.focus(false, true);
|
|
unfocussed = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (var pane, i = list.length - 1; i >= 0; i--) {
|
|
if ((pane = list[i]).localName == "tab") {
|
|
if (pane.getPages().length < 1
|
|
|| !pane.cloud9pane.visible)
|
|
continue;
|
|
|
|
var newFocus = pane.getPage().cloud9tab;
|
|
if (newFocus != focussedTab || unfocussed) {
|
|
var soft = e.toElement != newFocus.aml ? 2 : 0;
|
|
focusTab(newFocus, soft);
|
|
}
|
|
unfocussed = false;
|
|
|
|
iter.$activePaneName = pane.cloud9pane.name;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
focusTab(); // Blur
|
|
});
|
|
|
|
// Title
|
|
if (!c9.local) {
|
|
plugin.on("focusSync", function(e){ updateTitle(e.tab) });
|
|
plugin.on("tabDestroy", function(e){ if (e.last) updateTitle(); });
|
|
settings.on("user/tabs", function(){ updateTitle(focussedTab); });
|
|
}
|
|
}
|
|
|
|
var drawn = false;
|
|
function draw(){
|
|
if (drawn) return;
|
|
drawn = true;
|
|
|
|
// Don't draw Tabs if we're testing something that just depends
|
|
// on this plugin
|
|
if (options.testing != 2) {
|
|
container = layout.findParent(plugin).appendChild(new ui.bar({
|
|
"class" : "codeditorHolder",
|
|
"style" : "height:100%"
|
|
//style : "position:absolute;"
|
|
}));
|
|
containers.push(container);
|
|
plugin.addElement(container);
|
|
}
|
|
|
|
mnuEditors = apf.document.documentElement.appendChild(new ui.menu({
|
|
id: "mnuEditors",
|
|
style: "margin: 0px 0 0 4px",
|
|
"onprop.visible" : function(e) {
|
|
if (e.value)
|
|
mnuEditors.pane = this.opener.parentNode.cloud9pane;
|
|
}
|
|
}));
|
|
plugin.addElement(mnuEditors);
|
|
|
|
menus.addItemToMenu(mnuEditors,
|
|
new ui.item({
|
|
caption: "New File",
|
|
hotkey: "{commands.commandManager.newfile}",
|
|
onclick: function(e) {
|
|
e.pane = this.parentNode.pane;
|
|
plusNewFile(e, true);
|
|
},
|
|
enabled: !c9.readonly
|
|
}), 100, plugin);
|
|
|
|
emit("draw");
|
|
}
|
|
|
|
function plusNewFile(e, force) {
|
|
if (force || emit("plusClick", e) !== false) {
|
|
var name;
|
|
do{ name = "/Untitled" + counter++; } while (findTab(name));
|
|
|
|
open({
|
|
path: name,
|
|
active: true,
|
|
pane: e.pane,
|
|
value: "",
|
|
document: {
|
|
meta: {
|
|
newfile: true
|
|
}
|
|
}
|
|
}, function(){});
|
|
}
|
|
}
|
|
|
|
function createPane(state) {
|
|
state.createPane = createPane;
|
|
state.container = container;
|
|
|
|
var pane = new Pane(state);
|
|
|
|
pane.on("beforeSwitch", onbeforeswitch);
|
|
pane.on("afterSwitch", onafterswitch);
|
|
|
|
var btnPlus = pane.getElement("btnPlus");
|
|
btnPlus.setAttribute("submenu", mnuEditors);
|
|
|
|
pane.on("beforeClose", function(e) {
|
|
return emit("tabBeforeClose", e);
|
|
});
|
|
pane.on("afterClose", function(e) {
|
|
return emit("tabAfterClose", e);
|
|
});
|
|
|
|
pane.on("tabOrder", function(e) {
|
|
changed = true;
|
|
settings.save();
|
|
|
|
return emit("tabOrder", e);
|
|
});
|
|
|
|
pane.on("unload", function(){
|
|
panes.remove(pane);
|
|
|
|
if (focussedTab && !focussedTab.isActive()) {
|
|
if (!panes.length) {
|
|
focussedTab = null;
|
|
unfocussed = true;
|
|
}
|
|
else
|
|
focusTab(panes[0].getTab(), true);
|
|
}
|
|
|
|
emit("paneDestroy", { pane: pane });
|
|
|
|
setCornerPadding();
|
|
|
|
changed = true;
|
|
settings.save();
|
|
});
|
|
|
|
panes.push(pane);
|
|
|
|
pane.aml.once("DOMNodeInsertedIntoDocument", function(){
|
|
emit.sticky("paneCreate", { pane: pane }, pane);
|
|
});
|
|
|
|
if (!settings.getBool("user/tabs/@show"))
|
|
ui.setStyleClass(pane.aml.$ext, "notabs", ["notabs"]);
|
|
|
|
changed = true;
|
|
settings.save();
|
|
|
|
return pane;
|
|
}
|
|
|
|
function updateTitle(tab) {
|
|
document.title = tab && settings.getBool("user/tabs/@title") && tab.title
|
|
? tab.title + " - " + ideProviderName
|
|
: c9.projectName + " - " + ideProviderName;
|
|
}
|
|
|
|
var lastCorner;
|
|
function setCornerPadding(){
|
|
if (!collapsedMenu || c9.local) return;
|
|
|
|
var tab = (function recur(node){
|
|
if (!node)
|
|
return false;
|
|
else if (node.localName == "tab")
|
|
return node;
|
|
else if (node.$vbox)
|
|
return recur(node.getFirstChild());
|
|
else
|
|
return recur(node.getSecondChild() || node.getFirstChild());
|
|
})(container.childNodes[0]);
|
|
|
|
if (!tab || tab == lastCorner)
|
|
return;
|
|
|
|
if (lastCorner)
|
|
ui.setStyleClass(lastCorner.$ext, "", ["morepadding"]);
|
|
|
|
ui.setStyleClass(tab.$ext, "morepadding");
|
|
|
|
lastCorner = tab;
|
|
}
|
|
|
|
function createTab(state){
|
|
if (!state.pane) {
|
|
if (focussedTab && ui.isChildOf(container, focussedTab.aml)) {
|
|
state.pane = focussedTab.pane;
|
|
}
|
|
if (!state.pane) {
|
|
var i = 0;
|
|
while (panes[i] && !ui.isChildOf(container, panes[i].aml)) { i++ }
|
|
state.pane = panes[i];
|
|
}
|
|
if (!state.pane) {
|
|
throw new Error("Called open too early. Please wait until "
|
|
+ "a pane is available. Use the ready event.");
|
|
}
|
|
}
|
|
|
|
var tab = new Tab(state);
|
|
|
|
var id = !tab.document.meta.cloned && tab.path
|
|
? tab.path
|
|
: PREFIX + tab.name;
|
|
tabs[id] = tab;
|
|
|
|
tab.on("close", function(e) {
|
|
var id = !tab.document.meta.cloned && tab.path
|
|
? tab.path
|
|
: PREFIX + tab.name;
|
|
delete tabs[id];
|
|
|
|
if (tab.path)
|
|
watcher.unwatch(tab.path);
|
|
|
|
if (!(function(){for (var p in tabs){return true;}})()) {
|
|
focusTab();
|
|
focussedTab = null;
|
|
unfocussed = true;
|
|
}
|
|
else if (!tab.pane.getTabs().length) {
|
|
if (panes.every(function(pane) {
|
|
if (pane.activeTab && pane.visible) {
|
|
focusTab(pane.activeTab);
|
|
return false;
|
|
}
|
|
return true;
|
|
})) focusTab(); // blur
|
|
}
|
|
|
|
if (tab.document.canUnload() !== false)
|
|
tab.document.unload();
|
|
|
|
emit("tabDestroy", {
|
|
tab: tab,
|
|
last: e.last,
|
|
htmlEvent: e.htmlEvent
|
|
});
|
|
|
|
changed = true;
|
|
settings.save();
|
|
});
|
|
|
|
tab.on("setPath", function(e) {
|
|
var meta = tab.document.meta;
|
|
if (!meta.preview && !meta.ignoreState && !meta.cloned) {
|
|
watcher.unwatch(e.oldpath);
|
|
watcher.watch(e.path);
|
|
}
|
|
|
|
if (!meta.cloned) {
|
|
if (tabs[PREFIX + tab.name])
|
|
delete tabs[PREFIX + tab.name];
|
|
delete tabs[e.oldpath];
|
|
tabs[e.path] = tab;
|
|
}
|
|
|
|
updateTitle(tab);
|
|
|
|
changed = true;
|
|
settings.save();
|
|
});
|
|
|
|
tab.on("beforeReparent", function(e) {
|
|
e.tab = tab;
|
|
return emit("tabBeforeReparent", e);
|
|
});
|
|
tab.on("afterReparent", function(e) {
|
|
e.tab = tab;
|
|
emit("tabAfterReparent", e);
|
|
});
|
|
|
|
// Only watch pages with paths. Newfiles first need to be saved
|
|
if (tab.path) {
|
|
var meta = tab.document.meta;
|
|
if (meta.newfile) {
|
|
fs.on("afterWriteFile", function startWatch(e) {
|
|
if (e.path == tab.path) {
|
|
watcher.watch(tab.path);
|
|
fs.off("afterWriteFile", startWatch);
|
|
}
|
|
}, tab); // Only exists for the lifetime of the tab
|
|
}
|
|
else {
|
|
// Previewed files shouldn't be watched
|
|
if (!meta.preview && !meta.ignoreState && !meta.nofs && !meta.cloned)
|
|
watcher.watch(tab.path);
|
|
else
|
|
console.warn("[watchers] ignoring preview/ignoreState tab " + tab.path);
|
|
}
|
|
}
|
|
|
|
emit.sticky("tabCreate", { tab: tab }, tab);
|
|
|
|
changed = true;
|
|
settings.save();
|
|
|
|
return tab;
|
|
}
|
|
|
|
/* Event Handlers */
|
|
|
|
function onbeforeswitch(e) {
|
|
var amlPane = e.pane.aml;
|
|
var tab = e.tab;
|
|
|
|
var editorTab = amlPane.getPage(tab.aml.type);
|
|
if (!editorTab) return;
|
|
|
|
if (emit("tabBeforeActivate", {
|
|
tab: tab
|
|
}) === false)
|
|
return false;
|
|
}
|
|
|
|
function onafterswitch(e) {
|
|
var tab = e.tab;
|
|
|
|
if (!loaded || tab.document.meta.preview)
|
|
return;
|
|
|
|
var lastTab = focussedTab;
|
|
|
|
if (!focussedTab || focussedTab.pane == tab.pane && focussedTab != tab)
|
|
focusTab(tab, true, true);
|
|
|
|
// Undocumented
|
|
emit("tabAfterActivateSync", {
|
|
lastTab: lastTab,
|
|
tab: tab
|
|
});
|
|
|
|
setTimeout(function(){
|
|
emit("tabAfterActivate", {
|
|
lastTab: lastTab,
|
|
tab: tab
|
|
});
|
|
|
|
changed = true;
|
|
settings.save();
|
|
}, 500);
|
|
}
|
|
|
|
/***** Methods *****/
|
|
|
|
function getState(subcontainer, filter) {
|
|
var state = (function recur(node) {
|
|
if (!node) return;
|
|
|
|
var name = node.localName;
|
|
var isPane = name == "tab";
|
|
if (isPane) name = "pane";
|
|
|
|
var state = {
|
|
type: name,
|
|
name: isPane ? node.cloud9pane.name : undefined,
|
|
nodes: node.childNodes
|
|
.filter(function(child) {
|
|
if (isPane
|
|
&& (child.localName != "page" || !child.visible)
|
|
|| child.localName == "splitter")
|
|
return false;
|
|
|
|
// Exclude previewing pages;
|
|
var tab = child.cloud9tab;
|
|
return !tab || !tab.document.meta.preview
|
|
&& !tab.document.meta.ignoreState;
|
|
})
|
|
.map(function(child) {
|
|
return isPane
|
|
? child.cloud9tab.getState(filter)
|
|
: recur(child);
|
|
})
|
|
};
|
|
|
|
if (node.width) state.width = node.width;
|
|
if (node.height) state.height = node.height;
|
|
if (node.skin) state.skin = node.skin;
|
|
if (node.skinset) state.skinset = node.skinset;
|
|
|
|
if (isPane && node.cloud9pane.meta) {
|
|
var meta = node.cloud9pane.meta;
|
|
for (var prop in meta) {
|
|
if (prop.charAt(0) != "$") {
|
|
if (!state.meta)
|
|
state.meta = {};
|
|
state.meta[prop] = meta[prop] && meta[prop].toJson
|
|
? meta[prop].toJson() : meta[prop];
|
|
}
|
|
}
|
|
}
|
|
|
|
return state;
|
|
})(subcontainer || container.firstChild);
|
|
|
|
if (state)
|
|
state.focus = focussedTab
|
|
&& (focussedTab.path || focussedTab.name);
|
|
|
|
return state;
|
|
}
|
|
|
|
function setState(state, init, callback) {
|
|
if (typeof init == "function") {
|
|
callback = init;
|
|
init = false;
|
|
}
|
|
|
|
// Remove all existing tabs
|
|
if (!init && !options.testing)
|
|
clear();
|
|
|
|
var count = 0;
|
|
|
|
// Load State
|
|
(function recur(parent, list) {
|
|
list.forEach(function(state) {
|
|
var p;
|
|
|
|
if (state.type == "pane") {
|
|
state.preventAutoActivate = state.nodes.length > 0;
|
|
p = createPane(state).aml;
|
|
parent.appendChild(p);
|
|
}
|
|
else if (state.type == "hsplitbox" || state.type == "vsplitbox") {
|
|
p = parent.appendChild(new ui[state.type]({
|
|
splitter: true,
|
|
padding: 1,
|
|
width: state.width,
|
|
height: state.height
|
|
}));
|
|
}
|
|
else if (state.type == "tab") {
|
|
var tab = findTab(state.id);
|
|
if (!tab) {
|
|
state.pane = parent.cloud9pane;
|
|
state.init = init;
|
|
|
|
count++;
|
|
open(state, function(err, tab) {
|
|
if (!--count)
|
|
callback();
|
|
});
|
|
}
|
|
else {
|
|
tab.attachTo(parent);
|
|
}
|
|
return;
|
|
}
|
|
recur(p, state.nodes);
|
|
|
|
// If somehow we didn't record an active tab,
|
|
// set the first tab as active.
|
|
if (state.type == "pane") {
|
|
var pane = p.cloud9pane;
|
|
if (!pane.activeTab) {
|
|
var next = pane.getTabs()[0];
|
|
next && next.activate();
|
|
}
|
|
}
|
|
});
|
|
})(state.container || container, [state]);
|
|
|
|
// Set Focus
|
|
if (state.focus)
|
|
focusTab(findTab(state.focus));
|
|
|
|
setCornerPadding();
|
|
|
|
if (!count)
|
|
callback();
|
|
}
|
|
|
|
function clear(soft, clearTabs /* For testing only */){
|
|
var list = getPanes(container);
|
|
|
|
for (var i = list.length - 1; i >= 0; i--) {
|
|
var pane = list[i], nodes = pane.getTabs();
|
|
for (var j = nodes.length - 1; j >= 0; j--) {
|
|
var tab = nodes[j];
|
|
if (!soft) {
|
|
tab.meta.$closeSync = true;
|
|
tab.unload();
|
|
}
|
|
else {
|
|
tab.aml.parentNode.removeChild(tab.aml);
|
|
tab.pane = null;
|
|
}
|
|
}
|
|
// if (i > 0)
|
|
pane.unload();
|
|
}
|
|
if (!soft || clearTabs) tabs = [];
|
|
panes = [];
|
|
}
|
|
|
|
function toggleButtons(to) {
|
|
showTabs = to !== undefined ? to : !showTabs;
|
|
|
|
settings.set("user/tabs/@show", showTabs);
|
|
emit("visible", {value: showTabs});
|
|
|
|
getPanes(container).forEach(function(pane) {
|
|
ui.setStyleClass(pane.aml.$ext, showTabs ? "" : "notabs", ["notabs"]);
|
|
});
|
|
}
|
|
|
|
var closeTimer;
|
|
function resizePanes(cancel) {
|
|
clearTimeout(closeTimer);
|
|
|
|
if (cancel)
|
|
return;
|
|
|
|
closeTimer = setTimeout(function(){
|
|
panes.forEach(function(pane) {
|
|
pane.aml.$waitForMouseOut = false;
|
|
pane.aml.$scaleinit(null, "sync");
|
|
});
|
|
}, 500);
|
|
}
|
|
|
|
function activateTab(tab) {
|
|
if (typeof tab == "string")
|
|
tab = findTab(tab);
|
|
|
|
tab.activate();
|
|
|
|
return tab;
|
|
}
|
|
|
|
function focusTab(tab, soft, async) {
|
|
if (typeof tab == "string")
|
|
tab = findTab(tab);
|
|
|
|
if (previewTab && tab != previewTab)
|
|
preview({ cancel: true });
|
|
|
|
// do not focus a tab during close animation
|
|
if (tab && tab.meta && tab.meta.$closing) {
|
|
// Focus
|
|
if (focussedTab && (!soft || soft !== 2 && !unfocussed) && focussedTab.editor) {
|
|
unfocussed = false;
|
|
focussedTab.editor.focus();
|
|
}
|
|
|
|
return tab;
|
|
}
|
|
|
|
if (focussedTab != tab) {
|
|
// Blur
|
|
if (focussedTab) {
|
|
var blurTab = focussedTab;
|
|
var blur = function (){
|
|
emit("blur", { tab: blurTab });
|
|
|
|
// During destroy of the pane the editor can
|
|
// not exist for a tab
|
|
if (blurTab.editor && focussedTab
|
|
&& blurTab.editor != focussedTab.editor)
|
|
blurTab.editor.blur();
|
|
};
|
|
|
|
blurTab.classList.remove("focus");
|
|
|
|
blur();
|
|
}
|
|
|
|
if (!tab) {
|
|
focussedTab = null;
|
|
unfocussed = true;
|
|
return;
|
|
}
|
|
|
|
// Change focussedTab
|
|
focussedTab = tab;
|
|
|
|
tab.activate();
|
|
|
|
// Focus
|
|
if ((!soft || soft !== 2 && !unfocussed) && focussedTab.editor) {
|
|
unfocussed = false;
|
|
focussedTab.editor.focus();
|
|
}
|
|
|
|
focussedTab && focussedTab.classList.add("focus");
|
|
}
|
|
else {
|
|
if (!focussedTab)
|
|
return;
|
|
|
|
if ((!soft || soft !== 2 && !unfocussed) && focussedTab.editor) {
|
|
unfocussed = false;
|
|
focussedTab.editor.focus(true);
|
|
}
|
|
|
|
return tab;
|
|
}
|
|
|
|
emit("focusSync", { tab : tab });
|
|
|
|
if (async) {
|
|
setTimeout(function(){
|
|
emit("focus", { tab : tab });
|
|
}, 500);
|
|
}
|
|
else
|
|
emit("focus", { tab : tab });
|
|
|
|
return tab;
|
|
}
|
|
|
|
function findTab(path) {
|
|
return tabs[PREFIX + path] || tabs[util.normalizePath(path)];
|
|
}
|
|
|
|
function getTabs(container) {
|
|
var result = Object.keys(tabs).map(function(path) {
|
|
return tabs[PREFIX + path] || tabs[path];
|
|
});
|
|
|
|
if (!container)
|
|
return result;
|
|
|
|
return result.filter(function(tab) {
|
|
return ui.isChildOf(container, tab.aml);
|
|
});
|
|
}
|
|
|
|
function findPane(name) {
|
|
for (var i = 0; i < panes.length; i++) {
|
|
if (panes[i].name == name)
|
|
return panes[i];
|
|
}
|
|
}
|
|
|
|
function getPanes(container) {
|
|
return !container
|
|
? panes.slice()
|
|
: panes.filter(function(pane) {
|
|
return ui.isChildOf(container, pane.aml);
|
|
});
|
|
}
|
|
|
|
/**** Main entry point for opening tabs ****/
|
|
|
|
// Handler to show loading indicator
|
|
function setLoading(tab) {
|
|
tab.classList.add("loading");
|
|
|
|
// TODO this gets called for things that have nothing to do
|
|
// with filesystem :(
|
|
if (!tab.path)
|
|
return;
|
|
|
|
var loadStartT = Date.now();
|
|
fs.on("downloadProgress", function progress(e) {
|
|
if (e.path == tab.path) {
|
|
e.dt = Date.now() - loadStartT;
|
|
tab.document.progress(e);
|
|
if (e.complete)
|
|
fs.off("downloadProgress", progress);
|
|
}
|
|
});
|
|
// TODO move to fs
|
|
// tab.document.progress({dt: 0, loaded: 0, total: 1});
|
|
}
|
|
|
|
function openFile(path, active, callback) {
|
|
if (typeof active == "function")
|
|
callback = active, active = false;
|
|
|
|
return open({path: path, active: active}, callback);
|
|
}
|
|
|
|
function openEditor(type, active, callback) {
|
|
if (typeof active == "function")
|
|
callback = active, active = false;
|
|
|
|
return open({editorType: type, active: active}, callback);
|
|
}
|
|
|
|
function open(options, callback) {
|
|
var path = options.path = util.normalizePath(options.path);
|
|
var type = options.editorType;
|
|
var editor;
|
|
|
|
// If a tab for this file already exist, activate it
|
|
var id = !options.forceNew && !options.init && path || options.name;
|
|
var tab = options.tab || (id
|
|
? findTab(id)
|
|
: options.demandExisting
|
|
&& getTabs().filter(function(t) {
|
|
return t.editorType === options.editorType
|
|
&& (!options.title || (t.document
|
|
&& t.document.title === options.title));
|
|
})[0]);
|
|
|
|
// prevent opening of same tab twice in non cloned mode
|
|
// TODO move cloning into ace?
|
|
if (!tab)
|
|
tab = findTab(path);
|
|
|
|
// Clone Tab
|
|
if (((options.document || 0).meta || 0).cloned) {
|
|
if (!tab) {
|
|
if (options.init) {
|
|
plugin.on("open", function wait(e){
|
|
if (e.tab.path == path) {
|
|
plugin.off("open", wait);
|
|
options.tab = e.tab;
|
|
open(options, callback);
|
|
}
|
|
}, plugin);
|
|
return;
|
|
}
|
|
else {
|
|
throw new Error("Missing tab when cloning");
|
|
}
|
|
}
|
|
|
|
options.path = path = tab.path;
|
|
options.editorType = tab.editorType;
|
|
options.document = tab.document.clone();
|
|
|
|
// Tab variable will be overwritten below
|
|
tab.on("unload", function(){ tab.unload(); }, tab.document);
|
|
}
|
|
// Process Existing Tab
|
|
else if (tab) {
|
|
if (tab.document.meta.preview)
|
|
keepPreview();
|
|
|
|
if (options.active)
|
|
tab.activate();
|
|
if (options.focus)
|
|
focusTab(tab, options.focus !== true);
|
|
if (options.document)
|
|
tab.document.setState(options.document);
|
|
callback && callback(null, tab, null, true);
|
|
return tab;
|
|
}
|
|
|
|
// Find editor
|
|
if (!type) {
|
|
editor = path && editors.findEditorByFilename(path)
|
|
|| editors.findEditor(editors.defaultEditor);
|
|
type = editor && editor.type || editors.defaultEditor;
|
|
}
|
|
else {
|
|
editor = editors.findEditor(type);
|
|
}
|
|
|
|
// Set sensible defaults
|
|
if (typeof path != "string")
|
|
options.path = null;
|
|
if (!options.document)
|
|
options.document = {};
|
|
if (path && !options.document.title) {
|
|
options.document.title = basename(path);
|
|
options.document.tooltip = path;
|
|
}
|
|
if (typeof options.value == "string" && !options.newOnError)
|
|
options.document.value = options.value;
|
|
// if (options.document.filter === undefined)
|
|
// options.document.filter = true;
|
|
options.editorType = type;
|
|
|
|
// Create the tab
|
|
tab = createTab(options);
|
|
|
|
// Focus
|
|
if (options.focus)
|
|
focusTab(tab, options.focus !== true);
|
|
|
|
var doc = options.document;
|
|
var loadFromDisk = path
|
|
&& (!doc || doc.value === undefined)
|
|
&& (options.value === undefined || options.newOnError)
|
|
// autoload to false prevents loading data, used by image editor
|
|
&& (!editor || editor.autoload !== false);
|
|
|
|
// Handler to be used after content and state is loaded
|
|
function done(err, value) {
|
|
tab.classList.remove("loading");
|
|
|
|
if (err && options.newOnError) {
|
|
tab.document.meta.newfile = true;
|
|
value = options.value || "";
|
|
}
|
|
else if (err) {
|
|
tab.classList.add("error");
|
|
tab.document.meta.error = true;
|
|
|
|
alert("Error opening file",
|
|
"Could not open file: " + tab.path,
|
|
err.code == "ENOENT"
|
|
? "The file could not be found on the file system."
|
|
: "Unknown error: " + (err.message || err.code || err),
|
|
function(){
|
|
tab.close();
|
|
callback && callback(err);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
var doc = tab.document;
|
|
if (typeof value == "string")
|
|
doc.value = value;
|
|
|
|
// Set timestamp to now() to indicate that the file has been
|
|
// loaded from disk without any changes
|
|
if (!doc.meta.timestamp)
|
|
doc.meta.timestamp = Date.now() - settings.timeOffset;
|
|
|
|
doc.ready = true;
|
|
emit("open", { tab: tab, options: options });
|
|
callback && callback(null, tab);
|
|
}
|
|
|
|
if (loadFilesAtInit === false && options.init
|
|
&& emit.listeners("beforeOpen").length === 0)
|
|
return tab;
|
|
|
|
// Hooks for plugins that want to override value and state loading
|
|
var event = {
|
|
options: options,
|
|
tab: tab,
|
|
loadFromDisk: loadFromDisk,
|
|
setLoading: setLoading,
|
|
callback: done
|
|
};
|
|
if (emit("beforeOpen", event) === false)
|
|
return tab;
|
|
|
|
// todo why metadata does this?
|
|
loadFromDisk = event.options.loadFromDisk = loadFromDisk
|
|
&& !tab.document.meta.newfile
|
|
&& !tab.document.meta.nofs;
|
|
|
|
// If no value is specified, lets load it from storage
|
|
// todo metadata doesn't check for this! should we?
|
|
if (options.value === -1) {
|
|
setLoading(tab);
|
|
callback(null, tab, function(){
|
|
tab.classList.remove("loading");
|
|
emit("open", { tab: tab, options: options });
|
|
});
|
|
}
|
|
else if (loadFromDisk) {
|
|
setLoading(tab);
|
|
var xhr = fs.readFile(path, "utf8", done);
|
|
|
|
// Cancel file opening when tab is closed
|
|
var abort = function(){
|
|
xhr && xhr.abort();
|
|
};
|
|
tab.on("close", abort);
|
|
plugin.on("open", function(e) {
|
|
if (e.tab == tab)
|
|
tab.off("close", abort);
|
|
});
|
|
}
|
|
else {
|
|
// done has to be called asynchronously
|
|
setTimeout(function() {
|
|
done(null, null);
|
|
});
|
|
}
|
|
|
|
return tab;
|
|
}
|
|
|
|
function clone(tab, pane, callback){
|
|
open({
|
|
pane: pane,
|
|
tab: tab,
|
|
focus: true,
|
|
document: {
|
|
meta: { cloned: true }
|
|
}
|
|
}, function(err, tab){
|
|
if (tab)
|
|
tab.document.progress({ complete: true });
|
|
callback(err, tab);
|
|
});
|
|
}
|
|
|
|
function reload(tab, callback) {
|
|
if (!tab) tab = focussedTab;
|
|
if (!tab || !tab.path)
|
|
return callback();
|
|
|
|
if (tab.editor.autoload === false) {
|
|
tab.document.setBookmarkedValue(tab.document.value);
|
|
|
|
delete tab.document.meta.error;
|
|
tab.classList.remove("error");
|
|
tab.classList.remove("loading");
|
|
emit("reload", { tab: tab });
|
|
|
|
return callback();
|
|
}
|
|
|
|
setLoading(tab);
|
|
|
|
fs.readFile(tab.path, "utf8", function(err, data) {
|
|
if (err) {
|
|
tab.classList.remove("loading");
|
|
tab.classList.add("error");
|
|
tab.document.meta.error = true;
|
|
callback(err);
|
|
return;
|
|
}
|
|
|
|
// Update val
|
|
tab.document.setBookmarkedValue(data);
|
|
tab.document.meta.timestamp = Date.now() - settings.timeOffset;
|
|
|
|
// Make sure the newfile flag is gone
|
|
delete tab.document.meta.newfile;
|
|
|
|
// Remove the error flag
|
|
delete tab.document.meta.error;
|
|
|
|
tab.classList.remove("error");
|
|
tab.classList.remove("loading");
|
|
emit("reload", { tab: tab });
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function checkAllTabs() {
|
|
getTabs().forEach(function(tab) {
|
|
var meta = tab.document.meta;
|
|
if (tab.path && !meta.newfile && !meta.preview && !meta.ignoreState && !meta.nofs)
|
|
watcher.check(tab.path, meta.timestamp);
|
|
});
|
|
}
|
|
|
|
var lastPreviewTab;
|
|
function preview(options, callback) {
|
|
var pane;
|
|
|
|
// Cancel previewing and show focussedTab again
|
|
if (options.cancel)
|
|
return cancelPreview(options.keep);
|
|
|
|
// Exclude certain extensions from being previewed
|
|
if (XPREVIEW.test(options.path))
|
|
return;
|
|
|
|
if (!options.editorType && previewTab && previewTab.path === options.path) {
|
|
// keepPreview();
|
|
return previewTab;
|
|
}
|
|
// Remove existing preview pane if it's there
|
|
else if (previewTab) {
|
|
pane = previewTab.pane;
|
|
if (previewTab.document.meta.existing) {
|
|
delete previewTab.document.meta.preview;
|
|
delete previewTab.document.meta.existing;
|
|
|
|
previewTab.classList.remove("preview");
|
|
}
|
|
|
|
// Or keep tab until the new one is loaded
|
|
else {
|
|
previewTab.unload();
|
|
}
|
|
}
|
|
|
|
if (!options.path && !options.editorType)
|
|
throw new Error("No path or editorType specified for preview");
|
|
|
|
return createPreview(options, pane, callback);
|
|
}
|
|
|
|
function createPreview(options, pane, callback) {
|
|
var path = options.path;
|
|
// Check if pane is already loaded
|
|
var tab = findTab(path);
|
|
if (tab) {
|
|
// tab.document.meta.preview = true;
|
|
// tab.document.meta.existing = true;
|
|
tab.activate();
|
|
clearTimeout(previewTimeout);
|
|
callback(null, null);
|
|
}
|
|
// Else create preview pane
|
|
else if (!previewTimeout || options.immediate) {
|
|
var doc = options.document || {};
|
|
doc.meta = {
|
|
readonly: true,
|
|
preview: true
|
|
};
|
|
|
|
previewTab = open({
|
|
path: path,
|
|
editorType: options.editorType,
|
|
active: true,
|
|
pane: pane,
|
|
noanim: true,
|
|
document: doc
|
|
}, function(err, tab) {
|
|
// Previewing has already been cancelled
|
|
if (err || !tab.loaded)
|
|
return callback(new Error("Preview was cancelled"));
|
|
|
|
// Activate previewTab showing content
|
|
tab.activate();
|
|
|
|
// Open actual tab when the user starts editing
|
|
// @TODO cleanup
|
|
tab.document.undoManager.on("change", function(){
|
|
if (tab.document.undoManager.position > -1)
|
|
keepPreview();
|
|
});
|
|
|
|
callback(null, tab);
|
|
});
|
|
previewTab.classList.add("preview");
|
|
} else {
|
|
clearTimeout(previewTimeout);
|
|
previewTimeout = setTimeout(function() {
|
|
previewTimeout = null;
|
|
createPreview({
|
|
path: options.path,
|
|
immediate: true
|
|
}, pane, callback);
|
|
}, 200);
|
|
}
|
|
|
|
return previewTab;
|
|
}
|
|
|
|
function cancelPreview(keep) {
|
|
var lastFocussedTab = focussedTab;
|
|
|
|
// Unload last preview tab
|
|
if (lastPreviewTab) {
|
|
lastPreviewTab.unload();
|
|
lastPreviewTab = null;
|
|
}
|
|
|
|
if (!previewTab)
|
|
return false;
|
|
|
|
previewTab.classList.remove("preview");
|
|
|
|
if (keep)
|
|
return keepPreview();
|
|
|
|
if (previewTab.document.meta.existing) {
|
|
delete previewTab.document.meta.preview;
|
|
delete previewTab.document.meta.existing;
|
|
}
|
|
else {
|
|
var tab = previewTab;
|
|
previewTab = null;
|
|
tab.unload(); // TODO this focusses the last tab. If there is a speed concern, fix this.
|
|
}
|
|
|
|
previewTab = null;
|
|
lastFocussedTab && lastFocussedTab.activate();
|
|
return false;
|
|
}
|
|
|
|
// Open actual tab
|
|
function keepPreview(){
|
|
if (!previewTab) return;
|
|
|
|
if (previewTimeout) {
|
|
previewTimeout = null;
|
|
clearTimeout(previewTimeout);
|
|
}
|
|
|
|
if (!previewTab.loaded) {
|
|
previewTab.unload();
|
|
previewTab = null;
|
|
return false;
|
|
}
|
|
|
|
previewTab.classList.remove("preview");
|
|
|
|
if (previewTab.document.meta.existing) {
|
|
delete previewTab.document.meta.existing;
|
|
delete previewTab.document.meta.preview;
|
|
}
|
|
else {
|
|
delete previewTab.document.meta.readonly;
|
|
delete previewTab.document.meta.preview;
|
|
previewTab.document.undoManager.off("change", keepPreview);
|
|
emit("open", { tab: previewTab });
|
|
}
|
|
|
|
previewTab = null;
|
|
return true;
|
|
}
|
|
|
|
function switchEditor(tab, type, callback) {
|
|
tab.switchEditor(type, function(){
|
|
emit("switchEditor", { tab: tab });
|
|
callback.apply(this, arguments);
|
|
});
|
|
}
|
|
|
|
/**** Support for state preservation ****/
|
|
|
|
function pauseTabResize(){
|
|
panes.forEach(function(pane) {
|
|
pane.setAttribute("buttons", "close,order");
|
|
});
|
|
}
|
|
|
|
function continueTabResize(){
|
|
setTimeout(function(){
|
|
panes.forEach(function(pane) {
|
|
pane.setAttribute("buttons", "close,scale,order");
|
|
pane.$waitForMouseOut = false;
|
|
pane.$scaleinit(null, "sync");
|
|
});
|
|
}, 300);
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function(){
|
|
load();
|
|
});
|
|
plugin.on("enable", function(){
|
|
|
|
});
|
|
plugin.on("disable", function(){
|
|
|
|
});
|
|
plugin.on("unload", function(){
|
|
panes = [];
|
|
tabs = [];
|
|
loaded = false;
|
|
drawn = false;
|
|
lastPreviewTab = null;
|
|
});
|
|
|
|
/***** Register and define API *****/
|
|
|
|
/**
|
|
* Manages all tabs in all panes of Cloud9. This includes the panes
|
|
* in the console. Use this plugin to open files and editors, to change
|
|
* the focus of a tab, or to fetch panes and tabs.
|
|
*
|
|
* The tabManager manages the whole of panes and tabs that are organized
|
|
* as follows:
|
|
*
|
|
* * {@link Pane} - Represent a single pane, housing multiple tabs
|
|
* * {@link Tab} - A single tab (button) in a pane
|
|
* * {@link Editor} - The editor responsible for displaying the file in the tab
|
|
* * {@link Document} - The representation of a file in the tab
|
|
* * {@link Session} - The session information of the editor
|
|
* * {@link UndoManager} - The object that manages the undo stack for this document
|
|
*
|
|
* Panes can live in certain areas of Cloud9. By default these areas are:
|
|
*
|
|
* * {@link panes} - The main area where editor panes are displayed
|
|
* * {@link console} - The console in the bottom of the screen
|
|
*
|
|
* This is the default way to open a new file in an editor:
|
|
*
|
|
* tabManager.openFile("/file.js", true, function(err, tab) {
|
|
* var doc = tab.document;
|
|
* console.log("The value is: ", doc.value);
|
|
* });
|
|
*
|
|
* @class tabManager
|
|
* @alternateClassName panes
|
|
* @singleton
|
|
**/
|
|
plugin.freezePublicAPI({
|
|
/**
|
|
* The tab that currently has the focus. Multiple tabs can be
|
|
* active at the same time. However there can only be one tab
|
|
* that has the focus at one time. If there are no tabs this
|
|
* property is null.
|
|
*
|
|
* See also {@link tabManager#focusTab}
|
|
* @property {Tab} focussedTab
|
|
* @readonly
|
|
*/
|
|
get focussedTab(){ return focussedTab || null; },
|
|
/**
|
|
* Specifies whether the tabManager has focus. When a component
|
|
* outside of the editors has focus (such as the tree or a menu
|
|
* item), then this property is set to false. Otherwise it is set
|
|
* to true.
|
|
* @property {Boolean} focussed
|
|
* @readonly
|
|
*/
|
|
get focussed(){ return !unfocussed; },
|
|
/**
|
|
* The tab that is shown as a preview. This property is set to null
|
|
* if there is currently no preview being shown.
|
|
* @property {Tab} previewTab
|
|
* @readonly
|
|
*/
|
|
get previewTab(){ return previewTab; },
|
|
/**
|
|
* An Array of all the HTML Elements that act as containers for
|
|
* panes. Generally this is one for the main panes area and one for
|
|
* the console.
|
|
* @property {HTMLElement[]} containers
|
|
* @readonly
|
|
*/
|
|
get containers(){ return containers; },
|
|
/**
|
|
* The AMLElement for the main panes area.
|
|
* @property {AMLElement} container
|
|
* @readonly
|
|
*/
|
|
get container(){ return container; },
|
|
|
|
_events: [
|
|
/**
|
|
* Fires after a tab received focus. This event is fired
|
|
* when the focussedTab property is set. There is about 500ms
|
|
* delay between the user action and the firing of this event.
|
|
* This delay prevents the UI from getting stuck while plugins
|
|
* update their UIs. If you MUST have a sync event, check out
|
|
* {@link tabManager#focusSync}.
|
|
*
|
|
* @event focus
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that received focus
|
|
*/
|
|
"focus",
|
|
/**
|
|
* Fires after a tab received focus. This event is fired
|
|
* when the focussedTab property is set. Do not use this event
|
|
* unless you ABSOLUTELY have to. Use {@link tabManager#focus}
|
|
* instead.
|
|
*
|
|
* @event focusSync
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that received focus
|
|
*/
|
|
"focusSync",
|
|
/**
|
|
* Fires after a tab lost focus. This event is fired
|
|
* when the focussedTab property is changed to another tab.
|
|
*
|
|
* @event blur
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that lost focus
|
|
* @private
|
|
*/
|
|
"blur",
|
|
/**
|
|
* Fires before a tab is activated. A tab becomes active when
|
|
* it is the selected tab (button) of the pane it belongs to.
|
|
* This is different from receiving the focus. A tab that has
|
|
* focus is always the active tab of it's parent pane. If a pane
|
|
* has at least one tab, it always has an active tab. You can
|
|
* cancel the activation of a tab by returning false in the
|
|
* event listener. This will prevent the tab from becoming
|
|
* active.
|
|
*
|
|
* See also {@link Pane#activeTab}.
|
|
*
|
|
* @event tabBeforeActivate
|
|
* @cancellable
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that will become active
|
|
*/
|
|
"tabBeforeActivate",
|
|
/**
|
|
* Fires after a tab is activated asynchronously. See
|
|
* {@link #tabAfterActivateSync} for the synchronous event.
|
|
* A tab becomes active when
|
|
* it is the selected tab (button) of the pane it belongs to.
|
|
* This is different from receiving the focus. A tab that has
|
|
* focus is always the active tab of it's parent pane. If a pane
|
|
* has at least one tab, it always has an active tab. You can
|
|
* cancel the activation of a tab by returning false in the
|
|
* event listener. This will prevent the tab from becoming
|
|
* active.
|
|
*
|
|
* See also {@link Pane#activeTab}.
|
|
*
|
|
* @event tabAfterActivate
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that has become active
|
|
* @param {Tab} e.lastTab the tab that is no longer active
|
|
*/
|
|
"tabAfterActivate",
|
|
/**
|
|
* Fires after a tab is activated synchronously. See
|
|
* {@link #tabAfterActivate} for the asynchronous event.
|
|
* A tab becomes active when
|
|
* it is the selected tab (button) of the pane it belongs to.
|
|
* This is different from receiving the focus. A tab that has
|
|
* focus is always the active tab of it's parent pane. If a pane
|
|
* has at least one tab, it always has an active tab. You can
|
|
* cancel the activation of a tab by returning false in the
|
|
* event listener. This will prevent the tab from becoming
|
|
* active.
|
|
*
|
|
* See also {@link Pane#activeTab}.
|
|
*
|
|
* @event tabAfterActivateSync
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that has become active
|
|
* @param {Tab} e.lastTab the tab that is no longer active
|
|
*/
|
|
"tabAfterActivateSync",
|
|
/**
|
|
* Fires prior to closing a tab. The default action of closing
|
|
* the tab can be cancelled by returning false.
|
|
*
|
|
* @event tabBeforeClose
|
|
* @cancellable
|
|
* @param {Object} e
|
|
* @param {Object} e.tab the tab that is to be closed
|
|
*/
|
|
"tabBeforeClose",
|
|
/**
|
|
* Fires after a tab is closed, but not yet destroyed. Generally
|
|
* you want to use {@link tabManager#tabDestroy}.
|
|
*
|
|
* @event tabAfterClose
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that is to be closed
|
|
*/
|
|
"tabAfterClose",
|
|
/**
|
|
* Fires after a tab is newly created.
|
|
* @event tabCreate
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that is created
|
|
*/
|
|
"tabCreate",
|
|
/**
|
|
* Fires after a tab is destroyed
|
|
* @event tabDestroy
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that has been destroyed
|
|
* @param {Tab} e.last whether this was the last tab of a pane that was destroyed
|
|
*/
|
|
"tabDestroy",
|
|
/**
|
|
* Fires when a tab is moved within a pane.
|
|
* @event tabOrder
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that has been moved
|
|
* @param {Tab} e.next the tab on the right of e.tab.
|
|
*/
|
|
"tabOrder",
|
|
/**
|
|
* Fires before a tab is moved to a different pane. This usually
|
|
* happens when a pane is split, or when a tab is dragged to a
|
|
* different pane.
|
|
*
|
|
* @event tabBeforeReparent
|
|
* @cancelable
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that is moved.
|
|
* @param {Tab} e.lastPane the previous pane that this tab was part of.
|
|
* @param {Pane} e.pane the pane that the tab is moved to.
|
|
*/
|
|
"tabBeforeReparent",
|
|
/**
|
|
* Fires after a tab is moved to a different pane. This usually
|
|
* happens when a pane is split, or when a tab is dragged to a
|
|
* different pane.
|
|
*
|
|
* @event tabAfterReparent
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab the tab that is moved.
|
|
* @param {Tab} e.lastPane the previous pane that this tab was part of.
|
|
* @param {Pane} e.pane the pane that the tab is moved to.
|
|
*/
|
|
"tabAfterReparent",
|
|
/**
|
|
* Fires after a pane is newly created
|
|
* @event paneCreate
|
|
* @param {Object} e
|
|
* @param {Pane} e.pane the pane that is created
|
|
*/
|
|
"paneCreate",
|
|
/**
|
|
* Fires after a pane is destroyed
|
|
* @event paneDestroy
|
|
* @param {Object} e
|
|
* @param {Pane} e.pane the pane that is destroyed
|
|
*/
|
|
"paneDestroy",
|
|
/**
|
|
* Fires after a tab is opened and before it's contents is
|
|
* loaded. This event can be cancelled by returning false.
|
|
* Cancelling this event will prevent the data from being
|
|
* loaded. It then becomes your reponsibility to load the
|
|
* contents and call e.callback(err, value) when done.
|
|
*
|
|
* This event is primarily used by the {@link metadata} plugin.
|
|
*
|
|
* @event beforeOpen
|
|
* @cancellable
|
|
* @param {Object} e
|
|
* @param {Object} e.options The options object that was passed to the {@link tabManager#method-open} method.
|
|
* @param {Tab} e.tab The created tab that will present the file contents to the user.
|
|
* @param {Boolean} e.loadFromDisk Specifies whether the file contents will be loaded from disk if the event is not cancelled.
|
|
* @param {Function} e.setLoading Call this function to set the tab into a loading state.
|
|
* @param {Tab} e.setLoading.tab Specify the tab to set in a loading state.
|
|
* @param {Function} e.callback Call this function when the content is loaded.
|
|
* @param {Error} e.callback.err Specify an error if loading the contents has failed.
|
|
* @param {String} e.callback.value Specify the value of the content that is loaded.
|
|
*/
|
|
"beforeOpen",
|
|
/**
|
|
* Fires after a tab is opened and it's contents loaded
|
|
* @event open
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab The tab that has been opened
|
|
* @param {Object} e.options The options object that was passed to the {@link tabManager#method-open} method.
|
|
*/
|
|
"open",
|
|
/**
|
|
* Fires after the contents of a tab is reloaded
|
|
* @event reload
|
|
* @param {Object} e
|
|
* @param {Tab} e.tab The tab for which the contents is reloaded
|
|
*/
|
|
"reload",
|
|
/**
|
|
* Fires when the tab manager is ready to open tabs
|
|
* @event ready
|
|
*/
|
|
"ready",
|
|
/**
|
|
* Fires when the user clicked on the + button in pane.
|
|
* @event plusClick
|
|
* @cancellable
|
|
* @param {Object} e
|
|
* @param {Tab} e.pane The pane to which the + button belongs to
|
|
*/
|
|
"plusClick",
|
|
/**
|
|
* Fires after the panels container is drawn
|
|
* @event draw
|
|
*/
|
|
"draw"
|
|
],
|
|
|
|
/**
|
|
* Prevent all pane buttons from resizing for all panes.
|
|
*/
|
|
pauseTabResize: pauseTabResize,
|
|
|
|
/**
|
|
* Re-enable pane button resizing for all panes.
|
|
*/
|
|
continueTabResize: continueTabResize,
|
|
|
|
/**
|
|
* Sets the tab as the active tab of it's pane element
|
|
* @param {Tab} tab The tab to activate
|
|
*/
|
|
activateTab: activateTab,
|
|
|
|
/**
|
|
* Gives the focus to the tab and it's editor and sets the tab as
|
|
* the the active tab of it's pane.
|
|
* @param {Tab} tab The tab to activate
|
|
* @fires focus
|
|
* @fires focusSync
|
|
* @fires blur
|
|
*/
|
|
focusTab: focusTab,
|
|
|
|
/**
|
|
* Retrieves the state of all panes, tabs and documents
|
|
* as a single serializable object.
|
|
* @param {HTMLElement} container One of the items from the
|
|
* {@link tabManager#containers} set, specifying which area it
|
|
* should serialize. Defaults to the container from the panels.
|
|
* @returns {Object}
|
|
*/
|
|
getState: getState,
|
|
|
|
/**
|
|
* Loads the state of all panes panes, tabs anddocuments from a
|
|
* simple object.
|
|
* @param {Object} state The state describing the pane layout.
|
|
* @param {Boolean} [init] When set to true, optimizes the state
|
|
* loading for initialization of Cloud9.
|
|
* @param {Function} callback Called when the state loading is completed.
|
|
*/
|
|
setState: setState,
|
|
|
|
/**
|
|
* Removes all panes, except one, and destroys all tabs, documents
|
|
* and editors.
|
|
*
|
|
* @param {Boolean} [soft=false] When set to true clear
|
|
* will not unload tabs. This can be useful when loading a new
|
|
* state with exactly the same tabs. WARNING: this can lead to
|
|
* leaking tabs/documents, etc. Use with caution! getTabs() will
|
|
* continue to return all the tabs. Even though they are no longer
|
|
* attached to a pane.
|
|
*/
|
|
clear: clear,
|
|
|
|
/**
|
|
* Toggle the tab buttons between visible and hidden state.
|
|
* @param {Number} [force] When set to 1 the buttons become visible,
|
|
* when set to 2 they become hidden.
|
|
*/
|
|
toggleButtons: toggleButtons,
|
|
|
|
/**
|
|
* Force a resize of all the tab buttons in all the panes.
|
|
*/
|
|
resizePanes: resizePanes,
|
|
|
|
/**
|
|
* Retrieves a tab based on a path or it's unique identifier.
|
|
* @param {String} path The path or id of the tab to retrieve.
|
|
*/
|
|
findTab: findTab,
|
|
|
|
/**
|
|
* Returns an array containing all the tabs.
|
|
*/
|
|
getTabs: getTabs,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
checkAllTabs: checkAllTabs,
|
|
|
|
/**
|
|
* Retrieves a pane based on it's unique identifier.
|
|
* @param {String} name The name of the pane to retrieve.
|
|
*/
|
|
findPane: findPane,
|
|
|
|
/**
|
|
* Returns an array containing all the panes (in a container).
|
|
* @param {AMLElement} [container] The container in which the panes
|
|
* reside. If no container is specified all panes in all
|
|
* containers are returned.
|
|
*/
|
|
getPanes: getPanes,
|
|
|
|
/**
|
|
* Opens a new tab with an editor. If the tab with a specified
|
|
* path already exists, that tab is activated.
|
|
* (If no path was specified and option demandExisting is used,
|
|
* we look for a tab with the same editor type and title.)
|
|
* If state is given for a document, then that state is set prior
|
|
* to loading the tab. If a path is specified the file contents is
|
|
* loaded into the document. If no editorType is specified, the
|
|
* editor is determined based on the extension of the filename.
|
|
*
|
|
* @param options
|
|
* @param {String} [options.path] The path of the file to open
|
|
* @param {Pane} [options.pane] The pane to attach the new tab to
|
|
* @param {String} [options.editorType] The type of the editor for this tab
|
|
* @param {Boolean} [options.active=false] Whether this tab is set as active
|
|
* @param {Boolean} [options.focus=false] Whether this tab is set as focussed
|
|
* @param {Boolean} [options.forceNew=false] Always create a tab
|
|
* @param {Boolean} [options.demandExisting=false] Whether to try opening an
|
|
* existing tab even for tabs without a path.
|
|
* @param {Object} [options.document] Object describing the
|
|
* state of the document (see {@link Document#method-getState})
|
|
* @param {String} [options.value] The contents of the file
|
|
* @param {String} [options.title] The title of the tab
|
|
* @param {String} [options.tooltip] The tooltip at the button of the tab
|
|
* @param {Function} callback
|
|
* @param {Error} callback.err An error that might
|
|
* occur during the load of the file contents.
|
|
* @param {Tab} callback.tab The created tab.
|
|
* @param {Function} callback.done Call this function
|
|
* when done retrieving the value. This is only relevant if
|
|
* -1 is passed to `value`. You are responsible for settings the
|
|
* document value yourself, like so: `tab.document.value = "value";`
|
|
* @returns {Tab} The created tab.
|
|
* @fires open
|
|
* @fires beforeOpen
|
|
*/
|
|
open: open,
|
|
|
|
/**
|
|
* Opens a new pane tab with the default editor and loads the file
|
|
* contents into the document. This is a convenience method. For
|
|
* the full method see {@link tabManager#method-open}.
|
|
*
|
|
* @param {String} path The path of the file to open.
|
|
* @param {Boolean} [active] When set to true the new tab will become active in it's pane.
|
|
* @param {Function} callback Called when the file contents is loaded.
|
|
* @param {Error} callback.err An error that might
|
|
* occur during the load of the file contents.
|
|
* @param {Tab} callback.tab The created tab.
|
|
* @returns {Tab} The created tab.
|
|
*/
|
|
openFile: openFile,
|
|
|
|
/**
|
|
* Opens a new tab with a specific editor
|
|
* @param {String} editorType The type of the editor for this tab.
|
|
* @param {Boolean} [active] When set to true the new tab will become active in it's pane.
|
|
* @param {Function} callback Called when the editor is loaded.
|
|
* @param {Error} callback.err An error that might
|
|
* occur during the load of the editor.
|
|
* @param {Tab} callback.tab The created tab.
|
|
* @returns {Tab} The created tab.
|
|
*/
|
|
openEditor: openEditor,
|
|
|
|
/**
|
|
* Reloads the file contents of a tab. The tab needs to have a
|
|
* path property set.
|
|
* @param {Tab} tab The tab to reload.
|
|
*/
|
|
reload: reload,
|
|
|
|
/**
|
|
* Opens a tab in preview mode, displaying the contents of a file with
|
|
* it's editor. The options passed can be the same as for the
|
|
* {@link tabManager#method-open}
|
|
*
|
|
* @param options
|
|
* @param {String} [options.path] The path of the file to open
|
|
* @param {Pane} [options.pane] The pane to attach the new tab to
|
|
* @param {String} [options.editorType] The type of the editor for this tab
|
|
* @param {Boolean} [options.active=false] Whether this tab is set as active
|
|
* @param {Object} [options.document] Object describing the
|
|
* state of the document (see {@link Document#method-getState})
|
|
* @param {String} [options.value] The contents of the file
|
|
* @param {String} [options.title] The title of the tab
|
|
* @param {String} [options.tooltip] The tooltip at the button of the tab
|
|
* @param {Function} callback
|
|
* @param {Error} callback.err An error that might
|
|
* occur during the load of the file contents.
|
|
* @param {Tab} callback.tab The created tab.
|
|
* @param {Function} callback.done Call this function
|
|
* when done retrieving the value. This is only relevant if
|
|
* -1 is passed to `value`. You are responsible for settings the
|
|
* document value yourself, like so: `tab.document.value = "value";`
|
|
* @returns {Tab} The created tab.
|
|
* @fires open
|
|
* @fires beforeOpen
|
|
*/
|
|
preview: preview,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
clone: clone,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
switchEditor: switchEditor,
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
get isReady(){ return isReady; },
|
|
});
|
|
|
|
register(null, {
|
|
tabManager: plugin
|
|
// panes : plugin
|
|
});
|
|
}
|
|
});
|