/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */
/*global define, $, XMLHttpRequest */

/**
 * DOMAgent constructs and maintains a tree of {DOMNode}s that represents the
 * rendered DOM tree in the remote browser. Nodes can be accessed by id or
 * location (source offset). To update the DOM tree in response to a change of
 * the source document (replace [from,to] with text) call
 * `applyChange(from, to, text)`.
 *
 * The DOMAgent triggers `getDocument` once it has loaded
 * the document.
 */
define(function DOMAgent(require, exports, module) {
    "use strict";

    var $exports = $(exports);

    var Inspector = require("LiveDevelopment/Inspector/Inspector");
    var EditAgent = require("LiveDevelopment/Agents/EditAgent");
    var DOMNode = require("LiveDevelopment/Agents/DOMNode");
    var DOMHelpers = require("LiveDevelopment/Agents/DOMHelpers");

    var _load; // {$.Deferred} load promise
    var _idToNode; // {nodeId -> node}
    var _pendingRequests; // {integer} number of pending requests before initial loading is complete

    /** Get the last node before the given location
     * @param {integer} location
     */
    function nodeBeforeLocation(location) {
        var node;
        exports.root.each(function each(n) {
            if (!n.location || location < n.location) {
                return true;
            }
            if (!node || node.location < n.location) {
                node = n;
            }
        });
        return node;
    }

    /** Get the element node that encloses the given location
     * @param {location}
     */
    function allNodesAtLocation(location) {
        var nodes = [];
        exports.root.each(function each(n) {
            if (n.type === DOMNode.TYPE_ELEMENT && n.isAtLocation(location)) {
                nodes.push(n);
            }
        });
        return nodes;
    }

    /** Get the node at the given location
     * @param {location}
     */
    function nodeAtLocation(location) {
        return exports.root.find(function each(n) {
            return n.isAtLocation(location, false);
        });
    }

    /** Find the node for the given id
     * @param {DOMNode} node
     */
    function nodeWithId(nodeId) {
        return _idToNode[nodeId];
    }

    /** Update the node index
     * @param {DOMNode} node
     */
    function removeNode(node) {
        if (node.nodeId) {
            delete _idToNode[node.nodeId];
        }
    }

    /** Update the node index
     * @param {DOMNode} node
     */
    function addNode(node) {
        if (node.nodeId) {
            _idToNode[node.nodeId] = node;
        }
    }

    /** Request the child nodes for a node
     * @param {DOMNode} node
     */
    function requestChildNodes(node) {
        if (_pendingRequests >= 0) {
            _pendingRequests++;
        }
        Inspector.DOM.requestChildNodes(node.nodeId);
    }

    /** Eliminate the query string from a URL
     * @param {string} URL
     */
    function _cleanURL(url) {
        var index = url.search(/[#\?]/);
        if (index >= 0) {
            url = url.substr(0, index);
        }
        return url;
    }

    /** Map the DOM document to the source text
     * @param {string} source
     */
    function _mapDocumentToSource(source) {
        var node = exports.root;
        DOMHelpers.eachNode(source, function each(payload) {
            if (!node) {
                return true;
            }
            if (payload.closing) {
                var parent = node.findParentForNextNodeMatchingPayload(payload);
                if (!parent) {
                    return console.warn("Matching Parent not at " + payload.sourceOffset + " (" + payload.nodeName + ")");
                }
                parent.closeLocation = payload.sourceOffset;
                parent.closeLength = payload.sourceLength;
            } else {
                var next = node.findNextNodeMatchingPayload(payload);
                if (!next) {
                    return console.warn("Skipping Source Node at " + payload.sourceOffset);
                }
                node = next;
                node.location = payload.sourceOffset;
                node.length = payload.sourceLength;
                if (payload.closed) {
                    node.closed = payload.closed;
                }
            }
        });
    }

    /** Load the source document and match it with the DOM tree*/
    function _onFinishedLoadingDOM() {
        var request = new XMLHttpRequest();
        request.open("GET", exports.url);
        request.onload = function onLoad() {
            if ((request.status >= 200 && request.status < 300) ||
                    request.status === 304 || request.status === 0) {
                _mapDocumentToSource(request.response);
                _load.resolve();
            } else {
                var msg = "Received status " + request.status + " from XMLHttpRequest while attempting to load source file at " + exports.url;
                _load.reject(msg, { message: msg });
            }
        };
        request.onerror = function onError() {
            var msg = "Could not load source file at " + exports.url;
            _load.reject(msg, { message: msg });
        };
        request.send(null);
    }

    // WebInspector Event: Page.loadEventFired
    function _onLoadEventFired(event, res) {
        // res = {timestamp}
        Inspector.DOM.getDocument(function onGetDocument(res) {
            $exports.triggerHandler("getDocument", res);
            // res = {root}
            _idToNode = {};
            _pendingRequests = 0;
            exports.root = new DOMNode(exports, res.root);
        });
    }

    // WebInspector Event: Page.frameNavigated
    function _onFrameNavigated(event, res) {
        // res = {frame}
        if (!res.frame.parentId) {
            exports.url = _cleanURL(res.frame.url);
        }
    }

     // WebInspector Event: DOM.documentUpdated
    function _onDocumentUpdated(event, res) {
        // res = {}
    }

    // WebInspector Event: DOM.setChildNodes
    function _onSetChildNodes(event, res) {
        // res = {parentId, nodes}
        var node = nodeWithId(res.parentId);
        node.setChildrenPayload(res.nodes);
        if (_pendingRequests > 0 && --_pendingRequests === 0) {
            _onFinishedLoadingDOM();
        }
    }

    // WebInspector Event: DOM.childNodeCountUpdated
    function _onChildNodeCountUpdated(event, res) {
        // res = {nodeId, childNodeCount}
        if (res.nodeId > 0) {
            Inspector.DOM.requestChildNodes(res.nodeId);
        }
    }

    // WebInspector Event: DOM.childNodeInserted
    function _onChildNodeInserted(event, res) {
        // res = {parentNodeId, previousNodeId, node}
        if (res.node.nodeId > 0) {
            var parent = nodeWithId(res.parentNodeId);
            var previousNode = nodeWithId(res.previousNodeId);
            var node = new DOMNode(exports, res.node);
            parent.insertChildAfter(node, previousNode);
        }
    }

    // WebInspector Event: DOM.childNodeRemoved
    function _onChildNodeRemoved(event, res) {
        // res = {parentNodeId, nodeId}
        if (res.nodeId > 0) {
            var node = nodeWithId(res.nodeId);
            node.remove();
        }
    }

    /** Apply a change
     * @param {integer} start offset of the change
     * @param {integer} end offset of the change
     * @param {string} change text
     */
    function applyChange(from, to, text) {
        var delta = from - to + text.length;
        var node = nodeAtLocation(from);

        // insert a text node
        if (!node) {
            if (!(/^\s*$/).test(text)) {
                console.warn("Inserting nodes not supported.");
                node = nodeBeforeLocation(from);
            }
        } else if (node.type === 3) {
            // update a text node
            var value = node.value.substr(0, from - node.location);
            value += text;
            value += node.value.substr(to - node.location);
            node.value = value;
            if (!EditAgent.isEditing) {
                // only update the DOM if the change was not caused by the edit agent
                Inspector.DOM.setNodeValue(node.nodeId, node.value);
            }
        } else {
            console.warn("Changing non-text nodes not supported.");
        }

        // adjust the location of all nodes after the change
        if (node) {
            node.length += delta;
            exports.root.each(function each(n) {
                if (n.location > node.location) {
                    n.location += delta;
                }
                if (n.closeLocation !== undefined && n.closeLocation > node.location) {
                    n.closeLocation += delta;
                }
            });
        }
    }

    /** Initialize the agent */
    function load() {
        _load = new $.Deferred();
        $(Inspector.Page)
            .on("frameNavigated.DOMAgent", _onFrameNavigated)
            .on("loadEventFired.DOMAgent", _onLoadEventFired);
        $(Inspector.DOM)
            .on("documentUpdated.DOMAgent", _onDocumentUpdated)
            .on("setChildNodes.DOMAgent", _onSetChildNodes)
            .on("childNodeCountUpdated.DOMAgent", _onChildNodeCountUpdated)
            .on("childNodeInserted.DOMAgent", _onChildNodeInserted)
            .on("childNodeRemoved.DOMAgent", _onChildNodeRemoved);
        return _load.promise();
    }

    /** Clean up */
    function unload() {
        $(Inspector.Page).off(".DOMAgent");
        $(Inspector.DOM).off(".DOMAgent");
    }

    // Export private functions
    exports.nodeBeforeLocation = nodeBeforeLocation;
    exports.allNodesAtLocation = allNodesAtLocation;
    exports.nodeAtLocation = nodeAtLocation;
    exports.nodeWithId = nodeWithId;
    exports.removeNode = removeNode;
    exports.addNode = addNode;
    exports.requestChildNodes = requestChildNodes;
    exports.applyChange = applyChange;
    exports.load = load;
    exports.unload = unload;
});


width: selectionViewInfo.get("width") - this.props.widthAdjustment,

display: this.props.visible ? "block" : "none"

keep keep keep keep replace replace keep keep keep

bytes

byte size string parser / serializer

# node-bytes

  Byte string parser / formatter.

## Example:

```js
bytes('1kb')
// => 1024

bytes('2mb')
// => 2097152

bytes('1gb')
// => 1073741824

bytes(1073741824)
// => 1gb
```

## Installation

```
$ npm install bytes
$ component install visionmedia/bytes.js
```

## License

(The MIT License)

Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


* SPECIAL CASE NOTE: this does not trigger a change event because this data is

* explicitly set in the rendering process (see ProjectManager._renderTree).

* @param {int} scrollTop Scroll position

* @param {int=} scrollLeft Horizontal scroll position

* @param {int=} offsetTop top of the scroller

* @param {int} scrollWidth width of the tree content

/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*global define, $*/
/*unittests: FileTreeView*/

/**
 * This is the view layer (template) for the file tree in the sidebar. It takes a FileTreeViewModel
 * and renders it to the given element using React. User actions are signaled via an ActionCreator
 * (in the Flux sense).
 */
define(function (require, exports, module) {
    "use strict";

    var React             = require("thirdparty/react"),
        Immutable         = require("thirdparty/immutable"),
        _                 = require("thirdparty/lodash"),
        FileUtils         = require("file/FileUtils"),
        FileTreeViewModel = require("project/FileTreeViewModel"),
        ViewUtils         = require("utils/ViewUtils"),
        KeyEvent          = require("utils/KeyEvent");

    var DOM = React.DOM;

    /**
     * @private
     * @type {Immutable.Map}
     *
     * Stores the file tree extensions for adding classes and icons. The keys of the map
     * are the "categories" of the extensions and values are vectors of the callback functions.
     */
    var _extensions = Immutable.Map();

    // Constants

    // Time range from first click to second click to invoke renaming.
    var CLICK_RENAME_MINIMUM  = 500,
        RIGHT_MOUSE_BUTTON    = 2,
        LEFT_MOUSE_BUTTON     = 0;

    /**
     * @private
     *
     * Returns the name of a file without its extension.
     *
     * @param {string} fullname The complete name of the file (not including the rest of the path)
     * @param {string} extension The file extension
     * @return {string} The fullname without the extension
     */
    function _getName(fullname, extension) {
        return extension !== "" ? fullname.substring(0, fullname.length - extension.length - 1) : fullname;
    }

    /**
     * Mixin that allows a component to compute the full path to its directory entry.
     */
    var pathComputer = {
        /**
         * Computes the full path of the file represented by this input.
         */
        myPath: function () {
            var result = this.props.parentPath + this.props.name;

            // Add trailing slash for directories
            if (!FileTreeViewModel.isFile(this.props.entry) && _.last(result) !== "/") {
                result += "/";
            }

            return result;
        }
    };

    /**
     * This is a mixin that provides rename input behavior. It is responsible for taking keyboard input
     * and invoking the correct action based on that input.
     */
    var renameBehavior = {
        /**
         * If the user presses enter or escape, we either successfully complete or cancel, respectively,
         * the rename or create operation that is underway.
         */
        handleKeyDown: function (e) {
            if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                this.props.actions.cancelRename();
            } else if (e.keyCode === KeyEvent.DOM_VK_RETURN) {
                this.props.actions.performRename();
            }
        },

        /**
         * The rename or create operation can be completed or canceled by actions outside of
         * this component, so we keep the model up to date by sending every update via an action.
         */
        handleKeyUp: function () {
            this.props.actions.setRenameValue(this.refs.name.getDOMNode().value.trim());
        }
    };

    /**
     * @private
     *
     * Gets an appropriate width given the text provided.
     *
     * @param {string} text Text to measure
     * @return {int} Width to use
     */
    function _measureText(text) {
        var measuringElement = $("<div />", { css : { "position" : "absolute", "top" : "-200px", "left" : ("-1000px"), "visibility" : "hidden" } }).appendTo("body");
        measuringElement.text("pW" + text);
        var width = measuringElement.width();
        measuringElement.remove();
        return width;
    }

    /**
     * @private
     *
     * This component presents an input field to the user for renaming a file.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the file, including the extension
     * * actions: the action creator responsible for communicating actions the user has taken
     */
    var fileRenameInput = React.createClass({
        mixins: [renameBehavior],

        /**
         * When this component is displayed, we scroll it into view and select the portion
         * of the filename that excludes the extension.
         */
        componentDidMount: function () {
            var fullname = this.props.name,
                extension = FileUtils.getSmartFileExtension(fullname);

            var node = this.refs.name.getDOMNode();
            node.setSelectionRange(0, _getName(fullname, extension).length);
            ViewUtils.scrollElementIntoView($("#project-files-container"), $(node), true);
        },

        render: function () {
            var width = _measureText(this.props.name);

            return DOM.input({
                className: "jstree-rename-input " + this.props.fileClasses,
                type: "text",
                defaultValue: this.props.name,
                autoFocus: true,
                onKeyDown: this.handleKeyDown,
                onKeyUp: this.handleKeyUp,
                style: {
                    width: width
                },
                ref: "name"
            });
        }
    });

    /**
     * @private
     *
     * This mixin handles right click (or control click on Mac) action to make a file
     * the "context" object for performing operations like rename.
     */
    var contextSettable = {

        /**
         * Send matching mouseDown events to the action creator as a setContext action.
         */
        handleMouseDown: function (e) {
            if (e.button === RIGHT_MOUSE_BUTTON ||
                    (this.props.platform === "mac" && e.button === LEFT_MOUSE_BUTTON && e.ctrlKey)) {
                this.props.actions.setContext(this.myPath());
                return false;
            }
        }
    };

    /**
     * @private
     *
     * Returns true if the value is defined (used in `.filter`)
     *
     * @param {Object} value value to test
     * @return {boolean} true if value is defined
     */
    function isDefined(value) {
        return value !== undefined;
    }

    /**
     * Mixin for components that support the "icons" and "addClass" extension points.
     * `fileNode` and `directoryNode` support this.
     */
    var extendable = {

        /**
         * Calls the icon providers to get the collection of icons (most likely just one) for
         * the current file or directory.
         *
         * @return {Array.<ReactComponent>} icon components to render
         */
        getIcons: function () {
            var result,
                extensions = this.props.extensions;

            if (extensions && extensions.get("icons")) {
                var data = this.getDataForExtension();
                result = extensions.get("icons").map(function (callback) {
                    try {
                        var result = callback(data);
                        if (!React.isValidComponent(result)) {
                            result = React.DOM.span({
                                dangerouslySetInnerHTML: {
                                    __html: $(result)[0].outerHTML
                                }
                            });
                        }
                        return result;
                    } catch (e) {
                        console.warn("Exception thrown in FileTreeView icon provider:", e);
                    }
                }).filter(isDefined).toArray();
            }

            if (!result || result.length === 0) {
                result = [DOM.ins({
                    className: "jstree-icon"
                }, " ")];
            }
            return result;
        },

        /**
         * Calls the addClass providers to get the classes (in string form) to add for the current
         * file or directory.
         *
         * @param {string} classes Initial classes for this node
         * @return {string} classes for the current node
         */
        getClasses: function (classes) {
            var extensions = this.props.extensions;

            if (extensions && extensions.get("addClass")) {
                var data = this.getDataForExtension();
                classes = classes + " " + extensions.get("addClass").map(function (callback) {
                    try {
                        return callback(data);
                    } catch (e) {
                        console.warn("Exception thrown in FileTreeView addClass provider:", e);
                    }
                }).filter(isDefined).toArray().join(" ");
            }

            return classes;
        }
    };

    /**
     * @private
     *
     * Component to display a file in the tree.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the file, including the extension
     * * entry: the object with the relevant metadata for the file (whether it's selected or is the context file)
     * * actions: the action creator responsible for communicating actions the user has taken
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    var fileNode = React.createClass({
        mixins: [contextSettable, pathComputer, extendable],

        /**
         * Ensures that we always have a state object.
         */
        getInitialState: function () {
            return {
                clickTimer: null
            };
        },

        /**
         * Thanks to immutable objects, we can just do a start object identity check to know
         * whether or not we need to re-render.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.entry !== nextProps.entry ||
                this.props.extensions !== nextProps.extensions;
        },

        /**
         * If this node is newly selected, scroll it into view. Also, move the selection or
         * context boxes as appropriate.
         */
        componentDidUpdate: function (prevProps, prevState) {
            var wasSelected = prevProps.entry.get("selected"),
                isSelected  = this.props.entry.get("selected");

            if (isSelected && !wasSelected) {
                // TODO: This shouldn't really know about project-files-container
                // directly. It is probably the case that our React tree should actually
                // start with project-files-container instead of just the interior of
                // project-files-container and then the file tree will be one self-contained
                // functional unit.
                ViewUtils.scrollElementIntoView($("#project-files-container"), $(this.getDOMNode()), true);
            } else if (!isSelected && wasSelected && this.state.clickTimer !== null) {
                this.clearTimer();
            }
        },

        clearTimer: function () {
            if (this.state.clickTimer !== null) {
                window.clearTimeout(this.state.clickTimer);
                this.setState({
                    clickTimer: null
                });
            }
        },

        startRename: function () {
            if (!this.props.entry.get("rename")) {
                this.props.actions.startRename(this.myPath());
            }
            this.clearTimer();
        },

        /**
         * When the user clicks on the node, we'll either select it or, if they've clicked twice
         * with a bit of delay in between, we'll invoke the `startRename` action.
         */
        handleClick: function (e) {
            // If we're renaming, allow the click to go through to the rename input.
            if (this.props.entry.get("rename")) {
                return true;
            }

            if (e.button !== LEFT_MOUSE_BUTTON) {
                return;
            }

            if (this.props.entry.get("selected")) {
                if (this.state.clickTimer === null && !this.props.entry.get("rename")) {
                    var timer = window.setTimeout(this.startRename, CLICK_RENAME_MINIMUM);
                    this.setState({
                        clickTimer: timer
                    });
                }
            } else {
                this.props.actions.setSelected(this.myPath());
            }
            return false;
        },

        /**
         * When the user double clicks, we will select this file and add it to the working
         * set (via the `selectInWorkingSet` action.)
         */
        handleDoubleClick: function () {
            if (!this.props.entry.get("rename")) {
                if (this.state.clickTimer !== null) {
                    this.clearTimer();
                }
                this.props.actions.selectInWorkingSet(this.myPath());
            }
        },

        /**
         * Create the data object to pass to extensions.
         *
         * @return {{name: {string}, isFile: {boolean}, fullPath: {string}}} Data for extensions
         */
        getDataForExtension: function () {
            return {
                name: this.props.name,
                isFile: true,
                fullPath: this.myPath()
            };
        },

        render: function () {
            var fullname = this.props.name,
                extension = FileUtils.getSmartFileExtension(fullname),
                name = _getName(fullname, extension);

            if (extension) {
                extension = DOM.span({
                    className: "extension"
                }, "." + extension);
            }

            var nameDisplay,
                cx = React.addons.classSet;

            var fileClasses = cx({
                'jstree-clicked selected-node': this.props.entry.get("selected"),
                'context-node': this.props.entry.get("context")
            });

            if (this.props.entry.get("rename")) {
                nameDisplay = fileRenameInput({
                    actions: this.props.actions,
                    entry: this.props.entry,
                    name: this.props.name,
                    parentPath: this.props.parentPath,
                    fileClasses: fileClasses
                });
            } else {
                // Need to flatten the argument list because getIcons returns an array
                var aArgs = _.flatten([{
                    href: "#",
                    className: fileClasses
                }, this.getIcons(), name, extension], true);
                nameDisplay = DOM.a.apply(DOM.a, aArgs);
            }

            return DOM.li({
                className: this.getClasses("jstree-leaf"),
                onClick: this.handleClick,
                onMouseDown: this.handleMouseDown,
                onDoubleClick: this.handleDoubleClick
            },
                DOM.ins({
                    className: "jstree-icon"
                }, " "),
                nameDisplay);
        }
    });

    /**
     * @private
     *
     * Creates a comparison function for sorting a directory's contents with directories
     * appearing before files.
     *
     * We're sorting the keys of the directory (the names) based partly on the values,
     * so we use a closure to capture the map itself so that we can look up the
     * values as needed.
     *
     * @param {Immutable.Map} contents The directory's contents
     * @return {function(string,string)} Comparator that sorts directories first.
     */
    function _buildDirsFirstComparator(contents) {
        function _dirsFirstCompare(a, b) {
            var aIsFile = FileTreeViewModel.isFile(contents.get(a)),
                bIsFile = FileTreeViewModel.isFile(contents.get(b));

            if (!aIsFile && bIsFile) {
                return -1;
            } else if (aIsFile && !bIsFile) {
                return 1;
            } else {
                return FileUtils.compareFilenames(a, b);
            }
        }
        return _dirsFirstCompare;
    }

    /**
     * @private
     *
     * Sort a directory either alphabetically or with subdirectories listed first.
     *
     * @param {Immutable.Map} contents the directory's contents
     * @param {boolean} dirsFirst true to sort subdirectories first
     * @return {Immutable.Map} sorted mapping
     */
    function _sortDirectoryContents(contents, dirsFirst) {
        if (dirsFirst) {
            return contents.keySeq().sort(_buildDirsFirstComparator(contents));
        } else {
            return contents.keySeq().sort(FileUtils.compareFilenames);
        }
    }

    // Forward references to keep JSLint happy.
    var directoryNode, directoryContents;

    /**
     * @private
     *
     * Component that provides the input for renaming a directory.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the file, including the extension
     * * actions: the action creator responsible for communicating actions the user has taken
     */
    var directoryRenameInput = React.createClass({
        mixins: [renameBehavior],

        render: function () {
            var width = _measureText(this.props.name);

            return DOM.input({
                className: "jstree-rename-input",
                type: "text",
                defaultValue: this.props.name,
                autoFocus: true,
                onKeyDown: this.handleKeyDown,
                onKeyUp: this.handleKeyUp,
                ref: "name",
                style: {
                    width: width
                }
            });
        }
    });

    /**
     * @private
     *
     * Displays a directory (but not its contents) in the tree.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the directory
     * * entry: the object with the relevant metadata for the file (whether it's selected or is the context file)
     * * actions: the action creator responsible for communicating actions the user has taken
     * * sortDirectoriesFirst: whether the directories should be displayed first when listing the contents of a directory
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    directoryNode = React.createClass({
        mixins: [contextSettable, pathComputer, extendable],

        /**
         * We need to update this component if the sort order changes or our entry object
         * changes. Thanks to immutability, if any of the directory contents change, our
         * entry object will change.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.entry !== nextProps.entry ||
                this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
                this.props.extensions !== nextProps.extensions;
        },

        /**
         * If you click on a directory, it will toggle between open and closed.
         */
        handleClick: function (event) {
            if (event.button !== LEFT_MOUSE_BUTTON) {
                return;
            }

            var isOpen = this.props.entry.get("open"),
                setOpen = isOpen ? false : true;

            if (event.metaKey || event.ctrlKey) {
                // ctrl-alt-click toggles this directory and its children
                if (event.altKey) {
                    if (setOpen) {
                        // when opening, we only open the immediate children because
                        // opening a whole subtree could be really slow (consider
                        // a `node_modules` directory, for example).
                        this.props.actions.toggleSubdirectories(this.myPath(), setOpen);
                        this.props.actions.setDirectoryOpen(this.myPath(), setOpen);
                    } else {
                        // When closing, we recursively close the whole subtree.
                        this.props.actions.closeSubtree(this.myPath());
                    }
                } else {
                    // ctrl-click toggles the sibling directories
                    this.props.actions.toggleSubdirectories(this.props.parentPath, setOpen);
                }
            } else {
                // directory toggle with no modifier
                this.props.actions.setDirectoryOpen(this.myPath(), setOpen);
            }
            return false;
        },

        /**
         * Create the data object to pass to extensions.
         *
         * @return {{name: {string}, isFile: {boolean}, fullPath: {string}}} Data for extensions
         */
        getDataForExtension: function () {
            return {
                name: this.props.name,
                isFile: false,
                fullPath: this.myPath()
            };
        },

        render: function () {
            var entry = this.props.entry,
                nodeClass,
                childNodes,
                children = entry.get("children"),
                isOpen = entry.get("open"),
                directoryClasses = "";

            if (isOpen && children) {
                nodeClass = "open";
                childNodes = directoryContents({
                    parentPath: this.myPath(),
                    contents: children,
                    extensions: this.props.extensions,
                    actions: this.props.actions,
                    forceRender: this.props.forceRender,
                    platform: this.props.platform,
                    sortDirectoriesFirst: this.props.sortDirectoriesFirst
                });
            } else {
                nodeClass = "closed";
            }

            if (this.props.entry.get("selected")) {
                directoryClasses += " jstree-clicked sidebar-selection";
            }

            if (entry.get("context")) {
                directoryClasses += " context-node";
            }

            var nameDisplay;
            if (entry.get("rename")) {
                nameDisplay = directoryRenameInput({
                    actions: this.props.actions,
                    entry: this.props.entry,
                    name: this.props.name,
                    parentPath: this.props.parentPath
                });
            } else {
                // Need to flatten the arguments because getIcons returns an array
                var aArgs = _.flatten([{
                    href: "#",
                    className: directoryClasses
                }, this.getIcons(), this.props.name], true);
                nameDisplay = DOM.a.apply(DOM.a, aArgs);
            }

            return DOM.li({
                className: this.getClasses("jstree-" + nodeClass),
                onClick: this.handleClick,
                onMouseDown: this.handleMouseDown
            },
                DOM.ins({
                    className: "jstree-icon"
                }, " "),
                nameDisplay,
                childNodes);
        }
    });

    /**
     * @private
     *
     * Displays the contents of a directory.
     *
     * Props:
     * * isRoot: whether this directory is the root of the tree
     * * parentPath: the full path of the directory containing this file
     * * contents: the map of name/child entry pairs for this directory
     * * actions: the action creator responsible for communicating actions the user has taken
     * * sortDirectoriesFirst: whether the directories should be displayed first when listing the contents of a directory
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    directoryContents = React.createClass({

        /**
         * Need to re-render if the sort order or the contents change.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.contents !== nextProps.contents ||
                this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
                this.props.extensions !== nextProps.extensions;
        },

        render: function () {
            var extensions = this.props.extensions,
                iconClass = extensions && extensions.get("icons") ? "jstree-icons" : "jstree-no-icons",
                ulProps = this.props.isRoot ? {
                    className: "jstree-brackets jstree-no-dots " + iconClass
                } : null;

            var contents = this.props.contents,
                namesInOrder = _sortDirectoryContents(contents, this.props.sortDirectoriesFirst);

            return DOM.ul(ulProps, namesInOrder.map(function (name) {
                var entry = contents.get(name);

                if (FileTreeViewModel.isFile(entry)) {
                    return fileNode({
                        parentPath: this.props.parentPath,
                        name: name,
                        entry: entry,
                        actions: this.props.actions,
                        extensions: this.props.extensions,
                        forceRender: this.props.forceRender,
                        platform: this.props.platform,
                        key: name
                    });
                } else {
                    return directoryNode({
                        parentPath: this.props.parentPath,
                        name: name,
                        entry: entry,
                        actions: this.props.actions,
                        extensions: this.props.extensions,
                        sortDirectoriesFirst: this.props.sortDirectoriesFirst,
                        forceRender: this.props.forceRender,
                        platform: this.props.platform,
                        key: name
                    });
                }
            }.bind(this)).toArray());
        }
    });

    /**
     * Displays the absolutely positioned box for the selection or context in the
     * file tree. Its position is determined by passed-in info about the scroller in which
     * the tree resides and the top of the selected node (as reported by the node itself).
     *
     * Props:
     * * selectionViewInfo: Immutable.Map with width, scrollTop, scrollLeft and offsetTop for the tree container
     * * visible: should this be visible now
     * * widthAdjustment: if this box should not fill the entire width, pass in a positive number here which is subtracted from the width in selectionViewInfo
     */
    var fileSelectionBox = React.createClass({

        /**
         * Sets up initial state.
         */
        getInitialState: function () {
            return {
                initialScroll: 0
            };
        },

        /**
         * When the component has updated in the DOM, reposition it to where the currently
         * selected node is located now.
         */
        componentDidUpdate: function () {
            if (!this.props.visible) {
                return;
            }

            var node = this.getDOMNode(),
                selectedNode = $(node.parentNode).find(this.props.selectedClassName),
                selectionViewInfo = this.props.selectionViewInfo;

            if (selectedNode.length === 0) {
                return;
            }

            node.style.top = selectedNode.offset().top - selectionViewInfo.get("offsetTop") + selectionViewInfo.get("scrollTop") - selectedNode.position().top + "px";
        },

        render: function () {
            var selectionViewInfo = this.props.selectionViewInfo;

            return DOM.div({
                style: {
                    overflow: "auto",
                    left: selectionViewInfo.get("scrollLeft"),
                    width: selectionViewInfo.get("width") - this.props.widthAdjustment,
                    display: this.props.visible ? "block" : "none"
                },
                className: this.props.className
            });
        }
    });

    /**
     * @private
     *
     * This is the root component of the file tree.
     *
     * Props:
     * * treeData: the root of the tree (an Immutable.Map with the contents of the project root)
     * * sortDirectoriesFirst: whether the directories should be displayed first when listing the contents of a directory
     * * parentPath: the full path of the directory containing this file
     * * actions: the action creator responsible for communicating actions the user has taken
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    var fileTreeView = React.createClass({

        /**
         * Update for any change in the tree data or directory sorting preference.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.treeData !== nextProps.treeData ||
                this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
                this.props.extensions !== nextProps.extensions ||
                this.props.selectionViewInfo !== nextProps.selectionViewInfo;
        },

        render: function () {
            var selectionBackground = fileSelectionBox({
                ref: "selectionBackground",
                selectionViewInfo: this.props.selectionViewInfo,
                className: "filetree-selection",
                visible: this.props.selectionViewInfo.get("hasSelection"),
                widthAdjustment: 0,
                selectedClassName: ".selected-node",
                forceUpdate: true
            }),
                contextBackground = fileSelectionBox({
                    ref: "contextBackground",
                    selectionViewInfo: this.props.selectionViewInfo,
                    className: "filetree-context",
                    visible: this.props.selectionViewInfo.get("hasContext"),
                    widthAdjustment: 0,
                    selectedClassName: ".context-node",
                    forceUpdate: true
                });

            return DOM.div(
                null,
                selectionBackground,
                contextBackground,
                directoryContents({
                    isRoot: true,
                    parentPath: this.props.parentPath,
                    sortDirectoriesFirst: this.props.sortDirectoriesFirst,
                    contents: this.props.treeData,
                    extensions: this.props.extensions,
                    actions: this.props.actions,
                    forceRender: this.props.forceRender,
                    platform: this.props.platform
                })
            );
        }
    });

    /**
     * Renders the file tree to the given element.
     *
     * @param {DOMNode|jQuery} element Element in which to render this file tree
     * @param {FileTreeViewModel} viewModel the data container
     * @param {Directory} projectRoot Directory object from which the fullPath of the project root is extracted
     * @param {ActionCreator} actions object with methods used to communicate events that originate from the user
     * @param {boolean} forceRender Run render on the entire tree (useful if an extension has new data that it needs rendered)
     * @param {string} platform mac, win, linux
     */
    function render(element, viewModel, projectRoot, actions, forceRender, platform) {
        if (!projectRoot) {
            return;
        }

        React.renderComponent(fileTreeView({
            treeData: viewModel.treeData,
            selectionViewInfo: viewModel.selectionViewInfo,
            sortDirectoriesFirst: viewModel.sortDirectoriesFirst,
            parentPath: projectRoot.fullPath,
            actions: actions,
            extensions: _extensions,
            platform: platform,
            forceRender: forceRender
        }),
              element);
    }

    /**
     * @private
     *
     * Add an extension for the given category (icons, addClass).
     *
     * @param {string} category Category to which the extension is being added
     * @param {function} callback The extension function itself
     */
    function _addExtension(category, callback) {
        if (!callback || typeof callback !== "function") {
            console.error("Attempt to add FileTreeView", category, "extension without a callback function");
            return;
        }
        var callbackList = _extensions.get(category);
        if (!callbackList) {
            callbackList = Immutable.Vector();
        }
        callbackList = callbackList.push(callback);
        _extensions = _extensions.set(category, callbackList);
    }

    /**
     * Adds an icon provider. The icon provider is a function which takes a data object and
     * returns a React.DOM.ins instance for the icons within the tree. The callback can
     * alternatively return a string, DOM node or a jQuery object for the ins node to be added.
     *
     * The data object contains:
     *
     * * `name`: the file or directory name
     * * `fullPath`: full path to the file or directory
     * * `isFile`: true if it's a file, false if it's a directory
     */
    function addIconProvider(callback) {
        _addExtension("icons", callback);
    }

    /**
     * Adds an additional classes provider which can return classes that should be added to a
     * given file or directory in the tree.
     *
     * The data object contains:
     *
     * * `name`: the file or directory name
     * * `fullPath`: full path to the file or directory
     * * `isFile`: true if it's a file, false if it's a directory
     */
    function addClassesProvider(callback) {
        _addExtension("addClass", callback);
    }

    // Private API for testing
    exports._sortFormattedDirectory = _sortDirectoryContents;
    exports._fileNode = fileNode;
    exports._directoryNode = directoryNode;
    exports._directoryContents = directoryContents;
    exports._fileTreeView = fileTreeView;

    // Public API
    exports.addIconProvider = addIconProvider;
    exports.addClassesProvider = addClassesProvider;
    exports.render = render;
});


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Accordion - Collapse content</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

