Modules (188)

WorkingSetView

Description

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.

Dependencies

Variables

$workingFilesContainer

#working-set-list-container

Type
jQuery
    var $workingFilesContainer;

LEFT_BUTTON Enumeration

Constants for event.which values

Type
number
    var LEFT_BUTTON = 1,
        MIDDLE_BUTTON = 2;

NOMANSLAND Enumeration

Constants for hitTest.where

Type
string
    var NOMANSLAND = "nomansland",
        NOMOVEITEM = "nomoveitem",
        ABOVEITEM  = "aboveitem",
        BELOWITEM  = "belowitem",
        TOPSCROLL  = "topscroll",
        BOTSCROLL  = "bottomscroll",
        BELOWVIEW  = "belowview",
        ABOVEVIEW  = "aboveview";
Private

_DRAG_MOVE_DETECTION_START

Drag an item has to move 3px before dragging starts

constant
    var _DRAG_MOVE_DETECTION_START = 3;
Private

_FILE_KEY

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

Type
string
    var _FILE_KEY = "file";
Private

_classProviders

Class Providers

See
addClassProvider
    var _classProviders = [];
Private

_contextEntry

The file/folder object of the current context

Type
FileSystemEntry
    var _contextEntry;
Private

_iconProviders

Icon Providers

See
addIconProvider
    var _iconProviders = [];
Private

_views

Open view dictionary Maps PaneId to WorkingSetView

Type
Object.<string, WorkingSetView>
    var _views = {};

Functions

Private

_deactivateAllViews

Deactivates all views so the selection marker does not show

deactivate Boolean
true to deactivate, false to reactivate
    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);
            }
        });
    }
Private

_isOpenAndDirty

Determines if a file is dirty

file non-nullable File
file to test
Returns: boolean
true if the file is dirty, false otherwise
    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;
    }
Private

_makeDraggable

Makes the specified element draggable

$el jQuery
the element to make 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();
        });
    }
Private

_suppressScrollShadowsOnAllViews

turns off the scroll shadow on view containers so they don't interfere with dragging

disable Boolean
true to disable, false to enable
    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);
            }
        });
    }
Private

_suppressSortRedrawForAllViews

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.

suppress boolean
true suppress, false to allow sort redrawing
    function _suppressSortRedrawForAllViews(suppress) {
        _.forEach(_views, function (view) {
            view.suppressSortRedraw = suppress;
        });
    }
Private

_updateListItemSelection

Updates the appearance of the list element based on the parameters provided.

listElement non-nullable HTMLLIElement
selectedFile nullable File
    function _updateListItemSelection(listItem, selectedFile) {
        var shouldBeSelected = (selectedFile && $(listItem).data(_FILE_KEY).fullPath === selectedFile.fullPath);
        ViewUtils.toggleClass($(listItem), "selected", shouldBeSelected);
    }
Private

_viewFromEl

Finds the WorkingSetView object for the specified element

$el jQuery
the element to find the view for
Returns: View
view object
    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];
    }
Public API

addClassProvider

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.

callback non-nullable function(!{name:string, fullPath:string, isFile:boolean}):?string
Return a string containing space-separated CSS class(es) to add, or undefined to leave CSS unchanged.
    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");
    });
Public API

addIconProvider

Adds an icon provider. The callback is invoked before each working set item is created, and can return content to prepend to the item.

callback non-nullable function(!{name:string, fullPath:string, isFile:boolean}):?string, jQuery, DOMNode
Return a string representing the HTML, a jQuery object or DOM node, or undefined. If undefined, nothing is prepended to the list 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);
    }
Public API

createWorkingSetViewForPane

Creates a new WorkingSetView object for the specified pane

$container non-nullable jQuery
the WorkingSetView's DOM parent node
paneId non-nullable string
the id of the pane the view is being created for
    function createWorkingSetViewForPane($container, paneId) {
        var view = _views[paneId];
        if (!view) {
            view = new WorkingSetView($container, paneId);
            _views[view.paneId] = view;
        }
    }
Public API

getContext

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;
});
Public API

refresh

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);
        });
    }
Public API

syncSelectionIndicator

Synchronizes the selection indicator for all views

    function syncSelectionIndicator() {
        _.forEach(_views, function (view) {
            view.$openFilesContainer.triggerHandler("scroll");
        });
    }
Public API

useClassProviders

To be used by other modules/default-extensions which needs to borrow working set entry custom classes

data non-nullable object
contains file info {fullPath, name, isFile}
$element non-nullable jQuery
jquery fn wrap for the list item
    function useClassProviders(data, $element) {
        _classProviders.forEach(function (provider) {
            $element.addClass(provider(data));
        });
    }
Public API

useIconProviders

To be used by other modules/default-extensions which needs to borrow working set entry icons

