PMA.UI Documentation by Pathomation

components/js/tree.js

import { Resources } from '../../resources/resources';
import { SessionLogin } from './sessionlogin';
import { checkBrowserCompatibility } from '../../view/helpers';
import { Events, ApiMethods, callApiMethod, getThumbnailUrl } from './components';
import $ from 'jquery';
import { createTree } from 'jquery.fancytree/dist/modules/jquery.fancytree';
import 'jquery.fancytree/dist/modules/jquery.fancytree.dnd5';
import 'jquery.fancytree/dist/skin-win8/ui.fancytree.min.css';
import 'font-awesome/css/font-awesome.css';

let PmaStartUrl = "http://127.0.0.1:54001/";

export
    /**
    * Represents a UI component that shows a tree view that allows browsing through the directories and slides from multiple PMA.core servers. This component uses {@link https://github.com/mar10/fancytree|fancytree} under the hood.
    * @memberof PMA.UI.Components
    * @alias Tree
    * @param  {Context} context
    * @param  {object} options - Configuration options
    * @param  {Tree~server[]} options.servers An array of servers to show files from
    * @param {string|HTMLElement} options.element - The element that hosts the tree view. It can be either a valid CSS selector or an HTMLElement instance.
    * @param  {function} [options.renderNode] - Allows tweaking after node state was rendered
    * @param  {Tree~rootDirSortCb} [options.rootDirSortCb] - Function that sorts an array of directories
    * @param  {Tree~filterDirectoryCb} [options.filterDirectoryCb] - Function that filters directories before they appear in the tree view
    * @param  {Tree~filterFileCb} [options.filterFileCb] - Function that filters files before they appear in the tree view
    * @param  {boolean} [options.checkboxes=false] - Allows multi selection of files with checkboxes
    * @param  {boolean} [options.autoDetectPmaStart=false] - Whether the tree should try to connect to a PMA.core lite server
    * @param  {boolean} [options.autoExpandNodes=false] - Whether the tree should expand nodes on single click
    * @param  {boolean} [options.preview=false] - Whether the tree should show a popover preview of slides
    * @param  {boolean} [options.search=false] - Whether the tree should show a textbox for enabling search
    * @fires PMA.UI.Components.Events.SlideSelected
    * @fires PMA.UI.Components.Events.DirectorySelected
    * @fires PMA.UI.Components.Events.ServerSelected
    * @fires PMA.UI.Components.Events.MultiSelectionChanged
    * @fires PMA.UI.Components.Events.TreeNodeDoubleClicked
    * @fires PMA.UI.Components.Events.ServerExpanded
    * @fires PMA.UI.Components.Events.DirectoryExpanded
    * @fires PMA.UI.Components.Events.SearchFinished
    * @fires PMA.UI.Components.Events.SearchFailed
    * @tutorial 04-tree
    */
    class Tree {
    /**
     * Holds information about a PMA.core server
     * @typedef {Object} Tree~server
     * @property {string} name - The display name of the server
     * @property {string} url - The url of the server
     * @property {string} [path] - An optional path of a folder in the server to show
     * @property {boolean} [showFiles=true] - Whether or not to display slides along with directories
     */

    /**
     * Holds information about a tree node
     * @typedef {Object} Tree~item
     * @property {string} serverUrl - The url of the server this node belongs to
     * @property {string} path - The path of the node
     * @property {boolean} isDirectory - Whether or not to the node is a directory
     */

    /**
     * Function that sorts an array of directories
     * @callback Tree~rootDirSortCb
     * @param {String[]} directories
     * @returns {String[]}
     */

    /**
     * Function that filters a directory given it's path
     * @callback Tree~filterDirectoryCb
     * @param {String} dirPath
     * @returns {boolean}
     */

    /**
     * Function that filters a file given it's path
     * @callback Tree~filterFileCb
     * @param {String} filePath
     * @returns {boolean}
     */

    /**
    * Function that gets called when an attempt to add a server completes.
    * @callback Tree~addServerCallback
    * @param {boolean} success - Whether the server was successfully added or not
    */

    constructor(context, options) {
        if (!checkBrowserCompatibility()) {
            return;
        }

        this.element = options.element;
        if (typeof options.element == "string") {
            var el = document.querySelector(options.element);
            if (!el) {
                console.error("Invalid selector for element");
            }
            else {
                this.element = el;
            }
        }

        this.context = context;
        this.servers = options.servers || [];
        var _this = this;

        this.navigating = false;
        this.lastNavigatePathRequest = null;

        this.autoExpand = options.autoExpandNodes === true;
        this.checkboxes = options.checkboxes === true;
        this.lastSearchResults = {};
        this.listeners = {};
        this.listeners[Events.DirectorySelected] = [];
        this.listeners[Events.SlideSelected] = [];
        this.listeners[Events.ServerSelected] = [];
        this.listeners[Events.MultiSelectionChanged] = [];
        this.listeners[Events.TreeNodeDoubleClicked] = [];
        this.listeners[Events.ServerExpanded] = [];
        this.listeners[Events.DirectoryExpanded] = [];
        this.listeners[Events.SearchFinished] = [];
        this.listeners[Events.SearchFailed] = [];

        this.lastSearchHash = 0;
        this.previewEnabled = false;

        if (typeof options.rootDirSortCb === "function") {
            this.rootDirSortCb = options.rootDirSortCb;
        }

        if (typeof options.filterDirectoryCb === "function") {
            this.filterDirectoryCb = options.filterDirectoryCb;
        }
        else {
            this.filterDirectoryCb = null;
        }

        if (typeof options.filterFileCb === "function") {
            this.filterFileCb = options.filterFileCb;
        }
        else {
            this.filterFileCb = null;
        }

        var sourceData = [];
        for (var i = 0; i < this.servers.length; i++) {
            sourceData.push({
                title: this.servers[i].name,
                serverNode: true,
                key: this.servers[i].url,
                serverIndex: i,
                extraClasses: "server",
                dirPath: (this.servers[i].path ? this.servers[i].path : "/"),
                lazy: true,
                unselectableStatus: false,
                unselectable: true,
                selected: false,
                checkbox: false
            });

            // try get version for each server
            this.context.getVersionInfo(this.servers[i].url, serverVersionResult.bind(this, this.servers[i]));
        }

        var searchBox = null;
        if (options.search === true) {
            var cls = options.searchClass ? options.searchClass : 'pma-ui-tree-search-box';
            searchBox = $("<input type='text' class='" + cls + "' placeholder='" + Resources.translate("Search") + "'/><hr />").appendTo(this.element);
        }

        this.fancytree = createTree(this.element, {
            keyPathSeparator: "?",
            checkbox: options.checkboxes === true,
            extensions: ["dnd5", /*"glyph", "wide"*/],
            dnd5: {
                dragStart: function (node, data) {
                    // Called when user starts dragging `node`.
                    // This method MUST be defined to enable dragging for tree nodes.
                    //
                    // We can
                    //   Add or modify the drag data using `data.dataTransfer.setData()`
                    //   Return false to cancel dragging of `node`.

                    // For example:
                    //    if( data.originalEvent.shiftKey ) ...          
                    //    if( node.isFolder() ) { return false; }
                    if (node.data.dirPath && !node.data.serverNode) {
                        node.data.dragging = true;
                        if (node.isActive) {
                            node.setActive(false);
                        }

                        data.dataTransfer.setData("text", JSON.stringify({
                            serverUrl: node.data.serverUrl,
                            path: node.data.dirPath,
                            isFolder: node.isFolder()
                        }));

                        return true;
                    }

                    return false;
                },
                dragEnd: function (node, data) {
                    node.data.dragging = false;
                }
            },
            //glyph: glyph_opts,
            selectMode: 3,
            toggleEffect: { options: { direction: "left" }, duration: 400 },
            // toggleEffect: false,
            wide: {
                iconWidth: "1em", // Adjust this if @fancy-icon-width != "16px"
                iconSpacing: "0.5em", // Adjust this if @fancy-icon-spacing != "3px"
                levelOfs: "1.5em" // Adjust this if ul padding != "16px"
            },
            icon: function (event, data) {
                if (data.node.key === "_searchResults") {
                    if (data.node.data.searching) {
                        return "fa fa-spinner fa-spin";
                    }
                    else {
                        return "fa fa-search";
                    }
                }
                else if (data.node.data.serverNode === true) {
                    if (data.node.data.authenticated === true) {
                        return "server success";
                    } else if (data.node.data.authenticated === false) {
                        return "server error";
                    } else {
                        return "server";
                    }
                }
                else if (data.node.data.rootDir === true) {
                    return "rootdir";
                }
                else if (!data.node.isFolder()) {
                    return "image";
                }
            },
            renderNode: (typeof options.renderNode === "function" ? options.renderNode : null),
            source: sourceData,
            lazyLoad: loadNode.bind(this),
            activate: function (event, data) {
                // A node was activated:
                var node = data.node;
                if (node.key == "_searchResults" && _this.autoExpand === true) {
                    node.setExpanded(true);
                }

                setTimeout(function () {
                    if (node.data && node.data.dragging === true) {
                        event.preventDefault();
                        return;
                    }

                    if (node.data.serverNode === true) {
                        _this.fireEvent(Events.ServerSelected, { serverUrl: _this.servers[node.data.serverIndex].url });
                        return;
                    }

                    if (node.data.dirPath !== "/") {
                        if (node.isFolder()) {
                            if (_this.autoExpand === true) {
                                node.setExpanded(true);
                            }

                            _this.fireEvent(Events.DirectorySelected, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.dirPath });
                        }
                        else {
                            if (_this.servers[node.data.serverIndex]) {
                                _this.fireEvent(Events.SlideSelected, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.dirPath });
                            }
                        }
                    }
                    else if (_this.autoExpand === true) {
                        node.setExpanded(true);
                    }
                }, 300);
            },
            select: function (event, data) {
                var n = data.tree.getSelectedNodes();
                var selectionArray = [];

                if (n && n.length > 0) {
                    for (var i = 0; i < n.length; i++) {
                        if (!(n[i] === null || n[i].data.serverNode === true || n[i].data.rootDir === true || n[i].isFolder())) {
                            selectionArray.push({ serverUrl: _this.servers[n[i].data.serverIndex].url, path: n[i].data.dirPath });
                        }
                    }
                }

                _this.fireEvent(Events.MultiSelectionChanged, selectionArray);
            },
            expand: function (event, data) {
                var node = data.node;
                setTimeout(function () {
                    if (node.data.serverNode === true) {
                        _this.fireEvent(Events.ServerExpanded, { serverUrl: _this.servers[node.data.serverIndex].url });
                        return;
                    }
                    else if (node.data.dirPath !== "/") {
                        if (node.isFolder()) {
                            _this.fireEvent(Events.DirectoryExpanded, { serverUrl: _this.servers[node.data.serverIndex].url, path: node.data.dirPath });
                            return;
                        }
                    }
                }, 300);
            },
            dblclick: function (event, data) {
                var node = data.node;
                _this.fireEvent(Events.TreeNodeDoubleClicked,
                    {
                        serverUrl: _this.servers[node.data.serverIndex].url,
                        path: node.data.dirPath,
                        isSlide: !(node.data.serverNode === true || node.data.rootDir === true || node.isFolder())
                    });
            }
        });

        if (options.preview === true) {
            enablePreview.call(this, this.fancytree);
        }

        if (options.autoDetectPmaStart === true) {
            this.addPmaStartServer();
        }

        if (options.search === true) {
            $(this.element).find(".ui-fancytree").addClass("ui-fancytree-search");
        }

        // this.fancytree = $.ui.fancytree.getTree(this.element);

        var self = this;
        var searchTimeout = null;
        var t = this.fancytree;
        if (searchBox) {
            searchBox.on('input propertychange paste', function () {
                var val = $(this).val();
                if (val && val.length > 3) {
                    clearTimeout(searchTimeout);
                    searchTimeout = setTimeout(function () {
                        startSearch.call(self, val);
                    }, 500);
                }
                else {
                    var n = t.getNodeByKey("_searchResults");
                    if (n) {
                        n.remove();
                    }
                }
            });
        }
    }

    /**
     * Toggle the live preview on/off
     * @param {boolean} enable 
     */
    togglePreview(enable) {
        if (enable && !this.previewEnabled) {
            enablePreview.call(this, $(this.element));
        }
        else if (!enable && this.previewEnabled) {
            disablePreview.call(this, $(this.element));
        }
    }

    /**
     * Adds a new server to the tree
     * @param  {Tree~server} server A server object
     */
    addServer(server) {
        if (server) {
            this.servers.push(server);

            var serverInfo = {
                title: this.servers[this.servers.length - 1].name,
                serverNode: true,
                key: this.servers[this.servers.length - 1].url,
                serverIndex: this.servers.length - 1,
                extraClasses: "server",
                dirPath: (this.servers[this.servers.length - 1].path ? this.servers[this.servers.length - 1].path : "/"),
                lazy: true,
                unselectableStatus: false,
                unselectable: true,
                selected: false,
                checkbox: false
            };

            // try get version for server
            this.context.getVersionInfo(server.url, serverVersionResult.bind(this, server));
            this.fancytree.getRootNode().addChildren(serverInfo);
        }
    }

    /**
     * Removes a server from the tree
     * @param {number} index The index of the server to remove
     */
    removeServer(index) {
        var children = this.fancytree.getRootNode().getChildren();
        if (children && children.length && index >= 0 && index < children.length) {
            if (children[index].data) {
                this.servers.splice(children[index].data.serverIndex, 1);
            }

            children[index].remove();
        }
        else {
            console.error("No children found or index out of range");
        }
    }

    /**
     * Removes a Pma.start server if it exists
     */
    removePmaStartServer() {
        var children = this.fancytree.getRootNode().getChildren();
        for (var i = 0; i < children.length; i++) {
            if (children[i].key == PmaStartUrl) {
                this.removeServer(children[i].data.serverIndex);
            }
        }
    }

    /**
     * Add a pma.start server if available
     * @param {Tree~addServerCallback} callback - The function to call when the attempt to add server completes
     */
    addPmaStartServer(callback) {
        // This function adds a SessionLogin provider to the context as many times as it is called. 
        // This needs fixing
        var _this = this;
        var children = this.fancytree.getRootNode().getChildren();
        for (var i = 0; i < children.length; i++) {
            if (children[i].key == PmaStartUrl) {
                return;
            }
        }

        callApiMethod({
            method: ApiMethods.GetVersionInfo,
            httpMethod: "GET",
            data: { rnd: Math.random() },
            serverUrl: PmaStartUrl,
            success: function () {
                new SessionLogin(_this.context, [{ serverUrl: PmaStartUrl, sessionId: "pma.core.lite" }]);
                _this.addServer({ name: Resources.translate("Computer"), url: PmaStartUrl });
                if (typeof callback === "function") {
                    callback.call(this, true);
                }
            },
            failure: function () {
                if (typeof callback === "function") {
                    callback.call(this, false);
                }
            }
        });
    }

    /**
     * Returns the list of servers currently under the tree view
     * @returns {Tree~server[]}
     */
    getServers() {
        return this.servers;
    }

    /**
     * Gets the currently selected slide or null
     * @returns {Tree~server}
     */
    getSelectedSlide() {
        var n = this.fancytree.getActiveNode();
        if (n === null || n.data.serverNode === true || n.data.rootDir === true || n.isFolder()) {
            return null;
        }

        return { server: this.servers[n.data.serverIndex].url, path: n.data.dirPath };
    }

    /**
     * Gets the currently selected directory or null
     * @returns {Tree~server}
     */
    getSelectedDirectory() {
        var n = this.fancytree.getActiveNode();
        if (n !== null && (n.data.rootDir === true || n.isFolder())) {
            return { server: this.servers[n.data.serverIndex].url, path: n.data.dirPath };
        }

        return null;
    }

    /**
     * Gets an array with the checked slides or an empty array
     * @param {("slides"|"directories"|"all")} [contentType="slides"] - The type of content to return
     * @returns {Tree~item[]}
     */
    getMultiSelection(contentType) {
        const contentTypes = ["slides", "directories", "all"];
        if (contentTypes.indexOf(contentType) === -1) {
            contentType = contentTypes[0];
        }

        var n = this.fancytree.getSelectedNodes();
        var selectionArray = [];
        if (n && n.length > 0) {
            for (var i = 0; i < n.length; i++) {
                if (n[i] === null || n[i].data.serverNode === true) {
                    continue;
                }

                const isDir = n[i].data.rootDir || n[i].isFolder();
                const addResult = (contentType === "slides" && !isDir) ||
                    (contentType === "directories" && isDir) ||
                    (contentType === "all");

                if (addResult) {
                    selectionArray.push({ serverUrl: this.servers[n[i].data.serverIndex].url, path: n[i].data.dirPath, isDirectory: isDir });
                }
            }
        }

        return selectionArray;
    }

    /**
     * Clears the selected nodes in the tree view
     */
    clearMultiSelection() {
        this.fancytree.selectAll(false);
    }

    /**
     * Navigates to a path in the tree
     * @param {string} path - The virtual path to navigate to. The server part of the path should be the server NAME(not the server url)
     */
    navigateTo(path) {
        // fancy tree needs a path separated with options.keyPathSeparator 
        //  ex. ?key1?key2?key3
        var self = this;
        if (self.navigating) {
            self.lastNavigatePathRequest = path;
            return;
        }

        self.navigating = true;

        var tree = this.fancytree;
        var key = virtualPathToTreePath.call(this, path, tree);

        return tree.loadKeyPath(key, function (node, status) {
            if (status === "ok") {
                node.setActive();
                node.scrollIntoView();
                self.navigating = false;
            }
        }).done(function () {
            self.navigating = false;

            if (self.lastNavigatePathRequest) {
                var p = self.lastNavigatePathRequest;
                self.lastNavigatePathRequest = null;
                self.navigateTo(p);
            }
        });
    }

    /**
     * Refreshes a node in the tree specified by the path (server or directory)
     * @param {string} path - The virtual path to refresh. The server part of the path should be the server NAME(not the server url)
     */
    refresh(path) {
        var tree = this.fancytree;
        var key = virtualPathToTreePath.call(this, path, tree);

        return tree.loadKeyPath(key, function (node, status) {
            if (status === "ok") {
                node.resetLazy();
            }
        });
    }

    /**
     * Returns the last search results, grouped by the server name
     * @return {Object.<string, string[]>} The object containing the result for each server available
     */
    getSearchResults() {
        return this.lastSearchResults;
    }

    /**  
     * Clears any search results for this tree
    */
    clearSearchResults() {
        var tree = this.fancytree;
        var n = tree.getNodeByKey("_searchResults");
        if (n) {
            n.remove();
        }
    }

    /**
     * Searches for files matching the specified value, and appends them to the tree. Use {@link Tree#getSearchResults|getSearchResults} to get the search results
     * @param {string} value - The value to search for
     * @fires PMA.UI.Components.Events.SearchFinished
     */
    search(value) {
        this.clearSearchResults();
        return startSearch.call(this, value);
    }

    /**
     * Attaches an event listener
     * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
     * @param {function} callback - The function to call when the event occurs
     */
    listen(eventName, callback) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " is not a valid event");
        }

        this.listeners[eventName].push(callback);
    }

    // fires an event
    fireEvent(eventName, eventArgs) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

        for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
            this.listeners[eventName][i].call(this, eventArgs);
        }
    }

    /**
     * Gets a value indicating whether files are shown for a specified server
     * @param {string} serverUrl - A server url to show/hide files for
     */
    getFilesVisibility(serverUrl) {
        var children = this.fancytree.getRootNode().getChildren();
        for (var i = 0; i < children.length; i++) {
            if (children[i].key == serverUrl) {
                for (var j = 0; j < this.servers.length; j++) {
                    if (this.servers[j].url == children[i].key) {
                        return this.servers[j].showFiles;
                    }
                }
            }
        }
    }

    /**
     * Shows or hides the files of a specified server
     * @param {string} serverUrl - A server url to show/hide files for
     * @param {boolean} visible - Whether to show or hide files for the specific server
     */
    setFilesVisibility(serverUrl, visible) {
        var children = this.fancytree.getRootNode().getChildren();
        for (var i = 0; i < children.length; i++) {
            if (children[i].key == serverUrl) {
                for (var j = 0; j < this.servers.length; j++) {
                    if (this.servers[j].url == children[i].key) {
                        this.servers[j].showFiles = visible;
                    }
                }

                children[i].resetLazy();
            }
        }
    }

    /**
     * Resets the state of all servers, collapses all visible folder, and resets the lazy load state
     */
    collapseAll() {
        var children = this.fancytree.getRootNode().getChildren();
        for (var i = 0; i < children.length; i++) {
            // children[i].setExpanded(false);
            children[i].resetLazy();
        }
    }

    /**
     * Resets the state of a specified server, collapses all visible folder, resets the lazy load state and invalidates the session 
     */
    signOut(serverUrl) {
        var children = this.fancytree.getRootNode().getChildren();
        for (var i = 0; i < children.length; i++) {
            if (children[i].key == serverUrl) {
                children[i].data.authenticated = undefined;
                children[i].renderTitle();
                children[i].resetLazy();
                this.context.deAuthenticate(serverUrl);
            }
        }
    }

}