Section 1

Mauris mauris ante, blandit et, ultrices a, suscipit eget, quam. Integer ut neque. Vivamus nisi metus, molestie vel, gravida in, condimentum sit amet, nunc. Nam a nibh. Donec suscipit eros. Nam mi. Proin viverra leo ut odio. Curabitur malesuada. Vestibulum a velit eu ante scelerisque vulputate.

Section 2

Sed non urna. Donec et ante. Phasellus eu ligula. Vestibulum sit amet purus. Vivamus hendrerit, dolor at aliquet laoreet, mauris turpis porttitor velit, faucibus interdum tellus libero ac justo. Vivamus non quam. In suscipit faucibus urna.

Section 3

Nam enim risus, molestie et, porta ac, aliquam ac, risus. Quisque lobortis. Phasellus pellentesque purus in massa. Aenean in pede. Phasellus ac libero ac tellus pellentesque semper. Sed ac felis. Sed commodo, magna quis lacinia ornare, quam ante aliquam nisi, eu iaculis leo purus venenatis dui.

List item one

List item two

List item three

Section 4

Cras dictum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aenean lacinia mauris vel est.

Suspendisse eu nisl. Nullam ut libero. Integer dignissim consequat lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

By default, accordions always keep one section open. To allow for all sections to be be collapsible, set the collapsible option to true. Click on the currently open section to collapse its content pane.


 /*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $ */

