define(function(require, module, exports) { main.consumes = ["Plugin", "ui", "Document", "dialog.alert", "settings"]; main.provides = ["Tab"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var Document = imports.Document; var ui = imports.ui; var alert = imports["dialog.alert"].show; var stylesheet = ui.createStylesheet(); function Tab(options) { var editorType, doc, path, amlPane, init, active, fg, bg, title, tooltip, amlTab, closed, rule, docInited; var meta = {}; var name = options.name; var classList = { names: [], get length(){ return classList.names.length; }, contains: function(name){ return classList.names.indexOf(name) > -1; }, add: function(name) { if (name) { var idx = this.names.indexOf(name); if (idx > -1) return; this.names.push(name); } amlTab && amlTab.setAttribute("class", this.names.join(" ")); }, remove: function(){ for (var i = 0, l = arguments.length; i < l; i++) { var idx = this.names.indexOf(arguments[i]); if (idx > -1) this.names.splice(idx, 1); } amlTab && amlTab.setAttribute("class", this.names.join(" ")); } }; function initStyleSheet(fg, bg) { var cssClass = plugin.name.replace(/[^a-zA-Z0-9\-_\u00A0-\uFFFF]/g, "-"); classList.add(cssClass); rule = "." + cssClass + ".curbtn .tab_middle, ." + cssClass + ".curbtn .tab_middle::after, ." + cssClass + ".curbtn .tab_middle::before"; if (!bg) bg = "inherit"; if (!fg) fg = "inherit"; ( ui.setStyleRule(rule, "background-color", bg, stylesheet) && ui.setStyleRule(rule, "foreground-color", fg, stylesheet) ) || ui.importStylesheet([ [rule, "background-color:" + bg + ";" + "color:" + fg + ";"] ], window, stylesheet); } /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); var loaded; function load(){ if (loaded) return; loaded = true; // Create Tab amlTab = new ui.page({ id: plugin.name, // path || type: "editor::" + editorType, autofocus: false, tooltip: tooltip, caption: title, tab: plugin, focussable: true, $focussable: true }); if (classList.names.length) classList.add(); plugin.addElement(amlTab); // Hack to get activate event in var activate = amlTab.$activate; amlTab.$activate = function(){ activate.apply(amlTab, arguments); emit("activate"); }; var deactivate = amlTab.$deactivate; amlTab.$deactivate = function(){ deactivate.apply(amlTab, arguments); emit("deactivate"); }; // amlTab.$doc = doc; // Backwards compatibility?? amlTab.cloud9tab = plugin; if (amlPane) { if (init) amlPane.setAttribute("buttons", "close"); attachTo(amlPane.cloud9pane, null, options.noanim); if (init) amlPane.setAttribute("buttons", "close,scale,order"); } // Activate tab if necessary if (active) { if (amlPane) amlPane.set(amlTab); // path || else { amlTab.on("DOMNodeInsertedIntoDocument", function insert(e) { amlTab.parentNode.set(amlTab); amlTab.off("DOMNodeInsertedIntoDocument", insert); }); } } init = false; active = false; } /***** Methods *****/ function getState(filter) { var state = { type: "tab", name: plugin.name, path: path, className: filterClassNames(classList.names), document: doc.getState(filter), editorType: editorType, active: isActive() }; emit("getState", { state : state }); return state; } function filterClassNames(classNames) { return classNames.filter(function(n) { return ["error", "dark", "loading"].indexOf(n) === -1; }); } function setState(state) { if (state.pane) amlPane = state.pane.aml; init = state.init; doc = doc ? doc.setState(state.document) : (state.document instanceof Plugin ? state.document : new Document(state.document)); // Connect to the document doc.tab = plugin; active = state.active; if (loaded && active) activate(); plugin.editorType = state.editorType; plugin.path = state.path; plugin.title = doc.title || ""; plugin.tooltip = doc.tooltip; if (state.className) { classList.names = filterClassNames(state.className); if (amlTab) classList.add(); } if (!docInited) { doc.on("setTitle", function(e) { plugin.title = e.title; }, plugin); doc.on("setTooltip", function(e) { plugin.tooltip = e.tooltip; }, plugin); // Changed marker var setChanged = function(e) { if (e.changed || doc.meta.newfile) doc.tab.classList.add("changed"); else doc.tab.classList.remove("changed"); }; doc.on("changed", setChanged, plugin); setChanged({ changed: doc.changed }); docInited = true; } } function isActive(){ return amlPane ? amlPane.getPage() == amlTab : options.active; } function activate(){ amlPane.set(amlTab); } function beforeClose(e) { return emit("beforeClose", e); } function attachTo(t, nextSibling, noAnim) { if (t.aml.localName != "tab") throw new Error("Incorrect Element: " + t.aml.localName); amlPane = t.aml; var lastPane = amlTab.parentNode; if (emit("beforeReparent", { lastPane: lastPane && lastPane.cloud9pane, pane: t }) === false) return; if (lastPane != amlPane) amlPane.skipAnimOnce = noAnim; amlPane.insertBefore(amlTab, nextSibling && nextSibling.aml); apf.setStyleClass(amlPane.$ext, "", ["empty"]); if (lastPane && !lastPane.childNodes.length) apf.setStyleClass(lastPane.$ext, "empty"); emit("afterReparent", { lastPane: lastPane && lastPane.cloud9pane, pane: t }); } function switchEditor(type, callback) { if (editorType == type) return; // var lastType = tab.editorType; amlPane.cloud9pane.createEditor(type, function(err, editor) { var info = {}; if (editor.isValid(amlTab.cloud9tab.document, info) === false) { alert( info.title || "Could not switch editor", info.head || "Could not switch editor because this document is invalid.", info.message || "Please fix the error and try again." ); return; } var currentValue = plugin.document.value; editorType = type; amlTab.setAttribute("type", "editor::" + type); if (amlPane.getPage() == amlTab) { amlPane.activepage = -1; amlPane.set(amlTab); plugin.document.value = currentValue; // TODO undo managers for different editors conflict // however, resetting removes changed state // plugin.document.undoManager.reset(); } callback(); }); } // @todo Explain difference with unload in docs function close(noAnim) { if (!amlPane.remove) return false; amlPane.remove(amlTab, null, noAnim); return true; } /***** Lifecycle *****/ plugin.on("load", function(){ load(); }); plugin.on("beforeUnload", function(){ if (!plugin.meta.$closing) { if (close()) return false; } }); plugin.on("unload", function(e) { closed = true; if (rule) ui.removeStyleRule(rule, stylesheet); // If there are no more pages left, reset location var last = e && e.last || amlPane.getPages().length === 0; if (last) apf.setStyleClass(amlPane.$ext, "empty"); loaded = false; emit("close", {last: last, htmlEvent: e && e.htmlEvent}); }); /***** Register and define API *****/ /** * Tab Class for Cloud9 Panes. Each file that is opened * in an editor has a tab object that allows a user to activate the * document in an editor in a pane. Tabs can be moved between * panes using drag&drop and key bindings. * * The tab relates to other objects as such: * * * {@link Pane} - Represent a single pane, housing multiple tabs * * **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 * * Tabs are managed by the {@link tabManager}. The default way to * open a new file in an editor uses the tabManager: * * tabManager.openFile("/file.js", true, function(err, tab) { * console.log("The tab title is: ", tab.title); * }); */ plugin.freezePublicAPI({ /** * The APF UI element that is presenting the tab in the UI. * This property is here for internal reasons only. *Do not * depend on this property in your plugin.* * @property {AMLElement} aml * @private * @readonly */ get aml(){ return amlTab; }, /** * The pane that this tab belongs to. This property changes when * a tab is moved to another pane. * @property {Pane} pane * @readonly */ get pane(){ return amlPane.cloud9pane; }, /** * The document loaded in this tab. This property is always the * same object. * @property {Document} document * @readonly */ get document(){ return doc; }, /** * Retrieves the meta object for this panel * @property {Object} meta */ get meta(){ return meta; }, /** * The path to the file loaded into this tab. This property will * be undefined when no path is set (for instance when no file * was loaded to create this tab). * @property {String} path */ get path(){ return path || undefined; }, set path(v) { var oldpath = path; path = v; emit("setPath", {path: path, oldpath: oldpath}); }, get relatedPath(){ return (this.editor || 1).relatedPath; }, /** * The type of the editor shown in this tab. * * See also {@link Editor#type} * * @property {String} editorType */ get editorType(){ return editorType; }, set editorType(type) { editorType = type; amlTab && amlTab.setProperty("type", "editor::" + type); }, /** * The title (or caption) of the tab button * @property {String} title */ get title(){ return title; }, set title(value) { title = value; amlTab && amlTab.setProperty("caption", value); }, /** * The value of this property is displayed when hovering the * mouse over the tab button. * @property {String} tooltip **/ get tooltip(){ return tooltip; }, set tooltip(value) { tooltip = value; amlTab && amlTab.setProperty("tooltip", value); }, /** * The background color of the tab button and it's body. It is * recommended to use the "rgb(r,g,b)" format. To get a light * text color use tab.classList.add("dark") to specify a dark * backgroundColor is used. * @property {String} backgroundColor */ get backgroundColor(){ return bg }, set backgroundColor(v) { bg = v; if (!rule) return initStyleSheet(fg, bg); ui.setStyleRule(rule, "background-color", bg, stylesheet); }, /** * The foreground color of the tab button and it's body. It is * recommended to use the "rgb(r,g,b)" format. In most cases * this property doesn't have to be set. To get a light text * color use tab.classList.add("dark") to specify a dark * backgroundColor is used. * @property {String} backgroundColor */ get foregroundColor(){ return bg }, set foregroundColor(v) { fg = v; if (!rule) return initStyleSheet(fg, bg); ui.setStyleRule(rule, "color", fg, stylesheet); }, /** * @property {Object} classList Object that * manages the class names of the tab button. Often used * class names are "loading", "saving", "error". * @property {Function} classList.add Adds a new class name to the tab button. * @property {String} classList.add.name The name of the class to add. * @property {Function} classList.remove Removes a class name from the tab button. * @property {String} classList.remove.name The name of the class to remove. * @readonly */ get classList(){ return classList; }, /** * Specifies whether this tab is the active tab within the panel * @property {Boolean} active * @readonly */ get active(){ return amlPane.getPage() == amlTab; }, /** * Specifies the editor that is displayed by this tab. * * Note that this property changes when the tab is moved to a * different pane. An editor is connected to a pane and thus * when this tab is moved to a pane this property will be set to * the editor of that pane. * * @property {Editor} editor * @readonly */ get editor(){ if (amlPane.$amlDestroyed) return false; var editorTab = amlPane.getPage("editor::" + editorType); // During destroy of pane, a tab that used to have an editor // can find itself without one. There is only one way to // detect that, which is when this getter returns false if (!editorTab) return false; return editorTab.editor; }, _events: [ /** * Fires before the tab is closed * @event beforeClose * @cancellable */ "beforeClose", /** * Fires when this tab becomes the active tab of it's pane parent. * * Note that this event is fired early, often before the editor is initialized. * For most cases you want to use the {@link tabManager#tabAfterActivate} or * {@link tabManager#tabAfterActivateSync} events on the {@link tabManager} plugin. * * @event activate */ "activate", /** * Fires when the state of this tab is retrieved * @event getState * @param {Object} e * @param {Object} e.state The state of the tab, it's document and underlying objects. */ "getState", /** * Fires when the path of this tab is updated. This happens * when the {@link Tab#path} property is set (i.e. * when the file that has opened this tab is renamed). * @event setPath * @param {Object} e * @param {String} e.path The new path for this tab * @param {String} e.oldpath The path that this tab had prior to setting the new path */ "setPath", /** * Fires prior to attaching this tab to a pane * @event beforeReparent * @cancelable * @param {Object} e * @param {Pane} e.lastPane The previous pane that this tab was part of * @param {Pane} e.pane The pane this tab is moved to */ "beforeReparent", /** * Fires prior to attaching this tab to a pane * @event afterReparent * @param {Object} e * @param {Pane} e.lastPane The previous pane that this tab was part of * @param {Pane} e.pane The pane this tab is moved to */ "afterReparent", /** * @event close Fires when this tab closes * @param {Object} e * @param {Boolean} e.last Specifies whether this tab was * the last tab of the pane to be closed * (e.g. the pane has no tabs left). */ "close" ], /** * Sets this tab as the active tab on it's pane */ activate: activate, /** * Checks whether this tab is actve in it's pane * @return {Boolean} */ isActive: isActive, /** * Retrieves the state of the tab. * * @return {Object} * @return {String} [return.type="tab"] This is always the string "tab" * @return {String} return.name The {@link Plugin#name} name of the tab plugin * @return {String} return.path The path of this tab * @return {Object} return.document The returned object of {@link Document#getState}. * @return {String} return.editorType The {@link Editor#type} of the editor * @return {Boolean} return.active Specifies whether the tab was active when the state was retrieved. */ getState: getState, /** * Moves this tab to a pane * @param {Pane} pane The pane to move this tab to. * @param {Tab} [nextSibling] The tab that will be on the right of this tab after it has been inserted. */ attachTo: attachTo, /** * Changes the editor that is used to display the content of * the document of this tab to another editor. This can be * useful when multiple editors can display the same content * (i.e. displaying an image in a hex editor). * * @param {String} editorType Specifies the {@link Editor#type} of the editor to switch to * @param {Function} callback */ switchEditor: switchEditor, /** * Closes this tab. Closing a tab will destroy all it's content * and state. */ close: close, /** * @ignore */ beforeClose: beforeClose }); if (options) setState(options); plugin.load(name, "tab"); return plugin; } /***** Register and define API *****/ register(null, { Tab: Tab }); } });