function loadNode(event, data) {
    let dfd = new $.Deferred();
    data.result = dfd.promise();

    let node = data.node;
    let path = node.data.dirPath === "/" ? "" : node.data.dirPath;
    let _this = this;

    // if the directories to be loaded are root directories or sub directories
    var isRootDir = node.data.dirPath === "/";

    this.context.getDirectories(_this.servers[node.data.serverIndex].url, path,
        function (sessionId, directories) {
            if (node.data.serverNode) {
                node.data.authenticated = true;
                node.renderTitle();
            }
            if (_this.rootDirSortCb && path === "") {
                directories = _this.rootDirSortCb(directories);
            }

            let result = [];
            for (let i = 0; i < directories.length; i++) {
                if (_this.filterDirectoryCb && _this.filterDirectoryCb.call(_this, directories[i]) === false) {
                    continue;
                }

                result.push({
                    title: directories[i].split('/').pop(),
                    lazy: true,
                    serverIndex: node.data.serverIndex,
                    serverUrl: _this.servers[node.data.serverIndex].url,
                    dirPath: directories[i],
                    key: directories[i],
                    folder: true,
                    rootDir: isRootDir,
                    extraClasses: isRootDir ? "rootdir" : "subdir",
                    // unselectableStatus: true,
                    checkbox: _this.checkboxes
                });
            }

            if (_this.servers[node.data.serverIndex].showFiles !== false && path && path !== "") {
                _this.context.getSlides(
                    {
                        serverUrl: _this.servers[node.data.serverIndex].url,
                        path: path,
                        success: function (sessionId, files) {
                            for (let i = 0; i < files.length; i++) {
                                if (_this.filterFileCb && _this.filterFileCb.call(_this, files[i]) === false) {
                                    continue;
                                }

                                result.push({
                                    title: files[i].split('/').pop(),
                                    lazy: false,
                                    serverIndex: node.data.serverIndex,
                                    dirPath: files[i],
                                    serverUrl: _this.servers[node.data.serverIndex].url,
                                    key: files[i],
                                    folder: false,
                                    extraClasses: "slide"
                                });
                            }

                            dfd.resolve(result);
                        },
                        failure: function (error) {
                            dfd.reject(error.Message ? error.Message : Resources.translate("Error loading files"));
                        }
                    });
            }
            else {
                dfd.resolve(result);
            }
        },
        function (error) {
            if (node.data.serverNode) {
                node.data.authenticated = false;
                node.renderTitle();
            }
            dfd.reject(error.Message ? error.Message : Resources.translate("Error loading directories"));
        });
}

