Pull out tabs package into a separate repo

This commit is contained in:
Kevin Sawicki
2013-08-13 11:01:13 -07:00
parent 63b1546212
commit dc775f93ff
7 changed files with 1 additions and 623 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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())

View File

@@ -1,5 +0,0 @@
TabBarView = require './tab-bar-view'
module.exports =
activate: ->
rootView.eachPane (pane) => new TabBarView(pane)

View File

@@ -1,2 +0,0 @@
'main': './lib/tabs'
'description': 'Display a selectable tab for each editor open.'

View File

@@ -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()

View File

@@ -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;
}