Browse Source

cmd/syncthing: UI for version restoration (fixes #2599) (#4602)

cmd/syncthing: Add UI for version restoration (fixes #2599)
Audrius Butkevicius 8 years ago
parent
commit
b0e2050cdb
33 changed files with 20045 additions and 65 deletions
  1. 40 0
      cmd/syncthing/gui.go
  2. 9 0
      cmd/syncthing/mocked_model_test.go
  3. 4 0
      gui/black/assets/css/theme.css
  4. 3 0
      gui/dark/assets/css/theme.css
  5. 4 0
      gui/default/assets/css/overrides.css
  6. 6 0
      gui/default/assets/css/theme.css
  7. 11 0
      gui/default/assets/lang/lang-en.json
  8. 10 0
      gui/default/index.html
  9. 72 0
      gui/default/syncthing/app.js
  10. 232 5
      gui/default/syncthing/core/syncthingController.js
  11. 13 13
      gui/default/syncthing/device/removeDeviceDialogView.html
  12. 16 16
      gui/default/syncthing/folder/removeFolderDialogView.html
  13. 15 0
      gui/default/syncthing/folder/restoreVersionsConfirmation.html
  14. 11 0
      gui/default/syncthing/folder/restoreVersionsMassActions.html
  15. 51 0
      gui/default/syncthing/folder/restoreVersionsModalView.html
  16. 17 0
      gui/default/syncthing/folder/restoreVersionsVersionSelector.html
  17. 269 0
      gui/default/vendor/bootstrap/css/daterangepicker.css
  18. 1626 0
      gui/default/vendor/bootstrap/js/daterangepicker.js
  19. 663 0
      gui/default/vendor/fancytree/css/ui.fancytree.css
  20. 12045 0
      gui/default/vendor/fancytree/jquery.fancytree-all-deps.js
  21. BIN
      gui/default/vendor/fancytree/skin-lion/icons.gif
  22. BIN
      gui/default/vendor/fancytree/skin-lion/loading.gif
  23. BIN
      gui/default/vendor/fancytree/skin-lion/vline.gif
  24. 4517 0
      gui/default/vendor/moment/moment.js
  25. 13 0
      lib/config/folderconfiguration.go
  26. 143 15
      lib/model/model.go
  27. 213 0
      lib/model/model_test.go
  28. 1 1
      lib/model/rwfolder.go
  29. 2 2
      lib/versioner/simple.go
  30. 2 2
      lib/versioner/simple_test.go
  31. 9 8
      lib/versioner/staggered.go
  32. 17 2
      lib/versioner/util.go
  33. 11 1
      lib/versioner/versioner.go

+ 40 - 0
cmd/syncthing/gui.go

@@ -40,6 +40,7 @@ import (
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/upgrade"
+	"github.com/syncthing/syncthing/lib/versioner"
 	"github.com/vitrun/qart/qr"
 	"golang.org/x/crypto/bcrypt"
 )
@@ -95,6 +96,8 @@ type modelIntf interface {
 	ResetFolder(folder string)
 	Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability
 	GetIgnores(folder string) ([]string, []string, error)
+	GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
+	RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
 	SetIgnores(folder string, content []string) error
 	DelayScan(folder string, next time.Duration)
 	ScanFolder(folder string) error
@@ -259,6 +262,7 @@ func (s *apiService) Serve() {
 	getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed)              // device folder [perpage] [page]
 	getRestMux.HandleFunc("/rest/db/status", s.getDBStatus)                      // folder
 	getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse)                      // folder [prefix] [dirsonly] [levels]
+	getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions)          // folder
 	getRestMux.HandleFunc("/rest/events", s.getIndexEvents)                      // [since] [limit] [timeout] [events]
 	getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents)                  // [since] [limit] [timeout]
 	getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats)                // -
@@ -287,6 +291,7 @@ func (s *apiService) Serve() {
 	postRestMux.HandleFunc("/rest/db/ignores", s.postDBIgnores)                    // folder
 	postRestMux.HandleFunc("/rest/db/override", s.postDBOverride)                  // folder
 	postRestMux.HandleFunc("/rest/db/scan", s.postDBScan)                          // folder [sub...] [delay]
+	postRestMux.HandleFunc("/rest/folder/versions", s.postFolderVersionsRestore)   // folder <body>
 	postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig)              // <body>
 	postRestMux.HandleFunc("/rest/system/error", s.postSystemError)                // <body>
 	postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear)     // -
@@ -1309,6 +1314,41 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, comp)
 }
 
+func (s *apiService) getFolderVersions(w http.ResponseWriter, r *http.Request) {
+	qs := r.URL.Query()
+	versions, err := s.model.GetFolderVersions(qs.Get("folder"))
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+	sendJSON(w, versions)
+}
+
+func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Request) {
+	qs := r.URL.Query()
+
+	bs, err := ioutil.ReadAll(r.Body)
+	r.Body.Close()
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+
+	var versions map[string]time.Time
+	err = json.Unmarshal(bs, &versions)
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+
+	ferr, err := s.model.RestoreFolderVersions(qs.Get("folder"), versions)
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+	sendJSON(w, ferr)
+}
+
 func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	current := qs.Get("current")

+ 9 - 0
cmd/syncthing/mocked_model_test.go

@@ -14,6 +14,7 @@ import (
 	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/stats"
+	"github.com/syncthing/syncthing/lib/versioner"
 )
 
 type mockedModel struct{}
@@ -75,6 +76,14 @@ func (m *mockedModel) SetIgnores(folder string, content []string) error {
 	return nil
 }
 
+func (m *mockedModel) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
+	return nil, nil
+}
+
+func (m *mockedModel) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
+	return nil, nil
+}
+
 func (m *mockedModel) PauseDevice(device protocol.DeviceID) {
 }
 

+ 4 - 0
gui/black/assets/css/theme.css

@@ -243,3 +243,7 @@ code.ng-binding{
 .progress .frontal {
     color: #222;
 }
+
+.fancytree-title {
+    color: #aaa !important;
+}

+ 3 - 0
gui/dark/assets/css/theme.css

@@ -256,3 +256,6 @@ code.ng-binding{
     color: #3fa9f0;
 }
 
+.fancytree-title {
+    color: #aaa !important;
+}

+ 4 - 0
gui/default/assets/css/overrides.css

@@ -371,3 +371,7 @@ ul.three-columns li, ul.two-columns li {
 .tab-content {
     padding-top: 10px;
 }
+
+.fancytree-ext-table {
+    width: 100% !important;
+}

+ 6 - 0
gui/default/assets/css/theme.css

@@ -27,3 +27,9 @@
 .panel-heading:hover, .panel-heading:focus {
     text-decoration: none;
 }
+
+.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
+.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
+    color: black !important;
+    font-weight: lighter !important;
+}

+ 11 - 0
gui/default/assets/lang/lang-en.json

@@ -28,6 +28,7 @@
    "Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
    "Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
    "Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
+   "Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
    "Auto Accept": "Auto Accept",
    "Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
    "Automatic upgrades": "Automatic upgrades",
@@ -67,6 +68,8 @@
    "Discovered": "Discovered",
    "Discovery": "Discovery",
    "Discovery Failures": "Discovery Failures",
+   "Do not restore": "Do not restore",
+   "Do not restore all": "Do not restore all",
    "Documentation": "Documentation",
    "Download Rate": "Download Rate",
    "Downloaded": "Downloaded",
@@ -95,6 +98,8 @@
    "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
    "Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.",
    "Filesystem Notifications": "Filesystem Notifications",
+   "Filter by date": "Filter by date",
+   "Filter by name": "Filter by name",
    "Folder": "Folder",
    "Folder ID": "Folder ID",
    "Folder Label": "Folder Label",
@@ -141,6 +146,7 @@
    "Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
    "Logs": "Logs",
    "Major Upgrade": "Major Upgrade",
+   "Mass actions": "Mass actions",
    "Master": "Master",
    "Maximum Age": "Maximum Age",
    "Metadata Only": "Metadata Only",
@@ -201,6 +207,8 @@
    "Restart": "Restart",
    "Restart Needed": "Restart Needed",
    "Restarting": "Restarting",
+   "Restore": "Restore",
+   "Restore Versions": "Restore Versions",
    "Resume": "Resume",
    "Resume All": "Resume All",
    "Reused": "Reused",
@@ -210,6 +218,8 @@
    "See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
    "See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
    "Select a version": "Select a version",
+   "Select latest version": "Select latest version",
+   "Select oldest version": "Select oldest version",
    "Select the devices to share this folder with.": "Select the devices to share this folder with.",
    "Select the folders to share with this device.": "Select the folders to share with this device.",
    "Send \u0026 Receive": "Send \u0026 Receive",
@@ -232,6 +242,7 @@
    "Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
    "Size": "Size",
    "Smallest First": "Smallest First",
+   "Some items could not be restored:": "Some items could not be restored:",
    "Source Code": "Source Code",
    "Stable releases and release candidates": "Stable releases and release candidates",
    "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",

+ 10 - 0
gui/default/index.html

@@ -19,10 +19,12 @@
 
   <title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
   <link href="vendor/bootstrap/css/bootstrap.css" rel="stylesheet"/>
+  <link href="vendor/bootstrap/css/daterangepicker.css" rel="stylesheet"/>
   <link href="assets/font/raleway.css" rel="stylesheet"/>
   <link href="vendor/font-awesome/css/font-awesome.css" rel="stylesheet"/>
   <link href="assets/css/overrides.css" rel="stylesheet"/>
   <link href="assets/css/theme.css" rel="stylesheet"/>
+  <link href="vendor/fancytree/css/ui.fancytree.css" rel="stylesheet"/>
 </head>
 
 <body>
@@ -434,6 +436,9 @@
                   <button ng-if="folder.paused" type="button" class="btn btn-sm btn-default" ng-click="setFolderPause(folder.id, false)">
                     <span class="fa fa-play"></span>&nbsp;<span translate>Resume</span>
                   </button>
+                  <button type="button" class="btn btn-default btn-sm" ng-click="restoreVersions.show(folder.id)" ng-if="folder.versioning.type">
+                    <span class="fa fa-undo"></span>&nbsp;<span translate>Versions</span>
+                  </button>
                   <button type="button" class="btn btn-sm btn-default" ng-click="rescanFolder(folder.id)" ng-show="['idle', 'stopped', 'unshared'].indexOf(folderStatus(folder)) > -1">
                     <span class="fa fa-refresh"></span>&nbsp;<span translate>Rescan</span>
                   </button>
@@ -723,6 +728,8 @@
   <ng-include src="'syncthing/device/globalChangesModalView.html'"></ng-include>
   <ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
   <ng-include src="'syncthing/folder/editIgnoresModalView.html'"></ng-include>
+  <ng-include src="'syncthing/folder/restoreVersionsModalView.html'"></ng-include>
+  <ng-include src="'syncthing/folder/restoreVersionsConfirmation.html'"></ng-include>
   <ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>
   <ng-include src="'syncthing/settings/advancedSettingsModalView.html'"></ng-include>
   <ng-include src="'syncthing/usagereport/usageReportModalView.html'"></ng-include>
@@ -744,7 +751,10 @@
   <script type="text/javascript" src="vendor/angular/angular-translate.js"></script>
   <script type="text/javascript" src="vendor/angular/angular-translate-loader-static-files.js"></script>
   <script type="text/javascript" src="vendor/angular/angular-dirPagination.js"></script>
+  <script type="text/javascript" src="vendor/moment/moment.js"></script>
   <script type="text/javascript" src="vendor/bootstrap/js/bootstrap.js"></script>
+  <script type="text/javascript" src="vendor/bootstrap/js/daterangepicker.js"></script>
+  <script type="text/javascript" src="vendor/fancytree/jquery.fancytree-all-deps.js"></script>
   <!-- / vendor scripts -->
 
   <!-- gui application code -->

+ 72 - 0
gui/default/syncthing/app.js

@@ -134,3 +134,75 @@ function debounce(func, wait) {
         return result;
     };
 }
+
+function buildTree(children) {
+    /* Converts
+    *
+    * {
+    *   'foo/bar': [...],
+    *   'foo/baz': [...]
+    * }
+    *
+    * to
+    *
+    * [
+    *   {
+    *     title: 'foo',
+    *     children: [
+    *       {
+    *         title: 'bar',
+    *         versions: [...],
+    *         ...
+    *       },
+    *       {
+    *         title: 'baz',
+    *         versions: [...],
+    *         ...
+    *       }
+    *     ],
+    *   }
+    * ]
+    */
+    var root = {
+        children: []
+    }
+
+    $.each(children, function(path, data) {
+        var parts = path.split('/');
+        var name = parts.splice(-1)[0];
+
+        var keySoFar = [];
+        var parent = root;
+        while (parts.length > 0) {
+            var part = parts.shift();
+            keySoFar.push(part);
+            var found = false;
+            for (var i = 0; i < parent.children.length; i++) {
+                if (parent.children[i].title == part) {
+                    parent = parent.children[i];
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                var child = {
+                    title: part,
+                    key: keySoFar.join('/'),
+                    folder: true,
+                    children: []
+                }
+                parent.children.push(child);
+                parent = child;
+            }
+        }
+
+        parent.children.push({
+            title: name,
+            key: path,
+            folder: false,
+            versions: data,
+        });
+    });
+
+    return root.children;
+}

+ 232 - 5
gui/default/syncthing/core/syncthingController.js

@@ -2,7 +2,7 @@ angular.module('syncthing.core')
     .config(function($locationProvider) {
         $locationProvider.html5Mode({enabled: true, requireBase: false}).hashPrefix('!');
     })
-    .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $interval) {
+    .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $compile, $timeout, $rootScope) {
         'use strict';
 
         // private/helper definitions
@@ -1107,9 +1107,9 @@ angular.module('syncthing.core')
             },
             show: function() {
                 $scope.logging.refreshFacilities();
-                $scope.logging.timer = $interval($scope.logging.fetch, 0, 1);
+                $scope.logging.timer = $timeout($scope.logging.fetch);
                 $('#logViewer').modal().on('hidden.bs.modal', function () {
-                    $interval.cancel($scope.logging.timer);
+                    $timeout.cancel($scope.logging.timer);
                     $scope.logging.timer = null;
                     $scope.logging.entries = [];
                 });
@@ -1138,7 +1138,7 @@ angular.module('syncthing.core')
                 var textArea = $('#logViewerText');
                 if (textArea.is(":focus")) {
                     if (!$scope.logging.timer) return;
-                    $scope.logging.timer = $interval($scope.logging.fetch, 500, 1);
+                    $scope.logging.timer = $timeout($scope.logging.fetch, 500);
                     return;
                 }
 
@@ -1149,7 +1149,7 @@ angular.module('syncthing.core')
 
                 $http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
                     if (!$scope.logging.timer) return;
-                    $scope.logging.timer = $interval($scope.logging.fetch, 2000, 1);
+                    $scope.logging.timer = $timeout($scope.logging.fetch, 2000);
                     if (!textArea.is(":focus")) {
                         if (data.messages) {
                             $scope.logging.entries.push.apply($scope.logging.entries, data.messages);
@@ -1767,6 +1767,233 @@ angular.module('syncthing.core')
                 });
         };
 
+        function resetRestoreVersions() {
+            $scope.restoreVersions = {
+                folder: null,
+                selections: {},
+                versions: null,
+                tree: null,
+                errors: null,
+                filters: {},
+                massAction: function (name, action) {
+                    $.each($scope.restoreVersions.versions, function(key) {
+                        if (key.startsWith(name + '/') && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
+                            if (action == 'unset') {
+                                delete $scope.restoreVersions.selections[key];
+                                return;
+                            }
+
+                            var availableVersions = [];
+                            $.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function(idx, version) {
+                                availableVersions.push(version.versionTime);
+                            })
+
+                            if (availableVersions.length) {
+                                availableVersions.sort(function (a, b) { return a - b; });
+                                if (action == 'latest') {
+                                    $scope.restoreVersions.selections[key] = availableVersions.pop();
+                                } else if (action == 'oldest') {
+                                    $scope.restoreVersions.selections[key] = availableVersions.shift();
+                                }
+                            }
+                        }
+                    });
+                },
+                filterVersions: function(versions) {
+                    var filteredVersions  = [];
+                    $.each(versions, function (idx, version) {
+                        if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
+                            filteredVersions.push(version);
+                        }
+                    });
+                    return filteredVersions;
+                },
+                selectionCount: function() {
+                    var count = 0;
+                    $.each($scope.restoreVersions.selections, function(key, value) {
+                        if (value) {
+                            count++;
+                        }
+                    });
+                    return count;
+                },
+
+                restore: function() {
+                    $scope.restoreVersions.tree.clear();
+                    $scope.restoreVersions.tree = null;
+                    $scope.restoreVersions.versions = null;
+                    var selections = {};
+                    $.each($scope.restoreVersions.selections, function(key, value) {
+                        if (value) {
+                            selections[key] = value;
+                        }
+                    });
+                    $scope.restoreVersions.selections = {};
+
+                    $http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
+                        if (Object.keys(data).length == 0) {
+                            $('#restoreVersions').modal('hide');
+                        } else {
+                            $scope.restoreVersions.errors = data;
+                        }
+                    });
+                },
+                show: function(folder) {
+                    $scope.restoreVersions.folder = folder;
+
+                    var closed = false;
+                    var modalShown = $q.defer();
+                    $('#restoreVersions').modal().on('hidden.bs.modal', function () {
+                        closed = true;
+                        resetRestoreVersions();
+                    }).on('shown.bs.modal', function() {
+                        modalShown.resolve();
+                    });
+
+                    var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
+                        .success(function (data) {
+                            $.each(data, function(key, values) {
+                                $.each(values, function(idx, value) {
+                                    value.modTime = new Date(value.modTime);
+                                    value.versionTime = new Date(value.versionTime);
+                                });
+                            });
+                            if (closed) return;
+                            $scope.restoreVersions.versions = data;
+                        });
+
+                    $q.all([dataReceived, modalShown.promise]).then(function() {
+                        if (closed) {
+                            resetRestoreVersions();
+                            return;
+                        }
+
+                        $scope.restoreVersions.tree = $("#restoreTree").fancytree({
+                            extensions: ["table", "filter"],
+                            quicksearch: true,
+                            filter: {
+                                autoApply: true,
+                                counter: true,
+                                hideExpandedCounter: true,
+                                hideExpanders: true,
+                                highlight: true,
+                                leavesOnly: false,
+                                nodata: true,
+                                mode: "hide"
+                            },
+                            table: {
+                                indentation: 20,
+                                nodeColumnIdx: 0,
+                            },
+                            debugLevel: 2,
+                            source: buildTree($scope.restoreVersions.versions),
+                            renderColumns: function(event, data) {
+                                var node = data.node,
+                                    $tdList = $(node.tr).find(">td"),
+                                    template;
+                                if (node.folder) {
+                                    template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
+                                } else {
+                                    template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
+                                }
+
+                                var scope = $rootScope.$new(true);
+                                scope.key = node.key;
+                                scope.restoreVersions = $scope.restoreVersions;
+
+                                $tdList.eq(1).html(
+                                    $compile(template)(scope)
+                                );
+
+                                // Force angular to redraw.
+                                $timeout(function() {
+                                    $scope.$apply();
+                                });
+                            }
+                        }).fancytree("getTree");
+
+                        var minDate = moment(),
+                            maxDate = moment(0, 'X'),
+                            date;
+
+                        // Find version window.
+                        $.each($scope.restoreVersions.versions, function(key) {
+                            $.each($scope.restoreVersions.versions[key], function(idx, version) {
+                                date = moment(version.versionTime);
+                                if (date.isBefore(minDate)) {
+                                    minDate = date;
+                                }
+                                if (date.isAfter(maxDate)) {
+                                    maxDate = date;
+                                }
+                            });
+                        });
+
+                        $scope.restoreVersions.filters['start'] = minDate;
+                        $scope.restoreVersions.filters['end'] = maxDate;
+
+                        var ranges = {
+                           'All time': [minDate, maxDate],
+                           'Today': [moment(), moment()],
+                           'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
+                           'Last 7 Days': [moment().subtract(6, 'days'), moment()],
+                           'Last 30 Days': [moment().subtract(29, 'days'), moment()],
+                           'This Month': [moment().startOf('month'), moment().endOf('month')],
+                           'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
+                        };
+
+                        // Filter out invalid ranges.
+                        $.each(ranges, function(key, range) {
+                            if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
+                                delete ranges[key];
+                            }
+                        });
+
+                        $("#restoreVersionDateRange").daterangepicker({
+                            timePicker: true,
+                            timePicker24Hour: true,
+                            timePickerSeconds: true,
+                            autoUpdateInput: true,
+                            opens: "left",
+                            drops: "up",
+                            startDate: minDate,
+                            endDate: maxDate,
+                            minDate: minDate,
+                            maxDate: maxDate,
+                            ranges: ranges,
+                            locale: {
+                                format: 'YYYY/MM/DD HH:mm:ss',
+                            }
+                        }).on('apply.daterangepicker', function(ev, picker) {
+                            $scope.restoreVersions.filters['start'] = picker.startDate;
+                            $scope.restoreVersions.filters['end'] = picker.endDate;
+                            // Events for this UI element are not managed by angular.
+                            // Force angular to wake up.
+                            $timeout(function() {
+                                $scope.$apply();
+                            });
+                        });
+                    });
+                }
+            };
+        }
+        resetRestoreVersions();
+
+        $scope.$watchCollection('restoreVersions.filters', function() {
+            if (!$scope.restoreVersions.tree) return;
+
+            $scope.restoreVersions.tree.filterNodes(function (node) {
+                if (node.folder) return false;
+                if ($scope.restoreVersions.filters.text && node.key.indexOf($scope.restoreVersions.filters.text) < 0) {
+                    return false;
+                }
+                if ($scope.restoreVersions.filterVersions(node.data.versions).length == 0) {
+                    return false;
+                }
+                return true;
+            });
+        });
+
         $scope.editIgnoresOnAddingFolder = function () {
             if ($scope.editingExisting) {
                 return;

+ 13 - 13
gui/default/syncthing/device/removeDeviceDialogView.html

@@ -1,15 +1,15 @@
 <modal id="remove-device-confirmation" status="warning" icon="exclamation-circle" heading="{{'Remove Device' | translate}}" large="no" closeable="yes">
-    <div class="modal-body">
-        <p ng-model="currentDevice.name" style=" overflow : hidden; text-overflow: ellipsis; white-space: nowrap;">
-            <span  translate translate-value-name="{{currentDevice.name}}">Are you sure you want to remove device {%name%}?</span>
-        </p>
-    </div>
-    <div class="modal-footer">
-        <button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteDevice()">
-            <span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
-        </button>
-        <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
-            <span class="fa fa-times"></span>&nbsp;<span translate>No</span>
-        </button>
-    </div>
+  <div class="modal-body">
+    <p ng-model="currentDevice.name" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
+      <span translate translate-value-name="{{currentDevice.name}}">Are you sure you want to remove device {%name%}?</span>
+    </p>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteDevice()">
+      <span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
+    </button>
+    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
+      <span class="fa fa-times"></span>&nbsp;<span translate>No</span>
+    </button>
+  </div>
 </modal>

+ 16 - 16
gui/default/syncthing/folder/removeFolderDialogView.html

@@ -1,18 +1,18 @@
 <modal id="remove-folder-confirmation" status="warning" icon="exclamation-circle" heading="{{'Remove Folder' | translate}}" large="no" closeable="yes">
-    <div class="modal-body">
-        <p ng-model="currentFolder.label" style=" overflow : hidden; text-overflow: ellipsis; white-space: nowrap;">
-            <span  translate translate-value-label="{{currentFolder.label}}">Are you sure you want to remove folder {%label%}?</span>
-        </p>
-        <p translate>
-            No files will be deleted as a result of this operation.
-        </p>
-    </div>
-    <div class="modal-footer">
-        <button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteFolder(currentFolder.id)">
-            <span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
-        </button>
-        <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
-            <span class="fa fa-times"></span>&nbsp;<span translate>No</span>
-        </button>
-    </div>
+  <div class="modal-body">
+    <p style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
+      <span translate translate-value-label="{{currentFolder.label}}">Are you sure you want to remove folder {%label%}?</span>
+    </p>
+    <p translate>
+      No files will be deleted as a result of this operation.
+    </p>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteFolder(currentFolder.id)">
+      <span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
+    </button>
+    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
+      <span class="fa fa-times"></span>&nbsp;<span translate>No</span>
+    </button>
+  </div>
 </modal>

+ 15 - 0
gui/default/syncthing/folder/restoreVersionsConfirmation.html

@@ -0,0 +1,15 @@
+<modal id="restore-versions-confirmation" status="warning" icon="exclamation-circle" heading="{{'Restore Versions' | translate}}" large="no" closeable="yes">
+  <div class="modal-body">
+    <p style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
+      <span translate-value-count="{{restoreVersions.selectionCount()}}" translate>Are you sure you want to restore {%count%} files?</span>
+    </p>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="restoreVersions.restore()">
+      <span class="fa fa-check"></span>&nbsp;<span translate>Yes</span>
+    </button>
+    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
+      <span class="fa fa-times"></span>&nbsp;<span translate>No</span>
+    </button>
+  </div>
+</modal>

+ 11 - 0
gui/default/syncthing/folder/restoreVersionsMassActions.html

@@ -0,0 +1,11 @@
+<div class="dropdown">
+  <button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown">
+    <span translate>Mass actions</span>
+    <span class="caret"></span>
+  </button>
+  <ul class="dropdown-menu">
+    <li><a href="#" ng-click="restoreVersions.massAction(key, 'unset')" translate>Do not restore all</a></li>
+    <li><a href="#" ng-click="restoreVersions.massAction(key, 'latest')" translate>Select latest version</a></li>
+    <li><a href="#" ng-click="restoreVersions.massAction(key, 'oldest')" translate>Select oldest version</a></li>
+  </ul>
+</div>

+ 51 - 0
gui/default/syncthing/folder/restoreVersionsModalView.html

@@ -0,0 +1,51 @@
+<modal id="restoreVersions" status="default" heading="{{'Restore Versions' | translate}}" large="yes" closeable="yes">
+  <div class="modal-body">
+    <span translate ng-if="!restoreVersions.versions && !restoreVersions.errors">Loading data...</span>
+    <div ng-if="restoreVersions.versions">
+      <table id="restoreTree">
+        <thead>
+          <tr>
+            <th></th>
+            <th></th>
+          </tr>
+        </thead>
+        <tbody>
+        </tbody>
+      </table>
+      <hr/>
+      <div class="row form-inline">
+        <div class="col-md-6">
+          <div class="form-group">
+            <label translate for="restoreVersionSearch">Filter by name</label>:&nbsp
+            <input id="restoreVersionSearch" class="form-control" type="text" ng-model="restoreVersions.filters.text">
+          </div>
+        </div>
+        <div class="col-md-6">
+          <div class="form-group">
+            <label translate for="restoreVersionDate">Filter by date</label>:&nbsp
+            <input id="restoreVersionDateRange" class="form-control">
+          </div>
+        </div>
+      </div>
+    </div>
+    <div ng-if="restoreVersions.errors">
+      <label><span translate>Some items could not be restored:</span></label>
+      <table class="table table-condensed table-striped">
+        <tbody>
+          <tr ng-repeat="(file, error) in restoreVersions.errors">
+            <td>{{ file }}</td>
+            <td>{{ error }}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#restore-versions-confirmation" ng-if="restoreVersions.versions" ng-disabled="restoreVersions.selectionCount() < 1">
+      <span class="fa fa-check"></span>&nbsp;<span translate>Restore</span>
+    </button>
+    <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
+      <span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
+    </button>
+  </div>
+</modal>

+ 17 - 0
gui/default/syncthing/folder/restoreVersionsVersionSelector.html

@@ -0,0 +1,17 @@
+<div class="dropdown">
+  <button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown">
+    <span ng-if="!restoreVersions.selections[key]" translate>Do not restore</span>
+    <span ng-if="restoreVersions.selections[key]">{{ restoreVersions.selections[key] | date:"yyyy/MM/dd HH:mm:ss" }}</span>
+    <span class="caret"></span>
+  </button>
+  <ul class="dropdown-menu">
+    <li>
+      <a href="#" ng-click="restoreVersions.selections[key] = undefined" translate>Do not restore</a>
+    </li>
+    <li ng-repeat="version in restoreVersions.filterVersions(restoreVersions.versions[key])">
+      <a href="#" ng-click="restoreVersions.selections[key] = version.versionTime">
+        {{ version.versionTime | date:"yyyy/MM/dd HH:mm:ss" }} {{ version.size | binary }}B
+      </a>
+    </li>
+  </ul>
+</div>

+ 269 - 0
gui/default/vendor/bootstrap/css/daterangepicker.css

@@ -0,0 +1,269 @@
+.daterangepicker {
+  position: absolute;
+  color: inherit;
+  background-color: #fff;
+  border-radius: 4px;
+  width: 278px;
+  padding: 4px;
+  margin-top: 1px;
+  top: 100px;
+  left: 20px;
+  /* Calendars */ }
+  .daterangepicker:before, .daterangepicker:after {
+    position: absolute;
+    display: inline-block;
+    border-bottom-color: rgba(0, 0, 0, 0.2);
+    content: ''; }
+  .daterangepicker:before {
+    top: -7px;
+    border-right: 7px solid transparent;
+    border-left: 7px solid transparent;
+    border-bottom: 7px solid #ccc; }
+  .daterangepicker:after {
+    top: -6px;
+    border-right: 6px solid transparent;
+    border-bottom: 6px solid #fff;
+    border-left: 6px solid transparent; }
+  .daterangepicker.opensleft:before {
+    right: 9px; }
+  .daterangepicker.opensleft:after {
+    right: 10px; }
+  .daterangepicker.openscenter:before {
+    left: 0;
+    right: 0;
+    width: 0;
+    margin-left: auto;
+    margin-right: auto; }
+  .daterangepicker.openscenter:after {
+    left: 0;
+    right: 0;
+    width: 0;
+    margin-left: auto;
+    margin-right: auto; }
+  .daterangepicker.opensright:before {
+    left: 9px; }
+  .daterangepicker.opensright:after {
+    left: 10px; }
+  .daterangepicker.dropup {
+    margin-top: -5px; }
+    .daterangepicker.dropup:before {
+      top: initial;
+      bottom: -7px;
+      border-bottom: initial;
+      border-top: 7px solid #ccc; }
+    .daterangepicker.dropup:after {
+      top: initial;
+      bottom: -6px;
+      border-bottom: initial;
+      border-top: 6px solid #fff; }
+  .daterangepicker.dropdown-menu {
+    max-width: none;
+    z-index: 3001; }
+  .daterangepicker.single .ranges, .daterangepicker.single .calendar {
+    float: none; }
+  .daterangepicker.show-calendar .calendar {
+    display: block; }
+  .daterangepicker .calendar {
+    display: none;
+    max-width: 270px;
+    margin: 4px; }
+    .daterangepicker .calendar.single .calendar-table {
+      border: none; }
+    .daterangepicker .calendar th, .daterangepicker .calendar td {
+      white-space: nowrap;
+      text-align: center;
+      min-width: 32px; }
+  .daterangepicker .calendar-table {
+    border: 1px solid #fff;
+    padding: 4px;
+    border-radius: 4px;
+    background-color: #fff; }
+  .daterangepicker table {
+    width: 100%;
+    margin: 0; }
+  .daterangepicker td, .daterangepicker th {
+    text-align: center;
+    width: 20px;
+    height: 20px;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    white-space: nowrap;
+    cursor: pointer; }
+    .daterangepicker td.available:hover, .daterangepicker th.available:hover {
+      background-color: #eee;
+      border-color: transparent;
+      color: inherit; }
+    .daterangepicker td.week, .daterangepicker th.week {
+      font-size: 80%;
+      color: #ccc; }
+  .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
+    background-color: #fff;
+    border-color: transparent;
+    color: #999; }
+  .daterangepicker td.in-range {
+    background-color: #ebf4f8;
+    border-color: transparent;
+    color: #000;
+    border-radius: 0; }
+  .daterangepicker td.start-date {
+    border-radius: 4px 0 0 4px; }
+  .daterangepicker td.end-date {
+    border-radius: 0 4px 4px 0; }
+  .daterangepicker td.start-date.end-date {
+    border-radius: 4px; }
+  .daterangepicker td.active, .daterangepicker td.active:hover {
+    background-color: #357ebd;
+    border-color: transparent;
+    color: #fff; }
+  .daterangepicker th.month {
+    width: auto; }
+  .daterangepicker td.disabled, .daterangepicker option.disabled {
+    color: #999;
+    cursor: not-allowed;
+    text-decoration: line-through; }
+  .daterangepicker select.monthselect, .daterangepicker select.yearselect {
+    font-size: 12px;
+    padding: 1px;
+    height: auto;
+    margin: 0;
+    cursor: default; }
+  .daterangepicker select.monthselect {
+    margin-right: 2%;
+    width: 56%; }
+  .daterangepicker select.yearselect {
+    width: 40%; }
+  .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
+    width: 50px;
+    margin-bottom: 0; }
+  .daterangepicker .input-mini {
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    color: #555;
+    height: 30px;
+    line-height: 30px;
+    display: block;
+    vertical-align: middle;
+    margin: 0 0 5px 0;
+    padding: 0 6px 0 28px;
+    width: 100%; }
+    .daterangepicker .input-mini.active {
+      border: 1px solid #08c;
+      border-radius: 4px; }
+  .daterangepicker .daterangepicker_input {
+    position: relative; }
+    .daterangepicker .daterangepicker_input i {
+      position: absolute;
+      left: 8px;
+      top: 8px; }
+  .daterangepicker.rtl .input-mini {
+    padding-right: 28px;
+    padding-left: 6px; }
+  .daterangepicker.rtl .daterangepicker_input i {
+    left: auto;
+    right: 8px; }
+  .daterangepicker .calendar-time {
+    text-align: center;
+    margin: 5px auto;
+    line-height: 30px;
+    position: relative;
+    padding-left: 28px; }
+    .daterangepicker .calendar-time select.disabled {
+      color: #ccc;
+      cursor: not-allowed; }
+
+.ranges {
+  font-size: 11px;
+  float: none;
+  margin: 4px;
+  text-align: left; }
+  .ranges ul {
+    list-style: none;
+    margin: 0 auto;
+    padding: 0;
+    width: 100%; }
+  .ranges li {
+    font-size: 13px;
+    background-color: #f5f5f5;
+    border: 1px solid #f5f5f5;
+    border-radius: 4px;
+    color: #08c;
+    padding: 3px 12px;
+    margin-bottom: 8px;
+    cursor: pointer; }
+    .ranges li:hover {
+      background-color: #08c;
+      border: 1px solid #08c;
+      color: #fff; }
+    .ranges li.active {
+      background-color: #08c;
+      border: 1px solid #08c;
+      color: #fff; }
+
+/*  Larger Screen Styling */
+@media (min-width: 564px) {
+  .daterangepicker {
+    width: auto; }
+    .daterangepicker .ranges ul {
+      width: 160px; }
+    .daterangepicker.single .ranges ul {
+      width: 100%; }
+    .daterangepicker.single .calendar.left {
+      clear: none; }
+    .daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .calendar {
+      float: left; }
+    .daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .calendar {
+      float: right; }
+    .daterangepicker.ltr {
+      direction: ltr;
+      text-align: left; }
+      .daterangepicker.ltr .calendar.left {
+        clear: left;
+        margin-right: 0; }
+        .daterangepicker.ltr .calendar.left .calendar-table {
+          border-right: none;
+          border-top-right-radius: 0;
+          border-bottom-right-radius: 0; }
+      .daterangepicker.ltr .calendar.right {
+        margin-left: 0; }
+        .daterangepicker.ltr .calendar.right .calendar-table {
+          border-left: none;
+          border-top-left-radius: 0;
+          border-bottom-left-radius: 0; }
+      .daterangepicker.ltr .left .daterangepicker_input {
+        padding-right: 12px; }
+      .daterangepicker.ltr .calendar.left .calendar-table {
+        padding-right: 12px; }
+      .daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar {
+        float: left; }
+    .daterangepicker.rtl {
+      direction: rtl;
+      text-align: right; }
+      .daterangepicker.rtl .calendar.left {
+        clear: right;
+        margin-left: 0; }
+        .daterangepicker.rtl .calendar.left .calendar-table {
+          border-left: none;
+          border-top-left-radius: 0;
+          border-bottom-left-radius: 0; }
+      .daterangepicker.rtl .calendar.right {
+        margin-right: 0; }
+        .daterangepicker.rtl .calendar.right .calendar-table {
+          border-right: none;
+          border-top-right-radius: 0;
+          border-bottom-right-radius: 0; }
+      .daterangepicker.rtl .left .daterangepicker_input {
+        padding-left: 12px; }
+      .daterangepicker.rtl .calendar.left .calendar-table {
+        padding-left: 12px; }
+      .daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar {
+        text-align: right;
+        float: right; } }
+@media (min-width: 730px) {
+  .daterangepicker .ranges {
+    width: auto; }
+  .daterangepicker.ltr .ranges {
+    float: left; }
+  .daterangepicker.rtl .ranges {
+    float: right; }
+  .daterangepicker .calendar.left {
+    clear: none !important; } }

+ 1626 - 0
gui/default/vendor/bootstrap/js/daterangepicker.js

@@ -0,0 +1,1626 @@
+/**
+* @version: 2.1.25
+* @author: Dan Grossman http://www.dangrossman.info/
+* @copyright: Copyright (c) 2012-2017 Dan Grossman. All rights reserved.
+* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php
+* @website: https://www.daterangepicker.com/
+*/
+// Follow the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Make globaly available as well
+        define(['moment', 'jquery'], function (moment, jquery) {
+            return (root.daterangepicker = factory(moment, jquery));
+        });
+    } else if (typeof module === 'object' && module.exports) {
+        // Node / Browserify
+        //isomorphic issue
+        var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined;
+        if (!jQuery) {
+            jQuery = require('jquery');
+            if (!jQuery.fn) jQuery.fn = {};
+        }
+        module.exports = factory(require('moment'), jQuery);
+    } else {
+        // Browser globals
+        root.daterangepicker = factory(root.moment, root.jQuery);
+    }
+}(this, function(moment, $) {
+    var DateRangePicker = function(element, options, cb) {
+
+        //default settings for options
+        this.parentEl = 'body';
+        this.element = $(element);
+        this.startDate = moment().startOf('day');
+        this.endDate = moment().endOf('day');
+        this.minDate = false;
+        this.maxDate = false;
+        this.dateLimit = false;
+        this.autoApply = false;
+        this.singleDatePicker = false;
+        this.showDropdowns = false;
+        this.showWeekNumbers = false;
+        this.showISOWeekNumbers = false;
+        this.showCustomRangeLabel = true;
+        this.timePicker = false;
+        this.timePicker24Hour = false;
+        this.timePickerIncrement = 1;
+        this.timePickerSeconds = false;
+        this.linkedCalendars = true;
+        this.autoUpdateInput = true;
+        this.alwaysShowCalendars = false;
+        this.ranges = {};
+
+        this.opens = 'right';
+        if (this.element.hasClass('pull-right'))
+            this.opens = 'left';
+
+        this.drops = 'down';
+        if (this.element.hasClass('dropup'))
+            this.drops = 'up';
+
+        this.buttonClasses = 'btn btn-sm';
+        this.applyClass = 'btn-success';
+        this.cancelClass = 'btn-default';
+
+        this.locale = {
+            direction: 'ltr',
+            format: moment.localeData().longDateFormat('L'),
+            separator: ' - ',
+            applyLabel: 'Apply',
+            cancelLabel: 'Cancel',
+            weekLabel: 'W',
+            customRangeLabel: 'Custom Range',
+            daysOfWeek: moment.weekdaysMin(),
+            monthNames: moment.monthsShort(),
+            firstDay: moment.localeData().firstDayOfWeek()
+        };
+
+        this.callback = function() { };
+
+        //some state information
+        this.isShowing = false;
+        this.leftCalendar = {};
+        this.rightCalendar = {};
+
+        //custom options from user
+        if (typeof options !== 'object' || options === null)
+            options = {};
+
+        //allow setting options with data attributes
+        //data-api options will be overwritten with custom javascript options
+        options = $.extend(this.element.data(), options);
+
+        //html template for the picker UI
+        if (typeof options.template !== 'string' && !(options.template instanceof $))
+            options.template = '<div class="daterangepicker dropdown-menu">' +
+                '<div class="calendar left">' +
+                    '<div class="daterangepicker_input">' +
+                      '<input class="input-mini form-control" type="text" name="daterangepicker_start" value="" />' +
+                      '<i class="fa fa-calendar glyphicon glyphicon-calendar"></i>' +
+                      '<div class="calendar-time">' +
+                        '<div></div>' +
+                        '<i class="fa fa-clock-o glyphicon glyphicon-time"></i>' +
+                      '</div>' +
+                    '</div>' +
+                    '<div class="calendar-table"></div>' +
+                '</div>' +
+                '<div class="calendar right">' +
+                    '<div class="daterangepicker_input">' +
+                      '<input class="input-mini form-control" type="text" name="daterangepicker_end" value="" />' +
+                      '<i class="fa fa-calendar glyphicon glyphicon-calendar"></i>' +
+                      '<div class="calendar-time">' +
+                        '<div></div>' +
+                        '<i class="fa fa-clock-o glyphicon glyphicon-time"></i>' +
+                      '</div>' +
+                    '</div>' +
+                    '<div class="calendar-table"></div>' +
+                '</div>' +
+                '<div class="ranges">' +
+                    '<div class="range_inputs">' +
+                        '<button class="applyBtn" disabled="disabled" type="button"></button> ' +
+                        '<button class="cancelBtn" type="button"></button>' +
+                    '</div>' +
+                '</div>' +
+            '</div>';
+
+        this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl);
+        this.container = $(options.template).appendTo(this.parentEl);
+
+        //
+        // handle all the possible options overriding defaults
+        //
+
+        if (typeof options.locale === 'object') {
+
+            if (typeof options.locale.direction === 'string')
+                this.locale.direction = options.locale.direction;
+
+            if (typeof options.locale.format === 'string')
+                this.locale.format = options.locale.format;
+
+            if (typeof options.locale.separator === 'string')
+                this.locale.separator = options.locale.separator;
+
+            if (typeof options.locale.daysOfWeek === 'object')
+                this.locale.daysOfWeek = options.locale.daysOfWeek.slice();
+
+            if (typeof options.locale.monthNames === 'object')
+              this.locale.monthNames = options.locale.monthNames.slice();
+
+            if (typeof options.locale.firstDay === 'number')
+              this.locale.firstDay = options.locale.firstDay;
+
+            if (typeof options.locale.applyLabel === 'string')
+              this.locale.applyLabel = options.locale.applyLabel;
+
+            if (typeof options.locale.cancelLabel === 'string')
+              this.locale.cancelLabel = options.locale.cancelLabel;
+
+            if (typeof options.locale.weekLabel === 'string')
+              this.locale.weekLabel = options.locale.weekLabel;
+
+            if (typeof options.locale.customRangeLabel === 'string'){
+                //Support unicode chars in the custom range name.
+                var elem = document.createElement('textarea');
+                elem.innerHTML = options.locale.customRangeLabel;
+                var rangeHtml = elem.value;
+                this.locale.customRangeLabel = rangeHtml;
+            }
+        }
+        this.container.addClass(this.locale.direction);
+
+        if (typeof options.startDate === 'string')
+            this.startDate = moment(options.startDate, this.locale.format);
+
+        if (typeof options.endDate === 'string')
+            this.endDate = moment(options.endDate, this.locale.format);
+
+        if (typeof options.minDate === 'string')
+            this.minDate = moment(options.minDate, this.locale.format);
+
+        if (typeof options.maxDate === 'string')
+            this.maxDate = moment(options.maxDate, this.locale.format);
+
+        if (typeof options.startDate === 'object')
+            this.startDate = moment(options.startDate);
+
+        if (typeof options.endDate === 'object')
+            this.endDate = moment(options.endDate);
+
+        if (typeof options.minDate === 'object')
+            this.minDate = moment(options.minDate);
+
+        if (typeof options.maxDate === 'object')
+            this.maxDate = moment(options.maxDate);
+
+        // sanity check for bad options
+        if (this.minDate && this.startDate.isBefore(this.minDate))
+            this.startDate = this.minDate.clone();
+
+        // sanity check for bad options
+        if (this.maxDate && this.endDate.isAfter(this.maxDate))
+            this.endDate = this.maxDate.clone();
+
+        if (typeof options.applyClass === 'string')
+            this.applyClass = options.applyClass;
+
+        if (typeof options.cancelClass === 'string')
+            this.cancelClass = options.cancelClass;
+
+        if (typeof options.dateLimit === 'object')
+            this.dateLimit = options.dateLimit;
+
+        if (typeof options.opens === 'string')
+            this.opens = options.opens;
+
+        if (typeof options.drops === 'string')
+            this.drops = options.drops;
+
+        if (typeof options.showWeekNumbers === 'boolean')
+            this.showWeekNumbers = options.showWeekNumbers;
+
+        if (typeof options.showISOWeekNumbers === 'boolean')
+            this.showISOWeekNumbers = options.showISOWeekNumbers;
+
+        if (typeof options.buttonClasses === 'string')
+            this.buttonClasses = options.buttonClasses;
+
+        if (typeof options.buttonClasses === 'object')
+            this.buttonClasses = options.buttonClasses.join(' ');
+
+        if (typeof options.showDropdowns === 'boolean')
+            this.showDropdowns = options.showDropdowns;
+
+        if (typeof options.showCustomRangeLabel === 'boolean')
+            this.showCustomRangeLabel = options.showCustomRangeLabel;
+
+        if (typeof options.singleDatePicker === 'boolean') {
+            this.singleDatePicker = options.singleDatePicker;
+            if (this.singleDatePicker)
+                this.endDate = this.startDate.clone();
+        }
+
+        if (typeof options.timePicker === 'boolean')
+            this.timePicker = options.timePicker;
+
+        if (typeof options.timePickerSeconds === 'boolean')
+            this.timePickerSeconds = options.timePickerSeconds;
+
+        if (typeof options.timePickerIncrement === 'number')
+            this.timePickerIncrement = options.timePickerIncrement;
+
+        if (typeof options.timePicker24Hour === 'boolean')
+            this.timePicker24Hour = options.timePicker24Hour;
+
+        if (typeof options.autoApply === 'boolean')
+            this.autoApply = options.autoApply;
+
+        if (typeof options.autoUpdateInput === 'boolean')
+            this.autoUpdateInput = options.autoUpdateInput;
+
+        if (typeof options.linkedCalendars === 'boolean')
+            this.linkedCalendars = options.linkedCalendars;
+
+        if (typeof options.isInvalidDate === 'function')
+            this.isInvalidDate = options.isInvalidDate;
+
+        if (typeof options.isCustomDate === 'function')
+            this.isCustomDate = options.isCustomDate;
+
+        if (typeof options.alwaysShowCalendars === 'boolean')
+            this.alwaysShowCalendars = options.alwaysShowCalendars;
+
+        // update day names order to firstDay
+        if (this.locale.firstDay != 0) {
+            var iterator = this.locale.firstDay;
+            while (iterator > 0) {
+                this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
+                iterator--;
+            }
+        }
+
+        var start, end, range;
+
+        //if no start/end dates set, check if an input element contains initial values
+        if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
+            if ($(this.element).is('input[type=text]')) {
+                var val = $(this.element).val(),
+                    split = val.split(this.locale.separator);
+
+                start = end = null;
+
+                if (split.length == 2) {
+                    start = moment(split[0], this.locale.format);
+                    end = moment(split[1], this.locale.format);
+                } else if (this.singleDatePicker && val !== "") {
+                    start = moment(val, this.locale.format);
+                    end = moment(val, this.locale.format);
+                }
+                if (start !== null && end !== null) {
+                    this.setStartDate(start);
+                    this.setEndDate(end);
+                }
+            }
+        }
+
+        if (typeof options.ranges === 'object') {
+            for (range in options.ranges) {
+
+                if (typeof options.ranges[range][0] === 'string')
+                    start = moment(options.ranges[range][0], this.locale.format);
+                else
+                    start = moment(options.ranges[range][0]);
+
+                if (typeof options.ranges[range][1] === 'string')
+                    end = moment(options.ranges[range][1], this.locale.format);
+                else
+                    end = moment(options.ranges[range][1]);
+
+                // If the start or end date exceed those allowed by the minDate or dateLimit
+                // options, shorten the range to the allowable period.
+                if (this.minDate && start.isBefore(this.minDate))
+                    start = this.minDate.clone();
+
+                var maxDate = this.maxDate;
+                if (this.dateLimit && maxDate && start.clone().add(this.dateLimit).isAfter(maxDate))
+                    maxDate = start.clone().add(this.dateLimit);
+                if (maxDate && end.isAfter(maxDate))
+                    end = maxDate.clone();
+
+                // If the end of the range is before the minimum or the start of the range is
+                // after the maximum, don't display this range option at all.
+                if ((this.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day')) 
+                  || (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day')))
+                    continue;
+
+                //Support unicode chars in the range names.
+                var elem = document.createElement('textarea');
+                elem.innerHTML = range;
+                var rangeHtml = elem.value;
+
+                this.ranges[rangeHtml] = [start, end];
+            }
+
+            var list = '<ul>';
+            for (range in this.ranges) {
+                list += '<li data-range-key="' + range + '">' + range + '</li>';
+            }
+            if (this.showCustomRangeLabel) {
+                list += '<li data-range-key="' + this.locale.customRangeLabel + '">' + this.locale.customRangeLabel + '</li>';
+            }
+            list += '</ul>';
+            this.container.find('.ranges').prepend(list);
+        }
+
+        if (typeof cb === 'function') {
+            this.callback = cb;
+        }
+
+        if (!this.timePicker) {
+            this.startDate = this.startDate.startOf('day');
+            this.endDate = this.endDate.endOf('day');
+            this.container.find('.calendar-time').hide();
+        }
+
+        //can't be used together for now
+        if (this.timePicker && this.autoApply)
+            this.autoApply = false;
+
+        if (this.autoApply && typeof options.ranges !== 'object') {
+            this.container.find('.ranges').hide();
+        } else if (this.autoApply) {
+            this.container.find('.applyBtn, .cancelBtn').addClass('hide');
+        }
+
+        if (this.singleDatePicker) {
+            this.container.addClass('single');
+            this.container.find('.calendar.left').addClass('single');
+            this.container.find('.calendar.left').show();
+            this.container.find('.calendar.right').hide();
+            this.container.find('.daterangepicker_input input, .daterangepicker_input > i').hide();
+            if (this.timePicker) {
+                this.container.find('.ranges ul').hide();
+            } else {
+                this.container.find('.ranges').hide();
+            }
+        }
+
+        if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) {
+            this.container.addClass('show-calendar');
+        }
+
+        this.container.addClass('opens' + this.opens);
+
+        //swap the position of the predefined ranges if opens right
+        if (typeof options.ranges !== 'undefined' && this.opens == 'right') {
+            this.container.find('.ranges').prependTo( this.container.find('.calendar.left').parent() );
+        }
+
+        //apply CSS classes and labels to buttons
+        this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses);
+        if (this.applyClass.length)
+            this.container.find('.applyBtn').addClass(this.applyClass);
+        if (this.cancelClass.length)
+            this.container.find('.cancelBtn').addClass(this.cancelClass);
+        this.container.find('.applyBtn').html(this.locale.applyLabel);
+        this.container.find('.cancelBtn').html(this.locale.cancelLabel);
+
+        //
+        // event listeners
+        //
+
+        this.container.find('.calendar')
+            .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this))
+            .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this))
+            .on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this))
+            .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this))
+            .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this))
+            .on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this))
+            .on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this))
+            .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this))
+            .on('click.daterangepicker', '.daterangepicker_input input', $.proxy(this.showCalendars, this))
+            .on('focus.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsFocused, this))
+            .on('blur.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsBlurred, this))
+            .on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this));
+
+        this.container.find('.ranges')
+            .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this))
+            .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this))
+            .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this))
+            .on('mouseenter.daterangepicker', 'li', $.proxy(this.hoverRange, this))
+            .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this));
+
+        if (this.element.is('input') || this.element.is('button')) {
+            this.element.on({
+                'click.daterangepicker': $.proxy(this.show, this),
+                'focus.daterangepicker': $.proxy(this.show, this),
+                'keyup.daterangepicker': $.proxy(this.elementChanged, this),
+                'keydown.daterangepicker': $.proxy(this.keydown, this)
+            });
+        } else {
+            this.element.on('click.daterangepicker', $.proxy(this.toggle, this));
+        }
+
+        //
+        // if attached to a text input, set the initial value
+        //
+
+        if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) {
+            this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format));
+            this.element.trigger('change');
+        } else if (this.element.is('input') && this.autoUpdateInput) {
+            this.element.val(this.startDate.format(this.locale.format));
+            this.element.trigger('change');
+        }
+
+    };
+
+    DateRangePicker.prototype = {
+
+        constructor: DateRangePicker,
+
+        setStartDate: function(startDate) {
+            if (typeof startDate === 'string')
+                this.startDate = moment(startDate, this.locale.format);
+
+            if (typeof startDate === 'object')
+                this.startDate = moment(startDate);
+
+            if (!this.timePicker)
+                this.startDate = this.startDate.startOf('day');
+
+            if (this.timePicker && this.timePickerIncrement)
+                this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
+
+            if (this.minDate && this.startDate.isBefore(this.minDate)) {
+                this.startDate = this.minDate.clone();
+                if (this.timePicker && this.timePickerIncrement)
+                    this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
+            }
+
+            if (this.maxDate && this.startDate.isAfter(this.maxDate)) {
+                this.startDate = this.maxDate.clone();
+                if (this.timePicker && this.timePickerIncrement)
+                    this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
+            }
+
+            if (!this.isShowing)
+                this.updateElement();
+
+            this.updateMonthsInView();
+        },
+
+        setEndDate: function(endDate) {
+            if (typeof endDate === 'string')
+                this.endDate = moment(endDate, this.locale.format);
+
+            if (typeof endDate === 'object')
+                this.endDate = moment(endDate);
+
+            if (!this.timePicker)
+                this.endDate = this.endDate.endOf('day');
+
+            if (this.timePicker && this.timePickerIncrement)
+                this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
+
+            if (this.endDate.isBefore(this.startDate))
+                this.endDate = this.startDate.clone();
+
+            if (this.maxDate && this.endDate.isAfter(this.maxDate))
+                this.endDate = this.maxDate.clone();
+
+            if (this.dateLimit && this.startDate.clone().add(this.dateLimit).isBefore(this.endDate))
+                this.endDate = this.startDate.clone().add(this.dateLimit);
+
+            this.previousRightTime = this.endDate.clone();
+
+            if (!this.isShowing)
+                this.updateElement();
+
+            this.updateMonthsInView();
+        },
+
+        isInvalidDate: function() {
+            return false;
+        },
+
+        isCustomDate: function() {
+            return false;
+        },
+
+        updateView: function() {
+            if (this.timePicker) {
+                this.renderTimePicker('left');
+                this.renderTimePicker('right');
+                if (!this.endDate) {
+                    this.container.find('.right .calendar-time select').attr('disabled', 'disabled').addClass('disabled');
+                } else {
+                    this.container.find('.right .calendar-time select').removeAttr('disabled').removeClass('disabled');
+                }
+            }
+            if (this.endDate) {
+                this.container.find('input[name="daterangepicker_end"]').removeClass('active');
+                this.container.find('input[name="daterangepicker_start"]').addClass('active');
+            } else {
+                this.container.find('input[name="daterangepicker_end"]').addClass('active');
+                this.container.find('input[name="daterangepicker_start"]').removeClass('active');
+            }
+            this.updateMonthsInView();
+            this.updateCalendars();
+            this.updateFormInputs();
+        },
+
+        updateMonthsInView: function() {
+            if (this.endDate) {
+
+                //if both dates are visible already, do nothing
+                if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month &&
+                    (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM'))
+                    &&
+                    (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM'))
+                    ) {
+                    return;
+                }
+
+                this.leftCalendar.month = this.startDate.clone().date(2);
+                if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) {
+                    this.rightCalendar.month = this.endDate.clone().date(2);
+                } else {
+                    this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month');
+                }
+
+            } else {
+                if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) {
+                    this.leftCalendar.month = this.startDate.clone().date(2);
+                    this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month');
+                }
+            }
+            if (this.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) {
+              this.rightCalendar.month = this.maxDate.clone().date(2);
+              this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month');
+            }
+        },
+
+        updateCalendars: function() {
+
+            if (this.timePicker) {
+                var hour, minute, second;
+                if (this.endDate) {
+                    hour = parseInt(this.container.find('.left .hourselect').val(), 10);
+                    minute = parseInt(this.container.find('.left .minuteselect').val(), 10);
+                    second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0;
+                    if (!this.timePicker24Hour) {
+                        var ampm = this.container.find('.left .ampmselect').val();
+                        if (ampm === 'PM' && hour < 12)
+                            hour += 12;
+                        if (ampm === 'AM' && hour === 12)
+                            hour = 0;
+                    }
+                } else {
+                    hour = parseInt(this.container.find('.right .hourselect').val(), 10);
+                    minute = parseInt(this.container.find('.right .minuteselect').val(), 10);
+                    second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0;
+                    if (!this.timePicker24Hour) {
+                        var ampm = this.container.find('.right .ampmselect').val();
+                        if (ampm === 'PM' && hour < 12)
+                            hour += 12;
+                        if (ampm === 'AM' && hour === 12)
+                            hour = 0;
+                    }
+                }
+                this.leftCalendar.month.hour(hour).minute(minute).second(second);
+                this.rightCalendar.month.hour(hour).minute(minute).second(second);
+            }
+
+            this.renderCalendar('left');
+            this.renderCalendar('right');
+
+            //highlight any predefined range matching the current start and end dates
+            this.container.find('.ranges li').removeClass('active');
+            if (this.endDate == null) return;
+
+            this.calculateChosenLabel();
+        },
+
+        renderCalendar: function(side) {
+
+            //
+            // Build the matrix of dates that will populate the calendar
+            //
+
+            var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar;
+            var month = calendar.month.month();
+            var year = calendar.month.year();
+            var hour = calendar.month.hour();
+            var minute = calendar.month.minute();
+            var second = calendar.month.second();
+            var daysInMonth = moment([year, month]).daysInMonth();
+            var firstDay = moment([year, month, 1]);
+            var lastDay = moment([year, month, daysInMonth]);
+            var lastMonth = moment(firstDay).subtract(1, 'month').month();
+            var lastYear = moment(firstDay).subtract(1, 'month').year();
+            var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth();
+            var dayOfWeek = firstDay.day();
+
+            //initialize a 6 rows x 7 columns array for the calendar
+            var calendar = [];
+            calendar.firstDay = firstDay;
+            calendar.lastDay = lastDay;
+
+            for (var i = 0; i < 6; i++) {
+                calendar[i] = [];
+            }
+
+            //populate the calendar with date objects
+            var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1;
+            if (startDay > daysInLastMonth)
+                startDay -= 7;
+
+            if (dayOfWeek == this.locale.firstDay)
+                startDay = daysInLastMonth - 6;
+
+            var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]);
+
+            var col, row;
+            for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) {
+                if (i > 0 && col % 7 === 0) {
+                    col = 0;
+                    row++;
+                }
+                calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second);
+                curDate.hour(12);
+
+                if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') {
+                    calendar[row][col] = this.minDate.clone();
+                }
+
+                if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') {
+                    calendar[row][col] = this.maxDate.clone();
+                }
+
+            }
+
+            //make the calendar object available to hoverDate/clickDate
+            if (side == 'left') {
+                this.leftCalendar.calendar = calendar;
+            } else {
+                this.rightCalendar.calendar = calendar;
+            }
+
+            //
+            // Display the calendar
+            //
+
+            var minDate = side == 'left' ? this.minDate : this.startDate;
+            var maxDate = this.maxDate;
+            var selected = side == 'left' ? this.startDate : this.endDate;
+            var arrow = this.locale.direction == 'ltr' ? {left: 'chevron-left', right: 'chevron-right'} : {left: 'chevron-right', right: 'chevron-left'};
+
+            var html = '<table class="table-condensed">';
+            html += '<thead>';
+            html += '<tr>';
+
+            // add empty cell for week number
+            if (this.showWeekNumbers || this.showISOWeekNumbers)
+                html += '<th></th>';
+
+            if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) {
+                html += '<th class="prev available"><i class="fa fa-' + arrow.left + ' glyphicon glyphicon-' + arrow.left + '"></i></th>';
+            } else {
+                html += '<th></th>';
+            }
+
+            var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY");
+
+            if (this.showDropdowns) {
+                var currentMonth = calendar[1][1].month();
+                var currentYear = calendar[1][1].year();
+                var maxYear = (maxDate && maxDate.year()) || (currentYear + 5);
+                var minYear = (minDate && minDate.year()) || (currentYear - 50);
+                var inMinYear = currentYear == minYear;
+                var inMaxYear = currentYear == maxYear;
+
+                var monthHtml = '<select class="monthselect">';
+                for (var m = 0; m < 12; m++) {
+                    if ((!inMinYear || m >= minDate.month()) && (!inMaxYear || m <= maxDate.month())) {
+                        monthHtml += "<option value='" + m + "'" +
+                            (m === currentMonth ? " selected='selected'" : "") +
+                            ">" + this.locale.monthNames[m] + "</option>";
+                    } else {
+                        monthHtml += "<option value='" + m + "'" +
+                            (m === currentMonth ? " selected='selected'" : "") +
+                            " disabled='disabled'>" + this.locale.monthNames[m] + "</option>";
+                    }
+                }
+                monthHtml += "</select>";
+
+                var yearHtml = '<select class="yearselect">';
+                for (var y = minYear; y <= maxYear; y++) {
+                    yearHtml += '<option value="' + y + '"' +
+                        (y === currentYear ? ' selected="selected"' : '') +
+                        '>' + y + '</option>';
+                }
+                yearHtml += '</select>';
+
+                dateHtml = monthHtml + yearHtml;
+            }
+
+            html += '<th colspan="5" class="month">' + dateHtml + '</th>';
+            if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) {
+                html += '<th class="next available"><i class="fa fa-' + arrow.right + ' glyphicon glyphicon-' + arrow.right + '"></i></th>';
+            } else {
+                html += '<th></th>';
+            }
+
+            html += '</tr>';
+            html += '<tr>';
+
+            // add week number label
+            if (this.showWeekNumbers || this.showISOWeekNumbers)
+                html += '<th class="week">' + this.locale.weekLabel + '</th>';
+
+            $.each(this.locale.daysOfWeek, function(index, dayOfWeek) {
+                html += '<th>' + dayOfWeek + '</th>';
+            });
+
+            html += '</tr>';
+            html += '</thead>';
+            html += '<tbody>';
+
+            //adjust maxDate to reflect the dateLimit setting in order to
+            //grey out end dates beyond the dateLimit
+            if (this.endDate == null && this.dateLimit) {
+                var maxLimit = this.startDate.clone().add(this.dateLimit).endOf('day');
+                if (!maxDate || maxLimit.isBefore(maxDate)) {
+                    maxDate = maxLimit;
+                }
+            }
+
+            for (var row = 0; row < 6; row++) {
+                html += '<tr>';
+
+                // add week number
+                if (this.showWeekNumbers)
+                    html += '<td class="week">' + calendar[row][0].week() + '</td>';
+                else if (this.showISOWeekNumbers)
+                    html += '<td class="week">' + calendar[row][0].isoWeek() + '</td>';
+
+                for (var col = 0; col < 7; col++) {
+
+                    var classes = [];
+
+                    //highlight today's date
+                    if (calendar[row][col].isSame(new Date(), "day"))
+                        classes.push('today');
+
+                    //highlight weekends
+                    if (calendar[row][col].isoWeekday() > 5)
+                        classes.push('weekend');
+
+                    //grey out the dates in other months displayed at beginning and end of this calendar
+                    if (calendar[row][col].month() != calendar[1][1].month())
+                        classes.push('off');
+
+                    //don't allow selection of dates before the minimum date
+                    if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day'))
+                        classes.push('off', 'disabled');
+
+                    //don't allow selection of dates after the maximum date
+                    if (maxDate && calendar[row][col].isAfter(maxDate, 'day'))
+                        classes.push('off', 'disabled');
+
+                    //don't allow selection of date if a custom function decides it's invalid
+                    if (this.isInvalidDate(calendar[row][col]))
+                        classes.push('off', 'disabled');
+
+                    //highlight the currently selected start date
+                    if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD'))
+                        classes.push('active', 'start-date');
+
+                    //highlight the currently selected end date
+                    if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD'))
+                        classes.push('active', 'end-date');
+
+                    //highlight dates in-between the selected dates
+                    if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate)
+                        classes.push('in-range');
+
+                    //apply custom classes for this date
+                    var isCustom = this.isCustomDate(calendar[row][col]);
+                    if (isCustom !== false) {
+                        if (typeof isCustom === 'string')
+                            classes.push(isCustom);
+                        else
+                            Array.prototype.push.apply(classes, isCustom);
+                    }
+
+                    var cname = '', disabled = false;
+                    for (var i = 0; i < classes.length; i++) {
+                        cname += classes[i] + ' ';
+                        if (classes[i] == 'disabled')
+                            disabled = true;
+                    }
+                    if (!disabled)
+                        cname += 'available';
+
+                    html += '<td class="' + cname.replace(/^\s+|\s+$/g, '') + '" data-title="' + 'r' + row + 'c' + col + '">' + calendar[row][col].date() + '</td>';
+
+                }
+                html += '</tr>';
+            }
+
+            html += '</tbody>';
+            html += '</table>';
+
+            this.container.find('.calendar.' + side + ' .calendar-table').html(html);
+
+        },
+
+        renderTimePicker: function(side) {
+
+            // Don't bother updating the time picker if it's currently disabled
+            // because an end date hasn't been clicked yet
+            if (side == 'right' && !this.endDate) return;
+
+            var html, selected, minDate, maxDate = this.maxDate;
+
+            if (this.dateLimit && (!this.maxDate || this.startDate.clone().add(this.dateLimit).isAfter(this.maxDate)))
+                maxDate = this.startDate.clone().add(this.dateLimit);
+
+            if (side == 'left') {
+                selected = this.startDate.clone();
+                minDate = this.minDate;
+            } else if (side == 'right') {
+                selected = this.endDate.clone();
+                minDate = this.startDate;
+
+                //Preserve the time already selected
+                var timeSelector = this.container.find('.calendar.right .calendar-time div');
+                if (timeSelector.html() != '') {
+
+                    selected.hour(timeSelector.find('.hourselect option:selected').val() || selected.hour());
+                    selected.minute(timeSelector.find('.minuteselect option:selected').val() || selected.minute());
+                    selected.second(timeSelector.find('.secondselect option:selected').val() || selected.second());
+
+                    if (!this.timePicker24Hour) {
+                        var ampm = timeSelector.find('.ampmselect option:selected').val();
+                        if (ampm === 'PM' && selected.hour() < 12)
+                            selected.hour(selected.hour() + 12);
+                        if (ampm === 'AM' && selected.hour() === 12)
+                            selected.hour(0);
+                    }
+
+                }
+
+                if (selected.isBefore(this.startDate))
+                    selected = this.startDate.clone();
+
+                if (maxDate && selected.isAfter(maxDate))
+                    selected = maxDate.clone();
+
+            }
+
+            //
+            // hours
+            //
+
+            html = '<select class="hourselect">';
+
+            var start = this.timePicker24Hour ? 0 : 1;
+            var end = this.timePicker24Hour ? 23 : 12;
+
+            for (var i = start; i <= end; i++) {
+                var i_in_24 = i;
+                if (!this.timePicker24Hour)
+                    i_in_24 = selected.hour() >= 12 ? (i == 12 ? 12 : i + 12) : (i == 12 ? 0 : i);
+
+                var time = selected.clone().hour(i_in_24);
+                var disabled = false;
+                if (minDate && time.minute(59).isBefore(minDate))
+                    disabled = true;
+                if (maxDate && time.minute(0).isAfter(maxDate))
+                    disabled = true;
+
+                if (i_in_24 == selected.hour() && !disabled) {
+                    html += '<option value="' + i + '" selected="selected">' + i + '</option>';
+                } else if (disabled) {
+                    html += '<option value="' + i + '" disabled="disabled" class="disabled">' + i + '</option>';
+                } else {
+                    html += '<option value="' + i + '">' + i + '</option>';
+                }
+            }
+
+            html += '</select> ';
+
+            //
+            // minutes
+            //
+
+            html += ': <select class="minuteselect">';
+
+            for (var i = 0; i < 60; i += this.timePickerIncrement) {
+                var padded = i < 10 ? '0' + i : i;
+                var time = selected.clone().minute(i);
+
+                var disabled = false;
+                if (minDate && time.second(59).isBefore(minDate))
+                    disabled = true;
+                if (maxDate && time.second(0).isAfter(maxDate))
+                    disabled = true;
+
+                if (selected.minute() == i && !disabled) {
+                    html += '<option value="' + i + '" selected="selected">' + padded + '</option>';
+                } else if (disabled) {
+                    html += '<option value="' + i + '" disabled="disabled" class="disabled">' + padded + '</option>';
+                } else {
+                    html += '<option value="' + i + '">' + padded + '</option>';
+                }
+            }
+
+            html += '</select> ';
+
+            //
+            // seconds
+            //
+
+            if (this.timePickerSeconds) {
+                html += ': <select class="secondselect">';
+
+                for (var i = 0; i < 60; i++) {
+                    var padded = i < 10 ? '0' + i : i;
+                    var time = selected.clone().second(i);
+
+                    var disabled = false;
+                    if (minDate && time.isBefore(minDate))
+                        disabled = true;
+                    if (maxDate && time.isAfter(maxDate))
+                        disabled = true;
+
+                    if (selected.second() == i && !disabled) {
+                        html += '<option value="' + i + '" selected="selected">' + padded + '</option>';
+                    } else if (disabled) {
+                        html += '<option value="' + i + '" disabled="disabled" class="disabled">' + padded + '</option>';
+                    } else {
+                        html += '<option value="' + i + '">' + padded + '</option>';
+                    }
+                }
+
+                html += '</select> ';
+            }
+
+            //
+            // AM/PM
+            //
+
+            if (!this.timePicker24Hour) {
+                html += '<select class="ampmselect">';
+
+                var am_html = '';
+                var pm_html = '';
+
+                if (minDate && selected.clone().hour(12).minute(0).second(0).isBefore(minDate))
+                    am_html = ' disabled="disabled" class="disabled"';
+
+                if (maxDate && selected.clone().hour(0).minute(0).second(0).isAfter(maxDate))
+                    pm_html = ' disabled="disabled" class="disabled"';
+
+                if (selected.hour() >= 12) {
+                    html += '<option value="AM"' + am_html + '>AM</option><option value="PM" selected="selected"' + pm_html + '>PM</option>';
+                } else {
+                    html += '<option value="AM" selected="selected"' + am_html + '>AM</option><option value="PM"' + pm_html + '>PM</option>';
+                }
+
+                html += '</select>';
+            }
+
+            this.container.find('.calendar.' + side + ' .calendar-time div').html(html);
+
+        },
+
+        updateFormInputs: function() {
+
+            //ignore mouse movements while an above-calendar text input has focus
+            if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus"))
+                return;
+
+            this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.locale.format));
+            if (this.endDate)
+                this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.locale.format));
+
+            if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) {
+                this.container.find('button.applyBtn').removeAttr('disabled');
+            } else {
+                this.container.find('button.applyBtn').attr('disabled', 'disabled');
+            }
+
+        },
+
+        move: function() {
+            var parentOffset = { top: 0, left: 0 },
+                containerTop;
+            var parentRightEdge = $(window).width();
+            if (!this.parentEl.is('body')) {
+                parentOffset = {
+                    top: this.parentEl.offset().top - this.parentEl.scrollTop(),
+                    left: this.parentEl.offset().left - this.parentEl.scrollLeft()
+                };
+                parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left;
+            }
+
+            if (this.drops == 'up')
+                containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top;
+            else
+                containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top;
+            this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('dropup');
+
+            if (this.opens == 'left') {
+                this.container.css({
+                    top: containerTop,
+                    right: parentRightEdge - this.element.offset().left - this.element.outerWidth(),
+                    left: 'auto'
+                });
+                if (this.container.offset().left < 0) {
+                    this.container.css({
+                        right: 'auto',
+                        left: 9
+                    });
+                }
+            } else if (this.opens == 'center') {
+                this.container.css({
+                    top: containerTop,
+                    left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2
+                            - this.container.outerWidth() / 2,
+                    right: 'auto'
+                });
+                if (this.container.offset().left < 0) {
+                    this.container.css({
+                        right: 'auto',
+                        left: 9
+                    });
+                }
+            } else {
+                this.container.css({
+                    top: containerTop,
+                    left: this.element.offset().left - parentOffset.left,
+                    right: 'auto'
+                });
+                if (this.container.offset().left + this.container.outerWidth() > $(window).width()) {
+                    this.container.css({
+                        left: 'auto',
+                        right: 0
+                    });
+                }
+            }
+        },
+
+        show: function(e) {
+            if (this.isShowing) return;
+
+            // Create a click proxy that is private to this instance of datepicker, for unbinding
+            this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this);
+
+            // Bind global datepicker mousedown for hiding and
+            $(document)
+              .on('mousedown.daterangepicker', this._outsideClickProxy)
+              // also support mobile devices
+              .on('touchend.daterangepicker', this._outsideClickProxy)
+              // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them
+              .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy)
+              // and also close when focus changes to outside the picker (eg. tabbing between controls)
+              .on('focusin.daterangepicker', this._outsideClickProxy);
+
+            // Reposition the picker if the window is resized while it's open
+            $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this));
+
+            this.oldStartDate = this.startDate.clone();
+            this.oldEndDate = this.endDate.clone();
+            this.previousRightTime = this.endDate.clone();
+
+            this.updateView();
+            this.container.show();
+            this.move();
+            this.element.trigger('show.daterangepicker', this);
+            this.isShowing = true;
+        },
+
+        hide: function(e) {
+            if (!this.isShowing) return;
+
+            //incomplete date selection, revert to last values
+            if (!this.endDate) {
+                this.startDate = this.oldStartDate.clone();
+                this.endDate = this.oldEndDate.clone();
+            }
+
+            //if a new date range was selected, invoke the user callback function
+            if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
+                this.callback(this.startDate, this.endDate, this.chosenLabel);
+
+            //if picker is attached to a text input, update it
+            this.updateElement();
+
+            $(document).off('.daterangepicker');
+            $(window).off('.daterangepicker');
+            this.container.hide();
+            this.element.trigger('hide.daterangepicker', this);
+            this.isShowing = false;
+        },
+
+        toggle: function(e) {
+            if (this.isShowing) {
+                this.hide();
+            } else {
+                this.show();
+            }
+        },
+
+        outsideClick: function(e) {
+            var target = $(e.target);
+            // if the page is clicked anywhere except within the daterangerpicker/button
+            // itself then call this.hide()
+            if (
+                // ie modal dialog fix
+                e.type == "focusin" ||
+                target.closest(this.element).length ||
+                target.closest(this.container).length ||
+                target.closest('.calendar-table').length
+                ) return;
+            this.hide();
+            this.element.trigger('outsideClick.daterangepicker', this);
+        },
+
+        showCalendars: function() {
+            this.container.addClass('show-calendar');
+            this.move();
+            this.element.trigger('showCalendar.daterangepicker', this);
+        },
+
+        hideCalendars: function() {
+            this.container.removeClass('show-calendar');
+            this.element.trigger('hideCalendar.daterangepicker', this);
+        },
+
+        hoverRange: function(e) {
+
+            //ignore mouse movements while an above-calendar text input has focus
+            if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus"))
+                return;
+
+            var label = e.target.getAttribute('data-range-key');
+
+            if (label == this.locale.customRangeLabel) {
+                this.updateView();
+            } else {
+                var dates = this.ranges[label];
+                this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.locale.format));
+                this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.locale.format));
+            }
+
+        },
+
+        clickRange: function(e) {
+            var label = e.target.getAttribute('data-range-key');
+            this.chosenLabel = label;
+            if (label == this.locale.customRangeLabel) {
+                this.showCalendars();
+            } else {
+                var dates = this.ranges[label];
+                this.startDate = dates[0];
+                this.endDate = dates[1];
+
+                if (!this.timePicker) {
+                    this.startDate.startOf('day');
+                    this.endDate.endOf('day');
+                }
+
+                if (!this.alwaysShowCalendars)
+                    this.hideCalendars();
+                this.clickApply();
+            }
+        },
+
+        clickPrev: function(e) {
+            var cal = $(e.target).parents('.calendar');
+            if (cal.hasClass('left')) {
+                this.leftCalendar.month.subtract(1, 'month');
+                if (this.linkedCalendars)
+                    this.rightCalendar.month.subtract(1, 'month');
+            } else {
+                this.rightCalendar.month.subtract(1, 'month');
+            }
+            this.updateCalendars();
+        },
+
+        clickNext: function(e) {
+            var cal = $(e.target).parents('.calendar');
+            if (cal.hasClass('left')) {
+                this.leftCalendar.month.add(1, 'month');
+            } else {
+                this.rightCalendar.month.add(1, 'month');
+                if (this.linkedCalendars)
+                    this.leftCalendar.month.add(1, 'month');
+            }
+            this.updateCalendars();
+        },
+
+        hoverDate: function(e) {
+
+            //ignore mouse movements while an above-calendar text input has focus
+            //if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus"))
+            //    return;
+
+            //ignore dates that can't be selected
+            if (!$(e.target).hasClass('available')) return;
+
+            //have the text inputs above calendars reflect the date being hovered over
+            var title = $(e.target).attr('data-title');
+            var row = title.substr(1, 1);
+            var col = title.substr(3, 1);
+            var cal = $(e.target).parents('.calendar');
+            var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
+
+            if (this.endDate && !this.container.find('input[name=daterangepicker_start]').is(":focus")) {
+                this.container.find('input[name=daterangepicker_start]').val(date.format(this.locale.format));
+            } else if (!this.endDate && !this.container.find('input[name=daterangepicker_end]').is(":focus")) {
+                this.container.find('input[name=daterangepicker_end]').val(date.format(this.locale.format));
+            }
+
+            //highlight the dates between the start date and the date being hovered as a potential end date
+            var leftCalendar = this.leftCalendar;
+            var rightCalendar = this.rightCalendar;
+            var startDate = this.startDate;
+            if (!this.endDate) {
+                this.container.find('.calendar tbody td').each(function(index, el) {
+
+                    //skip week numbers, only look at dates
+                    if ($(el).hasClass('week')) return;
+
+                    var title = $(el).attr('data-title');
+                    var row = title.substr(1, 1);
+                    var col = title.substr(3, 1);
+                    var cal = $(el).parents('.calendar');
+                    var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col];
+
+                    if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) {
+                        $(el).addClass('in-range');
+                    } else {
+                        $(el).removeClass('in-range');
+                    }
+
+                });
+            }
+
+        },
+
+        clickDate: function(e) {
+
+            if (!$(e.target).hasClass('available')) return;
+
+            var title = $(e.target).attr('data-title');
+            var row = title.substr(1, 1);
+            var col = title.substr(3, 1);
+            var cal = $(e.target).parents('.calendar');
+            var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
+
+            //
+            // this function needs to do a few things:
+            // * alternate between selecting a start and end date for the range,
+            // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date
+            // * if autoapply is enabled, and an end date was chosen, apply the selection
+            // * if single date picker mode, and time picker isn't enabled, apply the selection immediately
+            // * if one of the inputs above the calendars was focused, cancel that manual input
+            //
+
+            if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start
+                if (this.timePicker) {
+                    var hour = parseInt(this.container.find('.left .hourselect').val(), 10);
+                    if (!this.timePicker24Hour) {
+                        var ampm = this.container.find('.left .ampmselect').val();
+                        if (ampm === 'PM' && hour < 12)
+                            hour += 12;
+                        if (ampm === 'AM' && hour === 12)
+                            hour = 0;
+                    }
+                    var minute = parseInt(this.container.find('.left .minuteselect').val(), 10);
+                    var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0;
+                    date = date.clone().hour(hour).minute(minute).second(second);
+                }
+                this.endDate = null;
+                this.setStartDate(date.clone());
+            } else if (!this.endDate && date.isBefore(this.startDate)) {
+                //special case: clicking the same date for start/end,
+                //but the time of the end date is before the start date
+                this.setEndDate(this.startDate.clone());
+            } else { // picking end
+                if (this.timePicker) {
+                    var hour = parseInt(this.container.find('.right .hourselect').val(), 10);
+                    if (!this.timePicker24Hour) {
+                        var ampm = this.container.find('.right .ampmselect').val();
+                        if (ampm === 'PM' && hour < 12)
+                            hour += 12;
+                        if (ampm === 'AM' && hour === 12)
+                            hour = 0;
+                    }
+                    var minute = parseInt(this.container.find('.right .minuteselect').val(), 10);
+                    var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0;
+                    date = date.clone().hour(hour).minute(minute).second(second);
+                }
+                this.setEndDate(date.clone());
+                if (this.autoApply) {
+                  this.calculateChosenLabel();
+                  this.clickApply();
+                }
+            }
+
+            if (this.singleDatePicker) {
+                this.setEndDate(this.startDate);
+                if (!this.timePicker)
+                    this.clickApply();
+            }
+
+            this.updateView();
+
+            //This is to cancel the blur event handler if the mouse was in one of the inputs
+            e.stopPropagation();
+
+        },
+
+        calculateChosenLabel: function () {
+            var customRange = true;
+            var i = 0;
+            for (var range in this.ranges) {
+                if (this.timePicker) {
+                    if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) {
+                        customRange = false;
+                        this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html();
+                        break;
+                    }
+                } else {
+                    //ignore times when comparing dates if time picker is not enabled
+                    if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) {
+                        customRange = false;
+                        this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html();
+                        break;
+                    }
+                }
+                i++;
+            }
+            if (customRange) {
+                if (this.showCustomRangeLabel) {
+                    this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html();
+                } else {
+                    this.chosenLabel = null;
+                }
+                this.showCalendars();
+            }
+        },
+
+        clickApply: function(e) {
+            this.hide();
+            this.element.trigger('apply.daterangepicker', this);
+        },
+
+        clickCancel: function(e) {
+            this.startDate = this.oldStartDate;
+            this.endDate = this.oldEndDate;
+            this.hide();
+            this.element.trigger('cancel.daterangepicker', this);
+        },
+
+        monthOrYearChanged: function(e) {
+            var isLeft = $(e.target).closest('.calendar').hasClass('left'),
+                leftOrRight = isLeft ? 'left' : 'right',
+                cal = this.container.find('.calendar.'+leftOrRight);
+
+            // Month must be Number for new moment versions
+            var month = parseInt(cal.find('.monthselect').val(), 10);
+            var year = cal.find('.yearselect').val();
+
+            if (!isLeft) {
+                if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) {
+                    month = this.startDate.month();
+                    year = this.startDate.year();
+                }
+            }
+
+            if (this.minDate) {
+                if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) {
+                    month = this.minDate.month();
+                    year = this.minDate.year();
+                }
+            }
+
+            if (this.maxDate) {
+                if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) {
+                    month = this.maxDate.month();
+                    year = this.maxDate.year();
+                }
+            }
+
+            if (isLeft) {
+                this.leftCalendar.month.month(month).year(year);
+                if (this.linkedCalendars)
+                    this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month');
+            } else {
+                this.rightCalendar.month.month(month).year(year);
+                if (this.linkedCalendars)
+                    this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month');
+            }
+            this.updateCalendars();
+        },
+
+        timeChanged: function(e) {
+
+            var cal = $(e.target).closest('.calendar'),
+                isLeft = cal.hasClass('left');
+
+            var hour = parseInt(cal.find('.hourselect').val(), 10);
+            var minute = parseInt(cal.find('.minuteselect').val(), 10);
+            var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0;
+
+            if (!this.timePicker24Hour) {
+                var ampm = cal.find('.ampmselect').val();
+                if (ampm === 'PM' && hour < 12)
+                    hour += 12;
+                if (ampm === 'AM' && hour === 12)
+                    hour = 0;
+            }
+
+            if (isLeft) {
+                var start = this.startDate.clone();
+                start.hour(hour);
+                start.minute(minute);
+                start.second(second);
+                this.setStartDate(start);
+                if (this.singleDatePicker) {
+                    this.endDate = this.startDate.clone();
+                } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) {
+                    this.setEndDate(start.clone());
+                }
+            } else if (this.endDate) {
+                var end = this.endDate.clone();
+                end.hour(hour);
+                end.minute(minute);
+                end.second(second);
+                this.setEndDate(end);
+            }
+
+            //update the calendars so all clickable dates reflect the new time component
+            this.updateCalendars();
+
+            //update the form inputs above the calendars with the new time
+            this.updateFormInputs();
+
+            //re-render the time pickers because changing one selection can affect what's enabled in another
+            this.renderTimePicker('left');
+            this.renderTimePicker('right');
+
+        },
+
+        formInputsChanged: function(e) {
+            var isRight = $(e.target).closest('.calendar').hasClass('right');
+            var start = moment(this.container.find('input[name="daterangepicker_start"]').val(), this.locale.format);
+            var end = moment(this.container.find('input[name="daterangepicker_end"]').val(), this.locale.format);
+
+            if (start.isValid() && end.isValid()) {
+
+                if (isRight && end.isBefore(start))
+                    start = end.clone();
+
+                this.setStartDate(start);
+                this.setEndDate(end);
+
+                if (isRight) {
+                    this.container.find('input[name="daterangepicker_start"]').val(this.startDate.format(this.locale.format));
+                } else {
+                    this.container.find('input[name="daterangepicker_end"]').val(this.endDate.format(this.locale.format));
+                }
+
+            }
+
+            this.updateView();
+        },
+
+        formInputsFocused: function(e) {
+
+            // Highlight the focused input
+            this.container.find('input[name="daterangepicker_start"], input[name="daterangepicker_end"]').removeClass('active');
+            $(e.target).addClass('active');
+
+            // Set the state such that if the user goes back to using a mouse, 
+            // the calendars are aware we're selecting the end of the range, not
+            // the start. This allows someone to edit the end of a date range without
+            // re-selecting the beginning, by clicking on the end date input then
+            // using the calendar.
+            var isRight = $(e.target).closest('.calendar').hasClass('right');
+            if (isRight) {
+                this.endDate = null;
+                this.setStartDate(this.startDate.clone());
+                this.updateView();
+            }
+
+        },
+
+        formInputsBlurred: function(e) {
+
+            // this function has one purpose right now: if you tab from the first
+            // text input to the second in the UI, the endDate is nulled so that
+            // you can click another, but if you tab out without clicking anything
+            // or changing the input value, the old endDate should be retained
+
+            if (!this.endDate) {
+                var val = this.container.find('input[name="daterangepicker_end"]').val();
+                var end = moment(val, this.locale.format);
+                if (end.isValid()) {
+                    this.setEndDate(end);
+                    this.updateView();
+                }
+            }
+
+        },
+
+        elementChanged: function() {
+            if (!this.element.is('input')) return;
+            if (!this.element.val().length) return;
+            if (this.element.val().length < this.locale.format.length) return;
+
+            var dateString = this.element.val().split(this.locale.separator),
+                start = null,
+                end = null;
+
+            if (dateString.length === 2) {
+                start = moment(dateString[0], this.locale.format);
+                end = moment(dateString[1], this.locale.format);
+            }
+
+            if (this.singleDatePicker || start === null || end === null) {
+                start = moment(this.element.val(), this.locale.format);
+                end = start;
+            }
+
+            if (!start.isValid() || !end.isValid()) return;
+
+            this.setStartDate(start);
+            this.setEndDate(end);
+            this.updateView();
+        },
+
+        keydown: function(e) {
+            //hide on tab or enter
+            if ((e.keyCode === 9) || (e.keyCode === 13)) {
+                this.hide();
+            }
+        },
+
+        updateElement: function() {
+            if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) {
+                this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format));
+                this.element.trigger('change');
+            } else if (this.element.is('input') && this.autoUpdateInput) {
+                this.element.val(this.startDate.format(this.locale.format));
+                this.element.trigger('change');
+            }
+        },
+
+        remove: function() {
+            this.container.remove();
+            this.element.off('.daterangepicker');
+            this.element.removeData();
+        }
+
+    };
+
+    $.fn.daterangepicker = function(options, callback) {
+        this.each(function() {
+            var el = $(this);
+            if (el.data('daterangepicker'))
+                el.data('daterangepicker').remove();
+            el.data('daterangepicker', new DateRangePicker(el, options, callback));
+        });
+        return this;
+    };
+
+    return DateRangePicker;
+
+}));

+ 663 - 0
gui/default/vendor/fancytree/css/ui.fancytree.css

@@ -0,0 +1,663 @@
+/*!
+ * Fancytree "Lion" skin.
+ *
+ * DON'T EDIT THE CSS FILE DIRECTLY, since it is automatically generated from
+ * the LESS templates.
+ */
+/*
+ Lion colors:
+	gray highlight bar: #D4D4D4
+	blue highlight-bar and -border #3875D7
+
+*/
+/*******************************************************************************
+ * Common Styles for Fancytree Skins.
+ *
+ * This section is automatically generated from the `skin-common.less` template.
+ ******************************************************************************/
+/*------------------------------------------------------------------------------
+ * Helpers
+ *----------------------------------------------------------------------------*/
+.ui-helper-hidden {
+  display: none;
+}
+/*------------------------------------------------------------------------------
+ * Container and UL / LI
+ *----------------------------------------------------------------------------*/
+ul.fancytree-container {
+  font-family: tahoma, arial, helvetica;
+  font-size: 10pt;
+  white-space: nowrap;
+  padding: 3px;
+  margin: 0;
+  background-color: white;
+  border: 1px dotted gray;
+  min-height: 0%;
+  position: relative;
+}
+ul.fancytree-container ul {
+  padding: 0 0 0 16px;
+  margin: 0;
+}
+ul.fancytree-container ul > li:before {
+  content: none;
+}
+ul.fancytree-container li {
+  list-style-image: none;
+  list-style-position: outside;
+  list-style-type: none;
+  -moz-background-clip: border;
+  -moz-background-inline-policy: continuous;
+  -moz-background-origin: padding;
+  background-attachment: scroll;
+  background-color: transparent;
+  background-position: 0px 0px;
+  background-repeat: repeat-y;
+  background-image: none;
+  margin: 0;
+}
+ul.fancytree-container li.fancytree-lastsib {
+  background-image: none;
+}
+.ui-fancytree-disabled ul.fancytree-container {
+  opacity: 0.5;
+  background-color: silver;
+}
+ul.fancytree-connectors.fancytree-container li {
+  background-image: url("../skin-lion/vline.gif");
+  background-position: 0 0;
+}
+ul.fancytree-container li.fancytree-lastsib,
+ul.fancytree-no-connector > li {
+  background-image: none;
+}
+li.fancytree-animating {
+  position: relative;
+}
+/*------------------------------------------------------------------------------
+ * Common icon definitions
+ *----------------------------------------------------------------------------*/
+span.fancytree-empty,
+span.fancytree-vline,
+span.fancytree-expander,
+span.fancytree-icon,
+span.fancytree-checkbox,
+span.fancytree-drag-helper-img,
+#fancytree-drop-marker {
+  width: 16px;
+  height: 16px;
+  display: inline-block;
+  vertical-align: top;
+  background-repeat: no-repeat;
+  background-position: left;
+  background-image: url("../skin-lion/icons.gif");
+  background-position: 0px 0px;
+}
+span.fancytree-icon,
+span.fancytree-checkbox,
+span.fancytree-expander,
+span.fancytree-custom-icon {
+  margin-top: 0px;
+}
+/* Used by icon option: */
+span.fancytree-custom-icon {
+  width: 16px;
+  height: 16px;
+  display: inline-block;
+  margin-left: 3px;
+  background-position: 0px 0px;
+}
+/* Used by 'icon' node option: */
+img.fancytree-icon {
+  width: 16px;
+  height: 16px;
+  margin-left: 3px;
+  margin-top: 0px;
+  vertical-align: top;
+  border-style: none;
+}
+/*------------------------------------------------------------------------------
+ * Expander icon
+ *
+ * Note: IE6 doesn't correctly evaluate multiples class names,
+ *		 so we create combined class names that can be used in the CSS.
+ *
+ * Prefix: fancytree-exp-
+ * 1st character: 'e': expanded, 'c': collapsed, 'n': no children
+ * 2nd character (optional): 'd': lazy (Delayed)
+ * 3rd character (optional): 'l': Last sibling
+ *----------------------------------------------------------------------------*/
+span.fancytree-expander {
+  cursor: pointer;
+}
+.fancytree-exp-n span.fancytree-expander,
+.fancytree-exp-nl span.fancytree-expander {
+  background-image: none;
+  cursor: default;
+}
+.fancytree-connectors .fancytree-exp-n span.fancytree-expander,
+.fancytree-connectors .fancytree-exp-nl span.fancytree-expander {
+  background-image: url("../skin-lion/icons.gif");
+  margin-top: 0;
+}
+.fancytree-connectors .fancytree-exp-n span.fancytree-expander,
+.fancytree-connectors .fancytree-exp-n span.fancytree-expander:hover {
+  background-position: 0px -64px;
+}
+.fancytree-connectors .fancytree-exp-nl span.fancytree-expander,
+.fancytree-connectors .fancytree-exp-nl span.fancytree-expander:hover {
+  background-position: -16px -64px;
+}
+.fancytree-exp-c span.fancytree-expander {
+  background-position: 0px -80px;
+}
+.fancytree-exp-c span.fancytree-expander:hover {
+  background-position: -16px -80px;
+}
+.fancytree-exp-cl span.fancytree-expander {
+  background-position: 0px -96px;
+}
+.fancytree-exp-cl span.fancytree-expander:hover {
+  background-position: -16px -96px;
+}
+.fancytree-exp-cd span.fancytree-expander {
+  background-position: -64px -80px;
+}
+.fancytree-exp-cd span.fancytree-expander:hover {
+  background-position: -80px -80px;
+}
+.fancytree-exp-cdl span.fancytree-expander {
+  background-position: -64px -96px;
+}
+.fancytree-exp-cdl span.fancytree-expander:hover {
+  background-position: -80px -96px;
+}
+.fancytree-exp-e span.fancytree-expander,
+.fancytree-exp-ed span.fancytree-expander {
+  background-position: -32px -80px;
+}
+.fancytree-exp-e span.fancytree-expander:hover,
+.fancytree-exp-ed span.fancytree-expander:hover {
+  background-position: -48px -80px;
+}
+.fancytree-exp-el span.fancytree-expander,
+.fancytree-exp-edl span.fancytree-expander {
+  background-position: -32px -96px;
+}
+.fancytree-exp-el span.fancytree-expander:hover,
+.fancytree-exp-edl span.fancytree-expander:hover {
+  background-position: -48px -96px;
+}
+/* Fade out expanders, when container is not hovered or active */
+.fancytree-fade-expander span.fancytree-expander {
+  transition: opacity 1.5s;
+  opacity: 0;
+}
+.fancytree-fade-expander:hover span.fancytree-expander,
+.fancytree-fade-expander.fancytree-treefocus span.fancytree-expander,
+.fancytree-fade-expander .fancytree-treefocus span.fancytree-expander,
+.fancytree-fade-expander [class*='fancytree-statusnode-'] span.fancytree-expander {
+  transition: opacity 0.6s;
+  opacity: 1;
+}
+/*------------------------------------------------------------------------------
+ * Checkbox icon
+ *----------------------------------------------------------------------------*/
+span.fancytree-checkbox {
+  margin-left: 3px;
+  background-position: 0px -32px;
+}
+span.fancytree-checkbox:hover {
+  background-position: -16px -32px;
+}
+span.fancytree-checkbox.fancytree-radio {
+  background-position: 0px -48px;
+}
+span.fancytree-checkbox.fancytree-radio:hover {
+  background-position: -16px -48px;
+}
+.fancytree-partsel span.fancytree-checkbox {
+  background-position: -64px -32px;
+}
+.fancytree-partsel span.fancytree-checkbox:hover {
+  background-position: -80px -32px;
+}
+.fancytree-partsel span.fancytree-checkbox.fancytree-radio {
+  background-position: -64px -48px;
+}
+.fancytree-partsel span.fancytree-checkbox.fancytree-radio:hover {
+  background-position: -80px -48px;
+}
+.fancytree-selected span.fancytree-checkbox {
+  background-position: -32px -32px;
+}
+.fancytree-selected span.fancytree-checkbox:hover {
+  background-position: -48px -32px;
+}
+.fancytree-selected span.fancytree-checkbox.fancytree-radio {
+  background-position: -32px -48px;
+}
+.fancytree-selected span.fancytree-checkbox.fancytree-radio:hover {
+  background-position: -48px -48px;
+}
+.fancytree-unselectable span.fancytree-checkbox {
+  opacity: 0.4;
+  filter: alpha(opacity=40);
+}
+.fancytree-unselectable span.fancytree-checkbox:hover {
+  background-position: 0px -32px;
+}
+.fancytree-unselectable.fancytree-partsel span.fancytree-checkbox:hover {
+  background-position: -64px -32px;
+}
+.fancytree-unselectable.fancytree-selected span.fancytree-checkbox:hover {
+  background-position: -32px -32px;
+}
+/*------------------------------------------------------------------------------
+ * Node type icon
+ * Note: IE6 doesn't correctly evaluate multiples class names,
+ *		 so we create combined class names that can be used in the CSS.
+ *
+ * Prefix: fancytree-ico-
+ * 1st character: 'e': expanded, 'c': collapsed
+ * 2nd character (optional): 'f': folder
+ *----------------------------------------------------------------------------*/
+span.fancytree-icon {
+  margin-left: 3px;
+  background-position: 0px 0px;
+}
+/* Documents */
+.fancytree-ico-c span.fancytree-icon:hover {
+  background-position: -16px 0px;
+}
+.fancytree-has-children.fancytree-ico-c span.fancytree-icon {
+  background-position: -32px 0px;
+}
+.fancytree-has-children.fancytree-ico-c span.fancytree-icon:hover {
+  background-position: -48px 0px;
+}
+.fancytree-ico-e span.fancytree-icon {
+  background-position: -64px 0px;
+}
+.fancytree-ico-e span.fancytree-icon:hover {
+  background-position: -80px 0px;
+}
+/* Folders */
+.fancytree-ico-cf span.fancytree-icon {
+  background-position: 0px -16px;
+}
+.fancytree-ico-cf span.fancytree-icon:hover {
+  background-position: -16px -16px;
+}
+.fancytree-has-children.fancytree-ico-cf span.fancytree-icon {
+  background-position: -32px -16px;
+}
+.fancytree-has-children.fancytree-ico-cf span.fancytree-icon:hover {
+  background-position: -48px -16px;
+}
+.fancytree-ico-ef span.fancytree-icon {
+  background-position: -64px -16px;
+}
+.fancytree-ico-ef span.fancytree-icon:hover {
+  background-position: -80px -16px;
+}
+.fancytree-loading span.fancytree-expander,
+.fancytree-loading span.fancytree-expander:hover,
+.fancytree-statusnode-loading span.fancytree-icon,
+.fancytree-statusnode-loading span.fancytree-icon:hover {
+  background-image: url("../skin-lion/loading.gif");
+  background-position: 0px 0px;
+}
+/* Status node icons */
+.fancytree-statusnode-error span.fancytree-icon,
+.fancytree-statusnode-error span.fancytree-icon:hover {
+  background-position: 0px -112px;
+}
+/*------------------------------------------------------------------------------
+ * Node titles and highlighting
+ *----------------------------------------------------------------------------*/
+span.fancytree-node {
+  /* See #117 */
+  display: inherit;
+  width: 100%;
+  margin-top: 1px;
+  min-height: 16px;
+}
+span.fancytree-title {
+  color: black;
+  cursor: pointer;
+  display: inline-block;
+  vertical-align: top;
+  min-height: 16px;
+  padding: 0 3px 0 3px;
+  margin: 0px 0 0 3px;
+  border: 1px solid transparent;
+  -webkit-border-radius: 0px;
+  -moz-border-radius: 0px;
+  -ms-border-radius: 0px;
+  -o-border-radius: 0px;
+  border-radius: 0px;
+}
+span.fancytree-node.fancytree-error span.fancytree-title {
+  color: red;
+}
+/*------------------------------------------------------------------------------
+ * Drag'n'drop support
+ *----------------------------------------------------------------------------*/
+div.fancytree-drag-helper span.fancytree-childcounter,
+div.fancytree-drag-helper span.fancytree-dnd-modifier {
+  display: inline-block;
+  color: #fff;
+  background: #337ab7;
+  border: 1px solid gray;
+  min-width: 10px;
+  height: 10px;
+  line-height: 1;
+  vertical-align: baseline;
+  border-radius: 10px;
+  padding: 2px;
+  text-align: center;
+  font-size: 9px;
+}
+div.fancytree-drag-helper span.fancytree-childcounter {
+  position: absolute;
+  top: -6px;
+  right: -6px;
+}
+div.fancytree-drag-helper span.fancytree-dnd-modifier {
+  background: #5cb85c;
+  border: none;
+  font-weight: bolder;
+}
+div.fancytree-drag-helper.fancytree-drop-accept span.fancytree-drag-helper-img {
+  background-position: -32px -112px;
+}
+div.fancytree-drag-helper.fancytree-drop-reject span.fancytree-drag-helper-img {
+  background-position: -16px -112px;
+}
+/*** Drop marker icon *********************************************************/
+#fancytree-drop-marker {
+  width: 32px;
+  position: absolute;
+  background-position: 0px -128px;
+  margin: 0;
+}
+#fancytree-drop-marker.fancytree-drop-after,
+#fancytree-drop-marker.fancytree-drop-before {
+  width: 64px;
+  background-position: 0px -144px;
+}
+#fancytree-drop-marker.fancytree-drop-copy {
+  background-position: -64px -128px;
+}
+#fancytree-drop-marker.fancytree-drop-move {
+  background-position: -32px -128px;
+}
+/*** Source node while dragging ***********************************************/
+span.fancytree-drag-source.fancytree-drag-remove {
+  opacity: 0.15;
+}
+/*** Target node while dragging cursor is over it *****************************/
+/*------------------------------------------------------------------------------
+ * 'rtl' option
+ *----------------------------------------------------------------------------*/
+.fancytree-container.fancytree-rtl .fancytree-title {
+  /*unicode-bidi: bidi-override;*/
+  /* optional: reverse title letters */
+}
+.fancytree-container.fancytree-rtl span.fancytree-connector,
+.fancytree-container.fancytree-rtl span.fancytree-expander,
+.fancytree-container.fancytree-rtl span.fancytree-icon,
+.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img,
+.fancytree-container.fancytree-rtl #fancytree-drop-marker {
+  background-image: url("../skin-lion/icons-rtl.gif");
+}
+.fancytree-container.fancytree-rtl .fancytree-exp-n span.fancytree-expander,
+.fancytree-container.fancytree-rtl .fancytree-exp-nl span.fancytree-expander {
+  background-image: none;
+}
+.fancytree-container.fancytree-rtl.fancytree-connectors .fancytree-exp-n span.fancytree-expander,
+.fancytree-container.fancytree-rtl.fancytree-connectors .fancytree-exp-nl span.fancytree-expander {
+  background-image: url("../skin-lion/icons-rtl.gif");
+}
+ul.fancytree-container.fancytree-rtl ul {
+  padding: 0 16px 0 0;
+}
+ul.fancytree-container.fancytree-rtl.fancytree-connectors li {
+  background-position: right 0;
+  background-image: url("../skin-lion/vline-rtl.gif");
+}
+ul.fancytree-container.fancytree-rtl li.fancytree-lastsib,
+ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
+  background-image: none;
+}
+/*------------------------------------------------------------------------------
+ * 'table' extension
+ *----------------------------------------------------------------------------*/
+table.fancytree-ext-table {
+  border-collapse: collapse;
+}
+table.fancytree-ext-table span.fancytree-node {
+  display: inline-block;
+  box-sizing: border-box;
+}
+/*------------------------------------------------------------------------------
+ * 'columnview' extension
+ *----------------------------------------------------------------------------*/
+table.fancytree-ext-columnview tbody tr td {
+  position: relative;
+  border: 1px solid gray;
+  vertical-align: top;
+  overflow: auto;
+}
+table.fancytree-ext-columnview tbody tr td > ul {
+  padding: 0;
+}
+table.fancytree-ext-columnview tbody tr td > ul li {
+  list-style-image: none;
+  list-style-position: outside;
+  list-style-type: none;
+  -moz-background-clip: border;
+  -moz-background-inline-policy: continuous;
+  -moz-background-origin: padding;
+  background-attachment: scroll;
+  background-color: transparent;
+  background-position: 0px 0px;
+  background-repeat: repeat-y;
+  background-image: none;
+  /* no v-lines */
+  margin: 0;
+}
+table.fancytree-ext-columnview span.fancytree-node {
+  position: relative;
+  /* allow positioning of embedded spans */
+  display: inline-block;
+}
+table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded {
+  background-color: #CBE8F6;
+}
+table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right {
+  position: absolute;
+  right: 3px;
+  background-position: 0px -80px;
+}
+table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:hover {
+  background-position: -16px -80px;
+}
+/*------------------------------------------------------------------------------
+ * 'filter' extension
+ *----------------------------------------------------------------------------*/
+.fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title {
+  color: silver;
+  font-weight: lighter;
+}
+.fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title,
+.fancytree-ext-filter-dimm span.fancytree-node.fancytree-submatch span.fancytree-title {
+  color: black;
+  font-weight: normal;
+}
+.fancytree-ext-filter-dimm tr.fancytree-match span.fancytree-title,
+.fancytree-ext-filter-dimm span.fancytree-node.fancytree-match span.fancytree-title {
+  color: black;
+  font-weight: bold;
+}
+.fancytree-ext-filter-hide tr.fancytree-hide,
+.fancytree-ext-filter-hide span.fancytree-node.fancytree-hide {
+  display: none;
+}
+.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
+.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
+  color: silver;
+  font-weight: lighter;
+}
+.fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title,
+.fancytree-ext-filter-hide span.fancytree-node.fancytree-match span.fancytree-title {
+  color: black;
+  font-weight: normal;
+}
+/* Hide expanders if all child nodes are hidden by filter */
+.fancytree-ext-filter-hide-expanders tr.fancytree-match span.fancytree-expander,
+.fancytree-ext-filter-hide-expanders span.fancytree-node.fancytree-match span.fancytree-expander {
+  visibility: hidden;
+}
+.fancytree-ext-filter-hide-expanders tr.fancytree-submatch span.fancytree-expander,
+.fancytree-ext-filter-hide-expanders span.fancytree-node.fancytree-submatch span.fancytree-expander {
+  visibility: visible;
+}
+.fancytree-ext-childcounter span.fancytree-icon,
+.fancytree-ext-filter span.fancytree-icon {
+  position: relative;
+}
+.fancytree-ext-childcounter span.fancytree-childcounter,
+.fancytree-ext-filter span.fancytree-childcounter {
+  color: #fff;
+  background: #777;
+  border: 1px solid gray;
+  position: absolute;
+  top: -6px;
+  right: -6px;
+  min-width: 10px;
+  height: 10px;
+  line-height: 1;
+  vertical-align: baseline;
+  border-radius: 10px;
+  padding: 2px;
+  text-align: center;
+  font-size: 9px;
+}
+/*------------------------------------------------------------------------------
+ * 'wide' extension
+ *----------------------------------------------------------------------------*/
+ul.fancytree-ext-wide {
+  position: relative;
+  min-width: 100%;
+  z-index: 2;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+ul.fancytree-ext-wide span.fancytree-node > span {
+  position: relative;
+  z-index: 2;
+}
+ul.fancytree-ext-wide span.fancytree-node span.fancytree-title {
+  position: absolute;
+  z-index: 1;
+  left: 0px;
+  min-width: 100%;
+  margin-left: 0;
+  margin-right: 0;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+/*------------------------------------------------------------------------------
+ * 'fixed' extension
+ *----------------------------------------------------------------------------*/
+.fancytree-ext-fixed-wrapper .fancytree-ext-fixed-hidden {
+  display: none;
+}
+.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-scroll-border-bottom {
+  border-bottom: 3px solid rgba(0, 0, 0, 0.75);
+}
+.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-scroll-border-right {
+  border-right: 3px solid rgba(0, 0, 0, 0.75);
+}
+.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-tl {
+  position: absolute;
+  overflow: hidden;
+  z-index: 3;
+  top: 0px;
+  left: 0px;
+}
+.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-tr {
+  position: absolute;
+  overflow: hidden;
+  z-index: 2;
+  top: 0px;
+}
+.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-bl {
+  position: absolute;
+  overflow: hidden;
+  z-index: 2;
+  left: 0px;
+}
+.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-br {
+  position: absolute;
+  overflow: scroll;
+  z-index: 1;
+}
+/*******************************************************************************
+ * Styles specific to this skin.
+ *
+ * This section is automatically generated from the `ui-fancytree.less` template.
+ ******************************************************************************/
+/*******************************************************************************
+ * Node titles
+ */
+span.fancytree-title {
+  border: 1px solid transparent;
+  border-radius: 0;
+}
+span.fancytree-focused span.fancytree-title {
+  outline: 1px dotted black;
+}
+span.fancytree-selected span.fancytree-title,
+span.fancytree-active span.fancytree-title {
+  background-color: #D4D4D4;
+}
+span.fancytree-selected span.fancytree-title {
+  font-style: italic;
+}
+.fancytree-treefocus span.fancytree-selected span.fancytree-title,
+.fancytree-treefocus span.fancytree-active span.fancytree-title {
+  color: white;
+  background-color: #3875D7;
+}
+/*******************************************************************************
+ * 'table' extension
+ */
+table.fancytree-ext-table {
+  border-collapse: collapse;
+}
+table.fancytree-ext-table tbody tr.fancytree-focused {
+  background-color: #99DEFD;
+}
+table.fancytree-ext-table tbody tr.fancytree-active {
+  background-color: royalblue;
+}
+table.fancytree-ext-table tbody tr.fancytree-selected {
+  background-color: #99DEFD;
+}
+/*******************************************************************************
+ * 'columnview' extension
+ */
+table.fancytree-ext-columnview tbody tr td {
+  border: 1px solid gray;
+}
+table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded {
+  background-color: #ccc;
+}
+table.fancytree-ext-columnview span.fancytree-node.fancytree-active {
+  background-color: royalblue;
+}

+ 12045 - 0
gui/default/vendor/fancytree/jquery.fancytree-all-deps.js

@@ -0,0 +1,12045 @@
+/*! jQuery Fancytree Plugin - 2.26.0 - 2017-11-04T17:52:53Z
+  * https://github.com/mar10/fancytree
+  * Copyright (c) 2017 Martin Wendt; Licensed MIT
+ */
+/*! jQuery UI - v1.12.1 - 2017-02-23
+* http://jqueryui.com
+* Includes: widget.js, position.js, keycode.js, scroll-parent.js, unique-id.js, effect.js, effects/effect-blind.js
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+/*
+	NOTE: Original jQuery UI wrapper was replaced with a simple IIFE.
+	See README-Fancytree.md
+*/
+(function( $ ) {
+
+$.ui = $.ui || {};
+
+var version = $.ui.version = "1.12.1";
+
+
+/*!
+ * jQuery UI Widget 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Widget
+//>>group: Core
+//>>description: Provides a factory for creating stateful widgets with a common API.
+//>>docs: http://api.jqueryui.com/jQuery.widget/
+//>>demos: http://jqueryui.com/widget/
+
+
+
+var widgetUuid = 0;
+var widgetSlice = Array.prototype.slice;
+
+$.cleanData = ( function( orig ) {
+	return function( elems ) {
+		var events, elem, i;
+		for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) {
+			try {
+
+				// Only trigger remove when necessary to save time
+				events = $._data( elem, "events" );
+				if ( events && events.remove ) {
+					$( elem ).triggerHandler( "remove" );
+				}
+
+			// Http://bugs.jquery.com/ticket/8235
+			} catch ( e ) {}
+		}
+		orig( elems );
+	};
+} )( $.cleanData );
+
+$.widget = function( name, base, prototype ) {
+	var existingConstructor, constructor, basePrototype;
+
+	// ProxiedPrototype allows the provided prototype to remain unmodified
+	// so that it can be used as a mixin for multiple widgets (#8876)
+	var proxiedPrototype = {};
+
+	var namespace = name.split( "." )[ 0 ];
+	name = name.split( "." )[ 1 ];
+	var fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	if ( $.isArray( prototype ) ) {
+		prototype = $.extend.apply( null, [ {} ].concat( prototype ) );
+	}
+
+	// Create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+
+		// Allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// Allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+
+	// Extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+
+		// Copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+
+		// Track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	} );
+
+	basePrototype = new base();
+
+	// We need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( !$.isFunction( value ) ) {
+			proxiedPrototype[ prop ] = value;
+			return;
+		}
+		proxiedPrototype[ prop ] = ( function() {
+			function _super() {
+				return base.prototype[ prop ].apply( this, arguments );
+			}
+
+			function _superApply( args ) {
+				return base.prototype[ prop ].apply( this, args );
+			}
+
+			return function() {
+				var __super = this._super;
+				var __superApply = this._superApply;
+				var returnValue;
+
+				this._super = _super;
+				this._superApply = _superApply;
+
+				returnValue = value.apply( this, arguments );
+
+				this._super = __super;
+				this._superApply = __superApply;
+
+				return returnValue;
+			};
+		} )();
+	} );
+	constructor.prototype = $.widget.extend( basePrototype, {
+
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name
+	}, proxiedPrototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		widgetFullName: fullName
+	} );
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// Redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor,
+				child._proto );
+		} );
+
+		// Remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+
+	return constructor;
+};
+
+$.widget.extend = function( target ) {
+	var input = widgetSlice.call( arguments, 1 );
+	var inputIndex = 0;
+	var inputLength = input.length;
+	var key;
+	var value;
+
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+
+				// Clone objects
+				if ( $.isPlainObject( value ) ) {
+					target[ key ] = $.isPlainObject( target[ key ] ) ?
+						$.widget.extend( {}, target[ key ], value ) :
+
+						// Don't extend strings, arrays, etc. with objects
+						$.widget.extend( {}, value );
+
+				// Copy everything else by reference
+				} else {
+					target[ key ] = value;
+				}
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName || name;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string";
+		var args = widgetSlice.call( arguments, 1 );
+		var returnValue = this;
+
+		if ( isMethodCall ) {
+
+			// If this is an empty collection, we need to have the instance method
+			// return undefined instead of the jQuery instance
+			if ( !this.length && options === "instance" ) {
+				returnValue = undefined;
+			} else {
+				this.each( function() {
+					var methodValue;
+					var instance = $.data( this, fullName );
+
+					if ( options === "instance" ) {
+						returnValue = instance;
+						return false;
+					}
+
+					if ( !instance ) {
+						return $.error( "cannot call methods on " + name +
+							" prior to initialization; " +
+							"attempted to call method '" + options + "'" );
+					}
+
+					if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) {
+						return $.error( "no such method '" + options + "' for " + name +
+							" widget instance" );
+					}
+
+					methodValue = instance[ options ].apply( instance, args );
+
+					if ( methodValue !== instance && methodValue !== undefined ) {
+						returnValue = methodValue && methodValue.jquery ?
+							returnValue.pushStack( methodValue.get() ) :
+							methodValue;
+						return false;
+					}
+				} );
+			}
+		} else {
+
+			// Allow multiple hashes to be passed on init
+			if ( args.length ) {
+				options = $.widget.extend.apply( null, [ options ].concat( args ) );
+			}
+
+			this.each( function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} );
+					if ( instance._init ) {
+						instance._init();
+					}
+				} else {
+					$.data( this, fullName, new object( options, this ) );
+				}
+			} );
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( /* options, element */ ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+
+	options: {
+		classes: {},
+		disabled: false,
+
+		// Callbacks
+		create: null
+	},
+
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = widgetUuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+		this.classesElementLookup = {};
+
+		if ( element !== this ) {
+			$.data( element, this.widgetFullName, this );
+			this._on( true, this.element, {
+				remove: function( event ) {
+					if ( event.target === element ) {
+						this.destroy();
+					}
+				}
+			} );
+			this.document = $( element.style ?
+
+				// Element within the document
+				element.ownerDocument :
+
+				// Element is window or document
+				element.document || element );
+			this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow );
+		}
+
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this._create();
+
+		if ( this.options.disabled ) {
+			this._setOptionDisabled( this.options.disabled );
+		}
+
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+
+	_getCreateOptions: function() {
+		return {};
+	},
+
+	_getCreateEventData: $.noop,
+
+	_create: $.noop,
+
+	_init: $.noop,
+
+	destroy: function() {
+		var that = this;
+
+		this._destroy();
+		$.each( this.classesElementLookup, function( key, value ) {
+			that._removeClass( value, key );
+		} );
+
+		// We can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.off( this.eventNamespace )
+			.removeData( this.widgetFullName );
+		this.widget()
+			.off( this.eventNamespace )
+			.removeAttr( "aria-disabled" );
+
+		// Clean up events and states
+		this.bindings.off( this.eventNamespace );
+	},
+
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key;
+		var parts;
+		var curOption;
+		var i;
+
+		if ( arguments.length === 0 ) {
+
+			// Don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+
+			// Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( arguments.length === 1 ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( arguments.length === 1 ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "classes" ) {
+			this._setOptionClasses( value );
+		}
+
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this._setOptionDisabled( value );
+		}
+
+		return this;
+	},
+
+	_setOptionClasses: function( value ) {
+		var classKey, elements, currentElements;
+
+		for ( classKey in value ) {
+			currentElements = this.classesElementLookup[ classKey ];
+			if ( value[ classKey ] === this.options.classes[ classKey ] ||
+					!currentElements ||
+					!currentElements.length ) {
+				continue;
+			}
+
+			// We are doing this to create a new jQuery object because the _removeClass() call
+			// on the next line is going to destroy the reference to the current elements being
+			// tracked. We need to save a copy of this collection so that we can add the new classes
+			// below.
+			elements = $( currentElements.get() );
+			this._removeClass( currentElements, classKey );
+
+			// We don't use _addClass() here, because that uses this.options.classes
+			// for generating the string of classes. We want to use the value passed in from
+			// _setOption(), this is the new value of the classes option which was passed to
+			// _setOption(). We pass this value directly to _classes().
+			elements.addClass( this._classes( {
+				element: elements,
+				keys: classKey,
+				classes: value,
+				add: true
+			} ) );
+		}
+	},
+
+	_setOptionDisabled: function( value ) {
+		this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value );
+
+		// If the widget is becoming disabled, then nothing is interactive
+		if ( value ) {
+			this._removeClass( this.hoverable, null, "ui-state-hover" );
+			this._removeClass( this.focusable, null, "ui-state-focus" );
+		}
+	},
+
+	enable: function() {
+		return this._setOptions( { disabled: false } );
+	},
+
+	disable: function() {
+		return this._setOptions( { disabled: true } );
+	},
+
+	_classes: function( options ) {
+		var full = [];
+		var that = this;
+
+		options = $.extend( {
+			element: this.element,
+			classes: this.options.classes || {}
+		}, options );
+
+		function processClassString( classes, checkOption ) {
+			var current, i;
+			for ( i = 0; i < classes.length; i++ ) {
+				current = that.classesElementLookup[ classes[ i ] ] || $();
+				if ( options.add ) {
+					current = $( $.unique( current.get().concat( options.element.get() ) ) );
+				} else {
+					current = $( current.not( options.element ).get() );
+				}
+				that.classesElementLookup[ classes[ i ] ] = current;
+				full.push( classes[ i ] );
+				if ( checkOption && options.classes[ classes[ i ] ] ) {
+					full.push( options.classes[ classes[ i ] ] );
+				}
+			}
+		}
+
+		this._on( options.element, {
+			"remove": "_untrackClassesElement"
+		} );
+
+		if ( options.keys ) {
+			processClassString( options.keys.match( /\S+/g ) || [], true );
+		}
+		if ( options.extra ) {
+			processClassString( options.extra.match( /\S+/g ) || [] );
+		}
+
+		return full.join( " " );
+	},
+
+	_untrackClassesElement: function( event ) {
+		var that = this;
+		$.each( that.classesElementLookup, function( key, value ) {
+			if ( $.inArray( event.target, value ) !== -1 ) {
+				that.classesElementLookup[ key ] = $( value.not( event.target ).get() );
+			}
+		} );
+	},
+
+	_removeClass: function( element, keys, extra ) {
+		return this._toggleClass( element, keys, extra, false );
+	},
+
+	_addClass: function( element, keys, extra ) {
+		return this._toggleClass( element, keys, extra, true );
+	},
+
+	_toggleClass: function( element, keys, extra, add ) {
+		add = ( typeof add === "boolean" ) ? add : extra;
+		var shift = ( typeof element === "string" || element === null ),
+			options = {
+				extra: shift ? keys : extra,
+				keys: shift ? element : keys,
+				element: shift ? this.element : element,
+				add: add
+			};
+		options.element.toggleClass( this._classes( options ), add );
+		return this;
+	},
+
+	_on: function( suppressDisabledCheck, element, handlers ) {
+		var delegateElement;
+		var instance = this;
+
+		// No suppressDisabledCheck flag, shuffle arguments
+		if ( typeof suppressDisabledCheck !== "boolean" ) {
+			handlers = element;
+			element = suppressDisabledCheck;
+			suppressDisabledCheck = false;
+		}
+
+		// No element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+			delegateElement = this.widget();
+		} else {
+			element = delegateElement = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+
+				// Allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( !suppressDisabledCheck &&
+						( instance.options.disabled === true ||
+						$( this ).hasClass( "ui-state-disabled" ) ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// Copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^([\w:-]*)\s*(.*)$/ );
+			var eventName = match[ 1 ] + instance.eventNamespace;
+			var selector = match[ 2 ];
+
+			if ( selector ) {
+				delegateElement.on( eventName, selector, handlerProxy );
+			} else {
+				element.on( eventName, handlerProxy );
+			}
+		} );
+	},
+
+	_off: function( element, eventName ) {
+		eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) +
+			this.eventNamespace;
+		element.off( eventName ).off( eventName );
+
+		// Clear the stack to avoid memory leaks (#10056)
+		this.bindings = $( this.bindings.not( element ).get() );
+		this.focusable = $( this.focusable.not( element ).get() );
+		this.hoverable = $( this.hoverable.not( element ).get() );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				this._addClass( $( event.currentTarget ), null, "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				this._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
+			}
+		} );
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				this._addClass( $( event.currentTarget ), null, "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				this._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
+			}
+		} );
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig;
+		var callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+
+		// The original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// Copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+
+		var hasOptions;
+		var effectName = !options ?
+			method :
+			options === true || typeof options === "number" ?
+				defaultEffect :
+				options.effect || defaultEffect;
+
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+
+		if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue( function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			} );
+		}
+	};
+} );
+
+var widget = $.widget;
+
+
+/*!
+ * jQuery UI Position 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/position/
+ */
+
+//>>label: Position
+//>>group: Core
+//>>description: Positions elements relative to other elements.
+//>>docs: http://api.jqueryui.com/position/
+//>>demos: http://jqueryui.com/position/
+
+
+( function() {
+var cachedScrollbarWidth,
+	max = Math.max,
+	abs = Math.abs,
+	rhorizontal = /left|center|right/,
+	rvertical = /top|center|bottom/,
+	roffset = /[\+\-]\d+(\.[\d]+)?%?/,
+	rposition = /^\w+/,
+	rpercent = /%$/,
+	_position = $.fn.position;
+
+function getOffsets( offsets, width, height ) {
+	return [
+		parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
+		parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
+	];
+}
+
+function parseCss( element, property ) {
+	return parseInt( $.css( element, property ), 10 ) || 0;
+}
+
+function getDimensions( elem ) {
+	var raw = elem[ 0 ];
+	if ( raw.nodeType === 9 ) {
+		return {
+			width: elem.width(),
+			height: elem.height(),
+			offset: { top: 0, left: 0 }
+		};
+	}
+	if ( $.isWindow( raw ) ) {
+		return {
+			width: elem.width(),
+			height: elem.height(),
+			offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
+		};
+	}
+	if ( raw.preventDefault ) {
+		return {
+			width: 0,
+			height: 0,
+			offset: { top: raw.pageY, left: raw.pageX }
+		};
+	}
+	return {
+		width: elem.outerWidth(),
+		height: elem.outerHeight(),
+		offset: elem.offset()
+	};
+}
+
+$.position = {
+	scrollbarWidth: function() {
+		if ( cachedScrollbarWidth !== undefined ) {
+			return cachedScrollbarWidth;
+		}
+		var w1, w2,
+			div = $( "<div " +
+				"style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'>" +
+				"<div style='height:100px;width:auto;'></div></div>" ),
+			innerDiv = div.children()[ 0 ];
+
+		$( "body" ).append( div );
+		w1 = innerDiv.offsetWidth;
+		div.css( "overflow", "scroll" );
+
+		w2 = innerDiv.offsetWidth;
+
+		if ( w1 === w2 ) {
+			w2 = div[ 0 ].clientWidth;
+		}
+
+		div.remove();
+
+		return ( cachedScrollbarWidth = w1 - w2 );
+	},
+	getScrollInfo: function( within ) {
+		var overflowX = within.isWindow || within.isDocument ? "" :
+				within.element.css( "overflow-x" ),
+			overflowY = within.isWindow || within.isDocument ? "" :
+				within.element.css( "overflow-y" ),
+			hasOverflowX = overflowX === "scroll" ||
+				( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ),
+			hasOverflowY = overflowY === "scroll" ||
+				( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight );
+		return {
+			width: hasOverflowY ? $.position.scrollbarWidth() : 0,
+			height: hasOverflowX ? $.position.scrollbarWidth() : 0
+		};
+	},
+	getWithinInfo: function( element ) {
+		var withinElement = $( element || window ),
+			isWindow = $.isWindow( withinElement[ 0 ] ),
+			isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9,
+			hasOffset = !isWindow && !isDocument;
+		return {
+			element: withinElement,
+			isWindow: isWindow,
+			isDocument: isDocument,
+			offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 },
+			scrollLeft: withinElement.scrollLeft(),
+			scrollTop: withinElement.scrollTop(),
+			width: withinElement.outerWidth(),
+			height: withinElement.outerHeight()
+		};
+	}
+};
+
+$.fn.position = function( options ) {
+	if ( !options || !options.of ) {
+		return _position.apply( this, arguments );
+	}
+
+	// Make a copy, we don't want to modify arguments
+	options = $.extend( {}, options );
+
+	var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
+		target = $( options.of ),
+		within = $.position.getWithinInfo( options.within ),
+		scrollInfo = $.position.getScrollInfo( within ),
+		collision = ( options.collision || "flip" ).split( " " ),
+		offsets = {};
+
+	dimensions = getDimensions( target );
+	if ( target[ 0 ].preventDefault ) {
+
+		// Force left top to allow flipping
+		options.at = "left top";
+	}
+	targetWidth = dimensions.width;
+	targetHeight = dimensions.height;
+	targetOffset = dimensions.offset;
+
+	// Clone to reuse original targetOffset later
+	basePosition = $.extend( {}, targetOffset );
+
+	// Force my and at to have valid horizontal and vertical positions
+	// if a value is missing or invalid, it will be converted to center
+	$.each( [ "my", "at" ], function() {
+		var pos = ( options[ this ] || "" ).split( " " ),
+			horizontalOffset,
+			verticalOffset;
+
+		if ( pos.length === 1 ) {
+			pos = rhorizontal.test( pos[ 0 ] ) ?
+				pos.concat( [ "center" ] ) :
+				rvertical.test( pos[ 0 ] ) ?
+					[ "center" ].concat( pos ) :
+					[ "center", "center" ];
+		}
+		pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
+		pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
+
+		// Calculate offsets
+		horizontalOffset = roffset.exec( pos[ 0 ] );
+		verticalOffset = roffset.exec( pos[ 1 ] );
+		offsets[ this ] = [
+			horizontalOffset ? horizontalOffset[ 0 ] : 0,
+			verticalOffset ? verticalOffset[ 0 ] : 0
+		];
+
+		// Reduce to just the positions without the offsets
+		options[ this ] = [
+			rposition.exec( pos[ 0 ] )[ 0 ],
+			rposition.exec( pos[ 1 ] )[ 0 ]
+		];
+	} );
+
+	// Normalize collision option
+	if ( collision.length === 1 ) {
+		collision[ 1 ] = collision[ 0 ];
+	}
+
+	if ( options.at[ 0 ] === "right" ) {
+		basePosition.left += targetWidth;
+	} else if ( options.at[ 0 ] === "center" ) {
+		basePosition.left += targetWidth / 2;
+	}
+
+	if ( options.at[ 1 ] === "bottom" ) {
+		basePosition.top += targetHeight;
+	} else if ( options.at[ 1 ] === "center" ) {
+		basePosition.top += targetHeight / 2;
+	}
+
+	atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
+	basePosition.left += atOffset[ 0 ];
+	basePosition.top += atOffset[ 1 ];
+
+	return this.each( function() {
+		var collisionPosition, using,
+			elem = $( this ),
+			elemWidth = elem.outerWidth(),
+			elemHeight = elem.outerHeight(),
+			marginLeft = parseCss( this, "marginLeft" ),
+			marginTop = parseCss( this, "marginTop" ),
+			collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) +
+				scrollInfo.width,
+			collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) +
+				scrollInfo.height,
+			position = $.extend( {}, basePosition ),
+			myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
+
+		if ( options.my[ 0 ] === "right" ) {
+			position.left -= elemWidth;
+		} else if ( options.my[ 0 ] === "center" ) {
+			position.left -= elemWidth / 2;
+		}
+
+		if ( options.my[ 1 ] === "bottom" ) {
+			position.top -= elemHeight;
+		} else if ( options.my[ 1 ] === "center" ) {
+			position.top -= elemHeight / 2;
+		}
+
+		position.left += myOffset[ 0 ];
+		position.top += myOffset[ 1 ];
+
+		collisionPosition = {
+			marginLeft: marginLeft,
+			marginTop: marginTop
+		};
+
+		$.each( [ "left", "top" ], function( i, dir ) {
+			if ( $.ui.position[ collision[ i ] ] ) {
+				$.ui.position[ collision[ i ] ][ dir ]( position, {
+					targetWidth: targetWidth,
+					targetHeight: targetHeight,
+					elemWidth: elemWidth,
+					elemHeight: elemHeight,
+					collisionPosition: collisionPosition,
+					collisionWidth: collisionWidth,
+					collisionHeight: collisionHeight,
+					offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
+					my: options.my,
+					at: options.at,
+					within: within,
+					elem: elem
+				} );
+			}
+		} );
+
+		if ( options.using ) {
+
+			// Adds feedback as second argument to using callback, if present
+			using = function( props ) {
+				var left = targetOffset.left - position.left,
+					right = left + targetWidth - elemWidth,
+					top = targetOffset.top - position.top,
+					bottom = top + targetHeight - elemHeight,
+					feedback = {
+						target: {
+							element: target,
+							left: targetOffset.left,
+							top: targetOffset.top,
+							width: targetWidth,
+							height: targetHeight
+						},
+						element: {
+							element: elem,
+							left: position.left,
+							top: position.top,
+							width: elemWidth,
+							height: elemHeight
+						},
+						horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
+						vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
+					};
+				if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
+					feedback.horizontal = "center";
+				}
+				if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
+					feedback.vertical = "middle";
+				}
+				if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
+					feedback.important = "horizontal";
+				} else {
+					feedback.important = "vertical";
+				}
+				options.using.call( this, props, feedback );
+			};
+		}
+
+		elem.offset( $.extend( position, { using: using } ) );
+	} );
+};
+
+$.ui.position = {
+	fit: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
+				outerWidth = within.width,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = withinOffset - collisionPosLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
+				newOverRight;
+
+			// Element is wider than within
+			if ( data.collisionWidth > outerWidth ) {
+
+				// Element is initially over the left side of within
+				if ( overLeft > 0 && overRight <= 0 ) {
+					newOverRight = position.left + overLeft + data.collisionWidth - outerWidth -
+						withinOffset;
+					position.left += overLeft - newOverRight;
+
+				// Element is initially over right side of within
+				} else if ( overRight > 0 && overLeft <= 0 ) {
+					position.left = withinOffset;
+
+				// Element is initially over both left and right sides of within
+				} else {
+					if ( overLeft > overRight ) {
+						position.left = withinOffset + outerWidth - data.collisionWidth;
+					} else {
+						position.left = withinOffset;
+					}
+				}
+
+			// Too far left -> align with left edge
+			} else if ( overLeft > 0 ) {
+				position.left += overLeft;
+
+			// Too far right -> align with right edge
+			} else if ( overRight > 0 ) {
+				position.left -= overRight;
+
+			// Adjust based on position and margin
+			} else {
+				position.left = max( position.left - collisionPosLeft, position.left );
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
+				outerHeight = data.within.height,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = withinOffset - collisionPosTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
+				newOverBottom;
+
+			// Element is taller than within
+			if ( data.collisionHeight > outerHeight ) {
+
+				// Element is initially over the top of within
+				if ( overTop > 0 && overBottom <= 0 ) {
+					newOverBottom = position.top + overTop + data.collisionHeight - outerHeight -
+						withinOffset;
+					position.top += overTop - newOverBottom;
+
+				// Element is initially over bottom of within
+				} else if ( overBottom > 0 && overTop <= 0 ) {
+					position.top = withinOffset;
+
+				// Element is initially over both top and bottom of within
+				} else {
+					if ( overTop > overBottom ) {
+						position.top = withinOffset + outerHeight - data.collisionHeight;
+					} else {
+						position.top = withinOffset;
+					}
+				}
+
+			// Too far up -> align with top
+			} else if ( overTop > 0 ) {
+				position.top += overTop;
+
+			// Too far down -> align with bottom edge
+			} else if ( overBottom > 0 ) {
+				position.top -= overBottom;
+
+			// Adjust based on position and margin
+			} else {
+				position.top = max( position.top - collisionPosTop, position.top );
+			}
+		}
+	},
+	flip: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.left + within.scrollLeft,
+				outerWidth = within.width,
+				offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = collisionPosLeft - offsetLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
+				myOffset = data.my[ 0 ] === "left" ?
+					-data.elemWidth :
+					data.my[ 0 ] === "right" ?
+						data.elemWidth :
+						0,
+				atOffset = data.at[ 0 ] === "left" ?
+					data.targetWidth :
+					data.at[ 0 ] === "right" ?
+						-data.targetWidth :
+						0,
+				offset = -2 * data.offset[ 0 ],
+				newOverRight,
+				newOverLeft;
+
+			if ( overLeft < 0 ) {
+				newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth -
+					outerWidth - withinOffset;
+				if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			} else if ( overRight > 0 ) {
+				newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset +
+					atOffset + offset - offsetLeft;
+				if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.top + within.scrollTop,
+				outerHeight = within.height,
+				offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = collisionPosTop - offsetTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
+				top = data.my[ 1 ] === "top",
+				myOffset = top ?
+					-data.elemHeight :
+					data.my[ 1 ] === "bottom" ?
+						data.elemHeight :
+						0,
+				atOffset = data.at[ 1 ] === "top" ?
+					data.targetHeight :
+					data.at[ 1 ] === "bottom" ?
+						-data.targetHeight :
+						0,
+				offset = -2 * data.offset[ 1 ],
+				newOverTop,
+				newOverBottom;
+			if ( overTop < 0 ) {
+				newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight -
+					outerHeight - withinOffset;
+				if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			} else if ( overBottom > 0 ) {
+				newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset +
+					offset - offsetTop;
+				if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			}
+		}
+	},
+	flipfit: {
+		left: function() {
+			$.ui.position.flip.left.apply( this, arguments );
+			$.ui.position.fit.left.apply( this, arguments );
+		},
+		top: function() {
+			$.ui.position.flip.top.apply( this, arguments );
+			$.ui.position.fit.top.apply( this, arguments );
+		}
+	}
+};
+
+} )();
+
+var position = $.ui.position;
+
+
+/*!
+ * jQuery UI Keycode 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Keycode
+//>>group: Core
+//>>description: Provide keycodes as keynames
+//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/
+
+
+var keycode = $.ui.keyCode = {
+	BACKSPACE: 8,
+	COMMA: 188,
+	DELETE: 46,
+	DOWN: 40,
+	END: 35,
+	ENTER: 13,
+	ESCAPE: 27,
+	HOME: 36,
+	LEFT: 37,
+	PAGE_DOWN: 34,
+	PAGE_UP: 33,
+	PERIOD: 190,
+	RIGHT: 39,
+	SPACE: 32,
+	TAB: 9,
+	UP: 38
+};
+
+
+/*!
+ * jQuery UI Scroll Parent 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: scrollParent
+//>>group: Core
+//>>description: Get the closest ancestor element that is scrollable.
+//>>docs: http://api.jqueryui.com/scrollParent/
+
+
+
+var scrollParent = $.fn.scrollParent = function( includeHidden ) {
+	var position = this.css( "position" ),
+		excludeStaticParent = position === "absolute",
+		overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/,
+		scrollParent = this.parents().filter( function() {
+			var parent = $( this );
+			if ( excludeStaticParent && parent.css( "position" ) === "static" ) {
+				return false;
+			}
+			return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) +
+				parent.css( "overflow-x" ) );
+		} ).eq( 0 );
+
+	return position === "fixed" || !scrollParent.length ?
+		$( this[ 0 ].ownerDocument || document ) :
+		scrollParent;
+};
+
+
+/*!
+ * jQuery UI Unique ID 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: uniqueId
+//>>group: Core
+//>>description: Functions to generate and remove uniqueId's
+//>>docs: http://api.jqueryui.com/uniqueId/
+
+
+
+var uniqueId = $.fn.extend( {
+	uniqueId: ( function() {
+		var uuid = 0;
+
+		return function() {
+			return this.each( function() {
+				if ( !this.id ) {
+					this.id = "ui-id-" + ( ++uuid );
+				}
+			} );
+		};
+	} )(),
+
+	removeUniqueId: function() {
+		return this.each( function() {
+			if ( /^ui-id-\d+$/.test( this.id ) ) {
+				$( this ).removeAttr( "id" );
+			}
+		} );
+	}
+} );
+
+
+/*!
+ * jQuery UI Effects 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Effects Core
+//>>group: Effects
+// jscs:disable maximumLineLength
+//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects.
+// jscs:enable maximumLineLength
+//>>docs: http://api.jqueryui.com/category/effects-core/
+//>>demos: http://jqueryui.com/effect/
+
+
+
+var dataSpace = "ui-effects-",
+	dataSpaceStyle = "ui-effects-style",
+	dataSpaceAnimated = "ui-effects-animated",
+
+	// Create a local jQuery because jQuery Color relies on it and the
+	// global may not exist with AMD and a custom build (#10199)
+	jQuery = $;
+
+$.effects = {
+	effect: {}
+};
+
+/*!
+ * jQuery Color Animations v2.1.2
+ * https://github.com/jquery/jquery-color
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * Date: Wed Jan 16 08:47:09 2013 -0600
+ */
+( function( jQuery, undefined ) {
+
+	var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " +
+		"borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",
+
+	// Plusequals test for += 100 -= 100
+	rplusequals = /^([\-+])=\s*(\d+\.?\d*)/,
+
+	// A set of RE's that can match strings and generate color tuples.
+	stringParsers = [ {
+			re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ],
+					execResult[ 2 ],
+					execResult[ 3 ],
+					execResult[ 4 ]
+				];
+			}
+		}, {
+			re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ] * 2.55,
+					execResult[ 2 ] * 2.55,
+					execResult[ 3 ] * 2.55,
+					execResult[ 4 ]
+				];
+			}
+		}, {
+
+			// This regex ignores A-F because it's compared against an already lowercased string
+			re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,
+			parse: function( execResult ) {
+				return [
+					parseInt( execResult[ 1 ], 16 ),
+					parseInt( execResult[ 2 ], 16 ),
+					parseInt( execResult[ 3 ], 16 )
+				];
+			}
+		}, {
+
+			// This regex ignores A-F because it's compared against an already lowercased string
+			re: /#([a-f0-9])([a-f0-9])([a-f0-9])/,
+			parse: function( execResult ) {
+				return [
+					parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ),
+					parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ),
+					parseInt( execResult[ 3 ] + execResult[ 3 ], 16 )
+				];
+			}
+		}, {
+			re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			space: "hsla",
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ],
+					execResult[ 2 ] / 100,
+					execResult[ 3 ] / 100,
+					execResult[ 4 ]
+				];
+			}
+		} ],
+
+	// JQuery.Color( )
+	color = jQuery.Color = function( color, green, blue, alpha ) {
+		return new jQuery.Color.fn.parse( color, green, blue, alpha );
+	},
+	spaces = {
+		rgba: {
+			props: {
+				red: {
+					idx: 0,
+					type: "byte"
+				},
+				green: {
+					idx: 1,
+					type: "byte"
+				},
+				blue: {
+					idx: 2,
+					type: "byte"
+				}
+			}
+		},
+
+		hsla: {
+			props: {
+				hue: {
+					idx: 0,
+					type: "degrees"
+				},
+				saturation: {
+					idx: 1,
+					type: "percent"
+				},
+				lightness: {
+					idx: 2,
+					type: "percent"
+				}
+			}
+		}
+	},
+	propTypes = {
+		"byte": {
+			floor: true,
+			max: 255
+		},
+		"percent": {
+			max: 1
+		},
+		"degrees": {
+			mod: 360,
+			floor: true
+		}
+	},
+	support = color.support = {},
+
+	// Element for support tests
+	supportElem = jQuery( "<p>" )[ 0 ],
+
+	// Colors = jQuery.Color.names
+	colors,
+
+	// Local aliases of functions called often
+	each = jQuery.each;
+
+// Determine rgba support immediately
+supportElem.style.cssText = "background-color:rgba(1,1,1,.5)";
+support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1;
+
+// Define cache name and alpha properties
+// for rgba and hsla spaces
+each( spaces, function( spaceName, space ) {
+	space.cache = "_" + spaceName;
+	space.props.alpha = {
+		idx: 3,
+		type: "percent",
+		def: 1
+	};
+} );
+
+function clamp( value, prop, allowEmpty ) {
+	var type = propTypes[ prop.type ] || {};
+
+	if ( value == null ) {
+		return ( allowEmpty || !prop.def ) ? null : prop.def;
+	}
+
+	// ~~ is an short way of doing floor for positive numbers
+	value = type.floor ? ~~value : parseFloat( value );
+
+	// IE will pass in empty strings as value for alpha,
+	// which will hit this case
+	if ( isNaN( value ) ) {
+		return prop.def;
+	}
+
+	if ( type.mod ) {
+
+		// We add mod before modding to make sure that negatives values
+		// get converted properly: -10 -> 350
+		return ( value + type.mod ) % type.mod;
+	}
+
+	// For now all property types without mod have min and max
+	return 0 > value ? 0 : type.max < value ? type.max : value;
+}
+
+function stringParse( string ) {
+	var inst = color(),
+		rgba = inst._rgba = [];
+
+	string = string.toLowerCase();
+
+	each( stringParsers, function( i, parser ) {
+		var parsed,
+			match = parser.re.exec( string ),
+			values = match && parser.parse( match ),
+			spaceName = parser.space || "rgba";
+
+		if ( values ) {
+			parsed = inst[ spaceName ]( values );
+
+			// If this was an rgba parse the assignment might happen twice
+			// oh well....
+			inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ];
+			rgba = inst._rgba = parsed._rgba;
+
+			// Exit each( stringParsers ) here because we matched
+			return false;
+		}
+	} );
+
+	// Found a stringParser that handled it
+	if ( rgba.length ) {
+
+		// If this came from a parsed string, force "transparent" when alpha is 0
+		// chrome, (and maybe others) return "transparent" as rgba(0,0,0,0)
+		if ( rgba.join() === "0,0,0,0" ) {
+			jQuery.extend( rgba, colors.transparent );
+		}
+		return inst;
+	}
+
+	// Named colors
+	return colors[ string ];
+}
+
+color.fn = jQuery.extend( color.prototype, {
+	parse: function( red, green, blue, alpha ) {
+		if ( red === undefined ) {
+			this._rgba = [ null, null, null, null ];
+			return this;
+		}
+		if ( red.jquery || red.nodeType ) {
+			red = jQuery( red ).css( green );
+			green = undefined;
+		}
+
+		var inst = this,
+			type = jQuery.type( red ),
+			rgba = this._rgba = [];
+
+		// More than 1 argument specified - assume ( red, green, blue, alpha )
+		if ( green !== undefined ) {
+			red = [ red, green, blue, alpha ];
+			type = "array";
+		}
+
+		if ( type === "string" ) {
+			return this.parse( stringParse( red ) || colors._default );
+		}
+
+		if ( type === "array" ) {
+			each( spaces.rgba.props, function( key, prop ) {
+				rgba[ prop.idx ] = clamp( red[ prop.idx ], prop );
+			} );
+			return this;
+		}
+
+		if ( type === "object" ) {
+			if ( red instanceof color ) {
+				each( spaces, function( spaceName, space ) {
+					if ( red[ space.cache ] ) {
+						inst[ space.cache ] = red[ space.cache ].slice();
+					}
+				} );
+			} else {
+				each( spaces, function( spaceName, space ) {
+					var cache = space.cache;
+					each( space.props, function( key, prop ) {
+
+						// If the cache doesn't exist, and we know how to convert
+						if ( !inst[ cache ] && space.to ) {
+
+							// If the value was null, we don't need to copy it
+							// if the key was alpha, we don't need to copy it either
+							if ( key === "alpha" || red[ key ] == null ) {
+								return;
+							}
+							inst[ cache ] = space.to( inst._rgba );
+						}
+
+						// This is the only case where we allow nulls for ALL properties.
+						// call clamp with alwaysAllowEmpty
+						inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true );
+					} );
+
+					// Everything defined but alpha?
+					if ( inst[ cache ] &&
+							jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) {
+
+						// Use the default of 1
+						inst[ cache ][ 3 ] = 1;
+						if ( space.from ) {
+							inst._rgba = space.from( inst[ cache ] );
+						}
+					}
+				} );
+			}
+			return this;
+		}
+	},
+	is: function( compare ) {
+		var is = color( compare ),
+			same = true,
+			inst = this;
+
+		each( spaces, function( _, space ) {
+			var localCache,
+				isCache = is[ space.cache ];
+			if ( isCache ) {
+				localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || [];
+				each( space.props, function( _, prop ) {
+					if ( isCache[ prop.idx ] != null ) {
+						same = ( isCache[ prop.idx ] === localCache[ prop.idx ] );
+						return same;
+					}
+				} );
+			}
+			return same;
+		} );
+		return same;
+	},
+	_space: function() {
+		var used = [],
+			inst = this;
+		each( spaces, function( spaceName, space ) {
+			if ( inst[ space.cache ] ) {
+				used.push( spaceName );
+			}
+		} );
+		return used.pop();
+	},
+	transition: function( other, distance ) {
+		var end = color( other ),
+			spaceName = end._space(),
+			space = spaces[ spaceName ],
+			startColor = this.alpha() === 0 ? color( "transparent" ) : this,
+			start = startColor[ space.cache ] || space.to( startColor._rgba ),
+			result = start.slice();
+
+		end = end[ space.cache ];
+		each( space.props, function( key, prop ) {
+			var index = prop.idx,
+				startValue = start[ index ],
+				endValue = end[ index ],
+				type = propTypes[ prop.type ] || {};
+
+			// If null, don't override start value
+			if ( endValue === null ) {
+				return;
+			}
+
+			// If null - use end
+			if ( startValue === null ) {
+				result[ index ] = endValue;
+			} else {
+				if ( type.mod ) {
+					if ( endValue - startValue > type.mod / 2 ) {
+						startValue += type.mod;
+					} else if ( startValue - endValue > type.mod / 2 ) {
+						startValue -= type.mod;
+					}
+				}
+				result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop );
+			}
+		} );
+		return this[ spaceName ]( result );
+	},
+	blend: function( opaque ) {
+
+		// If we are already opaque - return ourself
+		if ( this._rgba[ 3 ] === 1 ) {
+			return this;
+		}
+
+		var rgb = this._rgba.slice(),
+			a = rgb.pop(),
+			blend = color( opaque )._rgba;
+
+		return color( jQuery.map( rgb, function( v, i ) {
+			return ( 1 - a ) * blend[ i ] + a * v;
+		} ) );
+	},
+	toRgbaString: function() {
+		var prefix = "rgba(",
+			rgba = jQuery.map( this._rgba, function( v, i ) {
+				return v == null ? ( i > 2 ? 1 : 0 ) : v;
+			} );
+
+		if ( rgba[ 3 ] === 1 ) {
+			rgba.pop();
+			prefix = "rgb(";
+		}
+
+		return prefix + rgba.join() + ")";
+	},
+	toHslaString: function() {
+		var prefix = "hsla(",
+			hsla = jQuery.map( this.hsla(), function( v, i ) {
+				if ( v == null ) {
+					v = i > 2 ? 1 : 0;
+				}
+
+				// Catch 1 and 2
+				if ( i && i < 3 ) {
+					v = Math.round( v * 100 ) + "%";
+				}
+				return v;
+			} );
+
+		if ( hsla[ 3 ] === 1 ) {
+			hsla.pop();
+			prefix = "hsl(";
+		}
+		return prefix + hsla.join() + ")";
+	},
+	toHexString: function( includeAlpha ) {
+		var rgba = this._rgba.slice(),
+			alpha = rgba.pop();
+
+		if ( includeAlpha ) {
+			rgba.push( ~~( alpha * 255 ) );
+		}
+
+		return "#" + jQuery.map( rgba, function( v ) {
+
+			// Default to 0 when nulls exist
+			v = ( v || 0 ).toString( 16 );
+			return v.length === 1 ? "0" + v : v;
+		} ).join( "" );
+	},
+	toString: function() {
+		return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString();
+	}
+} );
+color.fn.parse.prototype = color.fn;
+
+// Hsla conversions adapted from:
+// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021
+
+function hue2rgb( p, q, h ) {
+	h = ( h + 1 ) % 1;
+	if ( h * 6 < 1 ) {
+		return p + ( q - p ) * h * 6;
+	}
+	if ( h * 2 < 1 ) {
+		return q;
+	}
+	if ( h * 3 < 2 ) {
+		return p + ( q - p ) * ( ( 2 / 3 ) - h ) * 6;
+	}
+	return p;
+}
+
+spaces.hsla.to = function( rgba ) {
+	if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) {
+		return [ null, null, null, rgba[ 3 ] ];
+	}
+	var r = rgba[ 0 ] / 255,
+		g = rgba[ 1 ] / 255,
+		b = rgba[ 2 ] / 255,
+		a = rgba[ 3 ],
+		max = Math.max( r, g, b ),
+		min = Math.min( r, g, b ),
+		diff = max - min,
+		add = max + min,
+		l = add * 0.5,
+		h, s;
+
+	if ( min === max ) {
+		h = 0;
+	} else if ( r === max ) {
+		h = ( 60 * ( g - b ) / diff ) + 360;
+	} else if ( g === max ) {
+		h = ( 60 * ( b - r ) / diff ) + 120;
+	} else {
+		h = ( 60 * ( r - g ) / diff ) + 240;
+	}
+
+	// Chroma (diff) == 0 means greyscale which, by definition, saturation = 0%
+	// otherwise, saturation is based on the ratio of chroma (diff) to lightness (add)
+	if ( diff === 0 ) {
+		s = 0;
+	} else if ( l <= 0.5 ) {
+		s = diff / add;
+	} else {
+		s = diff / ( 2 - add );
+	}
+	return [ Math.round( h ) % 360, s, l, a == null ? 1 : a ];
+};
+
+spaces.hsla.from = function( hsla ) {
+	if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) {
+		return [ null, null, null, hsla[ 3 ] ];
+	}
+	var h = hsla[ 0 ] / 360,
+		s = hsla[ 1 ],
+		l = hsla[ 2 ],
+		a = hsla[ 3 ],
+		q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s,
+		p = 2 * l - q;
+
+	return [
+		Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ),
+		Math.round( hue2rgb( p, q, h ) * 255 ),
+		Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ),
+		a
+	];
+};
+
+each( spaces, function( spaceName, space ) {
+	var props = space.props,
+		cache = space.cache,
+		to = space.to,
+		from = space.from;
+
+	// Makes rgba() and hsla()
+	color.fn[ spaceName ] = function( value ) {
+
+		// Generate a cache for this space if it doesn't exist
+		if ( to && !this[ cache ] ) {
+			this[ cache ] = to( this._rgba );
+		}
+		if ( value === undefined ) {
+			return this[ cache ].slice();
+		}
+
+		var ret,
+			type = jQuery.type( value ),
+			arr = ( type === "array" || type === "object" ) ? value : arguments,
+			local = this[ cache ].slice();
+
+		each( props, function( key, prop ) {
+			var val = arr[ type === "object" ? key : prop.idx ];
+			if ( val == null ) {
+				val = local[ prop.idx ];
+			}
+			local[ prop.idx ] = clamp( val, prop );
+		} );
+
+		if ( from ) {
+			ret = color( from( local ) );
+			ret[ cache ] = local;
+			return ret;
+		} else {
+			return color( local );
+		}
+	};
+
+	// Makes red() green() blue() alpha() hue() saturation() lightness()
+	each( props, function( key, prop ) {
+
+		// Alpha is included in more than one space
+		if ( color.fn[ key ] ) {
+			return;
+		}
+		color.fn[ key ] = function( value ) {
+			var vtype = jQuery.type( value ),
+				fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ),
+				local = this[ fn ](),
+				cur = local[ prop.idx ],
+				match;
+
+			if ( vtype === "undefined" ) {
+				return cur;
+			}
+
+			if ( vtype === "function" ) {
+				value = value.call( this, cur );
+				vtype = jQuery.type( value );
+			}
+			if ( value == null && prop.empty ) {
+				return this;
+			}
+			if ( vtype === "string" ) {
+				match = rplusequals.exec( value );
+				if ( match ) {
+					value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 );
+				}
+			}
+			local[ prop.idx ] = value;
+			return this[ fn ]( local );
+		};
+	} );
+} );
+
+// Add cssHook and .fx.step function for each named hook.
+// accept a space separated string of properties
+color.hook = function( hook ) {
+	var hooks = hook.split( " " );
+	each( hooks, function( i, hook ) {
+		jQuery.cssHooks[ hook ] = {
+			set: function( elem, value ) {
+				var parsed, curElem,
+					backgroundColor = "";
+
+				if ( value !== "transparent" && ( jQuery.type( value ) !== "string" ||
+						( parsed = stringParse( value ) ) ) ) {
+					value = color( parsed || value );
+					if ( !support.rgba && value._rgba[ 3 ] !== 1 ) {
+						curElem = hook === "backgroundColor" ? elem.parentNode : elem;
+						while (
+							( backgroundColor === "" || backgroundColor === "transparent" ) &&
+							curElem && curElem.style
+						) {
+							try {
+								backgroundColor = jQuery.css( curElem, "backgroundColor" );
+								curElem = curElem.parentNode;
+							} catch ( e ) {
+							}
+						}
+
+						value = value.blend( backgroundColor && backgroundColor !== "transparent" ?
+							backgroundColor :
+							"_default" );
+					}
+
+					value = value.toRgbaString();
+				}
+				try {
+					elem.style[ hook ] = value;
+				} catch ( e ) {
+
+					// Wrapped to prevent IE from throwing errors on "invalid" values like
+					// 'auto' or 'inherit'
+				}
+			}
+		};
+		jQuery.fx.step[ hook ] = function( fx ) {
+			if ( !fx.colorInit ) {
+				fx.start = color( fx.elem, hook );
+				fx.end = color( fx.end );
+				fx.colorInit = true;
+			}
+			jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) );
+		};
+	} );
+
+};
+
+color.hook( stepHooks );
+
+jQuery.cssHooks.borderColor = {
+	expand: function( value ) {
+		var expanded = {};
+
+		each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) {
+			expanded[ "border" + part + "Color" ] = value;
+		} );
+		return expanded;
+	}
+};
+
+// Basic color names only.
+// Usage of any of the other color names requires adding yourself or including
+// jquery.color.svg-names.js.
+colors = jQuery.Color.names = {
+
+	// 4.1. Basic color keywords
+	aqua: "#00ffff",
+	black: "#000000",
+	blue: "#0000ff",
+	fuchsia: "#ff00ff",
+	gray: "#808080",
+	green: "#008000",
+	lime: "#00ff00",
+	maroon: "#800000",
+	navy: "#000080",
+	olive: "#808000",
+	purple: "#800080",
+	red: "#ff0000",
+	silver: "#c0c0c0",
+	teal: "#008080",
+	white: "#ffffff",
+	yellow: "#ffff00",
+
+	// 4.2.3. "transparent" color keyword
+	transparent: [ null, null, null, 0 ],
+
+	_default: "#ffffff"
+};
+
+} )( jQuery );
+
+/******************************************************************************/
+/****************************** CLASS ANIMATIONS ******************************/
+/******************************************************************************/
+( function() {
+
+var classAnimationActions = [ "add", "remove", "toggle" ],
+	shorthandStyles = {
+		border: 1,
+		borderBottom: 1,
+		borderColor: 1,
+		borderLeft: 1,
+		borderRight: 1,
+		borderTop: 1,
+		borderWidth: 1,
+		margin: 1,
+		padding: 1
+	};
+
+$.each(
+	[ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ],
+	function( _, prop ) {
+		$.fx.step[ prop ] = function( fx ) {
+			if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) {
+				jQuery.style( fx.elem, prop, fx.end );
+				fx.setAttr = true;
+			}
+		};
+	}
+);
+
+function getElementStyles( elem ) {
+	var key, len,
+		style = elem.ownerDocument.defaultView ?
+			elem.ownerDocument.defaultView.getComputedStyle( elem, null ) :
+			elem.currentStyle,
+		styles = {};
+
+	if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) {
+		len = style.length;
+		while ( len-- ) {
+			key = style[ len ];
+			if ( typeof style[ key ] === "string" ) {
+				styles[ $.camelCase( key ) ] = style[ key ];
+			}
+		}
+
+	// Support: Opera, IE <9
+	} else {
+		for ( key in style ) {
+			if ( typeof style[ key ] === "string" ) {
+				styles[ key ] = style[ key ];
+			}
+		}
+	}
+
+	return styles;
+}
+
+function styleDifference( oldStyle, newStyle ) {
+	var diff = {},
+		name, value;
+
+	for ( name in newStyle ) {
+		value = newStyle[ name ];
+		if ( oldStyle[ name ] !== value ) {
+			if ( !shorthandStyles[ name ] ) {
+				if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) {
+					diff[ name ] = value;
+				}
+			}
+		}
+	}
+
+	return diff;
+}
+
+// Support: jQuery <1.8
+if ( !$.fn.addBack ) {
+	$.fn.addBack = function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter( selector )
+		);
+	};
+}
+
+$.effects.animateClass = function( value, duration, easing, callback ) {
+	var o = $.speed( duration, easing, callback );
+
+	return this.queue( function() {
+		var animated = $( this ),
+			baseClass = animated.attr( "class" ) || "",
+			applyClassChange,
+			allAnimations = o.children ? animated.find( "*" ).addBack() : animated;
+
+		// Map the animated objects to store the original styles.
+		allAnimations = allAnimations.map( function() {
+			var el = $( this );
+			return {
+				el: el,
+				start: getElementStyles( this )
+			};
+		} );
+
+		// Apply class change
+		applyClassChange = function() {
+			$.each( classAnimationActions, function( i, action ) {
+				if ( value[ action ] ) {
+					animated[ action + "Class" ]( value[ action ] );
+				}
+			} );
+		};
+		applyClassChange();
+
+		// Map all animated objects again - calculate new styles and diff
+		allAnimations = allAnimations.map( function() {
+			this.end = getElementStyles( this.el[ 0 ] );
+			this.diff = styleDifference( this.start, this.end );
+			return this;
+		} );
+
+		// Apply original class
+		animated.attr( "class", baseClass );
+
+		// Map all animated objects again - this time collecting a promise
+		allAnimations = allAnimations.map( function() {
+			var styleInfo = this,
+				dfd = $.Deferred(),
+				opts = $.extend( {}, o, {
+					queue: false,
+					complete: function() {
+						dfd.resolve( styleInfo );
+					}
+				} );
+
+			this.el.animate( this.diff, opts );
+			return dfd.promise();
+		} );
+
+		// Once all animations have completed:
+		$.when.apply( $, allAnimations.get() ).done( function() {
+
+			// Set the final class
+			applyClassChange();
+
+			// For each animated element,
+			// clear all css properties that were animated
+			$.each( arguments, function() {
+				var el = this.el;
+				$.each( this.diff, function( key ) {
+					el.css( key, "" );
+				} );
+			} );
+
+			// This is guarnteed to be there if you use jQuery.speed()
+			// it also handles dequeuing the next anim...
+			o.complete.call( animated[ 0 ] );
+		} );
+	} );
+};
+
+$.fn.extend( {
+	addClass: ( function( orig ) {
+		return function( classNames, speed, easing, callback ) {
+			return speed ?
+				$.effects.animateClass.call( this,
+					{ add: classNames }, speed, easing, callback ) :
+				orig.apply( this, arguments );
+		};
+	} )( $.fn.addClass ),
+
+	removeClass: ( function( orig ) {
+		return function( classNames, speed, easing, callback ) {
+			return arguments.length > 1 ?
+				$.effects.animateClass.call( this,
+					{ remove: classNames }, speed, easing, callback ) :
+				orig.apply( this, arguments );
+		};
+	} )( $.fn.removeClass ),
+
+	toggleClass: ( function( orig ) {
+		return function( classNames, force, speed, easing, callback ) {
+			if ( typeof force === "boolean" || force === undefined ) {
+				if ( !speed ) {
+
+					// Without speed parameter
+					return orig.apply( this, arguments );
+				} else {
+					return $.effects.animateClass.call( this,
+						( force ? { add: classNames } : { remove: classNames } ),
+						speed, easing, callback );
+				}
+			} else {
+
+				// Without force parameter
+				return $.effects.animateClass.call( this,
+					{ toggle: classNames }, force, speed, easing );
+			}
+		};
+	} )( $.fn.toggleClass ),
+
+	switchClass: function( remove, add, speed, easing, callback ) {
+		return $.effects.animateClass.call( this, {
+			add: add,
+			remove: remove
+		}, speed, easing, callback );
+	}
+} );
+
+} )();
+
+/******************************************************************************/
+/*********************************** EFFECTS **********************************/
+/******************************************************************************/
+
+( function() {
+
+if ( $.expr && $.expr.filters && $.expr.filters.animated ) {
+	$.expr.filters.animated = ( function( orig ) {
+		return function( elem ) {
+			return !!$( elem ).data( dataSpaceAnimated ) || orig( elem );
+		};
+	} )( $.expr.filters.animated );
+}
+
+if ( $.uiBackCompat !== false ) {
+	$.extend( $.effects, {
+
+		// Saves a set of properties in a data storage
+		save: function( element, set ) {
+			var i = 0, length = set.length;
+			for ( ; i < length; i++ ) {
+				if ( set[ i ] !== null ) {
+					element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] );
+				}
+			}
+		},
+
+		// Restores a set of previously saved properties from a data storage
+		restore: function( element, set ) {
+			var val, i = 0, length = set.length;
+			for ( ; i < length; i++ ) {
+				if ( set[ i ] !== null ) {
+					val = element.data( dataSpace + set[ i ] );
+					element.css( set[ i ], val );
+				}
+			}
+		},
+
+		setMode: function( el, mode ) {
+			if ( mode === "toggle" ) {
+				mode = el.is( ":hidden" ) ? "show" : "hide";
+			}
+			return mode;
+		},
+
+		// Wraps the element around a wrapper that copies position properties
+		createWrapper: function( element ) {
+
+			// If the element is already wrapped, return it
+			if ( element.parent().is( ".ui-effects-wrapper" ) ) {
+				return element.parent();
+			}
+
+			// Wrap the element
+			var props = {
+					width: element.outerWidth( true ),
+					height: element.outerHeight( true ),
+					"float": element.css( "float" )
+				},
+				wrapper = $( "<div></div>" )
+					.addClass( "ui-effects-wrapper" )
+					.css( {
+						fontSize: "100%",
+						background: "transparent",
+						border: "none",
+						margin: 0,
+						padding: 0
+					} ),
+
+				// Store the size in case width/height are defined in % - Fixes #5245
+				size = {
+					width: element.width(),
+					height: element.height()
+				},
+				active = document.activeElement;
+
+			// Support: Firefox
+			// Firefox incorrectly exposes anonymous content
+			// https://bugzilla.mozilla.org/show_bug.cgi?id=561664
+			try {
+				active.id;
+			} catch ( e ) {
+				active = document.body;
+			}
+
+			element.wrap( wrapper );
+
+			// Fixes #7595 - Elements lose focus when wrapped.
+			if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+				$( active ).trigger( "focus" );
+			}
+
+			// Hotfix for jQuery 1.4 since some change in wrap() seems to actually
+			// lose the reference to the wrapped element
+			wrapper = element.parent();
+
+			// Transfer positioning properties to the wrapper
+			if ( element.css( "position" ) === "static" ) {
+				wrapper.css( { position: "relative" } );
+				element.css( { position: "relative" } );
+			} else {
+				$.extend( props, {
+					position: element.css( "position" ),
+					zIndex: element.css( "z-index" )
+				} );
+				$.each( [ "top", "left", "bottom", "right" ], function( i, pos ) {
+					props[ pos ] = element.css( pos );
+					if ( isNaN( parseInt( props[ pos ], 10 ) ) ) {
+						props[ pos ] = "auto";
+					}
+				} );
+				element.css( {
+					position: "relative",
+					top: 0,
+					left: 0,
+					right: "auto",
+					bottom: "auto"
+				} );
+			}
+			element.css( size );
+
+			return wrapper.css( props ).show();
+		},
+
+		removeWrapper: function( element ) {
+			var active = document.activeElement;
+
+			if ( element.parent().is( ".ui-effects-wrapper" ) ) {
+				element.parent().replaceWith( element );
+
+				// Fixes #7595 - Elements lose focus when wrapped.
+				if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+					$( active ).trigger( "focus" );
+				}
+			}
+
+			return element;
+		}
+	} );
+}
+
+$.extend( $.effects, {
+	version: "1.12.1",
+
+	define: function( name, mode, effect ) {
+		if ( !effect ) {
+			effect = mode;
+			mode = "effect";
+		}
+
+		$.effects.effect[ name ] = effect;
+		$.effects.effect[ name ].mode = mode;
+
+		return effect;
+	},
+
+	scaledDimensions: function( element, percent, direction ) {
+		if ( percent === 0 ) {
+			return {
+				height: 0,
+				width: 0,
+				outerHeight: 0,
+				outerWidth: 0
+			};
+		}
+
+		var x = direction !== "horizontal" ? ( ( percent || 100 ) / 100 ) : 1,
+			y = direction !== "vertical" ? ( ( percent || 100 ) / 100 ) : 1;
+
+		return {
+			height: element.height() * y,
+			width: element.width() * x,
+			outerHeight: element.outerHeight() * y,
+			outerWidth: element.outerWidth() * x
+		};
+
+	},
+
+	clipToBox: function( animation ) {
+		return {
+			width: animation.clip.right - animation.clip.left,
+			height: animation.clip.bottom - animation.clip.top,
+			left: animation.clip.left,
+			top: animation.clip.top
+		};
+	},
+
+	// Injects recently queued functions to be first in line (after "inprogress")
+	unshift: function( element, queueLength, count ) {
+		var queue = element.queue();
+
+		if ( queueLength > 1 ) {
+			queue.splice.apply( queue,
+				[ 1, 0 ].concat( queue.splice( queueLength, count ) ) );
+		}
+		element.dequeue();
+	},
+
+	saveStyle: function( element ) {
+		element.data( dataSpaceStyle, element[ 0 ].style.cssText );
+	},
+
+	restoreStyle: function( element ) {
+		element[ 0 ].style.cssText = element.data( dataSpaceStyle ) || "";
+		element.removeData( dataSpaceStyle );
+	},
+
+	mode: function( element, mode ) {
+		var hidden = element.is( ":hidden" );
+
+		if ( mode === "toggle" ) {
+			mode = hidden ? "show" : "hide";
+		}
+		if ( hidden ? mode === "hide" : mode === "show" ) {
+			mode = "none";
+		}
+		return mode;
+	},
+
+	// Translates a [top,left] array into a baseline value
+	getBaseline: function( origin, original ) {
+		var y, x;
+
+		switch ( origin[ 0 ] ) {
+		case "top":
+			y = 0;
+			break;
+		case "middle":
+			y = 0.5;
+			break;
+		case "bottom":
+			y = 1;
+			break;
+		default:
+			y = origin[ 0 ] / original.height;
+		}
+
+		switch ( origin[ 1 ] ) {
+		case "left":
+			x = 0;
+			break;
+		case "center":
+			x = 0.5;
+			break;
+		case "right":
+			x = 1;
+			break;
+		default:
+			x = origin[ 1 ] / original.width;
+		}
+
+		return {
+			x: x,
+			y: y
+		};
+	},
+
+	// Creates a placeholder element so that the original element can be made absolute
+	createPlaceholder: function( element ) {
+		var placeholder,
+			cssPosition = element.css( "position" ),
+			position = element.position();
+
+		// Lock in margins first to account for form elements, which
+		// will change margin if you explicitly set height
+		// see: http://jsfiddle.net/JZSMt/3/ https://bugs.webkit.org/show_bug.cgi?id=107380
+		// Support: Safari
+		element.css( {
+			marginTop: element.css( "marginTop" ),
+			marginBottom: element.css( "marginBottom" ),
+			marginLeft: element.css( "marginLeft" ),
+			marginRight: element.css( "marginRight" )
+		} )
+		.outerWidth( element.outerWidth() )
+		.outerHeight( element.outerHeight() );
+
+		if ( /^(static|relative)/.test( cssPosition ) ) {
+			cssPosition = "absolute";
+
+			placeholder = $( "<" + element[ 0 ].nodeName + ">" ).insertAfter( element ).css( {
+
+				// Convert inline to inline block to account for inline elements
+				// that turn to inline block based on content (like img)
+				display: /^(inline|ruby)/.test( element.css( "display" ) ) ?
+					"inline-block" :
+					"block",
+				visibility: "hidden",
+
+				// Margins need to be set to account for margin collapse
+				marginTop: element.css( "marginTop" ),
+				marginBottom: element.css( "marginBottom" ),
+				marginLeft: element.css( "marginLeft" ),
+				marginRight: element.css( "marginRight" ),
+				"float": element.css( "float" )
+			} )
+			.outerWidth( element.outerWidth() )
+			.outerHeight( element.outerHeight() )
+			.addClass( "ui-effects-placeholder" );
+
+			element.data( dataSpace + "placeholder", placeholder );
+		}
+
+		element.css( {
+			position: cssPosition,
+			left: position.left,
+			top: position.top
+		} );
+
+		return placeholder;
+	},
+
+	removePlaceholder: function( element ) {
+		var dataKey = dataSpace + "placeholder",
+				placeholder = element.data( dataKey );
+
+		if ( placeholder ) {
+			placeholder.remove();
+			element.removeData( dataKey );
+		}
+	},
+
+	// Removes a placeholder if it exists and restores
+	// properties that were modified during placeholder creation
+	cleanUp: function( element ) {
+		$.effects.restoreStyle( element );
+		$.effects.removePlaceholder( element );
+	},
+
+	setTransition: function( element, list, factor, value ) {
+		value = value || {};
+		$.each( list, function( i, x ) {
+			var unit = element.cssUnit( x );
+			if ( unit[ 0 ] > 0 ) {
+				value[ x ] = unit[ 0 ] * factor + unit[ 1 ];
+			}
+		} );
+		return value;
+	}
+} );
+
+// Return an effect options object for the given parameters:
+function _normalizeArguments( effect, options, speed, callback ) {
+
+	// Allow passing all options as the first parameter
+	if ( $.isPlainObject( effect ) ) {
+		options = effect;
+		effect = effect.effect;
+	}
+
+	// Convert to an object
+	effect = { effect: effect };
+
+	// Catch (effect, null, ...)
+	if ( options == null ) {
+		options = {};
+	}
+
+	// Catch (effect, callback)
+	if ( $.isFunction( options ) ) {
+		callback = options;
+		speed = null;
+		options = {};
+	}
+
+	// Catch (effect, speed, ?)
+	if ( typeof options === "number" || $.fx.speeds[ options ] ) {
+		callback = speed;
+		speed = options;
+		options = {};
+	}
+
+	// Catch (effect, options, callback)
+	if ( $.isFunction( speed ) ) {
+		callback = speed;
+		speed = null;
+	}
+
+	// Add options to effect
+	if ( options ) {
+		$.extend( effect, options );
+	}
+
+	speed = speed || options.duration;
+	effect.duration = $.fx.off ? 0 :
+		typeof speed === "number" ? speed :
+		speed in $.fx.speeds ? $.fx.speeds[ speed ] :
+		$.fx.speeds._default;
+
+	effect.complete = callback || options.complete;
+
+	return effect;
+}
+
+function standardAnimationOption( option ) {
+
+	// Valid standard speeds (nothing, number, named speed)
+	if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) {
+		return true;
+	}
+
+	// Invalid strings - treat as "normal" speed
+	if ( typeof option === "string" && !$.effects.effect[ option ] ) {
+		return true;
+	}
+
+	// Complete callback
+	if ( $.isFunction( option ) ) {
+		return true;
+	}
+
+	// Options hash (but not naming an effect)
+	if ( typeof option === "object" && !option.effect ) {
+		return true;
+	}
+
+	// Didn't match any standard API
+	return false;
+}
+
+$.fn.extend( {
+	effect: function( /* effect, options, speed, callback */ ) {
+		var args = _normalizeArguments.apply( this, arguments ),
+			effectMethod = $.effects.effect[ args.effect ],
+			defaultMode = effectMethod.mode,
+			queue = args.queue,
+			queueName = queue || "fx",
+			complete = args.complete,
+			mode = args.mode,
+			modes = [],
+			prefilter = function( next ) {
+				var el = $( this ),
+					normalizedMode = $.effects.mode( el, mode ) || defaultMode;
+
+				// Sentinel for duck-punching the :animated psuedo-selector
+				el.data( dataSpaceAnimated, true );
+
+				// Save effect mode for later use,
+				// we can't just call $.effects.mode again later,
+				// as the .show() below destroys the initial state
+				modes.push( normalizedMode );
+
+				// See $.uiBackCompat inside of run() for removal of defaultMode in 1.13
+				if ( defaultMode && ( normalizedMode === "show" ||
+						( normalizedMode === defaultMode && normalizedMode === "hide" ) ) ) {
+					el.show();
+				}
+
+				if ( !defaultMode || normalizedMode !== "none" ) {
+					$.effects.saveStyle( el );
+				}
+
+				if ( $.isFunction( next ) ) {
+					next();
+				}
+			};
+
+		if ( $.fx.off || !effectMethod ) {
+
+			// Delegate to the original method (e.g., .show()) if possible
+			if ( mode ) {
+				return this[ mode ]( args.duration, complete );
+			} else {
+				return this.each( function() {
+					if ( complete ) {
+						complete.call( this );
+					}
+				} );
+			}
+		}
+
+		function run( next ) {
+			var elem = $( this );
+
+			function cleanup() {
+				elem.removeData( dataSpaceAnimated );
+
+				$.effects.cleanUp( elem );
+
+				if ( args.mode === "hide" ) {
+					elem.hide();
+				}
+
+				done();
+			}
+
+			function done() {
+				if ( $.isFunction( complete ) ) {
+					complete.call( elem[ 0 ] );
+				}
+
+				if ( $.isFunction( next ) ) {
+					next();
+				}
+			}
+
+			// Override mode option on a per element basis,
+			// as toggle can be either show or hide depending on element state
+			args.mode = modes.shift();
+
+			if ( $.uiBackCompat !== false && !defaultMode ) {
+				if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) {
+
+					// Call the core method to track "olddisplay" properly
+					elem[ mode ]();
+					done();
+				} else {
+					effectMethod.call( elem[ 0 ], args, done );
+				}
+			} else {
+				if ( args.mode === "none" ) {
+
+					// Call the core method to track "olddisplay" properly
+					elem[ mode ]();
+					done();
+				} else {
+					effectMethod.call( elem[ 0 ], args, cleanup );
+				}
+			}
+		}
+
+		// Run prefilter on all elements first to ensure that
+		// any showing or hiding happens before placeholder creation,
+		// which ensures that any layout changes are correctly captured.
+		return queue === false ?
+			this.each( prefilter ).each( run ) :
+			this.queue( queueName, prefilter ).queue( queueName, run );
+	},
+
+	show: ( function( orig ) {
+		return function( option ) {
+			if ( standardAnimationOption( option ) ) {
+				return orig.apply( this, arguments );
+			} else {
+				var args = _normalizeArguments.apply( this, arguments );
+				args.mode = "show";
+				return this.effect.call( this, args );
+			}
+		};
+	} )( $.fn.show ),
+
+	hide: ( function( orig ) {
+		return function( option ) {
+			if ( standardAnimationOption( option ) ) {
+				return orig.apply( this, arguments );
+			} else {
+				var args = _normalizeArguments.apply( this, arguments );
+				args.mode = "hide";
+				return this.effect.call( this, args );
+			}
+		};
+	} )( $.fn.hide ),
+
+	toggle: ( function( orig ) {
+		return function( option ) {
+			if ( standardAnimationOption( option ) || typeof option === "boolean" ) {
+				return orig.apply( this, arguments );
+			} else {
+				var args = _normalizeArguments.apply( this, arguments );
+				args.mode = "toggle";
+				return this.effect.call( this, args );
+			}
+		};
+	} )( $.fn.toggle ),
+
+	cssUnit: function( key ) {
+		var style = this.css( key ),
+			val = [];
+
+		$.each( [ "em", "px", "%", "pt" ], function( i, unit ) {
+			if ( style.indexOf( unit ) > 0 ) {
+				val = [ parseFloat( style ), unit ];
+			}
+		} );
+		return val;
+	},
+
+	cssClip: function( clipObj ) {
+		if ( clipObj ) {
+			return this.css( "clip", "rect(" + clipObj.top + "px " + clipObj.right + "px " +
+				clipObj.bottom + "px " + clipObj.left + "px)" );
+		}
+		return parseClip( this.css( "clip" ), this );
+	},
+
+	transfer: function( options, done ) {
+		var element = $( this ),
+			target = $( options.to ),
+			targetFixed = target.css( "position" ) === "fixed",
+			body = $( "body" ),
+			fixTop = targetFixed ? body.scrollTop() : 0,
+			fixLeft = targetFixed ? body.scrollLeft() : 0,
+			endPosition = target.offset(),
+			animation = {
+				top: endPosition.top - fixTop,
+				left: endPosition.left - fixLeft,
+				height: target.innerHeight(),
+				width: target.innerWidth()
+			},
+			startPosition = element.offset(),
+			transfer = $( "<div class='ui-effects-transfer'></div>" )
+				.appendTo( "body" )
+				.addClass( options.className )
+				.css( {
+					top: startPosition.top - fixTop,
+					left: startPosition.left - fixLeft,
+					height: element.innerHeight(),
+					width: element.innerWidth(),
+					position: targetFixed ? "fixed" : "absolute"
+				} )
+				.animate( animation, options.duration, options.easing, function() {
+					transfer.remove();
+					if ( $.isFunction( done ) ) {
+						done();
+					}
+				} );
+	}
+} );
+
+function parseClip( str, element ) {
+		var outerWidth = element.outerWidth(),
+			outerHeight = element.outerHeight(),
+			clipRegex = /^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,
+			values = clipRegex.exec( str ) || [ "", 0, outerWidth, outerHeight, 0 ];
+
+		return {
+			top: parseFloat( values[ 1 ] ) || 0,
+			right: values[ 2 ] === "auto" ? outerWidth : parseFloat( values[ 2 ] ),
+			bottom: values[ 3 ] === "auto" ? outerHeight : parseFloat( values[ 3 ] ),
+			left: parseFloat( values[ 4 ] ) || 0
+		};
+}
+
+$.fx.step.clip = function( fx ) {
+	if ( !fx.clipInit ) {
+		fx.start = $( fx.elem ).cssClip();
+		if ( typeof fx.end === "string" ) {
+			fx.end = parseClip( fx.end, fx.elem );
+		}
+		fx.clipInit = true;
+	}
+
+	$( fx.elem ).cssClip( {
+		top: fx.pos * ( fx.end.top - fx.start.top ) + fx.start.top,
+		right: fx.pos * ( fx.end.right - fx.start.right ) + fx.start.right,
+		bottom: fx.pos * ( fx.end.bottom - fx.start.bottom ) + fx.start.bottom,
+		left: fx.pos * ( fx.end.left - fx.start.left ) + fx.start.left
+	} );
+};
+
+} )();
+
+/******************************************************************************/
+/*********************************** EASING ***********************************/
+/******************************************************************************/
+
+( function() {
+
+// Based on easing equations from Robert Penner (http://www.robertpenner.com/easing)
+
+var baseEasings = {};
+
+$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) {
+	baseEasings[ name ] = function( p ) {
+		return Math.pow( p, i + 2 );
+	};
+} );
+
+$.extend( baseEasings, {
+	Sine: function( p ) {
+		return 1 - Math.cos( p * Math.PI / 2 );
+	},
+	Circ: function( p ) {
+		return 1 - Math.sqrt( 1 - p * p );
+	},
+	Elastic: function( p ) {
+		return p === 0 || p === 1 ? p :
+			-Math.pow( 2, 8 * ( p - 1 ) ) * Math.sin( ( ( p - 1 ) * 80 - 7.5 ) * Math.PI / 15 );
+	},
+	Back: function( p ) {
+		return p * p * ( 3 * p - 2 );
+	},
+	Bounce: function( p ) {
+		var pow2,
+			bounce = 4;
+
+		while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
+		return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
+	}
+} );
+
+$.each( baseEasings, function( name, easeIn ) {
+	$.easing[ "easeIn" + name ] = easeIn;
+	$.easing[ "easeOut" + name ] = function( p ) {
+		return 1 - easeIn( 1 - p );
+	};
+	$.easing[ "easeInOut" + name ] = function( p ) {
+		return p < 0.5 ?
+			easeIn( p * 2 ) / 2 :
+			1 - easeIn( p * -2 + 2 ) / 2;
+	};
+} );
+
+} )();
+
+var effect = $.effects;
+
+
+/*!
+ * jQuery UI Effects Blind 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Blind Effect
+//>>group: Effects
+//>>description: Blinds the element.
+//>>docs: http://api.jqueryui.com/blind-effect/
+//>>demos: http://jqueryui.com/effect/
+
+
+
+var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, done ) {
+	var map = {
+			up: [ "bottom", "top" ],
+			vertical: [ "bottom", "top" ],
+			down: [ "top", "bottom" ],
+			left: [ "right", "left" ],
+			horizontal: [ "right", "left" ],
+			right: [ "left", "right" ]
+		},
+		element = $( this ),
+		direction = options.direction || "up",
+		start = element.cssClip(),
+		animate = { clip: $.extend( {}, start ) },
+		placeholder = $.effects.createPlaceholder( element );
+
+	animate.clip[ map[ direction ][ 0 ] ] = animate.clip[ map[ direction ][ 1 ] ];
+
+	if ( options.mode === "show" ) {
+		element.cssClip( animate.clip );
+		if ( placeholder ) {
+			placeholder.css( $.effects.clipToBox( animate ) );
+		}
+
+		animate.clip = start;
+	}
+
+	if ( placeholder ) {
+		placeholder.animate( $.effects.clipToBox( animate ), options.duration, options.easing );
+	}
+
+	element.animate( animate, {
+		queue: false,
+		duration: options.duration,
+		easing: options.easing,
+		complete: done
+	} );
+} );
+
+// NOTE: Original jQuery UI wrapper was replaced. See README-Fancytree.md
+// }));
+})(jQuery);
+
+(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+}(function( $ ) {
+
+
+/*! Fancytree Core *//*!
+ * jquery.fancytree.js
+ * Tree view control with support for lazy loading and much more.
+ * https://github.com/mar10/fancytree/
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+/** Core Fancytree module.
+ */
+
+// UMD wrapper for the Fancytree core module
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree.ui-deps" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree.ui-deps");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+// prevent duplicate loading
+if ( $.ui && $.ui.fancytree ) {
+	$.ui.fancytree.warn("Fancytree: ignored duplicate include");
+	return;
+}
+
+
+/* *****************************************************************************
+ * Private functions and variables
+ */
+
+var i, attr,
+	FT = null, // initialized below
+	TEST_IMG = new RegExp(/\.|\//),  // strings are considered image urls if they contain '.' or '/'
+	REX_HTML = /[&<>"'\/]/g,
+	REX_TOOLTIP = /[<>"'\/]/g,
+	RECURSIVE_REQUEST_ERROR = "$recursive_request",
+	ENTITY_MAP = {"&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;", "/": "&#x2F;"},
+	IGNORE_KEYCODES = { 16: true, 17: true, 18: true },
+	SPECIAL_KEYCODES = {
+		8: "backspace", 9: "tab", 10: "return", 13: "return",
+		// 16: null, 17: null, 18: null, // ignore shift, ctrl, alt
+		19: "pause", 20: "capslock", 27: "esc", 32: "space", 33: "pageup",
+		34: "pagedown", 35: "end", 36: "home", 37: "left", 38: "up",
+		39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=",
+		96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6",
+		103: "7", 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".",
+		111: "/", 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5",
+		117: "f6", 118: "f7", 119: "f8", 120: "f9", 121: "f10", 122: "f11",
+		123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=",
+		188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
+		221: "]", 222: "'"},
+	MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" },
+	// Boolean attributes that can be set with equivalent class names in the LI tags
+	// Note: v2.23: checkbox and hideCheckbox are *not* in this list
+	CLASS_ATTRS = "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split(" "),
+	CLASS_ATTR_MAP = {},
+	// Top-level Fancytree node attributes, that can be set by dict
+	NODE_ATTRS = "checkbox expanded extraClasses folder icon key lazy radiogroup refKey selected statusNodeType title tooltip unselectable unselectableIgnore unselectableStatus".split(" "),
+	NODE_ATTR_MAP = {},
+	// Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase)
+	NODE_ATTR_LOWERCASE_MAP = {},
+	// Attribute names that should NOT be added to node.data
+	NONE_NODE_DATA_MAP = {"active": true, "children": true, "data": true, "focus": true};
+
+for(i=0; i<CLASS_ATTRS.length; i++){ CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; }
+for(i=0; i<NODE_ATTRS.length; i++) {
+	attr = NODE_ATTRS[i];
+	NODE_ATTR_MAP[attr] = true;
+	if( attr !== attr.toLowerCase() ) {
+		NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr;
+	}
+}
+
+
+function _assert(cond, msg){
+	// TODO: see qunit.js extractStacktrace()
+	if(!cond){
+		msg = msg ? ": " + msg : "";
+		// consoleApply("assert", [!!cond, msg]);
+		$.error("Fancytree assertion failed" + msg);
+	}
+}
+
+_assert($.ui, "Fancytree requires jQuery UI (http://jqueryui.com)");
+
+function consoleApply(method, args){
+	var i, s,
+		fn = window.console ? window.console[method] : null;
+
+	if(fn){
+		try{
+			fn.apply(window.console, args);
+		} catch(e) {
+			// IE 8?
+			s = "";
+			for( i=0; i<args.length; i++ ) {
+				s += args[i];
+			}
+			fn(s);
+		}
+	}
+}
+
+/*Return true if x is a FancytreeNode.*/
+function _isNode(x){
+	return !!(x.tree && x.statusNodeType !== undefined);
+}
+
+/** Return true if dotted version string is equal or higher than requested version.
+ *
+ * See http://jsfiddle.net/mar10/FjSAN/
+ */
+function isVersionAtLeast(dottedVersion, major, minor, patch){
+	var i, v, t,
+		verParts = $.map($.trim(dottedVersion).split("."), function(e){ return parseInt(e, 10); }),
+		testParts = $.map(Array.prototype.slice.call(arguments, 1), function(e){ return parseInt(e, 10); });
+
+	for( i = 0; i < testParts.length; i++ ){
+		v = verParts[i] || 0;
+		t = testParts[i] || 0;
+		if( v !== t ){
+			return ( v > t );
+		}
+	}
+	return true;
+}
+
+/** Return a wrapper that calls sub.methodName() and exposes
+ *  this             : tree
+ *  this._local      : tree.ext.EXTNAME
+ *  this._super      : base.methodName.call()
+ *  this._superApply : base.methodName.apply()
+ */
+function _makeVirtualFunction(methodName, tree, base, extension, extName){
+	// $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName);
+	// if(rexTestSuper && !rexTestSuper.test(func)){
+	//     // extension.methodName() doesn't call _super(), so no wrapper required
+	//     return func;
+	// }
+	// Use an immediate function as closure
+	var proxy = (function(){
+		var prevFunc = tree[methodName],      // org. tree method or prev. proxy
+			baseFunc = extension[methodName], //
+			_local = tree.ext[extName],
+			_super = function(){
+				return prevFunc.apply(tree, arguments);
+			},
+			_superApply = function(args){
+				return prevFunc.apply(tree, args);
+			};
+
+		// Return the wrapper function
+		return function(){
+			var prevLocal = tree._local,
+				prevSuper = tree._super,
+				prevSuperApply = tree._superApply;
+
+			try{
+				tree._local = _local;
+				tree._super = _super;
+				tree._superApply = _superApply;
+				return  baseFunc.apply(tree, arguments);
+			}finally{
+				tree._local = prevLocal;
+				tree._super = prevSuper;
+				tree._superApply = prevSuperApply;
+			}
+		};
+	})(); // end of Immediate Function
+	return proxy;
+}
+
+/**
+ * Subclass `base` by creating proxy functions
+ */
+function _subclassObject(tree, base, extension, extName){
+	// $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName);
+	for(var attrName in extension){
+		if(typeof extension[attrName] === "function"){
+			if(typeof tree[attrName] === "function"){
+				// override existing method
+				tree[attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName);
+			}else if(attrName.charAt(0) === "_"){
+				// Create private methods in tree.ext.EXTENSION namespace
+				tree.ext[extName][attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName);
+			}else{
+				$.error("Could not override tree." + attrName + ". Use prefix '_' to create tree." + extName + "._" + attrName);
+			}
+		}else{
+			// Create member variables in tree.ext.EXTENSION namespace
+			if(attrName !== "options"){
+				tree.ext[extName][attrName] = extension[attrName];
+			}
+		}
+	}
+}
+
+
+function _getResolvedPromise(context, argArray){
+	if(context === undefined){
+		return $.Deferred(function(){this.resolve();}).promise();
+	}else{
+		return $.Deferred(function(){this.resolveWith(context, argArray);}).promise();
+	}
+}
+
+
+function _getRejectedPromise(context, argArray){
+	if(context === undefined){
+		return $.Deferred(function(){this.reject();}).promise();
+	}else{
+		return $.Deferred(function(){this.rejectWith(context, argArray);}).promise();
+	}
+}
+
+
+function _makeResolveFunc(deferred, context){
+	return function(){
+		deferred.resolveWith(context);
+	};
+}
+
+
+function _getElementDataAsDict($el){
+	// Evaluate 'data-NAME' attributes with special treatment for 'data-json'.
+	var d = $.extend({}, $el.data()),
+		json = d.json;
+
+	delete d.fancytree; // added to container by widget factory (old jQuery UI)
+	delete d.uiFancytree; // added to container by widget factory
+
+	if( json ) {
+		delete d.json;
+		// <li data-json='...'> is already returned as object (http://api.jquery.com/data/#data-html5)
+		d = $.extend(d, json);
+	}
+	return d;
+}
+
+
+function _escapeTooltip(s){
+	return ("" + s).replace(REX_TOOLTIP, function(s) {
+		return ENTITY_MAP[s];
+	});
+}
+
+
+// TODO: use currying
+function _makeNodeTitleMatcher(s){
+	s = s.toLowerCase();
+	return function(node){
+		return node.title.toLowerCase().indexOf(s) >= 0;
+	};
+}
+
+
+function _makeNodeTitleStartMatcher(s){
+	var reMatch = new RegExp("^" + s, "i");
+	return function(node){
+		return reMatch.test(node.title);
+	};
+}
+
+
+/* *****************************************************************************
+ * FancytreeNode
+ */
+
+
+/**
+ * Creates a new FancytreeNode
+ *
+ * @class FancytreeNode
+ * @classdesc A FancytreeNode represents the hierarchical data model and operations.
+ *
+ * @param {FancytreeNode} parent
+ * @param {NodeData} obj
+ *
+ * @property {Fancytree} tree The tree instance
+ * @property {FancytreeNode} parent The parent node
+ * @property {string} key Node id (must be unique inside the tree)
+ * @property {string} title Display name (may contain HTML)
+ * @property {object} data Contains all extra data that was passed on node creation
+ * @property {FancytreeNode[] | null | undefined} children Array of child nodes.<br>
+ *     For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array
+ *     to define a node that has no children.
+ * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property.
+ * @property {string} extraClasses Additional CSS classes, added to the node's `&lt;span>`.<br>
+ *     Note: use `node.add/remove/toggleClass()` to modify.
+ * @property {boolean} folder Folder nodes have different default icons and click behavior.<br>
+ *     Note: Also non-folders may have children.
+ * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'.
+ * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion.
+ * @property {boolean} selected Use isSelected(), setSelected() to access this property.
+ * @property {string} tooltip Alternative description used as hover popup
+ */
+function FancytreeNode(parent, obj){
+	var i, l, name, cl;
+
+	this.parent = parent;
+	this.tree = parent.tree;
+	this.ul = null;
+	this.li = null;  // <li id='key' ftnode=this> tag
+	this.statusNodeType = null; // if this is a temp. node to display the status of its parent
+	this._isLoading = false;    // if this node itself is loading
+	this._error = null;         // {message: '...'} if a load error occurred
+	this.data = {};
+
+	// TODO: merge this code with node.toDict()
+	// copy attributes from obj object
+	for(i=0, l=NODE_ATTRS.length; i<l; i++){
+		name = NODE_ATTRS[i];
+		this[name] = obj[name];
+	}
+	// unselectableIgnore and unselectableStatus imply unselectable
+	if( this.unselectableIgnore != null || this.unselectableStatus != null ) {
+		this.unselectable = true;
+	}
+	if( obj.hideCheckbox ) {
+		$.error("'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'");
+	}
+	// node.data += obj.data
+	if(obj.data){
+		$.extend(this.data, obj.data);
+	}
+	// copy all other attributes to this.data.NAME
+	for(name in obj){
+		if(!NODE_ATTR_MAP[name] && !$.isFunction(obj[name]) && !NONE_NODE_DATA_MAP[name]){
+			// node.data.NAME = obj.NAME
+			this.data[name] = obj[name];
+		}
+	}
+
+	// Fix missing key
+	if( this.key == null ){ // test for null OR undefined
+		if( this.tree.options.defaultKey ) {
+			this.key = this.tree.options.defaultKey(this);
+			_assert(this.key, "defaultKey() must return a unique key");
+		} else {
+			this.key = "_" + (FT._nextNodeKey++);
+		}
+	} else {
+		this.key = "" + this.key; // Convert to string (#217)
+	}
+
+	// Fix tree.activeNode
+	// TODO: not elegant: we use obj.active as marker to set tree.activeNode
+	// when loading from a dictionary.
+	if(obj.active){
+		_assert(this.tree.activeNode === null, "only one active node allowed");
+		this.tree.activeNode = this;
+	}
+	if( obj.selected ){ // #186
+		this.tree.lastSelectedNode = this;
+	}
+	// TODO: handle obj.focus = true
+
+	// Create child nodes
+	cl = obj.children;
+	if( cl ){
+		if( cl.length ){
+			this._setChildren(cl);
+		} else {
+			// if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded'
+			this.children = this.lazy ? [] : null;
+		}
+	} else {
+		this.children = null;
+	}
+	// Add to key/ref map (except for root node)
+//	if( parent ) {
+	this.tree._callHook("treeRegisterNode", this.tree, true, this);
+//	}
+}
+
+
+FancytreeNode.prototype = /** @lends FancytreeNode# */{
+	/* Return the direct child FancytreeNode with a given key, index. */
+	_findDirectChild: function(ptr){
+		var i, l,
+			cl = this.children;
+
+		if(cl){
+			if(typeof ptr === "string"){
+				for(i=0, l=cl.length; i<l; i++){
+					if(cl[i].key === ptr){
+						return cl[i];
+					}
+				}
+			}else if(typeof ptr === "number"){
+				return this.children[ptr];
+			}else if(ptr.parent === this){
+				return ptr;
+			}
+		}
+		return null;
+	},
+	// TODO: activate()
+	// TODO: activateSilently()
+	/* Internal helper called in recursive addChildren sequence.*/
+	_setChildren: function(children){
+		_assert(children && (!this.children || this.children.length === 0), "only init supported");
+		this.children = [];
+		for(var i=0, l=children.length; i<l; i++){
+			this.children.push(new FancytreeNode(this, children[i]));
+		}
+	},
+	/**
+	 * Append (or insert) a list of child nodes.
+	 *
+	 * @param {NodeData[]} children array of child node definitions (also single child accepted)
+	 * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such).
+	 *     If omitted, the new children are appended.
+	 * @returns {FancytreeNode} first child added
+	 *
+	 * @see FancytreeNode#applyPatch
+	 */
+	addChildren: function(children, insertBefore){
+		var i, l, pos,
+			origFirstChild = this.getFirstChild(),
+			origLastChild = this.getLastChild(),
+			firstNode = null,
+			nodeList = [];
+
+		if($.isPlainObject(children) ){
+			children = [children];
+		}
+		if(!this.children){
+			this.children = [];
+		}
+		for(i=0, l=children.length; i<l; i++){
+			nodeList.push(new FancytreeNode(this, children[i]));
+		}
+		firstNode = nodeList[0];
+		if(insertBefore == null){
+			this.children = this.children.concat(nodeList);
+		}else{
+			insertBefore = this._findDirectChild(insertBefore);
+			pos = $.inArray(insertBefore, this.children);
+			_assert(pos >= 0, "insertBefore must be an existing child");
+			// insert nodeList after children[pos]
+			this.children.splice.apply(this.children, [pos, 0].concat(nodeList));
+		}
+		if ( origFirstChild && !insertBefore ) {
+			// #708: Fast path -- don't render every child of root, just the new ones!
+			// #723, #729: but only if it's appended to an existing child list
+			for(i=0, l=nodeList.length; i<l; i++) {
+				nodeList[i].render();   // New nodes were never rendered before
+			}
+			// Adjust classes where status may have changed
+			// Has a first child
+			if (origFirstChild !== this.getFirstChild()) {
+				// Different first child -- recompute classes
+				origFirstChild.renderStatus();
+			}
+			if (origLastChild !== this.getLastChild()) {
+				// Different last child -- recompute classes
+				origLastChild.renderStatus();
+			}
+		} else if( !this.parent || this.parent.ul || this.tr ){
+			// render if the parent was rendered (or this is a root node)
+			this.render();
+		}
+		if( this.tree.options.selectMode === 3 ){
+			this.fixSelection3FromEndNodes();
+		}
+		this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
+		return firstNode;
+	},
+	/**
+	 * Add class to node's span tag and to .extraClasses.
+	 *
+	 * @param {string} className class name
+	 *
+	 * @since 2.17
+	 */
+	addClass: function(className){
+		return this.toggleClass(className, true);
+	},
+	/**
+	 * Append or prepend a node, or append a child node.
+	 *
+	 * This a convenience function that calls addChildren()
+	 *
+	 * @param {NodeData} node node definition
+	 * @param {string} [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child')
+	 * @returns {FancytreeNode} new node
+	 */
+	addNode: function(node, mode){
+		if(mode === undefined || mode === "over"){
+			mode = "child";
+		}
+		switch(mode){
+		case "after":
+			return this.getParent().addChildren(node, this.getNextSibling());
+		case "before":
+			return this.getParent().addChildren(node, this);
+		case "firstChild":
+			// Insert before the first child if any
+			var insertBefore = (this.children ? this.children[0] : null);
+			return this.addChildren(node, insertBefore);
+		case "child":
+		case "over":
+			return this.addChildren(node);
+		}
+		_assert(false, "Invalid mode: " + mode);
+	},
+	/**Add child status nodes that indicate 'More...', etc.
+	 *
+	 * This also maintains the node's `partload` property.
+	 * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes.
+	 * @param {string} [mode='child'] 'child'|firstChild'
+	 * @since 2.15
+	 */
+	addPagingNode: function(node, mode){
+		var i, n;
+
+		mode = mode || "child";
+		if( node === false ) {
+			for(i=this.children.length-1; i >= 0; i--) {
+				n = this.children[i];
+				if( n.statusNodeType === "paging" ) {
+					this.removeChild(n);
+				}
+			}
+			this.partload = false;
+			return;
+		}
+		node = $.extend({
+			title: this.tree.options.strings.moreData,
+			statusNodeType: "paging",
+			icon: false
+		}, node);
+		this.partload = true;
+		return this.addNode(node, mode);
+	},
+	/**
+	 * Append new node after this.
+	 *
+	 * This a convenience function that calls addNode(node, 'after')
+	 *
+	 * @param {NodeData} node node definition
+	 * @returns {FancytreeNode} new node
+	 */
+	appendSibling: function(node){
+		return this.addNode(node, "after");
+	},
+	/**
+	 * Modify existing child nodes.
+	 *
+	 * @param {NodePatch} patch
+	 * @returns {$.Promise}
+	 * @see FancytreeNode#addChildren
+	 */
+	applyPatch: function(patch) {
+		// patch [key, null] means 'remove'
+		if(patch === null){
+			this.remove();
+			return _getResolvedPromise(this);
+		}
+		// TODO: make sure that root node is not collapsed or modified
+		// copy (most) attributes to node.ATTR or node.data.ATTR
+		var name, promise, v,
+			IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global
+
+		for(name in patch){
+			v = patch[name];
+			if( !IGNORE_MAP[name] && !$.isFunction(v)){
+				if(NODE_ATTR_MAP[name]){
+					this[name] = v;
+				}else{
+					this.data[name] = v;
+				}
+			}
+		}
+		// Remove and/or create children
+		if(patch.hasOwnProperty("children")){
+			this.removeChildren();
+			if(patch.children){ // only if not null and not empty list
+				// TODO: addChildren instead?
+				this._setChildren(patch.children);
+			}
+			// TODO: how can we APPEND or INSERT child nodes?
+		}
+		if(this.isVisible()){
+			this.renderTitle();
+			this.renderStatus();
+		}
+		// Expand collapse (final step, since this may be async)
+		if(patch.hasOwnProperty("expanded")){
+			promise = this.setExpanded(patch.expanded);
+		}else{
+			promise = _getResolvedPromise(this);
+		}
+		return promise;
+	},
+	/** Collapse all sibling nodes.
+	 * @returns {$.Promise}
+	 */
+	collapseSiblings: function() {
+		return this.tree._callHook("nodeCollapseSiblings", this);
+	},
+	/** Copy this node as sibling or child of `node`.
+	 *
+	 * @param {FancytreeNode} node source node
+	 * @param {string} [mode=child] 'before' | 'after' | 'child'
+	 * @param {Function} [map] callback function(NodeData) that could modify the new node
+	 * @returns {FancytreeNode} new
+	 */
+	copyTo: function(node, mode, map) {
+		return node.addNode(this.toDict(true, map), mode);
+	},
+	/** Count direct and indirect children.
+	 *
+	 * @param {boolean} [deep=true] pass 'false' to only count direct children
+	 * @returns {int} number of child nodes
+	 */
+	countChildren: function(deep) {
+		var cl = this.children, i, l, n;
+		if( !cl ){
+			return 0;
+		}
+		n = cl.length;
+		if(deep !== false){
+			for(i=0, l=n; i<l; i++){
+				n += cl[i].countChildren();
+			}
+		}
+		return n;
+	},
+	// TODO: deactivate()
+	/** Write to browser console if debugLevel >= 2 (prepending node info)
+	 *
+	 * @param {*} msg string or object or array of such
+	 */
+	debug: function(msg){
+		if( this.tree.options.debugLevel >= 2 ) {
+			Array.prototype.unshift.call(arguments, this.toString());
+			consoleApply("log", arguments);
+		}
+	},
+	/** Deprecated.
+	 * @deprecated since 2014-02-16. Use resetLazy() instead.
+	 */
+	discard: function(){
+		this.warn("FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead.");
+		return this.resetLazy();
+	},
+	/** Remove DOM elements for all descendents. May be called on .collapse event
+	 * to keep the DOM small.
+	 * @param {boolean} [includeSelf=false]
+	 */
+	discardMarkup: function(includeSelf){
+		var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup";
+		this.tree._callHook(fn, this);
+	},
+	/**Find all nodes that match condition (excluding self).
+	 *
+	 * @param {string | function(node)} match title string to search for, or a
+	 *     callback function that returns `true` if a node is matched.
+	 * @returns {FancytreeNode[]} array of nodes (may be empty)
+	 */
+	findAll: function(match) {
+		match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
+		var res = [];
+		this.visit(function(n){
+			if(match(n)){
+				res.push(n);
+			}
+		});
+		return res;
+	},
+	/**Find first node that matches condition (excluding self).
+	 *
+	 * @param {string | function(node)} match title string to search for, or a
+	 *     callback function that returns `true` if a node is matched.
+	 * @returns {FancytreeNode} matching node or null
+	 * @see FancytreeNode#findAll
+	 */
+	findFirst: function(match) {
+		match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
+		var res = null;
+		this.visit(function(n){
+			if(match(n)){
+				res = n;
+				return false;
+			}
+		});
+		return res;
+	},
+	/* Apply selection state (internal use only) */
+	_changeSelectStatusAttrs: function(state) {
+		var changed = false,
+			opts = this.tree.options,
+			unselectable = FT.evalOption("unselectable", this, this, opts, false),
+			unselectableStatus = FT.evalOption("unselectableStatus", this, this, opts, undefined);
+
+		if( unselectable && unselectableStatus != null ) {
+			state = unselectableStatus;
+		}
+		switch(state){
+		case false:
+			changed = ( this.selected || this.partsel );
+			this.selected = false;
+			this.partsel = false;
+			break;
+		case true:
+			changed = ( !this.selected || !this.partsel );
+			this.selected = true;
+			this.partsel = true;
+			break;
+		case undefined:
+			changed = ( this.selected || !this.partsel );
+			this.selected = false;
+			this.partsel = true;
+			break;
+		default:
+			_assert(false, "invalid state: " + state);
+		}
+		// this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed);
+		if( changed ){
+			this.renderStatus();
+		}
+		return changed;
+	},
+	/**
+	 * Fix selection status, after this node was (de)selected in multi-hier mode.
+	 * This includes (de)selecting all children.
+	 */
+	fixSelection3AfterClick: function(callOpts) {
+		var flag = this.isSelected();
+
+//		this.debug("fixSelection3AfterClick()");
+
+		this.visit(function(node){
+			node._changeSelectStatusAttrs(flag);
+		});
+		this.fixSelection3FromEndNodes(callOpts);
+	},
+	/**
+	 * Fix selection status for multi-hier mode.
+	 * Only end-nodes are considered to update the descendants branch and parents.
+	 * Should be called after this node has loaded new children or after
+	 * children have been modified using the API.
+	 */
+	fixSelection3FromEndNodes: function(callOpts) {
+		var opts = this.tree.options;
+
+//		this.debug("fixSelection3FromEndNodes()");
+		_assert(opts.selectMode === 3, "expected selectMode 3");
+
+		// Visit all end nodes and adjust their parent's `selected` and `partsel`
+		// attributes. Return selection state true, false, or undefined.
+		function _walk(node){
+			var i, l, child, s, state, allSelected, someSelected, unselIgnore, unselState,
+				children = node.children;
+
+			if( children && children.length ){
+				// check all children recursively
+				allSelected = true;
+				someSelected = false;
+
+				for( i=0, l=children.length; i<l; i++ ){
+					child = children[i];
+					// the selection state of a node is not relevant; we need the end-nodes
+					s = _walk(child);
+					// if( !child.unselectableIgnore ) {
+					unselIgnore = FT.evalOption("unselectableIgnore", child, child, opts, false);
+					if( !unselIgnore ) {
+						if( s !== false ) {
+							someSelected = true;
+						}
+						if( s !== true ) {
+							allSelected = false;
+						}
+					}
+				}
+				state = allSelected ? true : (someSelected ? undefined : false);
+			}else{
+				// This is an end-node: simply report the status
+				unselState = FT.evalOption("unselectableStatus", node, node, opts, undefined);
+				state = ( unselState == null ) ? !!node.selected : !!unselState;
+			}
+			node._changeSelectStatusAttrs(state);
+			return state;
+		}
+		_walk(this);
+
+		// Update parent's state
+		this.visitParents(function(node){
+			var i, l, child, state, unselIgnore, unselState,
+				children = node.children,
+				allSelected = true,
+				someSelected = false;
+
+			for( i=0, l=children.length; i<l; i++ ){
+				child = children[i];
+				unselIgnore = FT.evalOption("unselectableIgnore", child, child, opts, false);
+				if( !unselIgnore ) {
+					unselState = FT.evalOption("unselectableStatus", child,  child, opts, undefined);
+					state = ( unselState == null ) ? !!child.selected : !!unselState;
+					// When fixing the parents, we trust the sibling status (i.e.
+					// we don't recurse)
+					if( state || child.partsel ) {
+						someSelected = true;
+					}
+					if( !state ) {
+						allSelected = false;
+					}
+				}
+			}
+			state = allSelected ? true : (someSelected ? undefined : false);
+			node._changeSelectStatusAttrs(state);
+		});
+	},
+	// TODO: focus()
+	/**
+	 * Update node data. If dict contains 'children', then also replace
+	 * the hole sub tree.
+	 * @param {NodeData} dict
+	 *
+	 * @see FancytreeNode#addChildren
+	 * @see FancytreeNode#applyPatch
+	 */
+	fromDict: function(dict) {
+		// copy all other attributes to this.data.xxx
+		for(var name in dict){
+			if(NODE_ATTR_MAP[name]){
+				// node.NAME = dict.NAME
+				this[name] = dict[name];
+			}else if(name === "data"){
+				// node.data += dict.data
+				$.extend(this.data, dict.data);
+			}else if(!$.isFunction(dict[name]) && !NONE_NODE_DATA_MAP[name]){
+				// node.data.NAME = dict.NAME
+				this.data[name] = dict[name];
+			}
+		}
+		if(dict.children){
+			// recursively set children and render
+			this.removeChildren();
+			this.addChildren(dict.children);
+		}
+		this.renderTitle();
+/*
+		var children = dict.children;
+		if(children === undefined){
+			this.data = $.extend(this.data, dict);
+			this.render();
+			return;
+		}
+		dict = $.extend({}, dict);
+		dict.children = undefined;
+		this.data = $.extend(this.data, dict);
+		this.removeChildren();
+		this.addChild(children);
+*/
+	},
+	/** Return the list of child nodes (undefined for unexpanded lazy nodes).
+	 * @returns {FancytreeNode[] | undefined}
+	 */
+	getChildren: function() {
+		if(this.hasChildren() === undefined){ // TODO: only required for lazy nodes?
+			return undefined; // Lazy node: unloaded, currently loading, or load error
+		}
+		return this.children;
+	},
+	/** Return the first child node or null.
+	 * @returns {FancytreeNode | null}
+	 */
+	getFirstChild: function() {
+		return this.children ? this.children[0] : null;
+	},
+	/** Return the 0-based child index.
+	 * @returns {int}
+	 */
+	getIndex: function() {
+//		return this.parent.children.indexOf(this);
+		return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7
+	},
+	/** Return the hierarchical child index (1-based, e.g. '3.2.4').
+	 * @param {string} [separator="."]
+	 * @param {int} [digits=1]
+	 * @returns {string}
+	 */
+	getIndexHier: function(separator, digits) {
+		separator = separator || ".";
+		var s,
+			res = [];
+		$.each(this.getParentList(false, true), function(i, o){
+			s = "" + (o.getIndex() + 1);
+			if( digits ){
+				// prepend leading zeroes
+				s = ("0000000" + s).substr(-digits);
+			}
+			res.push(s);
+		});
+		return res.join(separator);
+	},
+	/** Return the parent keys separated by options.keyPathSeparator, e.g. "id_1/id_17/id_32".
+	 * @param {boolean} [excludeSelf=false]
+	 * @returns {string}
+	 */
+	getKeyPath: function(excludeSelf) {
+		var path = [],
+			sep = this.tree.options.keyPathSeparator;
+		this.visitParents(function(n){
+			if(n.parent){
+				path.unshift(n.key);
+			}
+		}, !excludeSelf);
+		return sep + path.join(sep);
+	},
+	/** Return the last child of this node or null.
+	 * @returns {FancytreeNode | null}
+	 */
+	getLastChild: function() {
+		return this.children ? this.children[this.children.length - 1] : null;
+	},
+	/** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... .
+	 * @returns {int}
+	 */
+	getLevel: function() {
+		var level = 0,
+			dtn = this.parent;
+		while( dtn ) {
+			level++;
+			dtn = dtn.parent;
+		}
+		return level;
+	},
+	/** Return the successor node (under the same parent) or null.
+	 * @returns {FancytreeNode | null}
+	 */
+	getNextSibling: function() {
+		// TODO: use indexOf, if available: (not in IE6)
+		if( this.parent ){
+			var i, l,
+				ac = this.parent.children;
+
+			for(i=0, l=ac.length-1; i<l; i++){ // up to length-2, so next(last) = null
+				if( ac[i] === this ){
+					return ac[i+1];
+				}
+			}
+		}
+		return null;
+	},
+	/** Return the parent node (null for the system root node).
+	 * @returns {FancytreeNode | null}
+	 */
+	getParent: function() {
+		// TODO: return null for top-level nodes?
+		return this.parent;
+	},
+	/** Return an array of all parent nodes (top-down).
+	 * @param {boolean} [includeRoot=false] Include the invisible system root node.
+	 * @param {boolean} [includeSelf=false] Include the node itself.
+	 * @returns {FancytreeNode[]}
+	 */
+	getParentList: function(includeRoot, includeSelf) {
+		var l = [],
+			dtn = includeSelf ? this : this.parent;
+		while( dtn ) {
+			if( includeRoot || dtn.parent ){
+				l.unshift(dtn);
+			}
+			dtn = dtn.parent;
+		}
+		return l;
+	},
+	/** Return the predecessor node (under the same parent) or null.
+	 * @returns {FancytreeNode | null}
+	 */
+	getPrevSibling: function() {
+		if( this.parent ){
+			var i, l,
+				ac = this.parent.children;
+
+			for(i=1, l=ac.length; i<l; i++){ // start with 1, so prev(first) = null
+				if( ac[i] === this ){
+					return ac[i-1];
+				}
+			}
+		}
+		return null;
+	},
+	/**
+	 * Return an array of selected descendant nodes.
+	 * @param {boolean} [stopOnParents=false] only return the topmost selected
+	 *     node (useful with selectMode 3)
+	 * @returns {FancytreeNode[]}
+	 */
+	getSelectedNodes: function(stopOnParents) {
+		var nodeList = [];
+		this.visit(function(node){
+			if( node.selected ) {
+				nodeList.push(node);
+				if( stopOnParents === true ){
+					return "skip"; // stop processing this branch
+				}
+			}
+		});
+		return nodeList;
+	},
+	/** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded).
+	 * @returns {boolean | undefined}
+	 */
+	hasChildren: function() {
+		if(this.lazy){
+			if(this.children == null ){
+				// null or undefined: Not yet loaded
+				return undefined;
+			}else if(this.children.length === 0){
+				// Loaded, but response was empty
+				return false;
+			}else if(this.children.length === 1 && this.children[0].isStatusNode() ){
+				// Currently loading or load error
+				return undefined;
+			}
+			return true;
+		}
+		return !!( this.children && this.children.length );
+	},
+	/** Return true if node has keyboard focus.
+	 * @returns {boolean}
+	 */
+	hasFocus: function() {
+		return (this.tree.hasFocus() && this.tree.focusNode === this);
+	},
+	/** Write to browser console if debugLevel >= 1 (prepending node info)
+	 *
+	 * @param {*} msg string or object or array of such
+	 */
+	info: function(msg){
+		if( this.tree.options.debugLevel >= 1 ) {
+			Array.prototype.unshift.call(arguments, this.toString());
+			consoleApply("info", arguments);
+		}
+	},
+	/** Return true if node is active (see also FancytreeNode#isSelected).
+	 * @returns {boolean}
+	 */
+	isActive: function() {
+		return (this.tree.activeNode === this);
+	},
+	/** Return true if node is a direct child of otherNode.
+	 * @param {FancytreeNode} otherNode
+	 * @returns {boolean}
+	 */
+	isChildOf: function(otherNode) {
+		return (this.parent && this.parent === otherNode);
+	},
+	/** Return true, if node is a direct or indirect sub node of otherNode.
+	 * @param {FancytreeNode} otherNode
+	 * @returns {boolean}
+	 */
+	isDescendantOf: function(otherNode) {
+		if(!otherNode || otherNode.tree !== this.tree){
+			return false;
+		}
+		var p = this.parent;
+		while( p ) {
+			if( p === otherNode ){
+				return true;
+			}
+			if( p === p.parent ) { $.error("Recursive parent link: " + p); }
+			p = p.parent;
+		}
+		return false;
+	},
+	/** Return true if node is expanded.
+	 * @returns {boolean}
+	 */
+	isExpanded: function() {
+		return !!this.expanded;
+	},
+	/** Return true if node is the first node of its parent's children.
+	 * @returns {boolean}
+	 */
+	isFirstSibling: function() {
+		var p = this.parent;
+		return !p || p.children[0] === this;
+	},
+	/** Return true if node is a folder, i.e. has the node.folder attribute set.
+	 * @returns {boolean}
+	 */
+	isFolder: function() {
+		return !!this.folder;
+	},
+	/** Return true if node is the last node of its parent's children.
+	 * @returns {boolean}
+	 */
+	isLastSibling: function() {
+		var p = this.parent;
+		return !p || p.children[p.children.length-1] === this;
+	},
+	/** Return true if node is lazy (even if data was already loaded)
+	 * @returns {boolean}
+	 */
+	isLazy: function() {
+		return !!this.lazy;
+	},
+	/** Return true if node is lazy and loaded. For non-lazy nodes always return true.
+	 * @returns {boolean}
+	 */
+	isLoaded: function() {
+		return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node
+	},
+	/** Return true if children are currently beeing loaded, i.e. a Ajax request is pending.
+	 * @returns {boolean}
+	 */
+	isLoading: function() {
+		return !!this._isLoading;
+	},
+	/*
+	 * @deprecated since v2.4.0:  Use isRootNode() instead
+	 */
+	isRoot: function() {
+		return this.isRootNode();
+	},
+	/** Return true if node is partially selected (tri-state).
+	 * @returns {boolean}
+	 * @since 2.23
+	 */
+	isPartsel: function() {
+		return !this.selected && !!this.partsel;
+	},
+	/** (experimental) Return true if this is partially loaded.
+	 * @returns {boolean}
+	 * @since 2.15
+	 */
+	isPartload: function() {
+		return !!this.partload;
+	},
+	/** Return true if this is the (invisible) system root node.
+	 * @returns {boolean}
+	 * @since 2.4
+	 */
+	isRootNode: function() {
+		return (this.tree.rootNode === this);
+	},
+	/** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive).
+	 * @returns {boolean}
+	 */
+	isSelected: function() {
+		return !!this.selected;
+	},
+	/** Return true if this node is a temporarily generated system node like
+	 * 'loading', 'paging', or 'error' (node.statusNodeType contains the type).
+	 * @returns {boolean}
+	 */
+	isStatusNode: function() {
+		return !!this.statusNodeType;
+	},
+	/** Return true if this node is a status node of type 'paging'.
+	 * @returns {boolean}
+	 * @since 2.15
+	 */
+	isPagingNode: function() {
+		return this.statusNodeType === "paging";
+	},
+	/** Return true if this a top level node, i.e. a direct child of the (invisible) system root node.
+	 * @returns {boolean}
+	 * @since 2.4
+	 */
+	isTopLevel: function() {
+		return (this.tree.rootNode === this.parent);
+	},
+	/** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false.
+	 * @returns {boolean}
+	 */
+	isUndefined: function() {
+		return this.hasChildren() === undefined; // also checks if the only child is a status node
+	},
+	/** Return true if all parent nodes are expanded. Note: this does not check
+	 * whether the node is scrolled into the visible part of the screen.
+	 * @returns {boolean}
+	 */
+	isVisible: function() {
+		var i, l,
+			parents = this.getParentList(false, false);
+
+		for(i=0, l=parents.length; i<l; i++){
+			if( ! parents[i].expanded ){ return false; }
+		}
+		return true;
+	},
+	/** Deprecated.
+	 * @deprecated since 2014-02-16: use load() instead.
+	 */
+	lazyLoad: function(discard) {
+		this.warn("FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead.");
+		return this.load(discard);
+	},
+	/**
+	 * Load all children of a lazy node if neccessary. The <i>expanded</i> state is maintained.
+	 * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded.
+	 * @returns {$.Promise}
+	 */
+	load: function(forceReload) {
+		var res, source,
+			that = this,
+			wasExpanded = this.isExpanded();
+
+		_assert( this.isLazy(), "load() requires a lazy node" );
+		// _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" );
+		if( !forceReload && !this.isUndefined() ) {
+			return _getResolvedPromise(this);
+		}
+		if( this.isLoaded() ){
+			this.resetLazy(); // also collapses
+		}
+		// This method is also called by setExpanded() and loadKeyPath(), so we
+		// have to avoid recursion.
+		source = this.tree._triggerNodeEvent("lazyLoad", this);
+		if( source === false ) { // #69
+			return _getResolvedPromise(this);
+		}
+		_assert(typeof source !== "boolean", "lazyLoad event must return source in data.result");
+		res = this.tree._callHook("nodeLoadChildren", this, source);
+		if( wasExpanded ) {
+			this.expanded = true;
+			res.always(function(){
+				that.render();
+			});
+		} else {
+			res.always(function(){
+				that.renderStatus();  // fix expander icon to 'loaded'
+			});
+		}
+		return res;
+	},
+	/** Expand all parents and optionally scroll into visible area as neccessary.
+	 * Promise is resolved, when lazy loading and animations are done.
+	 * @param {object} [opts] passed to `setExpanded()`.
+	 *     Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
+	 * @returns {$.Promise}
+	 */
+	makeVisible: function(opts) {
+		var i,
+			that = this,
+			deferreds = [],
+			dfd = new $.Deferred(),
+			parents = this.getParentList(false, false),
+			len = parents.length,
+			effects = !(opts && opts.noAnimation === true),
+			scroll = !(opts && opts.scrollIntoView === false);
+
+		// Expand bottom-up, so only the top node is animated
+		for(i = len - 1; i >= 0; i--){
+			// that.debug("pushexpand" + parents[i]);
+			deferreds.push(parents[i].setExpanded(true, opts));
+		}
+		$.when.apply($, deferreds).done(function(){
+			// All expands have finished
+			// that.debug("expand DONE", scroll);
+			if( scroll ){
+				that.scrollIntoView(effects).done(function(){
+					// that.debug("scroll DONE");
+					dfd.resolve();
+				});
+			} else {
+				dfd.resolve();
+			}
+		});
+		return dfd.promise();
+	},
+	/** Move this node to targetNode.
+	 *  @param {FancytreeNode} targetNode
+	 *  @param {string} mode <pre>
+	 *      'child': append this node as last child of targetNode.
+	 *               This is the default. To be compatble with the D'n'd
+	 *               hitMode, we also accept 'over'.
+	 *      'firstChild': add this node as first child of targetNode.
+	 *      'before': add this node as sibling before targetNode.
+	 *      'after': add this node as sibling after targetNode.</pre>
+	 *  @param {function} [map] optional callback(FancytreeNode) to allow modifcations
+	 */
+	moveTo: function(targetNode, mode, map) {
+		if(mode === undefined || mode === "over"){
+			mode = "child";
+		} else if ( mode === "firstChild" ) {
+			if( targetNode.children && targetNode.children.length ) {
+				mode = "before";
+				targetNode = targetNode.children[0];
+			} else {
+				mode = "child";
+			}
+		}
+		var pos,
+			prevParent = this.parent,
+			targetParent = (mode === "child") ? targetNode : targetNode.parent;
+
+		if(this === targetNode){
+			return;
+		}else if( !this.parent  ){
+			$.error("Cannot move system root");
+		}else if( targetParent.isDescendantOf(this) ){
+			$.error("Cannot move a node to its own descendant");
+		}
+		if( targetParent !== prevParent ) {
+			prevParent.triggerModifyChild("remove", this);
+		}
+		// Unlink this node from current parent
+		if( this.parent.children.length === 1 ) {
+			if( this.parent === targetParent ){
+				return; // #258
+			}
+			this.parent.children = this.parent.lazy ? [] : null;
+			this.parent.expanded = false;
+		} else {
+			pos = $.inArray(this, this.parent.children);
+			_assert(pos >= 0, "invalid source parent");
+			this.parent.children.splice(pos, 1);
+		}
+		// Remove from source DOM parent
+//		if(this.parent.ul){
+//			this.parent.ul.removeChild(this.li);
+//		}
+
+		// Insert this node to target parent's child list
+		this.parent = targetParent;
+		if( targetParent.hasChildren() ) {
+			switch(mode) {
+			case "child":
+				// Append to existing target children
+				targetParent.children.push(this);
+				break;
+			case "before":
+				// Insert this node before target node
+				pos = $.inArray(targetNode, targetParent.children);
+				_assert(pos >= 0, "invalid target parent");
+				targetParent.children.splice(pos, 0, this);
+				break;
+			case "after":
+				// Insert this node after target node
+				pos = $.inArray(targetNode, targetParent.children);
+				_assert(pos >= 0, "invalid target parent");
+				targetParent.children.splice(pos+1, 0, this);
+				break;
+			default:
+				$.error("Invalid mode " + mode);
+			}
+		} else {
+			targetParent.children = [ this ];
+		}
+		// Parent has no <ul> tag yet:
+//		if( !targetParent.ul ) {
+//			// This is the parent's first child: create UL tag
+//			// (Hidden, because it will be
+//			targetParent.ul = document.createElement("ul");
+//			targetParent.ul.style.display = "none";
+//			targetParent.li.appendChild(targetParent.ul);
+//		}
+//		// Issue 319: Add to target DOM parent (only if node was already rendered(expanded))
+//		if(this.li){
+//			targetParent.ul.appendChild(this.li);
+//		}^
+
+		// Let caller modify the nodes
+		if( map ){
+			targetNode.visit(map, true);
+		}
+		if( targetParent === prevParent ) {
+			targetParent.triggerModifyChild("move", this);
+		} else {
+			// prevParent.triggerModifyChild("remove", this);
+			targetParent.triggerModifyChild("add", this);
+		}
+		// Handle cross-tree moves
+		if( this.tree !== targetNode.tree ) {
+			// Fix node.tree for all source nodes
+//			_assert(false, "Cross-tree move is not yet implemented.");
+			this.warn("Cross-tree moveTo is experimantal!");
+			this.visit(function(n){
+				// TODO: fix selection state and activation, ...
+				n.tree = targetNode.tree;
+			}, true);
+		}
+
+		// A collaposed node won't re-render children, so we have to remove it manually
+		// if( !targetParent.expanded ){
+		//   prevParent.ul.removeChild(this.li);
+		// }
+
+		// Update HTML markup
+		if( !prevParent.isDescendantOf(targetParent)) {
+			prevParent.render();
+		}
+		if( !targetParent.isDescendantOf(prevParent) && targetParent !== prevParent) {
+			targetParent.render();
+		}
+		// TODO: fix selection state
+		// TODO: fix active state
+
+/*
+		var tree = this.tree;
+		var opts = tree.options;
+		var pers = tree.persistence;
+
+
+		// Always expand, if it's below minExpandLevel
+//		tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel());
+		if ( opts.minExpandLevel >= ftnode.getLevel() ) {
+//			tree.logDebug ("Force expand for %o", ftnode);
+			this.bExpanded = true;
+		}
+
+		// In multi-hier mode, update the parents selection state
+		// DT issue #82: only if not initializing, because the children may not exist yet
+//		if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing )
+//			ftnode._fixSelectionState();
+
+		// In multi-hier mode, update the parents selection state
+		if( ftnode.bSelected && opts.selectMode==3 ) {
+			var p = this;
+			while( p ) {
+				if( !p.hasSubSel )
+					p._setSubSel(true);
+				p = p.parent;
+			}
+		}
+		// render this node and the new child
+		if ( tree.bEnableUpdate )
+			this.render();
+
+		return ftnode;
+
+*/
+	},
+	/** Set focus relative to this node and optionally activate.
+	 *
+	 * @param {number} where The keyCode that would normally trigger this move,
+	 *		e.g. `$.ui.keyCode.LEFT` would collapse the node if it
+	 *      is expanded or move to the parent oterwise.
+	 * @param {boolean} [activate=true]
+	 * @returns {$.Promise}
+	 */
+	navigate: function(where, activate) {
+		var i, parents, res,
+			handled = true,
+			KC = $.ui.keyCode,
+			sib = null;
+
+		// Navigate to node
+		function _goto(n){
+			if( n ){
+				// setFocus/setActive will scroll later (if autoScroll is specified)
+				try { n.makeVisible({scrollIntoView: false}); } catch(e) {} // #272
+				// Node may still be hidden by a filter
+				if( ! $(n.span).is(":visible") ) {
+					n.debug("Navigate: skipping hidden node");
+					n.navigate(where, activate);
+					return;
+				}
+				return activate === false ? n.setFocus() : n.setActive();
+			}
+		}
+
+		switch( where ) {
+			case KC.BACKSPACE:
+				if( this.parent && this.parent.parent ) {
+					res = _goto(this.parent);
+				}
+				break;
+			case KC.HOME:
+				this.tree.visit(function(n){  // goto first visible node
+					if( $(n.span).is(":visible") ) {
+						res = _goto(n);
+						return false;
+					}
+				});
+				break;
+			case KC.END:
+				this.tree.visit(function(n){  // goto last visible node
+					if( $(n.span).is(":visible") ) {
+						res = n;
+					}
+				});
+				if( res ) {
+					res = _goto(res);
+				}
+				break;
+			case KC.LEFT:
+				if( this.expanded ) {
+					this.setExpanded(false);
+					res = _goto(this);
+				} else if( this.parent && this.parent.parent ) {
+					res = _goto(this.parent);
+				}
+				break;
+			case KC.RIGHT:
+				if( !this.expanded && (this.children || this.lazy) ) {
+					this.setExpanded();
+					res = _goto(this);
+				} else if( this.children && this.children.length ) {
+					res = _goto(this.children[0]);
+				}
+				break;
+			case KC.UP:
+				sib = this.getPrevSibling();
+				// #359: skip hidden sibling nodes, preventing a _goto() recursion
+				while( sib && !$(sib.span).is(":visible") ) {
+					sib = sib.getPrevSibling();
+				}
+				while( sib && sib.expanded && sib.children && sib.children.length ) {
+					sib = sib.children[sib.children.length - 1];
+				}
+				if( !sib && this.parent && this.parent.parent ){
+					sib = this.parent;
+				}
+				res = _goto(sib);
+				break;
+			case KC.DOWN:
+				if( this.expanded && this.children && this.children.length ) {
+					sib = this.children[0];
+				} else {
+					parents = this.getParentList(false, true);
+					for(i=parents.length-1; i>=0; i--) {
+						sib = parents[i].getNextSibling();
+						// #359: skip hidden sibling nodes, preventing a _goto() recursion
+						while( sib && !$(sib.span).is(":visible") ) {
+							sib = sib.getNextSibling();
+						}
+						if( sib ){ break; }
+					}
+				}
+				res = _goto(sib);
+				break;
+			default:
+				handled = false;
+		}
+		return res || _getResolvedPromise();
+	},
+	/**
+	 * Remove this node (not allowed for system root).
+	 */
+	remove: function() {
+		return this.parent.removeChild(this);
+	},
+	/**
+	 * Remove childNode from list of direct children.
+	 * @param {FancytreeNode} childNode
+	 */
+	removeChild: function(childNode) {
+		return this.tree._callHook("nodeRemoveChild", this, childNode);
+	},
+	/**
+	 * Remove all child nodes and descendents. This converts the node into a leaf.<br>
+	 * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy()
+	 * in order to trigger lazyLoad on next expand.
+	 */
+	removeChildren: function() {
+		return this.tree._callHook("nodeRemoveChildren", this);
+	},
+	/**
+	 * Remove class from node's span tag and .extraClasses.
+	 *
+	 * @param {string} className class name
+	 *
+	 * @since 2.17
+	 */
+	removeClass: function(className){
+		return this.toggleClass(className, false);
+	},
+	/**
+	 * This method renders and updates all HTML markup that is required
+	 * to display this node in its current state.<br>
+	 * Note:
+	 * <ul>
+	 * <li>It should only be neccessary to call this method after the node object
+	 *     was modified by direct access to its properties, because the common
+	 *     API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...)
+	 *     already handle this.
+	 * <li> {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus}
+	 *     are implied. If changes are more local, calling only renderTitle() or
+	 *     renderStatus() may be sufficient and faster.
+	 * </ul>
+	 *
+	 * @param {boolean} [force=false] re-render, even if html markup was already created
+	 * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed
+	 */
+	render: function(force, deep) {
+		return this.tree._callHook("nodeRender", this, force, deep);
+	},
+	/** Create HTML markup for the node's outer &lt;span> (expander, checkbox, icon, and title).
+	 * Implies {@link FancytreeNode#renderStatus}.
+	 * @see Fancytree_Hooks#nodeRenderTitle
+	 */
+	renderTitle: function() {
+		return this.tree._callHook("nodeRenderTitle", this);
+	},
+	/** Update element's CSS classes according to node state.
+	 * @see Fancytree_Hooks#nodeRenderStatus
+	 */
+	renderStatus: function() {
+		return this.tree._callHook("nodeRenderStatus", this);
+	},
+	/**
+	 * (experimental) Replace this node with `source`.
+	 * (Currently only available for paging nodes.)
+	 * @param {NodeData[]} source List of child node definitions
+	 * @since 2.15
+	 */
+	replaceWith: function(source) {
+		var res,
+			parent = this.parent,
+			pos = $.inArray(this, parent.children),
+			that = this;
+
+		_assert( this.isPagingNode(), "replaceWith() currently requires a paging status node" );
+
+		res = this.tree._callHook("nodeLoadChildren", this, source);
+		res.done(function(data){
+			// New nodes are currently children of `this`.
+			var children = that.children;
+			// Prepend newly loaded child nodes to `this`
+			// Move new children after self
+			for( i=0; i<children.length; i++ ) {
+				children[i].parent = parent;
+			}
+			parent.children.splice.apply(parent.children, [pos + 1, 0].concat(children));
+
+			// Remove self
+			that.children = null;
+			that.remove();
+			// Redraw new nodes
+			parent.render();
+			// TODO: set node.partload = false if this was tha last paging node?
+			// parent.addPagingNode(false);
+		}).fail(function(){
+			that.setExpanded();
+		});
+		return res;
+		// $.error("Not implemented: replaceWith()");
+	},
+	/**
+	 * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
+	 * event is triggered on next expand.
+	 */
+	resetLazy: function() {
+		this.removeChildren();
+		this.expanded = false;
+		this.lazy = true;
+		this.children = undefined;
+		this.renderStatus();
+	},
+	/** Schedule activity for delayed execution (cancel any pending request).
+	 *  scheduleAction('cancel') will only cancel a pending request (if any).
+	 * @param {string} mode
+	 * @param {number} ms
+	 */
+	scheduleAction: function(mode, ms) {
+		if( this.tree.timer ) {
+			clearTimeout(this.tree.timer);
+//            this.tree.debug("clearTimeout(%o)", this.tree.timer);
+		}
+		this.tree.timer = null;
+		var self = this; // required for closures
+		switch (mode) {
+		case "cancel":
+			// Simply made sure that timer was cleared
+			break;
+		case "expand":
+			this.tree.timer = setTimeout(function(){
+				self.tree.debug("setTimeout: trigger expand");
+				self.setExpanded(true);
+			}, ms);
+			break;
+		case "activate":
+			this.tree.timer = setTimeout(function(){
+				self.tree.debug("setTimeout: trigger activate");
+				self.setActive(true);
+			}, ms);
+			break;
+		default:
+			$.error("Invalid mode " + mode);
+		}
+//        this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer);
+	},
+	/**
+	 *
+	 * @param {boolean | PlainObject} [effects=false] animation options.
+	 * @param {object} [options=null] {topNode: null, effects: ..., parent: ...} this node will remain visible in
+	 *     any case, even if `this` is outside the scroll pane.
+	 * @returns {$.Promise}
+	 */
+	scrollIntoView: function(effects, options) {
+		if( options !== undefined && _isNode(options) ) {
+			this.warn("scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead.");
+			options = {topNode: options};
+		}
+		// this.$scrollParent = (this.options.scrollParent === "auto") ? $ul.scrollParent() : $(this.options.scrollParent);
+		// this.$scrollParent = this.$scrollParent.length ? this.$scrollParent || this.$container;
+
+		var topNodeY, nodeY, horzScrollbarHeight, containerOffsetTop,
+			opts = $.extend({
+				effects: (effects === true) ? {duration: 200, queue: false} : effects,
+				scrollOfs: this.tree.options.scrollOfs,
+				scrollParent: this.tree.options.scrollParent || this.tree.$container,
+				topNode: null
+			}, options),
+			dfd = new $.Deferred(),
+			that = this,
+			nodeHeight = $(this.span).height(),
+			$container = $(opts.scrollParent),
+			topOfs = opts.scrollOfs.top || 0,
+			bottomOfs = opts.scrollOfs.bottom || 0,
+			containerHeight = $container.height(),// - topOfs - bottomOfs,
+			scrollTop = $container.scrollTop(),
+			$animateTarget = $container,
+			isParentWindow = $container[0] === window,
+			topNode = opts.topNode || null,
+			newScrollTop = null;
+
+		// this.debug("scrollIntoView(), scrollTop=" + scrollTop, opts.scrollOfs);
+//		_assert($(this.span).is(":visible"), "scrollIntoView node is invisible"); // otherwise we cannot calc offsets
+		if( !$(this.span).is(":visible") ) {
+			// We cannot calc offsets for hidden elements
+			this.warn("scrollIntoView(): node is invisible.");
+			return _getResolvedPromise();
+		}
+		if( isParentWindow ) {
+			nodeY = $(this.span).offset().top;
+			topNodeY = (topNode && topNode.span) ? $(topNode.span).offset().top : 0;
+			$animateTarget = $("html,body");
+
+		} else {
+			_assert($container[0] !== document && $container[0] !== document.body,
+				"scrollParent should be a simple element or `window`, not document or body.");
+
+			containerOffsetTop = $container.offset().top,
+			nodeY = $(this.span).offset().top - containerOffsetTop + scrollTop; // relative to scroll parent
+			topNodeY = topNode ? $(topNode.span).offset().top - containerOffsetTop + scrollTop : 0;
+			horzScrollbarHeight = Math.max(0, ($container.innerHeight() - $container[0].clientHeight));
+			containerHeight -= horzScrollbarHeight;
+		}
+
+		// this.debug("    scrollIntoView(), nodeY=" + nodeY + ", containerHeight=" + containerHeight);
+		if( nodeY < (scrollTop + topOfs) ){
+			// Node is above visible container area
+			newScrollTop = nodeY - topOfs;
+			// this.debug("    scrollIntoView(), UPPER newScrollTop=" + newScrollTop);
+
+		}else if((nodeY + nodeHeight) > (scrollTop + containerHeight - bottomOfs)){
+			newScrollTop = nodeY + nodeHeight - containerHeight + bottomOfs;
+			// this.debug("    scrollIntoView(), LOWER newScrollTop=" + newScrollTop);
+			// If a topNode was passed, make sure that it is never scrolled
+			// outside the upper border
+			if(topNode){
+				_assert(topNode.isRootNode() || $(topNode.span).is(":visible"), "topNode must be visible");
+				if( topNodeY < newScrollTop ){
+					newScrollTop = topNodeY - topOfs;
+					// this.debug("    scrollIntoView(), TOP newScrollTop=" + newScrollTop);
+				}
+			}
+		}
+
+		if(newScrollTop !== null){
+			// this.debug("    scrollIntoView(), SET newScrollTop=" + newScrollTop);
+			if(opts.effects){
+				opts.effects.complete = function(){
+					dfd.resolveWith(that);
+				};
+				$animateTarget.stop(true).animate({
+					scrollTop: newScrollTop
+				}, opts.effects);
+			}else{
+				$animateTarget[0].scrollTop = newScrollTop;
+				dfd.resolveWith(this);
+			}
+		}else{
+			dfd.resolveWith(this);
+		}
+		return dfd.promise();
+	},
+
+	/**Activate this node.
+	 * @param {boolean} [flag=true] pass false to deactivate
+	 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false}
+	 * @returns {$.Promise}
+	 */
+	setActive: function(flag, opts){
+		return this.tree._callHook("nodeSetActive", this, flag, opts);
+	},
+	/**Expand or collapse this node. Promise is resolved, when lazy loading and animations are done.
+	 * @param {boolean} [flag=true] pass false to collapse
+	 * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false}
+	 * @returns {$.Promise}
+	 */
+	setExpanded: function(flag, opts){
+		return this.tree._callHook("nodeSetExpanded", this, flag, opts);
+	},
+	/**Set keyboard focus to this node.
+	 * @param {boolean} [flag=true] pass false to blur
+	 * @see Fancytree#setFocus
+	 */
+	setFocus: function(flag){
+		return this.tree._callHook("nodeSetFocus", this, flag);
+	},
+	/**Select this node, i.e. check the checkbox.
+	 * @param {boolean} [flag=true] pass false to deselect
+	 * @param {object} [opts] additional options. Defaults to {noEvents: false, p
+	 *     propagateDown: null, propagateUp: null, callback: null }
+	 */
+	setSelected: function(flag, opts){
+		return this.tree._callHook("nodeSetSelected", this, flag, opts);
+	},
+	/**Mark a lazy node as 'error', 'loading', 'nodata', or 'ok'.
+	 * @param {string} status 'error'|'empty'|'ok'
+	 * @param {string} [message]
+	 * @param {string} [details]
+	 */
+	setStatus: function(status, message, details){
+		return this.tree._callHook("nodeSetStatus", this, status, message, details);
+	},
+	/**Rename this node.
+	 * @param {string} title
+	 */
+	setTitle: function(title){
+		this.title = title;
+		this.renderTitle();
+		this.triggerModify("rename");
+	},
+	/**Sort child list by title.
+	 * @param {function} [cmp] custom compare function(a, b) that returns -1, 0, or 1 (defaults to sort by title).
+	 * @param {boolean} [deep=false] pass true to sort all descendant nodes
+	 */
+	sortChildren: function(cmp, deep) {
+		var i,l,
+			cl = this.children;
+
+		if( !cl ){
+			return;
+		}
+		cmp = cmp || function(a, b) {
+			var x = a.title.toLowerCase(),
+				y = b.title.toLowerCase();
+			return x === y ? 0 : x > y ? 1 : -1;
+			};
+		cl.sort(cmp);
+		if( deep ){
+			for(i=0, l=cl.length; i<l; i++){
+				if( cl[i].children ){
+					cl[i].sortChildren(cmp, "$norender$");
+				}
+			}
+		}
+		if( deep !== "$norender$" ){
+			this.render();
+		}
+		this.triggerModifyChild("sort");
+	},
+	/** Convert node (or whole branch) into a plain object.
+	 *
+	 * The result is compatible with node.addChildren().
+	 *
+	 * @param {boolean} [recursive=false] include child nodes
+	 * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications
+	 * @returns {NodeData}
+	 */
+	toDict: function(recursive, callback) {
+		var i, l, node,
+			dict = {},
+			self = this;
+
+		$.each(NODE_ATTRS, function(i, a){
+			if(self[a] || self[a] === false){
+				dict[a] = self[a];
+			}
+		});
+		if(!$.isEmptyObject(this.data)){
+			dict.data = $.extend({}, this.data);
+			if($.isEmptyObject(dict.data)){
+				delete dict.data;
+			}
+		}
+		if( callback ){
+			callback(dict, self);
+		}
+		if( recursive ) {
+			if(this.hasChildren()){
+				dict.children = [];
+				for(i=0, l=this.children.length; i<l; i++ ){
+					node = this.children[i];
+					if( !node.isStatusNode() ){
+						dict.children.push(node.toDict(true, callback));
+					}
+				}
+			}else{
+//                dict.children = null;
+			}
+		}
+		return dict;
+	},
+	/**
+	 * Set, clear, or toggle class of node's span tag and .extraClasses.
+	 *
+	 * @param {string} className class name (separate multiple classes by space)
+	 * @param {boolean} [flag] true/false to add/remove class. If omitted, class is toggled.
+	 * @returns {boolean} true if a class was added
+	 *
+	 * @since 2.17
+	 */
+	toggleClass: function(value, flag){
+		var className, hasClass,
+			rnotwhite = ( /\S+/g ),
+			classNames = value.match( rnotwhite ) || [],
+			i = 0,
+			wasAdded = false,
+			statusElem = this[this.tree.statusClassPropName],
+			curClasses = (" " + (this.extraClasses || "") + " ");
+
+		// this.info("toggleClass('" + value + "', " + flag + ")", curClasses);
+		// Modify DOM element directly if it already exists
+		if( statusElem ) {
+			$(statusElem).toggleClass(value, flag);
+		}
+		// Modify node.extraClasses to make this change persistent
+		// Toggle if flag was not passed
+		while ( className = classNames[ i++ ] ) {
+			hasClass = curClasses.indexOf(" " + className + " ") >= 0;
+			flag = (flag === undefined) ? (!hasClass) : !!flag;
+			if ( flag ) {
+				if( !hasClass ) {
+					curClasses += className + " ";
+					wasAdded = true;
+				}
+			} else {
+				while ( curClasses.indexOf( " " + className + " " ) > -1 ) {
+					curClasses = curClasses.replace( " " + className + " ", " " );
+				}
+			}
+		}
+		this.extraClasses = $.trim(curClasses);
+		// this.info("-> toggleClass('" + value + "', " + flag + "): '" + this.extraClasses + "'");
+		return wasAdded;
+	},
+	/** Flip expanded status. */
+	toggleExpanded: function(){
+		return this.tree._callHook("nodeToggleExpanded", this);
+	},
+	/** Flip selection status. */
+	toggleSelected: function(){
+		return this.tree._callHook("nodeToggleSelected", this);
+	},
+	toString: function() {
+		return "<FancytreeNode(#" + this.key + ", '" + this.title + "')>";
+	},
+	/**
+	 * Trigger `modifyChild` event on a parent to signal that a child was modified.
+	 * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
+	 * @param {FancytreeNode} [childNode]
+	 * @param {object} [extra]
+	 */
+	triggerModifyChild: function(operation, childNode, extra){
+		var data,
+			modifyChild = this.tree.options.modifyChild;
+
+		if ( modifyChild ){
+			if( childNode && childNode.parent !== this ) {
+				$.error("childNode " + childNode + " is not a child of " + this);
+			}
+			data = {
+				node: this,
+				tree: this.tree,
+				operation: operation,
+				childNode: childNode || null
+			};
+			if( extra ) {
+				$.extend(data, extra);
+			}
+			modifyChild({type: "modifyChild"}, data);
+		}
+	},
+	/**
+	 * Trigger `modifyChild` event on node.parent(!).
+	 * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
+	 * @param {object} [extra]
+	 */
+	triggerModify: function(operation, extra){
+		this.parent.triggerModifyChild(operation, this, extra);
+	},
+	/** Call fn(node) for all child nodes.<br>
+	 * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
+	 * Return false if iteration was stopped.
+	 *
+	 * @param {function} fn the callback function.
+	 *     Return false to stop iteration, return "skip" to skip this node and
+	 *     its children only.
+	 * @param {boolean} [includeSelf=false]
+	 * @returns {boolean}
+	 */
+	visit: function(fn, includeSelf) {
+		var i, l,
+			res = true,
+			children = this.children;
+
+		if( includeSelf === true ) {
+			res = fn(this);
+			if( res === false || res === "skip" ){
+				return res;
+			}
+		}
+		if(children){
+			for(i=0, l=children.length; i<l; i++){
+				res = children[i].visit(fn, true);
+				if( res === false ){
+					break;
+				}
+			}
+		}
+		return res;
+	},
+	/** Call fn(node) for all child nodes and recursively load lazy children.<br>
+	 * <b>Note:</b> If you need this method, you probably should consider to review
+	 * your architecture! Recursivley loading nodes is a perfect way for lazy
+	 * programmers to flood the server with requests ;-)
+	 *
+	 * @param {function} [fn] optional callback function.
+	 *     Return false to stop iteration, return "skip" to skip this node and
+	 *     its children only.
+	 * @param {boolean} [includeSelf=false]
+	 * @returns {$.Promise}
+	 * @since 2.4
+	 */
+	visitAndLoad: function(fn, includeSelf, _recursion) {
+		var dfd, res, loaders,
+			node = this;
+
+		// node.debug("visitAndLoad");
+		if( fn && includeSelf === true ) {
+			res = fn(node);
+			if( res === false || res === "skip" ) {
+				return _recursion ? res : _getResolvedPromise();
+			}
+		}
+		if( !node.children && !node.lazy ) {
+			return _getResolvedPromise();
+		}
+		dfd = new $.Deferred();
+		loaders = [];
+		// node.debug("load()...");
+		node.load().done(function(){
+			// node.debug("load()... done.");
+			for(var i=0, l=node.children.length; i<l; i++){
+				res = node.children[i].visitAndLoad(fn, true, true);
+				if( res === false ) {
+					dfd.reject();
+					break;
+				} else if ( res !== "skip" ) {
+					loaders.push(res); // Add promise to the list
+				}
+			}
+			$.when.apply(this, loaders).then(function(){
+				dfd.resolve();
+			});
+		});
+		return dfd.promise();
+	},
+	/** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br>
+	 * Stop iteration, if fn() returns false.<br>
+	 * Return false if iteration was stopped.
+	 *
+	 * @param {function} fn the callback function.
+	 *     Return false to stop iteration, return "skip" to skip this node and children only.
+	 * @param {boolean} [includeSelf=false]
+	 * @returns {boolean}
+	 */
+	visitParents: function(fn, includeSelf) {
+		// Visit parent nodes (bottom up)
+		if(includeSelf && fn(this) === false){
+			return false;
+		}
+		var p = this.parent;
+		while( p ) {
+			if(fn(p) === false){
+				return false;
+			}
+			p = p.parent;
+		}
+		return true;
+	},
+	/** Call fn(node) for all sibling nodes.<br>
+	 * Stop iteration, if fn() returns false.<br>
+	 * Return false if iteration was stopped.
+	 *
+	 * @param {function} fn the callback function.
+	 *     Return false to stop iteration.
+	 * @param {boolean} [includeSelf=false]
+	 * @returns {boolean}
+	 */
+	visitSiblings: function(fn, includeSelf) {
+		var i, l, n,
+			ac = this.parent.children;
+
+		for (i=0, l=ac.length; i<l; i++) {
+			n = ac[i];
+			if ( includeSelf || n !== this ){
+				if( fn(n) === false ) {
+					return false;
+				}
+			}
+		}
+		return true;
+	},
+	/** Write warning to browser console (prepending node info)
+	 *
+	 * @param {*} msg string or object or array of such
+	 */
+	warn: function(msg){
+		Array.prototype.unshift.call(arguments, this.toString());
+		consoleApply("warn", arguments);
+	}
+};
+
+
+/* *****************************************************************************
+ * Fancytree
+ */
+/**
+ * Construct a new tree object.
+ *
+ * @class Fancytree
+ * @classdesc The controller behind a fancytree.
+ * This class also contains 'hook methods': see {@link Fancytree_Hooks}.
+ *
+ * @param {Widget} widget
+ *
+ * @property {string} _id Automatically generated unique tree instance ID, e.g. "1".
+ * @property {string} _ns Automatically generated unique tree namespace, e.g. ".fancytree-1".
+ * @property {FancytreeNode} activeNode Currently active node or null.
+ * @property {string} ariaPropName Property name of FancytreeNode that contains the element which will receive the aria attributes.
+ *     Typically "li", but "tr" for table extension.
+ * @property {jQueryObject} $container Outer &lt;ul> element (or &lt;table> element for ext-table).
+ * @property {jQueryObject} $div A jQuery object containing the element used to instantiate the tree widget (`widget.element`)
+ * @property {object} data Metadata, i.e. properties that may be passed to `source` in addition to a children array.
+ * @property {object} ext Hash of all active plugin instances.
+ * @property {FancytreeNode} focusNode Currently focused node or null.
+ * @property {FancytreeNode} lastSelectedNode Used to implement selectMode 1 (single select)
+ * @property {string} nodeContainerAttrName Property name of FancytreeNode that contains the outer element of single nodes.
+ *     Typically "li", but "tr" for table extension.
+ * @property {FancytreeOptions} options Current options, i.e. default options + options passed to constructor.
+ * @property {FancytreeNode} rootNode Invisible system root node.
+ * @property {string} statusClassPropName Property name of FancytreeNode that contains the element which will receive the status classes.
+ *     Typically "span", but "tr" for table extension.
+ * @property {object} widget Base widget instance.
+ */
+function Fancytree(widget) {
+	this.widget = widget;
+	this.$div = widget.element;
+	this.options = widget.options;
+	if( this.options ) {
+		if(  $.isFunction(this.options.lazyload ) && !$.isFunction(this.options.lazyLoad) ) {
+			this.options.lazyLoad = function() {
+				FT.warn("The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead.");
+				return widget.options.lazyload.apply(this, arguments);
+			};
+		}
+		if( $.isFunction(this.options.loaderror) ) {
+			$.error("The 'loaderror' event was renamed since 2014-07-03. Use 'loadError' (with uppercase E) instead.");
+		}
+		if( this.options.fx !== undefined ) {
+			FT.warn("The 'fx' option was replaced by 'toggleEffect' since 2014-11-30.");
+		}
+		if( this.options.removeNode !== undefined ) {
+			$.error("The 'removeNode' event was replaced by 'modifyChild' since 2.20 (2016-09-10).");
+		}
+	}
+	this.ext = {}; // Active extension instances
+	// allow to init tree.data.foo from <div data-foo=''>
+	this.data = _getElementDataAsDict(this.$div);
+	// TODO: use widget.uuid instead?
+	this._id = $.ui.fancytree._nextId++;
+	// TODO: use widget.eventNamespace instead?
+	this._ns = ".fancytree-" + this._id; // append for namespaced events
+	this.activeNode = null;
+	this.focusNode = null;
+	this._hasFocus = null;
+	this._tempCache = {};
+	this._lastMousedownNode = null;
+	this._enableUpdate = true;
+	// this._dirtyRoots = null;
+	this.lastSelectedNode = null;
+	this.systemFocusElement = null;
+	this.lastQuicksearchTerm = "";
+	this.lastQuicksearchTime = 0;
+
+	this.statusClassPropName = "span";
+	this.ariaPropName = "li";
+	this.nodeContainerAttrName = "li";
+
+	// Remove previous markup if any
+	this.$div.find(">ul.fancytree-container").remove();
+
+	// Create a node without parent.
+	var fakeParent = { tree: this },
+		$ul;
+	this.rootNode = new FancytreeNode(fakeParent, {
+		title: "root",
+		key: "root_" + this._id,
+		children: null,
+		expanded: true
+	});
+	this.rootNode.parent = null;
+
+	// Create root markup
+	$ul = $("<ul>", {
+		"class": "ui-fancytree fancytree-container fancytree-plain"
+	}).appendTo(this.$div);
+	this.$container = $ul;
+	this.rootNode.ul = $ul[0];
+
+	if(this.options.debugLevel == null){
+		this.options.debugLevel = FT.debugLevel;
+	}
+	// // Add container to the TAB chain
+	// // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant
+	// // #577: Allow to set tabindex to "0", "-1" and ""
+	// this.$container.attr("tabindex", this.options.tabindex);
+
+	// if( this.options.rtl ) {
+	// 	this.$container.attr("DIR", "RTL").addClass("fancytree-rtl");
+	// // }else{
+	// //	this.$container.attr("DIR", null).removeClass("fancytree-rtl");
+	// }
+	// if(this.options.aria){
+	// 	this.$container.attr("role", "tree");
+	// 	if( this.options.selectMode !== 1 ) {
+	// 		this.$container.attr("aria-multiselectable", true);
+	// 	}
+	// }
+}
+
+
+Fancytree.prototype = /** @lends Fancytree# */{
+	/* Return a context object that can be re-used for _callHook().
+	 * @param {Fancytree | FancytreeNode | EventData} obj
+	 * @param {Event} originalEvent
+	 * @param {Object} extra
+	 * @returns {EventData}
+	 */
+	_makeHookContext: function(obj, originalEvent, extra) {
+		var ctx, tree;
+		if(obj.node !== undefined){
+			// obj is already a context object
+			if(originalEvent && obj.originalEvent !== originalEvent){
+				$.error("invalid args");
+			}
+			ctx = obj;
+		}else if(obj.tree){
+			// obj is a FancytreeNode
+			tree = obj.tree;
+			ctx = { node: obj, tree: tree, widget: tree.widget, options: tree.widget.options, originalEvent: originalEvent };
+		}else if(obj.widget){
+			// obj is a Fancytree
+			ctx = { node: null, tree: obj, widget: obj.widget, options: obj.widget.options, originalEvent: originalEvent };
+		}else{
+			$.error("invalid args");
+		}
+		if(extra){
+			$.extend(ctx, extra);
+		}
+		return ctx;
+	},
+	/* Trigger a hook function: funcName(ctx, [...]).
+	 *
+	 * @param {string} funcName
+	 * @param {Fancytree|FancytreeNode|EventData} contextObject
+	 * @param {any}  [_extraArgs] optional additional arguments
+	 * @returns {any}
+	 */
+	_callHook: function(funcName, contextObject, _extraArgs) {
+		var ctx = this._makeHookContext(contextObject),
+			fn = this[funcName],
+			args = Array.prototype.slice.call(arguments, 2);
+		if(!$.isFunction(fn)){
+			$.error("_callHook('" + funcName + "') is not a function");
+		}
+		args.unshift(ctx);
+//		this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args);
+		return fn.apply(this, args);
+	},
+	_setExpiringValue: function(key, value, ms){
+		this._tempCache[key] = {value: value, expire: Date.now() + (+ms || 50)};
+	},
+	_getExpiringValue: function(key){
+		var entry = this._tempCache[key];
+		if( entry && entry.expire < Date.now() ) {
+			return entry.value;
+		}
+		delete this._tempCache[key];
+		return null;
+	},
+	/* Check if current extensions dependencies are met and throw an error if not.
+	 *
+	 * This method may be called inside the `treeInit` hook for custom extensions.
+	 *
+	 * @param {string} extension name of the required extension
+	 * @param {boolean} [required=true] pass `false` if the extension is optional, but we want to check for order if it is present
+	 * @param {boolean} [before] `true` if `name` must be included before this, `false` otherwise (use `null` if order doesn't matter)
+	 * @param {string} [message] optional error message (defaults to a descriptve error message)
+	 */
+	_requireExtension: function(name, required, before, message) {
+		before = !!before;
+		var thisName = this._local.name,
+			extList = this.options.extensions,
+			isBefore = $.inArray(name, extList) < $.inArray(thisName, extList),
+			isMissing = required && this.ext[name] == null,
+			badOrder = !isMissing && before != null && (before !== isBefore);
+
+		_assert(thisName && thisName !== name, "invalid or same name");
+
+		if( isMissing || badOrder ){
+			if( !message ){
+				if( isMissing || required ){
+					message = "'" + thisName + "' extension requires '" + name + "'";
+					if( badOrder ){
+						message += " to be registered " + (before ? "before" : "after") + " itself";
+					}
+				}else{
+					message = "If used together, `" + name + "` must be registered " + (before ? "before" : "after") + " `" + thisName + "`";
+				}
+			}
+			$.error(message);
+			return false;
+		}
+		return true;
+	},
+	/** Activate node with a given key and fire focus and activate events.
+	 *
+	 * A previously activated node will be deactivated.
+	 * If activeVisible option is set, all parents will be expanded as necessary.
+	 * Pass key = false, to deactivate the current node only.
+	 * @param {string} key
+	 * @returns {FancytreeNode} activated node (null, if not found)
+	 */
+	activateKey: function(key) {
+		var node = this.getNodeByKey(key);
+		if(node){
+			node.setActive();
+		}else if(this.activeNode){
+			this.activeNode.setActive(false);
+		}
+		return node;
+	},
+	/** (experimental) Add child status nodes that indicate 'More...', ....
+	 * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes.
+	 * @param {string} [mode='append'] 'child'|firstChild'
+	 * @since 2.15
+	 */
+	addPagingNode: function(node, mode){
+		return this.rootNode.addPagingNode(node, mode);
+	},
+	/** (experimental) Modify existing data model.
+	 *
+	 * @param {Array} patchList array of [key, NodePatch] arrays
+	 * @returns {$.Promise} resolved, when all patches have been applied
+	 * @see TreePatch
+	 */
+	applyPatch: function(patchList) {
+		var dfd, i, p2, key, patch, node,
+			patchCount = patchList.length,
+			deferredList = [];
+
+		for(i=0; i<patchCount; i++){
+			p2 = patchList[i];
+			_assert(p2.length === 2, "patchList must be an array of length-2-arrays");
+			key = p2[0];
+			patch = p2[1];
+			node = (key === null) ? this.rootNode : this.getNodeByKey(key);
+			if(node){
+				dfd = new $.Deferred();
+				deferredList.push(dfd);
+				node.applyPatch(patch).always(_makeResolveFunc(dfd, node));
+			}else{
+				this.warn("could not find node with key '" + key + "'");
+			}
+		}
+		// Return a promise that is resolved, when ALL patches were applied
+		return $.when.apply($, deferredList).promise();
+	},
+	/* TODO: implement in dnd extension
+	cancelDrag: function() {
+		var dd = $.ui.ddmanager.current;
+		if(dd){
+			dd.cancel();
+		}
+	},
+   */
+	/** Remove all nodes.
+	 * @since 2.14
+	 */
+	clear: function(source) {
+		this._callHook("treeClear", this);
+	},
+   /** Return the number of nodes.
+	* @returns {integer}
+	*/
+	count: function() {
+		return this.rootNode.countChildren();
+	},
+	/** Write to browser console if debugLevel >= 2 (prepending tree name)
+	 *
+	 * @param {*} msg string or object or array of such
+	 */
+	debug: function(msg){
+		if( this.options.debugLevel >= 2 ) {
+			Array.prototype.unshift.call(arguments, this.toString());
+			consoleApply("log", arguments);
+		}
+	},
+	// TODO: disable()
+	// TODO: enable()
+	/** Temporarily suppress rendering to improve performance on bulk-updates.
+	 *
+	 * @param {boolean} flag
+	 * @returns {boolean} previous status
+	 * @since 2.19
+	 */
+	enableUpdate: function(flag) {
+		flag = ( flag !== false );
+		/*jshint -W018 */  // Confusing use of '!'
+		if ( !!this._enableUpdate === !!flag ) {
+			return flag;
+		}
+		/*jshint +W018 */
+		this._enableUpdate = flag;
+		if ( flag ) {
+			this.debug("enableUpdate(true): redraw ");  //, this._dirtyRoots);
+			this.render();
+		} else {
+		// 	this._dirtyRoots = null;
+			this.debug("enableUpdate(false)...");
+		}
+		return !flag; // return previous value
+	},
+	/**Find all nodes that matches condition.
+	 *
+	 * @param {string | function(node)} match title string to search for, or a
+	 *     callback function that returns `true` if a node is matched.
+	 * @returns {FancytreeNode[]} array of nodes (may be empty)
+	 * @see FancytreeNode#findAll
+	 * @since 2.12
+	 */
+	findAll: function(match) {
+		return this.rootNode.findAll(match);
+	},
+	/**Find first node that matches condition.
+	 *
+	 * @param {string | function(node)} match title string to search for, or a
+	 *     callback function that returns `true` if a node is matched.
+	 * @returns {FancytreeNode} matching node or null
+	 * @see FancytreeNode#findFirst
+	 * @since 2.12
+	 */
+	findFirst: function(match) {
+		return this.rootNode.findFirst(match);
+	},
+	/** Find the next visible node that starts with `match`, starting at `startNode`
+	 * and wrap-around at the end.
+	 *
+	 * @param {string|function} match
+	 * @param {FancytreeNode} [startNode] defaults to first node
+	 * @returns {FancytreeNode} matching node or null
+	 */
+	findNextNode: function(match, startNode, visibleOnly) {
+		var stopNode = null,
+			parentChildren = startNode.parent.children,
+			matchingNode = null,
+			walkVisible = function(parent, idx, fn) {
+				var i, grandParent,
+					parentChildren = parent.children,
+					siblingCount = parentChildren.length,
+					node = parentChildren[idx];
+				// visit node itself
+				if( node && fn(node) === false ) {
+					return false;
+				}
+				// visit descendants
+				if( node && node.children && node.expanded ) {
+					if( walkVisible(node, 0, fn) === false ) {
+						return false;
+					}
+				}
+				// visit subsequent siblings
+				for( i = idx + 1; i < siblingCount; i++ ) {
+					if( walkVisible(parent, i, fn) === false ) {
+						return false;
+					}
+				}
+				// visit parent's subsequent siblings
+				grandParent = parent.parent;
+				if( grandParent ) {
+					return walkVisible(grandParent, grandParent.children.indexOf(parent) + 1, fn);
+				} else {
+					// wrap-around: restart with first node
+					return walkVisible(parent, 0, fn);
+				}
+			};
+
+		match = (typeof match === "string") ? _makeNodeTitleStartMatcher(match) : match;
+		startNode = startNode || this.getFirstChild();
+
+		walkVisible(startNode.parent, parentChildren.indexOf(startNode), function(node){
+			// Stop iteration if we see the start node a second time
+			if( node === stopNode ) {
+				return false;
+			}
+			stopNode = stopNode || node;
+			// Ignore nodes hidden by a filter
+			if( ! $(node.span).is(":visible") ) {
+				node.debug("quicksearch: skipping hidden node");
+				return;
+			}
+			// Test if we found a match, but search for a second match if this
+			// was the currently active node
+			if( match(node) ) {
+				// node.debug("quicksearch match " + node.title, startNode);
+				matchingNode = node;
+				if( matchingNode !== startNode ) {
+					return false;
+				}
+			}
+		});
+		return matchingNode;
+	},
+	// TODO: fromDict
+	/**
+	 * Generate INPUT elements that can be submitted with html forms.
+	 *
+	 * In selectMode 3 only the topmost selected nodes are considered, unless
+	 * `opts.stopOnParents: false` is passed.
+	 *
+	 * @example
+	 * // Generate input elements for active and selected nodes
+	 * tree.generateFormElements();
+	 * // Generate input elements selected nodes, using a custom `name` attribute
+	 * tree.generateFormElements("cust_sel", false);
+	 * // Generate input elements using a custom filter
+	 * tree.generateFormElements(true, true, { filter: function(node) {
+	 *     return node.isSelected() && node.data.yes;
+	 * }});
+	 *
+	 * @param {boolean | string} [selected=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID[]')
+	 * @param {boolean | string} [active=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID_active')
+	 * @param {object} [opts] default { filter: null, stopOnParents: true }
+	 */
+	generateFormElements: function(selected, active, opts) {
+		opts = opts || {};
+
+		var nodeList,
+			selectedName = (typeof selected === "string") ? selected : "ft_" + this._id + "[]",
+			activeName = (typeof active === "string") ? active : "ft_" + this._id + "_active",
+			id = "fancytree_result_" + this._id,
+			$result = $("#" + id),
+			stopOnParents = this.options.selectMode === 3 && opts.stopOnParents !== false;
+
+		if($result.length){
+			$result.empty();
+		}else{
+			$result = $("<div>", {
+				id: id
+			}).hide().insertAfter(this.$container);
+		}
+		if(active !== false && this.activeNode){
+			$result.append($("<input>", {
+				type: "radio",
+				name: activeName,
+				value: this.activeNode.key,
+				checked: true
+			}));
+		}
+		function _appender( node ) {
+			$result.append($("<input>", {
+				type: "checkbox",
+				name: selectedName,
+				value: node.key,
+				checked: true
+			}));
+		}
+		if ( opts.filter ) {
+			this.visit(function(node) {
+				var res = opts.filter(node);
+				if( res === "skip" ) { return res; }
+				if ( res !== false ) {
+					_appender(node);
+				}
+			});
+		} else if ( selected !== false ) {
+			nodeList = this.getSelectedNodes(stopOnParents);
+			$.each(nodeList, function(idx, node) {
+				_appender(node);
+			});
+		}
+	},
+	/**
+	 * Return the currently active node or null.
+	 * @returns {FancytreeNode}
+	 */
+	getActiveNode: function() {
+		return this.activeNode;
+	},
+	/** Return the first top level node if any (not the invisible root node).
+	 * @returns {FancytreeNode | null}
+	 */
+	getFirstChild: function() {
+		return this.rootNode.getFirstChild();
+	},
+	/**
+	 * Return node that has keyboard focus or null.
+	 * @returns {FancytreeNode}
+	 */
+	getFocusNode: function() {
+		return this.focusNode;
+	},
+	/**
+	 * Return node with a given key or null if not found.
+	 * @param {string} key
+	 * @param {FancytreeNode} [searchRoot] only search below this node
+	 * @returns {FancytreeNode | null}
+	 */
+	getNodeByKey: function(key, searchRoot) {
+		// Search the DOM by element ID (assuming this is faster than traversing all nodes).
+		// $("#...") has problems, if the key contains '.', so we use getElementById()
+		var el, match;
+		if(!searchRoot){
+			el = document.getElementById(this.options.idPrefix + key);
+			if( el ){
+				return el.ftnode ? el.ftnode : null;
+			}
+		}
+		// Not found in the DOM, but still may be in an unrendered part of tree
+		// TODO: optimize with specialized loop
+		// TODO: consider keyMap?
+		searchRoot = searchRoot || this.rootNode;
+		match = null;
+		searchRoot.visit(function(node){
+//            window.console.log("getNodeByKey(" + key + "): ", node.key);
+			if(node.key === key) {
+				match = node;
+				return false;
+			}
+		}, true);
+		return match;
+	},
+	/** Return the invisible system root node.
+	 * @returns {FancytreeNode}
+	 */
+	getRootNode: function() {
+		return this.rootNode;
+	},
+	/**
+	 * Return an array of selected nodes.
+	 * @param {boolean} [stopOnParents=false] only return the topmost selected
+	 *     node (useful with selectMode 3)
+	 * @returns {FancytreeNode[]}
+	 */
+	getSelectedNodes: function(stopOnParents) {
+		return this.rootNode.getSelectedNodes(stopOnParents);
+	},
+	/** Return true if the tree control has keyboard focus
+	 * @returns {boolean}
+	 */
+	hasFocus: function(){
+		return !!this._hasFocus;
+	},
+	/** Write to browser console if debugLevel >= 1 (prepending tree name)
+	 * @param {*} msg string or object or array of such
+	 */
+	info: function(msg){
+		if( this.options.debugLevel >= 1 ) {
+			Array.prototype.unshift.call(arguments, this.toString());
+			consoleApply("info", arguments);
+		}
+	},
+/*
+	TODO: isInitializing: function() {
+		return ( this.phase=="init" || this.phase=="postInit" );
+	},
+	TODO: isReloading: function() {
+		return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound;
+	},
+	TODO: isUserEvent: function() {
+		return ( this.phase=="userEvent" );
+	},
+*/
+
+	/**
+	 * Make sure that a node with a given ID is loaded, by traversing - and
+	 * loading - its parents. This method is ment for lazy hierarchies.
+	 * A callback is executed for every node as we go.
+	 * @example
+	 * tree.loadKeyPath("/_3/_23/_26/_27", function(node, status){
+	 *   if(status === "loaded") {
+	 *     console.log("loaded intermiediate node " + node);
+	 *   }else if(status === "ok") {
+	 *     node.activate();
+	 *   }
+	 * });
+	 *
+	 * @param {string | string[]} keyPathList one or more key paths (e.g. '/3/2_1/7')
+	 * @param {function} callback callback(node, status) is called for every visited node ('loading', 'loaded', 'ok', 'error')
+	 * @returns {$.Promise}
+	 */
+	loadKeyPath: function(keyPathList, callback, _rootNode) {
+		var deferredList, dfd, i, path, key, loadMap, node, root, segList,
+			sep = this.options.keyPathSeparator,
+			self = this;
+
+		callback = callback || $.noop;
+		if(!$.isArray(keyPathList)){
+			keyPathList = [keyPathList];
+		}
+		// Pass 1: handle all path segments for nodes that are already loaded
+		// Collect distinct top-most lazy nodes in a map
+		loadMap = {};
+
+		for(i=0; i<keyPathList.length; i++){
+			root = _rootNode || this.rootNode;
+			path = keyPathList[i];
+			// strip leading slash
+			if(path.charAt(0) === sep){
+				path = path.substr(1);
+			}
+			// traverse and strip keys, until we hit a lazy, unloaded node
+			segList = path.split(sep);
+			while(segList.length){
+				key = segList.shift();
+//                node = _findDirectChild(root, key);
+				node = root._findDirectChild(key);
+				if(!node){
+					this.warn("loadKeyPath: key not found: " + key + " (parent: " + root + ")");
+					callback.call(this, key, "error");
+					break;
+				}else if(segList.length === 0){
+					callback.call(this, node, "ok");
+					break;
+				}else if(!node.lazy || (node.hasChildren() !== undefined )){
+					callback.call(this, node, "loaded");
+					root = node;
+				}else{
+					callback.call(this, node, "loaded");
+//                    segList.unshift(key);
+					if(loadMap[key]){
+						loadMap[key].push(segList.join(sep));
+					}else{
+						loadMap[key] = [segList.join(sep)];
+					}
+					break;
+				}
+			}
+		}
+//        alert("loadKeyPath: loadMap=" + JSON.stringify(loadMap));
+		// Now load all lazy nodes and continue itearation for remaining paths
+		deferredList = [];
+		// Avoid jshint warning 'Don't make functions within a loop.':
+		function __lazyload(key, node, dfd){
+			callback.call(self, node, "loading");
+			node.load().done(function(){
+				self.loadKeyPath.call(self, loadMap[key], callback, node).always(_makeResolveFunc(dfd, self));
+			}).fail(function(errMsg){
+				self.warn("loadKeyPath: error loading: " + key + " (parent: " + root + ")");
+				callback.call(self, node, "error");
+				dfd.reject();
+			});
+		}
+		for(key in loadMap){
+			node = root._findDirectChild(key);
+			if (node == null) {  // #576
+				node = self.getNodeByKey(key);
+			}
+			dfd = new $.Deferred();
+			deferredList.push(dfd);
+			__lazyload(key, node, dfd);
+		}
+		// Return a promise that is resolved, when ALL paths were loaded
+		return $.when.apply($, deferredList).promise();
+	},
+	/** Re-fire beforeActivate, activate, and (optional) focus events.
+	 * Calling this method in the `init` event, will activate the node that
+	 * was marked 'active' in the source data, and optionally set the keyboard
+	 * focus.
+	 * @param [setFocus=false]
+	 */
+	reactivate: function(setFocus) {
+		var res,
+			node = this.activeNode;
+
+		if( !node ) {
+			return _getResolvedPromise();
+		}
+		this.activeNode = null; // Force re-activating
+		res = node.setActive(true, {noFocus: true});
+		if( setFocus ){
+			node.setFocus();
+		}
+		return res;
+	},
+	/** Reload tree from source and return a promise.
+	 * @param [source] optional new source (defaults to initial source data)
+	 * @returns {$.Promise}
+	 */
+	reload: function(source) {
+		this._callHook("treeClear", this);
+		return this._callHook("treeLoad", this, source);
+	},
+	/**Render tree (i.e. create DOM elements for all top-level nodes).
+	 * @param {boolean} [force=false] create DOM elemnts, even if parent is collapsed
+	 * @param {boolean} [deep=false]
+	 */
+	render: function(force, deep) {
+		return this.rootNode.render(force, deep);
+	},
+	// TODO: selectKey: function(key, select)
+	// TODO: serializeArray: function(stopOnParents)
+	/**
+	 * @param {boolean} [flag=true]
+	 */
+	setFocus: function(flag) {
+		return this._callHook("treeSetFocus", this, flag);
+	},
+	/**
+	 * Return all nodes as nested list of {@link NodeData}.
+	 *
+	 * @param {boolean} [includeRoot=false] Returns the hidden system root node (and its children)
+	 * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications
+	 * @returns {Array | object}
+	 * @see FancytreeNode#toDict
+	 */
+	toDict: function(includeRoot, callback){
+		var res = this.rootNode.toDict(true, callback);
+		return includeRoot ? res : res.children;
+	},
+	/* Implicitly called for string conversions.
+	 * @returns {string}
+	 */
+	toString: function(){
+		return "<Fancytree(#" + this._id + ")>";
+	},
+	/* _trigger a widget event with additional node ctx.
+	 * @see EventData
+	 */
+	_triggerNodeEvent: function(type, node, originalEvent, extra) {
+//		this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx);
+		var ctx = this._makeHookContext(node, originalEvent, extra),
+			res = this.widget._trigger(type, originalEvent, ctx);
+		if(res !== false && ctx.result !== undefined){
+			return ctx.result;
+		}
+		return res;
+	},
+	/* _trigger a widget event with additional tree data. */
+	_triggerTreeEvent: function(type, originalEvent, extra) {
+//		this.debug("_trigger(" + type + ")", ctx);
+		var ctx = this._makeHookContext(this, originalEvent, extra),
+			res = this.widget._trigger(type, originalEvent, ctx);
+
+		if(res !== false && ctx.result !== undefined){
+			return ctx.result;
+		}
+		return res;
+	},
+	/** Call fn(node) for all nodes.
+	 *
+	 * @param {function} fn the callback function.
+	 *     Return false to stop iteration, return "skip" to skip this node and children only.
+	 * @returns {boolean} false, if the iterator was stopped.
+	 */
+	visit: function(fn) {
+		return this.rootNode.visit(fn, false);
+	},
+	/** Write warning to browser console (prepending tree info)
+	 *
+	 * @param {*} msg string or object or array of such
+	 */
+	warn: function(msg){
+		Array.prototype.unshift.call(arguments, this.toString());
+		consoleApply("warn", arguments);
+	}
+};
+
+/**
+ * These additional methods of the {@link Fancytree} class are 'hook functions'
+ * that can be used and overloaded by extensions.
+ * (See <a href="https://github.com/mar10/fancytree/wiki/TutorialExtensions">writing extensions</a>.)
+ * @mixin Fancytree_Hooks
+ */
+$.extend(Fancytree.prototype,
+	/** @lends Fancytree_Hooks# */
+	{
+	/** Default handling for mouse click events.
+	 *
+	 * @param {EventData} ctx
+	 */
+	nodeClick: function(ctx) {
+		var activate, expand,
+			// event = ctx.originalEvent,
+			targetType = ctx.targetType,
+			node = ctx.node;
+
+//	    this.debug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which, ctx);
+		// TODO: use switch
+		// TODO: make sure clicks on embedded <input> doesn't steal focus (see table sample)
+		if( targetType === "expander" ) {
+			if( node.isLoading() ) {
+				// #495: we probably got a click event while a lazy load is pending.
+				// The 'expanded' state is not yet set, so 'toggle' would expand
+				// and trigger lazyLoad again.
+				// It would be better to allow to collapse/expand the status node
+				// while loading (instead of ignoring), but that would require some
+				// more work.
+				node.debug("Got 2nd click while loading: ignored");
+				return;
+			}
+			// Clicking the expander icon always expands/collapses
+			this._callHook("nodeToggleExpanded", ctx);
+
+		} else if( targetType === "checkbox" ) {
+			// Clicking the checkbox always (de)selects
+			this._callHook("nodeToggleSelected", ctx);
+			if( ctx.options.focusOnSelect ) { // #358
+				this._callHook("nodeSetFocus", ctx, true);
+			}
+
+		} else {
+			// Honor `clickFolderMode` for
+			expand = false;
+			activate = true;
+			if( node.folder ) {
+				switch( ctx.options.clickFolderMode ) {
+				case 2: // expand only
+					expand = true;
+					activate = false;
+					break;
+				case 3: // expand and activate
+					activate = true;
+					expand = true; //!node.isExpanded();
+					break;
+				// else 1 or 4: just activate
+				}
+			}
+			if( activate ) {
+				this.nodeSetFocus(ctx);
+				this._callHook("nodeSetActive", ctx, true);
+			}
+			if( expand ) {
+				if(!activate){
+//                    this._callHook("nodeSetFocus", ctx);
+				}
+//				this._callHook("nodeSetExpanded", ctx, true);
+				this._callHook("nodeToggleExpanded", ctx);
+			}
+		}
+		// Make sure that clicks stop, otherwise <a href='#'> jumps to the top
+		// if(event.target.localName === "a" && event.target.className === "fancytree-title"){
+		// 	event.preventDefault();
+		// }
+		// TODO: return promise?
+	},
+	/** Collapse all other  children of same parent.
+	 *
+	 * @param {EventData} ctx
+	 * @param {object} callOpts
+	 */
+	nodeCollapseSiblings: function(ctx, callOpts) {
+		// TODO: return promise?
+		var ac, i, l,
+			node = ctx.node;
+
+		if( node.parent ){
+			ac = node.parent.children;
+			for (i=0, l=ac.length; i<l; i++) {
+				if ( ac[i] !== node && ac[i].expanded ){
+					this._callHook("nodeSetExpanded", ac[i], false, callOpts);
+				}
+			}
+		}
+	},
+	/** Default handling for mouse douleclick events.
+	 * @param {EventData} ctx
+	 */
+	nodeDblclick: function(ctx) {
+		// TODO: return promise?
+		if( ctx.targetType === "title" && ctx.options.clickFolderMode === 4) {
+//			this.nodeSetFocus(ctx);
+//			this._callHook("nodeSetActive", ctx, true);
+			this._callHook("nodeToggleExpanded", ctx);
+		}
+		// TODO: prevent text selection on dblclicks
+		if( ctx.targetType === "title" ) {
+			ctx.originalEvent.preventDefault();
+		}
+	},
+	/** Default handling for mouse keydown events.
+	 *
+	 * NOTE: this may be called with node == null if tree (but no node) has focus.
+	 * @param {EventData} ctx
+	 */
+	nodeKeydown: function(ctx) {
+		// TODO: return promise?
+		var matchNode, stamp, res, focusNode,
+			event = ctx.originalEvent,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+			which = event.which,
+			whichChar = String.fromCharCode(which),
+			clean = !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey),
+			$target = $(event.target),
+			handled = true,
+			activate = !(event.ctrlKey || !opts.autoActivate );
+
+		// (node || FT).debug("ftnode.nodeKeydown(" + event.type + "): ftnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which);
+		// FT.debug("eventToString", which, '"' + String.fromCharCode(which) + '"', '"' + FT.eventToString(event) + '"');
+
+		// Set focus to active (or first node) if no other node has the focus yet
+		if( !node ){
+			focusNode = (this.getActiveNode() || this.getFirstChild());
+			if (focusNode){
+				focusNode.setFocus();
+				node = ctx.node = this.focusNode;
+				node.debug("Keydown force focus on active node");
+			}
+		}
+
+		if( opts.quicksearch && clean && /\w/.test(whichChar) &&
+				!SPECIAL_KEYCODES[which] &&  // #659
+				!$target.is(":input:enabled") ) {
+			// Allow to search for longer streaks if typed in quickly
+			stamp = new Date().getTime();
+			if( stamp - tree.lastQuicksearchTime > 500 ) {
+				tree.lastQuicksearchTerm = "";
+			}
+			tree.lastQuicksearchTime = stamp;
+			tree.lastQuicksearchTerm += whichChar;
+			// tree.debug("quicksearch find", tree.lastQuicksearchTerm);
+			matchNode = tree.findNextNode(tree.lastQuicksearchTerm, tree.getActiveNode());
+			if( matchNode ) {
+				matchNode.setActive();
+			}
+			event.preventDefault();
+			return;
+		}
+		switch( FT.eventToString(event) ) {
+			case "+":
+			case "=": // 187: '+' @ Chrome, Safari
+				tree.nodeSetExpanded(ctx, true);
+				break;
+			case "-":
+				tree.nodeSetExpanded(ctx, false);
+				break;
+			case "space":
+				if( node.isPagingNode() ) {
+					tree._triggerNodeEvent("clickPaging", ctx, event);
+				} else if( FT.evalOption("checkbox", node, node, opts, false) ) {  // #768
+					tree.nodeToggleSelected(ctx);
+				}else{
+					tree.nodeSetActive(ctx, true);
+				}
+				break;
+			case "return":
+				tree.nodeSetActive(ctx, true);
+				break;
+			case "home":
+			case "end":
+			case "backspace":
+			case "left":
+			case "right":
+			case "up":
+			case "down":
+				res = node.navigate(event.which, activate, true);
+				break;
+			default:
+				handled = false;
+		}
+		if(handled){
+			event.preventDefault();
+		}
+	},
+
+
+	// /** Default handling for mouse keypress events. */
+	// nodeKeypress: function(ctx) {
+	//     var event = ctx.originalEvent;
+	// },
+
+	// /** Trigger lazyLoad event (async). */
+	// nodeLazyLoad: function(ctx) {
+	//     var node = ctx.node;
+	//     if(this._triggerNodeEvent())
+	// },
+	/** Load child nodes (async).
+	 *
+	 * @param {EventData} ctx
+	 * @param {object[]|object|string|$.Promise|function} source
+	 * @returns {$.Promise} The deferred will be resolved as soon as the (ajax)
+	 *     data was rendered.
+	 */
+	nodeLoadChildren: function(ctx, source) {
+		var ajax, delay, dfd,
+			tree = ctx.tree,
+			node = ctx.node,
+			requestId = new Date().getTime();
+
+		if($.isFunction(source)){
+			source = source.call(tree, {type: "source"}, ctx);
+			_assert(!$.isFunction(source), "source callback must not return another function");
+		}
+		if(source.url){
+			if( node._requestId ) {
+				node.warn("Recursive load request #" + requestId + " while #" + node._requestId + " is pending.");
+			// } else {
+			// 	node.debug("Send load request #" + requestId);
+			}
+			// `source` is an Ajax options object
+			ajax = $.extend({}, ctx.options.ajax, source);
+			node._requestId = requestId;
+			if(ajax.debugDelay){
+				// simulate a slow server
+				delay = ajax.debugDelay;
+				if($.isArray(delay)){ // random delay range [min..max]
+					delay = delay[0] + Math.random() * (delay[1] - delay[0]);
+				}
+				node.warn("nodeLoadChildren waiting debugDelay " + Math.round(delay) + " ms ...");
+				ajax.debugDelay = false;
+				dfd = $.Deferred(function (dfd) {
+					setTimeout(function () {
+						$.ajax(ajax)
+							.done(function () {	dfd.resolveWith(this, arguments); })
+							.fail(function () {	dfd.rejectWith(this, arguments); });
+					}, delay);
+				});
+			}else{
+				dfd = $.ajax(ajax);
+			}
+
+			// Defer the deferred: we want to be able to reject, even if ajax
+			// resolved ok.
+			source = new $.Deferred();
+			dfd.done(function (data, textStatus, jqXHR) {
+				var errorObj, res;
+
+				if((this.dataType === "json" || this.dataType === "jsonp") && typeof data === "string"){
+					$.error("Ajax request returned a string (did you get the JSON dataType wrong?).");
+				}
+				if( node._requestId && node._requestId > requestId ) {
+					// The expected request time stamp is later than `requestId`
+					// (which was kept as as closure variable to this handler function)
+					// node.warn("Ignored load response for obsolete request #" + requestId + " (expected #" + node._requestId + ")");
+					source.rejectWith(this, [RECURSIVE_REQUEST_ERROR]);
+					return;
+				// } else {
+				// 	node.debug("Response returned for load request #" + requestId);
+				}
+				// postProcess is similar to the standard ajax dataFilter hook,
+				// but it is also called for JSONP
+				if( ctx.options.postProcess ){
+					try {
+						res = tree._triggerNodeEvent("postProcess", ctx, ctx.originalEvent, {
+							response: data, error: null, dataType: this.dataType
+						});
+					} catch(e) {
+						res = { error: e, message: "" + e, details: "postProcess failed"};
+					}
+					if( res.error ) {
+						errorObj = $.isPlainObject(res.error) ? res.error : {message: res.error};
+						errorObj = tree._makeHookContext(node, null, errorObj);
+						source.rejectWith(this, [errorObj]);
+						return;
+					}
+					data = $.isArray(res) ? res : data;
+
+				} else if (data && data.hasOwnProperty("d") && ctx.options.enableAspx ) {
+					// Process ASPX WebMethod JSON object inside "d" property
+					data = (typeof data.d === "string") ? $.parseJSON(data.d) : data.d;
+				}
+				source.resolveWith(this, [data]);
+			}).fail(function (jqXHR, textStatus, errorThrown) {
+				var errorObj = tree._makeHookContext(node, null, {
+					error: jqXHR,
+					args: Array.prototype.slice.call(arguments),
+					message: errorThrown,
+					details: jqXHR.status + ": " + errorThrown
+				});
+				source.rejectWith(this, [errorObj]);
+			});
+		}
+		// #383: accept and convert ECMAScript 6 Promise
+		if( $.isFunction(source.then) && $.isFunction(source["catch"]) ) {
+			dfd = source;
+			source = new $.Deferred();
+			dfd.then(function(value){
+				source.resolve(value);
+			}, function(reason){
+				source.reject(reason);
+			});
+		}
+		if($.isFunction(source.promise)){
+			// `source` is a deferred, i.e. ajax request
+			// _assert(!node.isLoading(), "recursive load");
+			tree.nodeSetStatus(ctx, "loading");
+
+			source.done(function (children) {
+				tree.nodeSetStatus(ctx, "ok");
+				node._requestId = null;
+			}).fail(function(error){
+				var ctxErr;
+
+				if ( error === RECURSIVE_REQUEST_ERROR ) {
+					node.warn("Ignored response for obsolete load request #" + requestId + " (expected #" + node._requestId + ")");
+					return;
+				} else if (error.node && error.error && error.message) {
+					// error is already a context object
+					ctxErr = error;
+				} else {
+					ctxErr = tree._makeHookContext(node, null, {
+						error: error, // it can be jqXHR or any custom error
+						args: Array.prototype.slice.call(arguments),
+						message: error ? (error.message || error.toString()) : ""
+					});
+					if( ctxErr.message === "[object Object]" ) {
+						ctxErr.message = "";
+					}
+				}
+				node.warn("Load children failed (" + ctxErr.message + ")", ctxErr);
+				if( tree._triggerNodeEvent("loadError", ctxErr, null) !== false ) {
+					tree.nodeSetStatus(ctx, "error", ctxErr.message, ctxErr.details);
+				}
+			});
+		} else {
+			if( ctx.options.postProcess ){
+				// #792: Call postProcess for non-deferred source
+				tree._triggerNodeEvent("postProcess", ctx, ctx.originalEvent, {
+					response: source, error: null, dataType: typeof source
+				});
+			}
+		}
+		// $.when(source) resolves also for non-deferreds
+		return $.when(source).done(function(children){
+			var metaData;
+
+			if( $.isPlainObject(children) ){
+				// We got {foo: 'abc', children: [...]}
+				// Copy extra properties to tree.data.foo
+				_assert(node.isRootNode(), "source may only be an object for root nodes (expecting an array of child objects otherwise)");
+				_assert($.isArray(children.children), "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')");
+				metaData = children;
+				children = children.children;
+				delete metaData.children;
+				$.extend(tree.data, metaData);
+			}
+			_assert($.isArray(children), "expected array of children");
+			node._setChildren(children);
+			// trigger fancytreeloadchildren
+			tree._triggerNodeEvent("loadChildren", node);
+		});
+	},
+	/** [Not Implemented]  */
+	nodeLoadKeyPath: function(ctx, keyPathList) {
+		// TODO: implement and improve
+		// http://code.google.com/p/dynatree/issues/detail?id=222
+	},
+	/**
+	 * Remove a single direct child of ctx.node.
+	 * @param {EventData} ctx
+	 * @param {FancytreeNode} childNode dircect child of ctx.node
+	 */
+	nodeRemoveChild: function(ctx, childNode) {
+		var idx,
+			node = ctx.node,
+			// opts = ctx.options,
+			subCtx = $.extend({}, ctx, {node: childNode}),
+			children = node.children;
+
+		// FT.debug("nodeRemoveChild()", node.toString(), childNode.toString());
+
+		if( children.length === 1 ) {
+			_assert(childNode === children[0], "invalid single child");
+			return this.nodeRemoveChildren(ctx);
+		}
+		if( this.activeNode && (childNode === this.activeNode || this.activeNode.isDescendantOf(childNode))){
+			this.activeNode.setActive(false); // TODO: don't fire events
+		}
+		if( this.focusNode && (childNode === this.focusNode || this.focusNode.isDescendantOf(childNode))){
+			this.focusNode = null;
+		}
+		// TODO: persist must take care to clear select and expand cookies
+		this.nodeRemoveMarkup(subCtx);
+		this.nodeRemoveChildren(subCtx);
+		idx = $.inArray(childNode, children);
+		_assert(idx >= 0, "invalid child");
+		// Notify listeners
+		node.triggerModifyChild("remove", childNode);
+		// Unlink to support GC
+		childNode.visit(function(n){
+			n.parent = null;
+		}, true);
+		this._callHook("treeRegisterNode", this, false, childNode);
+		// remove from child list
+		children.splice(idx, 1);
+	},
+	/**Remove HTML markup for all descendents of ctx.node.
+	 * @param {EventData} ctx
+	 */
+	nodeRemoveChildMarkup: function(ctx) {
+		var node = ctx.node;
+
+		// FT.debug("nodeRemoveChildMarkup()", node.toString());
+		// TODO: Unlink attr.ftnode to support GC
+		if(node.ul){
+			if( node.isRootNode() ) {
+				$(node.ul).empty();
+			} else {
+				$(node.ul).remove();
+				node.ul = null;
+			}
+			node.visit(function(n){
+				n.li = n.ul = null;
+			});
+		}
+	},
+	/**Remove all descendants of ctx.node.
+	* @param {EventData} ctx
+	*/
+	nodeRemoveChildren: function(ctx) {
+		var subCtx,
+			tree = ctx.tree,
+			node = ctx.node,
+			children = node.children;
+			// opts = ctx.options;
+
+		// FT.debug("nodeRemoveChildren()", node.toString());
+		if(!children){
+			return;
+		}
+		if( this.activeNode && this.activeNode.isDescendantOf(node)){
+			this.activeNode.setActive(false); // TODO: don't fire events
+		}
+		if( this.focusNode && this.focusNode.isDescendantOf(node)){
+			this.focusNode = null;
+		}
+		// TODO: persist must take care to clear select and expand cookies
+		this.nodeRemoveChildMarkup(ctx);
+		// Unlink children to support GC
+		// TODO: also delete this.children (not possible using visit())
+		subCtx = $.extend({}, ctx);
+		node.triggerModifyChild("remove", null);
+		node.visit(function(n){
+			n.parent = null;
+			tree._callHook("treeRegisterNode", tree, false, n);
+		});
+		if( node.lazy ){
+			// 'undefined' would be interpreted as 'not yet loaded' for lazy nodes
+			node.children = [];
+		} else{
+			node.children = null;
+		}
+		if( !node.isRootNode() ) {
+			node.expanded = false;  // #449, #459
+		}
+		this.nodeRenderStatus(ctx);
+	},
+	/**Remove HTML markup for ctx.node and all its descendents.
+	 * @param {EventData} ctx
+	 */
+	nodeRemoveMarkup: function(ctx) {
+		var node = ctx.node;
+		// FT.debug("nodeRemoveMarkup()", node.toString());
+		// TODO: Unlink attr.ftnode to support GC
+		if(node.li){
+			$(node.li).remove();
+			node.li = null;
+		}
+		this.nodeRemoveChildMarkup(ctx);
+	},
+	/**
+	 * Create `&lt;li>&lt;span>..&lt;/span> .. &lt;/li>` tags for this node.
+	 *
+	 * This method takes care that all HTML markup is created that is required
+	 * to display this node in its current state.
+	 *
+	 * Call this method to create new nodes, or after the strucuture
+	 * was changed (e.g. after moving this node or adding/removing children)
+	 * nodeRenderTitle() and nodeRenderStatus() are implied.
+	 *
+	 * &lt;code>
+	 * &lt;li id='KEY' ftnode=NODE>
+	 *     &lt;span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'>
+	 *         &lt;span class="fancytree-expander">&lt;/span>
+	 *         &lt;span class="fancytree-checkbox">&lt;/span> // only present in checkbox mode
+	 *         &lt;span class="fancytree-icon">&lt;/span>
+	 *         &lt;a href="#" class="fancytree-title"> Node 1 &lt;/a>
+	 *     &lt;/span>
+	 *     &lt;ul> // only present if node has children
+	 *         &lt;li id='KEY' ftnode=NODE> child1 ... &lt;/li>
+	 *         &lt;li id='KEY' ftnode=NODE> child2 ... &lt;/li>
+	 *     &lt;/ul>
+	 * &lt;/li>
+	 * &lt;/code>
+	 *
+	 * @param {EventData} ctx
+	 * @param {boolean} [force=false] re-render, even if html markup was already created
+	 * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed
+	 * @param {boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later
+	 */
+	nodeRender: function(ctx, force, deep, collapsed, _recursive) {
+		/* This method must take care of all cases where the current data mode
+		 * (i.e. node hierarchy) does not match the current markup.
+		 *
+		 * - node was not yet rendered:
+		 *   create markup
+		 * - node was rendered: exit fast
+		 * - children have been added
+		 * - children have been removed
+		 */
+		var childLI, childNode1, childNode2, i, l, next, subCtx,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+			aria = opts.aria,
+			firstTime = false,
+			parent = node.parent,
+			isRootNode = !parent,
+			children = node.children,
+			successorLi = null;
+		// FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString());
+
+		if( tree._enableUpdate === false ) {
+			// tree.debug("no render", tree._enableUpdate);
+			return;
+		}
+		if( ! isRootNode && ! parent.ul ) {
+			// Calling node.collapse on a deep, unrendered node
+			return;
+		}
+		_assert(isRootNode || parent.ul, "parent UL must exist");
+
+		// Render the node
+		if( !isRootNode ){
+			// Discard markup on force-mode, or if it is not linked to parent <ul>
+			if(node.li && (force || (node.li.parentNode !== node.parent.ul) ) ){
+				if( node.li.parentNode === node.parent.ul ){
+					// #486: store following node, so we can insert the new markup there later
+					successorLi = node.li.nextSibling;
+				}else{
+					// May happen, when a top-level node was dropped over another
+					this.debug("Unlinking " + node + " (must be child of " + node.parent + ")");
+				}
+//	            this.debug("nodeRemoveMarkup...");
+				this.nodeRemoveMarkup(ctx);
+			}
+			// Create <li><span /> </li>
+//			node.debug("render...");
+			if( !node.li ) {
+//	            node.debug("render... really");
+				firstTime = true;
+				node.li = document.createElement("li");
+				node.li.ftnode = node;
+
+				if( node.key && opts.generateIds ){
+					node.li.id = opts.idPrefix + node.key;
+				}
+				node.span = document.createElement("span");
+				node.span.className = "fancytree-node";
+				if( aria && !node.tr ) {
+					$(node.li).attr("role", "treeitem");
+				}
+				node.li.appendChild(node.span);
+
+				// Create inner HTML for the <span> (expander, checkbox, icon, and title)
+				this.nodeRenderTitle(ctx);
+
+				// Allow tweaking and binding, after node was created for the first time
+				if ( opts.createNode ){
+					opts.createNode.call(tree, {type: "createNode"}, ctx);
+				}
+			}else{
+//				this.nodeRenderTitle(ctx);
+				this.nodeRenderStatus(ctx);
+			}
+			// Allow tweaking after node state was rendered
+			if ( opts.renderNode ){
+				opts.renderNode.call(tree, {type: "renderNode"}, ctx);
+			}
+		}
+
+		// Visit child nodes
+		if( children ){
+			if( isRootNode || node.expanded || deep === true ) {
+				// Create a UL to hold the children
+				if( !node.ul ){
+					node.ul = document.createElement("ul");
+					if((collapsed === true && !_recursive) || !node.expanded){
+						// hide top UL, so we can use an animation to show it later
+						node.ul.style.display = "none";
+					}
+					if(aria){
+						$(node.ul).attr("role", "group");
+					}
+					if ( node.li ) { // issue #67
+						node.li.appendChild(node.ul);
+					} else {
+						node.tree.$div.append(node.ul);
+					}
+				}
+				// Add child markup
+				for(i=0, l=children.length; i<l; i++) {
+					subCtx = $.extend({}, ctx, {node: children[i]});
+					this.nodeRender(subCtx, force, deep, false, true);
+				}
+				// Remove <li> if nodes have moved to another parent
+				childLI = node.ul.firstChild;
+				while( childLI ){
+					childNode2 = childLI.ftnode;
+					if( childNode2 && childNode2.parent !== node ) {
+						node.debug("_fixParent: remove missing " + childNode2, childLI);
+						next = childLI.nextSibling;
+						childLI.parentNode.removeChild(childLI);
+						childLI = next;
+					}else{
+						childLI = childLI.nextSibling;
+					}
+				}
+				// Make sure, that <li> order matches node.children order.
+				childLI = node.ul.firstChild;
+				for(i=0, l=children.length-1; i<l; i++) {
+					childNode1 = children[i];
+					childNode2 = childLI.ftnode;
+					if( childNode1 !== childNode2 ) {
+						// node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2);
+						node.ul.insertBefore(childNode1.li, childNode2.li);
+					} else {
+						childLI = childLI.nextSibling;
+					}
+				}
+			}
+		}else{
+			// No children: remove markup if any
+			if( node.ul ){
+//				alert("remove child markup for " + node);
+				this.warn("remove child markup for " + node);
+				this.nodeRemoveChildMarkup(ctx);
+			}
+		}
+		if( !isRootNode ){
+			// Update element classes according to node state
+			// this.nodeRenderStatus(ctx);
+			// Finally add the whole structure to the DOM, so the browser can render
+			if( firstTime ){
+				// #486: successorLi is set, if we re-rendered (i.e. discarded)
+				// existing markup, which  we want to insert at the same position.
+				// (null is equivalent to append)
+//				parent.ul.appendChild(node.li);
+				parent.ul.insertBefore(node.li, successorLi);
+			}
+		}
+	},
+	/** Create HTML inside the node's outer &lt;span> (i.e. expander, checkbox,
+	 * icon, and title).
+	 *
+	 * nodeRenderStatus() is implied.
+	 * @param {EventData} ctx
+	 * @param {string} [title] optinal new title
+	 */
+	nodeRenderTitle: function(ctx, title) {
+		// set node connector images, links and text
+		var checkbox, className, icon, nodeTitle, role, tabindex, tooltip,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+			aria = opts.aria,
+			level = node.getLevel(),
+			ares = [];
+
+		if(title !== undefined){
+			node.title = title;
+		}
+		if ( !node.span || tree._enableUpdate === false ) {
+			// Silently bail out if node was not rendered yet, assuming
+			// node.render() will be called as the node becomes visible
+			return;
+		}
+		// Connector (expanded, expandable or simple)
+		role = (aria && node.hasChildren() !== false) ? " role='button'" : "";
+		if( level < opts.minExpandLevel ) {
+			if( !node.lazy ) {
+				node.expanded = true;
+			}
+			if(level > 1){
+				ares.push("<span " + role + " class='fancytree-expander fancytree-expander-fixed'></span>");
+			}
+			// .. else (i.e. for root level) skip expander/connector alltogether
+		} else {
+			ares.push("<span " + role + " class='fancytree-expander'></span>");
+		}
+		// Checkbox mode
+		checkbox = FT.evalOption("checkbox", node, node, opts, false);
+
+		if( checkbox && !node.isStatusNode() ) {
+			role = aria ? " role='checkbox'" : "";
+			className = "fancytree-checkbox";
+			if( checkbox === "radio" || (node.parent && node.parent.radiogroup) ) {
+				className += " fancytree-radio";
+			}
+			ares.push("<span " + role + " class='" + className + "'></span>");
+		}
+		// Folder or doctype icon
+		if( node.data.iconClass !== undefined ) {  // 2015-11-16
+			// Handle / warn about backward compatibility
+			if( node.icon ) {
+				$.error("'iconClass' node option is deprecated since v2.14.0: use 'icon' only instead");
+			} else {
+				node.warn("'iconClass' node option is deprecated since v2.14.0: use 'icon' instead");
+				node.icon = node.data.iconClass;
+			}
+		}
+		// If opts.icon is a callback and returns something other than undefined, use that
+		// else if node.icon is a boolean or string, use that
+		// else if opts.icon is a boolean or string, use that
+		// else show standard icon (which may be different for folders or documents)
+		icon = FT.evalOption("icon", node, node, opts, true);
+		if( typeof icon !== "boolean" ) {
+			// icon is defined, but not true/false: must be a string
+			icon = "" + icon;
+		}
+		if( icon !== false ) {
+			role = aria ? " role='presentation'" : "";
+			if ( typeof icon === "string" ) {
+				if( TEST_IMG.test(icon) ) {
+					// node.icon is an image url. Prepend imagePath
+					icon = (icon.charAt(0) === "/") ? icon : ((opts.imagePath || "") + icon);
+					ares.push("<img src='" + icon + "' class='fancytree-icon' alt='' />");
+				} else {
+					ares.push("<span " + role + " class='fancytree-custom-icon " + icon +  "'></span>");
+				}
+			} else {
+				// standard icon: theme css will take care of this
+				ares.push("<span " + role + " class='fancytree-icon'></span>");
+			}
+		}
+		// Node title
+		nodeTitle = "";
+		if ( opts.renderTitle ){
+			nodeTitle = opts.renderTitle.call(tree, {type: "renderTitle"}, ctx) || "";
+		}
+		if ( !nodeTitle ) {
+			tooltip = FT.evalOption("tooltip", node, node, opts, null);
+			if( tooltip === true ) {
+				tooltip = node.title;
+			}
+			// if( node.tooltip ) {
+			// 	tooltip = node.tooltip;
+			// } else if ( opts.tooltip ) {
+			// 	tooltip = opts.tooltip === true ? node.title : opts.tooltip.call(tree, node);
+			// }
+			tooltip = tooltip ? " title='" + _escapeTooltip(tooltip) + "'" : "";
+			tabindex = opts.titlesTabbable ? " tabindex='0'" : "";
+
+			nodeTitle = "<span class='fancytree-title'" +
+				tooltip + tabindex + ">" +
+				(opts.escapeTitles ? FT.escapeHtml(node.title) : node.title) +
+				"</span>";
+		}
+		ares.push(nodeTitle);
+		// Note: this will trigger focusout, if node had the focus
+		//$(node.span).html(ares.join("")); // it will cleanup the jQuery data currently associated with SPAN (if any), but it executes more slowly
+		node.span.innerHTML = ares.join("");
+		// Update CSS classes
+		this.nodeRenderStatus(ctx);
+		if ( opts.enhanceTitle ){
+			ctx.$title = $(">span.fancytree-title", node.span);
+			nodeTitle = opts.enhanceTitle.call(tree, {type: "enhanceTitle"}, ctx) || "";
+		}
+	},
+	/** Update element classes according to node state.
+	 * @param {EventData} ctx
+	 */
+	nodeRenderStatus: function(ctx) {
+		// Set classes for current status
+		var $ariaElem,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+//			nodeContainer = node[tree.nodeContainerAttrName],
+			hasChildren = node.hasChildren(),
+			isLastSib = node.isLastSibling(),
+			aria = opts.aria,
+			cn = opts._classNames,
+			cnList = [],
+			statusElem = node[tree.statusClassPropName];
+
+		if( !statusElem || tree._enableUpdate === false ){
+			// if this function is called for an unrendered node, ignore it (will be updated on nect render anyway)
+			return;
+		}
+		if( aria ) {
+			$ariaElem = $(node.tr || node.li);
+		}
+		// Build a list of class names that we will add to the node <span>
+		cnList.push(cn.node);
+		if( tree.activeNode === node ){
+			cnList.push(cn.active);
+//			$(">span.fancytree-title", statusElem).attr("tabindex", "0");
+//			tree.$container.removeAttr("tabindex");
+		// }else{
+//			$(">span.fancytree-title", statusElem).removeAttr("tabindex");
+//			tree.$container.attr("tabindex", "0");
+		}
+		if( tree.focusNode === node ){
+			cnList.push(cn.focused);
+		}
+		if( node.expanded ){
+			cnList.push(cn.expanded);
+		}
+		if( aria ){
+			if (hasChildren !== false) {
+				$ariaElem.attr("aria-expanded", Boolean(node.expanded));
+			}
+			else {
+				$ariaElem.removeAttr("aria-expanded");
+			}
+		}
+		if( node.folder ){
+			cnList.push(cn.folder);
+		}
+		if( hasChildren !== false ){
+			cnList.push(cn.hasChildren);
+		}
+		// TODO: required?
+		if( isLastSib ){
+			cnList.push(cn.lastsib);
+		}
+		if( node.lazy && node.children == null ){
+			cnList.push(cn.lazy);
+		}
+		if( node.partload ){
+			cnList.push(cn.partload);
+		}
+		if( node.partsel ){
+			cnList.push(cn.partsel);
+		}
+		if( FT.evalOption("unselectable", node, node, opts, false) ){
+			cnList.push(cn.unselectable);
+		}
+		if( node._isLoading ){
+			cnList.push(cn.loading);
+		}
+		if( node._error ){
+			cnList.push(cn.error);
+		}
+		if( node.statusNodeType ) {
+			cnList.push(cn.statusNodePrefix + node.statusNodeType);
+		}
+		if( node.selected ){
+			cnList.push(cn.selected);
+			if(aria){
+				$ariaElem.attr("aria-selected", true);
+			}
+		}else if(aria){
+			$ariaElem.attr("aria-selected", false);
+		}
+		if( node.extraClasses ){
+			cnList.push(node.extraClasses);
+		}
+		// IE6 doesn't correctly evaluate multiple class names,
+		// so we create combined class names that can be used in the CSS
+		if( hasChildren === false ){
+			cnList.push(cn.combinedExpanderPrefix + "n" +
+					(isLastSib ? "l" : "")
+					);
+		}else{
+			cnList.push(cn.combinedExpanderPrefix +
+					(node.expanded ? "e" : "c") +
+					(node.lazy && node.children == null ? "d" : "") +
+					(isLastSib ? "l" : "")
+					);
+		}
+		cnList.push(cn.combinedIconPrefix +
+				(node.expanded ? "e" : "c") +
+				(node.folder ? "f" : "")
+				);
+//        node.span.className = cnList.join(" ");
+		statusElem.className = cnList.join(" ");
+
+		// TODO: we should not set this in the <span> tag also, if we set it here:
+		// Maybe most (all) of the classes should be set in LI instead of SPAN?
+		if(node.li){
+			// #719: we have to consider that there may be already other classes:
+			$(node.li).toggleClass(cn.lastsib, isLastSib);
+		}
+	},
+	/** Activate node.
+	 * flag defaults to true.
+	 * If flag is true, the node is activated (must be a synchronous operation)
+	 * If flag is false, the node is deactivated (must be a synchronous operation)
+	 * @param {EventData} ctx
+	 * @param {boolean} [flag=true]
+	 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false}
+	 * @returns {$.Promise}
+	 */
+	nodeSetActive: function(ctx, flag, callOpts) {
+		// Handle user click / [space] / [enter], according to clickFolderMode.
+		callOpts = callOpts || {};
+		var subCtx,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+			noEvents = (callOpts.noEvents === true),
+			noFocus = (callOpts.noFocus === true),
+			isActive = (node === tree.activeNode);
+
+		// flag defaults to true
+		flag = (flag !== false);
+		// node.debug("nodeSetActive", flag);
+
+		if(isActive === flag){
+			// Nothing to do
+			return _getResolvedPromise(node);
+		}else if(flag && !noEvents && this._triggerNodeEvent("beforeActivate", node, ctx.originalEvent) === false ){
+			// Callback returned false
+			return _getRejectedPromise(node, ["rejected"]);
+		}
+		if(flag){
+			if(tree.activeNode){
+				_assert(tree.activeNode !== node, "node was active (inconsistency)");
+				subCtx = $.extend({}, ctx, {node: tree.activeNode});
+				tree.nodeSetActive(subCtx, false);
+				_assert(tree.activeNode === null, "deactivate was out of sync?");
+			}
+			if(opts.activeVisible){
+				// If no focus is set (noFocus: true) and there is no focused node, this node is made visible.
+				node.makeVisible({scrollIntoView: noFocus && tree.focusNode == null});
+			}
+			tree.activeNode = node;
+			tree.nodeRenderStatus(ctx);
+			if( !noFocus ) {
+				tree.nodeSetFocus(ctx);
+			}
+			if( !noEvents ) {
+				tree._triggerNodeEvent("activate", node, ctx.originalEvent);
+			}
+		}else{
+			_assert(tree.activeNode === node, "node was not active (inconsistency)");
+			tree.activeNode = null;
+			this.nodeRenderStatus(ctx);
+			if( !noEvents ) {
+				ctx.tree._triggerNodeEvent("deactivate", node, ctx.originalEvent);
+			}
+		}
+		return _getResolvedPromise(node);
+	},
+	/** Expand or collapse node, return Deferred.promise.
+	 *
+	 * @param {EventData} ctx
+	 * @param {boolean} [flag=true]
+	 * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false}
+	 * @returns {$.Promise} The deferred will be resolved as soon as the (lazy)
+	 *     data was retrieved, rendered, and the expand animation finshed.
+	 */
+	nodeSetExpanded: function(ctx, flag, callOpts) {
+		callOpts = callOpts || {};
+		var _afterLoad, dfd, i, l, parents, prevAC,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+			noAnimation = (callOpts.noAnimation === true),
+			noEvents = (callOpts.noEvents === true);
+
+		// flag defaults to true
+		flag = (flag !== false);
+
+		// node.debug("nodeSetExpanded(" + flag + ")");
+
+		if((node.expanded && flag) || (!node.expanded && !flag)){
+			// Nothing to do
+			// node.debug("nodeSetExpanded(" + flag + "): nothing to do");
+			return _getResolvedPromise(node);
+		}else if(flag && !node.lazy && !node.hasChildren() ){
+			// Prevent expanding of empty nodes
+			// return _getRejectedPromise(node, ["empty"]);
+			return _getResolvedPromise(node);
+		}else if( !flag && node.getLevel() < opts.minExpandLevel ) {
+			// Prevent collapsing locked levels
+			return _getRejectedPromise(node, ["locked"]);
+		}else if ( !noEvents && this._triggerNodeEvent("beforeExpand", node, ctx.originalEvent) === false ){
+			// Callback returned false
+			return _getRejectedPromise(node, ["rejected"]);
+		}
+		// If this node inside a collpased node, no animation and scrolling is needed
+		if( !noAnimation && !node.isVisible() ) {
+			noAnimation = callOpts.noAnimation = true;
+		}
+
+		dfd = new $.Deferred();
+
+		// Auto-collapse mode: collapse all siblings
+		if( flag && !node.expanded && opts.autoCollapse ) {
+			parents = node.getParentList(false, true);
+			prevAC = opts.autoCollapse;
+			try{
+				opts.autoCollapse = false;
+				for(i=0, l=parents.length; i<l; i++){
+					// TODO: should return promise?
+					this._callHook("nodeCollapseSiblings", parents[i], callOpts);
+				}
+			}finally{
+				opts.autoCollapse = prevAC;
+			}
+		}
+		// Trigger expand/collapse after expanding
+		dfd.done(function(){
+			var	lastChild = node.getLastChild();
+
+			if( flag && opts.autoScroll && !noAnimation && lastChild ) {
+				// Scroll down to last child, but keep current node visible
+				lastChild.scrollIntoView(true, {topNode: node}).always(function(){
+					if( !noEvents ) {
+						ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx);
+					}
+				});
+			} else {
+				if( !noEvents ) {
+					ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx);
+				}
+			}
+		});
+		// vvv Code below is executed after loading finished:
+		_afterLoad = function(callback){
+			var cn = opts._classNames,
+				isVisible, isExpanded,
+				effect = opts.toggleEffect;
+
+			node.expanded = flag;
+			// Create required markup, but make sure the top UL is hidden, so we
+			// can animate later
+			tree._callHook("nodeRender", ctx, false, false, true);
+
+			// Hide children, if node is collapsed
+			if( node.ul ) {
+				isVisible = (node.ul.style.display !== "none");
+				isExpanded = !!node.expanded;
+				if ( isVisible === isExpanded ) {
+					node.warn("nodeSetExpanded: UL.style.display already set");
+
+				} else if ( !effect || noAnimation ) {
+					node.ul.style.display = ( node.expanded || !parent ) ? "" : "none";
+
+				} else {
+					// The UI toggle() effect works with the ext-wide extension,
+					// while jQuery.animate() has problems when the title span
+					// has positon: absolute.
+					// Since jQuery UI 1.12, the blind effect requires the parent
+					// element to have 'position: relative'.
+					// See #716, #717
+					$(node.li).addClass(cn.animating);  // #717
+//					node.info("fancytree-animating start: " + node.li.className);
+					$(node.ul)
+						.addClass(cn.animating)  // # 716
+						.toggle(effect.effect, effect.options, effect.duration, function(){
+//							node.info("fancytree-animating end: " + node.li.className);
+							$(this).removeClass(cn.animating);  // #716
+							$(node.li).removeClass(cn.animating);  // #717
+							callback();
+						});
+					return;
+				}
+			}
+			callback();
+		};
+		// ^^^ Code above is executed after loading finshed.
+
+		// Load lazy nodes, if any. Then continue with _afterLoad()
+		if(flag && node.lazy && node.hasChildren() === undefined){
+			// node.debug("nodeSetExpanded: load start...");
+			node.load().done(function(){
+				// node.debug("nodeSetExpanded: load done");
+				if(dfd.notifyWith){ // requires jQuery 1.6+
+					dfd.notifyWith(node, ["loaded"]);
+				}
+				_afterLoad(function () { dfd.resolveWith(node); });
+			}).fail(function(errMsg){
+				_afterLoad(function () { dfd.rejectWith(node, ["load failed (" + errMsg + ")"]); });
+			});
+/*
+			var source = tree._triggerNodeEvent("lazyLoad", node, ctx.originalEvent);
+			_assert(typeof source !== "boolean", "lazyLoad event must return source in data.result");
+			node.debug("nodeSetExpanded: load start...");
+			this._callHook("nodeLoadChildren", ctx, source).done(function(){
+				node.debug("nodeSetExpanded: load done");
+				if(dfd.notifyWith){ // requires jQuery 1.6+
+					dfd.notifyWith(node, ["loaded"]);
+				}
+				_afterLoad.call(tree);
+			}).fail(function(errMsg){
+				dfd.rejectWith(node, ["load failed (" + errMsg + ")"]);
+			});
+*/
+		}else{
+			_afterLoad(function () { dfd.resolveWith(node); });
+		}
+		// node.debug("nodeSetExpanded: returns");
+		return dfd.promise();
+	},
+	/** Focus or blur this node.
+	 * @param {EventData} ctx
+	 * @param {boolean} [flag=true]
+	 */
+	nodeSetFocus: function(ctx, flag) {
+		// ctx.node.debug("nodeSetFocus(" + flag + ")");
+		var ctx2,
+			tree = ctx.tree,
+			node = ctx.node,
+			opts = tree.options,
+			// et = ctx.originalEvent && ctx.originalEvent.type,
+			isInput = ctx.originalEvent ? $(ctx.originalEvent.target).is(":input") : false;
+
+		flag = (flag !== false);
+
+		// (node || tree).debug("nodeSetFocus(" + flag + "), event: " + et + ", isInput: "+ isInput);
+		// Blur previous node if any
+		if(tree.focusNode){
+			if(tree.focusNode === node && flag){
+				// node.debug("nodeSetFocus(" + flag + "): nothing to do");
+				return;
+			}
+			ctx2 = $.extend({}, ctx, {node: tree.focusNode});
+			tree.focusNode = null;
+			this._triggerNodeEvent("blur", ctx2);
+			this._callHook("nodeRenderStatus", ctx2);
+		}
+		// Set focus to container and node
+		if(flag){
+			if( !this.hasFocus() ){
+				node.debug("nodeSetFocus: forcing container focus");
+				this._callHook("treeSetFocus", ctx, true, {calledByNode: true});
+			}
+			node.makeVisible({scrollIntoView: false});
+			tree.focusNode = node;
+			if( opts.titlesTabbable ) {
+				if( !isInput ) { // #621
+					$(node.span).find(".fancytree-title").focus();
+				}
+			} else {
+				// We cannot set KB focus to a node, so use the tree container
+				// #563, #570: IE scrolls on every call to .focus(), if the container
+				// is partially outside the viewport. So do it only, when absolutely
+				// neccessary:
+				if( $(document.activeElement).closest(".fancytree-container").length === 0 ) {
+					$(tree.$container).focus();
+				}
+			}
+			if( opts.aria ){
+				// Set active descendant to node's span ID (create one, if needed)
+				$(tree.$container).attr("aria-activedescendant",
+					$( node.tr || node.li ).uniqueId().attr("id"));
+					// "ftal_" + opts.idPrefix + node.key);
+			}
+//			$(node.span).find(".fancytree-title").focus();
+			this._triggerNodeEvent("focus", ctx);
+//          if( opts.autoActivate ){
+//              tree.nodeSetActive(ctx, true);
+//          }
+			if( opts.autoScroll ){
+				node.scrollIntoView();
+			}
+			this._callHook("nodeRenderStatus", ctx);
+		}
+	},
+	/** (De)Select node, return new status (sync).
+	 *
+	 * @param {EventData} ctx
+	 * @param {boolean} [flag=true]
+	 * @param {object} [opts] additional options. Defaults to {noEvents: false,
+	 *     propagateDown: null, propagateUp: null,
+	 *     callback: null,
+	 *     }
+	 * @returns {boolean} previous status
+	 */
+	nodeSetSelected: function(ctx, flag, callOpts) {
+		callOpts = callOpts || {};
+		var node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options,
+			noEvents = (callOpts.noEvents === true),
+			parent = node.parent;
+
+		// flag defaults to true
+		flag = (flag !== false);
+
+		// node.debug("nodeSetSelected(" + flag + ")", ctx);
+
+		// Cannot (de)select unselectable nodes directly (only by propagation or
+		// by setting the `.selected` property)
+		if( FT.evalOption("unselectable", node, node, opts, false) ){
+			return;
+		}
+
+		// Remember the user's intent, in case down -> up propagation prevents
+		// applying it to node.selected
+		node._lastSelectIntent = flag;
+
+		// Nothing to do?
+		/*jshint -W018 */  // Confusing use of '!'
+		if( !!node.selected === flag ){
+			if( opts.selectMode === 3 && node.partsel && !flag ){
+				// If propagation prevented selecting this node last time, we still
+				// want to allow to apply setSelected(false) now
+			}else{
+				return flag;
+			}
+		}
+		/*jshint +W018 */
+
+		if( !noEvents &&
+			this._triggerNodeEvent("beforeSelect", node, ctx.originalEvent) === false ) {
+				return !!node.selected;
+		}
+		if(flag && opts.selectMode === 1){
+			// single selection mode (we don't uncheck all tree nodes, for performance reasons)
+			if(tree.lastSelectedNode){
+				tree.lastSelectedNode.setSelected(false);
+			}
+			node.selected = flag;
+		}else if(opts.selectMode === 3 && parent && !parent.radiogroup && !node.radiogroup){
+			// multi-hierarchical selection mode
+			node.selected = flag;
+			node.fixSelection3AfterClick(callOpts);
+		}else if(parent && parent.radiogroup){
+			node.visitSiblings(function(n){
+				n._changeSelectStatusAttrs(flag && n === node);
+			}, true);
+		}else{
+			// default: selectMode: 2, multi selection mode
+			node.selected = flag;
+		}
+		this.nodeRenderStatus(ctx);
+		tree.lastSelectedNode = flag ? node : null;
+		if( !noEvents ) {
+			tree._triggerNodeEvent("select", ctx);
+		}
+	},
+	/** Show node status (ok, loading, error, nodata) using styles and a dummy child node.
+	 *
+	 * @param {EventData} ctx
+	 * @param status
+	 * @param message
+	 * @param details
+	 * @since 2.3
+	 */
+	nodeSetStatus: function(ctx, status, message, details) {
+		var node = ctx.node,
+			tree = ctx.tree;
+
+		function _clearStatusNode() {
+			// Remove dedicated dummy node, if any
+			var firstChild = ( node.children ? node.children[0] : null );
+			if ( firstChild && firstChild.isStatusNode() ) {
+				try{
+					// I've seen exceptions here with loadKeyPath...
+					if(node.ul){
+						node.ul.removeChild(firstChild.li);
+						firstChild.li = null; // avoid leaks (DT issue 215)
+					}
+				}catch(e){}
+				if( node.children.length === 1 ){
+					node.children = [];
+				}else{
+					node.children.shift();
+				}
+			}
+		}
+		function _setStatusNode(data, type) {
+			// Create/modify the dedicated dummy node for 'loading...' or
+			// 'error!' status. (only called for direct child of the invisible
+			// system root)
+			var firstChild = ( node.children ? node.children[0] : null );
+			if ( firstChild && firstChild.isStatusNode() ) {
+				$.extend(firstChild, data);
+				firstChild.statusNodeType = type;
+				tree._callHook("nodeRenderTitle", firstChild);
+			} else {
+				node._setChildren([data]);
+				node.children[0].statusNodeType = type;
+				tree.render();
+			}
+			return node.children[0];
+		}
+
+		switch( status ){
+		case "ok":
+			_clearStatusNode();
+			node._isLoading = false;
+			node._error = null;
+			node.renderStatus();
+			break;
+		case "loading":
+			if( !node.parent ) {
+				_setStatusNode({
+					title: tree.options.strings.loading + (message ? " (" + message + ")" : ""),
+					// icon: true,  // needed for 'loding' icon
+					checkbox: false,
+					tooltip: details
+				}, status);
+			}
+			node._isLoading = true;
+			node._error = null;
+			node.renderStatus();
+			break;
+		case "error":
+			_setStatusNode({
+				title: tree.options.strings.loadError + (message ? " (" + message + ")" : ""),
+				// icon: false,
+				checkbox: false,
+				tooltip: details
+			}, status);
+			node._isLoading = false;
+			node._error = { message: message, details: details };
+			node.renderStatus();
+			break;
+		case "nodata":
+			_setStatusNode({
+				title: tree.options.strings.noData,
+				// icon: false,
+				checkbox: false,
+				tooltip: details
+			}, status);
+			node._isLoading = false;
+			node._error = null;
+			node.renderStatus();
+			break;
+		default:
+			$.error("invalid node status " + status);
+		}
+	},
+	/**
+	 *
+	 * @param {EventData} ctx
+	 */
+	nodeToggleExpanded: function(ctx) {
+		return this.nodeSetExpanded(ctx, !ctx.node.expanded);
+	},
+	/**
+	 * @param {EventData} ctx
+	 */
+	nodeToggleSelected: function(ctx) {
+		var node = ctx.node,
+			flag = !node.selected;
+
+		// In selectMode: 3 this node may be unselected+partsel, even if
+		// setSelected(true) was called before, due to `unselectable` children.
+		// In this case, we now toggle as `setSelected(false)`
+		if( node.partsel && !node.selected && node._lastSelectIntent === true ) {
+			flag = false;
+			node.selected = true;  // so it is not considered 'nothing to do'
+		}
+		node._lastSelectIntent = flag;
+		return this.nodeSetSelected(ctx, flag);
+	},
+	/** Remove all nodes.
+	 * @param {EventData} ctx
+	 */
+	treeClear: function(ctx) {
+		var tree = ctx.tree;
+		tree.activeNode = null;
+		tree.focusNode = null;
+		tree.$div.find(">ul.fancytree-container").empty();
+		// TODO: call destructors and remove reference loops
+		tree.rootNode.children = null;
+	},
+	/** Widget was created (called only once, even it re-initialized).
+	 * @param {EventData} ctx
+	 */
+	treeCreate: function(ctx) {
+	},
+	/** Widget was destroyed.
+	 * @param {EventData} ctx
+	 */
+	treeDestroy: function(ctx) {
+		this.$div.find(">ul.fancytree-container").remove();
+		this.$source && this.$source.removeClass("ui-helper-hidden");
+	},
+	/** Widget was (re-)initialized.
+	 * @param {EventData} ctx
+	 */
+	treeInit: function(ctx) {
+		var tree = ctx.tree,
+			opts = tree.options;
+
+		//this.debug("Fancytree.treeInit()");
+		// Add container to the TAB chain
+		// See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant
+		// #577: Allow to set tabindex to "0", "-1" and ""
+		tree.$container.attr("tabindex", opts.tabindex);
+
+		if( opts.rtl ) {
+			tree.$container.attr("DIR", "RTL").addClass("fancytree-rtl");
+		}else{
+			tree.$container.removeAttr("DIR").removeClass("fancytree-rtl");
+		}
+		if( opts.aria ){
+			tree.$container.attr("role", "tree");
+			if( opts.selectMode !== 1 ) {
+				tree.$container.attr("aria-multiselectable", true);
+			}
+		}
+		this.treeLoad(ctx);
+	},
+	/** Parse Fancytree from source, as configured in the options.
+	 * @param {EventData} ctx
+	 * @param {object} [source] optional new source (use last data otherwise)
+	 */
+	treeLoad: function(ctx, source) {
+		var metaData, type, $ul,
+			tree = ctx.tree,
+			$container = ctx.widget.element,
+			dfd,
+			// calling context for root node
+			rootCtx = $.extend({}, ctx, {node: this.rootNode});
+
+		if(tree.rootNode.children){
+			this.treeClear(ctx);
+		}
+		source = source || this.options.source;
+
+		if(!source){
+			type = $container.data("type") || "html";
+			switch(type){
+			case "html":
+				$ul = $container.find(">ul:first");
+				$ul.addClass("ui-fancytree-source ui-helper-hidden");
+				source = $.ui.fancytree.parseHtml($ul);
+				// allow to init tree.data.foo from <ul data-foo=''>
+				this.data = $.extend(this.data, _getElementDataAsDict($ul));
+				break;
+			case "json":
+				source = $.parseJSON($container.text());
+				// $container already contains the <ul>, but we remove the plain (json) text
+				// $container.empty();
+				$container.contents().filter(function(){
+					return (this.nodeType === 3);
+				}).remove();
+				if( $.isPlainObject(source) ){
+					// We got {foo: 'abc', children: [...]}
+					// Copy extra properties to tree.data.foo
+					_assert($.isArray(source.children), "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')");
+					metaData = source;
+					source = source.children;
+					delete metaData.children;
+					$.extend(tree.data, metaData);
+				}
+				break;
+			default:
+				$.error("Invalid data-type: " + type);
+			}
+		}else if(typeof source === "string"){
+			// TODO: source is an element ID
+			$.error("Not implemented");
+		}
+
+		// Trigger fancytreeinit after nodes have been loaded
+		dfd = this.nodeLoadChildren(rootCtx, source).done(function(){
+			tree.render();
+			if( ctx.options.selectMode === 3 ){
+				tree.rootNode.fixSelection3FromEndNodes();
+			}
+			if( tree.activeNode && tree.options.activeVisible ) {
+				tree.activeNode.makeVisible();
+			}
+			tree._triggerTreeEvent("init", null, { status: true });
+		}).fail(function(){
+			tree.render();
+			tree._triggerTreeEvent("init", null, { status: false });
+		});
+		return dfd;
+	},
+	/** Node was inserted into or removed from the tree.
+	 * @param {EventData} ctx
+	 * @param {boolean} add
+	 * @param {FancytreeNode} node
+	 */
+	treeRegisterNode: function(ctx, add, node) {
+	},
+	/** Widget got focus.
+	 * @param {EventData} ctx
+	 * @param {boolean} [flag=true]
+	 */
+	treeSetFocus: function(ctx, flag, callOpts) {
+		var targetNode;
+
+		flag = (flag !== false);
+
+		// this.debug("treeSetFocus(" + flag + "), callOpts: ", callOpts, this.hasFocus());
+		// this.debug("    focusNode: " + this.focusNode);
+		// this.debug("    activeNode: " + this.activeNode);
+		if( flag !== this.hasFocus() ){
+			this._hasFocus = flag;
+			if( !flag && this.focusNode ) {
+				// Node also looses focus if widget blurs
+				this.focusNode.setFocus(false);
+			} else if ( flag && (!callOpts || !callOpts.calledByNode) ) {
+				$(this.$container).focus();
+			}
+			this.$container.toggleClass("fancytree-treefocus", flag);
+			this._triggerTreeEvent(flag ? "focusTree" : "blurTree");
+			if( flag && !this.activeNode ) {
+				// #712: Use last mousedowned node ('click' event fires after focusin)
+				targetNode = this._lastMousedownNode || this.getFirstChild();
+				targetNode && targetNode.setFocus();
+			}
+		}
+	},
+	/** Widget option was set using `$().fancytree("option", "foo", "bar")`.
+	 * @param {EventData} ctx
+	 * @param {string} key option name
+	 * @param {any} value option value
+	 */
+	treeSetOption: function(ctx, key, value) {
+		var tree = ctx.tree,
+			callDefault = true,
+			callCreate = false,
+			callRender = false;
+
+		switch( key ) {
+		case "aria":
+		case "checkbox":
+		case "icon":
+		case "minExpandLevel":
+		case "tabindex":
+			// tree._callHook("treeCreate", tree);
+			callCreate = true;
+			callRender = true;
+			break;
+		case "escapeTitles":
+		case "tooltip":
+			callRender = true;
+			break;
+		case "rtl":
+			if( value === false ) {
+				tree.$container.removeAttr("DIR").removeClass("fancytree-rtl");
+			}else{
+				tree.$container.attr("DIR", "RTL").addClass("fancytree-rtl");
+			}
+			callRender = true;
+			break;
+		case "source":
+			callDefault = false;
+			tree._callHook("treeLoad", tree, value);
+			callRender = true;
+			break;
+		}
+		tree.debug("set option " + key + "=" + value + " <" + typeof(value) + ">");
+		if(callDefault){
+			if( this.widget._super ) {
+				// jQuery UI 1.9+
+				this.widget._super.call( this.widget, key, value );
+			} else {
+				// jQuery UI <= 1.8, we have to manually invoke the _setOption method from the base widget
+				$.Widget.prototype._setOption.call(this.widget, key, value);
+			}
+		}
+		if(callCreate){
+			tree._callHook("treeCreate", tree);
+		}
+		if(callRender){
+			tree.render(true, false);  // force, not-deep
+		}
+	}
+});
+
+
+/* ******************************************************************************
+ * jQuery UI widget boilerplate
+ */
+
+/**
+ * The plugin (derrived from <a href=" http://api.jqueryui.com/jQuery.widget/">jQuery.Widget</a>).<br>
+ * This constructor is not called directly. Use `$(selector).fancytree({})`
+ * to initialize the plugin instead.<br>
+ * <pre class="sh_javascript sunlight-highlight-javascript">// Access widget methods and members:
+ * var tree = $("#tree").fancytree("getTree");
+ * var node = $("#tree").fancytree("getActiveNode", "1234");
+ * </pre>
+ *
+ * @mixin Fancytree_Widget
+ */
+
+$.widget("ui.fancytree",
+	/** @lends Fancytree_Widget# */
+	{
+	/**These options will be used as defaults
+	 * @type {FancytreeOptions}
+	 */
+	options:
+	{
+		activeVisible: true,
+		ajax: {
+			type: "GET",
+			cache: false, // false: Append random '_' argument to the request url to prevent caching.
+//          timeout: 0, // >0: Make sure we get an ajax error if server is unreachable
+			dataType: "json" // Expect json format and pass json object to callbacks.
+		},  //
+		aria: true,
+		autoActivate: true,
+		autoCollapse: false,
+		autoScroll: false,
+		checkbox: false,
+		clickFolderMode: 4,
+		debugLevel: null, // 0..2 (null: use global setting $.ui.fancytree.debugInfo)
+		disabled: false, // TODO: required anymore?
+		enableAspx: true,
+		escapeTitles: false,
+		extensions: [],
+		// fx: { height: "toggle", duration: 200 },
+		// toggleEffect: { effect: "drop", options: {direction: "left"}, duration: 200 },
+		// toggleEffect: { effect: "slide", options: {direction: "up"}, duration: 200 },
+		toggleEffect: { effect: "blind", options: {direction: "vertical", scale: "box"}, duration: 200 },
+		generateIds: false,
+		icon: true,
+		idPrefix: "ft_",
+		focusOnSelect: false,
+		keyboard: true,
+		keyPathSeparator: "/",
+		minExpandLevel: 1,
+		quicksearch: false,
+		rtl: false,
+		scrollOfs: {top: 0, bottom: 0},
+		scrollParent: null,
+		selectMode: 2,
+		strings: {
+			loading: "Loading...",  // &#8230; would be escaped when escapeTitles is true
+			loadError: "Load error!",
+			moreData: "More...",
+			noData: "No data."
+		},
+		tabindex: "0",
+		titlesTabbable: false,
+		tooltip: false,
+		_classNames: {
+			node: "fancytree-node",
+			folder: "fancytree-folder",
+			animating: "fancytree-animating",
+			combinedExpanderPrefix: "fancytree-exp-",
+			combinedIconPrefix: "fancytree-ico-",
+			hasChildren: "fancytree-has-children",
+			active: "fancytree-active",
+			selected: "fancytree-selected",
+			expanded: "fancytree-expanded",
+			lazy: "fancytree-lazy",
+			focused: "fancytree-focused",
+			partload: "fancytree-partload",
+			partsel: "fancytree-partsel",
+			radio: "fancytree-radio",
+			// radiogroup: "fancytree-radiogroup",
+			unselectable: "fancytree-unselectable",
+			lastsib: "fancytree-lastsib",
+			loading: "fancytree-loading",
+			error: "fancytree-error",
+			statusNodePrefix: "fancytree-statusnode-"
+		},
+		// events
+		lazyLoad: null,
+		postProcess: null
+	},
+	/* Set up the widget, Called on first $().fancytree() */
+	_create: function() {
+		this.tree = new Fancytree(this);
+
+		this.$source = this.source || this.element.data("type") === "json" ? this.element
+			: this.element.find(">ul:first");
+		// Subclass Fancytree instance with all enabled extensions
+		var extension, extName, i,
+			opts = this.options,
+			extensions = opts.extensions,
+			base = this.tree;
+
+		for(i=0; i<extensions.length; i++){
+			extName = extensions[i];
+			extension = $.ui.fancytree._extensions[extName];
+			if(!extension){
+				$.error("Could not apply extension '" + extName + "' (it is not registered, did you forget to include it?)");
+			}
+			// Add extension options as tree.options.EXTENSION
+//			_assert(!this.tree.options[extName], "Extension name must not exist as option name: " + extName);
+			this.tree.options[extName] = $.extend(true, {}, extension.options, this.tree.options[extName]);
+			// Add a namespace tree.ext.EXTENSION, to hold instance data
+			_assert(this.tree.ext[extName] === undefined, "Extension name must not exist as Fancytree.ext attribute: '" + extName + "'");
+//			this.tree[extName] = extension;
+			this.tree.ext[extName] = {};
+			// Subclass Fancytree methods using proxies.
+			_subclassObject(this.tree, base, extension, extName);
+			// current extension becomes base for the next extension
+			base = extension;
+		}
+		//
+		if( opts.icons !== undefined ) {  // 2015-11-16
+			if( opts.icon !== true ) {
+				$.error("'icons' tree option is deprecated since v2.14.0: use 'icon' only instead");
+			} else {
+				this.tree.warn("'icons' tree option is deprecated since v2.14.0: use 'icon' instead");
+				opts.icon = opts.icons;
+			}
+		}
+		if( opts.iconClass !== undefined ) {  // 2015-11-16
+			if( opts.icon ) {
+				$.error("'iconClass' tree option is deprecated since v2.14.0: use 'icon' only instead");
+			} else {
+				this.tree.warn("'iconClass' tree option is deprecated since v2.14.0: use 'icon' instead");
+				opts.icon = opts.iconClass;
+			}
+		}
+		if( opts.tabbable !== undefined ) {  // 2016-04-04
+			opts.tabindex = opts.tabbable ? "0" : "-1";
+			this.tree.warn("'tabbable' tree option is deprecated since v2.17.0: use 'tabindex='" + opts.tabindex + "' instead");
+		}
+		//
+		this.tree._callHook("treeCreate", this.tree);
+		// Note: 'fancytreecreate' event is fired by widget base class
+//        this.tree._triggerTreeEvent("create");
+	},
+
+	/* Called on every $().fancytree() */
+	_init: function() {
+		this.tree._callHook("treeInit", this.tree);
+		// TODO: currently we call bind after treeInit, because treeInit
+		// might change tree.$container.
+		// It would be better, to move ebent binding into hooks altogether
+		this._bind();
+	},
+
+	/* Use the _setOption method to respond to changes to options */
+	_setOption: function(key, value) {
+		return this.tree._callHook("treeSetOption", this.tree, key, value);
+	},
+
+	/** Use the destroy method to clean up any modifications your widget has made to the DOM */
+	destroy: function() {
+		this._unbind();
+		this.tree._callHook("treeDestroy", this.tree);
+		// In jQuery UI 1.8, you must invoke the destroy method from the base widget
+		$.Widget.prototype.destroy.call(this);
+		// TODO: delete tree and nodes to make garbage collect easier?
+		// TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method
+	},
+
+	// -------------------------------------------------------------------------
+
+	/* Remove all event handlers for our namespace */
+	_unbind: function() {
+		var ns = this.tree._ns;
+		this.element.off(ns);
+		this.tree.$container.off(ns);
+		$(document).off(ns);
+	},
+	/* Add mouse and kyboard handlers to the container */
+	_bind: function() {
+		var that = this,
+			opts = this.options,
+			tree = this.tree,
+			ns = tree._ns
+			// selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" )
+			;
+
+		// Remove all previuous handlers for this tree
+		this._unbind();
+
+		//alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container);
+		// tree.debug("bind events; container: ", tree.$container);
+		tree.$container.on("focusin" + ns + " focusout" + ns, function(event){
+			var node = FT.getNode(event),
+				flag = (event.type === "focusin");
+
+			// tree.treeOnFocusInOut.call(tree, event);
+			// tree.debug("Tree container got event " + event.type, node, event, FT.getEventTarget(event));
+			if( flag && tree._getExpiringValue("focusin") ) {
+				// #789: IE 11 may send duplicate focusin events
+				FT.info("Ignored double focusin.");
+				return;
+			}
+			tree._setExpiringValue("focusin", true, 50);
+
+			if( flag && !node ) {
+				// #789: IE 11 may send focusin before mousdown(?)
+				node = tree._getExpiringValue("mouseDownNode");
+				if( node ) { FT.info("Reconstruct mouse target for focusin from recent event."); }
+			}
+			if(node){
+				// For example clicking into an <input> that is part of a node
+				tree._callHook("nodeSetFocus", tree._makeHookContext(node, event), flag);
+			}else{
+				if( tree.tbody && $(event.target).parents("table.fancytree-container > thead").length ) {
+					// #767: ignore events in the table's header
+					tree.debug("Ignore focus event outside table body.", event);
+				} else {
+					tree._callHook("treeSetFocus", tree, flag);
+				}
+			}
+
+		}).on("selectstart" + ns, "span.fancytree-title", function(event){
+			// prevent mouse-drags to select text ranges
+			// tree.debug("<span title> got event " + event.type);
+			event.preventDefault();
+
+		}).on("keydown" + ns, function(event){
+			// TODO: also bind keyup and keypress
+			// tree.debug("got event " + event.type + ", hasFocus:" + tree.hasFocus());
+			// if(opts.disabled || opts.keyboard === false || !tree.hasFocus() ){
+			if(opts.disabled || opts.keyboard === false ){
+				return true;
+			}
+			var res,
+				node = tree.focusNode, // node may be null
+				ctx = tree._makeHookContext(node || tree, event),
+				prevPhase = tree.phase;
+
+			try {
+				tree.phase = "userEvent";
+				// If a 'fancytreekeydown' handler returns false, skip the default
+				// handling (implemented by tree.nodeKeydown()).
+				if(node){
+					res = tree._triggerNodeEvent("keydown", node, event);
+				}else{
+					res = tree._triggerTreeEvent("keydown", event);
+				}
+				if ( res === "preventNav" ){
+					res = true; // prevent keyboard navigation, but don't prevent default handling of embedded input controls
+				} else if ( res !== false ){
+					res = tree._callHook("nodeKeydown", ctx);
+				}
+				return res;
+			} finally {
+				tree.phase = prevPhase;
+			}
+
+		}).on("mousedown" + ns, function(event){
+			var et = FT.getEventTarget(event);
+			// that.tree.debug("event(" + event.type + "): node: ", et.node);
+			// #712: Store the clicked node, so we can use it when we get a focusin event
+			//       ('click' event fires after focusin)
+			// tree.debug("event(" + event.type + "): node: ", et.node);
+			tree._lastMousedownNode = et ? et.node : null;
+			// #789: Store the node also for a short period, so we can use it
+			// in a *resulting* focusin event
+			tree._setExpiringValue("mouseDownNode", tree._lastMousedownNode);
+
+		}).on("click" + ns + " dblclick" + ns, function(event){
+			if(opts.disabled){
+				return true;
+			}
+			var ctx,
+				et = FT.getEventTarget(event),
+				node = et.node,
+				tree = that.tree,
+				prevPhase = tree.phase;
+
+			// that.tree.debug("event(" + event.type + "): node: ", node);
+			if( !node ){
+				return true;  // Allow bubbling of other events
+			}
+			ctx = tree._makeHookContext(node, event);
+			// that.tree.debug("event(" + event.type + "): node: ", node);
+			try {
+				tree.phase = "userEvent";
+				switch(event.type) {
+				case "click":
+					ctx.targetType = et.type;
+					if( node.isPagingNode() ) {
+						return tree._triggerNodeEvent("clickPaging", ctx, event) === true;
+					}
+					return ( tree._triggerNodeEvent("click", ctx, event) === false ) ? false : tree._callHook("nodeClick", ctx);
+				case "dblclick":
+					ctx.targetType = et.type;
+					return ( tree._triggerNodeEvent("dblclick", ctx, event) === false ) ? false : tree._callHook("nodeDblclick", ctx);
+				}
+			} finally {
+				tree.phase = prevPhase;
+			}
+		});
+	},
+	/** Return the active node or null.
+	 * @returns {FancytreeNode}
+	 */
+	getActiveNode: function() {
+		return this.tree.activeNode;
+	},
+	/** Return the matching node or null.
+	 * @param {string} key
+	 * @returns {FancytreeNode}
+	 */
+	getNodeByKey: function(key) {
+		return this.tree.getNodeByKey(key);
+	},
+	/** Return the invisible system root node.
+	 * @returns {FancytreeNode}
+	 */
+	getRootNode: function() {
+		return this.tree.rootNode;
+	},
+	/** Return the current tree instance.
+	 * @returns {Fancytree}
+	 */
+	getTree: function() {
+		return this.tree;
+	}
+});
+
+// $.ui.fancytree was created by the widget factory. Create a local shortcut:
+FT = $.ui.fancytree;
+
+/**
+ * Static members in the `$.ui.fancytree` namespace.<br>
+ * <br>
+ * <pre class="sh_javascript sunlight-highlight-javascript">// Access static members:
+ * var node = $.ui.fancytree.getNode(element);
+ * alert($.ui.fancytree.version);
+ * </pre>
+ *
+ * @mixin Fancytree_Static
+ */
+$.extend($.ui.fancytree,
+	/** @lends Fancytree_Static# */
+	{
+	/** @type {string} */
+	version: "2.26.0",      // Set to semver by 'grunt release'
+	/** @type {string} */
+	buildType: "production", // Set to 'production' by 'grunt build'
+	/** @type {int} */
+	debugLevel: 1,            // Set to 1 by 'grunt build'
+							  // Used by $.ui.fancytree.debug() and as default for tree.options.debugLevel
+
+	_nextId: 1,
+	_nextNodeKey: 1,
+	_extensions: {},
+	// focusTree: null,
+
+	/** Expose class object as $.ui.fancytree._FancytreeClass */
+	_FancytreeClass: Fancytree,
+	/** Expose class object as $.ui.fancytree._FancytreeNodeClass */
+	_FancytreeNodeClass: FancytreeNode,
+	/* Feature checks to provide backwards compatibility */
+	jquerySupports: {
+		// http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at
+		positionMyOfs: isVersionAtLeast($.ui.version, 1, 9)
+		},
+	/** Throw an error if condition fails (debug method).
+	 * @param {boolean} cond
+	 * @param {string} msg
+	 */
+	assert: function(cond, msg){
+		return _assert(cond, msg);
+	},
+	/** Create a new Fancytree instance on a target element.
+	 *
+	 * @param {Element | jQueryObject | string} el Target DOM element or selector
+	 * @param {FancytreeOptions} [opts] Fancytree options
+	 * @returns {Fancytree} new tree instance
+	 * @example
+	 * var tree = $.ui.fancytree.createTree("#tree", {
+	 *     source: {url: "my/webservice"}
+	 * }); // Create tree for this matching element
+	 *
+	 * @since 2.25
+	 */
+	createTree: function(el, opts){
+		var tree = $(el).fancytree(opts).fancytree("getTree");
+		return tree;
+	},
+	/** Return a function that executes *fn* at most every *timeout* ms.
+	 * @param {integer} timeout
+	 * @param {function} fn
+	 * @param {boolean} [invokeAsap=false]
+	 * @param {any} [ctx]
+	 */
+	debounce: function(timeout, fn, invokeAsap, ctx) {
+		var timer;
+		if(arguments.length === 3 && typeof invokeAsap !== "boolean") {
+			ctx = invokeAsap;
+			invokeAsap = false;
+		}
+		return function() {
+			var args = arguments;
+			ctx = ctx || this;
+			invokeAsap && !timer && fn.apply(ctx, args);
+			clearTimeout(timer);
+			timer = setTimeout(function() {
+				invokeAsap || fn.apply(ctx, args);
+				timer = null;
+			}, timeout);
+		};
+	},
+	/** Write message to console if debugLevel >= 2
+	 * @param {string} msg
+	 */
+	debug: function(msg){
+		/*jshint expr:true */
+		($.ui.fancytree.debugLevel >= 2) && consoleApply("log", arguments);
+	},
+	/** Write error message to console.
+	 * @param {string} msg
+	 */
+	error: function(msg){
+		consoleApply("error", arguments);
+	},
+	/** Convert &lt;, &gt;, &amp;, &quot;, &#39;, &#x2F; to the equivalent entities.
+	 *
+	 * @param {string} s
+	 * @returns {string}
+	 */
+	escapeHtml: function(s){
+		return ("" + s).replace(REX_HTML, function(s) {
+			return ENTITY_MAP[s];
+		});
+	},
+	/** Make jQuery.position() arguments backwards compatible, i.e. if
+	 * jQuery UI version <= 1.8, convert
+	 *   { my: "left+3 center", at: "left bottom", of: $target }
+	 * to
+	 *   { my: "left center", at: "left bottom", of: $target, offset: "3  0" }
+	 *
+	 * See http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at
+	 * and http://jsfiddle.net/mar10/6xtu9a4e/
+	 */
+	fixPositionOptions: function(opts) {
+		if( opts.offset || ("" + opts.my + opts.at ).indexOf("%") >= 0 ) {
+		   $.error("expected new position syntax (but '%' is not supported)");
+		}
+		if( ! $.ui.fancytree.jquerySupports.positionMyOfs ) {
+			var // parse 'left+3 center' into ['left+3 center', 'left', '+3', 'center', undefined]
+				myParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec(opts.my),
+				atParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec(opts.at),
+				// convert to numbers
+				dx = (myParts[2] ? (+myParts[2]) : 0) + (atParts[2] ? (+atParts[2]) : 0),
+				dy = (myParts[4] ? (+myParts[4]) : 0) + (atParts[4] ? (+atParts[4]) : 0);
+
+			opts = $.extend({}, opts, { // make a copy and overwrite
+				my: myParts[1] + " " + myParts[3],
+				at: atParts[1] + " " + atParts[3]
+			});
+			if( dx || dy ) {
+				opts.offset = "" + dx + " " + dy;
+			}
+		}
+		return opts;
+	},
+	/** Return a {node: FancytreeNode, type: TYPE} object for a mouse event.
+	 *
+	 * @param {Event} event Mouse event, e.g. click, ...
+	 * @returns {object} Return a {node: FancytreeNode, type: TYPE} object
+	 *     TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
+	 */
+	getEventTarget: function(event){
+		var tcn = event && event.target ? event.target.className : "",
+			res = {node: this.getNode(event.target), type: undefined};
+		// We use a fast version of $(res.node).hasClass()
+		// See http://jsperf.com/test-for-classname/2
+		if( /\bfancytree-title\b/.test(tcn) ){
+			res.type = "title";
+		}else if( /\bfancytree-expander\b/.test(tcn) ){
+			res.type = (res.node.hasChildren() === false ? "prefix" : "expander");
+		// }else if( /\bfancytree-checkbox\b/.test(tcn) || /\bfancytree-radio\b/.test(tcn) ){
+		}else if( /\bfancytree-checkbox\b/.test(tcn) ){
+			res.type = "checkbox";
+		}else if( /\bfancytree(-custom)?-icon\b/.test(tcn) ){
+			res.type = "icon";
+		}else if( /\bfancytree-node\b/.test(tcn) ){
+			// Somewhere near the title
+			res.type = "title";
+		}else if( event && $(event.target).is("ul[role=group]") ) {
+			// #nnn: Clicking right to a node may hit the surrounding UL
+			FT.info("Ignoring click on outer UL.");
+			res.node = null;
+		}else if( event && event.target && $(event.target).closest(".fancytree-title").length ) {
+			// #228: clicking an embedded element inside a title
+			res.type = "title";
+		}
+		return res;
+	},
+	/** Return a string describing the affected node region for a mouse event.
+	 *
+	 * @param {Event} event Mouse event, e.g. click, mousemove, ...
+	 * @returns {string} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
+	 */
+	getEventTargetType: function(event){
+		return this.getEventTarget(event).type;
+	},
+	/** Return a FancytreeNode instance from element, event, or jQuery object.
+	 *
+	 * @param {Element | jQueryObject | Event} el
+	 * @returns {FancytreeNode} matching node or null
+	 */
+	getNode: function(el){
+		if(el instanceof FancytreeNode){
+			return el; // el already was a FancytreeNode
+		}else if( el instanceof $ ){
+			el = el[0]; // el was a jQuery object: use the DOM element
+		}else if(el.originalEvent !== undefined){
+			el = el.target; // el was an Event
+		}
+		while( el ) {
+			if(el.ftnode) {
+				return el.ftnode;
+			}
+			el = el.parentNode;
+		}
+		return null;
+	},
+	/** Return a Fancytree instance, from element, index, event, or jQueryObject.
+	 *
+	 * @param {Element | jQueryObject | Event | integer | string} [el]
+	 * @returns {Fancytree} matching tree or null
+	 * @example
+	 * $.ui.fancytree.getTree();   // Get first Fancytree instance on page
+	 * $.ui.fancytree.getTree(1);  // Get second Fancytree instance on page
+	 * $.ui.fancytree.getTree("#tree"); // Get tree for this matching element
+	 *
+	 * @since 2.13
+	 */
+	getTree: function(el){
+		var widget;
+
+		if( el instanceof Fancytree ) {
+			return el; // el already was a Fancytree
+		}
+		if( el === undefined ) {
+			el = 0;  // get first tree
+		}
+		if( typeof el === "number" ) {
+			el = $(".fancytree-container").eq(el); // el was an integer: return nth instance
+		} else if( typeof el === "string" ) {
+			el = $(el).eq(0); // el was a selector: use first match
+		} else if( el.selector !== undefined ) {
+			el = el.eq(0); // el was a jQuery object: use the first DOM element
+		} else if( el.originalEvent !== undefined ) {
+			el = $(el.target); // el was an Event
+		}
+		el = el.closest(":ui-fancytree");
+		widget = el.data("ui-fancytree") || el.data("fancytree"); // the latter is required by jQuery <= 1.8
+		return widget ? widget.tree : null;
+	},
+	/** Return an option value that has a default, but may be overridden by a
+	 * callback or a node instance attribute.
+	 *
+	 * Evaluation sequence:<br>
+	 *
+	 * If tree.options.<optionName> is a callback that returns something, use that.<br>
+	 * Else if node.<optionName> is defined, use that.<br>
+	 * Else if tree.options.<optionName> is a value, use that.<br>
+	 * Else use `defaultValue`.
+	 *
+	 * @param {string} optionName name of the option property (on node and tree)
+	 * @param {FancytreeNode} node passed to the callback
+	 * @param {object} nodeObject where to look for the local option property, e.g. `node` or `node.data`
+	 * @param {object} treeOption where to look for the tree option, e.g. `tree.options` or `tree.options.dnd5`
+	 * @param {any} [defaultValue]
+	 * @returns {any}
+	 *
+	 * @example
+	 * // Check for node.foo, tree,options.foo(), and tree.options.foo:
+	 * $.ui.fancytree.evalOption("foo", node, node, tree.options);
+	 * // Check for node.data.bar, tree,options.qux.bar(), and tree.options.qux.bar:
+	 * $.ui.fancytree.evalOption("bar", node, node.data, tree.options.qux);
+	 *
+	 * @since 2.22
+	 */
+	evalOption: function(optionName, node, nodeObject, treeOptions, defaultValue) {
+		var ctx, res,
+			tree = node.tree,
+			treeOpt = treeOptions[optionName],
+			nodeOpt = nodeObject[optionName];
+
+		if( $.isFunction(treeOpt) ) {
+			ctx = { node: node, tree: tree, widget: tree.widget, options: tree.widget.options };
+			res = treeOpt.call(tree, {type: optionName}, ctx);
+			if( res == null ) {
+				res = nodeOpt;
+			}
+		} else {
+			res = (nodeOpt != null) ? nodeOpt : treeOpt;
+		}
+		if( res == null ) {
+			res = defaultValue;  // no option set at all: return default
+		}
+		return res;
+	},
+	/** Convert a keydown or mouse event to a canonical string like 'ctrl+a',
+	 * 'ctrl+shift+f2', 'shift+leftdblclick'.
+	 *
+	 * This is especially handy for switch-statements in event handlers.
+	 *
+	 * @param {event}
+	 * @returns {string}
+	 *
+	 * @example
+
+	switch( $.ui.fancytree.eventToString(event) ) {
+		case "-":
+			tree.nodeSetExpanded(ctx, false);
+			break;
+		case "shift+return":
+			tree.nodeSetActive(ctx, true);
+			break;
+		case "down":
+			res = node.navigate(event.which, activate, true);
+			break;
+		default:
+			handled = false;
+	}
+	if( handled ){
+		event.preventDefault();
+	}
+	 */
+	eventToString: function(event) {
+		// Poor-man's hotkeys. See here for a complete implementation:
+		//   https://github.com/jeresig/jquery.hotkeys
+		var which = event.which,
+			et = event.type,
+			s = [];
+
+		if( event.altKey ) { s.push("alt"); }
+		if( event.ctrlKey ) { s.push("ctrl"); }
+		if( event.metaKey ) { s.push("meta"); }
+		if( event.shiftKey ) { s.push("shift"); }
+
+		if( et === "click" || et === "dblclick" ) {
+			s.push(MOUSE_BUTTONS[event.button] + et);
+		} else {
+			if( !IGNORE_KEYCODES[which] ) {
+				s.push( SPECIAL_KEYCODES[which] || String.fromCharCode(which).toLowerCase() );
+			}
+		}
+		return s.join("+");
+	},
+	/** Write message to console if debugLevel >= 1
+	 * @param {string} msg
+	 */
+	info: function(msg){
+		/*jshint expr:true */
+		($.ui.fancytree.debugLevel >= 1) && consoleApply("info", arguments);
+	},
+	/* @deprecated: use eventToString(event) instead.
+	 */
+	keyEventToString: function(event) {
+		this.warn("keyEventToString() is deprecated: use eventToString()");
+		return this.eventToString(event);
+	},
+	/** Return a wrapped handler method, that provides `this.super`.
+	 *
+	 * @example
+		// Implement `opts.createNode` event to add the 'draggable' attribute
+		$.ui.fancytree.overrideMethod(ctx.options, "createNode", function(event, data) {
+			// Default processing if any
+			this._super.apply(this, arguments);
+			// Add 'draggable' attribute
+			data.node.span.draggable = true;
+		});
+	 *
+	 * @param {object} instance
+	 * @param {string} methodName
+	 * @param {function} handler
+	 */
+	overrideMethod: function(instance, methodName, handler){
+		var prevSuper,
+			_super = instance[methodName] || $.noop;
+
+		// context = context || this;
+
+		instance[methodName] = function() {
+			try {
+				prevSuper = this._super;
+				this._super = _super;
+				return handler.apply(this, arguments);
+			} finally {
+				this._super = prevSuper;
+			}
+		};
+	},
+	/**
+	 * Parse tree data from HTML <ul> markup
+	 *
+	 * @param {jQueryObject} $ul
+	 * @returns {NodeData[]}
+	 */
+	parseHtml: function($ul) {
+		// TODO: understand this:
+		/*jshint validthis:true */
+		var classes, className, extraClasses, i, iPos, l, tmp, tmp2,
+			$children = $ul.find(">li"),
+			children = [];
+
+		$children.each(function() {
+			var allData, lowerCaseAttr,
+				$li = $(this),
+				$liSpan = $li.find(">span:first", this),
+				$liA = $liSpan.length ? null : $li.find(">a:first"),
+				d = { tooltip: null, data: {} };
+
+			if( $liSpan.length ) {
+				d.title = $liSpan.html();
+
+			} else if( $liA && $liA.length ) {
+				// If a <li><a> tag is specified, use it literally and extract href/target.
+				d.title = $liA.html();
+				d.data.href = $liA.attr("href");
+				d.data.target = $liA.attr("target");
+				d.tooltip = $liA.attr("title");
+
+			} else {
+				// If only a <li> tag is specified, use the trimmed string up to
+				// the next child <ul> tag.
+				d.title = $li.html();
+				iPos = d.title.search(/<ul/i);
+				if( iPos >= 0 ){
+					d.title = d.title.substring(0, iPos);
+				}
+			}
+			d.title = $.trim(d.title);
+
+			// Make sure all fields exist
+			for(i=0, l=CLASS_ATTRS.length; i<l; i++){
+				d[CLASS_ATTRS[i]] = undefined;
+			}
+			// Initialize to `true`, if class is set and collect extraClasses
+			classes = this.className.split(" ");
+			extraClasses = [];
+			for(i=0, l=classes.length; i<l; i++){
+				className = classes[i];
+				if(CLASS_ATTR_MAP[className]){
+					d[className] = true;
+				}else{
+					extraClasses.push(className);
+				}
+			}
+			d.extraClasses = extraClasses.join(" ");
+
+			// Parse node options from ID, title and class attributes
+			tmp = $li.attr("title");
+			if( tmp ){
+				d.tooltip = tmp; // overrides <a title='...'>
+			}
+			tmp = $li.attr("id");
+			if( tmp ){
+				d.key = tmp;
+			}
+			// Translate hideCheckbox -> checkbox:false
+			if( $li.attr("hideCheckbox") ){
+				d.checkbox = false;
+			}
+			// Add <li data-NAME='...'> as node.data.NAME
+			allData = _getElementDataAsDict($li);
+			if( allData && !$.isEmptyObject(allData) ) {
+				// #507: convert data-hidecheckbox (lower case) to hideCheckbox
+				for( lowerCaseAttr in NODE_ATTR_LOWERCASE_MAP ) {
+					if( allData.hasOwnProperty(lowerCaseAttr) ) {
+						allData[NODE_ATTR_LOWERCASE_MAP[lowerCaseAttr]] = allData[lowerCaseAttr];
+						delete allData[lowerCaseAttr];
+					}
+				}
+				// #56: Allow to set special node.attributes from data-...
+				for(i=0, l=NODE_ATTRS.length; i<l; i++){
+					tmp = NODE_ATTRS[i];
+					tmp2 = allData[tmp];
+					if( tmp2 != null ) {
+						delete allData[tmp];
+						d[tmp] = tmp2;
+					}
+				}
+				// All other data-... goes to node.data...
+				$.extend(d.data, allData);
+			}
+			// Recursive reading of child nodes, if LI tag contains an UL tag
+			$ul = $li.find(">ul:first");
+			if( $ul.length ) {
+				d.children = $.ui.fancytree.parseHtml($ul);
+			}else{
+				d.children = d.lazy ? undefined : null;
+			}
+			children.push(d);
+//            FT.debug("parse ", d, children);
+		});
+		return children;
+	},
+	/** Add Fancytree extension definition to the list of globally available extensions.
+	 *
+	 * @param {object} definition
+	 */
+	registerExtension: function(definition){
+		_assert(definition.name != null, "extensions must have a `name` property.");
+		_assert(definition.version != null, "extensions must have a `version` property.");
+		$.ui.fancytree._extensions[definition.name] = definition;
+	},
+	/** Inverse of escapeHtml().
+	 *
+	 * @param {string} s
+	 * @returns {string}
+	 */
+	unescapeHtml: function(s){
+		var e = document.createElement("div");
+		e.innerHTML = s;
+		return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
+	},
+	/** Write warning message to console.
+	 * @param {string} msg
+	 */
+	warn: function(msg){
+		consoleApply("warn", arguments);
+	}
+});
+
+// Value returned by `require('jquery.fancytree')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.childcounter.js' */// Extending Fancytree
+// ===================
+//
+// See also the [live demo](http://wwwendt.de/tech/fancytree/demo/sample-ext-childcounter.html) of this code.
+//
+// Every extension should have a comment header containing some information
+// about the author, copyright and licensing. Also a pointer to the latest
+// source code.
+// Prefix with `/*!` so the comment is not removed by the minifier.
+
+/*!
+ * jquery.fancytree.childcounter.js
+ *
+ * Add a child counter bubble to tree nodes.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+// To keep the global namespace clean, we wrap everything in a closure.
+// The UMD wrapper pattern defines the dependencies on jQuery and the
+// Fancytree core module, and makes sure that we can use the `require()`
+// syntax with package loaders.
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+// Consider to use [strict mode](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/)
+"use strict";
+
+// The [coding guidelines](http://contribute.jquery.org/style-guide/js/)
+// require jshint compliance.
+// But for this sample, we want to allow unused variables for demonstration purpose.
+
+/*jshint unused:false */
+
+
+// Adding methods
+// --------------
+
+// New member functions can be added to the `Fancytree` class.
+// This function will be available for every tree instance:
+//
+//     var tree = $("#tree").fancytree("getTree");
+//     tree.countSelected(false);
+
+$.ui.fancytree._FancytreeClass.prototype.countSelected = function(topOnly){
+	var tree = this,
+		treeOptions = tree.options;
+
+	return tree.getSelectedNodes(topOnly).length;
+};
+
+
+// The `FancytreeNode` class can also be easily extended. This would be called
+// like
+//     node.updateCounters();
+//
+// It is also good practice to add a docstring comment.
+/**
+ * [ext-childcounter] Update counter badges for `node` and its parents.
+ * May be called in the `loadChildren` event, to update parents of lazy loaded
+ * nodes.
+ * @alias FancytreeNode#updateCounters
+ * @requires jquery.fancytree.childcounters.js
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.updateCounters = function(){
+	var node = this,
+		$badge = $("span.fancytree-childcounter", node.span),
+		extOpts = node.tree.options.childcounter,
+		count = node.countChildren(extOpts.deep);
+
+	node.data.childCounter = count;
+	if( (count || !extOpts.hideZeros) && (!node.isExpanded() || !extOpts.hideExpanded) ) {
+		if( !$badge.length ) {
+			$badge = $("<span class='fancytree-childcounter'/>").appendTo($("span.fancytree-icon", node.span));
+		}
+		$badge.text(count);
+	} else {
+		$badge.remove();
+	}
+	if( extOpts.deep && !node.isTopLevel() && !node.isRoot() ) {
+		node.parent.updateCounters();
+	}
+};
+
+
+// Finally, we can extend the widget API and create functions that are called
+// like so:
+//
+//     $("#tree").fancytree("widgetMethod1", "abc");
+
+$.ui.fancytree.prototype.widgetMethod1 = function(arg1){
+	var tree = this.tree;
+	return arg1;
+};
+
+
+// Register a Fancytree extension
+// ------------------------------
+// A full blown extension, extension is available for all trees and can be
+// enabled like so (see also the [live demo](http://wwwendt.de/tech/fancytree/demo/sample-ext-childcounter.html)):
+//
+//    <script src="../src/jquery.fancytree.js"></script>
+//    <script src="../src/jquery.fancytree.childcounter.js"></script>
+//    ...
+//
+//     $("#tree").fancytree({
+//         extensions: ["childcounter"],
+//         childcounter: {
+//             hideExpanded: true
+//         },
+//         ...
+//     });
+//
+
+
+/* 'childcounter' extension */
+$.ui.fancytree.registerExtension({
+// Every extension must be registered by a unique name.
+	name: "childcounter",
+// Version information should be compliant with [semver](http://semver.org)
+	version: "2.26.0",
+
+// Extension specific options and their defaults.
+// This options will be available as `tree.options.childcounter.hideExpanded`
+
+	options: {
+		deep: true,
+		hideZeros: true,
+		hideExpanded: false
+	},
+
+// Attributes other than `options` (or functions) can be defined here, and
+// will be added to the tree.ext.EXTNAME namespace, in this case `tree.ext.childcounter.foo`.
+// They can also be accessed as `this._local.foo` from within the extension
+// methods.
+	foo: 42,
+
+// Local functions are prefixed with an underscore '_'.
+// Callable as `this._local._appendCounter()`.
+
+	_appendCounter: function(bar){
+		var tree = this;
+	},
+
+// **Override virtual methods for this extension.**
+//
+// Fancytree implements a number of 'hook methods', prefixed by 'node...' or 'tree...'.
+// with a `ctx` argument (see [EventData](http://www.wwwendt.de/tech/fancytree/doc/jsdoc/global.html#EventData)
+// for details) and an extended calling context:<br>
+// `this`       : the Fancytree instance<br>
+// `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME)<br>
+// `this._super`: the virtual function that was overridden (member of previous extension or Fancytree)
+//
+// See also the [complete list of available hook functions](http://www.wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Hooks.html).
+
+	/* Init */
+// `treeInit` is triggered when a tree is initalized. We can set up classes or
+// bind event handlers here...
+	treeInit: function(ctx){
+		var tree = this, // same as ctx.tree,
+			opts = ctx.options,
+			extOpts = ctx.options.childcounter;
+// Optionally check for dependencies with other extensions
+		/* this._requireExtension("glyph", false, false); */
+// Call the base implementation
+		this._superApply(arguments);
+// Add a class to the tree container
+		this.$container.addClass("fancytree-ext-childcounter");
+	},
+
+// Destroy this tree instance (we only call the default implementation, so
+// this method could as well be omitted).
+
+	treeDestroy: function(ctx){
+		this._superApply(arguments);
+	},
+
+// Overload the `renderTitle` hook, to append a counter badge
+	nodeRenderTitle: function(ctx, title) {
+		var node = ctx.node,
+			extOpts = ctx.options.childcounter,
+			count = (node.data.childCounter == null) ? node.countChildren(extOpts.deep) : +node.data.childCounter;
+// Let the base implementation render the title
+// We use `_super()` instead of `_superApply()` here, since it is a little bit
+// more performant when called often
+		this._super(ctx, title);
+// Append a counter badge
+		if( (count || ! extOpts.hideZeros) && (!node.isExpanded() || !extOpts.hideExpanded) ){
+			$("span.fancytree-icon", node.span).append($("<span class='fancytree-childcounter'/>").text(count));
+		}
+	},
+// Overload the `setExpanded` hook, so the counters are updated
+	nodeSetExpanded: function(ctx, flag, callOpts) {
+		var tree = ctx.tree,
+			node = ctx.node;
+// Let the base implementation expand/collapse the node, then redraw the title
+// after the animation has finished
+		return this._superApply(arguments).always(function(){
+			tree.nodeRenderTitle(ctx);
+		});
+	}
+
+// End of extension definition
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.clones.js' *//*!
+ *
+ * jquery.fancytree.clones.js
+ * Support faster lookup of nodes by key and shared ref-ids.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+/*******************************************************************************
+ * Private functions and variables
+ */
+function _assert(cond, msg){
+	// TODO: see qunit.js extractStacktrace()
+	if(!cond){
+		msg = msg ? ": " + msg : "";
+		$.error("Assertion failed" + msg);
+	}
+}
+
+
+/* Return first occurrence of member from array. */
+function _removeArrayMember(arr, elem) {
+	// TODO: use Array.indexOf for IE >= 9
+	var i;
+	for (i = arr.length - 1; i >= 0; i--) {
+		if (arr[i] === elem) {
+			arr.splice(i, 1);
+			return true;
+		}
+	}
+	return false;
+}
+
+
+// /**
+//  * Calculate a 32 bit FNV-1a hash
+//  * Found here: https://gist.github.com/vaiorabbit/5657561
+//  * Ref.: http://isthe.com/chongo/tech/comp/fnv/
+//  *
+//  * @param {string} str the input value
+//  * @param {boolean} [asString=false] set to true to return the hash value as
+//  *     8-digit hex string instead of an integer
+//  * @param {integer} [seed] optionally pass the hash of the previous chunk
+//  * @returns {integer | string}
+//  */
+// function hashFnv32a(str, asString, seed) {
+// 	/*jshint bitwise:false */
+// 	var i, l,
+// 		hval = (seed === undefined) ? 0x811c9dc5 : seed;
+
+// 	for (i = 0, l = str.length; i < l; i++) {
+// 		hval ^= str.charCodeAt(i);
+// 		hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
+// 	}
+// 	if( asString ){
+// 		// Convert to 8 digit hex string
+// 		return ("0000000" + (hval >>> 0).toString(16)).substr(-8);
+// 	}
+// 	return hval >>> 0;
+// }
+
+
+/**
+ * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
+ *
+ * @author <a href="mailto:[email protected]">Gary Court</a>
+ * @see http://github.com/garycourt/murmurhash-js
+ * @author <a href="mailto:[email protected]">Austin Appleby</a>
+ * @see http://sites.google.com/site/murmurhash/
+ *
+ * @param {string} key ASCII only
+ * @param {boolean} [asString=false]
+ * @param {number} seed Positive integer only
+ * @return {number} 32-bit positive integer hash
+ */
+function hashMurmur3(key, asString, seed) {
+	/*jshint bitwise:false */
+	var h1b, k1,
+		remainder = key.length & 3,
+		bytes = key.length - remainder,
+		h1 = seed,
+		c1 = 0xcc9e2d51,
+		c2 = 0x1b873593,
+		i = 0;
+
+	while (i < bytes) {
+		k1 =
+			((key.charCodeAt(i) & 0xff)) |
+			((key.charCodeAt(++i) & 0xff) << 8) |
+			((key.charCodeAt(++i) & 0xff) << 16) |
+			((key.charCodeAt(++i) & 0xff) << 24);
+		++i;
+
+		k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
+		k1 = (k1 << 15) | (k1 >>> 17);
+		k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
+
+		h1 ^= k1;
+		h1 = (h1 << 13) | (h1 >>> 19);
+		h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
+		h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
+	}
+
+	k1 = 0;
+
+	switch (remainder) {
+		/*jshint -W086:true */
+		case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
+		case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
+		case 1: k1 ^= (key.charCodeAt(i) & 0xff);
+
+		k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
+		k1 = (k1 << 15) | (k1 >>> 17);
+		k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
+		h1 ^= k1;
+	}
+
+	h1 ^= key.length;
+
+	h1 ^= h1 >>> 16;
+	h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
+	h1 ^= h1 >>> 13;
+	h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
+	h1 ^= h1 >>> 16;
+
+	if( asString ){
+		// Convert to 8 digit hex string
+		return ("0000000" + (h1 >>> 0).toString(16)).substr(-8);
+	}
+	return h1 >>> 0;
+}
+
+// console.info(hashMurmur3("costarring"));
+// console.info(hashMurmur3("costarring", true));
+// console.info(hashMurmur3("liquid"));
+// console.info(hashMurmur3("liquid", true));
+
+
+/*
+ * Return a unique key for node by calculationg the hash of the parents refKey-list
+ */
+function calcUniqueKey(node) {
+	var key,
+		path = $.map(node.getParentList(false, true), function(e){ return e.refKey || e.key; });
+	path = path.join("/");
+	key = "id_" + hashMurmur3(path, true);
+	// node.debug(path + " -> " + key);
+	return key;
+}
+
+
+/**
+ * [ext-clones] Return a list of clone-nodes or null.
+ * @param {boolean} [includeSelf=false]
+ * @returns {FancytreeNode[] | null}
+ *
+ * @alias FancytreeNode#getCloneList
+ * @requires jquery.fancytree.clones.js
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.getCloneList = function(includeSelf){
+	var key,
+		tree = this.tree,
+		refList = tree.refMap[this.refKey] || null,
+		keyMap = tree.keyMap;
+
+	if( refList ) {
+		key = this.key;
+		// Convert key list to node list
+		if( includeSelf ) {
+			refList = $.map(refList, function(val){ return keyMap[val]; });
+		} else {
+			refList = $.map(refList, function(val){ return val === key ? null : keyMap[val]; });
+			if( refList.length < 1 ) {
+				refList = null;
+			}
+		}
+	}
+	return refList;
+};
+
+
+/**
+ * [ext-clones] Return true if this node has at least another clone with same refKey.
+ * @returns {boolean}
+ *
+ * @alias FancytreeNode#isClone
+ * @requires jquery.fancytree.clones.js
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.isClone = function(){
+	var refKey = this.refKey || null,
+		refList = refKey && this.tree.refMap[refKey] || null;
+	return !!(refList && refList.length > 1);
+};
+
+
+/**
+ * [ext-clones] Update key and/or refKey for an existing node.
+ * @param {string} key
+ * @param {string} refKey
+ * @returns {boolean}
+ *
+ * @alias FancytreeNode#reRegister
+ * @requires jquery.fancytree.clones.js
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.reRegister = function(key, refKey){
+	key = (key == null) ? null :  "" + key;
+	refKey = (refKey == null) ? null :  "" + refKey;
+	// this.debug("reRegister", key, refKey);
+
+	var tree = this.tree,
+		prevKey = this.key,
+		prevRefKey = this.refKey,
+		keyMap = tree.keyMap,
+		refMap = tree.refMap,
+		refList = refMap[prevRefKey] || null,
+//		curCloneKeys = refList ? node.getCloneList(true),
+		modified = false;
+
+	// Key has changed: update all references
+	if( key != null && key !== this.key ) {
+		if( keyMap[key] ) {
+			$.error("[ext-clones] reRegister(" + key + "): already exists: " + this);
+		}
+		// Update keyMap
+		delete keyMap[prevKey];
+		keyMap[key] = this;
+		// Update refMap
+		if( refList ) {
+			refMap[prevRefKey] = $.map(refList, function(e){
+				return e === prevKey ? key : e;
+			});
+		}
+		this.key = key;
+		modified = true;
+	}
+
+	// refKey has changed
+	if( refKey != null && refKey !== this.refKey ) {
+		// Remove previous refKeys
+		if( refList ){
+			if( refList.length === 1 ){
+				delete refMap[prevRefKey];
+			}else{
+				refMap[prevRefKey] = $.map(refList, function(e){
+					return e === prevKey ? null : e;
+				});
+			}
+		}
+		// Add refKey
+		if( refMap[refKey] ) {
+			refMap[refKey].append(key);
+		}else{
+			refMap[refKey] = [ this.key ];
+		}
+		this.refKey = refKey;
+		modified = true;
+	}
+	return modified;
+};
+
+
+/**
+ * [ext-clones] Define a refKey for an existing node.
+ * @param {string} refKey
+ * @returns {boolean}
+ *
+ * @alias FancytreeNode#setRefKey
+ * @requires jquery.fancytree.clones.js
+ * @since 2.16
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.setRefKey = function(refKey){
+	return this.reRegister(null, refKey);
+};
+
+
+/**
+ * [ext-clones] Return all nodes with a given refKey (null if not found).
+ * @param {string} refKey
+ * @param {FancytreeNode} [rootNode] optionally restrict results to descendants of this node
+ * @returns {FancytreeNode[] | null}
+ * @alias Fancytree#getNodesByRef
+ * @requires jquery.fancytree.clones.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.getNodesByRef = function(refKey, rootNode){
+	var keyMap = this.keyMap,
+		refList = this.refMap[refKey] || null;
+
+	if( refList ) {
+		// Convert key list to node list
+		if( rootNode ) {
+			refList = $.map(refList, function(val){
+				var node = keyMap[val];
+				return node.isDescendantOf(rootNode) ? node : null;
+			});
+		}else{
+			refList = $.map(refList, function(val){ return keyMap[val]; });
+		}
+		if( refList.length < 1 ) {
+			refList = null;
+		}
+	}
+	return refList;
+};
+
+
+/**
+ * [ext-clones] Replace a refKey with a new one.
+ * @param {string} oldRefKey
+ * @param {string} newRefKey
+ * @alias Fancytree#changeRefKey
+ * @requires jquery.fancytree.clones.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.changeRefKey = function(oldRefKey, newRefKey) {
+	var i, node,
+		keyMap = this.keyMap,
+		refList = this.refMap[oldRefKey] || null;
+
+	if (refList) {
+		for (i = 0; i < refList.length; i++) {
+			node = keyMap[refList[i]];
+			node.refKey = newRefKey;
+		}
+		delete this.refMap[oldRefKey];
+		this.refMap[newRefKey] = refList;
+	}
+};
+
+
+/*******************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "clones",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
+		highlightClones: false       // set 'fancytree-clone' class on any node that has at least one clone
+	},
+
+	treeCreate: function(ctx){
+		this._superApply(arguments);
+		ctx.tree.refMap = {};
+		ctx.tree.keyMap = {};
+	},
+	treeInit: function(ctx){
+		this.$container.addClass("fancytree-ext-clones");
+		_assert(ctx.options.defaultKey == null);
+		// Generate unique / reproducible default keys
+		ctx.options.defaultKey = function(node){
+			return calcUniqueKey(node);
+		};
+		// The default implementation loads initial data
+		this._superApply(arguments);
+	},
+	treeClear: function(ctx){
+		ctx.tree.refMap = {};
+		ctx.tree.keyMap = {};
+		return this._superApply(arguments);
+	},
+	treeRegisterNode: function(ctx, add, node) {
+		var refList, len,
+			tree = ctx.tree,
+			keyMap = tree.keyMap,
+			refMap = tree.refMap,
+			key = node.key,
+			refKey = (node && node.refKey != null) ? "" + node.refKey : null;
+
+//		ctx.tree.debug("clones.treeRegisterNode", add, node);
+
+		if( node.isStatusNode() ){
+			return this._super(ctx, add, node);
+		}
+
+		if( add ) {
+			if( keyMap[node.key] != null ) {
+				$.error("clones.treeRegisterNode: node.key already exists: " + node);
+			}
+			keyMap[key] = node;
+			if( refKey ) {
+				refList = refMap[refKey];
+				if( refList ) {
+					refList.push(key);
+					if( refList.length === 2 && ctx.options.clones.highlightClones ) {
+						// Mark peer node, if it just became a clone (no need to
+						// mark current node, since it will be rendered later anyway)
+						keyMap[refList[0]].renderStatus();
+					}
+				} else {
+					refMap[refKey] = [key];
+				}
+				// node.debug("clones.treeRegisterNode: add clone =>", refMap[refKey]);
+			}
+		}else {
+			if( keyMap[key] == null ) {
+				$.error("clones.treeRegisterNode: node.key not registered: " + node.key);
+			}
+			delete keyMap[key];
+			if( refKey ) {
+				refList = refMap[refKey];
+				// node.debug("clones.treeRegisterNode: remove clone BEFORE =>", refMap[refKey]);
+				if( refList ) {
+					len = refList.length;
+					if( len <= 1 ){
+						_assert(len === 1);
+						_assert(refList[0] === key);
+						delete refMap[refKey];
+					}else{
+						_removeArrayMember(refList, key);
+						// Unmark peer node, if this was the only clone
+						if( len === 2 && ctx.options.clones.highlightClones ) {
+//							node.debug("clones.treeRegisterNode: last =>", node.getCloneList());
+							keyMap[refList[0]].renderStatus();
+						}
+					}
+					// node.debug("clones.treeRegisterNode: remove clone =>", refMap[refKey]);
+				}
+			}
+		}
+		return this._super(ctx, add, node);
+	},
+	nodeRenderStatus: function(ctx) {
+		var $span, res,
+			node = ctx.node;
+
+		res = this._super(ctx);
+
+		if( ctx.options.clones.highlightClones ) {
+			$span = $(node[ctx.tree.statusClassPropName]);
+			// Only if span already exists
+			if( $span.length && node.isClone() ){
+//				node.debug("clones.nodeRenderStatus: ", ctx.options.clones.highlightClones);
+				$span.addClass("fancytree-clone");
+			}
+		}
+		return res;
+	},
+	nodeSetActive: function(ctx, flag, callOpts) {
+		var res,
+			scpn = ctx.tree.statusClassPropName,
+			node = ctx.node;
+
+		res = this._superApply(arguments);
+
+		if( ctx.options.clones.highlightActiveClones && node.isClone() ) {
+			$.each(node.getCloneList(true), function(idx, n){
+				// n.debug("clones.nodeSetActive: ", flag !== false);
+				$(n[scpn]).toggleClass("fancytree-active-clone", flag !== false);
+			});
+		}
+		return res;
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.dnd5.js' *//*!
+ * jquery.fancytree.dnd5.js
+ *
+ * Drag-and-drop support (native HTML5).
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+
+ /*
+ #TODO
+	Compatiblity when dragging between *separate* windows:
+
+		   Drag from Chrome   Edge    FF    IE11    Safari
+	  To Chrome      ok       ok      ok    NO      ?
+		 Edge        ok       ok      ok    NO      ?
+		 FF          ok       ok      ok    NO      ?
+		 IE 11       ok       ok      ok    ok      ?
+		 Safari      ?        ?       ?     ?       ok
+
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+/* *****************************************************************************
+ * Private functions and variables
+ */
+var
+	classDragSource = "fancytree-drag-source",
+	classDragRemove = "fancytree-drag-remove",
+	classDropAccept = "fancytree-drop-accept",
+	classDropAfter = "fancytree-drop-after",
+	classDropBefore = "fancytree-drop-before",
+	classDropOver = "fancytree-drop-over",
+	classDropReject = "fancytree-drop-reject",
+	classDropTarget = "fancytree-drop-target",
+	nodeMimeType = "application/x-fancytree-node",
+	$dropMarker = null,
+	SOURCE_NODE = null,
+	DRAG_ENTER_RESPONSE = null,
+	LAST_HIT_MODE = null;
+
+/* Convert number to string and prepend +/-; return empty string for 0.*/
+function offsetString(n){
+	return n === 0 ? "" : (( n > 0 ) ? ("+" + n) : ("" + n));
+}
+
+/* Convert a dragEnter() or dragOver() response to a canonical form.
+ * Return false or plain object
+ * @param {string|object|boolean} r
+ * @return {object|false}
+ */
+function normalizeDragEnterResponse(r) {
+	var res;
+
+	if( !r ){
+		return false;
+	}
+	if ( $.isPlainObject(r) ) {
+		res = {
+			over: !!r.over,
+			before: !!r.before,
+			after: !!r.after
+		};
+	}else if ( $.isArray(r) ) {
+		res = {
+			over: ($.inArray("over", r) >= 0),
+			before: ($.inArray("before", r) >= 0),
+			after: ($.inArray("after", r) >= 0)
+		};
+	}else{
+		res = {
+			over: ((r === true) || (r === "over")),
+			before: ((r === true) || (r === "before")),
+			after: ((r === true) || (r === "after"))
+		};
+	}
+	if( Object.keys(res).length === 0 ) {
+		return false;
+	}
+	// if( Object.keys(res).length === 1 ) {
+	// 	res.unique = res[0];
+	// }
+	return res;
+}
+
+/* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
+function autoScroll(tree, event) {
+	var spOfs, scrollTop, delta,
+		dndOpts = tree.options.dnd5,
+		sp = tree.$scrollParent[0],
+		sensitivity = dndOpts.scrollSensitivity,
+		speed = dndOpts.scrollSpeed,
+		scrolled = 0;
+
+	if ( sp !== document && sp.tagName !== "HTML" ) {
+		spOfs = tree.$scrollParent.offset();
+		scrollTop = sp.scrollTop;
+		if ( spOfs.top + sp.offsetHeight - event.pageY < sensitivity ) {
+			delta = (sp.scrollHeight - tree.$scrollParent.innerHeight() - scrollTop);
+			// console.log ("sp.offsetHeight: " + sp.offsetHeight
+			// 	+ ", spOfs.top: " + spOfs.top
+			// 	+ ", scrollTop: " + scrollTop
+			// 	+ ", innerHeight: " + tree.$scrollParent.innerHeight()
+			// 	+ ", scrollHeight: " + sp.scrollHeight
+			// 	+ ", delta: " + delta
+			// 	);
+			if( delta > 0 ) {
+				sp.scrollTop = scrolled = scrollTop + speed;
+			}
+		} else if ( scrollTop > 0 && event.pageY - spOfs.top < sensitivity ) {
+			sp.scrollTop = scrolled = scrollTop - speed;
+		}
+	} else {
+		scrollTop = $(document).scrollTop();
+		if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
+			scrolled = scrollTop - speed;
+			$(document).scrollTop(scrolled);
+		} else if ($(window).height() - (event.pageY - scrollTop) < sensitivity) {
+			scrolled = scrollTop + speed;
+			$(document).scrollTop(scrolled);
+		}
+	}
+	if( scrolled ) {
+		tree.debug("autoScroll: " + scrolled + "px");
+	}
+	return scrolled;
+}
+
+/* Handle dragover event (fired every x ms) and return hitMode. */
+function handleDragOver(event, data) {
+	// Implement auto-scrolling
+	if ( data.options.dnd5.scroll ) {
+		autoScroll(data.tree, event);
+	}
+	// Bail out with previous response if we get an invalid dragover
+	if( !data.node ) {
+		data.tree.warn("Ignore dragover for non-node");  //, event, data);
+		return LAST_HIT_MODE;
+	}
+
+	var markerOffsetX, nodeOfs, relPosY, //res,
+		// eventHash = getEventHash(event),
+		hitMode = null,
+		tree = data.tree,
+		options = tree.options,
+		dndOpts = options.dnd5,
+		targetNode = data.node,
+		sourceNode = data.otherNode,
+		markerAt = "center",
+		// $source = sourceNode ? $(sourceNode.span) : null,
+		$target = $(targetNode.span),
+		$targetTitle = $target.find("span.fancytree-title");
+
+	if(DRAG_ENTER_RESPONSE === false){
+		tree.info("Ignore dragover, since dragenter returned false");  //, event, data);
+		// $.error("assert failed: dragenter returned false");
+		return false;
+	} else if(typeof DRAG_ENTER_RESPONSE === "string") {
+		$.error("assert failed: dragenter returned string");
+		// Use hitMode from onEnter if provided.
+		// hitMode = DRAG_ENTER_RESPONSE;
+	} else {
+		// Calculate hitMode from relative cursor position.
+		nodeOfs = $target.offset();
+		relPosY = (event.pageY - nodeOfs.top) / $target.height();
+
+		if( DRAG_ENTER_RESPONSE.after && relPosY > 0.75 ){
+			hitMode = "after";
+		} else if(!DRAG_ENTER_RESPONSE.over && DRAG_ENTER_RESPONSE.after && relPosY > 0.5 ){
+			hitMode = "after";
+		} else if(DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) {
+			hitMode = "before";
+		} else if(!DRAG_ENTER_RESPONSE.over && DRAG_ENTER_RESPONSE.before && relPosY <= 0.5) {
+			hitMode = "before";
+		} else if(DRAG_ENTER_RESPONSE.over) {
+			hitMode = "over";
+		}
+		// Prevent no-ops like 'before source node'
+		// TODO: these are no-ops when moving nodes, but not in copy mode
+		if( dndOpts.preventVoidMoves ){
+			if(targetNode === sourceNode){
+				targetNode.debug("drop over source node prevented");
+				hitMode = null;
+			}else if(hitMode === "before" && sourceNode && targetNode === sourceNode.getNextSibling()){
+				targetNode.debug("drop after source node prevented");
+				hitMode = null;
+			}else if(hitMode === "after" && sourceNode && targetNode === sourceNode.getPrevSibling()){
+				targetNode.debug("drop before source node prevented");
+				hitMode = null;
+			}else if(hitMode === "over" && sourceNode && sourceNode.parent === targetNode && sourceNode.isLastSibling() ){
+				targetNode.debug("drop last child over own parent prevented");
+				hitMode = null;
+			}
+		}
+	}
+	// Let callback modify the calculated hitMode
+	data.hitMode = hitMode;
+	if(hitMode && dndOpts.dragOver){
+		// TODO: http://code.google.com/p/dynatree/source/detail?r=625
+		dndOpts.dragOver(targetNode, data);
+		hitMode = data.hitMode;
+	}
+	// LAST_DROP_EFFECT = data.dataTransfer.dropEffect;
+	// LAST_EFFECT_ALLOWED = data.dataTransfer.effectAllowed;
+	LAST_HIT_MODE = hitMode;
+	//
+	if( hitMode === "after" || hitMode === "before" || hitMode === "over" ){
+		markerOffsetX = dndOpts.dropMarkerOffsetX || 0;
+		switch(hitMode){
+		case "before":
+			markerAt = "top";
+			markerOffsetX += (dndOpts.dropMarkerInsertOffsetX || 0);
+			break;
+		case "after":
+			markerAt = "bottom";
+			markerOffsetX += (dndOpts.dropMarkerInsertOffsetX || 0);
+			break;
+		}
+
+		$dropMarker
+			.toggleClass(classDropAfter, hitMode === "after")
+			.toggleClass(classDropOver, hitMode === "over")
+			.toggleClass(classDropBefore, hitMode === "before")
+			.show()
+			.position($.ui.fancytree.fixPositionOptions({
+				my: "left" + offsetString(markerOffsetX) + " center",
+				at: "left " + markerAt,
+				of: $targetTitle
+				}));
+	} else {
+		$dropMarker.hide();
+		// console.log("hide dropmarker")
+	}
+	// if( $source ){
+	// 	$source.toggleClass(classDragRemove, isMove);
+	// }
+	$(targetNode.span)
+		.toggleClass(classDropTarget, hitMode === "after" || hitMode === "before" || hitMode === "over")
+		.toggleClass(classDropAfter, hitMode === "after")
+		.toggleClass(classDropBefore, hitMode === "before")
+		.toggleClass(classDropAccept, hitMode === "over")
+		.toggleClass(classDropReject, hitMode === false);
+
+	return hitMode;
+}
+
+/* *****************************************************************************
+ *
+ */
+
+$.ui.fancytree.registerExtension({
+	name: "dnd5",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		autoExpandMS: 1500,          // Expand nodes after n milliseconds of hovering
+		setTextTypeJson: false,      // Allow dragging of nodes to different IE windows
+		preventForeignNodes: false,  // Prevent dropping nodes from different Fancytrees
+		preventNonNodes: false,      // Prevent dropping items other than Fancytree nodes
+		preventRecursiveMoves: true, // Prevent dropping nodes on own descendants
+		preventVoidMoves: true,      // Prevent dropping nodes 'before self', etc.
+		scroll: true,                // Enable auto-scrolling while dragging
+		scrollSensitivity: 20,       // Active top/bottom margin in pixel
+		scrollSpeed: 5,              // Pixel per event
+		dropMarkerOffsetX: -24,		 // absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
+		dropMarkerInsertOffsetX: -16,// additional offset for drop-marker with hitMode = "before"/"after"
+		// Events (drag support)
+		dragStart: null,       // Callback(sourceNode, data), return true, to enable dnd drag
+		dragDrag: $.noop,      // Callback(sourceNode, data)
+		dragEnd: $.noop,       // Callback(sourceNode, data)
+		// Events (drop support)
+		dragEnter: null,       // Callback(targetNode, data), return true, to enable dnd drop
+		dragOver: $.noop,      // Callback(targetNode, data)
+		dragExpand: $.noop,    // Callback(targetNode, data), return false to prevent autoExpand
+		dragDrop: $.noop,      // Callback(targetNode, data)
+		dragLeave: $.noop      // Callback(targetNode, data)
+	},
+
+	treeInit: function(ctx){
+		var $temp,
+			tree = ctx.tree,
+			opts = ctx.options,
+			glyph = opts.glyph || null,
+			dndOpts = opts.dnd5,
+			getNode = $.ui.fancytree.getNode;
+
+		if( $.inArray("dnd", opts.extensions) >= 0 ) {
+			$.error("Extensions 'dnd' and 'dnd5' are mutually exclusive.");
+		}
+		if( dndOpts.dragStop ) {
+			$.error("dragStop is not used by ext-dnd5. Use dragEnd instead.");
+		}
+
+		// Implement `opts.createNode` event to add the 'draggable' attribute
+		// #680: this must happen before calling super.treeInit()
+		if( dndOpts.dragStart ) {
+			$.ui.fancytree.overrideMethod(ctx.options, "createNode", function(event, data) {
+				// Default processing if any
+				this._super.apply(this, arguments);
+
+				data.node.span.draggable = true;
+			});
+		}
+		this._superApply(arguments);
+
+		this.$container.addClass("fancytree-ext-dnd5");
+
+		// Store the current scroll parent, which may be the tree
+		// container, any enclosing div, or the document.
+		// #761: scrollParent() always needs a container child
+		$temp = $("<span>").appendTo(this.$container);
+		this.$scrollParent = $temp.scrollParent();
+		$temp.remove();
+
+		$dropMarker = $("#fancytree-drop-marker");
+		if( !$dropMarker.length ) {
+			$dropMarker = $("<div id='fancytree-drop-marker'></div>")
+				.hide()
+				.css({
+					"z-index": 1000,
+					// Drop marker should not steal dragenter/dragover events:
+					"pointer-events": "none"
+				}).prependTo("body");
+			if( glyph ) {
+				$dropMarker.addClass(glyph.map.dropMarker);
+			}
+		}
+		// Enable drag support if dragStart() is specified:
+		if( dndOpts.dragStart ) {
+			// Bind drag event handlers
+			tree.$container.on("dragstart drag dragend", function(event){
+				var json,
+					node = getNode(event),
+					dataTransfer = event.dataTransfer || event.originalEvent.dataTransfer,
+					isMove = dataTransfer.dropEffect === "move",
+					$source = node ? $(node.span) : null,
+					data = {
+						node: node,
+						tree: tree,
+						options: tree.options,
+						originalEvent: event,
+						dataTransfer: dataTransfer,
+//						dropEffect: undefined,  // set by dragend
+						isCancelled: undefined  // set by dragend
+					};
+
+				switch( event.type ) {
+
+				case "dragstart":
+					$(node.span).addClass(classDragSource);
+
+					// Store current source node in different formats
+					SOURCE_NODE = node;
+
+					// Set payload
+					// Note:
+					// Transfer data is only accessible on dragstart and drop!
+					// For all other events the formats and kinds in the drag
+					// data store list of items representing dragged data can be
+					// enumerated, but the data itself is unavailable and no new
+					// data can be added.
+					json = JSON.stringify(node.toDict());
+					try {
+						dataTransfer.setData(nodeMimeType, json);
+						dataTransfer.setData("text/html", $(node.span).html());
+						dataTransfer.setData("text/plain", node.title);
+					} catch(ex) {
+						// IE only accepts 'text' type
+						tree.warn("Could not set data (IE only accepts 'text') - " + ex);
+					}
+					// We always need to set the 'text' type if we want to drag
+					// Because IE 11 only accepts this single type.
+					// If we pass JSON here, IE can can access all node properties,
+					// even when the source lives in another window. (D'n'd inside
+					// the same window will always work.)
+					// The drawback is, that in this case ALL browsers will see
+					// the JSON representation as 'text', so dragging
+					// to a text field will insert the JSON string instead of
+					// the node title.
+					if( dndOpts.setTextTypeJson ) {
+						dataTransfer.setData("text", json);
+					} else {
+						dataTransfer.setData("text", node.title);
+					}
+
+					// Set the allowed and current drag mode (move, copy, or link)
+					dataTransfer.effectAllowed = "all";  // "copyMove"
+					// dataTransfer.dropEffect = "move";
+
+					// Set the title as drag image (otherwise it would contain the expander)
+					if( dataTransfer.setDragImage ) {
+						// IE 11 does not support this
+						dataTransfer.setDragImage($(node.span).find(".fancytree-title")[0], -10, -10);
+						// dataTransfer.setDragImage($(node.span)[0], -10, -10);
+					}
+					// Let user modify above settings
+					return dndOpts.dragStart(node, data) !== false;
+
+				case "drag":
+					// Called every few miliseconds
+					$source.toggleClass(classDragRemove, isMove);
+					dndOpts.dragDrag(node, data);
+					break;
+
+				case "dragend":
+					$(node.span).removeClass(classDragSource + " " + classDragRemove);
+					SOURCE_NODE = null;
+					DRAG_ENTER_RESPONSE = null;
+//					data.dropEffect = dataTransfer.dropEffect;
+					data.isCancelled = (dataTransfer.dropEffect === "none");
+					$dropMarker.hide();
+					dndOpts.dragEnd(node, data);
+					break;
+				}
+			});
+		}
+		// Enable drop support if dragEnter() is specified:
+		if( dndOpts.dragEnter ) {
+			// Bind drop event handlers
+			tree.$container.on("dragenter dragover dragleave drop", function(event){
+				var json, nodeData, r, res,
+					allowDrop = null,
+					node = getNode(event),
+					dataTransfer = event.dataTransfer || event.originalEvent.dataTransfer,
+					data = {
+						node: node,
+						tree: tree,
+						options: tree.options,
+						hitMode: DRAG_ENTER_RESPONSE,
+						originalEvent: event,
+						dataTransfer: dataTransfer,
+						otherNode: SOURCE_NODE || null,
+						otherNodeData: null,    // set by drop event
+						dropEffect: undefined,  // set by drop event
+						isCancelled: undefined  // set by drop event
+					};
+
+				switch( event.type ) {
+
+				case "dragenter":
+					// The dragenter event is fired when a dragged element or
+					// text selection enters a valid drop target.
+
+					if( !node ) {
+						// Sometimes we get dragenter for the container element
+						tree.debug("Ignore non-node " + event.type + ": " + event.target.tagName + "." + event.target.className);
+						DRAG_ENTER_RESPONSE = false;
+						break;
+					}
+
+					$(node.span)
+						.addClass(classDropOver)
+						.removeClass(classDropAccept + " " + classDropReject);
+
+					if( dndOpts.preventNonNodes && !nodeData ) {
+						node.debug("Reject dropping a non-node");
+						DRAG_ENTER_RESPONSE = false;
+						break;
+					} else if( dndOpts.preventForeignNodes && (!SOURCE_NODE || SOURCE_NODE.tree !== node.tree ) ) {
+						node.debug("Reject dropping a foreign node");
+						DRAG_ENTER_RESPONSE = false;
+						break;
+					}
+
+					// NOTE: dragenter is fired BEFORE the dragleave event
+					// of the previous element!
+					// https://www.w3.org/Bugs/Public/show_bug.cgi?id=19041
+					setTimeout(function(){
+						// node.info("DELAYED " + event.type, event.target, DRAG_ENTER_RESPONSE);
+						// Auto-expand node (only when 'over' the node, not 'before', or 'after')
+						if( dndOpts.autoExpandMS &&
+							node.hasChildren() !== false && !node.expanded &&
+							(!dndOpts.dragExpand || dndOpts.dragExpand(node, data) !== false)
+							) {
+							node.scheduleAction("expand", dndOpts.autoExpandMS);
+						}
+					}, 0);
+
+					$dropMarker.show();
+
+					// Call dragEnter() to figure out if (and where) dropping is allowed
+					if( dndOpts.preventRecursiveMoves && node.isDescendantOf(data.otherNode) ){
+						res = false;
+					}else{
+						r = dndOpts.dragEnter(node, data);
+						res = normalizeDragEnterResponse(r);
+					}
+					DRAG_ENTER_RESPONSE = res;
+
+					allowDrop = res && ( res.over || res.before || res.after );
+					break;
+
+				case "dragover":
+					// The dragover event is fired when an element or text
+					// selection is being dragged over a valid drop target
+					// (every few hundred milliseconds).
+					LAST_HIT_MODE = handleDragOver(event, data);
+					allowDrop = !!LAST_HIT_MODE;
+					break;
+
+				case "dragleave":
+					// NOTE: dragleave is fired AFTER the dragenter event of the
+					// FOLLOWING element.
+					if( !node ) {
+						tree.debug("Ignore non-node " + event.type + ": " + event.target.tagName + "." + event.target.className);
+						break;
+					}
+					if( !$(node.span).hasClass(classDropOver) ) {
+						node.debug("Ignore dragleave (multi)"); //, event.currentTarget);
+						break;
+					}
+					$(node.span).removeClass(classDropOver + " " + classDropAccept + " " + classDropReject);
+					node.scheduleAction("cancel");
+					dndOpts.dragLeave(node, data);
+					$dropMarker.hide();
+					break;
+
+				case "drop":
+					// Data is only readable in the (dragenter and) drop event:
+
+					if( $.inArray(nodeMimeType, dataTransfer.types) >= 0 ) {
+						nodeData = dataTransfer.getData(nodeMimeType);
+						tree.info(event.type + ": getData('application/x-fancytree-node'): '" + nodeData + "'");
+					}
+					if( !nodeData ) {
+						// 1. Source is not a Fancytree node, or
+						// 2. If the FT mime type was set, but returns '', this
+						//    is probably IE 11 (which only supports 'text')
+						nodeData = dataTransfer.getData("text");
+						tree.info(event.type + ": getData('text'): '" + nodeData + "'");
+					}
+					if( nodeData ) {
+						try {
+							// 'text' type may contain JSON if IE is involved
+							// and setTextTypeJson option was set
+							json = JSON.parse(nodeData);
+							if( json.title !== undefined ) {
+								data.otherNodeData = json;
+							}
+						} catch(ex) {
+							// assume 'text' type contains plain text, so `otherNodeData`
+							// should not be set
+						}
+					}
+					tree.debug(event.type + ": nodeData: '" + nodeData + "', otherNodeData: ", data.otherNodeData);
+
+					$(node.span).removeClass(classDropOver + " " + classDropAccept + " " + classDropReject);
+					$dropMarker.hide();
+
+					data.hitMode = LAST_HIT_MODE;
+					data.dropEffect = dataTransfer.dropEffect;
+					data.isCancelled = data.dropEffect === "none";
+
+					// Let user implement the actual drop operation
+					dndOpts.dragDrop(node, data);
+
+					// Prevent browser's default drop handling
+					event.preventDefault();
+					break;
+				}
+				// Dnd API madness: we must PREVENT default handling to enable dropping
+				if( allowDrop ) {
+					event.preventDefault();
+					return false;
+				}
+			});
+		}
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.edit.js' *//*!
+ * jquery.fancytree.edit.js
+ *
+ * Make node titles editable.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+
+/*******************************************************************************
+ * Private functions and variables
+ */
+
+var isMac = /Mac/.test(navigator.platform),
+	escapeHtml = $.ui.fancytree.escapeHtml,
+	unescapeHtml = $.ui.fancytree.unescapeHtml;
+
+/**
+ * [ext-edit] Start inline editing of current node title.
+ *
+ * @alias FancytreeNode#editStart
+ * @requires Fancytree
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.editStart = function(){
+	var $input,
+		node = this,
+		tree = this.tree,
+		local = tree.ext.edit,
+		instOpts = tree.options.edit,
+		$title = $(".fancytree-title", node.span),
+		eventData = {
+			node: node,
+			tree: tree,
+			options: tree.options,
+			isNew: $(node[tree.statusClassPropName]).hasClass("fancytree-edit-new"),
+			orgTitle: node.title,
+			input: null,
+			dirty: false
+			};
+
+	// beforeEdit may want to modify the title before editing
+	if( instOpts.beforeEdit.call(node, {type: "beforeEdit"}, eventData) === false ) {
+		return false;
+	}
+	$.ui.fancytree.assert(!local.currentNode, "recursive edit");
+	local.currentNode = this;
+	local.eventData = eventData;
+
+	// Disable standard Fancytree mouse- and key handling
+	tree.widget._unbind();
+	// #116: ext-dnd prevents the blur event, so we have to catch outer clicks
+	$(document).on("mousedown.fancytree-edit", function(event){
+		if( ! $(event.target).hasClass("fancytree-edit-input") ){
+			node.editEnd(true, event);
+		}
+	});
+
+	// Replace node with <input>
+	$input = $("<input />", {
+		"class": "fancytree-edit-input",
+		type: "text",
+		value: tree.options.escapeTitles ? eventData.orgTitle : unescapeHtml(eventData.orgTitle)
+	});
+	local.eventData.input = $input;
+	if ( instOpts.adjustWidthOfs != null ) {
+		$input.width($title.width() + instOpts.adjustWidthOfs);
+	}
+	if ( instOpts.inputCss != null ) {
+		$input.css(instOpts.inputCss);
+	}
+
+	$title.html($input);
+
+	// Focus <input> and bind keyboard handler
+	$input
+		.focus()
+		.change(function(event){
+			$input.addClass("fancytree-edit-dirty");
+		}).keydown(function(event){
+			switch( event.which ) {
+			case $.ui.keyCode.ESCAPE:
+				node.editEnd(false, event);
+				break;
+			case $.ui.keyCode.ENTER:
+				node.editEnd(true, event);
+				return false; // so we don't start editmode on Mac
+			}
+			event.stopPropagation();
+		}).blur(function(event){
+			return node.editEnd(true, event);
+		});
+
+	instOpts.edit.call(node, {type: "edit"}, eventData);
+};
+
+
+/**
+ * [ext-edit] Stop inline editing.
+ * @param {Boolean} [applyChanges=false] false: cancel edit, true: save (if modified)
+ * @alias FancytreeNode#editEnd
+ * @requires jquery.fancytree.edit.js
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function(applyChanges, _event){
+	var newVal,
+		node = this,
+		tree = this.tree,
+		local = tree.ext.edit,
+		eventData = local.eventData,
+		instOpts = tree.options.edit,
+		$title = $(".fancytree-title", node.span),
+		$input = $title.find("input.fancytree-edit-input");
+
+	if( instOpts.trim ) {
+		$input.val($.trim($input.val()));
+	}
+	newVal = $input.val();
+
+	eventData.dirty = ( newVal !== node.title );
+	eventData.originalEvent = _event;
+
+	// Find out, if saving is required
+	if( applyChanges === false ) {
+		// If true/false was passed, honor this (except in rename mode, if unchanged)
+		eventData.save = false;
+	} else if( eventData.isNew ) {
+		// In create mode, we save everyting, except for empty text
+		eventData.save = (newVal !== "");
+	} else {
+		// In rename mode, we save everyting, except for empty or unchanged text
+		eventData.save = eventData.dirty && (newVal !== "");
+	}
+	// Allow to break (keep editor open), modify input, or re-define data.save
+	if( instOpts.beforeClose.call(node, {type: "beforeClose"}, eventData) === false){
+		return false;
+	}
+	if( eventData.save && instOpts.save.call(node, {type: "save"}, eventData) === false){
+		return false;
+	}
+	$input
+		.removeClass("fancytree-edit-dirty")
+		.off();
+	// Unbind outer-click handler
+	$(document).off(".fancytree-edit");
+
+	if( eventData.save ) {
+		// # 171: escape user input (not required if global escaping is on)
+		node.setTitle( tree.options.escapeTitles ? newVal : escapeHtml(newVal) );
+		node.setFocus();
+	}else{
+		if( eventData.isNew ) {
+			node.remove();
+			node = eventData.node = null;
+			local.relatedNode.setFocus();
+		} else {
+			node.renderTitle();
+			node.setFocus();
+		}
+	}
+	local.eventData = null;
+	local.currentNode = null;
+	local.relatedNode = null;
+	// Re-enable mouse and keyboard handling
+	tree.widget._bind();
+	// Set keyboard focus, even if setFocus() claims 'nothing to do'
+	$(tree.$container).focus();
+	eventData.input = null;
+	instOpts.close.call(node, {type: "close"}, eventData);
+	return true;
+};
+
+
+/**
+* [ext-edit] Create a new child or sibling node and start edit mode.
+*
+* @param {String} [mode='child'] 'before', 'after', or 'child'
+* @param {Object} [init] NodeData (or simple title string)
+* @alias FancytreeNode#editCreateNode
+* @requires jquery.fancytree.edit.js
+* @since 2.4
+*/
+$.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function(mode, init){
+	var newNode,
+		tree = this.tree,
+		self = this;
+
+	mode = mode || "child";
+	if( init == null ) {
+		init = { title: "" };
+	} else if( typeof init === "string" ) {
+		init = { title: init };
+	} else {
+		$.ui.fancytree.assert($.isPlainObject(init));
+	}
+	// Make sure node is expanded (and loaded) in 'child' mode
+	if( mode === "child" && !this.isExpanded() && this.hasChildren() !== false ) {
+		this.setExpanded().done(function(){
+			self.editCreateNode(mode, init);
+		});
+		return;
+	}
+	newNode = this.addNode(init, mode);
+
+	// #644: Don't filter new nodes.
+	newNode.match = true;
+	$(newNode[tree.statusClassPropName])
+		.removeClass("fancytree-hide")
+		.addClass("fancytree-match");
+
+	newNode.makeVisible(/*{noAnimation: true}*/).done(function(){
+		$(newNode[tree.statusClassPropName]).addClass("fancytree-edit-new");
+		self.tree.ext.edit.relatedNode = self;
+		newNode.editStart();
+	});
+};
+
+
+/**
+ * [ext-edit] Check if any node in this tree  in edit mode.
+ *
+ * @returns {FancytreeNode | null}
+ * @alias Fancytree#isEditing
+ * @requires jquery.fancytree.edit.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.isEditing = function(){
+	return this.ext.edit ? this.ext.edit.currentNode : null;
+};
+
+
+/**
+ * [ext-edit] Check if this node is in edit mode.
+ * @returns {Boolean} true if node is currently beeing edited
+ * @alias FancytreeNode#isEditing
+ * @requires jquery.fancytree.edit.js
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function(){
+	return this.tree.ext.edit ? this.tree.ext.edit.currentNode === this : false;
+};
+
+
+/*******************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "edit",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		adjustWidthOfs: 4,   // null: don't adjust input size to content
+		allowEmpty: false,   // Prevent empty input
+		inputCss: {minWidth: "3em"},
+		// triggerCancel: ["esc", "tab", "click"],
+		// triggerStart: ["f2", "dblclick", "shift+click", "mac+enter"],
+		triggerStart: ["f2", "shift+click", "mac+enter"],
+		trim: true,          // Trim whitespace before save
+		// Events:
+		beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available)
+		beforeEdit: $.noop,  // Return false to prevent edit mode
+		close: $.noop,       // Editor was removed
+		edit: $.noop,        // Editor was opened (available as data.input)
+//		keypress: $.noop,    // Not yet implemented
+		save: $.noop         // Save data.input.val() or return false to keep editor open
+	},
+	// Local attributes
+	currentNode: null,
+
+	treeInit: function(ctx){
+		this._superApply(arguments);
+		this.$container.addClass("fancytree-ext-edit");
+	},
+	nodeClick: function(ctx) {
+		if( $.inArray("shift+click", ctx.options.edit.triggerStart) >= 0 ){
+			if( ctx.originalEvent.shiftKey ){
+				ctx.node.editStart();
+				return false;
+			}
+		}
+		return this._superApply(arguments);
+	},
+	nodeDblclick: function(ctx) {
+		if( $.inArray("dblclick", ctx.options.edit.triggerStart) >= 0 ){
+			ctx.node.editStart();
+			return false;
+		}
+		return this._superApply(arguments);
+	},
+	nodeKeydown: function(ctx) {
+		switch( ctx.originalEvent.which ) {
+		case 113: // [F2]
+			if( $.inArray("f2", ctx.options.edit.triggerStart) >= 0 ){
+				ctx.node.editStart();
+				return false;
+			}
+			break;
+		case $.ui.keyCode.ENTER:
+			if( $.inArray("mac+enter", ctx.options.edit.triggerStart) >= 0 && isMac ){
+				ctx.node.editStart();
+				return false;
+			}
+			break;
+		}
+		return this._superApply(arguments);
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.filter.js' *//*!
+ * jquery.fancytree.filter.js
+ *
+ * Remove or highlight tree nodes, based on a filter.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+
+/*******************************************************************************
+ * Private functions and variables
+ */
+
+var KeyNoData = "__not_found__",
+	escapeHtml = $.ui.fancytree.escapeHtml;
+
+function _escapeRegex(str){
+	/*jshint regexdash:true */
+	return (str + "").replace(/([.?*+\^\$\[\]\\(){}|-])/g, "\\$1");
+}
+
+function extractHtmlText(s){
+	if( s.indexOf(">") >= 0 ) {
+		return $("<div/>").html(s).text();
+	}
+	return s;
+}
+
+$.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function(filter, branchMode, _opts){
+	var match, statusNode, re, reHighlight, temp,
+		count = 0,
+		treeOpts = this.options,
+		escapeTitles = treeOpts.escapeTitles,
+		prevAutoCollapse = treeOpts.autoCollapse,
+		opts = $.extend({}, treeOpts.filter, _opts),
+		hideMode = opts.mode === "hide",
+		leavesOnly = !!opts.leavesOnly && !branchMode;
+
+	// Default to 'match title substring (not case sensitive)'
+	if(typeof filter === "string"){
+		if( filter === "" ) {
+			this.warn("Fancytree passing an empty string as a filter is handled as clearFilter().");
+			this.clearFilter();
+			return;
+		}
+		if( opts.fuzzy ) {
+			// See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905
+			// and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed
+			// and http://www.dustindiaz.com/autocomplete-fuzzy-matching
+			match = filter.split("").reduce(function(a, b) {
+				return a + "[^" + b + "]*" + b;
+			});
+		} else {
+			match = _escapeRegex(filter); // make sure a '.' is treated literally
+		}
+		re = new RegExp(".*" + match + ".*", "i");
+		reHighlight = new RegExp(_escapeRegex(filter), "gi");
+		filter = function(node){
+			var text = escapeTitles ? node.title : extractHtmlText(node.title),
+				res = !!re.test(text);
+
+			if( res && opts.highlight ) {
+				if( escapeTitles ) {
+					// #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
+					// Use some exotic characters to mark matches:
+					temp = text.replace(reHighlight, function(s){
+						return "\uFFF7" + s + "\uFFF8";
+					});
+					// now we can escape the title...
+					node.titleWithHighlight = escapeHtml(temp)
+						// ... and finally insert the desired `<mark>` tags
+						.replace(/\uFFF7/g, "<mark>")
+						.replace(/\uFFF8/g, "</mark>");
+				} else {
+					node.titleWithHighlight = text.replace(reHighlight, function(s){
+						return "<mark>" + s + "</mark>";
+					});
+				}
+				// node.debug("filter", escapeTitles, text, node.titleWithHighlight);
+			}
+			return res;
+		};
+	}
+
+	this.enableFilter = true;
+	this.lastFilterArgs = arguments;
+
+	this.$div.addClass("fancytree-ext-filter");
+	if( hideMode ){
+		this.$div.addClass("fancytree-ext-filter-hide");
+	} else {
+		this.$div.addClass("fancytree-ext-filter-dimm");
+	}
+	this.$div.toggleClass("fancytree-ext-filter-hide-expanders", !!opts.hideExpanders);
+	// Reset current filter
+	this.visit(function(node){
+		delete node.match;
+		delete node.titleWithHighlight;
+		node.subMatchCount = 0;
+	});
+	statusNode = this.getRootNode()._findDirectChild(KeyNoData);
+	if( statusNode ) {
+		statusNode.remove();
+	}
+
+	// Adjust node.hide, .match, and .subMatchCount properties
+	treeOpts.autoCollapse = false;  // #528
+
+	this.visit(function(node){
+		if ( leavesOnly && node.children != null ) {
+			return;
+		}
+		var res = filter(node),
+			matchedByBranch = false;
+
+		if( res === "skip" ) {
+			node.visit(function(c){
+				c.match = false;
+			}, true);
+			return "skip";
+		}
+		if( !res && (branchMode || res === "branch") && node.parent.match ) {
+			res = true;
+			matchedByBranch = true;
+		}
+		if( res ) {
+			count++;
+			node.match = true;
+			node.visitParents(function(p){
+				p.subMatchCount += 1;
+				// Expand match (unless this is no real match, but only a node in a matched branch)
+				if( opts.autoExpand && !matchedByBranch && !p.expanded ) {
+					p.setExpanded(true, {noAnimation: true, noEvents: true, scrollIntoView: false});
+					p._filterAutoExpanded = true;
+				}
+			});
+		}
+	});
+	treeOpts.autoCollapse = prevAutoCollapse;
+
+	if( count === 0 && opts.nodata && hideMode ) {
+		statusNode = opts.nodata;
+		if( $.isFunction(statusNode) ) {
+			statusNode = statusNode();
+		}
+		if( statusNode === true ) {
+			statusNode = {};
+		} else if( typeof statusNode === "string" ) {
+			statusNode = { title: statusNode };
+		}
+		statusNode = $.extend({
+			statusNodeType: "nodata",
+			key: KeyNoData,
+			title: this.options.strings.noData
+		}, statusNode);
+
+		this.getRootNode().addNode(statusNode).match = true;
+	}
+	// Redraw whole tree
+	this.render();
+	return count;
+};
+
+/**
+ * [ext-filter] Dimm or hide nodes.
+ *
+ * @param {function | string} filter
+ * @param {boolean} [opts={autoExpand: false, leavesOnly: false}]
+ * @returns {integer} count
+ * @alias Fancytree#filterNodes
+ * @requires jquery.fancytree.filter.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.filterNodes = function(filter, opts) {
+	if( typeof opts === "boolean" ) {
+		opts = { leavesOnly: opts };
+		this.warn("Fancytree.filterNodes() leavesOnly option is deprecated since 2.9.0 / 2015-04-19. Use opts.leavesOnly instead.");
+	}
+	return this._applyFilterImpl(filter, false, opts);
+};
+
+/**
+ * @deprecated
+ */
+$.ui.fancytree._FancytreeClass.prototype.applyFilter = function(filter){
+	this.warn("Fancytree.applyFilter() is deprecated since 2.1.0 / 2014-05-29. Use .filterNodes() instead.");
+	return this.filterNodes.apply(this, arguments);
+};
+
+/**
+ * [ext-filter] Dimm or hide whole branches.
+ *
+ * @param {function | string} filter
+ * @param {boolean} [opts={autoExpand: false}]
+ * @returns {integer} count
+ * @alias Fancytree#filterBranches
+ * @requires jquery.fancytree.filter.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.filterBranches = function(filter, opts){
+	return this._applyFilterImpl(filter, true, opts);
+};
+
+
+/**
+ * [ext-filter] Reset the filter.
+ *
+ * @alias Fancytree#clearFilter
+ * @requires jquery.fancytree.filter.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.clearFilter = function(){
+	var $title,
+		statusNode = this.getRootNode()._findDirectChild(KeyNoData),
+		escapeTitles = this.options.escapeTitles,
+		enhanceTitle = this.options.enhanceTitle;
+
+	if( statusNode ) {
+		statusNode.remove();
+	}
+	this.visit(function(node){
+		if( node.match && node.span ) {  // #491, #601
+			$title = $(node.span).find(">span.fancytree-title");
+			if( escapeTitles ) {
+				$title.text(node.title);
+			} else {
+				$title.html(node.title);
+			}
+			if( enhanceTitle ) {
+				enhanceTitle({type: "enhanceTitle"}, {node: node, $title: $title});
+			}
+		}
+		delete node.match;
+		delete node.subMatchCount;
+		delete node.titleWithHighlight;
+		if ( node.$subMatchBadge ) {
+			node.$subMatchBadge.remove();
+			delete node.$subMatchBadge;
+		}
+		if( node._filterAutoExpanded && node.expanded ) {
+			node.setExpanded(false, {noAnimation: true, noEvents: true, scrollIntoView: false});
+		}
+		delete node._filterAutoExpanded;
+	});
+	this.enableFilter = false;
+	this.lastFilterArgs = null;
+	this.$div.removeClass("fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide");
+	this.render();
+};
+
+
+/**
+ * [ext-filter] Return true if a filter is currently applied.
+ *
+ * @returns {Boolean}
+ * @alias Fancytree#isFilterActive
+ * @requires jquery.fancytree.filter.js
+ * @since 2.13
+ */
+$.ui.fancytree._FancytreeClass.prototype.isFilterActive = function(){
+	return !!this.enableFilter;
+};
+
+
+/**
+ * [ext-filter] Return true if this node is matched by current filter (or no filter is active).
+ *
+ * @returns {Boolean}
+ * @alias FancytreeNode#isMatched
+ * @requires jquery.fancytree.filter.js
+ * @since 2.13
+ */
+$.ui.fancytree._FancytreeNodeClass.prototype.isMatched = function(){
+	return !(this.tree.enableFilter && !this.match);
+};
+
+
+/*******************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "filter",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		autoApply: true,   // Re-apply last filter if lazy data is loaded
+		autoExpand: false, // Expand all branches that contain matches while filtered
+		counter: true,     // Show a badge with number of matching child nodes near parent icons
+		fuzzy: false,      // Match single characters in order, e.g. 'fb' will match 'FooBar'
+		hideExpandedCounter: true,  // Hide counter badge if parent is expanded
+		hideExpanders: false,       // Hide expanders if all child nodes are hidden by filter
+		highlight: true,   // Highlight matches by wrapping inside <mark> tags
+		leavesOnly: false, // Match end nodes only
+		nodata: true,      // Display a 'no data' status node if result is empty
+		mode: "dimm"       // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
+	},
+	nodeLoadChildren: function(ctx, source) {
+		return this._superApply(arguments).done(function() {
+			if( ctx.tree.enableFilter && ctx.tree.lastFilterArgs && ctx.options.filter.autoApply ) {
+				ctx.tree._applyFilterImpl.apply(ctx.tree, ctx.tree.lastFilterArgs);
+			}
+		});
+	},
+	nodeSetExpanded: function(ctx, flag, callOpts) {
+		delete ctx.node._filterAutoExpanded;
+		// Make sure counter badge is displayed again, when node is beeing collapsed
+		if( !flag && ctx.options.filter.hideExpandedCounter && ctx.node.$subMatchBadge ) {
+			ctx.node.$subMatchBadge.show();
+		}
+		return this._superApply(arguments);
+	},
+	nodeRenderStatus: function(ctx) {
+		// Set classes for current status
+		var res,
+			node = ctx.node,
+			tree = ctx.tree,
+			opts = ctx.options.filter,
+			$title = $(node.span).find("span.fancytree-title"),
+			$span = $(node[tree.statusClassPropName]),
+			enhanceTitle = ctx.options.enhanceTitle,
+			escapeTitles = ctx.options.escapeTitles;
+
+		res = this._super(ctx);
+		// nothing to do, if node was not yet rendered
+		if( !$span.length || !tree.enableFilter ) {
+			return res;
+		}
+		$span
+			.toggleClass("fancytree-match", !!node.match)
+			.toggleClass("fancytree-submatch", !!node.subMatchCount)
+			.toggleClass("fancytree-hide", !(node.match || node.subMatchCount));
+		// Add/update counter badge
+		if( opts.counter && node.subMatchCount && (!node.isExpanded() || !opts.hideExpandedCounter) ) {
+			if( !node.$subMatchBadge ) {
+				node.$subMatchBadge = $("<span class='fancytree-childcounter'/>");
+				$("span.fancytree-icon, span.fancytree-custom-icon", node.span).append(node.$subMatchBadge);
+			}
+			node.$subMatchBadge.show().text(node.subMatchCount);
+		} else if ( node.$subMatchBadge ) {
+			node.$subMatchBadge.hide();
+		}
+		// node.debug("nodeRenderStatus", node.titleWithHighlight, node.title)
+		// #601: also chek for $title.length, because we don't need to render
+		// if node.span is null (i.e. not rendered)
+		if( node.span && (!node.isEditing || !node.isEditing.call(node)) ) {
+			if( node.titleWithHighlight ) {
+				$title.html(node.titleWithHighlight);
+			} else if ( escapeTitles ) {
+				$title.text(node.title);
+			} else {
+				$title.html(node.title);
+			}
+			if( enhanceTitle ) {
+				enhanceTitle({type: "enhanceTitle"}, {node: node, $title: $title});
+			}
+		}
+		return res;
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.glyph.js' *//*!
+ * jquery.fancytree.glyph.js
+ *
+ * Use glyph fonts as instead of icon sprites.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+/* *****************************************************************************
+ * Private functions and variables
+ */
+
+var FT = $.ui.fancytree,
+	PRESETS = {
+	"awesome3": {
+		checkbox: "icon-check-empty",
+		checkboxSelected: "icon-check",
+		checkboxUnknown: "icon-check icon-muted",
+		radio: "icon-circle-blank",
+		radioSelected: "icon-circle",
+		radioUnknown: "icon-circle icon-muted",
+		// radioUnknown: "icon-remove-circle-blank",
+		dragHelper: "icon-caret-right",
+		dropMarker: "icon-caret-right",
+		error: "icon-exclamation-sign",
+		expanderClosed: "icon-caret-right",
+		expanderLazy: "icon-angle-right",
+		expanderOpen: "icon-caret-down",
+		loading: "icon-refresh icon-spin",
+		nodata: "icon-meh",
+		noExpander: "",
+		// Default node icons.
+		// (Use tree.options.icon callback to define custom icons based on node data)
+		doc: "icon-file-alt",
+		docOpen: "icon-file-alt",
+		folder: "icon-folder-close-alt",
+		folderOpen: "icon-folder-open-alt"
+		},
+	"awesome4": {
+		checkbox: "fa fa-square-o",
+		checkboxSelected: "fa fa-check-square-o",
+		// checkboxUnknown: "fa fa-window-close-o",
+		checkboxUnknown: "fa fa-square",
+		radio: "fa fa-circle-o",
+		radioSelected: "fa fa-circle",
+		radioUnknown: "fa fa-dot-circle-o",
+		dragHelper: "fa fa-arrow-right",
+		dropMarker: "fa fa-long-arrow-right",
+		error: "fa fa-warning",
+		expanderClosed: "fa fa-caret-right",
+		expanderLazy: "fa fa-angle-right",
+		expanderOpen: "fa fa-caret-down",
+		loading: "fa fa-spinner fa-pulse",
+		nodata: "fa fa-meh-o",
+		noExpander: "",
+		// Default node icons.
+		// (Use tree.options.icon callback to define custom icons based on node data)
+		doc: "fa fa-file-o",
+		docOpen: "fa fa-file-o",
+		folder: "fa fa-folder-o",
+		folderOpen: "fa fa-folder-open-o"
+		},
+	"bootstrap3": {
+		checkbox: "glyphicon glyphicon-unchecked",
+		checkboxSelected: "glyphicon glyphicon-check",
+		checkboxUnknown: "glyphicon glyphicon-expand",
+		radio: "glyphicon glyphicon-remove-circle",
+		radioSelected: "glyphicon glyphicon-ok-circle",
+		radioUnknown: "glyphicon glyphicon-ban-circle",
+		dragHelper: "glyphicon glyphicon-play",
+		dropMarker: "glyphicon glyphicon-arrow-right",
+		error: "glyphicon glyphicon-warning-sign",
+		expanderClosed: "glyphicon glyphicon-menu-right",  // glyphicon-plus-sign
+		expanderLazy: "glyphicon glyphicon-menu-right",  // glyphicon-plus-sign
+		expanderOpen: "glyphicon glyphicon-menu-down",  // glyphicon-minus-sign
+		loading: "glyphicon glyphicon-refresh glyphicon-spin",
+		nodata: "glyphicon glyphicon-info-sign",
+		noExpander: "",
+		// Default node icons.
+		// (Use tree.options.icon callback to define custom icons based on node data)
+		doc: "glyphicon glyphicon-file",
+		docOpen: "glyphicon glyphicon-file",
+		folder: "glyphicon glyphicon-folder-close",
+		folderOpen: "glyphicon glyphicon-folder-open"
+		}
+	};
+
+
+function _getIcon(opts, type){
+	return opts.map[type];
+}
+
+
+$.ui.fancytree.registerExtension({
+	name: "glyph",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		preset: null,  // 'awesome3', 'awesome4', 'bootstrap3'
+		map: {
+		}
+	},
+
+	treeInit: function(ctx){
+		var tree = ctx.tree,
+			opts = ctx.options.glyph;
+
+		if( opts.preset ) {
+			FT.assert( !!PRESETS[opts.preset],
+				"Invalid value for `options.glyph.preset`: " + opts.preset);
+			opts.map = $.extend({}, PRESETS[opts.preset], opts.map);
+		} else {
+			tree.warn("ext-glyph: missing `preset` option.");
+		}
+		this._superApply(arguments);
+		tree.$container.addClass("fancytree-ext-glyph");
+	},
+	nodeRenderStatus: function(ctx) {
+		var checkbox, className, icon, res, span,
+			node = ctx.node,
+			$span = $(node.span),
+			opts = ctx.options.glyph,
+			map = opts.map;
+
+		res = this._super(ctx);
+
+		if( node.isRoot() ){
+			return res;
+		}
+		span = $span.children("span.fancytree-expander").get(0);
+		if( span ){
+			// if( node.isLoading() ){
+				// icon = "loading";
+			if( node.expanded && node.hasChildren() ){
+				icon = "expanderOpen";
+			}else if( node.isUndefined() ){
+				icon = "expanderLazy";
+			}else if( node.hasChildren() ){
+				icon = "expanderClosed";
+			}else{
+				icon = "noExpander";
+			}
+			span.className = "fancytree-expander " + map[icon];
+		}
+
+		if( node.tr ){
+			span = $("td", node.tr).find("span.fancytree-checkbox").get(0);
+		}else{
+			span = $span.children("span.fancytree-checkbox").get(0);
+		}
+		if( span ){
+			checkbox = FT.evalOption("checkbox", node, node, ctx.options, false);
+			if( checkbox && !node.isStatusNode() ) {
+				className = "fancytree-checkbox ";
+				if( checkbox === "radio" || (node.parent && node.parent.radiogroup) ) {
+					className += "fancytree-radio ";
+					icon = node.selected ? "radioSelected" : (node.partsel ? "radioUnknown" : "radio");
+				} else {
+					icon = node.selected ? "checkboxSelected" : (node.partsel ? "checkboxUnknown" : "checkbox");
+				}
+				span.className = className + map[icon];
+			}
+		}
+
+		// Standard icon (note that this does not match .fancytree-custom-icon,
+		// that might be set by opts.icon callbacks)
+		span = $span.children("span.fancytree-icon").get(0);
+		if( span ){
+			if( node.statusNodeType ){
+				icon = _getIcon(opts, node.statusNodeType); // loading, error
+			}else if( node.folder ){
+				icon = node.expanded && node.hasChildren() ? _getIcon(opts, "folderOpen") : _getIcon(opts, "folder");
+			}else{
+				icon = node.expanded ? _getIcon(opts, "docOpen") : _getIcon(opts, "doc");
+			}
+			span.className = "fancytree-icon " + icon;
+		}
+		return res;
+	},
+	nodeSetStatus: function(ctx, status, message, details) {
+		var res, span,
+			opts = ctx.options.glyph,
+			node = ctx.node;
+
+		res = this._superApply(arguments);
+
+		if( status === "error" || status === "loading" || status === "nodata" ){
+			if(node.parent){
+				span = $("span.fancytree-expander", node.span).get(0);
+				if( span ) {
+					span.className = "fancytree-expander " + _getIcon(opts, status);
+				}
+			}else{ //
+				span = $(".fancytree-statusnode-" + status, node[this.nodeContainerAttrName])
+					.find("span.fancytree-icon").get(0);
+				if( span ) {
+					span.className = "fancytree-icon " + _getIcon(opts, status);
+				}
+			}
+		}
+		return res;
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.gridnav.js' *//*!
+ * jquery.fancytree.gridnav.js
+ *
+ * Support keyboard navigation for trees with embedded input controls.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define([
+			 "jquery",
+			 "./jquery.fancytree",
+			 "./jquery.fancytree.table"
+		 ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree.table");  // core + table
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+
+/*******************************************************************************
+ * Private functions and variables
+ */
+
+// Allow these navigation keys even when input controls are focused
+
+var	KC = $.ui.keyCode,
+	// which keys are *not* handled by embedded control, but passed to tree
+	// navigation handler:
+	NAV_KEYS = {
+		"text": [KC.UP, KC.DOWN],
+		"checkbox": [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
+		"link": [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
+		"radiobutton": [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
+		"select-one": [KC.LEFT, KC.RIGHT],
+		"select-multiple": [KC.LEFT, KC.RIGHT]
+	};
+
+
+/* Calculate TD column index (considering colspans).*/
+function getColIdx($tr, $td) {
+	var colspan,
+		td = $td.get(0),
+		idx = 0;
+
+	$tr.children().each(function () {
+		if( this === td ) {
+			return false;
+		}
+		colspan = $(this).prop("colspan");
+		idx += colspan ? colspan : 1;
+	});
+	return idx;
+}
+
+
+/* Find TD at given column index (considering colspans).*/
+function findTdAtColIdx($tr, colIdx) {
+	var colspan,
+		res = null,
+		idx = 0;
+
+	$tr.children().each(function () {
+		if( idx >= colIdx ) {
+			res = $(this);
+			return false;
+		}
+		colspan = $(this).prop("colspan");
+		idx += colspan ? colspan : 1;
+	});
+	return res;
+}
+
+
+/* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */
+function findNeighbourTd($target, keyCode){
+	var $tr, colIdx,
+		$td = $target.closest("td"),
+		$tdNext = null;
+
+	switch( keyCode ){
+		case KC.LEFT:
+			$tdNext = $td.prev();
+			break;
+		case KC.RIGHT:
+			$tdNext = $td.next();
+			break;
+		case KC.UP:
+		case KC.DOWN:
+			$tr = $td.parent();
+			colIdx = getColIdx($tr, $td);
+			while( true ) {
+				$tr = (keyCode === KC.UP) ? $tr.prev() : $tr.next();
+				if( !$tr.length ) {
+					break;
+				}
+				// Skip hidden rows
+				if( $tr.is(":hidden") ) {
+					continue;
+				}
+				// Find adjacent cell in the same column
+				$tdNext = findTdAtColIdx($tr, colIdx);
+				// Skip cells that don't conatain a focusable element
+				if( $tdNext && $tdNext.find(":input,a").length ) {
+					break;
+				}
+			}
+			break;
+	}
+	return $tdNext;
+}
+
+
+/*******************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "gridnav",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		autofocusInput:   false,  // Focus first embedded input if node gets activated
+		handleCursorKeys: true   // Allow UP/DOWN in inputs to move to prev/next node
+	},
+
+	treeInit: function(ctx){
+		// gridnav requires the table extension to be loaded before itself
+		this._requireExtension("table", true, true);
+		this._superApply(arguments);
+
+		this.$container.addClass("fancytree-ext-gridnav");
+
+		// Activate node if embedded input gets focus (due to a click)
+		this.$container.on("focusin", function(event){
+			var ctx2,
+				node = $.ui.fancytree.getNode(event.target);
+
+			if( node && !node.isActive() ){
+				// Call node.setActive(), but also pass the event
+				ctx2 = ctx.tree._makeHookContext(node, event);
+				ctx.tree._callHook("nodeSetActive", ctx2, true);
+			}
+		});
+	},
+	nodeSetActive: function(ctx, flag, callOpts) {
+		var $outer,
+			opts = ctx.options.gridnav,
+			node = ctx.node,
+			event = ctx.originalEvent || {},
+			triggeredByInput = $(event.target).is(":input");
+
+		flag = (flag !== false);
+
+		this._superApply(arguments);
+
+		if( flag ){
+			if( ctx.options.titlesTabbable ){
+				if( !triggeredByInput ) {
+					$(node.span).find("span.fancytree-title").focus();
+					node.setFocus();
+				}
+				// If one node is tabbable, the container no longer needs to be
+				ctx.tree.$container.attr("tabindex", "-1");
+				// ctx.tree.$container.removeAttr("tabindex");
+			} else if( opts.autofocusInput && !triggeredByInput ){
+				// Set focus to input sub input (if node was clicked, but not
+				// when TAB was pressed )
+				$outer = $(node.tr || node.span);
+				$outer.find(":input:enabled:first").focus();
+			}
+		}
+	},
+	nodeKeydown: function(ctx) {
+		var inputType, handleKeys, $td,
+			opts = ctx.options.gridnav,
+			event = ctx.originalEvent,
+			$target = $(event.target);
+
+		if( $target.is(":input:enabled") ) {
+			inputType = $target.prop("type");
+		} else if( $target.is("a") ) {
+			inputType = "link";
+		}
+		// ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType);
+
+		if( inputType && opts.handleCursorKeys ){
+			handleKeys = NAV_KEYS[inputType];
+			if( handleKeys && $.inArray(event.which, handleKeys) >= 0 ){
+				$td = findNeighbourTd($target, event.which);
+				if( $td && $td.length ) {
+					// ctx.node.debug("ignore keydown in input", event.which, handleKeys);
+					$td.find(":input:enabled,a").focus();
+					// Prevent Fancytree default navigation
+					return false;
+				}
+			}
+			return true;
+		}
+		// ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType);
+		return this._superApply(arguments);
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.persist.js' *//*!
+ * jquery.fancytree.persist.js
+ *
+ * Persist tree status in cookiesRemove or highlight tree nodes, based on a filter.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * @depends: js-cookie or jquery-cookie
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+/* global Cookies:false */
+
+/*******************************************************************************
+ * Private functions and variables
+ */
+var cookieGetter, cookieRemover, cookieSetter,
+	_assert = $.ui.fancytree.assert,
+	ACTIVE = "active",
+	EXPANDED = "expanded",
+	FOCUS = "focus",
+	SELECTED = "selected";
+
+if( typeof Cookies === "function" ) {
+	// Assume https://github.com/js-cookie/js-cookie
+	cookieSetter = Cookies.set;
+	cookieGetter = Cookies.get;
+	cookieRemover = Cookies.remove;
+} else {
+	// Fall back to https://github.com/carhartl/jquery-cookie
+	cookieSetter = cookieGetter = $.cookie;
+	cookieRemover = $.removeCookie;
+}
+
+/* Recursively load lazy nodes
+ * @param {string} mode 'load', 'expand', false
+ */
+function _loadLazyNodes(tree, local, keyList, mode, dfd) {
+	var i, key, l, node,
+		foundOne = false,
+		expandOpts = tree.options.persist.expandOpts,
+		deferredList = [],
+		missingKeyList = [];
+
+	keyList = keyList || [];
+	dfd = dfd || $.Deferred();
+
+	for( i=0, l=keyList.length; i<l; i++ ) {
+		key = keyList[i];
+		node = tree.getNodeByKey(key);
+		if( node ) {
+			if( mode && node.isUndefined() ) {
+				foundOne = true;
+				tree.debug("_loadLazyNodes: " + node + " is lazy: loading...");
+				if( mode === "expand" ) {
+					deferredList.push(node.setExpanded(true, expandOpts));
+				} else {
+					deferredList.push(node.load());
+				}
+			} else {
+				tree.debug("_loadLazyNodes: " + node + " already loaded.");
+				node.setExpanded(true, expandOpts);
+			}
+		} else {
+			missingKeyList.push(key);
+			tree.debug("_loadLazyNodes: " + node + " was not yet found.");
+		}
+	}
+
+	$.when.apply($, deferredList).always(function(){
+		// All lazy-expands have finished
+		if( foundOne && missingKeyList.length > 0 ) {
+			// If we read new nodes from server, try to resolve yet-missing keys
+			_loadLazyNodes(tree, local, missingKeyList, mode, dfd);
+		} else {
+			if( missingKeyList.length ) {
+				tree.warn("_loadLazyNodes: could not load those keys: ", missingKeyList);
+				for( i=0, l=missingKeyList.length; i<l; i++ ) {
+					key = keyList[i];
+					local._appendKey(EXPANDED, keyList[i], false);
+				}
+			}
+			dfd.resolve();
+		}
+	});
+	return dfd;
+}
+
+
+/**
+ * [ext-persist] Remove persistence cookies of the given type(s).
+ * Called like
+ *     $("#tree").fancytree("getTree").clearCookies("active expanded focus selected");
+ *
+ * @alias Fancytree#clearCookies
+ * @requires jquery.fancytree.persist.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.clearCookies = function(types){
+	var local = this.ext.persist,
+		prefix = local.cookiePrefix;
+
+	types = types || "active expanded focus selected";
+	if(types.indexOf(ACTIVE) >= 0){
+		local._data(prefix + ACTIVE, null);
+	}
+	if(types.indexOf(EXPANDED) >= 0){
+		local._data(prefix + EXPANDED, null);
+	}
+	if(types.indexOf(FOCUS) >= 0){
+		local._data(prefix + FOCUS, null);
+	}
+	if(types.indexOf(SELECTED) >= 0){
+		local._data(prefix + SELECTED, null);
+	}
+};
+
+
+/**
+ * [ext-persist] Return persistence information from cookies
+ *
+ * Called like
+ *     $("#tree").fancytree("getTree").getPersistData();
+ *
+ * @alias Fancytree#getPersistData
+ * @requires jquery.fancytree.persist.js
+ */
+$.ui.fancytree._FancytreeClass.prototype.getPersistData = function(){
+	var local = this.ext.persist,
+		prefix = local.cookiePrefix,
+		delim = local.cookieDelimiter,
+		res = {};
+
+	res[ACTIVE] = local._data(prefix + ACTIVE);
+	res[EXPANDED] = (local._data(prefix + EXPANDED) || "").split(delim);
+	res[SELECTED] = (local._data(prefix + SELECTED) || "").split(delim);
+	res[FOCUS] = local._data(prefix + FOCUS);
+	return res;
+};
+
+
+/* *****************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "persist",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		cookieDelimiter: "~",
+		cookiePrefix: undefined, // 'fancytree-<treeId>-' by default
+		cookie: {
+			raw: false,
+			expires: "",
+			path: "",
+			domain: "",
+			secure: false
+		},
+		expandLazy: false,     // true: recursively expand and load lazy nodes
+		expandOpts: undefined, // optional `opts` argument passed to setExpanded()
+		fireActivate: true,    // false: suppress `activate` event after active node was restored
+		overrideSource: true,  // true: cookie takes precedence over `source` data attributes.
+		store: "auto",         // 'cookie': force cookie, 'local': force localStore, 'session': force sessionStore
+		types: "active expanded focus selected"
+	},
+
+	/* Generic read/write string data to cookie, sessionStorage or localStorage. */
+	_data: function(key, value){
+		var ls = this._local.localStorage; // null, sessionStorage, or localStorage
+
+		if( value === undefined ) {
+			return ls ? ls.getItem(key) : cookieGetter(key);
+		} else if ( value === null ) {
+			if( ls ) {
+				ls.removeItem(key);
+			} else {
+				cookieRemover(key);
+			}
+		} else {
+			if( ls ) {
+				ls.setItem(key, value);
+			} else {
+				cookieSetter(key, value, this.options.persist.cookie);
+			}
+		}
+	},
+
+	/* Append `key` to a cookie. */
+	_appendKey: function(type, key, flag){
+		key = "" + key; // #90
+		var local = this._local,
+			instOpts = this.options.persist,
+			delim = instOpts.cookieDelimiter,
+			cookieName = local.cookiePrefix + type,
+			data = local._data(cookieName),
+			keyList = data ? data.split(delim) : [],
+			idx = $.inArray(key, keyList);
+		// Remove, even if we add a key,  so the key is always the last entry
+		if(idx >= 0){
+			keyList.splice(idx, 1);
+		}
+		// Append key to cookie
+		if(flag){
+			keyList.push(key);
+		}
+		local._data(cookieName, keyList.join(delim));
+	},
+
+	treeInit: function(ctx){
+		var tree = ctx.tree,
+			opts = ctx.options,
+			local = this._local,
+			instOpts = this.options.persist;
+
+		// For 'auto' or 'cookie' mode, the cookie plugin must be available
+		_assert((instOpts.store !== "auto" && instOpts.store !== "cookie") || cookieGetter,
+			"Missing required plugin for 'persist' extension: js.cookie.js or jquery.cookie.js");
+
+		local.cookiePrefix = instOpts.cookiePrefix || ("fancytree-" + tree._id + "-");
+		local.storeActive = instOpts.types.indexOf(ACTIVE) >= 0;
+		local.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0;
+		local.storeSelected = instOpts.types.indexOf(SELECTED) >= 0;
+		local.storeFocus = instOpts.types.indexOf(FOCUS) >= 0;
+		if( instOpts.store === "cookie" || !window.localStorage ) {
+			local.localStorage = null;
+		} else {
+			local.localStorage = (instOpts.store === "local") ? window.localStorage : window.sessionStorage;
+		}
+
+		// Bind init-handler to apply cookie state
+		tree.$div.on("fancytreeinit", function(event){
+			if ( tree._triggerTreeEvent("beforeRestore", null, {}) === false ) {
+				return;
+			}
+
+			var cookie, dfd, i, keyList, node,
+				prevFocus = local._data(local.cookiePrefix + FOCUS), // record this before node.setActive() overrides it;
+				noEvents = instOpts.fireActivate === false;
+
+			// tree.debug("document.cookie:", document.cookie);
+
+			cookie = local._data(local.cookiePrefix + EXPANDED);
+			keyList = cookie && cookie.split(instOpts.cookieDelimiter);
+
+			if( local.storeExpanded ) {
+				// Recursively load nested lazy nodes if expandLazy is 'expand' or 'load'
+				// Also remove expand-cookies for unmatched nodes
+				dfd = _loadLazyNodes(tree, local, keyList, instOpts.expandLazy ? "expand" : false , null);
+			} else {
+				// nothing to do
+				dfd = new $.Deferred().resolve();
+			}
+
+			dfd.done(function(){
+				if(local.storeSelected){
+					cookie = local._data(local.cookiePrefix + SELECTED);
+					if(cookie){
+						keyList = cookie.split(instOpts.cookieDelimiter);
+						for(i=0; i<keyList.length; i++){
+							node = tree.getNodeByKey(keyList[i]);
+							if(node){
+								if(node.selected === undefined || instOpts.overrideSource && (node.selected === false)){
+//									node.setSelected();
+									node.selected = true;
+									node.renderStatus();
+								}
+							}else{
+								// node is no longer member of the tree: remove from cookie also
+								local._appendKey(SELECTED, keyList[i], false);
+							}
+						}
+					}
+					// In selectMode 3 we have to fix the child nodes, since we
+					// only stored the selected *top* nodes
+					if( tree.options.selectMode === 3 ){
+						tree.visit(function(n){
+							if( n.selected ) {
+								n.fixSelection3AfterClick();
+								return "skip";
+							}
+						});
+					}
+				}
+				if(local.storeActive){
+					cookie = local._data(local.cookiePrefix + ACTIVE);
+					if(cookie && (opts.persist.overrideSource || !tree.activeNode)){
+						node = tree.getNodeByKey(cookie);
+						if(node){
+							node.debug("persist: set active", cookie);
+							// We only want to set the focus if the container
+							// had the keyboard focus before
+							node.setActive(true, {
+								noFocus: true,
+								noEvents: noEvents
+							});
+						}
+					}
+				}
+				if(local.storeFocus && prevFocus){
+					node = tree.getNodeByKey(prevFocus);
+					if(node){
+						// node.debug("persist: set focus", cookie);
+						if( tree.options.titlesTabbable ) {
+							$(node.span).find(".fancytree-title").focus();
+						} else {
+							$(tree.$container).focus();
+						}
+						// node.setFocus();
+					}
+				}
+				tree._triggerTreeEvent("restore", null, {});
+			});
+		});
+		// Init the tree
+		return this._superApply(arguments);
+	},
+	nodeSetActive: function(ctx, flag, callOpts) {
+		var res,
+			local = this._local;
+
+		flag = (flag !== false);
+		res = this._superApply(arguments);
+
+		if(local.storeActive){
+			local._data(local.cookiePrefix + ACTIVE, this.activeNode ? this.activeNode.key : null);
+		}
+		return res;
+	},
+	nodeSetExpanded: function(ctx, flag, callOpts) {
+		var res,
+			node = ctx.node,
+			local = this._local;
+
+		flag = (flag !== false);
+		res = this._superApply(arguments);
+
+		if(local.storeExpanded){
+			local._appendKey(EXPANDED, node.key, flag);
+		}
+		return res;
+	},
+	nodeSetFocus: function(ctx, flag) {
+		var res,
+			local = this._local;
+
+		flag = (flag !== false);
+		res = this._superApply(arguments);
+
+		if( local.storeFocus ) {
+			local._data(local.cookiePrefix + FOCUS, this.focusNode ? this.focusNode.key : null);
+		}
+		return res;
+	},
+	nodeSetSelected: function(ctx, flag, callOpts) {
+		var res, selNodes,
+			tree = ctx.tree,
+			node = ctx.node,
+			local = this._local;
+
+		flag = (flag !== false);
+		res = this._superApply(arguments);
+
+		if(local.storeSelected){
+			if( tree.options.selectMode === 3 ){
+				// In selectMode 3 we only store the the selected *top* nodes.
+				// De-selecting a node may also de-select some parents, so we
+				// calculate the current status again
+				selNodes = $.map(tree.getSelectedNodes(true), function(n){
+					return n.key;
+				});
+				selNodes = selNodes.join(ctx.options.persist.cookieDelimiter);
+				local._data(local.cookiePrefix + SELECTED, selNodes);
+			} else {
+				// beforeSelect can prevent the change - flag doesn't reflect the node.selected state
+				local._appendKey(SELECTED, node.key, node.selected);
+			}
+		}
+		return res;
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.table.js' *//*!
+ * jquery.fancytree.table.js
+ *
+ * Render tree as table (aka 'tree grid', 'table tree').
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+/* *****************************************************************************
+ * Private functions and variables
+ */
+function _assert(cond, msg){
+	msg = msg || "";
+	if(!cond){
+		$.error("Assertion failed " + msg);
+	}
+}
+
+function insertFirstChild(referenceNode, newNode) {
+	referenceNode.insertBefore(newNode, referenceNode.firstChild);
+}
+
+function insertSiblingAfter(referenceNode, newNode) {
+	referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
+}
+
+/* Show/hide all rows that are structural descendants of `parent`. */
+function setChildRowVisibility(parent, flag) {
+	parent.visit(function(node){
+		var tr = node.tr;
+		// currentFlag = node.hide ? false : flag; // fix for ext-filter
+		if(tr){
+			tr.style.display = (node.hide || !flag) ? "none" : "";
+		}
+		if(!node.expanded){
+			return "skip";
+		}
+	});
+}
+
+/* Find node that is rendered in previous row. */
+function findPrevRowNode(node){
+	var i, last, prev,
+		parent = node.parent,
+		siblings = parent ? parent.children : null;
+
+	if(siblings && siblings.length > 1 && siblings[0] !== node){
+		// use the lowest descendant of the preceeding sibling
+		i = $.inArray(node, siblings);
+		prev = siblings[i - 1];
+		_assert(prev.tr);
+		// descend to lowest child (with a <tr> tag)
+		while(prev.children && prev.children.length){
+			last = prev.children[prev.children.length - 1];
+			if(!last.tr){
+				break;
+			}
+			prev = last;
+		}
+	}else{
+		// if there is no preceding sibling, use the direct parent
+		prev = parent;
+	}
+	return prev;
+}
+
+/* Render callback for 'wide' mode. */
+// function _renderStatusNodeWide(event, data) {
+// 	var node = data.node,
+// 		nodeColumnIdx = data.options.table.nodeColumnIdx,
+// 		$tdList = $(node.tr).find(">td");
+
+// 	$tdList.eq(nodeColumnIdx).attr("colspan", data.tree.columnCount);
+// 	$tdList.not(":eq(" + nodeColumnIdx + ")").remove();
+// }
+
+
+$.ui.fancytree.registerExtension({
+	name: "table",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
+		// customStatus: false,	 // true: generate renderColumns events for status nodes
+		indentation: 16,         // indent every node level by 16px
+		nodeColumnIdx: 0         // render node expander, icon, and title to this column (default: #0)
+	},
+	// Overide virtual methods for this extension.
+	// `this`       : is this extension object
+	// `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
+	treeInit: function(ctx){
+		var i, columnCount, n, $row, $tbody,
+			tree = ctx.tree,
+			opts = ctx.options,
+			tableOpts = opts.table,
+			$table = tree.widget.element;
+
+		if( tableOpts.customStatus != null ) {
+			if( opts.renderStatusColumns != null) {
+				$.error("The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead.");
+			} else {
+				tree.warn("The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead.");
+				opts.renderStatusColumns = tableOpts.customStatus;
+			}
+		}
+		if( opts.renderStatusColumns ) {
+			if( opts.renderStatusColumns === true ) {
+				opts.renderStatusColumns = opts.renderColumns;
+			// } else if( opts.renderStatusColumns === "wide" ) {
+			// 	opts.renderStatusColumns = _renderStatusNodeWide;
+			}
+		}
+
+		$table.addClass("fancytree-container fancytree-ext-table");
+		tree.tbody = $table.find(">tbody")[0];
+		$tbody = $(tree.tbody);
+
+		// Prepare row templates:
+		// Determine column count from table header if any
+		columnCount = $("thead >tr:last >th", $table).length;
+		// Read TR templates from tbody if any
+		$row = $tbody.children("tr:first");
+		if( $row.length ) {
+			n = $row.children("td").length;
+			if( columnCount && n !== columnCount ) {
+				tree.warn("Column count mismatch between thead (" + columnCount + ") and tbody (" + n + "): using tbody.");
+				columnCount = n;
+			}
+			$row = $row.clone();
+		} else {
+			// Only thead is defined: create default row markup
+			_assert(columnCount >= 1, "Need either <thead> or <tbody> with <td> elements to determine column count.");
+			$row = $("<tr />");
+			for(i=0; i<columnCount; i++) {
+				$row.append("<td />");
+			}
+		}
+		$row.find(">td").eq(tableOpts.nodeColumnIdx)
+			.html("<span class='fancytree-node' />");
+		if( opts.aria ) {
+			$row.attr("role", "row");
+			$row.find("td").attr("role", "gridcell");
+		}
+		tree.rowFragment = document.createDocumentFragment();
+		tree.rowFragment.appendChild($row.get(0));
+
+		// // If tbody contains a second row, use this as status node template
+		// $row = $tbody.children("tr:eq(1)");
+		// if( $row.length === 0 ) {
+		// 	tree.statusRowFragment = tree.rowFragment;
+		// } else {
+		// 	$row = $row.clone();
+		// 	tree.statusRowFragment = document.createDocumentFragment();
+		// 	tree.statusRowFragment.appendChild($row.get(0));
+		// }
+		//
+		$tbody.empty();
+
+		// Make sure that status classes are set on the node's <tr> elements
+		tree.statusClassPropName = "tr";
+		tree.ariaPropName = "tr";
+		this.nodeContainerAttrName = "tr";
+
+		// #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table
+		tree.$container = $table;
+
+		this._superApply(arguments);
+
+		// standard Fancytree created a root UL
+		$(tree.rootNode.ul).remove();
+		tree.rootNode.ul = null;
+
+		// Add container to the TAB chain
+		// #577: Allow to set tabindex to "0", "-1" and ""
+		this.$container.attr("tabindex", opts.tabindex);
+		// this.$container.attr("tabindex", opts.tabbable ? "0" : "-1");
+		if(opts.aria) {
+			tree.$container
+				.attr("role", "treegrid")
+				.attr("aria-readonly", true);
+		}
+	},
+	nodeRemoveChildMarkup: function(ctx) {
+		var node = ctx.node;
+//		node.debug("nodeRemoveChildMarkup()");
+		node.visit(function(n){
+			if(n.tr){
+				$(n.tr).remove();
+				n.tr = null;
+			}
+		});
+	},
+	nodeRemoveMarkup: function(ctx) {
+		var node = ctx.node;
+//		node.debug("nodeRemoveMarkup()");
+		if(node.tr){
+			$(node.tr).remove();
+			node.tr = null;
+		}
+		this.nodeRemoveChildMarkup(ctx);
+	},
+	/* Override standard render. */
+	nodeRender: function(ctx, force, deep, collapsed, _recursive) {
+		var children, firstTr, i, l, newRow, prevNode, prevTr, subCtx,
+			tree = ctx.tree,
+			node = ctx.node,
+			opts = ctx.options,
+			isRootNode = !node.parent;
+
+		if( tree._enableUpdate === false ) {
+			// $.ui.fancytree.debug("*** nodeRender _enableUpdate: false");
+			return;
+		}
+		if( !_recursive ){
+			ctx.hasCollapsedParents = node.parent && !node.parent.expanded;
+		}
+		// $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr));
+		if( !isRootNode ){
+			if( node.tr && force ) {
+				this.nodeRemoveMarkup(ctx);
+			}
+			if( !node.tr ) {
+				if( ctx.hasCollapsedParents && !deep ) {
+					// #166: we assume that the parent will be (recursively) rendered
+					// later anyway.
+					// node.debug("nodeRender ignored due to unrendered parent");
+					return;
+				}
+				// Create new <tr> after previous row
+				// if( node.isStatusNode() ) {
+				// 	newRow = tree.statusRowFragment.firstChild.cloneNode(true);
+				// } else {
+				newRow = tree.rowFragment.firstChild.cloneNode(true);
+				// }
+				prevNode = findPrevRowNode(node);
+				// $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key);
+				_assert(prevNode);
+				if(collapsed === true && _recursive){
+					// hide all child rows, so we can use an animation to show it later
+					newRow.style.display = "none";
+				}else if(deep && ctx.hasCollapsedParents){
+					// also hide this row if deep === true but any parent is collapsed
+					newRow.style.display = "none";
+//					newRow.style.color = "red";
+				}
+				if(!prevNode.tr){
+					_assert(!prevNode.parent, "prev. row must have a tr, or be system root");
+					// tree.tbody.appendChild(newRow);
+					insertFirstChild(tree.tbody, newRow);  // #675
+				}else{
+					insertSiblingAfter(prevNode.tr, newRow);
+				}
+				node.tr = newRow;
+				if( node.key && opts.generateIds ){
+					node.tr.id = opts.idPrefix + node.key;
+				}
+				node.tr.ftnode = node;
+				// if(opts.aria){
+				// 	$(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key);
+				// }
+				node.span = $("span.fancytree-node", node.tr).get(0);
+				// Set icon, link, and title (normally this is only required on initial render)
+				this.nodeRenderTitle(ctx);
+				// Allow tweaking, binding, after node was created for the first time
+//				tree._triggerNodeEvent("createNode", ctx);
+				if ( opts.createNode ){
+					opts.createNode.call(tree, {type: "createNode"}, ctx);
+				}
+			} else {
+				if( force ) {
+					// Set icon, link, and title (normally this is only required on initial render)
+					this.nodeRenderTitle(ctx); // triggers renderColumns()
+				} else {
+					// Update element classes according to node state
+					this.nodeRenderStatus(ctx);
+				}
+			}
+		}
+		// Allow tweaking after node state was rendered
+//		tree._triggerNodeEvent("renderNode", ctx);
+		if ( opts.renderNode ){
+			opts.renderNode.call(tree, {type: "renderNode"}, ctx);
+		}
+		// Visit child nodes
+		// Add child markup
+		children = node.children;
+		if(children && (isRootNode || deep || node.expanded)){
+			for(i=0, l=children.length; i<l; i++) {
+				subCtx = $.extend({}, ctx, {node: children[i]});
+				subCtx.hasCollapsedParents = subCtx.hasCollapsedParents || !node.expanded;
+				this.nodeRender(subCtx, force, deep, collapsed, true);
+			}
+		}
+		// Make sure, that <tr> order matches node.children order.
+		if(children && !_recursive){ // we only have to do it once, for the root branch
+			prevTr = node.tr || null;
+			firstTr = tree.tbody.firstChild;
+			// Iterate over all descendants
+			node.visit(function(n){
+				if(n.tr){
+					if(!n.parent.expanded && n.tr.style.display !== "none"){
+						// fix after a node was dropped over a collapsed
+						n.tr.style.display = "none";
+						setChildRowVisibility(n, false);
+					}
+					if(n.tr.previousSibling !== prevTr){
+						node.debug("_fixOrder: mismatch at node: " + n);
+						var nextTr = prevTr ? prevTr.nextSibling : firstTr;
+						tree.tbody.insertBefore(n.tr, nextTr);
+					}
+					prevTr = n.tr;
+				}
+			});
+		}
+		// Update element classes according to node state
+		// if(!isRootNode){
+		// 	this.nodeRenderStatus(ctx);
+		// }
+	},
+	nodeRenderTitle: function(ctx, title) {
+		var $cb, res,
+			node = ctx.node,
+			opts = ctx.options,
+			isStatusNode = node.isStatusNode();
+
+		res = this._super(ctx, title);
+
+		if( node.isRootNode() ) {
+			return res;
+		}
+		// Move checkbox to custom column
+		if(opts.checkbox && !isStatusNode && opts.table.checkboxColumnIdx != null ){
+			$cb = $("span.fancytree-checkbox", node.span); //.detach();
+			$(node.tr).find("td").eq(+opts.table.checkboxColumnIdx).html($cb);
+		}
+		// Update element classes according to node state
+		this.nodeRenderStatus(ctx);
+
+		if( isStatusNode ) {
+			if( opts.renderStatusColumns ) {
+				// Let user code write column content
+				opts.renderStatusColumns.call(ctx.tree, {type: "renderStatusColumns"}, ctx);
+			} // else: default rendering for status node: leave other cells empty
+		} else if ( opts.renderColumns ) {
+			opts.renderColumns.call(ctx.tree, {type: "renderColumns"}, ctx);
+		}
+		return res;
+	},
+	nodeRenderStatus: function(ctx) {
+		var indent,
+			node = ctx.node,
+			opts = ctx.options;
+
+		this._super(ctx);
+
+		$(node.tr).removeClass("fancytree-node");
+		// indent
+		indent = (node.getLevel() - 1) * opts.table.indentation;
+		$(node.span).css({paddingLeft: indent + "px"});  // #460
+		// $(node.span).css({marginLeft: indent + "px"});
+	 },
+	/* Expand node, return Deferred.promise. */
+	nodeSetExpanded: function(ctx, flag, callOpts) {
+		// flag defaults to true
+		flag = (flag !== false);
+
+		if((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) {
+			// Expanded state isn't changed - just call base implementation
+			return this._superApply(arguments);
+		}
+
+		var dfd = new $.Deferred(),
+			subOpts = $.extend({}, callOpts, {noEvents: true, noAnimation: true});
+
+		callOpts = callOpts || {};
+
+		function _afterExpand(ok) {
+			setChildRowVisibility(ctx.node, flag);
+			if( ok ) {
+				if( flag && ctx.options.autoScroll && !callOpts.noAnimation && ctx.node.hasChildren() ) {
+					// Scroll down to last child, but keep current node visible
+					ctx.node.getLastChild().scrollIntoView(true, {topNode: ctx.node}).always(function(){
+						if( !callOpts.noEvents ) {
+							ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx);
+						}
+						dfd.resolveWith(ctx.node);
+					});
+				} else {
+					if( !callOpts.noEvents ) {
+						ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx);
+					}
+					dfd.resolveWith(ctx.node);
+				}
+			} else {
+				if( !callOpts.noEvents ) {
+					ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx);
+				}
+				dfd.rejectWith(ctx.node);
+			}
+		}
+		// Call base-expand with disabled events and animation
+		this._super(ctx, flag, subOpts).done(function () {
+			_afterExpand(true);
+		}).fail(function () {
+			_afterExpand(false);
+		});
+		return dfd.promise();
+	},
+	nodeSetStatus: function(ctx, status, message, details) {
+		if(status === "ok"){
+			var node = ctx.node,
+				firstChild = ( node.children ? node.children[0] : null );
+			if ( firstChild && firstChild.isStatusNode() ) {
+				$(firstChild.tr).remove();
+			}
+		}
+		return this._superApply(arguments);
+	},
+	treeClear: function(ctx) {
+		this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode));
+		return this._superApply(arguments);
+	},
+	treeDestroy: function(ctx) {
+		this.$container.find("tbody").empty();
+		this.$source && this.$source.removeClass("ui-helper-hidden");
+		return this._superApply(arguments);
+	}
+	/*,
+	treeSetFocus: function(ctx, flag) {
+//	        alert("treeSetFocus" + ctx.tree.$container);
+		ctx.tree.$container.focus();
+		$.ui.fancytree.focusTree = ctx.tree;
+	}*/
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.themeroller.js' *//*!
+ * jquery.fancytree.themeroller.js
+ *
+ * Enable jQuery UI ThemeRoller styles.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * @see http://jqueryui.com/themeroller/
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+/*******************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "themeroller",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		activeClass: "ui-state-active",      // Class added to active node
+		// activeClass: "ui-state-highlight",
+		addClass: "ui-corner-all",           // Class added to all nodes
+		focusClass: "ui-state-focus",        // Class added to focused node
+		hoverClass: "ui-state-hover",        // Class added to hovered node
+		selectedClass: "ui-state-highlight"  // Class added to selected nodes
+		// selectedClass: "ui-state-active"
+	},
+
+	treeInit: function(ctx){
+		var $el = ctx.widget.element,
+			opts = ctx.options.themeroller;
+
+		this._superApply(arguments);
+
+		if($el[0].nodeName === "TABLE"){
+			$el.addClass("ui-widget ui-corner-all");
+			$el.find(">thead tr").addClass("ui-widget-header");
+			$el.find(">tbody").addClass("ui-widget-conent");
+		}else{
+			$el.addClass("ui-widget ui-widget-content ui-corner-all");
+		}
+
+		$el.delegate(".fancytree-node", "mouseenter mouseleave", function(event){
+			var node = $.ui.fancytree.getNode(event.target),
+				flag = (event.type === "mouseenter");
+
+			$(node.tr ? node.tr : node.span)
+				.toggleClass(opts.hoverClass + " " + opts.addClass, flag);
+		});
+	},
+	treeDestroy: function(ctx){
+		this._superApply(arguments);
+		ctx.widget.element.removeClass("ui-widget ui-widget-content ui-corner-all");
+	},
+	nodeRenderStatus: function(ctx){
+		var classes = {},
+			node = ctx.node,
+			$el = $(node.tr ? node.tr : node.span),
+			opts = ctx.options.themeroller;
+
+		this._super(ctx);
+/*
+		.ui-state-highlight: Class to be applied to highlighted or selected elements. Applies "highlight" container styles to an element and its child text, links, and icons.
+		.ui-state-error: Class to be applied to error messaging container elements. Applies "error" container styles to an element and its child text, links, and icons.
+		.ui-state-error-text: An additional class that applies just the error text color without background. Can be used on form labels for instance. Also applies error icon color to child icons.
+
+		.ui-state-default: Class to be applied to clickable button-like elements. Applies "clickable default" container styles to an element and its child text, links, and icons.
+		.ui-state-hover: Class to be applied on mouseover to clickable button-like elements. Applies "clickable hover" container styles to an element and its child text, links, and icons.
+		.ui-state-focus: Class to be applied on keyboard focus to clickable button-like elements. Applies "clickable hover" container styles to an element and its child text, links, and icons.
+		.ui-state-active: Class to be applied on mousedown to clickable button-like elements. Applies "clickable active" container styles to an element and its child text, links, and icons.
+*/
+		// Set ui-state-* class (handle the case that the same class is assigned
+		// to different states)
+		classes[opts.activeClass] = false;
+		classes[opts.focusClass] = false;
+		classes[opts.selectedClass] = false;
+		if( node.isActive() ) { classes[opts.activeClass] = true; }
+		if( node.hasFocus() ) { classes[opts.focusClass]  = true; }
+		// activeClass takes precedence before selectedClass:
+		if( node.isSelected() && !node.isActive() ) { classes[opts.selectedClass]  = true; }
+		$el.toggleClass(opts.activeClass, classes[opts.activeClass]);
+		$el.toggleClass(opts.focusClass, classes[opts.focusClass]);
+		$el.toggleClass(opts.selectedClass, classes[opts.selectedClass]);
+		// Additional classes (e.g. 'ui-corner-all')
+		$el.addClass(opts.addClass);
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+
+/*! Extension 'jquery.fancytree.wide.js' *//*!
+ * jquery.fancytree.wide.js
+ * Support for 100% wide selection bars.
+ * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
+ *
+ * Copyright (c) 2008-2017, Martin Wendt (http://wwWendt.de)
+ *
+ * Released under the MIT license
+ * https://github.com/mar10/fancytree/wiki/LicenseInfo
+ *
+ * @version 2.26.0
+ * @date 2017-11-04T17:52:53Z
+ */
+
+;(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		// AMD. Register as an anonymous module.
+		define( [ "jquery", "./jquery.fancytree" ], factory );
+	} else if ( typeof module === "object" && module.exports ) {
+		// Node/CommonJS
+		require("jquery.fancytree");
+		module.exports = factory(require("jquery"));
+	} else {
+		// Browser globals
+		factory( jQuery );
+	}
+
+}( function( $ ) {
+
+"use strict";
+
+var reNumUnit = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/; // split "1.5em" to ["1.5", "em"]
+
+/*******************************************************************************
+ * Private functions and variables
+ */
+// var _assert = $.ui.fancytree.assert;
+
+/* Calculate inner width without scrollbar */
+// function realInnerWidth($el) {
+// 	// http://blog.jquery.com/2012/08/16/jquery-1-8-box-sizing-width-csswidth-and-outerwidth/
+// //	inst.contWidth = parseFloat(this.$container.css("width"), 10);
+// 	// 'Client width without scrollbar' - 'padding'
+// 	return $el[0].clientWidth - ($el.innerWidth() -  parseFloat($el.css("width"), 10));
+// }
+
+/* Create a global embedded CSS style for the tree. */
+function defineHeadStyleElement(id, cssText) {
+	id = "fancytree-style-" + id;
+	var $headStyle = $("#" + id);
+
+	if( !cssText ) {
+		$headStyle.remove();
+		return null;
+	}
+	if( !$headStyle.length ) {
+		$headStyle = $("<style />")
+			.attr("id", id)
+			.addClass("fancytree-style")
+			.prop("type", "text/css")
+			.appendTo("head");
+	}
+	try {
+		$headStyle.html(cssText);
+	} catch ( e ) {
+		// fix for IE 6-8
+		$headStyle[0].styleSheet.cssText = cssText;
+	}
+	return $headStyle;
+}
+
+/* Calculate the CSS rules that indent title spans. */
+function renderLevelCss(containerId, depth, levelOfs, lineOfs, labelOfs, measureUnit)
+{
+	var i,
+		prefix = "#" + containerId + " span.fancytree-level-",
+		rules = [];
+
+	for(i = 0; i < depth; i++) {
+		rules.push(prefix + (i + 1) + " span.fancytree-title { padding-left: " +
+			(i * levelOfs + lineOfs) + measureUnit + "; }");
+	}
+	// Some UI animations wrap the UL inside a DIV and set position:relative on both.
+	// This breaks the left:0 and padding-left:nn settings of the title
+	rules.push(
+		"#" + containerId + " div.ui-effects-wrapper ul li span.fancytree-title, " +
+		"#" + containerId + " ul.fancytree-animating span.fancytree-title " +  // #716
+		"{ padding-left: " + labelOfs + measureUnit + "; position: static; width: auto; }");
+	return rules.join("\n");
+}
+
+
+// /**
+//  * [ext-wide] Recalculate the width of the selection bar after the tree container
+//  * was resized.<br>
+//  * May be called explicitly on container resize, since there is no resize event
+//  * for DIV tags.
+//  *
+//  * @alias Fancytree#wideUpdate
+//  * @requires jquery.fancytree.wide.js
+//  */
+// $.ui.fancytree._FancytreeClass.prototype.wideUpdate = function(){
+// 	var inst = this.ext.wide,
+// 		prevCw = inst.contWidth,
+// 		prevLo = inst.lineOfs;
+
+// 	inst.contWidth = realInnerWidth(this.$container);
+// 	// Each title is precceeded by 2 or 3 icons (16px + 3 margin)
+// 	//     + 1px title border and 3px title padding
+// 	// TODO: use code from treeInit() below
+// 	inst.lineOfs = (this.options.checkbox ? 3 : 2) * 19;
+// 	if( prevCw !== inst.contWidth || prevLo !== inst.lineOfs ) {
+// 		this.debug("wideUpdate: " + inst.contWidth);
+// 		this.visit(function(node){
+// 			node.tree._callHook("nodeRenderTitle", node);
+// 		});
+// 	}
+// };
+
+/*******************************************************************************
+ * Extension code
+ */
+$.ui.fancytree.registerExtension({
+	name: "wide",
+	version: "2.26.0",
+	// Default options for this extension.
+	options: {
+		iconWidth: null,     // Adjust this if @fancy-icon-width != "16px"
+		iconSpacing: null,   // Adjust this if @fancy-icon-spacing != "3px"
+		labelSpacing: null,  // Adjust this if padding between icon and label != "3px"
+		levelOfs: null       // Adjust this if ul padding != "16px"
+	},
+
+	treeCreate: function(ctx){
+		this._superApply(arguments);
+		this.$container.addClass("fancytree-ext-wide");
+
+		var containerId, cssText, iconSpacingUnit, labelSpacingUnit, iconWidthUnit, levelOfsUnit,
+			instOpts = ctx.options.wide,
+			// css sniffing
+			$dummyLI = $("<li id='fancytreeTemp'><span class='fancytree-node'><span class='fancytree-icon' /><span class='fancytree-title' /></span><ul />")
+				.appendTo(ctx.tree.$container),
+			$dummyIcon = $dummyLI.find(".fancytree-icon"),
+			$dummyUL = $dummyLI.find("ul"),
+			// $dummyTitle = $dummyLI.find(".fancytree-title"),
+			iconSpacing = instOpts.iconSpacing || $dummyIcon.css("margin-left"),
+			iconWidth = instOpts.iconWidth || $dummyIcon.css("width"),
+			labelSpacing = instOpts.labelSpacing || "3px",
+			levelOfs = instOpts.levelOfs || $dummyUL.css("padding-left");
+
+		$dummyLI.remove();
+
+		iconSpacingUnit = iconSpacing.match(reNumUnit)[2];
+		iconSpacing = parseFloat(iconSpacing, 10);
+		labelSpacingUnit = labelSpacing.match(reNumUnit)[2];
+		labelSpacing = parseFloat(labelSpacing, 10);
+		iconWidthUnit = iconWidth.match(reNumUnit)[2];
+		iconWidth = parseFloat(iconWidth, 10);
+		levelOfsUnit = levelOfs.match(reNumUnit)[2];
+		if( iconSpacingUnit !== iconWidthUnit || levelOfsUnit !== iconWidthUnit || labelSpacingUnit !== iconWidthUnit ) {
+			$.error("iconWidth, iconSpacing, and levelOfs must have the same css measure unit");
+		}
+		this._local.measureUnit = iconWidthUnit;
+		this._local.levelOfs = parseFloat(levelOfs);
+		this._local.lineOfs = (1 + (ctx.options.checkbox ? 1 : 0) +
+				(ctx.options.icon === false ? 0 : 1)) * (iconWidth + iconSpacing) +
+				iconSpacing;
+		this._local.labelOfs = labelSpacing;
+		this._local.maxDepth = 10;
+
+		// Get/Set a unique Id on the container (if not already exists)
+		containerId = this.$container.uniqueId().attr("id");
+		// Generated css rules for some levels (extended on demand)
+		cssText = renderLevelCss(containerId, this._local.maxDepth,
+			this._local.levelOfs, this._local.lineOfs, this._local.labelOfs,
+			this._local.measureUnit);
+		defineHeadStyleElement(containerId, cssText);
+	},
+	treeDestroy: function(ctx){
+		// Remove generated css rules
+		defineHeadStyleElement(this.$container.attr("id"), null);
+		return this._superApply(arguments);
+	},
+	nodeRenderStatus: function(ctx) {
+		var containerId, cssText, res,
+			node = ctx.node,
+			level = node.getLevel();
+
+		res = this._super(ctx);
+		// Generate some more level-n rules if required
+		if( level > this._local.maxDepth ) {
+			containerId = this.$container.attr("id");
+			this._local.maxDepth *= 2;
+			node.debug("Define global ext-wide css up to level " + this._local.maxDepth);
+			cssText = renderLevelCss(containerId, this._local.maxDepth,
+				this._local.levelOfs, this._local.lineOfs, this._local.labelSpacing,
+				this._local.measureUnit);
+			defineHeadStyleElement(containerId, cssText);
+		}
+		// Add level-n class to apply indentation padding.
+		// (Setting element style would not work, since it cannot easily be
+		// overriden while animations run)
+		$(node.span).addClass("fancytree-level-" + level);
+		return res;
+	}
+});
+// Value returned by `require('jquery.fancytree..')`
+return $.ui.fancytree;
+}));  // End of closure
+
+// Value returned by `require('jquery.fancytree')`
+return $.ui.fancytree;
+}));  // End of closure

BIN
gui/default/vendor/fancytree/skin-lion/icons.gif


BIN
gui/default/vendor/fancytree/skin-lion/loading.gif


BIN
gui/default/vendor/fancytree/skin-lion/vline.gif


+ 4517 - 0
gui/default/vendor/moment/moment.js

@@ -0,0 +1,4517 @@
+//! moment.js
+//! version : 2.19.4
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+
+;(function (global, factory) {
+    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+    typeof define === 'function' && define.amd ? define(factory) :
+    global.moment = factory()
+}(this, (function () { 'use strict';
+
+var hookCallback;
+
+function hooks () {
+    return hookCallback.apply(null, arguments);
+}
+
+// This is done to register the method called with moment()
+// without creating circular dependencies.
+function setHookCallback (callback) {
+    hookCallback = callback;
+}
+
+function isArray(input) {
+    return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]';
+}
+
+function isObject(input) {
+    // IE8 will treat undefined and null as object if it wasn't for
+    // input != null
+    return input != null && Object.prototype.toString.call(input) === '[object Object]';
+}
+
+function isObjectEmpty(obj) {
+    if (Object.getOwnPropertyNames) {
+        return (Object.getOwnPropertyNames(obj).length === 0);
+    } else {
+        var k;
+        for (k in obj) {
+            if (obj.hasOwnProperty(k)) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
+
+function isUndefined(input) {
+    return input === void 0;
+}
+
+function isNumber(input) {
+    return typeof input === 'number' || Object.prototype.toString.call(input) === '[object Number]';
+}
+
+function isDate(input) {
+    return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
+}
+
+function map(arr, fn) {
+    var res = [], i;
+    for (i = 0; i < arr.length; ++i) {
+        res.push(fn(arr[i], i));
+    }
+    return res;
+}
+
+function hasOwnProp(a, b) {
+    return Object.prototype.hasOwnProperty.call(a, b);
+}
+
+function extend(a, b) {
+    for (var i in b) {
+        if (hasOwnProp(b, i)) {
+            a[i] = b[i];
+        }
+    }
+
+    if (hasOwnProp(b, 'toString')) {
+        a.toString = b.toString;
+    }
+
+    if (hasOwnProp(b, 'valueOf')) {
+        a.valueOf = b.valueOf;
+    }
+
+    return a;
+}
+
+function createUTC (input, format, locale, strict) {
+    return createLocalOrUTC(input, format, locale, strict, true).utc();
+}
+
+function defaultParsingFlags() {
+    // We need to deep clone this object.
+    return {
+        empty           : false,
+        unusedTokens    : [],
+        unusedInput     : [],
+        overflow        : -2,
+        charsLeftOver   : 0,
+        nullInput       : false,
+        invalidMonth    : null,
+        invalidFormat   : false,
+        userInvalidated : false,
+        iso             : false,
+        parsedDateParts : [],
+        meridiem        : null,
+        rfc2822         : false,
+        weekdayMismatch : false
+    };
+}
+
+function getParsingFlags(m) {
+    if (m._pf == null) {
+        m._pf = defaultParsingFlags();
+    }
+    return m._pf;
+}
+
+var some;
+if (Array.prototype.some) {
+    some = Array.prototype.some;
+} else {
+    some = function (fun) {
+        var t = Object(this);
+        var len = t.length >>> 0;
+
+        for (var i = 0; i < len; i++) {
+            if (i in t && fun.call(this, t[i], i, t)) {
+                return true;
+            }
+        }
+
+        return false;
+    };
+}
+
+function isValid(m) {
+    if (m._isValid == null) {
+        var flags = getParsingFlags(m);
+        var parsedParts = some.call(flags.parsedDateParts, function (i) {
+            return i != null;
+        });
+        var isNowValid = !isNaN(m._d.getTime()) &&
+            flags.overflow < 0 &&
+            !flags.empty &&
+            !flags.invalidMonth &&
+            !flags.invalidWeekday &&
+            !flags.weekdayMismatch &&
+            !flags.nullInput &&
+            !flags.invalidFormat &&
+            !flags.userInvalidated &&
+            (!flags.meridiem || (flags.meridiem && parsedParts));
+
+        if (m._strict) {
+            isNowValid = isNowValid &&
+                flags.charsLeftOver === 0 &&
+                flags.unusedTokens.length === 0 &&
+                flags.bigHour === undefined;
+        }
+
+        if (Object.isFrozen == null || !Object.isFrozen(m)) {
+            m._isValid = isNowValid;
+        }
+        else {
+            return isNowValid;
+        }
+    }
+    return m._isValid;
+}
+
+function createInvalid (flags) {
+    var m = createUTC(NaN);
+    if (flags != null) {
+        extend(getParsingFlags(m), flags);
+    }
+    else {
+        getParsingFlags(m).userInvalidated = true;
+    }
+
+    return m;
+}
+
+// Plugins that add properties should also add the key here (null value),
+// so we can properly clone ourselves.
+var momentProperties = hooks.momentProperties = [];
+
+function copyConfig(to, from) {
+    var i, prop, val;
+
+    if (!isUndefined(from._isAMomentObject)) {
+        to._isAMomentObject = from._isAMomentObject;
+    }
+    if (!isUndefined(from._i)) {
+        to._i = from._i;
+    }
+    if (!isUndefined(from._f)) {
+        to._f = from._f;
+    }
+    if (!isUndefined(from._l)) {
+        to._l = from._l;
+    }
+    if (!isUndefined(from._strict)) {
+        to._strict = from._strict;
+    }
+    if (!isUndefined(from._tzm)) {
+        to._tzm = from._tzm;
+    }
+    if (!isUndefined(from._isUTC)) {
+        to._isUTC = from._isUTC;
+    }
+    if (!isUndefined(from._offset)) {
+        to._offset = from._offset;
+    }
+    if (!isUndefined(from._pf)) {
+        to._pf = getParsingFlags(from);
+    }
+    if (!isUndefined(from._locale)) {
+        to._locale = from._locale;
+    }
+
+    if (momentProperties.length > 0) {
+        for (i = 0; i < momentProperties.length; i++) {
+            prop = momentProperties[i];
+            val = from[prop];
+            if (!isUndefined(val)) {
+                to[prop] = val;
+            }
+        }
+    }
+
+    return to;
+}
+
+var updateInProgress = false;
+
+// Moment prototype object
+function Moment(config) {
+    copyConfig(this, config);
+    this._d = new Date(config._d != null ? config._d.getTime() : NaN);
+    if (!this.isValid()) {
+        this._d = new Date(NaN);
+    }
+    // Prevent infinite loop in case updateOffset creates new moment
+    // objects.
+    if (updateInProgress === false) {
+        updateInProgress = true;
+        hooks.updateOffset(this);
+        updateInProgress = false;
+    }
+}
+
+function isMoment (obj) {
+    return obj instanceof Moment || (obj != null && obj._isAMomentObject != null);
+}
+
+function absFloor (number) {
+    if (number < 0) {
+        // -0 -> 0
+        return Math.ceil(number) || 0;
+    } else {
+        return Math.floor(number);
+    }
+}
+
+function toInt(argumentForCoercion) {
+    var coercedNumber = +argumentForCoercion,
+        value = 0;
+
+    if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+        value = absFloor(coercedNumber);
+    }
+
+    return value;
+}
+
+// compare two arrays, return the number of differences
+function compareArrays(array1, array2, dontConvert) {
+    var len = Math.min(array1.length, array2.length),
+        lengthDiff = Math.abs(array1.length - array2.length),
+        diffs = 0,
+        i;
+    for (i = 0; i < len; i++) {
+        if ((dontConvert && array1[i] !== array2[i]) ||
+            (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+            diffs++;
+        }
+    }
+    return diffs + lengthDiff;
+}
+
+function warn(msg) {
+    if (hooks.suppressDeprecationWarnings === false &&
+            (typeof console !==  'undefined') && console.warn) {
+        console.warn('Deprecation warning: ' + msg);
+    }
+}
+
+function deprecate(msg, fn) {
+    var firstTime = true;
+
+    return extend(function () {
+        if (hooks.deprecationHandler != null) {
+            hooks.deprecationHandler(null, msg);
+        }
+        if (firstTime) {
+            var args = [];
+            var arg;
+            for (var i = 0; i < arguments.length; i++) {
+                arg = '';
+                if (typeof arguments[i] === 'object') {
+                    arg += '\n[' + i + '] ';
+                    for (var key in arguments[0]) {
+                        arg += key + ': ' + arguments[0][key] + ', ';
+                    }
+                    arg = arg.slice(0, -2); // Remove trailing comma and space
+                } else {
+                    arg = arguments[i];
+                }
+                args.push(arg);
+            }
+            warn(msg + '\nArguments: ' + Array.prototype.slice.call(args).join('') + '\n' + (new Error()).stack);
+            firstTime = false;
+        }
+        return fn.apply(this, arguments);
+    }, fn);
+}
+
+var deprecations = {};
+
+function deprecateSimple(name, msg) {
+    if (hooks.deprecationHandler != null) {
+        hooks.deprecationHandler(name, msg);
+    }
+    if (!deprecations[name]) {
+        warn(msg);
+        deprecations[name] = true;
+    }
+}
+
+hooks.suppressDeprecationWarnings = false;
+hooks.deprecationHandler = null;
+
+function isFunction(input) {
+    return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]';
+}
+
+function set (config) {
+    var prop, i;
+    for (i in config) {
+        prop = config[i];
+        if (isFunction(prop)) {
+            this[i] = prop;
+        } else {
+            this['_' + i] = prop;
+        }
+    }
+    this._config = config;
+    // Lenient ordinal parsing accepts just a number in addition to
+    // number + (possibly) stuff coming from _dayOfMonthOrdinalParse.
+    // TODO: Remove "ordinalParse" fallback in next major release.
+    this._dayOfMonthOrdinalParseLenient = new RegExp(
+        (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) +
+            '|' + (/\d{1,2}/).source);
+}
+
+function mergeConfigs(parentConfig, childConfig) {
+    var res = extend({}, parentConfig), prop;
+    for (prop in childConfig) {
+        if (hasOwnProp(childConfig, prop)) {
+            if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) {
+                res[prop] = {};
+                extend(res[prop], parentConfig[prop]);
+                extend(res[prop], childConfig[prop]);
+            } else if (childConfig[prop] != null) {
+                res[prop] = childConfig[prop];
+            } else {
+                delete res[prop];
+            }
+        }
+    }
+    for (prop in parentConfig) {
+        if (hasOwnProp(parentConfig, prop) &&
+                !hasOwnProp(childConfig, prop) &&
+                isObject(parentConfig[prop])) {
+            // make sure changes to properties don't modify parent config
+            res[prop] = extend({}, res[prop]);
+        }
+    }
+    return res;
+}
+
+function Locale(config) {
+    if (config != null) {
+        this.set(config);
+    }
+}
+
+var keys;
+
+if (Object.keys) {
+    keys = Object.keys;
+} else {
+    keys = function (obj) {
+        var i, res = [];
+        for (i in obj) {
+            if (hasOwnProp(obj, i)) {
+                res.push(i);
+            }
+        }
+        return res;
+    };
+}
+
+var defaultCalendar = {
+    sameDay : '[Today at] LT',
+    nextDay : '[Tomorrow at] LT',
+    nextWeek : 'dddd [at] LT',
+    lastDay : '[Yesterday at] LT',
+    lastWeek : '[Last] dddd [at] LT',
+    sameElse : 'L'
+};
+
+function calendar (key, mom, now) {
+    var output = this._calendar[key] || this._calendar['sameElse'];
+    return isFunction(output) ? output.call(mom, now) : output;
+}
+
+var defaultLongDateFormat = {
+    LTS  : 'h:mm:ss A',
+    LT   : 'h:mm A',
+    L    : 'MM/DD/YYYY',
+    LL   : 'MMMM D, YYYY',
+    LLL  : 'MMMM D, YYYY h:mm A',
+    LLLL : 'dddd, MMMM D, YYYY h:mm A'
+};
+
+function longDateFormat (key) {
+    var format = this._longDateFormat[key],
+        formatUpper = this._longDateFormat[key.toUpperCase()];
+
+    if (format || !formatUpper) {
+        return format;
+    }
+
+    this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) {
+        return val.slice(1);
+    });
+
+    return this._longDateFormat[key];
+}
+
+var defaultInvalidDate = 'Invalid date';
+
+function invalidDate () {
+    return this._invalidDate;
+}
+
+var defaultOrdinal = '%d';
+var defaultDayOfMonthOrdinalParse = /\d{1,2}/;
+
+function ordinal (number) {
+    return this._ordinal.replace('%d', number);
+}
+
+var defaultRelativeTime = {
+    future : 'in %s',
+    past   : '%s ago',
+    s  : 'a few seconds',
+    ss : '%d seconds',
+    m  : 'a minute',
+    mm : '%d minutes',
+    h  : 'an hour',
+    hh : '%d hours',
+    d  : 'a day',
+    dd : '%d days',
+    M  : 'a month',
+    MM : '%d months',
+    y  : 'a year',
+    yy : '%d years'
+};
+
+function relativeTime (number, withoutSuffix, string, isFuture) {
+    var output = this._relativeTime[string];
+    return (isFunction(output)) ?
+        output(number, withoutSuffix, string, isFuture) :
+        output.replace(/%d/i, number);
+}
+
+function pastFuture (diff, output) {
+    var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+    return isFunction(format) ? format(output) : format.replace(/%s/i, output);
+}
+
+var aliases = {};
+
+function addUnitAlias (unit, shorthand) {
+    var lowerCase = unit.toLowerCase();
+    aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit;
+}
+
+function normalizeUnits(units) {
+    return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined;
+}
+
+function normalizeObjectUnits(inputObject) {
+    var normalizedInput = {},
+        normalizedProp,
+        prop;
+
+    for (prop in inputObject) {
+        if (hasOwnProp(inputObject, prop)) {
+            normalizedProp = normalizeUnits(prop);
+            if (normalizedProp) {
+                normalizedInput[normalizedProp] = inputObject[prop];
+            }
+        }
+    }
+
+    return normalizedInput;
+}
+
+var priorities = {};
+
+function addUnitPriority(unit, priority) {
+    priorities[unit] = priority;
+}
+
+function getPrioritizedUnits(unitsObj) {
+    var units = [];
+    for (var u in unitsObj) {
+        units.push({unit: u, priority: priorities[u]});
+    }
+    units.sort(function (a, b) {
+        return a.priority - b.priority;
+    });
+    return units;
+}
+
+function zeroFill(number, targetLength, forceSign) {
+    var absNumber = '' + Math.abs(number),
+        zerosToFill = targetLength - absNumber.length,
+        sign = number >= 0;
+    return (sign ? (forceSign ? '+' : '') : '-') +
+        Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber;
+}
+
+var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;
+
+var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g;
+
+var formatFunctions = {};
+
+var formatTokenFunctions = {};
+
+// token:    'M'
+// padded:   ['MM', 2]
+// ordinal:  'Mo'
+// callback: function () { this.month() + 1 }
+function addFormatToken (token, padded, ordinal, callback) {
+    var func = callback;
+    if (typeof callback === 'string') {
+        func = function () {
+            return this[callback]();
+        };
+    }
+    if (token) {
+        formatTokenFunctions[token] = func;
+    }
+    if (padded) {
+        formatTokenFunctions[padded[0]] = function () {
+            return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
+        };
+    }
+    if (ordinal) {
+        formatTokenFunctions[ordinal] = function () {
+            return this.localeData().ordinal(func.apply(this, arguments), token);
+        };
+    }
+}
+
+function removeFormattingTokens(input) {
+    if (input.match(/\[[\s\S]/)) {
+        return input.replace(/^\[|\]$/g, '');
+    }
+    return input.replace(/\\/g, '');
+}
+
+function makeFormatFunction(format) {
+    var array = format.match(formattingTokens), i, length;
+
+    for (i = 0, length = array.length; i < length; i++) {
+        if (formatTokenFunctions[array[i]]) {
+            array[i] = formatTokenFunctions[array[i]];
+        } else {
+            array[i] = removeFormattingTokens(array[i]);
+        }
+    }
+
+    return function (mom) {
+        var output = '', i;
+        for (i = 0; i < length; i++) {
+            output += isFunction(array[i]) ? array[i].call(mom, format) : array[i];
+        }
+        return output;
+    };
+}
+
+// format date using native date object
+function formatMoment(m, format) {
+    if (!m.isValid()) {
+        return m.localeData().invalidDate();
+    }
+
+    format = expandFormat(format, m.localeData());
+    formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format);
+
+    return formatFunctions[format](m);
+}
+
+function expandFormat(format, locale) {
+    var i = 5;
+
+    function replaceLongDateFormatTokens(input) {
+        return locale.longDateFormat(input) || input;
+    }
+
+    localFormattingTokens.lastIndex = 0;
+    while (i >= 0 && localFormattingTokens.test(format)) {
+        format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+        localFormattingTokens.lastIndex = 0;
+        i -= 1;
+    }
+
+    return format;
+}
+
+var match1         = /\d/;            //       0 - 9
+var match2         = /\d\d/;          //      00 - 99
+var match3         = /\d{3}/;         //     000 - 999
+var match4         = /\d{4}/;         //    0000 - 9999
+var match6         = /[+-]?\d{6}/;    // -999999 - 999999
+var match1to2      = /\d\d?/;         //       0 - 99
+var match3to4      = /\d\d\d\d?/;     //     999 - 9999
+var match5to6      = /\d\d\d\d\d\d?/; //   99999 - 999999
+var match1to3      = /\d{1,3}/;       //       0 - 999
+var match1to4      = /\d{1,4}/;       //       0 - 9999
+var match1to6      = /[+-]?\d{1,6}/;  // -999999 - 999999
+
+var matchUnsigned  = /\d+/;           //       0 - inf
+var matchSigned    = /[+-]?\d+/;      //    -inf - inf
+
+var matchOffset    = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z
+var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z
+
+var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123
+
+// any word (or two) characters or numbers including two/three word month in arabic.
+// includes scottish gaelic two word and hyphenated months
+var matchWord = /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i;
+
+
+var regexes = {};
+
+function addRegexToken (token, regex, strictRegex) {
+    regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) {
+        return (isStrict && strictRegex) ? strictRegex : regex;
+    };
+}
+
+function getParseRegexForToken (token, config) {
+    if (!hasOwnProp(regexes, token)) {
+        return new RegExp(unescapeFormat(token));
+    }
+
+    return regexes[token](config._strict, config._locale);
+}
+
+// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+function unescapeFormat(s) {
+    return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+        return p1 || p2 || p3 || p4;
+    }));
+}
+
+function regexEscape(s) {
+    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
+var tokens = {};
+
+function addParseToken (token, callback) {
+    var i, func = callback;
+    if (typeof token === 'string') {
+        token = [token];
+    }
+    if (isNumber(callback)) {
+        func = function (input, array) {
+            array[callback] = toInt(input);
+        };
+    }
+    for (i = 0; i < token.length; i++) {
+        tokens[token[i]] = func;
+    }
+}
+
+function addWeekParseToken (token, callback) {
+    addParseToken(token, function (input, array, config, token) {
+        config._w = config._w || {};
+        callback(input, config._w, config, token);
+    });
+}
+
+function addTimeToArrayFromToken(token, input, config) {
+    if (input != null && hasOwnProp(tokens, token)) {
+        tokens[token](input, config._a, config, token);
+    }
+}
+
+var YEAR = 0;
+var MONTH = 1;
+var DATE = 2;
+var HOUR = 3;
+var MINUTE = 4;
+var SECOND = 5;
+var MILLISECOND = 6;
+var WEEK = 7;
+var WEEKDAY = 8;
+
+// FORMATTING
+
+addFormatToken('Y', 0, 0, function () {
+    var y = this.year();
+    return y <= 9999 ? '' + y : '+' + y;
+});
+
+addFormatToken(0, ['YY', 2], 0, function () {
+    return this.year() % 100;
+});
+
+addFormatToken(0, ['YYYY',   4],       0, 'year');
+addFormatToken(0, ['YYYYY',  5],       0, 'year');
+addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');
+
+// ALIASES
+
+addUnitAlias('year', 'y');
+
+// PRIORITIES
+
+addUnitPriority('year', 1);
+
+// PARSING
+
+addRegexToken('Y',      matchSigned);
+addRegexToken('YY',     match1to2, match2);
+addRegexToken('YYYY',   match1to4, match4);
+addRegexToken('YYYYY',  match1to6, match6);
+addRegexToken('YYYYYY', match1to6, match6);
+
+addParseToken(['YYYYY', 'YYYYYY'], YEAR);
+addParseToken('YYYY', function (input, array) {
+    array[YEAR] = input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input);
+});
+addParseToken('YY', function (input, array) {
+    array[YEAR] = hooks.parseTwoDigitYear(input);
+});
+addParseToken('Y', function (input, array) {
+    array[YEAR] = parseInt(input, 10);
+});
+
+// HELPERS
+
+function daysInYear(year) {
+    return isLeapYear(year) ? 366 : 365;
+}
+
+function isLeapYear(year) {
+    return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+}
+
+// HOOKS
+
+hooks.parseTwoDigitYear = function (input) {
+    return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+};
+
+// MOMENTS
+
+var getSetYear = makeGetSet('FullYear', true);
+
+function getIsLeapYear () {
+    return isLeapYear(this.year());
+}
+
+function makeGetSet (unit, keepTime) {
+    return function (value) {
+        if (value != null) {
+            set$1(this, unit, value);
+            hooks.updateOffset(this, keepTime);
+            return this;
+        } else {
+            return get(this, unit);
+        }
+    };
+}
+
+function get (mom, unit) {
+    return mom.isValid() ?
+        mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN;
+}
+
+function set$1 (mom, unit, value) {
+    if (mom.isValid() && !isNaN(value)) {
+        if (unit === 'FullYear' && isLeapYear(mom.year()) && mom.month() === 1 && mom.date() === 29) {
+            mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value, mom.month(), daysInMonth(value, mom.month()));
+        }
+        else {
+            mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+        }
+    }
+}
+
+// MOMENTS
+
+function stringGet (units) {
+    units = normalizeUnits(units);
+    if (isFunction(this[units])) {
+        return this[units]();
+    }
+    return this;
+}
+
+
+function stringSet (units, value) {
+    if (typeof units === 'object') {
+        units = normalizeObjectUnits(units);
+        var prioritized = getPrioritizedUnits(units);
+        for (var i = 0; i < prioritized.length; i++) {
+            this[prioritized[i].unit](units[prioritized[i].unit]);
+        }
+    } else {
+        units = normalizeUnits(units);
+        if (isFunction(this[units])) {
+            return this[units](value);
+        }
+    }
+    return this;
+}
+
+function mod(n, x) {
+    return ((n % x) + x) % x;
+}
+
+var indexOf;
+
+if (Array.prototype.indexOf) {
+    indexOf = Array.prototype.indexOf;
+} else {
+    indexOf = function (o) {
+        // I know
+        var i;
+        for (i = 0; i < this.length; ++i) {
+            if (this[i] === o) {
+                return i;
+            }
+        }
+        return -1;
+    };
+}
+
+function daysInMonth(year, month) {
+    if (isNaN(year) || isNaN(month)) {
+        return NaN;
+    }
+    var modMonth = mod(month, 12);
+    year += (month - modMonth) / 12;
+    return modMonth === 1 ? (isLeapYear(year) ? 29 : 28) : (31 - modMonth % 7 % 2);
+}
+
+// FORMATTING
+
+addFormatToken('M', ['MM', 2], 'Mo', function () {
+    return this.month() + 1;
+});
+
+addFormatToken('MMM', 0, 0, function (format) {
+    return this.localeData().monthsShort(this, format);
+});
+
+addFormatToken('MMMM', 0, 0, function (format) {
+    return this.localeData().months(this, format);
+});
+
+// ALIASES
+
+addUnitAlias('month', 'M');
+
+// PRIORITY
+
+addUnitPriority('month', 8);
+
+// PARSING
+
+addRegexToken('M',    match1to2);
+addRegexToken('MM',   match1to2, match2);
+addRegexToken('MMM',  function (isStrict, locale) {
+    return locale.monthsShortRegex(isStrict);
+});
+addRegexToken('MMMM', function (isStrict, locale) {
+    return locale.monthsRegex(isStrict);
+});
+
+addParseToken(['M', 'MM'], function (input, array) {
+    array[MONTH] = toInt(input) - 1;
+});
+
+addParseToken(['MMM', 'MMMM'], function (input, array, config, token) {
+    var month = config._locale.monthsParse(input, token, config._strict);
+    // if we didn't find a month name, mark the date as invalid.
+    if (month != null) {
+        array[MONTH] = month;
+    } else {
+        getParsingFlags(config).invalidMonth = input;
+    }
+});
+
+// LOCALES
+
+var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/;
+var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_');
+function localeMonths (m, format) {
+    if (!m) {
+        return isArray(this._months) ? this._months :
+            this._months['standalone'];
+    }
+    return isArray(this._months) ? this._months[m.month()] :
+        this._months[(this._months.isFormat || MONTHS_IN_FORMAT).test(format) ? 'format' : 'standalone'][m.month()];
+}
+
+var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_');
+function localeMonthsShort (m, format) {
+    if (!m) {
+        return isArray(this._monthsShort) ? this._monthsShort :
+            this._monthsShort['standalone'];
+    }
+    return isArray(this._monthsShort) ? this._monthsShort[m.month()] :
+        this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()];
+}
+
+function handleStrictParse(monthName, format, strict) {
+    var i, ii, mom, llc = monthName.toLocaleLowerCase();
+    if (!this._monthsParse) {
+        // this is not used
+        this._monthsParse = [];
+        this._longMonthsParse = [];
+        this._shortMonthsParse = [];
+        for (i = 0; i < 12; ++i) {
+            mom = createUTC([2000, i]);
+            this._shortMonthsParse[i] = this.monthsShort(mom, '').toLocaleLowerCase();
+            this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase();
+        }
+    }
+
+    if (strict) {
+        if (format === 'MMM') {
+            ii = indexOf.call(this._shortMonthsParse, llc);
+            return ii !== -1 ? ii : null;
+        } else {
+            ii = indexOf.call(this._longMonthsParse, llc);
+            return ii !== -1 ? ii : null;
+        }
+    } else {
+        if (format === 'MMM') {
+            ii = indexOf.call(this._shortMonthsParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._longMonthsParse, llc);
+            return ii !== -1 ? ii : null;
+        } else {
+            ii = indexOf.call(this._longMonthsParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._shortMonthsParse, llc);
+            return ii !== -1 ? ii : null;
+        }
+    }
+}
+
+function localeMonthsParse (monthName, format, strict) {
+    var i, mom, regex;
+
+    if (this._monthsParseExact) {
+        return handleStrictParse.call(this, monthName, format, strict);
+    }
+
+    if (!this._monthsParse) {
+        this._monthsParse = [];
+        this._longMonthsParse = [];
+        this._shortMonthsParse = [];
+    }
+
+    // TODO: add sorting
+    // Sorting makes sure if one month (or abbr) is a prefix of another
+    // see sorting in computeMonthsParse
+    for (i = 0; i < 12; i++) {
+        // make the regex if we don't have it already
+        mom = createUTC([2000, i]);
+        if (strict && !this._longMonthsParse[i]) {
+            this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i');
+            this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i');
+        }
+        if (!strict && !this._monthsParse[i]) {
+            regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+            this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+        }
+        // test the regex
+        if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) {
+            return i;
+        } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) {
+            return i;
+        } else if (!strict && this._monthsParse[i].test(monthName)) {
+            return i;
+        }
+    }
+}
+
+// MOMENTS
+
+function setMonth (mom, value) {
+    var dayOfMonth;
+
+    if (!mom.isValid()) {
+        // No op
+        return mom;
+    }
+
+    if (typeof value === 'string') {
+        if (/^\d+$/.test(value)) {
+            value = toInt(value);
+        } else {
+            value = mom.localeData().monthsParse(value);
+            // TODO: Another silent failure?
+            if (!isNumber(value)) {
+                return mom;
+            }
+        }
+    }
+
+    dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value));
+    mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+    return mom;
+}
+
+function getSetMonth (value) {
+    if (value != null) {
+        setMonth(this, value);
+        hooks.updateOffset(this, true);
+        return this;
+    } else {
+        return get(this, 'Month');
+    }
+}
+
+function getDaysInMonth () {
+    return daysInMonth(this.year(), this.month());
+}
+
+var defaultMonthsShortRegex = matchWord;
+function monthsShortRegex (isStrict) {
+    if (this._monthsParseExact) {
+        if (!hasOwnProp(this, '_monthsRegex')) {
+            computeMonthsParse.call(this);
+        }
+        if (isStrict) {
+            return this._monthsShortStrictRegex;
+        } else {
+            return this._monthsShortRegex;
+        }
+    } else {
+        if (!hasOwnProp(this, '_monthsShortRegex')) {
+            this._monthsShortRegex = defaultMonthsShortRegex;
+        }
+        return this._monthsShortStrictRegex && isStrict ?
+            this._monthsShortStrictRegex : this._monthsShortRegex;
+    }
+}
+
+var defaultMonthsRegex = matchWord;
+function monthsRegex (isStrict) {
+    if (this._monthsParseExact) {
+        if (!hasOwnProp(this, '_monthsRegex')) {
+            computeMonthsParse.call(this);
+        }
+        if (isStrict) {
+            return this._monthsStrictRegex;
+        } else {
+            return this._monthsRegex;
+        }
+    } else {
+        if (!hasOwnProp(this, '_monthsRegex')) {
+            this._monthsRegex = defaultMonthsRegex;
+        }
+        return this._monthsStrictRegex && isStrict ?
+            this._monthsStrictRegex : this._monthsRegex;
+    }
+}
+
+function computeMonthsParse () {
+    function cmpLenRev(a, b) {
+        return b.length - a.length;
+    }
+
+    var shortPieces = [], longPieces = [], mixedPieces = [],
+        i, mom;
+    for (i = 0; i < 12; i++) {
+        // make the regex if we don't have it already
+        mom = createUTC([2000, i]);
+        shortPieces.push(this.monthsShort(mom, ''));
+        longPieces.push(this.months(mom, ''));
+        mixedPieces.push(this.months(mom, ''));
+        mixedPieces.push(this.monthsShort(mom, ''));
+    }
+    // Sorting makes sure if one month (or abbr) is a prefix of another it
+    // will match the longer piece.
+    shortPieces.sort(cmpLenRev);
+    longPieces.sort(cmpLenRev);
+    mixedPieces.sort(cmpLenRev);
+    for (i = 0; i < 12; i++) {
+        shortPieces[i] = regexEscape(shortPieces[i]);
+        longPieces[i] = regexEscape(longPieces[i]);
+    }
+    for (i = 0; i < 24; i++) {
+        mixedPieces[i] = regexEscape(mixedPieces[i]);
+    }
+
+    this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+    this._monthsShortRegex = this._monthsRegex;
+    this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i');
+    this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i');
+}
+
+function createDate (y, m, d, h, M, s, ms) {
+    // can't just apply() to create a date:
+    // https://stackoverflow.com/q/181348
+    var date = new Date(y, m, d, h, M, s, ms);
+
+    // the date constructor remaps years 0-99 to 1900-1999
+    if (y < 100 && y >= 0 && isFinite(date.getFullYear())) {
+        date.setFullYear(y);
+    }
+    return date;
+}
+
+function createUTCDate (y) {
+    var date = new Date(Date.UTC.apply(null, arguments));
+
+    // the Date.UTC function remaps years 0-99 to 1900-1999
+    if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) {
+        date.setUTCFullYear(y);
+    }
+    return date;
+}
+
+// start-of-first-week - start-of-year
+function firstWeekOffset(year, dow, doy) {
+    var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
+        fwd = 7 + dow - doy,
+        // first-week day local weekday -- which local weekday is fwd
+        fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7;
+
+    return -fwdlw + fwd - 1;
+}
+
+// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+function dayOfYearFromWeeks(year, week, weekday, dow, doy) {
+    var localWeekday = (7 + weekday - dow) % 7,
+        weekOffset = firstWeekOffset(year, dow, doy),
+        dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset,
+        resYear, resDayOfYear;
+
+    if (dayOfYear <= 0) {
+        resYear = year - 1;
+        resDayOfYear = daysInYear(resYear) + dayOfYear;
+    } else if (dayOfYear > daysInYear(year)) {
+        resYear = year + 1;
+        resDayOfYear = dayOfYear - daysInYear(year);
+    } else {
+        resYear = year;
+        resDayOfYear = dayOfYear;
+    }
+
+    return {
+        year: resYear,
+        dayOfYear: resDayOfYear
+    };
+}
+
+function weekOfYear(mom, dow, doy) {
+    var weekOffset = firstWeekOffset(mom.year(), dow, doy),
+        week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1,
+        resWeek, resYear;
+
+    if (week < 1) {
+        resYear = mom.year() - 1;
+        resWeek = week + weeksInYear(resYear, dow, doy);
+    } else if (week > weeksInYear(mom.year(), dow, doy)) {
+        resWeek = week - weeksInYear(mom.year(), dow, doy);
+        resYear = mom.year() + 1;
+    } else {
+        resYear = mom.year();
+        resWeek = week;
+    }
+
+    return {
+        week: resWeek,
+        year: resYear
+    };
+}
+
+function weeksInYear(year, dow, doy) {
+    var weekOffset = firstWeekOffset(year, dow, doy),
+        weekOffsetNext = firstWeekOffset(year + 1, dow, doy);
+    return (daysInYear(year) - weekOffset + weekOffsetNext) / 7;
+}
+
+// FORMATTING
+
+addFormatToken('w', ['ww', 2], 'wo', 'week');
+addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek');
+
+// ALIASES
+
+addUnitAlias('week', 'w');
+addUnitAlias('isoWeek', 'W');
+
+// PRIORITIES
+
+addUnitPriority('week', 5);
+addUnitPriority('isoWeek', 5);
+
+// PARSING
+
+addRegexToken('w',  match1to2);
+addRegexToken('ww', match1to2, match2);
+addRegexToken('W',  match1to2);
+addRegexToken('WW', match1to2, match2);
+
+addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) {
+    week[token.substr(0, 1)] = toInt(input);
+});
+
+// HELPERS
+
+// LOCALES
+
+function localeWeek (mom) {
+    return weekOfYear(mom, this._week.dow, this._week.doy).week;
+}
+
+var defaultLocaleWeek = {
+    dow : 0, // Sunday is the first day of the week.
+    doy : 6  // The week that contains Jan 1st is the first week of the year.
+};
+
+function localeFirstDayOfWeek () {
+    return this._week.dow;
+}
+
+function localeFirstDayOfYear () {
+    return this._week.doy;
+}
+
+// MOMENTS
+
+function getSetWeek (input) {
+    var week = this.localeData().week(this);
+    return input == null ? week : this.add((input - week) * 7, 'd');
+}
+
+function getSetISOWeek (input) {
+    var week = weekOfYear(this, 1, 4).week;
+    return input == null ? week : this.add((input - week) * 7, 'd');
+}
+
+// FORMATTING
+
+addFormatToken('d', 0, 'do', 'day');
+
+addFormatToken('dd', 0, 0, function (format) {
+    return this.localeData().weekdaysMin(this, format);
+});
+
+addFormatToken('ddd', 0, 0, function (format) {
+    return this.localeData().weekdaysShort(this, format);
+});
+
+addFormatToken('dddd', 0, 0, function (format) {
+    return this.localeData().weekdays(this, format);
+});
+
+addFormatToken('e', 0, 0, 'weekday');
+addFormatToken('E', 0, 0, 'isoWeekday');
+
+// ALIASES
+
+addUnitAlias('day', 'd');
+addUnitAlias('weekday', 'e');
+addUnitAlias('isoWeekday', 'E');
+
+// PRIORITY
+addUnitPriority('day', 11);
+addUnitPriority('weekday', 11);
+addUnitPriority('isoWeekday', 11);
+
+// PARSING
+
+addRegexToken('d',    match1to2);
+addRegexToken('e',    match1to2);
+addRegexToken('E',    match1to2);
+addRegexToken('dd',   function (isStrict, locale) {
+    return locale.weekdaysMinRegex(isStrict);
+});
+addRegexToken('ddd',   function (isStrict, locale) {
+    return locale.weekdaysShortRegex(isStrict);
+});
+addRegexToken('dddd',   function (isStrict, locale) {
+    return locale.weekdaysRegex(isStrict);
+});
+
+addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) {
+    var weekday = config._locale.weekdaysParse(input, token, config._strict);
+    // if we didn't get a weekday name, mark the date as invalid
+    if (weekday != null) {
+        week.d = weekday;
+    } else {
+        getParsingFlags(config).invalidWeekday = input;
+    }
+});
+
+addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) {
+    week[token] = toInt(input);
+});
+
+// HELPERS
+
+function parseWeekday(input, locale) {
+    if (typeof input !== 'string') {
+        return input;
+    }
+
+    if (!isNaN(input)) {
+        return parseInt(input, 10);
+    }
+
+    input = locale.weekdaysParse(input);
+    if (typeof input === 'number') {
+        return input;
+    }
+
+    return null;
+}
+
+function parseIsoWeekday(input, locale) {
+    if (typeof input === 'string') {
+        return locale.weekdaysParse(input) % 7 || 7;
+    }
+    return isNaN(input) ? null : input;
+}
+
+// LOCALES
+
+var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_');
+function localeWeekdays (m, format) {
+    if (!m) {
+        return isArray(this._weekdays) ? this._weekdays :
+            this._weekdays['standalone'];
+    }
+    return isArray(this._weekdays) ? this._weekdays[m.day()] :
+        this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()];
+}
+
+var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_');
+function localeWeekdaysShort (m) {
+    return (m) ? this._weekdaysShort[m.day()] : this._weekdaysShort;
+}
+
+var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_');
+function localeWeekdaysMin (m) {
+    return (m) ? this._weekdaysMin[m.day()] : this._weekdaysMin;
+}
+
+function handleStrictParse$1(weekdayName, format, strict) {
+    var i, ii, mom, llc = weekdayName.toLocaleLowerCase();
+    if (!this._weekdaysParse) {
+        this._weekdaysParse = [];
+        this._shortWeekdaysParse = [];
+        this._minWeekdaysParse = [];
+
+        for (i = 0; i < 7; ++i) {
+            mom = createUTC([2000, 1]).day(i);
+            this._minWeekdaysParse[i] = this.weekdaysMin(mom, '').toLocaleLowerCase();
+            this._shortWeekdaysParse[i] = this.weekdaysShort(mom, '').toLocaleLowerCase();
+            this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase();
+        }
+    }
+
+    if (strict) {
+        if (format === 'dddd') {
+            ii = indexOf.call(this._weekdaysParse, llc);
+            return ii !== -1 ? ii : null;
+        } else if (format === 'ddd') {
+            ii = indexOf.call(this._shortWeekdaysParse, llc);
+            return ii !== -1 ? ii : null;
+        } else {
+            ii = indexOf.call(this._minWeekdaysParse, llc);
+            return ii !== -1 ? ii : null;
+        }
+    } else {
+        if (format === 'dddd') {
+            ii = indexOf.call(this._weekdaysParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._shortWeekdaysParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._minWeekdaysParse, llc);
+            return ii !== -1 ? ii : null;
+        } else if (format === 'ddd') {
+            ii = indexOf.call(this._shortWeekdaysParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._weekdaysParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._minWeekdaysParse, llc);
+            return ii !== -1 ? ii : null;
+        } else {
+            ii = indexOf.call(this._minWeekdaysParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._weekdaysParse, llc);
+            if (ii !== -1) {
+                return ii;
+            }
+            ii = indexOf.call(this._shortWeekdaysParse, llc);
+            return ii !== -1 ? ii : null;
+        }
+    }
+}
+
+function localeWeekdaysParse (weekdayName, format, strict) {
+    var i, mom, regex;
+
+    if (this._weekdaysParseExact) {
+        return handleStrictParse$1.call(this, weekdayName, format, strict);
+    }
+
+    if (!this._weekdaysParse) {
+        this._weekdaysParse = [];
+        this._minWeekdaysParse = [];
+        this._shortWeekdaysParse = [];
+        this._fullWeekdaysParse = [];
+    }
+
+    for (i = 0; i < 7; i++) {
+        // make the regex if we don't have it already
+
+        mom = createUTC([2000, 1]).day(i);
+        if (strict && !this._fullWeekdaysParse[i]) {
+            this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i');
+            this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i');
+            this._minWeekdaysParse[i] = new RegExp('^' + this.weekdaysMin(mom, '').replace('.', '\.?') + '$', 'i');
+        }
+        if (!this._weekdaysParse[i]) {
+            regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+            this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+        }
+        // test the regex
+        if (strict && format === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName)) {
+            return i;
+        } else if (strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName)) {
+            return i;
+        } else if (strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName)) {
+            return i;
+        } else if (!strict && this._weekdaysParse[i].test(weekdayName)) {
+            return i;
+        }
+    }
+}
+
+// MOMENTS
+
+function getSetDayOfWeek (input) {
+    if (!this.isValid()) {
+        return input != null ? this : NaN;
+    }
+    var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+    if (input != null) {
+        input = parseWeekday(input, this.localeData());
+        return this.add(input - day, 'd');
+    } else {
+        return day;
+    }
+}
+
+function getSetLocaleDayOfWeek (input) {
+    if (!this.isValid()) {
+        return input != null ? this : NaN;
+    }
+    var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+    return input == null ? weekday : this.add(input - weekday, 'd');
+}
+
+function getSetISODayOfWeek (input) {
+    if (!this.isValid()) {
+        return input != null ? this : NaN;
+    }
+
+    // behaves the same as moment#day except
+    // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+    // as a setter, sunday should belong to the previous week.
+
+    if (input != null) {
+        var weekday = parseIsoWeekday(input, this.localeData());
+        return this.day(this.day() % 7 ? weekday : weekday - 7);
+    } else {
+        return this.day() || 7;
+    }
+}
+
+var defaultWeekdaysRegex = matchWord;
+function weekdaysRegex (isStrict) {
+    if (this._weekdaysParseExact) {
+        if (!hasOwnProp(this, '_weekdaysRegex')) {
+            computeWeekdaysParse.call(this);
+        }
+        if (isStrict) {
+            return this._weekdaysStrictRegex;
+        } else {
+            return this._weekdaysRegex;
+        }
+    } else {
+        if (!hasOwnProp(this, '_weekdaysRegex')) {
+            this._weekdaysRegex = defaultWeekdaysRegex;
+        }
+        return this._weekdaysStrictRegex && isStrict ?
+            this._weekdaysStrictRegex : this._weekdaysRegex;
+    }
+}
+
+var defaultWeekdaysShortRegex = matchWord;
+function weekdaysShortRegex (isStrict) {
+    if (this._weekdaysParseExact) {
+        if (!hasOwnProp(this, '_weekdaysRegex')) {
+            computeWeekdaysParse.call(this);
+        }
+        if (isStrict) {
+            return this._weekdaysShortStrictRegex;
+        } else {
+            return this._weekdaysShortRegex;
+        }
+    } else {
+        if (!hasOwnProp(this, '_weekdaysShortRegex')) {
+            this._weekdaysShortRegex = defaultWeekdaysShortRegex;
+        }
+        return this._weekdaysShortStrictRegex && isStrict ?
+            this._weekdaysShortStrictRegex : this._weekdaysShortRegex;
+    }
+}
+
+var defaultWeekdaysMinRegex = matchWord;
+function weekdaysMinRegex (isStrict) {
+    if (this._weekdaysParseExact) {
+        if (!hasOwnProp(this, '_weekdaysRegex')) {
+            computeWeekdaysParse.call(this);
+        }
+        if (isStrict) {
+            return this._weekdaysMinStrictRegex;
+        } else {
+            return this._weekdaysMinRegex;
+        }
+    } else {
+        if (!hasOwnProp(this, '_weekdaysMinRegex')) {
+            this._weekdaysMinRegex = defaultWeekdaysMinRegex;
+        }
+        return this._weekdaysMinStrictRegex && isStrict ?
+            this._weekdaysMinStrictRegex : this._weekdaysMinRegex;
+    }
+}
+
+
+function computeWeekdaysParse () {
+    function cmpLenRev(a, b) {
+        return b.length - a.length;
+    }
+
+    var minPieces = [], shortPieces = [], longPieces = [], mixedPieces = [],
+        i, mom, minp, shortp, longp;
+    for (i = 0; i < 7; i++) {
+        // make the regex if we don't have it already
+        mom = createUTC([2000, 1]).day(i);
+        minp = this.weekdaysMin(mom, '');
+        shortp = this.weekdaysShort(mom, '');
+        longp = this.weekdays(mom, '');
+        minPieces.push(minp);
+        shortPieces.push(shortp);
+        longPieces.push(longp);
+        mixedPieces.push(minp);
+        mixedPieces.push(shortp);
+        mixedPieces.push(longp);
+    }
+    // Sorting makes sure if one weekday (or abbr) is a prefix of another it
+    // will match the longer piece.
+    minPieces.sort(cmpLenRev);
+    shortPieces.sort(cmpLenRev);
+    longPieces.sort(cmpLenRev);
+    mixedPieces.sort(cmpLenRev);
+    for (i = 0; i < 7; i++) {
+        shortPieces[i] = regexEscape(shortPieces[i]);
+        longPieces[i] = regexEscape(longPieces[i]);
+        mixedPieces[i] = regexEscape(mixedPieces[i]);
+    }
+
+    this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+    this._weekdaysShortRegex = this._weekdaysRegex;
+    this._weekdaysMinRegex = this._weekdaysRegex;
+
+    this._weekdaysStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i');
+    this._weekdaysShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i');
+    this._weekdaysMinStrictRegex = new RegExp('^(' + minPieces.join('|') + ')', 'i');
+}
+
+// FORMATTING
+
+function hFormat() {
+    return this.hours() % 12 || 12;
+}
+
+function kFormat() {
+    return this.hours() || 24;
+}
+
+addFormatToken('H', ['HH', 2], 0, 'hour');
+addFormatToken('h', ['hh', 2], 0, hFormat);
+addFormatToken('k', ['kk', 2], 0, kFormat);
+
+addFormatToken('hmm', 0, 0, function () {
+    return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2);
+});
+
+addFormatToken('hmmss', 0, 0, function () {
+    return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) +
+        zeroFill(this.seconds(), 2);
+});
+
+addFormatToken('Hmm', 0, 0, function () {
+    return '' + this.hours() + zeroFill(this.minutes(), 2);
+});
+
+addFormatToken('Hmmss', 0, 0, function () {
+    return '' + this.hours() + zeroFill(this.minutes(), 2) +
+        zeroFill(this.seconds(), 2);
+});
+
+function meridiem (token, lowercase) {
+    addFormatToken(token, 0, 0, function () {
+        return this.localeData().meridiem(this.hours(), this.minutes(), lowercase);
+    });
+}
+
+meridiem('a', true);
+meridiem('A', false);
+
+// ALIASES
+
+addUnitAlias('hour', 'h');
+
+// PRIORITY
+addUnitPriority('hour', 13);
+
+// PARSING
+
+function matchMeridiem (isStrict, locale) {
+    return locale._meridiemParse;
+}
+
+addRegexToken('a',  matchMeridiem);
+addRegexToken('A',  matchMeridiem);
+addRegexToken('H',  match1to2);
+addRegexToken('h',  match1to2);
+addRegexToken('k',  match1to2);
+addRegexToken('HH', match1to2, match2);
+addRegexToken('hh', match1to2, match2);
+addRegexToken('kk', match1to2, match2);
+
+addRegexToken('hmm', match3to4);
+addRegexToken('hmmss', match5to6);
+addRegexToken('Hmm', match3to4);
+addRegexToken('Hmmss', match5to6);
+
+addParseToken(['H', 'HH'], HOUR);
+addParseToken(['k', 'kk'], function (input, array, config) {
+    var kInput = toInt(input);
+    array[HOUR] = kInput === 24 ? 0 : kInput;
+});
+addParseToken(['a', 'A'], function (input, array, config) {
+    config._isPm = config._locale.isPM(input);
+    config._meridiem = input;
+});
+addParseToken(['h', 'hh'], function (input, array, config) {
+    array[HOUR] = toInt(input);
+    getParsingFlags(config).bigHour = true;
+});
+addParseToken('hmm', function (input, array, config) {
+    var pos = input.length - 2;
+    array[HOUR] = toInt(input.substr(0, pos));
+    array[MINUTE] = toInt(input.substr(pos));
+    getParsingFlags(config).bigHour = true;
+});
+addParseToken('hmmss', function (input, array, config) {
+    var pos1 = input.length - 4;
+    var pos2 = input.length - 2;
+    array[HOUR] = toInt(input.substr(0, pos1));
+    array[MINUTE] = toInt(input.substr(pos1, 2));
+    array[SECOND] = toInt(input.substr(pos2));
+    getParsingFlags(config).bigHour = true;
+});
+addParseToken('Hmm', function (input, array, config) {
+    var pos = input.length - 2;
+    array[HOUR] = toInt(input.substr(0, pos));
+    array[MINUTE] = toInt(input.substr(pos));
+});
+addParseToken('Hmmss', function (input, array, config) {
+    var pos1 = input.length - 4;
+    var pos2 = input.length - 2;
+    array[HOUR] = toInt(input.substr(0, pos1));
+    array[MINUTE] = toInt(input.substr(pos1, 2));
+    array[SECOND] = toInt(input.substr(pos2));
+});
+
+// LOCALES
+
+function localeIsPM (input) {
+    // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+    // Using charAt should be more compatible.
+    return ((input + '').toLowerCase().charAt(0) === 'p');
+}
+
+var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i;
+function localeMeridiem (hours, minutes, isLower) {
+    if (hours > 11) {
+        return isLower ? 'pm' : 'PM';
+    } else {
+        return isLower ? 'am' : 'AM';
+    }
+}
+
+
+// MOMENTS
+
+// Setting the hour should keep the time, because the user explicitly
+// specified which hour he wants. So trying to maintain the same hour (in
+// a new timezone) makes sense. Adding/subtracting hours does not follow
+// this rule.
+var getSetHour = makeGetSet('Hours', true);
+
+// months
+// week
+// weekdays
+// meridiem
+var baseConfig = {
+    calendar: defaultCalendar,
+    longDateFormat: defaultLongDateFormat,
+    invalidDate: defaultInvalidDate,
+    ordinal: defaultOrdinal,
+    dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse,
+    relativeTime: defaultRelativeTime,
+
+    months: defaultLocaleMonths,
+    monthsShort: defaultLocaleMonthsShort,
+
+    week: defaultLocaleWeek,
+
+    weekdays: defaultLocaleWeekdays,
+    weekdaysMin: defaultLocaleWeekdaysMin,
+    weekdaysShort: defaultLocaleWeekdaysShort,
+
+    meridiemParse: defaultLocaleMeridiemParse
+};
+
+// internal storage for locale config files
+var locales = {};
+var localeFamilies = {};
+var globalLocale;
+
+function normalizeLocale(key) {
+    return key ? key.toLowerCase().replace('_', '-') : key;
+}
+
+// pick the locale from the array
+// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+function chooseLocale(names) {
+    var i = 0, j, next, locale, split;
+
+    while (i < names.length) {
+        split = normalizeLocale(names[i]).split('-');
+        j = split.length;
+        next = normalizeLocale(names[i + 1]);
+        next = next ? next.split('-') : null;
+        while (j > 0) {
+            locale = loadLocale(split.slice(0, j).join('-'));
+            if (locale) {
+                return locale;
+            }
+            if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+                //the next array item is better than a shallower substring of this one
+                break;
+            }
+            j--;
+        }
+        i++;
+    }
+    return null;
+}
+
+function loadLocale(name) {
+    var oldLocale = null;
+    // TODO: Find a better way to register and load all the locales in Node
+    if (!locales[name] && (typeof module !== 'undefined') &&
+            module && module.exports) {
+        try {
+            oldLocale = globalLocale._abbr;
+            var aliasedRequire = require;
+            aliasedRequire('./locale/' + name);
+            getSetGlobalLocale(oldLocale);
+        } catch (e) {}
+    }
+    return locales[name];
+}
+
+// This function will load locale and then set the global locale.  If
+// no arguments are passed in, it will simply return the current global
+// locale key.
+function getSetGlobalLocale (key, values) {
+    var data;
+    if (key) {
+        if (isUndefined(values)) {
+            data = getLocale(key);
+        }
+        else {
+            data = defineLocale(key, values);
+        }
+
+        if (data) {
+            // moment.duration._locale = moment._locale = data;
+            globalLocale = data;
+        }
+    }
+
+    return globalLocale._abbr;
+}
+
+function defineLocale (name, config) {
+    if (config !== null) {
+        var parentConfig = baseConfig;
+        config.abbr = name;
+        if (locales[name] != null) {
+            deprecateSimple('defineLocaleOverride',
+                    'use moment.updateLocale(localeName, config) to change ' +
+                    'an existing locale. moment.defineLocale(localeName, ' +
+                    'config) should only be used for creating a new locale ' +
+                    'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.');
+            parentConfig = locales[name]._config;
+        } else if (config.parentLocale != null) {
+            if (locales[config.parentLocale] != null) {
+                parentConfig = locales[config.parentLocale]._config;
+            } else {
+                if (!localeFamilies[config.parentLocale]) {
+                    localeFamilies[config.parentLocale] = [];
+                }
+                localeFamilies[config.parentLocale].push({
+                    name: name,
+                    config: config
+                });
+                return null;
+            }
+        }
+        locales[name] = new Locale(mergeConfigs(parentConfig, config));
+
+        if (localeFamilies[name]) {
+            localeFamilies[name].forEach(function (x) {
+                defineLocale(x.name, x.config);
+            });
+        }
+
+        // backwards compat for now: also set the locale
+        // make sure we set the locale AFTER all child locales have been
+        // created, so we won't end up with the child locale set.
+        getSetGlobalLocale(name);
+
+
+        return locales[name];
+    } else {
+        // useful for testing
+        delete locales[name];
+        return null;
+    }
+}
+
+function updateLocale(name, config) {
+    if (config != null) {
+        var locale, tmpLocale, parentConfig = baseConfig;
+        // MERGE
+        tmpLocale = loadLocale(name);
+        if (tmpLocale != null) {
+            parentConfig = tmpLocale._config;
+        }
+        config = mergeConfigs(parentConfig, config);
+        locale = new Locale(config);
+        locale.parentLocale = locales[name];
+        locales[name] = locale;
+
+        // backwards compat for now: also set the locale
+        getSetGlobalLocale(name);
+    } else {
+        // pass null for config to unupdate, useful for tests
+        if (locales[name] != null) {
+            if (locales[name].parentLocale != null) {
+                locales[name] = locales[name].parentLocale;
+            } else if (locales[name] != null) {
+                delete locales[name];
+            }
+        }
+    }
+    return locales[name];
+}
+
+// returns locale data
+function getLocale (key) {
+    var locale;
+
+    if (key && key._locale && key._locale._abbr) {
+        key = key._locale._abbr;
+    }
+
+    if (!key) {
+        return globalLocale;
+    }
+
+    if (!isArray(key)) {
+        //short-circuit everything else
+        locale = loadLocale(key);
+        if (locale) {
+            return locale;
+        }
+        key = [key];
+    }
+
+    return chooseLocale(key);
+}
+
+function listLocales() {
+    return keys(locales);
+}
+
+function checkOverflow (m) {
+    var overflow;
+    var a = m._a;
+
+    if (a && getParsingFlags(m).overflow === -2) {
+        overflow =
+            a[MONTH]       < 0 || a[MONTH]       > 11  ? MONTH :
+            a[DATE]        < 1 || a[DATE]        > daysInMonth(a[YEAR], a[MONTH]) ? DATE :
+            a[HOUR]        < 0 || a[HOUR]        > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR :
+            a[MINUTE]      < 0 || a[MINUTE]      > 59  ? MINUTE :
+            a[SECOND]      < 0 || a[SECOND]      > 59  ? SECOND :
+            a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND :
+            -1;
+
+        if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+            overflow = DATE;
+        }
+        if (getParsingFlags(m)._overflowWeeks && overflow === -1) {
+            overflow = WEEK;
+        }
+        if (getParsingFlags(m)._overflowWeekday && overflow === -1) {
+            overflow = WEEKDAY;
+        }
+
+        getParsingFlags(m).overflow = overflow;
+    }
+
+    return m;
+}
+
+// Pick the first defined of two or three arguments.
+function defaults(a, b, c) {
+    if (a != null) {
+        return a;
+    }
+    if (b != null) {
+        return b;
+    }
+    return c;
+}
+
+function currentDateArray(config) {
+    // hooks is actually the exported moment object
+    var nowValue = new Date(hooks.now());
+    if (config._useUTC) {
+        return [nowValue.getUTCFullYear(), nowValue.getUTCMonth(), nowValue.getUTCDate()];
+    }
+    return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()];
+}
+
+// convert an array to a date.
+// the array should mirror the parameters below
+// note: all values past the year are optional and will default to the lowest possible value.
+// [year, month, day , hour, minute, second, millisecond]
+function configFromArray (config) {
+    var i, date, input = [], currentDate, expectedWeekday, yearToUse;
+
+    if (config._d) {
+        return;
+    }
+
+    currentDate = currentDateArray(config);
+
+    //compute day of the year from weeks and weekdays
+    if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+        dayOfYearFromWeekInfo(config);
+    }
+
+    //if the day of the year is set, figure out what it is
+    if (config._dayOfYear != null) {
+        yearToUse = defaults(config._a[YEAR], currentDate[YEAR]);
+
+        if (config._dayOfYear > daysInYear(yearToUse) || config._dayOfYear === 0) {
+            getParsingFlags(config)._overflowDayOfYear = true;
+        }
+
+        date = createUTCDate(yearToUse, 0, config._dayOfYear);
+        config._a[MONTH] = date.getUTCMonth();
+        config._a[DATE] = date.getUTCDate();
+    }
+
+    // Default to current date.
+    // * if no year, month, day of month are given, default to today
+    // * if day of month is given, default month and year
+    // * if month is given, default only year
+    // * if year is given, don't default anything
+    for (i = 0; i < 3 && config._a[i] == null; ++i) {
+        config._a[i] = input[i] = currentDate[i];
+    }
+
+    // Zero out whatever was not defaulted, including time
+    for (; i < 7; i++) {
+        config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+    }
+
+    // Check for 24:00:00.000
+    if (config._a[HOUR] === 24 &&
+            config._a[MINUTE] === 0 &&
+            config._a[SECOND] === 0 &&
+            config._a[MILLISECOND] === 0) {
+        config._nextDay = true;
+        config._a[HOUR] = 0;
+    }
+
+    config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
+    expectedWeekday = config._useUTC ? config._d.getUTCDay() : config._d.getDay();
+
+    // Apply timezone offset from input. The actual utcOffset can be changed
+    // with parseZone.
+    if (config._tzm != null) {
+        config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+    }
+
+    if (config._nextDay) {
+        config._a[HOUR] = 24;
+    }
+
+    // check for mismatching day of week
+    if (config._w && typeof config._w.d !== 'undefined' && config._w.d !== expectedWeekday) {
+        getParsingFlags(config).weekdayMismatch = true;
+    }
+}
+
+function dayOfYearFromWeekInfo(config) {
+    var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow;
+
+    w = config._w;
+    if (w.GG != null || w.W != null || w.E != null) {
+        dow = 1;
+        doy = 4;
+
+        // TODO: We need to take the current isoWeekYear, but that depends on
+        // how we interpret now (local, utc, fixed offset). So create
+        // a now version of current config (take local/utc/offset flags, and
+        // create now).
+        weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(createLocal(), 1, 4).year);
+        week = defaults(w.W, 1);
+        weekday = defaults(w.E, 1);
+        if (weekday < 1 || weekday > 7) {
+            weekdayOverflow = true;
+        }
+    } else {
+        dow = config._locale._week.dow;
+        doy = config._locale._week.doy;
+
+        var curWeek = weekOfYear(createLocal(), dow, doy);
+
+        weekYear = defaults(w.gg, config._a[YEAR], curWeek.year);
+
+        // Default to current week.
+        week = defaults(w.w, curWeek.week);
+
+        if (w.d != null) {
+            // weekday -- low day numbers are considered next week
+            weekday = w.d;
+            if (weekday < 0 || weekday > 6) {
+                weekdayOverflow = true;
+            }
+        } else if (w.e != null) {
+            // local weekday -- counting starts from begining of week
+            weekday = w.e + dow;
+            if (w.e < 0 || w.e > 6) {
+                weekdayOverflow = true;
+            }
+        } else {
+            // default to begining of week
+            weekday = dow;
+        }
+    }
+    if (week < 1 || week > weeksInYear(weekYear, dow, doy)) {
+        getParsingFlags(config)._overflowWeeks = true;
+    } else if (weekdayOverflow != null) {
+        getParsingFlags(config)._overflowWeekday = true;
+    } else {
+        temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy);
+        config._a[YEAR] = temp.year;
+        config._dayOfYear = temp.dayOfYear;
+    }
+}
+
+// iso 8601 regex
+// 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
+var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
+var basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
+
+var tzRegex = /Z|[+-]\d\d(?::?\d\d)?/;
+
+var isoDates = [
+    ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/],
+    ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/],
+    ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/],
+    ['GGGG-[W]WW', /\d{4}-W\d\d/, false],
+    ['YYYY-DDD', /\d{4}-\d{3}/],
+    ['YYYY-MM', /\d{4}-\d\d/, false],
+    ['YYYYYYMMDD', /[+-]\d{10}/],
+    ['YYYYMMDD', /\d{8}/],
+    // YYYYMM is NOT allowed by the standard
+    ['GGGG[W]WWE', /\d{4}W\d{3}/],
+    ['GGGG[W]WW', /\d{4}W\d{2}/, false],
+    ['YYYYDDD', /\d{7}/]
+];
+
+// iso time formats and regexes
+var isoTimes = [
+    ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/],
+    ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/],
+    ['HH:mm:ss', /\d\d:\d\d:\d\d/],
+    ['HH:mm', /\d\d:\d\d/],
+    ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/],
+    ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/],
+    ['HHmmss', /\d\d\d\d\d\d/],
+    ['HHmm', /\d\d\d\d/],
+    ['HH', /\d\d/]
+];
+
+var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i;
+
+// date from iso format
+function configFromISO(config) {
+    var i, l,
+        string = config._i,
+        match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string),
+        allowTime, dateFormat, timeFormat, tzFormat;
+
+    if (match) {
+        getParsingFlags(config).iso = true;
+
+        for (i = 0, l = isoDates.length; i < l; i++) {
+            if (isoDates[i][1].exec(match[1])) {
+                dateFormat = isoDates[i][0];
+                allowTime = isoDates[i][2] !== false;
+                break;
+            }
+        }
+        if (dateFormat == null) {
+            config._isValid = false;
+            return;
+        }
+        if (match[3]) {
+            for (i = 0, l = isoTimes.length; i < l; i++) {
+                if (isoTimes[i][1].exec(match[3])) {
+                    // match[2] should be 'T' or space
+                    timeFormat = (match[2] || ' ') + isoTimes[i][0];
+                    break;
+                }
+            }
+            if (timeFormat == null) {
+                config._isValid = false;
+                return;
+            }
+        }
+        if (!allowTime && timeFormat != null) {
+            config._isValid = false;
+            return;
+        }
+        if (match[4]) {
+            if (tzRegex.exec(match[4])) {
+                tzFormat = 'Z';
+            } else {
+                config._isValid = false;
+                return;
+            }
+        }
+        config._f = dateFormat + (timeFormat || '') + (tzFormat || '');
+        configFromStringAndFormat(config);
+    } else {
+        config._isValid = false;
+    }
+}
+
+// RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3
+var rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;
+
+function extractFromRFC2822Strings(yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) {
+    var result = [
+        untruncateYear(yearStr),
+        defaultLocaleMonthsShort.indexOf(monthStr),
+        parseInt(dayStr, 10),
+        parseInt(hourStr, 10),
+        parseInt(minuteStr, 10)
+    ];
+
+    if (secondStr) {
+        result.push(parseInt(secondStr, 10));
+    }
+
+    return result;
+}
+
+function untruncateYear(yearStr) {
+    var year = parseInt(yearStr, 10);
+    if (year <= 49) {
+        return 2000 + year;
+    } else if (year <= 999) {
+        return 1900 + year;
+    }
+    return year;
+}
+
+function preprocessRFC2822(s) {
+    // Remove comments and folding whitespace and replace multiple-spaces with a single space
+    return s.replace(/\([^)]*\)|[\n\t]/g, ' ').replace(/(\s\s+)/g, ' ').trim();
+}
+
+function checkWeekday(weekdayStr, parsedInput, config) {
+    if (weekdayStr) {
+        // TODO: Replace the vanilla JS Date object with an indepentent day-of-week check.
+        var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr),
+            weekdayActual = new Date(parsedInput[0], parsedInput[1], parsedInput[2]).getDay();
+        if (weekdayProvided !== weekdayActual) {
+            getParsingFlags(config).weekdayMismatch = true;
+            config._isValid = false;
+            return false;
+        }
+    }
+    return true;
+}
+
+var obsOffsets = {
+    UT: 0,
+    GMT: 0,
+    EDT: -4 * 60,
+    EST: -5 * 60,
+    CDT: -5 * 60,
+    CST: -6 * 60,
+    MDT: -6 * 60,
+    MST: -7 * 60,
+    PDT: -7 * 60,
+    PST: -8 * 60
+};
+
+function calculateOffset(obsOffset, militaryOffset, numOffset) {
+    if (obsOffset) {
+        return obsOffsets[obsOffset];
+    } else if (militaryOffset) {
+        // the only allowed military tz is Z
+        return 0;
+    } else {
+        var hm = parseInt(numOffset, 10);
+        var m = hm % 100, h = (hm - m) / 100;
+        return h * 60 + m;
+    }
+}
+
+// date and time from ref 2822 format
+function configFromRFC2822(config) {
+    var match = rfc2822.exec(preprocessRFC2822(config._i));
+    if (match) {
+        var parsedArray = extractFromRFC2822Strings(match[4], match[3], match[2], match[5], match[6], match[7]);
+        if (!checkWeekday(match[1], parsedArray, config)) {
+            return;
+        }
+
+        config._a = parsedArray;
+        config._tzm = calculateOffset(match[8], match[9], match[10]);
+
+        config._d = createUTCDate.apply(null, config._a);
+        config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+
+        getParsingFlags(config).rfc2822 = true;
+    } else {
+        config._isValid = false;
+    }
+}
+
+// date from iso format or fallback
+function configFromString(config) {
+    var matched = aspNetJsonRegex.exec(config._i);
+
+    if (matched !== null) {
+        config._d = new Date(+matched[1]);
+        return;
+    }
+
+    configFromISO(config);
+    if (config._isValid === false) {
+        delete config._isValid;
+    } else {
+        return;
+    }
+
+    configFromRFC2822(config);
+    if (config._isValid === false) {
+        delete config._isValid;
+    } else {
+        return;
+    }
+
+    // Final attempt, use Input Fallback
+    hooks.createFromInputFallback(config);
+}
+
+hooks.createFromInputFallback = deprecate(
+    'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' +
+    'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' +
+    'discouraged and will be removed in an upcoming major release. Please refer to ' +
+    'http://momentjs.com/guides/#/warnings/js-date/ for more info.',
+    function (config) {
+        config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+    }
+);
+
+// constant that refers to the ISO standard
+hooks.ISO_8601 = function () {};
+
+// constant that refers to the RFC 2822 form
+hooks.RFC_2822 = function () {};
+
+// date from string and format string
+function configFromStringAndFormat(config) {
+    // TODO: Move this to another part of the creation flow to prevent circular deps
+    if (config._f === hooks.ISO_8601) {
+        configFromISO(config);
+        return;
+    }
+    if (config._f === hooks.RFC_2822) {
+        configFromRFC2822(config);
+        return;
+    }
+    config._a = [];
+    getParsingFlags(config).empty = true;
+
+    // This array is used to make a Date, either with `new Date` or `Date.UTC`
+    var string = '' + config._i,
+        i, parsedInput, tokens, token, skipped,
+        stringLength = string.length,
+        totalParsedInputLength = 0;
+
+    tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
+
+    for (i = 0; i < tokens.length; i++) {
+        token = tokens[i];
+        parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+        // console.log('token', token, 'parsedInput', parsedInput,
+        //         'regex', getParseRegexForToken(token, config));
+        if (parsedInput) {
+            skipped = string.substr(0, string.indexOf(parsedInput));
+            if (skipped.length > 0) {
+                getParsingFlags(config).unusedInput.push(skipped);
+            }
+            string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+            totalParsedInputLength += parsedInput.length;
+        }
+        // don't parse if it's not a known token
+        if (formatTokenFunctions[token]) {
+            if (parsedInput) {
+                getParsingFlags(config).empty = false;
+            }
+            else {
+                getParsingFlags(config).unusedTokens.push(token);
+            }
+            addTimeToArrayFromToken(token, parsedInput, config);
+        }
+        else if (config._strict && !parsedInput) {
+            getParsingFlags(config).unusedTokens.push(token);
+        }
+    }
+
+    // add remaining unparsed input length to the string
+    getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
+    if (string.length > 0) {
+        getParsingFlags(config).unusedInput.push(string);
+    }
+
+    // clear _12h flag if hour is <= 12
+    if (config._a[HOUR] <= 12 &&
+        getParsingFlags(config).bigHour === true &&
+        config._a[HOUR] > 0) {
+        getParsingFlags(config).bigHour = undefined;
+    }
+
+    getParsingFlags(config).parsedDateParts = config._a.slice(0);
+    getParsingFlags(config).meridiem = config._meridiem;
+    // handle meridiem
+    config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);
+
+    configFromArray(config);
+    checkOverflow(config);
+}
+
+
+function meridiemFixWrap (locale, hour, meridiem) {
+    var isPm;
+
+    if (meridiem == null) {
+        // nothing to do
+        return hour;
+    }
+    if (locale.meridiemHour != null) {
+        return locale.meridiemHour(hour, meridiem);
+    } else if (locale.isPM != null) {
+        // Fallback
+        isPm = locale.isPM(meridiem);
+        if (isPm && hour < 12) {
+            hour += 12;
+        }
+        if (!isPm && hour === 12) {
+            hour = 0;
+        }
+        return hour;
+    } else {
+        // this is not supposed to happen
+        return hour;
+    }
+}
+
+// date from string and array of format strings
+function configFromStringAndArray(config) {
+    var tempConfig,
+        bestMoment,
+
+        scoreToBeat,
+        i,
+        currentScore;
+
+    if (config._f.length === 0) {
+        getParsingFlags(config).invalidFormat = true;
+        config._d = new Date(NaN);
+        return;
+    }
+
+    for (i = 0; i < config._f.length; i++) {
+        currentScore = 0;
+        tempConfig = copyConfig({}, config);
+        if (config._useUTC != null) {
+            tempConfig._useUTC = config._useUTC;
+        }
+        tempConfig._f = config._f[i];
+        configFromStringAndFormat(tempConfig);
+
+        if (!isValid(tempConfig)) {
+            continue;
+        }
+
+        // if there is any input that was not parsed add a penalty for that format
+        currentScore += getParsingFlags(tempConfig).charsLeftOver;
+
+        //or tokens
+        currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10;
+
+        getParsingFlags(tempConfig).score = currentScore;
+
+        if (scoreToBeat == null || currentScore < scoreToBeat) {
+            scoreToBeat = currentScore;
+            bestMoment = tempConfig;
+        }
+    }
+
+    extend(config, bestMoment || tempConfig);
+}
+
+function configFromObject(config) {
+    if (config._d) {
+        return;
+    }
+
+    var i = normalizeObjectUnits(config._i);
+    config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) {
+        return obj && parseInt(obj, 10);
+    });
+
+    configFromArray(config);
+}
+
+function createFromConfig (config) {
+    var res = new Moment(checkOverflow(prepareConfig(config)));
+    if (res._nextDay) {
+        // Adding is smart enough around DST
+        res.add(1, 'd');
+        res._nextDay = undefined;
+    }
+
+    return res;
+}
+
+function prepareConfig (config) {
+    var input = config._i,
+        format = config._f;
+
+    config._locale = config._locale || getLocale(config._l);
+
+    if (input === null || (format === undefined && input === '')) {
+        return createInvalid({nullInput: true});
+    }
+
+    if (typeof input === 'string') {
+        config._i = input = config._locale.preparse(input);
+    }
+
+    if (isMoment(input)) {
+        return new Moment(checkOverflow(input));
+    } else if (isDate(input)) {
+        config._d = input;
+    } else if (isArray(format)) {
+        configFromStringAndArray(config);
+    } else if (format) {
+        configFromStringAndFormat(config);
+    }  else {
+        configFromInput(config);
+    }
+
+    if (!isValid(config)) {
+        config._d = null;
+    }
+
+    return config;
+}
+
+function configFromInput(config) {
+    var input = config._i;
+    if (isUndefined(input)) {
+        config._d = new Date(hooks.now());
+    } else if (isDate(input)) {
+        config._d = new Date(input.valueOf());
+    } else if (typeof input === 'string') {
+        configFromString(config);
+    } else if (isArray(input)) {
+        config._a = map(input.slice(0), function (obj) {
+            return parseInt(obj, 10);
+        });
+        configFromArray(config);
+    } else if (isObject(input)) {
+        configFromObject(config);
+    } else if (isNumber(input)) {
+        // from milliseconds
+        config._d = new Date(input);
+    } else {
+        hooks.createFromInputFallback(config);
+    }
+}
+
+function createLocalOrUTC (input, format, locale, strict, isUTC) {
+    var c = {};
+
+    if (locale === true || locale === false) {
+        strict = locale;
+        locale = undefined;
+    }
+
+    if ((isObject(input) && isObjectEmpty(input)) ||
+            (isArray(input) && input.length === 0)) {
+        input = undefined;
+    }
+    // object construction must be done this way.
+    // https://github.com/moment/moment/issues/1423
+    c._isAMomentObject = true;
+    c._useUTC = c._isUTC = isUTC;
+    c._l = locale;
+    c._i = input;
+    c._f = format;
+    c._strict = strict;
+
+    return createFromConfig(c);
+}
+
+function createLocal (input, format, locale, strict) {
+    return createLocalOrUTC(input, format, locale, strict, false);
+}
+
+var prototypeMin = deprecate(
+    'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/',
+    function () {
+        var other = createLocal.apply(null, arguments);
+        if (this.isValid() && other.isValid()) {
+            return other < this ? this : other;
+        } else {
+            return createInvalid();
+        }
+    }
+);
+
+var prototypeMax = deprecate(
+    'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/',
+    function () {
+        var other = createLocal.apply(null, arguments);
+        if (this.isValid() && other.isValid()) {
+            return other > this ? this : other;
+        } else {
+            return createInvalid();
+        }
+    }
+);
+
+// Pick a moment m from moments so that m[fn](other) is true for all
+// other. This relies on the function fn to be transitive.
+//
+// moments should either be an array of moment objects or an array, whose
+// first element is an array of moment objects.
+function pickBy(fn, moments) {
+    var res, i;
+    if (moments.length === 1 && isArray(moments[0])) {
+        moments = moments[0];
+    }
+    if (!moments.length) {
+        return createLocal();
+    }
+    res = moments[0];
+    for (i = 1; i < moments.length; ++i) {
+        if (!moments[i].isValid() || moments[i][fn](res)) {
+            res = moments[i];
+        }
+    }
+    return res;
+}
+
+// TODO: Use [].sort instead?
+function min () {
+    var args = [].slice.call(arguments, 0);
+
+    return pickBy('isBefore', args);
+}
+
+function max () {
+    var args = [].slice.call(arguments, 0);
+
+    return pickBy('isAfter', args);
+}
+
+var now = function () {
+    return Date.now ? Date.now() : +(new Date());
+};
+
+var ordering = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond'];
+
+function isDurationValid(m) {
+    for (var key in m) {
+        if (!(indexOf.call(ordering, key) !== -1 && (m[key] == null || !isNaN(m[key])))) {
+            return false;
+        }
+    }
+
+    var unitHasDecimal = false;
+    for (var i = 0; i < ordering.length; ++i) {
+        if (m[ordering[i]]) {
+            if (unitHasDecimal) {
+                return false; // only allow non-integers for smallest unit
+            }
+            if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) {
+                unitHasDecimal = true;
+            }
+        }
+    }
+
+    return true;
+}
+
+function isValid$1() {
+    return this._isValid;
+}
+
+function createInvalid$1() {
+    return createDuration(NaN);
+}
+
+function Duration (duration) {
+    var normalizedInput = normalizeObjectUnits(duration),
+        years = normalizedInput.year || 0,
+        quarters = normalizedInput.quarter || 0,
+        months = normalizedInput.month || 0,
+        weeks = normalizedInput.week || 0,
+        days = normalizedInput.day || 0,
+        hours = normalizedInput.hour || 0,
+        minutes = normalizedInput.minute || 0,
+        seconds = normalizedInput.second || 0,
+        milliseconds = normalizedInput.millisecond || 0;
+
+    this._isValid = isDurationValid(normalizedInput);
+
+    // representation for dateAddRemove
+    this._milliseconds = +milliseconds +
+        seconds * 1e3 + // 1000
+        minutes * 6e4 + // 1000 * 60
+        hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
+    // Because of dateAddRemove treats 24 hours as different from a
+    // day when working around DST, we need to store them separately
+    this._days = +days +
+        weeks * 7;
+    // It is impossible to translate months into days without knowing
+    // which months you are are talking about, so we have to store
+    // it separately.
+    this._months = +months +
+        quarters * 3 +
+        years * 12;
+
+    this._data = {};
+
+    this._locale = getLocale();
+
+    this._bubble();
+}
+
+function isDuration (obj) {
+    return obj instanceof Duration;
+}
+
+function absRound (number) {
+    if (number < 0) {
+        return Math.round(-1 * number) * -1;
+    } else {
+        return Math.round(number);
+    }
+}
+
+// FORMATTING
+
+function offset (token, separator) {
+    addFormatToken(token, 0, 0, function () {
+        var offset = this.utcOffset();
+        var sign = '+';
+        if (offset < 0) {
+            offset = -offset;
+            sign = '-';
+        }
+        return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2);
+    });
+}
+
+offset('Z', ':');
+offset('ZZ', '');
+
+// PARSING
+
+addRegexToken('Z',  matchShortOffset);
+addRegexToken('ZZ', matchShortOffset);
+addParseToken(['Z', 'ZZ'], function (input, array, config) {
+    config._useUTC = true;
+    config._tzm = offsetFromString(matchShortOffset, input);
+});
+
+// HELPERS
+
+// timezone chunker
+// '+10:00' > ['10',  '00']
+// '-1530'  > ['-15', '30']
+var chunkOffset = /([\+\-]|\d\d)/gi;
+
+function offsetFromString(matcher, string) {
+    var matches = (string || '').match(matcher);
+
+    if (matches === null) {
+        return null;
+    }
+
+    var chunk   = matches[matches.length - 1] || [];
+    var parts   = (chunk + '').match(chunkOffset) || ['-', 0, 0];
+    var minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+    return minutes === 0 ?
+      0 :
+      parts[0] === '+' ? minutes : -minutes;
+}
+
+// Return a moment from input, that is local/utc/zone equivalent to model.
+function cloneWithOffset(input, model) {
+    var res, diff;
+    if (model._isUTC) {
+        res = model.clone();
+        diff = (isMoment(input) || isDate(input) ? input.valueOf() : createLocal(input).valueOf()) - res.valueOf();
+        // Use low-level api, because this fn is low-level api.
+        res._d.setTime(res._d.valueOf() + diff);
+        hooks.updateOffset(res, false);
+        return res;
+    } else {
+        return createLocal(input).local();
+    }
+}
+
+function getDateOffset (m) {
+    // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+    // https://github.com/moment/moment/pull/1871
+    return -Math.round(m._d.getTimezoneOffset() / 15) * 15;
+}
+
+// HOOKS
+
+// This function will be called whenever a moment is mutated.
+// It is intended to keep the offset in sync with the timezone.
+hooks.updateOffset = function () {};
+
+// MOMENTS
+
+// keepLocalTime = true means only change the timezone, without
+// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+// +0200, so we adjust the time as needed, to be valid.
+//
+// Keeping the time actually adds/subtracts (one hour)
+// from the actual represented time. That is why we call updateOffset
+// a second time. In case it wants us to change the offset again
+// _changeInProgress == true case, then we have to adjust, because
+// there is no such time in the given timezone.
+function getSetOffset (input, keepLocalTime, keepMinutes) {
+    var offset = this._offset || 0,
+        localAdjust;
+    if (!this.isValid()) {
+        return input != null ? this : NaN;
+    }
+    if (input != null) {
+        if (typeof input === 'string') {
+            input = offsetFromString(matchShortOffset, input);
+            if (input === null) {
+                return this;
+            }
+        } else if (Math.abs(input) < 16 && !keepMinutes) {
+            input = input * 60;
+        }
+        if (!this._isUTC && keepLocalTime) {
+            localAdjust = getDateOffset(this);
+        }
+        this._offset = input;
+        this._isUTC = true;
+        if (localAdjust != null) {
+            this.add(localAdjust, 'm');
+        }
+        if (offset !== input) {
+            if (!keepLocalTime || this._changeInProgress) {
+                addSubtract(this, createDuration(input - offset, 'm'), 1, false);
+            } else if (!this._changeInProgress) {
+                this._changeInProgress = true;
+                hooks.updateOffset(this, true);
+                this._changeInProgress = null;
+            }
+        }
+        return this;
+    } else {
+        return this._isUTC ? offset : getDateOffset(this);
+    }
+}
+
+function getSetZone (input, keepLocalTime) {
+    if (input != null) {
+        if (typeof input !== 'string') {
+            input = -input;
+        }
+
+        this.utcOffset(input, keepLocalTime);
+
+        return this;
+    } else {
+        return -this.utcOffset();
+    }
+}
+
+function setOffsetToUTC (keepLocalTime) {
+    return this.utcOffset(0, keepLocalTime);
+}
+
+function setOffsetToLocal (keepLocalTime) {
+    if (this._isUTC) {
+        this.utcOffset(0, keepLocalTime);
+        this._isUTC = false;
+
+        if (keepLocalTime) {
+            this.subtract(getDateOffset(this), 'm');
+        }
+    }
+    return this;
+}
+
+function setOffsetToParsedOffset () {
+    if (this._tzm != null) {
+        this.utcOffset(this._tzm, false, true);
+    } else if (typeof this._i === 'string') {
+        var tZone = offsetFromString(matchOffset, this._i);
+        if (tZone != null) {
+            this.utcOffset(tZone);
+        }
+        else {
+            this.utcOffset(0, true);
+        }
+    }
+    return this;
+}
+
+function hasAlignedHourOffset (input) {
+    if (!this.isValid()) {
+        return false;
+    }
+    input = input ? createLocal(input).utcOffset() : 0;
+
+    return (this.utcOffset() - input) % 60 === 0;
+}
+
+function isDaylightSavingTime () {
+    return (
+        this.utcOffset() > this.clone().month(0).utcOffset() ||
+        this.utcOffset() > this.clone().month(5).utcOffset()
+    );
+}
+
+function isDaylightSavingTimeShifted () {
+    if (!isUndefined(this._isDSTShifted)) {
+        return this._isDSTShifted;
+    }
+
+    var c = {};
+
+    copyConfig(c, this);
+    c = prepareConfig(c);
+
+    if (c._a) {
+        var other = c._isUTC ? createUTC(c._a) : createLocal(c._a);
+        this._isDSTShifted = this.isValid() &&
+            compareArrays(c._a, other.toArray()) > 0;
+    } else {
+        this._isDSTShifted = false;
+    }
+
+    return this._isDSTShifted;
+}
+
+function isLocal () {
+    return this.isValid() ? !this._isUTC : false;
+}
+
+function isUtcOffset () {
+    return this.isValid() ? this._isUTC : false;
+}
+
+function isUtc () {
+    return this.isValid() ? this._isUTC && this._offset === 0 : false;
+}
+
+// ASP.NET json date format regex
+var aspNetRegex = /^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/;
+
+// from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+// somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+// and further modified to allow for strings containing both week and day
+var isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;
+
+function createDuration (input, key) {
+    var duration = input,
+        // matching against regexp is expensive, do it on demand
+        match = null,
+        sign,
+        ret,
+        diffRes;
+
+    if (isDuration(input)) {
+        duration = {
+            ms : input._milliseconds,
+            d  : input._days,
+            M  : input._months
+        };
+    } else if (isNumber(input)) {
+        duration = {};
+        if (key) {
+            duration[key] = input;
+        } else {
+            duration.milliseconds = input;
+        }
+    } else if (!!(match = aspNetRegex.exec(input))) {
+        sign = (match[1] === '-') ? -1 : 1;
+        duration = {
+            y  : 0,
+            d  : toInt(match[DATE])                         * sign,
+            h  : toInt(match[HOUR])                         * sign,
+            m  : toInt(match[MINUTE])                       * sign,
+            s  : toInt(match[SECOND])                       * sign,
+            ms : toInt(absRound(match[MILLISECOND] * 1000)) * sign // the millisecond decimal point is included in the match
+        };
+    } else if (!!(match = isoRegex.exec(input))) {
+        sign = (match[1] === '-') ? -1 : (match[1] === '+') ? 1 : 1;
+        duration = {
+            y : parseIso(match[2], sign),
+            M : parseIso(match[3], sign),
+            w : parseIso(match[4], sign),
+            d : parseIso(match[5], sign),
+            h : parseIso(match[6], sign),
+            m : parseIso(match[7], sign),
+            s : parseIso(match[8], sign)
+        };
+    } else if (duration == null) {// checks for null or undefined
+        duration = {};
+    } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) {
+        diffRes = momentsDifference(createLocal(duration.from), createLocal(duration.to));
+
+        duration = {};
+        duration.ms = diffRes.milliseconds;
+        duration.M = diffRes.months;
+    }
+
+    ret = new Duration(duration);
+
+    if (isDuration(input) && hasOwnProp(input, '_locale')) {
+        ret._locale = input._locale;
+    }
+
+    return ret;
+}
+
+createDuration.fn = Duration.prototype;
+createDuration.invalid = createInvalid$1;
+
+function parseIso (inp, sign) {
+    // We'd normally use ~~inp for this, but unfortunately it also
+    // converts floats to ints.
+    // inp may be undefined, so careful calling replace on it.
+    var res = inp && parseFloat(inp.replace(',', '.'));
+    // apply sign while we're at it
+    return (isNaN(res) ? 0 : res) * sign;
+}
+
+function positiveMomentsDifference(base, other) {
+    var res = {milliseconds: 0, months: 0};
+
+    res.months = other.month() - base.month() +
+        (other.year() - base.year()) * 12;
+    if (base.clone().add(res.months, 'M').isAfter(other)) {
+        --res.months;
+    }
+
+    res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
+
+    return res;
+}
+
+function momentsDifference(base, other) {
+    var res;
+    if (!(base.isValid() && other.isValid())) {
+        return {milliseconds: 0, months: 0};
+    }
+
+    other = cloneWithOffset(other, base);
+    if (base.isBefore(other)) {
+        res = positiveMomentsDifference(base, other);
+    } else {
+        res = positiveMomentsDifference(other, base);
+        res.milliseconds = -res.milliseconds;
+        res.months = -res.months;
+    }
+
+    return res;
+}
+
+// TODO: remove 'name' arg after deprecation is removed
+function createAdder(direction, name) {
+    return function (val, period) {
+        var dur, tmp;
+        //invert the arguments, but complain about it
+        if (period !== null && !isNaN(+period)) {
+            deprecateSimple(name, 'moment().' + name  + '(period, number) is deprecated. Please use moment().' + name + '(number, period). ' +
+            'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.');
+            tmp = val; val = period; period = tmp;
+        }
+
+        val = typeof val === 'string' ? +val : val;
+        dur = createDuration(val, period);
+        addSubtract(this, dur, direction);
+        return this;
+    };
+}
+
+function addSubtract (mom, duration, isAdding, updateOffset) {
+    var milliseconds = duration._milliseconds,
+        days = absRound(duration._days),
+        months = absRound(duration._months);
+
+    if (!mom.isValid()) {
+        // No op
+        return;
+    }
+
+    updateOffset = updateOffset == null ? true : updateOffset;
+
+    if (months) {
+        setMonth(mom, get(mom, 'Month') + months * isAdding);
+    }
+    if (days) {
+        set$1(mom, 'Date', get(mom, 'Date') + days * isAdding);
+    }
+    if (milliseconds) {
+        mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding);
+    }
+    if (updateOffset) {
+        hooks.updateOffset(mom, days || months);
+    }
+}
+
+var add      = createAdder(1, 'add');
+var subtract = createAdder(-1, 'subtract');
+
+function getCalendarFormat(myMoment, now) {
+    var diff = myMoment.diff(now, 'days', true);
+    return diff < -6 ? 'sameElse' :
+            diff < -1 ? 'lastWeek' :
+            diff < 0 ? 'lastDay' :
+            diff < 1 ? 'sameDay' :
+            diff < 2 ? 'nextDay' :
+            diff < 7 ? 'nextWeek' : 'sameElse';
+}
+
+function calendar$1 (time, formats) {
+    // We want to compare the start of today, vs this.
+    // Getting start-of-today depends on whether we're local/utc/offset or not.
+    var now = time || createLocal(),
+        sod = cloneWithOffset(now, this).startOf('day'),
+        format = hooks.calendarFormat(this, sod) || 'sameElse';
+
+    var output = formats && (isFunction(formats[format]) ? formats[format].call(this, now) : formats[format]);
+
+    return this.format(output || this.localeData().calendar(format, this, createLocal(now)));
+}
+
+function clone () {
+    return new Moment(this);
+}
+
+function isAfter (input, units) {
+    var localInput = isMoment(input) ? input : createLocal(input);
+    if (!(this.isValid() && localInput.isValid())) {
+        return false;
+    }
+    units = normalizeUnits(!isUndefined(units) ? units : 'millisecond');
+    if (units === 'millisecond') {
+        return this.valueOf() > localInput.valueOf();
+    } else {
+        return localInput.valueOf() < this.clone().startOf(units).valueOf();
+    }
+}
+
+function isBefore (input, units) {
+    var localInput = isMoment(input) ? input : createLocal(input);
+    if (!(this.isValid() && localInput.isValid())) {
+        return false;
+    }
+    units = normalizeUnits(!isUndefined(units) ? units : 'millisecond');
+    if (units === 'millisecond') {
+        return this.valueOf() < localInput.valueOf();
+    } else {
+        return this.clone().endOf(units).valueOf() < localInput.valueOf();
+    }
+}
+
+function isBetween (from, to, units, inclusivity) {
+    inclusivity = inclusivity || '()';
+    return (inclusivity[0] === '(' ? this.isAfter(from, units) : !this.isBefore(from, units)) &&
+        (inclusivity[1] === ')' ? this.isBefore(to, units) : !this.isAfter(to, units));
+}
+
+function isSame (input, units) {
+    var localInput = isMoment(input) ? input : createLocal(input),
+        inputMs;
+    if (!(this.isValid() && localInput.isValid())) {
+        return false;
+    }
+    units = normalizeUnits(units || 'millisecond');
+    if (units === 'millisecond') {
+        return this.valueOf() === localInput.valueOf();
+    } else {
+        inputMs = localInput.valueOf();
+        return this.clone().startOf(units).valueOf() <= inputMs && inputMs <= this.clone().endOf(units).valueOf();
+    }
+}
+
+function isSameOrAfter (input, units) {
+    return this.isSame(input, units) || this.isAfter(input,units);
+}
+
+function isSameOrBefore (input, units) {
+    return this.isSame(input, units) || this.isBefore(input,units);
+}
+
+function diff (input, units, asFloat) {
+    var that,
+        zoneDelta,
+        delta, output;
+
+    if (!this.isValid()) {
+        return NaN;
+    }
+
+    that = cloneWithOffset(input, this);
+
+    if (!that.isValid()) {
+        return NaN;
+    }
+
+    zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4;
+
+    units = normalizeUnits(units);
+
+    switch (units) {
+        case 'year': output = monthDiff(this, that) / 12; break;
+        case 'month': output = monthDiff(this, that); break;
+        case 'quarter': output = monthDiff(this, that) / 3; break;
+        case 'second': output = (this - that) / 1e3; break; // 1000
+        case 'minute': output = (this - that) / 6e4; break; // 1000 * 60
+        case 'hour': output = (this - that) / 36e5; break; // 1000 * 60 * 60
+        case 'day': output = (this - that - zoneDelta) / 864e5; break; // 1000 * 60 * 60 * 24, negate dst
+        case 'week': output = (this - that - zoneDelta) / 6048e5; break; // 1000 * 60 * 60 * 24 * 7, negate dst
+        default: output = this - that;
+    }
+
+    return asFloat ? output : absFloor(output);
+}
+
+function monthDiff (a, b) {
+    // difference in months
+    var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()),
+        // b is in (anchor - 1 month, anchor + 1 month)
+        anchor = a.clone().add(wholeMonthDiff, 'months'),
+        anchor2, adjust;
+
+    if (b - anchor < 0) {
+        anchor2 = a.clone().add(wholeMonthDiff - 1, 'months');
+        // linear across the month
+        adjust = (b - anchor) / (anchor - anchor2);
+    } else {
+        anchor2 = a.clone().add(wholeMonthDiff + 1, 'months');
+        // linear across the month
+        adjust = (b - anchor) / (anchor2 - anchor);
+    }
+
+    //check for negative zero, return zero if negative zero
+    return -(wholeMonthDiff + adjust) || 0;
+}
+
+hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ';
+hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]';
+
+function toString () {
+    return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+}
+
+function toISOString() {
+    if (!this.isValid()) {
+        return null;
+    }
+    var m = this.clone().utc();
+    if (m.year() < 0 || m.year() > 9999) {
+        return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+    }
+    if (isFunction(Date.prototype.toISOString)) {
+        // native implementation is ~50x faster, use it when we can
+        return this.toDate().toISOString();
+    }
+    return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+}
+
+/**
+ * Return a human readable representation of a moment that can
+ * also be evaluated to get a new moment which is the same
+ *
+ * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects
+ */
+function inspect () {
+    if (!this.isValid()) {
+        return 'moment.invalid(/* ' + this._i + ' */)';
+    }
+    var func = 'moment';
+    var zone = '';
+    if (!this.isLocal()) {
+        func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone';
+        zone = 'Z';
+    }
+    var prefix = '[' + func + '("]';
+    var year = (0 <= this.year() && this.year() <= 9999) ? 'YYYY' : 'YYYYYY';
+    var datetime = '-MM-DD[T]HH:mm:ss.SSS';
+    var suffix = zone + '[")]';
+
+    return this.format(prefix + year + datetime + suffix);
+}
+
+function format (inputString) {
+    if (!inputString) {
+        inputString = this.isUtc() ? hooks.defaultFormatUtc : hooks.defaultFormat;
+    }
+    var output = formatMoment(this, inputString);
+    return this.localeData().postformat(output);
+}
+
+function from (time, withoutSuffix) {
+    if (this.isValid() &&
+            ((isMoment(time) && time.isValid()) ||
+             createLocal(time).isValid())) {
+        return createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
+    } else {
+        return this.localeData().invalidDate();
+    }
+}
+
+function fromNow (withoutSuffix) {
+    return this.from(createLocal(), withoutSuffix);
+}
+
+function to (time, withoutSuffix) {
+    if (this.isValid() &&
+            ((isMoment(time) && time.isValid()) ||
+             createLocal(time).isValid())) {
+        return createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix);
+    } else {
+        return this.localeData().invalidDate();
+    }
+}
+
+function toNow (withoutSuffix) {
+    return this.to(createLocal(), withoutSuffix);
+}
+
+// If passed a locale key, it will set the locale for this
+// instance.  Otherwise, it will return the locale configuration
+// variables for this instance.
+function locale (key) {
+    var newLocaleData;
+
+    if (key === undefined) {
+        return this._locale._abbr;
+    } else {
+        newLocaleData = getLocale(key);
+        if (newLocaleData != null) {
+            this._locale = newLocaleData;
+        }
+        return this;
+    }
+}
+
+var lang = deprecate(
+    'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+    function (key) {
+        if (key === undefined) {
+            return this.localeData();
+        } else {
+            return this.locale(key);
+        }
+    }
+);
+
+function localeData () {
+    return this._locale;
+}
+
+function startOf (units) {
+    units = normalizeUnits(units);
+    // the following switch intentionally omits break keywords
+    // to utilize falling through the cases.
+    switch (units) {
+        case 'year':
+            this.month(0);
+            /* falls through */
+        case 'quarter':
+        case 'month':
+            this.date(1);
+            /* falls through */
+        case 'week':
+        case 'isoWeek':
+        case 'day':
+        case 'date':
+            this.hours(0);
+            /* falls through */
+        case 'hour':
+            this.minutes(0);
+            /* falls through */
+        case 'minute':
+            this.seconds(0);
+            /* falls through */
+        case 'second':
+            this.milliseconds(0);
+    }
+
+    // weeks are a special case
+    if (units === 'week') {
+        this.weekday(0);
+    }
+    if (units === 'isoWeek') {
+        this.isoWeekday(1);
+    }
+
+    // quarters are also special
+    if (units === 'quarter') {
+        this.month(Math.floor(this.month() / 3) * 3);
+    }
+
+    return this;
+}
+
+function endOf (units) {
+    units = normalizeUnits(units);
+    if (units === undefined || units === 'millisecond') {
+        return this;
+    }
+
+    // 'date' is an alias for 'day', so it should be considered as such.
+    if (units === 'date') {
+        units = 'day';
+    }
+
+    return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
+}
+
+function valueOf () {
+    return this._d.valueOf() - ((this._offset || 0) * 60000);
+}
+
+function unix () {
+    return Math.floor(this.valueOf() / 1000);
+}
+
+function toDate () {
+    return new Date(this.valueOf());
+}
+
+function toArray () {
+    var m = this;
+    return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()];
+}
+
+function toObject () {
+    var m = this;
+    return {
+        years: m.year(),
+        months: m.month(),
+        date: m.date(),
+        hours: m.hours(),
+        minutes: m.minutes(),
+        seconds: m.seconds(),
+        milliseconds: m.milliseconds()
+    };
+}
+
+function toJSON () {
+    // new Date(NaN).toJSON() === null
+    return this.isValid() ? this.toISOString() : null;
+}
+
+function isValid$2 () {
+    return isValid(this);
+}
+
+function parsingFlags () {
+    return extend({}, getParsingFlags(this));
+}
+
+function invalidAt () {
+    return getParsingFlags(this).overflow;
+}
+
+function creationData() {
+    return {
+        input: this._i,
+        format: this._f,
+        locale: this._locale,
+        isUTC: this._isUTC,
+        strict: this._strict
+    };
+}
+
+// FORMATTING
+
+addFormatToken(0, ['gg', 2], 0, function () {
+    return this.weekYear() % 100;
+});
+
+addFormatToken(0, ['GG', 2], 0, function () {
+    return this.isoWeekYear() % 100;
+});
+
+function addWeekYearFormatToken (token, getter) {
+    addFormatToken(0, [token, token.length], 0, getter);
+}
+
+addWeekYearFormatToken('gggg',     'weekYear');
+addWeekYearFormatToken('ggggg',    'weekYear');
+addWeekYearFormatToken('GGGG',  'isoWeekYear');
+addWeekYearFormatToken('GGGGG', 'isoWeekYear');
+
+// ALIASES
+
+addUnitAlias('weekYear', 'gg');
+addUnitAlias('isoWeekYear', 'GG');
+
+// PRIORITY
+
+addUnitPriority('weekYear', 1);
+addUnitPriority('isoWeekYear', 1);
+
+
+// PARSING
+
+addRegexToken('G',      matchSigned);
+addRegexToken('g',      matchSigned);
+addRegexToken('GG',     match1to2, match2);
+addRegexToken('gg',     match1to2, match2);
+addRegexToken('GGGG',   match1to4, match4);
+addRegexToken('gggg',   match1to4, match4);
+addRegexToken('GGGGG',  match1to6, match6);
+addRegexToken('ggggg',  match1to6, match6);
+
+addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) {
+    week[token.substr(0, 2)] = toInt(input);
+});
+
+addWeekParseToken(['gg', 'GG'], function (input, week, config, token) {
+    week[token] = hooks.parseTwoDigitYear(input);
+});
+
+// MOMENTS
+
+function getSetWeekYear (input) {
+    return getSetWeekYearHelper.call(this,
+            input,
+            this.week(),
+            this.weekday(),
+            this.localeData()._week.dow,
+            this.localeData()._week.doy);
+}
+
+function getSetISOWeekYear (input) {
+    return getSetWeekYearHelper.call(this,
+            input, this.isoWeek(), this.isoWeekday(), 1, 4);
+}
+
+function getISOWeeksInYear () {
+    return weeksInYear(this.year(), 1, 4);
+}
+
+function getWeeksInYear () {
+    var weekInfo = this.localeData()._week;
+    return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+}
+
+function getSetWeekYearHelper(input, week, weekday, dow, doy) {
+    var weeksTarget;
+    if (input == null) {
+        return weekOfYear(this, dow, doy).year;
+    } else {
+        weeksTarget = weeksInYear(input, dow, doy);
+        if (week > weeksTarget) {
+            week = weeksTarget;
+        }
+        return setWeekAll.call(this, input, week, weekday, dow, doy);
+    }
+}
+
+function setWeekAll(weekYear, week, weekday, dow, doy) {
+    var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy),
+        date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear);
+
+    this.year(date.getUTCFullYear());
+    this.month(date.getUTCMonth());
+    this.date(date.getUTCDate());
+    return this;
+}
+
+// FORMATTING
+
+addFormatToken('Q', 0, 'Qo', 'quarter');
+
+// ALIASES
+
+addUnitAlias('quarter', 'Q');
+
+// PRIORITY
+
+addUnitPriority('quarter', 7);
+
+// PARSING
+
+addRegexToken('Q', match1);
+addParseToken('Q', function (input, array) {
+    array[MONTH] = (toInt(input) - 1) * 3;
+});
+
+// MOMENTS
+
+function getSetQuarter (input) {
+    return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+}
+
+// FORMATTING
+
+addFormatToken('D', ['DD', 2], 'Do', 'date');
+
+// ALIASES
+
+addUnitAlias('date', 'D');
+
+// PRIOROITY
+addUnitPriority('date', 9);
+
+// PARSING
+
+addRegexToken('D',  match1to2);
+addRegexToken('DD', match1to2, match2);
+addRegexToken('Do', function (isStrict, locale) {
+    // TODO: Remove "ordinalParse" fallback in next major release.
+    return isStrict ?
+      (locale._dayOfMonthOrdinalParse || locale._ordinalParse) :
+      locale._dayOfMonthOrdinalParseLenient;
+});
+
+addParseToken(['D', 'DD'], DATE);
+addParseToken('Do', function (input, array) {
+    array[DATE] = toInt(input.match(match1to2)[0]);
+});
+
+// MOMENTS
+
+var getSetDayOfMonth = makeGetSet('Date', true);
+
+// FORMATTING
+
+addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear');
+
+// ALIASES
+
+addUnitAlias('dayOfYear', 'DDD');
+
+// PRIORITY
+addUnitPriority('dayOfYear', 4);
+
+// PARSING
+
+addRegexToken('DDD',  match1to3);
+addRegexToken('DDDD', match3);
+addParseToken(['DDD', 'DDDD'], function (input, array, config) {
+    config._dayOfYear = toInt(input);
+});
+
+// HELPERS
+
+// MOMENTS
+
+function getSetDayOfYear (input) {
+    var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1;
+    return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
+}
+
+// FORMATTING
+
+addFormatToken('m', ['mm', 2], 0, 'minute');
+
+// ALIASES
+
+addUnitAlias('minute', 'm');
+
+// PRIORITY
+
+addUnitPriority('minute', 14);
+
+// PARSING
+
+addRegexToken('m',  match1to2);
+addRegexToken('mm', match1to2, match2);
+addParseToken(['m', 'mm'], MINUTE);
+
+// MOMENTS
+
+var getSetMinute = makeGetSet('Minutes', false);
+
+// FORMATTING
+
+addFormatToken('s', ['ss', 2], 0, 'second');
+
+// ALIASES
+
+addUnitAlias('second', 's');
+
+// PRIORITY
+
+addUnitPriority('second', 15);
+
+// PARSING
+
+addRegexToken('s',  match1to2);
+addRegexToken('ss', match1to2, match2);
+addParseToken(['s', 'ss'], SECOND);
+
+// MOMENTS
+
+var getSetSecond = makeGetSet('Seconds', false);
+
+// FORMATTING
+
+addFormatToken('S', 0, 0, function () {
+    return ~~(this.millisecond() / 100);
+});
+
+addFormatToken(0, ['SS', 2], 0, function () {
+    return ~~(this.millisecond() / 10);
+});
+
+addFormatToken(0, ['SSS', 3], 0, 'millisecond');
+addFormatToken(0, ['SSSS', 4], 0, function () {
+    return this.millisecond() * 10;
+});
+addFormatToken(0, ['SSSSS', 5], 0, function () {
+    return this.millisecond() * 100;
+});
+addFormatToken(0, ['SSSSSS', 6], 0, function () {
+    return this.millisecond() * 1000;
+});
+addFormatToken(0, ['SSSSSSS', 7], 0, function () {
+    return this.millisecond() * 10000;
+});
+addFormatToken(0, ['SSSSSSSS', 8], 0, function () {
+    return this.millisecond() * 100000;
+});
+addFormatToken(0, ['SSSSSSSSS', 9], 0, function () {
+    return this.millisecond() * 1000000;
+});
+
+
+// ALIASES
+
+addUnitAlias('millisecond', 'ms');
+
+// PRIORITY
+
+addUnitPriority('millisecond', 16);
+
+// PARSING
+
+addRegexToken('S',    match1to3, match1);
+addRegexToken('SS',   match1to3, match2);
+addRegexToken('SSS',  match1to3, match3);
+
+var token;
+for (token = 'SSSS'; token.length <= 9; token += 'S') {
+    addRegexToken(token, matchUnsigned);
+}
+
+function parseMs(input, array) {
+    array[MILLISECOND] = toInt(('0.' + input) * 1000);
+}
+
+for (token = 'S'; token.length <= 9; token += 'S') {
+    addParseToken(token, parseMs);
+}
+// MOMENTS
+
+var getSetMillisecond = makeGetSet('Milliseconds', false);
+
+// FORMATTING
+
+addFormatToken('z',  0, 0, 'zoneAbbr');
+addFormatToken('zz', 0, 0, 'zoneName');
+
+// MOMENTS
+
+function getZoneAbbr () {
+    return this._isUTC ? 'UTC' : '';
+}
+
+function getZoneName () {
+    return this._isUTC ? 'Coordinated Universal Time' : '';
+}
+
+var proto = Moment.prototype;
+
+proto.add               = add;
+proto.calendar          = calendar$1;
+proto.clone             = clone;
+proto.diff              = diff;
+proto.endOf             = endOf;
+proto.format            = format;
+proto.from              = from;
+proto.fromNow           = fromNow;
+proto.to                = to;
+proto.toNow             = toNow;
+proto.get               = stringGet;
+proto.invalidAt         = invalidAt;
+proto.isAfter           = isAfter;
+proto.isBefore          = isBefore;
+proto.isBetween         = isBetween;
+proto.isSame            = isSame;
+proto.isSameOrAfter     = isSameOrAfter;
+proto.isSameOrBefore    = isSameOrBefore;
+proto.isValid           = isValid$2;
+proto.lang              = lang;
+proto.locale            = locale;
+proto.localeData        = localeData;
+proto.max               = prototypeMax;
+proto.min               = prototypeMin;
+proto.parsingFlags      = parsingFlags;
+proto.set               = stringSet;
+proto.startOf           = startOf;
+proto.subtract          = subtract;
+proto.toArray           = toArray;
+proto.toObject          = toObject;
+proto.toDate            = toDate;
+proto.toISOString       = toISOString;
+proto.inspect           = inspect;
+proto.toJSON            = toJSON;
+proto.toString          = toString;
+proto.unix              = unix;
+proto.valueOf           = valueOf;
+proto.creationData      = creationData;
+
+// Year
+proto.year       = getSetYear;
+proto.isLeapYear = getIsLeapYear;
+
+// Week Year
+proto.weekYear    = getSetWeekYear;
+proto.isoWeekYear = getSetISOWeekYear;
+
+// Quarter
+proto.quarter = proto.quarters = getSetQuarter;
+
+// Month
+proto.month       = getSetMonth;
+proto.daysInMonth = getDaysInMonth;
+
+// Week
+proto.week           = proto.weeks        = getSetWeek;
+proto.isoWeek        = proto.isoWeeks     = getSetISOWeek;
+proto.weeksInYear    = getWeeksInYear;
+proto.isoWeeksInYear = getISOWeeksInYear;
+
+// Day
+proto.date       = getSetDayOfMonth;
+proto.day        = proto.days             = getSetDayOfWeek;
+proto.weekday    = getSetLocaleDayOfWeek;
+proto.isoWeekday = getSetISODayOfWeek;
+proto.dayOfYear  = getSetDayOfYear;
+
+// Hour
+proto.hour = proto.hours = getSetHour;
+
+// Minute
+proto.minute = proto.minutes = getSetMinute;
+
+// Second
+proto.second = proto.seconds = getSetSecond;
+
+// Millisecond
+proto.millisecond = proto.milliseconds = getSetMillisecond;
+
+// Offset
+proto.utcOffset            = getSetOffset;
+proto.utc                  = setOffsetToUTC;
+proto.local                = setOffsetToLocal;
+proto.parseZone            = setOffsetToParsedOffset;
+proto.hasAlignedHourOffset = hasAlignedHourOffset;
+proto.isDST                = isDaylightSavingTime;
+proto.isLocal              = isLocal;
+proto.isUtcOffset          = isUtcOffset;
+proto.isUtc                = isUtc;
+proto.isUTC                = isUtc;
+
+// Timezone
+proto.zoneAbbr = getZoneAbbr;
+proto.zoneName = getZoneName;
+
+// Deprecations
+proto.dates  = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth);
+proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth);
+proto.years  = deprecate('years accessor is deprecated. Use year instead', getSetYear);
+proto.zone   = deprecate('moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', getSetZone);
+proto.isDSTShifted = deprecate('isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', isDaylightSavingTimeShifted);
+
+function createUnix (input) {
+    return createLocal(input * 1000);
+}
+
+function createInZone () {
+    return createLocal.apply(null, arguments).parseZone();
+}
+
+function preParsePostFormat (string) {
+    return string;
+}
+
+var proto$1 = Locale.prototype;
+
+proto$1.calendar        = calendar;
+proto$1.longDateFormat  = longDateFormat;
+proto$1.invalidDate     = invalidDate;
+proto$1.ordinal         = ordinal;
+proto$1.preparse        = preParsePostFormat;
+proto$1.postformat      = preParsePostFormat;
+proto$1.relativeTime    = relativeTime;
+proto$1.pastFuture      = pastFuture;
+proto$1.set             = set;
+
+// Month
+proto$1.months            =        localeMonths;
+proto$1.monthsShort       =        localeMonthsShort;
+proto$1.monthsParse       =        localeMonthsParse;
+proto$1.monthsRegex       = monthsRegex;
+proto$1.monthsShortRegex  = monthsShortRegex;
+
+// Week
+proto$1.week = localeWeek;
+proto$1.firstDayOfYear = localeFirstDayOfYear;
+proto$1.firstDayOfWeek = localeFirstDayOfWeek;
+
+// Day of Week
+proto$1.weekdays       =        localeWeekdays;
+proto$1.weekdaysMin    =        localeWeekdaysMin;
+proto$1.weekdaysShort  =        localeWeekdaysShort;
+proto$1.weekdaysParse  =        localeWeekdaysParse;
+
+proto$1.weekdaysRegex       =        weekdaysRegex;
+proto$1.weekdaysShortRegex  =        weekdaysShortRegex;
+proto$1.weekdaysMinRegex    =        weekdaysMinRegex;
+
+// Hours
+proto$1.isPM = localeIsPM;
+proto$1.meridiem = localeMeridiem;
+
+function get$1 (format, index, field, setter) {
+    var locale = getLocale();
+    var utc = createUTC().set(setter, index);
+    return locale[field](utc, format);
+}
+
+function listMonthsImpl (format, index, field) {
+    if (isNumber(format)) {
+        index = format;
+        format = undefined;
+    }
+
+    format = format || '';
+
+    if (index != null) {
+        return get$1(format, index, field, 'month');
+    }
+
+    var i;
+    var out = [];
+    for (i = 0; i < 12; i++) {
+        out[i] = get$1(format, i, field, 'month');
+    }
+    return out;
+}
+
+// ()
+// (5)
+// (fmt, 5)
+// (fmt)
+// (true)
+// (true, 5)
+// (true, fmt, 5)
+// (true, fmt)
+function listWeekdaysImpl (localeSorted, format, index, field) {
+    if (typeof localeSorted === 'boolean') {
+        if (isNumber(format)) {
+            index = format;
+            format = undefined;
+        }
+
+        format = format || '';
+    } else {
+        format = localeSorted;
+        index = format;
+        localeSorted = false;
+
+        if (isNumber(format)) {
+            index = format;
+            format = undefined;
+        }
+
+        format = format || '';
+    }
+
+    var locale = getLocale(),
+        shift = localeSorted ? locale._week.dow : 0;
+
+    if (index != null) {
+        return get$1(format, (index + shift) % 7, field, 'day');
+    }
+
+    var i;
+    var out = [];
+    for (i = 0; i < 7; i++) {
+        out[i] = get$1(format, (i + shift) % 7, field, 'day');
+    }
+    return out;
+}
+
+function listMonths (format, index) {
+    return listMonthsImpl(format, index, 'months');
+}
+
+function listMonthsShort (format, index) {
+    return listMonthsImpl(format, index, 'monthsShort');
+}
+
+function listWeekdays (localeSorted, format, index) {
+    return listWeekdaysImpl(localeSorted, format, index, 'weekdays');
+}
+
+function listWeekdaysShort (localeSorted, format, index) {
+    return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort');
+}
+
+function listWeekdaysMin (localeSorted, format, index) {
+    return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin');
+}
+
+getSetGlobalLocale('en', {
+    dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/,
+    ordinal : function (number) {
+        var b = number % 10,
+            output = (toInt(number % 100 / 10) === 1) ? 'th' :
+            (b === 1) ? 'st' :
+            (b === 2) ? 'nd' :
+            (b === 3) ? 'rd' : 'th';
+        return number + output;
+    }
+});
+
+// Side effect imports
+hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', getSetGlobalLocale);
+hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', getLocale);
+
+var mathAbs = Math.abs;
+
+function abs () {
+    var data           = this._data;
+
+    this._milliseconds = mathAbs(this._milliseconds);
+    this._days         = mathAbs(this._days);
+    this._months       = mathAbs(this._months);
+
+    data.milliseconds  = mathAbs(data.milliseconds);
+    data.seconds       = mathAbs(data.seconds);
+    data.minutes       = mathAbs(data.minutes);
+    data.hours         = mathAbs(data.hours);
+    data.months        = mathAbs(data.months);
+    data.years         = mathAbs(data.years);
+
+    return this;
+}
+
+function addSubtract$1 (duration, input, value, direction) {
+    var other = createDuration(input, value);
+
+    duration._milliseconds += direction * other._milliseconds;
+    duration._days         += direction * other._days;
+    duration._months       += direction * other._months;
+
+    return duration._bubble();
+}
+
+// supports only 2.0-style add(1, 's') or add(duration)
+function add$1 (input, value) {
+    return addSubtract$1(this, input, value, 1);
+}
+
+// supports only 2.0-style subtract(1, 's') or subtract(duration)
+function subtract$1 (input, value) {
+    return addSubtract$1(this, input, value, -1);
+}
+
+function absCeil (number) {
+    if (number < 0) {
+        return Math.floor(number);
+    } else {
+        return Math.ceil(number);
+    }
+}
+
+function bubble () {
+    var milliseconds = this._milliseconds;
+    var days         = this._days;
+    var months       = this._months;
+    var data         = this._data;
+    var seconds, minutes, hours, years, monthsFromDays;
+
+    // if we have a mix of positive and negative values, bubble down first
+    // check: https://github.com/moment/moment/issues/2166
+    if (!((milliseconds >= 0 && days >= 0 && months >= 0) ||
+            (milliseconds <= 0 && days <= 0 && months <= 0))) {
+        milliseconds += absCeil(monthsToDays(months) + days) * 864e5;
+        days = 0;
+        months = 0;
+    }
+
+    // The following code bubbles up values, see the tests for
+    // examples of what that means.
+    data.milliseconds = milliseconds % 1000;
+
+    seconds           = absFloor(milliseconds / 1000);
+    data.seconds      = seconds % 60;
+
+    minutes           = absFloor(seconds / 60);
+    data.minutes      = minutes % 60;
+
+    hours             = absFloor(minutes / 60);
+    data.hours        = hours % 24;
+
+    days += absFloor(hours / 24);
+
+    // convert days to months
+    monthsFromDays = absFloor(daysToMonths(days));
+    months += monthsFromDays;
+    days -= absCeil(monthsToDays(monthsFromDays));
+
+    // 12 months -> 1 year
+    years = absFloor(months / 12);
+    months %= 12;
+
+    data.days   = days;
+    data.months = months;
+    data.years  = years;
+
+    return this;
+}
+
+function daysToMonths (days) {
+    // 400 years have 146097 days (taking into account leap year rules)
+    // 400 years have 12 months === 4800
+    return days * 4800 / 146097;
+}
+
+function monthsToDays (months) {
+    // the reverse of daysToMonths
+    return months * 146097 / 4800;
+}
+
+function as (units) {
+    if (!this.isValid()) {
+        return NaN;
+    }
+    var days;
+    var months;
+    var milliseconds = this._milliseconds;
+
+    units = normalizeUnits(units);
+
+    if (units === 'month' || units === 'year') {
+        days   = this._days   + milliseconds / 864e5;
+        months = this._months + daysToMonths(days);
+        return units === 'month' ? months : months / 12;
+    } else {
+        // handle milliseconds separately because of floating point math errors (issue #1867)
+        days = this._days + Math.round(monthsToDays(this._months));
+        switch (units) {
+            case 'week'   : return days / 7     + milliseconds / 6048e5;
+            case 'day'    : return days         + milliseconds / 864e5;
+            case 'hour'   : return days * 24    + milliseconds / 36e5;
+            case 'minute' : return days * 1440  + milliseconds / 6e4;
+            case 'second' : return days * 86400 + milliseconds / 1000;
+            // Math.floor prevents floating point math errors here
+            case 'millisecond': return Math.floor(days * 864e5) + milliseconds;
+            default: throw new Error('Unknown unit ' + units);
+        }
+    }
+}
+
+// TODO: Use this.as('ms')?
+function valueOf$1 () {
+    if (!this.isValid()) {
+        return NaN;
+    }
+    return (
+        this._milliseconds +
+        this._days * 864e5 +
+        (this._months % 12) * 2592e6 +
+        toInt(this._months / 12) * 31536e6
+    );
+}
+
+function makeAs (alias) {
+    return function () {
+        return this.as(alias);
+    };
+}
+
+var asMilliseconds = makeAs('ms');
+var asSeconds      = makeAs('s');
+var asMinutes      = makeAs('m');
+var asHours        = makeAs('h');
+var asDays         = makeAs('d');
+var asWeeks        = makeAs('w');
+var asMonths       = makeAs('M');
+var asYears        = makeAs('y');
+
+function clone$1 () {
+    return createDuration(this);
+}
+
+function get$2 (units) {
+    units = normalizeUnits(units);
+    return this.isValid() ? this[units + 's']() : NaN;
+}
+
+function makeGetter(name) {
+    return function () {
+        return this.isValid() ? this._data[name] : NaN;
+    };
+}
+
+var milliseconds = makeGetter('milliseconds');
+var seconds      = makeGetter('seconds');
+var minutes      = makeGetter('minutes');
+var hours        = makeGetter('hours');
+var days         = makeGetter('days');
+var months       = makeGetter('months');
+var years        = makeGetter('years');
+
+function weeks () {
+    return absFloor(this.days() / 7);
+}
+
+var round = Math.round;
+var thresholds = {
+    ss: 44,         // a few seconds to seconds
+    s : 45,         // seconds to minute
+    m : 45,         // minutes to hour
+    h : 22,         // hours to day
+    d : 26,         // days to month
+    M : 11          // months to year
+};
+
+// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+    return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+}
+
+function relativeTime$1 (posNegDuration, withoutSuffix, locale) {
+    var duration = createDuration(posNegDuration).abs();
+    var seconds  = round(duration.as('s'));
+    var minutes  = round(duration.as('m'));
+    var hours    = round(duration.as('h'));
+    var days     = round(duration.as('d'));
+    var months   = round(duration.as('M'));
+    var years    = round(duration.as('y'));
+
+    var a = seconds <= thresholds.ss && ['s', seconds]  ||
+            seconds < thresholds.s   && ['ss', seconds] ||
+            minutes <= 1             && ['m']           ||
+            minutes < thresholds.m   && ['mm', minutes] ||
+            hours   <= 1             && ['h']           ||
+            hours   < thresholds.h   && ['hh', hours]   ||
+            days    <= 1             && ['d']           ||
+            days    < thresholds.d   && ['dd', days]    ||
+            months  <= 1             && ['M']           ||
+            months  < thresholds.M   && ['MM', months]  ||
+            years   <= 1             && ['y']           || ['yy', years];
+
+    a[2] = withoutSuffix;
+    a[3] = +posNegDuration > 0;
+    a[4] = locale;
+    return substituteTimeAgo.apply(null, a);
+}
+
+// This function allows you to set the rounding function for relative time strings
+function getSetRelativeTimeRounding (roundingFunction) {
+    if (roundingFunction === undefined) {
+        return round;
+    }
+    if (typeof(roundingFunction) === 'function') {
+        round = roundingFunction;
+        return true;
+    }
+    return false;
+}
+
+// This function allows you to set a threshold for relative time strings
+function getSetRelativeTimeThreshold (threshold, limit) {
+    if (thresholds[threshold] === undefined) {
+        return false;
+    }
+    if (limit === undefined) {
+        return thresholds[threshold];
+    }
+    thresholds[threshold] = limit;
+    if (threshold === 's') {
+        thresholds.ss = limit - 1;
+    }
+    return true;
+}
+
+function humanize (withSuffix) {
+    if (!this.isValid()) {
+        return this.localeData().invalidDate();
+    }
+
+    var locale = this.localeData();
+    var output = relativeTime$1(this, !withSuffix, locale);
+
+    if (withSuffix) {
+        output = locale.pastFuture(+this, output);
+    }
+
+    return locale.postformat(output);
+}
+
+var abs$1 = Math.abs;
+
+function sign(x) {
+    return ((x > 0) - (x < 0)) || +x;
+}
+
+function toISOString$1() {
+    // for ISO strings we do not use the normal bubbling rules:
+    //  * milliseconds bubble up until they become hours
+    //  * days do not bubble at all
+    //  * months bubble up until they become years
+    // This is because there is no context-free conversion between hours and days
+    // (think of clock changes)
+    // and also not between days and months (28-31 days per month)
+    if (!this.isValid()) {
+        return this.localeData().invalidDate();
+    }
+
+    var seconds = abs$1(this._milliseconds) / 1000;
+    var days         = abs$1(this._days);
+    var months       = abs$1(this._months);
+    var minutes, hours, years;
+
+    // 3600 seconds -> 60 minutes -> 1 hour
+    minutes           = absFloor(seconds / 60);
+    hours             = absFloor(minutes / 60);
+    seconds %= 60;
+    minutes %= 60;
+
+    // 12 months -> 1 year
+    years  = absFloor(months / 12);
+    months %= 12;
+
+
+    // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+    var Y = years;
+    var M = months;
+    var D = days;
+    var h = hours;
+    var m = minutes;
+    var s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : '';
+    var total = this.asSeconds();
+
+    if (!total) {
+        // this is the same as C#'s (Noda) and python (isodate)...
+        // but not other JS (goog.date)
+        return 'P0D';
+    }
+
+    var totalSign = total < 0 ? '-' : '';
+    var ymSign = sign(this._months) !== sign(total) ? '-' : '';
+    var daysSign = sign(this._days) !== sign(total) ? '-' : '';
+    var hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : '';
+
+    return totalSign + 'P' +
+        (Y ? ymSign + Y + 'Y' : '') +
+        (M ? ymSign + M + 'M' : '') +
+        (D ? daysSign + D + 'D' : '') +
+        ((h || m || s) ? 'T' : '') +
+        (h ? hmsSign + h + 'H' : '') +
+        (m ? hmsSign + m + 'M' : '') +
+        (s ? hmsSign + s + 'S' : '');
+}
+
+var proto$2 = Duration.prototype;
+
+proto$2.isValid        = isValid$1;
+proto$2.abs            = abs;
+proto$2.add            = add$1;
+proto$2.subtract       = subtract$1;
+proto$2.as             = as;
+proto$2.asMilliseconds = asMilliseconds;
+proto$2.asSeconds      = asSeconds;
+proto$2.asMinutes      = asMinutes;
+proto$2.asHours        = asHours;
+proto$2.asDays         = asDays;
+proto$2.asWeeks        = asWeeks;
+proto$2.asMonths       = asMonths;
+proto$2.asYears        = asYears;
+proto$2.valueOf        = valueOf$1;
+proto$2._bubble        = bubble;
+proto$2.clone          = clone$1;
+proto$2.get            = get$2;
+proto$2.milliseconds   = milliseconds;
+proto$2.seconds        = seconds;
+proto$2.minutes        = minutes;
+proto$2.hours          = hours;
+proto$2.days           = days;
+proto$2.weeks          = weeks;
+proto$2.months         = months;
+proto$2.years          = years;
+proto$2.humanize       = humanize;
+proto$2.toISOString    = toISOString$1;
+proto$2.toString       = toISOString$1;
+proto$2.toJSON         = toISOString$1;
+proto$2.locale         = locale;
+proto$2.localeData     = localeData;
+
+// Deprecations
+proto$2.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', toISOString$1);
+proto$2.lang = lang;
+
+// Side effect imports
+
+// FORMATTING
+
+addFormatToken('X', 0, 0, 'unix');
+addFormatToken('x', 0, 0, 'valueOf');
+
+// PARSING
+
+addRegexToken('x', matchSigned);
+addRegexToken('X', matchTimestamp);
+addParseToken('X', function (input, array, config) {
+    config._d = new Date(parseFloat(input, 10) * 1000);
+});
+addParseToken('x', function (input, array, config) {
+    config._d = new Date(toInt(input));
+});
+
+// Side effect imports
+
+
+hooks.version = '2.19.4';
+
+setHookCallback(createLocal);
+
+hooks.fn                    = proto;
+hooks.min                   = min;
+hooks.max                   = max;
+hooks.now                   = now;
+hooks.utc                   = createUTC;
+hooks.unix                  = createUnix;
+hooks.months                = listMonths;
+hooks.isDate                = isDate;
+hooks.locale                = getSetGlobalLocale;
+hooks.invalid               = createInvalid;
+hooks.duration              = createDuration;
+hooks.isMoment              = isMoment;
+hooks.weekdays              = listWeekdays;
+hooks.parseZone             = createInZone;
+hooks.localeData            = getLocale;
+hooks.isDuration            = isDuration;
+hooks.monthsShort           = listMonthsShort;
+hooks.weekdaysMin           = listWeekdaysMin;
+hooks.defineLocale          = defineLocale;
+hooks.updateLocale          = updateLocale;
+hooks.locales               = listLocales;
+hooks.weekdaysShort         = listWeekdaysShort;
+hooks.normalizeUnits        = normalizeUnits;
+hooks.relativeTimeRounding  = getSetRelativeTimeRounding;
+hooks.relativeTimeThreshold = getSetRelativeTimeThreshold;
+hooks.calendarFormat        = getCalendarFormat;
+hooks.prototype             = proto;
+
+return hooks;
+
+})));

+ 13 - 0
lib/config/folderconfiguration.go

@@ -14,6 +14,7 @@ import (
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/util"
+	"github.com/syncthing/syncthing/lib/versioner"
 )
 
 var (
@@ -98,6 +99,18 @@ func (f FolderConfiguration) Filesystem() fs.Filesystem {
 	return f.cachedFilesystem
 }
 
+func (f FolderConfiguration) Versioner() versioner.Versioner {
+	if f.Versioning.Type == "" {
+		return nil
+	}
+	versionerFactory, ok := versioner.Factories[f.Versioning.Type]
+	if !ok {
+		l.Fatalf("Requested versioning type %q that does not exist", f.Versioning.Type)
+	}
+
+	return versionerFactory(f.ID, f.Filesystem(), f.Versioning.Params)
+}
+
 func (f *FolderConfiguration) CreateMarker() error {
 	if err := f.CheckPath(); err != errMarkerMissing {
 		return err

+ 143 - 15
lib/model/model.go

@@ -38,6 +38,16 @@ import (
 	"github.com/thejerf/suture"
 )
 
+var locationLocal *time.Location
+
+func init() {
+	var err error
+	locationLocal, err = time.LoadLocation("Local")
+	if err != nil {
+		panic(err.Error())
+	}
+}
+
 // How many files to send in each Index/IndexUpdate message.
 const (
 	maxBatchSizeBytes = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
@@ -232,21 +242,13 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
 		}
 	}
 
-	var ver versioner.Versioner
-	if len(cfg.Versioning.Type) > 0 {
-		versionerFactory, ok := versioner.Factories[cfg.Versioning.Type]
-		if !ok {
-			l.Fatalf("Requested versioning type %q that does not exist", cfg.Versioning.Type)
-		}
-
-		ver = versionerFactory(folder, cfg.Filesystem(), cfg.Versioning.Params)
-		if service, ok := ver.(suture.Service); ok {
-			// The versioner implements the suture.Service interface, so
-			// expects to be run in the background in addition to being called
-			// when files are going to be archived.
-			token := m.Add(service)
-			m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
-		}
+	ver := cfg.Versioner()
+	if service, ok := ver.(suture.Service); ok {
+		// The versioner implements the suture.Service interface, so
+		// expects to be run in the background in addition to being called
+		// when files are going to be archived.
+		token := m.Add(service)
+		m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
 	}
 
 	ffs := fs.MtimeFS()
@@ -2376,6 +2378,132 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
 	return output
 }
 
+func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
+	fcfg, ok := m.cfg.Folder(folder)
+	if !ok {
+		return nil, errFolderMissing
+	}
+
+	files := make(map[string][]versioner.FileVersion)
+
+	filesystem := fcfg.Filesystem()
+	err := filesystem.Walk(".stversions", func(path string, f fs.FileInfo, err error) error {
+		// Skip root (which is ok to be a symlink)
+		if path == ".stversions" {
+			return nil
+		}
+
+		// Ignore symlinks
+		if f.IsSymlink() {
+			return fs.SkipDir
+		}
+
+		// No records for directories
+		if f.IsDir() {
+			return nil
+		}
+
+		// Strip .stversions prefix.
+		path = strings.TrimPrefix(path, ".stversions"+string(fs.PathSeparator))
+
+		name, tag := versioner.UntagFilename(path)
+		// Something invalid
+		if name == "" || tag == "" {
+			return nil
+		}
+
+		name = osutil.NormalizedFilename(name)
+
+		versionTime, err := time.ParseInLocation(versioner.TimeFormat, tag, locationLocal)
+		if err != nil {
+			return nil
+		}
+
+		files[name] = append(files[name], versioner.FileVersion{
+			VersionTime: versionTime.Truncate(time.Second),
+			ModTime:     f.ModTime().Truncate(time.Second),
+			Size:        f.Size(),
+		})
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return files, nil
+}
+
+func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
+	fcfg, ok := m.cfg.Folder(folder)
+	if !ok {
+		return nil, errFolderMissing
+	}
+
+	filesystem := fcfg.Filesystem()
+	ver := fcfg.Versioner()
+
+	restore := make(map[string]string)
+	errors := make(map[string]string)
+
+	// Validation
+	for file, version := range versions {
+		file = osutil.NativeFilename(file)
+		tag := version.In(locationLocal).Truncate(time.Second).Format(versioner.TimeFormat)
+		versionedTaggedFilename := filepath.Join(".stversions", versioner.TagFilename(file, tag))
+		// Check that the thing we've been asked to restore is actually a file
+		// and that it exists.
+		if info, err := filesystem.Lstat(versionedTaggedFilename); err != nil {
+			errors[file] = err.Error()
+			continue
+		} else if !info.IsRegular() {
+			errors[file] = "not a file"
+			continue
+		}
+
+		// Check that the target location of where we are supposed to restore
+		// either does not exist, or is actually a file.
+		if info, err := filesystem.Lstat(file); err == nil && !info.IsRegular() {
+			errors[file] = "cannot replace a non-file"
+			continue
+		} else if err != nil && !fs.IsNotExist(err) {
+			errors[file] = err.Error()
+			continue
+		}
+
+		restore[file] = versionedTaggedFilename
+	}
+
+	// Execution
+	var err error
+	for target, source := range restore {
+		err = nil
+		if _, serr := filesystem.Lstat(target); serr == nil {
+			if ver != nil {
+				err = osutil.InWritableDir(ver.Archive, filesystem, target)
+			} else {
+				err = osutil.InWritableDir(filesystem.Remove, filesystem, target)
+			}
+		}
+
+		filesystem.MkdirAll(filepath.Dir(target), 0755)
+		if err == nil {
+			err = osutil.Copy(filesystem, source, target)
+		}
+
+		if err != nil {
+			errors[target] = err.Error()
+			continue
+		}
+	}
+
+	// Trigger scan
+	if !fcfg.FSWatcherEnabled {
+		m.ScanFolder(folder)
+	}
+
+	return errors, nil
+}
+
 func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
 	// The slightly unusual locking sequence here is because we need to hold
 	// pmut for the duration (as the value returned from foldersFiles can

+ 213 - 0
lib/model/model_test.go

@@ -29,9 +29,11 @@ import (
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
+	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
 	srand "github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/scanner"
+	"github.com/syncthing/syncthing/lib/versioner"
 )
 
 var device1, device2 protocol.DeviceID
@@ -2871,6 +2873,217 @@ func TestIssue4475(t *testing.T) {
 	}
 }
 
+func TestVersionRestore(t *testing.T) {
+	// We create a bunch of files which we restore
+	// In each file, we write the filename as the content
+	// We verify that the content matches at the expected filenames
+	// after the restore operation.
+	dir, err := ioutil.TempDir("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	dbi := db.OpenMemory()
+
+	fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, dir)
+	fcfg.Versioning.Type = "simple"
+	fcfg.FSWatcherEnabled = false
+	filesystem := fcfg.Filesystem()
+
+	rawConfig := config.Configuration{
+		Folders: []config.FolderConfiguration{fcfg},
+	}
+	cfg := config.Wrap("/tmp/test", rawConfig)
+
+	m := NewModel(cfg, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
+	m.AddFolder(fcfg)
+	m.StartFolder("default")
+	m.ServeBackground()
+	defer m.Stop()
+	m.ScanFolder("default")
+
+	sentinel, err := time.ParseInLocation(versioner.TimeFormat, "20200101-010101", locationLocal)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sentinelTag := sentinel.Format(versioner.TimeFormat)
+
+	for _, file := range []string{
+		// Versions directory
+		".stversions/file~20171210-040404.txt",  // will be restored
+		".stversions/existing~20171210-040404",  // exists, should expect to be archived.
+		".stversions/something~20171210-040404", // will become directory, hence error
+		".stversions/dir/file~20171210-040404.txt",
+		".stversions/dir/file~20171210-040405.txt",
+		".stversions/dir/file~20171210-040406.txt",
+		".stversions/very/very/deep/one~20171210-040406.txt", // lives deep down, no directory exists.
+		".stversions/dir/existing~20171210-040406.txt",       // exists, should expect to be archived.
+		".stversions/dir/file.txt~20171210-040405",           // incorrect tag format, ignored.
+		".stversions/dir/cat",                                // incorrect tag format, ignored.
+
+		// "file.txt" will be restored
+		"existing",
+		"something/file", // Becomes directory
+		"dir/file.txt",
+		"dir/existing.txt",
+	} {
+		if runtime.GOOS == "windows" {
+			file = filepath.FromSlash(file)
+		}
+		dir := filepath.Dir(file)
+		if err := filesystem.MkdirAll(dir, 0755); err != nil {
+			t.Fatal(err)
+		}
+		if fd, err := filesystem.Create(file); err != nil {
+			t.Fatal(err)
+		} else if _, err := fd.Write([]byte(file)); err != nil {
+			t.Fatal(err)
+		} else if err := fd.Close(); err != nil {
+			t.Fatal(err)
+		} else if err := filesystem.Chtimes(file, sentinel, sentinel); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	versions, err := m.GetFolderVersions("default")
+	if err != nil {
+		t.Fatal(err)
+	}
+	expectedVersions := map[string]int{
+		"file.txt":               1,
+		"existing":               1,
+		"something":              1,
+		"dir/file.txt":           3,
+		"dir/existing.txt":       1,
+		"very/very/deep/one.txt": 1,
+	}
+
+	for name, vers := range versions {
+		cnt, ok := expectedVersions[name]
+		if !ok {
+			t.Errorf("unexpected %s", name)
+		}
+		if len(vers) != cnt {
+			t.Errorf("%s: %s != %s", name, cnt, len(vers))
+		}
+		// Delete, so we can check if we didn't hit something we expect afterwards.
+		delete(expectedVersions, name)
+	}
+
+	for name := range expectedVersions {
+		t.Errorf("not found expected %s", name)
+	}
+
+	// Restoring non existing folder fails.
+	_, err = m.RestoreFolderVersions("does not exist", nil)
+	if err == nil {
+		t.Errorf("expected an error")
+	}
+
+	makeTime := func(s string) time.Time {
+		tm, err := time.ParseInLocation(versioner.TimeFormat, s, locationLocal)
+		if err != nil {
+			t.Error(err)
+		}
+		return tm.Truncate(time.Second)
+	}
+
+	restore := map[string]time.Time{
+		"file.txt":               makeTime("20171210-040404"),
+		"existing":               makeTime("20171210-040404"),
+		"something":              makeTime("20171210-040404"),
+		"dir/file.txt":           makeTime("20171210-040406"),
+		"dir/existing.txt":       makeTime("20171210-040406"),
+		"very/very/deep/one.txt": makeTime("20171210-040406"),
+	}
+
+	ferr, err := m.RestoreFolderVersions("default", restore)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err, ok := ferr["something"]; len(ferr) > 1 || !ok || err != "cannot replace a non-file" {
+		t.Fatalf("incorrect error or count: %d %s", len(ferr), ferr)
+	}
+
+	// Failed items are not expected to be restored.
+	// Remove them from expectations
+	for name := range ferr {
+		delete(restore, name)
+	}
+
+	// Check that content of files matches to the version they've been restored.
+	for file, version := range restore {
+		if runtime.GOOS == "windows" {
+			file = filepath.FromSlash(file)
+		}
+		tag := version.In(locationLocal).Truncate(time.Second).Format(versioner.TimeFormat)
+		taggedName := filepath.Join(".stversions", versioner.TagFilename(file, tag))
+		fd, err := filesystem.Open(file)
+		if err != nil {
+			t.Error(err)
+		}
+		defer fd.Close()
+
+		content, err := ioutil.ReadAll(fd)
+		if err != nil {
+			t.Error(err)
+		}
+		if !bytes.Equal(content, []byte(taggedName)) {
+			t.Errorf("%s: %s != %s", file, string(content), taggedName)
+		}
+	}
+
+	// Simple versioner uses modtime for timestamp generation, so we can check
+	// if existing stuff was correctly archived as we restored.
+	expectArchived := map[string]struct{}{
+		"existing":         {},
+		"dir/file.txt":     {},
+		"dir/existing.txt": {},
+	}
+
+	// Even if they are at the archived path, content should have the non
+	// archived name.
+	for file := range expectArchived {
+		if runtime.GOOS == "windows" {
+			file = filepath.FromSlash(file)
+		}
+		taggedName := versioner.TagFilename(file, sentinelTag)
+		taggedArchivedName := filepath.Join(".stversions", taggedName)
+
+		fd, err := filesystem.Open(taggedArchivedName)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer fd.Close()
+
+		content, err := ioutil.ReadAll(fd)
+		if err != nil {
+			t.Error(err)
+		}
+		if !bytes.Equal(content, []byte(file)) {
+			t.Errorf("%s: %s != %s", file, string(content), file)
+		}
+	}
+
+	// Check for other unexpected things that are tagged.
+	filesystem.Walk(".", func(path string, f fs.FileInfo, err error) error {
+		if !f.IsRegular() {
+			return nil
+		}
+		if strings.Contains(path, sentinelTag) {
+			path = osutil.NormalizedFilename(path)
+			name, _ := versioner.UntagFilename(path)
+			name = strings.TrimPrefix(name, ".stversions/")
+			if _, ok := expectArchived[name]; !ok {
+				t.Errorf("unexpected file with sentinel tag: %s", name)
+			}
+		}
+		return nil
+	})
+}
+
 func TestPausedFolders(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	cfg := defaultConfig.RawCopy()

+ 1 - 1
lib/model/rwfolder.go

@@ -1451,7 +1451,7 @@ func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, state *shared
 			// file before we replace it. Archiving a non-existent file is not
 			// an error.
 
-			if err = f.versioner.Archive(state.file.Name); err != nil {
+			if err = osutil.InWritableDir(f.versioner.Archive, f.fs, state.file.Name); err != nil {
 				return err
 			}
 		}

+ 2 - 2
lib/versioner/simple.go

@@ -77,7 +77,7 @@ func (v Simple) Archive(filePath string) error {
 		return err
 	}
 
-	ver := taggedFilename(file, info.ModTime().Format(TimeFormat))
+	ver := TagFilename(file, info.ModTime().Format(TimeFormat))
 	dst := filepath.Join(dir, ver)
 	l.Debugln("moving to", dst)
 	err = osutil.Rename(v.fs, filePath, dst)
@@ -86,7 +86,7 @@ func (v Simple) Archive(filePath string) error {
 	}
 
 	// Glob according to the new file~timestamp.ext pattern.
-	pattern := filepath.Join(dir, taggedFilename(file, TimeGlob))
+	pattern := filepath.Join(dir, TagFilename(file, TimeGlob))
 	newVersions, err := v.fs.Glob(pattern)
 	if err != nil {
 		l.Warnln("globbing:", err, "for", pattern)

+ 2 - 2
lib/versioner/simple_test.go

@@ -34,14 +34,14 @@ func TestTaggedFilename(t *testing.T) {
 	for _, tc := range cases {
 		if tc[0] != "" {
 			// Test tagger
-			tf := taggedFilename(tc[0], tc[1])
+			tf := TagFilename(tc[0], tc[1])
 			if tf != tc[2] {
 				t.Errorf("%s != %s", tf, tc[2])
 			}
 		}
 
 		// Test parser
-		tag := filenameTag(tc[2])
+		tag := ExtractTag(tc[2])
 		if tag != tc[1] {
 			t.Errorf("%s != %s", tag, tc[1])
 		}

+ 9 - 8
lib/versioner/staggered.go

@@ -124,12 +124,13 @@ func (v *Staggered) clean() {
 		}
 
 		// Regular file, or possibly a symlink.
-		ext := filepath.Ext(path)
-		versionTag := filenameTag(path)
-		withoutExt := path[:len(path)-len(ext)-len(versionTag)-1]
-		name := withoutExt + ext
-
 		dirTracker.addFile(path)
+
+		name, _ := UntagFilename(path)
+		if name == "" {
+			return nil
+		}
+
 		versionsPerFile[name] = append(versionsPerFile[name], path)
 
 		return nil
@@ -173,7 +174,7 @@ func (v *Staggered) toRemove(versions []string, now time.Time) []string {
 	var remove []string
 	for _, file := range versions {
 		loc, _ := time.LoadLocation("Local")
-		versionTime, err := time.ParseInLocation(TimeFormat, filenameTag(file), loc)
+		versionTime, err := time.ParseInLocation(TimeFormat, ExtractTag(file), loc)
 		if err != nil {
 			l.Debugf("Versioner: file name %q is invalid: %v", file, err)
 			continue
@@ -258,7 +259,7 @@ func (v *Staggered) Archive(filePath string) error {
 		return err
 	}
 
-	ver := taggedFilename(file, time.Now().Format(TimeFormat))
+	ver := TagFilename(file, time.Now().Format(TimeFormat))
 	dst := filepath.Join(inFolderPath, ver)
 	l.Debugln("moving to", dst)
 
@@ -273,7 +274,7 @@ func (v *Staggered) Archive(filePath string) error {
 	}
 
 	// Glob according to the new file~timestamp.ext pattern.
-	pattern := filepath.Join(inFolderPath, taggedFilename(file, TimeGlob))
+	pattern := filepath.Join(inFolderPath, TagFilename(file, TimeGlob))
 	newVersions, err := v.versionsFs.Glob(pattern)
 	if err != nil {
 		l.Warnln("globbing:", err, "for", pattern)

+ 17 - 2
lib/versioner/util.go

@@ -9,10 +9,11 @@ package versioner
 import (
 	"path/filepath"
 	"regexp"
+	"strings"
 )
 
 // Inserts ~tag just before the extension of the filename.
-func taggedFilename(name, tag string) string {
+func TagFilename(name, tag string) string {
 	dir, file := filepath.Dir(name), filepath.Base(name)
 	ext := filepath.Ext(file)
 	withoutExt := file[:len(file)-len(ext)]
@@ -22,7 +23,7 @@ func taggedFilename(name, tag string) string {
 var tagExp = regexp.MustCompile(`.*~([^~.]+)(?:\.[^.]+)?$`)
 
 // Returns the tag from a filename, whether at the end or middle.
-func filenameTag(path string) string {
+func ExtractTag(path string) string {
 	match := tagExp.FindStringSubmatch(path)
 	// match is []string{"whole match", "submatch"} when successful
 
@@ -31,3 +32,17 @@ func filenameTag(path string) string {
 	}
 	return match[1]
 }
+
+func UntagFilename(path string) (string, string) {
+	ext := filepath.Ext(path)
+	versionTag := ExtractTag(path)
+
+	// Files tagged with old style tags cannot be untagged.
+	if versionTag == "" || strings.HasSuffix(ext, versionTag) {
+		return "", ""
+	}
+
+	withoutExt := path[:len(path)-len(ext)-len(versionTag)-1]
+	name := withoutExt + ext
+	return name, versionTag
+}

+ 11 - 1
lib/versioner/versioner.go

@@ -8,12 +8,22 @@
 // simple default versioning scheme.
 package versioner
 
-import "github.com/syncthing/syncthing/lib/fs"
+import (
+	"time"
+
+	"github.com/syncthing/syncthing/lib/fs"
+)
 
 type Versioner interface {
 	Archive(filePath string) error
 }
 
+type FileVersion struct {
+	VersionTime time.Time `json:"versionTime"`
+	ModTime     time.Time `json:"modTime"`
+	Size        int64     `json:"size"`
+}
+
 var Factories = map[string]func(folderID string, filesystem fs.Filesystem, params map[string]string) Versioner{}
 
 const (