function searchResultsSuccess(searchNode, serverIndex, searchHash, sessionId, results) {
    if (this.lastSearchHash !== searchHash) {
        return;
    }

    this.lastSearchResults[this.servers[serverIndex].name] = results;

    var searchServerNode = searchNode.addChildren({
        title: this.servers[serverIndex].name,
        serverNode: true,
        key: "_searchServer_" + this.servers[serverIndex].url,
        serverIndex: serverIndex,
        dirPath: (this.servers[serverIndex].path ? this.servers[serverIndex].path : "/"),
        lazy: false,
        unselectableStatus: false,
        unselectable: true,
        selected: false,
        checkbox: false,
        resultCount: results.length
    });

    var tree = $(this.element).fancytree("getTree");
    for (var r = 0; r < results.length; r++) {
        var parts = results[r].split('/');

        var thispart = "";
        var currentLevel = searchServerNode;
        for (var i = 0; i < parts.length; i++) {
            thispart += i > 0 ? "/" + parts[i] : parts[i];

            var n = tree.getNodeByKey("_searchResult_" + thispart, currentLevel);
            if (n == null) {
                currentLevel = currentLevel.addChildren({
                    title: thispart.split("/").pop(),
                    lazy: false,
                    serverIndex: serverIndex,
                    dirPath: thispart,
                    key: "_searchResult_" + thispart,
                    folder: i < parts.length - 1,
                    rootDir: i == 0,
                    extraClasses: i == 0 ? "rootdir" : "subdir",
                    checkbox: this.checkboxes,
                    resultCount: 1
                });
            }
            else {
                currentLevel = n;
                n.data.resultCount += 1;
            }
        }
    }

    searchNode.data.resultCount += results.length;

    searchNode.visit(function (n) {
        if (!n.parent) {
            return;
        }

        if (n == searchNode) {
            n.setTitle(Resources.translate("Search results for \"{pattern}\" ({count})", { pattern: searchNode.data.pattern, count: n.data.resultCount }));
        }
        else if ((n.isFolder() || n.data.serverNode) && n == searchServerNode) {
            n.setTitle(n.title + " (" + n.data.resultCount + ")");
        }

    }, true);

    searchNode.data.serverDone++;
    if (searchNode.data.serverDone >= searchNode.data.serversSearched) {
        searchNode.data.searching = false;
        searchNode.renderTitle();
    }

    searchNode.makeVisible();
    searchNode.setExpanded(true);

    this.fireEvent(Events.SearchFinished, this.lastSearchResults);
}

