WorkingSetView generates the UI for the list of the files user is editing based on the model provided by EditorManager. The UI allows the user to see what files are open/dirty and allows them to close files and specify the current editor.
Constants for event.which values
var LEFT_BUTTON = 1,
MIDDLE_BUTTON = 2;
Constants for hitTest.where
var NOMANSLAND = "nomansland",
NOMOVEITEM = "nomoveitem",
ABOVEITEM = "aboveitem",
BELOWITEM = "belowitem",
TOPSCROLL = "topscroll",
BOTSCROLL = "bottomscroll",
BELOWVIEW = "belowview",
ABOVEVIEW = "aboveview";
Drag an item has to move 3px before dragging starts
var _DRAG_MOVE_DETECTION_START = 3;
Each list item in the working set stores a references to the related document in the list item's data.
Use listItem.data(_FILE_KEY)
to get the document reference
var _FILE_KEY = "file";
Deactivates all views so the selection marker does not show
function _deactivateAllViews(deactivate) {
_.forEach(_views, function (view) {
if (deactivate) {
if (view.$el.hasClass("active")) {
view.$el.removeClass("active").addClass("reactivate");
view.$openFilesList.trigger("selectionHide");
}
} else {
if (view.$el.hasClass("reactivate")) {
view.$el.removeClass("reactivate").addClass("active");
}
// don't update the scroll pos
view._fireSelectionChanged(false);
}
});
}
Determines if a file is dirty
function _isOpenAndDirty(file) {
// working set item might never have been opened; if so, then it's definitely not dirty
var docIfOpen = DocumentManager.getOpenDocumentForPath(file.fullPath);
return (docIfOpen && docIfOpen.isDirty);
}
function _hasSelectionFocus() {
return FileViewController.getFileSelectionFocus() === FileViewController.WORKING_SET_VIEW;
}
Makes the specified element draggable
function _makeDraggable($el) {
var interval,
sourceFile = $el.data(_FILE_KEY);
// turn off the "hover-scroll"
function endScroll($el) {
if (interval) {
window.clearInterval(interval);
interval = undefined;
}
}
// We scroll the list while hovering over the first or last visible list element
// in the working set, so that positioning a working set item before or after one
// that has been scrolled out of view can be performed.
//
// This function will call the drag interface repeatedly on an interval to allow
// the item to be dragged while scrolling the list until the mouse is moved off
// the first or last item or endScroll is called
function scroll($container, $el, dir, callback) {
var container = $container[0],
maxScroll = container.scrollHeight - container.clientHeight;
if (maxScroll && dir && !interval) {
// Scroll view if the mouse is over the first or last pixels of the container
interval = window.setInterval(function () {
var scrollTop = $container.scrollTop();
if ((dir === -1 && scrollTop <= 0) || (dir === 1 && scrollTop >= maxScroll)) {
endScroll($el);
} else {
$container.scrollTop(scrollTop + 7 * dir);
callback($el);
}
}, 50);
}
}
// The mouse down handler pretty much handles everything
$el.mousedown(function (e) {
var scrollDir = 0,
dragged = false,
startPageY = e.pageY,
lastPageY = startPageY,
lastHit = { where: NOMANSLAND },
tryClosing = $(e.target).hasClass("can-close"),
currentFile = MainViewManager.getCurrentlyViewedFile(),
activePaneId = MainViewManager.getActivePaneId(),
activeView = _views[activePaneId],
sourceView = _viewFromEl($el),
currentView = sourceView,
startingIndex = $el.index(),
itemHeight,
offset,
$copy,
$ghost,
draggingCurrentFile;
function initDragging() {
itemHeight = $el.height();
offset = $el.offset();
$copy = $el.clone();
$ghost = $("<div class='open-files-container wsv-drag-ghost' style='overflow: hidden; display: inline-block;'>").append($("<ul>").append($copy).css("padding", "0"));
draggingCurrentFile = ($el.hasClass("selected") && sourceView.paneId === activePaneId);
// setup our ghost element as position absolute
// so we can put it wherever we want to while dragging
if (draggingCurrentFile && _hasSelectionFocus()) {
$ghost.addClass("dragging-current-file");
}
$ghost.css({
top: offset.top,
left: offset.left,
width: $el.width() + 8
});
// this will give the element the appearence that it's ghosted if the user
// drags the element out of the view and goes off into no mans land
$ghost.appendTo($("body"));
}
// Switches the view context to match the hit context
function updateContext(hit) {
// just set the container and update
currentView = _viewFromEl(hit.which);
}
// Determines where the mouse hit was
function hitTest(e) {
var pageY = $ghost.offset().top,
direction = e.pageY - lastPageY,
result = {
where: NOMANSLAND
},
lookCount = 0,
hasScroller = false,
onTopScroller = false,
onBottomScroller = false,
$container,
$hit,
$item,
$view,
gTop,
gHeight,
gBottom,
containerOffset,
scrollerTopArea,
scrollerBottomArea;
// if the mouse is outside of the view then
// return nomansland -- this prevents some UI glitches
// that appear when dragging onto a second monitor
if (e.pageX < 0 || e.pageX > $workingFilesContainer.width()) {
return result;
}
do {
// Turn off the ghost so elementFromPoint ignores it
$ghost.hide();
$hit = $(window.document.elementFromPoint(e.pageX, pageY));
$view = $hit.closest(".working-set-view");
$item = $hit.closest("#working-set-list-container li");
// Show the ghost again
$ghost.show();
$container = $view.children(".open-files-container");
if ($container.length) {
containerOffset = $container.offset();
// Compute "scrollMe" regions
scrollerTopArea = { top: containerOffset.top - 14,
bottom: containerOffset.top + 7};
scrollerBottomArea = { top: containerOffset.top + $container.height() - 7,
bottom: containerOffset.top + $container.height() + 14};
}
// If we hit ourself then look for another
// element to insert before/after
if ($item[0] === $el[0]) {
if (direction > 0) {
$item = $item.next();
if ($item.length) {
pageY += itemHeight;
}
} else {
$item = $item.prev();
if ($item.length) {
pageY -= itemHeight;
}
}
}
// If we didn't hit anything then
// back up and try again in the other direction
if (!$item.length) {
pageY += itemHeight;
}
// look one more time below the mouse
// if we didn't get a hit
} while (!$item.length && ++lookCount < 2);
// if we hit a span or an anchor tag and didn't
// find an item then force the selection hit to
// the item so we can bail out on the scrollMe
// region at the top and bottom of the list
if ($item.length === 0 && ($hit.is("a") || $hit.is("span"))) {
$item = $hit.parents("#working-set-list-container li");
}
// compute ghost location, we compute the insertion point based
// on where the ghost is, not where the mouse is
gTop = $ghost.offset().top;
gHeight = $ghost.height();
gBottom = gTop + gHeight;
// data to help us determine if we have a scroller
hasScroller = $item.length && $container.length && $container[0].scrollHeight > $container[0].clientHeight;
// data to help determine if the ghost is in either of the scrollMe regions
onTopScroller = hasScroller && scrollerTopArea && ((gTop >= scrollerTopArea.top && gTop <= scrollerTopArea.bottom) ||
(gBottom >= scrollerTopArea.top && gBottom <= scrollerTopArea.bottom));
onBottomScroller = hasScroller && scrollerBottomArea && ((gTop >= scrollerBottomArea.top && gTop <= scrollerBottomArea.bottom) ||
(gBottom >= scrollerBottomArea.top && gBottom <= scrollerBottomArea.bottom));
// helpers
function mouseIsInTopHalf($elem) {
var top = $elem.offset().top,
height = $elem.height();
return (pageY < top + (height / 2));
}
function ghostIsAbove($elem) {
var top = $elem.offset().top,
checkVal = gTop;
if (direction > 0) {
checkVal += gHeight;
}
return (checkVal <= (top + (itemHeight / 2)));
}
function ghostIsBelow($elem) {
var top = $elem.offset().top,
checkVal = gTop;
if (direction > 0) {
checkVal += gHeight;
}
return (checkVal >= (top + (itemHeight / 2)));
}
function elIsClearBelow($a, $b) {
var aTop = $a.offset().top,
bTop = $b.offset().top;
return (aTop >= bTop + $b.height());
}
function draggingBelowWorkingSet() {
return ($hit.length === 0 || elIsClearBelow($hit, $workingFilesContainer));
}
function targetIsContainer() {
return ($hit.is(".working-set-view") ||
$hit.is(".open-files-container") ||
($hit.is("ul") && $hit.parent().is(".open-files-container")));
}
function targetIsNoDrop() {
return $hit.is(".working-set-header") ||
$hit.is(".working-set-header-title") ||
$hit.is(".scroller-shadow") ||
$hit.is(".scroller-shadow");
}
function findViewFor($elem) {
if ($elem.is(".working-set-view")) {
return $elem;
}
return $elem.parents(".working-set-view");
}
if ($item.length) {
// We hit an item (li)
if (onTopScroller && (direction <= 0 || lastHit.where === TOPSCROLL)) {
result = {
where: TOPSCROLL,
which: $item
};
} else if (onBottomScroller && (direction >= 0 || lastHit.where === BOTSCROLL)) {
result = {
where: BOTSCROLL,
which: $item
};
} else if (ghostIsAbove($item)) {
result = {
where: ABOVEITEM,
which: $item
};
} else if (ghostIsBelow($item)) {
result = {
where: BELOWITEM,
which: $item
};
}
} else if ($el.parent()[0] !== $hit[0]) {
// Didn't hit an li, figure out
// where to go from here
$view = $el.parents(".working-set-view");
if (targetIsNoDrop()) {
if (direction < 0) {
if (ghostIsBelow($hit)) {
return result;
}
} else {
return result;
}
}
if (draggingBelowWorkingSet()) {
return result;
}
if (targetIsContainer()) {
if (mouseIsInTopHalf($hit)) {
result = {
where: ABOVEVIEW,
which: findViewFor($hit)
};
} else {
result = {
where: BELOWVIEW,
which: findViewFor($hit)
};
}
return result;
}
// Data to determine to help determine if we should
// append to the previous or prepend to the next
var $prev = $view.prev(),
$next = $view.next();
if (direction < 0) {
// moving up, if there is a view above
// then we want to append to the view above
// otherwise we're in nomandsland
if ($prev.length) {
result = {
where: BELOWVIEW,
which: $prev
};
}
} else if (direction > 0) {
// moving down, if there is a view below
// then we want to append to the view below
// otherwise we're in nomandsland
if ($next.length) {
result = {
where: ABOVEVIEW,
which: $next
};
}
} else if (mouseIsInTopHalf($view)) {
// we're inside the top half of
// a view so prepend to the view we hit
result = {
where: ABOVEVIEW,
which: $view
};
} else {
// we're inside the bottom half of
// a view so append to the view we hit
result = {
where: BELOWVIEW,
which: $view
};
}
} else {
// The item doesn't need updating
result = {
where: NOMOVEITEM,
which: $hit
};
}
return result;
}
// mouse move handler -- this pretty much does
// the heavy lifting for dragging the item around
$(window).on("mousemove.wsvdragging", function (e) {
// The drag function
function drag(e) {
if (!dragged) {
initDragging();
// sort redraw and scroll shadows
// cause problems during drag so disable them
_suppressSortRedrawForAllViews(true);
_suppressScrollShadowsOnAllViews(true);
// remove the "active" class to remove the
// selection indicator so we don't have to
// keep it in sync while we're dragging
_deactivateAllViews(true);
// add a "dragging" class to the outer container
$workingFilesContainer.addClass("dragging");
// add a class to the element we're dragging if
// it's the currently selected file so that we
// can show it as selected while dragging
if (!draggingCurrentFile && FileViewController.getFileSelectionFocus() === FileViewController.WORKING_SET_VIEW) {
$(activeView._findListItemFromFile(currentFile)).addClass("drag-show-as-selected");
}
// we've dragged the item so set
// dragged to true so we don't try and open it
dragged = true;
}
// reset the scrolling direction to no-scroll
scrollDir = 0;
// Find out where to to drag it to
lastHit = hitTest(e);
// if the drag goes into nomansland then
// drop the opacity on the drag affordance
// and show the inserted item at reduced opacity
switch (lastHit.where) {
case NOMANSLAND:
case BELOWVIEW:
case ABOVEVIEW:
$el.css({opacity: ".75"});
$ghost.css("opacity", ".25");
break;
default:
$el.css({opacity: ".0001"});
$ghost.css("opacity", "");
break;
}
// now do the insertion
switch (lastHit.where) {
case TOPSCROLL:
case ABOVEITEM:
if (lastHit.where === TOPSCROLL) {
scrollDir = -1;
}
$el.insertBefore(lastHit.which);
updateContext(lastHit);
break;
case BOTSCROLL:
case BELOWITEM:
if (lastHit.where === BOTSCROLL) {
scrollDir = 1;
}
$el.insertAfter(lastHit.which);
updateContext(lastHit);
break;
case BELOWVIEW:
$el.appendTo(lastHit.which.find("ul"));
updateContext(lastHit);
break;
case ABOVEVIEW:
$el.prependTo(lastHit.which.find("ul"));
updateContext(lastHit);
break;
}
// we need to scroll
if (scrollDir) {
// we're in range to scroll
scroll(currentView.$openFilesContainer, $el, scrollDir, function () {
// as we scroll, recompute the element and insert
// it before/after the item to drag it in to place
drag(e);
});
} else {
// we've moved away from the top/bottom "scrolling" region
endScroll($el);
}
}
// Reposition the drag affordance if we've started dragging
if ($ghost) {
$ghost.css("top", $ghost.offset().top + (e.pageY - lastPageY));
}
// if we have't started dragging yet then we wait until
// the mouse has moved 3 pixels before we start dragging
// to avoid the item moving when clicked or double clicked
if (dragged || Math.abs(e.pageY - startPageY) > _DRAG_MOVE_DETECTION_START) {
drag(e);
}
lastPageY = e.pageY;
e.stopPropagation();
});
function scrollCurrentViewToBottom() {
var $container = currentView.$openFilesContainer,
container = $container[0],
maxScroll = container.scrollHeight - container.clientHeight;
if (maxScroll) {
$container.scrollTop(maxScroll);
}
}
// Close down the drag operation
function preDropCleanup() {
window.onmousewheel = window.document.onmousewheel = null;
$(window).off(".wsvdragging");
if (dragged) {
$workingFilesContainer.removeClass("dragging");
$workingFilesContainer.find(".drag-show-as-selected").removeClass("drag-show-as-selected");
endScroll($el);
// re-activate the views (adds the "active" class to the view that was previously active)
_deactivateAllViews(false);
// turn scroll wheel back on
$ghost.remove();
$el.css("opacity", "");
if ($el.next().length === 0) {
scrollCurrentViewToBottom();
}
}
}
// Final Cleanup
function postDropCleanup(noRefresh) {
if (dragged) {
// re-enable stuff we turned off
_suppressSortRedrawForAllViews(false);
_suppressScrollShadowsOnAllViews(false);
}
// we don't need to refresh if the item
// was dragged but not enough to not change
// its order in the working set
if (!noRefresh) {
// rebuild the view
refresh(true);
}
// focus the editor
MainViewManager.focusActivePane();
}
// Drop
function drop() {
preDropCleanup();
if (sourceView.paneId === currentView.paneId && startingIndex === $el.index()) {
// if the item was dragged but not moved then don't open or close
if (!dragged) {
// Click on close icon, or middle click anywhere - close the item without selecting it first
if (tryClosing || e.which === MIDDLE_BUTTON) {
CommandManager
.execute(Commands.FILE_CLOSE, {file: sourceFile,
paneId: sourceView.paneId})
.always(function () {
postDropCleanup();
});
} else {
// Normal right and left click - select the item
FileViewController.setFileViewFocus(FileViewController.WORKING_SET_VIEW);
CommandManager
.execute(Commands.FILE_OPEN, {fullPath: sourceFile.fullPath,
paneId: currentView.paneId})
.always(function () {
postDropCleanup();
});
}
} else {
// no need to refresh
postDropCleanup(true);
}
} else if (sourceView.paneId === currentView.paneId) {
// item was reordered
MainViewManager._moveWorkingSetItem(sourceView.paneId, startingIndex, $el.index());
postDropCleanup();
} else {
// If the same doc view is present in the destination pane prevent drop
if (!MainViewManager._getPane(currentView.paneId).getViewForPath(sourceFile.fullPath)) {
// item was dragged to another working set
MainViewManager._moveView(sourceView.paneId, currentView.paneId, sourceFile, $el.index())
.always(function () {
// if the current document was dragged to another working set
// then reopen it to make it the currently selected file
if (draggingCurrentFile) {
CommandManager
.execute(Commands.FILE_OPEN, {fullPath: sourceFile.fullPath,
paneId: currentView.paneId})
.always(function () {
postDropCleanup();
});
} else {
postDropCleanup();
}
});
} else {
postDropCleanup();
}
}
}
// prevent working set from grabbing focus no matter what type of click/drag occurs
e.preventDefault();
// initialization
$(window).on("mouseup.wsvdragging", function () {
drop();
});
// let escape cancel the drag
$(window).on("keydown.wsvdragging", function (e) {
if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
preDropCleanup();
postDropCleanup();
e.stopPropagation();
}
});
// turn off scroll wheel
window.onmousewheel = window.document.onmousewheel = function (e) {
e.preventDefault();
};
// close all menus, and disable sorting
Menus.closeAll();
// Dragging only happens with the left mouse button
// or (on the Mac) when the ctrl key isn't pressed
if (e.which !== LEFT_BUTTON || (e.ctrlKey && brackets.platform === "mac")) {
drop();
return;
}
e.stopPropagation();
});
}
turns off the scroll shadow on view containers so they don't interfere with dragging
function _suppressScrollShadowsOnAllViews(disable) {
_.forEach(_views, function (view) {
if (disable) {
ViewUtils.removeScrollerShadow(view.$openFilesContainer[0], null);
} else if (view.$openFilesContainer[0].scrollHeight > view.$openFilesContainer[0].clientHeight) {
ViewUtils.addScrollerShadow(view.$openFilesContainer[0], null, true);
}
});
}
Turns on/off the flag which suppresses rebuilding of the working set when the "workingSetSort" event is dispatched from MainViewManager. Only used while dragging things around in the working set to disable rebuilding the list while dragging.
function _suppressSortRedrawForAllViews(suppress) {
_.forEach(_views, function (view) {
view.suppressSortRedraw = suppress;
});
}
Updates the appearance of the list element based on the parameters provided.
function _updateListItemSelection(listItem, selectedFile) {
var shouldBeSelected = (selectedFile && $(listItem).data(_FILE_KEY).fullPath === selectedFile.fullPath);
ViewUtils.toggleClass($(listItem), "selected", shouldBeSelected);
}
Finds the WorkingSetView object for the specified element
function _viewFromEl($el) {
if (!$el.hasClass("working-set-view")) {
$el = $el.parents(".working-set-view");
}
var id = $el.attr("id").match(/working\-set\-list\-([\w]+[\w\d\-\.\:\_]*)/).pop();
return _views[id];
}
Adds a CSS class provider, invoked before each working set item is created or updated. When called to update an existing item, all previously applied classes have been cleared.
function addClassProvider(callback) {
if (!callback) {
return;
}
_classProviders.push(callback);
// build all views so the provider has a chance to style
// all items that have already been created
refresh(true);
}
AppInit.htmlReady(function () {
$workingFilesContainer = $("#working-set-list-container");
});
Adds an icon provider. The callback is invoked before each working set item is created, and can return content to prepend to the item.
function addIconProvider(callback) {
if (!callback) {
return;
}
_iconProviders.push(callback);
// build all views so the provider has a chance to add icons
// to all items that have already been created
refresh(true);
}
Creates a new WorkingSetView object for the specified pane
function createWorkingSetViewForPane($container, paneId) {
var view = _views[paneId];
if (!view) {
view = new WorkingSetView($container, paneId);
_views[view.paneId] = view;
}
}
Gets the filesystem object for the current context in the working set.
function getContext() {
return _contextEntry;
}
// Public API
exports.createWorkingSetViewForPane = createWorkingSetViewForPane;
exports.refresh = refresh;
exports.addIconProvider = addIconProvider;
exports.addClassProvider = addClassProvider;
exports.syncSelectionIndicator = syncSelectionIndicator;
exports.getContext = getContext;
// API to be used only by default extensions
exports.useIconProviders = useIconProviders;
exports.useClassProviders = useClassProviders;
});
Refreshes all Pane View List Views
function refresh(rebuild) {
_.forEach(_views, function (view) {
var top = view.$openFilesContainer.scrollTop();
if (rebuild) {
view._rebuildViewList(true);
} else {
view._redraw();
}
view.$openFilesContainer.scrollTop(top);
});
}
Synchronizes the selection indicator for all views
function syncSelectionIndicator() {
_.forEach(_views, function (view) {
view.$openFilesContainer.triggerHandler("scroll");
});
}
To be used by other modules/default-extensions which needs to borrow working set entry custom classes
function useClassProviders(data, $element) {
_classProviders.forEach(function (provider) {
$element.addClass(provider(data));
});
}
To be used by other modules/default-extensions which needs to borrow working set entry icons
function useIconProviders(data, $element) {
_iconProviders.forEach(function (provider) {
var icon = provider(data);
if (icon) {
$element.prepend($(icon));
}
});
}
WorkingSetView constructor
function WorkingSetView($container, paneId) {
var id = "working-set-list-" + paneId;
this.$header = null;
this.$openFilesList = null;
this.$container = $container;
this.$el = $container.append(Mustache.render(paneListTemplate, _.extend({id: id}, Strings))).find("#" + id);
this.suppressSortRedraw = false;
this.paneId = paneId;
this.init();
}
Adds directory names to elements representing passed files in working tree
WorkingSetView.prototype._addDirectoryNamesToWorkingTreeFiles = function (filesList) {
// filesList must have at least two files in it for this to make sense
if (filesList.length <= 1) {
return;
}
var displayPaths = ViewUtils.getDirNamesForDuplicateFiles(filesList);
// Go through open files and add directories to appropriate entries
this.$openFilesContainer.find("ul > li").each(function () {
var $li = $(this);
var io = filesList.indexOf($li.data(_FILE_KEY));
if (io !== -1) {
var dirSplit = displayPaths[io].split("/");
if (dirSplit.length > 3) {
displayPaths[io] = dirSplit[0] + "/\u2026/" + dirSplit[dirSplit.length - 1];
}
var $dir = $("<span class='directory'/>").html(" — " + displayPaths[io]);
$li.children("a").append($dir);
}
});
};
adds the style 'vertical-scroll' if a vertical scroll bar is present
WorkingSetView.prototype._adjustForScrollbars = function () {
if (this.$openFilesContainer[0].scrollHeight > this.$openFilesContainer[0].clientHeight) {
if (!this.$openFilesContainer.hasClass("vertical-scroll")) {
this.$openFilesContainer.addClass("vertical-scroll");
}
} else {
this.$openFilesContainer.removeClass("vertical-scroll");
}
};
Looks for files with the same name in the working set and adds a parent directory name to them
WorkingSetView.prototype._checkForDuplicatesInWorkingTree = function () {
var self = this,
map = {},
fileList = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES);
// We need to always clear current directories as files could be removed from working tree.
this.$openFilesContainer.find("ul > li > a > span.directory").remove();
// Go through files and fill map with arrays of files.
fileList.forEach(function (file) {
// Use the same function that is used to create html for file.
var displayHtml = ViewUtils.getFileEntryDisplay(file);
if (!map[displayHtml]) {
map[displayHtml] = [];
}
map[displayHtml].push(file);
});
// Go through the map and solve the arrays with length over 1. Ignore the rest.
_.forEach(map, function (value) {
if (value.length > 1) {
self._addDirectoryNamesToWorkingTreeFiles(value);
}
});
};
Builds the UI for a new list item and inserts in into the end of the list
WorkingSetView.prototype._createNewListItem = function (file) {
var self = this,
selectedFile = MainViewManager.getCurrentlyViewedFile(this.paneId),
data = {fullPath: file.fullPath,
name: file.name,
isFile: file.isFile};
// Create new list item with a link
var $link = $("<a href='#'></a>").html(ViewUtils.getFileEntryDisplay(file));
_iconProviders.forEach(function (provider) {
var icon = provider(data);
if (icon) {
$link.prepend($(icon));
}
});
var $newItem = $("<li></li>")
.append($link)
.data(_FILE_KEY, file);
this.$openFilesContainer.find("ul").append($newItem);
_classProviders.forEach(function (provider) {
$newItem.addClass(provider(data));
});
// Update the listItem's apperance
this._updateFileStatusIcon($newItem, _isOpenAndDirty(file), false);
_updateListItemSelection($newItem, selectedFile);
_makeDraggable($newItem);
$newItem.hover(
function () {
self._updateFileStatusIcon($(this), _isOpenAndDirty(file), true);
},
function () {
self._updateFileStatusIcon($(this), _isOpenAndDirty(file), false);
}
);
};
Finds the listItem item assocated with the file. Returns null if not found.
WorkingSetView.prototype._findListItemFromFile = function (file) {
var result = null;
if (file) {
var items = this.$openFilesContainer.find("ul").children();
items.each(function () {
var $listItem = $(this);
if ($listItem.data(_FILE_KEY).fullPath === file.fullPath) {
result = $listItem;
return false; // breaks each
}
});
}
return result;
};
Redraw selection when list size changes or DocumentManager currentDocument changes.
WorkingSetView.prototype._fireSelectionChanged = function (scrollIntoView) {
var reveal = (scrollIntoView === undefined || scrollIntoView === true);
if (reveal) {
this._scrollSelectedFileIntoView();
}
if (_hasSelectionFocus() && this.$el.hasClass("active")) {
this.$openFilesList.trigger("selectionChanged", reveal);
} else {
this.$openFilesList.trigger("selectionHide");
}
// in-lieu of resize events, manually trigger contentChanged to update scroll shadows
this.$openFilesContainer.trigger("contentChanged");
};
activePaneChange event handler
WorkingSetView.prototype._handleActivePaneChange = function () {
this._redraw();
};
dirtyFlagChange event handler
WorkingSetView.prototype._handleDirtyFlagChanged = function (e, doc) {
var listItem = this._findListItemFromFile(doc.file);
if (listItem) {
var canClose = $(listItem).find(".can-close").length === 1;
this._updateFileStatusIcon(listItem, doc.isDirty, canClose);
}
};
workingSetAdd event handler
WorkingSetView.prototype._handleFileAdded = function (e, fileAdded, index, paneId) {
if (paneId === this.paneId) {
this._rebuildViewList(true);
} else {
this._checkForDuplicatesInWorkingTree();
}
};
workingSetAddList event handler
WorkingSetView.prototype._handleFileListAdded = function (e, files, paneId) {
if (paneId === this.paneId) {
this._rebuildViewList(true);
} else {
this._checkForDuplicatesInWorkingTree();
}
};
workingSetRemove event handler
WorkingSetView.prototype._handleFileRemoved = function (e, file, suppressRedraw, paneId) {
paneLayoutChange event listener
WorkingSetView.prototype._handlePaneLayoutChange = function () {
var $titleEl = this.$el.find(".working-set-header-title"),
title = Strings.WORKING_FILES;
this._updateVisibility();
if (MainViewManager.getPaneCount() > 1) {
title = MainViewManager.getPaneTitle(this.paneId);
}
$titleEl.text(title);
};
workingSetRemoveList event handler
WorkingSetView.prototype._handleRemoveList = function (e, files, paneId) {
var self = this;
if (paneId === this.paneId) {
files.forEach(function (file) {
var $listItem = self._findListItemFromFile(file);
if ($listItem) {
$listItem.remove();
}
});
this._redraw();
} else {
this._checkForDuplicatesInWorkingTree();
}
};
workingSetSort event handler
WorkingSetView.prototype._handleWorkingSetSort = function (e, paneId) {
if (!this.suppressSortRedraw && paneId === this.paneId) {
this._rebuildViewList(true);
}
};
workingSetUpdate event handler
WorkingSetView.prototype._handleWorkingSetUpdate = function (e, paneId) {
if (this.paneId === paneId) {
this._rebuildViewList(true);
} else {
this._checkForDuplicatesInWorkingTree();
}
};
creates a name that is namespaced to this pane
WorkingSetView.prototype._makeEventName = function (name) {
return name + ".paneList" + this.paneId;
};
Deletes all the list items in the view and rebuilds them from the working set model
WorkingSetView.prototype._rebuildViewList = function (forceRedraw) {
var self = this,
fileList = MainViewManager.getWorkingSet(this.paneId);
this.$openFilesContainer.find("ul").empty();
fileList.forEach(function (file) {
self._createNewListItem(file);
});
if (forceRedraw) {
self._redraw();
}
};
Shows/Hides open files list based on working set content.
WorkingSetView.prototype._redraw = function () {
this._updateViewState();
this._updateVisibility();
this._updateItemClasses();
this._adjustForScrollbars();
this._fireSelectionChanged();
};
Scrolls the selected file into view
WorkingSetView.prototype._scrollSelectedFileIntoView = function () {
if (!_hasSelectionFocus()) {
return;
}
var file = MainViewManager.getCurrentlyViewedFile(this.paneId);
var $selectedFile = this._findListItemFromFile(file);
if (!$selectedFile) {
return;
}
ViewUtils.scrollElementIntoView(this.$openFilesContainer, $selectedFile, false);
};
Updates the appearance of the list element based on the parameters provided
WorkingSetView.prototype._updateFileStatusIcon = function (listElement, isDirty, canClose) {
var $fileStatusIcon = listElement.find(".file-status-icon"),
showIcon = isDirty || canClose;
// remove icon if its not needed
if (!showIcon && $fileStatusIcon.length !== 0) {
$fileStatusIcon.remove();
$fileStatusIcon = null;
// create icon if its needed and doesn't exist
} else if (showIcon && $fileStatusIcon.length === 0) {
$fileStatusIcon = $("<div class='file-status-icon'></div>")
.prependTo(listElement);
}
// Set icon's class
if ($fileStatusIcon) {
ViewUtils.toggleClass($fileStatusIcon, "dirty", isDirty);
ViewUtils.toggleClass($fileStatusIcon, "can-close", canClose);
}
};
Updates the working set item class list
WorkingSetView.prototype._updateItemClasses = function () {
if (_classProviders.length > 0) {
this.$openFilesContainer.find("ul > li").each(function () {
var $li = $(this),
file = $li.data(_FILE_KEY),
data = {fullPath: file.fullPath,
name: file.name,
isFile: file.isFile};
_classProviders.forEach(function (provider) {
$li.addClass(provider(data));
});
});
}
};
Updates the pane view's selection marker and scrolls the item into view
WorkingSetView.prototype._updateListSelection = function () {
var file = MainViewManager.getCurrentlyViewedFile(this.paneId);
this._updateViewState();
// Iterate through working set list and update the selection on each
this.$openFilesContainer.find("ul").children().each(function () {
_updateListItemSelection(this, file);
});
// Make sure selection is in view
this._scrollSelectedFileIntoView();
this._fireSelectionChanged();
};
Updates the pane view's selection state
WorkingSetView.prototype._updateViewState = function () {
var paneId = MainViewManager.getActivePaneId();
if (_hasSelectionFocus() && paneId === this.paneId) {
this.$el.addClass("active");
this.$openFilesContainer.addClass("active");
} else {
this.$el.removeClass("active");
this.$openFilesContainer.removeClass("active");
}
};
Hides or shows the WorkingSetView
WorkingSetView.prototype._updateVisibility = function () {
var fileList = MainViewManager.getWorkingSet(this.paneId);
if (MainViewManager.getPaneCount() === 1 && (!fileList || fileList.length === 0)) {
this.$openFilesContainer.hide();
this.$workingSetListViewHeader.hide();
} else {
this.$openFilesContainer.show();
this.$workingSetListViewHeader.show();
this._checkForDuplicatesInWorkingTree();
}
};
Destroys the WorkingSetView DOM element and removes all event handlers
WorkingSetView.prototype.destroy = function () {
ViewUtils.removeScrollerShadow(this.$openFilesContainer[0], null);
this.$openFilesContainer.off(".workingSetView");
this.$el.remove();
MainViewManager.off(this._makeEventName(""));
DocumentManager.off(this._makeEventName(""));
FileViewController.off(this._makeEventName(""));
};
Initializes the WorkingSetView object
WorkingSetView.prototype.init = function () {
this.$openFilesContainer = this.$el.find(".open-files-container");
this.$workingSetListViewHeader = this.$el.find(".working-set-header");
this.$openFilesList = this.$el.find("ul");
// Register listeners
MainViewManager.on(this._makeEventName("workingSetAdd"), _.bind(this._handleFileAdded, this));
MainViewManager.on(this._makeEventName("workingSetAddList"), _.bind(this._handleFileListAdded, this));
MainViewManager.on(this._makeEventName("workingSetRemove"), _.bind(this._handleFileRemoved, this));
MainViewManager.on(this._makeEventName("workingSetRemoveList"), _.bind(this._handleRemoveList, this));
MainViewManager.on(this._makeEventName("workingSetSort"), _.bind(this._handleWorkingSetSort, this));
MainViewManager.on(this._makeEventName("activePaneChange"), _.bind(this._handleActivePaneChange, this));
MainViewManager.on(this._makeEventName("paneLayoutChange"), _.bind(this._handlePaneLayoutChange, this));
MainViewManager.on(this._makeEventName("workingSetUpdate"), _.bind(this._handleWorkingSetUpdate, this));
DocumentManager.on(this._makeEventName("dirtyFlagChange"), _.bind(this._handleDirtyFlagChanged, this));
FileViewController.on(this._makeEventName("documentSelectionFocusChange") + " " + this._makeEventName("fileViewFocusChange"), _.bind(this._updateListSelection, this));
// Show scroller shadows when open-files-container scrolls
ViewUtils.addScrollerShadow(this.$openFilesContainer[0], null, true);
ViewUtils.sidebarList(this.$openFilesContainer);
// Disable horizontal scrolling until WebKit bug #99379 is fixed
this.$openFilesContainer.css("overflow-x", "hidden");
this.$openFilesContainer.on("contextmenu.workingSetView", function (e) {
_contextEntry = $(e.target).closest("li").data(_FILE_KEY);
Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU).open(e);
});
this._redraw();
};