define(function (require, exports, module) {
    "use strict";

    var KeyBindingManager = require("command/KeyBindingManager"),
        KeyEvent          = require("utils/KeyEvent"),
        PopUpManager      = require("widgets/PopUpManager"),
        ViewUtils         = require("utils/ViewUtils");

    /**
     * Object to handle events for a dropdown list.
     *
     * DropdownEventHandler handles these events:
     *
     * Mouse:
     * - click       - execute selection callback and dismiss list
     * - mouseover   - highlight item
     * - mouseleave  - remove mouse highlighting
     *
     * Keyboard:
     * - Enter       - execute selection callback and dismiss list
     * - Esc         - dismiss list
     * - Up/Down     - change selection
     * - PageUp/Down - change selection
     *
     * Items whose <a> has the .disabled class do not respond to selection.
     *
     * @constructor
     * @param {jQueryObject} $list  associated list object
     * @param {Function} selectionCallback  function called when list item is selected.
     */
    function DropdownEventHandler($list, selectionCallback, closeCallback) {

        this.$list = $list;
        this.$items = $list.find("li");
        this.selectionCallback = selectionCallback;
        this.closeCallback = closeCallback;
        this.scrolling = false;

        /**
         * @private
         * The selected position in the list; otherwise -1.
         * @type {number}
         */
        this._selectedIndex = -1;
    }

    /**
     * Public open method
     */
    DropdownEventHandler.prototype.open = function () {
        var self = this;

        /**
         * Convert keydown events into hint list navigation actions.
         *
         * @param {KeyboardEvent} event
         * @return {boolean} true if key was handled, otherwise false.
         */
        function _keydownHook(event) {
            var keyCode;

            // (page) up, (page) down, enter and tab key are handled by the list
            if (event.type === "keydown") {
                keyCode = event.keyCode;

                if (keyCode === KeyEvent.DOM_VK_TAB) {
                    self.close();
                } else if (keyCode === KeyEvent.DOM_VK_UP) {
                    // Move up one, wrapping at edges (if nothing selected, select the last item)
                    self._tryToSelect(self._selectedIndex === -1 ? -1 : self._selectedIndex - 1, -1);
                } else if (keyCode === KeyEvent.DOM_VK_DOWN) {
                    // Move down one, wrapping at edges (if nothing selected, select the first item)
                    self._tryToSelect(self._selectedIndex === -1 ? 0 : self._selectedIndex + 1, +1);
                } else if (keyCode === KeyEvent.DOM_VK_PAGE_UP) {
                    // Move up roughly one 'page', stopping at edges (not wrapping) (if nothing selected, selects the first item)
                    self._tryToSelect((self._selectedIndex || 0) - self._itemsPerPage(), -1, true);
                } else if (keyCode === KeyEvent.DOM_VK_PAGE_DOWN) {
                    // Move down roughly one 'page', stopping at edges (not wrapping) (if nothing selected, selects the item one page down from the top)
                    self._tryToSelect((self._selectedIndex || 0) + self._itemsPerPage(), +1, true);

                } else if (keyCode === KeyEvent.DOM_VK_HOME) {
                    self._tryToSelect(0, +1);
                } else if (keyCode === KeyEvent.DOM_VK_END) {
                    self._tryToSelect(self.$items.length - 1, -1);

                } else if (self._selectedIndex !== -1 &&
                        (keyCode === KeyEvent.DOM_VK_RETURN)) {

                    // Trigger a click handler to commmit the selected item
                    self._selectionHandler();
                } else {
                    // Let the event bubble.
                    return false;
                }

                event.stopImmediatePropagation();
                event.preventDefault();
                return true;
            }

            // If we didn't handle it, let other global keydown hooks handle it.
            return false;
        }

        /**
         * PopUpManager callback
         */
        function closeCallback() {
            KeyBindingManager.removeGlobalKeydownHook(_keydownHook);
            self._cleanup();
        }

        KeyBindingManager.addGlobalKeydownHook(_keydownHook);

        if (this.$list) {
            this._registerMouseEvents();
            PopUpManager.addPopUp(this.$list, closeCallback, true);
        }
    };

    /**
     * Public close method
     */
    DropdownEventHandler.prototype.close = function () {
        if (this.$list) {
            PopUpManager.removePopUp(this.$list);
        }
    };

    /**
     * Cleanup
     */
    DropdownEventHandler.prototype._cleanup = function () {
        if (this.$list) {
            this.$list.off(".dropdownEventHandler");
        }
        if (this.closeCallback) {
            this.closeCallback();
        }
    };

    /**
     * Try to select item at the given index. If it's disabled or a divider, keep trying by incrementing
     * index by 'direction' each time (wrapping around if needed).
     * @param {number} index  If out of bounds, index either wraps around to remain in range (e.g. -1 yields
     *                      last item, length+1 yields 2nd item) or if noWrap set, clips instead (e.g. -1 yields
     *                      first item, length+1 yields last item).
     * @param {number} direction  Either +1 or -1
     * @param {boolean=} noWrap  Clip out of range index values instead of wrapping. Default false (wrap).
     */
    DropdownEventHandler.prototype._tryToSelect = function (index, direction, noWrap) {
        // Fix up 'index' if out of bounds (>= len or < 0)
        var len = this.$items.length;
        if (noWrap) {
            // Clip to stay in range (and set direction so we don't wrap in the recursion case either)
            if (index < 0) {
                index = 0;
                direction = +1;
            } else if (index >= len) {
                index = len - 1;
                direction = -1;
            }
        } else {
            // Wrap around to keep index in bounds
            index %= len;
            if (index < 0) {
                index += len;
            }
        }

        var $item = this.$items.eq(index);
        if ($item.hasClass("divider") || $item.find("a.disabled").length) {
            // Desired item is ineligible for selection: try next one
            this._tryToSelect(index + direction, direction, noWrap);
        } else {
            this._setSelectedIndex(index, true);
        }
    };

    /**
     * @return {number} The number of items per scroll page.
     */
    DropdownEventHandler.prototype._itemsPerPage = function () {
        var itemsPerPage = 1,
            itemHeight;

        if (this.$items.length !== 0) {
            itemHeight = $(this.$items[0]).height();
            if (itemHeight) {
                // round down to integer value
                itemsPerPage = Math.floor(this.$list.height() / itemHeight);
                itemsPerPage = Math.max(1, Math.min(itemsPerPage, this.$items.length));
            }
        }

        return itemsPerPage;
    };

    /**
     * Call selectionCallback with selected index
     */
    DropdownEventHandler.prototype._selectionHandler = function () {

        if (this._selectedIndex === -1) {
            return;
        }

        var $link = this.$items.eq(this._selectedIndex).find("a");
        this._clickHandler($link);
    };

    /**
     * Call selectionCallback with selected item
     *
     * @param {jQueryObject} $item
     */
    DropdownEventHandler.prototype._clickHandler = function ($link) {

        if (!this.selectionCallback || !this.$list || !$link) {
            return;
        }
        if ($link.hasClass("disabled")) {
            return;
        }

        this.selectionCallback($link);
        PopUpManager.removePopUp(this.$list);
    };

    /**
     * Select the item in the hint list at the specified index, or remove the
     * selection if index < 0.
     *
     * @private
     * @param {number} index
     */
    DropdownEventHandler.prototype._setSelectedIndex = function (index, scrollIntoView) {

        // Range check
        index = Math.max(-1, Math.min(index, this.$items.length - 1));

        // Clear old highlight
        if (this._selectedIndex !== -1) {
            this.$items.eq(this._selectedIndex).find("a").removeClass("selected");
        }

        this._selectedIndex = index;

        // Highlight the new selected item, if necessary
        if (this._selectedIndex !== -1) {
            var $item = this.$items.eq(this._selectedIndex);

            $item.find("a").addClass("selected");
            if (scrollIntoView) {
                this.scrolling = true;
                ViewUtils.scrollElementIntoView(this.$list, $item, false);
            }
        }
    };

    /**
     * Register mouse event handlers
     */
    DropdownEventHandler.prototype._registerMouseEvents = function () {
        var self = this;

        this.$list
            .on("click.dropdownEventHandler", "a", function () {
                self._clickHandler($(this));
            })
            .on("mouseover.dropdownEventHandler", "a", function (e) {
                // Don't select item under mouse cursor when scrolling.
                if (self.scrolling) {
                    self.scrolling = false;
                    return;
                }

                var $link = $(e.currentTarget),
                    $item = $link.closest("li"),
                    viewOffset = self.$list.offset(),
                    elementOffset = $item.offset();

                // Only set selected if enabled & in view
                // (dividers are already screened out since they don't have an "a" tag in them)
                if (!$link.hasClass("disabled")) {
                    if (elementOffset.top < viewOffset.top + self.$list.height() && viewOffset.top <= elementOffset.top) {
                        self._setSelectedIndex(self.$items.index($item), false);
                    }
                }
            });
    };

    /**
     * Re-register mouse event handlers
     * @param {!jQueryObject} $list  newly updated list object
     */
    DropdownEventHandler.prototype.reRegisterMouseHandlers = function ($list) {
        if (this.$list) {
            this.$list.off(".dropdownEventHandler");

            this.$list = $list;
            this.$items = $list.find("li");

            this._registerMouseEvents();
        }
    };

    // Export public API
    exports.DropdownEventHandler    = DropdownEventHandler;
});