function searchResultError(searchNode, serverIndex, searchHash) {
    if (this.lastSearchHash !== searchHash) {
        return;
    }

    searchNode.data.serverDone++;
    if (searchNode.data.serverDone >= searchNode.data.serversSearched) {
        searchNode.data.searching = false;
        searchNode.renderTitle();
    }

    this.fireEvent(Events.SearchFailed, this.servers[serverIndex]);
}

function serverVersionResult(server, version) {
    server.version = version;
}

function startSearch(pattern) {
    this.lastSearchResults = {};
    var searchResultsNode = $(this.element).fancytree("getTree").getNodeByKey("_searchResults");
    if (searchResultsNode == null) {
        var searchResultsNodeInfo = {
            title: Resources.translate("Search results for \"{pattern}\"", { pattern: pattern }),
            key: "_searchResults",
            lazy: false,
            selected: false,
            checkbox: false,
            resultCount: 0,
            searching: true,
            serverDone: 0,
            serversSearched: 0,
            pattern: pattern
        };

        searchResultsNode = this.fancytree.getRootNode().addChildren(searchResultsNodeInfo);
    }
    else {
        searchResultsNode.resultCount = 0;
        searchResultsNode.removeChildren();
        searchResultsNode.data.searching = true;
        searchResultsNode.data.serverDone = 0;
        searchResultsNode.data.serversSearched = 0;
        searchResultsNode.data.pattern = pattern;
        searchResultsNode.renderTitle();
    }

    searchResultsNode.makeVisible();
    searchResultsNode.setExpanded(true);

    var self = this;
    this.lastSearchHash = Math.random();
    for (var i = 0; i < this.servers.length; i++) {
        if (!this.servers[i].version || this.servers[i].version.substring(0, "1.".length) === "1.") {
            continue;
        }

        searchResultsNode.data.serversSearched++;
        this.context.queryFilename(
            this.servers[i].url,
            "",
            pattern,
            searchResultsSuccess.bind(self, searchResultsNode, i, this.lastSearchHash),
            searchResultError.bind(self, searchResultsNode, i, this.lastSearchHash)
        );
    }
}