data non-nullable object
contains file info {fullPath, name, isFile}
$element non-nullable jQuery
jquery fn wrap for the list item
    function useIconProviders(data, $element) {
        _iconProviders.forEach(function (provider) {
            var icon = provider(data);
            if (icon) {
                $element.prepend($(icon));
            }
        });
    }

Classes

Constructor

WorkingSetView

WorkingSetView constructor

$container jQuery
owning container
paneId string
paneId of this view pertains to
    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();
    }

Methods

Private

_addDirectoryNamesToWorkingTreeFiles

Adds directory names to elements representing passed files in working tree

filesList Array.<File>
list of Files with the same filename
    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(" &mdash; " + displayPaths[io]);
                $li.children("a").append($dir);
            }
        });
    };
Private

_adjustForScrollbars

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");
        }
    };
Private

_checkForDuplicatesInWorkingTree

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);
            }
        });
    };
Private

_createNewListItem

Builds the UI for a new list item and inserts in into the end of the list

file File
Returns: HTMLLIElement
newListItem
    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);
            }
        );
    };
Private

_findListItemFromFile

Finds the listItem item assocated with the file. Returns null if not found.

file non-nullable File
Returns: HTMLLIItem
returns the DOM element of the item. null if one could not be 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;
    };
Private

_fireSelectionChanged

Redraw selection when list size changes or DocumentManager currentDocument changes.

scrollIntoView optional boolean
= Scrolls the selected item into view (the default behavior)
    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");
    };
Private

_handleActivePaneChange

activePaneChange event handler

    WorkingSetView.prototype._handleActivePaneChange = function () {
        this._redraw();
    };
Private

_handleDirtyFlagChanged

dirtyFlagChange event handler

e jQuery.Event
event object
doc Document
document whose dirty state has changed
    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);
        }
    };
Private

_handleFileAdded

workingSetAdd event handler

e jQuery.Event
event object
fileAdded non-nullable File
the file that was added
index non-nullable number
index where the file was added
paneId non-nullable string
the id of the pane the item that was to
    WorkingSetView.prototype._handleFileAdded = function (e, fileAdded, index, paneId) {
        if (paneId === this.paneId) {
            this._rebuildViewList(true);
        } else {
            this._checkForDuplicatesInWorkingTree();
        }
    };
Private

_handleFileListAdded

workingSetAddList event handler

e jQuery.Event
event object
files non-nullable Array.<File>
the files that were added
paneId non-nullable string
the id of the pane the item that was to
    WorkingSetView.prototype._handleFileListAdded = function (e, files, paneId) {
        if (paneId === this.paneId) {
            this._rebuildViewList(true);
        } else {
            this._checkForDuplicatesInWorkingTree();
        }
    };
Private

_handleFileRemoved

workingSetRemove event handler

e jQuery.Event
event object
file non-nullable File
the file that was removed
suppressRedraw nullable boolean
If true, suppress redraw
paneId non-nullable string
the id of the pane the item that was to
    WorkingSetView.prototype._handleFileRemoved = function (e, file, suppressRedraw, paneId) {
Private

_handlePaneLayoutChange

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);
    };
Private

_handleRemoveList

workingSetRemoveList event handler

e jQuery.Event
event object
files non-nullable Array.<File>
the files that were removed
paneId non-nullable string
the id of the pane the item that was to
    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();
        }
    };
Private

_handleWorkingSetSort

workingSetSort event handler

e jQuery.Event
event object
paneId non-nullable string
the id of the pane to sort
    WorkingSetView.prototype._handleWorkingSetSort = function (e, paneId) {
        if (!this.suppressSortRedraw && paneId === this.paneId) {
            this._rebuildViewList(true);
        }
    };
Private

_handleWorkingSetUpdate

workingSetUpdate event handler

e jQuery.Event
event object
paneId non-nullable string
the id of the pane to update
    WorkingSetView.prototype._handleWorkingSetUpdate = function (e, paneId) {
        if (this.paneId === paneId) {
            this._rebuildViewList(true);
        } else {
            this._checkForDuplicatesInWorkingTree();
        }
    };
Private

_makeEventName

creates a name that is namespaced to this pane

name non-nullable string
name of the event to create. use an empty string to get just the event name to turn off all events in the namespace
    WorkingSetView.prototype._makeEventName = function (name) {
        return name + ".paneList" + this.paneId;
    };
Private

_rebuildViewList

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();
        }
    };
Private

_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();
    };
Private

_scrollSelectedFileIntoView

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);
    };
Private

_updateFileStatusIcon

Updates the appearance of the list element based on the parameters provided

listElement non-nullable HTMLLIElement
isDirty bool
canClose bool
    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);
        }
    };
Private

_updateItemClasses

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));
                });
            });
        }
    };
Private

_updateListSelection

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();
    };
Private

_updateViewState

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");
        }
    };
Private

_updateVisibility

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();
        }
    };

destroy

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(""));
    };

init

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();
    };