// Load modules

var Crypto = require('crypto');
var Boom = require('boom');

// Declare internals

var internals = {};

// Generate a cryptographically strong pseudo-random data

exports.randomString = function (size) {

    var buffer = exports.randomBits((size + 1) * 6);
    if (buffer instanceof Error) {
        return buffer;
    }

    var string = buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
    return string.slice(0, size);
};

exports.randomBits = function (bits) {

    if (!bits ||
        bits < 0) {

        return Boom.internal('Invalid random bits count');
    }

    var bytes = Math.ceil(bits / 8);
    try {
        return Crypto.randomBytes(bytes);
    }
    catch (err) {
        return Boom.internal('Failed generating random bits: ' + err.message);
    }
};

// Compare two strings using fixed time algorithm (to prevent time-based analysis of MAC digest match)

exports.fixedTimeComparison = function (a, b) {

    if (typeof a !== 'string' ||
        typeof b !== 'string') {

        return false;
    }

    var mismatch = (a.length === b.length ? 0 : 1);
    if (mismatch) {
        b = a;
    }

    for (var i = 0, il = a.length; i < il; ++i) {
        var ac = a.charCodeAt(i);
        var bc = b.charCodeAt(i);
        mismatch += (ac === bc ? 0 : 1);
    }

    return (mismatch === 0);
};