diff --git a/package.json b/package.json index 9651deca2..59189f2af 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "package-generator": "0.1.0", "spell-check": "0.1.0", "symbols-view": "0.1.0", + "tabs": "0.1.0", "terminal": "0.3.0", "toml": "0.1.0", "whitespace": "0.1.0", diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee deleted file mode 100644 index 427cfd288..000000000 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ /dev/null @@ -1,160 +0,0 @@ -$ = require 'jquery' -{View} = require 'space-pen' -_ = require 'underscore' -TabView = require './tab-view' - -module.exports = -class TabBarView extends View - @content: -> - @ul tabindex: -1, class: "list-inline tabs" - - initialize: (@pane) -> - @on 'dragstart', '.sortable', @onDragStart - @on 'dragend', '.sortable', @onDragEnd - @on 'dragover', @onDragOver - @on 'drop', @onDrop - - @paneContainer = @pane.getContainer() - @addTabForItem(item) for item in @pane.getItems() - - @pane.on 'pane:item-added', (e, item, index) => @addTabForItem(item, index) - @pane.on 'pane:item-moved', (e, item, index) => @moveItemTabToIndex(item, index) - @pane.on 'pane:item-removed', (e, item) => @removeTabForItem(item) - @pane.on 'pane:active-item-changed', => @updateActiveTab() - - @updateActiveTab() - - @on 'click', '.tab', (e) => - tab = $(e.target).closest('.tab').view() - @pane.showItem(tab.item) - @pane.focus() - - @on 'click', '.tab .close-icon', (e) => - tab = $(e.target).closest('.tab').view() - @pane.destroyItem(tab.item) - false - - @pane.prepend(this) - - addTabForItem: (item, index) -> - @insertTabAtIndex(new TabView(item, @pane), index) - - moveItemTabToIndex: (item, index) -> - tab = @tabForItem(item) - tab.detach() - @insertTabAtIndex(tab, index) - - insertTabAtIndex: (tab, index) -> - followingTab = @tabAtIndex(index) if index? - if followingTab - tab.insertBefore(followingTab) - else - @append(tab) - tab.updateTitle() - - removeTabForItem: (item) -> - @tabForItem(item).remove() - tab.updateTitle() for tab in @getTabs() - - getTabs: -> - @children('.tab').toArray().map (elt) -> $(elt).view() - - tabAtIndex: (index) -> - @children(".tab:eq(#{index})").view() - - tabForItem: (item) -> - _.detect @getTabs(), (tab) -> tab.item is item - - setActiveTab: (tabView) -> - unless tabView.hasClass('active') - @find(".tab.active").removeClass('active') - tabView.addClass('active') - - updateActiveTab: -> - @setActiveTab(@tabForItem(@pane.activeItem)) - - shouldAllowDrag: -> - (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1) - - onDragStart: (event) => - if @shouldAllowDrag() - event.originalEvent.dataTransfer.setData 'atom-event', 'true' - - el = $(event.target).closest('.sortable') - el.addClass 'is-dragging' - event.originalEvent.dataTransfer.setData 'sortable-index', el.index() - - pane = $(event.target).closest('.pane') - paneIndex = @paneContainer.indexOfPane(pane) - event.originalEvent.dataTransfer.setData 'from-pane-index', paneIndex - - item = @pane.getItems()[el.index()] - if item.getPath? - event.originalEvent.dataTransfer.setData 'text/uri-list', 'file://' + item.getPath() - event.originalEvent.dataTransfer.setData 'text/plain', item.getPath() - - onDragEnd: (event) => - @find(".is-dragging").removeClass 'is-dragging' - @removeDropTargetClasses() - - onDragOver: (event) => - unless event.originalEvent.dataTransfer.getData('atom-event') is 'true' - event.preventDefault() - event.stopPropagation() - return - - @removeDropTargetClasses() - - event.preventDefault() - newDropTargetIndex = @getDropTargetIndex(event) - - sortableObjects = @find(".sortable") - if newDropTargetIndex < sortableObjects.length - sortableObjects.eq(newDropTargetIndex).addClass 'is-drop-target' - else - sortableObjects.eq(newDropTargetIndex - 1).addClass 'drop-target-is-after' - - onDrop: (event) => - unless event.originalEvent.dataTransfer.getData('atom-event') is 'true' - event.preventDefault() - event.stopPropagation() - return - - @find(".is-dragging").removeClass 'is-dragging' - @removeDropTargetClasses() - - event.stopPropagation() - - dataTransfer = event.originalEvent.dataTransfer - fromIndex = parseInt(dataTransfer.getData('sortable-index')) - fromPaneIndex = parseInt(dataTransfer.getData('from-pane-index')) - fromPane = @paneContainer.paneAtIndex(fromPaneIndex) - toIndex = @getDropTargetIndex(event) - toPane = $(event.target).closest('.pane').view() - draggedTab = fromPane.find(".tabs .sortable:eq(#{fromIndex})").view() - item = draggedTab.item - - if toPane is fromPane - toIndex-- if fromIndex < toIndex - toPane.moveItem(item, toIndex) - else - fromPane.moveItemToPane(item, toPane, toIndex--) - toPane.showItem(item) - toPane.focus() - - removeDropTargetClasses: -> - rootView.find('.tabs .is-drop-target').removeClass 'is-drop-target' - rootView.find('.tabs .drop-target-is-after').removeClass 'drop-target-is-after' - - getDropTargetIndex: (event) -> - el = $(event.target).closest('.sortable') - el = $(event.target).find('.sortable').last() if el.length == 0 - - elementCenter = el.offset().left + el.width() / 2 - - if event.originalEvent.pageX < elementCenter - el.index() - else if el.next().length > 0 - el.next().index() - else - el.index() + 1 diff --git a/src/packages/tabs/lib/tab-view.coffee b/src/packages/tabs/lib/tab-view.coffee deleted file mode 100644 index 08d7cb8dc..000000000 --- a/src/packages/tabs/lib/tab-view.coffee +++ /dev/null @@ -1,56 +0,0 @@ -$ = require 'jquery' -{View} = require 'space-pen' -fsUtils = require 'fs-utils' -path = require 'path' - -module.exports = -class TabView extends View - @content: -> - @li class: 'tab sortable', => - @div class: 'title', outlet: 'title' - @div class: 'close-icon' - - initialize: (@item, @pane) -> - @item.on? 'title-changed', => @updateTitle() - @item.on? 'modified-status-changed', => @updateModifiedStatus() - @updateTitle() - @updateModifiedStatus() - - updateTitle: -> - return if @updatingTitle - @updatingTitle = true - - title = @item.getTitle() - useLongTitle = false - for tab in @getSiblingTabs() - if tab.item.getTitle() is title - tab.updateTitle() - useLongTitle = true - title = @item.getLongTitle?() ? title if useLongTitle - - @title.text(title) - @updatingTitle = false - - getSiblingTabs: -> - @siblings('.tab').views() - - updateModifiedStatus: -> - if @item.isModified?() - @addClass('modified') unless @isModified - @isModified = true - else - @removeClass('modified') if @isModified - @isModified = false - - updateFileName: -> - fileNameText = @editSession.buffer.getBaseName() - if fileNameText? - duplicates = @editor.getEditSessions().filter (session) -> fileNameText is session.buffer.getBaseName() - if duplicates.length > 1 - directory = path.basename(path.dirname(@editSession.getPath())) - fileNameText = "#{fileNameText} - #{directory}" if directory - else - fileNameText = 'untitled' - - @fileName.text(fileNameText) - @fileName.attr('title', @editSession.getPath()) diff --git a/src/packages/tabs/lib/tabs.coffee b/src/packages/tabs/lib/tabs.coffee deleted file mode 100644 index ba2da6b3a..000000000 --- a/src/packages/tabs/lib/tabs.coffee +++ /dev/null @@ -1,5 +0,0 @@ -TabBarView = require './tab-bar-view' - -module.exports = - activate: -> - rootView.eachPane (pane) => new TabBarView(pane) diff --git a/src/packages/tabs/package.cson b/src/packages/tabs/package.cson deleted file mode 100644 index 130cdc1b8..000000000 --- a/src/packages/tabs/package.cson +++ /dev/null @@ -1,2 +0,0 @@ -'main': './lib/tabs' -'description': 'Display a selectable tab for each editor open.' diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee deleted file mode 100644 index 9259d363b..000000000 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ /dev/null @@ -1,304 +0,0 @@ -$ = require 'jquery' -_ = require 'underscore' -RootView = require 'root-view' -Pane = require 'pane' -PaneContainer = require 'pane-container' -TabBarView = require 'tabs/lib/tab-bar-view' -{View} = require 'space-pen' - -describe "Tabs package main", -> - beforeEach -> - window.rootView = new RootView - rootView.open('sample.js') - atom.activatePackage("tabs") - - describe ".activate()", -> - it "appends a tab bar all existing and new panes", -> - expect(rootView.panes.find('.pane').length).toBe 1 - expect(rootView.panes.find('.pane > .tabs').length).toBe 1 - rootView.getActivePane().splitRight() - expect(rootView.find('.pane').length).toBe 2 - expect(rootView.panes.find('.pane > .tabs').length).toBe 2 - -describe "TabBarView", -> - [item1, item2, editSession1, pane, tabBar] = [] - - class TestView extends View - @deserialize: ({title, longTitle}) -> new TestView(title, longTitle) - @content: (title) -> @div title - initialize: (@title, @longTitle) -> - getTitle: -> @title - getLongTitle: -> @longTitle - serialize: -> { deserializer: 'TestView', @title, @longTitle } - - beforeEach -> - window.rootView = new RootView - registerDeserializer(TestView) - item1 = new TestView('Item 1') - item2 = new TestView('Item 2') - editSession1 = project.open('sample.js') - paneContainer = new PaneContainer - pane = new Pane(item1, editSession1, item2) - pane.showItem(item2) - paneContainer.append(pane) - tabBar = new TabBarView(pane) - - afterEach -> - unregisterDeserializer(TestView) - - describe ".initialize(pane)", -> - it "creates a tab for each item on the tab bar's parent pane", -> - expect(pane.getItems().length).toBe 3 - expect(tabBar.find('.tab').length).toBe 3 - - expect(tabBar.find('.tab:eq(0) .title').text()).toBe item1.getTitle() - expect(tabBar.find('.tab:eq(1) .title').text()).toBe editSession1.getTitle() - expect(tabBar.find('.tab:eq(2) .title').text()).toBe item2.getTitle() - - it "highlights the tab for the active pane item", -> - expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active' - - describe "when the active pane item changes", -> - it "highlights the tab for the new active pane item", -> - pane.showItem(item1) - expect(tabBar.find('.active').length).toBe 1 - expect(tabBar.find('.tab:eq(0)')).toHaveClass 'active' - - pane.showItem(item2) - expect(tabBar.find('.active').length).toBe 1 - expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active' - - describe "when a new item is added to the pane", -> - it "adds a tab for the new item at the same index as the item in the pane", -> - pane.showItem(item1) - item3 = new TestView('Item 3') - pane.showItem(item3) - expect(tabBar.find('.tab').length).toBe 4 - expect(tabBar.tabAtIndex(1).find('.title')).toHaveText 'Item 3' - - it "adds the 'modified' class to the new tab if the item is initially modified", -> - editSession2 = project.open('sample.txt') - editSession2.insertText('x') - pane.showItem(editSession2) - expect(tabBar.tabForItem(editSession2)).toHaveClass 'modified' - - describe "when an item is removed from the pane", -> - it "removes the item's tab from the tab bar", -> - pane.removeItem(item2) - expect(tabBar.getTabs().length).toBe 2 - expect(tabBar.find('.tab:contains(Item 2)')).not.toExist() - - it "updates the titles of the remaining tabs", -> - expect(tabBar.tabForItem(item2)).toHaveText 'Item 2' - item2.longTitle = '2' - item2a = new TestView('Item 2') - item2a.longTitle = '2a' - pane.showItem(item2a) - expect(tabBar.tabForItem(item2)).toHaveText '2' - expect(tabBar.tabForItem(item2a)).toHaveText '2a' - pane.removeItem(item2a) - expect(tabBar.tabForItem(item2)).toHaveText 'Item 2' - - describe "when a tab is clicked", -> - it "shows the associated item on the pane and focuses the pane", -> - spyOn(pane, 'focus') - - tabBar.tabAtIndex(0).click() - expect(pane.activeItem).toBe pane.getItems()[0] - - tabBar.tabAtIndex(2).click() - expect(pane.activeItem).toBe pane.getItems()[2] - - expect(pane.focus.callCount).toBe 2 - - describe "when a tab's close icon is clicked", -> - it "destroys the tab's item on the pane", -> - tabBar.tabForItem(editSession1).find('.close-icon').click() - expect(pane.getItems().length).toBe 2 - expect(pane.getItems().indexOf(editSession1)).toBe -1 - expect(editSession1.destroyed).toBeTruthy() - expect(tabBar.getTabs().length).toBe 2 - expect(tabBar.find('.tab:contains(sample.js)')).not.toExist() - - describe "when a tab item's title changes", -> - it "updates the title of the item's tab", -> - editSession1.buffer.setPath('/this/is-a/test.txt') - expect(tabBar.tabForItem(editSession1)).toHaveText 'test.txt' - - describe "when two tabs have the same title", -> - it "displays the long title on the tab if it's available from the item", -> - item1.title = "Old Man" - item1.longTitle = "Grumpy Old Man" - item1.trigger 'title-changed' - item2.title = "Old Man" - item2.longTitle = "Jolly Old Man" - item2.trigger 'title-changed' - - expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" - expect(tabBar.tabForItem(item2)).toHaveText "Jolly Old Man" - - item2.longTitle = undefined - item2.trigger 'title-changed' - - expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" - expect(tabBar.tabForItem(item2)).toHaveText "Old Man" - - describe "when a tab item's modified status changes", -> - it "adds or removes the 'modified' class to the tab based on the status", -> - tab = tabBar.tabForItem(editSession1) - expect(editSession1.isModified()).toBeFalsy() - expect(tab).not.toHaveClass 'modified' - - editSession1.insertText('x') - advanceClock(editSession1.buffer.stoppedChangingDelay) - expect(editSession1.isModified()).toBeTruthy() - expect(tab).toHaveClass 'modified' - - editSession1.undo() - advanceClock(editSession1.buffer.stoppedChangingDelay) - expect(editSession1.isModified()).toBeFalsy() - expect(tab).not.toHaveClass 'modified' - - describe "when a pane item moves to a new index", -> - it "updates the order of the tabs to match the new item order", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - pane.moveItem(item2, 1) - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "Item 2", "sample.js"] - pane.moveItem(editSession1, 0) - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 1", "Item 2"] - pane.moveItem(item1, 2) - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 2", "Item 1"] - - describe "dragging and dropping tabs", -> - buildDragEvents = (dragged, dropTarget) -> - dataTransfer = - data: {} - setData: (key, value) -> @data[key] = value - getData: (key) -> @data[key] - - dragStartEvent = $.Event() - dragStartEvent.target = dragged[0] - dragStartEvent.originalEvent = { dataTransfer } - - dropEvent = $.Event() - dropEvent.target = dropTarget[0] - dropEvent.originalEvent = { dataTransfer } - - [dragStartEvent, dropEvent] - - describe "when a tab is dragged within the same pane", -> - describe "when it is dropped on tab that's later in the list", -> - it "moves the tab and its item, shows the tab's item, and focuses the pane", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - spyOn(pane, 'focus') - - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(1)) - tabBar.onDragStart(dragStartEvent) - tabBar.onDrop(dropEvent) - - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 1", "Item 2"] - expect(pane.getItems()).toEqual [editSession1, item1, item2] - expect(pane.activeItem).toBe item1 - expect(pane.focus).toHaveBeenCalled() - - describe "when it is dropped on a tab that's earlier in the list", -> - it "moves the tab and its item, shows the tab's item, and focuses the pane", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - spyOn(pane, 'focus') - - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(2), tabBar.tabAtIndex(0)) - tabBar.onDragStart(dragStartEvent) - tabBar.onDrop(dropEvent) - - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "Item 2", "sample.js"] - expect(pane.getItems()).toEqual [item1, item2, editSession1] - expect(pane.activeItem).toBe item2 - expect(pane.focus).toHaveBeenCalled() - - describe "when it is dropped on itself", -> - it "doesn't move the tab or item, but does make it the active item and focuses the pane", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - spyOn(pane, 'focus') - - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(0)) - tabBar.onDragStart(dragStartEvent) - tabBar.onDrop(dropEvent) - - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item1 - expect(pane.focus).toHaveBeenCalled() - - describe "when it is dropped on the tab bar", -> - it "moves the tab and its item to the end", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - spyOn(pane, 'focus') - - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar) - tabBar.onDragStart(dragStartEvent) - tabBar.onDrop(dropEvent) - - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 2", "Item 1"] - expect(pane.getItems()).toEqual [editSession1, item2, item1] - - describe "when a tab is dragged to a different pane", -> - [pane2, tabBar2, item2b] = [] - - beforeEach -> - pane2 = pane.splitRight() - [item2b] = pane2.getItems() - tabBar2 = new TabBarView(pane2) - - it "removes the tab and item from their original pane and moves them to the target pane", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - - expect(tabBar2.getTabs().map (tab) -> tab.text()).toEqual ["Item 2"] - expect(pane2.getItems()).toEqual [item2b] - expect(pane2.activeItem).toBe item2b - spyOn(pane2, 'focus') - - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar2.tabAtIndex(0)) - tabBar.onDragStart(dragStartEvent) - tabBar.onDrop(dropEvent) - - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 2"] - expect(pane.getItems()).toEqual [editSession1, item2] - expect(pane.activeItem).toBe item2 - - expect(tabBar2.getTabs().map (tab) -> tab.text()).toEqual ["Item 2", "Item 1"] - expect(pane2.getItems()).toEqual [item2b, item1] - expect(pane2.activeItem).toBe item1 - expect(pane2.focus).toHaveBeenCalled() - - describe "when a non-tab is dragged to pane", -> - it "has no effect", -> - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - spyOn(pane, 'focus') - - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(0)) - tabBar.onDrop(dropEvent) - - expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] - expect(pane.getItems()).toEqual [item1, editSession1, item2] - expect(pane.activeItem).toBe item2 - expect(pane.focus).not.toHaveBeenCalled() - - describe "when a tab is dragged out of application", -> - it "should carry file's information", -> - [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(1), tabBar.tabAtIndex(1)) - tabBar.onDragStart(dragStartEvent) - - expect(dragStartEvent.originalEvent.dataTransfer.getData("text/plain")).toEqual editSession1.getPath() - expect(dragStartEvent.originalEvent.dataTransfer.getData("text/uri-list")).toEqual 'file://' + editSession1.getPath() diff --git a/src/packages/tabs/stylesheets/tabs.less b/src/packages/tabs/stylesheets/tabs.less deleted file mode 100644 index da63921ec..000000000 --- a/src/packages/tabs/stylesheets/tabs.less +++ /dev/null @@ -1,96 +0,0 @@ -@import "bootstrap/less/variables.less"; -@import "octicon-mixins.less"; - -@close-icon-size: 11px; - -.tabs { - display: -webkit-flex; - -webkit-user-select: none; - margin: 0; - - .tab { - line-height: 30px; - font-size: 11px; - position: relative; - padding-left: 10px; - padding-right: 10px + @close-icon-size + 2px; - -webkit-user-drag: element; - -webkit-flex: 1; - max-width: 175px; - min-width: 40px; - - &.active { - -webkit-flex: 2; - } - - .title { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .close-icon { - .octicon(x, @close-icon-size); - position: absolute; - top: 0; - right: 10px; - cursor: pointer; - } - - &.modified:hover .close-icon { - color: #66a6ff; - } - - &.modified:not(:hover) .close-icon { - &:before { content: "" } - top: 11px; - right: 11px; - width: 8px; - height: 8px; - border: 2px solid #66a6ff; - border-radius: 12px; - } - } -} - -/* Drag and Drop */ - -.tab.is-drop-target:before, .tab.drop-target-is-after:after { - content: ""; - position: absolute; - top: 0; - z-index: 999; - display: inline-block; - width: 2px; - height: 30px; - display: inline-block; - background: #0098ff; -} - -.tab.is-drop-target:after, .tab.drop-target-is-after:before { - content: ""; - position: absolute; - top: 30px; - z-index: 9999; - width: 4px; - height: 4px; - background: #0098ff; - border-radius: 4px; - border: 1px solid transparent; -} - -.tab.is-drop-target:before { - left: -2px; -} - -.tab.is-drop-target:after { - left: -4px; -} - -.tab.drop-target-is-after:before { - right: -4px; -} - -.tab.drop-target-is-after:after { - right: -2px; -}