/**
 * Converts a virtual path to a key used internaly by the tree component to locate any node
 * @param {string} path - The virtual path to convert. The server part should be the NAME of the server (not the url)
 * @param {Object} tree - The instance of the fancytree
 * @returns {string} The key to the node
 * @ignore
 */
function virtualPathToTreePath(path, tree) {
    var parts = path.split("/").filter(function (e) { return e != null && e != ""; });
    var initialPath = "";

    if (parts.length > 0) {
        // find server url as first part of path
        for (var i = 0; i < this.servers.length; i++) {
            if (this.servers[i].name.toLowerCase() == parts[0].toLowerCase()) {
                parts[0] = this.servers[i].url;
                initialPath = this.servers[i].path ? this.servers[i].path : "";
                break;
            }
        }

        if (initialPath) {
            parts[1] = initialPath + "/" + parts[1];
        }

        for (i = 2; i < parts.length; i++) {
            parts[i] = parts[i - 1] + "/" + parts[i];
        }
    }

    parts = parts.join(tree.options.keyPathSeparator);

    return parts;
}

function disablePreview(tree) {
    if (this.previewEnabled) {
        $(this.element).off("mouseenter", "span.fancytree-title");
        $(this.element).off("mouseleave", "span.fancytree-title");
        $(this.element).off("mousemove", "span.fancytree-title");

        $("#fancytree-preview").remove();
        this.previewEnabled = false;
    }
}

function enablePreview(tree) {
    if (!this.previewEnabled) {
        var xOffset = 100, yOffset = -30;
        var hoverTimeout = null;
        var _this = this;

        $(this.element).on("mouseenter", "span.fancytree-title", function (event) {
            // Add a hover handler to all node titles (using event delegation)
            var node = $.ui.fancytree.getNode(event);
            if (!(node === null || node.data.serverNode === true || node.data.rootDir === true || node.isFolder()) && node.data.serverIndex !== undefined) {
                var serverUrl = _this.servers[node.data.serverIndex].url;

                var f = function () {
                    _this.context.getSession(serverUrl, function (sessionId) {
                        var tUrl = getThumbnailUrl(serverUrl, sessionId, node.data.dirPath, 0, 150, 0);
                        var el = $("#fancytree-preview");
                        if (el.length > 0) {
                            el.remove();
                        }

                        el = $("<p id='fancytree-preview' class='fancytree-preview'><i class='fa fa-spinner fa-spin'></i><img/></p>").appendTo("body");
                        el.css("position", "absolute")
                            .css("top", (event.pageY + yOffset) + "px")
                            .css("left", (event.pageX + xOffset) + "px")
                            .fadeIn("fast");

                        el.find("img").bind("load", function () {
                            el.find("i").remove();
                        }).attr("src", tUrl);

                    });
                };

                hoverTimeout = setTimeout(f, 250);
            }
        });

        $(this.element).on("mouseleave", "span.fancytree-title", function () {
            $("#fancytree-preview").remove();
            if (hoverTimeout) {
                clearTimeout(hoverTimeout);
                hoverTimeout = null;
            }
        });

        $(this.element).on("mousemove", "span.fancytree-title", function (event) {
            var el = $("#fancytree-preview");
            if (el.length > 0) {
                el.css("top", (event.pageY + yOffset) + "px")
                    .css("left", (event.pageX + xOffset) + "px");
            }
        });

        this.previewEnabled = true;
    }
}